632 lines
21 KiB
Rust
632 lines
21 KiB
Rust
use crate::{Align, GlyphAtlas, GlyphKey, PaintId, PlacedGlyph, RegionAlign, Textures, util::Vec2};
|
|
use parley::{
|
|
Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily,
|
|
Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
|
fontique::{Blob, FontInfoOverride},
|
|
};
|
|
use std::{collections::HashMap, fmt, ops::Range, sync::Arc};
|
|
use swash::{
|
|
FontRef,
|
|
scale::{Render, ScaleContext, Source, StrikeWith},
|
|
zeno::{Format, Vector},
|
|
};
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct FontDiagnostics {
|
|
pub families_found: usize,
|
|
pub default_family: Option<String>,
|
|
pub default_mono_family: Option<String>,
|
|
pub regular_resolved: Option<String>,
|
|
pub bold_resolved: Option<String>,
|
|
pub italic_resolved: Option<String>,
|
|
pub mono_resolved: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum FontRegistrationError {
|
|
AlreadyRegistered(String),
|
|
TextAlreadyShaped,
|
|
InvalidFont(String),
|
|
}
|
|
|
|
impl fmt::Display for FontRegistrationError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::AlreadyRegistered(family) => {
|
|
write!(f, "font data is already registered for {family:?}")
|
|
}
|
|
Self::TextAlreadyShaped => write!(
|
|
f,
|
|
"cannot register font data after text has been shaped; register application fonts before the first draw"
|
|
),
|
|
Self::InvalidFont(family) => {
|
|
write!(
|
|
f,
|
|
"font data registered for {family:?} contains no usable fonts"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for FontRegistrationError {}
|
|
|
|
pub struct TextData {
|
|
pub font_cx: FontContext,
|
|
pub layout_cx: LayoutContext<PaintId>,
|
|
scale_cx: ScaleContext,
|
|
pub atlas: GlyphAtlas,
|
|
/// Physical pixels per dp -- a second copy of
|
|
/// `UiRenderState::density`, kept here too because `TextEditCtx::layout`
|
|
/// (cursor movement and hit-testing, `widget/text/edit.rs`) shapes text
|
|
/// from an event callback that has a `TextData` but no `Painter`, so it
|
|
/// has nowhere else to read the display's density from. Both copies are
|
|
/// set together, from the one place either backend learns the real
|
|
/// value (`android::view::new_peer`); this is the same accepted
|
|
/// duplication as `AndroidRenderer::content_scale`; a single source of
|
|
/// truth would mean carrying a `Painter` (or output size) into every
|
|
/// input handler for the sake of one field.
|
|
pub density: f32,
|
|
registered_families: HashMap<String, String>,
|
|
next_registered_family: u64,
|
|
shaping_started: bool,
|
|
}
|
|
|
|
impl Default for TextData {
|
|
fn default() -> Self {
|
|
let mut font_cx = FontContext::new();
|
|
patch_android_monospace(&mut font_cx);
|
|
Self {
|
|
font_cx,
|
|
layout_cx: LayoutContext::new(),
|
|
scale_cx: ScaleContext::new(),
|
|
atlas: GlyphAtlas::default(),
|
|
density: 1.0,
|
|
registered_families: HashMap::new(),
|
|
next_registered_family: 0,
|
|
shaping_started: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// So this reads `fonts.xml` itself (already on-device, already the
|
|
/// authority Compose's own `Typeface.MONOSPACE` resolves through) for the
|
|
/// filename that declaration names, then finds which of fontique's
|
|
/// *actually* scanned families (from `/system/fonts`, which do carry real
|
|
/// font data, just under whatever name the font's own metadata gives it --
|
|
/// "Droid Sans Mono" here, but that name is never hardcoded) owns a font
|
|
/// file with that name, and registers that family as the `Monospace`
|
|
/// generic the way the backend itself would have if its parser had reified
|
|
/// the declaration. A no-op if the family is somehow already resolved
|
|
/// (future fontique) or nothing matches (no `fonts.xml`, e.g. a headless
|
|
/// test, or a device that names it some other way).
|
|
#[cfg(target_os = "android")]
|
|
fn patch_android_monospace(font_cx: &mut FontContext) {
|
|
use parley::fontique::SourceKind;
|
|
|
|
let already_resolved = font_cx
|
|
.collection
|
|
.generic_families(GenericFamily::Monospace)
|
|
.next()
|
|
.is_some();
|
|
if already_resolved {
|
|
return;
|
|
}
|
|
let Some(target_file) = android_monospace_font_filename() else {
|
|
return;
|
|
};
|
|
let names: Vec<String> = font_cx
|
|
.collection
|
|
.family_names()
|
|
.map(str::to_string)
|
|
.collect();
|
|
for name in names {
|
|
let Some(id) = font_cx.collection.family_id(&name) else {
|
|
continue;
|
|
};
|
|
let Some(info) = font_cx.collection.family(id) else {
|
|
continue;
|
|
};
|
|
let Some(font) = info.default_font() else {
|
|
continue;
|
|
};
|
|
let SourceKind::Path(path) = font.source().kind() else {
|
|
continue;
|
|
};
|
|
if path.file_name().and_then(|f| f.to_str()) == Some(target_file.as_str()) {
|
|
font_cx
|
|
.collection
|
|
.append_generic_families(GenericFamily::Monospace, std::iter::once(id));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
fn android_monospace_font_filename() -> Option<String> {
|
|
let android_root = std::env::var("ANDROID_ROOT").unwrap_or_else(|_| "/system".to_string());
|
|
let xml =
|
|
std::fs::read_to_string(std::path::Path::new(&android_root).join("etc/fonts.xml")).ok()?;
|
|
let family_start = xml.find("<family name=\"monospace\">")?;
|
|
let block = &xml[family_start..];
|
|
let block = &block[..block.find("</family>")?];
|
|
let font_tag = block.find("<font")?;
|
|
let after_tag = &block[font_tag..];
|
|
let content_start = after_tag.find('>')? + 1;
|
|
let content = &after_tag[content_start..];
|
|
let filename = content[..content.find('<')?].trim();
|
|
(!filename.is_empty()).then(|| filename.to_string())
|
|
}
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
fn patch_android_monospace(_font_cx: &mut FontContext) {}
|
|
|
|
impl TextData {
|
|
pub(crate) fn register_font(
|
|
&mut self,
|
|
family: impl AsRef<str>,
|
|
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
|
) -> Result<(), FontRegistrationError> {
|
|
let family = family.as_ref().to_owned();
|
|
if self.shaping_started {
|
|
return Err(FontRegistrationError::TextAlreadyShaped);
|
|
}
|
|
if self.registered_families.contains_key(&family) {
|
|
return Err(FontRegistrationError::AlreadyRegistered(family));
|
|
}
|
|
|
|
// Parley selects fonts by family rather than by a face handle. Give
|
|
// application data a private family name so selecting it cannot find
|
|
// a system font that happens to carry the same embedded metadata.
|
|
let private_name = format!("__iris_registered_font_{}__", self.next_registered_family);
|
|
let fonts = self.font_cx.collection.register_fonts(
|
|
Blob::new(Arc::new(data)),
|
|
Some(FontInfoOverride {
|
|
family_name: Some(&private_name),
|
|
..Default::default()
|
|
}),
|
|
);
|
|
if fonts.is_empty() {
|
|
return Err(FontRegistrationError::InvalidFont(family));
|
|
}
|
|
self.next_registered_family += 1;
|
|
self.registered_families.insert(family, private_name);
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn is_font_registered(&self, family: impl AsRef<str>) -> bool {
|
|
self.registered_families.contains_key(family.as_ref())
|
|
}
|
|
|
|
/// Cloned rather than borrowed because the caller needs it while the
|
|
/// layout builder holds `&mut self` -- a `String` per shaped registered
|
|
/// run, paid only when the layout is rebuilt.
|
|
pub fn resolve_family(&self, family: &str) -> String {
|
|
if let Some(name) = self.registered_families.get(family) {
|
|
return name.clone();
|
|
}
|
|
family.to_owned()
|
|
}
|
|
|
|
pub fn font_diagnostics(&mut self) -> FontDiagnostics {
|
|
use parley::fontique::{Attributes, FontWidth, QueryStatus};
|
|
let families_found = self.font_cx.collection.family_names().count();
|
|
let default_family_id = self
|
|
.font_cx
|
|
.collection
|
|
.generic_families(GenericFamily::SansSerif)
|
|
.next();
|
|
let default_family = default_family_id
|
|
.and_then(|id| self.font_cx.collection.family_name(id).map(str::to_string));
|
|
let default_mono_family_id = self
|
|
.font_cx
|
|
.collection
|
|
.generic_families(GenericFamily::Monospace)
|
|
.next();
|
|
let default_mono_family = default_mono_family_id
|
|
.and_then(|id| self.font_cx.collection.family_name(id).map(str::to_string));
|
|
|
|
let mut resolve_family =
|
|
|generic: GenericFamily, weight: FontWeight, style: FontStyle| -> Option<String> {
|
|
let mut family_id = None;
|
|
{
|
|
let mut query = self
|
|
.font_cx
|
|
.collection
|
|
.query(&mut self.font_cx.source_cache);
|
|
query.set_families([generic]);
|
|
query.set_attributes(Attributes {
|
|
width: FontWidth::NORMAL,
|
|
style,
|
|
weight,
|
|
});
|
|
query.matches_with(|font| {
|
|
family_id = Some(font.family.0);
|
|
QueryStatus::Stop
|
|
});
|
|
}
|
|
family_id.and_then(|id| self.font_cx.collection.family_name(id).map(str::to_string))
|
|
};
|
|
|
|
let regular_resolved = resolve_family(
|
|
GenericFamily::SansSerif,
|
|
FontWeight::NORMAL,
|
|
FontStyle::Normal,
|
|
);
|
|
let bold_resolved = resolve_family(
|
|
GenericFamily::SansSerif,
|
|
FontWeight::BOLD,
|
|
FontStyle::Normal,
|
|
);
|
|
let italic_resolved = resolve_family(
|
|
GenericFamily::SansSerif,
|
|
FontWeight::NORMAL,
|
|
FontStyle::Italic,
|
|
);
|
|
let mono_resolved = resolve_family(
|
|
GenericFamily::Monospace,
|
|
FontWeight::NORMAL,
|
|
FontStyle::Normal,
|
|
);
|
|
|
|
FontDiagnostics {
|
|
families_found,
|
|
default_family,
|
|
default_mono_family,
|
|
regular_resolved,
|
|
bold_resolved,
|
|
italic_resolved,
|
|
mono_resolved,
|
|
}
|
|
}
|
|
|
|
pub fn place(
|
|
&mut self,
|
|
buffer: &TextBuffer,
|
|
textures: &mut Textures,
|
|
) -> (Vec<PlacedGlyph>, Vec<PaintId>) {
|
|
let mut placed = Vec::new();
|
|
let mut paints = 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 run_color = run.style().brush.clone();
|
|
if !paints.contains(&run_color) {
|
|
paints.push(run_color.clone());
|
|
}
|
|
let Some(font_ref) = FontRef::from_index(font.data.as_ref(), font.index as usize)
|
|
else {
|
|
continue;
|
|
};
|
|
let coords_hash = hash_coords(coords);
|
|
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,
|
|
),
|
|
paint: run_color.slot(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
(placed, paints)
|
|
}
|
|
|
|
pub fn render(
|
|
&mut self,
|
|
buffer: &mut TextBuffer,
|
|
attrs: &TextAttrs,
|
|
width: Option<f32>,
|
|
textures: &mut Textures,
|
|
density: f32,
|
|
) -> RenderedText {
|
|
buffer.shape(self, attrs, width, density);
|
|
let (glyphs, paints) = self.place(buffer, textures);
|
|
RenderedText {
|
|
glyphs: std::sync::Arc::new(glyphs),
|
|
paints: std::sync::Arc::new(paints),
|
|
size: buffer.size(),
|
|
color: attrs.color.clone(),
|
|
generation: self.atlas.generation(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub const SANS_SERIF: &str = "sans-serif";
|
|
pub const SERIF: &str = "serif";
|
|
pub const MONOSPACE: &str = "monospace";
|
|
|
|
#[derive(Clone, PartialEq)]
|
|
pub struct SpanStyle {
|
|
pub range: Range<usize>,
|
|
pub color: Option<PaintId>,
|
|
pub family: Option<String>,
|
|
pub font_size: Option<f32>,
|
|
pub bold: bool,
|
|
pub italic: bool,
|
|
pub underline: bool,
|
|
}
|
|
|
|
impl SpanStyle {
|
|
pub fn new(range: Range<usize>) -> Self {
|
|
Self {
|
|
range,
|
|
color: None,
|
|
family: None,
|
|
font_size: None,
|
|
bold: false,
|
|
italic: false,
|
|
underline: false,
|
|
}
|
|
}
|
|
pub fn color(mut self, color: PaintId) -> Self {
|
|
self.color = Some(color);
|
|
self
|
|
}
|
|
pub fn family(mut self, family: impl AsRef<str>) -> Self {
|
|
self.family = Some(family.as_ref().to_owned());
|
|
self
|
|
}
|
|
pub fn font_size(mut self, size: f32) -> Self {
|
|
self.font_size = Some(size);
|
|
self
|
|
}
|
|
pub fn bold(mut self) -> Self {
|
|
self.bold = true;
|
|
self
|
|
}
|
|
pub fn italic(mut self) -> Self {
|
|
self.italic = true;
|
|
self
|
|
}
|
|
pub fn underline(mut self) -> Self {
|
|
self.underline = true;
|
|
self
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, PartialEq)]
|
|
pub struct TextAttrs {
|
|
pub color: PaintId,
|
|
pub font_size: f32,
|
|
pub line_height: f32,
|
|
pub family: String,
|
|
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: PaintId::WHITE,
|
|
font_size: size,
|
|
line_height: size * LINE_HEIGHT_MULT,
|
|
family: SANS_SERIF.to_owned(),
|
|
wrap: false,
|
|
align: Align::CENTER_LEFT,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The text and the layout live in one place because parley's `Layout` borrows
|
|
/// nothing but is only meaningful against the string it was built from: keeping
|
|
/// them apart is how they get out of step.
|
|
pub struct TextBuffer {
|
|
text: String,
|
|
layout: Layout<PaintId>,
|
|
spans: Vec<SpanStyle>,
|
|
shaped: Option<(TextAttrs, Option<f32>, f32)>,
|
|
}
|
|
|
|
impl TextBuffer {
|
|
pub fn new(text: impl Into<String>) -> Self {
|
|
Self {
|
|
text: text.into(),
|
|
layout: Layout::new(),
|
|
spans: Vec::new(),
|
|
shaped: None,
|
|
}
|
|
}
|
|
|
|
pub fn set_spans(&mut self, spans: Vec<SpanStyle>) {
|
|
self.spans = spans;
|
|
self.shaped = None;
|
|
}
|
|
|
|
pub fn new_empty() -> Self {
|
|
Self::new("")
|
|
}
|
|
|
|
pub fn text(&self) -> &str {
|
|
&self.text
|
|
}
|
|
|
|
pub fn layout(&self) -> &Layout<PaintId> {
|
|
&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;
|
|
}
|
|
}
|
|
|
|
/// Edit the string in place; invalidates the layout unconditionally, since
|
|
/// the caller is assumed to have changed something.
|
|
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>,
|
|
density: f32,
|
|
) {
|
|
data.shaping_started = true;
|
|
if self.shaped.as_ref() == Some(&(attrs.clone(), width, density)) {
|
|
return;
|
|
}
|
|
let base_family = data.resolve_family(&attrs.family);
|
|
let span_families: Vec<Option<String>> = self
|
|
.spans
|
|
.iter()
|
|
.map(|span| span.family.as_ref().map(|f| data.resolve_family(f)))
|
|
.collect();
|
|
let mut builder = data
|
|
.layout_cx
|
|
.ranged_builder(&mut data.font_cx, &self.text, 1.0, true);
|
|
builder.push_default(StyleProperty::FontFamily(FontFamily::from(
|
|
base_family.as_str(),
|
|
)));
|
|
builder.push_default(StyleProperty::FontSize(attrs.font_size * density));
|
|
builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute(
|
|
attrs.line_height * density,
|
|
)));
|
|
builder.push_default(StyleProperty::Brush(attrs.color.clone()));
|
|
for (span, family) in self.spans.iter().zip(&span_families) {
|
|
let range = span.range.clone();
|
|
if let Some(color) = &span.color {
|
|
builder.push(StyleProperty::Brush(color.clone()), range.clone());
|
|
}
|
|
if let Some(family) = family {
|
|
builder.push(
|
|
StyleProperty::FontFamily(FontFamily::from(family.as_str())),
|
|
range.clone(),
|
|
);
|
|
}
|
|
if let Some(size) = span.font_size {
|
|
builder.push(StyleProperty::FontSize(size * density), range.clone());
|
|
}
|
|
if span.bold {
|
|
builder.push(StyleProperty::FontWeight(FontWeight::BOLD), range.clone());
|
|
}
|
|
if span.italic {
|
|
builder.push(StyleProperty::FontStyle(FontStyle::Italic), range.clone());
|
|
}
|
|
if span.underline {
|
|
builder.push(StyleProperty::Underline(true), range.clone());
|
|
}
|
|
}
|
|
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, density));
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
/// Cheap to clone and to keep, which is the point -- a widget holds one across
|
|
/// frames and re-emits its quads without going near the rasteriser. `color`
|
|
/// is the buffer's *base* colour (`TextAttrs::color`) for a caller that wants
|
|
/// it as a whole (e.g. tinting a cursor to match); the colour each glyph is
|
|
/// actually drawn in is `PlacedGlyph::color`, which a `SpanStyle` can
|
|
/// override per range.
|
|
#[derive(Clone)]
|
|
pub struct RenderedText {
|
|
pub glyphs: std::sync::Arc<Vec<PlacedGlyph>>,
|
|
/// The unique handles whose compact slots the glyphs above carry.
|
|
pub paints: std::sync::Arc<Vec<PaintId>>,
|
|
pub size: Vec2,
|
|
pub color: PaintId,
|
|
/// The [`GlyphAtlas::generation`] the glyphs above were placed against.
|
|
/// A holder must re-render rather than re-emit these quads once the
|
|
/// atlas has moved on (`GlyphAtlas::clear`'s doc says what happens
|
|
/// otherwise); `Painter::glyphs` debug-asserts it.
|
|
pub generation: u64,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn invalid_font_data_is_reported() {
|
|
let mut data = TextData::default();
|
|
assert_eq!(
|
|
data.register_font("icons", b"not a font" as &'static [u8]),
|
|
Err(FontRegistrationError::InvalidFont("icons".to_owned()))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn registering_after_shaping_is_reported() {
|
|
let mut data = TextData::default();
|
|
let mut buffer = TextBuffer::new("ordinary platform text");
|
|
buffer.shape(&mut data, &TextAttrs::default(), None, 1.0);
|
|
assert_eq!(
|
|
data.register_font("icons", b"not a font" as &'static [u8]),
|
|
Err(FontRegistrationError::TextAlreadyShaped)
|
|
);
|
|
}
|
|
}
|