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

+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::{
Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily,
Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
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::{
FontRef,
scale::{Render, ScaleContext, Source, StrikeWith},
@@ -25,7 +38,6 @@ pub struct FontDiagnostics {
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FontRegistrationError {
AlreadyRegistered(String),
TextAlreadyShaped,
InvalidFont(String),
}
@@ -35,10 +47,6 @@ impl fmt::Display for FontRegistrationError {
Self::AlreadyRegistered(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) => {
write!(
f,
@@ -57,10 +65,9 @@ pub struct TextData {
scale_cx: ScaleContext,
pub atlas: GlyphAtlas,
/// Physical pixels per dp -- a second copy of
/// `UiRenderState::density`, kept here too because `TextEditCtx::layout`
/// (cursor movement and hit-testing, `widget/text/edit.rs`) shapes text
/// from an event callback that has a `TextData` but no `Painter`, so it
/// has nowhere else to read the display's density from. Both copies are
/// `UiRenderState::density`, kept here too because cursor movement and
/// hit-testing shape text from event callbacks that have no `Painter`,
/// so they have nowhere else to read the display's density from. Both copies are
/// set together, from the one place either backend learns the real
/// value (`android::view::new_peer`); this is the same accepted
/// duplication as `AndroidRenderer::content_scale`; a single source of
@@ -69,7 +76,6 @@ pub struct TextData {
pub density: f32,
registered_families: HashMap<String, String>,
next_registered_family: u64,
shaping_started: bool,
}
impl Default for TextData {
@@ -84,7 +90,6 @@ impl Default for TextData {
density: 1.0,
registered_families: HashMap::new(),
next_registered_family: 0,
shaping_started: false,
}
}
}
@@ -168,9 +173,6 @@ impl TextData {
data: impl AsRef<[u8]> + Send + Sync + 'static,
) -> Result<(), FontRegistrationError> {
let family = family.as_ref().to_owned();
if self.shaping_started {
return Err(FontRegistrationError::TextAlreadyShaped);
}
if self.registered_families.contains_key(&family) {
return Err(FontRegistrationError::AlreadyRegistered(family));
}
@@ -523,7 +525,6 @@ impl TextBuffer {
width: Option<f32>,
density: f32,
) {
data.shaping_started = true;
if self.shaped.as_ref() == Some(&(attrs.clone(), width, density)) {
return;
}
@@ -574,6 +575,10 @@ impl TextBuffer {
.align(Alignment::Start, AlignmentOptions::default());
self.shaped = Some((attrs.clone(), width, density));
}
fn invalidate(&mut self) {
self.shaped = None;
}
}
fn hash_coords(coords: &[i16]) -> u64 {
@@ -605,6 +610,261 @@ pub struct RenderedText {
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)]
mod tests {
use super::*;
@@ -619,13 +879,18 @@ mod tests {
}
#[test]
fn registering_after_shaping_is_reported() {
let mut data = TextData::default();
let mut buffer = TextBuffer::new("ordinary platform text");
buffer.shape(&mut data, &TextAttrs::default(), None, 1.0);
assert_eq!(
data.register_font("icons", b"not a font" as &'static [u8]),
Err(FontRegistrationError::TextAlreadyShaped)
fn dropped_handles_release_their_arena_entries() {
let resources = Rc::new(RefCell::new(TextResources::new()));
let text = TextResources::add(
resources.clone(),
TextBuffer::new("temporary"),
TextAttrs::default(),
);
assert_eq!(resources.borrow().entries.len(), 1);
drop(text);
resources.borrow_mut().free_released();
assert!(resources.borrow().entries.is_empty());
}
}