Skip to main content

agg_gui/widgets/text_area/
widget_impl.rs

1use super::*;
2
3impl Widget for TextArea {
4    fn type_name(&self) -> &'static str {
5        "TextArea"
6    }
7    fn bounds(&self) -> Rect {
8        self.bounds
9    }
10    fn set_bounds(&mut self, b: Rect) {
11        self.bounds = b;
12    }
13    fn children(&self) -> &[Box<dyn Widget>] {
14        &self.children
15    }
16    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
17        &mut self.children
18    }
19
20    fn is_focusable(&self) -> bool {
21        true
22    }
23
24    fn margin(&self) -> Insets {
25        self.base.margin
26    }
27    fn h_anchor(&self) -> HAnchor {
28        self.base.h_anchor
29    }
30    fn v_anchor(&self) -> VAnchor {
31        self.base.v_anchor
32    }
33    fn min_size(&self) -> Size {
34        self.base.min_size
35    }
36    fn max_size(&self) -> Size {
37        self.base.max_size
38    }
39
40    fn measure_min_height(&self, available_w: f64) -> f64 {
41        // Wrap our text at the supplied width and report the total
42        // visual height + vertical padding.  This is what an
43        // ancestor `Window::tight_content_fit` sums to derive a
44        // window minimum that prevents text from going off-screen,
45        // even when this widget sits in a flex-fill slot whose
46        // `layout` would otherwise just return the available area.
47        //
48        // Cheap: `wrap_text_indexed` is the same function `layout`
49        // already calls; it doesn't mutate any cache.  Always at
50        // least one line so the cursor has somewhere to sit.
51        let inner_w = (available_w - self.padding * 2.0).max(1.0);
52        let lines = wrap_text_indexed(
53            &self.font,
54            &self.edit.borrow().text,
55            self.font_size,
56            inner_w,
57        );
58        let line_h = self.font_size * 1.35;
59        (lines.len().max(1) as f64) * line_h + self.padding * 2.0
60    }
61
62    fn layout(&mut self, available: Size) -> Size {
63        // Fill the slot we're given.  A parent that allocates us via
64        // a flex-weight gets everything; a parent that asks for our
65        // natural size gets the same (the caller is opting into
66        // "whatever you want" with `available`).
67        let w = available.width.max(self.padding * 2.0 + 20.0);
68        let h = available
69            .height
70            .max(self.padding * 2.0 + self.font_size * 1.6);
71        self.bounds = Rect::new(0.0, 0.0, w, h);
72        let inner_w = (w - self.padding * 2.0).max(1.0);
73        self.refresh_wrap(inner_w);
74        Size::new(w, h)
75    }
76
77    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
78        let v = ctx.visuals();
79        let w = self.bounds.width;
80        let h = self.bounds.height;
81
82        // Background — theme widget fill.
83        ctx.set_fill_color(v.widget_bg);
84        ctx.begin_path();
85        ctx.rounded_rect(0.0, 0.0, w, h, 4.0);
86        ctx.fill();
87
88        // Clip content to the padded inner rect so overflow text can't
89        // leak across the border.
90        ctx.clip_rect(
91            self.padding,
92            self.padding,
93            (w - self.padding * 2.0).max(0.0),
94            (h - self.padding * 2.0).max(0.0),
95        );
96
97        ctx.set_font(Arc::clone(&self.font));
98        ctx.set_font_size(self.font_size);
99
100        // ── Selection highlight ───────────────────────────────────
101        let st = self.edit.borrow().clone();
102        if st.cursor != st.anchor {
103            let lo = st.cursor.min(st.anchor);
104            let hi = st.cursor.max(st.anchor);
105            let hl_color = if self.focused {
106                v.selection_bg
107            } else {
108                v.selection_bg_unfocused
109            };
110            ctx.set_fill_color(hl_color);
111            for (i, line) in self.cached_lines.iter().enumerate() {
112                if line.end < lo || line.start > hi {
113                    continue;
114                }
115                let sel_s = lo.max(line.start) - line.start;
116                let sel_e = hi.min(line.end) - line.start;
117                let sel_e = sel_e.min(line.text.len());
118                if sel_e <= sel_s {
119                    continue;
120                }
121                let x0 =
122                    self.padding + measure_advance(&self.font, &line.text[..sel_s], self.font_size);
123                let x1 =
124                    self.padding + measure_advance(&self.font, &line.text[..sel_e], self.font_size);
125                let line_top = h - self.padding - i as f64 * self.cached_line_h;
126                let line_bottom = line_top - self.cached_line_h;
127                ctx.begin_path();
128                ctx.rect(x0, line_bottom, x1 - x0, self.cached_line_h);
129                ctx.fill();
130            }
131        }
132
133        // ── Text ───────────────────────────────────────────────────
134        ctx.set_fill_color(v.text_color);
135        // Tight metrics for baseline positioning — the glyph baseline
136        // sits `descent` above each line's bottom edge.
137        let m = ctx.measure_text("Ag").unwrap_or_default();
138        for (i, line) in self.cached_lines.iter().enumerate() {
139            if line.text.is_empty() {
140                continue;
141            }
142            let line_top = h - self.padding - i as f64 * self.cached_line_h;
143            let line_bottom = line_top - self.cached_line_h;
144            let baseline_y =
145                line_bottom + (self.cached_line_h - (m.ascent - m.descent)) * 0.5 + m.descent;
146            ctx.fill_text(&line.text, self.padding, baseline_y);
147        }
148
149        // ── Placeholder when empty + unfocused ─────────────────────
150        if st.text.is_empty() && !self.focused {
151            ctx.set_fill_color(v.text_dim);
152            let line_top = h - self.padding;
153            let line_bottom = line_top - self.cached_line_h;
154            let baseline_y =
155                line_bottom + (self.cached_line_h - (m.ascent - m.descent)) * 0.5 + m.descent;
156            ctx.fill_text("Type here…", self.padding, baseline_y);
157        }
158
159        ctx.reset_clip();
160
161        // ── Border ────────────────────────────────────────────────
162        let border = if self.focused {
163            v.accent
164        } else if self.hovered {
165            v.widget_stroke_active
166        } else {
167            v.widget_stroke
168        };
169        let line_width = if self.focused { 2.0 } else { 1.0 };
170        ctx.set_stroke_color(border);
171        ctx.set_line_width(line_width);
172        ctx.begin_path();
173        let inset = line_width * 0.5;
174        ctx.rounded_rect(
175            inset,
176            inset,
177            (w - line_width).max(0.0),
178            (h - line_width).max(0.0),
179            4.0,
180        );
181        ctx.stroke();
182    }
183
184    fn paint_overlay(&mut self, ctx: &mut dyn DrawCtx) {
185        // Cursor blink (drawn in overlay so the blink doesn't
186        // invalidate a cached text bitmap).  500 ms half-cycle.
187        if !self.focused {
188            return;
189        }
190        if let Some(t) = self.focus_time {
191            let phase = (t.elapsed().as_millis() / 500) as u64;
192            self.blink_last_phase.set(phase);
193            if phase % 2 == 1 {
194                return;
195            }
196        }
197        let st = self.edit.borrow().clone();
198        let p = self.pos_for_cursor(st.cursor);
199        let v = ctx.visuals();
200        ctx.set_stroke_color(v.text_color);
201        ctx.set_line_width(1.5);
202        ctx.begin_path();
203        ctx.move_to(p.x, p.y);
204        ctx.line_to(p.x, p.y + self.cached_line_h);
205        ctx.stroke();
206    }
207
208    fn on_event(&mut self, event: &Event) -> EventResult {
209        match event {
210            Event::MouseMove { pos } => {
211                let was = self.hovered;
212                self.hovered = self.hit_test(*pos);
213                if self.hovered {
214                    set_cursor_icon(CursorIcon::Text);
215                }
216                if self.selecting_drag {
217                    let off = self.byte_offset_at(*pos);
218                    self.move_cursor_to(off, /*with_selection=*/ true);
219                    crate::animation::request_draw();
220                    return EventResult::Consumed;
221                }
222                if was != self.hovered {
223                    crate::animation::request_draw();
224                    return EventResult::Consumed;
225                }
226                EventResult::Ignored
227            }
228            Event::MouseDown {
229                button: MouseButton::Left,
230                pos,
231                modifiers,
232            } => {
233                let off = self.byte_offset_at(*pos);
234                self.move_cursor_to(off, /*with_selection=*/ modifiers.shift);
235                self.selecting_drag = true;
236                crate::animation::request_draw();
237                EventResult::Consumed
238            }
239            Event::MouseUp {
240                button: MouseButton::Left,
241                ..
242            } => {
243                self.selecting_drag = false;
244                EventResult::Consumed
245            }
246            Event::FocusGained => {
247                self.focused = true;
248                self.focus_time = Some(Instant::now());
249                crate::animation::request_draw();
250                EventResult::Ignored
251            }
252            Event::FocusLost => {
253                self.focused = false;
254                self.selecting_drag = false;
255                crate::animation::request_draw();
256                EventResult::Ignored
257            }
258            Event::KeyDown { key, modifiers } => {
259                let shift = modifiers.shift;
260                let cmd = modifiers.ctrl || modifiers.meta;
261                match key {
262                    Key::ArrowLeft => {
263                        self.move_char(-1, shift);
264                    }
265                    Key::ArrowRight => {
266                        self.move_char(1, shift);
267                    }
268                    Key::ArrowUp => {
269                        self.move_line(-1, shift);
270                    }
271                    Key::ArrowDown => {
272                        self.move_line(1, shift);
273                    }
274                    Key::Home => {
275                        let cur = self.edit.borrow().cursor;
276                        let line = self.line_for_cursor(cur);
277                        let start = self.cached_lines[line].start;
278                        self.move_cursor_to(start, shift);
279                    }
280                    Key::End => {
281                        let cur = self.edit.borrow().cursor;
282                        let line = self.line_for_cursor(cur);
283                        let end = self.cached_lines[line].end;
284                        self.move_cursor_to(end, shift);
285                    }
286                    Key::Backspace => {
287                        self.delete(-1);
288                    }
289                    Key::Delete => {
290                        self.delete(1);
291                    }
292                    Key::Enter => {
293                        self.insert_str("\n");
294                    }
295                    Key::Tab => {
296                        self.insert_str("    ");
297                    }
298                    Key::Char('a') | Key::Char('A') if cmd => {
299                        // Select-all — set anchor to start, cursor to
300                        // end.  Common Ctrl+A shortcut.
301                        let len = self.edit.borrow().text.len();
302                        self.move_cursor_to(0, false);
303                        self.move_cursor_to(len, true);
304                    }
305                    Key::Char('c') | Key::Char('C') if cmd => {
306                        let st = self.edit.borrow();
307                        let (lo, hi) = (st.cursor.min(st.anchor), st.cursor.max(st.anchor));
308                        if hi > lo {
309                            let sel = st.text[lo..hi].to_string();
310                            drop(st);
311                            clipboard_set(&sel);
312                        }
313                    }
314                    Key::Char('x') | Key::Char('X') if cmd => {
315                        let st = self.edit.borrow();
316                        let (lo, hi) = (st.cursor.min(st.anchor), st.cursor.max(st.anchor));
317                        if hi > lo {
318                            let sel = st.text[lo..hi].to_string();
319                            drop(st);
320                            clipboard_set(&sel);
321                            self.delete(0);
322                        }
323                    }
324                    Key::Char('v') | Key::Char('V') if cmd => {
325                        if let Some(t) = clipboard_get() {
326                            self.insert_str(&t);
327                        }
328                    }
329                    Key::Char(c) if !cmd => {
330                        let mut s = [0u8; 4];
331                        self.insert_str(c.encode_utf8(&mut s));
332                    }
333                    _ => return EventResult::Ignored,
334                }
335                self.focus_time = Some(Instant::now());
336                crate::animation::request_draw();
337                EventResult::Consumed
338            }
339            _ => EventResult::Ignored,
340        }
341    }
342
343    fn hit_test(&self, local_pos: Point) -> bool {
344        local_pos.x >= 0.0
345            && local_pos.x <= self.bounds.width
346            && local_pos.y >= 0.0
347            && local_pos.y <= self.bounds.height
348    }
349
350    fn properties(&self) -> Vec<(&'static str, String)> {
351        let st = self.edit.borrow();
352        vec![
353            ("len", st.text.len().to_string()),
354            ("cursor", st.cursor.to_string()),
355            ("lines", self.cached_lines.len().to_string()),
356            ("focused", self.focused.to_string()),
357        ]
358    }
359
360    fn needs_draw(&self) -> bool {
361        self.focused
362    }
363}