Glyphs on demand: FontFace replaces fontdue, DejaVu ahead of the CJK collection in the fallback chain; App::take_clipboard_request and App::on_pointer_secondary
Every face fontdue opened was parsed whole: `Font::from_bytes` walks the `cmap` and builds the outline of every glyph before returning. For the theme's Sora that is a few hundred glyphs and nobody notices. For a fallback it is the whole file: the first string with one arrow or one bullet resolved through the chain to `NotoSansCJK-Regular.ttc`, and a compositor or a notification daemon that had drawn a dozen labels was suddenly holding 330 MB of outlines for 44 810 glyphs, after 1.7 s of CPU, to paint the hundred or so glyphs a screen ever shows. Measured on forge: 240 MB before the first app-switcher label, 542 MB after it, and never given back, because the table is live, not garbage. `FontFace`. The new `src/font_face.rs` is the face type the whole crate now rasterises with, built on the `ttf-parser` that rustybuzz already brings in and on tiny-skia, which already fills paths. `from_bytes( data, index )` keeps the bytes and reads only what a face needs up front: units per em and the vertical line metrics. A `ttf_parser::Face` is rebuilt on every use rather than kept, since parsing one is table-offset arithmetic, in the noise next to the outline work it guards. Each glyph's outline is extracted the first time it is asked for, through `Outliner`, into a `tiny_skia::Path`, and cached for the process lifetime in a `HashMap<u16, Arc<Outline>>`; codepoint lookups are cached the same way. Rasterising fills that path into a `Mask` and hands back the 8-bit coverage, top row first, which is what both canvases stored before. The metrics keep fontdue's conventions, so no widget changed: sizes scale by `px / units_per_em`, `Metrics::xmin` / `ymin` are whole-pixel placements with the fractional remainder folded into the raster offset, `bounds` is the outline box in subpixels, `LineMetrics` carries ascent, descent, line gap and their sum. One thing is deliberately not what `glyf` declares: the box comes from the points the outline actually reaches, quadratic and cubic extrema included, because declared boxes may be loose and a box wider than the ink shifts the glyph on the pixel grid. Tests cover the turning-point arithmetic and the line-metric scaling. `FontFace`, `Metrics`, `LineMetrics` and `OutlineBounds` are exported, since `Canvas::font` and `font_for` hand them out. What changed around it. `fontdue` leaves `Cargo.toml`. The two canvases, their text and setup modules, the registry, the rich text and text widgets swap `Arc<Font>` for `Arc<FontFace>` and nothing else; the glyph caches still key on `(glyph_id, size_bits, font_id)`. `FontRegistry` now reads a source into an `Arc<Vec<u8>>` and gives the face the bytes it will pull outlines from, so the registry no longer owns a second copy; its parse error names the file rather than a library message. `FontHandle` keeps `font` and `bytes` side by side, now naming the same buffer, so shaping needs no second read and the comment that explained the duplication is gone. `system_fonts` builds fallback faces from the bytes each slot already holds, and the slot stays two-staged: the bytes are read once to probe coverage with `ttf-parser`, the face is built only when a glyph in that slot is needed, and a face that is built no longer costs anything until it draws. The chain. DejaVu Sans moves from last resort to just before the CJK collection. It owns the geometric shapes, arrows and box-drawing blocks, which are what a user interface most often needs outside its own family; behind the collection, every one of those pulled the 19 MB file in, since probing a slot for coverage reads it whole. The CJK entries stay last for the same reason. The two hooks are for a host that embeds another UI, trust in this case, and complete the ones `App` already had. `take_clipboard_request` is the counterpart of nothing: an app could paste from the system clipboard through its text fields but had no way to put text into it. Return `Some( text )` once and the run loop stores it and publishes it as the seat's selection through the data device, the same path `handle_copy` takes for Ctrl+C in a text field, so other applications see it. `on_pointer_secondary` reports a right-button press that no widget of ltk's own answered, in physical pixels like `on_pointer_button`: `on_pointer_right_click` already dispatched a widget's `on_long_press` and the built-in text menu, and in the remaining case only dismissed an open menu; it now also tells the app, which can forward the press to an embedded view that has a context menu of its own. Not changed. Variable fonts are read at their default instance, as before. Colour glyphs, `COLR` or bitmap, are not rendered by either rasteriser; the outline path draws what it can. The glyph caches are still unbounded per face, but they now hold what was drawn, not what the file contains.
This commit is contained in:
@@ -37,7 +37,6 @@ smithay-client-toolkit = { version = "0.20", features = ["calloop", "calloop-way
|
|||||||
calloop = "0.14"
|
calloop = "0.14"
|
||||||
calloop-wayland-source = "0.4"
|
calloop-wayland-source = "0.4"
|
||||||
tiny-skia = "0.12"
|
tiny-skia = "0.12"
|
||||||
fontdue = "=0.9.3"
|
|
||||||
unicode-bidi = "0.3"
|
unicode-bidi = "0.3"
|
||||||
rustybuzz = "0.14"
|
rustybuzz = "0.14"
|
||||||
image = { version = "=0.25.9", default-features = false, features = ["png", "jpeg", "webp"] }
|
image = { version = "=0.25.9", default-features = false, features = ["png", "jpeg", "webp"] }
|
||||||
|
|||||||
@@ -868,6 +868,9 @@ pub trait App: 'static
|
|||||||
/// Return `true` once to drop the keyboard focus, which folds the on-screen keyboard; a [`take_focus_request`](Self::take_focus_request) in the same cycle wins.
|
/// Return `true` once to drop the keyboard focus, which folds the on-screen keyboard; a [`take_focus_request`](Self::take_focus_request) in the same cycle wins.
|
||||||
fn take_blur_request( &mut self ) -> bool { false }
|
fn take_blur_request( &mut self ) -> bool { false }
|
||||||
|
|
||||||
|
/// Return `Some( text )` once to put it in the clipboard, for other applications as well.
|
||||||
|
fn take_clipboard_request( &mut self ) -> Option<String> { None }
|
||||||
|
|
||||||
/// Called on every pointer motion (mouse move, button press, button
|
/// Called on every pointer motion (mouse move, button press, button
|
||||||
/// release). `x`, `y` are in physical pixels of the main surface,
|
/// release). `x`, `y` are in physical pixels of the main surface,
|
||||||
/// the same coordinate space as widget rects. Default: no-op.
|
/// the same coordinate space as widget rects. Default: no-op.
|
||||||
@@ -958,6 +961,9 @@ pub trait App: 'static
|
|||||||
/// classifier collapses press+release into a single event.
|
/// classifier collapses press+release into a single event.
|
||||||
fn on_pointer_button( &mut self, _x: f32, _y: f32, _pressed: bool ) {}
|
fn on_pointer_button( &mut self, _x: f32, _y: f32, _pressed: bool ) {}
|
||||||
|
|
||||||
|
/// A secondary-button press that no widget of ltk's own answered, in physical pixels. Default: no-op.
|
||||||
|
fn on_pointer_secondary( &mut self, _x: f32, _y: f32 ) {}
|
||||||
|
|
||||||
/// Called on every keyboard event (press *and* release) before the
|
/// Called on every keyboard event (press *and* release) before the
|
||||||
/// regular focus-aware dispatch ([`Self::on_key`] /
|
/// regular focus-aware dispatch ([`Self::on_key`] /
|
||||||
/// [`Self::on_key_with_modifiers`]) runs. Default: no-op.
|
/// [`Self::on_key_with_modifiers`]) runs. Default: no-op.
|
||||||
|
|||||||
@@ -921,6 +921,12 @@ pub( crate ) fn try_run<A: App>( mut app: A ) -> Result<(), RunError>
|
|||||||
data.focus_retry = None;
|
data.focus_retry = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some( text ) = data.app.take_clipboard_request()
|
||||||
|
{
|
||||||
|
data.clipboard = text;
|
||||||
|
data.publish_clipboard_selection();
|
||||||
|
}
|
||||||
|
|
||||||
// Focus the widget with the requested WidgetId if the app requests it.
|
// Focus the widget with the requested WidgetId if the app requests it.
|
||||||
// Walks main + every overlay because the targeted widget can live
|
// Walks main + every overlay because the targeted widget can live
|
||||||
// on any of them (a search field on a launcher overlay, a text
|
// on any of them (a search field on a launcher overlay, a text
|
||||||
|
|||||||
@@ -0,0 +1,509 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-only
|
||||||
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||||
|
|
||||||
|
//! Glyph source: outlines extracted on first use, never up front.
|
||||||
|
//!
|
||||||
|
//! A face keeps its file bytes and rebuilds the `ttf_parser::Face` on
|
||||||
|
//! demand — parsing one is table-offset arithmetic, so the cost lands in
|
||||||
|
//! the noise next to the outline work it guards. Each glyph's outline is
|
||||||
|
//! turned into a [`tiny_skia::Path`] the first time it is asked for and
|
||||||
|
//! cached for the process lifetime; rasterising then fills that path
|
||||||
|
//! into an 8-bit coverage [`Mask`].
|
||||||
|
//!
|
||||||
|
//! The eager alternative — building every glyph in the `cmap` when the
|
||||||
|
//! face is constructed — costs 330 MB and 1.7 s of CPU for a 44 810
|
||||||
|
//! glyph CJK collection, to draw the hundred or so glyphs a screen
|
||||||
|
//! actually shows. A single arrow or bullet resolving through the
|
||||||
|
//! fallback chain was enough to pay it in full.
|
||||||
|
//!
|
||||||
|
//! [`Metrics`], [`OutlineBounds`] and [`LineMetrics`] keep the layout
|
||||||
|
//! conventions the renderers and widgets already lay text out with:
|
||||||
|
//! sizes scale by `px / units_per_em`, `bounds` is the outline box in
|
||||||
|
//! subpixels, `ymin` grows upwards from the baseline, and the bitmap
|
||||||
|
//! runs row-major from its top-left corner.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{ Arc, Mutex };
|
||||||
|
|
||||||
|
use rustybuzz::ttf_parser::{ Face, GlyphId, OutlineBuilder };
|
||||||
|
use tiny_skia::{ FillRule, Mask, Path, PathBuilder, Transform };
|
||||||
|
|
||||||
|
/// Bounding box of a glyph's outline, in subpixels, `ymin` upwards from
|
||||||
|
/// the baseline.
|
||||||
|
#[ derive( Copy, Clone, PartialEq, Debug, Default ) ]
|
||||||
|
pub struct OutlineBounds
|
||||||
|
{
|
||||||
|
pub xmin: f32,
|
||||||
|
pub ymin: f32,
|
||||||
|
pub width: f32,
|
||||||
|
pub height: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutlineBounds
|
||||||
|
{
|
||||||
|
fn scale( &self, scale: f32 ) -> Self
|
||||||
|
{
|
||||||
|
Self
|
||||||
|
{
|
||||||
|
xmin: self.xmin * scale,
|
||||||
|
ymin: self.ymin * scale,
|
||||||
|
width: self.width * scale,
|
||||||
|
height: self.height * scale,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Placement and size of one rasterised glyph.
|
||||||
|
#[ derive( Copy, Clone, PartialEq, Debug, Default ) ]
|
||||||
|
pub struct Metrics
|
||||||
|
{
|
||||||
|
/// Whole-pixel offset of the bitmap's left edge, negative when the
|
||||||
|
/// glyph reaches left of the origin.
|
||||||
|
pub xmin: i32,
|
||||||
|
/// Whole-pixel offset of the bitmap's bottom edge, negative below
|
||||||
|
/// the baseline.
|
||||||
|
pub ymin: i32,
|
||||||
|
pub width: usize,
|
||||||
|
pub height: usize,
|
||||||
|
pub advance_width: f32,
|
||||||
|
pub advance_height: f32,
|
||||||
|
pub bounds: OutlineBounds,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vertical metrics of a face at a given size.
|
||||||
|
#[ derive( Copy, Clone, PartialEq, Debug, Default ) ]
|
||||||
|
pub struct LineMetrics
|
||||||
|
{
|
||||||
|
pub ascent: f32,
|
||||||
|
pub descent: f32,
|
||||||
|
pub line_gap: f32,
|
||||||
|
/// `ascent - descent + line_gap`.
|
||||||
|
pub new_line_size: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LineMetrics
|
||||||
|
{
|
||||||
|
fn scale( &self, scale: f32 ) -> Self
|
||||||
|
{
|
||||||
|
Self
|
||||||
|
{
|
||||||
|
ascent: self.ascent * scale,
|
||||||
|
descent: self.descent * scale,
|
||||||
|
line_gap: self.line_gap * scale,
|
||||||
|
new_line_size: self.new_line_size * scale,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One glyph's outline in font units, plus the advances layout needs.
|
||||||
|
struct Outline
|
||||||
|
{
|
||||||
|
/// `None` for glyphs with no contours — a space, or a codepoint the
|
||||||
|
/// face maps to an empty shape.
|
||||||
|
path: Option<Path>,
|
||||||
|
bounds: OutlineBounds,
|
||||||
|
advance_width: f32,
|
||||||
|
advance_height: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A font face that materialises glyphs on demand.
|
||||||
|
pub struct FontFace
|
||||||
|
{
|
||||||
|
data: Arc<Vec<u8>>,
|
||||||
|
index: u32,
|
||||||
|
units_per_em: f32,
|
||||||
|
line: LineMetrics,
|
||||||
|
outlines: Mutex<HashMap<u16, Arc<Outline>>>,
|
||||||
|
indices: Mutex<HashMap<char, u16>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The glyph caches are left out: formatting must not block on them.
|
||||||
|
impl std::fmt::Debug for FontFace
|
||||||
|
{
|
||||||
|
fn fmt( &self, f: &mut std::fmt::Formatter<'_> ) -> std::fmt::Result
|
||||||
|
{
|
||||||
|
f.debug_struct( "FontFace" )
|
||||||
|
.field( "index", &self.index )
|
||||||
|
.field( "units_per_em", &self.units_per_em )
|
||||||
|
.field( "line", &self.line )
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FontFace
|
||||||
|
{
|
||||||
|
/// Parse the face at `index` of `data`, keeping the bytes alive for
|
||||||
|
/// the outline loads that follow. `None` when the face does not
|
||||||
|
/// parse or declares a zero em square.
|
||||||
|
pub fn from_bytes( data: Arc<Vec<u8>>, index: u32 ) -> Option<Self>
|
||||||
|
{
|
||||||
|
let ( units_per_em, line ) =
|
||||||
|
{
|
||||||
|
let face = Face::parse( data.as_slice(), index ).ok()?;
|
||||||
|
let upem = face.units_per_em() as f32;
|
||||||
|
if upem <= 0.0 { return None; }
|
||||||
|
let ascent = face.ascender() as f32;
|
||||||
|
let descent = face.descender() as f32;
|
||||||
|
let line_gap = face.line_gap() as f32;
|
||||||
|
let line = LineMetrics
|
||||||
|
{
|
||||||
|
ascent,
|
||||||
|
descent,
|
||||||
|
line_gap,
|
||||||
|
new_line_size: ascent - descent + line_gap,
|
||||||
|
};
|
||||||
|
( upem, line )
|
||||||
|
};
|
||||||
|
|
||||||
|
Some( Self
|
||||||
|
{
|
||||||
|
data,
|
||||||
|
index,
|
||||||
|
units_per_em,
|
||||||
|
line,
|
||||||
|
outlines: Mutex::new( HashMap::new() ),
|
||||||
|
indices: Mutex::new( HashMap::new() ),
|
||||||
|
} )
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn units_per_em( &self ) -> f32
|
||||||
|
{
|
||||||
|
self.units_per_em
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scale_factor( &self, px: f32 ) -> f32
|
||||||
|
{
|
||||||
|
px / self.units_per_em
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Glyph index for `ch`, or `0` when the face has no glyph for it.
|
||||||
|
pub fn lookup_glyph_index( &self, ch: char ) -> u16
|
||||||
|
{
|
||||||
|
if let Some( id ) = self.indices.lock().ok().and_then( |m| m.get( &ch ).copied() )
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
let id = self.face()
|
||||||
|
.and_then( |f| f.glyph_index( ch ) )
|
||||||
|
.map( |g| g.0 )
|
||||||
|
.unwrap_or( 0 );
|
||||||
|
if let Ok( mut m ) = self.indices.lock() { m.insert( ch, id ); }
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn horizontal_line_metrics( &self, px: f32 ) -> Option<LineMetrics>
|
||||||
|
{
|
||||||
|
Some( self.line.scale( self.scale_factor( px ) ) )
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn metrics( &self, ch: char, px: f32 ) -> Metrics
|
||||||
|
{
|
||||||
|
self.metrics_indexed( self.lookup_glyph_index( ch ), px )
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn metrics_indexed( &self, id: u16, px: f32 ) -> Metrics
|
||||||
|
{
|
||||||
|
if px <= 0.0 { return Metrics::default(); }
|
||||||
|
self.raw_metrics( &self.outline( id ), self.scale_factor( px ) ).0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coverage bitmap for `id` at `px`, row-major from the top-left
|
||||||
|
/// corner, alongside its placement.
|
||||||
|
pub fn rasterize_indexed( &self, id: u16, px: f32 ) -> ( Metrics, Vec<u8> )
|
||||||
|
{
|
||||||
|
if px <= 0.0 { return ( Metrics::default(), Vec::new() ); }
|
||||||
|
|
||||||
|
let outline = self.outline( id );
|
||||||
|
let scale = self.scale_factor( px );
|
||||||
|
let ( metrics, ox, oy ) = self.raw_metrics( &outline, scale );
|
||||||
|
if metrics.width == 0 || metrics.height == 0
|
||||||
|
{
|
||||||
|
return ( metrics, Vec::new() );
|
||||||
|
}
|
||||||
|
|
||||||
|
let blank = || vec![ 0u8; metrics.width * metrics.height ];
|
||||||
|
let Some( path ) = outline.path.as_ref() else { return ( metrics, blank() ) };
|
||||||
|
let Some( mut mask ) = Mask::new( metrics.width as u32, metrics.height as u32 ) else { return ( metrics, blank() ) };
|
||||||
|
|
||||||
|
// Font units are y-up from the baseline and the mask is y-down
|
||||||
|
// from its own corner, so the outline box's top-left lands on
|
||||||
|
// the fractional offsets the metrics reserved for it.
|
||||||
|
let ts = Transform::from_row(
|
||||||
|
scale,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
-scale,
|
||||||
|
ox - metrics.bounds.xmin,
|
||||||
|
oy + metrics.bounds.ymin + metrics.bounds.height,
|
||||||
|
);
|
||||||
|
mask.fill_path( path, FillRule::Winding, true, ts );
|
||||||
|
( metrics, mask.take() )
|
||||||
|
}
|
||||||
|
|
||||||
|
fn face( &self ) -> Option<Face<'_>>
|
||||||
|
{
|
||||||
|
Face::parse( self.data.as_slice(), self.index ).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scale `outline` and place it on the pixel grid, returning the
|
||||||
|
/// fractional offsets the rasteriser has to shift it by.
|
||||||
|
fn raw_metrics( &self, outline: &Outline, scale: f32 ) -> ( Metrics, f32, f32 )
|
||||||
|
{
|
||||||
|
let bounds = outline.bounds.scale( scale );
|
||||||
|
let mut ox = bounds.xmin.fract();
|
||||||
|
let mut oy = ( 1.0 - bounds.height.fract() - bounds.ymin.fract() ).fract();
|
||||||
|
if ox < 0.0 { ox += 1.0; }
|
||||||
|
if oy < 0.0 { oy += 1.0; }
|
||||||
|
|
||||||
|
let metrics = Metrics
|
||||||
|
{
|
||||||
|
xmin: bounds.xmin.floor() as i32,
|
||||||
|
ymin: bounds.ymin.floor() as i32,
|
||||||
|
width: ( bounds.width + ox ).ceil().max( 0.0 ) as usize,
|
||||||
|
height: ( bounds.height + oy ).ceil().max( 0.0 ) as usize,
|
||||||
|
advance_width: scale * outline.advance_width,
|
||||||
|
advance_height: scale * outline.advance_height,
|
||||||
|
bounds,
|
||||||
|
};
|
||||||
|
( metrics, ox, oy )
|
||||||
|
}
|
||||||
|
|
||||||
|
fn outline( &self, id: u16 ) -> Arc<Outline>
|
||||||
|
{
|
||||||
|
if let Some( o ) = self.outlines.lock().ok().and_then( |m| m.get( &id ).cloned() )
|
||||||
|
{
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
let built = Arc::new( self.build_outline( id ) );
|
||||||
|
if let Ok( mut m ) = self.outlines.lock() { m.insert( id, Arc::clone( &built ) ); }
|
||||||
|
built
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_outline( &self, id: u16 ) -> Outline
|
||||||
|
{
|
||||||
|
let empty = || Outline
|
||||||
|
{
|
||||||
|
path: None,
|
||||||
|
bounds: OutlineBounds::default(),
|
||||||
|
advance_width: 0.0,
|
||||||
|
advance_height: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some( face ) = self.face() else { return empty() };
|
||||||
|
|
||||||
|
let gid = GlyphId( id );
|
||||||
|
let advance_width = face.glyph_hor_advance( gid ).unwrap_or( 0 ) as f32;
|
||||||
|
let advance_height = face.glyph_ver_advance( gid ).unwrap_or( 0 ) as f32;
|
||||||
|
|
||||||
|
let mut sink = Outliner::new();
|
||||||
|
let _ = face.outline_glyph( gid, &mut sink );
|
||||||
|
|
||||||
|
// The box comes from the points the outline actually reaches,
|
||||||
|
// curve extrema included, rather than the one `glyf` declares:
|
||||||
|
// declared boxes are allowed to be loose, and a box wider than
|
||||||
|
// the ink shifts the glyph on the pixel grid.
|
||||||
|
let bounds = sink.aabb.finish().unwrap_or_default();
|
||||||
|
let path = sink.builder.finish();
|
||||||
|
|
||||||
|
Outline { path, bounds, advance_width, advance_height }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Running bounding box over every point the outline reaches, curve
|
||||||
|
/// extrema included.
|
||||||
|
struct Aabb
|
||||||
|
{
|
||||||
|
xmin: f32,
|
||||||
|
ymin: f32,
|
||||||
|
xmax: f32,
|
||||||
|
ymax: f32,
|
||||||
|
seen: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Aabb
|
||||||
|
{
|
||||||
|
fn new() -> Self
|
||||||
|
{
|
||||||
|
Self { xmin: f32::MAX, ymin: f32::MAX, xmax: f32::MIN, ymax: f32::MIN, seen: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add( &mut self, x: f32, y: f32 )
|
||||||
|
{
|
||||||
|
self.add_x( x );
|
||||||
|
self.add_y( y );
|
||||||
|
self.seen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_x( &mut self, x: f32 )
|
||||||
|
{
|
||||||
|
self.xmin = self.xmin.min( x );
|
||||||
|
self.xmax = self.xmax.max( x );
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_y( &mut self, y: f32 )
|
||||||
|
{
|
||||||
|
self.ymin = self.ymin.min( y );
|
||||||
|
self.ymax = self.ymax.max( y );
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish( &self ) -> Option<OutlineBounds>
|
||||||
|
{
|
||||||
|
self.seen.then( || OutlineBounds
|
||||||
|
{
|
||||||
|
xmin: self.xmin,
|
||||||
|
ymin: self.ymin,
|
||||||
|
width: self.xmax - self.xmin,
|
||||||
|
height: self.ymax - self.ymin,
|
||||||
|
} )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Outliner
|
||||||
|
{
|
||||||
|
builder: PathBuilder,
|
||||||
|
aabb: Aabb,
|
||||||
|
from: ( f32, f32 ),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Outliner
|
||||||
|
{
|
||||||
|
fn new() -> Self
|
||||||
|
{
|
||||||
|
Self { builder: PathBuilder::new(), aabb: Aabb::new(), from: ( 0.0, 0.0 ) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutlineBuilder for Outliner
|
||||||
|
{
|
||||||
|
fn move_to( &mut self, x: f32, y: f32 )
|
||||||
|
{
|
||||||
|
self.builder.move_to( x, y );
|
||||||
|
self.aabb.add( x, y );
|
||||||
|
self.from = ( x, y );
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_to( &mut self, x: f32, y: f32 )
|
||||||
|
{
|
||||||
|
self.builder.line_to( x, y );
|
||||||
|
self.aabb.add( x, y );
|
||||||
|
self.from = ( x, y );
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quad_to( &mut self, x1: f32, y1: f32, x: f32, y: f32 )
|
||||||
|
{
|
||||||
|
self.builder.quad_to( x1, y1, x, y );
|
||||||
|
self.aabb.add( x, y );
|
||||||
|
let ( x0, y0 ) = self.from;
|
||||||
|
if let Some( v ) = quad_extremum( x0, x1, x ) { self.aabb.add_x( v ); }
|
||||||
|
if let Some( v ) = quad_extremum( y0, y1, y ) { self.aabb.add_y( v ); }
|
||||||
|
self.from = ( x, y );
|
||||||
|
}
|
||||||
|
|
||||||
|
fn curve_to( &mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32 )
|
||||||
|
{
|
||||||
|
self.builder.cubic_to( x1, y1, x2, y2, x, y );
|
||||||
|
self.aabb.add( x, y );
|
||||||
|
let ( x0, y0 ) = self.from;
|
||||||
|
for v in cubic_extrema( x0, x1, x2, x ) { self.aabb.add_x( v ); }
|
||||||
|
for v in cubic_extrema( y0, y1, y2, y ) { self.aabb.add_y( v ); }
|
||||||
|
self.from = ( x, y );
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close( &mut self )
|
||||||
|
{
|
||||||
|
self.builder.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Value of a quadratic Bézier at its turning point, when that point
|
||||||
|
/// falls strictly inside the segment.
|
||||||
|
fn quad_extremum( p0: f32, p1: f32, p2: f32 ) -> Option<f32>
|
||||||
|
{
|
||||||
|
let denom = p0 - 2.0 * p1 + p2;
|
||||||
|
if denom.abs() <= f32::EPSILON { return None; }
|
||||||
|
let t = ( p0 - p1 ) / denom;
|
||||||
|
if !( t > 0.0 && t < 1.0 ) { return None; }
|
||||||
|
let mt = 1.0 - t;
|
||||||
|
Some( mt * mt * p0 + 2.0 * mt * t * p1 + t * t * p2 )
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Values of a cubic Bézier at the turning points inside the segment.
|
||||||
|
/// `B'(t)/3` is `a t² + b t + c` for the coefficients below.
|
||||||
|
fn cubic_extrema( p0: f32, p1: f32, p2: f32, p3: f32 ) -> Vec<f32>
|
||||||
|
{
|
||||||
|
let a = -p0 + 3.0 * p1 - 3.0 * p2 + p3;
|
||||||
|
let b = 2.0 * ( p0 - 2.0 * p1 + p2 );
|
||||||
|
let c = p1 - p0;
|
||||||
|
|
||||||
|
let mut roots: Vec<f32> = Vec::new();
|
||||||
|
if a.abs() <= f32::EPSILON
|
||||||
|
{
|
||||||
|
if b.abs() > f32::EPSILON { roots.push( -c / b ); }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
let disc = b * b - 4.0 * a * c;
|
||||||
|
if disc >= 0.0
|
||||||
|
{
|
||||||
|
let root = disc.sqrt();
|
||||||
|
roots.push( ( -b + root ) / ( 2.0 * a ) );
|
||||||
|
roots.push( ( -b - root ) / ( 2.0 * a ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
roots
|
||||||
|
.into_iter()
|
||||||
|
.filter( |t| *t > 0.0 && *t < 1.0 )
|
||||||
|
.map( |t|
|
||||||
|
{
|
||||||
|
let mt = 1.0 - t;
|
||||||
|
mt * mt * mt * p0
|
||||||
|
+ 3.0 * mt * mt * t * p1
|
||||||
|
+ 3.0 * mt * t * t * p2
|
||||||
|
+ t * t * t * p3
|
||||||
|
} )
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ cfg( test ) ]
|
||||||
|
mod tests
|
||||||
|
{
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn quadratic_turning_point_is_the_apex()
|
||||||
|
{
|
||||||
|
// 0 -> 10 -> 0 peaks at t = 0.5, i.e. 5.
|
||||||
|
assert_eq!( quad_extremum( 0.0, 10.0, 0.0 ), Some( 5.0 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn monotone_quadratic_has_no_interior_turning_point()
|
||||||
|
{
|
||||||
|
assert_eq!( quad_extremum( 0.0, 5.0, 10.0 ), None );
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn cubic_turning_points_bracket_the_control_hull()
|
||||||
|
{
|
||||||
|
let v = cubic_extrema( 0.0, 10.0, -10.0, 0.0 );
|
||||||
|
assert_eq!( v.len(), 2 );
|
||||||
|
for x in v { assert!( x.abs() < 10.0 ); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn monotone_cubic_has_no_interior_turning_point()
|
||||||
|
{
|
||||||
|
assert!( cubic_extrema( 0.0, 1.0, 2.0, 3.0 ).is_empty() );
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn line_metrics_scale_together()
|
||||||
|
{
|
||||||
|
let m = LineMetrics { ascent: 800.0, descent: -200.0, line_gap: 0.0, new_line_size: 1000.0 };
|
||||||
|
let s = m.scale( 0.02 );
|
||||||
|
assert_eq!( s.ascent, 16.0 );
|
||||||
|
assert_eq!( s.descent, -4.0 );
|
||||||
|
assert_eq!( s.new_line_size, 20.0 );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use fontdue::Font;
|
use crate::font_face::FontFace;
|
||||||
use glow::HasContext;
|
use glow::HasContext;
|
||||||
|
|
||||||
use crate::theme::FontRegistry;
|
use crate::theme::FontRegistry;
|
||||||
@@ -107,7 +107,7 @@ pub struct BorrowedGlesTexture
|
|||||||
/// Cached glyph: rasterized bitmap placed in the shared atlas.
|
/// Cached glyph: rasterized bitmap placed in the shared atlas.
|
||||||
pub ( super ) struct GlyphEntry
|
pub ( super ) struct GlyphEntry
|
||||||
{
|
{
|
||||||
pub ( super ) metrics: fontdue::Metrics,
|
pub ( super ) metrics: crate::font_face::Metrics,
|
||||||
pub ( super ) tex_w: i32,
|
pub ( super ) tex_w: i32,
|
||||||
pub ( super ) tex_h: i32,
|
pub ( super ) tex_h: i32,
|
||||||
pub ( super ) atlas_x: u32,
|
pub ( super ) atlas_x: u32,
|
||||||
@@ -118,7 +118,7 @@ pub ( super ) struct GlyphEntry
|
|||||||
/// software canvas's `GlyphKey`. `glyph_id` is the per-font index
|
/// software canvas's `GlyphKey`. `glyph_id` is the per-font index
|
||||||
/// returned by HarfBuzz shaping; `size_bits` is `f32::to_bits` of
|
/// returned by HarfBuzz shaping; `size_bits` is `f32::to_bits` of
|
||||||
/// `size * dpi_scale`; `font_id` is the address of the
|
/// `size * dpi_scale`; `font_id` is the address of the
|
||||||
/// `Arc<fontdue::Font>` used for rasterisation.
|
/// `Arc<FontFace>` used for rasterisation.
|
||||||
pub( super ) type GlyphAtlasKey = ( u16, u32, usize );
|
pub( super ) type GlyphAtlasKey = ( u16, u32, usize );
|
||||||
|
|
||||||
pub( super ) const ATLAS_SIZE: u32 = 2048;
|
pub( super ) const ATLAS_SIZE: u32 = 2048;
|
||||||
@@ -139,7 +139,7 @@ pub struct GlesCanvas
|
|||||||
/// Default font loaded from the system via `system_fonts::default_handle`.
|
/// Default font loaded from the system via `system_fonts::default_handle`.
|
||||||
/// Kept as a fallback for callers that do not route through the
|
/// Kept as a fallback for callers that do not route through the
|
||||||
/// theme registry.
|
/// theme registry.
|
||||||
pub font: Arc<Font>,
|
pub font: Arc<FontFace>,
|
||||||
/// Raw bytes of the default font. Required by rustybuzz for
|
/// Raw bytes of the default font. Required by rustybuzz for
|
||||||
/// HarfBuzz shaping (see the `text_shaping` private module). Kept on the
|
/// HarfBuzz shaping (see the `text_shaping` private module). Kept on the
|
||||||
/// canvas so the shape pipeline has direct access without a
|
/// canvas so the shape pipeline has direct access without a
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use fontdue::{ Font, LineMetrics, Metrics };
|
use crate::font_face::{ FontFace, LineMetrics, Metrics };
|
||||||
use glow::HasContext;
|
use glow::HasContext;
|
||||||
|
|
||||||
use crate::theme::{ FontRegistry, FontStyle };
|
use crate::theme::{ FontRegistry, FontStyle };
|
||||||
@@ -728,7 +728,7 @@ impl GlesCanvas
|
|||||||
pub fn set_global_alpha( &mut self, a: f32 ) { self.global_alpha = a; }
|
pub fn set_global_alpha( &mut self, a: f32 ) { self.global_alpha = a; }
|
||||||
|
|
||||||
/// The canvas default font, used when no specific face is resolved.
|
/// The canvas default font, used when no specific face is resolved.
|
||||||
pub fn font( &self ) -> &Font { &self.font }
|
pub fn font( &self ) -> &FontFace { &self.font }
|
||||||
|
|
||||||
/// Install a theme font registry so [`Self::font_for`] can resolve
|
/// Install a theme font registry so [`Self::font_for`] can resolve
|
||||||
/// family+weight+style triples declared by the theme's `fonts` block.
|
/// family+weight+style triples declared by the theme's `fonts` block.
|
||||||
@@ -740,7 +740,7 @@ impl GlesCanvas
|
|||||||
/// Resolve a specific font from the theme registry, falling back to the
|
/// Resolve a specific font from the theme registry, falling back to the
|
||||||
/// canvas' default [`Self::font`] when no registry is installed or the
|
/// canvas' default [`Self::font`] when no registry is installed or the
|
||||||
/// triple cannot be satisfied.
|
/// triple cannot be satisfied.
|
||||||
pub fn font_for( &self, family: &str, weight: u16, style: FontStyle ) -> Arc<Font>
|
pub fn font_for( &self, family: &str, weight: u16, style: FontStyle ) -> Arc<FontFace>
|
||||||
{
|
{
|
||||||
self.font_registry
|
self.font_registry
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -753,7 +753,7 @@ impl GlesCanvas
|
|||||||
/// fallback chain (lazy load of the relevant Noto pack). Falls
|
/// fallback chain (lazy load of the relevant Noto pack). Falls
|
||||||
/// back to the primary (which paints a `.notdef` box) when no
|
/// back to the primary (which paints a `.notdef` box) when no
|
||||||
/// installed fallback covers the codepoint.
|
/// installed fallback covers the codepoint.
|
||||||
pub fn font_for_char( &self, ch: char ) -> Arc<Font>
|
pub fn font_for_char( &self, ch: char ) -> Arc<FontFace>
|
||||||
{
|
{
|
||||||
if self.font.lookup_glyph_index( ch ) != 0
|
if self.font.lookup_glyph_index( ch ) != 0
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ void main()
|
|||||||
// Glyphs live in a shared GL_LUMINANCE atlas. `u_uv_scale` and `u_uv_offset`
|
// Glyphs live in a shared GL_LUMINANCE atlas. `u_uv_scale` and `u_uv_offset`
|
||||||
// map the unit quad's (0,0)–(1,1) UV range into the glyph's sub-region:
|
// map the unit quad's (0,0)–(1,1) UV range into the glyph's sub-region:
|
||||||
// tex_uv = vec2(v_uv.x, 1 - v_uv.y) * u_uv_scale + u_uv_offset
|
// tex_uv = vec2(v_uv.x, 1 - v_uv.y) * u_uv_scale + u_uv_offset
|
||||||
// The Y-flip (`1 - v_uv.y`) corrects for fontdue bitmaps being stored
|
// The Y-flip (`1 - v_uv.y`) corrects for glyph bitmaps being stored
|
||||||
// top-row-first while GL tex coords have V=0 at the bottom.
|
// top-row-first while GL tex coords have V=0 at the bottom.
|
||||||
//
|
//
|
||||||
// The atlas is GL_R8 on ES3 and GL_LUMINANCE on ES2 (see
|
// The atlas is GL_R8 on ES3 and GL_LUMINANCE on ES2 (see
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
//! per-glyph draw call. The line is shaped through
|
//! per-glyph draw call. The line is shaped through
|
||||||
//! [`crate::text_shaping::shape_line`] (Unicode BiDi + per-sub-run
|
//! [`crate::text_shaping::shape_line`] (Unicode BiDi + per-sub-run
|
||||||
//! rustybuzz / HarfBuzz) and each shaped glyph is rasterised by
|
//! rustybuzz / HarfBuzz) and each shaped glyph is rasterised by
|
||||||
//! glyph index via `fontdue::Font::rasterize_indexed`, so the cache
|
//! glyph index via [`FontFace::rasterize_indexed`], so the cache
|
||||||
//! is keyed on `(glyph_id, size_bits, font_id)` and Arabic
|
//! is keyed on `(glyph_id, size_bits, font_id)` and Arabic
|
||||||
//! connected forms / Devanagari clusters / CJK shaped glyphs are
|
//! connected forms / Devanagari clusters / CJK shaped glyphs are
|
||||||
//! cached correctly without colliding with the source codepoints.
|
//! cached correctly without colliding with the source codepoints.
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use fontdue::Font;
|
use crate::font_face::FontFace;
|
||||||
use glow::HasContext;
|
use glow::HasContext;
|
||||||
|
|
||||||
use crate::types::Color;
|
use crate::types::Color;
|
||||||
@@ -27,7 +27,7 @@ use super::{ ATLAS_SIZE, GlesCanvas, GlyphEntry };
|
|||||||
|
|
||||||
const GLYPH_CACHE_SOFT_CAP: usize = 8192;
|
const GLYPH_CACHE_SOFT_CAP: usize = 8192;
|
||||||
|
|
||||||
fn font_id( font: &Arc<Font> ) -> usize
|
fn font_id( font: &Arc<FontFace> ) -> usize
|
||||||
{
|
{
|
||||||
Arc::as_ptr( font ) as usize
|
Arc::as_ptr( font ) as usize
|
||||||
}
|
}
|
||||||
@@ -47,12 +47,12 @@ impl GlesCanvas
|
|||||||
/// Like [`Self::draw_text`] but leads the resolver with `font` instead of the
|
/// Like [`Self::draw_text`] but leads the resolver with `font` instead of the
|
||||||
/// canvas default, falling back to the system chain for codepoints it does not
|
/// canvas default, falling back to the system chain for codepoints it does not
|
||||||
/// cover.
|
/// cover.
|
||||||
pub fn draw_text_with_font( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: &Arc<Font> )
|
pub fn draw_text_with_font( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: &Arc<FontFace> )
|
||||||
{
|
{
|
||||||
self.draw_text_inner( text, x, y, size, color, Some( font ) );
|
self.draw_text_inner( text, x, y, size, color, Some( font ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_text_inner( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: Option<&Arc<Font>> )
|
fn draw_text_inner( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: Option<&Arc<FontFace>> )
|
||||||
{
|
{
|
||||||
self.activate_target();
|
self.activate_target();
|
||||||
let scaled = size * self.dpi_scale;
|
let scaled = size * self.dpi_scale;
|
||||||
@@ -110,7 +110,7 @@ impl GlesCanvas
|
|||||||
// `Arc<Font>::as_ptr` of the resolved handle, so we walk
|
// `Arc<Font>::as_ptr` of the resolved handle, so we walk
|
||||||
// every codepoint and ask the same resolver — exactly once
|
// every codepoint and ask the same resolver — exactly once
|
||||||
// per distinct id thanks to the dedup loop.
|
// per distinct id thanks to the dedup loop.
|
||||||
let mut fonts: Vec<( usize, Arc<Font> )> = Vec::new();
|
let mut fonts: Vec<( usize, Arc<FontFace> )> = Vec::new();
|
||||||
if !canvas_handle.bytes.is_empty()
|
if !canvas_handle.bytes.is_empty()
|
||||||
{
|
{
|
||||||
fonts.push( ( font_id( &canvas_handle.font ), Arc::clone( &canvas_handle.font ) ) );
|
fonts.push( ( font_id( &canvas_handle.font ), Arc::clone( &canvas_handle.font ) ) );
|
||||||
@@ -305,12 +305,12 @@ impl GlesCanvas
|
|||||||
|
|
||||||
/// Like [`Self::measure_text`] but measures with `font` leading the resolver,
|
/// Like [`Self::measure_text`] but measures with `font` leading the resolver,
|
||||||
/// so text laid out at one weight and drawn at another stays aligned.
|
/// so text laid out at one weight and drawn at another stays aligned.
|
||||||
pub fn measure_text_with_font( &self, text: &str, size: f32, font: &Arc<Font> ) -> f32
|
pub fn measure_text_with_font( &self, text: &str, size: f32, font: &Arc<FontFace> ) -> f32
|
||||||
{
|
{
|
||||||
self.measure_inner( text, size, Some( font ) )
|
self.measure_inner( text, size, Some( font ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
fn measure_inner( &self, text: &str, size: f32, font: Option<&Arc<Font>> ) -> f32
|
fn measure_inner( &self, text: &str, size: f32, font: Option<&Arc<FontFace>> ) -> f32
|
||||||
{
|
{
|
||||||
let scaled = size * self.dpi_scale;
|
let scaled = size * self.dpi_scale;
|
||||||
let canvas_handle = self.font_handle();
|
let canvas_handle = self.font_handle();
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ impl<A: App> AppData<A>
|
|||||||
self.show_context_menu( focus, idx, pos );
|
self.show_context_menu( focus, idx, pos );
|
||||||
} else {
|
} else {
|
||||||
self.hide_context_menu( focus );
|
self.hide_context_menu( focus );
|
||||||
|
self.app.on_pointer_secondary( pos.x, pos.y );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -13,7 +13,7 @@
|
|||||||
//! A lightweight Wayland UI toolkit built on top of
|
//! A lightweight Wayland UI toolkit built on top of
|
||||||
//! [smithay-client-toolkit](https://crates.io/crates/smithay-client-toolkit),
|
//! [smithay-client-toolkit](https://crates.io/crates/smithay-client-toolkit),
|
||||||
//! [tiny-skia](https://crates.io/crates/tiny-skia) and
|
//! [tiny-skia](https://crates.io/crates/tiny-skia) and
|
||||||
//! [fontdue](https://crates.io/crates/fontdue).
|
//! [rustybuzz](https://crates.io/crates/rustybuzz).
|
||||||
//!
|
//!
|
||||||
//! ltk is the UI toolkit for the Liberux desktop. The Liberux compositor (Forge)
|
//! ltk is the UI toolkit for the Liberux desktop. The Liberux compositor (Forge)
|
||||||
//! handles window management, decorations, and positioning — ltk focuses on
|
//! handles window management, decorations, and positioning — ltk focuses on
|
||||||
@@ -284,6 +284,7 @@ rust_i18n::i18n!( "locales", fallback = "en" );
|
|||||||
pub( crate ) static TEST_GLOBALS_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new( () );
|
pub( crate ) static TEST_GLOBALS_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new( () );
|
||||||
|
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
pub mod font_face;
|
||||||
pub( crate ) mod render;
|
pub( crate ) mod render;
|
||||||
pub( crate ) mod system_fonts;
|
pub( crate ) mod system_fonts;
|
||||||
pub( crate ) mod text_shaping;
|
pub( crate ) mod text_shaping;
|
||||||
@@ -344,6 +345,7 @@ pub use theme::{ decode_svg_bytes, icon_path as theme_icon_path, icon_rgba as th
|
|||||||
pub use gles_render::{ BorrowedGlesTexture, GlesVersion };
|
pub use gles_render::{ BorrowedGlesTexture, GlesVersion };
|
||||||
pub use render::is_software_render;
|
pub use render::is_software_render;
|
||||||
pub use render::Canvas;
|
pub use render::Canvas;
|
||||||
|
pub use font_face::{ FontFace, LineMetrics, Metrics, OutlineBounds };
|
||||||
pub use wallpaper::{ WallpaperBundle, ImageData };
|
pub use wallpaper::{ WallpaperBundle, ImageData };
|
||||||
pub use nav_stack::NavStack;
|
pub use nav_stack::NavStack;
|
||||||
pub use chassis::{ set_default_theme, theme_logo_rgba, theme_icon_tinted, branding_bundle_or_solid, wallpaper_bundle_or_solid, backdrop };
|
pub use chassis::{ set_default_theme, theme_logo_rgba, theme_icon_tinted, branding_bundle_or_solid, wallpaper_bundle_or_solid, backdrop };
|
||||||
|
|||||||
+12
-12
@@ -4,7 +4,7 @@
|
|||||||
//! Rendering surface used by every widget.
|
//! Rendering surface used by every widget.
|
||||||
//!
|
//!
|
||||||
//! [`Canvas`] is a thin enum wrapper over the per-frame rendering
|
//! [`Canvas`] is a thin enum wrapper over the per-frame rendering
|
||||||
//! backend. The CPU backend is [`SoftwareCanvas`] (tiny-skia + fontdue
|
//! backend. The CPU backend is [`SoftwareCanvas`] (tiny-skia, glyphs
|
||||||
//! rasterised into a `Pixmap`). The GPU backend is
|
//! rasterised into a `Pixmap`). The GPU backend is
|
||||||
//! [`crate::gles_render::GlesCanvas`] (EGL + GLES2/3).
|
//! [`crate::gles_render::GlesCanvas`] (EGL + GLES2/3).
|
||||||
//!
|
//!
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use fontdue::{ Font, LineMetrics, Metrics };
|
use crate::font_face::{ FontFace, LineMetrics, Metrics };
|
||||||
use tiny_skia::{ Mask, Pixmap };
|
use tiny_skia::{ Mask, Pixmap };
|
||||||
|
|
||||||
use crate::gles_render::{ BorrowedGlesTexture, GlesCanvas, GlesVersion };
|
use crate::gles_render::{ BorrowedGlesTexture, GlesCanvas, GlesVersion };
|
||||||
@@ -81,7 +81,7 @@ pub fn is_software_render() -> bool
|
|||||||
|
|
||||||
/// Cache key for a rasterized glyph. `size_bits` is the f32 bit
|
/// Cache key for a rasterized glyph. `size_bits` is the f32 bit
|
||||||
/// pattern of `size * dpi_scale`; `font_id` is the address of the
|
/// pattern of `size * dpi_scale`; `font_id` is the address of the
|
||||||
/// `Arc<Font>` used for the rasterisation, so distinct weights /
|
/// `Arc<FontFace>` used for the rasterisation, so distinct weights /
|
||||||
/// families of the same `(glyph_id, size)` do not collide on the
|
/// families of the same `(glyph_id, size)` do not collide on the
|
||||||
/// cache. `glyph_id` is the per-font glyph index returned by
|
/// cache. `glyph_id` is the per-font glyph index returned by
|
||||||
/// HarfBuzz shaping, so cached entries persist across script
|
/// HarfBuzz shaping, so cached entries persist across script
|
||||||
@@ -107,7 +107,7 @@ pub ( super ) struct GlyphEntry
|
|||||||
// ─── SoftwareCanvas ──────────────────────────────────────────────────────────
|
// ─── SoftwareCanvas ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Software rendering backend backed by a tiny-skia [`Pixmap`] and a
|
/// Software rendering backend backed by a tiny-skia [`Pixmap`] and a
|
||||||
/// fontdue [`Font`].
|
/// [`FontFace`].
|
||||||
///
|
///
|
||||||
/// Wrapped by [`Canvas`] so the GPU backend can be slotted in by the
|
/// Wrapped by [`Canvas`] so the GPU backend can be slotted in by the
|
||||||
/// runtime without changing widget code. Widgets themselves never see
|
/// runtime without changing widget code. Widgets themselves never see
|
||||||
@@ -122,10 +122,10 @@ pub struct SoftwareCanvas
|
|||||||
/// specific family through [`SoftwareCanvas::font_for`] keep
|
/// specific family through [`SoftwareCanvas::font_for`] keep
|
||||||
/// working. Populated from
|
/// working. Populated from
|
||||||
/// `crate::system_fonts::default_handle` at construction time.
|
/// `crate::system_fonts::default_handle` at construction time.
|
||||||
pub font: Arc<Font>,
|
pub font: Arc<FontFace>,
|
||||||
/// Raw bytes of the default font. Kept alongside `font` so the
|
/// Raw bytes of the default font. Kept alongside `font` so the
|
||||||
/// HarfBuzz shaper (rustybuzz) can be invoked without re-reading
|
/// HarfBuzz shaper (rustybuzz) can be invoked without re-reading
|
||||||
/// the file — fontdue does not expose its internal byte buffer.
|
/// the file; the face names the same buffer.
|
||||||
pub font_bytes: Arc<Vec<u8>>,
|
pub font_bytes: Arc<Vec<u8>>,
|
||||||
/// TTC sub-face index for the default font (0 for single-face
|
/// TTC sub-face index for the default font (0 for single-face
|
||||||
/// files; collection index for `.ttc` archives).
|
/// files; collection index for `.ttc` archives).
|
||||||
@@ -176,7 +176,7 @@ pub struct SoftwareCanvas
|
|||||||
/// inlinable in the hot path.
|
/// inlinable in the hot path.
|
||||||
pub enum Canvas
|
pub enum Canvas
|
||||||
{
|
{
|
||||||
/// CPU rasterisation via tiny-skia + fontdue, written to a
|
/// CPU rasterisation via tiny-skia, written to a
|
||||||
/// `wl_shm` buffer.
|
/// `wl_shm` buffer.
|
||||||
Software( SoftwareCanvas ),
|
Software( SoftwareCanvas ),
|
||||||
/// GPU rasterisation via EGL + GLES 2/3. Presents via
|
/// GPU rasterisation via EGL + GLES 2/3. Presents via
|
||||||
@@ -495,10 +495,10 @@ impl Canvas
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared font handle. Exposed so widgets that need raw `fontdue`
|
/// Shared font handle. Exposed so widgets that need raw face
|
||||||
/// access (e.g. `Text` for ascent/descent) do not have to go
|
/// access (e.g. `Text` for ascent/descent) do not have to go
|
||||||
/// through wrappers for every metric they read.
|
/// through wrappers for every metric they read.
|
||||||
pub fn font( &self ) -> &Font
|
pub fn font( &self ) -> &FontFace
|
||||||
{
|
{
|
||||||
match self
|
match self
|
||||||
{
|
{
|
||||||
@@ -523,7 +523,7 @@ impl Canvas
|
|||||||
/// Resolve a specific font via the theme registry, falling back
|
/// Resolve a specific font via the theme registry, falling back
|
||||||
/// to the system-default [`Self::font`] when no registry is
|
/// to the system-default [`Self::font`] when no registry is
|
||||||
/// installed or the triple cannot be satisfied.
|
/// installed or the triple cannot be satisfied.
|
||||||
pub fn font_for( &self, family: &str, weight: u16, style: FontStyle ) -> Arc<Font>
|
pub fn font_for( &self, family: &str, weight: u16, style: FontStyle ) -> Arc<FontFace>
|
||||||
{
|
{
|
||||||
match self
|
match self
|
||||||
{
|
{
|
||||||
@@ -911,7 +911,7 @@ impl Canvas
|
|||||||
/// Draw `text` with an explicitly supplied font instead of the
|
/// Draw `text` with an explicitly supplied font instead of the
|
||||||
/// canvas default. Use [`Self::font_for`] to resolve a `(family,
|
/// canvas default. Use [`Self::font_for`] to resolve a `(family,
|
||||||
/// weight, style)` triple from the active theme registry first.
|
/// weight, style)` triple from the active theme registry first.
|
||||||
pub fn draw_text_with_font( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: &Arc<Font> )
|
pub fn draw_text_with_font( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: &Arc<FontFace> )
|
||||||
{
|
{
|
||||||
match self
|
match self
|
||||||
{
|
{
|
||||||
@@ -932,7 +932,7 @@ impl Canvas
|
|||||||
/// Width of `text` rendered with `font`. Mirrors
|
/// Width of `text` rendered with `font`. Mirrors
|
||||||
/// [`Self::measure_text`] but bypasses the canvas default font so
|
/// [`Self::measure_text`] but bypasses the canvas default font so
|
||||||
/// text laid out at one weight and drawn at another stays aligned.
|
/// text laid out at one weight and drawn at another stays aligned.
|
||||||
pub fn measure_text_with_font( &self, text: &str, size: f32, font: &Arc<Font> ) -> f32
|
pub fn measure_text_with_font( &self, text: &str, size: f32, font: &Arc<FontFace> ) -> f32
|
||||||
{
|
{
|
||||||
match self
|
match self
|
||||||
{
|
{
|
||||||
|
|||||||
+3
-3
@@ -7,7 +7,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use fontdue::Font;
|
use crate::font_face::FontFace;
|
||||||
use tiny_skia::{ Pixmap, PixmapPaint, Transform };
|
use tiny_skia::{ Pixmap, PixmapPaint, Transform };
|
||||||
|
|
||||||
use crate::system_fonts::default_handle;
|
use crate::system_fonts::default_handle;
|
||||||
@@ -78,7 +78,7 @@ impl SoftwareCanvas
|
|||||||
/// Resolve a specific font from the theme registry, falling back
|
/// Resolve a specific font from the theme registry, falling back
|
||||||
/// to the canvas' default [`Self::font`] when no registry is
|
/// to the canvas' default [`Self::font`] when no registry is
|
||||||
/// installed or the triple cannot be satisfied.
|
/// installed or the triple cannot be satisfied.
|
||||||
pub fn font_for( &self, family: &str, weight: u16, style: FontStyle ) -> Arc<Font>
|
pub fn font_for( &self, family: &str, weight: u16, style: FontStyle ) -> Arc<FontFace>
|
||||||
{
|
{
|
||||||
self.font_registry
|
self.font_registry
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -91,7 +91,7 @@ impl SoftwareCanvas
|
|||||||
/// fallback chain (lazy load of the relevant Noto pack). Falls
|
/// fallback chain (lazy load of the relevant Noto pack). Falls
|
||||||
/// back to the primary (which paints a `.notdef` box) when no
|
/// back to the primary (which paints a `.notdef` box) when no
|
||||||
/// installed fallback covers the codepoint.
|
/// installed fallback covers the codepoint.
|
||||||
pub fn font_for_char( &self, ch: char ) -> Arc<Font>
|
pub fn font_for_char( &self, ch: char ) -> Arc<FontFace>
|
||||||
{
|
{
|
||||||
if self.font.lookup_glyph_index( ch ) != 0
|
if self.font.lookup_glyph_index( ch ) != 0
|
||||||
{
|
{
|
||||||
|
|||||||
+8
-8
@@ -4,7 +4,7 @@
|
|||||||
//! Text rendering for [`SoftwareCanvas`]. The line is shaped through
|
//! Text rendering for [`SoftwareCanvas`]. The line is shaped through
|
||||||
//! [`crate::text_shaping::shape_line`] (BiDi reordering + per-sub-run
|
//! [`crate::text_shaping::shape_line`] (BiDi reordering + per-sub-run
|
||||||
//! rustybuzz / HarfBuzz shaping) and each shaped glyph is rasterised
|
//! rustybuzz / HarfBuzz shaping) and each shaped glyph is rasterised
|
||||||
//! by glyph index via `fontdue::Font::rasterize_indexed`. The cache
|
//! by glyph index via [`FontFace::rasterize_indexed`]. The cache
|
||||||
//! is keyed on `(glyph_id, size, font_id)` so Arabic connected
|
//! is keyed on `(glyph_id, size, font_id)` so Arabic connected
|
||||||
//! forms, Devanagari clusters, and CJK shaped glyphs cluster
|
//! forms, Devanagari clusters, and CJK shaped glyphs cluster
|
||||||
//! correctly without colliding with the per-codepoint cache the
|
//! correctly without colliding with the per-codepoint cache the
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use fontdue::Font;
|
use crate::font_face::FontFace;
|
||||||
|
|
||||||
use crate::types::Color;
|
use crate::types::Color;
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ impl SoftwareCanvas
|
|||||||
/// the result under `(glyph_id, size_bits, font_id)`. Used by
|
/// the result under `(glyph_id, size_bits, font_id)`. Used by
|
||||||
/// both the public `draw_text` and `draw_text_with_font` paths
|
/// both the public `draw_text` and `draw_text_with_font` paths
|
||||||
/// after shaping has resolved every codepoint to a glyph index.
|
/// after shaping has resolved every codepoint to a glyph index.
|
||||||
fn rasterize_indexed_cached( &mut self, font: &Arc<Font>, glyph_id: u16, scaled: f32, font_id: usize ) -> &GlyphEntry
|
fn rasterize_indexed_cached( &mut self, font: &Arc<FontFace>, glyph_id: u16, scaled: f32, font_id: usize ) -> &GlyphEntry
|
||||||
{
|
{
|
||||||
let key = GlyphKey { glyph_id, size_bits: scaled.to_bits(), font_id };
|
let key = GlyphKey { glyph_id, size_bits: scaled.to_bits(), font_id };
|
||||||
self.evict_if_full( &key );
|
self.evict_if_full( &key );
|
||||||
@@ -59,12 +59,12 @@ impl SoftwareCanvas
|
|||||||
|
|
||||||
/// Draw `text` using the explicitly supplied font instead of the
|
/// Draw `text` using the explicitly supplied font instead of the
|
||||||
/// canvas default + fallback chain.
|
/// canvas default + fallback chain.
|
||||||
pub fn draw_text_with_font( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: &Arc<Font> )
|
pub fn draw_text_with_font( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: &Arc<FontFace> )
|
||||||
{
|
{
|
||||||
self.draw_text_inner( text, x, y, size, color, Some( font ) );
|
self.draw_text_inner( text, x, y, size, color, Some( font ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_text_inner( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: Option<&Arc<Font>> )
|
fn draw_text_inner( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: Option<&Arc<FontFace>> )
|
||||||
{
|
{
|
||||||
let scaled = size * self.dpi_scale;
|
let scaled = size * self.dpi_scale;
|
||||||
let line_h = scaled * 1.5;
|
let line_h = scaled * 1.5;
|
||||||
@@ -124,7 +124,7 @@ impl SoftwareCanvas
|
|||||||
// rasterization — we keep a per-font-id index into a small
|
// rasterization — we keep a per-font-id index into a small
|
||||||
// vec of `Arc<Font>` so the rasterizer step does not have to
|
// vec of `Arc<Font>` so the rasterizer step does not have to
|
||||||
// re-walk the resolve fn (mut self borrow conflicts).
|
// re-walk the resolve fn (mut self borrow conflicts).
|
||||||
let mut fonts: Vec<( usize, Arc<Font> )> = Vec::new();
|
let mut fonts: Vec<( usize, Arc<FontFace> )> = Vec::new();
|
||||||
let primary_id = Arc::as_ptr( &canvas_handle.font ) as usize;
|
let primary_id = Arc::as_ptr( &canvas_handle.font ) as usize;
|
||||||
if !canvas_handle.bytes.is_empty()
|
if !canvas_handle.bytes.is_empty()
|
||||||
{
|
{
|
||||||
@@ -224,12 +224,12 @@ impl SoftwareCanvas
|
|||||||
self.measure_with_font( text, size, None )
|
self.measure_with_font( text, size, None )
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn measure_text_with_font( &self, text: &str, size: f32, font: &Arc<Font> ) -> f32
|
pub fn measure_text_with_font( &self, text: &str, size: f32, font: &Arc<FontFace> ) -> f32
|
||||||
{
|
{
|
||||||
self.measure_with_font( text, size, Some( font ) )
|
self.measure_with_font( text, size, Some( font ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
fn measure_with_font( &self, text: &str, size: f32, font: Option<&Arc<Font>> ) -> f32
|
fn measure_with_font( &self, text: &str, size: f32, font: Option<&Arc<FontFace>> ) -> f32
|
||||||
{
|
{
|
||||||
let scaled = size * self.dpi_scale;
|
let scaled = size * self.dpi_scale;
|
||||||
let canvas_handle =
|
let canvas_handle =
|
||||||
|
|||||||
+35
-35
@@ -10,11 +10,11 @@
|
|||||||
//! that band — Cyrillic, Devanagari, Arabic, Hebrew, Thai, the CJK
|
//! that band — Cyrillic, Devanagari, Arabic, Hebrew, Thai, the CJK
|
||||||
//! ideographic block — relies on a chain of system Noto fonts loaded
|
//! ideographic block — relies on a chain of system Noto fonts loaded
|
||||||
//! on demand: a fallback file is read off disk the first time some
|
//! on demand: a fallback file is read off disk the first time some
|
||||||
//! glyph asks for it and the resulting `Arc<Font>` is cached
|
//! glyph asks for it and the resulting `Arc<FontFace>` is cached
|
||||||
//! process-wide.
|
//! process-wide.
|
||||||
//!
|
//!
|
||||||
//! Each `(path, face)` entry in [`FALLBACK_FONT_CANDIDATES`] gets its
|
//! Each `(path, face)` entry in [`FALLBACK_FONT_CANDIDATES`] gets its
|
||||||
//! own `OnceLock<Option<Arc<Font>>>` slot. `Some(font)` means the
|
//! own `OnceLock<Option<Arc<FontFace>>>` slot. `Some(font)` means the
|
||||||
//! file was read and parsed successfully; `None` means it's missing
|
//! file was read and parsed successfully; `None` means it's missing
|
||||||
//! or unparseable, locked in for the rest of the process so we don't
|
//! or unparseable, locked in for the rest of the process so we don't
|
||||||
//! re-`stat` the same path on every glyph miss. The `OnceLock`
|
//! re-`stat` the same path on every glyph miss. The `OnceLock`
|
||||||
@@ -26,28 +26,28 @@
|
|||||||
|
|
||||||
use std::sync::{ Arc, OnceLock };
|
use std::sync::{ Arc, OnceLock };
|
||||||
|
|
||||||
use fontdue::{ Font, FontSettings };
|
use crate::font_face::FontFace;
|
||||||
|
|
||||||
/// Bytes-aware font handle. The `font` is what fontdue rasterises
|
/// Bytes-aware font handle. The `font` is what glyphs rasterise
|
||||||
/// against; `bytes` is the raw OpenType / TrueType buffer that
|
/// against; `bytes` is the raw OpenType / TrueType buffer that
|
||||||
/// rustybuzz (HarfBuzz) requires for shaping. They are owned
|
/// rustybuzz (HarfBuzz) requires for shaping. Both name the same
|
||||||
/// separately because fontdue does not expose its internal byte
|
/// buffer — the face holds its own `Arc` so it can pull outlines in
|
||||||
/// buffer — we keep both alive in lockstep instead.
|
/// later — and are kept side by side so shaping needs no second read.
|
||||||
///
|
///
|
||||||
/// `face` is the TTC sub-face index (0 for single-face files, the
|
/// `face` is the TTC sub-face index (0 for single-face files, the
|
||||||
/// face index for collections like Noto Sans CJK).
|
/// face index for collections like Noto Sans CJK).
|
||||||
#[ derive( Clone ) ]
|
#[ derive( Clone ) ]
|
||||||
pub struct FontHandle
|
pub struct FontHandle
|
||||||
{
|
{
|
||||||
pub font: Arc<Font>,
|
pub font: Arc<FontFace>,
|
||||||
pub bytes: Arc<Vec<u8>>,
|
pub bytes: Arc<Vec<u8>>,
|
||||||
pub face: u32,
|
pub face: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-script fallback font path with the TrueType-collection face
|
/// Per-script fallback font path with the TrueType-collection face
|
||||||
/// index fontdue should load (most files are single-face → 0; CJK
|
/// index to load (most files are single-face → 0; CJK `.ttc` archives
|
||||||
/// `.ttc` archives carry many faces, see the SC face on the canonical
|
/// carry many faces, see the SC face on the canonical Adobe-built
|
||||||
/// Adobe-built `NotoSansCJK-Regular.ttc`).
|
/// `NotoSansCJK-Regular.ttc`).
|
||||||
struct FallbackFontSpec
|
struct FallbackFontSpec
|
||||||
{
|
{
|
||||||
path: &'static str,
|
path: &'static str,
|
||||||
@@ -58,8 +58,9 @@ struct FallbackFontSpec
|
|||||||
/// the first slot whose font owns a non-zero glyph index for the
|
/// the first slot whose font owns a non-zero glyph index for the
|
||||||
/// codepoint wins, so the broadest families come first (Noto Sans
|
/// codepoint wins, so the broadest families come first (Noto Sans
|
||||||
/// covers Cyrillic, Greek, extended Latin) and the script-specific
|
/// covers Cyrillic, Greek, extended Latin) and the script-specific
|
||||||
/// + CJK packs trail. DejaVu is the last resort — most distros carry
|
/// + CJK packs trail. DejaVu goes ahead of the CJK collection: it owns
|
||||||
/// it under one path or another.
|
/// the geometric shapes, arrows and box drawing that would otherwise
|
||||||
|
/// pull a 19 MB collection in for a single bullet.
|
||||||
const FALLBACK_FONT_CANDIDATES: &[ FallbackFontSpec ] =
|
const FALLBACK_FONT_CANDIDATES: &[ FallbackFontSpec ] =
|
||||||
&[
|
&[
|
||||||
// Noto Sans — Cyrillic, Greek, extended Latin.
|
// Noto Sans — Cyrillic, Greek, extended Latin.
|
||||||
@@ -75,25 +76,25 @@ const FALLBACK_FONT_CANDIDATES: &[ FallbackFontSpec ] =
|
|||||||
FallbackFontSpec { path: "/usr/share/fonts/truetype/noto/NotoSansHebrew-Regular.ttf", face: 0 },
|
FallbackFontSpec { path: "/usr/share/fonts/truetype/noto/NotoSansHebrew-Regular.ttf", face: 0 },
|
||||||
FallbackFontSpec { path: "/usr/share/fonts/truetype/noto/NotoSansThai-Regular.ttf", face: 0 },
|
FallbackFontSpec { path: "/usr/share/fonts/truetype/noto/NotoSansThai-Regular.ttf", face: 0 },
|
||||||
|
|
||||||
|
// DejaVu — broad-but-shallow coverage of most scripts, plus the
|
||||||
|
// symbol, arrow and box-drawing blocks. Ships almost everywhere.
|
||||||
|
FallbackFontSpec { path: "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", face: 0 },
|
||||||
|
|
||||||
// CJK packs ship as `.ttc`. The collection layout on the canonical
|
// CJK packs ship as `.ttc`. The collection layout on the canonical
|
||||||
// Adobe-built `NotoSansCJK-Regular.ttc` is 0=JP, 1=KR, 2=SC, 3=TC,
|
// Adobe-built `NotoSansCJK-Regular.ttc` is 0=JP, 1=KR, 2=SC, 3=TC,
|
||||||
// 4=HK; we want CJK shared ideographs available in any locale, so
|
// 4=HK; we want CJK shared ideographs available in any locale, so
|
||||||
// any of the faces is fine — pick SC (face 2) as the broadest
|
// any of the faces is fine — pick SC (face 2) as the broadest
|
||||||
// baseline. ~30 MB on disk; under the lazy loader this only fires
|
// baseline. Last, because probing a slot reads its whole file, and
|
||||||
// when a user-visible string actually contains a CJK codepoint.
|
// this one is 19 MB.
|
||||||
FallbackFontSpec { path: "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", face: 2 },
|
FallbackFontSpec { path: "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", face: 2 },
|
||||||
FallbackFontSpec { path: "/usr/share/fonts/opentype/noto-cjk/NotoSansCJK-Regular.ttc", face: 2 },
|
FallbackFontSpec { path: "/usr/share/fonts/opentype/noto-cjk/NotoSansCJK-Regular.ttc", face: 2 },
|
||||||
FallbackFontSpec { path: "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc", face: 2 },
|
FallbackFontSpec { path: "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc", face: 2 },
|
||||||
|
|
||||||
// Last resort — DejaVu has broad-but-shallow coverage of most
|
|
||||||
// scripts and ships almost everywhere.
|
|
||||||
FallbackFontSpec { path: "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", face: 0 },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Per-slot lazy state, two stages so probing a slot for coverage never
|
/// Per-slot lazy state, two stages so probing a slot for coverage never
|
||||||
/// pays the fontdue parse: `bytes` is the file read once, `font` is the
|
/// builds a face: `bytes` is the file read once, `font` is the face
|
||||||
/// fontdue face built only when a glyph in this slot is actually needed.
|
/// built only when a glyph in this slot is actually needed. `None` in a
|
||||||
/// `None` in a resolved stage is a sticky negative result.
|
/// resolved stage is a sticky negative result.
|
||||||
struct Slot
|
struct Slot
|
||||||
{
|
{
|
||||||
bytes: OnceLock<Option<Arc<Vec<u8>>>>,
|
bytes: OnceLock<Option<Arc<Vec<u8>>>>,
|
||||||
@@ -133,17 +134,16 @@ fn slot_covers( idx: usize, ch: char ) -> bool
|
|||||||
.is_some()
|
.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build (once) the fontdue face for slot `idx`. The raw bytes are
|
/// Build (once) the face for slot `idx`. The raw bytes are preserved
|
||||||
/// preserved inside the handle so the same `Arc<Vec<u8>>` can be handed
|
/// inside the handle so the same `Arc<Vec<u8>>` can be handed to
|
||||||
/// to rustybuzz for shaping without re-reading the file.
|
/// rustybuzz for shaping without re-reading the file.
|
||||||
fn slot_handle( idx: usize ) -> Option<FontHandle>
|
fn slot_handle( idx: usize ) -> Option<FontHandle>
|
||||||
{
|
{
|
||||||
slots()[ idx ].font.get_or_init( ||
|
slots()[ idx ].font.get_or_init( ||
|
||||||
{
|
{
|
||||||
let bytes = slot_bytes( idx )?;
|
let bytes = slot_bytes( idx )?;
|
||||||
let face = FALLBACK_FONT_CANDIDATES[ idx ].face;
|
let face = FALLBACK_FONT_CANDIDATES[ idx ].face;
|
||||||
let opts = FontSettings { collection_index: face, ..FontSettings::default() };
|
let font = FontFace::from_bytes( Arc::clone( &bytes ), face )?;
|
||||||
let font = Font::from_bytes( bytes.as_slice(), opts ).ok()?;
|
|
||||||
Some( FontHandle { font: Arc::new( font ), bytes, face } )
|
Some( FontHandle { font: Arc::new( font ), bytes, face } )
|
||||||
} )
|
} )
|
||||||
.clone()
|
.clone()
|
||||||
@@ -151,10 +151,10 @@ fn slot_handle( idx: usize ) -> Option<FontHandle>
|
|||||||
|
|
||||||
/// Find the first fallback font that has a non-zero glyph index for
|
/// Find the first fallback font that has a non-zero glyph index for
|
||||||
/// `ch`, loading it from disk on the first hit and caching the
|
/// `ch`, loading it from disk on the first hit and caching the
|
||||||
/// `Arc<Font>` for the rest of the process. Returns `None` if no
|
/// `Arc<FontFace>` for the rest of the process. Returns `None` if no
|
||||||
/// installed fallback covers the codepoint — the caller then paints
|
/// installed fallback covers the codepoint — the caller then paints
|
||||||
/// the primary font's `.notdef` rather than dropping the glyph.
|
/// the primary font's `.notdef` rather than dropping the glyph.
|
||||||
pub fn lookup( ch: char ) -> Option<Arc<Font>>
|
pub fn lookup( ch: char ) -> Option<Arc<FontFace>>
|
||||||
{
|
{
|
||||||
lookup_handle( ch ).map( |h| h.font )
|
lookup_handle( ch ).map( |h| h.font )
|
||||||
}
|
}
|
||||||
@@ -215,9 +215,9 @@ pub ( crate ) fn default_handle() -> FontHandle
|
|||||||
static PRIMARY: OnceLock<FontHandle> = OnceLock::new();
|
static PRIMARY: OnceLock<FontHandle> = OnceLock::new();
|
||||||
PRIMARY.get_or_init( ||
|
PRIMARY.get_or_init( ||
|
||||||
{
|
{
|
||||||
let bytes = load_default_font_bytes();
|
let bytes = Arc::new( load_default_font_bytes() );
|
||||||
let font = Font::from_bytes( bytes.as_slice(), FontSettings::default() ).expect( "primary font parses" );
|
let font = FontFace::from_bytes( Arc::clone( &bytes ), 0 ).expect( "primary font parses" );
|
||||||
FontHandle { font: Arc::new( font ), bytes: Arc::new( bytes ), face: 0 }
|
FontHandle { font: Arc::new( font ), bytes, face: 0 }
|
||||||
} ).clone()
|
} ).clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,9 +230,9 @@ pub fn primary_handle() -> FontHandle
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Bytes-aware variant of [`lookup`]. Returns the full
|
/// Bytes-aware variant of [`lookup`]. Returns the full
|
||||||
/// [`FontHandle`] (fontdue handle + raw bytes + face index) so
|
/// [`FontHandle`] (face + raw bytes + face index) so callers that
|
||||||
/// callers that need to invoke a HarfBuzz-style shaper can do so
|
/// need to invoke a HarfBuzz-style shaper can do so without
|
||||||
/// without re-reading the font file.
|
/// re-reading the font file.
|
||||||
pub fn lookup_handle( ch: char ) -> Option<FontHandle>
|
pub fn lookup_handle( ch: char ) -> Option<FontHandle>
|
||||||
{
|
{
|
||||||
( 0..FALLBACK_FONT_CANDIDATES.len() )
|
( 0..FALLBACK_FONT_CANDIDATES.len() )
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@
|
|||||||
//! visual order. Renderers consume that sequence directly: each
|
//! visual order. Renderers consume that sequence directly: each
|
||||||
//! `PositionedGlyph` carries the per-font `glyph_id`, the visual
|
//! `PositionedGlyph` carries the per-font `glyph_id`, the visual
|
||||||
//! advance, and ink offsets, which is exactly what
|
//! advance, and ink offsets, which is exactly what
|
||||||
//! `fontdue::Font::rasterize_indexed` needs to render Arabic
|
//! [`crate::font_face::FontFace::rasterize_indexed`] needs to render Arabic
|
||||||
//! connected forms, Devanagari clusters and CJK shaped glyphs
|
//! connected forms, Devanagari clusters and CJK shaped glyphs
|
||||||
//! correctly.
|
//! correctly.
|
||||||
//!
|
//!
|
||||||
|
|||||||
+22
-22
@@ -1,7 +1,7 @@
|
|||||||
// SPDX-License-Identifier: LGPL-2.1-only
|
// SPDX-License-Identifier: LGPL-2.1-only
|
||||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||||
|
|
||||||
//! Runtime font registry: families → (weight, style) → `fontdue::Font`.
|
//! Runtime font registry: families → (weight, style) → [`FontFace`].
|
||||||
//!
|
//!
|
||||||
//! The registry is the live, loaded-in-memory counterpart of the theme's
|
//! The registry is the live, loaded-in-memory counterpart of the theme's
|
||||||
//! `fonts` block ([`super::FontFamilyDef`]). It is built once at theme load
|
//! `fonts` block ([`super::FontFamilyDef`]). It is built once at theme load
|
||||||
@@ -27,7 +27,7 @@ use std::collections::HashMap;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use fontdue::{ Font, FontSettings };
|
use crate::font_face::FontFace;
|
||||||
|
|
||||||
use super::fonts::FontFamilyDef;
|
use super::fonts::FontFamilyDef;
|
||||||
use super::text_style::FontStyle;
|
use super::text_style::FontStyle;
|
||||||
@@ -54,7 +54,7 @@ pub enum FontLoadError
|
|||||||
{
|
{
|
||||||
/// The source file could not be read.
|
/// The source file could not be read.
|
||||||
Io( PathBuf, std::io::Error ),
|
Io( PathBuf, std::io::Error ),
|
||||||
/// The source file was read but `fontdue` rejected its contents.
|
/// The source file was read but its contents did not parse as a face.
|
||||||
Parse( PathBuf, String ),
|
Parse( PathBuf, String ),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,11 +75,11 @@ impl std::error::Error for FontLoadError {}
|
|||||||
// ─── Registry ────────────────────────────────────────────────────────────────
|
// ─── Registry ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// A loaded font registry: the theme's declared families materialised into
|
/// A loaded font registry: the theme's declared families materialised into
|
||||||
/// live `fontdue::Font` handles, indexed by family id / weight / style.
|
/// live [`FontFace`] handles, indexed by family id / weight / style.
|
||||||
#[ derive( Debug, Default ) ]
|
#[ derive( Debug, Default ) ]
|
||||||
pub struct FontRegistry
|
pub struct FontRegistry
|
||||||
{
|
{
|
||||||
by_key: HashMap<FontKey, Arc<Font>>,
|
by_key: HashMap<FontKey, Arc<FontFace>>,
|
||||||
fallbacks: HashMap<String, Vec<String>>,
|
fallbacks: HashMap<String, Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ impl FontRegistry
|
|||||||
family: impl Into<String>,
|
family: impl Into<String>,
|
||||||
weight: u16,
|
weight: u16,
|
||||||
style: FontStyle,
|
style: FontStyle,
|
||||||
font: Arc<Font>,
|
font: Arc<FontFace>,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
self.by_key.insert( FontKey { family: family.into(), weight, style }, font );
|
self.by_key.insert( FontKey { family: family.into(), weight, style }, font );
|
||||||
@@ -118,9 +118,9 @@ impl FontRegistry
|
|||||||
/// Whether the registry has no sources loaded.
|
/// Whether the registry has no sources loaded.
|
||||||
pub fn is_empty( &self ) -> bool { self.by_key.is_empty() }
|
pub fn is_empty( &self ) -> bool { self.by_key.is_empty() }
|
||||||
|
|
||||||
/// Resolve a triple to a loaded [`Font`]. See the module docs for the
|
/// Resolve a triple to a loaded [`FontFace`]. See the module docs for the
|
||||||
/// precedence order.
|
/// precedence order.
|
||||||
pub fn resolve( &self, family: &str, weight: u16, style: FontStyle ) -> Option<Arc<Font>>
|
pub fn resolve( &self, family: &str, weight: u16, style: FontStyle ) -> Option<Arc<FontFace>>
|
||||||
{
|
{
|
||||||
// 1. Exact match.
|
// 1. Exact match.
|
||||||
let exact = FontKey { family: family.to_string(), weight, style };
|
let exact = FontKey { family: family.to_string(), weight, style };
|
||||||
@@ -183,9 +183,10 @@ impl FontRegistry
|
|||||||
for src in &family.sources
|
for src in &family.sources
|
||||||
{
|
{
|
||||||
let bytes = std::fs::read( &src.path )
|
let bytes = std::fs::read( &src.path )
|
||||||
|
.map( Arc::new )
|
||||||
.map_err( |e| FontLoadError::Io( src.path.clone(), e ) )?;
|
.map_err( |e| FontLoadError::Io( src.path.clone(), e ) )?;
|
||||||
let font = Font::from_bytes( bytes.as_slice(), FontSettings::default() )
|
let font = FontFace::from_bytes( bytes, 0 )
|
||||||
.map_err( |e| FontLoadError::Parse( src.path.clone(), e.to_string() ) )?;
|
.ok_or_else( || FontLoadError::Parse( src.path.clone(), "unparseable face".to_string() ) )?;
|
||||||
reg.insert( id.clone(), src.weight, src.style, Arc::new( font ) );
|
reg.insert( id.clone(), src.weight, src.style, Arc::new( font ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,7 +219,7 @@ impl FontRegistry
|
|||||||
{
|
{
|
||||||
let bytes = match std::fs::read( &src.path )
|
let bytes = match std::fs::read( &src.path )
|
||||||
{
|
{
|
||||||
Ok( b ) => b,
|
Ok( b ) => Arc::new( b ),
|
||||||
Err( e ) =>
|
Err( e ) =>
|
||||||
{
|
{
|
||||||
eprintln!
|
eprintln!
|
||||||
@@ -229,15 +230,15 @@ impl FontRegistry
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let font = match Font::from_bytes( bytes.as_slice(), FontSettings::default() )
|
let font = match FontFace::from_bytes( bytes, 0 )
|
||||||
{
|
{
|
||||||
Ok( f ) => f,
|
Some( f ) => f,
|
||||||
Err( e ) =>
|
None =>
|
||||||
{
|
{
|
||||||
eprintln!
|
eprintln!
|
||||||
(
|
(
|
||||||
"[ltk] skipping font {} (weight {}, {:?}): parse error: {}",
|
"[ltk] skipping font {} (weight {}, {:?}): unparseable face",
|
||||||
src.path.display(), src.weight, src.style, e
|
src.path.display(), src.weight, src.style
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -258,14 +259,13 @@ mod tests
|
|||||||
|
|
||||||
/// Load a real TTF from the system via the same search chain the
|
/// Load a real TTF from the system via the same search chain the
|
||||||
/// software canvas uses. Only used by tests that need an actual
|
/// software canvas uses. Only used by tests that need an actual
|
||||||
/// `Font` — skipped when no font is found (keeps CI green on images
|
/// `FontFace` — skipped when no font is found (keeps CI green on
|
||||||
/// without the usual system fonts).
|
/// images without the usual system fonts).
|
||||||
fn system_font() -> Option<Arc<Font>>
|
fn system_font() -> Option<Arc<FontFace>>
|
||||||
{
|
{
|
||||||
let path = crate::system_fonts::find_font_opt()?;
|
let path = crate::system_fonts::find_font_opt()?;
|
||||||
let bytes = std::fs::read( path ).ok()?;
|
let bytes = Arc::new( std::fs::read( path ).ok()? );
|
||||||
let font = Font::from_bytes( bytes.as_slice(), FontSettings::default() ).ok()?;
|
Some( Arc::new( FontFace::from_bytes( bytes, 0 )? ) )
|
||||||
Some( Arc::new( font ) )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ test ]
|
#[ test ]
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@
|
|||||||
//!
|
//!
|
||||||
//! This module only models the **declaration**: a family name, a set of
|
//! This module only models the **declaration**: a family name, a set of
|
||||||
//! source files indexed by weight/style, and a fallback chain. Loading the
|
//! source files indexed by weight/style, and a fallback chain. Loading the
|
||||||
//! `.ttf` bytes into `fontdue` and resolving text styles against a live
|
//! `.ttf` bytes into a face and resolving text styles against a live
|
||||||
//! registry is the runtime registry's job (see [`super::FontRegistry`]).
|
//! registry is the runtime registry's job (see [`super::FontRegistry`]).
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -29,7 +29,7 @@ pub struct FontFamilyDef
|
|||||||
/// satisfy is used.
|
/// satisfy is used.
|
||||||
pub fallbacks: Vec<String>,
|
pub fallbacks: Vec<String>,
|
||||||
/// One source per weight/style the family ships. The runtime font
|
/// One source per weight/style the family ships. The runtime font
|
||||||
/// registry registers each source in `fontdue` at load time.
|
/// registry parses each source into a face at load time.
|
||||||
pub sources: Vec<FontSource>,
|
pub sources: Vec<FontSource>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use fontdue::Font;
|
use crate::font_face::FontFace;
|
||||||
|
|
||||||
use crate::theme::FontStyle;
|
use crate::theme::FontStyle;
|
||||||
use crate::types::{ Color, Length, Rect };
|
use crate::types::{ Color, Length, Rect };
|
||||||
@@ -100,12 +100,12 @@ impl<Msg: Clone> RichText<Msg>
|
|||||||
canvas.resolve_font( self.size )
|
canvas.resolve_font( self.size )
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_font( &self, canvas: &Canvas ) -> Option<Arc<Font>>
|
fn resolve_font( &self, canvas: &Canvas ) -> Option<Arc<FontFace>>
|
||||||
{
|
{
|
||||||
self.font.as_ref().map( |( family, weight, style )| canvas.font_for( family, *weight, *style ) )
|
self.font.as_ref().map( |( family, weight, style )| canvas.font_for( family, *weight, *style ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
fn measure( &self, text: &str, canvas: &Canvas, font: Option<&Arc<Font>> ) -> f32
|
fn measure( &self, text: &str, canvas: &Canvas, font: Option<&Arc<FontFace>> ) -> f32
|
||||||
{
|
{
|
||||||
let size = self.resolved_size( canvas );
|
let size = self.resolved_size( canvas );
|
||||||
match font
|
match font
|
||||||
@@ -231,7 +231,7 @@ impl VisualLine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wrap_tracked( content: &str, size: f32, max_width: f32, canvas: &Canvas, font: Option<&Arc<Font>> ) -> Vec<VisualLine>
|
fn wrap_tracked( content: &str, size: f32, max_width: f32, canvas: &Canvas, font: Option<&Arc<FontFace>> ) -> Vec<VisualLine>
|
||||||
{
|
{
|
||||||
let measure = |s: &str| -> f32
|
let measure = |s: &str| -> f32
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use fontdue::Font;
|
use crate::font_face::FontFace;
|
||||||
|
|
||||||
use crate::theme::FontStyle;
|
use crate::theme::FontStyle;
|
||||||
use crate::types::{ Color, Length, Rect };
|
use crate::types::{ Color, Length, Rect };
|
||||||
@@ -77,7 +77,7 @@ impl Text
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the font size against the canvas viewport. Internal
|
/// Resolve the font size against the canvas viewport. Internal
|
||||||
/// helper: every method that needs an `f32` size for a `fontdue`
|
/// helper: every method that needs an `f32` size for a face
|
||||||
/// call routes through this so the field can stay a `Length`.
|
/// call routes through this so the field can stay a `Length`.
|
||||||
#[ inline ]
|
#[ inline ]
|
||||||
fn resolved_size( &self, canvas: &Canvas ) -> f32
|
fn resolved_size( &self, canvas: &Canvas ) -> f32
|
||||||
@@ -160,7 +160,7 @@ impl Text
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_font( &self, canvas: &Canvas ) -> Option<Arc<Font>>
|
fn resolve_font( &self, canvas: &Canvas ) -> Option<Arc<FontFace>>
|
||||||
{
|
{
|
||||||
self.font.as_ref().map( |( family, weight, style )|
|
self.font.as_ref().map( |( family, weight, style )|
|
||||||
{
|
{
|
||||||
@@ -168,7 +168,7 @@ impl Text
|
|||||||
} )
|
} )
|
||||||
}
|
}
|
||||||
|
|
||||||
fn measure( &self, text: &str, canvas: &Canvas, font: Option<&Arc<Font>> ) -> f32
|
fn measure( &self, text: &str, canvas: &Canvas, font: Option<&Arc<FontFace>> ) -> f32
|
||||||
{
|
{
|
||||||
let size = self.resolved_size( canvas );
|
let size = self.resolved_size( canvas );
|
||||||
match font
|
match font
|
||||||
@@ -178,7 +178,7 @@ impl Text
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn measure_char( &self, ch: char, canvas: &Canvas, font: Option<&Arc<Font>> ) -> f32
|
fn measure_char( &self, ch: char, canvas: &Canvas, font: Option<&Arc<FontFace>> ) -> f32
|
||||||
{
|
{
|
||||||
let size = self.resolved_size( canvas );
|
let size = self.resolved_size( canvas );
|
||||||
match font
|
match font
|
||||||
@@ -188,7 +188,7 @@ impl Text
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint( &self, canvas: &mut Canvas, text: &str, x: f32, y: f32, font: Option<&Arc<Font>> )
|
fn paint( &self, canvas: &mut Canvas, text: &str, x: f32, y: f32, font: Option<&Arc<FontFace>> )
|
||||||
{
|
{
|
||||||
let size = self.resolved_size( canvas );
|
let size = self.resolved_size( canvas );
|
||||||
match font
|
match font
|
||||||
@@ -302,7 +302,7 @@ impl Text
|
|||||||
/// than `max_width` overflows on its own line rather than being
|
/// than `max_width` overflows on its own line rather than being
|
||||||
/// hyphenated. Routes through the font override when one is set so
|
/// hyphenated. Routes through the font override when one is set so
|
||||||
/// measurement and rendering agree on advance widths.
|
/// measurement and rendering agree on advance widths.
|
||||||
fn wrap_lines( text: &str, size: f32, max_width: f32, canvas: &Canvas, font: Option<&Arc<Font>> ) -> Vec<String>
|
fn wrap_lines( text: &str, size: f32, max_width: f32, canvas: &Canvas, font: Option<&Arc<FontFace>> ) -> Vec<String>
|
||||||
{
|
{
|
||||||
if max_width <= 0.0 || text.is_empty()
|
if max_width <= 0.0 || text.is_empty()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Pixel-level "golden-like" coverage. We deliberately avoid storing reference
|
// Pixel-level "golden-like" coverage. We deliberately avoid storing reference
|
||||||
// PNG bytes: their value depends on fontdue / tiny-skia / theme versions, so a
|
// PNG bytes: their value depends on the rasteriser / theme versions, so a
|
||||||
// byte-exact reference would break on every dependency bump. Instead these
|
// byte-exact reference would break on every dependency bump. Instead these
|
||||||
// tests assert *structural* properties of the rendered buffer (background
|
// tests assert *structural* properties of the rendered buffer (background
|
||||||
// colour, transparency, determinism, focus-state divergence) plus a few
|
// colour, transparency, determinism, focus-state divergence) plus a few
|
||||||
|
|||||||
Reference in New Issue
Block a user