Clarify text layout and rasterization state

This commit is contained in:
iris committed 2026-09-13 02:42:27 -04:00
1 parent 2d15195a58
commit 71e46c125a
1 file changed
+92 -51
+92 -51
View File
@@ -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::{ use parley::{
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout, Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
}; };
use std::hash::{DefaultHasher, Hash, Hasher};
use swash::{ use swash::{
FontRef, FontRef,
scale::{Render, ScaleContext, Source, StrikeWith}, scale::{Render, ScaleContext, Source, StrikeWith},
@@ -10,18 +14,18 @@ use swash::{
}; };
pub struct TextData { pub struct TextData {
pub font_cx: FontContext, pub font_ctx: FontContext,
pub layout_cx: LayoutContext<UiColor>, pub layout_ctx: LayoutContext<UiColor>,
scale_cx: ScaleContext, scale_ctx: ScaleContext,
pub atlas: GlyphAtlas, pub atlas: GlyphAtlas,
} }
impl Default for TextData { impl Default for TextData {
fn default() -> Self { fn default() -> Self {
Self { Self {
font_cx: FontContext::new(), font_ctx: FontContext::new(),
layout_cx: LayoutContext::new(), layout_ctx: LayoutContext::new(),
scale_cx: ScaleContext::new(), scale_ctx: ScaleContext::new(),
atlas: GlyphAtlas::default(), atlas: GlyphAtlas::default(),
} }
} }
@@ -77,7 +81,13 @@ impl Default for TextAttrs {
pub struct TextBuffer { pub struct TextBuffer {
text: String, text: String,
layout: Layout<UiColor>, layout: Layout<UiColor>,
shaped: Option<(TextAttrs, Option<f32>)>, layout_key: Option<LayoutKey>,
}
#[derive(PartialEq)]
struct LayoutKey {
attrs: TextAttrs,
max_width: Option<f32>,
} }
impl TextBuffer { impl TextBuffer {
@@ -85,7 +95,7 @@ impl TextBuffer {
Self { Self {
text: text.into(), text: text.into(),
layout: Layout::new(), layout: Layout::new(),
shaped: None, layout_key: None,
} }
} }
@@ -109,13 +119,13 @@ impl TextBuffer {
let text = text.into(); let text = text.into();
if text != self.text { if text != self.text {
self.text = text; self.text = text;
self.shaped = None; self.layout_key = None;
} }
} }
/// Invalidates the layout and returns the underlying string for editing. /// Invalidates the layout and returns the underlying string for editing.
pub fn edit(&mut self) -> &mut String { pub fn edit(&mut self) -> &mut String {
self.shaped = None; self.layout_key = None;
&mut self.text &mut self.text
} }
@@ -124,12 +134,16 @@ impl TextBuffer {
} }
pub fn shape(&mut self, data: &mut TextData, attrs: &TextAttrs, width: Option<f32>) { pub fn shape(&mut self, data: &mut TextData, attrs: &TextAttrs, width: Option<f32>) {
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; return;
} }
let mut builder = data let mut builder = data
.layout_cx .layout_ctx
.ranged_builder(&mut data.font_cx, &self.text, 1.0, true); .ranged_builder(&mut data.font_ctx, &self.text, 1.0, true);
builder.push_default(StyleProperty::FontFamily(attrs.family.family())); builder.push_default(StyleProperty::FontFamily(attrs.family.family()));
builder.push_default(StyleProperty::FontSize(attrs.font_size)); builder.push_default(StyleProperty::FontSize(attrs.font_size));
builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute( builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute(
@@ -140,7 +154,7 @@ impl TextBuffer {
self.layout.break_all_lines(width); self.layout.break_all_lines(width);
self.layout self.layout
.align(Alignment::Start, AlignmentOptions::default()); .align(Alignment::Start, AlignmentOptions::default());
self.shaped = Some((attrs.clone(), width)); self.layout_key = Some(layout_key);
} }
} }
@@ -160,8 +174,6 @@ impl TextData {
continue; continue;
}; };
let coords_hash = hash_coords(coords); 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(); let font_id = font.data.id();
for glyph in run.positioned_glyphs() { for glyph in run.positioned_glyphs() {
@@ -169,37 +181,21 @@ impl TextData {
let key = GlyphKey { let key = GlyphKey {
font: font_id, font: font_id,
glyph: glyph.id, glyph: glyph.id,
size: (font_size * 16.0).round() as u32, size: glyph_size_key(font_size),
subpixel, subpixel,
coords: coords_hash, coords: coords_hash,
}; };
let entry = match self.atlas.get(&key) { let entry = self.glyph_entry(
Some(entry) => entry, GlyphRaster {
None => { key,
let mut scaler = self font: font_ref,
.scale_cx font_size,
.builder(font_ref) coords,
.size(font_size) subpixel,
.hint(true) glyph_id: glyph.id,
.normalized_coords(coords) },
.build(); textures,
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 Some(entry) = entry else { continue }; let Some(entry) = entry else { continue };
placed.push(PlacedGlyph { placed.push(PlacedGlyph {
entry, entry,
@@ -213,15 +209,60 @@ impl TextData {
} }
placed placed
} }
fn glyph_entry(
&mut self,
glyph: GlyphRaster<'_>,
textures: &mut Textures,
) -> Option<GlyphEntry> {
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 { fn hash_coords(coords: &[i16]) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325; let mut hasher = DefaultHasher::new();
for c in coords { coords.hash(&mut hasher);
h ^= *c as u16 as u64; hasher.finish()
h = h.wrapping_mul(0x1000_0000_01b3); }
}
h 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)] #[derive(Clone)]