diff --git a/core/src/primitive/text.rs b/core/src/primitive/text.rs index 91076da..0770121 100644 --- a/core/src/primitive/text.rs +++ b/core/src/primitive/text.rs @@ -1,8 +1,12 @@ -use crate::{Align, GlyphAtlas, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor, util::Vec2}; +use crate::{ + Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor, + util::Vec2, +}; use parley::{ Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, }; +use std::hash::{DefaultHasher, Hash, Hasher}; use swash::{ FontRef, scale::{Render, ScaleContext, Source, StrikeWith}, @@ -10,18 +14,18 @@ use swash::{ }; pub struct TextData { - pub font_cx: FontContext, - pub layout_cx: LayoutContext, - scale_cx: ScaleContext, + pub font_ctx: FontContext, + pub layout_ctx: LayoutContext, + scale_ctx: ScaleContext, pub atlas: GlyphAtlas, } impl Default for TextData { fn default() -> Self { Self { - font_cx: FontContext::new(), - layout_cx: LayoutContext::new(), - scale_cx: ScaleContext::new(), + font_ctx: FontContext::new(), + layout_ctx: LayoutContext::new(), + scale_ctx: ScaleContext::new(), atlas: GlyphAtlas::default(), } } @@ -77,7 +81,13 @@ impl Default for TextAttrs { pub struct TextBuffer { text: String, layout: Layout, - shaped: Option<(TextAttrs, Option)>, + layout_key: Option, +} + +#[derive(PartialEq)] +struct LayoutKey { + attrs: TextAttrs, + max_width: Option, } impl TextBuffer { @@ -85,7 +95,7 @@ impl TextBuffer { Self { text: text.into(), layout: Layout::new(), - shaped: None, + layout_key: None, } } @@ -109,13 +119,13 @@ impl TextBuffer { let text = text.into(); if text != self.text { self.text = text; - self.shaped = None; + self.layout_key = None; } } /// Invalidates the layout and returns the underlying string for editing. pub fn edit(&mut self) -> &mut String { - self.shaped = None; + self.layout_key = None; &mut self.text } @@ -124,12 +134,16 @@ impl TextBuffer { } pub fn shape(&mut self, data: &mut TextData, attrs: &TextAttrs, width: Option) { - if self.shaped.as_ref() == Some(&(attrs.clone(), width)) { + let layout_key = LayoutKey { + attrs: attrs.clone(), + max_width: width, + }; + if self.layout_key.as_ref() == Some(&layout_key) { return; } let mut builder = data - .layout_cx - .ranged_builder(&mut data.font_cx, &self.text, 1.0, true); + .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( @@ -140,7 +154,7 @@ impl TextBuffer { self.layout.break_all_lines(width); self.layout .align(Alignment::Start, AlignmentOptions::default()); - self.shaped = Some((attrs.clone(), width)); + self.layout_key = Some(layout_key); } } @@ -160,8 +174,6 @@ impl TextData { continue; }; let coords_hash = hash_coords(coords); - // `font.data.id()` rather than the pointer, so the same font - // loaded twice is still one set of entries. let font_id = font.data.id(); for glyph in run.positioned_glyphs() { @@ -169,37 +181,21 @@ impl TextData { let key = GlyphKey { font: font_id, glyph: glyph.id, - size: (font_size * 16.0).round() as u32, + size: glyph_size_key(font_size), subpixel, coords: coords_hash, }; - let entry = match self.atlas.get(&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(key, &image, textures), - None => { - self.atlas.insert_empty(key); - None - } - } - } - }; + let entry = self.glyph_entry( + GlyphRaster { + key, + font: font_ref, + font_size, + coords, + subpixel, + glyph_id: glyph.id, + }, + textures, + ); let Some(entry) = entry else { continue }; placed.push(PlacedGlyph { entry, @@ -213,15 +209,60 @@ impl TextData { } placed } + + fn glyph_entry( + &mut self, + glyph: GlyphRaster<'_>, + textures: &mut Textures, + ) -> 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, textures) + } 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 h: u64 = 0xcbf2_9ce4_8422_2325; - for c in coords { - h ^= *c as u16 as u64; - h = h.wrapping_mul(0x1000_0000_01b3); - } - h + 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 } #[derive(Clone)]