@@ -4,7 +4,7 @@ use garfield::ui::pane::SplitDirection; |
| 4 | 4 | use garfield::ui::{AddressBar, Breadcrumb, HelpModal, Pane, Sidebar, StatusBar, TabBar, TabInfo, Toolbar, ToolbarAction, ViewMode, TAB_BAR_HEIGHT, TOOLBAR_HEIGHT}; |
| 5 | 5 | use anyhow::Result; |
| 6 | 6 | use gartk_core::{InputEvent, Key, Point, Rect, Theme}; |
| 7 | | -use gartk_render::{Renderer, Surface}; |
| 7 | +use gartk_render::{Renderer, Surface, TextStyle}; |
| 8 | 8 | use gartk_x11::{Connection, EventLoop, EventLoopConfig, Window, WindowConfig}; |
| 9 | 9 | use std::path::PathBuf; |
| 10 | 10 | use std::time::Instant; |
@@ -55,6 +55,16 @@ pub struct App { |
| 55 | 55 | last_click_time: Option<Instant>, |
| 56 | 56 | /// Last click position for double-click detection. |
| 57 | 57 | last_click_pos: Option<Point>, |
| 58 | + /// Path being dragged for bookmark drop (directory only). |
| 59 | + drag_source_path: Option<PathBuf>, |
| 60 | + /// Name of the item being dragged (for visual feedback). |
| 61 | + drag_label: Option<String>, |
| 62 | + /// Starting position of potential drag. |
| 63 | + drag_start_pos: Option<Point>, |
| 64 | + /// Current mouse position during drag (for visual feedback). |
| 65 | + drag_current_pos: Option<Point>, |
| 66 | + /// Whether drag is actively in progress (moved past threshold). |
| 67 | + drag_active: bool, |
| 58 | 68 | } |
| 59 | 69 | |
| 60 | 70 | impl App { |
@@ -182,6 +192,11 @@ impl App { |
| 182 | 192 | pane_resize_path: None, |
| 183 | 193 | last_click_time: None, |
| 184 | 194 | last_click_pos: None, |
| 195 | + drag_source_path: None, |
| 196 | + drag_label: None, |
| 197 | + drag_start_pos: None, |
| 198 | + drag_current_pos: None, |
| 199 | + drag_active: false, |
| 185 | 200 | }; |
| 186 | 201 | |
| 187 | 202 | app.update_status_bar(); |
@@ -217,8 +232,9 @@ impl App { |
| 217 | 232 | self.handle_mouse_press(pos, &mouse_event.modifiers); |
| 218 | 233 | ev.request_redraw(); |
| 219 | 234 | } |
| 220 | | - InputEvent::MouseRelease(_) => { |
| 221 | | - self.handle_mouse_release(); |
| 235 | + InputEvent::MouseRelease(mouse_event) => { |
| 236 | + let pos = Point::new(mouse_event.position.x, mouse_event.position.y); |
| 237 | + self.handle_mouse_release(pos); |
| 222 | 238 | ev.request_redraw(); |
| 223 | 239 | } |
| 224 | 240 | InputEvent::MouseMove(mouse_event) => { |
@@ -363,11 +379,43 @@ impl App { |
| 363 | 379 | tab.on_click(pos, modifiers); |
| 364 | 380 | } |
| 365 | 381 | } |
| 382 | + |
| 383 | + // Capture drag source from entry at click position (for bookmark drag) |
| 384 | + let entry_at_click = self.focused_pane() |
| 385 | + .and_then(|pane| pane.active_tab()) |
| 386 | + .and_then(|tab| tab.entry_at_point(pos)) |
| 387 | + .filter(|e| e.is_dir()) |
| 388 | + .map(|e| (e.path.clone(), e.name.clone())); |
| 389 | + |
| 390 | + if let Some((path, name)) = entry_at_click { |
| 391 | + self.drag_source_path = Some(path); |
| 392 | + self.drag_label = Some(name); |
| 393 | + self.drag_start_pos = Some(pos); |
| 394 | + self.drag_current_pos = Some(pos); |
| 395 | + self.drag_active = false; |
| 396 | + } |
| 366 | 397 | } |
| 367 | 398 | } |
| 368 | 399 | |
| 369 | 400 | /// Handle mouse release. |
| 370 | | - fn handle_mouse_release(&mut self) { |
| 401 | + fn handle_mouse_release(&mut self, pos: Point) { |
| 402 | + // Handle bookmark drag drop |
| 403 | + if self.drag_active { |
| 404 | + if let Some(path) = self.drag_source_path.take() { |
| 405 | + if self.sidebar.is_bookmark_drop_zone(pos) { |
| 406 | + self.sidebar.add_bookmark(&path); |
| 407 | + } |
| 408 | + } |
| 409 | + } |
| 410 | + |
| 411 | + // Clear drag state |
| 412 | + self.drag_source_path = None; |
| 413 | + self.drag_label = None; |
| 414 | + self.drag_start_pos = None; |
| 415 | + self.drag_current_pos = None; |
| 416 | + self.drag_active = false; |
| 417 | + self.sidebar.set_drop_highlight(false); |
| 418 | + |
| 371 | 419 | // Clear pane resize |
| 372 | 420 | self.pane_resize_path = None; |
| 373 | 421 | |
@@ -400,6 +448,26 @@ impl App { |
| 400 | 448 | } |
| 401 | 449 | } |
| 402 | 450 | |
| 451 | + // Handle bookmark drag in progress |
| 452 | + if self.drag_source_path.is_some() { |
| 453 | + // Update current drag position for visual feedback |
| 454 | + self.drag_current_pos = Some(pos); |
| 455 | + |
| 456 | + // Check if we've moved past the drag threshold (5px) |
| 457 | + if let Some(start_pos) = self.drag_start_pos { |
| 458 | + let distance = ((pos.x - start_pos.x).pow(2) + (pos.y - start_pos.y).pow(2)) as f64; |
| 459 | + if distance.sqrt() > 5.0 { |
| 460 | + self.drag_active = true; |
| 461 | + } |
| 462 | + } |
| 463 | + |
| 464 | + // Update sidebar drop highlight if drag is active |
| 465 | + if self.drag_active { |
| 466 | + let is_over_drop_zone = self.sidebar.is_bookmark_drop_zone(pos); |
| 467 | + self.sidebar.set_drop_highlight(is_over_drop_zone); |
| 468 | + } |
| 469 | + } |
| 470 | + |
| 403 | 471 | self.toolbar.on_mouse_move(pos); |
| 404 | 472 | self.breadcrumb.on_mouse_move(pos); |
| 405 | 473 | self.sidebar.on_mouse_move(pos); |
@@ -563,7 +631,15 @@ impl App { |
| 563 | 631 | |
| 564 | 632 | match key { |
| 565 | 633 | Key::Escape => { |
| 566 | | - if self.address_bar.is_active() { |
| 634 | + // Cancel drag first if active |
| 635 | + if self.drag_active || self.drag_source_path.is_some() { |
| 636 | + self.drag_source_path = None; |
| 637 | + self.drag_label = None; |
| 638 | + self.drag_start_pos = None; |
| 639 | + self.drag_current_pos = None; |
| 640 | + self.drag_active = false; |
| 641 | + self.sidebar.set_drop_highlight(false); |
| 642 | + } else if self.address_bar.is_active() { |
| 567 | 643 | self.address_bar.cancel(); |
| 568 | 644 | } else { |
| 569 | 645 | self.should_quit = true; |
@@ -1097,6 +1173,9 @@ impl App { |
| 1097 | 1173 | // Draw toolbar tooltip overlay (on top of other UI) |
| 1098 | 1174 | self.toolbar.render_tooltip_overlay(&self.renderer)?; |
| 1099 | 1175 | |
| 1176 | + // Draw drag label overlay (on top of other UI) |
| 1177 | + self.render_drag_label()?; |
| 1178 | + |
| 1100 | 1179 | // Draw help modal overlay (on top of everything) |
| 1101 | 1180 | self.help_modal.render(&self.renderer)?; |
| 1102 | 1181 | |
@@ -1107,6 +1186,60 @@ impl App { |
| 1107 | 1186 | Ok(()) |
| 1108 | 1187 | } |
| 1109 | 1188 | |
| 1189 | + /// Render the drag label overlay when dragging a folder. |
| 1190 | + fn render_drag_label(&self) -> Result<()> { |
| 1191 | + // Only render if drag is active and we have a label |
| 1192 | + if !self.drag_active { |
| 1193 | + return Ok(()); |
| 1194 | + } |
| 1195 | + |
| 1196 | + let (label, pos) = match (&self.drag_label, self.drag_current_pos) { |
| 1197 | + (Some(label), Some(pos)) => (label, pos), |
| 1198 | + _ => return Ok(()), |
| 1199 | + }; |
| 1200 | + |
| 1201 | + let theme = self.renderer.theme(); |
| 1202 | + |
| 1203 | + // Create text style for the drag label |
| 1204 | + let text_style = TextStyle::new() |
| 1205 | + .font_family(&theme.font_family) |
| 1206 | + .font_size(theme.font_size) |
| 1207 | + .color(theme.item_foreground); |
| 1208 | + |
| 1209 | + // Measure the text to size the background |
| 1210 | + let text_size = self.renderer.measure_text(label, &text_style)?; |
| 1211 | + |
| 1212 | + // Position the label slightly offset from the cursor |
| 1213 | + let label_x = pos.x + 16; |
| 1214 | + let label_y = pos.y + 8; |
| 1215 | + let padding = 8; |
| 1216 | + |
| 1217 | + // Draw background with rounded appearance |
| 1218 | + let bg_rect = Rect::new( |
| 1219 | + label_x - padding, |
| 1220 | + label_y - padding / 2, |
| 1221 | + text_size.width + (padding * 2) as u32, |
| 1222 | + text_size.height + padding as u32, |
| 1223 | + ); |
| 1224 | + |
| 1225 | + // Semi-transparent dark background |
| 1226 | + let bg_color = gartk_core::Color::from_u8(40, 40, 45, 230); |
| 1227 | + self.renderer.fill_rect(bg_rect, bg_color)?; |
| 1228 | + |
| 1229 | + // Border |
| 1230 | + let border_color = theme.selection_background.with_alpha(0.8); |
| 1231 | + self.renderer.stroke_rect(bg_rect, border_color, 1.0)?; |
| 1232 | + |
| 1233 | + // Folder icon prefix |
| 1234 | + let icon_style = text_style.clone().color(theme.selection_background); |
| 1235 | + self.renderer.text("*", label_x as f64, label_y as f64, &icon_style)?; |
| 1236 | + |
| 1237 | + // Draw the text |
| 1238 | + self.renderer.text(label, (label_x + 14) as f64, label_y as f64, &text_style)?; |
| 1239 | + |
| 1240 | + Ok(()) |
| 1241 | + } |
| 1242 | + |
| 1110 | 1243 | /// Blit the rendered surface to the window. |
| 1111 | 1244 | fn blit_surface(&mut self) -> Result<()> { |
| 1112 | 1245 | let size = self.renderer.size(); |