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

jzombie / term-wm / 20770181342

07 Jan 2026 04:02AM UTC coverage: 36.037% (+2.5%) from 33.582%
20770181342

Pull #3

github

web-flow
Merge 4d97c195c into ab0b82880
Pull Request #3: General UI improvements

454 of 2495 new or added lines in 26 files covered. (18.2%)

17 existing lines in 7 files now uncovered.

2515 of 6979 relevant lines covered (36.04%)

2.06 hits per line

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

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

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

9
use super::decorator::{DefaultDecorator, HeaderAction, WindowDecorator};
10
use crate::components::{
11
    Component, ConfirmAction, ConfirmOverlay, DebugLogComponent, DialogOverlay, install_panic_hook,
12
    set_global_debug_log,
13
};
14
use crate::layout::floating::*;
15
use crate::layout::{
16
    FloatingPane, InsertPosition, LayoutNode, LayoutPlan, RectSpec, RegionMap, SplitHandle,
17
    TilingLayout, rect_contains, render_handles_masked,
18
};
19
use crate::panel::Panel;
20
use crate::state::AppState;
21
use crate::ui::UiFrame;
22

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

33
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
34
pub enum SystemWindowId {
35
    DebugLog,
36
}
37

38
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
39
pub enum WindowId<R: Copy + Eq + Ord> {
40
    App(R),
41
    System(SystemWindowId),
42
}
43

44
impl<R: Copy + Eq + Ord> WindowId<R> {
45
    fn app(id: R) -> Self {
1✔
46
        Self::App(id)
1✔
47
    }
1✔
48

NEW
49
    fn system(id: SystemWindowId) -> Self {
×
NEW
50
        Self::System(id)
×
NEW
51
    }
×
52

NEW
53
    fn as_app(self) -> Option<R> {
×
NEW
54
        match self {
×
NEW
55
            Self::App(id) => Some(id),
×
NEW
56
            Self::System(_) => None,
×
57
        }
NEW
58
    }
×
59
}
60

61
#[derive(Debug, Clone, Copy, Default)]
62
pub struct ScrollState {
63
    pub offset: usize,
64
    pending: isize,
65
}
66

67
impl ScrollState {
NEW
68
    pub fn reset(&mut self) {
×
NEW
69
        self.offset = 0;
×
NEW
70
        self.pending = 0;
×
NEW
71
    }
×
72

73
    pub fn bump(&mut self, delta: isize) {
1✔
74
        self.pending = self.pending.saturating_add(delta);
1✔
75
    }
1✔
76

77
    pub fn apply(&mut self, total: usize, view: usize) {
3✔
78
        let max_offset = total.saturating_sub(view);
3✔
79
        if self.pending != 0 {
3✔
80
            let delta = self.pending;
1✔
81
            self.pending = 0;
1✔
82
            let next = if delta.is_negative() {
1✔
NEW
83
                self.offset.saturating_sub(delta.unsigned_abs())
×
84
            } else {
85
                self.offset.saturating_add(delta as usize)
1✔
86
            };
87
            self.offset = next.min(max_offset);
1✔
88
        } else if self.offset > max_offset {
2✔
89
            self.offset = max_offset;
1✔
90
        }
1✔
91
    }
3✔
92
}
93

94
#[derive(Debug, Clone, Copy)]
95
pub struct WindowSurface {
96
    pub full: Rect,
97
    pub inner: Rect,
98
}
99

100
#[derive(Debug, Clone, Copy)]
101
pub struct AppWindowDraw<R: Copy + Eq + Ord> {
102
    pub id: R,
103
    pub surface: WindowSurface,
104
    pub focused: bool,
105
}
106

107
#[derive(Debug, Clone)]
108
pub struct FocusRing<T: Copy + Eq> {
109
    order: Vec<T>,
110
    current: T,
111
}
112

113
impl<T: Copy + Eq> FocusRing<T> {
114
    pub fn new(current: T) -> Self {
1✔
115
        Self {
1✔
116
            order: Vec::new(),
1✔
117
            current,
1✔
118
        }
1✔
119
    }
1✔
120

121
    pub fn set_order(&mut self, order: Vec<T>) {
1✔
122
        self.order = order;
1✔
123
    }
1✔
124

125
    pub fn current(&self) -> T {
4✔
126
        self.current
4✔
127
    }
4✔
128

NEW
129
    pub fn set_current(&mut self, current: T) {
×
NEW
130
        self.current = current;
×
NEW
131
    }
×
132

133
    pub fn advance(&mut self, forward: bool) {
3✔
134
        if self.order.is_empty() {
3✔
NEW
135
            return;
×
136
        }
3✔
137
        let idx = self
3✔
138
            .order
3✔
139
            .iter()
3✔
140
            .position(|item| *item == self.current)
6✔
141
            .unwrap_or(0);
3✔
142
        let step = if forward { 1isize } else { -1isize };
3✔
143
        let next = ((idx as isize + step).rem_euclid(self.order.len() as isize)) as usize;
3✔
144
        self.current = self.order[next];
3✔
145
    }
3✔
146
}
147

148
pub struct WindowManager<W: Copy + Eq + Ord, R: Copy + Eq + Ord> {
149
    app_focus: FocusRing<W>,
150
    wm_focus: FocusRing<WindowId<R>>,
151
    windows: BTreeMap<WindowId<R>, Window>,
152
    regions: RegionMap<WindowId<R>>,
153
    scroll: BTreeMap<W, ScrollState>,
154
    handles: Vec<SplitHandle>,
155
    resize_handles: Vec<ResizeHandle<WindowId<R>>>,
156
    floating_headers: Vec<DragHandle<WindowId<R>>>,
157
    managed_draw_order: Vec<WindowId<R>>,
158
    managed_draw_order_app: Vec<R>,
159
    managed_layout: Option<TilingLayout<WindowId<R>>>,
160
    // queue of app ids removed this frame; runner drains via `take_closed_app_windows`
161
    closed_app_windows: Vec<R>,
162
    managed_area: Rect,
163
    panel: Panel<WindowId<R>>,
164
    drag_header: Option<HeaderDrag<WindowId<R>>>,
165
    drag_resize: Option<ResizeDrag<WindowId<R>>>,
166
    hover: Option<(u16, u16)>,
167
    capture_deadline: Option<Instant>,
168
    pending_deadline: Option<Instant>,
169
    state: AppState,
170
    layout_contract: LayoutContract,
171
    wm_overlay_opened_at: Option<Instant>,
172
    esc_passthrough_window: Duration,
173
    wm_overlay: DialogOverlay,
174
    exit_confirm: ConfirmOverlay,
175
    decorator: Box<dyn WindowDecorator>,
176
    floating_resize_offscreen: bool,
177
    z_order: Vec<WindowId<R>>,
178
    drag_snap: Option<(Option<WindowId<R>>, InsertPosition, Rect)>,
179
    debug_log: DebugLogComponent,
180
    debug_log_id: WindowId<R>,
181
    next_window_seq: usize,
182
}
183

184
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185
pub enum WmMenuAction {
186
    CloseMenu,
187
    NewWindow,
188
    ToggleDebugWindow,
189
    ExitUi,
190
    BringFloatingFront,
191
    MinimizeWindow,
192
    MaximizeWindow,
193
    CloseWindow,
194
    ToggleMouseCapture,
195
}
196

197
impl<W: Copy + Eq + Ord, R: Copy + Eq + Ord + std::fmt::Debug> WindowManager<W, R>
198
where
199
    R: PartialEq<W>,
