agg_gui/widgets/text_area/
widget_impl.rs1use super::*;
2use crate::color::Color;
3
4pub(crate) fn segment_highlight(
18 text: &str,
19 spans: &[(usize, usize, Color)],
20 base_color: Color,
21) -> Vec<(usize, usize, Color)> {
22 let len = text.len();
23 let mut valid: Vec<(usize, usize, Color)> = spans
24 .iter()
25 .copied()
26 .filter(|&(s, e, _)| {
27 s < e && e <= len && text.is_char_boundary(s) && text.is_char_boundary(e)
28 })
29 .collect();
30 valid.sort_by_key(|&(s, _, _)| s);
31
32 let mut out: Vec<(usize, usize, Color)> = Vec::new();
33 let mut pos = 0usize;
34 for (s, e, color) in valid {
35 if e <= pos {
36 continue;
38 }
39 let s = s.max(pos);
41 if s > pos {
42 out.push((pos, s, base_color)); }
44 out.push((s, e, color));
45 pos = e;
46 }
47 if pos < len {
48 out.push((pos, len, base_color));
49 }
50 out
51}
52
53pub(crate) fn caret_visible_segment(
59 p_y: f64,
60 line_h: f64,
61 inner_lo: f64,
62 inner_hi: f64,
63) -> Option<(f64, f64)> {
64 let y0 = p_y.max(inner_lo);
65 let y1 = (p_y + line_h).min(inner_hi);
66 if y1 > y0 {
67 Some((y0, y1))
68 } else {
69 None
70 }
71}
72
73impl TextArea {
74 fn paint_highlighted_line(
78 &self,
79 ctx: &mut dyn DrawCtx,
80 text: &str,
81 spans: &[(usize, usize, Color)],
82 x0: f64,
83 baseline_y: f64,
84 base_color: Color,
85 ) {
86 for (s, e, color) in segment_highlight(text, spans, base_color) {
87 let x = x0 + measure_advance(&self.font, &text[..s], self.font_size);
88 ctx.set_fill_color(color);
89 ctx.fill_text(&text[s..e], x, baseline_y);
90 }
91 }
92}
93
94impl Widget for TextArea {
95 fn type_name(&self) -> &'static str {
96 "TextArea"
97 }
98 fn bounds(&self) -> Rect {
99 self.bounds
100 }
101 fn set_bounds(&mut self, b: Rect) {
102 self.bounds = b;
103 }
104 fn children(&self) -> &[Box<dyn Widget>] {
105 &self.children
106 }
107 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
108 &mut self.children
109 }
110
111 fn is_focusable(&self) -> bool {
112 true
113 }
114
115 fn focus_id(&self) -> Option<crate::focus::FocusId> {
116 self.focus_request_id
117 }
118
119 fn accepts_text_input(&self) -> bool {
120 true
121 }
122
123 fn text_input_value(&self) -> Option<String> {
124 Some(self.text())
125 }
126
127 fn margin(&self) -> Insets {
128 self.base.margin
129 }
130 fn widget_base(&self) -> Option<&WidgetBase> {
131 Some(&self.base)
132 }
133 fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
134 Some(&mut self.base)
135 }
136 fn h_anchor(&self) -> HAnchor {
137 self.base.h_anchor
138 }
139 fn v_anchor(&self) -> VAnchor {
140 self.base.v_anchor
141 }
142 fn min_size(&self) -> Size {
143 self.base.min_size
144 }
145 fn max_size(&self) -> Size {
146 self.base.max_size
147 }
148
149 fn backbuffer_cache_mut(&mut self) -> Option<&mut BackbufferCache> {
150 Some(&mut self.cache)
151 }
152
153 fn backbuffer_mode(&self) -> BackbufferMode {
154 if crate::font_settings::lcd_enabled() {
159 BackbufferMode::LcdCoverage
160 } else {
161 BackbufferMode::Rgba
162 }
163 }
164
165 fn measure_min_height(&self, available_w: f64) -> f64 {
166 let inner_w = (available_w - self.padding * 2.0).max(1.0);
177 let lines = wrap_text_indexed(
178 &self.font,
179 &self.edit.borrow().text,
180 self.font_size,
181 inner_w,
182 );
183 let line_h = self.font_size * 1.35;
184 (lines.len().max(1) as f64) * line_h + self.padding * 2.0
185 }
186
187 fn layout(&mut self, available: Size) -> Size {
188 let w = available.width.max(self.padding * 2.0 + 20.0);
193 let h = available
194 .height
195 .max(self.padding * 2.0 + self.font_size * 1.6);
196 self.bounds = Rect::new(0.0, 0.0, w, h);
197 let inner_w = (w - self.padding * 2.0).max(1.0);
198 self.refresh_wrap(inner_w);
199 self.sync_scroll();
200 let cursor = self.edit.borrow().cursor;
206 if matches!(self.last_layout_cursor, Some(prev) if prev != cursor) {
207 self.ensure_cursor_visible();
208 }
209 self.last_layout_cursor = Some(cursor);
210
211 let sig = self.cache_sig();
215 if self.last_sig.as_ref() != Some(&sig) {
216 self.last_sig = Some(sig);
217 self.cache.invalidate();
218 }
219 Size::new(w, h)
220 }
221
222 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
223 let v = ctx.visuals();
224 let w = self.bounds.width;
225 let h = self.bounds.height;
226
227 ctx.set_fill_color(v.widget_bg);
229 ctx.begin_path();
230 ctx.rounded_rect(0.0, 0.0, w, h, 4.0);
231 ctx.fill();
232
233 ctx.clip_rect(
236 self.padding,
237 self.padding,
238 (w - self.padding * 2.0).max(0.0),
239 (h - self.padding * 2.0).max(0.0),
240 );
241
242 ctx.set_font(Arc::clone(&self.font));
243 ctx.set_font_size(self.font_size);
244
245 let st = self.edit.borrow().clone();
247 if st.cursor != st.anchor {
248 let lo = st.cursor.min(st.anchor);
249 let hi = st.cursor.max(st.anchor);
250 let hl_color = if self.focused {
251 v.selection_bg
252 } else {
253 v.selection_bg_unfocused
254 };
255 ctx.set_fill_color(hl_color);
256 for (i, line) in self.cached_lines.iter().enumerate() {
257 if line.end < lo || line.start > hi {
258 continue;
259 }
260 let sel_s = lo.max(line.start) - line.start;
261 let sel_e = hi.min(line.end) - line.start;
262 let sel_e = sel_e.min(line.text.len());
263 if sel_e <= sel_s {
264 continue;
265 }
266 let line_x = self.line_x_start(line);
267 let x0 = line_x + measure_advance(&self.font, &line.text[..sel_s], self.font_size);
268 let x1 = line_x + measure_advance(&self.font, &line.text[..sel_e], self.font_size);
269 let line_top = self.line_top_y(i);
270 let line_bottom = line_top - self.cached_line_h;
271 ctx.begin_path();
272 ctx.rect(x0, line_bottom, x1 - x0, self.cached_line_h);
273 ctx.fill();
274 }
275 }
276
277 ctx.set_fill_color(v.text_color);
279 for (i, line) in self.cached_lines.iter().enumerate() {
280 if line.text.is_empty() {
281 continue;
282 }
283 let baseline_y = self.line_baseline_y(i);
284 let x0 = self.line_x_start(line);
285 match &self.highlighter {
286 Some(hl) => {
289 let spans = hl(&line.text);
290 self.paint_highlighted_line(ctx, &line.text, &spans, x0, baseline_y, v.text_color);
291 }
292 None => {
293 ctx.fill_text(&line.text, x0, baseline_y);
294 }
295 }
296 }
297
298 if st.text.is_empty() && !self.hint.is_empty() {
303 ctx.set_fill_color(v.text_dim);
304 let hint_w = measure_advance(&self.font, &self.hint, self.font_size);
305 let slack = (self.inner_width() - hint_w).max(0.0);
306 let hint_x = self.padding
307 + match self.resolved_h_align() {
308 TextHAlign::Left => 0.0,
309 TextHAlign::Center => slack * 0.5,
310 TextHAlign::Right => slack,
311 };
312 let baseline_y = self.line_baseline_y(0);
313 ctx.fill_text(&self.hint, hint_x, baseline_y);
314 }
315
316 ctx.reset_clip();
317
318 let border = if self.focused {
320 v.accent
321 } else if self.hovered {
322 v.widget_stroke_active
323 } else {
324 v.widget_stroke
325 };
326 let line_width = if self.focused { 2.0 } else { 1.0 };
327 ctx.set_stroke_color(border);
328 ctx.set_line_width(line_width);
329 ctx.begin_path();
330 let inset = line_width * 0.5;
331 ctx.rounded_rect(
332 inset,
333 inset,
334 (w - line_width).max(0.0),
335 (h - line_width).max(0.0),
336 4.0,
337 );
338 ctx.stroke();
339 }
340
341 fn paint_overlay(&mut self, ctx: &mut dyn DrawCtx) {
342 self.paint_scrollbar(ctx);
346
347 if !self.focused {
350 return;
351 }
352 if let Some(t) = self.focus_time {
353 let phase = (t.elapsed().as_millis() / 500) as u64;
354 self.blink_last_phase.set(phase);
355 if phase % 2 == 1 {
356 return;
357 }
358 }
359 let st = self.edit.borrow().clone();
360 let p = self.pos_for_cursor(st.cursor);
361 let inner_lo = self.padding;
367 let inner_hi = (self.bounds.height - self.padding).max(inner_lo);
368 let Some((y0, y1)) =
369 caret_visible_segment(p.y, self.cached_line_h, inner_lo, inner_hi)
370 else {
371 return;
372 };
373 let v = ctx.visuals();
374 ctx.set_stroke_color(v.text_color);
375 ctx.set_line_width(1.5);
376 ctx.begin_path();
377 ctx.move_to(p.x, y0);
378 ctx.line_to(p.x, y1);
379 ctx.stroke();
380 }
381
382 fn on_event(&mut self, event: &Event) -> EventResult {
383 if let Some(result) = self.route_context_menu(event) {
386 return result;
387 }
388 match event {
389 Event::MouseMove { pos } => {
390 let bar_hover_changed = match self.scrollbar_on_mouse_move(*pos) {
393 super::scroll::ScrollMove::Dragging(moved) => {
394 if moved {
395 crate::animation::request_draw();
396 }
397 return EventResult::Consumed;
398 }
399 super::scroll::ScrollMove::Hover(changed) => changed,
400 };
401 let was = self.hovered;
402 self.hovered = self.hit_test(*pos);
403 if self.hovered {
404 set_cursor_icon(CursorIcon::Text);
405 }
406 if self.selecting_drag {
407 let off = self.byte_offset_at(*pos);
408 self.extend_selection_drag(off);
409 crate::animation::request_draw();
410 return EventResult::Consumed;
411 }
412 if was != self.hovered || bar_hover_changed {
413 crate::animation::request_draw();
414 return EventResult::Consumed;
415 }
416 EventResult::Ignored
417 }
418 Event::MouseDown {
419 button: MouseButton::Left,
420 pos,
421 modifiers,
422 } => {
423 if self.scrollbar_begin_drag(*pos) {
425 crate::animation::request_draw();
426 return EventResult::Consumed;
427 }
428 let off = self.byte_offset_at(*pos);
429 let clicks = self.multi_click.register(*pos);
430 self.begin_pointer_selection(off, clicks, modifiers.shift);
431 self.selecting_drag = true;
432 self.focus_time = Some(Instant::now());
433 crate::animation::request_draw();
434 EventResult::Consumed
435 }
436 Event::MouseDown {
437 button: MouseButton::Right,
438 pos,
439 ..
440 } => {
441 if self.context_menu_enabled {
442 self.open_context_menu(*pos);
443 EventResult::Consumed
444 } else {
445 EventResult::Ignored
446 }
447 }
448 Event::MouseUp {
449 button: MouseButton::Left,
450 ..
451 } => {
452 self.scrollbar_end_drag();
453 self.selecting_drag = false;
454 EventResult::Consumed
455 }
456 Event::MouseWheel { delta_y, .. } => {
457 if self.scroll_by_wheel(*delta_y) {
458 crate::animation::request_draw();
459 EventResult::Consumed
460 } else {
461 EventResult::Ignored
462 }
463 }
464 Event::FocusGained => {
465 self.focused = true;
466 self.focus_time = Some(Instant::now());
467 crate::animation::request_draw();
468 EventResult::Ignored
469 }
470 Event::FocusLost => {
471 self.focused = false;
472 self.selecting_drag = false;
473 crate::animation::request_draw();
474 EventResult::Ignored
475 }
476 Event::KeyDown { key, modifiers } => {
477 if let Some(cb) = self.on_key_chord.clone() {
482 let epoch_before = self.edit.borrow().epoch;
483 if (cb.borrow_mut())(key, modifiers) {
484 if self.edit.borrow().epoch != epoch_before {
489 self.mark_dirty();
490 self.notify_change();
491 self.ensure_cursor_visible();
492 }
493 crate::animation::request_draw();
494 return EventResult::Consumed;
495 }
496 }
497 let shift = modifiers.shift;
498 let cmd = modifiers.ctrl || modifiers.meta;
499 match key {
500 Key::ArrowLeft => {
501 self.move_char(-1, shift);
502 }
503 Key::ArrowRight => {
504 self.move_char(1, shift);
505 }
506 Key::ArrowUp => {
507 self.move_line(-1, shift);
508 }
509 Key::ArrowDown => {
510 self.move_line(1, shift);
511 }
512 Key::Home => {
513 let target = if cmd {
516 0
517 } else {
518 let cur = self.edit.borrow().cursor;
519 let line = self.line_for_cursor(cur);
520 self.cached_lines[line].start
521 };
522 self.move_cursor_to(target, shift);
523 }
524 Key::End => {
525 let target = if cmd {
528 self.edit.borrow().text.len()
529 } else {
530 let cur = self.edit.borrow().cursor;
531 let line = self.line_for_cursor(cur);
532 self.cached_lines[line].end
533 };
534 self.move_cursor_to(target, shift);
535 }
536 Key::PageUp => {
537 let n = self.page_lines() as isize;
538 self.move_lines(-n, shift);
539 }
540 Key::PageDown => {
541 let n = self.page_lines() as isize;
542 self.move_lines(n, shift);
543 }
544 Key::Backspace => {
545 self.delete(-1);
546 }
547 Key::Delete => {
548 self.delete(1);
549 }
550 Key::Enter => {
551 self.insert_str("\n");
552 }
553 Key::Tab => {
554 self.insert_str(" ");
555 }
556 Key::Char('a') | Key::Char('A') if cmd => {
557 self.select_all_text();
558 }
559 Key::Char('c') | Key::Char('C') if cmd => {
560 self.clipboard_copy();
561 }
562 Key::Char('x') | Key::Char('X') if cmd => {
563 self.clipboard_cut();
564 }
565 Key::Char('v') | Key::Char('V') if cmd => {
566 self.clipboard_paste();
567 }
568 Key::Char(c) if !cmd => {
569 let mut s = [0u8; 4];
570 self.insert_str(c.encode_utf8(&mut s));
571 }
572 _ => return EventResult::Ignored,
573 }
574 self.ensure_cursor_visible();
577 self.focus_time = Some(Instant::now());
578 crate::animation::request_draw();
579 EventResult::Consumed
580 }
581 _ => EventResult::Ignored,
582 }
583 }
584
585 fn hit_test(&self, local_pos: Point) -> bool {
586 local_pos.x >= 0.0
587 && local_pos.x <= self.bounds.width
588 && local_pos.y >= 0.0
589 && local_pos.y <= self.bounds.height
590 }
591
592 fn properties(&self) -> Vec<(&'static str, String)> {
593 let st = self.edit.borrow();
594 vec![
595 ("len", st.text.len().to_string()),
596 ("cursor", st.cursor.to_string()),
597 ("lines", self.cached_lines.len().to_string()),
598 ("focused", self.focused.to_string()),
599 ]
600 }
601
602 fn needs_draw(&self) -> bool {
603 if self.scrollbar_animating() {
610 return true;
611 }
612 if !self.focused {
613 return false;
614 }
615 let Some(t) = self.focus_time else {
616 return false;
617 };
618 let current_phase = (t.elapsed().as_millis() / 500) as u64;
619 current_phase != self.blink_last_phase.get()
620 }
621
622 fn next_draw_deadline(&self) -> Option<web_time::Instant> {
623 if !self.focused {
624 return None;
625 }
626 let t = self.focus_time?;
627 let ms = t.elapsed().as_millis() as u64;
628 let next_phase = (ms / 500) + 1;
629 Some(t + std::time::Duration::from_millis(next_phase * 500))
630 }
631
632 fn has_active_modal(&self) -> bool {
635 self.context_menu.is_open()
636 }
637
638 fn paint_global_overlay(&mut self, ctx: &mut dyn DrawCtx) {
641 let font = crate::font_settings::current_system_font()
642 .unwrap_or_else(|| Arc::clone(&self.font));
643 self.context_menu.paint(ctx, font, self.font_size);
644 }
645}