diff --git a/Cargo.toml b/Cargo.toml index b87a8e4..5bbd692 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,4 +17,5 @@ rustc-hash = "2.0" [dev-dependencies] winit = "0.30.3" wgpu = "22" +resvg = { version = "0.42", default-features = false } pollster = "0.3.0" diff --git a/examples/custom-glyphs.rs b/examples/custom-glyphs.rs new file mode 100644 index 0000000..6b1ecc5 --- /dev/null +++ b/examples/custom-glyphs.rs @@ -0,0 +1,332 @@ +use glyphon::{ + Attrs, Buffer, Cache, Color, ContentType, CustomGlyph, RasterizationRequest, RasterizedCustomGlyph, + 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::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() { + let event_loop = EventLoop::new().unwrap(); + 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, + + 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: RasterizationRequest| -> 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 svg_size = svg.size(); + let scale_x = input.width as f32 / svg_size.width(); + let scale_y = input.height as f32 / svg_size.height(); + + 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_x, scale_y); + + // Offset the glyph by the subpixel amount. + let offset_x = input.x_bin.as_float(); + let offset_y = input.y_bin.as_float(); + if offset_x != 0.0 || offset_y != 0.0 { + transform = transform.post_translate(offset_x, offset_y); + } + + resvg::render(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(RasterizedCustomGlyph { data, content_type }) + }; + + 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, +} + +impl winit::application::ApplicationHandler for Application { + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + if self.window_state.is_some() { + return; + } + + // 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))); + } + + 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; + }; + + let WindowState { + window, + device, + queue, + surface, + surface_config, + font_system, + swash_cache, + viewport, + atlas, + text_renderer, + text_buffer, + rasterize_svg, + .. + } = state; + + 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_with_custom( + 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: &[ + CustomGlyph { + id: 0, + left: 300.0, + top: 5.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, + width: 64.0, + height: 64.0, + color: None, + snap_to_physical_pixel: true, + metadata: 0, + }, + CustomGlyph { + id: 0, + left: 300.0, + top: 130.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, + width: 64.0, + height: 64.0, + color: None, + snap_to_physical_pixel: true, + 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, + }, + })], + 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 => event_loop.exit(), + _ => {} + } + } +} 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/hello-world.rs b/examples/hello-world.rs index 06ce896..51c5c49 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -186,6 +186,7 @@ impl winit::application::ApplicationHandler for Application { bottom: 160, }, default_color: Color::rgb(255, 255, 255), + custom_glyphs: &[], }], swash_cache, ) 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/src/custom_glyph.rs b/src/custom_glyph.rs new file mode 100644 index 0000000..a0df98c --- /dev/null +++ b/src/custom_glyph.rs @@ -0,0 +1,113 @@ +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 `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)] +pub struct RasterizationRequest { + /// 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, +} + +/// A rasterized custom glyph +#[derive(Debug, Clone)] +pub struct RasterizedCustomGlyph { + /// The raw image data + pub data: Vec, + /// The type of image data contained in `data` + pub content_type: ContentType, +} + +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); + } + + 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, +} + +/// 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, + Self::Mask => 1, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 3b31120..9be0cdc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,19 +5,21 @@ //! [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, RasterizationRequest, RasterizedCustomGlyph, +}; pub use error::{PrepareError, RenderError}; pub use text_atlas::{ColorMode, TextAtlas}; pub use text_render::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::{ @@ -117,4 +119,7 @@ pub struct TextArea<'a> { pub bounds: TextBounds, // The default color of the text area. pub default_color: Color, + + /// Additional custom glyphs to render + pub custom_glyphs: &'a [CustomGlyph], } diff --git a/src/text_atlas.rs b/src/text_atlas.rs index 1cabbc4..cf86464 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, RasterizationRequest, RasterizedCustomGlyph, + 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(RasterizationRequest) -> 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 = RasterizationRequest { + 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(RasterizationRequest) -> 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 d89fd02..21592b8 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -1,7 +1,9 @@ use crate::{ - ColorMode, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError, - SwashCache, SwashContent, TextArea, TextAtlas, Viewport, + custom_glyph::CustomGlyphCacheKey, ColorMode, ContentType, RasterizationRequest, RasterizedCustomGlyph, + FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError, SwashCache, + SwashContent, TextArea, TextAtlas, Viewport, }; +use cosmic_text::{Color, SubpixelBin}; use std::{slice, sync::Arc}; use wgpu::{ Buffer, BufferDescriptor, BufferUsages, DepthStencilState, Device, Extent3d, ImageCopyTexture, @@ -43,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(RasterizationRequest) -> 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, @@ -54,6 +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(RasterizationRequest) -> Option, ) -> Result<(), PrepareError> { self.glyph_vertices.clear(); @@ -65,6 +142,88 @@ 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); + for glyph in text_area.custom_glyphs.iter() { + 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 (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); + + 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, + |_cache, _font_system, rasterize_custom_glyph| -> Option { + if width == 0 || height == 0 { + return None; + } + + let input = RasterizationRequest { + 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| { 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; @@ -83,189 +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 mut x = physical_glyph.x + details.left as i32; - let mut 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, }; - let depth = metadata_to_depth(glyph.metadata); + 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, + |cache, + font_system, + _rasterize_custom_glyph| + -> Option { + let Some(image) = + cache.get_image_uncached(font_system, physical_glyph.cache_key) + else { + return None; + }; - 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, - }); + 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); + } } } } @@ -302,28 +333,6 @@ impl TextRenderer { Ok(()) } - 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( - device, - queue, - font_system, - atlas, - viewport, - text_areas, - cache, - zero_depth, - ) - } - /// Renders all layouts that were previously provided to `prepare`. pub fn render<'pass>( &'pass self, @@ -345,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 { @@ -359,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) @@ -385,3 +393,199 @@ fn create_oversized_buffer( fn zero_depth(_: usize) -> f32 { 0f32 } + +struct GetGlyphImageResult { + content_type: ContentType, + top: i16, + left: i16, + width: u16, + height: u16, + data: Vec, +} + +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(RasterizationRequest) -> 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, + })) +}