use std::{ f32, sync::{ Arc, atomic::{AtomicUsize, Ordering}, }, }; use cosmic_text::SubpixelBin; use image::RgbaImage; use resvg::usvg::{Options, Tree}; use crate::{assets::AssetPath, globals::WguiGlobals}; static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(0); #[derive(Debug, Clone)] pub enum CustomGlyphContent { Svg(Box), Image(RgbaImage), } impl CustomGlyphContent { pub fn from_bin_svg(data: &[u8]) -> anyhow::Result { let tree = Tree::from_data(data, &Options::default())?; Ok(Self::Svg(Box::new(tree))) } pub fn from_bin_raster(data: &[u8]) -> anyhow::Result { let image = image::load_from_memory(data)?.into_rgba8(); Ok(Self::Image(image)) } #[allow(clippy::case_sensitive_file_extension_comparisons)] pub fn from_assets(globals: &mut WguiGlobals, path: AssetPath) -> anyhow::Result { let path_str = path.get_str(); let data = globals.get_asset(path)?; if path_str.ends_with(".svg") || path_str.ends_with(".svgz") { Ok(Self::from_bin_svg(&data)?) } else { Ok(Self::from_bin_raster(&data)?) } } } #[derive(Debug, Clone)] pub struct CustomGlyphData { pub(super) id: usize, pub(super) content: Arc, } impl CustomGlyphData { pub fn new(content: CustomGlyphContent) -> Self { Self { id: AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed), content: Arc::new(content), } } pub fn dim_for_cache_key(&self, width: u16, height: u16) -> (u16, u16) { const MAX_RASTER_DIM: u16 = 256; match self.content.as_ref() { CustomGlyphContent::Svg(..) => ( width.next_power_of_two().min(MAX_RASTER_DIM), height.next_power_of_two().min(MAX_RASTER_DIM), ), CustomGlyphContent::Image(image) => (image.width() as _, image.height() as _), } } } impl PartialEq for CustomGlyphData { fn eq(&self, other: &Self) -> bool { self.id.eq(&other.id) } } /// A custom glyph to render #[derive(Debug, Clone, PartialEq)] pub struct CustomGlyph { /// The unique identifier for this glyph pub data: CustomGlyphData, /// The position of the left edge of the glyph pub left: f32, /// The position of the top edge of the glyph pub top: f32, /// The width of the glyph pub width: f32, /// The height of the glyph pub height: f32, /// The color of this glyph (only relevant if the glyph is rendered with the /// type [`ContentType::Mask`]) /// /// Set to `None` to use [`crate::TextArea::default_color`]. pub color: Option, /// If `true`, then this glyph will be snapped to the nearest whole physical /// pixel and the resulting `SubpixelBin`'s in `RasterizationRequest` will always /// be `Zero` (useful for images and other large glyphs). pub snap_to_physical_pixel: bool, } impl CustomGlyph { pub const fn new(data: CustomGlyphData) -> Self { Self { data, left: 0.0, top: 0.0, width: 0.0, height: 0.0, color: None, snap_to_physical_pixel: true, } } } /// A request to rasterize a custom glyph #[derive(Debug, Clone, PartialEq)] pub struct RasterizeCustomGlyphRequest { /// The unique identifier of the glyph pub data: CustomGlyphData, /// The width of the glyph in physical pixels pub width: u16, /// The height of the glyph in physical pixels pub height: u16, /// Binning of fractional X offset /// /// If `CustomGlyph::snap_to_physical_pixel` was set to `true`, then this /// will always be `Zero`. pub x_bin: SubpixelBin, /// Binning of fractional Y offset /// /// If `CustomGlyph::snap_to_physical_pixel` was set to `true`, then this /// will always be `Zero`. pub y_bin: SubpixelBin, /// The scaling factor applied to the text area (Note that `width` and /// `height` are already scaled by this factor.) pub scale: f32, } /// A rasterized custom glyph #[derive(Debug, Clone)] pub struct RasterizedCustomGlyph { /// The raw image data pub data: Vec, /// The type of image data contained in `data` pub content_type: ContentType, pub width: u16, pub height: u16, } impl RasterizedCustomGlyph { pub(super) fn try_from(input: &RasterizeCustomGlyphRequest) -> Option { match input.data.content.as_ref() { CustomGlyphContent::Svg(tree) => rasterize_svg(tree, input), CustomGlyphContent::Image(data) => Some(rasterize_image(data)), } } pub(super) fn validate(&self, input: &RasterizeCustomGlyphRequest, expected_type: Option) { if let Some(expected_type) = expected_type { assert_eq!( self.content_type, expected_type, "Custom glyph rasterizer must always produce the same content type for a given input. Expected {:?}, got {:?}. Input: {:?}", expected_type, self.content_type, input ); } assert_eq!( self.data.len(), self.width as usize * self.height as usize * self.content_type.bytes_per_pixel(), "Invalid custom glyph rasterizer output. Expected data of length {}, got length {}. Input: {:?}", self.width as usize * self.height as usize * self.content_type.bytes_per_pixel(), self.data.len(), input, ); } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct CustomGlyphCacheKey { /// Font ID pub glyph_id: usize, /// Glyph width pub width: u16, /// Glyph height pub height: u16, /// Binning of fractional X offset pub x_bin: SubpixelBin, /// Binning of fractional Y offset pub y_bin: SubpixelBin, } /// The type of image data contained in a rasterized glyph #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum ContentType { /// Each pixel contains 32 bits of rgba data Color, /// Each pixel contains a single 8 bit channel Mask, } impl ContentType { /// The number of bytes per pixel for this content type pub const fn bytes_per_pixel(&self) -> usize { match self { Self::Color => 4, Self::Mask => 1, } } } fn rasterize_svg(tree: &Tree, input: &RasterizeCustomGlyphRequest) -> Option { // Calculate the scale based on the "glyph size". let svg_size = tree.size(); let scale_x = f32::from(input.width) / svg_size.width(); let scale_y = f32::from(input.height) / svg_size.height(); let mut pixmap = resvg::tiny_skia::Pixmap::new(u32::from(input.width), u32::from(input.height))?; let mut transform = resvg::usvg::Transform::from_scale(scale_x, scale_y); // Offset the glyph by the subpixel amount. let offset_x = input.x_bin.as_float(); let offset_y = input.y_bin.as_float(); if offset_x != 0.0 || offset_y != 0.0 { transform = transform.post_translate(offset_x, offset_y); } resvg::render(tree, transform, &mut pixmap.as_mut()); Some(RasterizedCustomGlyph { data: pixmap.data().to_vec(), content_type: ContentType::Color, width: input.width, height: input.height, }) } fn rasterize_image(image: &RgbaImage) -> RasterizedCustomGlyph { RasterizedCustomGlyph { data: image.to_vec(), content_type: ContentType::Color, width: image.width() as _, height: image.height() as _, } }