@@ -0,0 +1,154 @@ |
| 1 | +//! Annotation toolbar UI. |
| 2 | + |
| 3 | +use crate::annotate::state::{ToolProperties, ToolType}; |
| 4 | +use gartk_core::{Color, Point, Rect}; |
| 5 | +use gartk_render::{ |
| 6 | + fill_rect, fill_rounded_rect, set_color, stroke_rounded_rect, Surface, |
| 7 | +}; |
| 8 | + |
| 9 | +/// Toolbar height in pixels. |
| 10 | +pub const TOOLBAR_HEIGHT: u32 = 48; |
| 11 | + |
| 12 | +/// Toolbar for selecting annotation tools. |
| 13 | +pub struct Toolbar { |
| 14 | + /// Toolbar bounds. |
| 15 | + rect: Rect, |
| 16 | + /// Currently selected tool. |
| 17 | + selected_tool: ToolType, |
| 18 | + /// Current color. |
| 19 | + current_color: Color, |
| 20 | + /// Current line width. |
| 21 | + line_width: f64, |
| 22 | +} |
| 23 | + |
| 24 | +impl Toolbar { |
| 25 | + /// Tool button width. |
| 26 | + const BUTTON_WIDTH: u32 = 40; |
| 27 | + /// Button padding. |
| 28 | + const PADDING: u32 = 4; |
| 29 | + |
| 30 | + /// Create a new toolbar. |
| 31 | + pub fn new(screen_width: u32) -> Self { |
| 32 | + Self { |
| 33 | + rect: Rect::new(0, 0, screen_width, TOOLBAR_HEIGHT), |
| 34 | + selected_tool: ToolType::Arrow, |
| 35 | + current_color: Color::new(1.0, 0.4, 0.0, 1.0), |
| 36 | + line_width: 3.0, |
| 37 | + } |
| 38 | + } |
| 39 | + |
| 40 | + /// Update toolbar state from annotation state. |
| 41 | + pub fn update(&mut self, tool: ToolType, props: &ToolProperties) { |
| 42 | + self.selected_tool = tool; |
| 43 | + self.current_color = props.color; |
| 44 | + self.line_width = props.line_width; |
| 45 | + } |
| 46 | + |
| 47 | + /// Get the toolbar bounds. |
| 48 | + pub fn rect(&self) -> Rect { |
| 49 | + self.rect |
| 50 | + } |
| 51 | + |
| 52 | + /// Get the height. |
| 53 | + pub fn height(&self) -> u32 { |
| 54 | + TOOLBAR_HEIGHT |
| 55 | + } |
| 56 | + |
| 57 | + /// Calculate button rect for a tool at index. |
| 58 | + fn button_rect(&self, index: usize) -> Rect { |
| 59 | + let x = Self::PADDING as i32 + (index as i32 * (Self::BUTTON_WIDTH as i32 + Self::PADDING as i32)); |
| 60 | + let y = Self::PADDING as i32; |
| 61 | + Rect::new(x, y, Self::BUTTON_WIDTH, TOOLBAR_HEIGHT - Self::PADDING * 2) |
| 62 | + } |
| 63 | + |
| 64 | + /// Draw the toolbar onto a surface. |
| 65 | + pub fn draw(&self, surface: &Surface) -> anyhow::Result<()> { |
| 66 | + let ctx = surface.context()?; |
| 67 | + |
| 68 | + // Draw background |
| 69 | + fill_rect( |
| 70 | + &ctx, |
| 71 | + self.rect, |
| 72 | + Color::new(0.15, 0.15, 0.15, 0.95), |
| 73 | + ); |
| 74 | + |
| 75 | + // Draw tool buttons |
| 76 | + for (i, tool) in ToolType::all().iter().enumerate() { |
| 77 | + let btn_rect = self.button_rect(i); |
| 78 | + let is_selected = *tool == self.selected_tool; |
| 79 | + |
| 80 | + // Button background |
| 81 | + if is_selected { |
| 82 | + fill_rounded_rect( |
| 83 | + &ctx, |
| 84 | + btn_rect, |
| 85 | + 4.0, |
| 86 | + Color::new(1.0, 1.0, 1.0, 0.2), |
| 87 | + ); |
| 88 | + } |
| 89 | + |
| 90 | + // Button label (shortcut key) |
| 91 | + let label = tool.shortcut().to_ascii_uppercase().to_string(); |
| 92 | + set_color(&ctx, Color::WHITE); |
| 93 | + ctx.select_font_face("monospace", cairo::FontSlant::Normal, cairo::FontWeight::Bold); |
| 94 | + ctx.set_font_size(16.0); |
| 95 | + |
| 96 | + let extents = ctx.text_extents(&label)?; |
| 97 | + let text_x = btn_rect.x as f64 + (btn_rect.width as f64 - extents.width()) / 2.0; |
| 98 | + let text_y = btn_rect.y as f64 + (btn_rect.height as f64 + extents.height()) / 2.0; |
| 99 | + |
| 100 | + ctx.move_to(text_x, text_y); |
| 101 | + ctx.show_text(&label)?; |
| 102 | + } |
| 103 | + |
| 104 | + // Draw color preview |
| 105 | + let color_rect = Rect::new( |
| 106 | + (ToolType::all().len() as i32 + 1) * (Self::BUTTON_WIDTH as i32 + Self::PADDING as i32), |
| 107 | + Self::PADDING as i32 + 4, |
| 108 | + 32, |
| 109 | + 32, |
| 110 | + ); |
| 111 | + fill_rounded_rect(&ctx, color_rect, 4.0, self.current_color); |
| 112 | + stroke_rounded_rect( |
| 113 | + &ctx, |
| 114 | + color_rect, |
| 115 | + 4.0, |
| 116 | + Color::WHITE, |
| 117 | + 1.0, |
| 118 | + ); |
| 119 | + |
| 120 | + // Draw line width indicator |
| 121 | + let width_x = color_rect.x + color_rect.width as i32 + Self::PADDING as i32 * 2; |
| 122 | + set_color(&ctx, Color::WHITE); |
| 123 | + ctx.set_font_size(12.0); |
| 124 | + ctx.move_to(width_x as f64, (TOOLBAR_HEIGHT / 2 + 4) as f64); |
| 125 | + ctx.show_text(&format!("{}px", self.line_width as i32))?; |
| 126 | + |
| 127 | + // Draw hint text on right side |
| 128 | + let hint = "Ctrl+Enter: Save | Escape: Cancel"; |
| 129 | + let hint_extents = ctx.text_extents(hint)?; |
| 130 | + let hint_x = self.rect.width as f64 - hint_extents.width() - 16.0; |
| 131 | + set_color(&ctx, Color::new(0.7, 0.7, 0.7, 1.0)); |
| 132 | + ctx.set_font_size(12.0); |
| 133 | + ctx.move_to(hint_x, (TOOLBAR_HEIGHT / 2 + 4) as f64); |
| 134 | + ctx.show_text(hint)?; |
| 135 | + |
| 136 | + Ok(()) |
| 137 | + } |
| 138 | + |
| 139 | + /// Handle click on toolbar, returns selected tool if a button was clicked. |
| 140 | + pub fn handle_click(&self, pos: Point) -> Option<ToolType> { |
| 141 | + if !self.rect.contains_point(pos) { |
| 142 | + return None; |
| 143 | + } |
| 144 | + |
| 145 | + for (i, tool) in ToolType::all().iter().enumerate() { |
| 146 | + let btn_rect = self.button_rect(i); |
| 147 | + if btn_rect.contains_point(pos) { |
| 148 | + return Some(*tool); |
| 149 | + } |
| 150 | + } |
| 151 | + |
| 152 | + None |
| 153 | + } |
| 154 | +} |