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#[cfg(test)]
58mod tab_indent_tests;
59#[cfg(test)]
60mod trailing_space_tests;
61#[cfg(test)]
62mod word_delete_tests;
63
64pub struct RichTextEdit {
66 bounds: Rect,
67 children: Vec<Box<dyn Widget>>, base: WidgetBase,
69
70 core: Rc<RefCell<RichEditCore>>,
72 resolver: SharedResolver,
73 padding: f64,
74
75 layout: Option<DocLayout>,
77 layout_width: f64,
78 layout_doc_rev: u64,
79
80 vbar: ScrollbarAxis,
83
84 focus_request_id: Option<FocusId>,
86
87 focused: bool,
89 hovered: bool,
90 selecting_drag: bool,
91 focus_time: Option<Instant>,
92 blink_last_phase: Cell<u64>,
93
94 multi_click: MultiClickTracker,
100 select_granularity: SelectGranularity,
101 select_pivot: (DocPos, DocPos),
102 start: Instant,
104 last_layout_rev: Option<u64>,
107
108 cache: BackbufferCache,
113 last_sig: Option<RichEditSig>,
115 layout_gen: u64,
122
123 context_menu: crate::widgets::text_context_menu::TextContextMenu,
126 context_menu_enabled: bool,
127}
128
129#[derive(Clone, Debug, PartialEq)]
138struct RichEditSig {
139 core_rev: u64,
140 focused: bool,
141 hovered: bool,
142 offset_bits: u64,
143 w_bits: u64,
144 h_bits: u64,
145 layout_gen: u64,
146}
147
148impl RichTextEdit {
149 pub fn new(initial: RichDoc, resolver: SharedResolver) -> Self {
151 Self {
152 bounds: Rect::default(),
153 children: Vec::new(),
154 base: WidgetBase::new(),
155 core: Rc::new(RefCell::new(RichEditCore::new(initial, 16.0))),
156 resolver,
157 padding: 8.0,
158 layout: None,
159 layout_width: -1.0,
160 layout_doc_rev: u64::MAX,
161 vbar: ScrollbarAxis {
162 enabled: true,
163 ..ScrollbarAxis::default()
164 },
165 focus_request_id: None,
166 focused: false,
167 hovered: false,
168 selecting_drag: false,
169 focus_time: None,
170 blink_last_phase: Cell::new(0),
171 multi_click: MultiClickTracker::default(),
172 select_granularity: SelectGranularity::default(),
173 select_pivot: (DocPos::new(0, 0), DocPos::new(0, 0)),
174 start: Instant::now(),
175 last_layout_rev: None,
176 cache: BackbufferCache::default(),
177 last_sig: None,
178 layout_gen: 0,
179 context_menu: crate::widgets::text_context_menu::TextContextMenu::new(),
180 context_menu_enabled: true,
181 }
182 }
183
184 pub fn with_context_menu(mut self, v: bool) -> Self {
187 self.context_menu_enabled = v;
188 self
189 }
190
191 fn cache_sig(&self) -> RichEditSig {
193 RichEditSig {
194 core_rev: self.core.borrow().rev(),
195 focused: self.focused,
196 hovered: self.hovered,
197 offset_bits: self.vbar.offset.to_bits(),
198 w_bits: self.bounds.width.to_bits(),
199 h_bits: self.bounds.height.to_bits(),
200 layout_gen: self.layout_gen,
201 }
202 }
203
204 pub fn with_font_size(self, size: f64) -> Self {
207 self.core.borrow_mut().set_default_font_size(size);
208 self
209 }
210 pub fn with_padding(mut self, p: f64) -> Self {
212 self.padding = p;
213 self
214 }
215 pub fn with_margin(mut self, m: Insets) -> Self {
217 self.base.margin = m;
218 self
219 }
220 pub fn with_focus_id(mut self, id: FocusId) -> Self {
223 self.focus_request_id = Some(id);
224 self
225 }
226 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
228 self.base.h_anchor = h;
229 self
230 }
231 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
233 self.base.v_anchor = v;
234 self
235 }
236
237 pub fn handle(&self) -> RichEditHandle {
239 RichEditHandle::new(Rc::clone(&self.core))
240 }
241
242 pub fn exec(&mut self, cmd: &RichCommand) {
246 self.core.borrow_mut().exec(cmd);
247 }
248 pub fn common_style_of_selection(&self) -> CommonStyle {
250 self.core.borrow().common_style_of_selection()
251 }
252
253 pub fn select_all(&mut self) {
255 self.core.borrow_mut().select_all();
256 }
257 pub fn set_caret(&mut self, pos: DocPos) {
259 self.core.borrow_mut().set_caret(pos, false);
260 }
261 pub fn set_selection(&mut self, range: DocRange) {
264 self.core.borrow_mut().set_selection(range.start, range.end);
265 }
266 pub fn selection(&self) -> DocRange {
268 self.core.borrow().selection()
269 }
270 pub fn load(&mut self, doc: RichDoc) {
274 self.core.borrow_mut().load(doc);
275 }
276 pub fn undo(&mut self) -> bool {
278 self.core.borrow_mut().undo()
279 }
280 pub fn redo(&mut self) -> bool {
282 self.core.borrow_mut().redo()
283 }
284 pub fn can_undo(&self) -> bool {
286 self.core.borrow().can_undo()
287 }
288 pub fn can_redo(&self) -> bool {
290 self.core.borrow().can_redo()
291 }
292
293 pub fn plain_text(&self) -> String {
295 self.core.borrow().doc().plain_text()
296 }
297
298 pub fn invalidate_layout(&mut self) {
303 self.layout = None;
304 self.layout_width = -1.0;
305 self.layout_doc_rev = u64::MAX;
306 self.layout_gen = self.layout_gen.wrapping_add(1);
310 }
311
312 fn ensure_layout(&mut self, width: f64) {
316 let doc_rev = self.core.borrow().doc_rev();
317 if self.layout.is_some()
318 && (self.layout_width - width).abs() < 0.5
319 && self.layout_doc_rev == doc_rev
320 {
321 return;
322 }
323 let resolver: &FontResolver = &*self.resolver;
324 let core = self.core.borrow();
325 self.layout = Some(layout_doc(
326 core.doc(),
327 width,
328 core.default_font_size(),
329 resolver,
330 ));
331 self.layout_width = width;
332 self.layout_doc_rev = doc_rev;
333 }
334
335 pub(crate) fn content_height(&self) -> f64 {
337 self.layout.as_ref().map(|l| l.height).unwrap_or(0.0)
338 }
339}
340
341impl Widget for RichTextEdit {
342 fn type_name(&self) -> &'static str {
343 "RichTextEdit"
344 }
345 fn bounds(&self) -> Rect {
346 self.bounds
347 }
348 fn set_bounds(&mut self, b: Rect) {
349 self.bounds = b;
350 }
351 fn children(&self) -> &[Box<dyn Widget>] {
352 &self.children
353 }
354 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
355 &mut self.children
356 }
357
358 fn is_focusable(&self) -> bool {
359 true
360 }
361 fn focus_id(&self) -> Option<FocusId> {
362 self.focus_request_id
363 }
364 fn accepts_text_input(&self) -> bool {
365 true
366 }
367 fn text_input_value(&self) -> Option<String> {
368 Some(self.plain_text())
369 }
370
371 fn margin(&self) -> Insets {
372 self.base.margin
373 }
374 fn widget_base(&self) -> Option<&WidgetBase> {
375 Some(&self.base)
376 }
377 fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
378 Some(&mut self.base)
379 }
380 fn h_anchor(&self) -> HAnchor {
381 self.base.h_anchor
382 }
383 fn v_anchor(&self) -> VAnchor {
384 self.base.v_anchor
385 }
386 fn min_size(&self) -> Size {
387 self.base.min_size
388 }
389 fn max_size(&self) -> Size {
390 self.base.max_size
391 }
392
393 fn backbuffer_cache_mut(&mut self) -> Option<&mut BackbufferCache> {
394 Some(&mut self.cache)
395 }
396
397 fn backbuffer_mode(&self) -> BackbufferMode {
398 if crate::font_settings::lcd_enabled() {
403 BackbufferMode::LcdCoverage
404 } else {
405 BackbufferMode::Rgba
406 }
407 }
408
409 fn measure_min_height(&self, available_w: f64) -> f64 {
410 let resolver: &FontResolver = &*self.resolver;
411 let core = self.core.borrow();
412 let inner_w = (available_w - self.padding * 2.0).max(1.0);
413 layout_doc(core.doc(), inner_w, core.default_font_size(), resolver).height
414 + self.padding * 2.0
415 }
416
417 fn layout(&mut self, available: Size) -> Size {
418 let w = available.width.max(self.padding * 2.0 + 20.0);
419 let h = available
420 .height
421 .max(self.padding * 2.0 + 20.0);
422 self.bounds = Rect::new(0.0, 0.0, w, h);
423 let inner_w = (w - self.padding * 2.0).max(1.0);
424 self.ensure_layout(inner_w);
425 self.sync_scroll();
426
427 let time = self.start.elapsed().as_secs_f64();
430 let in_flux = self.core.borrow_mut().feed_undo(time);
431 if in_flux {
432 crate::animation::request_draw_after_tagged(
433 std::time::Duration::from_millis(120),
434 "rich_text::editor undo-settle",
435 );
436 }
437
438 let rev = self.core.borrow().rev();
440 if matches!(self.last_layout_rev, Some(prev) if prev != rev) {
441 self.ensure_caret_visible();
442 }
443 self.last_layout_rev = Some(rev);
444
445 let sig = self.cache_sig();
449 if self.last_sig.as_ref() != Some(&sig) {
450 self.last_sig = Some(sig);
451 self.cache.invalidate();
452 }
453 Size::new(w, h)
454 }
455
456 fn paint(&mut self, ctx: &mut dyn crate::draw_ctx::DrawCtx) {
457 self.paint_content(ctx);
458 }
459
460 fn paint_overlay(&mut self, ctx: &mut dyn crate::draw_ctx::DrawCtx) {
461 self.paint_scrollbar(ctx);
465 self.paint_caret(ctx);
466 }
467
468 fn hit_test(&self, local: crate::geometry::Point) -> bool {
469 local.x >= 0.0
470 && local.x <= self.bounds.width
471 && local.y >= 0.0
472 && local.y <= self.bounds.height
473 }
474
475 fn needs_draw(&self) -> bool {
476 if self.scrollbar_animating() {
482 return true;
483 }
484 if !self.focused {
485 return false;
486 }
487 let Some(t) = self.focus_time else {
488 return false;
489 };
490 let current_phase = (t.elapsed().as_millis() / 500) as u64;
491 current_phase != self.blink_last_phase.get()
492 }
493
494 fn next_draw_deadline(&self) -> Option<web_time::Instant> {
495 if !self.focused {
496 return None;
497 }
498 let t = self.focus_time?;
499 let ms = t.elapsed().as_millis() as u64;
500 let next_phase = (ms / 500) + 1;
501 Some(t + std::time::Duration::from_millis(next_phase * 500))
502 }
503
504 fn on_event(&mut self, event: &Event) -> EventResult {
505 self.handle_event(event)
506 }
507
508 fn has_active_modal(&self) -> bool {
511 self.context_menu.is_open()
512 }
513
514 fn paint_global_overlay(&mut self, ctx: &mut dyn crate::draw_ctx::DrawCtx) {
517 let font = self.context_menu_font();
518 let size = self.core.borrow().default_font_size();
519 self.context_menu.paint(ctx, font, size);
520 }
521}