Skip to content

Commit ae2d64a

Browse files
larsbrubakerclaude
andcommitted
Deliver mac drags past the view edge, add diagonal resize cursors, and harden window drags
- MacSystemWindow: deliver dragged/up events outside the content view while a button pressed inside is held (OutOfViewMouseCapture); ignore MouseExited events whose location is still inside the view (AppKit posts them when cursor rects are rebuilt mid-drag) and never turn one into the leave sentinel while a drag is in flight. - MacSystemWindow: map SizeNWSE/SizeNESW to the private NSCursor diagonal resize cursors behind a respondsToSelector probe (arrow fallback), SizeAll to openHandCursor, and install an AggMacContentView with resetCursorRects so the requested cursor persists; skip the cursor set/invalidate when unchanged. - GrabControl/WindowWidget: track resize drags in screen space from a mouse-down snapshot and place the window absolutely; end the drag on any move that lacks the pressed button, so the (-10,-10) leave sentinel and a lost mouse up can no longer collapse the window to its minimum size. - TitleBarWidget: same pressed-button guard, so the sentinel cannot throw the window off screen. - ScrollableWidget: set the scroll-area margin through SetScrollAreaMargin instead of a raw VerticalScrollBar.Width that was double-scaled on Retina. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent cedf6f9 commit ae2d64a

10 files changed

Lines changed: 998 additions & 68 deletions

File tree

Gui/ScrollableWidget/ScrollableWidget.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,10 @@ public ScrollableWidget(double width, double height, bool autoScroll = false)
177177
SetScrollAreaMargin();
178178
};
179179

180-
scrollArea.Margin = scrollArea.Margin.Clone(right: VerticalScrollBar.Width);
180+
// through the same helper the VisibleChanged and SizeChanged handlers use - Margin is in design units
181+
// and layout multiplies it by DeviceScale, so assigning the bar's already scaled Width here made the
182+
// gap twice as wide as the bar on any display with a scale above 1
183+
SetScrollAreaMargin();
181184

182185
base.AddChild(scrollArea);
183186
base.AddChild(VerticalScrollBar);

Gui/WindowWidget/GrabControl.cs

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
using MatterHackers.VectorMath;
1+
using MatterHackers.VectorMath;
22
using System;
33

44
//----------------------------------------------------------------------------
55
// Anti-Grain Geometry - Version 2.4
6-
// Copyright (C) 2007 Lars Brubaker
6+
// Copyright (C) 2026 Lars Brubaker
77
// larsbrubaker@gmail.com
88
//
99
// Permission to copy, use, modify, sell and distribute this software
@@ -15,23 +15,65 @@
1515

