use std::cell::{Cell, RefCell};
use std::rc::Rc;
use web_time::Instant;
use crate::event::{Event, EventResult};
use crate::focus::FocusId;
use crate::geometry::{Rect, Size};
use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
use crate::widget::{BackbufferCache, BackbufferMode, Widget};
use crate::widgets::multi_click::{MultiClickTracker, SelectGranularity};
use crate::widgets::scrollbar::ScrollbarAxis;
use super::commands::{CommonStyle, RichCommand};
use super::model::{DocPos, DocRange};
use super::layout::{layout_doc, DocLayout, FontResolver};
use super::model::RichDoc;
use super::view::SharedResolver;
pub mod core;
mod context_menu;
mod geometry;
mod input;
mod paint;
mod scroll;
pub use core::{RichEditCore, RichEditHandle};
#[cfg(test)]
mod tests;
#[cfg(test)]
mod handle_api_tests;
#[cfg(test)]
mod preview_dirty_tests;
#[cfg(test)]
mod tab_indent_tests;
#[cfg(test)]
mod trailing_space_tests;
#[cfg(test)]
mod word_delete_tests;
pub struct RichTextEdit {
bounds: Rect,
children: Vec<Box<dyn Widget>>, base: WidgetBase,
core: Rc<RefCell<RichEditCore>>,
resolver: SharedResolver,
padding: f64,
layout: Option<DocLayout>,
layout_width: f64,
layout_doc_rev: u64,
vbar: ScrollbarAxis,
focus_request_id: Option<FocusId>,
focused: bool,
hovered: bool,
selecting_drag: bool,
focus_time: Option<Instant>,
blink_last_phase: Cell<u64>,
multi_click: MultiClickTracker,
select_granularity: SelectGranularity,
select_pivot: (DocPos, DocPos),
start: Instant,
last_layout_rev: Option<u64>,
cache: BackbufferCache,
last_sig: Option<RichEditSig>,
layout_gen: u64,
context_menu: crate::widgets::text_context_menu::TextContextMenu,
context_menu_enabled: bool,
}
#[derive(Clone, Debug, PartialEq)]
struct RichEditSig {
core_rev: u64,
focused: bool,
hovered: bool,
offset_bits: u64,
w_bits: u64,
h_bits: u64,
layout_gen: u64,
}
impl RichTextEdit {
pub fn new(initial: RichDoc, resolver: SharedResolver) -> Self {
Self {
bounds: Rect::default(),
children: Vec::new(),
base: WidgetBase::new(),
core: Rc::new(RefCell::new(RichEditCore::new(initial, 16.0))),
resolver,
padding: 8.0,
layout: None,
layout_width: -1.0,
layout_doc_rev: u64::MAX,
vbar: ScrollbarAxis {
enabled: true,
..ScrollbarAxis::default()
},
focus_request_id: None,
focused: false,
hovered: false,
selecting_drag: false,
focus_time: None,
blink_last_phase: Cell::new(0),
multi_click: MultiClickTracker::default(),
select_granularity: SelectGranularity::default(),
select_pivot: (DocPos::new(0, 0), DocPos::new(0, 0)),
start: Instant::now(),
last_layout_rev: None,
cache: BackbufferCache::default(),
last_sig: None,
layout_gen: 0,
context_menu: crate::widgets::text_context_menu::TextContextMenu::new(),
context_menu_enabled: true,
}
}
pub fn with_context_menu(mut self, v: bool) -> Self {
self.context_menu_enabled = v;
self
}
fn cache_sig(&self) -> RichEditSig {
RichEditSig {
core_rev: self.core.borrow().rev(),
focused: self.focused,
hovered: self.hovered,
offset_bits: self.vbar.offset.to_bits(),
w_bits: self.bounds.width.to_bits(),
h_bits: self.bounds.height.to_bits(),
layout_gen: self.layout_gen,
}
}
pub fn with_font_size(self, size: f64) -> Self {
self.core.borrow_mut().set_default_font_size(size);
self
}
pub fn with_padding(mut self, p: f64) -> Self {
self.padding = p;
self
}
pub fn with_margin(mut self, m: Insets) -> Self {
self.base.margin = m;
self
}
pub fn with_focus_id(mut self, id: FocusId) -> Self {
self.focus_request_id = Some(id);
self
}
pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
self.base.h_anchor = h;
self
}
pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
self.base.v_anchor = v;
self
}
pub fn handle(&self) -> RichEditHandle {
RichEditHandle::new(Rc::clone(&self.core))
}
pub fn exec(&mut self, cmd: &RichCommand) {
self.core.borrow_mut().exec(cmd);
}
pub fn common_style_of_selection(&self) -> CommonStyle {
self.core.borrow().common_style_of_selection()
}
pub fn select_all(&mut self) {
self.core.borrow_mut().select_all();
}
pub fn set_caret(&mut self, pos: DocPos) {
self.core.borrow_mut().set_caret(pos, false);
}
pub fn set_selection(&mut self, range: DocRange) {
self.core.borrow_mut().set_selection(range.start, range.end);
}
pub fn selection(&self) -> DocRange {
self.core.borrow().selection()
}
pub fn load(&mut self, doc: RichDoc) {
self.core.borrow_mut().load(doc);
}
pub fn undo(&mut self) -> bool {
self.core.borrow_mut().undo()
}
pub fn redo(&mut self) -> bool {
self.core.borrow_mut().redo()
}
pub fn can_undo(&self) -> bool {
self.core.borrow().can_undo()
}
pub fn can_redo(&self) -> bool {
self.core.borrow().can_redo()
}
pub fn plain_text(&self) -> String {
self.core.borrow().doc().plain_text()
}
pub fn invalidate_layout(&mut self) {
self.layout = None;
self.layout_width = -1.0;
self.layout_doc_rev = u64::MAX;
self.layout_gen = self.layout_gen.wrapping_add(1);
}
fn ensure_layout(&mut self, width: f64) {
let doc_rev = self.core.borrow().doc_rev();
if self.layout.is_some()
&& (self.layout_width - width).abs() < 0.5
&& self.layout_doc_rev == doc_rev
{
return;
}
let resolver: &FontResolver = &*self.resolver;
let core = self.core.borrow();
self.layout = Some(layout_doc(
core.doc(),
width,
core.default_font_size(),
resolver,
));
self.layout_width = width;
self.layout_doc_rev = doc_rev;
}
pub(crate) fn content_height(&self) -> f64 {
self.layout.as_ref().map(|l| l.height).unwrap_or(0.0)
}
}
impl Widget for RichTextEdit {
fn type_name(&self) -> &'static str {
"RichTextEdit"
}
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, b: Rect) {
self.bounds = b;
}
fn children(&self) -> &[Box<dyn Widget>] {
&self.children
}
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
&mut self.children
}
fn is_focusable(&self) -> bool {
true
}
fn focus_id(&self) -> Option<FocusId> {
self.focus_request_id
}
fn accepts_text_input(&self) -> bool {
true
}
fn text_input_value(&self) -> Option<String> {
Some(self.plain_text())
}
fn margin(&self) -> Insets {
self.base.margin
}
fn widget_base(&self) -> Option<&WidgetBase> {
Some(&self.base)
}
fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
Some(&mut self.base)
}
fn h_anchor(&self) -> HAnchor {
self.base.h_anchor
}
fn v_anchor(&self) -> VAnchor {
self.base.v_anchor
}
fn min_size(&self) -> Size {
self.base.min_size
}
fn max_size(&self) -> Size {
self.base.max_size
}
fn backbuffer_cache_mut(&mut self) -> Option<&mut BackbufferCache> {
Some(&mut self.cache)
}
fn backbuffer_mode(&self) -> BackbufferMode {
if crate::font_settings::lcd_enabled() {
BackbufferMode::LcdCoverage
} else {
BackbufferMode::Rgba
}
}
fn measure_min_height(&self, available_w: f64) -> f64 {
let resolver: &FontResolver = &*self.resolver;
let core = self.core.borrow();
let inner_w = (available_w - self.padding * 2.0).max(1.0);
layout_doc(core.doc(), inner_w, core.default_font_size(), resolver).height
+ self.padding * 2.0
}
fn layout(&mut self, available: Size) -> Size {
let w = available.width.max(self.padding * 2.0 + 20.0);
let h = available
.height
.max(self.padding * 2.0 + 20.0);
self.bounds = Rect::new(0.0, 0.0, w, h);
let inner_w = (w - self.padding * 2.0).max(1.0);
self.ensure_layout(inner_w);
self.sync_scroll();
let time = self.start.elapsed().as_secs_f64();
let in_flux = self.core.borrow_mut().feed_undo(time);
if in_flux {
crate::animation::request_draw_after_tagged(
std::time::Duration::from_millis(120),
"rich_text::editor undo-settle",
);
}
let rev = self.core.borrow().rev();
if matches!(self.last_layout_rev, Some(prev) if prev != rev) {
self.ensure_caret_visible();
}
self.last_layout_rev = Some(rev);
let sig = self.cache_sig();
if self.last_sig.as_ref() != Some(&sig) {
self.last_sig = Some(sig);
self.cache.invalidate();
}
Size::new(w, h)
}
fn paint(&mut self, ctx: &mut dyn crate::draw_ctx::DrawCtx) {
self.paint_content(ctx);
}
fn paint_overlay(&mut self, ctx: &mut dyn crate::draw_ctx::DrawCtx) {
self.paint_scrollbar(ctx);
self.paint_caret(ctx);
}
fn hit_test(&self, local: crate::geometry::Point) -> bool {
local.x >= 0.0
&& local.x <= self.bounds.width
&& local.y >= 0.0
&& local.y <= self.bounds.height
}
fn needs_draw(&self) -> bool {
if self.scrollbar_animating() {
return true;
}
if !self.focused {
return false;
}
let Some(t) = self.focus_time else {
return false;
};
let current_phase = (t.elapsed().as_millis() / 500) as u64;
current_phase != self.blink_last_phase.get()
}
fn next_draw_deadline(&self) -> Option<web_time::Instant> {
if !self.focused {
return None;
}
let t = self.focus_time?;
let ms = t.elapsed().as_millis() as u64;
let next_phase = (ms / 500) + 1;
Some(t + std::time::Duration::from_millis(next_phase * 500))
}
fn on_event(&mut self, event: &Event) -> EventResult {
self.handle_event(event)
}
fn has_active_modal(&self) -> bool {
self.context_menu.is_open()
}
fn paint_global_overlay(&mut self, ctx: &mut dyn crate::draw_ctx::DrawCtx) {
let font = self.context_menu_font();
let size = self.core.borrow().default_font_size();
self.context_menu.paint(ctx, font, size);
}
}