gardesk/garshot / cf74bd4

Browse files

annotate: add text annotation tool

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
cf74bd4a5b9b9da6153047bf029575cb3ccafe95
Parents
b9ae3d1
Tree
f77c9eb

1 changed file

StatusFile+-
A garshot/src/annotate/tools/text.rs 156 0
garshot/src/annotate/tools/text.rsadded
@@ -0,0 +1,156 @@
1
+//! Text tool - add text annotations.
2
+
3
+use super::Tool;
4
+use crate::annotate::state::ToolProperties;
5
+use gartk_core::{InputEvent, Key, MouseButton, Point};
6
+use gartk_render::cairo::Context;
7
+use gartk_render::set_color;
8
+use gartk_x11::CursorShape;
9
+
10
+/// Text annotation tool.
11
+pub struct TextTool {
12
+    /// Position of text anchor.
13
+    position: Option<Point>,
14
+    /// Current text content.
15
+    text: String,
16
+    /// Whether we're in text editing mode.
17
+    editing: bool,
18
+    /// Cursor blink state (for visual feedback).
19
+    cursor_visible: bool,
20
+}
21
+
22
+impl TextTool {
23
+    /// Create a new text tool.
24
+    pub fn new() -> Self {
25
+        Self {
26
+            position: None,
27
+            text: String::new(),
28
+            editing: false,
29
+            cursor_visible: true,
30
+        }
31
+    }
32
+
33
+    /// Draw the text with optional cursor.
34
+    fn draw_text(&self, ctx: &Context, props: &ToolProperties, show_cursor: bool) {
35
+        if let Some(pos) = self.position {
36
+            if self.text.is_empty() && !show_cursor {
37
+                return;
38
+            }
39
+
40
+            set_color(ctx, props.color);
41
+
42
+            // Set font
43
+            ctx.select_font_face("monospace", cairo::FontSlant::Normal, cairo::FontWeight::Normal);
44
+            ctx.set_font_size(props.font_size);
45
+
46
+            // Draw text
47
+            ctx.move_to(pos.x as f64, pos.y as f64 + props.font_size);
48
+
49
+            if self.text.is_empty() {
50
+                // Show placeholder cursor
51
+                if show_cursor && self.cursor_visible {
52
+                    let _ = ctx.show_text("|");
53
+                }
54
+            } else {
55
+                let _ = ctx.show_text(&self.text);
56
+
57
+                // Draw cursor at end
58
+                if show_cursor && self.cursor_visible && self.editing {
59
+                    let extents = match ctx.text_extents(&self.text) {
60
+                        Ok(e) => e,
61
+                        Err(_) => return,
62
+                    };
63
+                    ctx.move_to(
64
+                        pos.x as f64 + extents.width(),
65
+                        pos.y as f64 + props.font_size,
66
+                    );
67
+                    let _ = ctx.show_text("|");
68
+                }
69
+            }
70
+        }
71
+    }
72
+}
73
+
74
+impl Default for TextTool {
75
+    fn default() -> Self {
76
+        Self::new()
77
+    }
78
+}
79
+
80
+impl Tool for TextTool {
81
+    fn handle_event(&mut self, event: &InputEvent, _props: &ToolProperties) -> bool {
82
+        match event {
83
+            InputEvent::MousePress(e) if e.button == Some(MouseButton::Left) => {
84
+                if self.editing {
85
+                    // Clicking while editing commits the text at new position
86
+                    self.editing = false;
87
+                    // Don't reset - keep the text for commit
88
+                    return true;
89
+                }
90
+                // Start new text at click position
91
+                self.position = Some(e.position);
92
+                self.text.clear();
93
+                self.editing = true;
94
+                true
95
+            }
96
+            InputEvent::Key(e) if self.editing && e.pressed => {
97
+                match e.key {
98
+                    Key::Return => {
99
+                        // Finish editing
100
+                        self.editing = false;
101
+                        true
102
+                    }
103
+                    Key::Escape => {
104
+                        // Cancel text
105
+                        self.reset();
106
+                        true
107
+                    }
108
+                    Key::Backspace => {
109
+                        self.text.pop();
110
+                        true
111
+                    }
112
+                    Key::Char(c) => {
113
+                        // Handle character input
114
+                        if !c.is_control() {
115
+                            self.text.push(c);
116
+                            return true;
117
+                        }
118
+                        false
119
+                    }
120
+                    Key::Space => {
121
+                        self.text.push(' ');
122
+                        true
123
+                    }
124
+                    _ => false,
125
+                }
126
+            }
127
+            _ => false,
128
+        }
129
+    }
130
+
131
+    fn draw_preview(&self, ctx: &Context, props: &ToolProperties) {
132
+        self.draw_text(ctx, props, true);
133
+    }
134
+
135
+    fn commit(&self, ctx: &Context, props: &ToolProperties) {
136
+        self.draw_text(ctx, props, false);
137
+    }
138
+
139
+    fn reset(&mut self) {
140
+        self.position = None;
141
+        self.text.clear();
142
+        self.editing = false;
143
+    }
144
+
145
+    fn cursor(&self) -> CursorShape {
146
+        CursorShape::Text
147
+    }
148
+
149
+    fn is_drawing(&self) -> bool {
150
+        self.editing
151
+    }
152
+
153
+    fn can_commit(&self) -> bool {
154
+        self.position.is_some() && !self.text.is_empty() && !self.editing
155
+    }
156
+}