#[cfg(feature = "layout-diagnostics")] use crate::layout_diagnostics::{self as diag, Counter, TimerKind}; use crate::{ Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, Px, PxVec2, RegionAlign, UiColor, util::Vec2, }; use parley::{ Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, }; use std::{ collections::VecDeque, hash::{DefaultHasher, Hash, Hasher}, }; use swash::{ FontRef, scale::{Render, ScaleContext, Source, StrikeWith}, zeno::{Format, Vector}, }; pub struct TextData { pub font_ctx: FontContext, pub layout_ctx: LayoutContext, scale_ctx: ScaleContext, pub atlas: GlyphAtlas, spare: VecDeque, } /// The glyphs of one text at one width. A buffer holds the ones it is drawn /// as; these are the ones it had before, kept because a container measures a /// child by drawing it in a box it may not keep, and so comes back to widths /// it has already asked for. struct Placed { /// Where the glyphs land is a function of these three and nothing else, /// so no widget or buffer identity is involved and two texts of the same /// words share an answer. text: String, key: LayoutKey, glyphs: RenderedText, } /// How many to keep. Bounding the whole store rather than each buffer is what /// makes this a fixed cost instead of one a tree of ten thousand texts pays /// ten thousand times; the re-asks come from laying out one subtree, so they /// are close together and few are needed. Instructions over 500 resize frames /// of `tests/revision_cost.rs`, both the repeating widths and the sweep that /// cannot hit across frames: 13.7B at 32, 12.1B at 64, 10.4B and 12.1B at 128, /// and nothing past that -- so 128, which is no worse in the case that never /// repeats and better in the one that does. const SPARE_PLACED: usize = 128; impl Default for TextData { fn default() -> Self { Self { font_ctx: FontContext::new(), layout_ctx: LayoutContext::new(), scale_ctx: ScaleContext::new(), atlas: GlyphAtlas::default(), spare: VecDeque::new(), } } } #[derive(Clone, PartialEq)] pub enum Family { SansSerif, Serif, Monospace, Named(String), } impl Family { fn family(&self) -> FontFamily<'_> { let name = match self { Self::SansSerif => FontFamilyName::Generic(GenericFamily::SansSerif), Self::Serif => FontFamilyName::Generic(GenericFamily::Serif), Self::Monospace => FontFamilyName::Generic(GenericFamily::Monospace), Self::Named(name) => FontFamilyName::Named(name.as_str().into()), }; FontFamily::Single(name) } } #[derive(Clone, PartialEq)] pub struct TextAttrs { pub color: UiColor, pub font_size: f32, pub line_height: f32, pub family: Family, pub wrap: bool, pub align: RegionAlign, } pub const LINE_HEIGHT_MULT: f32 = 1.1; impl Default for TextAttrs { fn default() -> Self { let size = 16.0; Self { color: UiColor::WHITE, font_size: size, line_height: size * LINE_HEIGHT_MULT, family: Family::SansSerif, wrap: false, align: Align::CENTER_LEFT, } } } /// Keeps text and its corresponding layout from getting out of sync. pub struct TextBuffer { text: String, layout: Layout, layout_key: Option, /// The glyphs placed from `layout`, so drawing this text again at the /// width it already has places them once. placed: Option, } #[derive(PartialEq)] struct LayoutKey { attrs: TextAttrs, max_width: Option, } impl TextBuffer { pub fn new(text: impl Into) -> Self { Self { text: text.into(), layout: Layout::new(), layout_key: None, placed: 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.layout_key = None; self.placed = None; } } /// Invalidates the layout and returns the underlying string for editing. pub fn edit(&mut self) -> &mut String { self.layout_key = None; self.placed = None; &mut self.text } /// The glyphs of the shaping it is drawn as, once they are placed. pub fn rendered(&self) -> Option<&RenderedText> { self.placed.as_ref() } /// The width its shaping wraps at, and `None` where it does not wrap or /// has not been shaped. pub fn wrap_width(&self) -> Option { self.layout_key.as_ref()?.max_width } /// Widths covered by the current line breaks, including a wider shaping /// retained when a later draw requested a narrower box. pub fn width_holds(&self) -> crate::Holds { let Some(width) = self.wrap_width() else { return crate::Holds::ANY; }; let width = Px::from_f32(width); let soft_wrapped = self.layout.lines().any(|line| { matches!( line.break_reason(), parley::layout::BreakReason::Regular | parley::layout::BreakReason::Emergency ) }); let upper = if soft_wrapped { width } else { Px::MAX }; crate::Holds::from(Px::ceil_from_f32(self.layout.width()).min(width)..=upper) } 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) { let layout_key = LayoutKey { attrs: attrs.clone(), max_width: width, }; if self.layout_key.as_ref() == Some(&layout_key) { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::TextShapeHits); return; } // A greedy break at one width is the same break at every width down // to the longest line it produced: each line still fits, and none can // take a word that would not fit in the wider box. So the layout in // hand already answers, and re-breaking would only be work. // // At the longest line exactly, with no margin below it. A narrower // width really does break differently, so answering one from the // break in hand is how a warm tree keeps lines a cold tree would // never produce. The margin was here because a text reports the // width it used and a parent hands that back; the report is the step // at or above its longest line now, so what comes back fits. if let Some(key) = &self.layout_key && key.attrs == *attrs && let (Some(broke_at), Some(want)) = (key.max_width, width) && want <= broke_at && want >= self.layout.width() { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::TextShapeHits); return; } let same_shaping = self .layout_key .as_ref() .is_some_and(|key| key.attrs == *attrs); let old_key = self.layout_key.replace(layout_key); // The glyphs it holds are of the width it held, which the layout may // well come back to. if let Some(key) = old_key && let Some(glyphs) = self.placed.take() { data.keep_placed(Placed { text: self.text.clone(), key, glyphs, }); } // Only the line breaking depends on the width: the shaped runs under // it are a function of the text and the attrs, and parley re-breaks // them in place. So a new width is a break, not a shaping. if same_shaping { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::TextBreaks); #[cfg(feature = "layout-diagnostics")] let _break = diag::timer(TimerKind::TextBreak); self.break_lines(width); return; } #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::TextShapes); #[cfg(feature = "layout-diagnostics")] let _shape = diag::timer(TimerKind::TextShape); let mut builder = data .layout_ctx .ranged_builder(&mut data.font_ctx, &self.text, 1.0, true); builder.push_default(StyleProperty::FontFamily(attrs.family.family())); builder.push_default(StyleProperty::FontSize(attrs.font_size)); builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute( attrs.line_height, ))); builder.push_default(StyleProperty::Brush(attrs.color)); builder.build_into(&mut self.layout, &self.text); self.break_lines(width); } fn break_lines(&mut self, width: Option) { self.layout.break_all_lines(width); self.layout .align(Alignment::Start, AlignmentOptions::default()); } } impl TextData { pub fn place(&mut self, buffer: &TextBuffer) -> Vec { let mut placed = 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 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(); for glyph in run.positioned_glyphs() { let subpixel = ((glyph.x.fract() * 4.0).round() as i32).rem_euclid(4) as u8; let key = GlyphKey { font: font_id, glyph: glyph.id, size: glyph_size_key(font_size), subpixel, coords: coords_hash, }; let Some(entry) = self.glyph_entry(GlyphRaster { key, font: font_ref, font_size, coords, subpixel, glyph_id: glyph.id, }) else { continue; }; placed.push(PlacedGlyph { entry, offset: PxVec2::new( Px::from_int(glyph.x.floor() as i32 + entry.left), Px::from_int(glyph.y.floor() as i32 - entry.top), ), }); } } } placed } fn glyph_entry(&mut self, glyph: GlyphRaster<'_>) -> Option { if let Some(entry) = self.atlas.get(&glyph.key) { return entry; } let mut scaler = self .scale_ctx .builder(glyph.font) .size(glyph.font_size) .hint(true) .normalized_coords(glyph.coords) .build(); let image = Render::new(&[ Source::ColorOutline(0), Source::ColorBitmap(StrikeWith::BestFit), Source::Outline, ]) .format(Format::Alpha) .offset(Vector::new(glyph.subpixel as f32 / 4.0, 0.0)) .render(&mut scaler, glyph.glyph_id as u16); if let Some(image) = image { self.atlas.insert(glyph.key, &image) } else { self.atlas.insert_empty(glyph.key); None } } } struct GlyphRaster<'a> { key: GlyphKey, font: FontRef<'a>, font_size: f32, coords: &'a [i16], subpixel: u8, glyph_id: u32, } fn hash_coords(coords: &[i16]) -> u64 { let mut hasher = DefaultHasher::new(); coords.hash(&mut hasher); hasher.finish() } const GLYPH_SIZE_STEPS_PER_PIXEL: f32 = 16.0; fn glyph_size_key(font_size: f32) -> u32 { (font_size * GLYPH_SIZE_STEPS_PER_PIXEL).round() as u32 } pub struct RenderedText { pub glyphs: Vec, pub size: Vec2, pub color: UiColor, } impl TextData { /// The glyphs of this text at this width, taken out of what is kept. fn take_placed(&mut self, text: &str, key: &LayoutKey) -> Option { // From the newest, since a re-ask is usually of something recent. let at = self .spare .iter() .rposition(|spare| spare.key == *key && spare.text == text)?; self.spare.remove(at).map(|spare| spare.glyphs) } fn keep_placed(&mut self, placed: Placed) { if self.spare.len() >= SPARE_PLACED { self.spare.pop_front(); } self.spare.push_back(placed); } pub fn render<'b>( &mut self, buffer: &'b mut TextBuffer, attrs: &TextAttrs, width: Option, ) -> &'b RenderedText { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::TextRenders); #[cfg(feature = "layout-diagnostics")] let _render = diag::timer(TimerKind::TextRender); buffer.shape(self, attrs, width); // Only asked for when the buffer no longer holds them: taking one out // of the store to then drop it would throw an answer away. let placed = buffer.placed.take().or_else(|| { let key = buffer.layout_key.as_ref()?; self.take_placed(&buffer.text, key) }); let placed = match placed { Some(placed) => placed, None => { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::GlyphPlacements); #[cfg(feature = "layout-diagnostics")] let _place = diag::timer(TimerKind::GlyphPlacement); RenderedText { glyphs: self.place(buffer), size: buffer.size(), color: attrs.color, } } }; buffer.placed.insert(placed) } }