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