@@ -39,6 +39,16 @@ pub enum ToolbarAction { |
| 39 | 39 | GoUp, |
| 40 | 40 | /// Show help modal. |
| 41 | 41 | Help, |
| 42 | + /// Copy selected files. |
| 43 | + Copy, |
| 44 | + /// Cut selected files. |
| 45 | + Cut, |
| 46 | + /// Paste files from clipboard. |
| 47 | + Paste, |
| 48 | + /// Delete selected files to trash. |
| 49 | + Trash, |
| 50 | + /// Create new folder. |
| 51 | + NewFolder, |
| 42 | 52 | } |
| 43 | 53 | |
| 44 | 54 | /// A toolbar button. |
@@ -57,6 +67,8 @@ pub struct Toolbar { |
| 57 | 67 | active_view: ToolbarAction, |
| 58 | 68 | can_go_back: bool, |
| 59 | 69 | can_go_forward: bool, |
| 70 | + has_selection: bool, |
| 71 | + has_clipboard: bool, |
| 60 | 72 | } |
| 61 | 73 | |
| 62 | 74 | impl Toolbar { |
@@ -69,6 +81,8 @@ impl Toolbar { |
| 69 | 81 | active_view: ToolbarAction::ViewList, |
| 70 | 82 | can_go_back: false, |
| 71 | 83 | can_go_forward: false, |
| 84 | + has_selection: false, |
| 85 | + has_clipboard: false, |
| 72 | 86 | }; |
| 73 | 87 | toolbar.layout_buttons(); |
| 74 | 88 | toolbar |
@@ -91,6 +105,12 @@ impl Toolbar { |
| 91 | 105 | self.can_go_forward = can_forward; |
| 92 | 106 | } |
| 93 | 107 | |
| 108 | + /// Set file operation state. |
| 109 | + pub fn set_file_ops_state(&mut self, has_selection: bool, has_clipboard: bool) { |
| 110 | + self.has_selection = has_selection; |
| 111 | + self.has_clipboard = has_clipboard; |
| 112 | + } |
| 113 | + |
| 94 | 114 | /// Layout buttons. |
| 95 | 115 | fn layout_buttons(&mut self) { |
| 96 | 116 | self.buttons.clear(); |
@@ -150,6 +170,26 @@ impl Toolbar { |
| 150 | 170 | x += BUTTON_SIZE as i32 + BUTTON_PADDING as i32; |
| 151 | 171 | } |
| 152 | 172 | |
| 173 | + x += GROUP_SEPARATOR as i32; |
| 174 | + |
| 175 | + // File operation buttons |
| 176 | + let file_buttons = [ |
| 177 | + (ToolbarAction::Copy, "Copy (Ctrl+C)"), |
| 178 | + (ToolbarAction::Cut, "Cut (Ctrl+X)"), |
| 179 | + (ToolbarAction::Paste, "Paste (Ctrl+V)"), |
| 180 | + (ToolbarAction::Trash, "Delete (Del)"), |
| 181 | + (ToolbarAction::NewFolder, "New Folder (Ctrl+Shift+N)"), |
| 182 | + ]; |
| 183 | + |
| 184 | + for (action, tooltip) in file_buttons { |
| 185 | + self.buttons.push(ToolbarButton { |
| 186 | + action, |
| 187 | + bounds: Rect::new(x, y, BUTTON_SIZE, BUTTON_SIZE), |
| 188 | + tooltip, |
| 189 | + }); |
| 190 | + x += BUTTON_SIZE as i32 + BUTTON_PADDING as i32; |
| 191 | + } |
| 192 | + |
| 153 | 193 | // Help button (right-aligned) |
| 154 | 194 | let help_x = self.bounds.x + self.bounds.width as i32 - BUTTON_SIZE as i32 - BUTTON_PADDING as i32; |
| 155 | 195 | self.buttons.push(ToolbarButton { |
@@ -173,10 +213,13 @@ impl Toolbar { |
| 173 | 213 | pub fn on_click(&self, pos: Point) -> Option<ToolbarAction> { |
| 174 | 214 | for button in &self.buttons { |
| 175 | 215 | if button.bounds.contains_point(pos) { |
| 176 | | - // Don't trigger disabled nav buttons |
| 216 | + // Don't trigger disabled buttons |
| 177 | 217 | match button.action { |
| 178 | 218 | ToolbarAction::GoBack if !self.can_go_back => return None, |
| 179 | 219 | ToolbarAction::GoForward if !self.can_go_forward => return None, |
| 220 | + ToolbarAction::Copy | ToolbarAction::Cut | ToolbarAction::Trash |
| 221 | + if !self.has_selection => return None, |
| 222 | + ToolbarAction::Paste if !self.has_clipboard => return None, |
| 180 | 223 | _ => return Some(button.action), |
| 181 | 224 | } |
| 182 | 225 | } |
@@ -283,6 +326,8 @@ impl Toolbar { |
| 283 | 326 | let is_disabled = match button.action { |
| 284 | 327 | ToolbarAction::GoBack => !self.can_go_back, |
| 285 | 328 | ToolbarAction::GoForward => !self.can_go_forward, |
| 329 | + ToolbarAction::Copy | ToolbarAction::Cut | ToolbarAction::Trash => !self.has_selection, |
| 330 | + ToolbarAction::Paste => !self.has_clipboard, |
| 286 | 331 | _ => false, |
| 287 | 332 | }; |
| 288 | 333 | |
@@ -321,6 +366,11 @@ impl Toolbar { |
| 321 | 366 | ToolbarAction::SplitHorizontal => self.draw_split_h_icon(renderer, cx, cy, icon_color)?, |
| 322 | 367 | ToolbarAction::SplitVertical => self.draw_split_v_icon(renderer, cx, cy, icon_color)?, |
| 323 | 368 | ToolbarAction::Help => self.draw_help_icon(renderer, cx, cy, icon_color)?, |
| 369 | + ToolbarAction::Copy => self.draw_copy_icon(renderer, cx, cy, icon_color)?, |
| 370 | + ToolbarAction::Cut => self.draw_cut_icon(renderer, cx, cy, icon_color)?, |
| 371 | + ToolbarAction::Paste => self.draw_paste_icon(renderer, cx, cy, icon_color)?, |
| 372 | + ToolbarAction::Trash => self.draw_trash_icon(renderer, cx, cy, icon_color)?, |
| 373 | + ToolbarAction::NewFolder => self.draw_new_folder_icon(renderer, cx, cy, icon_color)?, |
| 324 | 374 | } |
| 325 | 375 | |
| 326 | 376 | Ok(()) |
@@ -457,4 +507,144 @@ impl Toolbar { |
| 457 | 507 | renderer.fill_rect(Rect::new((cx - 1.0) as i32, (cy + 3.0) as i32, 3, 3), color)?; |
| 458 | 508 | Ok(()) |
| 459 | 509 | } |
| 510 | + |
| 511 | + fn draw_copy_icon(&self, renderer: &Renderer, cx: f64, cy: f64, color: gartk_core::Color) -> Result<()> { |
| 512 | + // Two overlapping rectangles (copy symbol) |
| 513 | + let w = 7.0; |
| 514 | + let h = 9.0; |
| 515 | + let offset = 3.0; |
| 516 | + |
| 517 | + // Back rectangle (slightly offset) |
| 518 | + let bx = cx - w/2.0 - offset/2.0; |
| 519 | + let by = cy - h/2.0 - offset/2.0; |
| 520 | + renderer.stroke_rect(Rect::new(bx as i32, by as i32, w as u32, h as u32), color, 1.5)?; |
| 521 | + |
| 522 | + // Front rectangle |
| 523 | + let fx = cx - w/2.0 + offset/2.0; |
| 524 | + let fy = cy - h/2.0 + offset/2.0; |
| 525 | + renderer.fill_rect(Rect::new(fx as i32, fy as i32, w as u32, h as u32), color.with_alpha(0.3))?; |
| 526 | + renderer.stroke_rect(Rect::new(fx as i32, fy as i32, w as u32, h as u32), color, 1.5)?; |
| 527 | + Ok(()) |
| 528 | + } |
| 529 | + |
| 530 | + fn draw_cut_icon(&self, renderer: &Renderer, cx: f64, cy: f64, color: gartk_core::Color) -> Result<()> { |
| 531 | + // Scissors shape - two circles with crossed lines |
| 532 | + let r = 3.0; |
| 533 | + let segments = 12; |
| 534 | + |
| 535 | + // Left circle (handle) |
| 536 | + let lcx = cx - 4.0; |
| 537 | + let lcy = cy + 3.0; |
| 538 | + for i in 0..segments { |
| 539 | + let a1 = (i as f64 / segments as f64) * std::f64::consts::TAU; |
| 540 | + let a2 = ((i + 1) as f64 / segments as f64) * std::f64::consts::TAU; |
| 541 | + renderer.line( |
| 542 | + lcx + r * a1.cos(), lcy + r * a1.sin(), |
| 543 | + lcx + r * a2.cos(), lcy + r * a2.sin(), |
| 544 | + color, 1.5 |
| 545 | + )?; |
| 546 | + } |
| 547 | + |
| 548 | + // Right circle (handle) |
| 549 | + let rcx = cx + 4.0; |
| 550 | + let rcy = cy + 3.0; |
| 551 | + for i in 0..segments { |
| 552 | + let a1 = (i as f64 / segments as f64) * std::f64::consts::TAU; |
| 553 | + let a2 = ((i + 1) as f64 / segments as f64) * std::f64::consts::TAU; |
| 554 | + renderer.line( |
| 555 | + rcx + r * a1.cos(), rcy + r * a1.sin(), |
| 556 | + rcx + r * a2.cos(), rcy + r * a2.sin(), |
| 557 | + color, 1.5 |
| 558 | + )?; |
| 559 | + } |
| 560 | + |
| 561 | + // Blades (crossed lines going up) |
| 562 | + renderer.line(lcx, lcy - r, cx + 2.0, cy - 6.0, color, 1.5)?; |
| 563 | + renderer.line(rcx, rcy - r, cx - 2.0, cy - 6.0, color, 1.5)?; |
| 564 | + Ok(()) |
| 565 | + } |
| 566 | + |
| 567 | + fn draw_paste_icon(&self, renderer: &Renderer, cx: f64, cy: f64, color: gartk_core::Color) -> Result<()> { |
| 568 | + // Clipboard with paper |
| 569 | + let w = 10.0; |
| 570 | + let h = 12.0; |
| 571 | + |
| 572 | + // Clipboard outline |
| 573 | + let bx = cx - w/2.0; |
| 574 | + let by = cy - h/2.0 + 1.0; |
| 575 | + renderer.stroke_rect(Rect::new(bx as i32, by as i32, w as u32, h as u32), color, 1.5)?; |
| 576 | + |
| 577 | + // Clip at top (small rectangle) |
| 578 | + let clip_w = 5.0; |
| 579 | + let clip_h = 3.0; |
| 580 | + let clip_x = cx - clip_w/2.0; |
| 581 | + let clip_y = by - clip_h/2.0; |
| 582 | + renderer.fill_rect(Rect::new(clip_x as i32, clip_y as i32, clip_w as u32, clip_h as u32), color)?; |
| 583 | + |
| 584 | + // Lines on clipboard (document content) |
| 585 | + let line_y1 = by + 4.0; |
| 586 | + let line_y2 = by + 7.0; |
| 587 | + renderer.line(bx + 2.0, line_y1, bx + w - 2.0, line_y1, color, 1.5)?; |
| 588 | + renderer.line(bx + 2.0, line_y2, bx + w - 2.0, line_y2, color, 1.5)?; |
| 589 | + Ok(()) |
| 590 | + } |
| 591 | + |
| 592 | + fn draw_trash_icon(&self, renderer: &Renderer, cx: f64, cy: f64, color: gartk_core::Color) -> Result<()> { |
| 593 | + // Trash can shape |
| 594 | + let w = 10.0; |
| 595 | + let h = 10.0; |
| 596 | + |
| 597 | + // Body (trapezoid-ish) |
| 598 | + let bx = cx - w/2.0; |
| 599 | + let by = cy - h/2.0 + 2.0; |
| 600 | + let bw = w; |
| 601 | + let bh = h - 2.0; |
| 602 | + renderer.line(bx, by, bx + 1.0, by + bh, color, 1.5)?; |
| 603 | + renderer.line(bx + 1.0, by + bh, bx + bw - 1.0, by + bh, color, 1.5)?; |
| 604 | + renderer.line(bx + bw - 1.0, by + bh, bx + bw, by, color, 1.5)?; |
| 605 | + |
| 606 | + // Lid |
| 607 | + let lid_y = cy - h/2.0; |
| 608 | + renderer.line(cx - w/2.0 - 1.0, lid_y, cx + w/2.0 + 1.0, lid_y, color, 2.0)?; |
| 609 | + |
| 610 | + // Handle on lid |
| 611 | + renderer.line(cx - 2.0, lid_y, cx - 2.0, lid_y - 2.0, color, 1.5)?; |
| 612 | + renderer.line(cx - 2.0, lid_y - 2.0, cx + 2.0, lid_y - 2.0, color, 1.5)?; |
| 613 | + renderer.line(cx + 2.0, lid_y - 2.0, cx + 2.0, lid_y, color, 1.5)?; |
| 614 | + |
| 615 | + // Vertical lines inside |
| 616 | + renderer.line(cx - 2.0, by + 2.0, cx - 2.0, by + bh - 2.0, color, 1.0)?; |
| 617 | + renderer.line(cx, by + 2.0, cx, by + bh - 2.0, color, 1.0)?; |
| 618 | + renderer.line(cx + 2.0, by + 2.0, cx + 2.0, by + bh - 2.0, color, 1.0)?; |
| 619 | + Ok(()) |
| 620 | + } |
| 621 | + |
| 622 | + fn draw_new_folder_icon(&self, renderer: &Renderer, cx: f64, cy: f64, color: gartk_core::Color) -> Result<()> { |
| 623 | + // Folder shape with plus sign |
| 624 | + let w = 12.0; |
| 625 | + let h = 9.0; |
| 626 | + let tab_w = 5.0; |
| 627 | + let tab_h = 2.0; |
| 628 | + |
| 629 | + let fx = cx - w/2.0; |
| 630 | + let fy = cy - h/2.0; |
| 631 | + |
| 632 | + // Folder tab |
| 633 | + renderer.line(fx, fy + tab_h, fx, fy, color, 1.5)?; |
| 634 | + renderer.line(fx, fy, fx + tab_w, fy, color, 1.5)?; |
| 635 | + renderer.line(fx + tab_w, fy, fx + tab_w + 2.0, fy + tab_h, color, 1.5)?; |
| 636 | + |
| 637 | + // Folder body |
| 638 | + renderer.line(fx + tab_w + 2.0, fy + tab_h, fx + w, fy + tab_h, color, 1.5)?; |
| 639 | + renderer.line(fx + w, fy + tab_h, fx + w, fy + h, color, 1.5)?; |
| 640 | + renderer.line(fx + w, fy + h, fx, fy + h, color, 1.5)?; |
| 641 | + renderer.line(fx, fy + h, fx, fy + tab_h, color, 1.5)?; |
| 642 | + |
| 643 | + // Plus sign in center |
| 644 | + let plus_size = 4.0; |
| 645 | + let pcy = cy + 1.0; |
| 646 | + renderer.line(cx - plus_size/2.0, pcy, cx + plus_size/2.0, pcy, color, 1.5)?; |
| 647 | + renderer.line(cx, pcy - plus_size/2.0, cx, pcy + plus_size/2.0, color, 1.5)?; |
| 648 | + Ok(()) |
| 649 | + } |
| 460 | 650 | } |