mod build; mod edit; mod selection; pub use build::*; pub use edit::*; use iris_core::util::MutDetect; pub use selection::*; use crate::prelude::*; use std::ops::{Deref, DerefMut}; pub struct Text { pub content: MutDetect, view: TextView, } pub struct TextView { pub attrs: MutDetect, pub buf: MutDetect, tex: Option, width: Option, pub hint: Option, selection: TextSelection, } impl TextView { fn is_blank(&self) -> bool { self.buf.is_empty() } pub fn wrap_width(&self) -> Option { self.width } pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option) -> Self { Self { attrs: attrs.into(), buf: buf.into(), tex: None, width: None, hint, selection: TextSelection::default(), } } pub fn region(&self) -> UiRegion { self.tex() .map(|t| t.size) .unwrap_or(Vec2::ZERO) .align(self.align) } fn render(&mut self, painter: &mut Painter) -> RenderedText { let width = if self.attrs.wrap { Some(painter.px_size().x) } else { None }; let generation = painter.atlas_generation(); if width == self.width && let Some(tex) = &self.tex && tex.generation == generation && !self.attrs.changed && !self.buf.changed { return tex.clone(); } self.width = width; let tex = painter.render_text(&mut self.buf, &self.attrs, width); if crate::diagnostics::trace_enabled() { log::debug!( target: "iris::frame", "iris text render: chars={} width={width:?} glyphs={} size={:?}", self.buf.text().chars().count(), tex.glyphs.len(), tex.size, ); } self.tex = Some(tex.clone()); self.attrs.changed = false; self.buf.changed = false; tex } pub fn tex(&self) -> Option<&RenderedText> { self.tex.as_ref() } 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.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 layout = self.buf.layout(); for (rect, _) in selection.geometry(layout) { 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 = 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.buf.text().to_string() } } impl Text { pub fn new(content: impl Into) -> Self { let content: String = content.into(); Self { view: TextView::new(TextBuffer::new(&content), TextAttrs::default(), None), content: content.into(), } } fn update_buf(&mut self) { if self.content.changed { self.content.changed = false; self.view.buf.set_text(self.content.as_str()); self.view.selection.deselect(); } } pub fn selected_text(&self) -> Option { self.view.selection.selected_text(self.view.buf.text()) } pub fn selection_range(&self) -> Option> { self.view.selection.range() } pub fn set_with_spans(&mut self, content: impl Into, spans: Vec) { let content = content.into(); *self.content = content.clone(); self.content.changed = false; self.view.buf.set_text(content); self.view.buf.set_spans(spans); self.view.selection.deselect(); } } impl Widget for Text { fn draw(&mut self, painter: &mut Painter) { self.update_buf(); 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; impl Deref for Text { type Target = TextAttrs; fn deref(&self) -> &Self::Target { &self.view } } impl DerefMut for Text { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.view } } impl Deref for TextView { type Target = TextAttrs; fn deref(&self) -> &Self::Target { &self.attrs } } impl DerefMut for TextView { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.attrs } } #[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: UiData::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 view = TextView::new(TextBuffer::new("hello there"), TextAttrs::default(), None); let mut edit = TextEdit::new(view, EditMode::MultiLine); let mut data = TextData::default(); TextEditCtx { text: &mut edit, data: &mut data, } .select_all(); assert_eq!( rsc.ui.widgets[text].selection_range(), edit.selection_range() ); assert_eq!(rsc.ui.widgets[text].selected_text(), edit.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].content = "after".to_string(); 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 view = TextView::new(TextBuffer::new("selected"), TextAttrs::default(), None); let edit = rsc .ui .widgets .add_strong(TextEdit::new(view, EditMode::MultiLine)); 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.edit(&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: UiData::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.atlas.glyph_count(); assert!(rasterised > 0, "the first frame rasterised no glyphs"); rsc.ui.text.atlas.clear(); assert_eq!(rsc.ui.text.atlas.glyph_count(), 0); render.resize((800.0, 600.0)); render.update(&root, &mut rsc); assert_eq!( rsc.ui.text.atlas.glyph_count(), rasterised, "the second frame re-emitted its cached glyphs instead of \ re-rendering them against the fresh atlas" ); } }