1use crate::config::Config;
6use crate::profile::{ProfileId, ProfileManager};
7use crate::ui_constants::{PROFILE_DRAWER_MAX_WIDTH, PROFILE_DRAWER_MIN_WIDTH};
8
9#[derive(Debug, Clone, PartialEq)]
11pub enum ProfileDrawerAction {
12 None,
14 OpenProfile(ProfileId),
16 ManageProfiles,
18}
19
20pub struct ProfileDrawerUI {
22 pub expanded: bool,
24 pub selected: Option<ProfileId>,
26 pub hovered: Option<ProfileId>,
28 pub width: f32,
30 pub tag_filter: String,
32}
33
34impl ProfileDrawerUI {
35 const DEFAULT_WIDTH: f32 = 220.0;
37 const COLLAPSED_WIDTH: f32 = 12.0;
39 const BUTTON_HEIGHT: f32 = 30.0;
41
42 pub fn new() -> Self {
44 Self {
45 expanded: false,
46 selected: None,
47 hovered: None,
48 width: Self::DEFAULT_WIDTH,
49 tag_filter: String::new(),
50 }
51 }
52
53 pub fn get_toggle_button_rect(
55 &self,
56 window_width: f32,
57 window_height: f32,
58 ) -> (f32, f32, f32, f32) {
59 let x = if self.expanded {
61 window_width - self.width - Self::COLLAPSED_WIDTH - 2.0
62 } else {
63 window_width - Self::COLLAPSED_WIDTH - 2.0
64 };
65 let y = (window_height - Self::BUTTON_HEIGHT) / 2.0;
66 (x, y, Self::COLLAPSED_WIDTH, Self::BUTTON_HEIGHT)
67 }
68
69 pub fn is_point_in_toggle_button(
71 &self,
72 px: f32,
73 py: f32,
74 window_width: f32,
75 window_height: f32,
76 ) -> bool {
77 let (x, y, w, h) = self.get_toggle_button_rect(window_width, window_height);
78 px >= x && px <= x + w && py >= y && py <= y + h
79 }
80
81 pub fn toggle(&mut self) {
83 self.expanded = !self.expanded;
84 log::info!(
85 "Profile drawer toggled: {}",
86 if self.expanded {
87 "expanded"
88 } else {
89 "collapsed"
90 }
91 );
92 }
93
94 pub fn render(
100 &mut self,
101 ctx: &mut egui::Ui,
102 profile_manager: &ProfileManager,
103 config: &Config,
104 modal_visible: bool,
105 bottom_margin: f32,
106 ) -> ProfileDrawerAction {
107 let mut action = ProfileDrawerAction::None;
108 let mut toggle_clicked = false;
109
110 if bottom_margin > 0.0 {
114 egui::Panel::bottom("profile_drawer_bottom_margin")
115 .exact_size(bottom_margin)
116 .frame(egui::Frame::NONE)
117 .show(ctx, |_ui| {});
118 }
119
120 let panel_rect = if self.expanded {
123 let response = egui::Panel::right("profile_drawer")
124 .resizable(true)
125 .default_size(self.width)
126 .min_size(PROFILE_DRAWER_MIN_WIDTH)
127 .max_size(PROFILE_DRAWER_MAX_WIDTH)
128 .frame(
129 egui::Frame::side_top_panel(&ctx.global_style())
130 .fill(egui::Color32::from_rgba_unmultiplied(30, 30, 30, 245))
131 .inner_margin(egui::Margin::same(8)),
132 )
133 .show(ctx, |ui| {
134 self.render_panel_contents(ui, profile_manager, &mut action);
135 });
136
137 self.width = response.response.rect.width();
139 Some(response.response.rect)
140 } else {
141 None
142 };
143
144 let button_width = Self::COLLAPSED_WIDTH;
146 let button_height = Self::BUTTON_HEIGHT;
147 let viewport_rect = ctx.input(|i| i.viewport_rect());
148
149 let button_x = if let Some(rect) = panel_rect {
150 rect.left() - button_width - 2.0
152 } else {
153 viewport_rect.right() - button_width - 2.0
155 };
156
157 let button_rect = egui::Rect::from_min_size(
158 egui::pos2(button_x, viewport_rect.center().y - button_height / 2.0),
159 egui::vec2(button_width, button_height),
160 );
161
162 if !modal_visible && config.tabs.show_profile_drawer_button {
165 egui::Area::new(egui::Id::new("profile_drawer_toggle_area"))
166 .fixed_pos(button_rect.min)
167 .order(egui::Order::Foreground)
168 .show(ctx, |ui| {
169 let response = ui.allocate_response(button_rect.size(), egui::Sense::click());
170
171 let bg_color = if response.hovered() {
172 egui::Color32::from_rgba_unmultiplied(60, 60, 60, 220)
173 } else {
174 egui::Color32::from_rgba_unmultiplied(40, 40, 40, 200)
175 };
176
177 ui.painter().rect_filled(response.rect, 4.0, bg_color);
178
179 let arrow = if self.expanded { "▶" } else { "◀" };
180 ui.painter().text(
181 response.rect.center(),
182 egui::Align2::CENTER_CENTER,
183 arrow,
184 egui::FontId::proportional(7.0),
185 egui::Color32::WHITE,
186 );
187
188 if response.clicked_by(egui::PointerButton::Primary) {
191 toggle_clicked = true;
192 }
193 });
194
195 if toggle_clicked {
196 self.toggle();
197 ctx.request_repaint();
198 }
199 }
200
201 action
202 }
203
204 fn render_panel_contents(
206 &mut self,
207 ui: &mut egui::Ui,
208 profile_manager: &ProfileManager,
209 action: &mut ProfileDrawerAction,
210 ) {
211 ui.horizontal(|ui| {
213 ui.heading("Profiles");
214 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
215 if ui.small_button("Manage").clicked() {
216 *action = ProfileDrawerAction::ManageProfiles;
217 }
218 });
219 });
220
221 ui.horizontal(|ui| {
223 ui.label("🔍");
224 let response = ui.add(
225 egui::TextEdit::singleline(&mut self.tag_filter)
226 .hint_text("Filter by tag or name...")
227 .desired_width(ui.available_width() - 20.0),
228 );
229 if response.changed() {
230 self.selected = None;
232 }
233 });
234 ui.separator();
235
236 crate::debug_info!(
238 "PROFILE",
239 "Profile drawer render: {} profiles",
240 profile_manager.len()
241 );
242 if profile_manager.is_empty() {
243 ui.vertical_centered(|ui| {
244 ui.add_space(20.0);
245 ui.label(
246 egui::RichText::new("No profiles")
247 .italics()
248 .color(egui::Color32::GRAY),
249 );
250 ui.add_space(10.0);
251 if ui.button("Create Profile").clicked() {
252 *action = ProfileDrawerAction::ManageProfiles;
253 }
254 });
255 } else {
256 let filtered_profiles = profile_manager.filter_by_tags(&self.tag_filter);
258
259 let available = ui.available_height();
261 let button_area_height = 40.0;
262 let scroll_height = (available - button_area_height).max(100.0);
263
264 if filtered_profiles.is_empty() {
265 ui.vertical_centered(|ui| {
266 ui.add_space(20.0);
267 ui.label(
268 egui::RichText::new("No matching profiles")
269 .italics()
270 .color(egui::Color32::GRAY),
271 );
272 ui.add_space(10.0);
273 if ui.small_button("Clear filter").clicked() {
274 self.tag_filter.clear();
275 }
276 });
277 } else {
278 egui::ScrollArea::vertical()
280 .max_height(scroll_height)
281 .auto_shrink([false, false])
282 .show(ui, |ui| {
283 for profile in filtered_profiles {
284 let is_selected = self.selected == Some(profile.id);
285 let is_dynamic = profile.source.is_dynamic();
286
287 let label = if let Some(icon) = &profile.icon {
289 format!("{} {}", icon, profile.name)
290 } else {
291 profile.name.clone()
292 };
293
294 let has_custom = profile.command.is_some()
296 || profile.working_directory.is_some()
297 || profile.parent_id.is_some();
298 let label = if has_custom {
299 format!("{} ...", label)
300 } else {
301 label
302 };
303
304 ui.horizontal(|ui| {
305 let response = ui.selectable_label(is_selected, &label);
307
308 if is_dynamic {
310 ui.label(
311 egui::RichText::new("[dynamic]")
312 .color(egui::Color32::from_rgb(100, 180, 255))
313 .small(),
314 );
315 }
316
317 if response.clicked() {
319 self.selected = Some(profile.id);
320 }
321
322 if response.double_clicked() {
324 *action = ProfileDrawerAction::OpenProfile(profile.id);
325 }
326
327 if let Some(shortcut) = &profile.keyboard_shortcut {
329 ui.with_layout(
330 egui::Layout::right_to_left(egui::Align::Center),
331 |ui| {
332 ui.label(
333 egui::RichText::new(shortcut)
334 .small()
335 .color(egui::Color32::DARK_GRAY),
336 );
337 },
338 );
339 }
340 });
341
342 if !profile.tags.is_empty() {
344 ui.horizontal(|ui| {
345 ui.add_space(16.0); for tag in &profile.tags {
347 ui.label(
348 egui::RichText::new(format!("#{}", tag))
349 .small()
350 .color(egui::Color32::from_rgb(100, 150, 200)),
351 );
352 }
353 });
354 }
355 }
356 });
357 }
358
359 ui.separator();
361 ui.horizontal(|ui| {
362 let open_enabled = self.selected.is_some();
363 if ui
364 .add_enabled(open_enabled, egui::Button::new("Open"))
365 .clicked()
366 && let Some(id) = self.selected
367 {
368 *action = ProfileDrawerAction::OpenProfile(id);
369 }
370 });
371 }
372 }
373}
374
375impl Default for ProfileDrawerUI {
376 fn default() -> Self {
377 Self::new()
378 }
379}