200
{
NEW
201
    fn window_mut(&mut self, id: WindowId<R>) -> &mut Window {
×
NEW
202
        let seq = &mut self.next_window_seq;
×
NEW
203
        self.windows.entry(id).or_insert_with(|| {
×
NEW
204
            let order = *seq;
×
NEW
205
            *seq = order.saturating_add(1);
×
NEW
206
            Window::new(order)
×
NEW
207
        })
×
NEW
208
    }
×
209

NEW
210
    fn window(&self, id: WindowId<R>) -> Option<&Window> {
×
NEW
211
        self.windows.get(&id)
×
NEW
212
    }
×
213

NEW
214
    fn is_minimized(&self, id: WindowId<R>) -> bool {
×
NEW
215
        self.window(id).is_some_and(|window| window.minimized)
×
NEW
216
    }
×
217

NEW
218
    fn set_minimized(&mut self, id: WindowId<R>, value: bool) {
×
NEW
219
        self.window_mut(id).minimized = value;
×
NEW
220
    }
×
221

NEW
222
    fn floating_rect(&self, id: WindowId<R>) -> Option<RectSpec> {
×
NEW
223
        self.window(id).and_then(|window| window.floating_rect)
×
NEW
224
    }
×
225

NEW
226
    fn set_floating_rect(&mut self, id: WindowId<R>, rect: Option<RectSpec>) {
×
NEW
227
        self.window_mut(id).floating_rect = rect;
×
NEW
228
    }
×
229

NEW
230
    fn clear_floating_rect(&mut self, id: WindowId<R>) {
×
NEW
231
        self.window_mut(id).floating_rect = None;
×
NEW
232
    }
×
233

NEW
234
    fn set_prev_floating_rect(&mut self, id: WindowId<R>, rect: Option<RectSpec>) {
×
NEW
235
        self.window_mut(id).prev_floating_rect = rect;
×
NEW
236
    }
×
237

NEW
238
    fn take_prev_floating_rect(&mut self, id: WindowId<R>) -> Option<RectSpec> {
×
NEW
239
        self.window_mut(id).prev_floating_rect.take()
×
NEW
240
    }
×
NEW
241
    fn is_window_floating(&self, id: WindowId<R>) -> bool {
×
NEW
242
        self.window(id).is_some_and(|window| window.is_floating())
×
NEW
243
    }
×
244

NEW
245
    fn window_title(&self, id: WindowId<R>) -> String {
×
NEW
246
        self.window(id)
×
NEW
247
            .map(|window| window.title_or_default(id))
×
NEW
248
            .unwrap_or_else(|| match id {
×
NEW
249
                WindowId::App(app_id) => format!("{:?}", app_id),
×
NEW
250
                WindowId::System(SystemWindowId::DebugLog) => "Debug Log".to_string(),
×
NEW
251
            })
×
NEW
252
    }
×
253

NEW
254
    fn clear_all_floating(&mut self) {
×
NEW
255
        for window in self.windows.values_mut() {
×
NEW
256
            window.floating_rect = None;
×
NEW
257
            window.prev_floating_rect = None;
×
NEW
258
        }
×
NEW
259
    }
×
260

NEW
261
    pub fn new(current: W) -> Self {
×
NEW
262
        Self {
×
NEW
263
            app_focus: FocusRing::new(current),
×
NEW
264
            wm_focus: FocusRing::new(WindowId::system(SystemWindowId::DebugLog)),
×
NEW
265
            windows: BTreeMap::new(),
×
NEW
266
            regions: RegionMap::default(),
×
NEW
267
            scroll: BTreeMap::new(),
×
NEW
268
            handles: Vec::new(),
×
NEW
269
            resize_handles: Vec::new(),
×
NEW
270
            floating_headers: Vec::new(),
×
NEW
271
            managed_draw_order: Vec::new(),
×
NEW
272
            managed_draw_order_app: Vec::new(),
×
NEW
273
            managed_layout: None,
×
NEW
274
            closed_app_windows: Vec::new(),
×
NEW
275
            managed_area: Rect::default(),
×
NEW
276
            panel: Panel::new(),
×
NEW
277
            drag_header: None,
×
NEW
278
            drag_resize: None,
×
NEW
279
            hover: None,
×
NEW
280
            capture_deadline: None,
×
NEW
281
            pending_deadline: None,
×
NEW
282
            state: AppState::new(),
×
NEW
283
            layout_contract: LayoutContract::AppManaged,
×
NEW
284
            wm_overlay_opened_at: None,
×
NEW
285
            esc_passthrough_window: esc_passthrough_window_default(),
×
NEW
286
            wm_overlay: DialogOverlay::new(),
×
NEW
287
            exit_confirm: ConfirmOverlay::new(),
×
NEW
288
            decorator: Box::new(DefaultDecorator),
×
NEW
289
            floating_resize_offscreen: true,
×
NEW
290
            z_order: Vec::new(),
×
NEW
291
            drag_snap: None,
×
NEW
292
            debug_log: {
×
NEW
293
                let (component, handle) = DebugLogComponent::new_default();
×
NEW
294
                let _ = set_global_debug_log(handle);
×
NEW
295
                install_panic_hook();
×
NEW
296
                component
×
NEW
297
            },
×
NEW
298
            debug_log_id: WindowId::system(SystemWindowId::DebugLog),
×
NEW
299
            next_window_seq: 0,
×
NEW
300
        }
×
NEW
301
    }
×
302

NEW
303
    pub fn new_managed(current: W) -> Self {
×
NEW
304
        let mut manager = Self::new(current);
×
NEW
305
        manager.layout_contract = LayoutContract::WindowManaged;
×
NEW
306
        manager
×
NEW
307
    }
×
308

NEW
309
    pub fn set_layout_contract(&mut self, contract: LayoutContract) {
×
NEW
310
        self.layout_contract = contract;
×
NEW
311
    }
×
312

313
    /// Drain and return any app ids whose windows were closed since the last call.
NEW
314
    pub fn take_closed_app_windows(&mut self) -> Vec<R> {
×
NEW
315
        std::mem::take(&mut self.closed_app_windows)
×
NEW
316
    }
×
317

NEW
318
    pub fn layout_contract(&self) -> LayoutContract {
×
NEW
319
        self.layout_contract
×
NEW
320
    }
×
321

NEW
322
    pub fn set_floating_resize_offscreen(&mut self, enabled: bool) {
×
NEW
323
        self.floating_resize_offscreen = enabled;
×
NEW
324
    }
×
325

NEW
326
    pub fn floating_resize_offscreen(&self) -> bool {
×
NEW
327
        self.floating_resize_offscreen
×
NEW
328
    }
×
329

NEW
330
    pub fn begin_frame(&mut self) {
×
NEW
331
        self.regions = RegionMap::default();
×
NEW
332
        self.handles.clear();
×
NEW
333
        self.resize_handles.clear();
×
NEW
334
        self.floating_headers.clear();
×
NEW
335
        self.managed_draw_order.clear();
×
NEW
336
        self.managed_draw_order_app.clear();
×
NEW
337
        self.panel.begin_frame();
×
338
        // If a panic occurred earlier, ensure the debug log is shown and focused.
NEW
339
        if crate::components::take_panic_pending() {
×
NEW
340
            self.state.set_debug_log_visible(true);
×
NEW
341
            self.ensure_debug_log_in_layout();
×
NEW
342
            self.focus_window_id(self.debug_log_id);
×
NEW
343
        }
×
NEW
344
        if self.layout_contract == LayoutContract::AppManaged {
×
NEW
345
            self.clear_capture();
×
NEW
346
        } else {
×
NEW
347
            // Refresh deadlines so overlay badges can expire without events.
×
NEW
348
            self.refresh_capture();
×
NEW
349
        }
×
NEW
350
    }
×
351

NEW
352
    pub fn arm_capture(&mut self, timeout: Duration) {
×
NEW
353
        self.capture_deadline = Some(Instant::now() + timeout);
×
NEW
354
        self.pending_deadline = None;
×
NEW
355
    }
×
356

NEW
357
    pub fn arm_pending(&mut self, timeout: Duration) {
×
358
        // Shows an "Esc pending" badge while waiting for the chord.
NEW
359
        self.pending_deadline = Some(Instant::now() + timeout);
×
NEW
360
    }
×
361

NEW
362
    pub fn clear_capture(&mut self) {
×
NEW
363
        self.capture_deadline = None;
×
NEW
364
        self.pending_deadline = None;
×
NEW
365
        self.state.set_overlay_visible(false);
×
NEW
366
        self.wm_overlay_opened_at = None;
×
NEW
367
        self.wm_overlay.set_visible(false);
×
NEW
368
        self.state.set_wm_menu_selected(0);
×
NEW
369
    }
×
370

NEW
371
    pub fn capture_active(&mut self) -> bool {
×
NEW
372
        if !self.state.mouse_capture_enabled() {
×
NEW
373
            return false;
×
NEW
374
        }
×
NEW
375
        if self.layout_contract == LayoutContract::WindowManaged && self.state.overlay_visible() {
×
NEW
376
            return true;
×
NEW
377
        }
×
NEW
378
        self.refresh_capture();
×
NEW
379
        self.capture_deadline.is_some()
×
NEW
380
    }
×
381

NEW
382
    pub fn mouse_capture_enabled(&self) -> bool {
×
NEW
383
        self.state.mouse_capture_enabled()
×
NEW
384
    }
×
385

NEW
386
    pub fn set_mouse_capture_enabled(&mut self, enabled: bool) {
×
NEW
387
        self.state.set_mouse_capture_enabled(enabled);
×
NEW
388
        if !self.state.mouse_capture_enabled() {
×
NEW
389
            self.clear_capture();
×
NEW
390
        }
×
NEW
391
    }
×
392

NEW
393
    pub fn toggle_mouse_capture(&mut self) {
×
NEW
394
        self.state.toggle_mouse_capture();
×
NEW
395
        if !self.state.mouse_capture_enabled() {
×
NEW
396
            self.clear_capture();
×
NEW
397
        }
×
NEW
398
    }
×
399

NEW
400
    pub fn take_mouse_capture_change(&mut self) -> Option<bool> {
×
NEW
401
        self.state.take_mouse_capture_change()
×
NEW
402
    }
×
403

NEW
404
    fn refresh_capture(&mut self) {
×
NEW
405
        if let Some(deadline) = self.capture_deadline
×
NEW
406
            && Instant::now() > deadline
×
NEW
407
        {
×
NEW
408
            self.capture_deadline = None;
×
NEW
409
        }
×
NEW
410
        if let Some(deadline) = self.pending_deadline
×
NEW
411
            && Instant::now() > deadline
×
NEW
412
        {
×
NEW
413
            self.pending_deadline = None;
×
NEW
414
        }
×
NEW
415
    }
×
416

NEW
417
    pub fn open_wm_overlay(&mut self) {
×
NEW
418
        self.state.set_overlay_visible(true);
×
NEW
419
        self.wm_overlay_opened_at = Some(Instant::now());
×
NEW
420
        self.wm_overlay.set_visible(true);
×
NEW
421
        self.state.set_wm_menu_selected(0);
×
NEW
422
    }
×
423

NEW
424
    pub fn close_wm_overlay(&mut self) {
×
NEW
425
        self.state.set_overlay_visible(false);
×
NEW
426
        self.wm_overlay_opened_at = None;
×
NEW
427
        self.wm_overlay.set_visible(false);
×
NEW
428
        self.state.set_wm_menu_selected(0);
×
NEW
429
    }
×
430

NEW
431
    pub fn open_exit_confirm(&mut self) {
×
NEW
432
        self.exit_confirm.open(
×
NEW
433
            "Exit App",
×
NEW
434
            "Exit the application?\nUnsaved changes will be lost.",
×
435
        );
NEW
436
    }
×
437

NEW
438
    pub fn close_exit_confirm(&mut self) {
×
NEW
439
        self.exit_confirm.close();
×
NEW
440
    }
×
441

NEW
442
    pub fn exit_confirm_visible(&self) -> bool {
×
NEW
443
        self.exit_confirm.visible()
×
NEW
444
    }
×
445

NEW
446
    pub fn wm_overlay_visible(&self) -> bool {
×
NEW
447
        self.state.overlay_visible()
×
NEW
448
    }
×
449

NEW
450
    pub fn toggle_debug_window(&mut self) {
×
NEW
451
        self.state.toggle_debug_log_visible();
×
NEW
452
        if self.state.debug_log_visible() {
×
NEW
453
            self.ensure_debug_log_in_layout();
×
NEW
454
            self.focus_window_id(self.debug_log_id);
×
NEW
455
        } else {
×
NEW
456
            self.remove_debug_log_from_layout();
×
NEW
457
            if self.wm_focus.current() == self.debug_log_id {
×
NEW
458
                self.select_fallback_focus();
×
NEW
459
            }
×
460
        }
NEW
461
    }
×
462

NEW
463
    fn ensure_debug_log_in_layout(&mut self) {
×
NEW
464
        if self.layout_contract != LayoutContract::WindowManaged {
×
NEW
465
            return;
×
NEW
466
        }
×
NEW
467
        if self.layout_contains(self.debug_log_id) {
×
NEW
468
            return;
×
NEW
469
        }
×
NEW
470
        if self.managed_layout.is_none() {
×
NEW
471
            self.managed_layout = Some(TilingLayout::new(LayoutNode::leaf(self.debug_log_id)));
×
NEW
472
            return;
×
NEW
473
        }
×
NEW
474
        let _ = self.tile_window_id(self.debug_log_id);
×
NEW
475
    }
×
476

NEW
477
    fn remove_debug_log_from_layout(&mut self) {
×
NEW
478
        self.clear_floating_rect(self.debug_log_id);
×
NEW
479
        if let Some(layout) = &mut self.managed_layout {
×
NEW
480
            if matches!(layout.root(), LayoutNode::Leaf(id) if *id == self.debug_log_id) {
×
NEW
481
                self.managed_layout = None;
×
NEW
482
            } else {
×
NEW
483
                layout.root_mut().remove_leaf(self.debug_log_id);
×
NEW
484
            }
×
NEW
485
        }
×
NEW
486
        self.z_order.retain(|id| *id != self.debug_log_id);
×
NEW
487
    }
×
488

NEW
489
    pub fn esc_passthrough_active(&self) -> bool {
×
NEW
490
        self.esc_passthrough_remaining().is_some()
×
NEW
491
    }
×
492

NEW
493
    pub fn esc_passthrough_remaining(&self) -> Option<Duration> {
×
NEW
494
        if !self.wm_overlay_visible() {
×
NEW
495
            return None;
×
NEW
496
        }
×
NEW
497
        let opened_at = self.wm_overlay_opened_at?;
×
NEW
498
        let elapsed = opened_at.elapsed();
×
NEW
499
        if elapsed >= self.esc_passthrough_window {
×
NEW
500
            return None;
×
NEW
501
        }
×
NEW
502
        Some(self.esc_passthrough_window.saturating_sub(elapsed))
×
NEW
503
    }
×
504

NEW
505
    pub fn focus(&self) -> W {
×
NEW
506
        self.app_focus.current()
×
NEW
507
    }
×
508

NEW
509
    pub fn set_focus(&mut self, focus: W) {
×
NEW
510
        self.app_focus.set_current(focus);
×
NEW
511
    }
×
512

NEW
513
    pub fn set_focus_order(&mut self, order: Vec<W>) {
×
NEW
514
        self.app_focus.set_order(order);
×
NEW
515
        if !self.app_focus.order.is_empty()
×
NEW
516
            && !self.app_focus.order.contains(&self.app_focus.current)
×
NEW
517
        {
×
NEW
518
            self.app_focus.current = self.app_focus.order[0];
×
NEW
519
        }
×
NEW
520
    }
×
521

NEW
522
    pub fn advance_focus(&mut self, forward: bool) {
×
NEW
523
        self.app_focus.advance(forward);
×
NEW
524
    }
×
525

NEW
526
    pub fn wm_focus(&self) -> WindowId<R> {
×
NEW
527
        self.wm_focus.current()
×
NEW
528
    }
×
529

NEW
530
    pub fn wm_focus_app(&self) -> Option<R> {
×
NEW
531
        self.wm_focus.current().as_app()
×
NEW
532
    }
×
533

NEW
534
    fn set_wm_focus(&mut self, focus: WindowId<R>) {
×
NEW
535
        self.wm_focus.set_current(focus);
×
NEW
536
        if let Some(app_id) = focus.as_app()
×
NEW
537
            && let Some(app_focus) = self.focus_for_region(app_id)
×
NEW
538
        {
×
NEW
539
            self.app_focus.set_current(app_focus);
×
NEW
540
        }
×
NEW
541
    }
×
542

543
    /// Unified focus API: set WM focus, bring the window to front, update draw order,
544
    /// and sync app-level focus if applicable.
NEW
545
    fn focus_window_id(&mut self, id: WindowId<R>) {
×
NEW
546
        self.set_wm_focus(id);
×
NEW
547
        self.bring_to_front_id(id);
×
NEW
548
        self.managed_draw_order = self.z_order.clone();
×
NEW
549
        if let Some(app_id) = id.as_app()
×
NEW
550
            && let Some(app_focus) = self.focus_for_region(app_id)
×
NEW
551
        {
×
NEW
552
            self.app_focus.set_current(app_focus);
×
NEW
553
        }
×
NEW
554
    }
×
555

NEW
556
    fn set_wm_focus_order(&mut self, order: Vec<WindowId<R>>) {
×
NEW
557
        self.wm_focus.set_order(order);
×
NEW
558
        if !self.wm_focus.order.is_empty() && !self.wm_focus.order.contains(&self.wm_focus.current)
×
NEW
559
        {
×
NEW
560
            self.wm_focus.current = self.wm_focus.order[0];
×
NEW
561
        }
×
NEW
562
    }
×
563

NEW
564
    fn rebuild_wm_focus_ring(&mut self, active_ids: &[WindowId<R>]) {
×
NEW
565
        if active_ids.is_empty() {
×
NEW
566
            self.set_wm_focus_order(Vec::new());
×
NEW
567
            return;
×
NEW
568
        }
×
NEW
569
        let active: BTreeSet<_> = active_ids.iter().copied().collect();
×
NEW
570
        let mut next_order: Vec<WindowId<R>> = Vec::with_capacity(active.len());
×
NEW
571
        let mut seen: BTreeSet<WindowId<R>> = BTreeSet::new();
×
572

NEW
573
        for &id in &self.wm_focus.order {
×
NEW
574
            if active.contains(&id) && seen.insert(id) {
×
NEW
575
                next_order.push(id);
×
NEW
576
            }
×
577
        }
NEW
578
        for &id in active_ids {
×
NEW
579
            if seen.insert(id) {
×
NEW
580
                next_order.push(id);
×
NEW
581
            }
×
582
        }
NEW
583
        self.set_wm_focus_order(next_order);
×
NEW
584
    }
×
585

NEW
586
    fn advance_wm_focus(&mut self, forward: bool) {
×
NEW
587
        if self.wm_focus.order.is_empty() {
×
NEW
588
            return;
×
NEW
589
        }
×
NEW
590
        self.wm_focus.advance(forward);
×
NEW
591
        let focused = self.wm_focus.current();
×
NEW
592
        self.focus_window_id(focused);
×
NEW
593
    }
×
594

NEW
595
    fn select_fallback_focus(&mut self) {
×
NEW
596
        if let Some(fallback) = self.wm_focus.order.first().copied() {
×
NEW
597
            self.set_wm_focus(fallback);
×
NEW
598
        }
×
NEW
599
    }
×
600

NEW
601
    pub fn scroll(&self, id: W) -> ScrollState {
×
NEW
602
        self.scroll.get(&id).copied().unwrap_or_default()
×
NEW
603
    }
×
604

NEW
605
    pub fn scroll_mut(&mut self, id: W) -> &mut ScrollState {
×
NEW
606
        self.scroll.entry(id).or_default()
×
NEW
607
    }
×
608

NEW
609
    pub fn scroll_offset(&self, id: W) -> usize {
×
NEW
610
        self.scroll(id).offset
×
NEW
611
    }
×
612

NEW
613
    pub fn reset_scroll(&mut self, id: W) {
×
NEW
614
        self.scroll_mut(id).reset();
×
NEW
615
    }
×
616

NEW
617
    pub fn apply_scroll(&mut self, id: W, total: usize, view: usize) {
×
NEW
618
        self.scroll_mut(id).apply(total, view);
×
NEW
619
    }
×
620

NEW
621
    pub fn set_region(&mut self, id: R, rect: Rect) {
×
NEW
622
        self.regions.set(WindowId::app(id), rect);
×
NEW
623
    }
×
624

NEW
625
    pub fn full_region(&self, id: R) -> Rect {
×
NEW
626
        self.full_region_for_id(WindowId::app(id))
×
NEW
627
    }
×
628

NEW
629
    pub fn region(&self, id: R) -> Rect {
×
NEW
630
        self.region_for_id(WindowId::app(id))
×
NEW
631
    }
×
632

NEW
633
    fn full_region_for_id(&self, id: WindowId<R>) -> Rect {
×
NEW
634
        self.regions.get(id).unwrap_or_default()
×
NEW
635
    }
×
636

NEW
637
    fn region_for_id(&self, id: WindowId<R>) -> Rect {
×
NEW
638
        let rect = self.regions.get(id).unwrap_or_default();
×
NEW
639
        if self.layout_contract == LayoutContract::WindowManaged {
×
NEW
640
            let area = if self.floating_resize_offscreen {
×
641
                // If we allow off-screen resizing/dragging, we shouldn't clamp the
642
                // logical region to the bounds, otherwise the PTY will be resized
643
                // (shrinking the content) instead of just being clipped during render.
NEW
644
                rect
×
645
            } else {
NEW
646
                clamp_rect(rect, self.managed_area)
×
647
            };
NEW
648
            if area.width < 3 || area.height < 4 {
×
NEW
649
                return Rect::default();
×
NEW
650
            }
×
NEW
651
            Rect {
×
NEW
652
                x: area.x + 1,
×
NEW
653
                y: area.y + 2,
×
NEW
654
                width: area.width.saturating_sub(2),
×
NEW
655
                height: area.height.saturating_sub(3),
×
NEW
656
            }
×
657
        } else {
NEW
658
            rect
×
659
        }
NEW
660
    }
×
661

NEW
662
    pub fn set_regions_from_layout(&mut self, layout: &LayoutNode<R>, area: Rect) {
×
NEW
663
        self.regions = RegionMap::default();
×
NEW
664
        for (id, rect) in layout.layout(area) {
×
NEW
665
            self.regions.set(WindowId::app(id), rect);
×
NEW
666
        }
×
NEW
667
    }
×
668

NEW
669
    pub fn register_tiling_layout(&mut self, layout: &TilingLayout<R>, area: Rect) {
×
NEW
670
        let (regions, handles) = layout.root().layout_with_handles(area);
×
NEW
671
        for (id, rect) in regions {
×
NEW
672
            self.regions.set(WindowId::app(id), rect);
×
NEW
673
        }
×
NEW
674
        self.handles.extend(handles);
×
NEW
675
    }
×
676

NEW
677
    pub fn set_managed_layout(&mut self, layout: TilingLayout<R>) {
×
NEW
678
        self.managed_layout = Some(TilingLayout::new(map_layout_node(layout.root())));
×
NEW
679
        self.clear_all_floating();
×
NEW
680
        if self.state.debug_log_visible() {
×
NEW
681
            self.ensure_debug_log_in_layout();
×
NEW
682
        }
×
NEW
683
    }
×
684

NEW
685
    pub fn set_panel_visible(&mut self, visible: bool) {
×
NEW
686
        self.panel.set_visible(visible);
×
NEW
687
    }
×
688

NEW
689
    pub fn set_panel_height(&mut self, height: u16) {
×
NEW
690
        self.panel.set_height(height);
×
NEW
691
    }
×
692

NEW
693
    pub fn register_managed_layout(&mut self, area: Rect) {
×
NEW
694
        let (_, managed_area) = self.panel.split_area(self.panel_active(), area);
×
NEW
695
        self.managed_area = managed_area;
×
NEW
696
        self.clamp_floating_to_bounds();
×
NEW
697
        if self.state.debug_log_visible() {
×
NEW
698
            self.ensure_debug_log_in_layout();
×
NEW
699
        }
×
NEW
700
        let z_snapshot = self.z_order.clone();
×
NEW
701
        let mut active_ids: Vec<WindowId<R>> = Vec::new();
×
702

NEW
703
        if let Some(layout) = self.managed_layout.as_ref() {
×
NEW
704
            let (regions, handles) = layout.root().layout_with_handles(self.managed_area);
×
NEW
705
            for (id, rect) in &regions {
×
NEW
706
                if self.is_window_floating(*id) {
×
NEW
707
                    continue;
×
NEW
708
                }
×
709
                // skip minimized windows
NEW
710
                if self.is_minimized(*id) {
×
NEW
711
                    continue;
×
NEW
712
                }
×
NEW
713
                self.regions.set(*id, *rect);
×
NEW
714
                if let Some(header) = floating_header_for_region(*id, *rect, self.managed_area) {
×
NEW
715
                    self.floating_headers.push(header);
×
NEW
716
                }
×
NEW
717
                active_ids.push(*id);
×
718
            }
NEW
719
            let filtered_handles: Vec<SplitHandle> = handles
×
NEW
720
                .into_iter()
×
NEW
721
                .filter(|handle| {
×
NEW
722
                    let Some(LayoutNode::Split { children, .. }) =
×
NEW
723
                        layout.root().node_at_path(&handle.path)
×
724
                    else {
NEW
725
                        return false;
×
726
                    };
NEW
727
                    let left = children.get(handle.index);
×
NEW
728
                    let right = children.get(handle.index + 1);
×
NEW
729
                    left.is_some_and(|node| node.subtree_any(|id| !self.is_window_floating(id)))
×
NEW
730
                        || right
×
NEW
731
                            .is_some_and(|node| node.subtree_any(|id| !self.is_window_floating(id)))
×
NEW
732
                })
×
NEW
733
                .collect();
×
NEW
734
            self.handles.extend(filtered_handles);
×
NEW
735
        }
×
NEW
736
        let mut floating_ids: Vec<WindowId<R>> = self
×
NEW
737
            .windows
×
NEW
738
            .iter()
×
NEW
739
            .filter_map(|(&id, window)| {
×
NEW
740
                if window.is_floating() && !window.minimized {
×
NEW
741
                    Some(id)
×
742
                } else {
NEW
743
                    None
×
744
                }
NEW
745
            })
×
NEW
746
            .collect();
×
NEW
747
        floating_ids.sort_by_key(|id| {
×
NEW
748
            z_snapshot
×
NEW
749
                .iter()
×
NEW
750
                .position(|existing| existing == id)
×
NEW
751
                .unwrap_or(usize::MAX)
×
NEW
752
        });
×
NEW
753
        for floating_id in floating_ids {
×
NEW
754
            let Some(spec) = self.floating_rect(floating_id) else {
×
NEW
755
                continue;
×
756
            };
NEW
757
            let rect = spec.resolve(self.managed_area);
×
NEW
758
            self.regions.set(floating_id, rect);
×
NEW
759
            self.resize_handles.extend(resize_handles_for_region(
×
NEW
760
                floating_id,
×
NEW
761
                rect,
×
NEW
762
                self.managed_area,
×
763
            ));
NEW
764
            if let Some(header) = floating_header_for_region(floating_id, rect, self.managed_area) {
×
NEW
765
                self.floating_headers.push(header);
×
NEW
766
            }
×
NEW
767
            active_ids.push(floating_id);
×
768
        }
769

NEW
770
        self.z_order.retain(|id| active_ids.contains(id));
×
NEW
771
        for &id in &active_ids {
×
NEW
772
            if !self.z_order.contains(&id) {
×
NEW
773
                self.z_order.push(id);
×
NEW
774
            }
×
775
        }
NEW
776
        self.managed_draw_order = self.z_order.clone();
×
NEW
777
        self.managed_draw_order_app = self
×
NEW
778
            .managed_draw_order
×
NEW
779
            .iter()
×
NEW
780
            .filter_map(|id| id.as_app())
×
NEW
781
            .collect();
×
NEW
782
        self.rebuild_wm_focus_ring(&active_ids);
×
783
        // Ensure the current focus is actually on top and synced after layout registration.
784
        // Only bring the focused window to front if it's not already the topmost window
785
        // to avoid repeatedly forcing focus every frame.
NEW
786
        let focused = self.wm_focus.current();
×
NEW
787
        if self.z_order.last().copied() != Some(focused) {
×
NEW
788
            self.focus_window_id(focused);
×
NEW
789
        }
×
NEW
790
    }
×
791

NEW
792
    pub fn managed_draw_order(&self) -> &[R] {
×
NEW
793
        &self.managed_draw_order_app
×
NEW
794
    }
×
795

796
    /// Build a stable display order for UI components.
797
    /// By default this returns the canonical creation order filtered to active managed windows,
798
    /// appending any windows that are active but not yet present in the canonical ordering.
NEW
799
    pub fn build_display_order(&self) -> Vec<WindowId<R>> {
×
NEW
800
        let mut ordered: Vec<(WindowId<R>, &Window)> = self
×
NEW
801
            .windows
×
NEW
802
            .iter()
×
NEW
803
            .map(|(id, window)| (*id, window))
×
NEW
804
            .collect();
×
NEW
805
        ordered.sort_by_key(|(_, window)| window.creation_order);
×
806

NEW
807
        let mut out: Vec<WindowId<R>> = Vec::new();
×
NEW
808
        for (id, window) in ordered {
×
NEW
809
            if self.managed_draw_order.contains(&id) || window.minimized {
×
NEW
810
                out.push(id);
×
NEW
811
            }
×
812
        }
NEW
813
        for id in &self.managed_draw_order {
×
NEW
814
            if !out.contains(id) {
×
NEW
815
                out.push(*id);
×
NEW
816
            }
×
817
        }
NEW
818
        out
×
NEW
819
    }
×
820

821
    /// Set a user-visible title for an app window. This overrides the default
822
    /// Debug-derived title displayed for the given `id`.
NEW
823
    pub fn set_window_title(&mut self, id: R, title: impl Into<String>) {
×
NEW
824
        self.window_mut(WindowId::app(id)).title = Some(title.into());
×
NEW
825
    }
×
826

NEW
827
    pub fn handle_managed_event(&mut self, event: &Event) -> bool {
×
NEW
828
        if self.layout_contract != LayoutContract::WindowManaged {
×
NEW
829
            return false;
×
NEW
830
        }
×
NEW
831
        if let Event::Mouse(mouse) = event
×
NEW
832
            && self.panel_active()
×
NEW
833
            && rect_contains(self.panel.area(), mouse.column, mouse.row)
×
834
        {
NEW
835
            if self.panel.hit_test_menu(event) {
×
NEW
836
                if self.wm_overlay_visible() {
×
NEW
837
                    self.close_wm_overlay();
×
NEW
838
                } else {
×
NEW
839
                    self.open_wm_overlay();
×
NEW
840
                }
×
NEW
841
            } else if self.panel.hit_test_mouse_capture(event) {
×
NEW
842
                self.toggle_mouse_capture();
×
NEW
843
            } else if let Some(id) = self.panel.hit_test_window(event) {
×
844
                // If the clicked window is minimized, restore it first so it appears
845
                // in the layout; otherwise just focus and bring to front.
NEW
846
                if self.is_minimized(id) {
×
NEW
847
                    self.restore_minimized(id);
×
NEW
848
                }
×
NEW
849
                self.focus_window_id(id);
×
NEW
850
            }
×
NEW
851
            return true;
×
NEW
852
        }
×
NEW
853
        if self.state.debug_log_visible() {
×
NEW
854
            match event {
×
NEW
855
                Event::Mouse(mouse) => {
×
NEW
856
                    let rect = self.full_region_for_id(self.debug_log_id);
×
NEW
857
                    if rect_contains(rect, mouse.column, mouse.row) {
×
NEW
858
                        if matches!(mouse.kind, MouseEventKind::Down(_)) {
×
NEW
859
                            self.focus_window_id(self.debug_log_id);
×
NEW
860
                        }
×
NEW
861
                        if self.debug_log.handle_event(event) {
×
NEW
862
                            return true;
×
NEW
863
                        }
×
NEW
864
                    } else if matches!(mouse.kind, MouseEventKind::Down(_))
×
NEW
865
                        && self.wm_focus.current() == self.debug_log_id
×
NEW
866
                    {
×
NEW
867
                        self.select_fallback_focus();
×
NEW
868
                    }
×
869
                }
NEW
870
                Event::Key(_) if self.wm_focus.current() == self.debug_log_id => {
×
NEW
871
                    if self.debug_log.handle_event(event) {
×
NEW
872
                        return true;
×
NEW
873
                    }
×
874
                }
NEW
875
                _ => {}
×
876
            }
NEW
877
        }
×
NEW
878
        if let Event::Mouse(mouse) = event {
×
NEW
879
            self.hover = Some((mouse.column, mouse.row));
×
NEW
880
        }
×
NEW
881
        if self.handle_resize_event(event) {
×
NEW
882
            return true;
×
NEW
883
        }
×
NEW
884
        if self.handle_header_drag_event(event) {
×
NEW
885
            return true;
×
NEW
886
        }
×
NEW
887
        if let Some(layout) = self.managed_layout.as_mut() {
×
NEW
888
            return layout.handle_event(event, self.managed_area);
×
NEW
889
        }
×
NEW
890
        false
×
NEW
891
    }
×
892

NEW
893
    pub fn minimize_window(&mut self, id: WindowId<R>) {
×
NEW
894
        if self.is_minimized(id) {
×
NEW
895
            return;
×
NEW
896
        }
×
897
        // remove from floating and regions; keep canonical order so it can be restored
NEW
898
        self.clear_floating_rect(id);
×
NEW
899
        self.z_order.retain(|x| *x != id);
×
NEW
900
        self.managed_draw_order.retain(|x| *x != id);
×
NEW
901
        self.set_minimized(id, true);
×
902
        // ensure focus moves if needed
NEW
903
        if self.wm_focus.current() == id {
×
NEW
904
            self.select_fallback_focus();
×
NEW
905
        }
×
NEW
906
    }
×
907

NEW
908
    pub fn restore_minimized(&mut self, id: WindowId<R>) {
×
NEW
909
        if !self.is_minimized(id) {
×
NEW
910
            return;
×
NEW
911
        }
×
NEW
912
        self.set_minimized(id, false);
×
913
        // reinstall into z_order and draw order
NEW
914
        if !self.z_order.contains(&id) {
×
NEW
915
            self.z_order.push(id);
×
NEW
916
        }
×
NEW
917
        if !self.managed_draw_order.contains(&id) {
×
NEW
918
            self.managed_draw_order.push(id);
×
NEW
919
        }
×
NEW
920
    }
×
921

NEW
922
    pub fn toggle_maximize(&mut self, id: WindowId<R>) {
×
923
        // maximize toggles the floating rect to full managed_area
NEW
924
        let full = RectSpec::Absolute(self.managed_area);
×
NEW
925
        if let Some(current) = self.floating_rect(id) {
×
NEW
926
            if current == full {
×
NEW
927
                if let Some(prev) = self.take_prev_floating_rect(id) {
×
NEW
928
                    self.set_floating_rect(id, Some(prev));
×
NEW
929
                }
×
NEW
930
            } else {
×
NEW
931
                self.set_prev_floating_rect(id, Some(current));
×
NEW
932
                self.set_floating_rect(id, Some(full));
×
NEW
933
            }
×
NEW
934
            self.bring_floating_to_front_id(id);
×
NEW
935
            return;
×
NEW
936
        }
×
937
        // not floating: add floating pane covering full area
938
        // Save the current region (if available) so we can restore later.
NEW
939
        let prev_rect = if let Some(rect) = self.regions.get(id) {
×
NEW
940
            RectSpec::Absolute(rect)
×
941
        } else {
NEW
942
            RectSpec::Percent {
×
NEW
943
                x: 0,
×
NEW
944
                y: 0,
×
NEW
945
                width: 100,
×
NEW
946
                height: 100,
×
NEW
947
            }
×
948
        };
NEW
949
        self.set_prev_floating_rect(id, Some(prev_rect));
×
NEW
950
        self.set_floating_rect(id, Some(full));
×
NEW
951
        self.bring_floating_to_front_id(id);
×
NEW
952
    }
×
953

NEW
954
    pub fn close_window(&mut self, id: WindowId<R>) {
×
NEW
955
        if id == self.debug_log_id {
×
NEW
956
            self.toggle_debug_window();
×
NEW
957
            return;
×
NEW
958
        }
×
959

960
        // Remove references to this window
NEW
961
        self.clear_floating_rect(id);
×
NEW
962
        self.z_order.retain(|x| *x != id);
×
NEW
963
        self.managed_draw_order.retain(|x| *x != id);
×
NEW
964
        self.set_minimized(id, false);
×
NEW
965
        self.regions.remove(id);
×
966
        // update focus
NEW
967
        if self.wm_focus.current() == id {
×
NEW
968
            self.select_fallback_focus();
×
NEW
969
        }
×
970
        // If this window corresponded to an app id, enqueue it for the runner to drain.
NEW
971
        if let Some(app_id) = id.as_app() {
×
NEW
972
            self.closed_app_windows.push(app_id);
×
NEW
973
        }
×
NEW
974
    }
×
975

NEW
976
    fn handle_header_drag_event(&mut self, event: &Event) -> bool {
×
977
        use crossterm::event::MouseEventKind;
NEW
978
        let Event::Mouse(mouse) = event else {
×
NEW
979
            return false;
×
980
        };
NEW
981
        match mouse.kind {
×
982
            MouseEventKind::Down(_) => {
983
                // Check if the mouse is blocked by a window above
NEW
984
                let topmost_hit = if self.layout_contract == LayoutContract::WindowManaged
×
NEW
985
                    && !self.managed_draw_order.is_empty()
×
986
                {
NEW
987
                    self.hit_test_region_topmost(mouse.column, mouse.row, &self.managed_draw_order)
×
988
                } else {
NEW
989
                    None
×
990
                };
991

NEW
992
                if let Some(header) = self
×
NEW
993
                    .floating_headers
×
NEW
994
                    .iter()
×
NEW
995
                    .rev()
×
NEW
996
                    .find(|handle| rect_contains(handle.rect, mouse.column, mouse.row))
×
NEW
997
                    .copied()
×
998
                {
999
                    // If we hit a window body that is NOT the owner of this header,
1000
                    // then the header is obscured.
NEW
1001
                    if let Some(hit_id) = topmost_hit
×
NEW
1002
                        && hit_id != header.id
×
1003
                    {
NEW
1004
                        return false;
×
NEW
1005
                    }
×
1006

NEW
1007
                    let rect = self.full_region_for_id(header.id);
×
NEW
1008
                    match self.decorator.hit_test(rect, mouse.column, mouse.row) {
×
1009
                        HeaderAction::Minimize => {
NEW
1010
                            self.minimize_window(header.id);
×
NEW
1011
                            return true;
×
1012
                        }
1013
                        HeaderAction::Maximize => {
NEW
1014
                            self.toggle_maximize(header.id);
×
NEW
1015
                            return true;
×
1016
                        }
1017
                        HeaderAction::Close => {
NEW
1018
                            self.close_window(header.id);
×
NEW
1019
                            return true;
×
1020
                        }
NEW
1021
                        HeaderAction::Drag => {
×
NEW
1022
                            // Proceed to drag below
×
NEW
1023
                        }
×
NEW
1024
                        HeaderAction::None => {
×
NEW
1025
                            // Should not happen as we already checked rect contains
×
NEW
1026
                        }
×
1027
                    }
1028

1029
                    // Standard floating drag start
NEW
1030
                    if self.is_window_floating(header.id) {
×
NEW
1031
                        self.bring_floating_to_front_id(header.id);
×
NEW
1032
                    } else {
×
NEW
1033
                        // If Tiled: We detach immediately to floating (responsive drag).
×
NEW
1034
                        // Keep the tiling slot reserved so the sibling doesn't expand to full screen.
×
NEW
1035
                        let _ = self.detach_to_floating(header.id, rect);
×
NEW
1036
                    }
×
1037

NEW
1038
                    self.drag_header = Some(HeaderDrag {
×
NEW
1039
                        id: header.id,
×
NEW
1040
                        offset_x: mouse.column.saturating_sub(rect.x),
×
NEW
1041
                        offset_y: mouse.row.saturating_sub(rect.y),
×
NEW
1042
                        start_x: mouse.column,
×
NEW
1043
                        start_y: mouse.row,
×
NEW
1044
                    });
×
NEW
1045
                    return true;
×
NEW
1046
                }
×
1047
            }
1048
            MouseEventKind::Drag(_) => {
NEW
1049
                if let Some(drag) = self.drag_header {
×
NEW
1050
                    if self.is_window_floating(drag.id) {
×
NEW
1051
                        self.move_floating(
×
NEW
1052
                            drag.id,
×
NEW
1053
                            mouse.column,
×
NEW
1054
                            mouse.row,
×
NEW
1055
                            drag.offset_x,
×
NEW
1056
                            drag.offset_y,
×
1057
                        );
1058
                        // Only show snap preview if dragged a bit
NEW
1059
                        let dx = mouse.column.abs_diff(drag.start_x);
×
NEW
1060
                        let dy = mouse.row.abs_diff(drag.start_y);
×
NEW
1061
                        if dx + dy > 2 {
×
NEW
1062
                            self.update_snap_preview(drag.id, mouse.column, mouse.row);
×
NEW
1063
                        } else {
×
NEW
1064
                            self.drag_snap = None;
×
NEW
1065
                        }
×
NEW
1066
                    }
×
NEW
1067
                    return true;
×
NEW
1068
                }
×
1069
            }
1070
            MouseEventKind::Up(_) => {
NEW
1071
                if let Some(drag) = self.drag_header.take() {
×
NEW
1072
                    if self.drag_snap.is_some() {
×
NEW
1073
                        self.apply_snap(drag.id);
×
NEW
1074
                    }
×
NEW
1075
                    return true;
×
NEW
1076
                }
×
1077
            }
NEW
1078
            _ => {}
×
1079
        }
NEW
1080
        false
×
NEW
1081
    }
×
1082

NEW
1083
    fn handle_resize_event(&mut self, event: &Event) -> bool {
×
1084
        use crossterm::event::MouseEventKind;
NEW
1085
        let Event::Mouse(mouse) = event else {
×
NEW
1086
            return false;
×
1087
        };
NEW
1088
        match mouse.kind {
×
1089
            MouseEventKind::Down(_) => {
1090
                // Check if the mouse is blocked by a window above
NEW
1091
                let topmost_hit = if self.layout_contract == LayoutContract::WindowManaged
×
NEW
1092
                    && !self.managed_draw_order.is_empty()
×
1093
                {
NEW
1094
                    self.hit_test_region_topmost(mouse.column, mouse.row, &self.managed_draw_order)
×
1095
                } else {
NEW
1096
                    None
×
1097
                };
1098

NEW
1099
                let hit = self
×
NEW
1100
                    .resize_handles
×
NEW
1101
                    .iter()
×
NEW
1102
                    .rev()
×
NEW
1103
                    .find(|handle| rect_contains(handle.rect, mouse.column, mouse.row))
×
NEW
1104
                    .copied();
×
NEW
1105
                if let Some(handle) = hit {
×
1106
                    // If we hit a window body that is NOT the owner of this handle,
1107
                    // then the handle is obscured.
NEW
1108
                    if let Some(hit_id) = topmost_hit
×
NEW
1109
                        && hit_id != handle.id
×
1110
                    {
NEW
1111
                        return false;
×
NEW
1112
                    }
×
1113

NEW
1114
                    let rect = self.full_region_for_id(handle.id);
×
NEW
1115
                    if !self.is_window_floating(handle.id) {
×
NEW
1116
                        return false;
×
NEW
1117
                    }
×
NEW
1118
                    self.bring_floating_to_front_id(handle.id);
×
NEW
1119
                    self.drag_resize = Some(ResizeDrag {
×
NEW
1120
                        id: handle.id,
×
NEW
1121
                        edge: handle.edge,
×
NEW
1122
                        start_rect: rect,
×
NEW
1123
                        start_col: mouse.column,
×
NEW
1124
                        start_row: mouse.row,
×
NEW
1125
                    });
×
NEW
1126
                    return true;
×
NEW
1127
                }
×
1128
            }
1129
            MouseEventKind::Drag(_) => {
NEW
1130
                if let Some(drag) = self.drag_resize.as_ref()
×
NEW
1131
                    && self.is_window_floating(drag.id)
×
1132
                {
NEW
1133
                    let resized = apply_resize_drag(
×
NEW
1134
                        drag.start_rect,
×
NEW
1135
                        drag.edge,
×
NEW
1136
                        mouse.column,
×
NEW
1137
                        mouse.row,
×
NEW
1138
                        drag.start_col,
×
NEW
1139
                        drag.start_row,
×
NEW
1140
                        self.managed_area,
×
NEW
1141
                        self.floating_resize_offscreen,
×
1142
                    );
NEW
1143
                    self.set_floating_rect(drag.id, Some(RectSpec::Absolute(resized)));
×
NEW
1144
                    return true;
×
NEW
1145
                }
×
1146
            }
1147
            MouseEventKind::Up(_) => {
NEW
1148
                if self.drag_resize.take().is_some() {
×
NEW
1149
                    return true;
×
NEW
1150
                }
×
1151
            }
NEW
1152
            _ => {}
×
1153
        }
NEW
1154
        false
×
NEW
1155
    }
×
1156

NEW
1157
    fn detach_to_floating(&mut self, id: WindowId<R>, rect: Rect) -> bool {
×
NEW
1158
        if self.is_window_floating(id) {
×
NEW
1159
            return true;
×
NEW
1160
        }
×
NEW
1161
        if self.managed_layout.is_none() {
×
NEW
1162
            return false;
×
NEW
1163
        }
×
1164

NEW
1165
        let width = rect.width.max(1);
×
NEW
1166
        let height = rect.height.max(1);
×
NEW
1167
        let x = rect.x;
×
NEW
1168
        let y = rect.y;
×
NEW
1169
        self.set_floating_rect(
×
NEW
1170
            id,
×
NEW
1171
            Some(RectSpec::Absolute(Rect {
×
NEW
1172
                x,
×
NEW
1173
                y,
×
NEW
1174
                width,
×
NEW
1175
                height,
×
NEW
1176
            })),
×
1177
        );
NEW
1178
        self.bring_to_front_id(id);
×
NEW
1179
        true
×
NEW
1180
    }
×
1181

NEW
1182
    fn layout_contains(&self, id: WindowId<R>) -> bool {
×
NEW
1183
        self.managed_layout
×
NEW
1184
            .as_ref()
×
NEW
1185
            .is_some_and(|layout| layout.root().subtree_any(|node_id| node_id == id))
×
NEW
1186
    }
×
1187

NEW
1188
    fn move_floating(
×
NEW
1189
        &mut self,
×
NEW
1190
        id: WindowId<R>,
×
NEW
1191
        column: u16,
×
NEW
1192
        row: u16,
×
NEW
1193
        offset_x: u16,
×
NEW
1194
        offset_y: u16,
×
NEW
1195
    ) {
×
NEW
1196
        let panel_active = self.panel_active();
×
NEW
1197
        let bounds = self.managed_area;
×
NEW
1198
        let Some(RectSpec::Absolute(rect)) = self.floating_rect(id) else {
×
NEW
1199
            return;
×
1200
        };
NEW
1201
        let width = rect.width.max(1);
×
NEW
1202
        let height = rect.height.max(1);
×
NEW
1203
        let x = column.saturating_sub(offset_x);
×
NEW
1204
        let mut y = row.saturating_sub(offset_y);
×
NEW
1205
        if panel_active && y < bounds.y {
×
NEW
1206
            y = bounds.y;
×
NEW
1207
        }
×
NEW
1208
        self.set_floating_rect(
×
NEW
1209
            id,
×
NEW
1210
            Some(RectSpec::Absolute(Rect {
×
NEW
1211
                x,
×
NEW
1212
                y,
×
NEW
1213
                width,
×
NEW
1214
                height,
×
NEW
1215
            })),
×
1216
        );
NEW
1217
    }
×
1218

NEW
1219
    fn update_snap_preview(&mut self, dragging_id: WindowId<R>, mouse_x: u16, mouse_y: u16) {
×
NEW
1220
        self.drag_snap = None;
×
NEW
1221
        let area = self.managed_area;
×
1222

1223
        // 1. Check Window Snap first (more specific)
1224
        // We iterate z-order (top-to-bottom) to find the first valid target under mouse.
1225
        // We only allow snapping to windows that are already tiled, unless the layout is empty.
NEW
1226
        let target = self.z_order.iter().rev().find_map(|&id| {
×
NEW
1227
            if id == dragging_id {
×
NEW
1228
                return None;
×
NEW
1229
            }
×
1230
            // If we have a layout, ignore floating windows as snap targets
1231
            // to prevent "bait and switch" (offering to split a float, then splitting root).
NEW
1232
            if self.managed_layout.is_some() && self.is_window_floating(id) {
×
NEW
1233
                return None;
×
NEW
1234
            }
×
1235

NEW
1236
            let rect = self.regions.get(id)?;
×
NEW
1237
            if rect_contains(rect, mouse_x, mouse_y) {
×
NEW
1238
                Some((id, rect))
×
1239
            } else {
NEW
1240
                None
×
1241
            }
NEW
1242
        });
×
1243

NEW
1244
        if let Some((target_id, rect)) = target {
×
NEW
1245
            let h = rect.height;
×
1246

1247
            // Distance to edges
NEW
1248
            let d_top = mouse_y.saturating_sub(rect.y);
×
NEW
1249
            let d_bottom = (rect.y + h).saturating_sub(1).saturating_sub(mouse_y);
×
1250

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

1256
            // Check if the closest edge is within its sensitivity limit
1257
            // Only allow snapping on horizontal seams (top/bottom of tiled panes).
NEW
1258
            let snap = if d_top < sens_y && d_top <= d_bottom {
×
NEW
1259
                Some((
×
NEW
1260
                    InsertPosition::Top,
×
NEW
1261
                    Rect {
×
NEW
1262
                        height: h / 2,
×
NEW
1263
                        ..rect
×
NEW
1264
                    },
×
NEW
1265
                ))
×
NEW
1266
            } else if d_bottom < sens_y {
×
NEW
1267
                Some((
×
NEW
1268
                    InsertPosition::Bottom,
×
NEW
1269
                    Rect {
×
NEW
1270
                        y: rect.y + h / 2,
×
NEW
1271
                        height: h / 2,
×
NEW
1272
                        ..rect
×
NEW
1273
                    },
×
NEW
1274
                ))
×
1275
            } else {
NEW
1276
                None
×
1277
            };
1278

NEW
1279
            if let Some((pos, preview)) = snap {
×
NEW
1280
                self.drag_snap = Some((Some(target_id), pos, preview));
×
NEW
1281
                return;
×
NEW
1282
            }
×
NEW
1283
        }
×
1284

1285
        // 2. Check Screen Edge Snap (fallback, less specific)
NEW
1286
        let sensitivity = 2; // Strict sensitivity for screen edge
×
1287

NEW
1288
        let d_left = mouse_x.saturating_sub(area.x);
×
NEW
1289
        let d_right = (area.x + area.width)
×
NEW
1290
            .saturating_sub(1)
×
NEW
1291
            .saturating_sub(mouse_x);
×
NEW
1292
        let d_top = mouse_y.saturating_sub(area.y);
×
NEW
1293
        let d_bottom = (area.y + area.height)
×
NEW
1294
            .saturating_sub(1)
×
NEW
1295
            .saturating_sub(mouse_y);
×
1296

NEW
1297
        let min_screen_dist = d_left.min(d_right).min(d_top).min(d_bottom);
×
1298

NEW
1299
        let position = if min_screen_dist < sensitivity {
×
NEW
1300
            if d_left == min_screen_dist {
×
NEW
1301
                Some(InsertPosition::Left)
×
NEW
1302
            } else if d_right == min_screen_dist {
×
NEW
1303
                Some(InsertPosition::Right)
×
NEW
1304
            } else if d_top == min_screen_dist {
×
NEW
1305
                Some(InsertPosition::Top)
×
NEW
1306
            } else if d_bottom == min_screen_dist {
×
NEW
1307
                Some(InsertPosition::Bottom)
×
1308
            } else {
NEW
1309
                None
×
1310
            }
1311
        } else {
NEW
1312
            None
×
1313
        };
1314

NEW
1315
        if let Some(pos) = position {
×
NEW
1316
            let mut preview = match pos {
×
NEW
1317
                InsertPosition::Left => Rect {
×
NEW
1318
                    width: area.width / 2,
×
NEW
1319
                    ..area
×
NEW
1320
                },
×
NEW
1321
                InsertPosition::Right => Rect {
×
NEW
1322
                    x: area.x + area.width / 2,
×
NEW
1323
                    width: area.width / 2,
×
NEW
1324
                    ..area
×
NEW
1325
                },
×
NEW
1326
                InsertPosition::Top => Rect {
×
NEW
1327
                    height: area.height / 2,
×
NEW
1328
                    ..area
×
NEW
1329
                },
×
NEW
1330
                InsertPosition::Bottom => Rect {
×
NEW
1331
                    y: area.y + area.height / 2,
×
NEW
1332
                    height: area.height / 2,
×
NEW
1333
                    ..area
×
NEW
1334
                },
×
1335
            };
1336

1337
            // If there's no layout to split, dragging to edge just re-tiles to full screen.
NEW
1338
            if self.managed_layout.is_none() {
×
NEW
1339
                preview = area;
×
NEW
1340
            }
×
1341

NEW
1342
            self.drag_snap = Some((None, pos, preview));
×
NEW
1343
        }
×
NEW
1344
    }
×
1345

NEW
1346
    fn apply_snap(&mut self, id: WindowId<R>) {
×
NEW
1347
        if let Some((target, position, preview)) = self.drag_snap.take() {
×
1348
            // Check if we should tile or float-snap
1349
            // We float-snap if we are snapping to a screen edge (target is None)
1350
            // AND the layout is empty (no other tiled windows).
NEW
1351
            let other_windows_exist = if let Some(layout) = &self.managed_layout {
×
NEW
1352
                !layout.regions(self.managed_area).is_empty()
×
1353
            } else {
NEW
1354
                false
×
1355
            };
1356

NEW
1357
            if target.is_none() && !other_windows_exist {
×
1358
                // Single window edge snap -> Floating Resize
NEW
1359
                if self.is_window_floating(id) {
×
NEW
1360
                    self.set_floating_rect(id, Some(RectSpec::Absolute(preview)));
×
NEW
1361
                }
×
NEW
1362
                return;
×
NEW
1363
            }
×
1364

NEW
1365
            if self.is_window_floating(id) {
×
NEW
1366
                self.clear_floating_rect(id);
×
NEW
1367
            }
×
1368

NEW
1369
            if self.layout_contains(id)
×
NEW
1370
                && let Some(layout) = &mut self.managed_layout
×
1371
            {
NEW
1372
                let should_retile = match target {
×
NEW
1373
                    Some(target_id) => target_id != id,
×
NEW
1374
                    None => true,
×
1375
                };
NEW
1376
                if should_retile {
×
NEW
1377
                    layout.root_mut().remove_leaf(id);
×
NEW
1378
                } else {
×
NEW
1379
                    self.bring_to_front_id(id);
×
NEW
1380
                    return;
×
1381
                }
NEW
1382
            }
×
1383

1384
            // Handle case where target is floating (and thus not in layout yet)
NEW
1385
            if let Some(target_id) = target
×
NEW
1386
                && self.is_window_floating(target_id)
×
1387
            {
1388
                // Target is floating. We must initialize layout with it.
NEW
1389
                self.clear_floating_rect(target_id);
×
NEW
1390
                if self.managed_layout.is_none() {
×
NEW
1391
                    self.managed_layout = Some(TilingLayout::new(LayoutNode::leaf(target_id)));
×
NEW
1392
                } else {
×
NEW
1393
                    // This case is tricky: managed_layout exists (implied other windows), but target is floating.
×
NEW
1394
                    // We need to tile 'id' based on 'target'.
×
NEW
1395
                    // However, 'target' itself isn't in the tree.
×
NEW
1396
                    // This implies we want to perform a "Merge" of two floating windows into a new tiled group?
×
NEW
1397
                    // But we only support one root.
×
NEW
1398
                    // If managed_layout exists, we probably shouldn't be here if target is floating?
×
NEW
1399
                    // Actually, if we have {C} tiled, and {B} floating. Snap A to B.
×
NEW
1400
                    // We want {A, B} tiled?
×
NEW
1401
                    // Current logic: If managed_layout is Some, we try insert_leaf.
×
NEW
1402
                    // If insert_leaf fails, we fallback to split_root.
×
NEW
1403
                    // If we fall back to split_root, A is added to root. B remains floating.
×
NEW
1404
                    // This is acceptable/safe.
×
NEW
1405
                    // The critical case is when managed_layout is None (the 2-window case).
×
NEW
1406
                }
×
NEW
1407
            }
×
1408

NEW
1409
            if let Some(layout) = &mut self.managed_layout {
×
NEW
1410
                let success = if let Some(target_id) = target {
×
NEW
1411
                    layout.root_mut().insert_leaf(target_id, id, position)
×
1412
                } else {
NEW
1413
                    false
×
1414
                };
1415

NEW
1416
                if !success {
×
NEW
1417
                    // If insert failed (e.g. target was missing or we are splitting root),
×
NEW
1418
                    // If target was the one we just initialized (in the floating case above), it should be at root.
×
NEW
1419
                    // insert_leaf should have worked if target is root.
×
NEW
1420
                    layout.split_root(id, position);
×
NEW
1421
                }
×
1422

1423
                // Ensure the snapped window is brought to front/focused
NEW
1424
                if let Some(pos) = self.z_order.iter().position(|&z_id| z_id == id) {
×
NEW
1425
                    self.z_order.remove(pos);
×
NEW
1426
                }
×
NEW
1427
                self.z_order.push(id);
×
NEW
1428
                self.managed_draw_order = self.z_order.clone();
×
NEW
1429
            } else {
×
NEW
1430
                self.managed_layout = Some(TilingLayout::new(LayoutNode::leaf(id)));
×
NEW
1431
            }
×
1432

1433
            // If we snapped a window into place, any other floating windows should snap as well.
NEW
1434
            let mut pending_snap = Vec::new();
×
NEW
1435
            for r_id in self.regions.ids() {
×
NEW
1436
                if r_id != id && self.is_window_floating(r_id) {
×
NEW
1437
                    pending_snap.push(r_id);
×
NEW
1438
                }
×
1439
            }
NEW
1440
            for float_id in pending_snap {
×
NEW
1441
                self.tile_window_id(float_id);
×
NEW
1442
            }
×
NEW
1443
        }
×
NEW
1444
    }
×
1445

1446
    /// Smartly insert a window into the tiling layout.
1447
    /// If there is a focused tiled window, split it.
1448
    /// Otherwise, split the root.
NEW
1449
    pub fn tile_window(&mut self, id: R) -> bool {
×
NEW
1450
        self.tile_window_id(WindowId::app(id))
×
NEW
1451
    }
×
1452

NEW
1453
    fn tile_window_id(&mut self, id: WindowId<R>) -> bool {
×
1454
        // If already in layout or floating, do nothing (or move it?)
1455
        // For now, assume this is for new windows.
NEW
1456
        if self.layout_contains(id) {
×
NEW
1457
            if self.is_window_floating(id) {
×
NEW
1458
                self.clear_floating_rect(id);
×
NEW
1459
            }
×
NEW
1460
            self.bring_to_front_id(id);
×
NEW
1461
            return true;
×
NEW
1462
        }
×
NEW
1463
        if self.managed_layout.is_none() {
×
NEW
1464
            self.managed_layout = Some(TilingLayout::new(LayoutNode::leaf(id)));
×
NEW
1465
            self.bring_to_front_id(id);
×
NEW
1466
            return true;
×
NEW
1467
        }
×
1468

1469
        // Try to find a focused node that is in the layout
NEW
1470
        let current_focus = self.wm_focus.current();
×
1471

NEW
1472
        let mut target_r = None;
×
NEW
1473
        for r_id in self.regions.ids() {
×
NEW
1474
            if r_id == current_focus {
×
NEW
1475
                target_r = Some(r_id);
×
NEW
1476
                break;
×
NEW
1477
            }
×
1478
        }
1479

NEW
1480
        let Some(layout) = self.managed_layout.as_mut() else {
×
NEW
1481
            return false;
×
1482
        };
1483

1484
        // If we found a focused region, split it
NEW
1485
        if let Some(target) = target_r {
×
1486
            // Prefer splitting horizontally (side-by-side) for wide windows, vertically for tall?
1487
            // Or just default to Right/Bottom.
1488
            // Let's default to Right for now as it's common.
NEW
1489
            if layout
×
NEW
1490
                .root_mut()
×
NEW
1491
                .insert_leaf(target, id, InsertPosition::Right)
×
1492
            {
NEW
1493
                self.bring_to_front_id(id);
×
NEW
1494
                return true;
×
NEW
1495
            }
×
NEW
1496
        }
×
1497

1498
        // Fallback: split root
NEW
1499
        layout.split_root(id, InsertPosition::Right);
×
NEW
1500
        self.bring_to_front_id(id);
×
NEW
1501
        true
×
NEW
1502
    }
×
1503

NEW
1504
    pub fn bring_to_front(&mut self, id: R) {
×
NEW
1505
        self.bring_to_front_id(WindowId::app(id));
×
NEW
1506
    }
×
1507

NEW
1508
    fn bring_to_front_id(&mut self, id: WindowId<R>) {
×
NEW
1509
        if let Some(pos) = self.z_order.iter().position(|&x| x == id) {
×
NEW
1510
            let item = self.z_order.remove(pos);
×
NEW
1511
            self.z_order.push(item);
×
NEW
1512
        }
×
NEW
1513
    }
×
1514

NEW
1515
    pub fn bring_all_floating_to_front(&mut self) {
×
NEW
1516
        let ids: Vec<WindowId<R>> = self
×
NEW
1517
            .z_order
×
NEW
1518
            .iter()
×
NEW
1519
            .copied()
×
NEW
1520
            .filter(|id| self.is_window_floating(*id))
×
NEW
1521
            .collect();
×
NEW
1522
        for id in ids {
×
NEW
1523
            self.bring_to_front_id(id);
×
NEW
1524
        }
×
NEW
1525
    }
×
1526

NEW
1527
    fn bring_floating_to_front_id(&mut self, id: WindowId<R>) {
×
NEW
1528
        self.bring_to_front_id(id);
×
NEW
1529
    }
×
1530

NEW
1531
    fn bring_floating_to_front(&mut self, id: R) {
×
NEW
1532
        self.bring_floating_to_front_id(WindowId::app(id));
×
NEW
1533
    }
×
1534

NEW
1535
    fn clamp_floating_to_bounds(&mut self) {
×
NEW
1536
        let bounds = self.managed_area;
×
NEW
1537
        if bounds.width == 0 || bounds.height == 0 {
×
NEW
1538
            return;
×
NEW
1539
        }
×
1540
        // Collect updates first to avoid borrowing `self` mutably while iterating
NEW
1541
        let mut updates: Vec<(WindowId<R>, RectSpec)> = Vec::new();
×
NEW
1542
        let floating_ids: Vec<WindowId<R>> = self
×
NEW
1543
            .windows
×
NEW
1544
            .iter()
×
NEW
1545
            .filter_map(|(&id, window)| window.floating_rect.as_ref().map(|_| id))
×
NEW
1546
            .collect();
×
NEW
1547
        for id in floating_ids {
×
NEW
1548
            let Some(RectSpec::Absolute(rect)) = self.floating_rect(id) else {
×
NEW
1549
                continue;
×
1550
            };
NEW
1551
            if rects_intersect(rect, bounds) {
×
NEW
1552
                continue;
×
NEW
1553
            }
×
1554
            // Only recover panes that are fully off-screen; keep normal dragging untouched.
NEW
1555
            let rect_right = rect.x.saturating_add(rect.width);
×
NEW
1556
            let rect_bottom = rect.y.saturating_add(rect.height);
×
NEW
1557
            let bounds_right = bounds.x.saturating_add(bounds.width);
×
NEW
1558
            let bounds_bottom = bounds.y.saturating_add(bounds.height);
×
1559
            // Clamp only the axis that is fully outside the viewport.
NEW
1560
            let out_x = rect_right <= bounds.x || rect.x >= bounds_right;
×
NEW
1561
            let out_y = rect_bottom <= bounds.y || rect.y >= bounds_bottom;
×
NEW
1562
            let min_w = FLOATING_MIN_WIDTH.min(bounds.width.max(1));
×
NEW
1563
            let min_h = FLOATING_MIN_HEIGHT.min(bounds.height.max(1));
×
1564

1565
            // Ensure at least a small portion of the window (e.g. handle) is always visible
1566
            // so the user can grab it back.
NEW
1567
            let min_visible_margin = 4u16;
×
1568

NEW
1569
            let width = if self.floating_resize_offscreen {
×
NEW
1570
                rect.width.max(min_w)
×
1571
            } else {
NEW
1572
                rect.width.max(min_w).min(bounds.width)
×
1573
            };
NEW
1574
            let height = if self.floating_resize_offscreen {
×
NEW
1575
                rect.height.max(min_h)
×
1576
            } else {
NEW
1577
                rect.height.max(min_h).min(bounds.height)
×
1578
            };
1579

NEW
1580
            let max_x = if self.floating_resize_offscreen {
×
NEW
1581
                bounds
×
NEW
1582
                    .x
×
NEW
1583
                    .saturating_add(bounds.width)
×
NEW
1584
                    .saturating_sub(min_visible_margin.min(width))
×
1585
            } else {
NEW
1586
                bounds.x.saturating_add(bounds.width.saturating_sub(width))
×
1587
            };
1588

NEW
1589
            let max_y = if self.floating_resize_offscreen {
×
NEW
1590
                bounds.y.saturating_add(bounds.height).saturating_sub(1) // Header is usually top line
×
1591
            } else {
NEW
1592
                bounds
×
NEW
1593
                    .y
×
NEW
1594
                    .saturating_add(bounds.height.saturating_sub(height))
×
1595
            };
1596

NEW
1597
            let x = if out_x || !self.floating_resize_offscreen {
×
NEW
1598
                rect.x.clamp(bounds.x, max_x)
×
1599
            } else {
NEW
1600
                rect.x.max(bounds.x).min(max_x)
×
1601
            };
1602

NEW
1603
            let y = if out_y || !self.floating_resize_offscreen {
×
NEW
1604
                rect.y.clamp(bounds.y, max_y)
×
1605
            } else {
NEW
1606
                rect.y.max(bounds.y).min(max_y)
×
1607
            };
NEW
1608
            updates.push((
×
NEW
1609
                id,
×
NEW
1610
                RectSpec::Absolute(Rect {
×
NEW
1611
                    x,
×
NEW
1612
                    y,
×
NEW
1613
                    width,
×
NEW
1614
                    height,
×
NEW
1615
                }),
×
NEW
1616
            ));
×
1617
        }
NEW
1618
        for (id, spec) in updates {
×
NEW
1619
            self.set_floating_rect(id, Some(spec));
×
NEW
1620
        }
×
NEW
1621
    }
×
1622

NEW
1623
    pub fn window_draw_plan(&mut self, frame: &mut UiFrame<'_>) -> Vec<AppWindowDraw<R>> {
×
NEW
1624
        let mut plan = Vec::new();
×
NEW
1625
        let focused_app = self.wm_focus.current().as_app();
×
NEW
1626
        for &id in &self.managed_draw_order {
×
NEW
1627
            let full = self.full_region_for_id(id);
×
NEW
1628
            if full.width == 0 || full.height == 0 {
×
NEW
1629
                continue;
×
NEW
1630
            }
×
NEW
1631
            frame.render_widget(Clear, full);
×
NEW
1632
            let WindowId::App(app_id) = id else {
×
NEW
1633
                continue;
×
1634
            };
NEW
1635
            let inner = self.region(app_id);
×
NEW
1636
            if inner.width == 0 || inner.height == 0 {
×
NEW
1637
                continue;
×
NEW
1638
            }
×
NEW
1639
            plan.push(AppWindowDraw {
×
NEW
1640
                id: app_id,
×
NEW
1641
                surface: WindowSurface { full, inner },
×
NEW
1642
                focused: focused_app == Some(app_id),
×
NEW
1643
            });
×
1644
        }
NEW
1645
        plan
×
NEW
1646
    }
×
1647

NEW
1648
    pub fn render_overlays(&mut self, frame: &mut UiFrame<'_>) {
×
NEW
1649
        let hovered = self.hover.and_then(|(column, row)| {
×
NEW
1650
            self.handles
×
NEW
1651
                .iter()
×
NEW
1652
                .find(|handle| rect_contains(handle.rect, column, row))
×
NEW
1653
        });
×
NEW
1654
        let hovered_resize = self.hover.and_then(|(column, row)| {
×
NEW
1655
            self.resize_handles
×
NEW
1656
                .iter()
×
NEW
1657
                .find(|handle| rect_contains(handle.rect, column, row))
×
NEW
1658
        });
×
NEW
1659
        let obscuring: Vec<Rect> = self
×
NEW
1660
            .managed_draw_order
×
NEW
1661
            .iter()
×
NEW
1662
            .filter_map(|&id| self.regions.get(id))
×
NEW
1663
            .collect();
×
NEW
1664
        let is_obscured =
×
NEW
1665
            |x: u16, y: u16| -> bool { obscuring.iter().any(|r| rect_contains(*r, x, y)) };
×
NEW
1666
        render_handles_masked(frame, &self.handles, hovered, is_obscured);
×
NEW
1667
        let focused = self.wm_focus.current();
×
1668

NEW
1669
        for (i, &id) in self.managed_draw_order.iter().enumerate() {
×
NEW
1670
            let Some(rect) = self.regions.get(id) else {
×
NEW
1671
                continue;
×
1672
            };
NEW
1673
            if rect.width < 3 || rect.height < 3 {
×
NEW
1674
                continue;
×
NEW
1675
            }
×
1676

NEW
1677
            if id == self.debug_log_id && self.state.debug_log_visible() {
×
NEW
1678
                let area = self.region_for_id(id);
×
NEW
1679
                if area.width > 0 && area.height > 0 {
×
NEW
1680
                    self.debug_log.render(frame, area, id == focused);
×
NEW
1681
                }
×
NEW
1682
            }
×
1683

1684
            // Collect obscuring rects (windows above this one)
NEW
1685
            let obscuring: Vec<Rect> = self.managed_draw_order[i + 1..]
×
NEW
1686
                .iter()
×
NEW
1687
                .filter_map(|&above_id| self.regions.get(above_id))
×
NEW
1688
                .collect();
×
1689

NEW
1690
            let is_obscured =
×
NEW
1691
                |x: u16, y: u16| -> bool { obscuring.iter().any(|r| rect_contains(*r, x, y)) };
×
1692

NEW
1693
            let title = self.window_title(id);
×
NEW
1694
            let focused_window = id == focused;
×
NEW
1695
            self.decorator.render_window(
×
NEW
1696
                frame,
×
NEW
1697
                rect,
×
NEW
1698
                self.managed_area,
×
NEW
1699
                &title,
×
NEW
1700
                focused_window,
×
NEW
1701
                &is_obscured,
×
1702
            );
1703
        }
1704

1705
        // Build floating panes list from per-window entries for resize outline rendering
NEW
1706
        let floating_panes: Vec<FloatingPane<WindowId<R>>> = self
×
NEW
1707
            .windows
×
NEW
1708
            .iter()
×
NEW
1709
            .filter_map(|(&id, window)| window.floating_rect.map(|rect| FloatingPane { id, rect }))
×
NEW
1710
            .collect();
×
1711

NEW
1712
        render_resize_outline(
×
NEW
1713
            frame,
×
NEW
1714
            hovered_resize.map(|handle| handle.id),
×
NEW
1715
            self.drag_resize.as_ref().map(|drag| drag.id),
×
NEW
1716
            &self.regions,
×
NEW
1717
            self.managed_area,
×
NEW
1718
            &floating_panes,
×
NEW
1719
            &self.managed_draw_order,
×
1720
        );
1721

NEW
1722
        if let Some((_, _, rect)) = self.drag_snap {
×
NEW
1723
            let buffer = frame.buffer_mut();
×
NEW
1724
            let color = crate::theme::accent();
×
NEW
1725
            let clip = rect.intersection(buffer.area);
×
NEW
1726
            if clip.width > 0 && clip.height > 0 {
×
NEW
1727
                for y in clip.y..clip.y.saturating_add(clip.height) {
×
NEW
1728
                    for x in clip.x..clip.x.saturating_add(clip.width) {
×
NEW
1729
                        if let Some(cell) = buffer.cell_mut((x, y)) {
×
NEW
1730
                            let mut style = cell.style();
×
NEW
1731
                            style.bg = Some(color);
×
NEW
1732
                            cell.set_style(style);
×
NEW
1733
                        }
×
1734
                    }
1735
                }
NEW
1736
            }
×
NEW
1737
        }
×
1738

NEW
1739
        let status_line = if self.wm_overlay_visible() {
×
NEW
1740
            let esc_state = if let Some(remaining) = self.esc_passthrough_remaining() {
×
NEW
1741
                format!("Esc passthrough: active ({}ms)", remaining.as_millis())
×
1742
            } else {
NEW
1743
                "Esc passthrough: inactive".to_string()
×
1744
            };
NEW
1745
            Some(format!("{esc_state} · Tab/Shift-Tab: cycle windows"))
×
1746
        } else {
NEW
1747
            None
×
1748
        };
NEW
1749
        let display = self.build_display_order();
×
1750
        // Build a small title map to avoid borrowing `self` inside the panel closure
NEW
1751
        let titles_map: BTreeMap<WindowId<R>, String> = self
×
NEW
1752
            .windows
×
NEW
1753
            .keys()
×
NEW
1754
            .map(|id| (*id, self.window_title(*id)))
×
NEW
1755
            .collect();
×
1756

NEW
1757
        self.panel.render(
×
NEW
1758
            frame,
×
NEW
1759
            self.panel_active(),
×
NEW
1760
            self.wm_focus.current(),
×
NEW
1761
            &display,
×
NEW
1762
            status_line.as_deref(),
×
NEW
1763
            self.mouse_capture_enabled(),
×
NEW
1764
            self.wm_overlay_visible(),
×
NEW
1765
            move |id| {
×
NEW
1766
                titles_map.get(&id).cloned().unwrap_or_else(|| match id {
×
NEW
1767
                    WindowId::App(app_id) => format!("{:?}", app_id),
×
NEW
1768
                    WindowId::System(SystemWindowId::DebugLog) => "Debug Log".to_string(),
×
NEW
1769
                })
×
NEW
1770
            },
×
1771
        );
NEW
1772
        let menu_labels = wm_menu_items(self.mouse_capture_enabled())
×
NEW
1773
            .iter()
×
NEW
1774
            .map(|item| (item.icon, item.label))
×
NEW
1775
            .collect::<Vec<_>>();
×
NEW
1776
        let bounds = frame.area();
×
NEW
1777
        self.panel.render_menu(
×
NEW
1778
            frame,
×
NEW
1779
            self.wm_overlay_visible(),
×
NEW
1780
            bounds,
×
NEW
1781
            &menu_labels,
×
NEW
1782
            self.state.wm_menu_selected(),
×
1783
        );
NEW
1784
        self.panel.render_menu_backdrop(
×
NEW
1785
            frame,
×
NEW
1786
            self.wm_overlay_visible(),
×
NEW
1787
            self.managed_area,
×
NEW
1788
            self.panel.area(),
×
1789
        );
NEW
1790
        if self.exit_confirm.visible() {
×
NEW
1791
            self.exit_confirm.render(frame, frame.area(), false);
×
NEW
1792
        }
×
NEW
1793
    }
×
1794

NEW
1795
    pub fn clear_window_backgrounds(&self, frame: &mut UiFrame<'_>) {
×
NEW
1796
        for id in self.regions.ids() {
×
NEW
1797
            let rect = self.full_region_for_id(id);
×
NEW
1798
            frame.render_widget(Clear, rect);
×
NEW
1799
        }
×
NEW
1800
    }
×
1801

NEW
1802
    pub fn set_regions_from_plan(&mut self, plan: &LayoutPlan<R>, area: Rect) {
×
NEW
1803
        let plan_regions = plan.regions(area);
×
NEW
1804
        self.regions = RegionMap::default();
×
NEW
1805
        for id in plan_regions.ids() {
×
NEW
1806
            if let Some(rect) = plan_regions.get(id) {
×
NEW
1807
                self.regions.set(WindowId::app(id), rect);
×
NEW
1808
            }
×
1809
        }
NEW
1810
    }
×
1811

NEW
1812
    pub fn hit_test_region(&self, column: u16, row: u16, ids: &[R]) -> Option<R> {
×
NEW
1813
        for id in ids {
×
NEW
1814
            if let Some(rect) = self.regions.get(WindowId::app(*id))
×
NEW
1815
                && rect_contains(rect, column, row)
×
1816
            {
NEW
1817
                return Some(*id);
×
NEW
1818
            }
×
1819
        }
NEW
1820
        None
×
NEW
1821
    }
×
1822

1823
    /// Hit-test regions by draw order so overlapping panes pick the topmost one.
1824
    /// This avoids clicks "falling through" floating panes to windows behind them.
NEW
1825
    fn hit_test_region_topmost(
×
NEW
1826
        &self,
×
NEW
1827
        column: u16,
×
NEW
1828
        row: u16,
×
NEW
1829
        ids: &[WindowId<R>],
×
NEW
1830
    ) -> Option<WindowId<R>> {
×
NEW
1831
        for id in ids.iter().rev() {
×
NEW
1832
            if let Some(rect) = self.regions.get(*id)
×
NEW
1833
                && rect_contains(rect, column, row)
×
1834
            {
NEW
1835
                return Some(*id);
×
NEW
1836
            }
×
1837
        }
NEW
1838
        None
×
NEW
1839
    }
×
1840

NEW
1841
    pub fn handle_focus_event<F, G>(
×
NEW
1842
        &mut self,
×
NEW
1843
        event: &Event,
×
NEW
1844
        hit_targets: &[R],
×
NEW
1845
        map: F,
×
NEW
1846
        map_focus: G,
×
NEW
1847
    ) -> bool
×
NEW
1848
    where
×
NEW
1849
        F: Fn(R) -> W,
×
NEW
1850
        G: Fn(W) -> Option<R>,
×
1851
    {
NEW
1852
        match event {
×
NEW
1853
            Event::Key(key) => match key.code {
×
1854
                KeyCode::Tab => {
NEW
1855
                    if self.layout_contract == LayoutContract::WindowManaged {
×
NEW
1856
                        self.advance_wm_focus(true);
×
NEW
1857
                    } else {
×
NEW
1858
                        self.app_focus.advance(true);
×
1859
                        // Ensure app-level tab switching also brings the corresponding window to front
NEW
1860
                        let focused_app = self.app_focus.current();
×
NEW
1861
                        if let Some(region) = map_focus(focused_app) {
×
NEW
1862
                            self.set_wm_focus(WindowId::app(region));
×
NEW
1863
                            self.bring_to_front_id(WindowId::app(region));
×
NEW
1864
                            self.managed_draw_order = self.z_order.clone();
×
NEW
1865
                        }
×
1866
                    }
NEW
1867
                    true
×
1868
                }
1869
                KeyCode::BackTab => {
NEW
1870
                    if self.layout_contract == LayoutContract::WindowManaged {
×
NEW
1871
                        self.advance_wm_focus(false);
×
NEW
1872
                    } else {
×
NEW
1873
                        self.app_focus.advance(false);
×
1874
                        // Mirror behavior for reverse tabbing as well
NEW
1875
                        let focused_app = self.app_focus.current();
×
NEW
1876
                        if let Some(region) = map_focus(focused_app) {
×
NEW
1877
                            self.set_wm_focus(WindowId::app(region));
×
NEW
1878
                            self.bring_to_front_id(WindowId::app(region));
×
NEW
1879
                            self.managed_draw_order = self.z_order.clone();
×
NEW
1880
                        }
×
1881
                    }
NEW
1882
                    true
×
1883
                }
NEW
1884
                _ => false,
×
1885
            },
NEW
1886
            Event::Mouse(mouse) => {
×
NEW
1887
                self.hover = Some((mouse.column, mouse.row));
×
NEW
1888
                match mouse.kind {
×
1889
                    MouseEventKind::Down(_) => {
NEW
1890
                        if self.layout_contract == LayoutContract::WindowManaged
×
NEW
1891
                            && !self.managed_draw_order.is_empty()
×
1892
                        {
NEW
1893
                            let hit = self.hit_test_region_topmost(
×
NEW
1894
                                mouse.column,
×
NEW
1895
                                mouse.row,
×
NEW
1896
                                &self.managed_draw_order,
×
1897
                            );
NEW
1898
                            if let Some(id) = hit {
×
NEW
1899
                                self.set_wm_focus(id);
×
NEW
1900
                                self.bring_floating_to_front_id(id);
×
NEW
1901
                                return true;
×
NEW
1902
                            }
×
NEW
1903
                            return false;
×
NEW
1904
                        }
×
NEW
1905
                        let hit = self.hit_test_region(mouse.column, mouse.row, hit_targets);
×
NEW
1906
                        if let Some(hit) = hit {
×
NEW
1907
                            self.app_focus.set_current(map(hit));
×
NEW
1908
                            if self.layout_contract == LayoutContract::WindowManaged {
×
NEW
1909
                                self.set_wm_focus(WindowId::app(hit));
×
NEW
1910
                                self.bring_floating_to_front(hit);
×
NEW
1911
                            }
×
NEW
1912
                            true
×
1913
                        } else {
NEW
1914
                            false
×
1915
                        }
1916
                    }
NEW
1917
                    _ => false,
×
1918
                }
1919
            }
NEW
1920
            _ => false,
×
1921
        }
NEW
1922
    }
×
1923

NEW
1924
    fn panel_active(&self) -> bool {
×
NEW
1925
        self.layout_contract == LayoutContract::WindowManaged
×
NEW
1926
            && self.panel.visible()
×
NEW
1927
            && self.panel.height() > 0
×
NEW
1928
    }
×
1929

NEW
1930
    fn focus_for_region(&self, id: R) -> Option<W> {
×
NEW
1931
        if self.app_focus.order.is_empty() {
×
NEW
1932
            if id == self.app_focus.current {
×
NEW
1933
                Some(self.app_focus.current)
×
1934
            } else {
NEW
1935
                None
×
1936
            }
1937
        } else {
NEW
1938
            self.app_focus
×
NEW
1939
                .order
×
NEW
1940
                .iter()
×
NEW
1941
                .copied()
×
NEW
1942
                .find(|focus| id == *focus)
×
1943
        }
NEW
1944
    }
×
1945

NEW
1946
    pub fn handle_wm_menu_event(&mut self, event: &Event) -> Option<WmMenuAction> {
×
NEW
1947
        if !self.wm_overlay_visible() {
×
NEW
1948
            return None;
×
NEW
1949
        }
×
NEW
1950
        if let Event::Mouse(mouse) = event
×
NEW
1951
            && matches!(mouse.kind, MouseEventKind::Down(_))
×
1952
        {
NEW
1953
            if let Some(index) = self.panel.hit_test_menu_item(event) {
×
NEW
1954
                let items = wm_menu_items(self.mouse_capture_enabled());
×
NEW
1955
                let selected = index.min(items.len().saturating_sub(1));
×
NEW
1956
                self.state.set_wm_menu_selected(selected);
×
NEW
1957
                return items.get(selected).map(|item| item.action);
×
NEW
1958
            }
×
NEW
1959
            if self.panel.menu_icon_contains_point(mouse.column, mouse.row) {
×
NEW
1960
                return Some(WmMenuAction::CloseMenu);
×
NEW
1961
            }
×
NEW
1962
            if !self.panel.menu_contains_point(mouse.column, mouse.row) {
×
NEW
1963
                return Some(WmMenuAction::CloseMenu);
×
NEW
1964
            }
×
NEW
1965
        }
×
NEW
1966
        let Event::Key(key) = event else {
×
NEW
1967
            return None;
×
1968
        };
NEW
1969
        match key.code {
×
1970
            KeyCode::Up => {
NEW
1971
                let total = wm_menu_items(self.mouse_capture_enabled()).len();
×
NEW
1972
                if total > 0 {
×
NEW
1973
                    let current = self.state.wm_menu_selected();
×
NEW
1974
                    if current == 0 {
×
NEW
1975
                        self.state.set_wm_menu_selected(total - 1);
×
NEW
1976
                    } else {
×
NEW
1977
                        self.state.set_wm_menu_selected(current - 1);
×
NEW
1978
                    }
×
NEW
1979
                }
×
NEW
1980
                None
×
1981
            }
1982
            KeyCode::Down => {
NEW
1983
                let total = wm_menu_items(self.mouse_capture_enabled()).len();
×
NEW
1984
                if total > 0 {
×
NEW
1985
                    let current = self.state.wm_menu_selected();
×
NEW
1986
                    self.state.set_wm_menu_selected((current + 1) % total);
×
NEW
1987
                }
×
NEW
1988
                None
×
1989
            }
1990
            KeyCode::Char('k') => {
NEW
1991
                let total = wm_menu_items(self.mouse_capture_enabled()).len();
×
NEW
1992
                if total > 0 {
×
NEW
1993
                    let current = self.state.wm_menu_selected();
×
NEW
1994
                    if current == 0 {
×
NEW
1995
                        self.state.set_wm_menu_selected(total - 1);
×
NEW
1996
                    } else {
×
NEW
1997
                        self.state.set_wm_menu_selected(current - 1);
×
NEW
1998
                    }
×
NEW
1999
                }
×
NEW
2000
                None
×
2001
            }
2002
            KeyCode::Char('j') => {
NEW
2003
                let total = wm_menu_items(self.mouse_capture_enabled()).len();
×
NEW
2004
                if total > 0 {
×
NEW
2005
                    let current = self.state.wm_menu_selected();
×
NEW
2006
                    self.state.set_wm_menu_selected((current + 1) % total);
×
NEW
2007
                }
×
NEW
2008
                None
×
2009
            }
NEW
2010
            KeyCode::Enter => wm_menu_items(self.mouse_capture_enabled())
×
NEW
2011
                .get(self.state.wm_menu_selected())
×
NEW
2012
                .map(|item| item.action),
×
NEW
2013
            _ => None,
×
2014
        }
NEW
2015
    }
×
2016

NEW
2017
    pub fn handle_exit_confirm_event(&mut self, event: &Event) -> Option<ConfirmAction> {
×
NEW
2018
        if !self.exit_confirm.visible() {
×
NEW
2019
            return None;
×
NEW
2020
        }
×
NEW
2021
        self.exit_confirm.handle_confirm_event(event)
×
NEW
2022
    }
×
2023

NEW
2024
    pub fn wm_menu_consumes_event(&self, event: &Event) -> bool {
×
NEW
2025
        if !self.wm_overlay_visible() {
×
NEW
2026
            return false;
×
NEW
2027
        }
×
NEW
2028
        let Event::Key(key) = event else {
×
NEW
2029
            return false;
×
2030
        };
NEW
2031
        matches!(
×
NEW
2032
            key.code,
×
2033
            KeyCode::Up | KeyCode::Down | KeyCode::Enter | KeyCode::Char('j') | KeyCode::Char('k')
2034
        )
NEW
2035
    }
×
2036
}
2037

