246 lines
6.7 KiB
Rust
246 lines
6.7 KiB
Rust
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<Tree>),
|
|
Image(RgbaImage),
|
|
}
|
|
|
|
impl CustomGlyphContent {
|
|
pub fn from_bin_svg(data: &[u8]) -> anyhow::Result<Self> {
|
|
let tree = Tree::from_data(data, &Options::default())?;
|
|
Ok(Self::Svg(Box::new(tree)))
|
|
}
|
|
|
|
pub fn from_bin_raster(data: &[u8]) -> anyhow::Result<Self> {
|
|
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<Self> {
|
|
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<CustomGlyphContent>,
|
|
}
|
|
|
|
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<cosmic_text::Color>,
|
|
/// 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<u8>,
|
|
/// 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<Self> {
|
|
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<ContentType>) {
|
|
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<RasterizedCustomGlyph> {
|
|
// 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 _,
|
|
}
|
|
}
|