1616
namespace MatterHackers.Agg.UI
1717
{
18+
/// <summary>
19+
/// One edge or corner handle of a <see cref="WindowWidget"/>. It does no resizing itself - it tracks the
20+
/// drag and hands <see cref="AdjustParent"/> everything needed to place the window absolutely.
21+
/// </summary>
1822
public class GrabControl : GuiWidget
1923
{
20-
public Vector2 downPosition;
21-
internal Action<GrabControl, MouseEventArgs> AdjustParent;
24+
/// <summary>
25+
/// Called on every move of a drag. It takes only the handle, because everything a resize needs -
26+
/// how far the mouse has moved and where the window started - is read from it.
27+
/// </summary>
28+
internal Action<GrabControl> AdjustParent;
2229
private Cursors cursor;
2330
private bool mouseIsDown = false;
31+
private MouseButtons dragButton = MouseButtons.None;
32+
private Vector2 downScreenPosition;
2433
private GuiWidget perviousParent;
2534

2635
public GrabControl(Cursors cursor)
2736
{
2837
this.cursor = cursor;
2938
}
3039

40+
/// <summary>
41+
/// How far the mouse has travelled since the press that started the drag, in screen space.
42+
/// </summary>
43+
/// <remarks>
44+
/// Screen space, and measured from the press rather than from the previous move, because the handle is
45+
/// edge anchored: it slides out from under the mouse as the window resizes, so its local coordinates
46+
/// are a reference frame that moves with what is being measured.
47+
/// </remarks>
48+
public Vector2 DragDelta { get; private set; }
49+
50+
/// <summary>
51+
/// The size the parent had when the drag started. Handlers size the window from this rather than from
52+
/// its current size, so a move that arrives out of order, twice, or after a skipped one still lands the
53+
/// window exactly where the mouse is.
54+
/// </summary>
55+
public Vector2 ParentSizeAtMouseDown { get; private set; }
56+
57+
/// <summary>
58+
/// The position the parent had when the drag started - the other half of what an absolute placement
59+
/// needs, for the handles that move the window's left or bottom edge.
60+
/// </summary>
61+
public Vector2 ParentPositionAtMouseDown { get; private set; }
62+
3163
public override void OnMouseDown(MouseEventArgs mouseEvent)
3264
{
33-
mouseIsDown = true;
34-
downPosition = mouseEvent.Position;
65+
// Which button started the drag is remembered so OnMouseMove can tell a real drag from a move with
66+
// nothing held down, and a press with no button at all starts no drag.
67+
dragButton = mouseEvent.Button;
68+
mouseIsDown = dragButton != MouseButtons.None;
69+
70+
if (mouseIsDown)
71+
{
72+
downScreenPosition = this.TransformToScreenSpace(mouseEvent.Position);
73+
DragDelta = Vector2.Zero;
74+
ParentSizeAtMouseDown = Parent == null ? Vector2.Zero : Parent.Size;
75+
ParentPositionAtMouseDown = Parent == null ? Vector2.Zero : Parent.Position;
76+
}
3577

3678
base.OnMouseDown(mouseEvent);
3779
}
@@ -40,9 +82,18 @@ public override void OnMouseMove(MouseEventArgs mouseEvent)
4082
{
4183
if (mouseIsDown)
4284
{
43-
if (Parent?.Resizable == true)
85+
if (mouseEvent.Button != dragButton)
86+
{
87+
// The drag is over even though no mouse up reached us. Both platform sinks report the pointer
88+
// leaving the window as a buttonless move to (-10, -10), and a mouse up that lands outside the
89+
// window can be dropped before it ever gets here - taking either for a drag snapped the window
90+
// to its minimum size and then had it chase the pointer around with no button held.
91+
mouseIsDown = false;
92+
}
93+
else if (Parent?.Resizable == true)
4494
{
45-
AdjustParent?.Invoke(this, mouseEvent);
95+
DragDelta = this.TransformToScreenSpace(mouseEvent.Position) - downScreenPosition;
96+
AdjustParent?.Invoke(this);
4697
}
4798
}
4899

@@ -85,4 +136,4 @@ private void PerviousParent_ResizeableChanged(object sender, EventArgs e)
85136
}
86137
}
87138
}
88-
}
139+
}

Gui/WindowWidget/TitleBarWidget.cs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
//----------------------------------------------------------------------------
1+
//----------------------------------------------------------------------------
22
// Anti-Grain Geometry - Version 2.4
3-
// Copyright (C) 2007 Lars Brubaker
3+
// Copyright (C) 2026 Lars Brubaker
44
// larsbrubaker@gmail.com
55
//
66
// Permission to copy, use, modify, sell and distribute this software
@@ -15,10 +15,17 @@
1515

