1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
// Pointer / keyboard event handling for `Window`. Lifted out of
// `widget_impl.rs` so that file stays under the 800-line limit enforced
// by `tests/file_line_count.rs`, following the same shape as the
// sibling `paint.rs`: each helper takes `&mut Window` instead of
// `&mut self` so the trait `impl Widget for Window` stays a thin
// dispatcher in `widget_impl.rs`.
//
// This module owns the whole interactive surface of a window: title-bar
// drag and double-click maximise, the close / maximise chrome buttons,
// resize-edge grabs (whose geometry lives in `window.rs` via
// `resize_dir` / `apply_resize`, and whose snapping lives in
// `snap_glue.rs`), the modal click-away and Escape dismissals routed
// through `close.rs`, and the frameless (`chrome == false`) fast path
// that forwards everything to the content child.
use super::*;
impl Window {
/// Whether `local` (window-local, Y-up) lies within this window's bounds.
/// Used by the click-away path to tell an inside press from an outside one
/// once the modal grab has routed it here regardless of position.
fn point_in_local_bounds(&self, local: Point) -> bool {
local.x >= 0.0
&& local.x <= self.bounds.width
&& local.y >= 0.0
&& local.y <= self.bounds.height
}
}
/// Body of `<Window as Widget>::on_event`.
pub(super) fn on_event(window: &mut Window, event: &Event) -> EventResult {
if !window.requested_visible() {
return EventResult::Ignored;
}
// Frameless windows are thin hosts for an inline editor (a chrome-less
// TextField sized to a value pill). They have no title bar, resize
// handles, or chrome buttons, so — apart from the modal click-away /
// Escape dismissals — every event routes straight to the content
// child. That is what lets the field place its cursor and receive
// typing when a host dispatches events to us directly (the node
// editor's in-pane overlay path), mirroring what the framework's
// subtree walk already does for a child mounted in the main tree.
if !window.chrome {
if let Event::MouseDown { pos, .. } = event {
if window.modal
&& window.click_away == ClickAwayAction::Close
&& !window.point_in_local_bounds(*pos)
{
// Click-away: the on_close callback decides commit vs
// revert from the `ClickAway` reason.
window.close(CloseReason::ClickAway);
return EventResult::Consumed;
}
}
if let Event::KeyDown {
key: Key::Escape, ..
} = event
{
if window.modal {
window.close(CloseReason::Escape);
return EventResult::Consumed;
}
}
// This forward only re-delivers events the child IGNORED via the
// framework's normal subtree routing (e.g. hover moves) — a benign
// duplicate. Cursor placement (MouseDown) is single-dispatch: the
// subtree walk hands it to the child first, which consumes it, so
// it never reaches this fallback twice.
if let Some(child) = window.children.first_mut() {
let cb = child.bounds();
let forwarded = match event {
Event::MouseDown {
pos,
button,
modifiers,
} => Event::MouseDown {
pos: Point::new(pos.x - cb.x, pos.y - cb.y),
button: *button,
modifiers: *modifiers,
},
Event::MouseUp {
pos,
button,
modifiers,
} => Event::MouseUp {
pos: Point::new(pos.x - cb.x, pos.y - cb.y),
button: *button,
modifiers: *modifiers,
},
Event::MouseMove { pos } => Event::MouseMove {
pos: Point::new(pos.x - cb.x, pos.y - cb.y),
},
other => other.clone(),
};
return child.on_event(&forwarded);
}
return EventResult::Ignored;
}
match event {
Event::MouseMove { pos } => {
let was_close = window.close_hovered;
let was_max = window.maximize_hovered;
let was_dir = window.hover_dir;
window.close_hovered = window.in_close_button(*pos);
window.maximize_hovered = window.in_maximize_button(*pos);
match window.drag_mode {
DragMode::Move => {
let world = Point::new(pos.x + window.bounds.x, pos.y + window.bounds.y);
let dx = world.x - window.drag_start_world.x;
let dy = world.y - window.drag_start_world.y;
window.bounds.x = (window.drag_start_bounds.x + dx).round();
window.bounds.y = (window.drag_start_bounds.y + dy).round();
// Snap pass — runs only when the global flag
// is on. Reads the thread-local target list
// populated by every other window's `layout`
// and writes the resulting visual guides for
// `SnapOverlay` to render.
window.apply_move_snap();
window.clamp_to_canvas();
window.hover_dir = None;
set_cursor_icon(CursorIcon::Grabbing);
crate::animation::request_draw_without_invalidation();
return EventResult::Ignored;
}
DragMode::Resize(dir) => {
let world = Point::new(pos.x + window.bounds.x, pos.y + window.bounds.y);
window.apply_resize(world);
window.apply_resize_snap(dir);
set_cursor_icon(resize_cursor(dir));
crate::animation::request_draw();
return EventResult::Consumed;
}
DragMode::None => {
// Track which edge/corner the cursor is hovering over so
// paint_overlay can draw the appropriate highlight.
window.hover_dir = window.resize_dir(*pos);
if let Some(dir) = window.hover_dir {
set_cursor_icon(resize_cursor(dir));
}
}
}
if was_close != window.close_hovered
|| was_max != window.maximize_hovered
|| was_dir != window.hover_dir
{
crate::animation::request_draw();
}
EventResult::Ignored
}
// Click-away dismissal: while this window holds the modal grab, the
// App routes EVERY press to our subtree — including presses OUTSIDE
// our bounds (they arrive here with an out-of-range local `pos`). If
// click-away is enabled, ANY button pressed outside closes us through
// the unified `close()` path with `ClickAway` and swallows the press
// so it never activates whatever sat underneath. This arm precedes
// the button-specific handlers so a right/middle press-away dismisses
// too (a right-click must not, e.g., start a title drag). Wheel
// events are `MouseWheel`, not `MouseDown`, so scrolling outside
// stays inert.
Event::MouseDown { pos, .. }
if window.modal
&& window.click_away == ClickAwayAction::Close
&& !window.point_in_local_bounds(*pos) =>
{
window.close(CloseReason::ClickAway);
EventResult::Consumed
}
Event::MouseDown { button, pos, .. }
if matches!(*button, MouseButton::Left | MouseButton::Middle) =>
{
let is_left_click = *button == MouseButton::Left;
// Press-to-raise: any direct press on this window brings it forward.
window.raise_request.set(true);
// Z-order changes are visible; repaint.
crate::animation::request_draw();
if let Some(cb) = window.on_raised.as_mut() {
cb(&window.title);
}
// Close button — highest priority.
if is_left_click && window.in_close_button(*pos) {
window.close(CloseReason::CloseButton);
return EventResult::Consumed;
}
// Maximize / Restore button.
if is_left_click && window.in_maximize_button(*pos) {
window.toggle_maximize();
crate::animation::request_draw();
return EventResult::Consumed;
}
// Route the click into the title-bar sub-tree FIRST so
// any child widget there (currently the chevron) gets a
// chance to consume it. `WindowTitleBar` lives outside
// `Window.children` because the body content owns that
// slot, so the framework's normal hit-test pass never
// descends into it — we run the framework's hit-test
// + dispatch helpers manually on the sub-tree instead.
if is_left_click && window.in_title_bar(*pos) {
let tb_bounds = window.title_bar.bounds();
let tb_local = Point::new(pos.x - tb_bounds.x, pos.y - tb_bounds.y);
if let Some(path) = crate::widget::hit_test_subtree(&window.title_bar, tb_local) {
// Path could be empty (clicked the bar itself
// but not a child) — skip in that case so the
// title-drag handling further down still runs.
if !path.is_empty() {
// Preserve modifiers from the original event.
let mods = match event {
Event::MouseDown { modifiers, .. } => *modifiers,
_ => Default::default(),
};
let translated = Event::MouseDown {
pos: tb_local,
button: *button,
modifiers: mods,
};
let result = crate::widget::dispatch_event_dyn(
&mut window.title_bar,
&path,
&translated,
tb_local,
);
if result.is_consumed() {
// Chevron flag is drained in `layout`,
// but we also want this frame to redraw
// before that.
crate::animation::request_draw();
return EventResult::Consumed;
}
}
}
}
// Resize edge — check before title bar to handle corner overlap.
if let Some(dir) = window.resize_dir(*pos) {
// Only start resize if not in the close button area and not a pure title bar drag.
// The N edge overlaps the title bar — prefer resize over drag from the top N px.
let world = Point::new(pos.x + window.bounds.x, pos.y + window.bounds.y);
window.drag_mode = DragMode::Resize(dir);
window.drag_start_world = world;
window.drag_start_bounds = window.bounds;
return EventResult::Consumed;
}
// Title bar drag + double-click maximize.
if window.in_title_bar(*pos) {
// Double-click detection.
let is_double = if is_left_click {
let now = Instant::now();
window
.last_title_click
.map(|t| now.duration_since(t).as_millis() < DBL_CLICK_MS)
.unwrap_or(false)
} else {
false
};
if is_double {
// Windows convention: double-click title bar toggles
// maximize / restore. Collapse/expand lives on the
// chevron button to the left.
window.toggle_maximize();
window.last_title_click = None;
crate::animation::request_draw();
} else {
if is_left_click {
window.last_title_click = Some(Instant::now());
}
let world = Point::new(pos.x + window.bounds.x, pos.y + window.bounds.y);
window.drag_mode = DragMode::Move;
window.drag_start_world = world;
window.drag_start_bounds = window.bounds;
}
return EventResult::Consumed;
}
// Click on content area: consume so it doesn't fall through.
if is_left_click && !window.collapsed {
EventResult::Consumed
} else {
EventResult::Ignored
}
}
Event::MouseUp {
button: MouseButton::Left | MouseButton::Middle,
..
} => {
let was_dragging = window.drag_mode != DragMode::None;
window.drag_mode = DragMode::None;
if was_dragging {
// Drag ended — wipe the snap guides so the
// overlay clears. Cheap no-op when snapping was
// off (guide buffer was already empty).
crate::snap::clear_guides();
crate::animation::request_draw();
EventResult::Consumed
} else {
EventResult::Ignored
}
}
// Escape closes a modal window (standard dialog convention),
// running the same teardown as the × button so `on_close` fires.
// Modal key routing bubbles Escape up to us when no inner field
// consumed it (see `App::on_key_down`); non-modal windows ignore it
// so app-level shortcuts still see the key.
Event::KeyDown {
key: Key::Escape, ..
} if window.modal => {
window.close(CloseReason::Escape);
EventResult::Consumed
}
_ => EventResult::Ignored,
}
}