diff --git a/Cargo.toml b/Cargo.toml index 79d0539..bc45c33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,6 @@ smithay-client-toolkit = { version = "0.20", features = ["calloop", "calloop-way calloop = "0.14" calloop-wayland-source = "0.4" tiny-skia = "0.12" -fontdue = "=0.9.3" unicode-bidi = "0.3" rustybuzz = "0.14" image = { version = "=0.25.9", default-features = false, features = ["png", "jpeg", "webp"] } diff --git a/src/app.rs b/src/app.rs index c5ee34c..bc2c82f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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. 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 { None } + /// Called on every pointer motion (mouse move, button press, button /// release). `x`, `y` are in physical pixels of the main surface, /// 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. 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 /// regular focus-aware dispatch ([`Self::on_key`] / /// [`Self::on_key_with_modifiers`]) runs. Default: no-op. diff --git a/src/event_loop/run.rs b/src/event_loop/run.rs index eabc991..a44745a 100644 --- a/src/event_loop/run.rs +++ b/src/event_loop/run.rs @@ -921,6 +921,12 @@ pub( crate ) fn try_run( mut app: A ) -> Result<(), RunError> 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. // Walks main + every overlay because the targeted widget can live // on any of them (a search field on a launcher overlay, a text diff --git a/src/font_face.rs b/src/font_face.rs new file mode 100644 index 0000000..78fb979 --- /dev/null +++ b/src/font_face.rs @@ -0,0 +1,509 @@ +// SPDX-License-Identifier: LGPL-2.1-only +// Copyright (C) 2026 Liberux Labs, S. L. + +//! 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, + bounds: OutlineBounds, + advance_width: f32, + advance_height: f32, +} + +/// A font face that materialises glyphs on demand. +pub struct FontFace +{ + data: Arc>, + index: u32, + units_per_em: f32, + line: LineMetrics, + outlines: Mutex>>, + indices: Mutex>, +} + +/// 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>, index: u32 ) -> Option + { + 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 + { + 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 ) + { + 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::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 + { + 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 + { + 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 +{ + 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 +{ + 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 = 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 ); + } +} diff --git a/src/gles_render/mod.rs b/src/gles_render/mod.rs index a105eb0..7f26891 100644 --- a/src/gles_render/mod.rs +++ b/src/gles_render/mod.rs @@ -48,7 +48,7 @@ use std::collections::HashMap; use std::sync::Arc; -use fontdue::Font; +use crate::font_face::FontFace; use glow::HasContext; use crate::theme::FontRegistry; @@ -107,7 +107,7 @@ pub struct BorrowedGlesTexture /// Cached glyph: rasterized bitmap placed in the shared atlas. pub ( super ) struct GlyphEntry { - pub ( super ) metrics: fontdue::Metrics, + pub ( super ) metrics: crate::font_face::Metrics, pub ( super ) tex_w: i32, pub ( super ) tex_h: i32, pub ( super ) atlas_x: u32, @@ -118,7 +118,7 @@ pub ( super ) struct GlyphEntry /// software canvas's `GlyphKey`. `glyph_id` is the per-font index /// returned by HarfBuzz shaping; `size_bits` is `f32::to_bits` of /// `size * dpi_scale`; `font_id` is the address of the -/// `Arc` used for rasterisation. +/// `Arc` used for rasterisation. pub( super ) type GlyphAtlasKey = ( u16, u32, usize ); 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`. /// Kept as a fallback for callers that do not route through the /// theme registry. - pub font: Arc, + pub font: Arc, /// Raw bytes of the default font. Required by rustybuzz for /// HarfBuzz shaping (see the `text_shaping` private module). Kept on the /// canvas so the shape pipeline has direct access without a diff --git a/src/gles_render/setup.rs b/src/gles_render/setup.rs index e2d534f..160f44b 100644 --- a/src/gles_render/setup.rs +++ b/src/gles_render/setup.rs @@ -14,7 +14,7 @@ use std::collections::HashMap; use std::sync::Arc; -use fontdue::{ Font, LineMetrics, Metrics }; +use crate::font_face::{ FontFace, LineMetrics, Metrics }; use glow::HasContext; use crate::theme::{ FontRegistry, FontStyle }; @@ -728,7 +728,7 @@ impl GlesCanvas pub fn set_global_alpha( &mut self, a: f32 ) { self.global_alpha = a; } /// 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 /// 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 /// canvas' default [`Self::font`] when no registry is installed or the /// triple cannot be satisfied. - pub fn font_for( &self, family: &str, weight: u16, style: FontStyle ) -> Arc + pub fn font_for( &self, family: &str, weight: u16, style: FontStyle ) -> Arc { self.font_registry .as_ref() @@ -753,7 +753,7 @@ impl GlesCanvas /// fallback chain (lazy load of the relevant Noto pack). Falls /// back to the primary (which paints a `.notdef` box) when no /// installed fallback covers the codepoint. - pub fn font_for_char( &self, ch: char ) -> Arc + pub fn font_for_char( &self, ch: char ) -> Arc { if self.font.lookup_glyph_index( ch ) != 0 { diff --git a/src/gles_render/shaders.rs b/src/gles_render/shaders.rs index 10a3704..13b253f 100644 --- a/src/gles_render/shaders.rs +++ b/src/gles_render/shaders.rs @@ -179,7 +179,7 @@ void main() // 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: // 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. // // The atlas is GL_R8 on ES3 and GL_LUMINANCE on ES2 (see diff --git a/src/gles_render/text.rs b/src/gles_render/text.rs index 2480b8c..733309e 100644 --- a/src/gles_render/text.rs +++ b/src/gles_render/text.rs @@ -5,7 +5,7 @@ //! per-glyph draw call. The line is shaped through //! [`crate::text_shaping::shape_line`] (Unicode BiDi + per-sub-run //! 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 //! connected forms / Devanagari clusters / CJK shaped glyphs are //! cached correctly without colliding with the source codepoints. @@ -18,7 +18,7 @@ use std::sync::Arc; -use fontdue::Font; +use crate::font_face::FontFace; use glow::HasContext; use crate::types::Color; @@ -27,7 +27,7 @@ use super::{ ATLAS_SIZE, GlesCanvas, GlyphEntry }; const GLYPH_CACHE_SOFT_CAP: usize = 8192; -fn font_id( font: &Arc ) -> usize +fn font_id( font: &Arc ) -> 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 /// canvas default, falling back to the system chain for codepoints it does not /// cover. - pub fn draw_text_with_font( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: &Arc ) + pub fn draw_text_with_font( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: &Arc ) { 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> ) + fn draw_text_inner( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: Option<&Arc> ) { self.activate_target(); let scaled = size * self.dpi_scale; @@ -110,7 +110,7 @@ impl GlesCanvas // `Arc::as_ptr` of the resolved handle, so we walk // every codepoint and ask the same resolver — exactly once // per distinct id thanks to the dedup loop. - let mut fonts: Vec<( usize, Arc )> = Vec::new(); + let mut fonts: Vec<( usize, Arc )> = Vec::new(); if !canvas_handle.bytes.is_empty() { 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, /// 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 ) -> f32 + pub fn measure_text_with_font( &self, text: &str, size: f32, font: &Arc ) -> f32 { self.measure_inner( text, size, Some( font ) ) } - fn measure_inner( &self, text: &str, size: f32, font: Option<&Arc> ) -> f32 + fn measure_inner( &self, text: &str, size: f32, font: Option<&Arc> ) -> f32 { let scaled = size * self.dpi_scale; let canvas_handle = self.font_handle(); diff --git a/src/input/pointer/press.rs b/src/input/pointer/press.rs index 4633905..c388a52 100644 --- a/src/input/pointer/press.rs +++ b/src/input/pointer/press.rs @@ -65,6 +65,7 @@ impl AppData self.show_context_menu( focus, idx, pos ); } else { self.hide_context_menu( focus ); + self.app.on_pointer_secondary( pos.x, pos.y ); } } } diff --git a/src/lib.rs b/src/lib.rs index cf8671a..2caf886 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,7 +13,7 @@ //! A lightweight Wayland UI toolkit built on top of //! [smithay-client-toolkit](https://crates.io/crates/smithay-client-toolkit), //! [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) //! 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 mod types; +pub mod font_face; pub( crate ) mod render; pub( crate ) mod system_fonts; 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 render::is_software_render; pub use render::Canvas; +pub use font_face::{ FontFace, LineMetrics, Metrics, OutlineBounds }; pub use wallpaper::{ WallpaperBundle, ImageData }; 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 }; diff --git a/src/render/mod.rs b/src/render/mod.rs index 945f0ea..a25b167 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -4,7 +4,7 @@ //! Rendering surface used by every widget. //! //! [`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 //! [`crate::gles_render::GlesCanvas`] (EGL + GLES2/3). //! @@ -34,7 +34,7 @@ use std::cell::Cell; use std::sync::Arc; -use fontdue::{ Font, LineMetrics, Metrics }; +use crate::font_face::{ FontFace, LineMetrics, Metrics }; use tiny_skia::{ Mask, Pixmap }; 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 /// pattern of `size * dpi_scale`; `font_id` is the address of the -/// `Arc` used for the rasterisation, so distinct weights / +/// `Arc` used for the rasterisation, so distinct weights / /// families of the same `(glyph_id, size)` do not collide on the /// cache. `glyph_id` is the per-font glyph index returned by /// HarfBuzz shaping, so cached entries persist across script @@ -107,7 +107,7 @@ pub ( super ) struct GlyphEntry // ─── SoftwareCanvas ────────────────────────────────────────────────────────── /// 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 /// runtime without changing widget code. Widgets themselves never see @@ -122,10 +122,10 @@ pub struct SoftwareCanvas /// specific family through [`SoftwareCanvas::font_for`] keep /// working. Populated from /// `crate::system_fonts::default_handle` at construction time. - pub font: Arc, + pub font: Arc, /// Raw bytes of the default font. Kept alongside `font` so the /// 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>, /// TTC sub-face index for the default font (0 for single-face /// files; collection index for `.ttc` archives). @@ -176,7 +176,7 @@ pub struct SoftwareCanvas /// inlinable in the hot path. pub enum Canvas { - /// CPU rasterisation via tiny-skia + fontdue, written to a + /// CPU rasterisation via tiny-skia, written to a /// `wl_shm` buffer. Software( SoftwareCanvas ), /// 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 /// through wrappers for every metric they read. - pub fn font( &self ) -> &Font + pub fn font( &self ) -> &FontFace { match self { @@ -523,7 +523,7 @@ impl Canvas /// Resolve a specific font via the theme registry, falling back /// to the system-default [`Self::font`] when no registry is /// installed or the triple cannot be satisfied. - pub fn font_for( &self, family: &str, weight: u16, style: FontStyle ) -> Arc + pub fn font_for( &self, family: &str, weight: u16, style: FontStyle ) -> Arc { match self { @@ -911,7 +911,7 @@ impl Canvas /// Draw `text` with an explicitly supplied font instead of the /// canvas default. Use [`Self::font_for`] to resolve a `(family, /// 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 ) + pub fn draw_text_with_font( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: &Arc ) { match self { @@ -932,7 +932,7 @@ impl Canvas /// Width of `text` rendered with `font`. Mirrors /// [`Self::measure_text`] but bypasses the canvas default font 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 ) -> f32 + pub fn measure_text_with_font( &self, text: &str, size: f32, font: &Arc ) -> f32 { match self { diff --git a/src/render/setup.rs b/src/render/setup.rs index d221aca..a735a68 100644 --- a/src/render/setup.rs +++ b/src/render/setup.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use std::sync::Arc; -use fontdue::Font; +use crate::font_face::FontFace; use tiny_skia::{ Pixmap, PixmapPaint, Transform }; use crate::system_fonts::default_handle; @@ -78,7 +78,7 @@ impl SoftwareCanvas /// Resolve a specific font from the theme registry, falling back /// to the canvas' default [`Self::font`] when no registry is /// installed or the triple cannot be satisfied. - pub fn font_for( &self, family: &str, weight: u16, style: FontStyle ) -> Arc + pub fn font_for( &self, family: &str, weight: u16, style: FontStyle ) -> Arc { self.font_registry .as_ref() @@ -91,7 +91,7 @@ impl SoftwareCanvas /// fallback chain (lazy load of the relevant Noto pack). Falls /// back to the primary (which paints a `.notdef` box) when no /// installed fallback covers the codepoint. - pub fn font_for_char( &self, ch: char ) -> Arc + pub fn font_for_char( &self, ch: char ) -> Arc { if self.font.lookup_glyph_index( ch ) != 0 { diff --git a/src/render/text.rs b/src/render/text.rs index 204e30e..705e1af 100644 --- a/src/render/text.rs +++ b/src/render/text.rs @@ -4,7 +4,7 @@ //! Text rendering for [`SoftwareCanvas`]. The line is shaped through //! [`crate::text_shaping::shape_line`] (BiDi reordering + per-sub-run //! 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 //! forms, Devanagari clusters, and CJK shaped glyphs cluster //! correctly without colliding with the per-codepoint cache the @@ -12,7 +12,7 @@ use std::sync::Arc; -use fontdue::Font; +use crate::font_face::FontFace; use crate::types::Color; @@ -40,7 +40,7 @@ impl SoftwareCanvas /// the result under `(glyph_id, size_bits, font_id)`. Used by /// both the public `draw_text` and `draw_text_with_font` paths /// after shaping has resolved every codepoint to a glyph index. - fn rasterize_indexed_cached( &mut self, font: &Arc, glyph_id: u16, scaled: f32, font_id: usize ) -> &GlyphEntry + fn rasterize_indexed_cached( &mut self, font: &Arc, glyph_id: u16, scaled: f32, font_id: usize ) -> &GlyphEntry { let key = GlyphKey { glyph_id, size_bits: scaled.to_bits(), font_id }; self.evict_if_full( &key ); @@ -59,12 +59,12 @@ impl SoftwareCanvas /// Draw `text` using the explicitly supplied font instead of the /// canvas default + fallback chain. - pub fn draw_text_with_font( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: &Arc ) + pub fn draw_text_with_font( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: &Arc ) { 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> ) + fn draw_text_inner( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: Option<&Arc> ) { let scaled = size * self.dpi_scale; let line_h = scaled * 1.5; @@ -124,7 +124,7 @@ impl SoftwareCanvas // rasterization — we keep a per-font-id index into a small // vec of `Arc` so the rasterizer step does not have to // re-walk the resolve fn (mut self borrow conflicts). - let mut fonts: Vec<( usize, Arc )> = Vec::new(); + let mut fonts: Vec<( usize, Arc )> = Vec::new(); let primary_id = Arc::as_ptr( &canvas_handle.font ) as usize; if !canvas_handle.bytes.is_empty() { @@ -224,12 +224,12 @@ impl SoftwareCanvas self.measure_with_font( text, size, None ) } - pub fn measure_text_with_font( &self, text: &str, size: f32, font: &Arc ) -> f32 + pub fn measure_text_with_font( &self, text: &str, size: f32, font: &Arc ) -> f32 { self.measure_with_font( text, size, Some( font ) ) } - fn measure_with_font( &self, text: &str, size: f32, font: Option<&Arc> ) -> f32 + fn measure_with_font( &self, text: &str, size: f32, font: Option<&Arc> ) -> f32 { let scaled = size * self.dpi_scale; let canvas_handle = diff --git a/src/system_fonts.rs b/src/system_fonts.rs index 7d75c42..cf6faf3 100644 --- a/src/system_fonts.rs +++ b/src/system_fonts.rs @@ -10,11 +10,11 @@ //! that band — Cyrillic, Devanagari, Arabic, Hebrew, Thai, the CJK //! ideographic block — relies on a chain of system Noto fonts loaded //! on demand: a fallback file is read off disk the first time some -//! glyph asks for it and the resulting `Arc` is cached +//! glyph asks for it and the resulting `Arc` is cached //! process-wide. //! //! Each `(path, face)` entry in [`FALLBACK_FONT_CANDIDATES`] gets its -//! own `OnceLock>>` slot. `Some(font)` means the +//! own `OnceLock>>` slot. `Some(font)` means the //! 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 //! re-`stat` the same path on every glyph miss. The `OnceLock` @@ -26,28 +26,28 @@ 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 -/// rustybuzz (HarfBuzz) requires for shaping. They are owned -/// separately because fontdue does not expose its internal byte -/// buffer — we keep both alive in lockstep instead. +/// rustybuzz (HarfBuzz) requires for shaping. Both name the same +/// buffer — the face holds its own `Arc` so it can pull outlines in +/// 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 index for collections like Noto Sans CJK). #[ derive( Clone ) ] pub struct FontHandle { - pub font: Arc, + pub font: Arc, pub bytes: Arc>, pub face: u32, } /// Per-script fallback font path with the TrueType-collection face -/// index fontdue should load (most files are single-face → 0; CJK -/// `.ttc` archives carry many faces, see the SC face on the canonical -/// Adobe-built `NotoSansCJK-Regular.ttc`). +/// index to load (most files are single-face → 0; CJK `.ttc` archives +/// carry many faces, see the SC face on the canonical Adobe-built +/// `NotoSansCJK-Regular.ttc`). struct FallbackFontSpec { path: &'static str, @@ -58,8 +58,9 @@ struct FallbackFontSpec /// the first slot whose font owns a non-zero glyph index for the /// codepoint wins, so the broadest families come first (Noto Sans /// covers Cyrillic, Greek, extended Latin) and the script-specific -/// + CJK packs trail. DejaVu is the last resort — most distros carry -/// it under one path or another. +/// + CJK packs trail. DejaVu goes ahead of the CJK collection: it owns +/// 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 ] = &[ // 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/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 // 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 // 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 - // when a user-visible string actually contains a CJK codepoint. + // baseline. Last, because probing a slot reads its whole file, and + // this one is 19 MB. 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/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 -/// pays the fontdue parse: `bytes` is the file read once, `font` is the -/// fontdue face built only when a glyph in this slot is actually needed. -/// `None` in a resolved stage is a sticky negative result. +/// builds a face: `bytes` is the file read once, `font` is the face +/// built only when a glyph in this slot is actually needed. `None` in a +/// resolved stage is a sticky negative result. struct Slot { bytes: OnceLock>>>, @@ -133,17 +134,16 @@ fn slot_covers( idx: usize, ch: char ) -> bool .is_some() } -/// Build (once) the fontdue face for slot `idx`. The raw bytes are -/// preserved inside the handle so the same `Arc>` can be handed -/// to rustybuzz for shaping without re-reading the file. +/// Build (once) the face for slot `idx`. The raw bytes are preserved +/// inside the handle so the same `Arc>` can be handed to +/// rustybuzz for shaping without re-reading the file. fn slot_handle( idx: usize ) -> Option { slots()[ idx ].font.get_or_init( || { let bytes = slot_bytes( idx )?; let face = FALLBACK_FONT_CANDIDATES[ idx ].face; - let opts = FontSettings { collection_index: face, ..FontSettings::default() }; - let font = Font::from_bytes( bytes.as_slice(), opts ).ok()?; + let font = FontFace::from_bytes( Arc::clone( &bytes ), face )?; Some( FontHandle { font: Arc::new( font ), bytes, face } ) } ) .clone() @@ -151,10 +151,10 @@ fn slot_handle( idx: usize ) -> Option /// 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 -/// `Arc` for the rest of the process. Returns `None` if no +/// `Arc` for the rest of the process. Returns `None` if no /// installed fallback covers the codepoint — the caller then paints /// the primary font's `.notdef` rather than dropping the glyph. -pub fn lookup( ch: char ) -> Option> +pub fn lookup( ch: char ) -> Option> { lookup_handle( ch ).map( |h| h.font ) } @@ -215,9 +215,9 @@ pub ( crate ) fn default_handle() -> FontHandle static PRIMARY: OnceLock = OnceLock::new(); PRIMARY.get_or_init( || { - let bytes = load_default_font_bytes(); - let font = Font::from_bytes( bytes.as_slice(), FontSettings::default() ).expect( "primary font parses" ); - FontHandle { font: Arc::new( font ), bytes: Arc::new( bytes ), face: 0 } + let bytes = Arc::new( load_default_font_bytes() ); + let font = FontFace::from_bytes( Arc::clone( &bytes ), 0 ).expect( "primary font parses" ); + FontHandle { font: Arc::new( font ), bytes, face: 0 } } ).clone() } @@ -230,9 +230,9 @@ pub fn primary_handle() -> FontHandle } /// Bytes-aware variant of [`lookup`]. Returns the full -/// [`FontHandle`] (fontdue handle + raw bytes + face index) so -/// callers that need to invoke a HarfBuzz-style shaper can do so -/// without re-reading the font file. +/// [`FontHandle`] (face + raw bytes + face index) so callers that +/// need to invoke a HarfBuzz-style shaper can do so without +/// re-reading the font file. pub fn lookup_handle( ch: char ) -> Option { ( 0..FALLBACK_FONT_CANDIDATES.len() ) diff --git a/src/text_shaping.rs b/src/text_shaping.rs index 39a94aa..2bd4154 100644 --- a/src/text_shaping.rs +++ b/src/text_shaping.rs @@ -13,7 +13,7 @@ //! visual order. Renderers consume that sequence directly: each //! `PositionedGlyph` carries the per-font `glyph_id`, the visual //! 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 //! correctly. //! diff --git a/src/theme/font_registry.rs b/src/theme/font_registry.rs index d43bdaa..6a35db0 100644 --- a/src/theme/font_registry.rs +++ b/src/theme/font_registry.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. -//! 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 //! `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::sync::Arc; -use fontdue::{ Font, FontSettings }; +use crate::font_face::FontFace; use super::fonts::FontFamilyDef; use super::text_style::FontStyle; @@ -54,7 +54,7 @@ pub enum FontLoadError { /// The source file could not be read. 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 ), } @@ -75,11 +75,11 @@ impl std::error::Error for FontLoadError {} // ─── Registry ──────────────────────────────────────────────────────────────── /// 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 ) ] pub struct FontRegistry { - by_key: HashMap>, + by_key: HashMap>, fallbacks: HashMap>, } @@ -100,7 +100,7 @@ impl FontRegistry family: impl Into, weight: u16, style: FontStyle, - font: Arc, + font: Arc, ) { self.by_key.insert( FontKey { family: family.into(), weight, style }, font ); @@ -118,9 +118,9 @@ impl FontRegistry /// Whether the registry has no sources loaded. 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. - pub fn resolve( &self, family: &str, weight: u16, style: FontStyle ) -> Option> + pub fn resolve( &self, family: &str, weight: u16, style: FontStyle ) -> Option> { // 1. Exact match. let exact = FontKey { family: family.to_string(), weight, style }; @@ -183,9 +183,10 @@ impl FontRegistry for src in &family.sources { let bytes = std::fs::read( &src.path ) + .map( Arc::new ) .map_err( |e| FontLoadError::Io( src.path.clone(), e ) )?; - let font = Font::from_bytes( bytes.as_slice(), FontSettings::default() ) - .map_err( |e| FontLoadError::Parse( src.path.clone(), e.to_string() ) )?; + let font = FontFace::from_bytes( bytes, 0 ) + .ok_or_else( || FontLoadError::Parse( src.path.clone(), "unparseable face".to_string() ) )?; 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 ) { - Ok( b ) => b, + Ok( b ) => Arc::new( b ), Err( e ) => { eprintln! @@ -229,15 +230,15 @@ impl FontRegistry continue; } }; - let font = match Font::from_bytes( bytes.as_slice(), FontSettings::default() ) + let font = match FontFace::from_bytes( bytes, 0 ) { - Ok( f ) => f, - Err( e ) => + Some( f ) => f, + None => { eprintln! ( - "[ltk] skipping font {} (weight {}, {:?}): parse error: {}", - src.path.display(), src.weight, src.style, e + "[ltk] skipping font {} (weight {}, {:?}): unparseable face", + src.path.display(), src.weight, src.style ); continue; } @@ -258,14 +259,13 @@ mod tests /// Load a real TTF from the system via the same search chain the /// software canvas uses. Only used by tests that need an actual - /// `Font` — skipped when no font is found (keeps CI green on images - /// without the usual system fonts). - fn system_font() -> Option> + /// `FontFace` — skipped when no font is found (keeps CI green on + /// images without the usual system fonts). + fn system_font() -> Option> { - let path = crate::system_fonts::find_font_opt()?; - let bytes = std::fs::read( path ).ok()?; - let font = Font::from_bytes( bytes.as_slice(), FontSettings::default() ).ok()?; - Some( Arc::new( font ) ) + let path = crate::system_fonts::find_font_opt()?; + let bytes = Arc::new( std::fs::read( path ).ok()? ); + Some( Arc::new( FontFace::from_bytes( bytes, 0 )? ) ) } #[ test ] diff --git a/src/theme/fonts.rs b/src/theme/fonts.rs index 123f8d6..3cda795 100644 --- a/src/theme/fonts.rs +++ b/src/theme/fonts.rs @@ -5,7 +5,7 @@ //! //! This module only models the **declaration**: a family name, a set of //! 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`]). use std::path::PathBuf; @@ -29,7 +29,7 @@ pub struct FontFamilyDef /// satisfy is used. pub fallbacks: Vec, /// 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, } diff --git a/src/widget/rich_text/mod.rs b/src/widget/rich_text/mod.rs index 0f95a45..aa1345c 100644 --- a/src/widget/rich_text/mod.rs +++ b/src/widget/rich_text/mod.rs @@ -8,7 +8,7 @@ use std::sync::Arc; -use fontdue::Font; +use crate::font_face::FontFace; use crate::theme::FontStyle; use crate::types::{ Color, Length, Rect }; @@ -100,12 +100,12 @@ impl RichText canvas.resolve_font( self.size ) } - fn resolve_font( &self, canvas: &Canvas ) -> Option> + fn resolve_font( &self, canvas: &Canvas ) -> Option> { self.font.as_ref().map( |( family, weight, style )| canvas.font_for( family, *weight, *style ) ) } - fn measure( &self, text: &str, canvas: &Canvas, font: Option<&Arc> ) -> f32 + fn measure( &self, text: &str, canvas: &Canvas, font: Option<&Arc> ) -> f32 { let size = self.resolved_size( canvas ); match font @@ -231,7 +231,7 @@ impl VisualLine } } -fn wrap_tracked( content: &str, size: f32, max_width: f32, canvas: &Canvas, font: Option<&Arc> ) -> Vec +fn wrap_tracked( content: &str, size: f32, max_width: f32, canvas: &Canvas, font: Option<&Arc> ) -> Vec { let measure = |s: &str| -> f32 { diff --git a/src/widget/text/mod.rs b/src/widget/text/mod.rs index b599fbd..48b1284 100644 --- a/src/widget/text/mod.rs +++ b/src/widget/text/mod.rs @@ -7,7 +7,7 @@ use std::sync::Arc; -use fontdue::Font; +use crate::font_face::FontFace; use crate::theme::FontStyle; use crate::types::{ Color, Length, Rect }; @@ -77,7 +77,7 @@ impl Text } /// 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`. #[ inline ] fn resolved_size( &self, canvas: &Canvas ) -> f32 @@ -160,7 +160,7 @@ impl Text self } - fn resolve_font( &self, canvas: &Canvas ) -> Option> + fn resolve_font( &self, canvas: &Canvas ) -> Option> { self.font.as_ref().map( |( family, weight, style )| { @@ -168,7 +168,7 @@ impl Text } ) } - fn measure( &self, text: &str, canvas: &Canvas, font: Option<&Arc> ) -> f32 + fn measure( &self, text: &str, canvas: &Canvas, font: Option<&Arc> ) -> f32 { let size = self.resolved_size( canvas ); match font @@ -178,7 +178,7 @@ impl Text } } - fn measure_char( &self, ch: char, canvas: &Canvas, font: Option<&Arc> ) -> f32 + fn measure_char( &self, ch: char, canvas: &Canvas, font: Option<&Arc> ) -> f32 { let size = self.resolved_size( canvas ); match font @@ -188,7 +188,7 @@ impl Text } } - fn paint( &self, canvas: &mut Canvas, text: &str, x: f32, y: f32, font: Option<&Arc> ) + fn paint( &self, canvas: &mut Canvas, text: &str, x: f32, y: f32, font: Option<&Arc> ) { let size = self.resolved_size( canvas ); match font @@ -302,7 +302,7 @@ impl Text /// than `max_width` overflows on its own line rather than being /// hyphenated. Routes through the font override when one is set so /// measurement and rendering agree on advance widths. -fn wrap_lines( text: &str, size: f32, max_width: f32, canvas: &Canvas, font: Option<&Arc> ) -> Vec +fn wrap_lines( text: &str, size: f32, max_width: f32, canvas: &Canvas, font: Option<&Arc> ) -> Vec { if max_width <= 0.0 || text.is_empty() { diff --git a/tests/render_pixels.rs b/tests/render_pixels.rs index 740ecf9..d74b206 100644 --- a/tests/render_pixels.rs +++ b/tests/render_pixels.rs @@ -1,5 +1,5 @@ // 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 // tests assert *structural* properties of the rendered buffer (background // colour, transparency, determinism, focus-state divergence) plus a few