1616
namespace MatterHackers.Agg.UI
1717
{
18+
/// <summary>
19+
/// The bar across the top of a <see cref="WindowWidget"/>. Dragging it moves the window.
20+
/// </summary>
1821
public class TitleBarWidget : GuiWidget
1922
{
2023
private Vector2 DownPosition;
2124
private bool mouseDownOnBar = false;
25+
26+
// which button started the drag, so a move arriving without it can be told from one that is part of it
27+
private MouseButtons dragButton = MouseButtons.None;
28+
2229
GuiWidget windowToDrag;
2330

2431
public TitleBarWidget(GuiWidget windowToDrag)
@@ -36,7 +43,11 @@ protected bool MouseDownOnBar
3643

3744
public override void OnMouseDown(MouseEventArgs mouseEvent)
3845
{
39-
if (PositionWithinLocalBounds(mouseEvent.X, mouseEvent.Y))
46+
// a press with no button at all starts no drag - the same rule GrabControl follows
47+
dragButton = mouseEvent.Button;
48+
49+
if (dragButton != MouseButtons.None
50+
&& PositionWithinLocalBounds(mouseEvent.X, mouseEvent.Y))
4051
{
4152
MouseDownOnBar = true;
4253
Vector2 mouseRelClient = new Vector2(mouseEvent.X, mouseEvent.Y);
@@ -52,6 +63,16 @@ public override void OnMouseDown(MouseEventArgs mouseEvent)
5263

5364
public override void OnMouseMove(MouseEventArgs mouseEvent)
5465
{
66+
if (MouseDownOnBar
67+
&& mouseEvent.Button != dragButton)
68+
{
69+
// The drag is over even though no mouse up reached us. Both platform sinks report the pointer
70+
// leaving the window as a buttonless move to (-10, -10), and a mouse up that lands outside the
71+
// window can be dropped before it ever gets here - taking either for a drag threw the window at
72+
// the corner of the screen, or had it follow the pointer around with nothing held down.
73+
MouseDownOnBar = false;
74+
}
75+
5576
if (MouseDownOnBar)
5677
{
5778
Vector2 mousePosition = new Vector2(mouseEvent.X, mouseEvent.Y);
@@ -83,6 +104,7 @@ public override void OnMouseMove(MouseEventArgs mouseEvent)
83104
public override void OnMouseUp(MouseEventArgs mouseEvent)
84105
{
85106
MouseDownOnBar = false;
107+
dragButton = MouseButtons.None;
86108
base.OnMouseUp(mouseEvent);
87109
}
88110
}

Gui/WindowWidget/WindowWidget.cs

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,17 @@ public override void OnDrawBackground(Graphics2D graphics2D)
227227
}
228228
}
229229

230+
/// <summary>
231+
/// Adds the eight edge and corner handles that resize the window.
232+
/// </summary>
233+
/// <remarks>
234+
/// Every handler places the window absolutely - the size and position the window had when the drag
235+
/// started, plus how far the mouse has moved since, in screen space. Nothing is accumulated from the
236+
/// previous move, because the handle is anchored to the edge it drags: it slides out from under the
237+
/// mouse on every resize, so a delta measured in its own coordinates is measured against a moving
238+
/// reference frame. Position is always derived from the size the window actually took, so the minimum
239+
/// size clamp stops the moving edge instead of sliding the whole window.
240+
/// </remarks>
230241
private void AddGrabControls()
231242
{
232243
// this is for debugging
@@ -241,13 +252,13 @@ private void AddGrabControls()
241252
VAnchor = VAnchor.Stretch,
242253
Margin = new BorderDouble(0, deviceGrabWidth, 0, deviceGrabWidth),
243254
Size = new Vector2(deviceGrabWidth, 0),
244-
AdjustParent = (s, e) =>
255+
AdjustParent = (s) =>
245256
{
246-
var delta = e.Position - s.downPosition;
247-
delta.Y = 0;
248-
var startSize = Size;
249-
Size = new Vector2(Size.X - delta.X, Size.Y);
250-
Position += startSize - Size;
257+
var startSize = s.ParentSizeAtMouseDown;
258+
Size = new Vector2(startSize.X - s.DragDelta.X, startSize.Y);
259+
// from the size that was actually taken, not the one asked for, so a drag past the minimum
260+
// width stops the left edge rather than walking the whole window across the screen
261+
Position = new Vector2(s.ParentPositionAtMouseDown.X + (startSize.X - Size.X), s.ParentPositionAtMouseDown.Y);
251262
}
252263
});
253264

@@ -259,13 +270,11 @@ private void AddGrabControls()
259270
VAnchor = VAnchor.Bottom,
260271
Margin = new BorderDouble(deviceGrabWidth, 0, deviceGrabWidth, 0),
261272
Size = new Vector2(0, deviceGrabWidth),
262-
AdjustParent = (s, e) =>
273+
AdjustParent = (s) =>
263274
{
264-
var delta = e.Position - s.downPosition;
265-
delta.X = 0;
266-
var startSize = Size;
267-
Size = new Vector2(Size.X, Size.Y - delta.Y);
268-
Position = Position + startSize - Size;
275+
var startSize = s.ParentSizeAtMouseDown;
276+
Size = new Vector2(startSize.X, startSize.Y - s.DragDelta.Y);
277+
Position = new Vector2(s.ParentPositionAtMouseDown.X, s.ParentPositionAtMouseDown.Y + (startSize.Y - Size.Y));
269278
}
270279
});
271280

