@@ -1,12 +1,13 @@ |
| 1 | 1 | //! Application state and event loop. |
| 2 | 2 | |
| 3 | +use crate::PickerConfig; |
| 3 | 4 | use garfield::core::{ |
| 4 | 5 | Clipboard, ClipboardOperation, DragTarget, FileOperation, FileDragController, ImagePreviewLoader, PdfPreviewLoader, PreviewLoader, UndoStack, |
| 5 | 6 | copy_files, move_files, delete_files, create_directory, |
| 6 | | - trash_files, restore_from_trash, |
| 7 | + trash_files, restore_from_trash, matches_any_filter, |
| 7 | 8 | }; |
| 8 | 9 | use garfield::ui::pane::SplitDirection; |
| 9 | | -use garfield::ui::{AddressBar, AppPickerDialog, AppPickerResult, Breadcrumb, ConfirmDialog, ConflictAction, ConflictDialog, ContextMenu, ContextMenuAction, ContextType, DialogResult, HelpModal, IconSize, InputDialog, InputResult, Pane, ProgressDialog, Sidebar, StatusBar, TabBar, TabInfo, Toolbar, ToolbarAction, ViewMode, TAB_BAR_HEIGHT, TOOLBAR_HEIGHT}; |
| 10 | +use garfield::ui::{AddressBar, AppPickerDialog, AppPickerResult, Breadcrumb, ConfirmDialog, ConflictAction, ConflictDialog, ContextMenu, ContextMenuAction, ContextType, DialogResult, HelpModal, IconSize, InputDialog, InputResult, Pane, PaneToolbarClick, PickerToolbar, PickerToolbarClick, ProgressDialog, Sidebar, StatusBar, TabBar, TabBarClickResult, TabInfo, Toolbar, ToolbarAction, ViewMode, TAB_BAR_HEIGHT, TOOLBAR_HEIGHT, PICKER_TOOLBAR_HEIGHT}; |
| 10 | 11 | use anyhow::Result; |
| 11 | 12 | use gartk_core::{InputEvent, Key, MouseButton, Point, Rect, Theme}; |
| 12 | 13 | use gartk_render::{Renderer, TextStyle}; |
@@ -104,6 +105,10 @@ pub struct App { |
| 104 | 105 | pdf_preview_loader: PdfPreviewLoader, |
| 105 | 106 | /// X11 clipboard manager for system clipboard integration. |
| 106 | 107 | x11_clipboard: ClipboardManager, |
| 108 | + /// Picker mode configuration (None for normal browser). |
| 109 | + picker_config: PickerConfig, |
| 110 | + /// Picker toolbar (only used in picker mode). |
| 111 | + picker_toolbar: Option<PickerToolbar>, |
| 107 | 112 | } |
| 108 | 113 | |
| 109 | 114 | /// State for a paste operation with conflicts. |
@@ -120,31 +125,48 @@ struct PendingPaste { |
| 120 | 125 | |
| 121 | 126 | impl App { |
| 122 | 127 | /// Create a new application. |
| 123 | | - pub fn new(start_dir: Option<PathBuf>) -> Result<Self> { |
| 128 | + pub fn new(start_dir: Option<PathBuf>, picker_config: PickerConfig) -> Result<Self> { |
| 124 | 129 | // Connect to X11 |
| 125 | 130 | let conn = Connection::connect(None)?; |
| 126 | 131 | |
| 127 | 132 | // Get primary monitor for window sizing |
| 128 | 133 | let monitor = gartk_x11::primary_monitor(&conn)?; |
| 129 | 134 | |
| 130 | | - // Calculate window size (70% of screen) |
| 131 | | - let width = (monitor.rect.width as f64 * 0.7) as u32; |
| 132 | | - let height = (monitor.rect.height as f64 * 0.7) as u32; |
| 135 | + // Calculate window size (70% of screen, smaller for picker mode) |
| 136 | + let scale = if picker_config.is_picker() { 0.5 } else { 0.7 }; |
| 137 | + let width = (monitor.rect.width as f64 * scale) as u32; |
| 138 | + let height = (monitor.rect.height as f64 * scale) as u32; |
| 133 | 139 | let x = monitor.rect.x + (monitor.rect.width as i32 - width as i32) / 2; |
| 134 | 140 | let y = monitor.rect.y + (monitor.rect.height as i32 - height as i32) / 2; |
| 135 | 141 | |
| 136 | | - // Create window |
| 137 | | - let window = Window::create( |
| 138 | | - conn.clone(), |
| 139 | | - WindowConfig::default() |
| 140 | | - .title("garfield") |
| 141 | | - .class("garfield") |
| 142 | | - .position(x, y) |
| 143 | | - .size(width, height) |
| 144 | | - .transparent(false), |
| 145 | | - )?; |
| 142 | + // Window title depends on mode |
| 143 | + let title = if picker_config.is_picker() { |
| 144 | + picker_config.title.clone().unwrap_or_else(|| { |
| 145 | + if picker_config.mode.is_directory_mode() { |
| 146 | + "Select Folder".to_string() |
| 147 | + } else { |
| 148 | + "Open File".to_string() |
| 149 | + } |
| 150 | + }) |
| 151 | + } else { |
| 152 | + "garfield".to_string() |
| 153 | + }; |
| 146 | 154 | |
| 147 | | - window.focus()?; |
| 155 | + // Create window - use Dialog type for picker mode |
| 156 | + let mut window_config = WindowConfig::default() |
| 157 | + .title(&title) |
| 158 | + .class("garfield") |
| 159 | + .position(x, y) |
| 160 | + .size(width, height) |
| 161 | + .transparent(false); |
| 162 | + |
| 163 | + // Use Dialog window type for picker mode (better focus handling) |
| 164 | + if picker_config.is_picker() { |
| 165 | + window_config = window_config.window_type(gartk_x11::WindowType::Dialog); |
| 166 | + } |
| 167 | + |
| 168 | + let window = Window::create(conn.clone(), window_config)?; |
| 169 | + conn.flush()?; |
| 148 | 170 | |
| 149 | 171 | // Create X11 clipboard manager for system clipboard integration |
| 150 | 172 | let x11_clipboard = ClipboardManager::new(conn.clone(), window.id())?; |
@@ -174,6 +196,26 @@ impl App { |
| 174 | 196 | ); |
| 175 | 197 | let toolbar = Toolbar::new(toolbar_bounds); |
| 176 | 198 | |
| 199 | + // Create picker toolbar at BOTTOM of window (only if in picker mode) |
| 200 | + let picker_toolbar = if picker_config.is_picker() { |
| 201 | + let picker_toolbar_bounds = Rect::new( |
| 202 | + sidebar_w as i32, |
| 203 | + (height - PICKER_TOOLBAR_HEIGHT) as i32, |
| 204 | + width - sidebar_w, |
| 205 | + PICKER_TOOLBAR_HEIGHT, |
| 206 | + ); |
| 207 | + let mut pt = PickerToolbar::new(picker_toolbar_bounds, picker_config.accept_label.clone()); |
| 208 | + // Set filter description if we have filters |
| 209 | + let filters = picker_config.mode.filters(); |
| 210 | + if !filters.is_empty() { |
| 211 | + let desc = format!("Filter: {}", filters.join(", ")); |
| 212 | + pt.set_filter_description(Some(desc)); |
| 213 | + } |
| 214 | + Some(pt) |
| 215 | + } else { |
| 216 | + None |
| 217 | + }; |
| 218 | + |
| 177 | 219 | // Create breadcrumb (below toolbar) |
| 178 | 220 | let breadcrumb_bounds = Rect::new( |
| 179 | 221 | sidebar_w as i32, |
@@ -227,11 +269,17 @@ impl App { |
| 227 | 269 | let app_picker = AppPickerDialog::new(Rect::new(0, 0, width, height)); |
| 228 | 270 | |
| 229 | 271 | // Content area bounds (for panes) |
| 272 | + // In picker mode, picker toolbar replaces status bar at bottom |
| 273 | + let footer_height = if picker_config.is_picker() { |
| 274 | + PICKER_TOOLBAR_HEIGHT |
| 275 | + } else { |
| 276 | + STATUS_BAR_HEIGHT |
| 277 | + }; |
| 230 | 278 | let content_bounds = Rect::new( |
| 231 | 279 | sidebar_w as i32, |
| 232 | 280 | header_height as i32, |
| 233 | 281 | width - sidebar_w, |
| 234 | | - height - header_height - STATUS_BAR_HEIGHT, |
| 282 | + height - header_height - footer_height, |
| 235 | 283 | ); |
| 236 | 284 | |
| 237 | 285 | // Create root pane with initial tab |
@@ -286,6 +334,8 @@ impl App { |
| 286 | 334 | image_preview_loader: ImagePreviewLoader::new(), |
| 287 | 335 | pdf_preview_loader: PdfPreviewLoader::new(), |
| 288 | 336 | x11_clipboard, |
| 337 | + picker_config, |
| 338 | + picker_toolbar, |
| 289 | 339 | }; |
| 290 | 340 | |
| 291 | 341 | app.update_status_bar(); |
@@ -303,6 +353,96 @@ impl App { |
| 303 | 353 | self.root_pane.leaf_by_id_mut(self.focused_pane_id) |
| 304 | 354 | } |
| 305 | 355 | |
| 356 | + /// Check if the current selection is valid for picker mode. |
| 357 | + fn has_valid_picker_selection(&self) -> bool { |
| 358 | + let Some(pane) = self.focused_pane() else { |
| 359 | + return false; |
| 360 | + }; |
| 361 | + let Some(tab) = pane.active_tab() else { |
| 362 | + return false; |
| 363 | + }; |
| 364 | + |
| 365 | + let selected = tab.selected_entries(); |
| 366 | + let filters = self.picker_config.mode.filters(); |
| 367 | + |
| 368 | + // In directory mode, we can always accept (use current directory if nothing selected) |
| 369 | + if self.picker_config.mode.is_directory_mode() { |
| 370 | + // If nothing selected, the current directory is the selection |
| 371 | + if selected.is_empty() { |
| 372 | + return true; |
| 373 | + } |
| 374 | + // Otherwise, at least one directory must be selected |
| 375 | + return selected.iter().any(|e| e.is_dir()); |
| 376 | + } |
| 377 | + |
| 378 | + // In file mode, we need at least one file selected that matches filters |
| 379 | + // If multiple is disabled, we need exactly one matching file |
| 380 | + let matching_file_count = selected.iter() |
| 381 | + .filter(|e| !e.is_dir() && matches_any_filter(e, filters)) |
| 382 | + .count(); |
| 383 | + |
| 384 | + if matching_file_count == 0 { |
| 385 | + return false; |
| 386 | + } |
| 387 | + |
| 388 | + if !self.picker_config.mode.allows_multiple() && matching_file_count > 1 { |
| 389 | + return false; |
| 390 | + } |
| 391 | + |
| 392 | + true |
| 393 | + } |
| 394 | + |
| 395 | + /// Get the selected paths for picker mode. |
| 396 | + fn get_picker_selection(&self) -> Vec<PathBuf> { |
| 397 | + let Some(pane) = self.focused_pane() else { |
| 398 | + return Vec::new(); |
| 399 | + }; |
| 400 | + let Some(tab) = pane.active_tab() else { |
| 401 | + return Vec::new(); |
| 402 | + }; |
| 403 | + |
| 404 | + let filters = self.picker_config.mode.filters(); |
| 405 | + |
| 406 | + if self.picker_config.mode.is_directory_mode() { |
| 407 | + // In directory mode, return selected directories or current directory |
| 408 | + let dirs: Vec<_> = tab.selected_entries().iter() |
| 409 | + .filter(|e| e.is_dir()) |
| 410 | + .map(|e| e.path.clone()) |
| 411 | + .collect(); |
| 412 | + |
| 413 | + if dirs.is_empty() { |
| 414 | + // Return current directory |
| 415 | + vec![tab.current_path().to_path_buf()] |
| 416 | + } else { |
| 417 | + dirs |
| 418 | + } |
| 419 | + } else { |
| 420 | + // In file mode, return selected files that match filters |
| 421 | + tab.selected_entries().iter() |
| 422 | + .filter(|e| !e.is_dir() && matches_any_filter(e, filters)) |
| 423 | + .map(|e| e.path.clone()) |
| 424 | + .collect() |
| 425 | + } |
| 426 | + } |
| 427 | + |
| 428 | + /// Output picker selection and exit. |
| 429 | + fn accept_picker_selection(&mut self) { |
| 430 | + let paths = self.get_picker_selection(); |
| 431 | + |
| 432 | + // Output paths to stdout (one per line) |
| 433 | + for path in &paths { |
| 434 | + println!("{}", path.display()); |
| 435 | + } |
| 436 | + |
| 437 | + self.should_quit = true; |
| 438 | + } |
| 439 | + |
| 440 | + /// Cancel picker and exit with no output. |
| 441 | + fn cancel_picker(&mut self) { |
| 442 | + // Exit with code 1 to indicate cancellation |
| 443 | + self.should_quit = true; |
| 444 | + } |
| 445 | + |
| 306 | 446 | /// Run the application event loop. |
| 307 | 447 | pub fn run(&mut self) -> Result<()> { |
| 308 | 448 | let mut event_loop = EventLoop::new(&self.window, EventLoopConfig::default())?; |
@@ -527,19 +667,48 @@ impl App { |
| 527 | 667 | return; |
| 528 | 668 | } |
| 529 | 669 | |
| 530 | | - // Handle tab bar close button clicks (non-drag) |
| 531 | | - if let Some((tab_index, is_close)) = self.tab_bar.on_click(pos) { |
| 532 | | - if is_close { |
| 533 | | - self.close_tab(tab_index); |
| 670 | + // Handle tab bar clicks (non-drag) |
| 671 | + match self.tab_bar.on_click(pos) { |
| 672 | + TabBarClickResult::Tab(tab_index, is_close) => { |
| 673 | + if is_close { |
| 674 | + self.close_tab(tab_index); |
| 675 | + } |
| 676 | + // Tab selection happens on mouse release if not dragged |
| 677 | + return; |
| 678 | + } |
| 679 | + TabBarClickResult::NewTab => { |
| 680 | + self.new_tab(); |
| 681 | + return; |
| 682 | + } |
| 683 | + TabBarClickResult::None => {} |
| 684 | + } |
| 685 | + } |
| 686 | + |
| 687 | + // Check picker toolbar clicks (if in picker mode) |
| 688 | + if let Some(picker_toolbar) = &self.picker_toolbar { |
| 689 | + match picker_toolbar.on_click(pos) { |
| 690 | + PickerToolbarClick::Accept => { |
| 691 | + self.accept_picker_selection(); |
| 692 | + return; |
| 693 | + } |
| 694 | + PickerToolbarClick::Cancel => { |
| 695 | + self.cancel_picker(); |
| 696 | + return; |
| 534 | 697 | } |
| 535 | | - // Tab selection happens on mouse release if not dragged |
| 698 | + PickerToolbarClick::None => {} |
| 699 | + } |
| 700 | + } else { |
| 701 | + // Check normal toolbar clicks |
| 702 | + if let Some(action) = self.toolbar.on_click(pos) { |
| 703 | + self.handle_toolbar_action(action); |
| 536 | 704 | return; |
| 537 | 705 | } |
| 538 | 706 | } |
| 539 | 707 | |
| 540 | | - // Check toolbar clicks |
| 541 | | - if let Some(action) = self.toolbar.on_click(pos) { |
| 542 | | - self.handle_toolbar_action(action); |
| 708 | + // Check pane toolbar clicks (view mode buttons) |
| 709 | + if let PaneToolbarClick::ViewMode(_mode) = self.root_pane.on_toolbar_click(pos) { |
| 710 | + self.sync_toolbar_view(); |
| 711 | + self.update_status_bar(); |
| 543 | 712 | return; |
| 544 | 713 | } |
| 545 | 714 | |
@@ -888,10 +1057,15 @@ impl App { |
| 888 | 1057 | } |
| 889 | 1058 | |
| 890 | 1059 | // Check hover states - only redraw if any changed |
| 891 | | - needs_redraw |= self.toolbar.on_mouse_move(pos); |
| 1060 | + if let Some(picker_toolbar) = &mut self.picker_toolbar { |
| 1061 | + needs_redraw |= picker_toolbar.on_mouse_move(pos); |
| 1062 | + } else { |
| 1063 | + needs_redraw |= self.toolbar.on_mouse_move(pos); |
| 1064 | + } |
| 892 | 1065 | needs_redraw |= self.breadcrumb.on_mouse_move(pos); |
| 893 | 1066 | needs_redraw |= self.sidebar.on_mouse_move(pos); |
| 894 | 1067 | needs_redraw |= self.tab_bar.on_mouse_move(pos); |
| 1068 | + needs_redraw |= self.root_pane.on_toolbar_mouse_move(pos); |
| 895 | 1069 | |
| 896 | 1070 | let mut is_dragging = false; |
| 897 | 1071 | let mut selection_count = 0; |
@@ -1003,6 +1177,12 @@ impl App { |
| 1003 | 1177 | return; |
| 1004 | 1178 | } |
| 1005 | 1179 | |
| 1180 | + // Handle Escape in picker mode (cancel) |
| 1181 | + if *key == Key::Escape && self.picker_config.is_picker() { |
| 1182 | + self.cancel_picker(); |
| 1183 | + return; |
| 1184 | + } |
| 1185 | + |
| 1006 | 1186 | // F1 toggles help |
| 1007 | 1187 | if *key == Key::F1 { |
| 1008 | 1188 | self.help_modal.show(); |
@@ -1350,13 +1530,7 @@ impl App { |
| 1350 | 1530 | |
| 1351 | 1531 | /// Create a new tab in the focused pane. |
| 1352 | 1532 | fn new_tab(&mut self) { |
| 1353 | | - let path = if let Some(pane) = self.focused_pane() { |
| 1354 | | - pane.active_tab().map(|t| t.current_path().clone()) |
| 1355 | | - } else { |
| 1356 | | - None |
| 1357 | | - }; |
| 1358 | | - |
| 1359 | | - let path = path.unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"))); |
| 1533 | + let path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/")); |
| 1360 | 1534 | |
| 1361 | 1535 | if let Some(pane) = self.focused_pane_mut() { |
| 1362 | 1536 | pane.add_tab(path); |
@@ -1438,17 +1612,21 @@ impl App { |
| 1438 | 1612 | |
| 1439 | 1613 | /// Split the focused pane horizontally. |
| 1440 | 1614 | fn split_horizontal(&mut self) { |
| 1441 | | - let path = if let Some(pane) = self.focused_pane() { |
| 1442 | | - pane.active_tab().map(|t| t.current_path().clone()) |
| 1615 | + let (path, view_mode) = if let Some(pane) = self.focused_pane() { |
| 1616 | + if let Some(tab) = pane.active_tab() { |
| 1617 | + (Some(tab.current_path().clone()), Some(tab.view_mode())) |
| 1618 | + } else { |
| 1619 | + (None, None) |
| 1620 | + } |
| 1443 | 1621 | } else { |
| 1444 | | - None |
| 1622 | + (None, None) |
| 1445 | 1623 | }; |
| 1446 | 1624 | |
| 1447 | 1625 | let path = path.unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"))); |
| 1448 | 1626 | let new_id = self.next_pane_id; |
| 1449 | 1627 | |
| 1450 | 1628 | if let Some(pane) = self.focused_pane_mut() { |
| 1451 | | - if pane.split(SplitDirection::Horizontal, path, new_id).is_some() { |
| 1629 | + if pane.split(SplitDirection::Horizontal, path, new_id, view_mode).is_some() { |
| 1452 | 1630 | self.next_pane_id += 1; |
| 1453 | 1631 | self.focused_pane_id = new_id; |
| 1454 | 1632 | } |
@@ -1461,17 +1639,21 @@ impl App { |
| 1461 | 1639 | |
| 1462 | 1640 | /// Split the focused pane vertically. |
| 1463 | 1641 | fn split_vertical(&mut self) { |
| 1464 | | - let path = if let Some(pane) = self.focused_pane() { |
| 1465 | | - pane.active_tab().map(|t| t.current_path().clone()) |
| 1642 | + let (path, view_mode) = if let Some(pane) = self.focused_pane() { |
| 1643 | + if let Some(tab) = pane.active_tab() { |
| 1644 | + (Some(tab.current_path().clone()), Some(tab.view_mode())) |
| 1645 | + } else { |
| 1646 | + (None, None) |
| 1647 | + } |
| 1466 | 1648 | } else { |
| 1467 | | - None |
| 1649 | + (None, None) |
| 1468 | 1650 | }; |
| 1469 | 1651 | |
| 1470 | 1652 | let path = path.unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"))); |
| 1471 | 1653 | let new_id = self.next_pane_id; |
| 1472 | 1654 | |
| 1473 | 1655 | if let Some(pane) = self.focused_pane_mut() { |
| 1474 | | - if pane.split(SplitDirection::Vertical, path, new_id).is_some() { |
| 1656 | + if pane.split(SplitDirection::Vertical, path, new_id, view_mode).is_some() { |
| 1475 | 1657 | self.next_pane_id += 1; |
| 1476 | 1658 | self.focused_pane_id = new_id; |
| 1477 | 1659 | } |
@@ -1553,6 +1735,29 @@ impl App { |
| 1553 | 1735 | /// Enter the selected entry. |
| 1554 | 1736 | fn enter_selected(&mut self) { |
| 1555 | 1737 | self.status_bar.clear_status_message(); |
| 1738 | + |
| 1739 | + // In picker mode, check if we should accept the selection instead of navigating |
| 1740 | + if self.picker_config.is_picker() { |
| 1741 | + if let Some(pane) = self.focused_pane() { |
| 1742 | + if let Some(tab) = pane.active_tab() { |
| 1743 | + let selected = tab.selected_entries(); |
| 1744 | + if !selected.is_empty() { |
| 1745 | + // If it's a directory in non-directory mode, navigate into it |
| 1746 | + if !self.picker_config.mode.is_directory_mode() { |
| 1747 | + let first = &selected[0]; |
| 1748 | + if first.is_dir() { |
| 1749 | + // Fall through to normal enter behavior |
| 1750 | + } else { |
| 1751 | + // File selected - accept it |
| 1752 | + self.accept_picker_selection(); |
| 1753 | + return; |
| 1754 | + } |
| 1755 | + } |
| 1756 | + } |
| 1757 | + } |
| 1758 | + } |
| 1759 | + } |
| 1760 | + |
| 1556 | 1761 | if let Some(pane) = self.focused_pane_mut() { |
| 1557 | 1762 | if let Some(tab) = pane.active_tab_mut() { |
| 1558 | 1763 | tab.enter_selected(); |
@@ -2962,12 +3167,13 @@ impl App { |
| 2962 | 3167 | TAB_BAR_HEIGHT, |
| 2963 | 3168 | )); |
| 2964 | 3169 | |
| 2965 | | - self.toolbar.set_bounds(Rect::new( |
| 3170 | + let toolbar_bounds = Rect::new( |
| 2966 | 3171 | sidebar_w as i32, |
| 2967 | 3172 | TAB_BAR_HEIGHT as i32, |
| 2968 | 3173 | width - sidebar_w, |
| 2969 | 3174 | TOOLBAR_HEIGHT, |
| 2970 | | - )); |
| 3175 | + ); |
| 3176 | + self.toolbar.set_bounds(toolbar_bounds); |
| 2971 | 3177 | |
| 2972 | 3178 | let breadcrumb_bounds = Rect::new( |
| 2973 | 3179 | sidebar_w as i32, |
@@ -2978,20 +3184,38 @@ impl App { |
| 2978 | 3184 | self.breadcrumb.set_bounds(breadcrumb_bounds); |
| 2979 | 3185 | self.address_bar.set_bounds(breadcrumb_bounds); |
| 2980 | 3186 | |
| 3187 | + // Calculate footer height (status bar + picker toolbar if present) |
| 3188 | + let footer_height = if self.picker_toolbar.is_some() { |
| 3189 | + PICKER_TOOLBAR_HEIGHT // Picker toolbar replaces status bar |
| 3190 | + } else { |
| 3191 | + STATUS_BAR_HEIGHT |
| 3192 | + }; |
| 3193 | + |
| 2981 | 3194 | let content_bounds = Rect::new( |
| 2982 | 3195 | sidebar_w as i32, |
| 2983 | 3196 | header_height as i32, |
| 2984 | 3197 | width - sidebar_w, |
| 2985 | | - height - header_height - STATUS_BAR_HEIGHT, |
| 3198 | + height - header_height - footer_height, |
| 2986 | 3199 | ); |
| 2987 | 3200 | self.root_pane.set_bounds(content_bounds); |
| 2988 | 3201 | |
| 2989 | | - self.status_bar.set_bounds(Rect::new( |
| 2990 | | - sidebar_w as i32, |
| 2991 | | - (height - STATUS_BAR_HEIGHT) as i32, |
| 2992 | | - width - sidebar_w, |
| 2993 | | - STATUS_BAR_HEIGHT, |
| 2994 | | - )); |
| 3202 | + // Update picker toolbar bounds at bottom (if in picker mode) |
| 3203 | + if let Some(ref mut picker_toolbar) = self.picker_toolbar { |
| 3204 | + picker_toolbar.set_bounds(Rect::new( |
| 3205 | + sidebar_w as i32, |
| 3206 | + (height - PICKER_TOOLBAR_HEIGHT) as i32, |
| 3207 | + width - sidebar_w, |
| 3208 | + PICKER_TOOLBAR_HEIGHT, |
| 3209 | + )); |
| 3210 | + } else { |
| 3211 | + // Only show status bar when not in picker mode |
| 3212 | + self.status_bar.set_bounds(Rect::new( |
| 3213 | + sidebar_w as i32, |
| 3214 | + (height - STATUS_BAR_HEIGHT) as i32, |
| 3215 | + width - sidebar_w, |
| 3216 | + STATUS_BAR_HEIGHT, |
| 3217 | + )); |
| 3218 | + } |
| 2995 | 3219 | |
| 2996 | 3220 | self.help_modal.set_bounds(Rect::new(0, 0, width, height)); |
| 2997 | 3221 | self.confirm_dialog.set_bounds(Rect::new(0, 0, width, height)); |
@@ -3018,7 +3242,7 @@ impl App { |
| 3018 | 3242 | // Draw tab bar |
| 3019 | 3243 | self.tab_bar.render(&self.renderer)?; |
| 3020 | 3244 | |
| 3021 | | - // Update and draw toolbar |
| 3245 | + // Update and draw toolbar (or picker toolbar) |
| 3022 | 3246 | let (can_back, can_forward) = if let Some(pane) = self.focused_pane() { |
| 3023 | 3247 | if let Some(tab) = pane.active_tab() { |
| 3024 | 3248 | (tab.can_go_back(), tab.can_go_forward()) |
@@ -3028,6 +3252,8 @@ impl App { |
| 3028 | 3252 | } else { |
| 3029 | 3253 | (false, false) |
| 3030 | 3254 | }; |
| 3255 | + |
| 3256 | + // Always render the regular toolbar |
| 3031 | 3257 | self.toolbar.set_nav_state(can_back, can_forward); |
| 3032 | 3258 | self.toolbar.render(&self.renderer)?; |
| 3033 | 3259 | |
@@ -3051,8 +3277,17 @@ impl App { |
| 3051 | 3277 | // Draw pane content |
| 3052 | 3278 | self.root_pane.render(&self.renderer, Some(self.focused_pane_id))?; |
| 3053 | 3279 | |
| 3054 | | - // Draw status bar |
| 3055 | | - self.status_bar.render(&self.renderer)?; |
| 3280 | + // Draw status bar or picker toolbar at bottom |
| 3281 | + if self.picker_toolbar.is_some() { |
| 3282 | + // Picker mode: draw picker toolbar instead of status bar |
| 3283 | + let has_valid_selection = self.has_valid_picker_selection(); |
| 3284 | + let picker_toolbar = self.picker_toolbar.as_mut().unwrap(); |
| 3285 | + picker_toolbar.set_accept_enabled(has_valid_selection); |
| 3286 | + picker_toolbar.render(&self.renderer)?; |
| 3287 | + } else { |
| 3288 | + // Normal mode: draw status bar |
| 3289 | + self.status_bar.render(&self.renderer)?; |
| 3290 | + } |
| 3056 | 3291 | |
| 3057 | 3292 | // Draw toolbar tooltip overlay (on top of other UI) |
| 3058 | 3293 | self.toolbar.render_tooltip_overlay(&self.renderer)?; |