agg_gui/widgets/rich_text/
editor.rs1use std::cell::{Cell, RefCell};
24use std::rc::Rc;
25
26use web_time::Instant;
27
28use crate::event::{Event, EventResult};
29use crate::focus::FocusId;
30use crate::geometry::{Rect, Size};
31use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
32use crate::widget::{BackbufferCache, BackbufferMode, Widget};
33use crate::widgets::multi_click::{MultiClickTracker, SelectGranularity};
34use crate::widgets::scrollbar::ScrollbarAxis;
35
36use super::commands::{CommonStyle, RichCommand};
37use super::model::{DocPos, DocRange};
38use super::layout::{layout_doc, DocLayout, FontResolver};
39use super::model::RichDoc;
40use super::view::SharedResolver;
41
42pub mod core;
43mod context_menu;
44mod geometry;
45mod input;
46mod paint;
47mod scroll;
48
49pub use core::{RichEditCore, RichEditHandle};
50
51#[cfg(test)]
52mod tests;
53#[cfg(test)]
54mod handle_api_tests;
55#[cfg(test)]
56mod preview_dirty_tests;
57
58pub struct RichTextEdit {
60 bounds: Rect,
61 children: Vec<Box<dyn Widget>>, base: WidgetBase,
63
64 core: Rc<RefCell<RichEditCore>>,
66 resolver: SharedResolver,
67 padding: f64,
68
69 layout: Option<DocLayout>,
71 layout_width: f64,
72 layout_doc_rev: u64,
73
74 vbar: ScrollbarAxis,
77
78 focus_request_id: Option<FocusId>,
80
81 focused: bool,
83 hovered: bool,
84 selecting_drag: bool,
85 focus_time: Option<Instant>,
86 blink_last_phase: Cell<u64>,
87
88 multi_click: MultiClickTracker,
94 select_granularity: SelectGranularity,
95 select_pivot: (DocPos, DocPos),
96 start: Instant,
98 last_layout_rev: Option<u64>,
101
102 cache: BackbufferCache,
107 last_sig: Option<RichEditSig>,
109 layout_gen: u64,
116
117 context_menu: crate::widgets::text_context_menu::TextContextMenu,
120 context_menu_enabled: bool,
121}
122
123#[derive(Clone, Debug, PartialEq)]
132struct RichEditSig {
133 core_rev: u64,
134 focused: bool,
135 hovered: bool,
136 offset_bits: u64,
137 w_bits: u64,
138 h_bits: u64,
139 layout_gen: u64,
140}
141
142impl RichTextEdit {
143 pub fn new(initial: RichDoc, resolver: SharedResolver) -> Self {
145 Self {
146 bounds: Rect::default(),
147 children: Vec::new(),
148 base: WidgetBase::new(),
149 core: Rc::new(RefCell::new(RichEditCore::new(initial, 16.0))),
150 resolver,
151 padding: 8.0,
152 layout: None,
153 layout_width: -1.0,
154 layout_doc_rev: u64::MAX,
155 vbar: ScrollbarAxis {
156 enabled: true,
157 ..ScrollbarAxis::default()
158 },
159 focus_request_id: None,
160 focused: false,
161 hovered: false,
162 selecting_drag: false,
163 focus_time: None,
164 blink_last_phase: Cell::new(0),
165 multi_click: MultiClickTracker::default(),
166 select_granularity: SelectGranularity::default(),
167 select_pivot: (DocPos::new(0, 0), DocPos::new(0, 0)),
168 start: Instant::now(),
169 last_layout_rev: None,
170 cache: BackbufferCache::default(),
171 last_sig: None,
172 layout_gen: 0,
173 context_menu: crate::widgets::text_context_menu::TextContextMenu::new(),
174 context_menu_enabled: true,
175 }
176 }
177
178 pub fn with_context_menu(mut self, v: bool) -> Self {
181 self.context_menu_enabled = v;
182 self
183 }
184
185 fn cache_sig(&self) -> RichEditSig {
187 RichEditSig {
188 core_rev: self.core.borrow().rev(),
189 focused: self.focused,
190 hovered: self.hovered,
191 offset_bits: self.vbar.offset.to_bits(),
192 w_bits: self.bounds.width.to_bits(),
193 h_bits: self.bounds.height.to_bits(),
194 layout_gen: self.layout_gen,
195 }
196 }
197
198 pub fn with_font_size(self, size: f64) -> Self {
201 self.core.borrow_mut().set_default_font_size(size);
202 self
203 }
204 pub fn with_padding(mut self, p: f64) -> Self {
206 self.padding = p;
207 self
208 }
209 pub fn with_margin(mut self, m: Insets) -> Self {
211 self.base.margin = m;
212 self
213 }
214 pub fn with_focus_id(mut self, id: FocusId) -> Self {
217 self.focus_request_id = Some(id);
218 self
219 }
220 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
222 self.base.h_anchor = h;
223 self
224 }
225 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
227 self.base.v_anchor = v;
228 self
229 }
230
231 pub fn handle(&self) -> RichEditHandle {
233 RichEditHandle::new(Rc::clone(&self.core))
234 }
235
236 pub fn exec(&mut self, cmd: &RichCommand) {
240 self.core.borrow_mut().exec(cmd);
241 }
242 pub fn common_style_of_selection(&self) -> CommonStyle {
244 self.core.borrow().common_style_of_selection()
245 }
246
247 pub fn select_all(&mut self) {
249 self.core.borrow_mut().select_all();
250 }
251 pub fn set_caret(&mut self, pos: DocPos) {
253 self.core.borrow_mut().set_caret(pos, false);
254 }
255 pub fn set_selection(&mut self, range: DocRange) {
258 self.core.borrow_mut().set_selection(range.start, range.end);
259 }
260 pub fn selection(&self) -> DocRange {
262 self.core.borrow().selection()
263 }
264 pub fn load(&mut self, doc: RichDoc) {
268 self.core.borrow_mut().load(doc);
269 }
270 pub fn undo(&mut self) -> bool {
272 self.core.borrow_mut().undo()
273 }
274 pub fn redo(&mut self) -> bool {
276 self.core.borrow_mut().redo()
277 }
278 pub fn can_undo(&self) -> bool {
280 self.core.borrow().can_undo()
281 }
282 pub fn can_redo(&self) -> bool {
284 self.core.borrow().can_redo()
285 }
286
287 pub fn plain_text(&self) -> String {
289 self.core.borrow().doc().plain_text()
290 }
291
292 pub fn invalidate_layout(&mut self) {
297 self.layout = None;
298 self.layout_width = -1.0;
299 self.layout_doc_rev = u64::MAX;
300 self.layout_gen = self.layout_gen.wrapping_add(1);
304 }
305
306 fn ensure_layout(&mut self, width: f64) {
310 let doc_rev = self.core.borrow().doc_rev();
311 if self.layout.is_some()
312 && (self.layout_width - width).abs() < 0.5
313 && self.layout_doc_rev == doc_rev
314 {
315 return;
316 }
317 let resolver: &FontResolver = &*self.resolver;
318 let core = self.core.borrow();
319 self.layout = Some(layout_doc(
320 core.doc(),
321 width,
322 core.default_font_size(),
323 resolver,
324 ));
325 self.layout_width = width;
326 self.layout_doc_rev = doc_rev;
327 }
328
329 pub(crate) fn content_height(&self) -> f64 {
331 self.layout.as_ref().map(|l| l.height).unwrap_or(0.0)
332 }
333}
334
335impl Widget for RichTextEdit {
336 fn type_name(&self) -> &'static str {
337 "RichTextEdit"
338 }
339 fn bounds(&self) -> Rect {
340 self.bounds
341 }
342 fn set_bounds(&mut self, b: Rect) {
343 self.bounds = b;
344 }
345 fn children(&self) -> &[Box<dyn Widget>] {
346 &self.children
347 }
348 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
349 &mut self.children
350 }
351
352 fn is_focusable(&self) -> bool {
353 true
354 }
355 fn focus_id(&self) -> Option<FocusId> {
356 self.focus_request_id
357 }
358 fn accepts_text_input(&self) -> bool {
359 true
360 }
361 fn text_input_value(&self) -> Option<String> {
362 Some(self.plain_text())
363 }
364
365 fn margin(&self) -> Insets {
366 self.base.margin
367 }
368 fn widget_base(&self) -> Option<&WidgetBase> {
369 Some(&self.base)
370 }
371 fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
372 Some(&mut self.base)
373 }
374 fn h_anchor(&self) -> HAnchor {
375 self.base.h_anchor
376 }
377 fn v_anchor(&self) -> VAnchor {
378 self.base.v_anchor
379 }
380 fn min_size(&self) -> Size {
381 self.base.min_size
382 }
383 fn max_size(&self) -> Size {
384 self.base.max_size
385 }
386
387 fn backbuffer_cache_mut(&mut self) -> Option<&mut BackbufferCache> {
388 Some(&mut self.cache)
389 }
390
391 fn backbuffer_mode(&self) -> BackbufferMode {
392 if crate::font_settings::lcd_enabled() {
397 BackbufferMode::LcdCoverage
398 } else {
399 BackbufferMode::Rgba
400 }
401 }
402
403 fn measure_min_height(&self, available_w: f64) -> f64 {
404 let resolver: &FontResolver = &*self.resolver;
405 let core = self.core.borrow();
406 let inner_w = (available_w - self.padding * 2.0).max(1.0);
407 layout_doc(core.doc(), inner_w, core.default_font_size(), resolver).height
408 + self.padding * 2.0
409 }
410
411 fn layout(&mut self, available: Size) -> Size {
412 let w = available.width.max(self.padding * 2.0 + 20.0);
413 let h = available
414 .height
415 .max(self.padding * 2.0 + 20.0);
416 self.bounds = Rect::new(0.0, 0.0, w, h);
417 let inner_w = (w - self.padding * 2.0).max(1.0);
418 self.ensure_layout(inner_w);
419 self.sync_scroll();
420
421 let time = self.start.elapsed().as_secs_f64();
424 let in_flux = self.core.borrow_mut().feed_undo(time);
425 if in_flux {
426 crate::animation::request_draw_after(std::time::Duration::from_millis(120));
427 }
428
429 let rev = self.core.borrow().rev();
431 if matches!(self.last_layout_rev, Some(prev) if prev != rev) {
432 self.ensure_caret_visible();
433 }
434 self.last_layout_rev = Some(rev);
435
436 let sig = self.cache_sig();
440 if self.last_sig.as_ref() != Some(&sig) {
441 self.last_sig = Some(sig);
442 self.cache.invalidate();
443 }
444 Size::new(w, h)
445 }
446
447 fn paint(&mut self, ctx: &mut dyn crate::draw_ctx::DrawCtx) {
448 self.paint_content(ctx);
449 }
450
451 fn paint_overlay(&mut self, ctx: &mut dyn crate::draw_ctx::DrawCtx) {
452 self.paint_scrollbar(ctx);
456 self.paint_caret(ctx);
457 }
458
459 fn hit_test(&self, local: crate::geometry::Point) -> bool {
460 local.x >= 0.0
461 && local.x <= self.bounds.width
462 && local.y >= 0.0
463 && local.y <= self.bounds.height
464 }
465
466 fn needs_draw(&self) -> bool {
467 if self.scrollbar_animating() {
473 return true;
474 }
475 if !self.focused {
476 return false;
477 }
478 let Some(t) = self.focus_time else {
479 return false;
480 };
481 let current_phase = (t.elapsed().as_millis() / 500) as u64;
482 current_phase != self.blink_last_phase.get()
483 }
484
485 fn next_draw_deadline(&self) -> Option<web_time::Instant> {
486 if !self.focused {
487 return None;
488 }
489 let t = self.focus_time?;
490 let ms = t.elapsed().as_millis() as u64;
491 let next_phase = (ms / 500) + 1;
492 Some(t + std::time::Duration::from_millis(next_phase * 500))
493 }
494
495 fn on_event(&mut self, event: &Event) -> EventResult {
496 self.handle_event(event)
497 }
498
499 fn has_active_modal(&self) -> bool {
502 self.context_menu.is_open()
503 }
504
505 fn paint_global_overlay(&mut self, ctx: &mut dyn crate::draw_ctx::DrawCtx) {
508 let font = self.context_menu_font();
509 let size = self.core.borrow().default_font_size();
510 self.context_menu.paint(ctx, font, size);
511 }
512}