1136 lines
36 KiB
Rust
1136 lines
36 KiB
Rust
use crate::{
|
|
Align, GlyphAtlas, GlyphKey, Len, PaintId, PlacedGlyph, RegionAlign, Textures, WidgetId,
|
|
util::{Resources, RscHandle, StrongRscId, Vec2, WeakRscId},
|
|
};
|
|
use parley::{
|
|
Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily,
|
|
Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
|
fontique::{Blob, Collection, CollectionOptions, FamilyId, FontInfoOverride, FontWidth},
|
|
};
|
|
use std::{
|
|
cell::{Ref, RefCell, RefMut},
|
|
collections::HashMap,
|
|
fmt,
|
|
ops::{Deref, DerefMut, Range},
|
|
rc::Rc,
|
|
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),
|
|
NotRegistered(String),
|
|
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::NotRegistered(family) => {
|
|
write!(f, "no font data is registered for {family:?}")
|
|
}
|
|
Self::InvalidFont(family) => {
|
|
write!(
|
|
f,
|
|
"font data registered for {family:?} contains no usable fonts"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for FontRegistrationError {}
|
|
|
|
/// The bucket used by [`crate::Ui::register_font`] when none is specified.
|
|
pub const DEFAULT_GLYPH_BUCKET: &str = "default";
|
|
|
|
struct RegisteredFace {
|
|
family: FamilyId,
|
|
width: FontWidth,
|
|
style: FontStyle,
|
|
weight: FontWeight,
|
|
}
|
|
|
|
struct RegisteredFont {
|
|
private_name: String,
|
|
bucket: u64,
|
|
font_id: u64,
|
|
faces: Vec<RegisteredFace>,
|
|
}
|
|
|
|
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 cursor movement and
|
|
/// hit-testing shape text from event callbacks that have no `Painter`,
|
|
/// so they have 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, RegisteredFont>,
|
|
glyph_buckets: HashMap<String, u64>,
|
|
font_buckets: HashMap<u64, u64>,
|
|
next_registered_family: u64,
|
|
next_glyph_bucket: u64,
|
|
}
|
|
|
|
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(),
|
|
glyph_buckets: HashMap::new(),
|
|
font_buckets: HashMap::new(),
|
|
next_registered_family: 0,
|
|
next_glyph_bucket: 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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> {
|
|
self.register_font_in(family, DEFAULT_GLYPH_BUCKET, data)
|
|
}
|
|
|
|
pub(crate) fn register_font_in(
|
|
&mut self,
|
|
family: impl AsRef<str>,
|
|
bucket: impl AsRef<str>,
|
|
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
|
) -> Result<(), FontRegistrationError> {
|
|
let family = family.as_ref().to_owned();
|
|
if self.registered_families.contains_key(&family) {
|
|
return Err(FontRegistrationError::AlreadyRegistered(family));
|
|
}
|
|
|
|
let private_name = format!("__iris_registered_font_{}__", self.next_registered_family);
|
|
let blob = Blob::new(Arc::new(data));
|
|
Self::validate_font(&family, blob.clone())?;
|
|
let bucket = self.glyph_bucket(bucket.as_ref());
|
|
let registered = self.add_font(private_name, bucket, blob);
|
|
self.next_registered_family += 1;
|
|
self.font_buckets.insert(registered.font_id, bucket);
|
|
self.registered_families.insert(family, registered);
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn replace_font(
|
|
&mut self,
|
|
family: impl AsRef<str>,
|
|
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
|
) -> Result<(), FontRegistrationError> {
|
|
let family = family.as_ref();
|
|
let Some(bucket) = self.registered_families.get(family).map(|font| font.bucket) else {
|
|
return Err(FontRegistrationError::NotRegistered(family.to_owned()));
|
|
};
|
|
self.replace_font_with(family, bucket, data)
|
|
}
|
|
|
|
pub(crate) fn replace_font_in(
|
|
&mut self,
|
|
family: impl AsRef<str>,
|
|
bucket: impl AsRef<str>,
|
|
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
|
) -> Result<(), FontRegistrationError> {
|
|
let family = family.as_ref();
|
|
if !self.registered_families.contains_key(family) {
|
|
return Err(FontRegistrationError::NotRegistered(family.to_owned()));
|
|
}
|
|
let bucket = self.glyph_bucket(bucket.as_ref());
|
|
self.replace_font_with(family, bucket, data)
|
|
}
|
|
|
|
fn replace_font_with(
|
|
&mut self,
|
|
family: &str,
|
|
bucket: u64,
|
|
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
|
) -> Result<(), FontRegistrationError> {
|
|
let blob = Blob::new(Arc::new(data));
|
|
Self::validate_font(family, blob.clone())?;
|
|
let old = self.registered_families.remove(family).unwrap();
|
|
for face in old.faces {
|
|
self.font_cx.collection.unregister_font(
|
|
face.family,
|
|
face.width,
|
|
face.style,
|
|
face.weight,
|
|
);
|
|
}
|
|
self.font_buckets.remove(&old.font_id);
|
|
self.atlas.clear_bucket(old.bucket);
|
|
|
|
let registered = self.add_font(old.private_name, bucket, blob);
|
|
self.font_buckets.insert(registered.font_id, bucket);
|
|
self.registered_families
|
|
.insert(family.to_owned(), registered);
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_font(family: &str, blob: Blob<u8>) -> Result<(), FontRegistrationError> {
|
|
let mut collection = Collection::new(CollectionOptions {
|
|
shared: false,
|
|
system_fonts: false,
|
|
});
|
|
let fonts = collection.register_fonts(blob, None);
|
|
if fonts.is_empty() {
|
|
Err(FontRegistrationError::InvalidFont(family.to_owned()))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn add_font(&mut self, private_name: String, bucket: u64, blob: Blob<u8>) -> RegisteredFont {
|
|
let font_id = blob.id();
|
|
let fonts = self.font_cx.collection.register_fonts(
|
|
blob,
|
|
Some(FontInfoOverride {
|
|
family_name: Some(&private_name),
|
|
..Default::default()
|
|
}),
|
|
);
|
|
debug_assert!(!fonts.is_empty(), "validated font failed its second scan");
|
|
let faces = fonts
|
|
.into_iter()
|
|
.flat_map(|(family, fonts)| {
|
|
fonts.into_iter().map(move |font| RegisteredFace {
|
|
family,
|
|
width: font.width(),
|
|
style: font.style(),
|
|
weight: font.weight(),
|
|
})
|
|
})
|
|
.collect();
|
|
RegisteredFont {
|
|
private_name,
|
|
bucket,
|
|
font_id,
|
|
faces,
|
|
}
|
|
}
|
|
|
|
fn glyph_bucket(&mut self, name: &str) -> u64 {
|
|
if name == DEFAULT_GLYPH_BUCKET {
|
|
return crate::render::DEFAULT_GLYPH_BUCKET_ID;
|
|
}
|
|
if let Some(bucket) = self.glyph_buckets.get(name) {
|
|
return *bucket;
|
|
}
|
|
let bucket = self.next_glyph_bucket;
|
|
self.next_glyph_bucket += 1;
|
|
self.glyph_buckets.insert(name.to_owned(), bucket);
|
|
bucket
|
|
}
|
|
|
|
fn bucket_for_font(&self, font: u64) -> u64 {
|
|
self.font_buckets
|
|
.get(&font)
|
|
.copied()
|
|
.unwrap_or(crate::render::DEFAULT_GLYPH_BUCKET_ID)
|
|
}
|
|
|
|
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(font) = self.registered_families.get(family) {
|
|
return font.private_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();
|
|
let bucket = self.bucket_for_font(font_id);
|
|
|
|
for glyph in run.positioned_glyphs() {
|
|
let (x, subpixel) = horizontal_phase(glyph.x);
|
|
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(bucket, &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(bucket, key, &image, textures),
|
|
None => {
|
|
self.atlas.insert_empty(bucket, key);
|
|
None
|
|
}
|
|
}
|
|
}
|
|
};
|
|
let Some(entry) = entry else { continue };
|
|
placed.push(PlacedGlyph {
|
|
entry,
|
|
offset: Vec2::new(
|
|
x + 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(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn horizontal_phase(x: f32) -> (f32, u8) {
|
|
let quarters = (x * 4.0).round() as i32;
|
|
(quarters.div_euclid(4) as f32, quarters.rem_euclid(4) as u8)
|
|
}
|
|
|
|
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 overflow: TextOverflow,
|
|
pub overflow_position: Len,
|
|
pub align: RegionAlign,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
|
pub enum TextOverflow {
|
|
#[default]
|
|
Visible,
|
|
Wrap,
|
|
Hidden,
|
|
Ellipsis,
|
|
}
|
|
|
|
#[derive(Clone, PartialEq)]
|
|
struct TextShapeAttrs {
|
|
color: PaintId,
|
|
font_size: f32,
|
|
line_height: f32,
|
|
family: String,
|
|
}
|
|
|
|
impl From<&TextAttrs> for TextShapeAttrs {
|
|
fn from(attrs: &TextAttrs) -> Self {
|
|
Self {
|
|
color: attrs.color.clone(),
|
|
font_size: attrs.font_size,
|
|
line_height: attrs.line_height,
|
|
family: attrs.family.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TextShapeAttrs {
|
|
fn matches(&self, attrs: &TextAttrs) -> bool {
|
|
self.color == attrs.color
|
|
&& self.font_size == attrs.font_size
|
|
&& self.line_height == attrs.line_height
|
|
&& self.family == attrs.family
|
|
}
|
|
}
|
|
|
|
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(),
|
|
overflow: TextOverflow::Visible,
|
|
overflow_position: Len::ZERO,
|
|
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<(TextShapeAttrs, 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,
|
|
) {
|
|
if self
|
|
.shaped
|
|
.as_ref()
|
|
.is_some_and(|(old, old_width, old_density)| {
|
|
old.matches(attrs) && *old_width == width && *old_density == density
|
|
})
|
|
{
|
|
return;
|
|
}
|
|
let shape_attrs = TextShapeAttrs::from(attrs);
|
|
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((shape_attrs, width, density));
|
|
}
|
|
|
|
fn invalidate(&mut self) {
|
|
// A layout owns the font blobs it was shaped with. Drop it now so
|
|
// replacing a registered font does not retain the old bytes until an
|
|
// off-screen text resource happens to be shaped again.
|
|
self.layout = Layout::new();
|
|
self.shaped = None;
|
|
}
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
pub struct TextRsc {
|
|
buffer: TextBuffer,
|
|
ellipsis_buffer: TextBuffer,
|
|
attrs: TextAttrs,
|
|
rendered: Option<RenderedText>,
|
|
ellipsis_rendered: Option<(TextShapeAttrs, f32, RenderedText)>,
|
|
width: Option<f32>,
|
|
density: f32,
|
|
owner: Option<WidgetId>,
|
|
}
|
|
|
|
impl TextRsc {
|
|
fn new(buffer: TextBuffer, attrs: TextAttrs) -> Self {
|
|
Self {
|
|
buffer,
|
|
ellipsis_buffer: TextBuffer::new("…"),
|
|
attrs,
|
|
rendered: None,
|
|
ellipsis_rendered: None,
|
|
width: None,
|
|
density: 0.0,
|
|
owner: None,
|
|
}
|
|
}
|
|
|
|
fn invalidate(&mut self) {
|
|
self.buffer.invalidate();
|
|
self.rendered = None;
|
|
self.ellipsis_buffer.invalidate();
|
|
self.ellipsis_rendered = None;
|
|
}
|
|
}
|
|
|
|
/// All text storage and shaping state for one [`crate::Ui`]. Widgets retain a
|
|
/// compact [`TextHandle`] into this arena rather than owning Parley layouts.
|
|
pub struct TextResources {
|
|
data: TextData,
|
|
entries: Rc<RefCell<Resources<TextRsc>>>,
|
|
}
|
|
|
|
impl TextResources {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
data: TextData::default(),
|
|
entries: Rc::new(RefCell::new(Resources::new())),
|
|
}
|
|
}
|
|
|
|
pub fn add(resources: Rc<RefCell<Self>>, buffer: TextBuffer, attrs: TextAttrs) -> TextHandle {
|
|
let entries = {
|
|
let mut resources = resources.borrow_mut();
|
|
resources.free_released();
|
|
resources.entries.clone()
|
|
};
|
|
TextHandle {
|
|
rsc: RscHandle::add(entries, TextRsc::new(buffer, attrs)),
|
|
resources,
|
|
}
|
|
}
|
|
|
|
pub fn handle(resources: Rc<RefCell<Self>>, id: StrongRscId<TextRsc>) -> TextHandle {
|
|
let entries = resources.borrow().entries.clone();
|
|
TextHandle {
|
|
rsc: RscHandle::new(id, entries),
|
|
resources,
|
|
}
|
|
}
|
|
|
|
pub fn upgrade(resources: Rc<RefCell<Self>>, id: WeakRscId<TextRsc>) -> Option<TextHandle> {
|
|
let entries = {
|
|
let mut resources = resources.borrow_mut();
|
|
resources.free_released();
|
|
resources.entries.clone()
|
|
};
|
|
let id = entries.borrow_mut().upgrade(id)?;
|
|
Some(Self::handle(resources, id))
|
|
}
|
|
|
|
pub fn free_released(&mut self) {
|
|
self.entries.borrow_mut().apply(|_, _| {});
|
|
}
|
|
|
|
pub fn invalidate_all(&mut self) -> Vec<WidgetId> {
|
|
let mut owners = Vec::new();
|
|
for resource in self.entries.borrow_mut().values_mut() {
|
|
resource.invalidate();
|
|
if let Some(owner) = resource.owner
|
|
&& !owners.contains(&owner)
|
|
{
|
|
owners.push(owner);
|
|
}
|
|
}
|
|
owners
|
|
}
|
|
|
|
fn shape(&mut self, resource: &mut TextRsc) {
|
|
let density = self.data.density;
|
|
resource
|
|
.buffer
|
|
.shape(&mut self.data, &resource.attrs, resource.width, density);
|
|
}
|
|
|
|
fn render(
|
|
&mut self,
|
|
resource: &mut TextRsc,
|
|
width: Option<f32>,
|
|
owner: WidgetId,
|
|
textures: &mut Textures,
|
|
density: f32,
|
|
) -> (RenderedText, bool) {
|
|
let atlas_generation = self.data.atlas.generation();
|
|
resource.owner = Some(owner);
|
|
if resource.width == width
|
|
&& resource.density == density
|
|
&& let Some(rendered) = &resource.rendered
|
|
&& rendered.generation == atlas_generation
|
|
{
|
|
return (rendered.clone(), false);
|
|
}
|
|
resource.width = width;
|
|
resource.density = density;
|
|
let rendered = self.data.render(
|
|
&mut resource.buffer,
|
|
&resource.attrs,
|
|
width,
|
|
textures,
|
|
density,
|
|
);
|
|
resource.rendered = Some(rendered.clone());
|
|
(rendered, true)
|
|
}
|
|
|
|
fn render_ellipsis(
|
|
&mut self,
|
|
resource: &mut TextRsc,
|
|
owner: WidgetId,
|
|
textures: &mut Textures,
|
|
density: f32,
|
|
) -> (RenderedText, bool) {
|
|
let shape_attrs = TextShapeAttrs::from(&resource.attrs);
|
|
let atlas_generation = self.data.atlas.generation();
|
|
resource.owner = Some(owner);
|
|
if let Some((cached_attrs, cached_density, rendered)) = &resource.ellipsis_rendered
|
|
&& cached_attrs == &shape_attrs
|
|
&& *cached_density == density
|
|
&& rendered.generation == atlas_generation
|
|
{
|
|
return (rendered.clone(), false);
|
|
}
|
|
let rendered = self.data.render(
|
|
&mut resource.ellipsis_buffer,
|
|
&resource.attrs,
|
|
None,
|
|
textures,
|
|
density,
|
|
);
|
|
resource.ellipsis_rendered = Some((shape_attrs, density, rendered.clone()));
|
|
(rendered, true)
|
|
}
|
|
}
|
|
|
|
impl Default for TextResources {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl Deref for TextResources {
|
|
type Target = TextData;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.data
|
|
}
|
|
}
|
|
|
|
impl DerefMut for TextResources {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
&mut self.data
|
|
}
|
|
}
|
|
|
|
/// A widget-owned reference to one entry in [`TextResources`]. The arena is
|
|
/// shared once per UI; constructing a handle does not allocate a resource of
|
|
/// its own.
|
|
pub struct TextHandle {
|
|
rsc: RscHandle<TextRsc>,
|
|
resources: Rc<RefCell<TextResources>>,
|
|
}
|
|
|
|
impl TextHandle {
|
|
pub fn strong(&self) -> StrongRscId<TextRsc> {
|
|
self.rsc.strong()
|
|
}
|
|
|
|
pub fn weak(&self) -> WeakRscId<TextRsc> {
|
|
self.rsc.weak()
|
|
}
|
|
|
|
pub fn text(&self) -> Ref<'_, str> {
|
|
Ref::map(self.rsc.get(), |resource| resource.buffer.text())
|
|
}
|
|
|
|
pub fn attrs(&self) -> Ref<'_, TextAttrs> {
|
|
Ref::map(self.rsc.get(), |resource| &resource.attrs)
|
|
}
|
|
|
|
pub fn set_text(&mut self, text: impl Into<String>) -> bool {
|
|
let text = text.into();
|
|
let mut resource = self.rsc.get_mut();
|
|
if resource.buffer.text() == text {
|
|
return false;
|
|
}
|
|
resource.buffer.set_text(text);
|
|
resource.rendered = None;
|
|
true
|
|
}
|
|
|
|
pub fn edit_text<R>(&mut self, edit: impl FnOnce(&mut String) -> R) -> R {
|
|
let mut resource = self.rsc.get_mut();
|
|
resource.rendered = None;
|
|
edit(resource.buffer.edit())
|
|
}
|
|
|
|
pub fn set_spans(&mut self, spans: Vec<SpanStyle>) {
|
|
let mut resource = self.rsc.get_mut();
|
|
resource.buffer.set_spans(spans);
|
|
resource.rendered = None;
|
|
}
|
|
|
|
pub fn attrs_mut(&mut self) -> RefMut<'_, TextAttrs> {
|
|
let mut resource = self.rsc.get_mut();
|
|
resource.invalidate();
|
|
RefMut::map(resource, |resource| &mut resource.attrs)
|
|
}
|
|
|
|
pub fn set_overflow_position(&mut self, position: Len) {
|
|
self.rsc.get_mut().attrs.overflow_position = position;
|
|
}
|
|
|
|
pub fn set_overflow(&mut self, overflow: TextOverflow) {
|
|
self.rsc.get_mut().attrs.overflow = overflow;
|
|
}
|
|
|
|
pub fn rendered(&self) -> Option<RenderedText> {
|
|
self.rsc.get().rendered.clone()
|
|
}
|
|
|
|
pub fn width(&self) -> Option<f32> {
|
|
self.rsc.get().width
|
|
}
|
|
|
|
pub fn with_layout<R>(&self, f: impl FnOnce(&Layout<PaintId>, &str) -> R) -> R {
|
|
let mut resources = self.resources.borrow_mut();
|
|
let mut resource = self.rsc.get_mut_shared();
|
|
resources.shape(&mut resource);
|
|
f(resource.buffer.layout(), resource.buffer.text())
|
|
}
|
|
|
|
pub fn layout(&self) -> Ref<'_, Layout<PaintId>> {
|
|
{
|
|
let mut resources = self.resources.borrow_mut();
|
|
let mut resource = self.rsc.get_mut_shared();
|
|
resources.shape(&mut resource);
|
|
}
|
|
Ref::map(self.rsc.get(), |resource| resource.buffer.layout())
|
|
}
|
|
|
|
pub fn render(
|
|
&self,
|
|
width: Option<f32>,
|
|
owner: WidgetId,
|
|
textures: &mut Textures,
|
|
density: f32,
|
|
) -> (RenderedText, bool) {
|
|
let mut resources = self.resources.borrow_mut();
|
|
let mut resource = self.rsc.get_mut_shared();
|
|
resources.render(&mut resource, width, owner, textures, density)
|
|
}
|
|
|
|
pub fn render_ellipsis(
|
|
&self,
|
|
owner: WidgetId,
|
|
textures: &mut Textures,
|
|
density: f32,
|
|
) -> (RenderedText, bool) {
|
|
let mut resources = self.resources.borrow_mut();
|
|
let mut resource = self.rsc.get_mut_shared();
|
|
resources.render_ellipsis(&mut resource, owner, textures, density)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn a_horizontal_phase_carries_across_pixel_boundaries() {
|
|
assert_eq!(horizontal_phase(10.20), (10.0, 1));
|
|
assert_eq!(horizontal_phase(10.90), (11.0, 0));
|
|
assert_eq!(horizontal_phase(-0.20), (-1.0, 3));
|
|
}
|
|
|
|
#[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 dropped_handles_release_their_arena_entries() {
|
|
let resources = Rc::new(RefCell::new(TextResources::new()));
|
|
let text = TextResources::add(
|
|
resources.clone(),
|
|
TextBuffer::new("temporary"),
|
|
TextAttrs::default(),
|
|
);
|
|
assert_eq!(resources.borrow().entries.borrow().len(), 1);
|
|
|
|
drop(text);
|
|
resources.borrow_mut().free_released();
|
|
|
|
assert!(resources.borrow().entries.borrow().is_empty());
|
|
}
|
|
}
|