@@ -276,12 +285,11 @@ private void AddGrabControls()
276285
HAnchor = HAnchor.Left,
277286
VAnchor = VAnchor.Bottom,
278287
Size = new Vector2(deviceGrabWidth, deviceGrabWidth),
279-
AdjustParent = (s, e) =>
288+
AdjustParent = (s) =>
280289
{
281-
var delta = e.Position - s.downPosition;
282-
var startSize = Size;
283-
Size -= delta;
284-
Position = Position + startSize - Size;
290+
var startSize = s.ParentSizeAtMouseDown;
291+
Size = startSize - s.DragDelta;
292+
Position = s.ParentPositionAtMouseDown + startSize - Size;
285293
}
286294
});
287295

@@ -292,12 +300,11 @@ private void AddGrabControls()
292300
HAnchor = HAnchor.Left,
293301
VAnchor = VAnchor.Top,
294302
Size = new Vector2(deviceGrabWidth, deviceGrabWidth),
295-
AdjustParent = (s, e) =>
303+
AdjustParent = (s) =>
296304
{
297-
var delta = e.Position - s.downPosition;
298-
var startSize = Size;
299-
Size = new Vector2(Size.X - delta.X, Size.Y + delta.Y);
300-
Position += new Vector2(startSize.X - Size.X, 0);
305+
var startSize = s.ParentSizeAtMouseDown;
306+
Size = new Vector2(startSize.X - s.DragDelta.X, startSize.Y + s.DragDelta.Y);
307+
Position = new Vector2(s.ParentPositionAtMouseDown.X + (startSize.X - Size.X), s.ParentPositionAtMouseDown.Y);
301308
}
302309
});
303310

@@ -309,10 +316,10 @@ private void AddGrabControls()
309316
HAnchor = HAnchor.Right,
310317
Margin = new BorderDouble(0, deviceGrabWidth, 0, deviceGrabWidth),
311318
Size = new Vector2(deviceGrabWidth, 0),
312-
AdjustParent = (s, e) =>
319+
AdjustParent = (s) =>
313320
{
314-
var delta = e.Position - s.downPosition;
315-
Size = new Vector2(Size.X + delta.X, Size.Y);
321+
var startSize = s.ParentSizeAtMouseDown;
322+
Size = new Vector2(startSize.X + s.DragDelta.X, startSize.Y);
316323
}
317324
});
318325

@@ -323,10 +330,9 @@ private void AddGrabControls()
323330
HAnchor = HAnchor.Right,
324331
VAnchor = VAnchor.Top,
325332
Size = new Vector2(deviceGrabWidth, deviceGrabWidth),
326-
AdjustParent = (s, e) =>
333+
AdjustParent = (s) =>
327334
{
328-
var delta = e.Position - s.downPosition;
329-
Size = new Vector2(Size.X + delta.X, Size.Y + delta.Y);
335+
Size = s.ParentSizeAtMouseDown + s.DragDelta;
330336
}
331337
});
332338

@@ -338,10 +344,10 @@ private void AddGrabControls()
338344
VAnchor = VAnchor.Top,
339345
Margin = new BorderDouble(deviceGrabWidth, 0, deviceGrabWidth, 0),
340346
Size = new Vector2(0, deviceGrabWidth),
341-
AdjustParent = (s, e) =>
347+
AdjustParent = (s) =>
342348
{
343-
var delta = e.Position - s.downPosition;
344-
Size = new Vector2(Size.X, Size.Y + delta.Y);
349+
var startSize = s.ParentSizeAtMouseDown;
350+
Size = new Vector2(startSize.X, startSize.Y + s.DragDelta.Y);
345351
}
346352
});
347353

@@ -352,12 +358,11 @@ private void AddGrabControls()
352358
HAnchor = HAnchor.Right,
353359
VAnchor = VAnchor.Bottom,
354360
Size = new Vector2(deviceGrabWidth, deviceGrabWidth),
355-
AdjustParent = (s, e) =>
361+
AdjustParent = (s) =>
356362
{
357-
var delta = e.Position - s.downPosition;
358-
var startSize = Size;
359-
Size = new Vector2(Size.X + delta.X, Size.Y - delta.Y);
360-
Position = new Vector2(Position.X, Position.Y + (startSize.Y - Size.Y));
363+
var startSize = s.ParentSizeAtMouseDown;
364+
Size = new Vector2(startSize.X + s.DragDelta.X, startSize.Y - s.DragDelta.Y);
365+
Position = new Vector2(s.ParentPositionAtMouseDown.X, s.ParentPositionAtMouseDown.Y + (startSize.Y - Size.Y));
361366
}
362367
});
363368
}

0 commit comments

Comments
 (0)