// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! Construction + accessors for [`SoftwareCanvas`]: new / sub_canvas //! / resize plus the font-registry installer and `blit`. use std::collections::HashMap; use std::sync::{ Arc, OnceLock }; use fontdue::{ Font, FontSettings }; use tiny_skia::{ Pixmap, PixmapPaint, Transform }; use crate::system_fonts::FontHandle; use crate::theme::{ FontRegistry, FontStyle }; use super::helpers::load_default_font_bytes; use super::SoftwareCanvas; /// Process-wide cache of the default font face. The handle keeps the /// raw bytes (`Arc>`) alongside the fontdue `Arc` so /// the HarfBuzz shaper can be invoked without re-reading the file. /// Sora is small (~50 KB) so the cost was minor in absolute terms — /// but a layer shell that brings up a launcher overlay, a QS panel, /// a calendar popup and a handful of toast surfaces would still pay /// the parse cost a dozen times in a single session, all of which /// is wasted work. static DEFAULT_FONT: OnceLock = OnceLock::new(); fn default_handle() -> FontHandle { DEFAULT_FONT.get_or_init( || { let bytes = load_default_font_bytes(); let font = Font::from_bytes( bytes.as_slice(), FontSettings::default() ) .expect( "bad font" ); FontHandle { font: Arc::new( font ), bytes: Arc::new( bytes ), face: 0, } } ).clone() } impl SoftwareCanvas { /// Create a canvas of the given pixel dimensions, loading a system font. /// /// Fallback fonts (Noto Sans / CJK / Devanagari / …) are NOT loaded /// here — they are owned by the crate-private system-fonts chain /// and loaded lazily per codepoint. A canvas that only ever paints /// Latin text will never touch those files; the first non-Latin /// glyph triggers a single targeted load and the rest of the /// process reuses the cached `Arc`. pub fn new( width: u32, height: u32 ) -> Self { let handle = default_handle(); Self { pixmap: Pixmap::new( width, height ).expect( "pixmap" ), font: handle.font.clone(), font_bytes: handle.bytes.clone(), font_face: handle.face, font_registry: None, dpi_scale: 1.0, global_alpha: 1.0, glyph_cache: HashMap::new(), clip_mask: None, clip_bounds: Vec::new(), } } /// Create a blank sub-canvas sharing the same font and DPI scale. pub fn sub_canvas( &self, width: u32, height: u32 ) -> SoftwareCanvas { SoftwareCanvas { pixmap: Pixmap::new( width, height ).expect( "pixmap" ), font: Arc::clone( &self.font ), font_bytes: Arc::clone( &self.font_bytes ), font_face: self.font_face, font_registry: self.font_registry.as_ref().map( Arc::clone ), dpi_scale: self.dpi_scale, global_alpha: self.global_alpha, glyph_cache: HashMap::new(), clip_mask: None, clip_bounds: Vec::new(), } } /// Install a theme font registry so [`Self::font_for`] can /// resolve family+weight+style triples declared by the theme's /// `fonts` block. The default [`Self::font`] stays in place as a /// fallback. pub fn set_font_registry( &mut self, registry: Arc ) { self.font_registry = Some( registry ); } /// 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 { self.font_registry .as_ref() .and_then( |r| r.resolve( family, weight, style ) ) .unwrap_or_else( || Arc::clone( &self.font ) ) } /// Pick the right font for `ch`. Tries the primary [`Self::font`] /// first; on a miss, delegates to the crate-private system-fonts /// 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 { if self.font.lookup_glyph_index( ch ) != 0 { return Arc::clone( &self.font ); } crate::system_fonts::lookup( ch ).unwrap_or_else( || Arc::clone( &self.font ) ) } /// Bytes-aware variant of [`Self::font_for_char`]. Returns the /// full `FontHandle` so callers that invoke the HarfBuzz shaper /// can hand the raw bytes directly to rustybuzz. The primary /// font supplies the bytes from /// [`Self::font_bytes`]; fallback chars borrow the bytes that /// were loaded into the system fallback cache. pub fn font_handle_for_char( &self, ch: char ) -> crate::system_fonts::FontHandle { if self.font.lookup_glyph_index( ch ) != 0 { return crate::system_fonts::FontHandle { font: Arc::clone( &self.font ), bytes: Arc::clone( &self.font_bytes ), face: self.font_face, }; } crate::system_fonts::lookup_handle( ch ).unwrap_or_else( || crate::system_fonts::FontHandle { font: Arc::clone( &self.font ), bytes: Arc::clone( &self.font_bytes ), face: self.font_face, } ) } pub fn blit( &mut self, src: &SoftwareCanvas, dest_x: i32, dest_y: i32 ) { let paint = PixmapPaint::default(); let t = Transform::from_translate( dest_x as f32, dest_y as f32 ); self.pixmap.draw_pixmap( 0, 0, src.pixmap.as_ref(), &paint, t, self.clip_mask.as_ref() ); } pub fn resize( &mut self, width: u32, height: u32 ) { self.pixmap = Pixmap::new( width, height ).expect( "pixmap" ); } }