add window resize handles for frameless mode

Without OS decorations there are no resize handles. Added 6px
edge and 12px corner invisible hit zones around the window
perimeter via ClideResizeBorder. Each zone changes the cursor
and calls gtk_window_begin_resize_drag via the clide/window
method channel with a GdkWindowEdge direction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 09:13:32 +02:00
co-authored by Claude Opus 4.6
parent 8938a0d502
commit 8475e2e33a
5 changed files with 95 additions and 8 deletions
+11 -8
View File
@@ -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()),
],
),
),
),
),
+10
View File
@@ -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<void> startResize(ResizeEdge edge) async {
try {
await _channel.invokeMethod('startResize', edge.index);
} on MissingPluginException {
// no-op
}
}
Future<void> startDrag() async {
try {
await _channel.invokeMethod('startDrag');
+53
View File
@@ -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)),
),
);
}
}
+1
View File
@@ -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';
+20
View File
@@ -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))));