From a7613a87efe7a61cf7490023bbb75606ff94993e Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Sun, 7 Jul 2024 12:26:54 -0500 Subject: [PATCH 01/22] add support for svg icons --- Cargo.toml | 9 + examples/eagle.svg | 72 +++++++ examples/lion.svg | 169 +++++++++++++++ examples/svg-icons.rs | 249 ++++++++++++++++++++++ src/icon.rs | 482 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + src/text_render.rs | 7 +- 7 files changed, 988 insertions(+), 3 deletions(-) create mode 100644 examples/eagle.svg create mode 100644 examples/lion.svg create mode 100644 examples/svg-icons.rs create mode 100644 src/icon.rs diff --git a/Cargo.toml b/Cargo.toml index b87a8e4..4b4061a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,14 +7,23 @@ homepage = "https://github.com/grovesNL/glyphon.git" repository = "https://github.com/grovesNL/glyphon" license = "MIT OR Apache-2.0 OR Zlib" +[features] +svg-icons = ["dep:resvg"] + [dependencies] wgpu = { version = "22", default-features = false, features = ["wgsl"] } etagere = "0.2.10" cosmic-text = "0.12" lru = { version = "0.12.1", default-features = false } rustc-hash = "2.0" +resvg = { version = "0.42", default-features = false, optional = true } [dev-dependencies] winit = "0.30.3" wgpu = "22" pollster = "0.3.0" + +[[example]] +name = "svg-icons" +path = "examples/svg-icons.rs" +required-features = ["svg-icons"] \ No newline at end of file diff --git a/examples/eagle.svg b/examples/eagle.svg new file mode 100644 index 0000000..53ad249 --- /dev/null +++ b/examples/eagle.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Openclipart + + + Eagle + 2007-01-24T06:25:54 + animal, animal, bird, bird, clip art, clipart, eagle, eagle, head, head, image, media, nature, nature, public domain, svg, + http://openclipart.org/detail/2962/eagle-by-nfroidure + + + nfroidure + + + + + animal + bird + clip art + clipart + eagle + head + image + media + nature + public domain + svg + + + + + + + + + + + diff --git a/examples/lion.svg b/examples/lion.svg new file mode 100644 index 0000000..6fbde49 --- /dev/null +++ b/examples/lion.svg @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Openclipart + + + + + + + + + + + diff --git a/examples/svg-icons.rs b/examples/svg-icons.rs new file mode 100644 index 0000000..ee8a93a --- /dev/null +++ b/examples/svg-icons.rs @@ -0,0 +1,249 @@ +use glyphon::{ + icon::{IconDesc, IconRenderer, IconSourceID, IconSystem, SvgSource}, + Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, + TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, +}; +use std::sync::Arc; +use wgpu::{ + CommandEncoderDescriptor, CompositeAlphaMode, DeviceDescriptor, Instance, InstanceDescriptor, + LoadOp, MultisampleState, Operations, PresentMode, RenderPassColorAttachment, + RenderPassDescriptor, RequestAdapterOptions, SurfaceConfiguration, TextureFormat, + TextureUsages, TextureViewDescriptor, +}; +use winit::{ + dpi::LogicalSize, + event::{Event, WindowEvent}, + event_loop::EventLoop, + window::WindowBuilder, +}; + +// Example SVG icons are from https://publicdomainvectors.org/ +static LION_SVG: &[u8] = include_bytes!("./lion.svg"); +static EAGLE_SVG: &[u8] = include_bytes!("./eagle.svg"); + +fn main() { + pollster::block_on(run()); +} + +async fn run() { + // Set up window + let (width, height) = (800, 600); + let event_loop = EventLoop::new().unwrap(); + let window = Arc::new( + WindowBuilder::new() + .with_inner_size(LogicalSize::new(width as f64, height as f64)) + .with_title("glyphon svg icons") + .build(&event_loop) + .unwrap(), + ); + let size = window.inner_size(); + let scale_factor = window.scale_factor(); + + // Set up surface + let instance = Instance::new(InstanceDescriptor::default()); + let adapter = instance + .request_adapter(&RequestAdapterOptions::default()) + .await + .unwrap(); + let (device, queue) = adapter + .request_device(&DeviceDescriptor::default(), None) + .await + .unwrap(); + + let surface = instance + .create_surface(window.clone()) + .expect("Create surface"); + let swapchain_format = TextureFormat::Bgra8UnormSrgb; + let mut config = SurfaceConfiguration { + usage: TextureUsages::RENDER_ATTACHMENT, + format: swapchain_format, + width: size.width, + height: size.height, + present_mode: PresentMode::Fifo, + alpha_mode: CompositeAlphaMode::Opaque, + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + surface.configure(&device, &config); + + // Set up text renderer + let mut font_system = FontSystem::new(); + let mut swash_cache = SwashCache::new(); + let cache = Cache::new(&device); + let mut viewport = Viewport::new(&device, &cache); + let mut atlas = TextAtlas::new(&device, &queue, &cache, swapchain_format); + let mut text_renderer = + TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None); + let mut buffer = Buffer::new(&mut font_system, Metrics::new(30.0, 42.0)); + + let physical_width = (width as f64 * scale_factor) as f32; + let physical_height = (height as f64 * scale_factor) as f32; + + buffer.set_size(&mut font_system, physical_width, physical_height); + buffer.set_text( + &mut font_system, + "SVG icons! --->\n\nThe icons below should be partially clipped.", + Attrs::new().family(Family::SansSerif), + Shaping::Advanced, + ); + buffer.shape_until_scroll(&mut font_system, false); + + // Set up icon renderer + let mut icon_system = IconSystem::new(); + let mut icon_renderer = + IconRenderer::new(&mut atlas, &device, MultisampleState::default(), None); + + // Add SVG sources to the icon system. + icon_system.add_svg( + IconSourceID(0), + SvgSource::Data(LION_SVG).load(&Default::default()).unwrap(), + true, + ); + icon_system.add_svg( + IconSourceID(1), + SvgSource::Data(EAGLE_SVG) + .load(&Default::default()) + .unwrap(), + false, + ); + + event_loop + .run(move |event, target| { + if let Event::WindowEvent { + window_id: _, + event, + } = event + { + match event { + WindowEvent::Resized(size) => { + config.width = size.width; + config.height = size.height; + surface.configure(&device, &config); + window.request_redraw(); + } + WindowEvent::RedrawRequested => { + viewport.update( + &queue, + Resolution { + width: config.width, + height: config.height, + }, + ); + + let bounds = TextBounds { + left: 0, + top: 0, + right: 650, + bottom: 180, + }; + + text_renderer + .prepare( + &device, + &queue, + &mut font_system, + &mut atlas, + &viewport, + [TextArea { + buffer: &buffer, + left: 10.0, + top: 10.0, + scale: 1.0, + bounds, + default_color: Color::rgb(255, 255, 255), + }], + &mut swash_cache, + ) + .unwrap(); + + icon_renderer + .prepare( + &device, + &queue, + &mut icon_system, + &mut font_system, + &mut atlas, + &viewport, + [ + IconDesc { + id: IconSourceID(0), + size: 64.0, + left: 300, + top: 15, + color: Color::rgb(200, 200, 255), + bounds, + metadata: 0, + }, + IconDesc { + id: IconSourceID(1), + size: 64.0, + left: 400, + top: 15, + color: Color::rgb(255, 255, 255), + bounds, + metadata: 0, + }, + IconDesc { + id: IconSourceID(0), + size: 64.0, + left: 300, + top: 140, + color: Color::rgb(200, 255, 200), + bounds, + metadata: 0, + }, + IconDesc { + id: IconSourceID(1), + size: 64.0, + left: 400, + top: 140, + color: Color::rgb(255, 255, 255), + bounds, + metadata: 0, + }, + ], + &mut swash_cache, + ) + .unwrap(); + + let frame = surface.get_current_texture().unwrap(); + let view = frame.texture.create_view(&TextureViewDescriptor::default()); + let mut encoder = device + .create_command_encoder(&CommandEncoderDescriptor { label: None }); + { + let mut pass = encoder.begin_render_pass(&RenderPassDescriptor { + label: None, + color_attachments: &[Some(RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(wgpu::Color { + r: 0.02, + g: 0.02, + b: 0.02, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + text_renderer.render(&atlas, &viewport, &mut pass).unwrap(); + icon_renderer.render(&atlas, &viewport, &mut pass).unwrap(); + } + + queue.submit(Some(encoder.finish())); + frame.present(); + + atlas.trim(); + } + WindowEvent::CloseRequested => target.exit(), + _ => {} + } + } + }) + .unwrap(); +} diff --git a/src/icon.rs b/src/icon.rs new file mode 100644 index 0000000..6da86a0 --- /dev/null +++ b/src/icon.rs @@ -0,0 +1,482 @@ +use cosmic_text::{CacheKey, CacheKeyFlags, Color, SubpixelBin}; +use resvg::{ + tiny_skia::Pixmap, + usvg::{self, Transform}, +}; +use rustc_hash::FxHashMap; +use std::{path::Path, slice, sync::Arc}; +use wgpu::{ + Buffer, BufferDescriptor, BufferUsages, DepthStencilState, Device, Extent3d, ImageCopyTexture, + ImageDataLayout, MultisampleState, Origin3d, Queue, RenderPass, RenderPipeline, TextureAspect, +}; + +use crate::{ + text_render::{ContentType, TextColorConversion}, + ColorMode, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError, + SwashCache, TextAtlas, TextBounds, Viewport, +}; + +/// An svg icon renderer that uses cached glyphs to render icons into an existing render pass. +pub struct IconRenderer { + vertex_buffer: Buffer, + vertex_buffer_size: u64, + pipeline: Arc, + glyph_vertices: Vec, +} + +impl IconRenderer { + /// Creates a new [`IconRenderer`]. + pub fn new( + atlas: &mut TextAtlas, + device: &Device, + multisample: MultisampleState, + depth_stencil: Option, + ) -> Self { + let vertex_buffer_size = crate::text_render::next_copy_buffer_size(32); + let vertex_buffer = device.create_buffer(&BufferDescriptor { + label: Some("glyphon icon vertices"), + size: vertex_buffer_size, + usage: BufferUsages::VERTEX | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let pipeline = atlas.get_or_create_pipeline(device, multisample, depth_stencil); + + Self { + vertex_buffer, + vertex_buffer_size, + pipeline, + glyph_vertices: Vec::new(), + } + } + + /// Prepares all of the given icons for rendering. + pub fn prepare_with_depth<'a>( + &mut self, + device: &Device, + queue: &Queue, + icon_system: &mut IconSystem, + font_system: &mut FontSystem, + atlas: &mut TextAtlas, + viewport: &Viewport, + icons: impl IntoIterator, + cache: &mut SwashCache, + mut metadata_to_depth: impl FnMut(usize) -> f32, + ) -> Result<(), PrepareError> { + self.glyph_vertices.clear(); + + let resolution = viewport.resolution(); + + let font_id = cosmic_text::fontdb::ID::dummy(); + // This is a bit of a hacky way to reserve a slot for icons in the text + // atlas, but this is a simple way to ensure that there will be no + // conflicts in the atlas without the need to create our own custom + // `CacheKey` struct with extra bytes. + let flags = CacheKeyFlags::from_bits_retain(u32::MAX); + + for icon in icons { + let cache_key = CacheKey { + font_id, + glyph_id: icon.id.0, + font_size_bits: icon.size.to_bits(), + x_bin: SubpixelBin::Zero, + y_bin: SubpixelBin::Zero, + flags, + }; + + if atlas.mask_atlas.glyph_cache.contains(&cache_key) { + atlas.mask_atlas.promote(cache_key); + } else if atlas.color_atlas.glyph_cache.contains(&cache_key) { + atlas.color_atlas.promote(cache_key); + } else { + let Some(svg_data) = icon_system.svgs.get(&icon.id) else { + continue; + }; + + let content_type = if svg_data.is_symbolic { + ContentType::Mask + } else { + ContentType::Color + }; + + let icon_size = svg_data.tree.size(); + let max_side_len = icon_size.width().max(icon_size.height()); + + let should_rasterize = max_side_len > 0.0; + + let (scale, width, height, mut pixmap) = if should_rasterize { + let scale = icon.size / max_side_len; + let width = (icon_size.width() * scale).ceil(); + let height = (icon_size.height() * scale).ceil(); + + if width <= 0.0 || height <= 0.0 { + (0.0, 0, 0, None) + } else if let Some(pixmap) = Pixmap::new(width as u32, height as u32) { + (scale, width as u32, height as u32, Some(pixmap)) + } else { + (0.0, 0, 0, None) + } + } else { + (0.0, 0, 0, None) + }; + + let (gpu_cache, atlas_id, inner) = if let Some(mut pixmap) = pixmap.take() { + let transform = Transform::from_scale(scale, scale); + + resvg::render(&svg_data.tree, transform, &mut pixmap.as_mut()); + + let alpha_image: Vec; + let data = if let ContentType::Mask = content_type { + // Only use the alpha channel for symbolic icons. + alpha_image = pixmap.data().iter().skip(3).step_by(4).copied().collect(); + &alpha_image + } else { + pixmap.data() + }; + + let mut inner = atlas.inner_for_content_mut(content_type); + + // Find a position in the packer + let allocation = loop { + match inner.try_allocate(width as usize, height as usize) { + Some(a) => break a, + None => { + if !atlas.grow(device, queue, font_system, cache, content_type) { + return Err(PrepareError::AtlasFull); + } + + inner = atlas.inner_for_content_mut(content_type); + } + } + }; + let atlas_min = allocation.rectangle.min; + + queue.write_texture( + ImageCopyTexture { + texture: &inner.texture, + mip_level: 0, + origin: Origin3d { + x: atlas_min.x as u32, + y: atlas_min.y as u32, + z: 0, + }, + aspect: TextureAspect::All, + }, + data, + ImageDataLayout { + offset: 0, + bytes_per_row: Some(width as u32 * inner.num_channels() as u32), + rows_per_image: None, + }, + Extent3d { + width: width as u32, + height: height as u32, + depth_or_array_layers: 1, + }, + ); + + ( + GpuCacheStatus::InAtlas { + x: atlas_min.x as u16, + y: atlas_min.y as u16, + content_type, + }, + Some(allocation.id), + inner, + ) + } else { + let inner = &mut atlas.color_atlas; + (GpuCacheStatus::SkipRasterization, None, inner) + }; + + inner.put( + cache_key, + GlyphDetails { + width: width as u16, + height: height as u16, + gpu_cache, + atlas_id, + top: 0, + left: 0, + }, + ); + } + + let details = atlas.glyph(&cache_key).unwrap(); + + let mut x = icon.left; + let mut y = icon.top; + + let (mut atlas_x, mut atlas_y, content_type) = match details.gpu_cache { + GpuCacheStatus::InAtlas { x, y, content_type } => (x, y, content_type), + GpuCacheStatus::SkipRasterization => continue, + }; + + let mut width = details.width as i32; + let mut height = details.height as i32; + + let bounds_min_x = icon.bounds.left.max(0); + let bounds_min_y = icon.bounds.top.max(0); + let bounds_max_x = icon.bounds.right.min(resolution.width as i32); + let bounds_max_y = icon.bounds.bottom.min(resolution.height as i32); + + // Starts beyond right edge or ends beyond left edge + let max_x = x + width; + if x > bounds_max_x || max_x < bounds_min_x { + continue; + } + + // Starts beyond bottom edge or ends beyond top edge + let max_y = y + height; + if y > bounds_max_y || max_y < bounds_min_y { + continue; + } + + // Clip left ege + if x < bounds_min_x { + let right_shift = bounds_min_x - x; + + x = bounds_min_x; + width = max_x - bounds_min_x; + atlas_x += right_shift as u16; + } + + // Clip right edge + if x + width > bounds_max_x { + width = bounds_max_x - x; + } + + // Clip top edge + if y < bounds_min_y { + let bottom_shift = bounds_min_y - y; + + y = bounds_min_y; + height = max_y - bounds_min_y; + atlas_y += bottom_shift as u16; + } + + // Clip bottom edge + if y + height > bounds_max_y { + height = bounds_max_y - y; + } + + let depth = metadata_to_depth(icon.metadata); + + self.glyph_vertices.push(GlyphToRender { + pos: [x, y], + dim: [width as u16, height as u16], + uv: [atlas_x, atlas_y], + color: icon.color.0, + content_type_with_srgb: [ + content_type as u16, + match atlas.color_mode { + ColorMode::Accurate => TextColorConversion::ConvertToLinear, + ColorMode::Web => TextColorConversion::None, + } as u16, + ], + depth, + }); + } + + let will_render = !self.glyph_vertices.is_empty(); + if !will_render { + return Ok(()); + } + + let vertices = self.glyph_vertices.as_slice(); + let vertices_raw = unsafe { + slice::from_raw_parts( + vertices as *const _ as *const u8, + std::mem::size_of_val(vertices), + ) + }; + + if self.vertex_buffer_size >= vertices_raw.len() as u64 { + queue.write_buffer(&self.vertex_buffer, 0, vertices_raw); + } else { + self.vertex_buffer.destroy(); + + let (buffer, buffer_size) = crate::text_render::create_oversized_buffer( + device, + Some("glyphon icon vertices"), + vertices_raw, + BufferUsages::VERTEX | BufferUsages::COPY_DST, + ); + + self.vertex_buffer = buffer; + self.vertex_buffer_size = buffer_size; + } + + Ok(()) + } + + /// Prepares all of the given icons for rendering. + pub fn prepare<'a>( + &mut self, + device: &Device, + queue: &Queue, + icon_system: &mut IconSystem, + font_system: &mut FontSystem, + atlas: &mut TextAtlas, + viewport: &Viewport, + icons: impl IntoIterator, + cache: &mut SwashCache, + ) -> Result<(), PrepareError> { + self.prepare_with_depth( + device, + queue, + icon_system, + font_system, + atlas, + viewport, + icons, + cache, + zero_depth, + ) + } + + /// Renders all icons that were previously provided to [`IconRenderer::prepare`]. + pub fn render<'pass>( + &'pass self, + atlas: &'pass TextAtlas, + viewport: &'pass Viewport, + pass: &mut RenderPass<'pass>, + ) -> Result<(), RenderError> { + if self.glyph_vertices.is_empty() { + return Ok(()); + } + + pass.set_pipeline(&self.pipeline); + pass.set_bind_group(0, &atlas.bind_group, &[]); + pass.set_bind_group(1, &viewport.bind_group, &[]); + pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); + pass.draw(0..4, 0..self.glyph_vertices.len() as u32); + + Ok(()) + } +} + +/// The description of an icon to be rendered. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct IconDesc { + /// The unique identifier for the source of data to use for this icon. + pub id: IconSourceID, + /// The size of the icon in points. This will be the length of the longest side. + pub size: f32, + /// The left edge of the icon. + pub left: i32, + /// The top edge of the icon. + pub top: i32, + /// The color of the icon. This is only relevant if the icon source data is symbolic. + pub color: Color, + /// The visible bounds of the text area. This is used to clip the icon and doesn't have to + /// match the `left` and `top` values. + pub bounds: TextBounds, + /// Additional metadata about this icon. + pub metadata: usize, +} + +/// A unique identifier for a given source of icon data. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct IconSourceID(pub u16); + +struct IconData { + tree: usvg::Tree, + is_symbolic: bool, +} + +/// A system of loaded resources for icons. +pub struct IconSystem { + svgs: FxHashMap, +} + +impl IconSystem { + /// Construct a new [`IconSystem`]. + pub fn new() -> Self { + Self { + svgs: FxHashMap::default(), + } + } + + /// Add an svg source to this system. + /// + /// * id - A unique identifier for this resource. + /// * source - The parsed SVG data. + /// * is_symbolic - If `true`, then only the alpha channel will be used and the icon can + /// be filled with any solid color. If `false`, then the icon will be rendered in full + /// color. + pub fn add_svg(&mut self, id: IconSourceID, source: usvg::Tree, is_symbolic: bool) { + self.svgs.insert( + id, + IconData { + tree: source, + is_symbolic, + }, + ); + } + + // Returns `true` if the source was removed, or `false` if there was + // no source with that ID. + pub fn remove(&mut self, id: IconSourceID) -> bool { + self.svgs.remove(&id).is_some() + } +} + +fn zero_depth(_: usize) -> f32 { + 0f32 +} + +/// A helper struct to load SVG data. +pub enum SvgSource<'a> { + Data(&'a [u8]), + String(&'a str), + Path(&'a Path), +} + +impl<'a> SvgSource<'a> { + /// Load and parse the SVG data. + pub fn load(self, opt: &usvg::Options<'_>) -> Result { + let tree = match self { + Self::Data(data) => usvg::Tree::from_data(data, opt)?, + Self::String(text) => usvg::Tree::from_str(text, opt)?, + Self::Path(path) => { + let data = std::fs::read(path)?; + usvg::Tree::from_data(&data, opt)? + } + }; + + Ok(tree) + } +} + +/// An error that occured while loading and parsing an [`SvgSource`]. +#[derive(Debug)] +pub enum SvgSourceError { + /// An error occured while parsing the SVG data. + SvgError(usvg::Error), + /// An error occured while loading the SVG file. + IoError(std::io::Error), +} + +impl std::error::Error for SvgSourceError {} + +impl std::fmt::Display for SvgSourceError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::SvgError(e) => write!(f, "Could not load svg source: vg error: {}", e), + Self::IoError(e) => write!(f, "Could not load svg source: io error: {}", e), + } + } +} + +impl From for SvgSourceError { + fn from(e: usvg::Error) -> Self { + Self::SvgError(e) + } +} + +impl From for SvgSourceError { + fn from(e: std::io::Error) -> Self { + Self::IoError(e) + } +} diff --git a/src/lib.rs b/src/lib.rs index 3b31120..390b5d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,9 @@ mod text_atlas; mod text_render; mod viewport; +#[cfg(feature = "svg-icons")] +pub mod icon; + pub use cache::Cache; pub use error::{PrepareError, RenderError}; pub use text_atlas::{ColorMode, TextAtlas}; diff --git a/src/text_render.rs b/src/text_render.rs index d89fd02..d42f7bc 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -302,6 +302,7 @@ impl TextRenderer { Ok(()) } + /// Prepares all of the provided text areas for rendering. pub fn prepare<'a>( &mut self, device: &Device, @@ -354,17 +355,17 @@ pub enum ContentType { #[repr(u16)] #[derive(Debug, Clone, Copy, Eq, PartialEq)] -enum TextColorConversion { +pub(crate) enum TextColorConversion { None = 0, ConvertToLinear = 1, } -fn next_copy_buffer_size(size: u64) -> u64 { +pub(crate) fn next_copy_buffer_size(size: u64) -> u64 { let align_mask = COPY_BUFFER_ALIGNMENT - 1; ((size.next_power_of_two() + align_mask) & !align_mask).max(COPY_BUFFER_ALIGNMENT) } -fn create_oversized_buffer( +pub(crate) fn create_oversized_buffer( device: &Device, label: Option<&str>, contents: &[u8], From 9ff0c0e37aa4f5ff12362c39a8b62573d3968eee Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Sun, 7 Jul 2024 13:15:19 -0500 Subject: [PATCH 02/22] remove SVG helper struct --- Cargo.toml | 1 + examples/svg-icons.rs | 8 +++---- src/icon.rs | 55 ------------------------------------------- 3 files changed, 4 insertions(+), 60 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4b4061a..dfcb9c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ repository = "https://github.com/grovesNL/glyphon" license = "MIT OR Apache-2.0 OR Zlib" [features] +default = ["svg-icons"] svg-icons = ["dep:resvg"] [dependencies] diff --git a/examples/svg-icons.rs b/examples/svg-icons.rs index ee8a93a..6534f61 100644 --- a/examples/svg-icons.rs +++ b/examples/svg-icons.rs @@ -1,5 +1,5 @@ use glyphon::{ - icon::{IconDesc, IconRenderer, IconSourceID, IconSystem, SvgSource}, + icon::{IconDesc, IconRenderer, IconSourceID, IconSystem}, Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, }; @@ -96,14 +96,12 @@ async fn run() { // Add SVG sources to the icon system. icon_system.add_svg( IconSourceID(0), - SvgSource::Data(LION_SVG).load(&Default::default()).unwrap(), + resvg::usvg::Tree::from_data(LION_SVG, &Default::default()).unwrap(), true, ); icon_system.add_svg( IconSourceID(1), - SvgSource::Data(EAGLE_SVG) - .load(&Default::default()) - .unwrap(), + resvg::usvg::Tree::from_data(EAGLE_SVG, &Default::default()).unwrap(), false, ); diff --git a/src/icon.rs b/src/icon.rs index 6da86a0..37c4442 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -425,58 +425,3 @@ impl IconSystem { fn zero_depth(_: usize) -> f32 { 0f32 } - -/// A helper struct to load SVG data. -pub enum SvgSource<'a> { - Data(&'a [u8]), - String(&'a str), - Path(&'a Path), -} - -impl<'a> SvgSource<'a> { - /// Load and parse the SVG data. - pub fn load(self, opt: &usvg::Options<'_>) -> Result { - let tree = match self { - Self::Data(data) => usvg::Tree::from_data(data, opt)?, - Self::String(text) => usvg::Tree::from_str(text, opt)?, - Self::Path(path) => { - let data = std::fs::read(path)?; - usvg::Tree::from_data(&data, opt)? - } - }; - - Ok(tree) - } -} - -/// An error that occured while loading and parsing an [`SvgSource`]. -#[derive(Debug)] -pub enum SvgSourceError { - /// An error occured while parsing the SVG data. - SvgError(usvg::Error), - /// An error occured while loading the SVG file. - IoError(std::io::Error), -} - -impl std::error::Error for SvgSourceError {} - -impl std::fmt::Display for SvgSourceError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::SvgError(e) => write!(f, "Could not load svg source: vg error: {}", e), - Self::IoError(e) => write!(f, "Could not load svg source: io error: {}", e), - } - } -} - -impl From for SvgSourceError { - fn from(e: usvg::Error) -> Self { - Self::SvgError(e) - } -} - -impl From for SvgSourceError { - fn from(e: std::io::Error) -> Self { - Self::IoError(e) - } -} From bbb0dc35c8d52c728b300b19027f6257dadb6a15 Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Sun, 7 Jul 2024 13:16:11 -0500 Subject: [PATCH 03/22] forgot to remove default features --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index dfcb9c9..4b4061a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,6 @@ repository = "https://github.com/grovesNL/glyphon" license = "MIT OR Apache-2.0 OR Zlib" [features] -default = ["svg-icons"] svg-icons = ["dep:resvg"] [dependencies] From 44288ea2161ace80f85bd28558648e3343bc4ce2 Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:14:39 -0500 Subject: [PATCH 04/22] rework api for custom glyphs --- Cargo.toml | 7 +- examples/svg-icons.rs | 173 ++++++++++++++++++++++-------------------- src/icon.rs | 5 +- src/lib.rs | 70 +++++++++++++++-- src/svg.rs | 102 +++++++++++++++++++++++++ src/text_render.rs | 154 ++++++++++++++++++++++++++++++++----- 6 files changed, 399 insertions(+), 112 deletions(-) create mode 100644 src/svg.rs diff --git a/Cargo.toml b/Cargo.toml index 4b4061a..e30dee9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,9 @@ repository = "https://github.com/grovesNL/glyphon" license = "MIT OR Apache-2.0 OR Zlib" [features] -svg-icons = ["dep:resvg"] +custom-glyphs = [] +svg = ["dep:resvg", "custom-glyphs"] +svg-raster-images = ["resvg?/raster-images"] [dependencies] wgpu = { version = "22", default-features = false, features = ["wgsl"] } @@ -21,9 +23,10 @@ resvg = { version = "0.42", default-features = false, optional = true } [dev-dependencies] winit = "0.30.3" wgpu = "22" +resvg = { version = "0.42", default-features = false } pollster = "0.3.0" [[example]] name = "svg-icons" path = "examples/svg-icons.rs" -required-features = ["svg-icons"] \ No newline at end of file +required-features = ["svg"] \ No newline at end of file diff --git a/examples/svg-icons.rs b/examples/svg-icons.rs index 6534f61..4742fdc 100644 --- a/examples/svg-icons.rs +++ b/examples/svg-icons.rs @@ -1,7 +1,8 @@ use glyphon::{ - icon::{IconDesc, IconRenderer, IconSourceID, IconSystem}, - Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, - TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, + svg::{usvg, SvgGlyphSystem}, + Attrs, Buffer, Cache, Color, ContentType, Family, FontSystem, InlineBox, InlineBoxContent, + Metrics, Resolution, Shaping, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, + Viewport, }; use std::sync::Arc; use wgpu::{ @@ -74,35 +75,37 @@ async fn run() { let mut atlas = TextAtlas::new(&device, &queue, &cache, swapchain_format); let mut text_renderer = TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None); - let mut buffer = Buffer::new(&mut font_system, Metrics::new(30.0, 42.0)); + let mut text_buffer = Buffer::new(&mut font_system, Metrics::new(30.0, 42.0)); let physical_width = (width as f64 * scale_factor) as f32; let physical_height = (height as f64 * scale_factor) as f32; - buffer.set_size(&mut font_system, physical_width, physical_height); - buffer.set_text( + text_buffer.set_size( + &mut font_system, + Some(physical_width), + Some(physical_height), + ); + text_buffer.set_text( &mut font_system, "SVG icons! --->\n\nThe icons below should be partially clipped.", Attrs::new().family(Family::SansSerif), Shaping::Advanced, ); - buffer.shape_until_scroll(&mut font_system, false); - - // Set up icon renderer - let mut icon_system = IconSystem::new(); - let mut icon_renderer = - IconRenderer::new(&mut atlas, &device, MultisampleState::default(), None); - - // Add SVG sources to the icon system. - icon_system.add_svg( - IconSourceID(0), - resvg::usvg::Tree::from_data(LION_SVG, &Default::default()).unwrap(), - true, + text_buffer.shape_until_scroll(&mut font_system, false); + + // Set up svg system + let mut svg_system = SvgGlyphSystem::default(); + + // Add SVG sources + svg_system.add_svg( + 0, + usvg::Tree::from_data(LION_SVG, &Default::default()).unwrap(), + ContentType::Mask, ); - icon_system.add_svg( - IconSourceID(1), - resvg::usvg::Tree::from_data(EAGLE_SVG, &Default::default()).unwrap(), - false, + svg_system.add_svg( + 1, + usvg::Tree::from_data(EAGLE_SVG, &Default::default()).unwrap(), + ContentType::Color, ); event_loop @@ -128,13 +131,6 @@ async fn run() { }, ); - let bounds = TextBounds { - left: 0, - top: 0, - right: 650, - bottom: 180, - }; - text_renderer .prepare( &device, @@ -143,64 +139,78 @@ async fn run() { &mut atlas, &viewport, [TextArea { - buffer: &buffer, + buffer: &text_buffer, left: 10.0, top: 10.0, scale: 1.0, - bounds, + bounds: TextBounds { + left: 0, + top: 0, + right: 650, + bottom: 180, + }, default_color: Color::rgb(255, 255, 255), + inline_boxes: vec![ + InlineBox { + left: 300.0, + top: 15.0, + width: 64.0, + height: 64.0, + content: InlineBoxContent::CustomGlyph { + id: 0, + size: 64.0, + left: 0.0, + top: 0.0, + color: Some(Color::rgb(200, 200, 255)), + metadata: 0, + }, + }, + InlineBox { + left: 400.0, + top: 15.0, + width: 64.0, + height: 64.0, + content: InlineBoxContent::CustomGlyph { + id: 1, + size: 64.0, + left: 0.0, + top: 0.0, + color: None, + metadata: 0, + }, + }, + InlineBox { + left: 300.0, + top: 140.0, + width: 64.0, + height: 64.0, + content: InlineBoxContent::CustomGlyph { + id: 0, + size: 64.0, + left: 0.0, + top: 0.0, + color: Some(Color::rgb(200, 255, 200)), + metadata: 0, + }, + }, + InlineBox { + left: 400.0, + top: 140.0, + width: 64.0, + height: 64.0, + content: InlineBoxContent::CustomGlyph { + id: 1, + size: 64.0, + left: 0.0, + top: 0.0, + color: None, + metadata: 0, + }, + }, + ], }], &mut swash_cache, - ) - .unwrap(); - - icon_renderer - .prepare( - &device, - &queue, - &mut icon_system, - &mut font_system, - &mut atlas, - &viewport, - [ - IconDesc { - id: IconSourceID(0), - size: 64.0, - left: 300, - top: 15, - color: Color::rgb(200, 200, 255), - bounds, - metadata: 0, - }, - IconDesc { - id: IconSourceID(1), - size: 64.0, - left: 400, - top: 15, - color: Color::rgb(255, 255, 255), - bounds, - metadata: 0, - }, - IconDesc { - id: IconSourceID(0), - size: 64.0, - left: 300, - top: 140, - color: Color::rgb(200, 255, 200), - bounds, - metadata: 0, - }, - IconDesc { - id: IconSourceID(1), - size: 64.0, - left: 400, - top: 140, - color: Color::rgb(255, 255, 255), - bounds, - metadata: 0, - }, - ], - &mut swash_cache, + |input| svg_system.render_custom_glyph(input), ) .unwrap(); @@ -230,7 +240,6 @@ async fn run() { }); text_renderer.render(&atlas, &viewport, &mut pass).unwrap(); - icon_renderer.render(&atlas, &viewport, &mut pass).unwrap(); } queue.submit(Some(encoder.finish())); diff --git a/src/icon.rs b/src/icon.rs index 37c4442..5fe6c68 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -68,10 +68,7 @@ impl IconRenderer { let resolution = viewport.resolution(); let font_id = cosmic_text::fontdb::ID::dummy(); - // This is a bit of a hacky way to reserve a slot for icons in the text - // atlas, but this is a simple way to ensure that there will be no - // conflicts in the atlas without the need to create our own custom - // `CacheKey` struct with extra bytes. + let flags = CacheKeyFlags::from_bits_retain(u32::MAX); for icon in icons { diff --git a/src/lib.rs b/src/lib.rs index 390b5d7..1f42b82 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,17 +10,15 @@ mod text_atlas; mod text_render; mod viewport; -#[cfg(feature = "svg-icons")] -pub mod icon; +#[cfg(feature = "svg")] +pub mod svg; pub use cache::Cache; pub use error::{PrepareError, RenderError}; pub use text_atlas::{ColorMode, TextAtlas}; -pub use text_render::TextRenderer; +pub use text_render::{ContentType, TextRenderer}; pub use viewport::Viewport; -use text_render::ContentType; - // Re-export all top-level types from `cosmic-text` for convenience. #[doc(no_inline)] pub use cosmic_text::{ @@ -120,4 +118,66 @@ pub struct TextArea<'a> { pub bounds: TextBounds, // The default color of the text area. pub default_color: Color, + + // Since this has no effect on text layout yet, only expose this if + // the custom glyph feature is enabled. + #[cfg(feature = "custom-glyphs")] + /// Any additional boxes of non-textual content that is inline with text. + /// + /// Note, this currently does not affect layout of text. + /// (see: https://github.com/pop-os/cosmic-text/issues/80) + pub inline_boxes: Vec, } + +// Since this has no effect on text layout yet, only expose this if +// the custom glyph feature is enabled. +#[cfg(feature = "custom-glyphs")] +/// An arbitrary box of non-textual content that is inline with text. +/// +/// Note, this currently does not affect layout of text +/// (see: https://github.com/pop-os/cosmic-text/issues/80) +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct InlineBox { + /// The position of the left edge of the rectangular area. + pub left: f32, + /// The position of the top edge of the rectangular area. + pub top: f32, + /// The width of the rectangular area. + pub width: f32, + /// The height of the rectangular area. + pub height: f32, + + /// The content of the box. + pub content: InlineBoxContent, +} + +// Since this has no effect on text layout yet, only expose this if +// the custom glyph feature is enabled. +#[cfg(feature = "custom-glyphs")] +/// The contents of an [`InlineBox`] +#[non_exhaustive] +#[derive(Default, Debug, Clone, Copy, PartialEq)] +pub enum InlineBoxContent { + #[default] + None, + CustomGlyph { + /// The unique identifier for this glyph + id: CustomGlyphID, + /// The size of the glyph + size: f32, + /// The x offset of the glyph relative to the box + left: f32, + /// The y offset of the glyph relative to the box + top: f32, + /// The color of this glyph (only relevant if the glyph is rendered with the + /// type [`ContentType::Mask`]) + /// + /// Set to `None` to use [`TextArea::default_color`]. + color: Option, + /// Additional metadata about the glyph + metadata: usize, + }, +} + +#[cfg(feature = "custom-glyphs")] +pub type CustomGlyphID = u16; diff --git a/src/svg.rs b/src/svg.rs new file mode 100644 index 0000000..012a882 --- /dev/null +++ b/src/svg.rs @@ -0,0 +1,102 @@ +use resvg::{tiny_skia::Pixmap, usvg::Transform}; +use rustc_hash::FxHashMap; + +// Re-export resvg for convenience. +pub use resvg::*; + +use crate::{ + text_render::{ContentType, CustomGlyphInput, CustomGlyphOutput}, + CustomGlyphID, +}; + +#[derive(Default, Clone)] +pub struct SvgGlyphSystem { + svgs: FxHashMap, +} + +impl SvgGlyphSystem { + /// Add an svg source to this system. + /// + /// * id - A unique identifier for this resource. + /// * source - The parsed SVG data. + /// * is_symbolic - If `true`, then only the alpha channel will be used and the icon can + /// be filled with any solid color. If `false`, then the icon will be rendered in full + /// color. + pub fn add_svg(&mut self, id: CustomGlyphID, source: usvg::Tree, content_type: ContentType) { + self.svgs.insert( + id, + SvgData { + tree: source, + content_type, + }, + ); + } + + // Returns `true` if the source was removed, or `false` if there was + // no source with that ID. + pub fn remove(&mut self, id: CustomGlyphID) -> bool { + self.svgs.remove(&id).is_some() + } + + pub fn render_custom_glyph(&mut self, input: CustomGlyphInput) -> Option { + let Some(svg_data) = self.svgs.get(&input.id) else { + return None; + }; + + let svg_size = svg_data.tree.size(); + let max_side_len = svg_size.width().max(svg_size.height()); + + let should_rasterize = max_side_len > 0.0; + + let (scale, width, height, pixmap) = if should_rasterize { + let scale = input.size / max_side_len; + let width = (svg_size.width() * scale).ceil(); + let height = (svg_size.height() * scale).ceil(); + + if width <= 0.0 || height <= 0.0 { + (0.0, 0, 0, None) + } else if let Some(pixmap) = Pixmap::new(width as u32, height as u32) { + (scale, width as u32, height as u32, Some(pixmap)) + } else { + (0.0, 0, 0, None) + } + } else { + (0.0, 0, 0, None) + }; + + if let Some(mut pixmap) = pixmap { + let mut transform = Transform::from_scale(scale, scale); + + 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(&svg_data.tree, transform, &mut pixmap.as_mut()); + + let data: Vec = if let ContentType::Mask = svg_data.content_type { + // Only use the alpha channel for symbolic icons. + pixmap.data().iter().skip(3).step_by(4).copied().collect() + } else { + pixmap.data().to_vec() + }; + + Some(CustomGlyphOutput { + data, + width, + height, + content_type: svg_data.content_type, + }) + } else { + None + } + } +} + +#[derive(Clone)] +struct SvgData { + tree: usvg::Tree, + content_type: ContentType, +} diff --git a/src/text_render.rs b/src/text_render.rs index d42f7bc..6504db9 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -1,7 +1,8 @@ use crate::{ ColorMode, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError, - SwashCache, SwashContent, TextArea, TextAtlas, Viewport, + SwashCache, SwashContent, TextArea, TextAtlas, TextBounds, Viewport, }; +use cosmic_text::Color; use std::{slice, sync::Arc}; use wgpu::{ Buffer, BufferDescriptor, BufferUsages, DepthStencilState, Device, Extent3d, ImageCopyTexture, @@ -54,11 +55,103 @@ impl TextRenderer { text_areas: impl IntoIterator>, cache: &mut SwashCache, mut metadata_to_depth: impl FnMut(usize) -> f32, + #[cfg(feature = "custom-glyphs")] mut render_custom_glyph: impl FnMut( + CustomGlyphInput, + ) -> Option< + CustomGlyphOutput, + >, ) -> Result<(), PrepareError> { self.glyph_vertices.clear(); let resolution = viewport.resolution(); + #[cfg(feature = "custom-glyphs")] + let custom_glyph_font_id = cosmic_text::fontdb::ID::dummy(); + #[cfg(feature = "custom-glyphs")] + // This is a bit of a hacky way to reserve a slot for icons in the text + // atlas, but this is a simple way to ensure that there will be no + // conflicts in the atlas without the need to create our own custom + // `CacheKey` struct with extra bytes. + let custom_glyph_flags = cosmic_text::CacheKeyFlags::from_bits_retain(u32::MAX); + + let mut clip_and_add_glyph = |details: &GlyphDetails, + mut x: i32, + mut y: i32, + bounds: TextBounds, + color: Color, + metadata: usize, + color_mode: ColorMode| { + let (mut atlas_x, mut atlas_y, content_type) = match details.gpu_cache { + GpuCacheStatus::InAtlas { x, y, content_type } => (x, y, content_type), + GpuCacheStatus::SkipRasterization => return, + }; + + let mut width = details.width as i32; + let mut height = details.height as i32; + + let bounds_min_x = bounds.left.max(0); + let bounds_min_y = bounds.top.max(0); + let bounds_max_x = bounds.right.min(resolution.width as i32); + let bounds_max_y = bounds.bottom.min(resolution.height as i32); + + // Starts beyond right edge or ends beyond left edge + let max_x = x + width; + if x > bounds_max_x || max_x < bounds_min_x { + return; + } + + // Starts beyond bottom edge or ends beyond top edge + let max_y = y + height; + if y > bounds_max_y || max_y < bounds_min_y { + return; + } + + // Clip left ege + if x < bounds_min_x { + let right_shift = bounds_min_x - x; + + x = bounds_min_x; + width = max_x - bounds_min_x; + atlas_x += right_shift as u16; + } + + // Clip right edge + if x + width > bounds_max_x { + width = bounds_max_x - x; + } + + // Clip top edge + if y < bounds_min_y { + let bottom_shift = bounds_min_y - y; + + y = bounds_min_y; + height = max_y - bounds_min_y; + atlas_y += bottom_shift as u16; + } + + // Clip bottom edge + if y + height > bounds_max_y { + height = bounds_max_y - y; + } + + let depth = metadata_to_depth(metadata); + + self.glyph_vertices.push(GlyphToRender { + pos: [x, y], + dim: [width as u16, height as u16], + uv: [atlas_x, atlas_y], + color: color.0, + content_type_with_srgb: [ + content_type as u16, + match color_mode { + ColorMode::Accurate => TextColorConversion::ConvertToLinear, + ColorMode::Web => TextColorConversion::None, + } as u16, + ], + depth, + }); + }; + for text_area in text_areas { let bounds_min_x = text_area.bounds.left.max(0); let bounds_min_y = text_area.bounds.top.max(0); @@ -193,8 +286,8 @@ impl TextRenderer { let details = atlas.glyph(&physical_glyph.cache_key).unwrap(); - let mut x = physical_glyph.x + details.left as i32; - let mut y = (run.line_y * text_area.scale).round() as i32 + physical_glyph.y + let x = physical_glyph.x + details.left as i32; + let y = (run.line_y * text_area.scale).round() as i32 + physical_glyph.y - details.top as i32; let (mut atlas_x, mut atlas_y, content_type) = match details.gpu_cache { @@ -250,22 +343,15 @@ impl TextRenderer { None => text_area.default_color, }; - let depth = metadata_to_depth(glyph.metadata); - - self.glyph_vertices.push(GlyphToRender { - pos: [x, y], - dim: [width as u16, height as u16], - uv: [atlas_x, atlas_y], - color: color.0, - content_type_with_srgb: [ - content_type as u16, - match atlas.color_mode { - ColorMode::Accurate => TextColorConversion::ConvertToLinear, - ColorMode::Web => TextColorConversion::None, - } as u16, - ], - depth, - }); + clip_and_add_glyph( + atlas.glyph(&physical_glyph.cache_key).unwrap(), + x, + y, + text_area.bounds, + color, + glyph.metadata, + atlas.color_mode, + ); } } } @@ -312,6 +398,10 @@ impl TextRenderer { viewport: &Viewport, text_areas: impl IntoIterator>, cache: &mut SwashCache, + #[cfg(feature = "custom-glyphs")] render_custom_glyph: impl FnMut( + CustomGlyphInput, + ) + -> Option, ) -> Result<(), PrepareError> { self.prepare_with_depth( device, @@ -322,6 +412,8 @@ impl TextRenderer { text_areas, cache, zero_depth, + #[cfg(feature = "custom-glyphs")] + render_custom_glyph, ) } @@ -386,3 +478,27 @@ pub(crate) fn create_oversized_buffer( fn zero_depth(_: usize) -> f32 { 0f32 } + +#[cfg(feature = "custom-glyphs")] +#[derive(Debug, Clone, Copy, PartialEq)] +/// The input data to render a custom glyph +pub struct CustomGlyphInput { + /// The unique identifier of the glyph. + pub id: crate::CustomGlyphID, + /// The size of the glyph. + pub size: f32, + /// Binning of fractional X offset + pub x_bin: cosmic_text::SubpixelBin, + /// Binning of fractional Y offset + pub y_bin: cosmic_text::SubpixelBin, +} + +#[cfg(feature = "custom-glyphs")] +#[derive(Debug, Clone)] +/// The output of a rendered custom glyph +pub struct CustomGlyphOutput { + pub data: Vec, + pub width: u32, + pub height: u32, + pub content_type: ContentType, +} From 2ad0205486ac1657d84d93be8253a420bdc26f30 Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:19:27 -0500 Subject: [PATCH 05/22] remove unused file --- src/icon.rs | 424 ---------------------------------------------------- 1 file changed, 424 deletions(-) delete mode 100644 src/icon.rs diff --git a/src/icon.rs b/src/icon.rs deleted file mode 100644 index 5fe6c68..0000000 --- a/src/icon.rs +++ /dev/null @@ -1,424 +0,0 @@ -use cosmic_text::{CacheKey, CacheKeyFlags, Color, SubpixelBin}; -use resvg::{ - tiny_skia::Pixmap, - usvg::{self, Transform}, -}; -use rustc_hash::FxHashMap; -use std::{path::Path, slice, sync::Arc}; -use wgpu::{ - Buffer, BufferDescriptor, BufferUsages, DepthStencilState, Device, Extent3d, ImageCopyTexture, - ImageDataLayout, MultisampleState, Origin3d, Queue, RenderPass, RenderPipeline, TextureAspect, -}; - -use crate::{ - text_render::{ContentType, TextColorConversion}, - ColorMode, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError, - SwashCache, TextAtlas, TextBounds, Viewport, -}; - -/// An svg icon renderer that uses cached glyphs to render icons into an existing render pass. -pub struct IconRenderer { - vertex_buffer: Buffer, - vertex_buffer_size: u64, - pipeline: Arc, - glyph_vertices: Vec, -} - -impl IconRenderer { - /// Creates a new [`IconRenderer`]. - pub fn new( - atlas: &mut TextAtlas, - device: &Device, - multisample: MultisampleState, - depth_stencil: Option, - ) -> Self { - let vertex_buffer_size = crate::text_render::next_copy_buffer_size(32); - let vertex_buffer = device.create_buffer(&BufferDescriptor { - label: Some("glyphon icon vertices"), - size: vertex_buffer_size, - usage: BufferUsages::VERTEX | BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - let pipeline = atlas.get_or_create_pipeline(device, multisample, depth_stencil); - - Self { - vertex_buffer, - vertex_buffer_size, - pipeline, - glyph_vertices: Vec::new(), - } - } - - /// Prepares all of the given icons for rendering. - pub fn prepare_with_depth<'a>( - &mut self, - device: &Device, - queue: &Queue, - icon_system: &mut IconSystem, - font_system: &mut FontSystem, - atlas: &mut TextAtlas, - viewport: &Viewport, - icons: impl IntoIterator, - cache: &mut SwashCache, - mut metadata_to_depth: impl FnMut(usize) -> f32, - ) -> Result<(), PrepareError> { - self.glyph_vertices.clear(); - - let resolution = viewport.resolution(); - - let font_id = cosmic_text::fontdb::ID::dummy(); - - let flags = CacheKeyFlags::from_bits_retain(u32::MAX); - - for icon in icons { - let cache_key = CacheKey { - font_id, - glyph_id: icon.id.0, - font_size_bits: icon.size.to_bits(), - x_bin: SubpixelBin::Zero, - y_bin: SubpixelBin::Zero, - flags, - }; - - if atlas.mask_atlas.glyph_cache.contains(&cache_key) { - atlas.mask_atlas.promote(cache_key); - } else if atlas.color_atlas.glyph_cache.contains(&cache_key) { - atlas.color_atlas.promote(cache_key); - } else { - let Some(svg_data) = icon_system.svgs.get(&icon.id) else { - continue; - }; - - let content_type = if svg_data.is_symbolic { - ContentType::Mask - } else { - ContentType::Color - }; - - let icon_size = svg_data.tree.size(); - let max_side_len = icon_size.width().max(icon_size.height()); - - let should_rasterize = max_side_len > 0.0; - - let (scale, width, height, mut pixmap) = if should_rasterize { - let scale = icon.size / max_side_len; - let width = (icon_size.width() * scale).ceil(); - let height = (icon_size.height() * scale).ceil(); - - if width <= 0.0 || height <= 0.0 { - (0.0, 0, 0, None) - } else if let Some(pixmap) = Pixmap::new(width as u32, height as u32) { - (scale, width as u32, height as u32, Some(pixmap)) - } else { - (0.0, 0, 0, None) - } - } else { - (0.0, 0, 0, None) - }; - - let (gpu_cache, atlas_id, inner) = if let Some(mut pixmap) = pixmap.take() { - let transform = Transform::from_scale(scale, scale); - - resvg::render(&svg_data.tree, transform, &mut pixmap.as_mut()); - - let alpha_image: Vec; - let data = if let ContentType::Mask = content_type { - // Only use the alpha channel for symbolic icons. - alpha_image = pixmap.data().iter().skip(3).step_by(4).copied().collect(); - &alpha_image - } else { - pixmap.data() - }; - - let mut inner = atlas.inner_for_content_mut(content_type); - - // Find a position in the packer - let allocation = loop { - match inner.try_allocate(width as usize, height as usize) { - Some(a) => break a, - None => { - if !atlas.grow(device, queue, font_system, cache, content_type) { - return Err(PrepareError::AtlasFull); - } - - inner = atlas.inner_for_content_mut(content_type); - } - } - }; - let atlas_min = allocation.rectangle.min; - - queue.write_texture( - ImageCopyTexture { - texture: &inner.texture, - mip_level: 0, - origin: Origin3d { - x: atlas_min.x as u32, - y: atlas_min.y as u32, - z: 0, - }, - aspect: TextureAspect::All, - }, - data, - ImageDataLayout { - offset: 0, - bytes_per_row: Some(width as u32 * inner.num_channels() as u32), - rows_per_image: None, - }, - Extent3d { - width: width as u32, - height: height as u32, - depth_or_array_layers: 1, - }, - ); - - ( - GpuCacheStatus::InAtlas { - x: atlas_min.x as u16, - y: atlas_min.y as u16, - content_type, - }, - Some(allocation.id), - inner, - ) - } else { - let inner = &mut atlas.color_atlas; - (GpuCacheStatus::SkipRasterization, None, inner) - }; - - inner.put( - cache_key, - GlyphDetails { - width: width as u16, - height: height as u16, - gpu_cache, - atlas_id, - top: 0, - left: 0, - }, - ); - } - - let details = atlas.glyph(&cache_key).unwrap(); - - let mut x = icon.left; - let mut y = icon.top; - - let (mut atlas_x, mut atlas_y, content_type) = match details.gpu_cache { - GpuCacheStatus::InAtlas { x, y, content_type } => (x, y, content_type), - GpuCacheStatus::SkipRasterization => continue, - }; - - let mut width = details.width as i32; - let mut height = details.height as i32; - - let bounds_min_x = icon.bounds.left.max(0); - let bounds_min_y = icon.bounds.top.max(0); - let bounds_max_x = icon.bounds.right.min(resolution.width as i32); - let bounds_max_y = icon.bounds.bottom.min(resolution.height as i32); - - // Starts beyond right edge or ends beyond left edge - let max_x = x + width; - if x > bounds_max_x || max_x < bounds_min_x { - continue; - } - - // Starts beyond bottom edge or ends beyond top edge - let max_y = y + height; - if y > bounds_max_y || max_y < bounds_min_y { - continue; - } - - // Clip left ege - if x < bounds_min_x { - let right_shift = bounds_min_x - x; - - x = bounds_min_x; - width = max_x - bounds_min_x; - atlas_x += right_shift as u16; - } - - // Clip right edge - if x + width > bounds_max_x { - width = bounds_max_x - x; - } - - // Clip top edge - if y < bounds_min_y { - let bottom_shift = bounds_min_y - y; - - y = bounds_min_y; - height = max_y - bounds_min_y; - atlas_y += bottom_shift as u16; - } - - // Clip bottom edge - if y + height > bounds_max_y { - height = bounds_max_y - y; - } - - let depth = metadata_to_depth(icon.metadata); - - self.glyph_vertices.push(GlyphToRender { - pos: [x, y], - dim: [width as u16, height as u16], - uv: [atlas_x, atlas_y], - color: icon.color.0, - content_type_with_srgb: [ - content_type as u16, - match atlas.color_mode { - ColorMode::Accurate => TextColorConversion::ConvertToLinear, - ColorMode::Web => TextColorConversion::None, - } as u16, - ], - depth, - }); - } - - let will_render = !self.glyph_vertices.is_empty(); - if !will_render { - return Ok(()); - } - - let vertices = self.glyph_vertices.as_slice(); - let vertices_raw = unsafe { - slice::from_raw_parts( - vertices as *const _ as *const u8, - std::mem::size_of_val(vertices), - ) - }; - - if self.vertex_buffer_size >= vertices_raw.len() as u64 { - queue.write_buffer(&self.vertex_buffer, 0, vertices_raw); - } else { - self.vertex_buffer.destroy(); - - let (buffer, buffer_size) = crate::text_render::create_oversized_buffer( - device, - Some("glyphon icon vertices"), - vertices_raw, - BufferUsages::VERTEX | BufferUsages::COPY_DST, - ); - - self.vertex_buffer = buffer; - self.vertex_buffer_size = buffer_size; - } - - Ok(()) - } - - /// Prepares all of the given icons for rendering. - pub fn prepare<'a>( - &mut self, - device: &Device, - queue: &Queue, - icon_system: &mut IconSystem, - font_system: &mut FontSystem, - atlas: &mut TextAtlas, - viewport: &Viewport, - icons: impl IntoIterator, - cache: &mut SwashCache, - ) -> Result<(), PrepareError> { - self.prepare_with_depth( - device, - queue, - icon_system, - font_system, - atlas, - viewport, - icons, - cache, - zero_depth, - ) - } - - /// Renders all icons that were previously provided to [`IconRenderer::prepare`]. - pub fn render<'pass>( - &'pass self, - atlas: &'pass TextAtlas, - viewport: &'pass Viewport, - pass: &mut RenderPass<'pass>, - ) -> Result<(), RenderError> { - if self.glyph_vertices.is_empty() { - return Ok(()); - } - - pass.set_pipeline(&self.pipeline); - pass.set_bind_group(0, &atlas.bind_group, &[]); - pass.set_bind_group(1, &viewport.bind_group, &[]); - pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); - pass.draw(0..4, 0..self.glyph_vertices.len() as u32); - - Ok(()) - } -} - -/// The description of an icon to be rendered. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct IconDesc { - /// The unique identifier for the source of data to use for this icon. - pub id: IconSourceID, - /// The size of the icon in points. This will be the length of the longest side. - pub size: f32, - /// The left edge of the icon. - pub left: i32, - /// The top edge of the icon. - pub top: i32, - /// The color of the icon. This is only relevant if the icon source data is symbolic. - pub color: Color, - /// The visible bounds of the text area. This is used to clip the icon and doesn't have to - /// match the `left` and `top` values. - pub bounds: TextBounds, - /// Additional metadata about this icon. - pub metadata: usize, -} - -/// A unique identifier for a given source of icon data. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct IconSourceID(pub u16); - -struct IconData { - tree: usvg::Tree, - is_symbolic: bool, -} - -/// A system of loaded resources for icons. -pub struct IconSystem { - svgs: FxHashMap, -} - -impl IconSystem { - /// Construct a new [`IconSystem`]. - pub fn new() -> Self { - Self { - svgs: FxHashMap::default(), - } - } - - /// Add an svg source to this system. - /// - /// * id - A unique identifier for this resource. - /// * source - The parsed SVG data. - /// * is_symbolic - If `true`, then only the alpha channel will be used and the icon can - /// be filled with any solid color. If `false`, then the icon will be rendered in full - /// color. - pub fn add_svg(&mut self, id: IconSourceID, source: usvg::Tree, is_symbolic: bool) { - self.svgs.insert( - id, - IconData { - tree: source, - is_symbolic, - }, - ); - } - - // Returns `true` if the source was removed, or `false` if there was - // no source with that ID. - pub fn remove(&mut self, id: IconSourceID) -> bool { - self.svgs.remove(&id).is_some() - } -} - -fn zero_depth(_: usize) -> f32 { - 0f32 -} From 0d013ebfa6250ee8fab369c77ab556df43753371 Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:24:23 -0500 Subject: [PATCH 06/22] expose custom glyph structs --- src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 1f42b82..2e1b3b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,9 @@ pub use text_atlas::{ColorMode, TextAtlas}; pub use text_render::{ContentType, TextRenderer}; pub use viewport::Viewport; +#[cfg(feature = "custom-glyphs")] +pub use text_render::{CustomGlyphInput, CustomGlyphOutput}; + // Re-export all top-level types from `cosmic-text` for convenience. #[doc(no_inline)] pub use cosmic_text::{ From 3b9bb0be3b84cb1bf34b45082e081e178fe9d4b1 Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:41:26 -0500 Subject: [PATCH 07/22] remove `InlineBox` --- examples/svg-icons.rs | 71 ++++++++++++++----------------------------- src/lib.rs | 68 +++++++++++------------------------------ 2 files changed, 40 insertions(+), 99 deletions(-) diff --git a/examples/svg-icons.rs b/examples/svg-icons.rs index 4742fdc..510da7d 100644 --- a/examples/svg-icons.rs +++ b/examples/svg-icons.rs @@ -1,8 +1,7 @@ use glyphon::{ svg::{usvg, SvgGlyphSystem}, - Attrs, Buffer, Cache, Color, ContentType, Family, FontSystem, InlineBox, InlineBoxContent, - Metrics, Resolution, Shaping, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, - Viewport, + Attrs, Buffer, Cache, Color, ContentType, CustomGlyphDesc, Family, FontSystem, Metrics, + Resolution, Shaping, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, }; use std::sync::Arc; use wgpu::{ @@ -150,62 +149,38 @@ async fn run() { bottom: 180, }, default_color: Color::rgb(255, 255, 255), - inline_boxes: vec![ - InlineBox { + custom_glyphs: vec![ + CustomGlyphDesc { + id: 0, left: 300.0, top: 15.0, - width: 64.0, - height: 64.0, - content: InlineBoxContent::CustomGlyph { - id: 0, - size: 64.0, - left: 0.0, - top: 0.0, - color: Some(Color::rgb(200, 200, 255)), - metadata: 0, - }, + size: 64.0, + color: Some(Color::rgb(200, 200, 255)), + metadata: 0, }, - InlineBox { + CustomGlyphDesc { + id: 1, left: 400.0, top: 15.0, - width: 64.0, - height: 64.0, - content: InlineBoxContent::CustomGlyph { - id: 1, - size: 64.0, - left: 0.0, - top: 0.0, - color: None, - metadata: 0, - }, + size: 64.0, + color: None, + metadata: 0, }, - InlineBox { + CustomGlyphDesc { + id: 0, left: 300.0, top: 140.0, - width: 64.0, - height: 64.0, - content: InlineBoxContent::CustomGlyph { - id: 0, - size: 64.0, - left: 0.0, - top: 0.0, - color: Some(Color::rgb(200, 255, 200)), - metadata: 0, - }, + size: 64.0, + color: Some(Color::rgb(200, 255, 200)), + metadata: 0, }, - InlineBox { + CustomGlyphDesc { + id: 1, left: 400.0, top: 140.0, - width: 64.0, - height: 64.0, - content: InlineBoxContent::CustomGlyph { - id: 1, - size: 64.0, - left: 0.0, - top: 0.0, - color: None, - metadata: 0, - }, + size: 64.0, + color: None, + metadata: 0, }, ], }], diff --git a/src/lib.rs b/src/lib.rs index 2e1b3b1..617990d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -122,64 +122,30 @@ pub struct TextArea<'a> { // The default color of the text area. pub default_color: Color, - // Since this has no effect on text layout yet, only expose this if - // the custom glyph feature is enabled. #[cfg(feature = "custom-glyphs")] - /// Any additional boxes of non-textual content that is inline with text. - /// - /// Note, this currently does not affect layout of text. - /// (see: https://github.com/pop-os/cosmic-text/issues/80) - pub inline_boxes: Vec, + /// Additional custom glyphs to render + pub custom_glyphs: Vec, } -// Since this has no effect on text layout yet, only expose this if -// the custom glyph feature is enabled. #[cfg(feature = "custom-glyphs")] -/// An arbitrary box of non-textual content that is inline with text. -/// -/// Note, this currently does not affect layout of text -/// (see: https://github.com/pop-os/cosmic-text/issues/80) +/// A custom glyph to render #[derive(Debug, Clone, Copy, PartialEq)] -pub struct InlineBox { - /// The position of the left edge of the rectangular area. +pub struct CustomGlyphDesc { + /// The unique identifier for this glyph + pub id: CustomGlyphID, + /// The position of the left edge of the glyph pub left: f32, - /// The position of the top edge of the rectangular area. + /// The position of the top edge of the glyph pub top: f32, - /// The width of the rectangular area. - pub width: f32, - /// The height of the rectangular area. - pub height: f32, - - /// The content of the box. - pub content: InlineBoxContent, -} - -// Since this has no effect on text layout yet, only expose this if -// the custom glyph feature is enabled. -#[cfg(feature = "custom-glyphs")] -/// The contents of an [`InlineBox`] -#[non_exhaustive] -#[derive(Default, Debug, Clone, Copy, PartialEq)] -pub enum InlineBoxContent { - #[default] - None, - CustomGlyph { - /// The unique identifier for this glyph - id: CustomGlyphID, - /// The size of the glyph - size: f32, - /// The x offset of the glyph relative to the box - left: f32, - /// The y offset of the glyph relative to the box - top: f32, - /// The color of this glyph (only relevant if the glyph is rendered with the - /// type [`ContentType::Mask`]) - /// - /// Set to `None` to use [`TextArea::default_color`]. - color: Option, - /// Additional metadata about the glyph - metadata: usize, - }, + /// The size of the glyph + pub size: f32, + /// The color of this glyph (only relevant if the glyph is rendered with the + /// type [`ContentType::Mask`]) + /// + /// Set to `None` to use [`TextArea::default_color`]. + pub color: Option, + /// Additional metadata about the glyph + pub metadata: usize, } #[cfg(feature = "custom-glyphs")] From e7170eee2cfafe414886792526d57ba116769400 Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Tue, 9 Jul 2024 14:50:40 -0500 Subject: [PATCH 08/22] use slice for TextArea::custom_glyphs --- examples/svg-icons.rs | 2 +- src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/svg-icons.rs b/examples/svg-icons.rs index 510da7d..d93faef 100644 --- a/examples/svg-icons.rs +++ b/examples/svg-icons.rs @@ -149,7 +149,7 @@ async fn run() { bottom: 180, }, default_color: Color::rgb(255, 255, 255), - custom_glyphs: vec![ + custom_glyphs: &[ CustomGlyphDesc { id: 0, left: 300.0, diff --git a/src/lib.rs b/src/lib.rs index 617990d..3dd145e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -124,7 +124,7 @@ pub struct TextArea<'a> { #[cfg(feature = "custom-glyphs")] /// Additional custom glyphs to render - pub custom_glyphs: Vec, + pub custom_glyphs: &'a [CustomGlyphDesc], } #[cfg(feature = "custom-glyphs")] From d3c98104b76d0c4b8b393a1ab611eaa56d91ccc2 Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Tue, 9 Jul 2024 16:15:24 -0500 Subject: [PATCH 09/22] offset custom glyphs by text area position --- examples/svg-icons.rs | 8 ++++---- src/lib.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/svg-icons.rs b/examples/svg-icons.rs index d93faef..ac7874a 100644 --- a/examples/svg-icons.rs +++ b/examples/svg-icons.rs @@ -153,7 +153,7 @@ async fn run() { CustomGlyphDesc { id: 0, left: 300.0, - top: 15.0, + top: 5.0, size: 64.0, color: Some(Color::rgb(200, 200, 255)), metadata: 0, @@ -161,7 +161,7 @@ async fn run() { CustomGlyphDesc { id: 1, left: 400.0, - top: 15.0, + top: 5.0, size: 64.0, color: None, metadata: 0, @@ -169,7 +169,7 @@ async fn run() { CustomGlyphDesc { id: 0, left: 300.0, - top: 140.0, + top: 130.0, size: 64.0, color: Some(Color::rgb(200, 255, 200)), metadata: 0, @@ -177,7 +177,7 @@ async fn run() { CustomGlyphDesc { id: 1, left: 400.0, - top: 140.0, + top: 130.0, size: 64.0, color: None, metadata: 0, diff --git a/src/lib.rs b/src/lib.rs index 3dd145e..fa947b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -129,7 +129,7 @@ pub struct TextArea<'a> { #[cfg(feature = "custom-glyphs")] /// A custom glyph to render -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Default, Debug, Clone, Copy, PartialEq)] pub struct CustomGlyphDesc { /// The unique identifier for this glyph pub id: CustomGlyphID, From af65741595c7d4cdce3146113edd7637086f14ee Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Thu, 11 Jul 2024 15:44:35 -0500 Subject: [PATCH 10/22] remove svg feature --- Cargo.toml | 9 +-- examples/{svg-icons.rs => custom-glyphs.rs} | 72 +++++++++++++++------ src/lib.rs | 3 - src/svg.rs | 2 +- src/text_render.rs | 11 ++-- 5 files changed, 64 insertions(+), 33 deletions(-) rename examples/{svg-icons.rs => custom-glyphs.rs} (79%) diff --git a/Cargo.toml b/Cargo.toml index e30dee9..069bffb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,6 @@ license = "MIT OR Apache-2.0 OR Zlib" [features] custom-glyphs = [] -svg = ["dep:resvg", "custom-glyphs"] -svg-raster-images = ["resvg?/raster-images"] [dependencies] wgpu = { version = "22", default-features = false, features = ["wgsl"] } @@ -18,7 +16,6 @@ etagere = "0.2.10" cosmic-text = "0.12" lru = { version = "0.12.1", default-features = false } rustc-hash = "2.0" -resvg = { version = "0.42", default-features = false, optional = true } [dev-dependencies] winit = "0.30.3" @@ -27,6 +24,6 @@ resvg = { version = "0.42", default-features = false } pollster = "0.3.0" [[example]] -name = "svg-icons" -path = "examples/svg-icons.rs" -required-features = ["svg"] \ No newline at end of file +name = "custom-glyphs" +path = "examples/custom-glyphs.rs" +required-features = ["custom-glyphs"] \ No newline at end of file diff --git a/examples/svg-icons.rs b/examples/custom-glyphs.rs similarity index 79% rename from examples/svg-icons.rs rename to examples/custom-glyphs.rs index ac7874a..7098cbf 100644 --- a/examples/svg-icons.rs +++ b/examples/custom-glyphs.rs @@ -1,7 +1,7 @@ use glyphon::{ - svg::{usvg, SvgGlyphSystem}, - Attrs, Buffer, Cache, Color, ContentType, CustomGlyphDesc, Family, FontSystem, Metrics, - Resolution, Shaping, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, + Attrs, Buffer, Cache, Color, ContentType, CustomGlyphDesc, CustomGlyphInput, CustomGlyphOutput, + Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, TextArea, TextAtlas, TextBounds, + TextRenderer, Viewport, }; use std::sync::Arc; use wgpu::{ @@ -32,7 +32,7 @@ async fn run() { let window = Arc::new( WindowBuilder::new() .with_inner_size(LogicalSize::new(width as f64, height as f64)) - .with_title("glyphon svg icons") + .with_title("glyphon custom glyphs") .build(&event_loop) .unwrap(), ); @@ -92,20 +92,56 @@ async fn run() { ); text_buffer.shape_until_scroll(&mut font_system, false); - // Set up svg system - let mut svg_system = SvgGlyphSystem::default(); + // Set up custom svg renderer - // Add SVG sources - svg_system.add_svg( - 0, - usvg::Tree::from_data(LION_SVG, &Default::default()).unwrap(), - ContentType::Mask, - ); - svg_system.add_svg( - 1, - usvg::Tree::from_data(EAGLE_SVG, &Default::default()).unwrap(), - ContentType::Color, - ); + let svg_0 = resvg::usvg::Tree::from_data(LION_SVG, &Default::default()).unwrap(); + let svg_1 = resvg::usvg::Tree::from_data(EAGLE_SVG, &Default::default()).unwrap(); + + let rasterize_svg = move |input: CustomGlyphInput| -> Option { + // Select the svg data based on the custom glyph ID. + let (svg, content_type) = match input.id { + 0 => (&svg_0, ContentType::Mask), + 1 => (&svg_1, ContentType::Color), + _ => return None, + }; + + // Calculate the scale based on the "font size". + let svg_size = svg.size(); + let max_side_len = svg_size.width().max(svg_size.height()); + let scale = input.size / max_side_len; + + // Create a buffer to write pixels to. + let width = (svg_size.width() * scale).ceil() as u32; + let height = (svg_size.height() * scale).ceil() as u32; + let Some(mut pixmap) = resvg::tiny_skia::Pixmap::new(width, height) else { + return None; + }; + + let mut transform = resvg::usvg::Transform::from_scale(scale, scale); + + // 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(svg, transform, &mut pixmap.as_mut()); + + let data: Vec = if let ContentType::Mask = content_type { + // Only use the alpha channel for symbolic icons. + pixmap.data().iter().skip(3).step_by(4).copied().collect() + } else { + pixmap.data().to_vec() + }; + + Some(CustomGlyphOutput { + data, + width, + height, + content_type, + }) + }; event_loop .run(move |event, target| { @@ -185,7 +221,7 @@ async fn run() { ], }], &mut swash_cache, - |input| svg_system.render_custom_glyph(input), + |input| rasterize_svg(input), ) .unwrap(); diff --git a/src/lib.rs b/src/lib.rs index fa947b9..0adc9bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,9 +10,6 @@ mod text_atlas; mod text_render; mod viewport; -#[cfg(feature = "svg")] -pub mod svg; - pub use cache::Cache; pub use error::{PrepareError, RenderError}; pub use text_atlas::{ColorMode, TextAtlas}; diff --git a/src/svg.rs b/src/svg.rs index 012a882..7c13c79 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -38,7 +38,7 @@ impl SvgGlyphSystem { self.svgs.remove(&id).is_some() } - pub fn render_custom_glyph(&mut self, input: CustomGlyphInput) -> Option { + pub fn rasterize_custom_glyph(&mut self, input: CustomGlyphInput) -> Option { let Some(svg_data) = self.svgs.get(&input.id) else { return None; }; diff --git a/src/text_render.rs b/src/text_render.rs index 6504db9..0c20346 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -55,7 +55,7 @@ impl TextRenderer { text_areas: impl IntoIterator>, cache: &mut SwashCache, mut metadata_to_depth: impl FnMut(usize) -> f32, - #[cfg(feature = "custom-glyphs")] mut render_custom_glyph: impl FnMut( + #[cfg(feature = "custom-glyphs")] mut rasterize_custom_glyph: impl FnMut( CustomGlyphInput, ) -> Option< CustomGlyphOutput, @@ -398,10 +398,11 @@ impl TextRenderer { viewport: &Viewport, text_areas: impl IntoIterator>, cache: &mut SwashCache, - #[cfg(feature = "custom-glyphs")] render_custom_glyph: impl FnMut( + #[cfg(feature = "custom-glyphs")] rasterize_custom_glyph: impl FnMut( CustomGlyphInput, - ) - -> Option, + ) -> Option< + CustomGlyphOutput, + >, ) -> Result<(), PrepareError> { self.prepare_with_depth( device, @@ -413,7 +414,7 @@ impl TextRenderer { cache, zero_depth, #[cfg(feature = "custom-glyphs")] - render_custom_glyph, + rasterize_custom_glyph, ) } From 55bcbf3365ed866803272ba503fc8ab20ad851ae Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Thu, 11 Jul 2024 15:55:44 -0500 Subject: [PATCH 11/22] remove unused file --- src/svg.rs | 102 ----------------------------------------------------- 1 file changed, 102 deletions(-) delete mode 100644 src/svg.rs diff --git a/src/svg.rs b/src/svg.rs deleted file mode 100644 index 7c13c79..0000000 --- a/src/svg.rs +++ /dev/null @@ -1,102 +0,0 @@ -use resvg::{tiny_skia::Pixmap, usvg::Transform}; -use rustc_hash::FxHashMap; - -// Re-export resvg for convenience. -pub use resvg::*; - -use crate::{ - text_render::{ContentType, CustomGlyphInput, CustomGlyphOutput}, - CustomGlyphID, -}; - -#[derive(Default, Clone)] -pub struct SvgGlyphSystem { - svgs: FxHashMap, -} - -impl SvgGlyphSystem { - /// Add an svg source to this system. - /// - /// * id - A unique identifier for this resource. - /// * source - The parsed SVG data. - /// * is_symbolic - If `true`, then only the alpha channel will be used and the icon can - /// be filled with any solid color. If `false`, then the icon will be rendered in full - /// color. - pub fn add_svg(&mut self, id: CustomGlyphID, source: usvg::Tree, content_type: ContentType) { - self.svgs.insert( - id, - SvgData { - tree: source, - content_type, - }, - ); - } - - // Returns `true` if the source was removed, or `false` if there was - // no source with that ID. - pub fn remove(&mut self, id: CustomGlyphID) -> bool { - self.svgs.remove(&id).is_some() - } - - pub fn rasterize_custom_glyph(&mut self, input: CustomGlyphInput) -> Option { - let Some(svg_data) = self.svgs.get(&input.id) else { - return None; - }; - - let svg_size = svg_data.tree.size(); - let max_side_len = svg_size.width().max(svg_size.height()); - - let should_rasterize = max_side_len > 0.0; - - let (scale, width, height, pixmap) = if should_rasterize { - let scale = input.size / max_side_len; - let width = (svg_size.width() * scale).ceil(); - let height = (svg_size.height() * scale).ceil(); - - if width <= 0.0 || height <= 0.0 { - (0.0, 0, 0, None) - } else if let Some(pixmap) = Pixmap::new(width as u32, height as u32) { - (scale, width as u32, height as u32, Some(pixmap)) - } else { - (0.0, 0, 0, None) - } - } else { - (0.0, 0, 0, None) - }; - - if let Some(mut pixmap) = pixmap { - let mut transform = Transform::from_scale(scale, scale); - - 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(&svg_data.tree, transform, &mut pixmap.as_mut()); - - let data: Vec = if let ContentType::Mask = svg_data.content_type { - // Only use the alpha channel for symbolic icons. - pixmap.data().iter().skip(3).step_by(4).copied().collect() - } else { - pixmap.data().to_vec() - }; - - Some(CustomGlyphOutput { - data, - width, - height, - content_type: svg_data.content_type, - }) - } else { - None - } - } -} - -#[derive(Clone)] -struct SvgData { - tree: usvg::Tree, - content_type: ContentType, -} From 49f1180a0c40868a7270db385e3fd8476076a423 Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Sun, 21 Jul 2024 17:26:56 -0500 Subject: [PATCH 12/22] add scale field to CustomGlyphInput --- examples/custom-glyphs.rs | 5 +++-- src/text_render.rs | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/custom-glyphs.rs b/examples/custom-glyphs.rs index 7098cbf..3223137 100644 --- a/examples/custom-glyphs.rs +++ b/examples/custom-glyphs.rs @@ -105,10 +105,11 @@ async fn run() { _ => return None, }; - // Calculate the scale based on the "font size". + // Calculate the scale based on the "glyph size". + let glyph_size = input.size * input.scale; let svg_size = svg.size(); let max_side_len = svg_size.width().max(svg_size.height()); - let scale = input.size / max_side_len; + let scale = glyph_size / max_side_len; // Create a buffer to write pixels to. let width = (svg_size.width() * scale).ceil() as u32; diff --git a/src/text_render.rs b/src/text_render.rs index 0c20346..025bab6 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -486,8 +486,10 @@ fn zero_depth(_: usize) -> f32 { pub struct CustomGlyphInput { /// The unique identifier of the glyph. pub id: crate::CustomGlyphID, - /// The size of the glyph. + /// The size of the glyph in points (not scaled by the text area's scaling factor) pub size: f32, + /// The scaling factor applied to the text area. + pub scale: f32, /// Binning of fractional X offset pub x_bin: cosmic_text::SubpixelBin, /// Binning of fractional Y offset From 8ef9b55c5726a8e1361ef720115f8c21e0779cc3 Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Fri, 26 Jul 2024 12:27:41 -0500 Subject: [PATCH 13/22] update custom-glyphs example to winit 0.30 --- examples/custom-glyphs.rs | 527 +++++++++++++++++++++----------------- 1 file changed, 295 insertions(+), 232 deletions(-) diff --git a/examples/custom-glyphs.rs b/examples/custom-glyphs.rs index 3223137..83b2b71 100644 --- a/examples/custom-glyphs.rs +++ b/examples/custom-glyphs.rs @@ -10,259 +10,322 @@ use wgpu::{ RenderPassDescriptor, RequestAdapterOptions, SurfaceConfiguration, TextureFormat, TextureUsages, TextureViewDescriptor, }; -use winit::{ - dpi::LogicalSize, - event::{Event, WindowEvent}, - event_loop::EventLoop, - window::WindowBuilder, -}; +use winit::{dpi::LogicalSize, event::WindowEvent, event_loop::EventLoop, window::Window}; // Example SVG icons are from https://publicdomainvectors.org/ static LION_SVG: &[u8] = include_bytes!("./lion.svg"); static EAGLE_SVG: &[u8] = include_bytes!("./eagle.svg"); fn main() { - pollster::block_on(run()); -} - -async fn run() { - // Set up window - let (width, height) = (800, 600); let event_loop = EventLoop::new().unwrap(); - let window = Arc::new( - WindowBuilder::new() - .with_inner_size(LogicalSize::new(width as f64, height as f64)) - .with_title("glyphon custom glyphs") - .build(&event_loop) - .unwrap(), - ); - let size = window.inner_size(); - let scale_factor = window.scale_factor(); - - // Set up surface - let instance = Instance::new(InstanceDescriptor::default()); - let adapter = instance - .request_adapter(&RequestAdapterOptions::default()) - .await - .unwrap(); - let (device, queue) = adapter - .request_device(&DeviceDescriptor::default(), None) - .await + event_loop + .run_app(&mut Application { window_state: None }) .unwrap(); +} + +struct WindowState { + device: wgpu::Device, + queue: wgpu::Queue, + surface: wgpu::Surface<'static>, + surface_config: SurfaceConfiguration, + + font_system: FontSystem, + swash_cache: SwashCache, + viewport: glyphon::Viewport, + atlas: glyphon::TextAtlas, + text_renderer: glyphon::TextRenderer, + text_buffer: glyphon::Buffer, - let surface = instance - .create_surface(window.clone()) - .expect("Create surface"); - let swapchain_format = TextureFormat::Bgra8UnormSrgb; - let mut config = SurfaceConfiguration { - usage: TextureUsages::RENDER_ATTACHMENT, - format: swapchain_format, - width: size.width, - height: size.height, - present_mode: PresentMode::Fifo, - alpha_mode: CompositeAlphaMode::Opaque, - view_formats: vec![], - desired_maximum_frame_latency: 2, - }; - surface.configure(&device, &config); - - // Set up text renderer - let mut font_system = FontSystem::new(); - let mut swash_cache = SwashCache::new(); - let cache = Cache::new(&device); - let mut viewport = Viewport::new(&device, &cache); - let mut atlas = TextAtlas::new(&device, &queue, &cache, swapchain_format); - let mut text_renderer = - TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None); - let mut text_buffer = Buffer::new(&mut font_system, Metrics::new(30.0, 42.0)); - - let physical_width = (width as f64 * scale_factor) as f32; - let physical_height = (height as f64 * scale_factor) as f32; - - text_buffer.set_size( - &mut font_system, - Some(physical_width), - Some(physical_height), - ); - text_buffer.set_text( - &mut font_system, - "SVG icons! --->\n\nThe icons below should be partially clipped.", - Attrs::new().family(Family::SansSerif), - Shaping::Advanced, - ); - text_buffer.shape_until_scroll(&mut font_system, false); - - // Set up custom svg renderer - - let svg_0 = resvg::usvg::Tree::from_data(LION_SVG, &Default::default()).unwrap(); - let svg_1 = resvg::usvg::Tree::from_data(EAGLE_SVG, &Default::default()).unwrap(); - - let rasterize_svg = move |input: CustomGlyphInput| -> Option { - // Select the svg data based on the custom glyph ID. - let (svg, content_type) = match input.id { - 0 => (&svg_0, ContentType::Mask), - 1 => (&svg_1, ContentType::Color), - _ => return None, + rasterize_svg: Box Option>, + + // Make sure that the winit window is last in the struct so that + // it is dropped after the wgpu surface is dropped, otherwise the + // program may crash when closed. This is probably a bug in wgpu. + window: Arc, +} + +impl WindowState { + async fn new(window: Arc) -> Self { + let physical_size = window.inner_size(); + let scale_factor = window.scale_factor(); + + // Set up surface + let instance = Instance::new(InstanceDescriptor::default()); + let adapter = instance + .request_adapter(&RequestAdapterOptions::default()) + .await + .unwrap(); + let (device, queue) = adapter + .request_device(&DeviceDescriptor::default(), None) + .await + .unwrap(); + + let surface = instance + .create_surface(window.clone()) + .expect("Create surface"); + let swapchain_format = TextureFormat::Bgra8UnormSrgb; + let surface_config = SurfaceConfiguration { + usage: TextureUsages::RENDER_ATTACHMENT, + format: swapchain_format, + width: physical_size.width, + height: physical_size.height, + present_mode: PresentMode::Fifo, + alpha_mode: CompositeAlphaMode::Opaque, + view_formats: vec![], + desired_maximum_frame_latency: 2, }; + surface.configure(&device, &surface_config); + + // Set up text renderer + let mut font_system = FontSystem::new(); + let swash_cache = SwashCache::new(); + let cache = Cache::new(&device); + let viewport = Viewport::new(&device, &cache); + let mut atlas = TextAtlas::new(&device, &queue, &cache, swapchain_format); + let text_renderer = + TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None); + let mut text_buffer = Buffer::new(&mut font_system, Metrics::new(30.0, 42.0)); + + let physical_width = (physical_size.width as f64 * scale_factor) as f32; + let physical_height = (physical_size.height as f64 * scale_factor) as f32; + + text_buffer.set_size( + &mut font_system, + Some(physical_width), + Some(physical_height), + ); + text_buffer.set_text( + &mut font_system, + "SVG icons! --->\n\nThe icons below should be partially clipped.", + Attrs::new().family(Family::SansSerif), + Shaping::Advanced, + ); + text_buffer.shape_until_scroll(&mut font_system, false); + + // Set up custom svg renderer + let svg_0 = resvg::usvg::Tree::from_data(LION_SVG, &Default::default()).unwrap(); + let svg_1 = resvg::usvg::Tree::from_data(EAGLE_SVG, &Default::default()).unwrap(); + + let rasterize_svg = move |input: CustomGlyphInput| -> Option { + // Select the svg data based on the custom glyph ID. + let (svg, content_type) = match input.id { + 0 => (&svg_0, ContentType::Mask), + 1 => (&svg_1, ContentType::Color), + _ => return None, + }; + + // Calculate the scale based on the "glyph size". + let glyph_size = input.size * input.scale; + let svg_size = svg.size(); + let max_side_len = svg_size.width().max(svg_size.height()); + let scale = glyph_size / max_side_len; + + // Create a buffer to write pixels to. + let width = (svg_size.width() * scale).ceil() as u32; + let height = (svg_size.height() * scale).ceil() as u32; + let Some(mut pixmap) = resvg::tiny_skia::Pixmap::new(width, height) else { + return None; + }; - // Calculate the scale based on the "glyph size". - let glyph_size = input.size * input.scale; - let svg_size = svg.size(); - let max_side_len = svg_size.width().max(svg_size.height()); - let scale = glyph_size / max_side_len; - - // Create a buffer to write pixels to. - let width = (svg_size.width() * scale).ceil() as u32; - let height = (svg_size.height() * scale).ceil() as u32; - let Some(mut pixmap) = resvg::tiny_skia::Pixmap::new(width, height) else { - return None; + let mut transform = resvg::usvg::Transform::from_scale(scale, scale); + + // 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(svg, transform, &mut pixmap.as_mut()); + + let data: Vec = if let ContentType::Mask = content_type { + // Only use the alpha channel for symbolic icons. + pixmap.data().iter().skip(3).step_by(4).copied().collect() + } else { + pixmap.data().to_vec() + }; + + Some(CustomGlyphOutput { + data, + width, + height, + content_type, + }) }; - let mut transform = resvg::usvg::Transform::from_scale(scale, scale); + Self { + device, + queue, + surface, + surface_config, + font_system, + swash_cache, + viewport, + atlas, + text_renderer, + text_buffer, + rasterize_svg: Box::new(rasterize_svg), + window, + } + } +} + +struct Application { + window_state: Option, +} - // 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); +impl winit::application::ApplicationHandler for Application { + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + if self.window_state.is_some() { + return; } - resvg::render(svg, transform, &mut pixmap.as_mut()); + // Set up window + let (width, height) = (800, 600); + let window_attributes = Window::default_attributes() + .with_inner_size(LogicalSize::new(width as f64, height as f64)) + .with_title("glyphon hello world"); + let window = Arc::new(event_loop.create_window(window_attributes).unwrap()); + + self.window_state = Some(pollster::block_on(WindowState::new(window))); + } - let data: Vec = if let ContentType::Mask = content_type { - // Only use the alpha channel for symbolic icons. - pixmap.data().iter().skip(3).step_by(4).copied().collect() - } else { - pixmap.data().to_vec() + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + _window_id: winit::window::WindowId, + event: WindowEvent, + ) { + let Some(state) = &mut self.window_state else { + return; }; - Some(CustomGlyphOutput { - data, - width, - height, - content_type, - }) - }; + let WindowState { + window, + device, + queue, + surface, + surface_config, + font_system, + swash_cache, + viewport, + atlas, + text_renderer, + text_buffer, + rasterize_svg, + .. + } = state; - event_loop - .run(move |event, target| { - if let Event::WindowEvent { - window_id: _, - event, - } = event - { - match event { - WindowEvent::Resized(size) => { - config.width = size.width; - config.height = size.height; - surface.configure(&device, &config); - window.request_redraw(); - } - WindowEvent::RedrawRequested => { - viewport.update( - &queue, - Resolution { - width: config.width, - height: config.height, + match event { + WindowEvent::Resized(size) => { + surface_config.width = size.width; + surface_config.height = size.height; + surface.configure(&device, &surface_config); + window.request_redraw(); + } + WindowEvent::RedrawRequested => { + viewport.update( + &queue, + Resolution { + width: surface_config.width, + height: surface_config.height, + }, + ); + + text_renderer + .prepare( + device, + queue, + font_system, + atlas, + viewport, + [TextArea { + buffer: &text_buffer, + left: 10.0, + top: 10.0, + scale: 1.0, + bounds: TextBounds { + left: 0, + top: 0, + right: 650, + bottom: 180, + }, + default_color: Color::rgb(255, 255, 255), + custom_glyphs: &[ + CustomGlyphDesc { + id: 0, + left: 300.0, + top: 5.0, + size: 64.0, + color: Some(Color::rgb(200, 200, 255)), + metadata: 0, + }, + CustomGlyphDesc { + id: 1, + left: 400.0, + top: 5.0, + size: 64.0, + color: None, + metadata: 0, + }, + CustomGlyphDesc { + id: 0, + left: 300.0, + top: 130.0, + size: 64.0, + color: Some(Color::rgb(200, 255, 200)), + metadata: 0, + }, + CustomGlyphDesc { + id: 1, + left: 400.0, + top: 130.0, + size: 64.0, + color: None, + metadata: 0, + }, + ], + }], + swash_cache, + rasterize_svg, + ) + .unwrap(); + + let frame = surface.get_current_texture().unwrap(); + let view = frame.texture.create_view(&TextureViewDescriptor::default()); + let mut encoder = + device.create_command_encoder(&CommandEncoderDescriptor { label: None }); + { + let mut pass = encoder.begin_render_pass(&RenderPassDescriptor { + label: None, + color_attachments: &[Some(RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(wgpu::Color { + r: 0.02, + g: 0.02, + b: 0.02, + a: 1.0, + }), + store: wgpu::StoreOp::Store, }, - ); - - text_renderer - .prepare( - &device, - &queue, - &mut font_system, - &mut atlas, - &viewport, - [TextArea { - buffer: &text_buffer, - left: 10.0, - top: 10.0, - scale: 1.0, - bounds: TextBounds { - left: 0, - top: 0, - right: 650, - bottom: 180, - }, - default_color: Color::rgb(255, 255, 255), - custom_glyphs: &[ - CustomGlyphDesc { - id: 0, - left: 300.0, - top: 5.0, - size: 64.0, - color: Some(Color::rgb(200, 200, 255)), - metadata: 0, - }, - CustomGlyphDesc { - id: 1, - left: 400.0, - top: 5.0, - size: 64.0, - color: None, - metadata: 0, - }, - CustomGlyphDesc { - id: 0, - left: 300.0, - top: 130.0, - size: 64.0, - color: Some(Color::rgb(200, 255, 200)), - metadata: 0, - }, - CustomGlyphDesc { - id: 1, - left: 400.0, - top: 130.0, - size: 64.0, - color: None, - metadata: 0, - }, - ], - }], - &mut swash_cache, - |input| rasterize_svg(input), - ) - .unwrap(); - - let frame = surface.get_current_texture().unwrap(); - let view = frame.texture.create_view(&TextureViewDescriptor::default()); - let mut encoder = device - .create_command_encoder(&CommandEncoderDescriptor { label: None }); - { - let mut pass = encoder.begin_render_pass(&RenderPassDescriptor { - label: None, - color_attachments: &[Some(RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: Operations { - load: LoadOp::Clear(wgpu::Color { - r: 0.02, - g: 0.02, - b: 0.02, - a: 1.0, - }), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - text_renderer.render(&atlas, &viewport, &mut pass).unwrap(); - } - - queue.submit(Some(encoder.finish())); - frame.present(); - - atlas.trim(); - } - WindowEvent::CloseRequested => target.exit(), - _ => {} + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + text_renderer.render(&atlas, &viewport, &mut pass).unwrap(); } + + queue.submit(Some(encoder.finish())); + frame.present(); + + atlas.trim(); } - }) - .unwrap(); + WindowEvent::CloseRequested => event_loop.exit(), + _ => {} + } + } } From 4f19f0f4deed1274e08c13e1f03901bd88fa7eae Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Mon, 29 Jul 2024 12:20:48 -0500 Subject: [PATCH 14/22] fix the mess merge conflicts made --- src/text_render.rs | 184 +++++++++++++++++++++++++++++++-------------- 1 file changed, 129 insertions(+), 55 deletions(-) diff --git a/src/text_render.rs b/src/text_render.rs index 025bab6..4800b6b 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -77,7 +77,10 @@ impl TextRenderer { let mut clip_and_add_glyph = |details: &GlyphDetails, mut x: i32, mut y: i32, - bounds: TextBounds, + bounds_min_x: i32, + bounds_min_y: i32, + bounds_max_x: i32, + bounds_max_y: i32, color: Color, metadata: usize, color_mode: ColorMode| { @@ -89,11 +92,6 @@ impl TextRenderer { let mut width = details.width as i32; let mut height = details.height as i32; - let bounds_min_x = bounds.left.max(0); - let bounds_min_y = bounds.top.max(0); - let bounds_max_x = bounds.right.min(resolution.width as i32); - let bounds_max_y = bounds.bottom.min(resolution.height as i32); - // Starts beyond right edge or ends beyond left edge let max_x = x + width; if x > bounds_max_x || max_x < bounds_min_x { @@ -158,6 +156,127 @@ impl TextRenderer { let bounds_max_x = text_area.bounds.right.min(resolution.width as i32); let bounds_max_y = text_area.bounds.bottom.min(resolution.height as i32); + #[cfg(feature = "custom-glyphs")] + for glyph in text_area.custom_glyphs.iter() { + let (cache_key, x, y) = cosmic_text::CacheKey::new( + custom_glyph_font_id, + glyph.id, + glyph.size, + (text_area.left + glyph.left, text_area.top + glyph.top), + custom_glyph_flags, + ); + + if atlas.mask_atlas.glyph_cache.contains(&cache_key) { + atlas.mask_atlas.promote(cache_key); + } else if atlas.color_atlas.glyph_cache.contains(&cache_key) { + atlas.color_atlas.promote(cache_key); + } else { + let input = CustomGlyphInput { + id: glyph.id, + size: glyph.size, + scale: text_area.scale, + x_bin: cache_key.x_bin, + y_bin: cache_key.y_bin, + }; + + let (gpu_cache, atlas_id, inner, width, height) = if let Some(output) = + rasterize_custom_glyph(input) + { + let mut inner = atlas.inner_for_content_mut(output.content_type); + + // Find a position in the packer + let allocation = loop { + match inner.try_allocate(output.width as usize, output.height as usize) + { + Some(a) => break a, + None => { + if !atlas.grow( + device, + queue, + font_system, + cache, + output.content_type, + ) { + return Err(PrepareError::AtlasFull); + } + + inner = atlas.inner_for_content_mut(output.content_type); + } + } + }; + let atlas_min = allocation.rectangle.min; + + queue.write_texture( + ImageCopyTexture { + texture: &inner.texture, + mip_level: 0, + origin: Origin3d { + x: atlas_min.x as u32, + y: atlas_min.y as u32, + z: 0, + }, + aspect: TextureAspect::All, + }, + &output.data, + ImageDataLayout { + offset: 0, + bytes_per_row: Some(output.width * inner.num_channels() as u32), + rows_per_image: None, + }, + Extent3d { + width: output.width, + height: output.height, + depth_or_array_layers: 1, + }, + ); + + ( + GpuCacheStatus::InAtlas { + x: atlas_min.x as u16, + y: atlas_min.y as u16, + content_type: output.content_type, + }, + Some(allocation.id), + inner, + output.width, + output.height, + ) + } else { + let inner = &mut atlas.color_atlas; + (GpuCacheStatus::SkipRasterization, None, inner, 0, 0) + }; + + inner.put( + cache_key, + GlyphDetails { + width: width as u16, + height: height as u16, + gpu_cache, + atlas_id, + top: 0, + left: 0, + }, + ); + } + + let details = atlas.glyph(&cache_key).unwrap(); + + let color = glyph.color.unwrap_or(text_area.default_color); + + clip_and_add_glyph( + details, + x, + y, + bounds_min_x, + bounds_min_y, + bounds_max_x, + bounds_max_y, + color, + glyph.metadata, + atlas.color_mode, + ); + } + let is_run_visible = |run: &cosmic_text::LayoutRun| { let start_y = (text_area.top + run.line_top) as i32; let end_y = (text_area.top + run.line_top + run.line_height) as i32; @@ -290,54 +409,6 @@ impl TextRenderer { let y = (run.line_y * text_area.scale).round() as i32 + physical_glyph.y - details.top as i32; - let (mut atlas_x, mut atlas_y, content_type) = match details.gpu_cache { - GpuCacheStatus::InAtlas { x, y, content_type } => (x, y, content_type), - GpuCacheStatus::SkipRasterization => continue, - }; - - let mut width = details.width as i32; - let mut height = details.height as i32; - - // Starts beyond right edge or ends beyond left edge - let max_x = x + width; - if x > bounds_max_x || max_x < bounds_min_x { - continue; - } - - // Starts beyond bottom edge or ends beyond top edge - let max_y = y + height; - if y > bounds_max_y || max_y < bounds_min_y { - continue; - } - - // Clip left ege - if x < bounds_min_x { - let right_shift = bounds_min_x - x; - - x = bounds_min_x; - width = max_x - bounds_min_x; - atlas_x += right_shift as u16; - } - - // Clip right edge - if x + width > bounds_max_x { - width = bounds_max_x - x; - } - - // Clip top edge - if y < bounds_min_y { - let bottom_shift = bounds_min_y - y; - - y = bounds_min_y; - height = max_y - bounds_min_y; - atlas_y += bottom_shift as u16; - } - - // Clip bottom edge - if y + height > bounds_max_y { - height = bounds_max_y - y; - } - let color = match glyph.color_opt { Some(some) => some, None => text_area.default_color, @@ -347,7 +418,10 @@ impl TextRenderer { atlas.glyph(&physical_glyph.cache_key).unwrap(), x, y, - text_area.bounds, + bounds_min_x, + bounds_min_y, + bounds_max_x, + bounds_max_y, color, glyph.metadata, atlas.color_mode, From 2f77acac8be15c02fb471b9b8463e70bb8c7bedd Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:12:53 -0500 Subject: [PATCH 15/22] add final newline --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 069bffb..4d5f29c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ repository = "https://github.com/grovesNL/glyphon" license = "MIT OR Apache-2.0 OR Zlib" [features] +default = ["custom-glyphs"] custom-glyphs = [] [dependencies] @@ -26,4 +27,4 @@ pollster = "0.3.0" [[example]] name = "custom-glyphs" path = "examples/custom-glyphs.rs" -required-features = ["custom-glyphs"] \ No newline at end of file +required-features = ["custom-glyphs"] From fc7ef1450c0907eda36c06a173312325d668079b Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:13:12 -0500 Subject: [PATCH 16/22] make custom-glyphs a default feature --- examples/hello-world.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/hello-world.rs b/examples/hello-world.rs index 06ce896..59cf022 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -186,8 +186,10 @@ impl winit::application::ApplicationHandler for Application { bottom: 160, }, default_color: Color::rgb(255, 255, 255), + custom_glyphs: &[], }], swash_cache, + |_| None, ) .unwrap(); From 7f7c664f0dc0f815a13d82f277f89609130ecfab Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:18:21 -0500 Subject: [PATCH 17/22] remove custom-glyphs feature --- Cargo.toml | 9 --------- src/lib.rs | 7 +------ src/text_render.rs | 20 +++----------------- 3 files changed, 4 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4d5f29c..5bbd692 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,10 +7,6 @@ homepage = "https://github.com/grovesNL/glyphon.git" repository = "https://github.com/grovesNL/glyphon" license = "MIT OR Apache-2.0 OR Zlib" -[features] -default = ["custom-glyphs"] -custom-glyphs = [] - [dependencies] wgpu = { version = "22", default-features = false, features = ["wgsl"] } etagere = "0.2.10" @@ -23,8 +19,3 @@ winit = "0.30.3" wgpu = "22" resvg = { version = "0.42", default-features = false } pollster = "0.3.0" - -[[example]] -name = "custom-glyphs" -path = "examples/custom-glyphs.rs" -required-features = ["custom-glyphs"] diff --git a/src/lib.rs b/src/lib.rs index 0adc9bd..489baa5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,10 +14,8 @@ pub use cache::Cache; pub use error::{PrepareError, RenderError}; pub use text_atlas::{ColorMode, TextAtlas}; pub use text_render::{ContentType, TextRenderer}; -pub use viewport::Viewport; - -#[cfg(feature = "custom-glyphs")] pub use text_render::{CustomGlyphInput, CustomGlyphOutput}; +pub use viewport::Viewport; // Re-export all top-level types from `cosmic-text` for convenience. #[doc(no_inline)] @@ -119,12 +117,10 @@ pub struct TextArea<'a> { // The default color of the text area. pub default_color: Color, - #[cfg(feature = "custom-glyphs")] /// Additional custom glyphs to render pub custom_glyphs: &'a [CustomGlyphDesc], } -#[cfg(feature = "custom-glyphs")] /// A custom glyph to render #[derive(Default, Debug, Clone, Copy, PartialEq)] pub struct CustomGlyphDesc { @@ -145,5 +141,4 @@ pub struct CustomGlyphDesc { pub metadata: usize, } -#[cfg(feature = "custom-glyphs")] pub type CustomGlyphID = u16; diff --git a/src/text_render.rs b/src/text_render.rs index 4800b6b..3f6903f 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -1,6 +1,6 @@ use crate::{ ColorMode, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError, - SwashCache, SwashContent, TextArea, TextAtlas, TextBounds, Viewport, + SwashCache, SwashContent, TextArea, TextAtlas, Viewport, }; use cosmic_text::Color; use std::{slice, sync::Arc}; @@ -55,19 +55,13 @@ impl TextRenderer { text_areas: impl IntoIterator>, cache: &mut SwashCache, mut metadata_to_depth: impl FnMut(usize) -> f32, - #[cfg(feature = "custom-glyphs")] mut rasterize_custom_glyph: impl FnMut( - CustomGlyphInput, - ) -> Option< - CustomGlyphOutput, - >, + mut rasterize_custom_glyph: impl FnMut(CustomGlyphInput) -> Option, ) -> Result<(), PrepareError> { self.glyph_vertices.clear(); let resolution = viewport.resolution(); - #[cfg(feature = "custom-glyphs")] let custom_glyph_font_id = cosmic_text::fontdb::ID::dummy(); - #[cfg(feature = "custom-glyphs")] // This is a bit of a hacky way to reserve a slot for icons in the text // atlas, but this is a simple way to ensure that there will be no // conflicts in the atlas without the need to create our own custom @@ -156,7 +150,6 @@ impl TextRenderer { let bounds_max_x = text_area.bounds.right.min(resolution.width as i32); let bounds_max_y = text_area.bounds.bottom.min(resolution.height as i32); - #[cfg(feature = "custom-glyphs")] for glyph in text_area.custom_glyphs.iter() { let (cache_key, x, y) = cosmic_text::CacheKey::new( custom_glyph_font_id, @@ -472,11 +465,7 @@ impl TextRenderer { viewport: &Viewport, text_areas: impl IntoIterator>, cache: &mut SwashCache, - #[cfg(feature = "custom-glyphs")] rasterize_custom_glyph: impl FnMut( - CustomGlyphInput, - ) -> Option< - CustomGlyphOutput, - >, + rasterize_custom_glyph: impl FnMut(CustomGlyphInput) -> Option, ) -> Result<(), PrepareError> { self.prepare_with_depth( device, @@ -487,7 +476,6 @@ impl TextRenderer { text_areas, cache, zero_depth, - #[cfg(feature = "custom-glyphs")] rasterize_custom_glyph, ) } @@ -554,7 +542,6 @@ fn zero_depth(_: usize) -> f32 { 0f32 } -#[cfg(feature = "custom-glyphs")] #[derive(Debug, Clone, Copy, PartialEq)] /// The input data to render a custom glyph pub struct CustomGlyphInput { @@ -570,7 +557,6 @@ pub struct CustomGlyphInput { pub y_bin: cosmic_text::SubpixelBin, } -#[cfg(feature = "custom-glyphs")] #[derive(Debug, Clone)] /// The output of a rendered custom glyph pub struct CustomGlyphOutput { From e99eda25b7147253666ff1f0c71a64a44525c760 Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:21:28 -0500 Subject: [PATCH 18/22] remove unnecessary pub(crate) --- src/text_render.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/text_render.rs b/src/text_render.rs index 3f6903f..50a86f3 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -510,17 +510,17 @@ pub enum ContentType { #[repr(u16)] #[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub(crate) enum TextColorConversion { +enum TextColorConversion { None = 0, ConvertToLinear = 1, } -pub(crate) fn next_copy_buffer_size(size: u64) -> u64 { +fn next_copy_buffer_size(size: u64) -> u64 { let align_mask = COPY_BUFFER_ALIGNMENT - 1; ((size.next_power_of_two() + align_mask) & !align_mask).max(COPY_BUFFER_ALIGNMENT) } -pub(crate) fn create_oversized_buffer( +fn create_oversized_buffer( device: &Device, label: Option<&str>, contents: &[u8], From 28470b6fdbc333ede416a3212df4c5654bf4e446 Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:23:13 -0500 Subject: [PATCH 19/22] rename CustomGlyphDesc to CustomGlyph --- examples/custom-glyphs.rs | 10 +++++----- src/lib.rs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/custom-glyphs.rs b/examples/custom-glyphs.rs index 83b2b71..7428cc9 100644 --- a/examples/custom-glyphs.rs +++ b/examples/custom-glyphs.rs @@ -1,5 +1,5 @@ use glyphon::{ - Attrs, Buffer, Cache, Color, ContentType, CustomGlyphDesc, CustomGlyphInput, CustomGlyphOutput, + Attrs, Buffer, Cache, Color, ContentType, CustomGlyph, CustomGlyphInput, CustomGlyphOutput, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, }; @@ -252,7 +252,7 @@ impl winit::application::ApplicationHandler for Application { }, default_color: Color::rgb(255, 255, 255), custom_glyphs: &[ - CustomGlyphDesc { + CustomGlyph { id: 0, left: 300.0, top: 5.0, @@ -260,7 +260,7 @@ impl winit::application::ApplicationHandler for Application { color: Some(Color::rgb(200, 200, 255)), metadata: 0, }, - CustomGlyphDesc { + CustomGlyph { id: 1, left: 400.0, top: 5.0, @@ -268,7 +268,7 @@ impl winit::application::ApplicationHandler for Application { color: None, metadata: 0, }, - CustomGlyphDesc { + CustomGlyph { id: 0, left: 300.0, top: 130.0, @@ -276,7 +276,7 @@ impl winit::application::ApplicationHandler for Application { color: Some(Color::rgb(200, 255, 200)), metadata: 0, }, - CustomGlyphDesc { + CustomGlyph { id: 1, left: 400.0, top: 130.0, diff --git a/src/lib.rs b/src/lib.rs index 489baa5..941f12f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -118,12 +118,12 @@ pub struct TextArea<'a> { pub default_color: Color, /// Additional custom glyphs to render - pub custom_glyphs: &'a [CustomGlyphDesc], + pub custom_glyphs: &'a [CustomGlyph], } /// A custom glyph to render #[derive(Default, Debug, Clone, Copy, PartialEq)] -pub struct CustomGlyphDesc { +pub struct CustomGlyph { /// The unique identifier for this glyph pub id: CustomGlyphID, /// The position of the left edge of the glyph From 23a1bff4a83132abf9edf73f10aca34f96473809 Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:24:04 -0500 Subject: [PATCH 20/22] rename CustomGlyphID to CustomGlyphId --- src/lib.rs | 4 ++-- src/text_render.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 941f12f..0cb3a37 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -125,7 +125,7 @@ pub struct TextArea<'a> { #[derive(Default, Debug, Clone, Copy, PartialEq)] pub struct CustomGlyph { /// The unique identifier for this glyph - pub id: CustomGlyphID, + pub id: CustomGlyphId, /// The position of the left edge of the glyph pub left: f32, /// The position of the top edge of the glyph @@ -141,4 +141,4 @@ pub struct CustomGlyph { pub metadata: usize, } -pub type CustomGlyphID = u16; +pub type CustomGlyphId = u16; diff --git a/src/text_render.rs b/src/text_render.rs index 50a86f3..9751668 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -546,7 +546,7 @@ fn zero_depth(_: usize) -> f32 { /// The input data to render a custom glyph pub struct CustomGlyphInput { /// The unique identifier of the glyph. - pub id: crate::CustomGlyphID, + pub id: crate::CustomGlyphId, /// The size of the glyph in points (not scaled by the text area's scaling factor) pub size: f32, /// The scaling factor applied to the text area. From 9f09f551102325c24070483a72c90f8f9948d35a Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Sat, 24 Aug 2024 17:09:55 -0500 Subject: [PATCH 21/22] improve custom glyph API and refactor text renderer --- examples/custom-glyphs.rs | 39 +- examples/hello-world.rs | 1 - src/custom_glyph.rs | 107 ++++++ src/lib.rs | 29 +- src/text_atlas.rs | 78 +++- src/text_render.rs | 758 ++++++++++++++++++++------------------ 6 files changed, 589 insertions(+), 423 deletions(-) create mode 100644 src/custom_glyph.rs diff --git a/examples/custom-glyphs.rs b/examples/custom-glyphs.rs index 7428cc9..3320e3c 100644 --- a/examples/custom-glyphs.rs +++ b/examples/custom-glyphs.rs @@ -115,19 +115,17 @@ impl WindowState { }; // Calculate the scale based on the "glyph size". - let glyph_size = input.size * input.scale; let svg_size = svg.size(); - let max_side_len = svg_size.width().max(svg_size.height()); - let scale = glyph_size / max_side_len; + let scale_x = input.width as f32 / svg_size.width(); + let scale_y = input.height as f32 / svg_size.height(); - // Create a buffer to write pixels to. - let width = (svg_size.width() * scale).ceil() as u32; - let height = (svg_size.height() * scale).ceil() as u32; - let Some(mut pixmap) = resvg::tiny_skia::Pixmap::new(width, height) else { + let Some(mut pixmap) = + resvg::tiny_skia::Pixmap::new(input.width as u32, input.height as u32) + else { return None; }; - let mut transform = resvg::usvg::Transform::from_scale(scale, scale); + 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(); @@ -145,12 +143,7 @@ impl WindowState { pixmap.data().to_vec() }; - Some(CustomGlyphOutput { - data, - width, - height, - content_type, - }) + Some(CustomGlyphOutput { data, content_type }) }; Self { @@ -233,7 +226,7 @@ impl winit::application::ApplicationHandler for Application { ); text_renderer - .prepare( + .prepare_with_custom( device, queue, font_system, @@ -256,32 +249,40 @@ impl winit::application::ApplicationHandler for Application { id: 0, left: 300.0, top: 5.0, - size: 64.0, + width: 64.0, + height: 64.0, color: Some(Color::rgb(200, 200, 255)), + snap_to_physical_pixel: true, metadata: 0, }, CustomGlyph { id: 1, left: 400.0, top: 5.0, - size: 64.0, + width: 64.0, + height: 64.0, color: None, + snap_to_physical_pixel: true, metadata: 0, }, CustomGlyph { id: 0, left: 300.0, top: 130.0, - size: 64.0, + width: 64.0, + height: 64.0, color: Some(Color::rgb(200, 255, 200)), + snap_to_physical_pixel: true, metadata: 0, }, CustomGlyph { id: 1, left: 400.0, top: 130.0, - size: 64.0, + width: 64.0, + height: 64.0, color: None, + snap_to_physical_pixel: true, metadata: 0, }, ], diff --git a/examples/hello-world.rs b/examples/hello-world.rs index 59cf022..51c5c49 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -189,7 +189,6 @@ impl winit::application::ApplicationHandler for Application { custom_glyphs: &[], }], swash_cache, - |_| None, ) .unwrap(); diff --git a/src/custom_glyph.rs b/src/custom_glyph.rs new file mode 100644 index 0000000..3823a8e --- /dev/null +++ b/src/custom_glyph.rs @@ -0,0 +1,107 @@ +use crate::Color; +use cosmic_text::SubpixelBin; + +pub type CustomGlyphId = u16; + +/// A custom glyph to render +#[derive(Default, Debug, Clone, Copy, PartialEq)] +pub struct CustomGlyph { + /// The unique identifier for this glyph + pub id: CustomGlyphId, + /// 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 [`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 `CustomGlyphInput` will always + /// be `Zero` (useful for images and other large glyphs). + pub snap_to_physical_pixel: bool, + /// Additional metadata about the glyph + pub metadata: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +/// The input data to render a custom glyph +pub struct CustomGlyphInput { + /// The unique identifier of the glyph. + pub id: CustomGlyphId, + /// 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, +} + +#[derive(Debug, Clone)] +/// The output of a rendered custom glyph +pub struct CustomGlyphOutput { + pub data: Vec, + pub content_type: ContentType, +} + +impl CustomGlyphOutput { + pub(crate) fn validate(&self, input: &CustomGlyphInput, 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(), + input.width as usize * input.height as usize * self.content_type.bytes_per_pixel(), + "Invalid custom glyph rasterizer output. Expected data of length {}, got length {}. Input: {:?}", + input.width as usize * input.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: CustomGlyphId, + /// 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, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum ContentType { + Color, + Mask, +} + +impl ContentType { + pub fn bytes_per_pixel(&self) -> usize { + match self { + Self::Color => 4, + Self::Mask => 1, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 0cb3a37..001ffd8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,16 +5,19 @@ //! [etagere]: https://github.com/nical/etagere mod cache; +mod custom_glyph; mod error; mod text_atlas; mod text_render; mod viewport; pub use cache::Cache; +pub use custom_glyph::{ + ContentType, CustomGlyph, CustomGlyphId, CustomGlyphInput, CustomGlyphOutput, +}; pub use error::{PrepareError, RenderError}; pub use text_atlas::{ColorMode, TextAtlas}; -pub use text_render::{ContentType, TextRenderer}; -pub use text_render::{CustomGlyphInput, CustomGlyphOutput}; +pub use text_render::TextRenderer; pub use viewport::Viewport; // Re-export all top-level types from `cosmic-text` for convenience. @@ -120,25 +123,3 @@ pub struct TextArea<'a> { /// Additional custom glyphs to render pub custom_glyphs: &'a [CustomGlyph], } - -/// A custom glyph to render -#[derive(Default, Debug, Clone, Copy, PartialEq)] -pub struct CustomGlyph { - /// The unique identifier for this glyph - pub id: CustomGlyphId, - /// 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 size of the glyph - pub size: f32, - /// The color of this glyph (only relevant if the glyph is rendered with the - /// type [`ContentType::Mask`]) - /// - /// Set to `None` to use [`TextArea::default_color`]. - pub color: Option, - /// Additional metadata about the glyph - pub metadata: usize, -} - -pub type CustomGlyphId = u16; diff --git a/src/text_atlas.rs b/src/text_atlas.rs index 1cabbc4..cf6876b 100644 --- a/src/text_atlas.rs +++ b/src/text_atlas.rs @@ -1,5 +1,6 @@ use crate::{ - text_render::ContentType, Cache, CacheKey, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, + text_render::GlyphonCacheKey, Cache, ContentType, CustomGlyphInput, CustomGlyphOutput, + FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, }; use etagere::{size2, Allocation, BucketedAtlasAllocator}; use lru::LruCache; @@ -20,8 +21,8 @@ pub(crate) struct InnerAtlas { pub texture_view: TextureView, pub packer: BucketedAtlasAllocator, pub size: u32, - pub glyph_cache: LruCache, - pub glyphs_in_use: HashSet, + pub glyph_cache: LruCache, + pub glyphs_in_use: HashSet, pub max_texture_dimension_2d: u32, } @@ -106,12 +107,12 @@ impl InnerAtlas { self.kind.num_channels() } - pub(crate) fn promote(&mut self, glyph: CacheKey) { + pub(crate) fn promote(&mut self, glyph: GlyphonCacheKey) { self.glyph_cache.promote(&glyph); self.glyphs_in_use.insert(glyph); } - pub(crate) fn put(&mut self, glyph: CacheKey, details: GlyphDetails) { + pub(crate) fn put(&mut self, glyph: GlyphonCacheKey, details: GlyphDetails) { self.glyph_cache.put(glyph, details); self.glyphs_in_use.insert(glyph); } @@ -122,6 +123,8 @@ impl InnerAtlas { queue: &wgpu::Queue, font_system: &mut FontSystem, cache: &mut SwashCache, + scale_factor: f32, + mut rasterize_custom_glyph: impl FnMut(CustomGlyphInput) -> Option, ) -> bool { if self.size >= self.max_texture_dimension_2d { return false; @@ -157,10 +160,38 @@ impl InnerAtlas { GpuCacheStatus::SkipRasterization => continue, }; - let image = cache.get_image_uncached(font_system, cache_key).unwrap(); + let (image_data, width, height) = match cache_key { + GlyphonCacheKey::Text(cache_key) => { + let image = cache.get_image_uncached(font_system, cache_key).unwrap(); + let width = image.placement.width as usize; + let height = image.placement.height as usize; - let width = image.placement.width as usize; - let height = image.placement.height as usize; + (image.data, width, height) + } + GlyphonCacheKey::Custom(cache_key) => { + let input = CustomGlyphInput { + id: cache_key.glyph_id, + width: cache_key.width, + height: cache_key.height, + x_bin: cache_key.x_bin, + y_bin: cache_key.y_bin, + scale: scale_factor, + }; + + let Some(rasterized_glyph) = (rasterize_custom_glyph)(input) else { + panic!("Custom glyph rasterizer returned `None` when it previously returned `Some` for the same input {:?}", &input); + }; + + // Sanity checks on the rasterizer output + rasterized_glyph.validate(&input, Some(self.kind.as_content_type())); + + ( + rasterized_glyph.data, + cache_key.width as usize, + cache_key.height as usize, + ) + } + }; queue.write_texture( ImageCopyTexture { @@ -173,7 +204,7 @@ impl InnerAtlas { }, aspect: TextureAspect::All, }, - &image.data, + &image_data, ImageDataLayout { offset: 0, bytes_per_row: Some(width as u32 * self.kind.num_channels() as u32), @@ -224,6 +255,13 @@ impl Kind { } } } + + fn as_content_type(&self) -> ContentType { + match self { + Self::Mask => ContentType::Mask, + Self::Color { .. } => ContentType::Color, + } + } } /// The color mode of an [`Atlas`]. @@ -313,10 +351,26 @@ impl TextAtlas { font_system: &mut FontSystem, cache: &mut SwashCache, content_type: ContentType, + scale_factor: f32, + rasterize_custom_glyph: impl FnMut(CustomGlyphInput) -> Option, ) -> bool { let did_grow = match content_type { - ContentType::Mask => self.mask_atlas.grow(device, queue, font_system, cache), - ContentType::Color => self.color_atlas.grow(device, queue, font_system, cache), + ContentType::Mask => self.mask_atlas.grow( + device, + queue, + font_system, + cache, + scale_factor, + rasterize_custom_glyph, + ), + ContentType::Color => self.color_atlas.grow( + device, + queue, + font_system, + cache, + scale_factor, + rasterize_custom_glyph, + ), }; if did_grow { @@ -326,7 +380,7 @@ impl TextAtlas { did_grow } - pub(crate) fn glyph(&self, glyph: &CacheKey) -> Option<&GlyphDetails> { + pub(crate) fn glyph(&self, glyph: &GlyphonCacheKey) -> Option<&GlyphDetails> { self.mask_atlas .glyph_cache .peek(glyph) diff --git a/src/text_render.rs b/src/text_render.rs index 9751668..1d1a801 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -1,8 +1,9 @@ use crate::{ - ColorMode, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError, - SwashCache, SwashContent, TextArea, TextAtlas, Viewport, + custom_glyph::CustomGlyphCacheKey, ColorMode, ContentType, CustomGlyphInput, CustomGlyphOutput, + FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError, SwashCache, + SwashContent, TextArea, TextAtlas, Viewport, }; -use cosmic_text::Color; +use cosmic_text::{Color, SubpixelBin}; use std::{slice, sync::Arc}; use wgpu::{ Buffer, BufferDescriptor, BufferUsages, DepthStencilState, Device, Extent3d, ImageCopyTexture, @@ -44,8 +45,82 @@ impl TextRenderer { } } + /// Prepares all of the provided text areas for rendering. + pub fn prepare<'a>( + &mut self, + device: &Device, + queue: &Queue, + font_system: &mut FontSystem, + atlas: &mut TextAtlas, + viewport: &Viewport, + text_areas: impl IntoIterator>, + cache: &mut SwashCache, + ) -> Result<(), PrepareError> { + self.prepare_with_depth_and_custom( + device, + queue, + font_system, + atlas, + viewport, + text_areas, + cache, + zero_depth, + |_| None, + ) + } + /// Prepares all of the provided text areas for rendering. pub fn prepare_with_depth<'a>( + &mut self, + device: &Device, + queue: &Queue, + font_system: &mut FontSystem, + atlas: &mut TextAtlas, + viewport: &Viewport, + text_areas: impl IntoIterator>, + cache: &mut SwashCache, + metadata_to_depth: impl FnMut(usize) -> f32, + ) -> Result<(), PrepareError> { + self.prepare_with_depth_and_custom( + device, + queue, + font_system, + atlas, + viewport, + text_areas, + cache, + metadata_to_depth, + |_| None, + ) + } + + /// Prepares all of the provided text areas for rendering. + pub fn prepare_with_custom<'a>( + &mut self, + device: &Device, + queue: &Queue, + font_system: &mut FontSystem, + atlas: &mut TextAtlas, + viewport: &Viewport, + text_areas: impl IntoIterator>, + cache: &mut SwashCache, + rasterize_custom_glyph: impl FnMut(CustomGlyphInput) -> Option, + ) -> Result<(), PrepareError> { + self.prepare_with_depth_and_custom( + device, + queue, + font_system, + atlas, + viewport, + text_areas, + cache, + zero_depth, + rasterize_custom_glyph, + ) + } + + /// Prepares all of the provided text areas for rendering. + pub fn prepare_with_depth_and_custom<'a>( &mut self, device: &Device, queue: &Queue, @@ -61,89 +136,6 @@ impl TextRenderer { let resolution = viewport.resolution(); - let custom_glyph_font_id = cosmic_text::fontdb::ID::dummy(); - // This is a bit of a hacky way to reserve a slot for icons in the text - // atlas, but this is a simple way to ensure that there will be no - // conflicts in the atlas without the need to create our own custom - // `CacheKey` struct with extra bytes. - let custom_glyph_flags = cosmic_text::CacheKeyFlags::from_bits_retain(u32::MAX); - - let mut clip_and_add_glyph = |details: &GlyphDetails, - mut x: i32, - mut y: i32, - bounds_min_x: i32, - bounds_min_y: i32, - bounds_max_x: i32, - bounds_max_y: i32, - color: Color, - metadata: usize, - color_mode: ColorMode| { - let (mut atlas_x, mut atlas_y, content_type) = match details.gpu_cache { - GpuCacheStatus::InAtlas { x, y, content_type } => (x, y, content_type), - GpuCacheStatus::SkipRasterization => return, - }; - - let mut width = details.width as i32; - let mut height = details.height as i32; - - // Starts beyond right edge or ends beyond left edge - let max_x = x + width; - if x > bounds_max_x || max_x < bounds_min_x { - return; - } - - // Starts beyond bottom edge or ends beyond top edge - let max_y = y + height; - if y > bounds_max_y || max_y < bounds_min_y { - return; - } - - // Clip left ege - if x < bounds_min_x { - let right_shift = bounds_min_x - x; - - x = bounds_min_x; - width = max_x - bounds_min_x; - atlas_x += right_shift as u16; - } - - // Clip right edge - if x + width > bounds_max_x { - width = bounds_max_x - x; - } - - // Clip top edge - if y < bounds_min_y { - let bottom_shift = bounds_min_y - y; - - y = bounds_min_y; - height = max_y - bounds_min_y; - atlas_y += bottom_shift as u16; - } - - // Clip bottom edge - if y + height > bounds_max_y { - height = bounds_max_y - y; - } - - let depth = metadata_to_depth(metadata); - - self.glyph_vertices.push(GlyphToRender { - pos: [x, y], - dim: [width as u16, height as u16], - uv: [atlas_x, atlas_y], - color: color.0, - content_type_with_srgb: [ - content_type as u16, - match color_mode { - ColorMode::Accurate => TextColorConversion::ConvertToLinear, - ColorMode::Web => TextColorConversion::None, - } as u16, - ], - depth, - }); - }; - for text_area in text_areas { let bounds_min_x = text_area.bounds.left.max(0); let bounds_min_y = text_area.bounds.top.max(0); @@ -151,123 +143,85 @@ impl TextRenderer { let bounds_max_y = text_area.bounds.bottom.min(resolution.height as i32); for glyph in text_area.custom_glyphs.iter() { - let (cache_key, x, y) = cosmic_text::CacheKey::new( - custom_glyph_font_id, - glyph.id, - glyph.size, - (text_area.left + glyph.left, text_area.top + glyph.top), - custom_glyph_flags, - ); - - if atlas.mask_atlas.glyph_cache.contains(&cache_key) { - atlas.mask_atlas.promote(cache_key); - } else if atlas.color_atlas.glyph_cache.contains(&cache_key) { - atlas.color_atlas.promote(cache_key); + let x = text_area.left + (glyph.left * text_area.scale); + let y = text_area.top + (glyph.top * text_area.scale); + let width = (glyph.width * text_area.scale).round() as u16; + let height = (glyph.height * text_area.scale).round() as u16; + + let (x, y, x_bin, y_bin) = if glyph.snap_to_physical_pixel { + ( + x.round() as i32, + y.round() as i32, + SubpixelBin::Zero, + SubpixelBin::Zero, + ) } else { - let input = CustomGlyphInput { - id: glyph.id, - size: glyph.size, - scale: text_area.scale, - x_bin: cache_key.x_bin, - y_bin: cache_key.y_bin, - }; - - let (gpu_cache, atlas_id, inner, width, height) = if let Some(output) = - rasterize_custom_glyph(input) - { - let mut inner = atlas.inner_for_content_mut(output.content_type); - - // Find a position in the packer - let allocation = loop { - match inner.try_allocate(output.width as usize, output.height as usize) - { - Some(a) => break a, - None => { - if !atlas.grow( - device, - queue, - font_system, - cache, - output.content_type, - ) { - return Err(PrepareError::AtlasFull); - } - - inner = atlas.inner_for_content_mut(output.content_type); - } - } - }; - let atlas_min = allocation.rectangle.min; - - queue.write_texture( - ImageCopyTexture { - texture: &inner.texture, - mip_level: 0, - origin: Origin3d { - x: atlas_min.x as u32, - y: atlas_min.y as u32, - z: 0, - }, - aspect: TextureAspect::All, - }, - &output.data, - ImageDataLayout { - offset: 0, - bytes_per_row: Some(output.width * inner.num_channels() as u32), - rows_per_image: None, - }, - Extent3d { - width: output.width, - height: output.height, - depth_or_array_layers: 1, - }, - ); - - ( - GpuCacheStatus::InAtlas { - x: atlas_min.x as u16, - y: atlas_min.y as u16, - content_type: output.content_type, - }, - Some(allocation.id), - inner, - output.width, - output.height, - ) - } else { - let inner = &mut atlas.color_atlas; - (GpuCacheStatus::SkipRasterization, None, inner, 0, 0) - }; - - inner.put( - cache_key, - GlyphDetails { - width: width as u16, - height: height as u16, - gpu_cache, - atlas_id, - top: 0, - left: 0, - }, - ); - } - - let details = atlas.glyph(&cache_key).unwrap(); + let (x, x_bin) = SubpixelBin::new(x); + let (y, y_bin) = SubpixelBin::new(y); + (x, y, x_bin, y_bin) + }; + + let cache_key = GlyphonCacheKey::Custom(CustomGlyphCacheKey { + glyph_id: glyph.id, + width, + height, + x_bin, + y_bin, + }); let color = glyph.color.unwrap_or(text_area.default_color); - clip_and_add_glyph( - details, + if let Some(glyph_to_render) = prepare_glyph( x, y, + 0.0, + color, + glyph.metadata, + cache_key, + atlas, + device, + queue, + cache, + font_system, + text_area.scale, bounds_min_x, bounds_min_y, bounds_max_x, bounds_max_y, - color, - glyph.metadata, - atlas.color_mode, - ); + |_cache, _font_system, rasterize_custom_glyph| -> Option { + if width == 0 || height == 0 { + return None; + } + + let input = CustomGlyphInput { + id: glyph.id, + width, + height, + x_bin, + y_bin, + scale: text_area.scale, + }; + + let Some(output) = (rasterize_custom_glyph)(input) else { + return None; + }; + + output.validate(&input, None); + + Some(GetGlyphImageResult { + content_type: output.content_type, + top: 0, + left: 0, + width, + height, + data: output.data, + }) + }, + &mut metadata_to_depth, + &mut rasterize_custom_glyph, + )? { + self.glyph_vertices.push(glyph_to_render); + } } let is_run_visible = |run: &cosmic_text::LayoutRun| { @@ -288,137 +242,61 @@ impl TextRenderer { let physical_glyph = glyph.physical((text_area.left, text_area.top), text_area.scale); - if atlas - .mask_atlas - .glyph_cache - .contains(&physical_glyph.cache_key) - { - atlas.mask_atlas.promote(physical_glyph.cache_key); - } else if atlas - .color_atlas - .glyph_cache - .contains(&physical_glyph.cache_key) - { - atlas.color_atlas.promote(physical_glyph.cache_key); - } else { - let Some(image) = - cache.get_image_uncached(font_system, physical_glyph.cache_key) - else { - continue; - }; - - let content_type = match image.content { - SwashContent::Color => ContentType::Color, - SwashContent::Mask => ContentType::Mask, - SwashContent::SubpixelMask => { - // Not implemented yet, but don't panic if this happens. - ContentType::Mask - } - }; - - let width = image.placement.width as usize; - let height = image.placement.height as usize; - - let should_rasterize = width > 0 && height > 0; - - let (gpu_cache, atlas_id, inner) = if should_rasterize { - let mut inner = atlas.inner_for_content_mut(content_type); - - // Find a position in the packer - let allocation = loop { - match inner.try_allocate(width, height) { - Some(a) => break a, - None => { - if !atlas.grow( - device, - queue, - font_system, - cache, - content_type, - ) { - return Err(PrepareError::AtlasFull); - } - - inner = atlas.inner_for_content_mut(content_type); - } - } - }; - let atlas_min = allocation.rectangle.min; - - queue.write_texture( - ImageCopyTexture { - texture: &inner.texture, - mip_level: 0, - origin: Origin3d { - x: atlas_min.x as u32, - y: atlas_min.y as u32, - z: 0, - }, - aspect: TextureAspect::All, - }, - &image.data, - ImageDataLayout { - offset: 0, - bytes_per_row: Some(width as u32 * inner.num_channels() as u32), - rows_per_image: None, - }, - Extent3d { - width: width as u32, - height: height as u32, - depth_or_array_layers: 1, - }, - ); - - ( - GpuCacheStatus::InAtlas { - x: atlas_min.x as u16, - y: atlas_min.y as u16, - content_type, - }, - Some(allocation.id), - inner, - ) - } else { - let inner = &mut atlas.color_atlas; - (GpuCacheStatus::SkipRasterization, None, inner) - }; - - inner.put( - physical_glyph.cache_key, - GlyphDetails { - width: width as u16, - height: height as u16, - gpu_cache, - atlas_id, - top: image.placement.top as i16, - left: image.placement.left as i16, - }, - ); - } - - let details = atlas.glyph(&physical_glyph.cache_key).unwrap(); - - let x = physical_glyph.x + details.left as i32; - let y = (run.line_y * text_area.scale).round() as i32 + physical_glyph.y - - details.top as i32; - let color = match glyph.color_opt { Some(some) => some, None => text_area.default_color, }; - clip_and_add_glyph( - atlas.glyph(&physical_glyph.cache_key).unwrap(), - x, - y, + if let Some(glyph_to_render) = prepare_glyph( + physical_glyph.x, + physical_glyph.y, + run.line_y, + color, + glyph.metadata, + GlyphonCacheKey::Text(physical_glyph.cache_key), + atlas, + device, + queue, + cache, + font_system, + text_area.scale, bounds_min_x, bounds_min_y, bounds_max_x, bounds_max_y, - color, - glyph.metadata, - atlas.color_mode, - ); + |cache, + font_system, + _rasterize_custom_glyph| + -> Option { + let Some(image) = + cache.get_image_uncached(font_system, physical_glyph.cache_key) + else { + return None; + }; + + let content_type = match image.content { + SwashContent::Color => ContentType::Color, + SwashContent::Mask => ContentType::Mask, + SwashContent::SubpixelMask => { + // Not implemented yet, but don't panic if this happens. + ContentType::Mask + } + }; + + Some(GetGlyphImageResult { + content_type, + top: image.placement.top as i16, + left: image.placement.left as i16, + width: image.placement.width as u16, + height: image.placement.height as u16, + data: image.data, + }) + }, + &mut metadata_to_depth, + &mut rasterize_custom_glyph, + )? { + self.glyph_vertices.push(glyph_to_render); + } } } } @@ -455,31 +333,6 @@ impl TextRenderer { Ok(()) } - /// Prepares all of the provided text areas for rendering. - pub fn prepare<'a>( - &mut self, - device: &Device, - queue: &Queue, - font_system: &mut FontSystem, - atlas: &mut TextAtlas, - viewport: &Viewport, - text_areas: impl IntoIterator>, - cache: &mut SwashCache, - rasterize_custom_glyph: impl FnMut(CustomGlyphInput) -> Option, - ) -> Result<(), PrepareError> { - self.prepare_with_depth( - device, - queue, - font_system, - atlas, - viewport, - text_areas, - cache, - zero_depth, - rasterize_custom_glyph, - ) - } - /// Renders all layouts that were previously provided to `prepare`. pub fn render<'pass>( &'pass self, @@ -501,13 +354,6 @@ impl TextRenderer { } } -#[repr(u16)] -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub enum ContentType { - Color = 0, - Mask = 1, -} - #[repr(u16)] #[derive(Debug, Clone, Copy, Eq, PartialEq)] enum TextColorConversion { @@ -515,6 +361,12 @@ enum TextColorConversion { ConvertToLinear = 1, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum GlyphonCacheKey { + Text(cosmic_text::CacheKey), + Custom(CustomGlyphCacheKey), +} + fn next_copy_buffer_size(size: u64) -> u64 { let align_mask = COPY_BUFFER_ALIGNMENT - 1; ((size.next_power_of_two() + align_mask) & !align_mask).max(COPY_BUFFER_ALIGNMENT) @@ -542,26 +394,198 @@ fn zero_depth(_: usize) -> f32 { 0f32 } -#[derive(Debug, Clone, Copy, PartialEq)] -/// The input data to render a custom glyph -pub struct CustomGlyphInput { - /// The unique identifier of the glyph. - pub id: crate::CustomGlyphId, - /// The size of the glyph in points (not scaled by the text area's scaling factor) - pub size: f32, - /// The scaling factor applied to the text area. - pub scale: f32, - /// Binning of fractional X offset - pub x_bin: cosmic_text::SubpixelBin, - /// Binning of fractional Y offset - pub y_bin: cosmic_text::SubpixelBin, +struct GetGlyphImageResult { + content_type: ContentType, + top: i16, + left: i16, + width: u16, + height: u16, + data: Vec, } -#[derive(Debug, Clone)] -/// The output of a rendered custom glyph -pub struct CustomGlyphOutput { - pub data: Vec, - pub width: u32, - pub height: u32, - pub content_type: ContentType, +fn prepare_glyph( + x: i32, + y: i32, + line_y: f32, + color: Color, + metadata: usize, + cache_key: GlyphonCacheKey, + atlas: &mut TextAtlas, + device: &Device, + queue: &Queue, + cache: &mut SwashCache, + font_system: &mut FontSystem, + scale_factor: f32, + bounds_min_x: i32, + bounds_min_y: i32, + bounds_max_x: i32, + bounds_max_y: i32, + get_glyph_image: impl FnOnce( + &mut SwashCache, + &mut FontSystem, + &mut R, + ) -> Option, + mut metadata_to_depth: impl FnMut(usize) -> f32, + mut rasterize_custom_glyph: R, +) -> Result, PrepareError> +where + R: FnMut(CustomGlyphInput) -> Option, +{ + if atlas.mask_atlas.glyph_cache.contains(&cache_key) { + atlas.mask_atlas.promote(cache_key); + } else if atlas.color_atlas.glyph_cache.contains(&cache_key) { + atlas.color_atlas.promote(cache_key); + } else { + let Some(image) = (get_glyph_image)(cache, font_system, &mut rasterize_custom_glyph) else { + return Ok(None); + }; + + let should_rasterize = image.width > 0 && image.height > 0; + + let (gpu_cache, atlas_id, inner) = if should_rasterize { + let mut inner = atlas.inner_for_content_mut(image.content_type); + + // Find a position in the packer + let allocation = loop { + match inner.try_allocate(image.width as usize, image.height as usize) { + Some(a) => break a, + None => { + if !atlas.grow( + device, + queue, + font_system, + cache, + image.content_type, + scale_factor, + &mut rasterize_custom_glyph, + ) { + return Err(PrepareError::AtlasFull); + } + + inner = atlas.inner_for_content_mut(image.content_type); + } + } + }; + let atlas_min = allocation.rectangle.min; + + queue.write_texture( + ImageCopyTexture { + texture: &inner.texture, + mip_level: 0, + origin: Origin3d { + x: atlas_min.x as u32, + y: atlas_min.y as u32, + z: 0, + }, + aspect: TextureAspect::All, + }, + &image.data, + ImageDataLayout { + offset: 0, + bytes_per_row: Some(image.width as u32 * inner.num_channels() as u32), + rows_per_image: None, + }, + Extent3d { + width: image.width as u32, + height: image.height as u32, + depth_or_array_layers: 1, + }, + ); + + ( + GpuCacheStatus::InAtlas { + x: atlas_min.x as u16, + y: atlas_min.y as u16, + content_type: image.content_type, + }, + Some(allocation.id), + inner, + ) + } else { + let inner = &mut atlas.color_atlas; + (GpuCacheStatus::SkipRasterization, None, inner) + }; + + inner.put( + cache_key, + GlyphDetails { + width: image.width, + height: image.height, + gpu_cache, + atlas_id, + top: image.top, + left: image.left, + }, + ); + } + + let details = atlas.glyph(&cache_key).unwrap(); + + let mut x = x + details.left as i32; + let mut y = (line_y * scale_factor).round() as i32 + y - details.top as i32; + + let (mut atlas_x, mut atlas_y, content_type) = match details.gpu_cache { + GpuCacheStatus::InAtlas { x, y, content_type } => (x, y, content_type), + GpuCacheStatus::SkipRasterization => return Ok(None), + }; + + let mut width = details.width as i32; + let mut height = details.height as i32; + + // Starts beyond right edge or ends beyond left edge + let max_x = x + width; + if x > bounds_max_x || max_x < bounds_min_x { + return Ok(None); + } + + // Starts beyond bottom edge or ends beyond top edge + let max_y = y + height; + if y > bounds_max_y || max_y < bounds_min_y { + return Ok(None); + } + + // Clip left ege + if x < bounds_min_x { + let right_shift = bounds_min_x - x; + + x = bounds_min_x; + width = max_x - bounds_min_x; + atlas_x += right_shift as u16; + } + + // Clip right edge + if x + width > bounds_max_x { + width = bounds_max_x - x; + } + + // Clip top edge + if y < bounds_min_y { + let bottom_shift = bounds_min_y - y; + + y = bounds_min_y; + height = max_y - bounds_min_y; + atlas_y += bottom_shift as u16; + } + + // Clip bottom edge + if y + height > bounds_max_y { + height = bounds_max_y - y; + } + + let depth = metadata_to_depth(metadata); + + Ok(Some(GlyphToRender { + pos: [x, y], + dim: [width as u16, height as u16], + uv: [atlas_x, atlas_y], + color: color.0, + content_type_with_srgb: [ + content_type as u16, + match atlas.color_mode { + ColorMode::Accurate => TextColorConversion::ConvertToLinear, + ColorMode::Web => TextColorConversion::None, + } as u16, + ], + depth, + })) } From 18c4b1348d319c82d47ef012815eb888b033f75f Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Sat, 24 Aug 2024 17:19:08 -0500 Subject: [PATCH 22/22] rename CustomGlyphInput and CustomGlyphOutput, add some docs --- examples/custom-glyphs.rs | 8 ++++---- src/custom_glyph.rs | 22 ++++++++++++++-------- src/lib.rs | 2 +- src/text_atlas.rs | 8 ++++---- src/text_render.rs | 10 +++++----- 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/examples/custom-glyphs.rs b/examples/custom-glyphs.rs index 3320e3c..6b1ecc5 100644 --- a/examples/custom-glyphs.rs +++ b/examples/custom-glyphs.rs @@ -1,5 +1,5 @@ use glyphon::{ - Attrs, Buffer, Cache, Color, ContentType, CustomGlyph, CustomGlyphInput, CustomGlyphOutput, + Attrs, Buffer, Cache, Color, ContentType, CustomGlyph, RasterizationRequest, RasterizedCustomGlyph, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, }; @@ -36,7 +36,7 @@ struct WindowState { text_renderer: glyphon::TextRenderer, text_buffer: glyphon::Buffer, - rasterize_svg: Box Option>, + rasterize_svg: Box Option>, // Make sure that the winit window is last in the struct so that // it is dropped after the wgpu surface is dropped, otherwise the @@ -106,7 +106,7 @@ impl WindowState { let svg_0 = resvg::usvg::Tree::from_data(LION_SVG, &Default::default()).unwrap(); let svg_1 = resvg::usvg::Tree::from_data(EAGLE_SVG, &Default::default()).unwrap(); - let rasterize_svg = move |input: CustomGlyphInput| -> Option { + let rasterize_svg = move |input: RasterizationRequest| -> Option { // Select the svg data based on the custom glyph ID. let (svg, content_type) = match input.id { 0 => (&svg_0, ContentType::Mask), @@ -143,7 +143,7 @@ impl WindowState { pixmap.data().to_vec() }; - Some(CustomGlyphOutput { data, content_type }) + Some(RasterizedCustomGlyph { data, content_type }) }; Self { diff --git a/src/custom_glyph.rs b/src/custom_glyph.rs index 3823a8e..a0df98c 100644 --- a/src/custom_glyph.rs +++ b/src/custom_glyph.rs @@ -22,17 +22,17 @@ pub struct CustomGlyph { /// Set to `None` to use [`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 `CustomGlyphInput` will always + /// 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, /// Additional metadata about the glyph pub metadata: usize, } +/// A request to rasterize a custom glyph #[derive(Debug, Clone, Copy, PartialEq)] -/// The input data to render a custom glyph -pub struct CustomGlyphInput { - /// The unique identifier of the glyph. +pub struct RasterizationRequest { + /// The unique identifier of the glyph pub id: CustomGlyphId, /// The width of the glyph in physical pixels pub width: u16, @@ -53,15 +53,17 @@ pub struct CustomGlyphInput { pub scale: f32, } +/// A rasterized custom glyph #[derive(Debug, Clone)] -/// The output of a rendered custom glyph -pub struct CustomGlyphOutput { +pub struct RasterizedCustomGlyph { + /// The raw image data pub data: Vec, + /// The type of image data contained in `data` pub content_type: ContentType, } -impl CustomGlyphOutput { - pub(crate) fn validate(&self, input: &CustomGlyphInput, expected_type: Option) { +impl RasterizedCustomGlyph { + pub(crate) fn validate(&self, input: &RasterizationRequest, 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); } @@ -91,13 +93,17 @@ pub struct CustomGlyphCacheKey { 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 fn bytes_per_pixel(&self) -> usize { match self { Self::Color => 4, diff --git a/src/lib.rs b/src/lib.rs index 001ffd8..9be0cdc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,7 +13,7 @@ mod viewport; pub use cache::Cache; pub use custom_glyph::{ - ContentType, CustomGlyph, CustomGlyphId, CustomGlyphInput, CustomGlyphOutput, + ContentType, CustomGlyph, CustomGlyphId, RasterizationRequest, RasterizedCustomGlyph, }; pub use error::{PrepareError, RenderError}; pub use text_atlas::{ColorMode, TextAtlas}; diff --git a/src/text_atlas.rs b/src/text_atlas.rs index cf6876b..cf86464 100644 --- a/src/text_atlas.rs +++ b/src/text_atlas.rs @@ -1,5 +1,5 @@ use crate::{ - text_render::GlyphonCacheKey, Cache, ContentType, CustomGlyphInput, CustomGlyphOutput, + text_render::GlyphonCacheKey, Cache, ContentType, RasterizationRequest, RasterizedCustomGlyph, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, }; use etagere::{size2, Allocation, BucketedAtlasAllocator}; @@ -124,7 +124,7 @@ impl InnerAtlas { font_system: &mut FontSystem, cache: &mut SwashCache, scale_factor: f32, - mut rasterize_custom_glyph: impl FnMut(CustomGlyphInput) -> Option, + mut rasterize_custom_glyph: impl FnMut(RasterizationRequest) -> Option, ) -> bool { if self.size >= self.max_texture_dimension_2d { return false; @@ -169,7 +169,7 @@ impl InnerAtlas { (image.data, width, height) } GlyphonCacheKey::Custom(cache_key) => { - let input = CustomGlyphInput { + let input = RasterizationRequest { id: cache_key.glyph_id, width: cache_key.width, height: cache_key.height, @@ -352,7 +352,7 @@ impl TextAtlas { cache: &mut SwashCache, content_type: ContentType, scale_factor: f32, - rasterize_custom_glyph: impl FnMut(CustomGlyphInput) -> Option, + rasterize_custom_glyph: impl FnMut(RasterizationRequest) -> Option, ) -> bool { let did_grow = match content_type { ContentType::Mask => self.mask_atlas.grow( diff --git a/src/text_render.rs b/src/text_render.rs index 1d1a801..21592b8 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -1,5 +1,5 @@ use crate::{ - custom_glyph::CustomGlyphCacheKey, ColorMode, ContentType, CustomGlyphInput, CustomGlyphOutput, + custom_glyph::CustomGlyphCacheKey, ColorMode, ContentType, RasterizationRequest, RasterizedCustomGlyph, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError, SwashCache, SwashContent, TextArea, TextAtlas, Viewport, }; @@ -104,7 +104,7 @@ impl TextRenderer { viewport: &Viewport, text_areas: impl IntoIterator>, cache: &mut SwashCache, - rasterize_custom_glyph: impl FnMut(CustomGlyphInput) -> Option, + rasterize_custom_glyph: impl FnMut(RasterizationRequest) -> Option, ) -> Result<(), PrepareError> { self.prepare_with_depth_and_custom( device, @@ -130,7 +130,7 @@ impl TextRenderer { text_areas: impl IntoIterator>, cache: &mut SwashCache, mut metadata_to_depth: impl FnMut(usize) -> f32, - mut rasterize_custom_glyph: impl FnMut(CustomGlyphInput) -> Option, + mut rasterize_custom_glyph: impl FnMut(RasterizationRequest) -> Option, ) -> Result<(), PrepareError> { self.glyph_vertices.clear(); @@ -193,7 +193,7 @@ impl TextRenderer { return None; } - let input = CustomGlyphInput { + let input = RasterizationRequest { id: glyph.id, width, height, @@ -429,7 +429,7 @@ fn prepare_glyph( mut rasterize_custom_glyph: R, ) -> Result, PrepareError> where - R: FnMut(CustomGlyphInput) -> Option, + R: FnMut(RasterizationRequest) -> Option, { if atlas.mask_atlas.glyph_cache.contains(&cache_key) { atlas.mask_atlas.promote(cache_key);