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 @@
+
+
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 @@
+
+
+
+
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