use crate::{Align, GlyphAtlas, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor, util::Vec2}; use parley::{ Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, }; use swash::{ FontRef, scale::{Render, ScaleContext, Source, StrikeWith}, zeno::{Format, Vector}, }; pub struct TextData { pub font_cx: FontContext, pub layout_cx: LayoutContext, scale_cx: ScaleContext, pub atlas: GlyphAtlas, } impl Default for TextData { fn default() -> Self { Self { font_cx: FontContext::new(), layout_cx: LayoutContext::new(), scale_cx: ScaleContext::new(), atlas: GlyphAtlas::default(), } } } #[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, shaped: Option<(TextAttrs, Option)>, } impl TextBuffer { pub fn new(text: impl Into) -> Self { Self { text: text.into(), layout: Layout::new(), 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; } } /// Invalidates the layout and returns the underlying string for editing. 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) { if self.shaped.as_ref() == Some(&(attrs.clone(), width)) { return; } let mut builder = data .layout_cx .ranged_builder(&mut data.font_cx, &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.layout.break_all_lines(width); self.layout .align(Alignment::Start, AlignmentOptions::default()); self.shaped = Some((attrs.clone(), width)); } } impl TextData { pub fn place(&mut self, buffer: &TextBuffer, textures: &mut Textures) -> 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); // `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() { 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: (font_size * 16.0).round() as u32, 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 Some(entry) = entry else { continue }; placed.push(PlacedGlyph { entry, offset: Vec2::new( glyph.x.floor() + entry.left as f32, glyph.y.floor() - entry.top as f32, ), }); } } } placed } } 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 } #[derive(Clone)] pub struct RenderedText { pub glyphs: std::sync::Arc>, pub size: Vec2, pub color: UiColor, } impl TextData { pub fn render( &mut self, buffer: &mut TextBuffer, attrs: &TextAttrs, width: Option, textures: &mut Textures, ) -> RenderedText { buffer.shape(self, attrs, width); let glyphs = self.place(buffer, textures); RenderedText { glyphs: std::sync::Arc::new(glyphs), size: buffer.size(), color: attrs.color, } } }