251 lines
7.7 KiB
Rust
251 lines
7.7 KiB
Rust
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<UiColor>,
|
|
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<UiColor>,
|
|
shaped: Option<(TextAttrs, Option<f32>)>,
|
|
}
|
|
|
|
impl TextBuffer {
|
|
pub fn new(text: impl Into<String>) -> 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<UiColor> {
|
|
&self.layout
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.text.is_empty()
|
|
}
|
|
|
|
pub fn set_text(&mut self, text: impl Into<String>) {
|
|
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<f32>) {
|
|
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<PlacedGlyph> {
|
|
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<Vec<PlacedGlyph>>,
|
|
pub size: Vec2,
|
|
pub color: UiColor,
|
|
}
|
|
|
|
impl TextData {
|
|
pub fn render(
|
|
&mut self,
|
|
buffer: &mut TextBuffer,
|
|
attrs: &TextAttrs,
|
|
width: Option<f32>,
|
|
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,
|
|
}
|
|
}
|
|
}
|