ltk/render/setup.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! 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<Vec<u8>>`) alongside the fontdue `Arc<Font>` 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<FontHandle> = 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<Font>`.
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<FontRegistry> )
{
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<Font>
{
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<Font>
{
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" );
}
}