284 lines
8.5 KiB
Rust
284 lines
8.5 KiB
Rust
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<StrongWidget>,
|
|
selection: TextSelection,
|
|
}
|
|
|
|
impl TextView {
|
|
fn is_blank(&self) -> bool {
|
|
self.text.text().is_empty()
|
|
}
|
|
|
|
pub fn wrap_width(&self) -> Option<f32> {
|
|
self.text.width()
|
|
}
|
|
|
|
pub fn new(text: TextHandle, hint: Option<StrongWidget>) -> 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<R>(&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<String>) {
|
|
if self.view.text.set_text(content) {
|
|
self.view.selection.deselect();
|
|
}
|
|
}
|
|
|
|
pub fn selected_text(&self) -> Option<String> {
|
|
self.view.selection.selected_text(&self.view.text.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>) {
|
|
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<R>(&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<Text>, 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"
|
|
);
|
|
}
|
|
}
|