iris: move text state into shared resources

This commit is contained in:
iris committed 2026-09-12 14:18:12 -04:00
1 parent 7c23c5f146
commit 51719ed121
17 files changed
+631 -434

No files matched your search

+5 -6
View File
@@ -21,9 +21,10 @@ exists only to give a row enough text to wrap across several lines at a \
typical phone column width."; typical phone column width.";
fn build_row(rsc: &mut BenchRsc, i: usize, image_every: usize) -> StrongWidget { fn build_row(rsc: &mut BenchRsc, i: usize, image_every: usize) -> StrongWidget {
let mut text = Text::new(format!("Message {i}: {BODY}")); let text = wtext(format!("Message {i}: {BODY}"))
text.wrap = true; .wrap(true)
let text = rsc.ui.widgets.add_strong(text).any(); .add_strong(rsc)
.any();
if image_every > 0 && i.is_multiple_of(image_every) { if image_every > 0 && i.is_multiple_of(image_every) {
let img = image::DynamicImage::new_rgba8(64, 64); let img = image::DynamicImage::new_rgba8(64, 64);
@@ -321,9 +322,7 @@ fn bench_redraw_big_text(chars: usize, redraws: usize) {
let content: String = (0..chars) let content: String = (0..chars)
.map(|i| char::from(b'a' + (i % 26) as u8)) .map(|i| char::from(b'a' + (i % 26) as u8))
.collect(); .collect();
let mut text = Text::new(content); let text = wtext(content).wrap(true).add_strong(&mut rsc);
text.wrap = true;
let text = rsc.ui.widgets.add_strong(text);
let handle = text.weak(); let handle = text.weak();
let root = text.any(); let root = text.any();
+289 -24
View File
@@ -1,10 +1,23 @@
use crate::{Align, GlyphAtlas, GlyphKey, PaintId, PlacedGlyph, RegionAlign, Textures, util::Vec2}; use crate::{
Align, GlyphAtlas, GlyphKey, PaintId, PlacedGlyph, RegionAlign, Textures, WidgetId,
util::{SlotId, SlotVec, Vec2},
};
use parley::{ use parley::{
Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily, Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily,
Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
fontique::{Blob, FontInfoOverride}, fontique::{Blob, FontInfoOverride},
}; };
use std::{collections::HashMap, fmt, ops::Range, sync::Arc}; use std::{
cell::{Ref, RefCell},
collections::HashMap,
fmt,
ops::{Deref, DerefMut, Range},
rc::Rc,
sync::{
Arc,
mpsc::{Receiver, Sender, channel},
},
};
use swash::{ use swash::{
FontRef, FontRef,
scale::{Render, ScaleContext, Source, StrikeWith}, scale::{Render, ScaleContext, Source, StrikeWith},
@@ -25,7 +38,6 @@ pub struct FontDiagnostics {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum FontRegistrationError { pub enum FontRegistrationError {
AlreadyRegistered(String), AlreadyRegistered(String),
TextAlreadyShaped,
InvalidFont(String), InvalidFont(String),
} }
@@ -35,10 +47,6 @@ impl fmt::Display for FontRegistrationError {
Self::AlreadyRegistered(family) => { Self::AlreadyRegistered(family) => {
write!(f, "font data is already registered for {family:?}") write!(f, "font data is already registered for {family:?}")
} }
Self::TextAlreadyShaped => write!(
f,
"cannot register font data after text has been shaped; register application fonts before the first draw"
),
Self::InvalidFont(family) => { Self::InvalidFont(family) => {
write!( write!(
f, f,
@@ -57,10 +65,9 @@ pub struct TextData {
scale_cx: ScaleContext, scale_cx: ScaleContext,
pub atlas: GlyphAtlas, pub atlas: GlyphAtlas,
/// Physical pixels per dp -- a second copy of /// Physical pixels per dp -- a second copy of
/// `UiRenderState::density`, kept here too because `TextEditCtx::layout` /// `UiRenderState::density`, kept here too because cursor movement and
/// (cursor movement and hit-testing, `widget/text/edit.rs`) shapes text /// hit-testing shape text from event callbacks that have no `Painter`,
/// from an event callback that has a `TextData` but no `Painter`, so it /// so they have nowhere else to read the display's density from. Both copies are
/// has nowhere else to read the display's density from. Both copies are
/// set together, from the one place either backend learns the real /// set together, from the one place either backend learns the real
/// value (`android::view::new_peer`); this is the same accepted /// value (`android::view::new_peer`); this is the same accepted
/// duplication as `AndroidRenderer::content_scale`; a single source of /// duplication as `AndroidRenderer::content_scale`; a single source of
@@ -69,7 +76,6 @@ pub struct TextData {
pub density: f32, pub density: f32,
registered_families: HashMap<String, String>, registered_families: HashMap<String, String>,
next_registered_family: u64, next_registered_family: u64,
shaping_started: bool,
} }
impl Default for TextData { impl Default for TextData {
@@ -84,7 +90,6 @@ impl Default for TextData {
density: 1.0, density: 1.0,
registered_families: HashMap::new(), registered_families: HashMap::new(),
next_registered_family: 0, next_registered_family: 0,
shaping_started: false,
} }
} }
} }
@@ -168,9 +173,6 @@ impl TextData {
data: impl AsRef<[u8]> + Send + Sync + 'static, data: impl AsRef<[u8]> + Send + Sync + 'static,
) -> Result<(), FontRegistrationError> { ) -> Result<(), FontRegistrationError> {
let family = family.as_ref().to_owned(); let family = family.as_ref().to_owned();
if self.shaping_started {
return Err(FontRegistrationError::TextAlreadyShaped);
}
if self.registered_families.contains_key(&family) { if self.registered_families.contains_key(&family) {
return Err(FontRegistrationError::AlreadyRegistered(family)); return Err(FontRegistrationError::AlreadyRegistered(family));
} }
@@ -523,7 +525,6 @@ impl TextBuffer {
width: Option<f32>, width: Option<f32>,
density: f32, density: f32,
) { ) {
data.shaping_started = true;
if self.shaped.as_ref() == Some(&(attrs.clone(), width, density)) { if self.shaped.as_ref() == Some(&(attrs.clone(), width, density)) {
return; return;
} }
@@ -574,6 +575,10 @@ impl TextBuffer {
.align(Alignment::Start, AlignmentOptions::default()); .align(Alignment::Start, AlignmentOptions::default());
self.shaped = Some((attrs.clone(), width, density)); self.shaped = Some((attrs.clone(), width, density));
} }
fn invalidate(&mut self) {
self.shaped = None;
}
} }
fn hash_coords(coords: &[i16]) -> u64 { fn hash_coords(coords: &[i16]) -> u64 {
@@ -605,6 +610,261 @@ pub struct RenderedText {
pub generation: u64, pub generation: u64,
} }
pub struct TextResource {
buffer: TextBuffer,
attrs: TextAttrs,
rendered: Option<RenderedText>,
width: Option<f32>,
density: f32,
owner: Option<WidgetId>,
}
impl TextResource {
fn new(buffer: TextBuffer, attrs: TextAttrs) -> Self {
Self {
buffer,
attrs,
rendered: None,
width: None,
density: 0.0,
owner: None,
}
}
fn invalidate(&mut self) {
self.buffer.invalidate();
self.rendered = None;
}
}
/// All text storage and shaping state for one [`crate::Ui`]. Widgets retain a
/// compact [`TextHandle`] into this arena rather than owning Parley layouts.
pub struct TextResources {
data: TextData,
entries: SlotVec<TextResource>,
send: Sender<SlotId>,
recv: Receiver<SlotId>,
}
impl TextResources {
pub fn new() -> Self {
let (send, recv) = channel();
Self {
data: TextData::default(),
entries: SlotVec::new(),
send,
recv,
}
}
pub fn add(resources: Rc<RefCell<Self>>, buffer: TextBuffer, attrs: TextAttrs) -> TextHandle {
let (id, send) = {
let mut resources = resources.borrow_mut();
resources.free_released();
let id = resources.entries.add(TextResource::new(buffer, attrs));
(id, resources.send.clone())
};
TextHandle {
id,
resources,
send,
}
}
fn entry(&self, id: SlotId) -> &TextResource {
self.entries
.get(id)
.expect("text resource handle points at a released slot")
}
fn entry_mut(&mut self, id: SlotId) -> &mut TextResource {
self.entries
.get_mut(id)
.expect("text resource handle points at a released slot")
}
pub fn free_released(&mut self) {
for id in self.recv.try_iter() {
self.entries.free(id);
}
}
pub fn invalidate_all(&mut self) -> Vec<WidgetId> {
let mut owners = Vec::new();
for resource in self.entries.values_mut() {
resource.invalidate();
if let Some(owner) = resource.owner
&& !owners.contains(&owner)
{
owners.push(owner);
}
}
owners
}
fn shape(&mut self, id: SlotId) {
let density = self.data.density;
let Self { data, entries, .. } = self;
let resource = entries
.get_mut(id)
.expect("text resource handle points at a released slot");
resource
.buffer
.shape(data, &resource.attrs, resource.width, density);
}
fn render(
&mut self,
id: SlotId,
width: Option<f32>,
owner: WidgetId,
textures: &mut Textures,
density: f32,
) -> (RenderedText, bool) {
let atlas_generation = self.data.atlas.generation();
let resource = self.entry_mut(id);
resource.owner = Some(owner);
if resource.width == width
&& resource.density == density
&& let Some(rendered) = &resource.rendered
&& rendered.generation == atlas_generation
{
return (rendered.clone(), false);
}
resource.width = width;
resource.density = density;
let Self { data, entries, .. } = self;
let resource = entries
.get_mut(id)
.expect("text resource handle points at a released slot");
let rendered = data.render(
&mut resource.buffer,
&resource.attrs,
width,
textures,
density,
);
resource.rendered = Some(rendered.clone());
(rendered, true)
}
}
impl Default for TextResources {
fn default() -> Self {
Self::new()
}
}
impl Deref for TextResources {
type Target = TextData;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl DerefMut for TextResources {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.data
}
}
/// A widget-owned reference to one entry in [`TextResources`]. The arena is
/// shared once per UI; constructing a handle does not allocate a resource of
/// its own.
pub struct TextHandle {
id: SlotId,
resources: Rc<RefCell<TextResources>>,
send: Sender<SlotId>,
}
impl TextHandle {
pub fn text(&self) -> Ref<'_, str> {
Ref::map(self.resources.borrow(), |resources| {
resources.entry(self.id).buffer.text()
})
}
pub fn attrs(&self) -> Ref<'_, TextAttrs> {
Ref::map(self.resources.borrow(), |resources| {
&resources.entry(self.id).attrs
})
}
pub fn set_text(&mut self, text: impl Into<String>) -> bool {
let text = text.into();
let mut resources = self.resources.borrow_mut();
let resource = resources.entry_mut(self.id);
if resource.buffer.text() == text {
return false;
}
resource.buffer.set_text(text);
resource.rendered = None;
true
}
pub fn edit_text<R>(&mut self, edit: impl FnOnce(&mut String) -> R) -> R {
let mut resources = self.resources.borrow_mut();
let resource = resources.entry_mut(self.id);
resource.rendered = None;
edit(resource.buffer.edit())
}
pub fn set_spans(&mut self, spans: Vec<SpanStyle>) {
let mut resources = self.resources.borrow_mut();
let resource = resources.entry_mut(self.id);
resource.buffer.set_spans(spans);
resource.rendered = None;
}
pub fn update_attrs<R>(&mut self, update: impl FnOnce(&mut TextAttrs) -> R) -> R {
let mut resources = self.resources.borrow_mut();
let resource = resources.entry_mut(self.id);
resource.invalidate();
update(&mut resource.attrs)
}
pub fn rendered(&self) -> Option<RenderedText> {
self.resources.borrow().entry(self.id).rendered.clone()
}
pub fn width(&self) -> Option<f32> {
self.resources.borrow().entry(self.id).width
}
pub fn with_layout<R>(&self, f: impl FnOnce(&Layout<PaintId>, &str) -> R) -> R {
let mut resources = self.resources.borrow_mut();
resources.shape(self.id);
let resource = resources.entry(self.id);
f(resource.buffer.layout(), resource.buffer.text())
}
pub fn layout(&self) -> Ref<'_, Layout<PaintId>> {
self.resources.borrow_mut().shape(self.id);
Ref::map(self.resources.borrow(), |resources| {
resources.entry(self.id).buffer.layout()
})
}
pub fn render(
&self,
width: Option<f32>,
owner: WidgetId,
textures: &mut Textures,
density: f32,
) -> (RenderedText, bool) {
self.resources
.borrow_mut()
.render(self.id, width, owner, textures, density)
}
}
impl Drop for TextHandle {
fn drop(&mut self) {
let _ = self.send.send(self.id);
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -619,13 +879,18 @@ mod tests {
} }
#[test] #[test]
fn registering_after_shaping_is_reported() { fn dropped_handles_release_their_arena_entries() {
let mut data = TextData::default(); let resources = Rc::new(RefCell::new(TextResources::new()));
let mut buffer = TextBuffer::new("ordinary platform text"); let text = TextResources::add(
buffer.shape(&mut data, &TextAttrs::default(), None, 1.0); resources.clone(),
assert_eq!( TextBuffer::new("temporary"),
data.register_font("icons", b"not a font" as &'static [u8]), TextAttrs::default(),
Err(FontRegistrationError::TextAlreadyShaped)
); );
assert_eq!(resources.borrow().entries.len(), 1);
drop(text);
resources.borrow_mut().free_released();
assert!(resources.borrow().entries.is_empty());
} }
} }
+22 -9
View File
@@ -1,5 +1,6 @@
use crate::{ use crate::{
Mask, MoveOffset, Paints, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena, Mask, MoveOffset, Paints, TextResources, Textures, WeakWidget, WidgetId, Widgets,
util::TrackedArena,
}; };
use std::{ use std::{
cell::{Ref, RefCell, RefMut}, cell::{Ref, RefCell, RefMut},
@@ -22,7 +23,7 @@ pub struct UiData {
pub widgets: Widgets, pub widgets: Widgets,
pub paints: Paints, pub paints: Paints,
pub textures: Textures, pub textures: Textures,
pub text: TextData, pub text: Rc<RefCell<TextResources>>,
pub masks: TrackedArena<Mask, u32>, pub masks: TrackedArena<Mask, u32>,
pub move_offsets: TrackedArena<MoveOffset, u32>, pub move_offsets: TrackedArena<MoveOffset, u32>,
animating: Vec<WidgetId>, animating: Vec<WidgetId>,
@@ -67,21 +68,32 @@ pub struct Ui {
impl Ui { impl Ui {
/// Register application-owned font data for a semantic or named family. /// Register application-owned font data for a semantic or named family.
/// /// Existing text resources are invalidated and their active widgets are
/// This must happen before the first text shape. Existing text layouts /// scheduled for layout again.
/// cache their resolved faces, so accepting a later registration would
/// leave already-shaped widgets displaying the old result.
#[track_caller] #[track_caller]
pub fn register_font( pub fn register_font(
&mut self, &mut self,
family: impl AsRef<str>, family: impl AsRef<str>,
data: impl AsRef<[u8]> + Send + Sync + 'static, data: impl AsRef<[u8]> + Send + Sync + 'static,
) -> Result<(), crate::FontRegistrationError> { ) -> Result<(), crate::FontRegistrationError> {
self.data.text.register_font(family, data) let owners = {
let mut text = self.data.text.borrow_mut();
text.register_font(family, data)?;
text.invalidate_all()
};
let active: Vec<WidgetId> = {
let render = self.render_state.get();
owners
.into_iter()
.filter(|owner| render.active.contains_key(owner))
.collect()
};
self.data.widgets.needs_redraw.extend(active);
Ok(())
} }
pub fn is_font_registered(&self, family: impl AsRef<str>) -> bool { pub fn is_font_registered(&self, family: impl AsRef<str>) -> bool {
self.data.text.is_font_registered(family) self.data.text.borrow().is_font_registered(family)
} }
/// A read-only handle to the retained result of the last completed frame. /// A read-only handle to the retained result of the last completed frame.
@@ -96,7 +108,7 @@ impl Ui {
} }
pub fn set_density(&mut self, density: f32) { pub fn set_density(&mut self, density: f32) {
self.data.text.density = density; self.data.text.borrow_mut().density = density;
self.render_state.get_mut().set_density(density); self.render_state.get_mut().set_density(density);
} }
} }
@@ -172,5 +184,6 @@ pub trait UiRsc {
} }
self.ui_mut().textures.free(); self.ui_mut().textures.free();
self.ui_mut().paints.free_released(); self.ui_mut().paints.free_released();
self.ui_mut().text.borrow_mut().free_released();
} }
} }
+11 -15
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
Axis, Len, MoveOffset, PaintId, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs, Axis, Len, MoveOffset, PaintId, RegionAlign, RenderedText, Size, StrongWidget, TextHandle,
TextBuffer, TextData, TextureHandle, UiData, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, TextResources, TextureHandle, UiData, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2,
WidgetId, WidgetId,
render::{ render::{
Drawn, GlyphPrimitive, IMAGE_BINDING, Mask, MaskIdx, MoveIdx, NOT_DRAWN, Primitive, Drawn, GlyphPrimitive, IMAGE_BINDING, Mask, MaskIdx, MoveIdx, NOT_DRAWN, Primitive,
@@ -9,6 +9,7 @@ use crate::{
ui::render_state::Retained, ui::render_state::Retained,
util::Vec2, util::Vec2,
}; };
use std::{cell::RefCell, rc::Rc};
pub struct Painter<'a> { pub struct Painter<'a> {
pub(super) render_state: &'a mut UiRenderState, pub(super) render_state: &'a mut UiRenderState,
@@ -413,21 +414,16 @@ impl<'a> Painter<'a> {
self.own(h); self.own(h);
} }
pub fn render_text( pub fn render_text(&mut self, text: &TextHandle, width: Option<f32>) -> RenderedText {
&mut self,
buffer: &mut TextBuffer,
attrs: &TextAttrs,
width: Option<f32>,
) -> RenderedText {
let density = self.render_state.density; let density = self.render_state.density;
self.render_state.shape_count += 1;
let ui: &mut UiData = self.rsc.ui_mut(); let ui: &mut UiData = self.rsc.ui_mut();
ui.text let (rendered, prepared) = text.render(width, self.id, &mut ui.textures, density);
.render(buffer, attrs, width, &mut ui.textures, density) self.render_state.shape_count += u64::from(prepared);
rendered
} }
pub fn atlas_generation(&mut self) -> u64 { fn atlas_generation(&self) -> u64 {
self.rsc.ui_mut().text.atlas.generation() self.rsc.ui().text.borrow().atlas.generation()
} }
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) { pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
@@ -493,8 +489,8 @@ impl<'a> Painter<'a> {
self.region.size().to_abs(self.render_state.output_size) self.region.size().to_abs(self.render_state.output_size)
} }
pub fn text_data(&mut self) -> &mut TextData { pub fn text_resources(&mut self) -> Rc<RefCell<TextResources>> {
&mut self.rsc.ui_mut().text self.rsc.ui().text.clone()
} }
pub fn child_layer(&mut self) { pub fn child_layer(&mut self) {
+4
View File
@@ -71,6 +71,10 @@ impl<T> SlotVec<T> {
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.len() == 0 self.len() == 0
} }
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut T> {
self.data.iter_mut().filter_map(|(_, value)| value.as_mut())
}
} }
impl<T> Default for SlotVec<T> { impl<T> Default for SlotVec<T> {
+3 -3
View File
@@ -8,8 +8,8 @@ pub(crate) fn update_info<Rsc: UiRsc>(rsc: &mut Rsc, info: WeakWidget<Text>, vie
rsc.widgets().len(), rsc.widgets().len(),
render_state.get().active_widgets(), render_state.get().active_widgets(),
); );
if new != *rsc.widgets()[info].content { if new != rsc.widgets()[info].content() {
*rsc.widgets_mut()[info].content = new; rsc.widgets_mut()[info].set_text(new);
} }
} }
@@ -118,7 +118,7 @@ where
.attr::<Selectable>(()) .attr::<Selectable>(())
.on(Submit, move |ctx, rsc: &mut Rsc| { .on(Submit, move |ctx, rsc: &mut Rsc| {
let w = ctx.widget; let w = ctx.widget;
let content = w.edit(rsc).take(); let content = w(rsc).take();
let text = wtext(content) let text = wtext(content)
.editable(EditMode::MultiLine) .editable(EditMode::MultiLine)
.size(30) .size(30)
+1 -1
View File
@@ -59,7 +59,7 @@ the retained widget tree dirty:
```rust ```rust
rsc.spawn_task(async move |mut ctx| { rsc.spawn_task(async move |mut ctx| {
let text = load_text().await; let text = load_text().await;
ctx.update(move |_, rsc| label.edit(rsc).set(&text)); ctx.update(move |_, rsc| label(rsc).set(&text));
}); });
``` ```
+30 -29
View File
@@ -43,11 +43,11 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
return; return;
}; };
let content = text.text(); let content = text.text();
let sel_start = byte_to_utf16(content, sel.start) as i32; let sel_start = byte_to_utf16(&content, sel.start) as i32;
let sel_end = byte_to_utf16(content, sel.end) as i32; let sel_end = byte_to_utf16(&content, sel.end) as i32;
let compose_len = self.state.android_state().compose_len; let compose_len = self.state.android_state().compose_len;
let (comp_start, comp_end) = if compose_len > 0 { let (comp_start, comp_end) = if compose_len > 0 {
let caret = byte_to_utf16(content, text.caret().unwrap_or(sel.end)) as i32; let caret = byte_to_utf16(&content, text.caret().unwrap_or(sel.end)) as i32;
(caret - compose_len as i32, caret) (caret - compose_len as i32, caret)
} else { } else {
(-1, -1) (-1, -1)
@@ -89,16 +89,12 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
if let Some(focus) = self.focus() { if let Some(focus) = self.focus() {
let text = focus.get(&self.rsc); let text = focus.get(&self.rsc);
let sel = text.selection_range().unwrap_or(0..0); let sel = text.selection_range().unwrap_or(0..0);
let start = byte_to_utf16(text.text(), sel.start) as i32; let content = text.text();
let end = byte_to_utf16(text.text(), sel.end) as i32; let start = byte_to_utf16(&content, sel.start) as i32;
let end = byte_to_utf16(&content, sel.end) as i32;
out_attrs.set_initial_sel_start(&mut ctx.env, start); out_attrs.set_initial_sel_start(&mut ctx.env, start);
out_attrs.set_initial_sel_end(&mut ctx.env, end); out_attrs.set_initial_sel_end(&mut ctx.env, end);
let caps = caps_mode( let caps = caps_mode(&mut ctx.env, &content, start as usize, CAP_MODE_SENTENCES);
&mut ctx.env,
text.text(),
start as usize,
CAP_MODE_SENTENCES,
);
out_attrs.set_initial_caps_mode(&mut ctx.env, caps); out_attrs.set_initial_caps_mode(&mut ctx.env, caps);
} }
} }
@@ -114,10 +110,11 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
let focus = self.focus()?; let focus = self.focus()?;
let text = focus.get(&self.rsc); let text = focus.get(&self.rsc);
let sel = text.selection_range()?; let sel = text.selection_range()?;
let end_16 = byte_to_utf16(text.text(), sel.start); let content = text.text();
let end_16 = byte_to_utf16(&content, sel.start);
let start_16 = end_16.saturating_sub(n as usize); let start_16 = end_16.saturating_sub(n as usize);
let start = utf16_to_byte(text.text(), start_16); let start = utf16_to_byte(&content, start_16);
Some(Cow::Borrowed(&text.text()[start..sel.start])) Some(Cow::Owned(content[start..sel.start].to_owned()))
} }
fn text_after_cursor<'slf>( fn text_after_cursor<'slf>(
@@ -131,11 +128,12 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
let focus = self.focus()?; let focus = self.focus()?;
let text = focus.get(&self.rsc); let text = focus.get(&self.rsc);
let sel = text.selection_range()?; let sel = text.selection_range()?;
let len_16 = byte_to_utf16(text.text(), text.text().len()); let content = text.text();
let start_16 = byte_to_utf16(text.text(), sel.end); let len_16 = byte_to_utf16(&content, content.len());
let start_16 = byte_to_utf16(&content, sel.end);
let end_16 = (start_16 + n as usize).min(len_16); let end_16 = (start_16 + n as usize).min(len_16);
let end = utf16_to_byte(text.text(), end_16); let end = utf16_to_byte(&content, end_16);
Some(Cow::Borrowed(&text.text()[sel.end..end])) Some(Cow::Owned(content[sel.end..end].to_owned()))
} }
fn selected_text<'slf>(&'slf mut self, _ctx: &mut CallbackCtx) -> Option<Cow<'slf, str>> { fn selected_text<'slf>(&'slf mut self, _ctx: &mut CallbackCtx) -> Option<Cow<'slf, str>> {
@@ -151,8 +149,9 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
let Some(caret) = text.caret() else { let Some(caret) = text.caret() else {
return 0; return 0;
}; };
let off = byte_to_utf16(text.text(), caret); let content = text.text();
caps_mode(&mut ctx.env, text.text(), off, req_modes) let off = byte_to_utf16(&content, caret);
caps_mode(&mut ctx.env, &content, off, req_modes)
} }
fn delete_surrounding_text( fn delete_surrounding_text(
@@ -170,12 +169,13 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
}; };
let content = text.text(); let content = text.text();
let start_16 = let start_16 =
byte_to_utf16(content, sel.start).saturating_sub(before_length.max(0) as usize); byte_to_utf16(&content, sel.start).saturating_sub(before_length.max(0) as usize);
let len_16 = byte_to_utf16(content, content.len()); let len_16 = byte_to_utf16(&content, content.len());
let end_16 = (byte_to_utf16(content, sel.end) + after_length.max(0) as usize).min(len_16); let end_16 = (byte_to_utf16(&content, sel.end) + after_length.max(0) as usize).min(len_16);
let start = utf16_to_byte(content, start_16); let start = utf16_to_byte(&content, start_16);
let end = utf16_to_byte(content, end_16); let end = utf16_to_byte(&content, end_16);
focus.edit(&mut self.rsc).delete_byte_range(start, end); drop(content);
focus(&mut self.rsc).delete_byte_range(start, end);
self.after_input(ctx); self.after_input(ctx);
true true
} }
@@ -199,7 +199,7 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
return false; return false;
}; };
let compose_len = self.state.android_state().compose_len; let compose_len = self.state.android_state().compose_len;
focus.edit(&mut self.rsc).replace(compose_len, text); focus(&mut self.rsc).replace(compose_len, text);
self.state.android_state_mut().compose_len = text.chars().count(); self.state.android_state_mut().compose_len = text.chars().count();
self.after_input(ctx); self.after_input(ctx);
true true
@@ -225,8 +225,9 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
}; };
let text = focus.get(&self.rsc); let text = focus.get(&self.rsc);
let content = text.text(); let content = text.text();
let byte = utf16_to_byte(content, end.max(0) as usize); let byte = utf16_to_byte(&content, end.max(0) as usize);
focus.edit(&mut self.rsc).set_cursor_byte(byte); drop(content);
focus(&mut self.rsc).set_cursor_byte(byte);
let _ = start; let _ = start;
self.after_input(ctx); self.after_input(ctx);
true true
+1 -1
View File
@@ -19,7 +19,7 @@ pub(super) fn on_key<'local, State: AndroidAppState>(
let Some(focus) = state.android_state().focus else { let Some(focus) = state.android_state().focus else {
return false; return false;
}; };
let mut text = focus.edit(rsc); let text = focus(rsc);
match key_code { match key_code {
Keycode::Del => text.backspace(false), Keycode::Del => text.backspace(false),
Keycode::ForwardDel => text.delete(false), Keycode::ForwardDel => text.delete(false),
+8 -8
View File
@@ -217,7 +217,7 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
if old_focus != ui_state.focus if old_focus != ui_state.focus
&& let Some(old) = old_focus && let Some(old) = old_focus
{ {
old.edit(&mut self.rsc).deselect(); old(&mut self.rsc).deselect();
} }
if std::mem::take(&mut ui_state.pending_show_keyboard) { if std::mem::take(&mut ui_state.pending_show_keyboard) {
show_soft_input(&mut ctx.env, &ctx.view); show_soft_input(&mut ctx.env, &ctx.view);
@@ -619,7 +619,7 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
if !gain_focus { if !gain_focus {
let ui_state = self.state.android_state_mut(); let ui_state = self.state.android_state_mut();
if let Some(focus) = ui_state.focus.take() { if let Some(focus) = ui_state.focus.take() {
focus.edit(&mut self.rsc).deselect(); focus(&mut self.rsc).deselect();
} }
} }
self.after_input(ctx); self.after_input(ctx);
@@ -646,8 +646,8 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
log::info!( log::info!(
"iris surface: surface_changed {width}x{height} already_live={already_live} \ "iris surface: surface_changed {width}x{height} already_live={already_live} \
glyphs_cached={} atlas_pages={}", glyphs_cached={} atlas_pages={}",
self.rsc.ui().text.atlas.glyph_count(), self.rsc.ui().text.borrow().atlas.glyph_count(),
self.rsc.ui().text.atlas.page_count(), self.rsc.ui().text.borrow().atlas.page_count(),
); );
if already_live { if already_live {
let ui_state = self.state.android_state_mut(); let ui_state = self.state.android_state_mut();
@@ -691,8 +691,8 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
"iris surface: new renderer built ({:?}), re-uploading textures: \ "iris surface: new renderer built ({:?}), re-uploading textures: \
glyphs={} pages={}", glyphs={} pages={}",
renderer.adapter_backend, renderer.adapter_backend,
self.rsc.ui().text.atlas.glyph_count(), self.rsc.ui().text.borrow().atlas.glyph_count(),
self.rsc.ui().text.atlas.page_count(), self.rsc.ui().text.borrow().atlas.page_count(),
); );
self.rsc.ui_mut().textures.reupload(); self.rsc.ui_mut().textures.reupload();
self.rsc.ui_mut().paints.reupload(); self.rsc.ui_mut().paints.reupload();
@@ -716,8 +716,8 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
log::info!( log::info!(
"iris surface: surface_destroyed, tearing the renderer down \ "iris surface: surface_destroyed, tearing the renderer down \
(glyphs_cached={} atlas_pages={})", (glyphs_cached={} atlas_pages={})",
self.rsc.ui().text.atlas.glyph_count(), self.rsc.ui().text.borrow().atlas.glyph_count(),
self.rsc.ui().text.atlas.page_count(), self.rsc.ui().text.borrow().atlas.page_count(),
); );
self.state.android_state_mut().renderer = None; self.state.android_state_mut().renderer = None;
} }
+3 -3
View File
@@ -250,7 +250,7 @@ impl<State: DesktopAppState> AppState for DesktopApp<State> {
if old != ui_state.focus if old != ui_state.focus
&& let Some(old) = old && let Some(old) = old
{ {
old.edit(rsc).deselect(); old(rsc).deselect();
} }
match &event { match &event {
WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::CloseRequested => event_loop.exit(),
@@ -315,7 +315,7 @@ impl<State: DesktopAppState> AppState for DesktopApp<State> {
&& let Some(sel) = ui_state.focus && let Some(sel) = ui_state.focus
&& event.state.is_pressed() && event.state.is_pressed()
{ {
let mut text = sel.edit(rsc); let text = sel(rsc);
match text.apply_event(event, &ui_state.input.modifiers) { match text.apply_event(event, &ui_state.input.modifiers) {
TextInputResult::Unfocus => { TextInputResult::Unfocus => {
ui_state.focus = None; ui_state.focus = None;
@@ -346,7 +346,7 @@ impl<State: DesktopAppState> AppState for DesktopApp<State> {
if !rsc.events.controllers.command_target_blocks_input() if !rsc.events.controllers.command_target_blocks_input()
&& let Some(sel) = ui_state.focus && let Some(sel) = ui_state.focus
{ {
let mut text = sel.edit(rsc); let text = sel(rsc);
match ime { match ime {
Ime::Enabled | Ime::Disabled => (), Ime::Enabled | Ime::Disabled => (),
Ime::Preedit(content, _pos) => { Ime::Preedit(content, _pos) => {
+3 -5
View File
@@ -467,10 +467,8 @@ fn composing_text_after_a_keyboard_resize_lands_in_the_bars_own_region() {
render.resize((1080.0, 2298.0)); render.resize((1080.0, 2298.0));
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
field field(&mut rsc).select(vec2(40.0, 2250.0), vec2(1080.0, 2298.0), false, false);
.edit(&mut rsc) field(&mut rsc).insert("a");
.select(vec2(40.0, 2250.0), vec2(1080.0, 2298.0), false, false);
field.edit(&mut rsc).insert("a");
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
let before_px = render.window_region(&field, &rsc).unwrap(); let before_px = render.window_region(&field, &rsc).unwrap();
@@ -485,7 +483,7 @@ fn composing_text_after_a_keyboard_resize_lands_in_the_bars_own_region() {
render.resize((1080.0, 1478.0)); render.resize((1080.0, 1478.0));
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
field.edit(&mut rsc).insert("b"); field(&mut rsc).insert("b");
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
let after_px = render.window_region(&field, &rsc).unwrap(); let after_px = render.window_region(&field, &rsc).unwrap();
+14 -14
View File
@@ -127,29 +127,29 @@ fn on_press(
match sense { match sense {
CursorSense::PressStart(_) => { CursorSense::PressStart(_) => {
let recent = state.recent_click(); let recent = state.recent_click();
id.edit(rsc).text.press_origin = Some(pos); id(rsc).press_origin = Some(pos);
id.edit(rsc).select(pos, size, false, recent); id(rsc).select(pos, size, false, recent);
} }
CursorSense::Pressing(_) | CursorSense::PressEnd(_) => { CursorSense::Pressing(_) | CursorSense::PressEnd(_) => {
let mut ctx = id.edit(rsc); let ctx = id(rsc);
let Some(origin) = ctx.text.press_origin else { let Some(origin) = ctx.press_origin else {
return; return;
}; };
let (dx, dy) = (pos.x - origin.x, pos.y - origin.y); let (dx, dy) = (pos.x - origin.x, pos.y - origin.y);
if dy.abs() > DRAG_SLOP && dy.abs() >= dx.abs() { if dy.abs() > DRAG_SLOP && dy.abs() >= dx.abs() {
ctx.text.press_origin = None; ctx.press_origin = None;
return; return;
} }
let ended = matches!(sense, CursorSense::PressEnd(_)); let ended = matches!(sense, CursorSense::PressEnd(_));
if ended { if ended {
ctx.text.press_origin = None; ctx.press_origin = None;
} }
ctx.select(pos, size, true, false); ctx.select(pos, size, true, false);
if ended && dx.abs() <= DRAG_SLOP && dy.abs() <= DRAG_SLOP { if ended && dx.abs() <= DRAG_SLOP && dy.abs() <= DRAG_SLOP {
state.focus_gained(render.window_region(&id, &*rsc)); state.focus_gained(render.window_region(&id, &*rsc));
} }
} }
CursorSense::Cancel => id.edit(rsc).text.press_origin = None, CursorSense::Cancel => id(rsc).press_origin = None,
_ => {} _ => {}
} }
return; return;
@@ -157,24 +157,24 @@ fn on_press(
match sense { match sense {
CursorSense::PressStart(_) => { CursorSense::PressStart(_) => {
id.edit(rsc).text.press_origin = Some(pos); id(rsc).press_origin = Some(pos);
} }
CursorSense::Pressing(_) => { CursorSense::Pressing(_) => {
let ctx = id.edit(rsc); let ctx = id(rsc);
if let Some(origin) = ctx.text.press_origin if let Some(origin) = ctx.press_origin
&& ((pos.x - origin.x).abs() > DRAG_SLOP || (pos.y - origin.y).abs() > DRAG_SLOP) && ((pos.x - origin.x).abs() > DRAG_SLOP || (pos.y - origin.y).abs() > DRAG_SLOP)
{ {
ctx.text.press_origin = None; ctx.press_origin = None;
} }
} }
// The gesture was taken by somebody else, so it is not a tap and // The gesture was taken by somebody else, so it is not a tap and
// must not grant focus when it ends out of this widget's sight. // must not grant focus when it ends out of this widget's sight.
CursorSense::Cancel => id.edit(rsc).text.press_origin = None, CursorSense::Cancel => id(rsc).press_origin = None,
CursorSense::PressEnd(_) => { CursorSense::PressEnd(_) => {
let was_tap = id.edit(rsc).text.press_origin.take().is_some(); let was_tap = id(rsc).press_origin.take().is_some();
if was_tap { if was_tap {
let recent = state.recent_click(); let recent = state.recent_click();
id.edit(rsc).select(pos, size, false, recent); id(rsc).select(pos, size, false, recent);
state.set_focus(Some(id)); state.set_focus(Some(id));
state.focus_gained(render.window_region(&id, &*rsc)); state.focus_gained(render.window_region(&id, &*rsc));
} }
+9 -7
View File
@@ -91,12 +91,10 @@ impl<Rsc: UiRsc> TextBuilderOutput<Rsc> for TextOutput {
let mut buf = TextBuffer::new(&builder.content); let mut buf = TextBuffer::new(&builder.content);
buf.set_spans(builder.spans); buf.set_spans(builder.spans);
let hint = builder.hint.get(state); let hint = builder.hint.get(state);
let mut text = Text { let resources = state.ui().text.clone();
content: builder.content.into(), Text {
view: TextView::new(buf, builder.attrs, hint), view: TextView::new(TextResources::add(resources, buf, builder.attrs), hint),
}; }
text.content.changed = false;
text
} }
} }
@@ -113,8 +111,12 @@ impl<State: UiRsc> TextBuilderOutput<State> for TextEditOutput {
) -> Self::Output { ) -> Self::Output {
let mut buf = TextBuffer::new(&builder.content); let mut buf = TextBuffer::new(&builder.content);
buf.set_spans(builder.spans); buf.set_spans(builder.spans);
let resources = state.ui().text.clone();
TextEdit::new( TextEdit::new(
TextView::new(buf, builder.attrs, builder.hint.get(state)), TextView::new(
TextResources::add(resources, buf, builder.attrs),
builder.hint.get(state),
),
builder.output.mode, builder.output.mode,
) )
} }
+132 -148
View File
@@ -1,9 +1,10 @@
use crate::prelude::*; use crate::prelude::*;
use iris_core::{PaintId, TextData}; use iris_core::PaintId;
use parley::{Affinity, Layout, Selection}; use parley::{Affinity, Layout, Selection};
use std::ops::{Deref, DerefMut}; use std::{
cell::Ref,
use super::selection_layout; ops::{Deref, DerefMut},
};
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
use winit::{ use winit::{
event::KeyEvent, event::KeyEvent,
@@ -47,15 +48,15 @@ impl TextEdit {
} }
pub fn selected_text(&self) -> Option<String> { pub fn selected_text(&self) -> Option<String> {
self.view.selection.selected_text(self.view.buf.text()) self.view.selection.selected_text(&self.view.text.text())
} }
/// The field's content. Byte-indexed, like everything else here since /// The field's content. Byte-indexed, like everything else here since
/// I1 moved to parley -- an IME bridge (`android/ime.rs`) converts to /// I1 moved to parley -- an IME bridge (`android/ime.rs`) converts to
/// and from UTF-16 code units at its own edge rather than this type /// and from UTF-16 code units at its own edge rather than this type
/// knowing about that encoding. /// knowing about that encoding.
pub fn text(&self) -> &str { pub fn text(&self) -> Ref<'_, str> {
self.view.buf.text() self.view.text.text()
} }
/// The selection as a byte range, collapsed to `caret..caret` when /// The selection as a byte range, collapsed to `caret..caret` when
@@ -89,21 +90,15 @@ impl Widget for TextEdit {
} }
} }
pub struct TextEditCtx<'a> { impl TextEdit {
pub text: &'a mut TextEdit,
pub data: &'a mut TextData,
}
impl<'a> TextEditCtx<'a> {
fn selection_ctx(&mut self) -> TextSelectionCtx<'_> { fn selection_ctx(&mut self) -> TextSelectionCtx<'_> {
TextSelectionCtx { TextSelectionCtx {
view: &mut self.text.view, view: &mut self.view,
data: self.data,
} }
} }
fn layout(&mut self) -> &Layout<iris_core::PaintId> { fn layout(&self) -> Ref<'_, Layout<iris_core::PaintId>> {
selection_layout(&mut self.text.view, self.data) self.view.text.layout()
} }
#[cfg_attr(target_os = "android", allow(dead_code))] #[cfg_attr(target_os = "android", allow(dead_code))]
@@ -112,44 +107,44 @@ impl<'a> TextEditCtx<'a> {
} }
pub fn take(&mut self) -> String { pub fn take(&mut self) -> String {
let text = self.text.view.buf.text().to_string(); let text = self.view.text.text().to_string();
self.set(""); self.set("");
text text
} }
pub fn set(&mut self, text: &str) { pub fn set(&mut self, text: &str) {
let text = self.string(text); let text = self.string(text);
self.text.view.buf.set_text(text); self.view.text.set_text(text);
self.text.view.buf.changed = true; self.view.selection.deselect();
self.text.view.selection.deselect();
} }
pub fn set_with_spans(&mut self, text: &str, spans: Vec<SpanStyle>) { pub fn set_with_spans(&mut self, text: &str, spans: Vec<SpanStyle>) {
let text = self.string(text); let text = self.string(text);
self.text.view.buf.set_text(text); self.view.text.set_text(text);
self.text.view.buf.set_spans(spans); self.view.text.set_spans(spans);
self.text.view.selection.deselect(); self.view.selection.deselect();
} }
pub fn motion(&mut self, motion: Motion, select: bool) { pub fn motion(&mut self, motion: Motion, select: bool) {
let Some(sel) = self.text.view.selection.range else { let Some(sel) = self.view.selection.range else {
return; return;
}; };
let layout = self.layout(); let layout = self.layout();
let sel = if !select && !sel.is_collapsed() { let sel = if !select && !sel.is_collapsed() {
match motion { match motion {
Motion::Left | Motion::LeftWord => { Motion::Left | Motion::LeftWord => {
Selection::from(sel.text_range().start_cursor(layout)) Selection::from(sel.text_range().start_cursor(&layout))
} }
Motion::Right | Motion::RightWord => { Motion::Right | Motion::RightWord => {
Selection::from(sel.text_range().end_cursor(layout)) Selection::from(sel.text_range().end_cursor(&layout))
} }
_ => apply_motion(sel, layout, motion, false), _ => apply_motion(sel, &layout, motion, false),
} }
} else { } else {
apply_motion(sel, layout, motion, select) apply_motion(sel, &layout, motion, select)
}; };
self.text.view.selection.range = Some(sel); drop(layout);
self.view.selection.range = Some(sel);
} }
pub fn replace(&mut self, len: usize, text: &str) { pub fn replace(&mut self, len: usize, text: &str) {
@@ -161,7 +156,7 @@ impl<'a> TextEditCtx<'a> {
} }
fn string(&self, text: &str) -> String { fn string(&self, text: &str) -> String {
if self.text.mode == EditMode::SingleLine { if self.mode == EditMode::SingleLine {
text.replace('\n', "") text.replace('\n', "")
} else { } else {
text.to_string() text.to_string()
@@ -178,7 +173,7 @@ impl<'a> TextEditCtx<'a> {
return; return;
} }
self.clear_span(); self.clear_span();
let at = match self.text.view.selection.range { let at = match self.view.selection.range {
Some(sel) => sel.focus().index(), Some(sel) => sel.focus().index(),
// No caret means nowhere to put the text, so this drops the // No caret means nowhere to put the text, so this drops the
// keystroke -- which is invisible, and was the whole of the // keystroke -- which is invisible, and was the whole of the
@@ -196,22 +191,24 @@ impl<'a> TextEditCtx<'a> {
return; return;
} }
}; };
let at = at.min(self.text.view.buf.text().len()); let at = at.min(self.view.text.text().len());
self.text.view.buf.edit().insert_str(at, text); self.view
self.text.view.buf.changed = true; .text
.edit_text(|content| content.insert_str(at, text));
self.set_caret(at + text.len()); self.set_caret(at + text.len());
} }
pub fn clear_span(&mut self) -> bool { pub fn clear_span(&mut self) -> bool {
let Some(sel) = self.text.view.selection.range else { let Some(sel) = self.view.selection.range else {
return false; return false;
}; };
if sel.is_collapsed() { if sel.is_collapsed() {
return false; return false;
} }
let range = sel.text_range(); let range = sel.text_range();
self.text.view.buf.edit().replace_range(range.clone(), ""); self.view
self.text.view.buf.changed = true; .text
.edit_text(|content| content.replace_range(range.clone(), ""));
self.set_caret(range.start); self.set_caret(range.start);
true true
} }
@@ -221,7 +218,7 @@ impl<'a> TextEditCtx<'a> {
} }
pub fn newline(&mut self) { pub fn newline(&mut self) {
if self.text.mode == EditMode::MultiLine { if self.mode == EditMode::MultiLine {
self.insert_str("\n"); self.insert_str("\n");
} }
} }
@@ -230,7 +227,7 @@ impl<'a> TextEditCtx<'a> {
if self.clear_span() { if self.clear_span() {
return; return;
} }
let Some(sel) = self.text.view.selection.range else { let Some(sel) = self.view.selection.range else {
return; return;
}; };
let end = sel.focus().index(); let end = sel.focus().index();
@@ -239,10 +236,11 @@ impl<'a> TextEditCtx<'a> {
} }
let layout = self.layout(); let layout = self.layout();
let start = if word { let start = if word {
sel.focus().previous_logical_word(layout).index() sel.focus().previous_logical_word(&layout).index()
} else { } else {
sel.focus().previous_visual(layout).index() sel.focus().previous_visual(&layout).index()
}; };
drop(layout);
self.delete_range(start, end); self.delete_range(start, end);
} }
@@ -250,30 +248,32 @@ impl<'a> TextEditCtx<'a> {
if self.clear_span() { if self.clear_span() {
return; return;
} }
let Some(sel) = self.text.view.selection.range else { let Some(sel) = self.view.selection.range else {
return; return;
}; };
let start = sel.focus().index(); let start = sel.focus().index();
if start >= self.text.view.buf.text().len() { if start >= self.view.text.text().len() {
return; return;
} }
let layout = self.layout(); let layout = self.layout();
let end = if word { let end = if word {
sel.focus().next_logical_word(layout).index() sel.focus().next_logical_word(&layout).index()
} else { } else {
sel.focus().next_visual(layout).index() sel.focus().next_visual(&layout).index()
}; };
drop(layout);
self.delete_range(start, end); self.delete_range(start, end);
} }
fn delete_range(&mut self, start: usize, end: usize) { fn delete_range(&mut self, start: usize, end: usize) {
let len = self.text.view.buf.text().len(); let len = self.view.text.text().len();
let (start, end) = (start.min(end).min(len), start.max(end).min(len)); let (start, end) = (start.min(end).min(len), start.max(end).min(len));
if start == end { if start == end {
return; return;
} }
self.text.view.buf.edit().replace_range(start..end, ""); self.view
self.text.view.buf.changed = true; .text
.edit_text(|content| content.replace_range(start..end, ""));
self.set_caret(start); self.set_caret(start);
} }
@@ -326,20 +326,17 @@ impl<'a> TextEditCtx<'a> {
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult { pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
let old = ( let old = (self.view.text.text().to_string(), self.view.selection.range);
self.text.view.buf.text().to_string(),
self.text.view.selection.range,
);
let mut undo = false; let mut undo = false;
let res = self.apply_event_inner(event, modifiers, &mut undo); let res = self.apply_event_inner(event, modifiers, &mut undo);
if undo { if undo {
if let Some((old, selection)) = self.text.history.pop() { if let Some((old, selection)) = self.history.pop() {
self.set(&old); self.set(&old);
self.text.view.selection.range = selection; self.view.selection.range = selection;
self.refresh(); self.refresh();
} }
} else if self.text.view.buf.text() != old.0 { } else if self.view.text.text().as_ref() != old.0 {
self.text.history.push(old); self.history.push(old);
} }
res res
} }
@@ -394,12 +391,12 @@ impl<'a> TextEditCtx<'a> {
match text.as_str() { match text.as_str() {
"v" => return TextInputResult::Paste, "v" => return TextInputResult::Paste,
"c" => { "c" => {
if let Some(content) = self.text.selected_text() { if let Some(content) = self.selected_text() {
return TextInputResult::Copy(content); return TextInputResult::Copy(content);
} }
} }
"x" => { "x" => {
if let Some(content) = self.text.selected_text() { if let Some(content) = self.selected_text() {
self.clear_span(); self.clear_span();
return TextInputResult::Copy(content); return TextInputResult::Copy(content);
} }
@@ -492,226 +489,213 @@ impl DerefMut for TextEdit {
} }
} }
pub trait TextEditable {
fn edit<'a>(&self, ui: &'a mut impl UiRsc) -> TextEditCtx<'a>;
}
impl<I: IdLike<Widget = TextEdit>> TextEditable for I {
fn edit<'a>(&self, ui: &'a mut impl UiRsc) -> TextEditCtx<'a> {
let ui: &mut UiData = ui.ui_mut();
TextEditCtx {
text: ui.widgets.get_mut(self).unwrap(),
data: &mut ui.text,
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use iris_core::{TextAttrs, TextBuffer}; use iris_core::{TextAttrs, TextBuffer, TextResources};
fn edit(text: &str, mode: EditMode) -> (TextEdit, TextData) { fn edit(text: &str, mode: EditMode) -> TextEdit {
let view = TextView::new(TextBuffer::new(text), TextAttrs::default(), None); let resources = std::rc::Rc::new(std::cell::RefCell::new(TextResources::default()));
(TextEdit::new(view, mode), TextData::default()) let handle = TextResources::add(resources, TextBuffer::new(text), TextAttrs::default());
TextEdit::new(TextView::new(handle, None), mode)
} }
fn ctx<'a>(text: &'a mut TextEdit, data: &'a mut TextData) -> TextEditCtx<'a> { fn ctx(text: &mut TextEdit) -> &mut TextEdit {
TextEditCtx { text, data } text
} }
fn content(text: &TextEdit) -> String { fn content(text: &TextEdit) -> String {
text.buf.text().to_string() text.text().to_string()
} }
#[test] #[test]
fn insert_at_the_caret() { fn insert_at_the_caret() {
let (mut t, mut d) = edit("ac", EditMode::SingleLine); let mut t = edit("ac", EditMode::SingleLine);
ctx(&mut t, &mut d).set_caret(1); ctx(&mut t).set_caret(1);
ctx(&mut t, &mut d).insert("b"); ctx(&mut t).insert("b");
assert_eq!(content(&t), "abc"); assert_eq!(content(&t), "abc");
assert_eq!(t.caret(), Some(2)); assert_eq!(t.caret(), Some(2));
} }
#[test] #[test]
fn backspace_removes_the_character_before_the_caret() { fn backspace_removes_the_character_before_the_caret() {
let (mut t, mut d) = edit("abc", EditMode::SingleLine); let mut t = edit("abc", EditMode::SingleLine);
ctx(&mut t, &mut d).set_caret(2); ctx(&mut t).set_caret(2);
ctx(&mut t, &mut d).backspace(false); ctx(&mut t).backspace(false);
assert_eq!(content(&t), "ac"); assert_eq!(content(&t), "ac");
} }
#[test] #[test]
fn backspace_at_the_start_does_nothing() { fn backspace_at_the_start_does_nothing() {
let (mut t, mut d) = edit("abc", EditMode::SingleLine); let mut t = edit("abc", EditMode::SingleLine);
ctx(&mut t, &mut d).set_caret(0); ctx(&mut t).set_caret(0);
ctx(&mut t, &mut d).backspace(false); ctx(&mut t).backspace(false);
assert_eq!(content(&t), "abc"); assert_eq!(content(&t), "abc");
} }
#[test] #[test]
fn delete_removes_the_character_after_the_caret() { fn delete_removes_the_character_after_the_caret() {
let (mut t, mut d) = edit("abc", EditMode::SingleLine); let mut t = edit("abc", EditMode::SingleLine);
ctx(&mut t, &mut d).set_caret(1); ctx(&mut t).set_caret(1);
ctx(&mut t, &mut d).delete(false); ctx(&mut t).delete(false);
assert_eq!(content(&t), "ac"); assert_eq!(content(&t), "ac");
} }
#[test] #[test]
fn delete_at_the_end_does_nothing() { fn delete_at_the_end_does_nothing() {
let (mut t, mut d) = edit("abc", EditMode::SingleLine); let mut t = edit("abc", EditMode::SingleLine);
ctx(&mut t, &mut d).set_caret(3); ctx(&mut t).set_caret(3);
ctx(&mut t, &mut d).delete(false); ctx(&mut t).delete(false);
assert_eq!(content(&t), "abc"); assert_eq!(content(&t), "abc");
} }
#[test] #[test]
fn select_all_then_typing_replaces_everything() { fn select_all_then_typing_replaces_everything() {
let (mut t, mut d) = edit("hello", EditMode::SingleLine); let mut t = edit("hello", EditMode::SingleLine);
ctx(&mut t, &mut d).select_all(); ctx(&mut t).select_all();
assert_eq!(t.selected_text().as_deref(), Some("hello")); assert_eq!(t.selected_text().as_deref(), Some("hello"));
ctx(&mut t, &mut d).insert("x"); ctx(&mut t).insert("x");
assert_eq!(content(&t), "x"); assert_eq!(content(&t), "x");
} }
#[test] #[test]
fn clearing_a_span_leaves_the_caret_at_its_start() { fn clearing_a_span_leaves_the_caret_at_its_start() {
let (mut t, mut d) = edit("abcdef", EditMode::SingleLine); let mut t = edit("abcdef", EditMode::SingleLine);
ctx(&mut t, &mut d).select_all(); ctx(&mut t).select_all();
assert!(ctx(&mut t, &mut d).clear_span()); assert!(ctx(&mut t).clear_span());
assert_eq!(content(&t), ""); assert_eq!(content(&t), "");
assert_eq!(t.caret(), Some(0)); assert_eq!(t.caret(), Some(0));
} }
#[test] #[test]
fn tapping_an_empty_field_places_a_caret_so_typing_lands() { fn tapping_an_empty_field_places_a_caret_so_typing_lands() {
let (mut t, mut d) = edit("", EditMode::MultiLine); let mut t = edit("", EditMode::MultiLine);
ctx(&mut t, &mut d).select(vec2(40.0, 20.0), vec2(1080.0, 2400.0), false, false); ctx(&mut t).select(vec2(40.0, 20.0), vec2(1080.0, 2400.0), false, false);
assert!(t.caret().is_some(), "a tap must leave a caret behind"); assert!(t.caret().is_some(), "a tap must leave a caret behind");
ctx(&mut t, &mut d).insert("hi"); ctx(&mut t).insert("hi");
assert_eq!(content(&t), "hi"); assert_eq!(content(&t), "hi");
} }
#[test] #[test]
fn tapping_past_the_end_of_the_text_clamps_to_the_end() { fn tapping_past_the_end_of_the_text_clamps_to_the_end() {
let (mut t, mut d) = edit("abc", EditMode::MultiLine); let mut t = edit("abc", EditMode::MultiLine);
ctx(&mut t, &mut d).select(vec2(9000.0, 9000.0), vec2(1080.0, 2400.0), false, false); ctx(&mut t).select(vec2(9000.0, 9000.0), vec2(1080.0, 2400.0), false, false);
assert_eq!(t.caret(), Some(3)); assert_eq!(t.caret(), Some(3));
} }
#[test] #[test]
fn dragging_without_a_previous_selection_selects_nothing() { fn dragging_without_a_previous_selection_selects_nothing() {
let (mut t, mut d) = edit("abc", EditMode::MultiLine); let mut t = edit("abc", EditMode::MultiLine);
ctx(&mut t, &mut d).select(vec2(10.0, 10.0), vec2(1080.0, 2400.0), true, false); ctx(&mut t).select(vec2(10.0, 10.0), vec2(1080.0, 2400.0), true, false);
assert!(t.selection_range().is_none()); assert!(t.selection_range().is_none());
} }
#[test] #[test]
fn a_single_line_field_refuses_newlines() { fn a_single_line_field_refuses_newlines() {
let (mut t, mut d) = edit("", EditMode::SingleLine); let mut t = edit("", EditMode::SingleLine);
ctx(&mut t, &mut d).set_caret(0); ctx(&mut t).set_caret(0);
ctx(&mut t, &mut d).insert("a\nb"); ctx(&mut t).insert("a\nb");
assert_eq!(content(&t), "ab"); assert_eq!(content(&t), "ab");
ctx(&mut t, &mut d).newline(); ctx(&mut t).newline();
assert_eq!(content(&t), "ab"); assert_eq!(content(&t), "ab");
} }
#[test] #[test]
fn a_multi_line_field_keeps_newlines() { fn a_multi_line_field_keeps_newlines() {
let (mut t, mut d) = edit("", EditMode::MultiLine); let mut t = edit("", EditMode::MultiLine);
ctx(&mut t, &mut d).set_caret(0); ctx(&mut t).set_caret(0);
ctx(&mut t, &mut d).insert("a\nb"); ctx(&mut t).insert("a\nb");
assert_eq!(content(&t), "a\nb"); assert_eq!(content(&t), "a\nb");
} }
#[test] #[test]
fn take_empties_the_field_and_hands_back_what_was_there() { fn take_empties_the_field_and_hands_back_what_was_there() {
let (mut t, mut d) = edit("some text", EditMode::SingleLine); let mut t = edit("some text", EditMode::SingleLine);
assert_eq!(ctx(&mut t, &mut d).take(), "some text"); assert_eq!(ctx(&mut t).take(), "some text");
assert_eq!(content(&t), ""); assert_eq!(content(&t), "");
} }
#[test] #[test]
fn ime_preedit_replaces_its_own_previous_text() { fn ime_preedit_replaces_its_own_previous_text() {
let (mut t, mut d) = edit("", EditMode::SingleLine); let mut t = edit("", EditMode::SingleLine);
ctx(&mut t, &mut d).set_caret(0); ctx(&mut t).set_caret(0);
ctx(&mut t, &mut d).replace(0, "n"); ctx(&mut t).replace(0, "n");
assert_eq!(content(&t), "n"); assert_eq!(content(&t), "n");
ctx(&mut t, &mut d).replace(1, "ni"); ctx(&mut t).replace(1, "ni");
assert_eq!(content(&t), "ni"); assert_eq!(content(&t), "ni");
ctx(&mut t, &mut d).replace(2, ""); ctx(&mut t).replace(2, "");
assert_eq!(content(&t), ""); assert_eq!(content(&t), "");
} }
#[test] #[test]
fn composing_advances_the_caret_with_the_growing_text() { fn composing_advances_the_caret_with_the_growing_text() {
let (mut t, mut d) = edit("", EditMode::SingleLine); let mut t = edit("", EditMode::SingleLine);
ctx(&mut t, &mut d).set_caret(0); ctx(&mut t).set_caret(0);
ctx(&mut t, &mut d).replace(0, "h"); ctx(&mut t).replace(0, "h");
assert_eq!(t.caret(), Some(1)); assert_eq!(t.caret(), Some(1));
ctx(&mut t, &mut d).replace(1, "hi"); ctx(&mut t).replace(1, "hi");
assert_eq!(content(&t), "hi"); assert_eq!(content(&t), "hi");
assert_eq!(t.caret(), Some(2)); assert_eq!(t.caret(), Some(2));
ctx(&mut t, &mut d).replace(2, "hit"); ctx(&mut t).replace(2, "hit");
assert_eq!(content(&t), "hit"); assert_eq!(content(&t), "hit");
assert_eq!(t.caret(), Some(3)); assert_eq!(t.caret(), Some(3));
} }
#[test] #[test]
fn committing_composed_text_leaves_it_in_place_with_the_caret_after_it() { fn committing_composed_text_leaves_it_in_place_with_the_caret_after_it() {
let (mut t, mut d) = edit("say ", EditMode::SingleLine); let mut t = edit("say ", EditMode::SingleLine);
ctx(&mut t, &mut d).set_caret(4); ctx(&mut t).set_caret(4);
ctx(&mut t, &mut d).replace(0, "hi"); ctx(&mut t).replace(0, "hi");
assert_eq!(content(&t), "say hi"); assert_eq!(content(&t), "say hi");
ctx(&mut t, &mut d).replace(0, " "); ctx(&mut t).replace(0, " ");
assert_eq!(content(&t), "say hi "); assert_eq!(content(&t), "say hi ");
assert_eq!(t.caret(), Some(7)); assert_eq!(t.caret(), Some(7));
} }
#[test] #[test]
fn delete_byte_range_removes_exactly_that_range() { fn delete_byte_range_removes_exactly_that_range() {
let (mut t, mut d) = edit("hello world", EditMode::SingleLine); let mut t = edit("hello world", EditMode::SingleLine);
ctx(&mut t, &mut d).delete_byte_range(5, 11); ctx(&mut t).delete_byte_range(5, 11);
assert_eq!(content(&t), "hello"); assert_eq!(content(&t), "hello");
assert_eq!(t.caret(), Some(5)); assert_eq!(t.caret(), Some(5));
} }
#[test] #[test]
fn set_cursor_byte_collapses_to_a_caret_there() { fn set_cursor_byte_collapses_to_a_caret_there() {
let (mut t, mut d) = edit("hello world", EditMode::SingleLine); let mut t = edit("hello world", EditMode::SingleLine);
ctx(&mut t, &mut d).select_all(); ctx(&mut t).select_all();
ctx(&mut t, &mut d).set_cursor_byte(5); ctx(&mut t).set_cursor_byte(5);
assert_eq!(t.selected_text(), None); assert_eq!(t.selected_text(), None);
assert_eq!(t.caret(), Some(5)); assert_eq!(t.caret(), Some(5));
} }
#[test] #[test]
fn motion_moves_the_caret_and_shift_extends_a_span() { fn motion_moves_the_caret_and_shift_extends_a_span() {
let (mut t, mut d) = edit("abc", EditMode::SingleLine); let mut t = edit("abc", EditMode::SingleLine);
ctx(&mut t, &mut d).set_caret(0); ctx(&mut t).set_caret(0);
ctx(&mut t, &mut d).motion(Motion::Right, false); ctx(&mut t).motion(Motion::Right, false);
assert_eq!(t.caret(), Some(1)); assert_eq!(t.caret(), Some(1));
ctx(&mut t, &mut d).motion(Motion::Right, true); ctx(&mut t).motion(Motion::Right, true);
assert_eq!(t.selected_text().as_deref(), Some("b")); assert_eq!(t.selected_text().as_deref(), Some("b"));
} }
#[test] #[test]
fn an_unshifted_arrow_collapses_a_span_to_its_edge() { fn an_unshifted_arrow_collapses_a_span_to_its_edge() {
let (mut t, mut d) = edit("abcdef", EditMode::SingleLine); let mut t = edit("abcdef", EditMode::SingleLine);
ctx(&mut t, &mut d).select_all(); ctx(&mut t).select_all();
ctx(&mut t, &mut d).motion(Motion::Left, false); ctx(&mut t).motion(Motion::Left, false);
assert_eq!(t.caret(), Some(0)); assert_eq!(t.caret(), Some(0));
ctx(&mut t, &mut d).select_all(); ctx(&mut t).select_all();
ctx(&mut t, &mut d).motion(Motion::Right, false); ctx(&mut t).motion(Motion::Right, false);
assert_eq!(t.caret(), Some(6)); assert_eq!(t.caret(), Some(6));
} }
#[test] #[test]
fn multibyte_text_is_edited_by_byte_offset() { fn multibyte_text_is_edited_by_byte_offset() {
let (mut t, mut d) = edit("", EditMode::SingleLine); let mut t = edit("", EditMode::SingleLine);
ctx(&mut t, &mut d).set_caret(3); ctx(&mut t).set_caret(3);
ctx(&mut t, &mut d).backspace(false); ctx(&mut t).backspace(false);
assert_eq!(content(&t), "a"); assert_eq!(content(&t), "a");
} }
} }
+58 -107
View File
@@ -4,87 +4,65 @@ mod selection;
pub use build::*; pub use build::*;
pub use edit::*; pub use edit::*;
use iris_core::util::MutDetect;
pub use selection::*; pub use selection::*;
use crate::prelude::*; use crate::prelude::*;
use std::ops::{Deref, DerefMut};
pub struct Text { pub struct Text {
pub content: MutDetect<String>,
view: TextView, view: TextView,
} }
pub struct TextView { pub struct TextView {
pub attrs: MutDetect<TextAttrs>, text: TextHandle,
pub buf: MutDetect<TextBuffer>,
tex: Option<RenderedText>,
width: Option<f32>,
pub hint: Option<StrongWidget>, pub hint: Option<StrongWidget>,
selection: TextSelection, selection: TextSelection,
} }
impl TextView { impl TextView {
fn is_blank(&self) -> bool { fn is_blank(&self) -> bool {
self.buf.is_empty() self.text.text().is_empty()
} }
pub fn wrap_width(&self) -> Option<f32> { pub fn wrap_width(&self) -> Option<f32> {
self.width self.text.width()
} }
pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self { pub fn new(text: TextHandle, hint: Option<StrongWidget>) -> Self {
Self { Self {
attrs: attrs.into(), text,
buf: buf.into(),
tex: None,
width: None,
hint, hint,
selection: TextSelection::default(), selection: TextSelection::default(),
} }
} }
pub fn region(&self) -> UiRegion { pub fn region(&self) -> UiRegion {
self.tex() let align = self.text.attrs().align;
self.text
.rendered()
.map(|t| t.size) .map(|t| t.size)
.unwrap_or(Vec2::ZERO) .unwrap_or(Vec2::ZERO)
.align(self.align) .align(align)
} }
fn render(&mut self, painter: &mut Painter) -> RenderedText { fn render(&mut self, painter: &mut Painter) -> RenderedText {
let width = if self.attrs.wrap { let width = if self.text.attrs().wrap {
Some(painter.px_size().x) Some(painter.px_size().x)
} else { } else {
None None
}; };
let generation = painter.atlas_generation(); let tex = painter.render_text(&self.text, width);
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() { if crate::diagnostics::trace_enabled() {
log::debug!( log::debug!(
target: "iris::frame", target: "iris::frame",
"iris text render: chars={} width={width:?} glyphs={} size={:?}", "iris text render: chars={} width={width:?} glyphs={} size={:?}",
self.buf.text().chars().count(), self.text.text().chars().count(),
tex.glyphs.len(), tex.glyphs.len(),
tex.size, tex.size,
); );
} }
self.tex = Some(tex.clone());
self.attrs.changed = false;
self.buf.changed = false;
tex tex
} }
pub fn tex(&self) -> Option<&RenderedText> {
self.tex.as_ref()
}
pub fn draw(&mut self, painter: &mut Painter) -> Size { pub fn draw(&mut self, painter: &mut Painter) -> Size {
let tex = self.render(painter); let tex = self.render(painter);
if self.is_blank() if self.is_blank()
@@ -92,7 +70,7 @@ impl TextView {
{ {
return painter.widget(hint).size(); return painter.widget(hint).size();
} }
let region = tex.size.align(self.align); let region = tex.size.align(self.text.attrs().align);
let within = region.within(&painter.region()); let within = region.within(&painter.region());
painter.glyphs(&tex, within); painter.glyphs(&tex, within);
Size::abs(tex.size) Size::abs(tex.size)
@@ -108,8 +86,10 @@ impl TextView {
let Some(selection) = self.selection.range else { let Some(selection) = self.selection.range else {
return used; return used;
}; };
let layout = self.buf.layout(); let geometry = self
for (rect, _) in selection.geometry(layout) { .text
.with_layout(|layout, _| selection.geometry(layout));
for (rect, _) in geometry {
let size = vec2(rect.width() as f32, rect.height() as f32); let size = vec2(rect.width() as f32, rect.height() as f32);
let top_left = vec2(rect.x0 as f32, rect.y0 as f32); let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
let paint = painter.paint(&PaintId::SKY); let paint = painter.paint(&PaintId::SKY);
@@ -119,7 +99,9 @@ impl TextView {
); );
} }
if caret { if caret {
let caret = selection.focus().geometry(layout, CARET_WIDTH); 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 size = vec2(caret.width() as f32, caret.height() as f32);
let top_left = vec2(caret.x0 as f32, caret.y0 as f32); let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
let paint = painter.paint(&PaintId::WHITE); let paint = painter.paint(&PaintId::WHITE);
@@ -132,28 +114,23 @@ impl TextView {
} }
pub fn content(&self) -> String { pub fn content(&self) -> String {
self.buf.text().to_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 { impl Text {
pub fn new(content: impl Into<String>) -> Self { pub fn set_text(&mut self, content: impl Into<String>) {
let content: String = content.into(); if self.view.text.set_text(content) {
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(); self.view.selection.deselect();
} }
} }
pub fn selected_text(&self) -> Option<String> { pub fn selected_text(&self) -> Option<String> {
self.view.selection.selected_text(self.view.buf.text()) self.view.selection.selected_text(&self.view.text.text())
} }
pub fn selection_range(&self) -> Option<std::ops::Range<usize>> { pub fn selection_range(&self) -> Option<std::ops::Range<usize>> {
@@ -161,18 +138,22 @@ impl Text {
} }
pub fn set_with_spans(&mut self, content: impl Into<String>, spans: Vec<SpanStyle>) { pub fn set_with_spans(&mut self, content: impl Into<String>, spans: Vec<SpanStyle>) {
let content = content.into(); self.view.text.set_text(content);
*self.content = content.clone(); self.view.text.set_spans(spans);
self.content.changed = false;
self.view.buf.set_text(content);
self.view.buf.set_spans(spans);
self.view.selection.deselect(); 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 { impl Widget for Text {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) {
self.update_buf();
let size = if self.view.selection.range.is_some() { let size = if self.view.selection.range.is_some() {
self.view.draw_selectable(painter, false) self.view.draw_selectable(painter, false)
} else { } else {
@@ -188,34 +169,6 @@ impl Widget for Text {
pub(super) const CARET_WIDTH: f32 = 1.0; 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)] #[cfg(test)]
mod tests { mod tests {
use crate::layout_tests::TestRsc; use crate::layout_tests::TestRsc;
@@ -237,20 +190,20 @@ mod tests {
let (mut rsc, _render, text, _root) = rendered_text("hello there"); let (mut rsc, _render, text, _root) = rendered_text("hello there");
text.selection(&mut rsc).select_all(); text.selection(&mut rsc).select_all();
let view = TextView::new(TextBuffer::new("hello there"), TextAttrs::default(), None); let edit = wtext("hello there")
let mut edit = TextEdit::new(view, EditMode::MultiLine); .editable(EditMode::MultiLine)
let mut data = TextData::default(); .add_strong(&mut rsc);
TextEditCtx { let edit_id = edit.weak();
text: &mut edit, edit_id(&mut rsc).select_all();
data: &mut data,
}
.select_all();
assert_eq!( assert_eq!(
rsc.ui.widgets[text].selection_range(), rsc.ui.widgets[text].selection_range(),
edit.selection_range() rsc.ui.widgets[edit_id].selection_range()
);
assert_eq!(
rsc.ui.widgets[text].selected_text(),
rsc.ui.widgets[edit_id].selected_text()
); );
assert_eq!(rsc.ui.widgets[text].selected_text(), edit.selected_text());
} }
#[test] #[test]
@@ -262,7 +215,7 @@ mod tests {
Some("before") Some("before")
); );
*rsc.ui.widgets[text].content = "after".to_string(); rsc.ui.widgets[text].set_text("after");
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
assert_eq!(rsc.ui.widgets[text].selected_text(), None); assert_eq!(rsc.ui.widgets[text].selected_text(), None);
@@ -277,17 +230,15 @@ mod tests {
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
let selected = render.active[&text.id()].primitives.len(); let selected = render.active[&text.id()].primitives.len();
let view = TextView::new(TextBuffer::new("selected"), TextAttrs::default(), None); let edit = wtext("selected")
let edit = rsc .editable(EditMode::MultiLine)
.ui .add_strong(&mut rsc);
.widgets
.add_strong(TextEdit::new(view, EditMode::MultiLine));
let edit_id = edit.weak(); let edit_id = edit.weak();
let edit_root = edit.any(); let edit_root = edit.any();
let mut edit_render = UiRenderState::new(); let mut edit_render = UiRenderState::new();
edit_render.resize((800.0, 600.0)); edit_render.resize((800.0, 600.0));
edit_render.update(&edit_root, &mut rsc); edit_render.update(&edit_root, &mut rsc);
edit_id.edit(&mut rsc).select_all(); edit_id(&mut rsc).select_all();
edit_render.update(&edit_root, &mut rsc); edit_render.update(&edit_root, &mut rsc);
let editable = edit_render.active[&edit_id.id()].primitives.len(); let editable = edit_render.active[&edit_id.id()].primitives.len();
@@ -314,16 +265,16 @@ mod tests {
render.resize((800.0, 600.0)); render.resize((800.0, 600.0));
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
let rasterised = rsc.ui.text.atlas.glyph_count(); let rasterised = rsc.ui.text.borrow().atlas.glyph_count();
assert!(rasterised > 0, "the first frame rasterised no glyphs"); assert!(rasterised > 0, "the first frame rasterised no glyphs");
rsc.ui.text.atlas.clear(); rsc.ui.text.borrow_mut().atlas.clear();
assert_eq!(rsc.ui.text.atlas.glyph_count(), 0); assert_eq!(rsc.ui.text.borrow().atlas.glyph_count(), 0);
render.resize((800.0, 600.0)); render.resize((800.0, 600.0));
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
assert_eq!( assert_eq!(
rsc.ui.text.atlas.glyph_count(), rsc.ui.text.borrow().atlas.glyph_count(),
rasterised, rasterised,
"the second frame re-emitted its cached glyphs instead of \ "the second frame re-emitted its cached glyphs instead of \
re-rendering them against the fresh atlas" re-rendering them against the fresh atlas"
+38 -54
View File
@@ -1,6 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
use iris_core::{PaintId, TextData}; use parley::{Affinity, Selection as ParleySelection};
use parley::{Affinity, Layout, Selection as ParleySelection};
use std::time::Instant; use std::time::Instant;
/// The selection state shared by display text and editable text. Editing, /// The selection state shared by display text and editable text. Editing,
@@ -35,23 +34,21 @@ impl TextSelection {
} }
} }
/// Selection operations that need both a text widget's shaped buffer and /// Selection operations shared by ordinary and editable text, using the
/// iris's text resources. `TextEditCtx` delegates to this same context rather /// widget's arena-backed layout rather than maintaining an editable-only copy
/// than maintaining an editable-only copy of the geometry and hit testing. /// of the geometry and hit testing.
pub struct TextSelectionCtx<'a> { pub struct TextSelectionCtx<'a> {
pub(super) view: &'a mut TextView, pub(super) view: &'a mut TextView,
pub(super) data: &'a mut TextData,
} }
impl TextSelectionCtx<'_> { impl TextSelectionCtx<'_> {
pub(super) fn layout(&mut self) -> &Layout<PaintId> {
selection_layout(self.view, self.data)
}
pub(crate) fn refresh(&mut self) { pub(crate) fn refresh(&mut self) {
if let Some(selection) = self.view.selection.range { if let Some(selection) = self.view.selection.range {
let layout = self.layout(); self.view.selection.range = Some(
self.view.selection.range = Some(selection.refresh(layout)); self.view
.text
.with_layout(|layout, _| selection.refresh(layout)),
);
} }
} }
@@ -59,21 +56,23 @@ impl TextSelectionCtx<'_> {
/// the same widget-local coordinates as a `CursorSense` event. /// the same widget-local coordinates as a `CursorSense` event.
pub fn byte_at(&mut self, pos: Vec2, size: Vec2) -> usize { pub fn byte_at(&mut self, pos: Vec2, size: Vec2) -> usize {
let pos = pos - self.view.region().top_left().to_abs(size); let pos = pos - self.view.region().top_left().to_abs(size);
let layout = self.layout(); self.view.text.with_layout(|layout, _| {
ParleySelection::from_point(layout, pos.x, pos.y) ParleySelection::from_point(layout, pos.x, pos.y)
.focus() .focus()
.index() .index()
})
} }
pub fn select_all(&mut self) { pub fn select_all(&mut self) {
let len = self.view.buf.text().len(); let len = self.view.text.text().len();
if len == 0 { if len == 0 {
return; return;
} }
let layout = self.layout(); self.view.selection.range = Some(self.view.text.with_layout(|layout, _| {
let anchor = parley::Cursor::from_byte_index(layout, 0, Affinity::default()); let anchor = parley::Cursor::from_byte_index(layout, 0, Affinity::default());
let focus = parley::Cursor::from_byte_index(layout, len, Affinity::default()); let focus = parley::Cursor::from_byte_index(layout, len, Affinity::default());
self.view.selection.range = Some(ParleySelection::new(anchor, focus)); ParleySelection::new(anchor, focus)
}));
} }
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) { pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
@@ -81,8 +80,7 @@ impl TextSelectionCtx<'_> {
let previous = self.view.selection.range; let previous = self.view.selection.range;
let previous_hit = self.view.selection.double_hit; let previous_hit = self.view.selection.double_hit;
let outcome = { let outcome = self.view.text.with_layout(|layout, _| {
let layout = self.layout();
if drag { if drag {
previous.map(|selection| { previous.map(|selection| {
( (
@@ -109,7 +107,7 @@ impl TextSelectionCtx<'_> {
(Some(hit), None) (Some(hit), None)
}) })
} }
}; });
if let Some((range, double_hit)) = outcome { if let Some((range, double_hit)) = outcome {
self.view.selection.range = range; self.view.selection.range = range;
@@ -122,39 +120,28 @@ impl TextSelectionCtx<'_> {
} }
pub(crate) fn set_caret(&mut self, index: usize) { pub(crate) fn set_caret(&mut self, index: usize) {
let index = index.min(self.view.buf.text().len()); let index = index.min(self.view.text.text().len());
let layout = self.layout(); self.view.selection.range = Some(self.view.text.with_layout(|layout, _| {
self.view.selection.range = Some(ParleySelection::from_byte_index( ParleySelection::from_byte_index(layout, index, Affinity::default())
layout, }));
index,
Affinity::default(),
));
} }
fn select_between(&mut self, anchor: usize, focus: usize) { fn select_between(&mut self, anchor: usize, focus: usize) {
let len = self.view.buf.text().len(); let len = self.view.text.text().len();
let layout = self.layout(); self.view.selection.range = Some(self.view.text.with_layout(|layout, _| {
let anchor = parley::Cursor::from_byte_index(layout, anchor.min(len), Affinity::default()); let anchor =
let focus = parley::Cursor::from_byte_index(layout, focus.min(len), Affinity::default()); parley::Cursor::from_byte_index(layout, anchor.min(len), Affinity::default());
self.view.selection.range = Some(ParleySelection::new(anchor, focus)); let focus =
parley::Cursor::from_byte_index(layout, focus.min(len), Affinity::default());
ParleySelection::new(anchor, focus)
}));
} }
} }
pub(super) fn selection_layout<'a>(
view: &'a mut TextView,
data: &mut TextData,
) -> &'a Layout<PaintId> {
let attrs = view.attrs.clone();
let width = view.wrap_width();
let density = data.density;
view.buf.shape(data, &attrs, width, density);
view.buf.layout()
}
/// Gives an ordinary `Text` handle access to the same selection operations as /// Gives an ordinary `Text` handle access to the same selection operations as
/// `TextEditCtx`. Gesture policy is intentionally not part of this trait; a /// `TextEdit`. Gesture policy is intentionally not part of this trait; a
/// selection controller and an editor's focus handler do different /// selection controller and an editor's focus handler do different things
/// things with the same mechanics. /// with the same mechanics.
pub trait TextSelectable { pub trait TextSelectable {
fn selection<'a>(&self, ui: &'a mut impl UiRsc) -> TextSelectionCtx<'a>; fn selection<'a>(&self, ui: &'a mut impl UiRsc) -> TextSelectionCtx<'a>;
} }
@@ -164,7 +151,6 @@ impl<I: IdLike<Widget = Text>> TextSelectable for I {
let ui: &mut UiData = ui.ui_mut(); let ui: &mut UiData = ui.ui_mut();
TextSelectionCtx { TextSelectionCtx {
view: &mut ui.widgets.get_mut(self).unwrap().view, view: &mut ui.widgets.get_mut(self).unwrap().view,
data: &mut ui.text,
} }
} }
} }
@@ -256,10 +242,8 @@ impl SelectionController {
.get_dyn_mut(id)? .get_dyn_mut(id)?
.as_any_mut() .as_any_mut()
.downcast_mut::<Text>()?; .downcast_mut::<Text>()?;
text.update_buf();
let mut ctx = TextSelectionCtx { let mut ctx = TextSelectionCtx {
view: &mut text.view, view: &mut text.view,
data: &mut ui.text,
}; };
Some(f(&mut ctx)) Some(f(&mut ctx))
} }
@@ -334,7 +318,7 @@ impl SelectionController {
for &text_id in &self.order[lo..=hi] { for &text_id in &self.order[lo..=hi] {
let forward = anchor_at <= focus_at; let forward = anchor_at <= focus_at;
Self::with_text(rsc, text_id, |text| { Self::with_text(rsc, text_id, |text| {
let len = text.view.buf.text().len(); let len = text.view.text.text().len();
let (start, end) = if text_id == anchor && text_id == id { let (start, end) = if text_id == anchor && text_id == id {
(anchor_byte, focus_byte) (anchor_byte, focus_byte)
} else if text_id == anchor { } else if text_id == anchor {