use crate::{ Align, GlyphAtlas, GlyphKey, Len, PaintId, PlacedGlyph, RegionAlign, Textures, WidgetId, util::{Resources, RscHandle, StrongRscId, Vec2, WeakRscId}, }; use parley::{ Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, fontique::{Blob, Collection, CollectionOptions, FamilyId, FontInfoOverride, FontWidth}, }; use std::{ cell::{Ref, RefCell, RefMut}, collections::HashMap, fmt, ops::{Deref, DerefMut, Range}, rc::Rc, sync::Arc, }; use swash::{ FontRef, scale::{Render, ScaleContext, Source, StrikeWith}, zeno::{Format, Vector}, }; #[derive(Clone, Debug)] pub struct FontDiagnostics { pub families_found: usize, pub default_family: Option, pub default_mono_family: Option, pub regular_resolved: Option, pub bold_resolved: Option, pub italic_resolved: Option, pub mono_resolved: Option, } #[derive(Clone, Debug, PartialEq, Eq)] pub enum FontRegistrationError { AlreadyRegistered(String), NotRegistered(String), InvalidFont(String), } impl fmt::Display for FontRegistrationError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::AlreadyRegistered(family) => { write!(f, "font data is already registered for {family:?}") } Self::NotRegistered(family) => { write!(f, "no font data is registered for {family:?}") } Self::InvalidFont(family) => { write!( f, "font data registered for {family:?} contains no usable fonts" ) } } } } impl std::error::Error for FontRegistrationError {} /// The bucket used by [`crate::Ui::register_font`] when none is specified. pub const DEFAULT_GLYPH_BUCKET: &str = "default"; struct RegisteredFace { family: FamilyId, width: FontWidth, style: FontStyle, weight: FontWeight, } struct RegisteredFont { private_name: String, bucket: u64, font_id: u64, faces: Vec, } pub struct TextData { pub font_cx: FontContext, pub layout_cx: LayoutContext, scale_cx: ScaleContext, pub atlas: GlyphAtlas, /// Physical pixels per dp -- a second copy of /// `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 /// truth would mean carrying a `Painter` (or output size) into every /// input handler for the sake of one field. pub density: f32, registered_families: HashMap, glyph_buckets: HashMap, font_buckets: HashMap, next_registered_family: u64, next_glyph_bucket: u64, } impl Default for TextData { fn default() -> Self { let mut font_cx = FontContext::new(); patch_android_monospace(&mut font_cx); Self { font_cx, layout_cx: LayoutContext::new(), scale_cx: ScaleContext::new(), atlas: GlyphAtlas::default(), density: 1.0, registered_families: HashMap::new(), glyph_buckets: HashMap::new(), font_buckets: HashMap::new(), next_registered_family: 0, next_glyph_bucket: 1, } } } /// So this reads `fonts.xml` itself (already on-device, already the /// authority Compose's own `Typeface.MONOSPACE` resolves through) for the /// filename that declaration names, then finds which of fontique's /// *actually* scanned families (from `/system/fonts`, which do carry real /// font data, just under whatever name the font's own metadata gives it -- /// "Droid Sans Mono" here, but that name is never hardcoded) owns a font /// file with that name, and registers that family as the `Monospace` /// generic the way the backend itself would have if its parser had reified /// the declaration. A no-op if the family is somehow already resolved /// (future fontique) or nothing matches (no `fonts.xml`, e.g. a headless /// test, or a device that names it some other way). #[cfg(target_os = "android")] fn patch_android_monospace(font_cx: &mut FontContext) { use parley::fontique::SourceKind; let already_resolved = font_cx .collection .generic_families(GenericFamily::Monospace) .next() .is_some(); if already_resolved { return; } let Some(target_file) = android_monospace_font_filename() else { return; }; let names: Vec = font_cx .collection .family_names() .map(str::to_string) .collect(); for name in names { let Some(id) = font_cx.collection.family_id(&name) else { continue; }; let Some(info) = font_cx.collection.family(id) else { continue; }; let Some(font) = info.default_font() else { continue; }; let SourceKind::Path(path) = font.source().kind() else { continue; }; if path.file_name().and_then(|f| f.to_str()) == Some(target_file.as_str()) { font_cx .collection .append_generic_families(GenericFamily::Monospace, std::iter::once(id)); return; } } } #[cfg(target_os = "android")] fn android_monospace_font_filename() -> Option { let android_root = std::env::var("ANDROID_ROOT").unwrap_or_else(|_| "/system".to_string()); let xml = std::fs::read_to_string(std::path::Path::new(&android_root).join("etc/fonts.xml")).ok()?; let family_start = xml.find("")?; let block = &xml[family_start..]; let block = &block[..block.find("")?]; let font_tag = block.find("')? + 1; let content = &after_tag[content_start..]; let filename = content[..content.find('<')?].trim(); (!filename.is_empty()).then(|| filename.to_string()) } #[cfg(not(target_os = "android"))] fn patch_android_monospace(_font_cx: &mut FontContext) {} impl TextData { pub(crate) fn register_font( &mut self, family: impl AsRef, data: impl AsRef<[u8]> + Send + Sync + 'static, ) -> Result<(), FontRegistrationError> { self.register_font_in(family, DEFAULT_GLYPH_BUCKET, data) } pub(crate) fn register_font_in( &mut self, family: impl AsRef, bucket: impl AsRef, data: impl AsRef<[u8]> + Send + Sync + 'static, ) -> Result<(), FontRegistrationError> { let family = family.as_ref().to_owned(); if self.registered_families.contains_key(&family) { return Err(FontRegistrationError::AlreadyRegistered(family)); } let private_name = format!("__iris_registered_font_{}__", self.next_registered_family); let blob = Blob::new(Arc::new(data)); Self::validate_font(&family, blob.clone())?; let bucket = self.glyph_bucket(bucket.as_ref()); let registered = self.add_font(private_name, bucket, blob); self.next_registered_family += 1; self.font_buckets.insert(registered.font_id, bucket); self.registered_families.insert(family, registered); Ok(()) } pub(crate) fn replace_font( &mut self, family: impl AsRef, data: impl AsRef<[u8]> + Send + Sync + 'static, ) -> Result<(), FontRegistrationError> { let family = family.as_ref(); let Some(bucket) = self.registered_families.get(family).map(|font| font.bucket) else { return Err(FontRegistrationError::NotRegistered(family.to_owned())); }; self.replace_font_with(family, bucket, data) } pub(crate) fn replace_font_in( &mut self, family: impl AsRef, bucket: impl AsRef, data: impl AsRef<[u8]> + Send + Sync + 'static, ) -> Result<(), FontRegistrationError> { let family = family.as_ref(); if !self.registered_families.contains_key(family) { return Err(FontRegistrationError::NotRegistered(family.to_owned())); } let bucket = self.glyph_bucket(bucket.as_ref()); self.replace_font_with(family, bucket, data) } fn replace_font_with( &mut self, family: &str, bucket: u64, data: impl AsRef<[u8]> + Send + Sync + 'static, ) -> Result<(), FontRegistrationError> { let blob = Blob::new(Arc::new(data)); Self::validate_font(family, blob.clone())?; let old = self.registered_families.remove(family).unwrap(); for face in old.faces { self.font_cx.collection.unregister_font( face.family, face.width, face.style, face.weight, ); } self.font_buckets.remove(&old.font_id); self.atlas.clear_bucket(old.bucket); let registered = self.add_font(old.private_name, bucket, blob); self.font_buckets.insert(registered.font_id, bucket); self.registered_families .insert(family.to_owned(), registered); Ok(()) } fn validate_font(family: &str, blob: Blob) -> Result<(), FontRegistrationError> { let mut collection = Collection::new(CollectionOptions { shared: false, system_fonts: false, }); let fonts = collection.register_fonts(blob, None); if fonts.is_empty() { Err(FontRegistrationError::InvalidFont(family.to_owned())) } else { Ok(()) } } fn add_font(&mut self, private_name: String, bucket: u64, blob: Blob) -> RegisteredFont { let font_id = blob.id(); let fonts = self.font_cx.collection.register_fonts( blob, Some(FontInfoOverride { family_name: Some(&private_name), ..Default::default() }), ); debug_assert!(!fonts.is_empty(), "validated font failed its second scan"); let faces = fonts .into_iter() .flat_map(|(family, fonts)| { fonts.into_iter().map(move |font| RegisteredFace { family, width: font.width(), style: font.style(), weight: font.weight(), }) }) .collect(); RegisteredFont { private_name, bucket, font_id, faces, } } fn glyph_bucket(&mut self, name: &str) -> u64 { if name == DEFAULT_GLYPH_BUCKET { return crate::render::DEFAULT_GLYPH_BUCKET_ID; } if let Some(bucket) = self.glyph_buckets.get(name) { return *bucket; } let bucket = self.next_glyph_bucket; self.next_glyph_bucket += 1; self.glyph_buckets.insert(name.to_owned(), bucket); bucket } fn bucket_for_font(&self, font: u64) -> u64 { self.font_buckets .get(&font) .copied() .unwrap_or(crate::render::DEFAULT_GLYPH_BUCKET_ID) } pub(crate) fn is_font_registered(&self, family: impl AsRef) -> bool { self.registered_families.contains_key(family.as_ref()) } /// Cloned rather than borrowed because the caller needs it while the /// layout builder holds `&mut self` -- a `String` per shaped registered /// run, paid only when the layout is rebuilt. pub fn resolve_family(&self, family: &str) -> String { if let Some(font) = self.registered_families.get(family) { return font.private_name.clone(); } family.to_owned() } pub fn font_diagnostics(&mut self) -> FontDiagnostics { use parley::fontique::{Attributes, FontWidth, QueryStatus}; let families_found = self.font_cx.collection.family_names().count(); let default_family_id = self .font_cx .collection .generic_families(GenericFamily::SansSerif) .next(); let default_family = default_family_id .and_then(|id| self.font_cx.collection.family_name(id).map(str::to_string)); let default_mono_family_id = self .font_cx .collection .generic_families(GenericFamily::Monospace) .next(); let default_mono_family = default_mono_family_id .and_then(|id| self.font_cx.collection.family_name(id).map(str::to_string)); let mut resolve_family = |generic: GenericFamily, weight: FontWeight, style: FontStyle| -> Option { let mut family_id = None; { let mut query = self .font_cx .collection .query(&mut self.font_cx.source_cache); query.set_families([generic]); query.set_attributes(Attributes { width: FontWidth::NORMAL, style, weight, }); query.matches_with(|font| { family_id = Some(font.family.0); QueryStatus::Stop }); } family_id.and_then(|id| self.font_cx.collection.family_name(id).map(str::to_string)) }; let regular_resolved = resolve_family( GenericFamily::SansSerif, FontWeight::NORMAL, FontStyle::Normal, ); let bold_resolved = resolve_family( GenericFamily::SansSerif, FontWeight::BOLD, FontStyle::Normal, ); let italic_resolved = resolve_family( GenericFamily::SansSerif, FontWeight::NORMAL, FontStyle::Italic, ); let mono_resolved = resolve_family( GenericFamily::Monospace, FontWeight::NORMAL, FontStyle::Normal, ); FontDiagnostics { families_found, default_family, default_mono_family, regular_resolved, bold_resolved, italic_resolved, mono_resolved, } } pub fn place( &mut self, buffer: &TextBuffer, textures: &mut Textures, ) -> (Vec, Vec) { let mut placed = Vec::new(); let mut paints = Vec::new(); for line in buffer.layout.lines() { for item in line.items() { let PositionedLayoutItem::GlyphRun(run) = item else { continue; }; let font = run.run().font(); let font_size = run.run().font_size(); let coords = run.run().normalized_coords(); let run_color = run.style().brush.clone(); if !paints.contains(&run_color) { paints.push(run_color.clone()); } let Some(font_ref) = FontRef::from_index(font.data.as_ref(), font.index as usize) else { continue; }; let coords_hash = hash_coords(coords); let font_id = font.data.id(); let bucket = self.bucket_for_font(font_id); for glyph in run.positioned_glyphs() { let (x, subpixel) = horizontal_phase(glyph.x); let key = GlyphKey { font: font_id, glyph: glyph.id, size: (font_size * 16.0).round() as u32, subpixel, coords: coords_hash, }; let entry = match self.atlas.get(bucket, &key) { Some(entry) => entry, None => { let mut scaler = self .scale_cx .builder(font_ref) .size(font_size) .hint(true) .normalized_coords(coords) .build(); let image = Render::new(&[ Source::ColorOutline(0), Source::ColorBitmap(StrikeWith::BestFit), Source::Outline, ]) .format(Format::Alpha) .offset(Vector::new(subpixel as f32 / 4.0, 0.0)) .render(&mut scaler, glyph.id as u16); match image { Some(image) => self.atlas.insert(bucket, key, &image, textures), None => { self.atlas.insert_empty(bucket, key); None } } } }; let Some(entry) = entry else { continue }; placed.push(PlacedGlyph { entry, offset: Vec2::new( x + entry.left as f32, glyph.y.floor() - entry.top as f32, ), paint: run_color.slot(), }); } } } (placed, paints) } pub fn render( &mut self, buffer: &mut TextBuffer, attrs: &TextAttrs, width: Option, textures: &mut Textures, density: f32, ) -> RenderedText { buffer.shape(self, attrs, width, density); let (glyphs, paints) = self.place(buffer, textures); RenderedText { glyphs: std::sync::Arc::new(glyphs), paints: std::sync::Arc::new(paints), size: buffer.size(), color: attrs.color.clone(), generation: self.atlas.generation(), } } } fn horizontal_phase(x: f32) -> (f32, u8) { let quarters = (x * 4.0).round() as i32; (quarters.div_euclid(4) as f32, quarters.rem_euclid(4) as u8) } pub const SANS_SERIF: &str = "sans-serif"; pub const SERIF: &str = "serif"; pub const MONOSPACE: &str = "monospace"; #[derive(Clone, PartialEq)] pub struct SpanStyle { pub range: Range, pub color: Option, pub family: Option, pub font_size: Option, pub bold: bool, pub italic: bool, pub underline: bool, } impl SpanStyle { pub fn new(range: Range) -> Self { Self { range, color: None, family: None, font_size: None, bold: false, italic: false, underline: false, } } pub fn color(mut self, color: PaintId) -> Self { self.color = Some(color); self } pub fn family(mut self, family: impl AsRef) -> Self { self.family = Some(family.as_ref().to_owned()); self } pub fn font_size(mut self, size: f32) -> Self { self.font_size = Some(size); self } pub fn bold(mut self) -> Self { self.bold = true; self } pub fn italic(mut self) -> Self { self.italic = true; self } pub fn underline(mut self) -> Self { self.underline = true; self } } #[derive(Clone, PartialEq)] pub struct TextAttrs { pub color: PaintId, pub font_size: f32, pub line_height: f32, pub family: String, pub overflow: TextOverflow, pub overflow_position: Len, pub align: RegionAlign, } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum TextOverflow { #[default] Visible, Wrap, Hidden, Ellipsis, } #[derive(Clone, PartialEq)] struct TextShapeAttrs { color: PaintId, font_size: f32, line_height: f32, family: String, } impl From<&TextAttrs> for TextShapeAttrs { fn from(attrs: &TextAttrs) -> Self { Self { color: attrs.color.clone(), font_size: attrs.font_size, line_height: attrs.line_height, family: attrs.family.clone(), } } } impl TextShapeAttrs { fn matches(&self, attrs: &TextAttrs) -> bool { self.color == attrs.color && self.font_size == attrs.font_size && self.line_height == attrs.line_height && self.family == attrs.family } } pub const LINE_HEIGHT_MULT: f32 = 1.1; impl Default for TextAttrs { fn default() -> Self { let size = 16.0; Self { color: PaintId::WHITE, font_size: size, line_height: size * LINE_HEIGHT_MULT, family: SANS_SERIF.to_owned(), overflow: TextOverflow::Visible, overflow_position: Len::ZERO, align: Align::CENTER_LEFT, } } } /// The text and the layout live in one place because parley's `Layout` borrows /// nothing but is only meaningful against the string it was built from: keeping /// them apart is how they get out of step. pub struct TextBuffer { text: String, layout: Layout, spans: Vec, shaped: Option<(TextShapeAttrs, Option, f32)>, } impl TextBuffer { pub fn new(text: impl Into) -> Self { Self { text: text.into(), layout: Layout::new(), spans: Vec::new(), shaped: None, } } pub fn set_spans(&mut self, spans: Vec) { self.spans = spans; self.shaped = None; } pub fn new_empty() -> Self { Self::new("") } pub fn text(&self) -> &str { &self.text } pub fn layout(&self) -> &Layout { &self.layout } pub fn is_empty(&self) -> bool { self.text.is_empty() } pub fn set_text(&mut self, text: impl Into) { let text = text.into(); if text != self.text { self.text = text; self.shaped = None; } } /// Edit the string in place; invalidates the layout unconditionally, since /// the caller is assumed to have changed something. pub fn edit(&mut self) -> &mut String { self.shaped = None; &mut self.text } pub fn size(&self) -> Vec2 { Vec2::new(self.layout.width(), self.layout.height()) } pub fn shape( &mut self, data: &mut TextData, attrs: &TextAttrs, width: Option, density: f32, ) { if self .shaped .as_ref() .is_some_and(|(old, old_width, old_density)| { old.matches(attrs) && *old_width == width && *old_density == density }) { return; } let shape_attrs = TextShapeAttrs::from(attrs); let base_family = data.resolve_family(&attrs.family); let span_families: Vec> = self .spans .iter() .map(|span| span.family.as_ref().map(|f| data.resolve_family(f))) .collect(); let mut builder = data .layout_cx .ranged_builder(&mut data.font_cx, &self.text, 1.0, true); builder.push_default(StyleProperty::FontFamily(FontFamily::from( base_family.as_str(), ))); builder.push_default(StyleProperty::FontSize(attrs.font_size * density)); builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute( attrs.line_height * density, ))); builder.push_default(StyleProperty::Brush(attrs.color.clone())); for (span, family) in self.spans.iter().zip(&span_families) { let range = span.range.clone(); if let Some(color) = &span.color { builder.push(StyleProperty::Brush(color.clone()), range.clone()); } if let Some(family) = family { builder.push( StyleProperty::FontFamily(FontFamily::from(family.as_str())), range.clone(), ); } if let Some(size) = span.font_size { builder.push(StyleProperty::FontSize(size * density), range.clone()); } if span.bold { builder.push(StyleProperty::FontWeight(FontWeight::BOLD), range.clone()); } if span.italic { builder.push(StyleProperty::FontStyle(FontStyle::Italic), range.clone()); } if span.underline { builder.push(StyleProperty::Underline(true), range.clone()); } } builder.build_into(&mut self.layout, &self.text); self.layout.break_all_lines(width); self.layout .align(Alignment::Start, AlignmentOptions::default()); self.shaped = Some((shape_attrs, width, density)); } fn invalidate(&mut self) { // A layout owns the font blobs it was shaped with. Drop it now so // replacing a registered font does not retain the old bytes until an // off-screen text resource happens to be shaped again. self.layout = Layout::new(); self.shaped = None; } } fn hash_coords(coords: &[i16]) -> u64 { let mut h: u64 = 0xcbf2_9ce4_8422_2325; for c in coords { h ^= *c as u16 as u64; h = h.wrapping_mul(0x1000_0000_01b3); } h } /// Cheap to clone and to keep, which is the point -- a widget holds one across /// frames and re-emits its quads without going near the rasteriser. `color` /// is the buffer's *base* colour (`TextAttrs::color`) for a caller that wants /// it as a whole (e.g. tinting a cursor to match); the colour each glyph is /// actually drawn in is `PlacedGlyph::color`, which a `SpanStyle` can /// override per range. #[derive(Clone)] pub struct RenderedText { pub glyphs: std::sync::Arc>, /// The unique handles whose compact slots the glyphs above carry. pub paints: std::sync::Arc>, pub size: Vec2, pub color: PaintId, /// The [`GlyphAtlas::generation`] the glyphs above were placed against. /// A holder must re-render rather than re-emit these quads once the /// atlas has moved on (`GlyphAtlas::clear`'s doc says what happens /// otherwise); `Painter::glyphs` debug-asserts it. pub generation: u64, } pub struct TextRsc { buffer: TextBuffer, ellipsis_buffer: TextBuffer, attrs: TextAttrs, rendered: Option, ellipsis_rendered: Option<(TextShapeAttrs, f32, RenderedText)>, width: Option, density: f32, owner: Option, } impl TextRsc { fn new(buffer: TextBuffer, attrs: TextAttrs) -> Self { Self { buffer, ellipsis_buffer: TextBuffer::new("…"), attrs, rendered: None, ellipsis_rendered: None, width: None, density: 0.0, owner: None, } } fn invalidate(&mut self) { self.buffer.invalidate(); self.rendered = None; self.ellipsis_buffer.invalidate(); self.ellipsis_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: Rc>>, } impl TextResources { pub fn new() -> Self { Self { data: TextData::default(), entries: Rc::new(RefCell::new(Resources::new())), } } pub fn add(resources: Rc>, buffer: TextBuffer, attrs: TextAttrs) -> TextHandle { let entries = { let mut resources = resources.borrow_mut(); resources.free_released(); resources.entries.clone() }; TextHandle { rsc: RscHandle::add(entries, TextRsc::new(buffer, attrs)), resources, } } pub fn handle(resources: Rc>, id: StrongRscId) -> TextHandle { let entries = resources.borrow().entries.clone(); TextHandle { rsc: RscHandle::new(id, entries), resources, } } pub fn upgrade(resources: Rc>, id: WeakRscId) -> Option { let entries = { let mut resources = resources.borrow_mut(); resources.free_released(); resources.entries.clone() }; let id = entries.borrow_mut().upgrade(id)?; Some(Self::handle(resources, id)) } pub fn free_released(&mut self) { self.entries.borrow_mut().apply(|_, _| {}); } pub fn invalidate_all(&mut self) -> Vec { let mut owners = Vec::new(); for resource in self.entries.borrow_mut().values_mut() { resource.invalidate(); if let Some(owner) = resource.owner && !owners.contains(&owner) { owners.push(owner); } } owners } fn shape(&mut self, resource: &mut TextRsc) { let density = self.data.density; resource .buffer .shape(&mut self.data, &resource.attrs, resource.width, density); } fn render( &mut self, resource: &mut TextRsc, width: Option, owner: WidgetId, textures: &mut Textures, density: f32, ) -> (RenderedText, bool) { let atlas_generation = self.data.atlas.generation(); 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 rendered = self.data.render( &mut resource.buffer, &resource.attrs, width, textures, density, ); resource.rendered = Some(rendered.clone()); (rendered, true) } fn render_ellipsis( &mut self, resource: &mut TextRsc, owner: WidgetId, textures: &mut Textures, density: f32, ) -> (RenderedText, bool) { let shape_attrs = TextShapeAttrs::from(&resource.attrs); let atlas_generation = self.data.atlas.generation(); resource.owner = Some(owner); if let Some((cached_attrs, cached_density, rendered)) = &resource.ellipsis_rendered && cached_attrs == &shape_attrs && *cached_density == density && rendered.generation == atlas_generation { return (rendered.clone(), false); } let rendered = self.data.render( &mut resource.ellipsis_buffer, &resource.attrs, None, textures, density, ); resource.ellipsis_rendered = Some((shape_attrs, density, 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 { rsc: RscHandle, resources: Rc>, } impl TextHandle { pub fn strong(&self) -> StrongRscId { self.rsc.strong() } pub fn weak(&self) -> WeakRscId { self.rsc.weak() } pub fn text(&self) -> Ref<'_, str> { Ref::map(self.rsc.get(), |resource| resource.buffer.text()) } pub fn attrs(&self) -> Ref<'_, TextAttrs> { Ref::map(self.rsc.get(), |resource| &resource.attrs) } pub fn set_text(&mut self, text: impl Into) -> bool { let text = text.into(); let mut resource = self.rsc.get_mut(); if resource.buffer.text() == text { return false; } resource.buffer.set_text(text); resource.rendered = None; true } pub fn edit_text(&mut self, edit: impl FnOnce(&mut String) -> R) -> R { let mut resource = self.rsc.get_mut(); resource.rendered = None; edit(resource.buffer.edit()) } pub fn set_spans(&mut self, spans: Vec) { let mut resource = self.rsc.get_mut(); resource.buffer.set_spans(spans); resource.rendered = None; } pub fn attrs_mut(&mut self) -> RefMut<'_, TextAttrs> { let mut resource = self.rsc.get_mut(); resource.invalidate(); RefMut::map(resource, |resource| &mut resource.attrs) } pub fn set_overflow_position(&mut self, position: Len) { self.rsc.get_mut().attrs.overflow_position = position; } pub fn set_overflow(&mut self, overflow: TextOverflow) { self.rsc.get_mut().attrs.overflow = overflow; } pub fn rendered(&self) -> Option { self.rsc.get().rendered.clone() } pub fn width(&self) -> Option { self.rsc.get().width } pub fn with_layout(&self, f: impl FnOnce(&Layout, &str) -> R) -> R { let mut resources = self.resources.borrow_mut(); let mut resource = self.rsc.get_mut_shared(); resources.shape(&mut resource); f(resource.buffer.layout(), resource.buffer.text()) } pub fn layout(&self) -> Ref<'_, Layout> { { let mut resources = self.resources.borrow_mut(); let mut resource = self.rsc.get_mut_shared(); resources.shape(&mut resource); } Ref::map(self.rsc.get(), |resource| resource.buffer.layout()) } pub fn render( &self, width: Option, owner: WidgetId, textures: &mut Textures, density: f32, ) -> (RenderedText, bool) { let mut resources = self.resources.borrow_mut(); let mut resource = self.rsc.get_mut_shared(); resources.render(&mut resource, width, owner, textures, density) } pub fn render_ellipsis( &self, owner: WidgetId, textures: &mut Textures, density: f32, ) -> (RenderedText, bool) { let mut resources = self.resources.borrow_mut(); let mut resource = self.rsc.get_mut_shared(); resources.render_ellipsis(&mut resource, owner, textures, density) } } #[cfg(test)] mod tests { use super::*; #[test] fn a_horizontal_phase_carries_across_pixel_boundaries() { assert_eq!(horizontal_phase(10.20), (10.0, 1)); assert_eq!(horizontal_phase(10.90), (11.0, 0)); assert_eq!(horizontal_phase(-0.20), (-1.0, 3)); } #[test] fn invalid_font_data_is_reported() { let mut data = TextData::default(); assert_eq!( data.register_font("icons", b"not a font" as &'static [u8]), Err(FontRegistrationError::InvalidFont("icons".to_owned())) ); } #[test] 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.borrow().len(), 1); drop(text); resources.borrow_mut().free_released(); assert!(resources.borrow().entries.borrow().is_empty()); } }