iris: move text state into shared resources
This commit is contained in:
1 parent
7c23c5f146
commit
51719ed121
17 files changed
+631
-434
No files matched your search
+289
-24
@@ -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());
|
||||
}
|
||||
}
|
||||
+22
-9
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
Mask, MoveOffset, Paints, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
|
||||
Mask, MoveOffset, Paints, TextResources, Textures, WeakWidget, WidgetId, Widgets,
|
||||
util::TrackedArena,
|
||||
};
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
@@ -22,7 +23,7 @@ pub struct UiData {
|
||||
pub widgets: Widgets,
|
||||
pub paints: Paints,
|
||||
pub textures: Textures,
|
||||
pub text: TextData,
|
||||
pub text: Rc<RefCell<TextResources>>,
|
||||
pub masks: TrackedArena<Mask, u32>,
|
||||
pub move_offsets: TrackedArena<MoveOffset, u32>,
|
||||
animating: Vec<WidgetId>,
|
||||
@@ -67,21 +68,32 @@ pub struct Ui {
|
||||
|
||||
impl Ui {
|
||||
/// Register application-owned font data for a semantic or named family.
|
||||
///
|
||||
/// This must happen before the first text shape. Existing text layouts
|
||||
/// cache their resolved faces, so accepting a later registration would
|
||||
/// leave already-shaped widgets displaying the old result.
|
||||
/// Existing text resources are invalidated and their active widgets are
|
||||
/// scheduled for layout again.
|
||||
#[track_caller]
|
||||
pub fn register_font(
|
||||
&mut self,
|
||||
family: impl AsRef<str>,
|
||||
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||
) -> 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 {
|
||||
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.
|
||||
@@ -96,7 +108,7 @@ impl Ui {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -172,5 +184,6 @@ pub trait UiRsc {
|
||||
}
|
||||
self.ui_mut().textures.free();
|
||||
self.ui_mut().paints.free_released();
|
||||
self.ui_mut().text.borrow_mut().free_released();
|
||||
}
|
||||
}
|
||||
+11
-15
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
Axis, Len, MoveOffset, PaintId, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs,
|
||||
TextBuffer, TextData, TextureHandle, UiData, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2,
|
||||
Axis, Len, MoveOffset, PaintId, RegionAlign, RenderedText, Size, StrongWidget, TextHandle,
|
||||
TextResources, TextureHandle, UiData, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2,
|
||||
WidgetId,
|
||||
render::{
|
||||
Drawn, GlyphPrimitive, IMAGE_BINDING, Mask, MaskIdx, MoveIdx, NOT_DRAWN, Primitive,
|
||||
@@ -9,6 +9,7 @@ use crate::{
|
||||
ui::render_state::Retained,
|
||||
util::Vec2,
|
||||
};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
pub struct Painter<'a> {
|
||||
pub(super) render_state: &'a mut UiRenderState,
|
||||
@@ -413,21 +414,16 @@ impl<'a> Painter<'a> {
|
||||
self.own(h);
|
||||
}
|
||||
|
||||
pub fn render_text(
|
||||
&mut self,
|
||||
buffer: &mut TextBuffer,
|
||||
attrs: &TextAttrs,
|
||||
width: Option<f32>,
|
||||
) -> RenderedText {
|
||||
pub fn render_text(&mut self, text: &TextHandle, width: Option<f32>) -> RenderedText {
|
||||
let density = self.render_state.density;
|
||||
self.render_state.shape_count += 1;
|
||||
let ui: &mut UiData = self.rsc.ui_mut();
|
||||
ui.text
|
||||
.render(buffer, attrs, width, &mut ui.textures, density)
|
||||
let (rendered, prepared) = text.render(width, self.id, &mut ui.textures, density);
|
||||
self.render_state.shape_count += u64::from(prepared);
|
||||
rendered
|
||||
}
|
||||
|
||||
pub fn atlas_generation(&mut self) -> u64 {
|
||||
self.rsc.ui_mut().text.atlas.generation()
|
||||
fn atlas_generation(&self) -> u64 {
|
||||
self.rsc.ui().text.borrow().atlas.generation()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn text_data(&mut self) -> &mut TextData {
|
||||
&mut self.rsc.ui_mut().text
|
||||
pub fn text_resources(&mut self) -> Rc<RefCell<TextResources>> {
|
||||
self.rsc.ui().text.clone()
|
||||
}
|
||||
|
||||
pub fn child_layer(&mut self) {
|
||||
|
||||
@@ -71,6 +71,10 @@ impl<T> SlotVec<T> {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
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> {
|
||||
|
||||
Reference in new issue
Block a user