iris: move text state into shared resources

This commit is contained in:
iris committed 2026-09-12 14:18:12 -04:00
1 parent 4cbe7baea0
commit a8093b002b
26 files changed
+686 -467

No files matched your search

+3 -3
View File
@@ -56,7 +56,7 @@ impl DesktopAppState for Client {
let screen = match ai_app::ui::fixture::open(rsc, &mut ui_state) { let screen = match ai_app::ui::fixture::open(rsc, &mut ui_state) {
Ok(opened) => { Ok(opened) => {
if let Some(message) = message_argv() { if let Some(message) = message_argv() {
opened.screen.composer.field.edit(rsc).set(&message); (opened.screen.composer.field)(rsc).set(&message);
} }
if let Some(text) = typed_argv() { if let Some(text) = typed_argv() {
let field = opened.screen.composer.field; let field = opened.screen.composer.field;
@@ -66,8 +66,8 @@ impl DesktopAppState for Client {
ctx.update(move |state: &mut Client, rsc| { ctx.update(move |state: &mut Client, rsc| {
state.set_focus(Some(field)); state.set_focus(Some(field));
let end = rsc[field].text().len(); let end = rsc[field].text().len();
let mut edit = field.edit(rsc); let edit = field(rsc);
if edit.text.caret().is_none() { if edit.caret().is_none() {
edit.set_cursor_byte(end); edit.set_cursor_byte(end);
} }
edit.insert(&ch.to_string()); edit.insert(&ch.to_string());
+7 -7
View File
@@ -144,7 +144,7 @@ impl BenchClient {
.any(); .any();
ui_state.set_root(rsc, tree); ui_state.set_root(rsc, tree);
let font = rsc.ui.text.font_diagnostics(); let font = rsc.ui.text.borrow_mut().font_diagnostics();
log::info!( log::info!(
"iris fonts: {} families found, default={:?} mono={:?}, resolved regular={:?} \ "iris fonts: {} families found, default={:?} mono={:?}, resolved regular={:?} \
bold={:?} italic={:?} mono={:?}", bold={:?} italic={:?} mono={:?}",
@@ -400,7 +400,7 @@ impl BenchClient {
fn show_diagnostics(&mut self, rsc: &mut Rsc) { fn show_diagnostics(&mut self, rsc: &mut Rsc) {
let report = self.diagnostics_text(rsc); let report = self.diagnostics_text(rsc);
self.report_display.edit(rsc).set(&report); self.report_display(rsc).set(&report);
self.last_report = Some(report); self.last_report = Some(report);
} }
@@ -423,7 +423,7 @@ impl BenchClient {
/// the keyboard-open capture (which only logs it), so the two can /// the keyboard-open capture (which only logs it), so the two can
/// never drift into reporting different things. /// never drift into reporting different things.
fn diagnostics_text(&self, rsc: &mut Rsc) -> String { fn diagnostics_text(&self, rsc: &mut Rsc) -> String {
let font = rsc.ui.text.font_diagnostics(); let font = rsc.ui.text.borrow_mut().font_diagnostics();
let frame_report = match self.android_state().frame_report.report() { let frame_report = match self.android_state().frame_report.report() {
Some(stats) => format!("{stats}"), Some(stats) => format!("{stats}"),
None => "no frames recorded yet".to_string(), None => "no frames recorded yet".to_string(),
@@ -481,7 +481,7 @@ impl BenchClient {
} }
self.running = true; self.running = true;
self.android_state_mut().frame_report.reset(); self.android_state_mut().frame_report.reset();
self.report_display.edit(rsc).set("Running benchmark..."); self.report_display(rsc).set("Running benchmark...");
let platform = self.platform.clone(); let platform = self.platform.clone();
let stream_tail = self.stream_tail.clone(); let stream_tail = self.stream_tail.clone();
@@ -620,7 +620,7 @@ impl BenchClient {
{rss_line}\n{battery}" {rss_line}\n{battery}"
); );
log::info!("iris bench report: {report}"); log::info!("iris bench report: {report}");
state.report_display.edit(rsc).set(&report); state.report_display(rsc).set(&report);
state.last_report = Some(report); state.last_report = Some(report);
}); });
}); });
@@ -776,7 +776,7 @@ async fn run_type_phase(
let text = typed.clone(); let text = typed.clone();
ctx.update(move |state: &mut BenchClient, rsc| { ctx.update(move |state: &mut BenchClient, rsc| {
if let Some(screen) = &state.screen { if let Some(screen) = &state.screen {
screen.composer.field.edit(rsc).set(&text); (screen.composer.field)(rsc).set(&text);
} }
}); });
tokio::time::sleep(Duration::from_millis(TYPE_CHAR_MS)).await; tokio::time::sleep(Duration::from_millis(TYPE_CHAR_MS)).await;
@@ -787,7 +787,7 @@ async fn run_type_phase(
let text = typed.clone(); let text = typed.clone();
ctx.update(move |state: &mut BenchClient, rsc| { ctx.update(move |state: &mut BenchClient, rsc| {
if let Some(screen) = &state.screen { if let Some(screen) = &state.screen {
screen.composer.field.edit(rsc).set(&text); (screen.composer.field)(rsc).set(&text);
} }
}); });
tokio::time::sleep(Duration::from_millis(TYPE_CHAR_MS)).await; tokio::time::sleep(Duration::from_millis(TYPE_CHAR_MS)).await;
+3 -3
View File
@@ -245,19 +245,19 @@ impl TranscriptClient {
let in_progress = self let in_progress = self
.screen .screen
.as_ref() .as_ref()
.map(|screen| screen.composer.field.edit(rsc).text.text().to_string()) .map(|screen| (screen.composer.field)(rsc).text().to_string())
.filter(|t| !t.is_empty()); .filter(|t| !t.is_empty());
let rows = group_tool_runs(&self.items); let rows = group_tool_runs(&self.items);
let (screen, tree) = ui::build_tree(rsc, rows); let (screen, tree) = ui::build_tree(rsc, rows);
if let Some(text) = in_progress { if let Some(text) = in_progress {
screen.composer.field.edit(rsc).set(&text); (screen.composer.field)(rsc).set(&text);
} }
if let Some(session_id) = self.session_id.clone() { if let Some(session_id) = self.session_id.clone() {
let field = screen.composer.field; let field = screen.composer.field;
rsc.register_event(field, Submit, move |ctx, rsc| { rsc.register_event(field, Submit, move |ctx, rsc| {
let text = field.edit(rsc).take(); let text = field(rsc).take();
let text = text.trim().to_string(); let text = text.trim().to_string();
if !text.is_empty() { if !text.is_empty() {
ctx.state.send_message(session_id.clone(), text); ctx.state.send_message(session_id.clone(), text);
+3 -3
View File
@@ -290,19 +290,19 @@ impl Client {
let in_progress = self let in_progress = self
.screen .screen
.as_ref() .as_ref()
.map(|screen| screen.composer.field.edit(rsc).text.text().to_string()) .map(|screen| (screen.composer.field)(rsc).text().to_string())
.filter(|t| !t.is_empty()); .filter(|t| !t.is_empty());
let rows = group_tool_runs(&self.items); let rows = group_tool_runs(&self.items);
let (screen, tree) = crate::ui::build_tree(rsc, rows); let (screen, tree) = crate::ui::build_tree(rsc, rows);
if let Some(text) = in_progress { if let Some(text) = in_progress {
screen.composer.field.edit(rsc).set(&text); (screen.composer.field)(rsc).set(&text);
} }
if let Some(session_id) = self.selected.clone() { if let Some(session_id) = self.selected.clone() {
let field = screen.composer.field; let field = screen.composer.field;
rsc.register_event(field, Submit, move |ctx, rsc| { rsc.register_event(field, Submit, move |ctx, rsc| {
let text = field.edit(rsc).take(); let text = field(rsc).take();
let text = text.trim().to_string(); let text = text.trim().to_string();
if !text.is_empty() { if !text.is_empty() {
ctx.state.send_message(session_id.clone(), text); ctx.state.send_message(session_id.clone(), text);
+1 -2
View File
@@ -6,8 +6,7 @@ const APPROX_LINE_HEIGHT_DP: f32 = 24.0;
const FIELD_PAD_DP: f32 = 12.0; const FIELD_PAD_DP: f32 = 12.0;
/// `field` is exposed so the caller can read its content on submit /// `field` is exposed so the caller can read its content on submit
/// (`field.edit(rsc).text()`) and clear it afterward /// (`field(rsc).text()`) and clear it afterward (`field(rsc).set("")`).
/// (`field.edit(rsc).set("")`).
pub struct Composer { pub struct Composer {
pub field: WeakWidget<TextEdit>, pub field: WeakWidget<TextEdit>,
/// The bar's own outer padding -- only `bottom` is ever changed, by /// The bar's own outer padding -- only `bottom` is ever changed, by
+18
View File
@@ -532,6 +532,24 @@ mod apply_tests {
out out
} }
#[test]
fn registering_a_font_after_drawing_invalidates_its_text_owner() {
let mut rsc = TestRsc {
ui: Ui::default(),
events: EventManager::default(),
};
rsc.ui.resize((800.0, 600.0));
let root = wtext("already shaped").add_strong(&mut rsc).any();
rsc.draw(&root);
assert!(!rsc.widgets().has_updates());
rsc.ui.register_font("late-icons", ICON_FONT).unwrap();
assert!(rsc.widgets().has_updates());
rsc.draw(&root);
assert!(!rsc.widgets().has_updates());
}
fn cost_of_one_delta(paragraphs: usize) -> (u64, u64) { fn cost_of_one_delta(paragraphs: usize) -> (u64, u64) {
let mut rsc = TestRsc { let mut rsc = TestRsc {
ui: Ui::default(), ui: Ui::default(),
+5 -5
View File
@@ -197,14 +197,14 @@ fn a_space_in_the_composer_finishes_layout() {
screen.composer.set_bottom_inset(&mut h.rsc, 1000.0); screen.composer.set_bottom_inset(&mut h.rsc, 1000.0);
h.frame(PHONE_FRAME_MS * 2); h.frame(PHONE_FRAME_MS * 2);
h.state.set_focus(Some(screen.composer.field)); h.state.set_focus(Some(screen.composer.field));
screen.composer.field.edit(&mut h.rsc).set_cursor_byte(0); (screen.composer.field)(&mut h.rsc).set_cursor_byte(0);
for text in ["h", "i", " "] { for text in ["h", "i", " "] {
screen.composer.field.edit(&mut h.rsc).insert(text); (screen.composer.field)(&mut h.rsc).insert(text);
h.frame(PHONE_FRAME_MS); h.frame(PHONE_FRAME_MS);
} }
assert_eq!(screen.composer.field.edit(&mut h.rsc).text.text(), "hi "); assert_eq!(&*(screen.composer.field)(&mut h.rsc).text(), "hi ");
let region = h let region = h
.rsc .rsc
.ui .ui
@@ -232,9 +232,9 @@ fn a_newline_leaves_the_caret_inside_the_composers_padding() {
h.frame(PHONE_FRAME_MS * 2); h.frame(PHONE_FRAME_MS * 2);
h.state.set_focus(Some(screen.composer.field)); h.state.set_focus(Some(screen.composer.field));
screen.composer.field.edit(&mut h.rsc).set_cursor_byte(0); (screen.composer.field)(&mut h.rsc).set_cursor_byte(0);
for _ in 0..12 { for _ in 0..12 {
screen.composer.field.edit(&mut h.rsc).insert("a\n"); (screen.composer.field)(&mut h.rsc).insert("a\n");
h.frame(PHONE_FRAME_MS); h.frame(PHONE_FRAME_MS);
} }
let render_handle = h.rsc.ui.render_state(); let render_handle = h.rsc.ui.render_state();
+3 -3
View File
@@ -279,9 +279,9 @@ rasteriser (`TextData::place`, which reads back whatever `shape` set) is
the display's *physical* size, and the glyph atlas holds a bitmap at the the display's *physical* size, and the glyph atlas holds a bitmap at the
resolution it is actually shown at. `GlyphKey.size` already keys on the resolution it is actually shown at. `GlyphKey.size` already keys on the
resolved size, so a cache entry is naturally per-physical-size with no resolved size, so a cache entry is naturally per-physical-size with no
further change. The one caller with no `Painter` to read density from further change. The callers with no `Painter` to read density from (cursor
(`TextEditCtx::layout`, cursor movement and hit-testing) reads a second movement and hit-testing through `TextHandle::layout`) read a second copy kept
copy kept directly on `TextData` (`TextData::density`) instead — an directly on `TextData` (`TextData::density`) instead — an
accepted duplication rather than threading a `Painter` into every input accepted duplication rather than threading a `Painter` into every input
handler for one field, the same tradeoff `AndroidRenderer::content_scale` handler for one field, the same tradeoff `AndroidRenderer::content_scale`
already makes for the Diagnostics page. already makes for the Diagnostics page.
+12 -7
View File
@@ -814,13 +814,18 @@ private implementations of that wake; applications do not define platform
event types or manually request redraws. event types or manually request redraws.
**Iris ships no fonts, and font families are application-named strings** **Iris ships no fonts, and font families are application-named strings**
(2026-09-12). Applications register their own font bytes on `Ui` before the (2026-09-12). Applications register their own font bytes on `Ui` and select
first text shape and select them by the same string used by text widgets. The them by the same string used by text widgets. The public boundaries accept
public boundaries accept `AsRef<str>`, so an application can use bare strings `AsRef<str>`, so an application can use bare strings or put its own semantic
or put its own semantic enum in front of them without Iris hardcoding the enum in front of them without Iris hardcoding the roles. Text buffers, layouts,
roles. ai-app owns the `ai-app-icons` family, its Nerd Fonts subset, its and prepared glyphs live in a per-`Ui` generational arena; a text widget holds
codepoints, its license and the script that rebuilds it; the CSS generic names one compact handle into it, while the arena and its shaping state are shared by
`sans-serif` and `monospace` continue to resolve through the platform. one `Rc<RefCell<_>>`. Registering another family invalidates the live arena
entries and dirties their active owner widgets, so registration is also valid
after the first shape. ai-app owns the `ai-app-icons` family, its Nerd Fonts
subset, its codepoints, its license and the script that rebuilds it; the CSS
generic names `sans-serif` and `monospace` continue to resolve through the
platform.
`cargo-iris` is an installable Cargo subcommand, rather than a script callers `cargo-iris` is an installable Cargo subcommand, rather than a script callers
must find inside an Iris checkout. `cargo iris apk` builds the Rust `cdylib` must find inside an Iris checkout. `cargo iris apk` builds the Rust `cdylib`
+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 {