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.is_using_pointer() || ctx.wants_pointer_input()
} else {
false
}
}
/// Canonical check: is any modal UI overlay visible?
///
/// This is the single source of truth for "should input be blocked from the terminal
/// because a modal dialog is open?" When adding a new modal panel, add it here.
///
/// Note: Side panels (ai_inspector, profile drawer) and inline edit states
/// (tab_bar_ui.is_renaming()) are NOT modals — they are checked separately
/// at call sites that need them. The resize overlay is also not a modal.
pub(crate) fn any_modal_ui_visible(&self) -> bool {
self.overlay_ui.help_ui.visible
|| self.overlay_ui.clipboard_history_ui.visible
|| self.overlay_ui.command_history_ui.visible
|| self.overlay_ui.search_ui.visible
|| self.overlay_ui.tmux_session_picker_ui.visible
|| self.overlay_ui.shader_install_ui.visible
|| self.overlay_ui.integrations_ui.visible
|| self.overlay_ui.ssh_connect_ui.is_visible()
|| self.overlay_ui.remote_shell_install_ui.is_visible()
|| self.overlay_ui.quit_confirmation_ui.is_visible()
}
/// 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 {
// If any UI panel is visible, check if egui wants keyboard input
// Note: Settings are handled by standalone SettingsWindow, not embedded UI
// Note: Profile drawer does NOT block input - only modal dialogs do
// Also check ai_inspector (side panel with text input) and tab rename (inline edit)
let any_ui_visible = self.any_modal_ui_visible()
|| self.overlay_ui.ai_inspector.open
|| 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.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.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.scrollbar_position != "left"
&& (width - tab.active_mouse().position.0) <= padding;
let near_left = self.config.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.scrollbar_autohide_delay as u128
}
}