mod build; mod edit; mod selection; pub use build::*; pub use edit::*; pub use selection::*; use crate::prelude::*; pub struct Text { view: TextView, } pub struct TextView { text: TextHandle, pub hint: Option, selection: TextSelection, } impl TextView { fn is_blank(&self) -> bool { self.text.text().is_empty() } pub fn wrap_width(&self) -> Option { self.text.width() } pub fn new(text: TextHandle, hint: Option) -> Self { Self { text, hint, selection: TextSelection::default(), } } pub fn region(&self) -> UiRegion { let align = self.text.attrs().align; self.text .rendered() .map(|t| t.size) .unwrap_or(Vec2::ZERO) .align(align) } fn render(&mut self, painter: &mut Painter) -> RenderedText { let width = if self.text.attrs().wrap { Some(painter.px_size().x) } else { None }; let tex = painter.render_text(&self.text, width); if crate::diagnostics::trace_enabled() { log::debug!( target: "iris::frame", "iris text render: chars={} width={width:?} glyphs={} size={:?}", self.text.text().chars().count(), tex.glyphs.len(), tex.size, ); } tex } pub fn draw(&mut self, painter: &mut Painter) -> Size { let tex = self.render(painter); if self.is_blank() && let Some(hint) = &self.hint { return painter.widget(hint).size(); } let region = tex.size.align(self.text.attrs().align); let within = region.within(&painter.region()); painter.glyphs(&tex, within); Size::abs(tex.size) } pub(super) fn draw_selectable(&mut self, painter: &mut Painter, caret: bool) -> Size { let base = painter.layer; painter.child_layer(); let used = self.draw(painter); painter.layer = base; let region = self.region(); let Some(selection) = self.selection.range else { return used; }; let geometry = self .text .with_layout(|layout, _| selection.geometry(layout)); for (rect, _) in geometry { let size = vec2(rect.width() as f32, rect.height() as f32); let top_left = vec2(rect.x0 as f32, rect.y0 as f32); let paint = painter.paint(&PaintId::SKY); painter.primitive_within( RectPrimitive::color(paint), size.align(Align::TOP_LEFT).offset(top_left).within(®ion), ); } if caret { let caret = self .text .with_layout(|layout, _| selection.focus().geometry(layout, CARET_WIDTH)); let size = vec2(caret.width() as f32, caret.height() as f32); let top_left = vec2(caret.x0 as f32, caret.y0 as f32); let paint = painter.paint(&PaintId::WHITE); painter.primitive_within( RectPrimitive::color(paint), size.align(Align::TOP_LEFT).offset(top_left).within(®ion), ); } used } pub fn content(&self) -> String { self.text.text().to_string() } pub fn update_attrs(&mut self, update: impl FnOnce(&mut TextAttrs) -> R) -> R { self.text.update_attrs(update) } } impl Text { pub fn set_text(&mut self, content: impl Into) { if self.view.text.set_text(content) { self.view.selection.deselect(); } } pub fn selected_text(&self) -> Option { self.view.selection.selected_text(&self.view.text.text()) } pub fn selection_range(&self) -> Option> { self.view.selection.range() } pub fn set_with_spans(&mut self, content: impl Into, spans: Vec) { self.view.text.set_text(content); self.view.text.set_spans(spans); self.view.selection.deselect(); } pub fn content(&self) -> String { self.view.content() } pub fn update_attrs(&mut self, update: impl FnOnce(&mut TextAttrs) -> R) -> R { self.view.update_attrs(update) } } impl Widget for Text { fn draw(&mut self, painter: &mut Painter) { let size = if self.view.selection.range.is_some() { self.view.draw_selectable(painter, false) } else { self.view.draw(painter) }; painter.set_size(size); } fn requires_exact_region(&self) -> bool { true } } pub(super) const CARET_WIDTH: f32 = 1.0; #[cfg(test)] mod tests { use crate::layout_tests::TestRsc; use crate::prelude::*; fn rendered_text(content: &str) -> (TestRsc, UiRenderState, WeakWidget, StrongWidget) { let mut rsc = TestRsc { ui: Ui::default() }; let text = wtext(content).add_strong(&mut rsc); let id = text.weak(); let root = text.any(); let mut render = UiRenderState::new(); render.resize((800.0, 600.0)); render.update(&root, &mut rsc); (rsc, render, id, root) } #[test] fn display_text_and_edit_text_use_the_same_selection_engine() { let (mut rsc, _render, text, _root) = rendered_text("hello there"); text.selection(&mut rsc).select_all(); let edit = wtext("hello there") .editable(EditMode::MultiLine) .add_strong(&mut rsc); let edit_id = edit.weak(); edit_id(&mut rsc).select_all(); assert_eq!( rsc.ui.widgets[text].selection_range(), rsc.ui.widgets[edit_id].selection_range() ); assert_eq!( rsc.ui.widgets[text].selected_text(), rsc.ui.widgets[edit_id].selected_text() ); } #[test] fn changing_display_text_clears_its_now_stale_selection() { let (mut rsc, mut render, text, root) = rendered_text("before"); text.selection(&mut rsc).select_all(); assert_eq!( rsc.ui.widgets[text].selected_text().as_deref(), Some("before") ); rsc.ui.widgets[text].set_text("after"); render.update(&root, &mut rsc); assert_eq!(rsc.ui.widgets[text].selected_text(), None); assert_eq!(rsc.ui.widgets[text].selection_range(), None); } #[test] fn display_text_draws_the_shared_highlight_without_an_editing_caret() { let (mut rsc, mut render, text, root) = rendered_text("selected"); let plain = render.active[&text.id()].primitives.len(); text.selection(&mut rsc).select_all(); render.update(&root, &mut rsc); let selected = render.active[&text.id()].primitives.len(); let edit = wtext("selected") .editable(EditMode::MultiLine) .add_strong(&mut rsc); let edit_id = edit.weak(); let edit_root = edit.any(); let mut edit_render = UiRenderState::new(); edit_render.resize((800.0, 600.0)); edit_render.update(&edit_root, &mut rsc); edit_id(&mut rsc).select_all(); edit_render.update(&edit_root, &mut rsc); let editable = edit_render.active[&edit_id.id()].primitives.len(); assert!( selected > plain, "the selection added no highlight primitive" ); assert_eq!( editable, selected + 1, "editable text should add exactly its caret to the shared highlight" ); } #[test] fn clearing_the_atlas_re_renders_cached_text_instead_of_reusing_it() { let mut rsc = TestRsc { ui: Ui::default() }; let root = wtext("hello there") .size(18) .color(PaintId::WHITE) .add_strong(&mut rsc) .any(); let mut render = UiRenderState::new(); render.resize((800.0, 600.0)); render.update(&root, &mut rsc); let rasterised = rsc.ui.text.borrow().atlas.glyph_count(); assert!(rasterised > 0, "the first frame rasterised no glyphs"); rsc.ui.text.borrow_mut().atlas.clear(); assert_eq!(rsc.ui.text.borrow().atlas.glyph_count(), 0); render.resize((800.0, 600.0)); render.update(&root, &mut rsc); assert_eq!( rsc.ui.text.borrow().atlas.glyph_count(), rasterised, "the second frame re-emitted its cached glyphs instead of \ re-rendering them against the fresh atlas" ); } }