diff --git a/lib/app.dart b/lib/app.dart index 5e6204a4..ee7b6643 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -84,14 +84,17 @@ class _RootShellState extends State<_RootShell> { onKeyEvent: _onKey, child: ColoredBox( color: tokens.globalBackground, - child: DialogHost( - router: widget.services.dialog, - child: Stack( - children: [ - const Positioned.fill(child: RootLayout()), - const ClidePalette(), - const Positioned.fill(child: _WelcomeOverlay()), - ], + child: ClideResizeBorder( + windowControls: widget.services.window, + child: DialogHost( + router: widget.services.dialog, + child: Stack( + children: [ + const Positioned.fill(child: RootLayout()), + const ClidePalette(), + const Positioned.fill(child: _WelcomeOverlay()), + ], + ), ), ), ), diff --git a/lib/kernel/src/window_controls.dart b/lib/kernel/src/window_controls.dart index 29c4eacf..c29981a6 100644 --- a/lib/kernel/src/window_controls.dart +++ b/lib/kernel/src/window_controls.dart @@ -3,6 +3,8 @@ import 'package:flutter/services.dart'; enum ChromeStyle { seam, prompt, inline } +enum ResizeEdge { topLeft, top, topRight, left, right, bottomLeft, bottom, bottomRight } + class WindowControls extends ChangeNotifier { static const _channel = MethodChannel('clide/window'); @@ -15,6 +17,14 @@ class WindowControls extends ChangeNotifier { notifyListeners(); } + Future startResize(ResizeEdge edge) async { + try { + await _channel.invokeMethod('startResize', edge.index); + } on MissingPluginException { + // no-op + } + } + Future startDrag() async { try { await _channel.invokeMethod('startDrag'); diff --git a/lib/widgets/src/clide_resize_border.dart b/lib/widgets/src/clide_resize_border.dart new file mode 100644 index 00000000..3298a83c --- /dev/null +++ b/lib/widgets/src/clide_resize_border.dart @@ -0,0 +1,53 @@ +import 'package:clide/kernel/src/window_controls.dart'; +import 'package:flutter/widgets.dart'; + +class ClideResizeBorder extends StatelessWidget { + const ClideResizeBorder({super.key, required this.windowControls, required this.child}); + + final WindowControls windowControls; + final Widget child; + + static const double _edge = 6; + static const double _corner = 12; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned.fill(child: child), + // Corners + _zone(Alignment.topLeft, _corner, _corner, SystemMouseCursors.resizeUpLeft, ResizeEdge.topLeft), + _zone(Alignment.topRight, _corner, _corner, SystemMouseCursors.resizeUpRight, ResizeEdge.topRight), + _zone(Alignment.bottomLeft, _corner, _corner, SystemMouseCursors.resizeDownLeft, ResizeEdge.bottomLeft), + _zone(Alignment.bottomRight, _corner, _corner, SystemMouseCursors.resizeDownRight, ResizeEdge.bottomRight), + // Edges + Positioned(top: _corner, bottom: _corner, left: 0, width: _edge, child: _edgeZone(SystemMouseCursors.resizeLeft, ResizeEdge.left)), + Positioned(top: _corner, bottom: _corner, right: 0, width: _edge, child: _edgeZone(SystemMouseCursors.resizeRight, ResizeEdge.right)), + Positioned(left: _corner, right: _corner, top: 0, height: _edge, child: _edgeZone(SystemMouseCursors.resizeUp, ResizeEdge.top)), + Positioned(left: _corner, right: _corner, bottom: 0, height: _edge, child: _edgeZone(SystemMouseCursors.resizeDown, ResizeEdge.bottom)), + ], + ); + } + + Widget _zone(Alignment alignment, double w, double h, MouseCursor cursor, ResizeEdge edge) { + return Positioned( + left: alignment.x < 0 ? 0 : null, + right: alignment.x > 0 ? 0 : null, + top: alignment.y < 0 ? 0 : null, + bottom: alignment.y > 0 ? 0 : null, + width: w, + height: h, + child: _edgeZone(cursor, edge), + ); + } + + Widget _edgeZone(MouseCursor cursor, ResizeEdge edge) { + return MouseRegion( + cursor: cursor, + child: GestureDetector( + onPanStart: (_) => windowControls.startResize(edge), + child: const ColoredBox(color: Color(0x00000000)), + ), + ); + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 2af58d4c..bc888657 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -13,6 +13,7 @@ export 'src/clide_icon.dart'; export 'src/clide_icon_rail.dart'; export 'src/clide_palette.dart'; export 'src/clide_pane_chrome.dart'; +export 'src/clide_resize_border.dart'; export 'src/clide_pty_view.dart'; export 'src/clide_scrollbar.dart'; export 'src/clide_spine.dart'; diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc index 9c3c95a9..e9bdabb1 100644 --- a/linux/runner/my_application.cc +++ b/linux/runner/my_application.cc @@ -113,6 +113,26 @@ static void my_application_activate(GApplication* application) { gtk_window_close(w); response = FL_METHOD_RESPONSE( fl_method_success_response_new(fl_value_new_null())); + } else if (g_strcmp0(method, "startResize") == 0) { + // edge: 0=topLeft 1=top 2=topRight 3=left 4=right + // 5=bottomLeft 6=bottom 7=bottomRight + FlValue* args = fl_method_call_get_args(method_call); + int edge = 7; // default: bottom-right + if (fl_value_get_type(args) == FL_VALUE_TYPE_INT) { + edge = (int)fl_value_get_int(args); + } + static const GdkWindowEdge edges[] = { + GDK_WINDOW_EDGE_NORTH_WEST, GDK_WINDOW_EDGE_NORTH, + GDK_WINDOW_EDGE_NORTH_EAST, GDK_WINDOW_EDGE_WEST, + GDK_WINDOW_EDGE_EAST, GDK_WINDOW_EDGE_SOUTH_WEST, + GDK_WINDOW_EDGE_SOUTH, GDK_WINDOW_EDGE_SOUTH_EAST, + }; + if (edge >= 0 && edge < 8) { + gtk_window_begin_resize_drag(w, edges[edge], 1, 0, 0, + GDK_CURRENT_TIME); + } + response = FL_METHOD_RESPONSE( + fl_method_success_response_new(fl_value_new_null())); } else if (g_strcmp0(method, "isMaximized") == 0) { response = FL_METHOD_RESPONSE(fl_method_success_response_new( fl_value_new_bool(gtk_window_is_maximized(w))));