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::{
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<UiColor>,
scale_cx: ScaleContext,
pub font_ctx: FontContext,
pub layout_ctx: LayoutContext<UiColor>,
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<UiColor>,
shaped: Option<(TextAttrs, Option<f32>)>,
layout_key: Option<LayoutKey>,
}
#[derive(PartialEq)]
struct LayoutKey {
attrs: TextAttrs,
max_width: Option<f32>,
}
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<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;
}
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<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 {
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)]