• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jzombie / term-wm / 20885255367

10 Jan 2026 10:20PM UTC coverage: 57.056% (+10.0%) from 47.071%
20885255367

Pull #20

github

web-flow
Merge 123a43984 into bfecf0f75
Pull Request #20: Initial clipboard and offscreen buffer support

2045 of 3183 new or added lines in 26 files covered. (64.25%)

76 existing lines in 15 files now uncovered.

6788 of 11897 relevant lines covered (57.06%)

9.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

50.46
/src/window/window_manager.rs
1
use super::Window;
2
use std::collections::{BTreeMap, BTreeSet};
3
use std::sync::Arc;
4
use std::time::{Duration, Instant};
5

6
use crossterm::event::{Event, MouseEvent, MouseEventKind};
7
use ratatui::prelude::Rect;
8
use ratatui::widgets::Clear;
9

10
use super::decorator::{DefaultDecorator, HeaderAction, WindowDecorator};
11
use crate::clipboard;
12
use crate::components::{
13
    Component, ComponentContext, ConfirmAction, ConfirmOverlayComponent, Overlay,
14
    sys::debug_log::{DebugLogComponent, install_panic_hook, set_global_debug_log},
15
    sys::help_overlay::HelpOverlayComponent,
16
};
17
use crate::constants::MIN_FLOATING_VISIBLE_MARGIN;
18
use crate::layout::floating::*;
19
use crate::layout::{
20
    FloatingPane, InsertPosition, LayoutNode, LayoutPlan, RegionMap, SplitHandle, TilingLayout,
21
    rect_contains, render_handles_masked,
22
};
23
use crate::panel::Panel;
24
use crate::state::AppState;
25
use crate::ui::UiFrame;
26

27
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28
/// Describes who owns layout placement and how WM-level input is handled.
29
///
30
/// - AppManaged: the app owns regions; `Esc` passes through.
31
/// - WindowManaged: the WM owns layout; `Esc` enters WM mode/overlay.
32
pub enum LayoutContract {
33
    AppManaged,
34
    WindowManaged,
35
}
36

37
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
38
pub enum SystemWindowId {
39
    DebugLog,
40
}
41

42
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
43
pub enum OverlayId {
44
    Help,
45
    ExitConfirm,
46
}
47

48
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
49
pub enum WindowId<R: Copy + Eq + Ord> {
50
    App(R),
51
    System(SystemWindowId),
52
}
53

54
impl<R: Copy + Eq + Ord> WindowId<R> {
55
    fn app(id: R) -> Self {
36✔
56
        Self::App(id)
36✔
57
    }
36✔
58

59
    fn system(id: SystemWindowId) -> Self {
19✔
60
        Self::System(id)
19✔
61
    }
19✔
62

63
    fn as_app(self) -> Option<R> {
16✔
64
        match self {
16✔
65
            Self::App(id) => Some(id),
10✔
66
            Self::System(_) => None,
6✔
67
        }
68
    }
16✔
69
}
70

71
#[derive(Debug, Clone, Copy, Default)]
72
pub struct ScrollState {
73
    pub offset: usize,
74
    pending: isize,
75
}
76

77
impl ScrollState {
UNCOV
78
    pub fn reset(&mut self) {
×
UNCOV
79
        self.offset = 0;
×
UNCOV
80
        self.pending = 0;
×
UNCOV
81
    }
×
82

83
    pub fn bump(&mut self, delta: isize) {
1✔
84
        self.pending = self.pending.saturating_add(delta);
1✔
85
    }
1✔
86

87
    pub fn apply(&mut self, total: usize, view: usize) {
2✔
88
        let max_offset = total.saturating_sub(view);
2✔
89
        if self.pending != 0 {
2✔
90
            let delta = self.pending;
1✔
91
            self.pending = 0;
1✔
92
            let next = if delta.is_negative() {
1✔
93
                self.offset.saturating_sub(delta.unsigned_abs())
×
94
            } else {
95
                self.offset.saturating_add(delta as usize)
1✔
96
            };
97
            self.offset = next.min(max_offset);
1✔
98
        } else if self.offset > max_offset {
1✔
99
            self.offset = max_offset;
1✔
100
        }
1✔
101
    }
2✔
102
}
103

104
#[derive(Debug, Clone, Copy)]
105
pub struct WindowSurface {
106
    pub full: Rect,
107
    pub inner: Rect,
108
    pub dest: crate::window::FloatRect,
109
}
110

111
#[derive(Debug, Clone, Copy)]
112
pub struct AppWindowDraw<R: Copy + Eq + Ord> {
113
    pub id: R,
114
    pub surface: WindowSurface,
115
    pub focused: bool,
116
}
117

118
#[derive(Debug, Clone, Copy)]
119
pub enum WindowDrawTask<R: Copy + Eq + Ord> {
120
    App(AppWindowDraw<R>),
121
    System(SystemWindowDraw),
122
}
123

124
#[derive(Debug, Clone, Copy)]
125
pub struct SystemWindowDraw {
126
    pub id: SystemWindowId,
127
    pub surface: WindowSurface,
128
    pub focused: bool,
129
}
130