2038
#[derive(Debug, Clone, Copy)]
2039
struct WmMenuItem {
2040
    label: &'static str,
2041
    icon: Option<&'static str>,
2042
    action: WmMenuAction,
2043
}
NEW
2044
fn wm_menu_items(mouse_capture_enabled: bool) -> [WmMenuItem; 6] {
×
NEW
2045
    let mouse_label = if mouse_capture_enabled {
×
NEW
2046
        "Mouse Capture: On"
×
2047
    } else {
NEW
2048
        "Mouse Capture: Off"
×
2049
    };
NEW
2050
    [
×
NEW
2051
        WmMenuItem {
×
NEW
2052
            label: "Resume",
×
NEW
2053
            icon: None,
×
NEW
2054
            action: WmMenuAction::CloseMenu,
×
NEW
2055
        },
×
NEW
2056
        WmMenuItem {
×
NEW
2057
            label: mouse_label,
×
NEW
2058
            icon: Some("🖱"),
×
NEW
2059
            action: WmMenuAction::ToggleMouseCapture,
×
NEW
2060
        },
×
NEW
2061
        WmMenuItem {
×
NEW
2062
            label: "Floating Front",
×
NEW
2063
            icon: Some("↑"),
×
NEW
2064
            action: WmMenuAction::BringFloatingFront,
×
NEW
2065
        },
×
NEW
2066
        WmMenuItem {
×
NEW
2067
            label: "New Window",
×
NEW
2068
            icon: Some("+"),
×
NEW
2069
            action: WmMenuAction::NewWindow,
×
NEW
2070
        },
×
NEW
2071
        WmMenuItem {
×
NEW
2072
            label: "Debug Log",
×
NEW
2073
            icon: Some("≣"),
×
NEW
2074
            action: WmMenuAction::ToggleDebugWindow,
×
NEW
2075
        },
×
NEW
2076
        WmMenuItem {
×
NEW
2077
            label: "Exit UI",
×
NEW
2078
            icon: Some("⏻"),
×
NEW
2079
            action: WmMenuAction::ExitUi,
×
NEW
2080
        },
×
NEW
2081
    ]
×
NEW
2082
}
×
2083

2084
fn esc_passthrough_window_default() -> Duration {
1✔
2085
    #[cfg(windows)]
2086
    {
2087
        Duration::from_millis(1200)
2088
    }
2089
    #[cfg(not(windows))]
2090
    {
2091
        Duration::from_millis(600)
1✔
2092
    }
2093
}
1✔
2094

2095
fn clamp_rect(area: Rect, bounds: Rect) -> Rect {
2✔
2096
    let x0 = area.x.max(bounds.x);
2✔
2097
    let y0 = area.y.max(bounds.y);
2✔
2098
    let x1 = area
2✔
2099
        .x
2✔
2100
        .saturating_add(area.width)
2✔
2101
        .min(bounds.x.saturating_add(bounds.width));
2✔
2102
    let y1 = area
2✔
2103
        .y
2✔
2104
        .saturating_add(area.height)
2✔
2105
        .min(bounds.y.saturating_add(bounds.height));
2✔
2106
    if x1 <= x0 || y1 <= y0 {
2✔
2107
        return Rect::default();
1✔
2108
    }
1✔
2109
    Rect {
1✔
2110
        x: x0,
1✔
2111
        y: y0,
1✔
2112
        width: x1 - x0,
1✔
2113
        height: y1 - y0,
1✔
2114
    }
1✔
2115
}
2✔
2116

2117
fn rects_intersect(a: Rect, b: Rect) -> bool {
2✔
2118
    if a.width == 0 || a.height == 0 || b.width == 0 || b.height == 0 {
2✔
NEW
2119
        return false;
×
2120
    }
2✔
2121
    let a_right = a.x.saturating_add(a.width);
2✔
2122
    let a_bottom = a.y.saturating_add(a.height);
2✔
2123
    let b_right = b.x.saturating_add(b.width);
2✔
2124
    let b_bottom = b.y.saturating_add(b.height);
2✔
2125
    a.x < b_right && a_right > b.x && a.y < b_bottom && a_bottom > b.y
2✔
2126
}
2✔
2127

2128
fn map_layout_node<R: Copy + Eq + Ord>(node: &LayoutNode<R>) -> LayoutNode<WindowId<R>> {
1✔
2129
    match node {
1✔
2130
        LayoutNode::Leaf(id) => LayoutNode::leaf(WindowId::app(*id)),
1✔
2131
        LayoutNode::Split {
NEW
2132
            direction,
×
NEW
2133
            children,
×
NEW
2134
            weights,
×
NEW
2135
            constraints,
×
NEW
2136
            resizable,
×
NEW
2137
        } => LayoutNode::Split {
×
NEW
2138
            direction: *direction,
×
NEW
2139
            children: children.iter().map(map_layout_node).collect(),
×
NEW
2140
            weights: weights.clone(),
×
NEW
2141
            constraints: constraints.clone(),
×
NEW
2142
            resizable: *resizable,
×
NEW
2143
        },
×
2144
    }
2145
}
1✔
2146

2147
#[cfg(test)]
2148
mod tests {
2149
    use super::*;
2150
    use ratatui::layout::Rect;
2151

2152
    #[test]
2153
    fn clamp_rect_inside_and_outside() {
1✔
2154
        let area = Rect {
1✔
2155
            x: 2,
1✔
2156
            y: 2,
1✔
2157
            width: 4,
1✔
2158
            height: 4,
1✔
2159
        };
1✔
2160
        let bounds = Rect {
1✔
2161
            x: 0,
1✔
2162
            y: 0,
1✔
2163
            width: 10,
1✔
2164
            height: 10,
1✔
2165
        };
1✔
2166
        let r = clamp_rect(area, bounds);
1✔
2167
        assert_eq!(r.x, 2);
1✔
2168
        assert_eq!(r.y, 2);
1✔
2169

2170
        // non-overlapping
2171
        let area2 = Rect {
1✔
2172
            x: 50,
1✔
2173
            y: 50,
1✔
2174
            width: 1,
1✔
2175
            height: 1,
1✔
2176
        };
1✔
2177
        let r2 = clamp_rect(area2, bounds);
1✔
2178
        assert_eq!(r2, Rect::default());
1✔
2179
    }
1✔
2180

2181
    #[test]
2182
    fn rects_intersect_true_and_false() {
1✔
2183
        let a = Rect {
1✔
2184
            x: 0,
1✔
2185
            y: 0,
1✔
2186
            width: 5,
1✔
2187
            height: 5,
1✔
2188
        };
1✔
2189
        let b = Rect {
1✔
2190
            x: 4,
1✔
2191
            y: 4,
1✔
2192
            width: 5,
1✔
2193
            height: 5,
1✔
2194
        };
1✔
2195
        assert!(rects_intersect(a, b));
1✔
2196
        let c = Rect {
1✔
2197
            x: 10,
1✔
2198
            y: 10,
1✔
2199
            width: 1,
1✔
2200
            height: 1,
1✔
2201
        };
1✔
2202
        assert!(!rects_intersect(a, c));
1✔
2203
    }
1✔
2204

2205
    #[test]
2206
    fn map_layout_node_maps_leaf_to_windowid_app() {
1✔
2207
        let node = LayoutNode::leaf(3usize);
1✔
2208
        let mapped = map_layout_node(&node);
1✔
2209
        match mapped {
1✔
2210
            LayoutNode::Leaf(id) => match id {
1✔
2211
                WindowId::App(r) => assert_eq!(r, 3usize),
1✔
NEW
2212
                _ => panic!("expected App window id"),
×
2213
            },
NEW
2214
            _ => panic!("expected leaf"),
×
2215
        }
2216
    }
1✔
2217

2218
    #[test]
2219
    fn esc_passthrough_default_nonzero() {
1✔
2220
        let d = esc_passthrough_window_default();
1✔
2221
        assert!(d.as_millis() > 0);
1✔
2222
    }
1✔
2223

2224
    #[test]
2225
    fn focus_ring_wraps_and_advances() {
1✔
2226
        let mut ring = FocusRing::new(2usize);
1✔
2227
        ring.set_order(vec![1usize, 2usize, 3usize]);
1✔
2228
        assert_eq!(ring.current(), 2);
1✔
2229
        ring.advance(true);
1✔
2230
        assert_eq!(ring.current(), 3);
1✔
2231
        ring.advance(true);
1✔
2232
        assert_eq!(ring.current(), 1);
1✔
2233
        ring.advance(false);
1✔
2234
        assert_eq!(ring.current(), 3);
1✔
2235
    }
1✔
2236

2237
    #[test]
2238
    fn scroll_state_apply_and_bump() {
1✔
2239
        let mut s = ScrollState::default();
1✔
2240
        s.bump(5);
1✔
2241
        s.apply(100, 10);
1✔
2242
        assert_eq!(s.offset, 5usize);
1✔
2243

2244
        s.offset = 1000;
1✔
2245
        s.apply(20, 5);
1✔
2246
        let max_off = 20usize.saturating_sub(5usize);
1✔
2247
        assert_eq!(s.offset, max_off);
1✔
2248
    }
1✔
2249
}
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