Add retained paints and shared text selection

This commit is contained in:
iris committed 2026-09-10 18:35:24 -04:00
1 parent 1e6d3b1edd
commit a33fbca966
42 files changed
+2424 -470

No files matched your search

+146 -2
View File
@@ -1,9 +1,11 @@
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};
@@ -19,6 +21,7 @@ pub struct TextView {
tex: Option<RenderedText>,
width: Option<f32>,
pub hint: Option<StrongWidget>,
selection: TextSelection,
}
impl TextView {
@@ -37,6 +40,7 @@ impl TextView {
tex: None,
width: None,
hint,
selection: TextSelection::default(),
}
}
@@ -94,6 +98,39 @@ impl TextView {
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(&region),
);
}
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(&region),
);
}
used
}
pub fn content(&self) -> String {
self.buf.text().to_string()
}
@@ -111,14 +148,36 @@ impl Text {
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<String> {
self.view.selection.selected_text(self.view.buf.text())
}
pub fn selection_range(&self) -> Option<std::ops::Range<usize>> {
self.view.selection.range()
}
pub fn set_with_spans(&mut self, content: impl Into<String>, spans: Vec<SpanStyle>) {
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 = self.view.draw(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);
}
@@ -127,6 +186,8 @@ impl Widget for Text {
}
}
pub(super) const CARET_WIDTH: f32 = 1.0;
impl Deref for Text {
type Target = TextAttrs;
@@ -160,6 +221,89 @@ mod tests {
use crate::layout_tests::TestRsc;
use crate::prelude::*;
fn rendered_text(content: &str) -> (TestRsc, UiRenderState, WeakWidget<Text>, 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 {
@@ -167,7 +311,7 @@ mod tests {
};
let root = wtext("hello there")
.size(18)
.color(UiColor::WHITE)
.color(PaintId::WHITE)
.add_strong(&mut rsc)
.any();
let mut render = UiRenderState::new();