131
trait SystemWindowView {
132
    fn render(&mut self, frame: &mut UiFrame<'_>, surface: WindowSurface, focused: bool);
133
    fn handle_event(&mut self, event: &Event) -> bool;
NEW
134
    fn set_selection_enabled(&mut self, _enabled: bool) {}
×
135
}
136

137
impl SystemWindowView for DebugLogComponent {
NEW
138
    fn render(&mut self, frame: &mut UiFrame<'_>, surface: WindowSurface, focused: bool) {
×
NEW
139
        let ctx = ComponentContext::new(focused);
×
NEW
140
        <DebugLogComponent as Component>::render(self, frame, surface.inner, &ctx);
×
UNCOV
141
    }
×
142

143
    fn handle_event(&mut self, event: &Event) -> bool {
×
NEW
144
        Component::handle_event(self, event, &ComponentContext::default())
×
NEW
145
    }
×
146

NEW
147
    fn set_selection_enabled(&mut self, enabled: bool) {
×
NEW
148
        DebugLogComponent::set_selection_enabled(self, enabled);
×
UNCOV
149
    }
×
150
}
151

152
struct SystemWindowEntry {
153
    component: Box<dyn SystemWindowView>,
154
    visible: bool,
155
}
156

157
impl SystemWindowEntry {
158
    fn new(component: Box<dyn SystemWindowView>) -> Self {
13✔
159
        Self {
13✔
160
            component,
13✔
161
            visible: false,
13✔
162
        }
13✔
163
    }
13✔
164

165
    fn visible(&self) -> bool {
10✔
166
        self.visible
10✔
167
    }
10✔
168

169
    fn set_visible(&mut self, visible: bool) {
1✔
170
        self.visible = visible;
1✔
171
    }
1✔
172

173
    fn render(&mut self, frame: &mut UiFrame<'_>, surface: WindowSurface, focused: bool) {
×
NEW
174
        self.component.render(frame, surface, focused);
×
175
    }
×
176

177
    fn handle_event(&mut self, event: &Event) -> bool {
×
178
        self.component.handle_event(event)
×
179
    }
×
180

NEW
181
    fn set_selection_enabled(&mut self, enabled: bool) {
×
NEW
182
        self.component.set_selection_enabled(enabled);
×
NEW
183
    }
×
184
}
185

186
#[derive(Debug, Clone)]
187
pub struct FocusRing<T: Copy + Eq> {
188
    order: Vec<T>,
189
    current: T,
190
}
191

192
impl<T: Copy + Eq> FocusRing<T> {
193
    pub fn new(current: T) -> Self {
27✔
194
        Self {
27✔
195
            order: Vec::new(),
27✔
196
            current,
27✔
197
        }
27✔
198
    }
27✔
199

200
    pub fn set_order(&mut self, order: Vec<T>) {
8✔
201
        self.order = order;
8✔
202
    }
8✔
203

204
    pub fn current(&self) -> T {
16✔
205
        self.current
16✔
206
    }
16✔
207

208
    pub fn set_current(&mut self, current: T) {
4✔
209
        self.current = current;
4✔
210
    }
4✔
211

212
    pub fn advance(&mut self, forward: bool) {
3✔
213
        if self.order.is_empty() {
3✔
214
            return;
×
215
        }
3✔
216
        let idx = self
3✔
217
            .order
3✔
218
            .iter()
3✔
219
            .position(|item| *item == self.current)
6✔
220
            .unwrap_or(0);
3✔
221
        let step = if forward { 1isize } else { -1isize };
3✔
222
        let next = ((idx as isize + step).rem_euclid(self.order.len() as isize)) as usize;
3✔
223
        self.current = self.order[next];
3✔
224
    }
3✔
225
}
226

227
pub struct WindowManager<W: Copy + Eq + Ord, R: Copy + Eq + Ord> {
228
    app_focus: FocusRing<W>,
229
    wm_focus: FocusRing<WindowId<R>>,
230
    windows: BTreeMap<WindowId<R>, Window>,
231
    regions: RegionMap<WindowId<R>>,
232
    scroll: BTreeMap<W, ScrollState>,
233
    handles: Vec<SplitHandle>,
234
    resize_handles: Vec<ResizeHandle<WindowId<R>>>,
235
    floating_headers: Vec<DragHandle<WindowId<R>>>,
236
    managed_draw_order: Vec<WindowId<R>>,
237
    managed_draw_order_app: Vec<R>,
238
    managed_layout: Option<TilingLayout<WindowId<R>>>,
239
    // queue of app ids removed this frame; runner drains via `take_closed_app_windows`
240
    closed_app_windows: Vec<R>,
241
    managed_area: Rect,
242
    panel: Panel<WindowId<R>>,
243
    drag_header: Option<HeaderDrag<WindowId<R>>>,
244
    last_header_click: Option<(WindowId<R>, Instant)>,
245
    drag_resize: Option<ResizeDrag<WindowId<R>>>,
246
    hover: Option<(u16, u16)>,
247
    capture_deadline: Option<Instant>,
248
    pending_deadline: Option<Instant>,
249
    state: AppState,
250
    clipboard_available: bool,
251
    layout_contract: LayoutContract,
252
    wm_overlay_opened_at: Option<Instant>,
253
    last_frame_area: ratatui::prelude::Rect,
254
    esc_passthrough_window: Duration,
255
    overlays: BTreeMap<OverlayId, Box<dyn Overlay>>,
256
    // Central default for whether ScrollViewComponent keyboard handling should be enabled
257
    // for UI components that opt into it. Individual components can override.
258
    scroll_keyboard_enabled_default: bool,
259
    decorator: Arc<dyn WindowDecorator>,
260
    floating_resize_offscreen: bool,
261
    z_order: Vec<WindowId<R>>,
262
    drag_snap: Option<(Option<WindowId<R>>, InsertPosition, Rect)>,
263
    system_windows: BTreeMap<SystemWindowId, SystemWindowEntry>,
264
    next_window_seq: usize,
265
}
266

267
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
268
pub enum WmMenuAction {
269
    CloseMenu,
270
    Help,
271
    NewWindow,
272
    ToggleDebugWindow,
273
    ExitUi,
274
    BringFloatingFront,
275
    MinimizeWindow,
276
    MaximizeWindow,
277
    CloseWindow,
278
    ToggleMouseCapture,
279
    ToggleClipboardMode,
280
}
281

282
impl<W: Copy + Eq + Ord, R: Copy + Eq + Ord + std::fmt::Debug> WindowManager<W, R>
283
where
284
    R: PartialEq<W>,
285
{
286
    fn window_mut(&mut self, id: WindowId<R>) -> &mut Window {
15✔
287
        let seq = &mut self.next_window_seq;
15✔
288
        self.windows.entry(id).or_insert_with(|| {
15✔
289
            let order = *seq;
6✔
290
            *seq = order.saturating_add(1);
6✔
291
            tracing::debug!(window_id = ?id, seq = order, "opened window");
6✔
292
            Window::new(order)
6✔
293
        })
6✔
294
    }
15✔
295

296
    fn window(&self, id: WindowId<R>) -> Option<&Window> {
43✔
297
        self.windows.get(&id)
43✔
298
    }
43✔
299

300
    fn is_minimized(&self, id: WindowId<R>) -> bool {
1✔
301
        self.window(id).is_some_and(|window| window.minimized)
1✔
302
    }
1✔
303

304
    fn set_minimized(&mut self, id: WindowId<R>, value: bool) {
×
305
        self.window_mut(id).minimized = value;
×
306
    }
×
307

308
    fn floating_rect(&self, id: WindowId<R>) -> Option<crate::window::FloatRectSpec> {
36✔
309
        self.window(id).and_then(|window| window.floating_rect)
36✔
310
    }
36✔
311

312
    fn set_floating_rect(&mut self, id: WindowId<R>, rect: Option<crate::window::FloatRectSpec>) {
12✔
313
        self.window_mut(id).floating_rect = rect;
12✔
314
    }
12✔
315

316
    fn clear_floating_rect(&mut self, id: WindowId<R>) {
×
317
        self.window_mut(id).floating_rect = None;
×
318
    }
×
319

320
    fn set_prev_floating_rect(
1✔
321
        &mut self,
1✔
322
        id: WindowId<R>,
1✔
323
        rect: Option<crate::window::FloatRectSpec>,
1✔
324
    ) {
1✔
325
        self.window_mut(id).prev_floating_rect = rect;
1✔
326
    }
1✔
327

NEW
328
    fn take_prev_floating_rect(&mut self, id: WindowId<R>) -> Option<crate::window::FloatRectSpec> {
×
329
        self.window_mut(id).prev_floating_rect.take()
×
330
    }
×
331
    fn is_window_floating(&self, id: WindowId<R>) -> bool {
6✔
332
        self.window(id).is_some_and(|window| window.is_floating())
6✔
333
    }
6✔
334

NEW
335
    pub fn window_title(&self, id: WindowId<R>) -> String {
×
336
        self.window(id)
×
337
            .map(|window| window.title_or_default(id))
×
338
            .unwrap_or_else(|| match id {
×
339
                WindowId::App(app_id) => format!("{:?}", app_id),
×
340
                WindowId::System(SystemWindowId::DebugLog) => "Debug Log".to_string(),
×
341
            })
×
342
    }
×
343

344
    fn clear_all_floating(&mut self) {
×
345
        for window in self.windows.values_mut() {
×
346
            window.floating_rect = None;
×
347
            window.prev_floating_rect = None;
×
348
        }
×
349
    }
×
350

351
    fn system_window_entry(&self, id: SystemWindowId) -> Option<&SystemWindowEntry> {
9✔
352
        self.system_windows.get(&id)
9✔
353
    }
9✔
354

355
    fn system_window_entry_mut(&mut self, id: SystemWindowId) -> Option<&mut SystemWindowEntry> {
1✔
356
        self.system_windows.get_mut(&id)
1✔
357
    }
1✔
358

359
    fn system_window_visible(&self, id: SystemWindowId) -> bool {
8✔
360
        self.system_window_entry(id)
8✔
361
            .map(|entry| entry.visible())
8✔
362
            .unwrap_or(false)
8✔
363
    }
8✔
364

365
    fn set_system_window_visible(&mut self, id: SystemWindowId, visible: bool) {
1✔
366
        if let Some(entry) = self.system_window_entry_mut(id) {
1✔
367
            entry.set_visible(visible);
1✔
368
        }
1✔
369
    }
1✔
370

371
    fn show_system_window(&mut self, id: SystemWindowId) {
1✔
372
        if self.system_window_visible(id) {
1✔
373
            return;
×
374
        }
1✔
375
        if self.system_window_entry(id).is_none() {
1✔
376
            return;
×
377
        }
1✔
378
        self.set_system_window_visible(id, true);
1✔
379
        if self.layout_contract != LayoutContract::WindowManaged {
1✔
380
            return;
×
381
        }
1✔
382
        let window_id = WindowId::system(id);
1✔
383
        let _ = self.window_mut(window_id);
1✔
384
        self.ensure_system_window_in_layout(window_id);
1✔
385
        self.focus_window_id(window_id);
1✔
386
    }
1✔
387

388
    fn hide_system_window(&mut self, id: SystemWindowId) {
×
389
        if !self.system_window_visible(id) {
×
390
            return;
×
391
        }
×
392
        let window_id = WindowId::system(id);
×
393
        self.set_system_window_visible(id, false);
×
394
        self.remove_system_window_from_layout(window_id);
×
395
        if self.wm_focus.current() == window_id {
×
396
            self.select_fallback_focus();
×
397
        }
×
398
    }
×
399

400
    fn ensure_system_window_in_layout(&mut self, id: WindowId<R>) {
2✔
401
        if self.layout_contract != LayoutContract::WindowManaged {
2✔
402
            return;
×
403
        }
2✔
404
        if self.layout_contains(id) {
2✔
405
            return;
1✔
406
        }
1✔
407
        let _ = self.window_mut(id);
1✔
408
        if self.managed_layout.is_none() {
1✔
409
            self.managed_layout = Some(TilingLayout::new(LayoutNode::leaf(id)));
1✔
410
            return;
1✔
411
        }
×
412
        let _ = self.tile_window_id(id);
×
413
    }
2✔
414

415
    fn remove_system_window_from_layout(&mut self, id: WindowId<R>) {
×
416
        self.clear_floating_rect(id);
×
417
        if let Some(layout) = &mut self.managed_layout {
×
418
            if matches!(layout.root(), LayoutNode::Leaf(root_id) if *root_id == id) {
×
419
                self.managed_layout = None;
×
420
            } else {
×
421
                layout.root_mut().remove_leaf(id);
×
422
            }
×
423
        }
×
424
        self.z_order.retain(|window_id| *window_id != id);
×
425
        self.managed_draw_order.retain(|window_id| *window_id != id);
×
426
    }
×
427

428
    fn dispatch_system_window_event(&mut self, id: SystemWindowId, event: &Event) -> bool {
×
NEW
429
        if let Some(localized) = self.localize_event_content(WindowId::system(id), event) {
×
NEW
430
            return self.dispatch_system_window_event_localized(id, &localized);
×
NEW
431
        }
×
432
        self.system_window_entry_mut(id)
×
433
            .map(|entry| entry.handle_event(event))
×
434
            .unwrap_or(false)
×
435
    }
×
436

NEW
437
    fn dispatch_system_window_event_localized(
×
NEW
438
        &mut self,
×
NEW
439
        id: SystemWindowId,
×
NEW
440
        event: &Event,
×
NEW
441
    ) -> bool {
×
NEW
442
        let adjusted = self.adjust_event_for_window(WindowId::system(id), event);
×
NEW
443
        self.system_window_entry_mut(id)
×
NEW
444
            .map(|entry| entry.handle_event(&adjusted))
×
NEW
445
            .unwrap_or(false)
×
NEW
446
    }
×
447

448
    fn render_system_window_entry(&mut self, frame: &mut UiFrame<'_>, draw: SystemWindowDraw) {
×
449
        if let Some(entry) = self.system_window_entry_mut(draw.id) {
×
450
            entry.render(frame, draw.surface, draw.focused);
×
451
        }
×
452
    }
×
453

454
    pub fn new(current: W) -> Self {
13✔
455
        let clipboard_available = clipboard::available();
13✔
456
        let mut state = AppState::new();
13✔
457
        if !clipboard_available {
13✔
458
            state.set_clipboard_enabled(false);
13✔
459
        }
13✔
460
        let selection_enabled = state.clipboard_enabled();
13✔
461
        Self {
13✔
462
            app_focus: FocusRing::new(current),
13✔
463
            wm_focus: FocusRing::new(WindowId::system(SystemWindowId::DebugLog)),
13✔
464
            windows: BTreeMap::new(),
13✔
465
            regions: RegionMap::default(),
13✔
466
            scroll: BTreeMap::new(),
13✔
467
            handles: Vec::new(),
13✔
468
            resize_handles: Vec::new(),
13✔
469
            floating_headers: Vec::new(),
13✔
470
            managed_draw_order: Vec::new(),
13✔
471
            managed_draw_order_app: Vec::new(),
13✔
472
            managed_layout: None,
13✔
473
            closed_app_windows: Vec::new(),
13✔
474
            managed_area: Rect::default(),
13✔
475
            panel: Panel::new(),
13✔
476
            drag_header: None,
13✔
477
            last_header_click: None,
13✔
478
            drag_resize: None,
13✔
479
            hover: None,
13✔
480
            capture_deadline: None,
13✔
481
            pending_deadline: None,
13✔
482
            state,
13✔
483
            clipboard_available,
13✔
484
            layout_contract: LayoutContract::AppManaged,
13✔
485
            wm_overlay_opened_at: None,
13✔
486
            last_frame_area: Rect::default(),
13✔
487
            esc_passthrough_window: esc_passthrough_window_default(),
13✔
488
            overlays: BTreeMap::new(),
13✔
489
            scroll_keyboard_enabled_default: true,
13✔
490
            decorator: Arc::new(DefaultDecorator),
13✔
491
            floating_resize_offscreen: true,
13✔
492
            z_order: Vec::new(),
13✔
493
            drag_snap: None,
13✔
494
            system_windows: {
13✔
495
                let (mut component, handle) = DebugLogComponent::new_default();
13✔
496
                component.set_selection_enabled(selection_enabled);
13✔
497
                set_global_debug_log(handle);
13✔
498
                // Initialize tracing now that the global debug log handle exists
13✔
499
                // so tracing will write into the in-memory debug buffer by default.
13✔
500
                crate::tracing_sub::init_default();
13✔
501
                install_panic_hook();
13✔
502
                let mut map = BTreeMap::new();
13✔
503
                map.insert(
13✔
504
                    SystemWindowId::DebugLog,
13✔
505
                    SystemWindowEntry::new(Box::new(component)),
13✔
506
                );
13✔
507
                map
13✔
508
            },
13✔
509
            next_window_seq: 0,
13✔
510
        }
13✔
511
    }
13✔
512

513
    pub fn new_managed(current: W) -> Self {
13✔
514
        let mut manager = Self::new(current);
13✔
515
        manager.layout_contract = LayoutContract::WindowManaged;
13✔
516
        manager
13✔
517
    }
13✔
518

519
    pub fn set_layout_contract(&mut self, contract: LayoutContract) {
×
520
        self.layout_contract = contract;
×
521
    }
×
522

523
    /// Drain and return any app ids whose windows were closed since the last call.
524
    pub fn take_closed_app_windows(&mut self) -> Vec<R> {
×
525
        std::mem::take(&mut self.closed_app_windows)
×
526
    }
×
527

528
    pub fn layout_contract(&self) -> LayoutContract {
×
529
        self.layout_contract
×
530
    }
×
531

532
    pub fn set_floating_resize_offscreen(&mut self, enabled: bool) {
4✔
533
        self.floating_resize_offscreen = enabled;
4✔
534
    }
4✔
535

536
    pub fn floating_resize_offscreen(&self) -> bool {
×
537
        self.floating_resize_offscreen
×
538
    }
×
539

540
    pub fn begin_frame(&mut self) {
×
541
        self.regions = RegionMap::default();
×
542
        self.handles.clear();
×
543
        self.resize_handles.clear();
×
544
        self.floating_headers.clear();
×
545
        self.managed_draw_order.clear();
×
546
        self.managed_draw_order_app.clear();
×
547
        self.panel.begin_frame();
×
548
        // If a panic occurred earlier, ensure the debug log is shown and focused.
549
        if crate::components::sys::debug_log::take_panic_pending() {
×
550
            self.show_system_window(SystemWindowId::DebugLog);
×
551
        }
×
552
        if self.layout_contract == LayoutContract::AppManaged {
×
553
            self.clear_capture();
×
554
        } else {
×
555
            // Refresh deadlines so overlay badges can expire without events.
×
556
            self.refresh_capture();
×
557
        }
×
558
    }
×
559

560
    pub fn arm_capture(&mut self, timeout: Duration) {
×
561
        self.capture_deadline = Some(Instant::now() + timeout);
×
562
        self.pending_deadline = None;
×
563
    }
×
564

565
    pub fn arm_pending(&mut self, timeout: Duration) {
×
566
        // Shows an "Esc pending" badge while waiting for the chord.
567
        self.pending_deadline = Some(Instant::now() + timeout);
×
568
    }
×
569

570
    pub fn clear_capture(&mut self) {
×
571
        self.capture_deadline = None;
×
572
        self.pending_deadline = None;
×
573
        self.state.set_overlay_visible(false);
×
574
        self.wm_overlay_opened_at = None;
×
575
        self.state.set_wm_menu_selected(0);
×
576
    }
×
577

578
    pub fn capture_active(&mut self) -> bool {
×
579
        if !self.state.mouse_capture_enabled() {
×
580
            return false;
×
581
        }
×
582
        if self.layout_contract == LayoutContract::WindowManaged && self.state.overlay_visible() {
×
583
            return true;
×
584
        }
×
585
        self.refresh_capture();
×
586
        self.capture_deadline.is_some()
×
587
    }
×
588

589
    pub fn mouse_capture_enabled(&self) -> bool {
×
590
        self.state.mouse_capture_enabled()
×
591
    }
×
592

593
    pub fn set_mouse_capture_enabled(&mut self, enabled: bool) {
×
594
        self.state.set_mouse_capture_enabled(enabled);
×
595
        if !self.state.mouse_capture_enabled() {
×
596
            self.clear_capture();
×
597
        }
×
598
    }
×
599

600
    pub fn toggle_mouse_capture(&mut self) {
×
601
        self.state.toggle_mouse_capture();
×
602
        if !self.state.mouse_capture_enabled() {
×
603
            self.clear_capture();
×
604
        }
×
605
    }
×
606

607
    pub fn take_mouse_capture_change(&mut self) -> Option<bool> {
×
608
        self.state.take_mouse_capture_change()
×
609
    }
×
610

NEW
611
    pub fn take_clipboard_change(&mut self) -> Option<bool> {
×
NEW
612
        self.state.take_clipboard_change()
×
NEW
613
    }
×
614

NEW
615
    pub fn clipboard_available(&self) -> bool {
×
NEW
616
        self.clipboard_available
×
NEW
617
    }
×
618

NEW
619
    pub fn clipboard_enabled(&self) -> bool {
×
NEW
620
        self.state.clipboard_enabled()
×
NEW
621
    }
×
622

NEW
623
    pub fn set_clipboard_enabled(&mut self, enabled: bool) {
×
NEW
624
        if !self.clipboard_available {
×
NEW
625
            return;
×
NEW
626
        }
×
NEW
627
        if self.state.clipboard_enabled() == enabled {
×
NEW
628
            return;
×
NEW
629
        }
×
NEW
630
        self.state.set_clipboard_enabled(enabled);
×
NEW
631
        self.apply_clipboard_selection_state(enabled);
×
NEW
632
    }
×
633

NEW
634
    pub fn toggle_clipboard_enabled(&mut self) {
×
NEW
635
        if !self.clipboard_available {
×
NEW
636
            return;
×
NEW
637
        }
×
NEW
638
        let next = !self.state.clipboard_enabled();
×
NEW
639
        self.set_clipboard_enabled(next);
×
NEW
640
    }
×
641

NEW
642
    fn apply_clipboard_selection_state(&mut self, enabled: bool) {
×
NEW
643
        for entry in self.system_windows.values_mut() {
×
NEW
644
            entry.set_selection_enabled(enabled);
×
NEW
645
        }
×
NEW
646
        for overlay in self.overlays.values_mut() {
×
NEW
647
            if let Some(help) = overlay.as_any_mut().downcast_mut::<HelpOverlayComponent>() {
×
NEW
648
                help.set_selection_enabled(enabled);
×
NEW
649
            }
×
650
        }
NEW
651
    }
×
652

653
    fn refresh_capture(&mut self) {
×
654
        if let Some(deadline) = self.capture_deadline
×
655
            && Instant::now() > deadline
×
656
        {
×
657
            self.capture_deadline = None;
×
658
        }
×
659
        if let Some(deadline) = self.pending_deadline
×
660
            && Instant::now() > deadline
×
661
        {
×
662
            self.pending_deadline = None;
×
663
        }
×
664
    }
×
665

666
    pub fn open_wm_overlay(&mut self) {
×
667
        self.state.set_overlay_visible(true);
×
668
        self.wm_overlay_opened_at = Some(Instant::now());
×
669
        self.state.set_wm_menu_selected(0);
×
670
    }
×
671

672
    pub fn close_wm_overlay(&mut self) {
×
673
        self.state.set_overlay_visible(false);
×
674
        self.wm_overlay_opened_at = None;
×
675
        self.state.set_wm_menu_selected(0);
×
676
    }
×
677

678
    pub fn open_exit_confirm(&mut self) {
×
679
        let mut confirm = ConfirmOverlayComponent::new();
×
680
        // Area is likely set during render/event handling, but ConfirmOverlayComponent uses default.
681
        confirm.open(
×
682
            "Exit App",
×
683
            "Exit the application?\nUnsaved changes will be lost.",
×
684
        );
685
        self.overlays
×
686
            .insert(OverlayId::ExitConfirm, Box::new(confirm));
×
687
    }
×
688

689
    pub fn close_exit_confirm(&mut self) {
×
690
        self.overlays.remove(&OverlayId::ExitConfirm);
×
691
    }
×
692

693
    pub fn exit_confirm_visible(&self) -> bool {
×
694
        self.overlays.contains_key(&OverlayId::ExitConfirm)
×
695
    }
×
696

697
    pub fn help_overlay_visible(&self) -> bool {
×
698
        self.overlays.contains_key(&OverlayId::Help)
×
699
    }
×
700

701
    pub fn open_help_overlay(&mut self) {
×
702
        let mut h = HelpOverlayComponent::new();
×
703
        h.show();
×
NEW
704
        h.set_selection_enabled(self.clipboard_enabled());
×
705
        // respect central default: if globally disabled, ensure the overlay doesn't enable keys
706
        if !self.scroll_keyboard_enabled_default {
×
707
            h.set_keyboard_enabled(false);
×
708
        }
×
709
        self.overlays.insert(OverlayId::Help, Box::new(h));
×
710
    }
×
711

712
    pub fn close_help_overlay(&mut self) {
×
713
        self.overlays.remove(&OverlayId::Help);
×
714
    }
×
715

716
    /// Set the central default for enabling scroll-keyboard handling.
717
    pub fn set_scroll_keyboard_enabled(&mut self, enabled: bool) {
×
718
        self.scroll_keyboard_enabled_default = enabled;
×
719
    }
×
720

721
    pub fn handle_help_event(&mut self, event: &Event) -> bool {
×
722
        // Retrieve component as &mut Box<dyn Overlay>
723
        let Some(boxed) = self.overlays.get_mut(&OverlayId::Help) else {
×
724
            return false;
×
725
        };
726

727
        // We need to invoke handle_event on the HelpOverlayComponent.
728
        // Since we refactored it to use internal area and Component::handle_event,
729
        // we can just use the trait method.
730
        // BUT, HelpOverlayComponent::handle_event now calls handle_help_event_in_area with stored area.
731
        // Update area to ensure correct hit-testing (Component::resize)
NEW
732
        boxed.resize(
×
NEW
733
            self.last_frame_area,
×
NEW
734
            &ComponentContext::new(true).with_overlay(true),
×
735
        );
736

NEW
737
        let handled = boxed.handle_event(event, &ComponentContext::new(true).with_overlay(true));
×
738

739
        // Remove the overlay if it has closed itself
740
        let should_close = if let Some(help) = boxed.as_any().downcast_ref::<HelpOverlayComponent>()
×
741
        {
742
            !help.visible()
×
743
        } else {
744
            false
×
745
        };
746

747
        if should_close {
×
748
            self.overlays.remove(&OverlayId::Help);
×
749
        }
×
750

751
        handled
×
752
    }
×
753

754
    pub fn wm_overlay_visible(&self) -> bool {
×
755
        self.state.overlay_visible()
×
756
    }
×
757

758
    pub fn toggle_debug_window(&mut self) {
×
759
        if self.system_window_visible(SystemWindowId::DebugLog) {
×
760
            self.hide_system_window(SystemWindowId::DebugLog);
×
761
        } else {
×
762
            self.show_system_window(SystemWindowId::DebugLog);
×
763
        }
×
764
    }
×
765

766
    pub fn open_debug_window(&mut self) {
×
767
        if !self.system_window_visible(SystemWindowId::DebugLog) {
×
768
            self.show_system_window(SystemWindowId::DebugLog);
×
769
        }
×
770
    }
×
771

772
    pub fn debug_window_visible(&self) -> bool {
×
773
        self.system_window_visible(SystemWindowId::DebugLog)
×
774
    }
×
775

776
    pub fn has_active_system_windows(&self) -> bool {
2✔
777
        self.system_windows.values().any(|w| w.visible()) || !self.overlays.is_empty()
2✔
778
    }
2✔
779

780
    /// Returns true when any windows are still active (app or system).
781
    ///
782
    /// This is intended as a simple, conservative check for callers that
783
    /// want to know whether the window manager currently has any visible
784
    /// or managed windows remaining.
785
    pub fn has_any_active_windows(&self) -> bool {
1✔
786
        // Active system windows or overlays
787
        if self.has_active_system_windows() {
1✔
788
            return true;
×
789
        }
1✔
790
        // Any regions (tiled or floating) indicate active app windows
791
        if !self.regions.ids().is_empty() {
1✔
792
            return true;
×
793
        }
1✔
794
        // Z-order may contain app windows (including floating ones)
795
        if self.z_order.iter().any(|id| id.as_app().is_some()) {
1✔
796
            return true;
×
797
        }
1✔
798
        false
1✔
799
    }
1✔
800

801
    pub fn esc_passthrough_active(&self) -> bool {
×
802
        self.esc_passthrough_remaining().is_some()
×
803
    }
×
804

805
    pub fn esc_passthrough_remaining(&self) -> Option<Duration> {
×
806
        if !self.wm_overlay_visible() {
×
807
            return None;
×
808
        }
×
809
        let opened_at = self.wm_overlay_opened_at?;
×
810
        let elapsed = opened_at.elapsed();
×
811
        if elapsed >= self.esc_passthrough_window {
×
812
            return None;
×
813
        }
×
814
        Some(self.esc_passthrough_window.saturating_sub(elapsed))
×
815
    }
×
816

817
    pub fn focus(&self) -> W {
×
818
        self.app_focus.current()
×
819
    }
×
820

NEW
821
    pub fn focused_window(&self) -> WindowId<R> {
×
NEW
822
        self.wm_focus.current()
×
NEW
823
    }
×
824

825
    /// Returns the currently focused window along with an event localized to its content area.
NEW
826
    pub fn focused_window_event(&self, event: &Event) -> Option<(WindowId<R>, Event)> {
×
NEW
827
        let window_id = self.focused_window();
×
NEW
828
        let localized = self
×
NEW
829
            .localize_event_content(window_id, event)
×
NEW
830
            .unwrap_or_else(|| event.clone());
×
NEW
831
        Some((window_id, localized))
×
NEW
832
    }
×
833

834
    /// Handle managed chrome interactions first, then dispatch the localized event to either the
835
    /// focused system window or the provided app handler. Returns true when the event was consumed.
NEW
836
    pub fn dispatch_focused_event<F>(&mut self, event: &Event, mut on_app: F) -> bool
×
NEW
837
    where
×
NEW
838
        F: FnMut(R, &Event) -> bool,
×
839
    {
NEW
840
        if matches!(event, Event::Mouse(_)) && self.handle_managed_event(event) {
×
NEW
841
            return true;
×
NEW
842
        }
×
NEW
843
        let Some((window_id, localized)) = self.focused_window_event(event) else {
×
NEW
844
            return false;
×
845
        };
NEW
846
        let adjusted = self.adjust_event_for_window(window_id, &localized);
×
NEW
847
        match window_id {
×
NEW
848
            WindowId::App(id) => on_app(id, &adjusted),
×
NEW
849
            WindowId::System(system_id) => {
×
NEW
850
                self.dispatch_system_window_event_localized(system_id, &adjusted)
×
851
            }
852
        }
NEW
853
    }
×
854

855
    pub fn set_focus(&mut self, focus: W) {
×
856
        self.app_focus.set_current(focus);
×
857
    }
×
858

859
    pub fn set_focus_order(&mut self, order: Vec<W>) {
×
860
        self.app_focus.set_order(order);
×
861
        if !self.app_focus.order.is_empty()
×
862
            && !self.app_focus.order.contains(&self.app_focus.current)
×
863
        {
×
864
            self.app_focus.current = self.app_focus.order[0];
×
865
        }
×
866
    }
×
867

868
    pub fn advance_focus(&mut self, forward: bool) {
×
869
        self.app_focus.advance(forward);
×
870
    }
×
871

872
    pub fn wm_focus(&self) -> WindowId<R> {
×
873
        self.wm_focus.current()
×
874
    }
×
875

876
    pub fn wm_focus_app(&self) -> Option<R> {
2✔
877
        self.wm_focus.current().as_app()
2✔
878
    }
2✔
879

880
    fn set_wm_focus(&mut self, focus: WindowId<R>) {
4✔
881
        self.wm_focus.set_current(focus);
4✔
882
        if let Some(app_id) = focus.as_app()
4✔
883
            && let Some(app_focus) = self.focus_for_region(app_id)
2✔
884
        {
×
885
            self.app_focus.set_current(app_focus);
×
886
        }
4✔
887
    }
4✔
888

889
    /// Unified focus API: set WM focus, bring the window to front, update draw order,
890
    /// and sync app-level focus if applicable.
891
    fn focus_window_id(&mut self, id: WindowId<R>) {
4✔
892
        self.set_wm_focus(id);
4✔
893
        self.bring_to_front_id(id);
4✔
894
        self.managed_draw_order = self.z_order.clone();
4✔
895
        if let Some(app_id) = id.as_app()
4✔
896
            && let Some(app_focus) = self.focus_for_region(app_id)
2✔
897
        {
×
898
            self.app_focus.set_current(app_focus);
×
899
        }
4✔
900
    }
4✔
901

902
    fn set_wm_focus_order(&mut self, order: Vec<WindowId<R>>) {
7✔
903
        self.wm_focus.set_order(order);
7✔
904
        if !self.wm_focus.order.is_empty() && !self.wm_focus.order.contains(&self.wm_focus.current)
7✔
905
        {
5✔
906
            self.wm_focus.current = self.wm_focus.order[0];
5✔
907
        }
5✔
908
    }
7✔
909

910
    fn rebuild_wm_focus_ring(&mut self, active_ids: &[WindowId<R>]) {
7✔
911
        if active_ids.is_empty() {
7✔
912
            self.set_wm_focus_order(Vec::new());
1✔
913
            return;
1✔
914
        }
6✔
915
        let active: BTreeSet<_> = active_ids.iter().copied().collect();
6✔
916
        let mut next_order: Vec<WindowId<R>> = Vec::with_capacity(active.len());
6✔
917
        let mut seen: BTreeSet<WindowId<R>> = BTreeSet::new();
6✔
918

919
        for &id in &self.wm_focus.order {
6✔
920
            if active.contains(&id) && seen.insert(id) {
×
921
                next_order.push(id);
×
922
            }
×
923
        }
924
        for &id in active_ids {
12✔
925
            if seen.insert(id) {
6✔
926
                next_order.push(id);
6✔
927
            }
6✔
928
        }
929
        self.set_wm_focus_order(next_order);
6✔
930
    }
7✔
931

932
    fn advance_wm_focus(&mut self, forward: bool) {
×
933
        if self.wm_focus.order.is_empty() {
×
934
            return;
×
935
        }
×
936
        self.wm_focus.advance(forward);
×
937
        let focused = self.wm_focus.current();
×
938
        self.focus_window_id(focused);
×
939
    }
×
940

941
    fn select_fallback_focus(&mut self) {
×
942
        if let Some(fallback) = self.wm_focus.order.first().copied() {
×
943
            self.set_wm_focus(fallback);
×
944
        }
×
945
    }
×
946

947
    pub fn scroll(&self, id: W) -> ScrollState {
×
948
        self.scroll.get(&id).copied().unwrap_or_default()
×
949
    }
×
950

951
    pub fn scroll_mut(&mut self, id: W) -> &mut ScrollState {
×
952
        self.scroll.entry(id).or_default()
×
953
    }
×
954

955
    pub fn scroll_offset(&self, id: W) -> usize {
×
956
        self.scroll(id).offset
×
957
    }
×
958

959
    pub fn reset_scroll(&mut self, id: W) {
×
960
        self.scroll_mut(id).reset();
×
961
    }
×
962

963
    pub fn apply_scroll(&mut self, id: W, total: usize, view: usize) {
×
964
        self.scroll_mut(id).apply(total, view);
×
965
    }
×
966

967
    pub fn set_region(&mut self, id: R, rect: Rect) {
1✔
968
        self.regions.set(WindowId::app(id), rect);
1✔
969
    }
1✔
970

971
    pub fn full_region(&self, id: R) -> Rect {
×
972
        self.full_region_for_id(WindowId::app(id))
×
973
    }
×
974

975
    pub fn region(&self, id: R) -> Rect {
×
976
        self.region_for_id(WindowId::app(id))
×
977
    }
×
978

979
    fn window_content_offset(&self, id: WindowId<R>) -> (u16, u16) {
4✔
980
        let full = self.full_region_for_id(id);
4✔
981
        let content = self.region_for_id(id);
4✔
982
        (
4✔
983
            content.x.saturating_sub(full.x),
4✔
984
            content.y.saturating_sub(full.y),
4✔
985
        )
4✔
986
    }
4✔
987

988
    fn adjust_event_for_window(&self, id: WindowId<R>, event: &Event) -> Event {
2✔
989
        if let Event::Mouse(mut mouse) = event.clone() {
2✔
990
            let (offset_x, offset_y) = self.window_content_offset(id);
2✔
991
            mouse.column = mouse.column.saturating_add(offset_x);
2✔
992
            mouse.row = mouse.row.saturating_add(offset_y);
2✔
993
            Event::Mouse(mouse)
2✔
994
        } else {
NEW
995
            event.clone()
×
996
        }
997
    }
2✔
998

999
    /// Translate a mouse event into the content coordinate space for the given app window.
1000
    /// Returns a new `Event` when translation occurs; otherwise returns `None`.
1001
    pub fn localize_event_to_app(&self, id: R, event: &Event) -> Option<Event> {
2✔
1002
        self.localize_event_content(WindowId::app(id), event)
2✔
1003
    }
2✔
1004

1005
    /// Translate mouse coordinates into the window-local coordinate space, including chrome.
1006
    pub fn localize_event(&self, id: WindowId<R>, event: &Event) -> Option<Event> {
2✔
1007
        match event {
2✔
1008
            Event::Mouse(mouse) => {
2✔
1009
                let dest = self.window_dest(id, self.full_region_for_id(id));
2✔
1010
                let column =
2✔
1011
                    (i32::from(mouse.column) - dest.x).clamp(0, i32::from(u16::MAX)) as u16;
2✔
1012
                let row = (i32::from(mouse.row) - dest.y).clamp(0, i32::from(u16::MAX)) as u16;
2✔
1013
                Some(Event::Mouse(MouseEvent {
2✔
1014
                    column,
2✔
1015
                    row,
2✔
1016
                    kind: mouse.kind,
2✔
1017
                    modifiers: mouse.modifiers,
2✔
1018
                }))
2✔
1019
            }
NEW
1020
            _ => None,
×
1021
        }
1022
    }
2✔
1023

1024
    /// Translate mouse coordinates into the content-area coordinate space for the provided window id.
1025
    fn localize_event_content(&self, id: WindowId<R>, event: &Event) -> Option<Event> {
2✔
1026
        match event {
2✔
1027
            Event::Mouse(mouse) => {
2✔
1028
                let dest = self.window_dest(id, self.full_region_for_id(id));
2✔
1029
                let (offset_x, offset_y) = self.window_content_offset(id);
2✔
1030
                let content_x = dest.x + i32::from(offset_x);
2✔
1031
                let content_y = dest.y + i32::from(offset_y);
2✔
1032
                let column =
2✔
1033
                    (i32::from(mouse.column) - content_x).clamp(0, i32::from(u16::MAX)) as u16;
2✔
1034
                let row = (i32::from(mouse.row) - content_y).clamp(0, i32::from(u16::MAX)) as u16;
2✔
1035
                Some(Event::Mouse(MouseEvent {
2✔
1036
                    column,
2✔
1037
                    row,
2✔
1038
                    kind: mouse.kind,
2✔
1039
                    modifiers: mouse.modifiers,
2✔
1040
                }))
2✔
1041
            }
NEW
1042
            _ => None,
×
1043
        }
1044
    }
2✔
1045

1046
    fn full_region_for_id(&self, id: WindowId<R>) -> Rect {
22✔
1047
        self.regions.get(id).unwrap_or_default()
22✔
1048
    }
22✔
1049

1050
    fn region_for_id(&self, id: WindowId<R>) -> Rect {
6✔
1051
        let rect = self.regions.get(id).unwrap_or_default();
6✔
1052
        if self.layout_contract == LayoutContract::WindowManaged {
6✔
1053
            let area = if self.floating_resize_offscreen {
6✔
1054
                // If we allow off-screen resizing/dragging, we shouldn't clamp the
1055
                // logical region to the bounds, otherwise the PTY will be resized
1056
                // (shrinking the content) instead of just being clipped during render.
1057
                rect
6✔
1058
            } else {
1059
                clamp_rect(rect, self.managed_area)
×
1060
            };
1061
            if area.width < 3 || area.height < 4 {
6✔
1062
                return Rect::default();
×
1063
            }
6✔
1064
            Rect {
6✔
1065
                x: area.x + 1,
6✔
1066
                y: area.y + 2,
6✔
1067
                width: area.width.saturating_sub(2),
6✔
1068
                height: area.height.saturating_sub(3),
6✔
1069
            }
6✔
1070
        } else {
1071
            rect
×
1072
        }
1073
    }
6✔
1074

1075
    pub fn set_regions_from_layout(&mut self, layout: &LayoutNode<R>, area: Rect) {
×
1076
        self.regions = RegionMap::default();
×
1077
        for (id, rect) in layout.layout(area) {
×
1078
            self.regions.set(WindowId::app(id), rect);
×
1079
        }
×
1080
    }
×
1081

1082
    pub fn register_tiling_layout(&mut self, layout: &TilingLayout<R>, area: Rect) {
×
1083
        let (regions, handles) = layout.root().layout_with_handles(area);
×
1084
        for (id, rect) in regions {
×
1085
            self.regions.set(WindowId::app(id), rect);
×
1086
        }
×
1087
        self.handles.extend(handles);
×
1088
    }
×
1089

1090
    pub fn set_managed_layout(&mut self, layout: TilingLayout<R>) {
×
1091
        self.managed_layout = Some(TilingLayout::new(map_layout_node(layout.root())));
×
1092
        self.clear_all_floating();
×
1093
        if self.system_window_visible(SystemWindowId::DebugLog) {
×
1094
            self.ensure_system_window_in_layout(WindowId::system(SystemWindowId::DebugLog));
×
1095
        }
×
1096
    }
×
1097

1098
    pub fn set_managed_layout_none(&mut self) {
×
1099
        if self.managed_layout.is_none() {
×
1100
            return;
×
1101
        }
×
1102
        self.managed_layout = None;
×
1103
        if self.system_window_visible(SystemWindowId::DebugLog) {
×
1104
            self.ensure_system_window_in_layout(WindowId::system(SystemWindowId::DebugLog));
×
1105
        }
×
1106
    }
×
1107

1108
    pub fn set_panel_visible(&mut self, visible: bool) {
1✔
1109
        self.panel.set_visible(visible);
1✔
1110
    }
1✔
1111

1112
    pub fn set_panel_height(&mut self, height: u16) {
×
1113
        self.panel.set_height(height);
×
1114
    }
×
1115

NEW
1116
    pub fn decorator(&self) -> Arc<dyn WindowDecorator> {
×
NEW
1117
        Arc::clone(&self.decorator)
×
NEW
1118
    }
×
1119

1120
    pub fn register_managed_layout(&mut self, area: Rect) {
7✔
1121
        self.last_frame_area = area;
7✔
1122
        let (_, _, managed_area) = self.panel.split_area(self.panel_active(), area);
7✔
1123
        // Preserve the previous managed area so we can update any windows that
1124
        // were maximized to it; this ensures a maximized window remains
1125
        // maximized when the terminal (and thus managed area) resizes.
1126
        let prev_managed = self.managed_area;
7✔
1127
        self.managed_area = managed_area;
7✔
1128
        // If any window's floating rect exactly matched the previous managed
1129
        // area (i.e. it was maximized), update it to the new managed area so
1130
        // maximize persists across resizes.
1131
        if prev_managed.width > 0 && prev_managed.height > 0 {
7✔
1132
            let prev_full = crate::window::FloatRectSpec::Absolute(crate::window::FloatRect {
1✔
1133
                x: prev_managed.x as i32,
1✔
1134
                y: prev_managed.y as i32,
1✔
1135
                width: prev_managed.width,
1✔
1136
                height: prev_managed.height,
1✔
1137
            });
1✔
1138
            let new_full = crate::window::FloatRectSpec::Absolute(crate::window::FloatRect {
1✔
1139
                x: self.managed_area.x as i32,
1✔
1140
                y: self.managed_area.y as i32,
1✔
1141
                width: self.managed_area.width,
1✔
1142
                height: self.managed_area.height,
1✔
1143
            });
1✔
1144
            for (_id, window) in self.windows.iter_mut() {
1✔
1145
                if window.floating_rect == Some(prev_full) {
1✔
1146
                    window.floating_rect = Some(new_full);
1✔
1147
                }
1✔
1148
            }
1149
        }
6✔
1150
        self.clamp_floating_to_bounds();
7✔
1151
        if self.system_window_visible(SystemWindowId::DebugLog) {
7✔
1152
            self.ensure_system_window_in_layout(WindowId::system(SystemWindowId::DebugLog));
1✔
1153
        }
6✔
1154
        let z_snapshot = self.z_order.clone();
7✔
1155
        let mut active_ids: Vec<WindowId<R>> = Vec::new();
7✔
1156

1157
        if let Some(layout) = self.managed_layout.as_ref() {
7✔
1158
            let (regions, handles) = layout.root().layout_with_handles(self.managed_area);
1✔
1159
            for (id, rect) in &regions {
2✔
1160
                if self.is_window_floating(*id) {
1✔
1161
                    continue;
×
1162
                }
1✔
1163
                // skip minimized windows
1164
                if self.is_minimized(*id) {
1✔
1165
                    continue;
×
1166
                }
1✔
1167
                self.regions.set(*id, *rect);
1✔
1168
                if let Some(header) = floating_header_for_region(*id, *rect, self.managed_area) {
1✔
1169
                    self.floating_headers.push(header);
1✔
1170
                }
1✔
1171
                active_ids.push(*id);
1✔
1172
            }
1173
            let filtered_handles: Vec<SplitHandle> = handles
1✔
1174
                .into_iter()
1✔
1175
                .filter(|handle| {
1✔
1176
                    let Some(LayoutNode::Split { children, .. }) =
×
1177
                        layout.root().node_at_path(&handle.path)
×
1178
                    else {
1179
                        return false;
×
1180
                    };
1181
                    let left = children.get(handle.index);
×
1182
                    let right = children.get(handle.index + 1);
×
1183
                    left.is_some_and(|node| node.subtree_any(|id| !self.is_window_floating(id)))
×
1184
                        || right
×
1185
                            .is_some_and(|node| node.subtree_any(|id| !self.is_window_floating(id)))
×
1186
                })
×
1187
                .collect();
1✔
1188
            self.handles.extend(filtered_handles);
1✔
1189
        }
6✔
1190
        let mut floating_ids: Vec<WindowId<R>> = self
7✔
1191
            .windows
7✔
1192
            .iter()
7✔
1193
            .filter_map(|(&id, window)| {
7✔
1194
                if window.is_floating() && !window.minimized {
6✔
1195
                    Some(id)
5✔
1196
                } else {
1197
                    None
1✔
1198
                }
1199
            })
6✔
1200
            .collect();
7✔
1201
        floating_ids.sort_by_key(|id| {
7✔
1202
            z_snapshot
×
1203
                .iter()
×
1204
                .position(|existing| existing == id)
×
1205
                .unwrap_or(usize::MAX)
×
1206
        });
×
1207
        for floating_id in floating_ids {
12✔
1208
            let Some(spec) = self.floating_rect(floating_id) else {
5✔
1209
                continue;
×
1210
            };
1211
            let rect = spec.resolve(self.managed_area);
5✔
1212
            self.regions.set(floating_id, rect);
5✔
1213
            let visible = self.visible_rect_from_spec(spec);
5✔
1214
            if visible.width > 0 && visible.height > 0 {
5✔
1215
                self.resize_handles.extend(resize_handles_for_region(
5✔
1216
                    floating_id,
5✔
1217
                    visible,
5✔
1218
                    self.managed_area,
5✔
1219
                ));
1220
                if let Some(header) =
5✔
1221
                    floating_header_for_region(floating_id, visible, self.managed_area)
5✔
1222
                {
5✔
1223
                    self.floating_headers.push(header);
5✔
1224
                }
5✔
1225
            }
×
1226
            active_ids.push(floating_id);
5✔
1227
        }
1228

1229
        self.z_order.retain(|id| active_ids.contains(id));
7✔
1230
        for &id in &active_ids {
13✔
1231
            if !self.z_order.contains(&id) {
6✔
1232
                self.z_order.push(id);
6✔
1233
            }
6✔
1234
        }
1235
        self.managed_draw_order = self.z_order.clone();
7✔
1236
        self.managed_draw_order_app = self
7✔
1237
            .managed_draw_order
7✔
1238
            .iter()
7✔
1239
            .filter_map(|id| id.as_app())
7✔
1240
            .collect();
7✔
1241
        self.rebuild_wm_focus_ring(&active_ids);
7✔
1242
        // Ensure the current focus is actually on top and synced after layout registration.
1243
        // Only bring the focused window to front if it's not already the topmost window
1244
        // to avoid repeatedly forcing focus every frame.
1245
        let focused = self.wm_focus.current();
7✔
1246
        if self.z_order.last().copied() != Some(focused) {
7✔
1247
            self.focus_window_id(focused);
1✔
1248
        }
6✔
1249
    }
7✔
1250

1251
    pub fn managed_draw_order(&self) -> &[R] {
×
1252
        &self.managed_draw_order_app
×
1253
    }
×
1254

1255
    /// Build a stable display order for UI components.
1256
    /// By default this returns the canonical creation order filtered to active managed windows,
1257
    /// appending any windows that are active but not yet present in the canonical ordering.
1258
    pub fn build_display_order(&self) -> Vec<WindowId<R>> {
×
1259
        let mut ordered: Vec<(WindowId<R>, &Window)> = self
×
1260
            .windows
×
1261
            .iter()
×
1262
            .map(|(id, window)| (*id, window))
×
1263
            .collect();
×
1264
        ordered.sort_by_key(|(_, window)| window.creation_order);
×
1265

1266
        let mut out: Vec<WindowId<R>> = Vec::new();
×
1267
        for (id, window) in ordered {
×
1268
            if self.managed_draw_order.contains(&id) || window.minimized {
×
1269
                out.push(id);
×
1270
            }
×
1271
        }
1272
        for id in &self.managed_draw_order {
×
1273
            if !out.contains(id) {
×
1274
                out.push(*id);
×
1275
            }
×
1276
        }
1277
        out
×
1278
    }
×
1279

1280
    /// Set a user-visible title for an app window. This overrides the default
1281
    /// Debug-derived title displayed for the given `id`.
1282
    pub fn set_window_title(&mut self, id: R, title: impl Into<String>) {
×
1283
        self.window_mut(WindowId::app(id)).title = Some(title.into());
×
1284
    }
×
1285

1286
    pub fn handle_managed_event(&mut self, event: &Event) -> bool {
4✔
1287
        if self.layout_contract != LayoutContract::WindowManaged {
4✔
1288
            return false;
×
1289
        }
4✔
1290
        if let Event::Mouse(mouse) = event
4✔
1291
            && self.panel_active()
4✔
1292
            && rect_contains(self.panel.area(), mouse.column, mouse.row)
1✔
1293
        {
1294
            if self.panel.hit_test_menu(event) {
×
1295
                if self.wm_overlay_visible() {
×
1296
                    self.close_wm_overlay();
×
1297
                } else {
×
1298
                    self.open_wm_overlay();
×
1299
                }
×
1300
            } else if self.panel.hit_test_mouse_capture(event) {
×
1301
                self.toggle_mouse_capture();
×
NEW
1302
            } else if self.panel.hit_test_clipboard(event) {
×
NEW
1303
                self.toggle_clipboard_enabled();
×
UNCOV
1304
            } else if let Some(id) = self.panel.hit_test_window(event) {
×
1305
                // If the clicked window is minimized, restore it first so it appears
1306
                // in the layout; otherwise just focus and bring to front.
1307
                if self.is_minimized(id) {
×
1308
                    self.restore_minimized(id);
×
1309
                }
×
1310
                self.focus_window_id(id);
×
1311
            }
×
1312
            return true;
×
1313
        }
4✔
1314
        if let Event::Mouse(mouse) = event {
4✔
1315
            self.hover = Some((mouse.column, mouse.row));
4✔
1316
            if matches!(mouse.kind, MouseEventKind::Down(_)) {
4✔
1317
                self.focus_window_at(mouse.column, mouse.row);
2✔
1318
            }
2✔
1319
        }
×
1320
        if self.handle_resize_event(event) {
4✔
1321
            return true;
×
1322
        }
4✔
1323
        if self.handle_header_drag_event(event) {
4✔
1324
            return true;
3✔
1325
        }
1✔
1326
        if self.handle_system_window_event(event) {
1✔
NEW
1327
            return true;
×
1328
        }
1✔
1329
        if let Some(layout) = self.managed_layout.as_mut() {
1✔
1330
            return layout.handle_event(event, self.managed_area);
×
1331
        }
1✔
1332
        false
1✔
1333
    }
4✔
1334

1335
    pub fn minimize_window(&mut self, id: WindowId<R>) {
×
1336
        if self.is_minimized(id) {
×
1337
            return;
×
1338
        }
×
1339
        // remove from floating and regions; keep canonical order so it can be restored
1340
        self.clear_floating_rect(id);
×
1341
        self.z_order.retain(|x| *x != id);
×
1342
        self.managed_draw_order.retain(|x| *x != id);
×
1343
        self.set_minimized(id, true);
×
1344
        // ensure focus moves if needed
1345
        if self.wm_focus.current() == id {
×
1346
            self.select_fallback_focus();
×
1347
        }
×
1348
    }
×
1349

1350
    pub fn restore_minimized(&mut self, id: WindowId<R>) {
×
1351
        if !self.is_minimized(id) {
×
1352
            return;
×
1353
        }
×
1354
        self.set_minimized(id, false);
×
1355
        // reinstall into z_order and draw order
1356
        if !self.z_order.contains(&id) {
×
1357
            self.z_order.push(id);
×
1358
        }
×
1359
        if !self.managed_draw_order.contains(&id) {
×
1360
            self.managed_draw_order.push(id);
×
1361
        }
×
1362
    }
×
1363

1364
    pub fn toggle_maximize(&mut self, id: WindowId<R>) {
1✔
1365
        // maximize toggles the floating rect to full managed_area
1366
        let full = crate::window::FloatRectSpec::Absolute(crate::window::FloatRect {
1✔
1367
            x: self.managed_area.x as i32,
1✔
1368
            y: self.managed_area.y as i32,
1✔
1369
            width: self.managed_area.width,
1✔
1370
            height: self.managed_area.height,
1✔
1371
        });
1✔
1372
        if let Some(current) = self.floating_rect(id) {
1✔
1373
            if current == full {
×
1374
                if let Some(prev) = self.take_prev_floating_rect(id) {
×
1375
                    self.set_floating_rect(id, Some(prev));
×
1376
                }
×
1377
            } else {
×
1378
                self.set_prev_floating_rect(id, Some(current));
×
1379
                self.set_floating_rect(id, Some(full));
×
1380
            }
×
1381
            self.bring_floating_to_front_id(id);
×
1382
            return;
×
1383
        }
1✔
1384
        // not floating: add floating pane covering full area
1385
        // Save the current region (if available) so we can restore later.
1386
        let prev_rect = if let Some(rect) = self.regions.get(id) {
1✔
NEW
1387
            crate::window::FloatRectSpec::Absolute(crate::window::FloatRect {
×
NEW
1388
                x: rect.x as i32,
×
NEW
1389
                y: rect.y as i32,
×
NEW
1390
                width: rect.width,
×
NEW
1391
                height: rect.height,
×
NEW
1392
            })
×
1393
        } else {
1394
            crate::window::FloatRectSpec::Percent {
1✔
1395
                x: 0,
1✔
1396
                y: 0,
1✔
1397
                width: 100,
1✔
1398
                height: 100,
1✔
1399
            }
1✔
1400
        };
1401
        self.set_prev_floating_rect(id, Some(prev_rect));
1✔
1402
        self.set_floating_rect(id, Some(full));
1✔
1403
        self.bring_floating_to_front_id(id);
1✔
1404
    }
1✔
1405

1406
    pub fn close_window(&mut self, id: WindowId<R>) {
×
1407
        tracing::debug!(window_id = ?id, "closing window");
×
1408
        if let WindowId::System(system_id) = id {
×
1409
            self.hide_system_window(system_id);
×
1410
            return;
×
1411
        }
×
1412

1413
        // Remove references to this window
1414
        self.clear_floating_rect(id);
×
1415
        self.z_order.retain(|x| *x != id);
×
1416
        self.managed_draw_order.retain(|x| *x != id);
×
1417
        self.set_minimized(id, false);
×
1418
        self.regions.remove(id);
×
1419
        // update focus
1420
        if self.wm_focus.current() == id {
×
1421
            self.select_fallback_focus();
×
1422
        }
×
1423
        // If this window corresponded to an app id, enqueue it for the runner to drain.
1424
        if let Some(app_id) = id.as_app() {
×
1425
            self.closed_app_windows.push(app_id);
×
1426
        }
×
1427
    }
×
1428

1429
    fn handle_header_drag_event(&mut self, event: &Event) -> bool {
4✔
1430
        use crossterm::event::MouseEventKind;
1431
        let Event::Mouse(mouse) = event else {
4✔
1432
            return false;
×
1433
        };
1434
        match mouse.kind {
4✔
1435
            MouseEventKind::Down(_) => {
1436
                // Check if the mouse is blocked by a window above
1437
                let topmost_hit = if self.layout_contract == LayoutContract::WindowManaged
2✔
1438
                    && !self.managed_draw_order.is_empty()
2✔
1439
                {
1440
                    self.hit_test_region_topmost(mouse.column, mouse.row, &self.managed_draw_order)
2✔
1441
                } else {
1442
                    None
×
1443
                };
1444

1445
                if let Some(header) = self
2✔
1446
                    .floating_headers
2✔
1447
                    .iter()
2✔
1448
                    .rev()
2✔
1449
                    .find(|handle| rect_contains(handle.rect, mouse.column, mouse.row))
2✔
1450
                    .copied()
2✔
1451
                {
1452
                    // If we hit a window body that is NOT the owner of this header,
1453
                    // then the header is obscured.
1454
                    if let Some(hit_id) = topmost_hit
1✔
1455
                        && hit_id != header.id
1✔
1456
                    {
1457
                        return false;
×
1458
                    }
1✔
1459

1460
                    let rect = self.full_region_for_id(header.id);
1✔
1461
                    match self.decorator.hit_test(rect, mouse.column, mouse.row) {
1✔
1462
                        HeaderAction::Minimize => {
1463
                            self.minimize_window(header.id);
×
1464
                            self.last_header_click = None;
×
1465
                            return true;
×
1466
                        }
1467
                        HeaderAction::Maximize => {
1468
                            self.toggle_maximize(header.id);
×
1469
                            self.last_header_click = None;
×
1470
                            return true;
×
1471
                        }
1472
                        HeaderAction::Close => {
1473
                            self.close_window(header.id);
×
1474
                            self.last_header_click = None;
×
1475
                            return true;
×
1476
                        }
1477
                        HeaderAction::Drag => {
1478
                            // Double-click on header toggles maximize/restore.
1479
                            let now = Instant::now();
1✔
1480
                            if let Some((prev_id, prev)) = self.last_header_click
1✔
1481
                                && prev_id == header.id
×
1482
                                && now.duration_since(prev) <= Duration::from_millis(500)
×
1483
                            {
1484
                                self.toggle_maximize(header.id);
×
1485
                                self.last_header_click = None;
×
1486
                                return true;
×
1487
                            }
1✔
1488
                            // Record this click time and proceed to drag below.
1489
                            self.last_header_click = Some((header.id, now));
1✔
1490
                        }
1491
                        HeaderAction::None => {
×
1492
                            // Should not happen as we already checked rect contains
×
1493
                        }
×
1494
                    }
1495

1496
                    // Standard floating drag start
1497
                    if self.is_window_floating(header.id) {
1✔
1498
                        self.bring_floating_to_front_id(header.id);
×
1499
                    } else {
1✔
1500
                        // If Tiled: We detach immediately to floating (responsive drag).
1✔
1501
                        // Keep the tiling slot reserved so the sibling doesn't expand to full screen.
1✔
1502
                        let _ = self.detach_to_floating(header.id, rect);
1✔
1503
                    }
1✔
1504

1505
                    let (initial_x, initial_y) =
1✔
1506
                        if let Some(crate::window::FloatRectSpec::Absolute(fr)) =
1✔
1507
                            self.floating_rect(header.id)
1✔
1508
                        {
1509
                            (fr.x, fr.y)
1✔
1510
                        } else {
NEW
1511
                            (rect.x as i32, rect.y as i32)
×
1512
                        };
1513
                    self.drag_header = Some(HeaderDrag {
1✔
1514
                        id: header.id,
1✔
1515
                        initial_x,
1✔
1516
                        initial_y,
1✔
1517
                        start_x: mouse.column,
1✔
1518
                        start_y: mouse.row,
1✔
1519
                    });
1✔
1520
                    return true;
1✔
1521
                }
1✔
1522
            }
1523
            MouseEventKind::Drag(_) => {
1524
                if let Some(drag) = self.drag_header {
1✔
1525
                    if self.is_window_floating(drag.id) {
1✔
1526
                        self.move_floating(
1✔
1527
                            drag.id,
1✔
1528
                            mouse.column,
1✔
1529
                            mouse.row,
1✔
1530
                            drag.start_x,
1✔
1531
                            drag.start_y,
1✔
1532
                            drag.initial_x,
1✔
1533
                            drag.initial_y,
1✔
1534
                        );
1535
                        // Only show snap preview if dragged a bit
1536
                        let dx = mouse.column.abs_diff(drag.start_x);
1✔
1537
                        let dy = mouse.row.abs_diff(drag.start_y);
1✔
1538
                        if dx + dy > 2 {
1✔
1539
                            self.update_snap_preview(drag.id, mouse.column, mouse.row);
1✔
1540
                        } else {
1✔
1541
                            self.drag_snap = None;
×
1542
                        }
×
1543
                    }
×
1544
                    return true;
1✔
1545
                }
×
1546
            }
1547
            MouseEventKind::Up(_) => {
1548
                if let Some(drag) = self.drag_header.take() {
1✔
1549
                    if self.drag_snap.is_some() {
1✔
1550
                        self.apply_snap(drag.id);
×
1551
                    }
1✔
1552
                    return true;
1✔
1553
                }
×
1554
            }
1555
            _ => {}
×
1556
        }
1557
        false
1✔
1558
    }
4✔
1559

1560
    fn focus_window_at(&mut self, column: u16, row: u16) -> bool {
2✔
1561
        if self.layout_contract != LayoutContract::WindowManaged
2✔
1562
            || self.managed_draw_order.is_empty()
2✔
1563
        {
1564
            return false;
×
1565
        }
2✔
1566
        let Some(hit) = self.hit_test_region_topmost(column, row, &self.managed_draw_order) else {
2✔
1567
            return false;
×
1568
        };
1569
        if !matches!(hit, WindowId::App(_)) {
2✔
1570
            return false;
1✔
1571
        }
1✔
1572
        self.focus_window_id(hit);
1✔
1573
        true
1✔
1574
    }
2✔
1575

1576
    fn handle_resize_event(&mut self, event: &Event) -> bool {
4✔
1577
        use crossterm::event::MouseEventKind;
1578
        let Event::Mouse(mouse) = event else {
4✔
1579
            return false;
×
1580
        };
1581
        match mouse.kind {
4✔
1582
            MouseEventKind::Down(_) => {
1583
                // Check if the mouse is blocked by a window above
1584
                let topmost_hit = if self.layout_contract == LayoutContract::WindowManaged
2✔
1585
                    && !self.managed_draw_order.is_empty()
2✔
1586
                {
1587
                    self.hit_test_region_topmost(mouse.column, mouse.row, &self.managed_draw_order)
2✔
1588
                } else {
1589
                    None
×
1590
                };
1591

1592
                let hit = self
2✔
1593
                    .resize_handles
2✔
1594
                    .iter()
2✔
1595
                    .rev()
2✔
1596
                    .find(|handle| rect_contains(handle.rect, mouse.column, mouse.row))
2✔
1597
                    .copied();
2✔
1598
                if let Some(handle) = hit {
2✔
1599
                    // If we hit a window body that is NOT the owner of this handle,
1600
                    // then the handle is obscured.
1601
                    if let Some(hit_id) = topmost_hit
×
1602
                        && hit_id != handle.id
×
1603
                    {
1604
                        return false;
×
1605
                    }
×
1606

1607
                    let rect = self.full_region_for_id(handle.id);
×
1608
                    if !self.is_window_floating(handle.id) {
×
1609
                        return false;
×
1610
                    }
×
1611
                    self.bring_floating_to_front_id(handle.id);
×
NEW
1612
                    let (start_x, start_y, start_width, start_height) =
×
NEW
1613
                        if let Some(crate::window::FloatRectSpec::Absolute(fr)) =
×
NEW
1614
                            self.floating_rect(handle.id)
×
1615
                        {
NEW
1616
                            (fr.x, fr.y, fr.width, fr.height)
×
1617
                        } else {
NEW
1618
                            (rect.x as i32, rect.y as i32, rect.width, rect.height)
×
1619
                        };
1620
                    self.drag_resize = Some(ResizeDrag {
×
1621
                        id: handle.id,
×
1622
                        edge: handle.edge,
×
1623
                        start_rect: rect,
×
1624
                        start_col: mouse.column,
×
1625
                        start_row: mouse.row,
×
NEW
1626
                        start_x,
×
NEW
1627
                        start_y,
×
NEW
1628
                        start_width,
×
NEW
1629
                        start_height,
×
1630
                    });
×
1631
                    return true;
×
1632
                }
2✔
1633
            }
1634
            MouseEventKind::Drag(_) => {
1635
                if let Some(drag) = self.drag_resize.as_ref()
1✔
1636
                    && self.is_window_floating(drag.id)
×
1637
                {
NEW
1638
                    let resized = apply_resize_drag_signed(
×
NEW
1639
                        drag.start_x,
×
NEW
1640
                        drag.start_y,
×
NEW
1641
                        drag.start_width,
×
NEW
1642
                        drag.start_height,
×
1643
                        drag.edge,
×
1644
                        mouse.column,
×
1645
                        mouse.row,
×
1646
                        drag.start_col,
×
1647
                        drag.start_row,
×
1648
                        self.managed_area,
×
1649
                        self.floating_resize_offscreen,
×
1650
                    );
NEW
1651
                    self.set_floating_rect(
×
NEW
1652
                        drag.id,
×
NEW
1653
                        Some(crate::window::FloatRectSpec::Absolute(resized)),
×
1654
                    );
1655
                    return true;
×
1656
                }
1✔
1657
            }
1658
            MouseEventKind::Up(_) => {
1659
                if self.drag_resize.take().is_some() {
1✔
1660
                    return true;
×
1661
                }
1✔
1662
            }
1663
            _ => {}
×
1664
        }
1665
        false
4✔
1666
    }
4✔
1667

1668
    fn detach_to_floating(&mut self, id: WindowId<R>, rect: Rect) -> bool {
1✔
1669
        if self.is_window_floating(id) {
1✔
1670
            return true;
×
1671
        }
1✔
1672
        if self.managed_layout.is_none() {
1✔
1673
            return false;
×
1674
        }
1✔
1675

1676
        let width = rect.width.max(1);
1✔
1677
        let height = rect.height.max(1);
1✔
1678
        let x = rect.x;
1✔
1679
        let y = rect.y;
1✔
1680
        self.set_floating_rect(
1✔
1681
            id,
1✔
1682
            Some(crate::window::FloatRectSpec::Absolute(
1✔
1683
                crate::window::FloatRect {
1✔
1684
                    x: x as i32,
1✔
1685
                    y: y as i32,
1✔
1686
                    width,
1✔
1687
                    height,
1✔
1688
                },
1✔
1689
            )),
1✔
1690
        );
1691
        self.bring_to_front_id(id);
1✔
1692
        true
1✔
1693
    }
1✔
1694

1695
    fn layout_contains(&self, id: WindowId<R>) -> bool {
3✔
1696
        self.managed_layout
3✔
1697
            .as_ref()
3✔
1698
            .is_some_and(|layout| layout.root().subtree_any(|node_id| node_id == id))
3✔
1699
    }
3✔
1700

1701
    // narrow allow: refactor into a small struct if/when argument list needs reduction
1702
    #[allow(clippy::too_many_arguments)]
1703
    fn move_floating(
1✔
1704
        &mut self,
1✔
1705
        id: WindowId<R>,
1✔
1706
        column: u16,
1✔
1707
        row: u16,
1✔
1708
        start_mouse_x: u16,
1✔
1709
        start_mouse_y: u16,
1✔
1710
        initial_x: i32,
1✔
1711
        initial_y: i32,
1✔
1712
    ) {
1✔
1713
        let panel_active = self.panel_active();
1✔
1714
        let bounds = self.managed_area;
1✔
1715
        let Some(crate::window::FloatRectSpec::Absolute(fr)) = self.floating_rect(id) else {
1✔
1716
            return;
×
1717
        };
1718
        let width = fr.width.max(1);
1✔
1719
        let height = fr.height.max(1);
1✔
1720
        let dx = column as i32 - start_mouse_x as i32;
1✔
1721
        let dy = row as i32 - start_mouse_y as i32;
1✔
1722
        let x = initial_x + dx;
1✔
1723
        let mut y = initial_y + dy;
1✔
1724
        let bounds_y = bounds.y as i32;
1✔
1725
        if panel_active && y < bounds_y {
1✔
NEW
1726
            y = bounds_y;
×
1727
        }
1✔
1728
        self.set_floating_rect(
1✔
1729
            id,
1✔
1730
            Some(crate::window::FloatRectSpec::Absolute(
1✔
1731
                crate::window::FloatRect {
1✔
1732
                    x,
1✔
1733
                    y,
1✔
1734
                    width,
1✔
1735
                    height,
1✔
1736
                },
1✔
1737
            )),
1✔
1738
        );
1739
    }
1✔
1740

1741
    fn update_snap_preview(&mut self, dragging_id: WindowId<R>, mouse_x: u16, mouse_y: u16) {
1✔
1742
        self.drag_snap = None;
1✔
1743
        let area = self.managed_area;
1✔
1744

1745
        // 1. Check Window Snap first (more specific)
1746
        // We iterate z-order (top-to-bottom) to find the first valid target under mouse.
1747
        // We only allow snapping to windows that are already tiled, unless the layout is empty.
1748
        let target = self.z_order.iter().rev().find_map(|&id| {
1✔
1749
            if id == dragging_id {
1✔
1750
                return None;
1✔
1751
            }
×
1752
            // If we have a layout, ignore floating windows as snap targets
1753
            // to prevent "bait and switch" (offering to split a float, then splitting root).
1754
            if self.managed_layout.is_some() && self.is_window_floating(id) {
×
1755
                return None;
×
1756
            }
×
1757

1758
            let rect = self.regions.get(id)?;
×
1759
            if rect_contains(rect, mouse_x, mouse_y) {
×
1760
                Some((id, rect))
×
1761
            } else {
1762
                None
×
1763
            }
1764
        });
1✔
1765

1766
        if let Some((target_id, rect)) = target {
1✔
1767
            let h = rect.height;
×
1768

1769
            // Distance to edges
1770
            let d_top = mouse_y.saturating_sub(rect.y);
×
1771
            let d_bottom = (rect.y + h).saturating_sub(1).saturating_sub(mouse_y);
×
1772

1773
            // Sensitivity: Allow a reasonable localized zone.
1774
            // Reduced sensitivity to prevent accidental snaps when crossing windows.
1775
            // w/10 is 10%. Clamped to [2, 6] means you must be quite close to the edge.
1776
            let sens_y = (h / 10).clamp(1, 4);
×
1777

1778
            // Check if the closest edge is within its sensitivity limit
1779
            // Only allow snapping on horizontal seams (top/bottom of tiled panes).
1780
            let snap = if d_top < sens_y && d_top <= d_bottom {
×
1781
                Some((
×
1782
                    InsertPosition::Top,
×
1783
                    Rect {
×
1784
                        height: h / 2,
×
1785
                        ..rect
×
1786
                    },
×
1787
                ))
×
1788
            } else if d_bottom < sens_y {
×
1789
                Some((
×
1790
                    InsertPosition::Bottom,
×
1791
                    Rect {
×
1792
                        y: rect.y + h / 2,
×
1793
                        height: h / 2,
×
1794
                        ..rect
×
1795
                    },
×
1796
                ))
×
1797
            } else {
1798
                None
×
1799
            };
1800

1801
            if let Some((pos, preview)) = snap {
×
1802
                self.drag_snap = Some((Some(target_id), pos, preview));
×
1803
                return;
×
1804
            }
×
1805
        }
1✔
1806

1807
        // 2. Check Screen Edge Snap (fallback, less specific)
1808
        let sensitivity = 2; // Strict sensitivity for screen edge
1✔
1809

1810
        let d_left = mouse_x.saturating_sub(area.x);
1✔
1811
        let d_right = (area.x + area.width)
1✔
1812
            .saturating_sub(1)
1✔
1813
            .saturating_sub(mouse_x);
1✔
1814
        let d_top = mouse_y.saturating_sub(area.y);
1✔
1815
        let d_bottom = (area.y + area.height)
1✔
1816
            .saturating_sub(1)
1✔
1817
            .saturating_sub(mouse_y);
1✔
1818

1819
        let min_screen_dist = d_left.min(d_right).min(d_top).min(d_bottom);
1✔
1820

1821
        let position = if min_screen_dist < sensitivity {
1✔
1822
            if d_left == min_screen_dist {
×
1823
                Some(InsertPosition::Left)
×
1824
            } else if d_right == min_screen_dist {
×
1825
                Some(InsertPosition::Right)
×
1826
            } else if d_top == min_screen_dist {
×
1827
                Some(InsertPosition::Top)
×
1828
            } else if d_bottom == min_screen_dist {
×
1829
                Some(InsertPosition::Bottom)
×
1830
            } else {
1831
                None
×
1832
            }
1833
        } else {
1834
            None
1✔
1835
        };
1836

1837
        if let Some(pos) = position {
1✔
1838
            let mut preview = match pos {
×
1839
                InsertPosition::Left => Rect {
×
1840
                    width: area.width / 2,
×
1841
                    ..area
×
1842
                },
×
1843
                InsertPosition::Right => Rect {
×
1844
                    x: area.x + area.width / 2,
×
1845
                    width: area.width / 2,
×
1846
                    ..area
×
1847
                },
×
1848
                InsertPosition::Top => Rect {
×
1849
                    height: area.height / 2,
×
1850
                    ..area
×
1851
                },
×
1852
                InsertPosition::Bottom => Rect {
×
1853
                    y: area.y + area.height / 2,
×
1854
                    height: area.height / 2,
×
1855
                    ..area
×
1856
                },
×
1857
            };
1858

1859
            // If there's no layout to split, dragging to edge just re-tiles to full screen.
1860
            if self.managed_layout.is_none() {
×
1861
                preview = area;
×
1862
            }
×
1863

1864
            self.drag_snap = Some((None, pos, preview));
×
1865
        }
1✔
1866
    }
1✔
1867

1868
    fn apply_snap(&mut self, id: WindowId<R>) {
×
1869
        if let Some((target, position, preview)) = self.drag_snap.take() {
×
1870
            // Check if we should tile or float-snap
1871
            // We float-snap if we are snapping to a screen edge (target is None)
1872
            // AND the layout is empty (no other tiled windows).
1873
            let other_windows_exist = if let Some(layout) = &self.managed_layout {
×
1874
                !layout.regions(self.managed_area).is_empty()
×
1875
            } else {
1876
                false
×
1877
            };
1878

1879
            if target.is_none() && !other_windows_exist {
×
1880
                // Single window edge snap -> Floating Resize
1881
                if self.is_window_floating(id) {
×
NEW
1882
                    self.set_floating_rect(
×
NEW
1883
                        id,
×
NEW
1884
                        Some(crate::window::FloatRectSpec::Absolute(
×
NEW
1885
                            crate::window::FloatRect {
×
NEW
1886
                                x: preview.x as i32,
×
NEW
1887
                                y: preview.y as i32,
×
NEW
1888
                                width: preview.width,
×
NEW
1889
                                height: preview.height,
×
NEW
1890
                            },
×
NEW
1891
                        )),
×
NEW
1892
                    );
×
1893
                }
×
1894
                return;
×
1895
            }
×
1896

1897
            if self.is_window_floating(id) {
×
1898
                self.clear_floating_rect(id);
×
1899
            }
×
1900

1901
            if self.layout_contains(id)
×
1902
                && let Some(layout) = &mut self.managed_layout
×
1903
            {
1904
                let should_retile = match target {
×
1905
                    Some(target_id) => target_id != id,
×
1906
                    None => true,
×
1907
                };
1908
                if should_retile {
×
1909
                    layout.root_mut().remove_leaf(id);
×
1910
                } else {
×
1911
                    self.bring_to_front_id(id);
×
1912
                    return;
×
1913
                }
1914
            }
×
1915

1916
            // Handle case where target is floating (and thus not in layout yet)
1917
            if let Some(target_id) = target
×
1918
                && self.is_window_floating(target_id)
×
1919
            {
1920
                // Target is floating. We must initialize layout with it.
1921
                self.clear_floating_rect(target_id);
×
1922
                if self.managed_layout.is_none() {
×
1923
                    self.managed_layout = Some(TilingLayout::new(LayoutNode::leaf(target_id)));
×
1924
                } else {
×
1925
                    // This case is tricky: managed_layout exists (implied other windows), but target is floating.
×
1926
                    // We need to tile 'id' based on 'target'.
×
1927
                    // However, 'target' itself isn't in the tree.
×
1928
                    // This implies we want to perform a "Merge" of two floating windows into a new tiled group?
×
1929
                    // But we only support one root.
×
1930
                    // If managed_layout exists, we probably shouldn't be here if target is floating?
×
1931
                    // Actually, if we have {C} tiled, and {B} floating. Snap A to B.
×
1932
                    // We want {A, B} tiled?
×
1933
                    // Current logic: If managed_layout is Some, we try insert_leaf.
×
1934
                    // If insert_leaf fails, we fallback to split_root.
×
1935
                    // If we fall back to split_root, A is added to root. B remains floating.
×
1936
                    // This is acceptable/safe.
×
1937
                    // The critical case is when managed_layout is None (the 2-window case).
×
1938
                }
×
1939
            }
×
1940

1941
            if let Some(layout) = &mut self.managed_layout {
×
1942
                let success = if let Some(target_id) = target {
×
1943
                    layout.root_mut().insert_leaf(target_id, id, position)
×
1944
                } else {
1945
                    false
×
1946
                };
1947

1948
                if !success {
×
1949
                    // If insert failed (e.g. target was missing or we are splitting root),
×
1950
                    // If target was the one we just initialized (in the floating case above), it should be at root.
×
1951
                    // insert_leaf should have worked if target is root.
×
1952
                    layout.split_root(id, position);
×
1953
                }
×
1954

1955
                // Ensure the snapped window is brought to front/focused
1956
                if let Some(pos) = self.z_order.iter().position(|&z_id| z_id == id) {
×
1957
                    self.z_order.remove(pos);
×
1958
                }
×
1959
                self.z_order.push(id);
×
1960
                self.managed_draw_order = self.z_order.clone();
×
1961
            } else {
×
1962
                self.managed_layout = Some(TilingLayout::new(LayoutNode::leaf(id)));
×
1963
            }
×
1964

1965
            // If we snapped a window into place, any other floating windows should snap as well.
1966
            let mut pending_snap = Vec::new();
×
1967
            for r_id in self.regions.ids() {
×
1968
                if r_id != id && self.is_window_floating(r_id) {
×
1969
                    pending_snap.push(r_id);
×
1970
                }
×
1971
            }
1972
            for float_id in pending_snap {
×
1973
                self.tile_window_id(float_id);
×
1974
            }
×
1975
        }
×
1976
    }
×
1977

1978
    /// Smartly insert a window into the tiling layout.
1979
    /// If there is a focused tiled window, split it.
1980
    /// Otherwise, split the root.
1981
    pub fn tile_window(&mut self, id: R) -> bool {
1✔
1982
        self.tile_window_id(WindowId::app(id))
1✔
1983
    }
1✔
1984

1985
    fn tile_window_id(&mut self, id: WindowId<R>) -> bool {
1✔
1986
        // If already in layout or floating, do nothing (or move it?)
1987
        // For now, assume this is for new windows.
1988
        if self.layout_contains(id) {
1✔
1989
            if self.is_window_floating(id) {
×
1990
                self.clear_floating_rect(id);
×
1991
            }
×
1992
            self.focus_window_id(id);
×
1993
            return true;
×
1994
        }
1✔
1995
        if self.managed_layout.is_none() {
1✔
1996
            self.managed_layout = Some(TilingLayout::new(LayoutNode::leaf(id)));
1✔
1997
            self.focus_window_id(id);
1✔
1998
            return true;
1✔
1999
        }
×
2000

2001
        // Try to find a focused node that is in the layout
2002
        let current_focus = self.wm_focus.current();
×
2003

2004
        let mut target_r = None;
×
2005
        for r_id in self.regions.ids() {
×
2006
            if r_id == current_focus {
×
2007
                target_r = Some(r_id);
×
2008
                break;
×
2009
            }
×
2010
        }
2011

2012
        let Some(layout) = self.managed_layout.as_mut() else {
×
2013
            return false;
×
2014
        };
2015

2016
        // If we found a focused region, split it
2017
        if let Some(target) = target_r {
×
2018
            // Prefer splitting horizontally (side-by-side) for wide windows, vertically for tall?
2019
            // Or just default to Right/Bottom.
2020
            // Let's default to Right for now as it's common.
2021
            if layout
×
2022
                .root_mut()
×
2023
                .insert_leaf(target, id, InsertPosition::Right)
×
2024
            {
2025
                self.focus_window_id(id);
×
2026
                return true;
×
2027
            }
×
2028
        }
×
2029

2030
        // Fallback: split root
2031
        layout.split_root(id, InsertPosition::Right);
×
2032
        self.focus_window_id(id);
×
2033
        true
×
2034
    }
1✔
2035

2036
    pub fn bring_to_front(&mut self, id: R) {
×
2037
        self.bring_to_front_id(WindowId::app(id));
×
2038
    }
×
2039

2040
    fn bring_to_front_id(&mut self, id: WindowId<R>) {
6✔
2041
        if let Some(pos) = self.z_order.iter().position(|&x| x == id) {
6✔
2042
            let item = self.z_order.remove(pos);
2✔
2043
            self.z_order.push(item);
2✔
2044
        }
4✔
2045
    }
6✔
2046

2047
    fn window_dest(&self, id: WindowId<R>, fallback: Rect) -> crate::window::FloatRect {
4✔
2048
        if let Some(spec) = self.floating_rect(id) {
4✔
2049
            spec.resolve_signed(self.managed_area)
2✔
2050
        } else {
2051
            crate::window::FloatRect {
2✔
2052
                x: fallback.x as i32,
2✔
2053
                y: fallback.y as i32,
2✔
2054
                width: fallback.width,
2✔
2055
                height: fallback.height,
2✔
2056
            }
2✔
2057
        }
2058
    }
4✔
2059

2060
    fn visible_rect_from_spec(&self, spec: crate::window::FloatRectSpec) -> Rect {
6✔
2061
        float_rect_visible(spec.resolve_signed(self.managed_area), self.managed_area)
6✔
2062
    }
6✔
2063

2064
    fn visible_region_for_id(&self, id: WindowId<R>) -> Rect {
14✔
2065
        if let Some(spec) = self.floating_rect(id) {
14✔
2066
            self.visible_rect_from_spec(spec)
1✔
2067
        } else {
2068
            self.full_region_for_id(id)
13✔
2069
        }
2070
    }
14✔
2071

2072
    pub fn bring_all_floating_to_front(&mut self) {
×
2073
        let ids: Vec<WindowId<R>> = self
×
2074
            .z_order
×
2075
            .iter()
×
2076
            .copied()
×
2077
            .filter(|id| self.is_window_floating(*id))
×
2078
            .collect();
×
2079
        for id in ids {
×
2080
            self.bring_to_front_id(id);
×
2081
        }
×
2082
    }
×
2083

2084
    fn bring_floating_to_front_id(&mut self, id: WindowId<R>) {
1✔
2085
        self.bring_to_front_id(id);
1✔
2086
    }
1✔
2087

2088
    fn bring_floating_to_front(&mut self, id: R) {
×
2089
        self.bring_floating_to_front_id(WindowId::app(id));
×
2090
    }
×
2091

2092
    fn clamp_floating_to_bounds(&mut self) {
7✔
2093
        let bounds = self.managed_area;
7✔
2094
        if bounds.width == 0 || bounds.height == 0 {
7✔
2095
            return;
×
2096
        }
7✔
2097
        // Collect updates first to avoid borrowing `self` mutably while iterating
2098
        let mut updates: Vec<(WindowId<R>, crate::window::FloatRectSpec)> = Vec::new();
7✔
2099
        let floating_ids: Vec<WindowId<R>> = self
7✔
2100
            .windows
7✔
2101
            .iter()
7✔
2102
            .filter_map(|(&id, window)| window.floating_rect.as_ref().map(|_| id))
7✔
2103
            .collect();
7✔
2104
        for id in floating_ids {
12✔
2105
            let Some(crate::window::FloatRectSpec::Absolute(fr)) = self.floating_rect(id) else {
5✔
2106
                continue;
×
2107
            };
2108

2109
            // Only recover panes that are fully off-screen; keep normal dragging untouched.
2110
            // Use signed arithmetic for off-screen detection.
2111
            let rect_left = fr.x;
5✔
2112
            let rect_top = fr.y;
5✔
2113
            let rect_right = fr.x.saturating_add(fr.width as i32);
5✔
2114
            let rect_bottom = fr.y.saturating_add(fr.height as i32);
5✔
2115
            let bounds_left = bounds.x as i32;
5✔
2116
            let bounds_top = bounds.y as i32;
5✔
2117
            let bounds_right = bounds_left.saturating_add(bounds.width as i32);
5✔
2118
            let bounds_bottom = bounds_top.saturating_add(bounds.height as i32);
5✔
2119

2120
            let min_w = FLOATING_MIN_WIDTH.min(bounds.width.max(1));
5✔
2121
            let min_h = FLOATING_MIN_HEIGHT.min(bounds.height.max(1));
5✔
2122

2123
            // Ensure at least a small portion of the window (e.g. handle) is always visible
2124
            // so the user can grab it back.
2125
            let min_visible_margin = MIN_FLOATING_VISIBLE_MARGIN;
5✔
2126

2127
            let width = if self.floating_resize_offscreen {
5✔
2128
                fr.width.max(min_w)
5✔
2129
            } else {
NEW
2130
                fr.width.max(min_w).min(bounds.width)
×
2131
            };
2132
            let height = if self.floating_resize_offscreen {
5✔
2133
                fr.height.max(min_h)
5✔
2134
            } else {
NEW
2135
                fr.height.max(min_h).min(bounds.height)
×
2136
            };
2137

2138
            let max_x = if self.floating_resize_offscreen {
5✔
2139
                (bounds
5✔
2140
                    .x
5✔
2141
                    .saturating_add(bounds.width)
5✔
2142
                    .saturating_sub(min_visible_margin.min(width))) as i32
5✔
2143
            } else {
NEW
2144
                bounds.x.saturating_add(bounds.width.saturating_sub(width)) as i32
×
2145
            };
2146

2147
            let max_y = if self.floating_resize_offscreen {
5✔
2148
                (bounds
5✔
2149
                    .y
5✔
2150
                    .saturating_add(bounds.height)
5✔
2151
                    .saturating_sub(min_visible_margin.min(height))) as i32
5✔
2152
            } else {
2153
                bounds
×
2154
                    .y
×
NEW
2155
                    .saturating_add(bounds.height.saturating_sub(height)) as i32
×
2156
            };
2157

2158
            // Clamp an axis if the rect is fully outside it, or if the
2159
            // visible portion is smaller than the minimum visible margin.
2160
            let out_x = rect_right <= bounds_left || rect_left >= bounds_right;
5✔
2161
            let out_y = rect_bottom <= bounds_top || rect_top >= bounds_bottom;
5✔
2162

2163
            // When `floating_resize_offscreen` is enabled we allow dragging a
2164
            // floating pane partially off the edges while ensuring a small
2165
            // visible margin remains. If the pane is fully off-screen on an
2166
            // axis (`out_x`/`out_y`) or offscreen handling is disabled, recover
2167
            // it into the visible bounds as before.
2168
            let x = if out_x || !self.floating_resize_offscreen {
5✔
NEW
2169
                fr.x.clamp(bounds_left, max_x)
×
2170
            } else {
2171
                // Compute left-most allowed x such that at least
2172
                // `min_visible_margin` columns remain visible inside `bounds`.
2173
                let left_allowed =
5✔
2174
                    bounds_left.saturating_sub(width as i32 - min_visible_margin.min(width) as i32);
5✔
2175
                let left_allowed = left_allowed.min(max_x);
5✔
2176
                fr.x.clamp(left_allowed, max_x)
5✔
2177
            };
2178

2179
            let y = if out_y || !self.floating_resize_offscreen {
5✔
2180
                fr.y.clamp(bounds_top, max_y)
1✔
2181
            } else {
2182
                let visible_height = min_visible_margin.min(height) as i32;
4✔
2183
                let top_allowed = bounds_top.saturating_sub(height as i32 - visible_height);
4✔
2184
                let top_allowed = top_allowed.min(max_y);
4✔
2185
                fr.y.clamp(top_allowed, max_y)
4✔
2186
            };
2187

2188
            updates.push((
5✔
2189
                id,
5✔
2190
                crate::window::FloatRectSpec::Absolute(crate::window::FloatRect {
5✔
2191
                    x,
5✔
2192
                    y,
5✔
2193
                    width,
5✔
2194
                    height,
5✔
2195
                }),
5✔
2196
            ));
5✔
2197
        }
2198
        for (id, spec) in updates {
12✔
2199
            self.set_floating_rect(id, Some(spec));
5✔
2200
        }
5✔
2201
    }
7✔
2202

2203
    pub fn window_draw_plan(&mut self, frame: &mut UiFrame<'_>) -> Vec<WindowDrawTask<R>> {
×
2204
        let mut plan = Vec::new();
×
2205
        let focused_window = self.wm_focus.current();
×
2206
        for &id in &self.managed_draw_order {
×
2207
            let full = self.full_region_for_id(id);
×
2208
            if full.width == 0 || full.height == 0 {
×
2209
                continue;
×
2210
            }
×
NEW
2211
            let visible_full = self.visible_region_for_id(id);
×
NEW
2212
            if visible_full.width == 0 || visible_full.height == 0 {
×
NEW
2213
                continue;
×
NEW
2214
            }
×
NEW
2215
            frame.render_widget(Clear, visible_full);
×
NEW
2216
            let dest = self.window_dest(id, full);
×
2217
            match id {
×
2218
                WindowId::System(system_id) => {
×
2219
                    if !self.system_window_visible(system_id) {
×
2220
                        continue;
×
2221
                    }
×
NEW
2222
                    let inner_abs = self.region_for_id(id);
×
NEW
2223
                    let inner = Rect {
×
NEW
2224
                        x: inner_abs.x.saturating_sub(full.x),
×
NEW
2225
                        y: inner_abs.y.saturating_sub(full.y),
×
NEW
2226
                        width: inner_abs.width,
×
NEW
2227
                        height: inner_abs.height,
×
NEW
2228
                    };
×
2229
                    if inner.width == 0 || inner.height == 0 {
×
2230
                        continue;
×
2231
                    }
×
2232
                    plan.push(WindowDrawTask::System(SystemWindowDraw {
×
2233
                        id: system_id,
×
NEW
2234
                        surface: WindowSurface { full, inner, dest },
×
2235
                        focused: focused_window == id,
×
2236
                    }));
×
2237
                }
2238
                WindowId::App(app_id) => {
×
NEW
2239
                    let inner_abs = self.region(app_id);
×
NEW
2240
                    let inner = Rect {
×
NEW
2241
                        x: inner_abs.x.saturating_sub(full.x),
×
NEW
2242
                        y: inner_abs.y.saturating_sub(full.y),
×
NEW
2243
                        width: inner_abs.width,
×
NEW
2244
                        height: inner_abs.height,
×
NEW
2245
                    };
×
2246
                    if inner.width == 0 || inner.height == 0 {
×
2247
                        continue;
×
2248
                    }
×
2249
                    plan.push(WindowDrawTask::App(AppWindowDraw {
×
2250
                        id: app_id,
×
NEW
2251
                        surface: WindowSurface { full, inner, dest },
×
2252
                        focused: focused_window == WindowId::app(app_id),
×
2253
                    }));
×
2254
                }
2255
            }
2256
        }
2257
        plan
×
2258
    }
×
2259

2260
    pub fn render_system_window(&mut self, frame: &mut UiFrame<'_>, window: SystemWindowDraw) {
×
2261
        if window.surface.inner.width == 0 || window.surface.inner.height == 0 {
×
2262
            return;
×
2263
        }
×
2264
        self.render_system_window_entry(frame, window);
×
2265
    }
×
2266

2267
    fn hover_targets(&self) -> (Option<&SplitHandle>, Option<&ResizeHandle<WindowId<R>>>) {
3✔
2268
        let Some((column, row)) = self.hover else {
3✔
NEW
2269
            return (None, None);
×
2270
        };
2271
        let topmost = self.hit_test_region_topmost(column, row, &self.managed_draw_order);
3✔
2272
        let hovered = if topmost.is_none() {
3✔
2273
            self.handles
1✔
2274
                .iter()
1✔
2275
                .find(|handle| rect_contains(handle.rect, column, row))
1✔
2276
        } else {
2277
            None
2✔
2278
        };
2279
        let hovered_resize = self
3✔
2280
            .resize_handles
3✔
2281
            .iter()
3✔
2282
            .find(|handle| rect_contains(handle.rect, column, row) && topmost == Some(handle.id));
8✔
2283
        (hovered, hovered_resize)
3✔
2284
    }
3✔
2285

NEW
2286
    pub fn render_overlays(&mut self, frame: &mut UiFrame<'_>) {
×
NEW
2287
        let (hovered, hovered_resize) = self.hover_targets();
×
2288
        let obscuring: Vec<Rect> = self
×
2289
            .managed_draw_order
×
2290
            .iter()
×
2291
            .filter_map(|&id| self.regions.get(id))
×
2292
            .collect();
×
2293
        let is_obscured =
×
2294
            |x: u16, y: u16| -> bool { obscuring.iter().any(|r| rect_contains(*r, x, y)) };
×
2295
        render_handles_masked(frame, &self.handles, hovered, is_obscured);
×
2296
        // Build floating panes list from per-window entries for resize outline rendering
UNCOV
2297
        let floating_panes: Vec<FloatingPane<WindowId<R>>> = self
×
2298
            .windows
×
2299
            .iter()
×
NEW
2300
            .filter_map(|(&id, window)| {
×
NEW
2301
                window.floating_rect.map(|rect| match rect {
×
NEW
2302
                    crate::window::FloatRectSpec::Absolute(fr) => FloatingPane {
×
NEW
2303
                        id,
×
NEW
2304
                        rect: crate::layout::RectSpec::Absolute(ratatui::prelude::Rect {
×
NEW
2305
                            x: fr.x.max(0) as u16,
×
NEW
2306
                            y: fr.y.max(0) as u16,
×
NEW
2307
                            width: fr.width,
×
NEW
2308
                            height: fr.height,
×
NEW
2309
                        }),
×
NEW
2310
                    },
×
2311
                    crate::window::FloatRectSpec::Percent {
NEW
2312
                        x,
×
NEW
2313
                        y,
×
NEW
2314
                        width,
×
NEW
2315
                        height,
×
NEW
2316
                    } => FloatingPane {
×
NEW
2317
                        id,
×
NEW
2318
                        rect: crate::layout::RectSpec::Percent {
×
NEW
2319
                            x,
×
NEW
2320
                            y,
×
NEW
2321
                            width,
×
NEW
2322
                            height,
×
NEW
2323
                        },
×
NEW
2324
                    },
×
NEW
2325
                })
×
NEW
2326
            })
×
UNCOV
2327
            .collect();
×
2328

NEW
2329
        let mut visible_regions = RegionMap::default();
×
NEW
2330
        for id in self.regions.ids() {
×
NEW
2331
            visible_regions.set(id, self.visible_region_for_id(id));
×
NEW
2332
        }
×
2333

2334
        render_resize_outline(
×
2335
            frame,
×
NEW
2336
            hovered_resize.copied(),
×
NEW
2337
            self.drag_resize,
×
NEW
2338
            &visible_regions,
×
2339
            self.managed_area,
×
2340
            &floating_panes,
×
2341
            &self.managed_draw_order,
×
2342
        );
2343

2344
        if let Some((_, _, rect)) = self.drag_snap {
×
2345
            let buffer = frame.buffer_mut();
×
2346
            let color = crate::theme::accent();
×
2347
            let clip = rect.intersection(buffer.area);
×
2348
            if clip.width > 0 && clip.height > 0 {
×
2349
                for y in clip.y..clip.y.saturating_add(clip.height) {
×
2350
                    for x in clip.x..clip.x.saturating_add(clip.width) {
×
2351
                        if let Some(cell) = buffer.cell_mut((x, y)) {
×
2352
                            let mut style = cell.style();
×
2353
                            style.bg = Some(color);
×
2354
                            cell.set_style(style);
×
2355
                        }
×
2356
                    }
2357
                }
2358
            }
×
2359
        }
×
2360

2361
        let status_line = if self.wm_overlay_visible() {
×
2362
            let esc_state = if let Some(remaining) = self.esc_passthrough_remaining() {
×
2363
                format!("Esc passthrough: active ({}ms)", remaining.as_millis())
×
2364
            } else {
2365
                "Esc passthrough: inactive".to_string()
×
2366
            };
2367
            Some(format!("{esc_state} · Tab/Shift-Tab: cycle windows"))
×
2368
        } else {
2369
            None
×
2370
        };
2371
        let display = self.build_display_order();
×
2372
        // Build a small title map to avoid borrowing `self` inside the panel closure
2373
        let titles_map: BTreeMap<WindowId<R>, String> = self
×
2374
            .windows
×
2375
            .keys()
×
2376
            .map(|id| (*id, self.window_title(*id)))
×
2377
            .collect();
×
2378

2379
        self.panel.render(
×
2380
            frame,
×
2381
            self.panel_active(),
×
2382
            self.wm_focus.current(),
×
2383
            &display,
×
2384
            status_line.as_deref(),
×
2385
            self.mouse_capture_enabled(),
×
NEW
2386
            self.clipboard_enabled(),
×
NEW
2387
            self.clipboard_available(),
×
2388
            self.wm_overlay_visible(),
×
2389
            move |id| {
×
2390
                titles_map.get(&id).cloned().unwrap_or_else(|| match id {
×
2391
                    WindowId::App(app_id) => format!("{:?}", app_id),
×
2392
                    WindowId::System(SystemWindowId::DebugLog) => "Debug Log".to_string(),
×
2393
                })
×
2394
            },
×
2395
        );
NEW
2396
        let menu_items = wm_menu_items(
×
NEW
2397
            self.mouse_capture_enabled(),
×
NEW
2398
            self.clipboard_enabled(),
×
NEW
2399
            self.clipboard_available(),
×
2400
        );
NEW
2401
        let menu_labels = menu_items
×
2402
            .iter()
×
2403
            .map(|item| (item.icon, item.label))
×
2404
            .collect::<Vec<_>>();
×
2405
        let bounds = frame.area();
×
2406
        self.panel.render_menu(
×
2407
            frame,
×
2408
            self.wm_overlay_visible(),
×
2409
            bounds,
×
2410
            &menu_labels,
×
2411
            self.state.wm_menu_selected(),
×
2412
        );
2413
        self.panel.render_menu_backdrop(
×
2414
            frame,
×
2415
            self.wm_overlay_visible(),
×
2416
            self.managed_area,
×
2417
            self.panel.area(),
×
2418
        );
2419

2420
        // Render overlays in fixed order if they exist
2421
        if let Some(confirm) = self.overlays.get_mut(&OverlayId::ExitConfirm) {
×
NEW
2422
            confirm.render(
×
NEW
2423
                frame,
×
NEW
2424
                frame.area(),
×
NEW
2425
                &ComponentContext::new(false).with_overlay(true),
×
NEW
2426
            );
×
2427
        }
×
2428
        if let Some(help) = self.overlays.get_mut(&OverlayId::Help) {
×
NEW
2429
            help.render(
×
NEW
2430
                frame,
×
NEW
2431
                frame.area(),
×
NEW
2432
                &ComponentContext::new(false).with_overlay(true),
×
NEW
2433
            );
×
2434
        }
×
2435
    }
×
2436

2437
    pub fn clear_window_backgrounds(&self, frame: &mut UiFrame<'_>) {
×
2438
        for id in self.regions.ids() {
×
2439
            let rect = self.full_region_for_id(id);
×
2440
            frame.render_widget(Clear, rect);
×
2441
        }
×
2442
    }
×
2443

2444
    pub fn set_regions_from_plan(&mut self, plan: &LayoutPlan<R>, area: Rect) {
×
2445
        let plan_regions = plan.regions(area);
×
2446
        self.regions = RegionMap::default();
×
2447
        for id in plan_regions.ids() {
×
2448
            if let Some(rect) = plan_regions.get(id) {
×
2449
                self.regions.set(WindowId::app(id), rect);
×
2450
            }
×
2451
        }
2452
    }
×
2453

2454
    pub fn hit_test_region(&self, column: u16, row: u16, ids: &[R]) -> Option<R> {
×
2455
        for id in ids {
×
NEW
2456
            let rect = self.visible_region_for_id(WindowId::app(*id));
×
NEW
2457
            if rect.width > 0 && rect.height > 0 && rect_contains(rect, column, row) {
×
2458
                return Some(*id);
×
2459
            }
×
2460
        }
2461
        None
×
2462
    }
×
2463

2464
    /// Hit-test regions by draw order so overlapping panes pick the topmost one.
2465
    /// This avoids clicks "falling through" floating panes to windows behind them.
2466
    fn hit_test_region_topmost(
11✔
2467
        &self,
11✔
2468
        column: u16,
11✔
2469
        row: u16,
11✔
2470
        ids: &[WindowId<R>],
11✔
2471
    ) -> Option<WindowId<R>> {
11✔
2472
        for id in ids.iter().rev() {
14✔
2473
            let rect = self.visible_region_for_id(*id);
14✔
2474
            if rect.width > 0 && rect.height > 0 && rect_contains(rect, column, row) {
14✔
2475
                return Some(*id);
10✔
2476
            }
4✔
2477
        }
2478
        None
1✔
2479
    }
11✔
2480

2481
    pub fn handle_focus_event<F, G>(
×
2482
        &mut self,
×
2483
        event: &Event,
×
2484
        hit_targets: &[R],
×
2485
        map: F,
×
2486
        map_focus: G,
×
2487
    ) -> bool
×
2488
    where
×
2489
        F: Fn(R) -> W,
×
2490
        G: Fn(W) -> Option<R>,
×
2491
    {
2492
        match event {
×
2493
            Event::Key(key) => {
×
2494
                let kb = crate::keybindings::KeyBindings::default();
×
2495
                if kb.matches(crate::keybindings::Action::FocusNext, key) {
×
2496
                    if self.layout_contract == LayoutContract::WindowManaged {
×
2497
                        self.advance_wm_focus(true);
×
2498
                    } else {
×
2499
                        self.app_focus.advance(true);
×
2500
                        let focused_app = self.app_focus.current();
×
2501
                        if let Some(region) = map_focus(focused_app) {
×
2502
                            self.set_wm_focus(WindowId::app(region));
×
2503
                            self.bring_to_front_id(WindowId::app(region));
×
2504
                            self.managed_draw_order = self.z_order.clone();
×
2505
                        }
×
2506
                    }
2507
                    true
×
2508
                } else if kb.matches(crate::keybindings::Action::FocusPrev, key) {
×
2509
                    if self.layout_contract == LayoutContract::WindowManaged {
×
2510
                        self.advance_wm_focus(false);
×
2511
                    } else {
×
2512
                        self.app_focus.advance(false);
×
2513
                        let focused_app = self.app_focus.current();
×
2514
                        if let Some(region) = map_focus(focused_app) {
×
2515
                            self.set_wm_focus(WindowId::app(region));
×
2516
                            self.bring_to_front_id(WindowId::app(region));
×
2517
                            self.managed_draw_order = self.z_order.clone();
×
2518
                        }
×
2519
                    }
2520
                    true
×
2521
                } else {
2522
                    false
×
2523
                }
2524
            }
2525
            Event::Mouse(mouse) => {
×
2526
                self.hover = Some((mouse.column, mouse.row));
×
2527
                match mouse.kind {
×
2528
                    MouseEventKind::Down(_) => {
2529
                        if self.layout_contract == LayoutContract::WindowManaged
×
2530
                            && !self.managed_draw_order.is_empty()
×
2531
                        {
2532
                            let hit = self.hit_test_region_topmost(
×
2533
                                mouse.column,
×
2534
                                mouse.row,
×
2535
                                &self.managed_draw_order,
×
2536
                            );
2537
                            if let Some(id) = hit {
×
2538
                                self.set_wm_focus(id);
×
2539
                                self.bring_floating_to_front_id(id);
×
2540
                                return true;
×
2541
                            }
×
2542
                            return false;
×
2543
                        }
×
2544
                        let hit = self.hit_test_region(mouse.column, mouse.row, hit_targets);
×
2545
                        if let Some(hit) = hit {
×
2546
                            self.app_focus.set_current(map(hit));
×
2547
                            if self.layout_contract == LayoutContract::WindowManaged {
×
2548
                                self.set_wm_focus(WindowId::app(hit));
×
2549
                                self.bring_floating_to_front(hit);
×
2550
                            }
×
2551
                            true
×
2552
                        } else {
2553
                            false
×
2554
                        }
2555
                    }
2556
                    _ => false,
×
2557
                }
2558
            }
2559
            _ => false,
×
2560
        }
2561
    }
×
2562

2563
    fn handle_system_window_event(&mut self, event: &Event) -> bool {
1✔
2564
        if self.layout_contract != LayoutContract::WindowManaged {
1✔
2565
            return false;
×
2566
        }
1✔
2567
        match event {
1✔
2568
            Event::Mouse(mouse) => {
1✔
2569
                if self.managed_draw_order.is_empty() {
1✔
2570
                    return false;
×
2571
                }
1✔
2572
                let hit =
1✔
2573
                    self.hit_test_region_topmost(mouse.column, mouse.row, &self.managed_draw_order);
1✔
2574
                if let Some(WindowId::System(system_id)) = hit {
1✔
2575
                    if !self.system_window_visible(system_id) {
×
2576
                        return false;
×
2577
                    }
×
2578
                    if matches!(mouse.kind, MouseEventKind::Down(_)) {
×
2579
                        self.focus_window_id(WindowId::system(system_id));
×
2580
                    }
×
2581
                    return self.dispatch_system_window_event(system_id, event);
×
2582
                }
1✔
2583
                if matches!(mouse.kind, MouseEventKind::Down(_))
1✔
2584
                    && let WindowId::System(system_id) = self.wm_focus.current()
1✔
UNCOV
2585
                    && self.system_window_visible(system_id)
×
2586
                {
×
2587
                    self.select_fallback_focus();
×
2588
                }
1✔
2589
                false
1✔
2590
            }
2591
            Event::Key(_) => {
2592
                if let WindowId::System(system_id) = self.wm_focus.current()
×
2593
                    && self.system_window_visible(system_id)
×
2594
                {
2595
                    return self.dispatch_system_window_event(system_id, event);
×
2596
                }
×
2597
                false
×
2598
            }
2599
            _ => false,
×
2600
        }
2601
    }
1✔
2602

2603
    fn panel_active(&self) -> bool {
12✔
2604
        self.layout_contract == LayoutContract::WindowManaged
12✔
2605
            && self.panel.visible()
12✔
2606
            && self.panel.height() > 0
7✔
2607
    }
12✔
2608

2609
    fn focus_for_region(&self, id: R) -> Option<W> {
4✔
2610
        if self.app_focus.order.is_empty() {
4✔
2611
            if id == self.app_focus.current {
4✔
2612
                Some(self.app_focus.current)
×
2613
            } else {
2614
                None
4✔
2615
            }
2616
        } else {
2617
            self.app_focus
×
2618
                .order
×
2619
                .iter()
×
2620
                .copied()
×
2621
                .find(|focus| id == *focus)
×
2622
        }
2623
    }
4✔
2624

2625
    pub fn handle_wm_menu_event(&mut self, event: &Event) -> Option<WmMenuAction> {
×
2626
        if !self.wm_overlay_visible() {
×
2627
            return None;
×
2628
        }
×
NEW
2629
        let items = wm_menu_items(
×
NEW
2630
            self.mouse_capture_enabled(),
×
NEW
2631
            self.clipboard_enabled(),
×
NEW
2632
            self.clipboard_available(),
×
2633
        );
2634
        if let Event::Mouse(mouse) = event
×
2635
            && matches!(mouse.kind, MouseEventKind::Down(_))
×
2636
        {
2637
            if let Some(index) = self.panel.hit_test_menu_item(event) {
×
2638
                let selected = index.min(items.len().saturating_sub(1));
×
2639
                self.state.set_wm_menu_selected(selected);
×
2640
                return items.get(selected).map(|item| item.action);
×
2641
            }
×
2642
            if self.panel.menu_icon_contains_point(mouse.column, mouse.row) {
×
2643
                return Some(WmMenuAction::CloseMenu);
×
2644
            }
×
2645
            if !self.panel.menu_contains_point(mouse.column, mouse.row) {
×
2646
                return Some(WmMenuAction::CloseMenu);
×
2647
            }
×
2648
        }
×
2649
        let Event::Key(key) = event else {
×
2650
            return None;
×
2651
        };
2652
        let kb = crate::keybindings::KeyBindings::default();
×
2653
        if kb.matches(crate::keybindings::Action::MenuUp, key)
×
2654
            || kb.matches(crate::keybindings::Action::MenuPrev, key)
×
2655
        {
NEW
2656
            let total = items.len();
×
2657
            if total > 0 {
×
2658
                let current = self.state.wm_menu_selected();
×
2659
                if current == 0 {
×
2660
                    self.state.set_wm_menu_selected(total - 1);
×
2661
                } else {
×
2662
                    self.state.set_wm_menu_selected(current - 1);
×
2663
                }
×
2664
            }
×
2665
            None
×
2666
        } else if kb.matches(crate::keybindings::Action::MenuDown, key)
×
2667
            || kb.matches(crate::keybindings::Action::MenuNext, key)
×
2668
        {
NEW
2669
            let total = items.len();
×
2670
            if total > 0 {
×
2671
                let current = self.state.wm_menu_selected();
×
2672
                self.state.set_wm_menu_selected((current + 1) % total);
×
2673
            }
×
2674
            None
×
2675
        } else if kb.matches(crate::keybindings::Action::MenuSelect, key) {
×
NEW
2676
            items
×
2677
                .get(self.state.wm_menu_selected())
×
2678
                .map(|item| item.action)
×
2679
        } else {
2680
            None
×
2681
        }
2682
    }
×
2683

2684
    pub fn handle_exit_confirm_event(&mut self, event: &Event) -> Option<ConfirmAction> {
×
2685
        let comp = self.overlays.get_mut(&OverlayId::ExitConfirm)?;
×
2686
        // Downcast to ConfirmOverlayComponent to access specific method
2687
        if let Some(confirm) = comp.as_any_mut().downcast_mut::<ConfirmOverlayComponent>() {
×
2688
            return confirm.handle_confirm_event(event);
×
2689
        }
×
2690
        None
×
2691
    }
×
2692

2693
    pub fn wm_menu_consumes_event(&self, event: &Event) -> bool {
×
2694
        if !self.wm_overlay_visible() {
×
2695
            return false;
×
2696
        }
×
2697
        let Event::Key(key) = event else {
×
2698
            return false;
×
2699
        };
2700
        let kb = crate::keybindings::KeyBindings::default();
×
2701
        kb.matches(crate::keybindings::Action::MenuUp, key)
×
2702
            || kb.matches(crate::keybindings::Action::MenuDown, key)
×
2703
            || kb.matches(crate::keybindings::Action::MenuSelect, key)
×
2704
            || kb.matches(crate::keybindings::Action::MenuNext, key)
×
2705
            || kb.matches(crate::keybindings::Action::MenuPrev, key)
×
2706
    }
×
2707
}
2708

2709
#[derive(Debug, Clone, Copy)]
2710
struct WmMenuItem {
2711
    label: &'static str,
2712
    icon: Option<&'static str>,
2713
    action: WmMenuAction,
2714
}
NEW
2715
fn wm_menu_items(
×
NEW
2716
    mouse_capture_enabled: bool,
×
NEW
2717
    clipboard_enabled: bool,
×
NEW
2718
    clipboard_available: bool,
×
NEW
2719
) -> [WmMenuItem; 8] {
×
2720
    let mouse_label = if mouse_capture_enabled {
×
2721
        "Mouse Capture: On"
×
2722
    } else {
2723
        "Mouse Capture: Off"
×
2724
    };
NEW
2725
    let clipboard_label = if clipboard_available {
×
NEW
2726
        if clipboard_enabled {
×
NEW
2727
            "Clipboard Mode: On"
×
2728
        } else {
NEW
2729
            "Clipboard Mode: Off"
×
2730
        }
2731
    } else {
NEW
2732
        "Clipboard Mode: Unavailable"
×
2733
    };
2734
    [
×
2735
        WmMenuItem {
×
2736
            label: "Resume",
×
2737
            icon: None,
×
2738
            action: WmMenuAction::CloseMenu,
×
2739
        },
×
2740
        WmMenuItem {
×
2741
            label: mouse_label,
×
2742
            icon: Some("🖱"),
×
2743
            action: WmMenuAction::ToggleMouseCapture,
×
2744
        },
×
NEW
2745
        WmMenuItem {
×
NEW
2746
            label: clipboard_label,
×
NEW
2747
            icon: Some("📋"),
×
NEW
2748
            action: WmMenuAction::ToggleClipboardMode,
×
NEW
2749
        },
×
2750
        WmMenuItem {
×
2751
            label: "Floating Front",
×
2752
            icon: Some("↑"),
×
2753
            action: WmMenuAction::BringFloatingFront,
×
2754
        },
×
2755
        WmMenuItem {
×
2756
            label: "New Window",
×
2757
            icon: Some("+"),
×
2758
            action: WmMenuAction::NewWindow,
×
2759
        },
×
2760
        WmMenuItem {
×
2761
            label: "Debug Log",
×
2762
            icon: Some("≣"),
×
2763
            action: WmMenuAction::ToggleDebugWindow,
×
2764
        },
×
2765
        WmMenuItem {
×
2766
            label: "Help",
×
2767
            icon: Some("?"),
×
2768
            action: WmMenuAction::Help,
×
2769
        },
×
2770
        WmMenuItem {
×
2771
            label: "Exit UI",
×
2772
            icon: Some("⏻"),
×
2773
            action: WmMenuAction::ExitUi,
×
2774
        },
×
2775
    ]
×
2776
}
×
2777

2778
fn esc_passthrough_window_default() -> Duration {
14✔
2779
    #[cfg(windows)]
2780
    {
2781
        Duration::from_millis(1200)
2782
    }
2783
    #[cfg(not(windows))]
2784
    {
2785
        Duration::from_millis(600)
14✔
2786
    }
2787
}
14✔
2788

2789
fn clamp_rect(area: Rect, bounds: Rect) -> Rect {
2✔
2790
    let x0 = area.x.max(bounds.x);
2✔
2791
    let y0 = area.y.max(bounds.y);
2✔
2792
    let x1 = area
2✔
2793
        .x
2✔
2794
        .saturating_add(area.width)
2✔
2795
        .min(bounds.x.saturating_add(bounds.width));
2✔
2796
    let y1 = area
2✔
2797
        .y
2✔
2798
        .saturating_add(area.height)
2✔
2799
        .min(bounds.y.saturating_add(bounds.height));
2✔
2800
    if x1 <= x0 || y1 <= y0 {
2✔
2801
        return Rect::default();
1✔
2802
    }
1✔
2803
    Rect {
1✔
2804
        x: x0,
1✔
2805
        y: y0,
1✔
2806
        width: x1 - x0,
1✔
2807
        height: y1 - y0,
1✔
2808
    }
1✔
2809
}
2✔
2810

2811
fn float_rect_visible(rect: crate::window::FloatRect, bounds: Rect) -> Rect {
7✔
2812
    let bounds_x0 = bounds.x as i32;
7✔
2813
    let bounds_y0 = bounds.y as i32;
7✔
2814
    let bounds_x1 = bounds_x0 + bounds.width as i32;
7✔
2815
    let bounds_y1 = bounds_y0 + bounds.height as i32;
7✔
2816
    let rect_x0 = rect.x;
7✔
2817
    let rect_y0 = rect.y;
7✔
2818
    let rect_x1 = rect.x + rect.width as i32;
7✔
2819
    let rect_y1 = rect.y + rect.height as i32;
7✔
2820
    let x0 = rect_x0.max(bounds_x0);
7✔
2821
    let y0 = rect_y0.max(bounds_y0);
7✔
2822
    let x1 = rect_x1.min(bounds_x1);
7✔
2823
    let y1 = rect_y1.min(bounds_y1);
7✔
2824
    if x1 <= x0 || y1 <= y0 {
7✔
NEW
2825
        return Rect::default();
×
2826
    }
7✔
2827
    Rect {
7✔
2828
        x: x0 as u16,
7✔
2829
        y: y0 as u16,
7✔
2830
        width: (x1 - x0) as u16,
7✔
2831
        height: (y1 - y0) as u16,
7✔
2832
    }
7✔
2833
}
7✔
2834

2835
fn map_layout_node<R: Copy + Eq + Ord>(node: &LayoutNode<R>) -> LayoutNode<WindowId<R>> {
1✔
2836
    match node {
1✔
2837
        LayoutNode::Leaf(id) => LayoutNode::leaf(WindowId::app(*id)),
1✔
2838
        LayoutNode::Split {
2839
            direction,
×
2840
            children,
×
2841
            weights,
×
2842
            constraints,
×
2843
            resizable,
×
2844
        } => LayoutNode::Split {
×
2845
            direction: *direction,
×
2846
            children: children.iter().map(map_layout_node).collect(),
×
2847
            weights: weights.clone(),
×
2848
            constraints: constraints.clone(),
×
2849
            resizable: *resizable,
×
2850
        },
×
2851
    }
2852
}
1✔
2853

2854
#[cfg(test)]
2855
fn rects_intersect(a: Rect, b: Rect) -> bool {
2✔
2856
    if a.width == 0 || a.height == 0 || b.width == 0 || b.height == 0 {
2✔
NEW
2857
        return false;
×
2858
    }
2✔
2859
    let a_right = a.x.saturating_add(a.width);
2✔
2860
    let a_bottom = a.y.saturating_add(a.height);
2✔
2861
    let b_right = b.x.saturating_add(b.width);
2✔
2862
    let b_bottom = b.y.saturating_add(b.height);
2✔
2863
    a.x < b_right && a_right > b.x && a.y < b_bottom && a_bottom > b.y
2✔
2864
}
2✔
2865

2866
#[cfg(test)]
2867
mod tests {
2868
    use super::*;
2869
    use ratatui::layout::{Direction, Rect};
2870

2871
    #[test]
2872
    fn clamp_rect_inside_and_outside() {
1✔
2873
        let area = Rect {
1✔
2874
            x: 2,
1✔
2875
            y: 2,
1✔
2876
            width: 4,
1✔
2877
            height: 4,
1✔
2878
        };
1✔
2879
        let bounds = Rect {
1✔
2880
            x: 0,
1✔
2881
            y: 0,
1✔
2882
            width: 10,
1✔
2883
            height: 10,
1✔
2884
        };
1✔
2885
        let r = clamp_rect(area, bounds);
1✔
2886
        assert_eq!(r.x, 2);
1✔
2887
        assert_eq!(r.y, 2);
1✔
2888

2889
        // non-overlapping
2890
        let area2 = Rect {
1✔
2891
            x: 50,
1✔
2892
            y: 50,
1✔
2893
            width: 1,
1✔
2894
            height: 1,
1✔
2895
        };
1✔
2896
        let r2 = clamp_rect(area2, bounds);
1✔
2897
        assert_eq!(r2, Rect::default());
1✔
2898
    }
1✔
2899

2900
    #[test]
2901
    fn float_rect_visible_clips_negative_offsets() {
1✔
2902
        let bounds = Rect {
1✔
2903
            x: 0,
1✔
2904
            y: 0,
1✔
2905
            width: 80,
1✔
2906
            height: 24,
1✔
2907
        };
1✔
2908
        let rect = crate::window::FloatRect {
1✔
2909
            x: -5,
1✔
2910
            y: 3,
1✔
2911
            width: 20,
1✔
2912
            height: 6,
1✔
2913
        };
1✔
2914
        let visible = float_rect_visible(rect, bounds);
1✔
2915
        assert_eq!(visible.x, 0);
1✔
2916
        assert_eq!(visible.y, 3);
1✔
2917
        assert_eq!(visible.width, 15);
1✔
2918
        assert_eq!(visible.height, 6);
1✔
2919
    }
1✔
2920

2921
    #[test]
2922
    fn rects_intersect_true_and_false() {
1✔
2923
        let a = Rect {
1✔
2924
            x: 0,
1✔
2925
            y: 0,
1✔
2926
            width: 5,
1✔
2927
            height: 5,
1✔
2928
        };
1✔
2929
        let b = Rect {
1✔
2930
            x: 4,
1✔
2931
            y: 4,
1✔
2932
            width: 5,
1✔
2933
            height: 5,
1✔
2934
        };
1✔
2935
        assert!(rects_intersect(a, b));
1✔
2936
        let c = Rect {
1✔
2937
            x: 10,
1✔
2938
            y: 10,
1✔
2939
            width: 1,
1✔
2940
            height: 1,
1✔
2941
        };
1✔
2942
        assert!(!rects_intersect(a, c));
1✔
2943
    }
1✔
2944

2945
    #[test]
2946
    fn map_layout_node_maps_leaf_to_windowid_app() {
1✔
2947
        let node = LayoutNode::leaf(3usize);
1✔
2948
        let mapped = map_layout_node(&node);
1✔
2949
        match mapped {
1✔
2950
            LayoutNode::Leaf(id) => match id {
1✔
2951
                WindowId::App(r) => assert_eq!(r, 3usize),
1✔
2952
                _ => panic!("expected App window id"),
×
2953
            },
2954
            _ => panic!("expected leaf"),
×
2955
        }
2956
    }
1✔
2957

2958
    #[test]
2959
    fn esc_passthrough_default_nonzero() {
1✔
2960
        let d = esc_passthrough_window_default();
1✔
2961
        assert!(d.as_millis() > 0);
1✔
2962
    }
1✔
2963

2964
    #[test]
2965
    fn focus_ring_wraps_and_advances() {
1✔
2966
        let mut ring = FocusRing::new(2usize);
1✔
2967
        ring.set_order(vec![1usize, 2usize, 3usize]);
1✔
2968
        assert_eq!(ring.current(), 2);
1✔
2969
        ring.advance(true);
1✔
2970
        assert_eq!(ring.current(), 3);
1✔
2971
        ring.advance(true);
1✔
2972
        assert_eq!(ring.current(), 1);
1✔
2973
        ring.advance(false);
1✔
2974
        assert_eq!(ring.current(), 3);
1✔
2975
    }
1✔
2976

2977
    #[test]
2978
    fn scroll_state_apply_and_bump() {
1✔
2979
        let mut s = ScrollState::default();
1✔
2980
        s.bump(5);
1✔
2981
        s.apply(100, 10);
1✔
2982
        assert_eq!(s.offset, 5usize);
1✔
2983

2984
        s.offset = 1000;
1✔
2985
        s.apply(20, 5);
1✔
2986
        let max_off = 20usize.saturating_sub(5usize);
1✔
2987
        assert_eq!(s.offset, max_off);
1✔
2988
    }
1✔
2989

2990
    #[test]
2991
    fn click_focusing_topmost_window() {
1✔
2992
        use crossterm::event::{Event, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
2993
        let mut wm = WindowManager::<usize, usize>::new_managed(0);
1✔
2994

2995
        // Two overlapping regions: window 1 underneath, window 2 on top
2996
        let r1 = Rect {
1✔
2997
            x: 0,
1✔
2998
            y: 0,
1✔
2999
            width: 10,
1✔
3000
            height: 10,
1✔
3001
        };
1✔
3002
        let r2 = Rect {
1✔
3003
            x: 5,
1✔
3004
            y: 5,
1✔
3005
            width: 10,
1✔
3006
            height: 10,
1✔
3007
        };
1✔
3008
        wm.regions.set(WindowId::app(1usize), r1);
1✔
3009
        wm.regions.set(WindowId::app(2usize), r2);
1✔
3010
        wm.z_order.push(WindowId::app(1usize));
1✔
3011
        wm.z_order.push(WindowId::app(2usize));
1✔
3012
        wm.managed_draw_order = wm.z_order.clone();
1✔
3013

3014
        // initial wm focus defaults to a system window (DebugLog)
3015
        assert!(matches!(wm.wm_focus.current(), WindowId::System(_)));
1✔
3016

3017
        // Click inside the overlapping area that belongs to window 2 (topmost)
3018
        let clicked_col = 6u16;
1✔
3019
        let clicked_row = 6u16;
1✔
3020
        let mouse = MouseEvent {
1✔
3021
            kind: MouseEventKind::Down(MouseButton::Left),
1✔
3022
            column: clicked_col,
1✔
3023
            row: clicked_row,
1✔
3024
            modifiers: KeyModifiers::NONE,
1✔
3025
        };
1✔
3026
        let evt = Event::Mouse(mouse);
1✔
3027
        // Call the public handler path as in runtime
3028
        let _handled = wm.handle_managed_event(&evt);
1✔
3029
        assert_eq!(wm.wm_focus.current(), WindowId::app(2usize));
1✔
3030
    }
1✔
3031

3032
    #[test]
3033
    fn enforce_min_visible_margin_horizontal() {
1✔
3034
        use crate::window::{FloatRect, FloatRectSpec};
3035
        let mut wm = WindowManager::<usize, usize>::new_managed(0);
1✔
3036
        wm.set_floating_resize_offscreen(true);
1✔
3037
        // place a floating window such that only 2 columns are visible but margin is 4
3038
        wm.set_floating_rect(
1✔
3039
            WindowId::app(1usize),
1✔
3040
            Some(FloatRectSpec::Absolute(FloatRect {
1✔
3041
                x: -4,
1✔
3042
                y: 0,
1✔
3043
                width: 6,
1✔
3044
                height: 3,
1✔
3045
            })),
1✔
3046
        );
3047
        wm.register_managed_layout(ratatui::layout::Rect {
1✔
3048
            x: 0,
1✔
3049
            y: 0,
1✔
3050
            width: 10,
1✔
3051
            height: 10,
1✔
3052
        });
1✔
3053
        let got = wm
1✔
3054
            .floating_rect(WindowId::app(1))
1✔
3055
            .expect("floating rect present");
1✔
3056
        match got {
1✔
3057
            FloatRectSpec::Absolute(fr) => {
1✔
3058
                let bounds = wm.managed_area;
1✔
3059
                let left_allowed = bounds.x as i32
1✔
3060
                    - (6i32 - crate::constants::MIN_FLOATING_VISIBLE_MARGIN.min(6) as i32);
1✔
3061
                assert_eq!(fr.x, left_allowed);
1✔
3062
            }
NEW
3063
            _ => panic!("expected absolute rect"),
×
3064
        }
3065
    }
1✔
3066

3067
    #[test]
3068
    fn enforce_min_visible_margin_vertical() {
1✔
3069
        use crate::window::{FloatRect, FloatRectSpec};
3070
        let mut wm = WindowManager::<usize, usize>::new_managed(0);
1✔
3071
        wm.set_floating_resize_offscreen(true);
1✔
3072
        // place a floating window such that only 1 row is visible but margin is 4
3073
        wm.set_floating_rect(
1✔
3074
            WindowId::app(2usize),
1✔
3075
            Some(FloatRectSpec::Absolute(FloatRect {
1✔
3076
                x: 0,
1✔
3077
                y: -3,
1✔
3078
                width: 6,
1✔
3079
                height: 4,
1✔
3080
            })),
1✔
3081
        );
3082
        wm.register_managed_layout(ratatui::layout::Rect {
1✔
3083
            x: 0,
1✔
3084
            y: 0,
1✔
3085
            width: 10,
1✔
3086
            height: 10,
1✔
3087
        });
1✔
3088
        let got = wm
1✔
3089
            .floating_rect(WindowId::app(2))
1✔
3090
            .expect("floating rect present");
1✔
3091
        match got {
1✔
3092
            FloatRectSpec::Absolute(fr) => {
1✔
3093
                // top_allowed = 0 - (4 - MIN_MARGIN) => 0 - (4-4) = 0
3094
                // but since original y=-3, it should clamp up to 0
3095
                assert!(fr.y >= 0);
1✔
3096
            }
NEW
3097
            _ => panic!("expected absolute rect"),
×
3098
        }
3099
    }
1✔
3100

3101
    #[test]
3102
    fn maximize_persists_across_resize() {
1✔
3103
        use crate::window::FloatRectSpec;
3104
        let mut wm = WindowManager::<usize, usize>::new_managed(0);
1✔
3105
        // initial managed area
3106
        wm.register_managed_layout(ratatui::layout::Rect {
1✔
3107
            x: 0,
1✔
3108
            y: 0,
1✔
3109
            width: 20,
1✔
3110
            height: 15,
1✔
3111
        });
1✔
3112
        // maximize window 3
3113
        wm.toggle_maximize(WindowId::app(3usize));
1✔
3114
        // change managed area (simulate resize)
3115
        wm.register_managed_layout(ratatui::layout::Rect {
1✔
3116
            x: 0,
1✔
3117
            y: 0,
1✔
3118
            width: 30,
1✔
3119
            height: 20,
1✔
3120
        });
1✔
3121
        let got = wm
1✔
3122
            .floating_rect(WindowId::app(3))
1✔
3123
            .expect("floating rect present");
1✔
3124
        match got {
1✔
3125
            FloatRectSpec::Absolute(fr) => {
1✔
3126
                // should match the current managed_area after resize
3127
                assert_eq!(fr.width, wm.managed_area.width);
1✔
3128
                assert_eq!(fr.height, wm.managed_area.height);
1✔
3129
            }
NEW
3130
            _ => panic!("expected absolute rect"),
×
3131
        }
3132
    }
1✔
3133

3134
    #[test]
3135
    fn localize_event_converts_to_local_coords() {
1✔
3136
        use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
3137
        let mut wm = WindowManager::<usize, usize>::new_managed(0);
1✔
3138
        let target_rect = ratatui::layout::Rect {
1✔
3139
            x: 10,
1✔
3140
            y: 5,
1✔
3141
            width: 20,
1✔
3142
            height: 8,
1✔
3143
        };
1✔
3144
        wm.set_region(1, target_rect);
1✔
3145
        let mouse = MouseEvent {
1✔
3146
            kind: MouseEventKind::Down(MouseButton::Left),
1✔
3147
            column: 15,
1✔
3148
            row: 9,
1✔
3149
            modifiers: crossterm::event::KeyModifiers::NONE,
1✔
3150
        };
1✔
3151
        let event = Event::Mouse(mouse);
1✔
3152
        // Window-local coordinates include chrome offsets.
3153
        let window_local = wm
1✔
3154
            .localize_event(WindowId::app(1), &event)
1✔
3155
            .expect("window-local event");
1✔
3156
        if let Event::Mouse(local) = window_local {
1✔
3157
            assert_eq!(local.column, 5); // 15 - target_rect.x
1✔
3158
            assert_eq!(local.row, 4); // 9 - target_rect.y
1✔
3159
        } else {
NEW
3160
            panic!("expected mouse event");
×
3161
        }
3162

3163
        // Content-local coordinates subtract decorator padding.
3164
        let content_local = wm
1✔
3165
            .localize_event_to_app(1, &event)
1✔
3166
            .expect("content-local event");
1✔
3167
        if let Event::Mouse(local) = content_local {
1✔
3168
            assert_eq!(local.column, 4);
1✔
3169
            assert_eq!(local.row, 2);
1✔
3170
        } else {
NEW
3171
            panic!("expected mouse event");
×
3172
        }
3173
    }
1✔
3174

3175
    #[test]
3176
    fn localize_event_handles_negative_origin() {
1✔
3177
        use crate::window::{FloatRect, FloatRectSpec};
3178
        use crossterm::event::{Event, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
3179
        let mut wm = WindowManager::<usize, usize>::new_managed(0);
1✔
3180
        wm.set_floating_resize_offscreen(true);
1✔
3181
        wm.set_floating_rect(
1✔
3182
            WindowId::app(1usize),
1✔
3183
            Some(FloatRectSpec::Absolute(FloatRect {
1✔
3184
                x: -5,
1✔
3185
                y: 1,
1✔
3186
                width: 10,
1✔
3187
                height: 5,
1✔
3188
            })),
1✔
3189
        );
3190
        wm.register_managed_layout(ratatui::layout::Rect {
1✔
3191
            x: 0,
1✔
3192
            y: 0,
1✔
3193
            width: 40,
1✔
3194
            height: 20,
1✔
3195
        });
1✔
3196
        let mouse = MouseEvent {
1✔
3197
            kind: MouseEventKind::Down(MouseButton::Left),
1✔
3198
            column: 0,
1✔
3199
            row: 3,
1✔
3200
            modifiers: KeyModifiers::NONE,
1✔
3201
        };
1✔
3202
        let event = Event::Mouse(mouse);
1✔
3203

3204
        let window_local = wm
1✔
3205
            .localize_event(WindowId::app(1), &event)
1✔
3206
            .expect("window-local event");
1✔
3207
        if let Event::Mouse(local) = window_local {
1✔
3208
            assert_eq!(local.column, 5);
1✔
3209
            assert_eq!(local.row, 2);
1✔
3210
        } else {
NEW
3211
            panic!("expected mouse event");
×
3212
        }
3213

3214
        let content_local = wm
1✔
3215
            .localize_event_to_app(1, &event)
1✔
3216
            .expect("content-local event");
1✔
3217
        if let Event::Mouse(local) = content_local {
1✔
3218
            assert_eq!(local.column, 4);
1✔
3219
            assert_eq!(local.row, 0);
1✔
3220
        } else {
NEW
3221
            panic!("expected mouse event");
×
3222
        }
3223
    }
1✔
3224

3225
    #[test]
3226
    fn hit_test_uses_visible_bounds_for_floating_windows() {
1✔
3227
        use crate::window::{FloatRect, FloatRectSpec};
3228
        let mut wm = WindowManager::<usize, usize>::new_managed(0);
1✔
3229
        wm.set_floating_resize_offscreen(true);
1✔
3230
        wm.set_floating_rect(
1✔
3231
            WindowId::app(1usize),
1✔
3232
            Some(FloatRectSpec::Absolute(FloatRect {
1✔
3233
                x: -5,
1✔
3234
                y: 0,
1✔
3235
                width: 10,
1✔
3236
                height: 5,
1✔
3237
            })),
1✔
3238
        );
3239
        wm.register_managed_layout(ratatui::layout::Rect {
1✔
3240
            x: 0,
1✔
3241
            y: 0,
1✔
3242
            width: 30,
1✔
3243
            height: 10,
1✔
3244
        });
1✔
3245
        // Background window occupies most of the visible area.
3246
        wm.regions.set(
1✔
3247
            WindowId::app(2usize),
1✔
3248
            ratatui::layout::Rect {
1✔
3249
                x: 0,
1✔
3250
                y: 0,
1✔
3251
                width: 30,
1✔
3252
                height: 10,
1✔
3253
            },
1✔
3254
        );
3255
        wm.managed_draw_order = vec![WindowId::app(2usize), WindowId::app(1usize)];
1✔
3256

3257
        // Click to the right of the clipped floating window. Without clipping, window 1 would eat
3258
        // the event; with visible bounds it should fall through to the background window.
3259
        let hit = wm.hit_test_region_topmost(8, 2, &wm.managed_draw_order);
1✔
3260
        assert_eq!(hit, Some(WindowId::app(2usize)));
1✔
3261
    }
1✔
3262

3263
    #[test]
3264
    fn hover_targets_respects_occlusion() {
1✔
3265
        use crate::layout::floating::{ResizeEdge, ResizeHandle};
3266
        use crate::layout::tiling::SplitHandle;
3267
        let mut wm = WindowManager::<usize, usize>::new_managed(0);
1✔
3268
        wm.regions.set(
1✔
3269
            WindowId::app(1usize),
1✔
3270
            Rect {
1✔
3271
                x: 0,
1✔
3272
                y: 0,
1✔
3273
                width: 10,
1✔
3274
                height: 5,
1✔
3275
            },
1✔
3276
        );
3277
        wm.regions.set(
1✔
3278
            WindowId::app(2usize),
1✔
3279
            Rect {
1✔
3280
                x: 0,
1✔
3281
                y: 0,
1✔
3282
                width: 5,
1✔
3283
                height: 5,
1✔
3284
            },
1✔
3285
        );
3286
        wm.managed_draw_order = vec![WindowId::app(1usize), WindowId::app(2usize)];
1✔
3287
        // Two resize handles sharing the same coordinates but belonging to different windows
3288
        let overlapping = Rect {
1✔
3289
            x: 2,
1✔
3290
            y: 1,
1✔
3291
            width: 1,
1✔
3292
            height: 1,
1✔
3293
        };
1✔
3294
        wm.resize_handles.push(ResizeHandle {
1✔
3295
            id: WindowId::app(1usize),
1✔
3296
            rect: overlapping,
1✔
3297
            edge: ResizeEdge::Left,
1✔
3298
        });
1✔
3299
        wm.resize_handles.push(ResizeHandle {
1✔
3300
            id: WindowId::app(2usize),
1✔
3301
            rect: overlapping,
1✔
3302
            edge: ResizeEdge::Left,
1✔
3303
        });
1✔
3304
        // Background-only handle to ensure uncovered areas still hover
3305
        wm.resize_handles.push(ResizeHandle {
1✔
3306
            id: WindowId::app(1usize),
1✔
3307
            rect: Rect {
1✔
3308
                x: 8,
1✔
3309
                y: 1,
1✔
3310
                width: 1,
1✔
3311
                height: 1,
1✔
3312
            },
1✔
3313
            edge: ResizeEdge::Right,
1✔
3314
        });
1✔
3315
        // Split handle positioned outside any window to verify handle hover only triggers there
3316
        wm.handles.push(SplitHandle {
1✔
3317
            rect: Rect {
1✔
3318
                x: 15,
1✔
3319
                y: 1,
1✔
3320
                width: 1,
1✔
3321
                height: 1,
1✔
3322
            },
1✔
3323
            path: Vec::new(),
1✔
3324
            index: 0,
1✔
3325
            direction: Direction::Horizontal,
1✔
3326
        });
1✔
3327

3328
        wm.hover = Some((2, 1));
1✔
3329
        let (handle_hover, resize_hover) = wm.hover_targets();
1✔
3330
        assert!(
1✔
3331
            handle_hover.is_none(),
1✔
NEW
3332
            "floating window should mask layout handles"
×
3333
        );
3334
        assert_eq!(
1✔
3335
            resize_hover.map(|handle| handle.id),
1✔
3336
            Some(WindowId::app(2usize)),
1✔
NEW
3337
            "topmost window should own the hover"
×
3338
        );
3339

3340
        wm.hover = Some((8, 1));
1✔
3341
        let (_, resize_hover) = wm.hover_targets();
1✔
3342
        assert_eq!(
1✔
3343
            resize_hover.map(|handle| handle.id),
1✔
3344
            Some(WindowId::app(1usize)),
1✔
NEW
3345
            "background window should hover once it is exposed"
×
3346
        );
3347

3348
        wm.hover = Some((15, 1));
1✔
3349
        let (handle_hover, resize_hover) = wm.hover_targets();
1✔
3350
        assert!(resize_hover.is_none());
1✔
3351
        assert!(
1✔
3352
            handle_hover.is_some(),
1✔
NEW
3353
            "layout handles should respond off-window"
×
3354
        );
3355
    }
1✔
3356

3357
    #[test]
3358
    fn system_window_header_drag_detaches_to_floating() {
1✔
3359
        use crossterm::event::{Event, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
3360

3361
        let mut wm = WindowManager::<usize, usize>::new_managed(0);
1✔
3362
        wm.set_panel_visible(false);
1✔
3363
        wm.show_system_window(SystemWindowId::DebugLog);
1✔
3364
        wm.register_managed_layout(Rect {
1✔
3365
            x: 0,
1✔
3366
            y: 0,
1✔
3367
            width: 80,
1✔
3368
            height: 24,
1✔
3369
        });
1✔
3370

3371
        let debug_id = WindowId::system(SystemWindowId::DebugLog);
1✔
3372
        let header_rect = wm
1✔
3373
            .floating_headers
1✔
3374
            .iter()
1✔
3375
            .find(|handle| handle.id == debug_id)
1✔
3376
            .expect("debug header present")
1✔
3377
            .rect;
3378
        assert!(!wm.is_window_floating(debug_id));
1✔
3379

3380
        let down = Event::Mouse(MouseEvent {
1✔
3381
            kind: MouseEventKind::Down(MouseButton::Left),
1✔
3382
            column: header_rect.x,
1✔
3383
            row: header_rect.y,
1✔
3384
            modifiers: KeyModifiers::NONE,
1✔
3385
        });
1✔
3386
        assert!(wm.handle_managed_event(&down));
1✔
3387
        assert!(wm.is_window_floating(debug_id));
1✔
3388
        let start_rect = match wm.floating_rect(debug_id).expect("floating rect present") {
1✔
3389
            crate::window::FloatRectSpec::Absolute(fr) => fr,
1✔
NEW
3390
            _ => panic!("expected absolute rect"),
×
3391
        };
3392

3393
        let drag_col = header_rect.x.saturating_add(2);
1✔
3394
        let drag_row = header_rect.y.saturating_add(1);
1✔
3395
        let drag = Event::Mouse(MouseEvent {
1✔
3396
            kind: MouseEventKind::Drag(MouseButton::Left),
1✔
3397
            column: drag_col,
1✔
3398
            row: drag_row,
1✔
3399
            modifiers: KeyModifiers::NONE,
1✔
3400
        });
1✔
3401
        assert!(wm.handle_managed_event(&drag));
1✔
3402

3403
        let moved = match wm.floating_rect(debug_id).expect("floating rect present") {
1✔
3404
            crate::window::FloatRectSpec::Absolute(fr) => fr,
1✔
NEW
3405
            _ => panic!("expected absolute rect"),
×
3406
        };
3407
        assert_eq!(moved.x, start_rect.x + 2);
1✔
3408
        assert_eq!(moved.y, start_rect.y + 1);
1✔
3409

3410
        let up = Event::Mouse(MouseEvent {
1✔
3411
            kind: MouseEventKind::Up(MouseButton::Left),
1✔
3412
            column: drag_col,
1✔
3413
            row: drag_row,
1✔
3414
            modifiers: KeyModifiers::NONE,
1✔
3415
        });
1✔
3416
        assert!(wm.handle_managed_event(&up));
1✔
3417
        assert!(wm.drag_header.is_none());
1✔
3418
    }
1✔
3419

3420
    #[test]
3421
    fn adjust_event_rebases_app_mouse_coordinates() {
1✔
3422
        use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
3423
        let mut wm = WindowManager::<usize, usize>::new_managed(0);
1✔
3424
        let full = Rect {
1✔
3425
            x: 10,
1✔
3426
            y: 3,
1✔
3427
            width: 12,
1✔
3428
            height: 8,
1✔
3429
        };
1✔
3430
        wm.regions.set(WindowId::app(1usize), full);
1✔
3431

3432
        let global = MouseEvent {
1✔
3433
            kind: MouseEventKind::Down(MouseButton::Left),
1✔
3434
            column: 16,
1✔
3435
            row: 9,
1✔
3436
            modifiers: KeyModifiers::NONE,
1✔
3437
        };
1✔
3438
        let content = wm.region_for_id(WindowId::app(1));
1✔
3439
        let localized = Event::Mouse(MouseEvent {
1✔
3440
            column: global.column.saturating_sub(content.x),
1✔
3441
            row: global.row.saturating_sub(content.y),
1✔
3442
            kind: global.kind,
1✔
3443
            modifiers: global.modifiers,
1✔
3444
        });
1✔
3445

3446
        let rebased = wm.adjust_event_for_window(WindowId::app(1), &localized);
1✔
3447
        let Event::Mouse(result) = rebased else {
1✔
NEW
3448
            panic!("expected mouse event");
×
3449
        };
3450
        assert_eq!(result.column, global.column - full.x);
1✔
3451
        assert_eq!(result.row, global.row - full.y);
1✔
3452
    }
1✔
3453

3454
    #[test]
3455
    fn adjust_event_rebases_system_mouse_coordinates() {
1✔
3456
        use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
3457
        let mut wm = WindowManager::<usize, usize>::new_managed(0);
1✔
3458
        let full = Rect {
1✔
3459
            x: 2,
1✔
3460
            y: 4,
1✔
3461
            width: 15,
1✔
3462
            height: 6,
1✔
3463
        };
1✔
3464
        wm.regions
1✔
3465
            .set(WindowId::system(SystemWindowId::DebugLog), full);
1✔
3466

3467
        let global = MouseEvent {
1✔
3468
            kind: MouseEventKind::Down(MouseButton::Left),
1✔
3469
            column: 7,
1✔
3470
            row: 8,
1✔
3471
            modifiers: KeyModifiers::NONE,
1✔
3472
        };
1✔
3473
        let content = wm.region_for_id(WindowId::system(SystemWindowId::DebugLog));
1✔
3474
        let localized = Event::Mouse(MouseEvent {
1✔
3475
            column: global.column.saturating_sub(content.x),
1✔
3476
            row: global.row.saturating_sub(content.y),
1✔
3477
            kind: global.kind,
1✔
3478
            modifiers: global.modifiers,
1✔
3479
        });
1✔
3480

3481
        let rebased =
1✔
3482
            wm.adjust_event_for_window(WindowId::system(SystemWindowId::DebugLog), &localized);
1✔
3483
        let Event::Mouse(result) = rebased else {
1✔
NEW
3484
            panic!("expected mouse event");
×
3485
        };
3486
        assert_eq!(result.column, global.column - full.x);
1✔
3487
        assert_eq!(result.row, global.row - full.y);
1✔
3488
    }
1✔
3489
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc