1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
//! UI state query helpers for `WindowState`.
//!
//! Covers:
//! - egui pointer / keyboard ownership queries (`is_egui_using_pointer`, `is_egui_using_keyboard`)
//! - Modal-visibility query helpers (`any_modal_ui_visible`, `has_egui_text_overlay_visible`)
//! - Scrollbar visibility logic (`should_show_scrollbar`)
use super::WindowState;
impl WindowState {
// ========================================================================
// egui / UI state queries
// ========================================================================
/// Check if egui is currently using the pointer (mouse is over an egui UI element)
pub(crate) fn is_egui_using_pointer(&self) -> bool {
// AI Inspector resize handle uses direct pointer tracking (not egui widgets),
// so egui doesn't know about it. Check explicitly to prevent mouse events
// from reaching the terminal during resize drag or initial click on the handle.
if self.overlay_ui.ai_inspector.wants_pointer() {
return true;
}
// Before first render, egui state is unreliable - allow mouse events through
if !self.egui.initialized {
return false;
}
// Always check egui context - the tab bar is always rendered via egui
// and can consume pointer events (e.g., close button clicks)
if let Some(ctx) = &self.egui.ctx {
ctx.egui_is_using_pointer() || ctx.egui_wants_pointer_input()
} else {
false
}
}
/// Canonical check: is any modal UI overlay visible?
///
/// The single source of truth for "should input be blocked from the
/// terminal because a dialog, picker, or menu is open?" It is derived
/// from the window's overlay stack (UX.md OV1): an overlay joins by
/// being registered in [`crate::app::overlay::OverlayId`] with a kind
/// whose `guards_terminal()` is true — never by being listed here.
///
/// Docked panels (assistant, profile drawer), inline edits, and the
/// keyboard modes that consume their own keys are stack members too,
/// but they do not guard the terminal as a whole; they own keys through
/// egui focus or their own handlers (see `is_egui_using_keyboard`).
pub(crate) fn any_modal_ui_visible(&self) -> bool {
crate::app::overlay::OverlayId::ALL
.iter()
.any(|id| id.guards_terminal() && self.overlay_is_open(*id))
}
/// Check if any egui overlay with text input is visible.
/// Used to route clipboard operations (paste/copy/select-all) to egui
/// instead of the terminal when a modal dialog or the AI inspector is active.
pub(crate) fn has_egui_text_overlay_visible(&self) -> bool {
self.any_modal_ui_visible() || self.overlay_ui.ai_inspector.open
}
/// Check if egui is currently using keyboard input (e.g., text input or ComboBox has focus)
pub(crate) fn is_egui_using_keyboard(&self) -> bool {
// Ask egui only while an overlay that can hold egui focus is open
// (`OverlayId::may_hold_egui_focus`): every guarding overlay, the
// docked panels — the profile drawer's tag filter must keep typed
// keys off the PTY (B61) — the pane rename field, and the in-app
// menu. The tab rename field lives inside the tab bar, not above it.
// Settings are handled by the standalone SettingsWindow.
let any_ui_visible = crate::app::overlay::OverlayId::ALL
.iter()
.any(|id| id.may_hold_egui_focus() && self.overlay_is_open(*id))
|| self.tab_bar_ui.is_renaming();
if !any_ui_visible {
return false;
}
// Check egui context for keyboard usage
if let Some(ctx) = &self.egui.ctx {
ctx.egui_wants_keyboard_input()
} else {
false
}
}
// ========================================================================
// Scrollbar visibility
// ========================================================================
/// Determine if scrollbar should be visible based on autohide setting and recent activity
pub(crate) fn should_show_scrollbar(&self) -> bool {
let tab = match self.tab_manager.active_tab() {
Some(t) => t,
None => return false,
};
// No scrollbar needed if no scrollback available
if tab.active_cache().scrollback_len == 0 {
return false;
}
// Always show when dragging or moving
if tab.active_scroll_state().dragging {
return true;
}
// If autohide disabled, always show
if self.config.load().scrollbar.scrollbar_autohide_delay == 0 {
return true;
}
// If scrolled away from bottom, keep visible
if tab.active_scroll_state().offset > 0 || tab.active_scroll_state().target_offset > 0 {
return true;
}
// Show when pointer is near the scrollbar edge (hover reveal)
if let Some(window) = &self.window {
let padding = 32.0; // px hover band
let width = window.inner_size().width as f64;
let near_right = self.config.load().scrollbar.scrollbar_position != "left"
&& (width - tab.active_mouse().position.0) <= padding;
let near_left = self.config.load().scrollbar.scrollbar_position == "left"
&& tab.active_mouse().position.0 <= padding;
if near_left || near_right {
return true;
}
}
// Otherwise, hide after delay
tab.active_scroll_state()
.last_activity
.elapsed()
.as_millis()
< self.config.load().scrollbar.scrollbar_autohide_delay as u128
}
}