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