1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
//! The Profiles drawer (UX.md PR2): Open Profiles… pinned to the window's
//! right edge.
//!
//! The drawer is a panel shell — its open state, width, and edge toggle
//! button — around the launcher's own list ([`ProfileLauncherUI`]): the same
//! rows, keys, and actions as the popup. The terminal reflows beside it
//! (`WindowState::profile_drawer_inset`) instead of drawing under it, it
//! joins the overlay stack as a Panel (keys go to it only while its filter
//! holds focus), and Escape in the filter closes it.
use crate::config::Config;
use crate::profile_launcher_ui::{LauncherChoice, ProfileLauncherUI};
use crate::ui_constants::{PROFILE_DRAWER_MAX_WIDTH, PROFILE_DRAWER_MIN_WIDTH};
/// The drawer's panel state.
pub struct ProfileDrawerUI {
/// Whether the drawer is expanded (visible)
pub expanded: bool,
/// Drawer width in pixels
pub width: f32,
}
impl ProfileDrawerUI {
/// Default drawer width
const DEFAULT_WIDTH: f32 = 220.0;
/// Collapsed tab width (the toggle button)
const COLLAPSED_WIDTH: f32 = 12.0;
/// Toggle button height
const BUTTON_HEIGHT: f32 = 30.0;
/// Create a new profile drawer UI (collapsed by default)
pub fn new() -> Self {
Self {
expanded: false,
width: Self::DEFAULT_WIDTH,
}
}
/// Calculate the toggle button rectangle given the window size
pub fn get_toggle_button_rect(
&self,
window_width: f32,
window_height: f32,
) -> (f32, f32, f32, f32) {
// When expanded, button is at left edge of drawer; when collapsed, at right edge of window
let x = if self.expanded {
window_width - self.width - Self::COLLAPSED_WIDTH - 2.0
} else {
window_width - Self::COLLAPSED_WIDTH - 2.0
};
let y = (window_height - Self::BUTTON_HEIGHT) / 2.0;
(x, y, Self::COLLAPSED_WIDTH, Self::BUTTON_HEIGHT)
}
/// Check if a point (in window coordinates) is inside the toggle button
pub fn is_point_in_toggle_button(
&self,
px: f32,
py: f32,
window_width: f32,
window_height: f32,
) -> bool {
let (x, y, w, h) = self.get_toggle_button_rect(window_width, window_height);
px >= x && px <= x + w && py >= y && py <= y + h
}
/// The width the open drawer takes from the terminal, in logical
/// pixels (0 when collapsed): the panel plus its edge button.
pub fn consumed_width(&self) -> f32 {
if self.expanded {
self.width + Self::COLLAPSED_WIDTH + 2.0
} else {
0.0
}
}
/// Toggle drawer expanded state
pub fn toggle(&mut self) {
self.expanded = !self.expanded;
log::info!(
"Profile drawer toggled: {}",
if self.expanded {
"expanded"
} else {
"collapsed"
}
);
}
/// Render the drawer and return what its launcher list chose.
///
/// `bottom_margin` should be set to the height of any floating status bar
/// (e.g. the custom status bar rendered as an `egui::Area`) so the side
/// panel stops above it rather than extending behind it.
pub fn render(
&mut self,
ctx: &mut egui::Ui,
launcher: &mut ProfileLauncherUI,
config: &Config,
bottom_margin: f32,
) -> Option<LauncherChoice> {
let mut choice = None;
let mut toggle_clicked = false;
// Reserve space for any floating status bar rendered via egui::Area.
// egui::Area does not participate in the panel layout, so without this
// spacer the SidePanel would extend behind the status bar.
if bottom_margin > 0.0 && self.expanded {
egui::Panel::bottom("profile_drawer_bottom_margin")
.exact_size(bottom_margin)
.frame(egui::Frame::NONE)
.show(ctx, |_ui| {});
}
// Render the side panel FIRST if expanded, so we get the current width
// This ensures the toggle button position is accurate during resize
let panel_rect = if self.expanded {
let response = egui::Panel::right("profile_drawer")
.resizable(true)
.default_size(self.width)
.min_size(PROFILE_DRAWER_MIN_WIDTH)
.max_size(PROFILE_DRAWER_MAX_WIDTH)
.frame(
egui::Frame::side_top_panel(&ctx.global_style())
.fill(crate::app::overlay::theme::PANEL_FILL)
.inner_margin(egui::Margin::same(8)),
)
.show(ctx, |ui| {
let (picked, close) = launcher.show_pinned(ui);
choice = picked;
if close {
self.expanded = false;
}
});
// Update width from the panel's actual rect
self.width = response.response.rect.width();
Some(response.response.rect)
} else {
None
};
// Calculate toggle button position using the actual panel rect
let button_width = Self::COLLAPSED_WIDTH;
let button_height = Self::BUTTON_HEIGHT;
let viewport_rect = ctx.input(|i| i.viewport_rect());
let button_x = if let Some(rect) = panel_rect {
// Position at left edge of the actual panel rect
rect.left() - button_width - 2.0
} else {
// Collapsed: position at right edge of window
viewport_rect.right() - button_width - 2.0
};
let button_rect = egui::Rect::from_min_size(
egui::pos2(button_x, viewport_rect.center().y - button_height / 2.0),
egui::vec2(button_width, button_height),
);
// Render toggle button (skip if the button is disabled in config)
if config.tabs.show_profile_drawer_button {
egui::Area::new(egui::Id::new("profile_drawer_toggle_area"))
.fixed_pos(button_rect.min)
.order(egui::Order::Foreground)
.show(ctx, |ui| {
let response = ui.allocate_response(button_rect.size(), egui::Sense::click());
let bg_color = if response.hovered() {
crate::app::overlay::theme::DETAIL_FILL
} else {
crate::app::overlay::theme::TOAST_FILL
};
ui.painter().rect_filled(response.rect, 4.0, bg_color);
let arrow = if self.expanded { "▶" } else { "◀" };
ui.painter().text(
response.rect.center(),
egui::Align2::CENTER_CENTER,
arrow,
egui::FontId::proportional(7.0),
crate::app::overlay::theme::TEXT_ON_FILL,
);
// Use clicked_by to only respond to mouse clicks, not keyboard Enter/Space
// This prevents Enter key in the terminal from toggling the drawer
if response.clicked_by(egui::PointerButton::Primary) {
toggle_clicked = true;
}
response.on_hover_text("Profiles");
});
if toggle_clicked {
self.toggle();
if self.expanded {
launcher.prepare_pinned();
}
ctx.request_repaint();
}
}
choice
}
}
impl Default for ProfileDrawerUI {
fn default() -> Self {
Self::new()
}
}