Add support for custom icons/glyphs (#102)

* add support for svg icons

* remove SVG helper struct

* forgot to remove default features

* rework api for custom glyphs

* remove unused file

* expose custom glyph structs

* remove `InlineBox`

* use slice for TextArea::custom_glyphs

* offset custom glyphs by text area position

* remove svg feature

* remove unused file

* add scale field to CustomGlyphInput

* update custom-glyphs example to winit 0.30

* fix the mess merge conflicts made

* add final newline

* make custom-glyphs a default feature

* remove custom-glyphs feature

* remove unnecessary pub(crate)

* rename CustomGlyphDesc to CustomGlyph

* rename CustomGlyphID to CustomGlyphId

* improve custom glyph API and refactor text renderer

* rename CustomGlyphInput and CustomGlyphOutput, add some docs
This commit is contained in:
Billy Messenger 2024-09-16 23:57:40 -05:00 committed by GitHub
parent ce6ede951c
commit b2129f1765
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1173 additions and 222 deletions

View file

@ -17,4 +17,5 @@ rustc-hash = "2.0"
[dev-dependencies] [dev-dependencies]
winit = "0.30.3" winit = "0.30.3"
wgpu = "22" wgpu = "22"
resvg = { version = "0.42", default-features = false }
pollster = "0.3.0" pollster = "0.3.0"

332
examples/custom-glyphs.rs Normal file
View file

@ -0,0 +1,332 @@
use glyphon::{
Attrs, Buffer, Cache, Color, ContentType, CustomGlyph, RasterizationRequest, RasterizedCustomGlyph,
Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, TextArea, TextAtlas, TextBounds,
TextRenderer, Viewport,
};
use std::sync::Arc;
use wgpu::{
CommandEncoderDescriptor, CompositeAlphaMode, DeviceDescriptor, Instance, InstanceDescriptor,
LoadOp, MultisampleState, Operations, PresentMode, RenderPassColorAttachment,
RenderPassDescriptor, RequestAdapterOptions, SurfaceConfiguration, TextureFormat,
TextureUsages, TextureViewDescriptor,
};
use winit::{dpi::LogicalSize, event::WindowEvent, event_loop::EventLoop, window::Window};
// Example SVG icons are from https://publicdomainvectors.org/
static LION_SVG: &[u8] = include_bytes!("./lion.svg");
static EAGLE_SVG: &[u8] = include_bytes!("./eagle.svg");
fn main() {
let event_loop = EventLoop::new().unwrap();
event_loop
.run_app(&mut Application { window_state: None })
.unwrap();
}
struct WindowState {
device: wgpu::Device,
queue: wgpu::Queue,
surface: wgpu::Surface<'static>,
surface_config: SurfaceConfiguration,
font_system: FontSystem,
swash_cache: SwashCache,
viewport: glyphon::Viewport,
atlas: glyphon::TextAtlas,
text_renderer: glyphon::TextRenderer,
text_buffer: glyphon::Buffer,
rasterize_svg: Box<dyn Fn(RasterizationRequest) -> Option<RasterizedCustomGlyph>>,
// 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<Window>,
}
impl WindowState {
async fn new(window: Arc<Window>) -> Self {
let physical_size = window.inner_size();
let scale_factor = window.scale_factor();
// Set up surface
let instance = Instance::new(InstanceDescriptor::default());
let adapter = instance
.request_adapter(&RequestAdapterOptions::default())
.await
.unwrap();
let (device, queue) = adapter
.request_device(&DeviceDescriptor::default(), None)
.await
.unwrap();
let surface = instance
.create_surface(window.clone())
.expect("Create surface");
let swapchain_format = TextureFormat::Bgra8UnormSrgb;
let surface_config = SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format: swapchain_format,
width: physical_size.width,
height: physical_size.height,
present_mode: PresentMode::Fifo,
alpha_mode: CompositeAlphaMode::Opaque,
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surface.configure(&device, &surface_config);
// Set up text renderer
let mut font_system = FontSystem::new();
let swash_cache = SwashCache::new();
let cache = Cache::new(&device);
let viewport = Viewport::new(&device, &cache);
let mut atlas = TextAtlas::new(&device, &queue, &cache, swapchain_format);
let text_renderer =
TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None);
let mut text_buffer = Buffer::new(&mut font_system, Metrics::new(30.0, 42.0));
let physical_width = (physical_size.width as f64 * scale_factor) as f32;
let physical_height = (physical_size.height as f64 * scale_factor) as f32;
text_buffer.set_size(
&mut font_system,
Some(physical_width),
Some(physical_height),
);
text_buffer.set_text(
&mut font_system,
"SVG icons! --->\n\nThe icons below should be partially clipped.",
Attrs::new().family(Family::SansSerif),
Shaping::Advanced,
);
text_buffer.shape_until_scroll(&mut font_system, false);
// Set up custom svg renderer
let svg_0 = resvg::usvg::Tree::from_data(LION_SVG, &Default::default()).unwrap();
let svg_1 = resvg::usvg::Tree::from_data(EAGLE_SVG, &Default::default()).unwrap();
let rasterize_svg = move |input: RasterizationRequest| -> Option<RasterizedCustomGlyph> {
// Select the svg data based on the custom glyph ID.
let (svg, content_type) = match input.id {
0 => (&svg_0, ContentType::Mask),
1 => (&svg_1, ContentType::Color),
_ => return None,
};
// Calculate the scale based on the "glyph size".
let svg_size = svg.size();
let scale_x = input.width as f32 / svg_size.width();
let scale_y = input.height as f32 / svg_size.height();
let Some(mut pixmap) =
resvg::tiny_skia::Pixmap::new(input.width as u32, input.height as u32)
else {
return None;
};
let mut transform = resvg::usvg::Transform::from_scale(scale_x, scale_y);
// Offset the glyph by the subpixel amount.
let offset_x = input.x_bin.as_float();
let offset_y = input.y_bin.as_float();
if offset_x != 0.0 || offset_y != 0.0 {
transform = transform.post_translate(offset_x, offset_y);
}
resvg::render(svg, transform, &mut pixmap.as_mut());
let data: Vec<u8> = if let ContentType::Mask = content_type {
// Only use the alpha channel for symbolic icons.
pixmap.data().iter().skip(3).step_by(4).copied().collect()
} else {
pixmap.data().to_vec()
};
Some(RasterizedCustomGlyph { data, content_type })
};
Self {
device,
queue,
surface,
surface_config,
font_system,
swash_cache,
viewport,
atlas,
text_renderer,
text_buffer,
rasterize_svg: Box::new(rasterize_svg),
window,
}
}
}
struct Application {
window_state: Option<WindowState>,
}
impl winit::application::ApplicationHandler for Application {
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
if self.window_state.is_some() {
return;
}
// Set up window
let (width, height) = (800, 600);
let window_attributes = Window::default_attributes()
.with_inner_size(LogicalSize::new(width as f64, height as f64))
.with_title("glyphon hello world");
let window = Arc::new(event_loop.create_window(window_attributes).unwrap());
self.window_state = Some(pollster::block_on(WindowState::new(window)));
}
fn window_event(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
_window_id: winit::window::WindowId,
event: WindowEvent,
) {
let Some(state) = &mut self.window_state else {
return;
};
let WindowState {
window,
device,
queue,
surface,
surface_config,
font_system,
swash_cache,
viewport,
atlas,
text_renderer,
text_buffer,
rasterize_svg,
..
} = state;
match event {
WindowEvent::Resized(size) => {
surface_config.width = size.width;
surface_config.height = size.height;
surface.configure(&device, &surface_config);
window.request_redraw();
}
WindowEvent::RedrawRequested => {
viewport.update(
&queue,
Resolution {
width: surface_config.width,
height: surface_config.height,
},
);
text_renderer
.prepare_with_custom(
device,
queue,
font_system,
atlas,
viewport,
[TextArea {
buffer: &text_buffer,
left: 10.0,
top: 10.0,
scale: 1.0,
bounds: TextBounds {
left: 0,
top: 0,
right: 650,
bottom: 180,
},
default_color: Color::rgb(255, 255, 255),
custom_glyphs: &[
CustomGlyph {
id: 0,
left: 300.0,
top: 5.0,
width: 64.0,
height: 64.0,
color: Some(Color::rgb(200, 200, 255)),
snap_to_physical_pixel: true,
metadata: 0,
},
CustomGlyph {
id: 1,
left: 400.0,
top: 5.0,
width: 64.0,
height: 64.0,
color: None,
snap_to_physical_pixel: true,
metadata: 0,
},
CustomGlyph {
id: 0,
left: 300.0,
top: 130.0,
width: 64.0,
height: 64.0,
color: Some(Color::rgb(200, 255, 200)),
snap_to_physical_pixel: true,
metadata: 0,
},
CustomGlyph {
id: 1,
left: 400.0,
top: 130.0,
width: 64.0,
height: 64.0,
color: None,
snap_to_physical_pixel: true,
metadata: 0,
},
],
}],
swash_cache,
rasterize_svg,
)
.unwrap();
let frame = surface.get_current_texture().unwrap();
let view = frame.texture.create_view(&TextureViewDescriptor::default());
let mut encoder =
device.create_command_encoder(&CommandEncoderDescriptor { label: None });
{
let mut pass = encoder.begin_render_pass(&RenderPassDescriptor {
label: None,
color_attachments: &[Some(RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(wgpu::Color {
r: 0.02,
g: 0.02,
b: 0.02,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
text_renderer.render(&atlas, &viewport, &mut pass).unwrap();
}
queue.submit(Some(encoder.finish()));
frame.present();
atlas.trim();
}
WindowEvent::CloseRequested => event_loop.exit(),
_ => {}
}
}
}

72
examples/eagle.svg Normal file
View file

@ -0,0 +1,72 @@
<?xml version="1.0"?>
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://web.resource.org/cc/" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" id="svg2" viewBox="0 0 141.78 179.81" version="1.0">
<defs id="defs4">
<linearGradient id="linearGradient9890" x1="347" gradientUnits="userSpaceOnUse" y1="417.11" gradientTransform="matrix(.96509 0 0 .96509 -293.49 -301.38)" x2="376.5" y2="375.11">
<stop id="stop8114" stop-color="#5a5a5a" offset="0"/>
<stop id="stop8116" stop-color="#c6c6c6" stop-opacity="0" offset="1"/>
</linearGradient>
<radialGradient id="radialGradient15226" gradientUnits="userSpaceOnUse" cy="330.67" cx="452.81" gradientTransform="matrix(-.63914 -.42728 .71829 -1.0744 74.046 594.02)" r="7.1607">
<stop id="stop14335" stop-color="#eebe9e" offset="0"/>
<stop id="stop14337" stop-color="#eebe9e" stop-opacity="0" offset="1"/>
</radialGradient>
<linearGradient id="linearGradient4561" y2="25.336" gradientUnits="userSpaceOnUse" y1="125.39" x2="108.14" x1="-43.815">
<stop id="stop16117" stop-color="#b3b3b3" offset="0"/>
<stop id="stop16119" stop-color="#f7f7f7" offset="1"/>
</linearGradient>
</defs>
<g id="layer1" transform="translate(-7.3576 -5.5473)">
<path id="path1894" fill-rule="evenodd" d="m58.516 106.37l-2.642 11.93c-28.766 2.73-22.398 47.57-24.71 67.17l117.9 0.25 0.07-98.485-14.49-13.058c-28.7 8.017-56.132 11.116-76.124 32.193z"/>
<path id="path1886" fill-rule="evenodd" fill="url(#linearGradient4561)" d="m27.249 31.823c1.452-14.893 18.492-18.294 36.559-19.986 23.107 0.077 35.052 6.85 45.332 14.624 15.34 15.16 15.31 28.398 21.45 42.407l16.08 16.086c-38.59-9.253-64.004 6.637-88.712 23.886l-0.487-36.072c-3.625-1.037-11.68-12.167-20.96-17.061l-9.262-23.884z"/>
<path id="path1884" fill-rule="evenodd" fill="#d38d5f" d="m65.782 44.837c-1.036 3.412-7.139 6.824-14.624 10.237-10.561-0.65-21.804 2.464-32.366 6.97 3.921-6.85 14.622-12.551 46.99-17.207z"/>
<path id="path2775" fill-rule="evenodd" fill="#a05a2c" d="m43.335 32.31l20.473-4.874c0.978 3.513 2.129 7.038-1.95 10.236-2.856-0.224-5.613 1.324-8.774-4.387l-9.749-0.975z"/>
<path id="path1882" fill-rule="evenodd" fill="#cf7b00" d="m16.526 68.868c-5.801-12.64-8.6819-25.194 12.324-36.558l5.224 0.487 17.06 10.237 15.111 1.949c-16.305 4.237-34.523 3.371-47.282 17.061l-2.437 6.824z"/>
<path id="path2777" fill-rule="evenodd" fill="#d0d0d0" d="m57.593 29.629c2.992 0.09 7.856-2.564 6.336 4.143-1.204 4.683-4.186 2.702-6.58 2.925-3.515-2.356-2.724-4.712 0.244-7.068z"/>
<path id="path2779" opacity=".69512" transform="matrix(.96509 0 0 .96509 -293.49 -301.38)" fill="none" d="m367.19 344.37a2.5885 0.25254 0 1 1 -5.18 0 2.5885 0.25254 0 1 1 5.18 0z"/>
<path id="path8102" fill-rule="evenodd" fill="url(#linearGradient9890)" d="m51.048 55.088l28.47-1.448 12.547 35.226c-13.29 5.876-22.533 11.784-34.232 20.204l-0.029-36.611c-5.487-2.133-16.434-14.231-20.749-16.889l13.993-0.482z"/>
<path id="path10777" opacity=".69512" transform="matrix(.96509 0 0 .96509 -414.61 -272.91)" fill="#333" d="m492.85 316.79a1.591 1.591 0 1 1 -3.18 0 1.591 1.591 0 1 1 3.18 0z"/>
<path id="path12553" opacity=".43089" fill-rule="evenodd" fill="url(#radialGradient15226)" d="m28.384 34.419c-9.284 3.339-14.14 10.143-12.966 21.667l13.648-8.36-0.682-13.307z"/>
</g>
<metadata>
<rdf:RDF>
<cc:Work>
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/>
<dc:publisher>
<cc:Agent rdf:about="http://openclipart.org/">
<dc:title>Openclipart</dc:title>
</cc:Agent>
</dc:publisher>
<dc:title>Eagle</dc:title>
<dc:date>2007-01-24T06:25:54</dc:date>
<dc:description>animal, animal, bird, bird, clip art, clipart, eagle, eagle, head, head, image, media, nature, nature, public domain, svg, </dc:description>
<dc:source>http://openclipart.org/detail/2962/eagle-by-nfroidure</dc:source>
<dc:creator>
<cc:Agent>
<dc:title>nfroidure</dc:title>
</cc:Agent>
</dc:creator>
<dc:subject>
<rdf:Bag>
<rdf:li>animal</rdf:li>
<rdf:li>bird</rdf:li>
<rdf:li>clip art</rdf:li>
<rdf:li>clipart</rdf:li>
<rdf:li>eagle</rdf:li>
<rdf:li>head</rdf:li>
<rdf:li>image</rdf:li>
<rdf:li>media</rdf:li>
<rdf:li>nature</rdf:li>
<rdf:li>public domain</rdf:li>
<rdf:li>svg</rdf:li>
</rdf:Bag>
</dc:subject>
</cc:Work>
<cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/">
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
</cc:License>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -186,6 +186,7 @@ impl winit::application::ApplicationHandler for Application {
bottom: 160, bottom: 160,
}, },
default_color: Color::rgb(255, 255, 255), default_color: Color::rgb(255, 255, 255),
custom_glyphs: &[],
}], }],
swash_cache, swash_cache,
) )

169
examples/lion.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

113
src/custom_glyph.rs Normal file
View file

@ -0,0 +1,113 @@
use crate::Color;
use cosmic_text::SubpixelBin;
pub type CustomGlyphId = u16;
/// A custom glyph to render
#[derive(Default, Debug, Clone, Copy, PartialEq)]
pub struct CustomGlyph {
/// The unique identifier for this glyph
pub id: CustomGlyphId,
/// The position of the left edge of the glyph
pub left: f32,
/// The position of the top edge of the glyph
pub top: f32,
/// The width of the glyph
pub width: f32,
/// The height of the glyph
pub height: f32,
/// The color of this glyph (only relevant if the glyph is rendered with the
/// type [`ContentType::Mask`])
///
/// Set to `None` to use [`TextArea::default_color`].
pub color: Option<Color>,
/// If `true`, then this glyph will be snapped to the nearest whole physical
/// pixel and the resulting `SubpixelBin`'s in `RasterizationRequest` will always
/// be `Zero` (useful for images and other large glyphs).
pub snap_to_physical_pixel: bool,
/// Additional metadata about the glyph
pub metadata: usize,
}
/// A request to rasterize a custom glyph
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RasterizationRequest {
/// The unique identifier of the glyph
pub id: CustomGlyphId,
/// The width of the glyph in physical pixels
pub width: u16,
/// The height of the glyph in physical pixels
pub height: u16,
/// Binning of fractional X offset
///
/// If `CustomGlyph::snap_to_physical_pixel` was set to `true`, then this
/// will always be `Zero`.
pub x_bin: SubpixelBin,
/// Binning of fractional Y offset
///
/// If `CustomGlyph::snap_to_physical_pixel` was set to `true`, then this
/// will always be `Zero`.
pub y_bin: SubpixelBin,
/// The scaling factor applied to the text area (Note that `width` and
/// `height` are already scaled by this factor.)
pub scale: f32,
}
/// A rasterized custom glyph
#[derive(Debug, Clone)]
pub struct RasterizedCustomGlyph {
/// The raw image data
pub data: Vec<u8>,
/// The type of image data contained in `data`
pub content_type: ContentType,
}
impl RasterizedCustomGlyph {
pub(crate) fn validate(&self, input: &RasterizationRequest, expected_type: Option<ContentType>) {
if let Some(expected_type) = expected_type {
assert_eq!(self.content_type, expected_type, "Custom glyph rasterizer must always produce the same content type for a given input. Expected {:?}, got {:?}. Input: {:?}", expected_type, self.content_type, input);
}
assert_eq!(
self.data.len(),
input.width as usize * input.height as usize * self.content_type.bytes_per_pixel(),
"Invalid custom glyph rasterizer output. Expected data of length {}, got length {}. Input: {:?}",
input.width as usize * input.height as usize * self.content_type.bytes_per_pixel(),
self.data.len(),
input,
);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CustomGlyphCacheKey {
/// Font ID
pub glyph_id: CustomGlyphId,
/// Glyph width
pub width: u16,
/// Glyph height
pub height: u16,
/// Binning of fractional X offset
pub x_bin: SubpixelBin,
/// Binning of fractional Y offset
pub y_bin: SubpixelBin,
}
/// The type of image data contained in a rasterized glyph
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum ContentType {
/// Each pixel contains 32 bits of rgba data
Color,
/// Each pixel contains a single 8 bit channel
Mask,
}
impl ContentType {
/// The number of bytes per pixel for this content type
pub fn bytes_per_pixel(&self) -> usize {
match self {
Self::Color => 4,
Self::Mask => 1,
}
}
}

View file

@ -5,19 +5,21 @@
//! [etagere]: https://github.com/nical/etagere //! [etagere]: https://github.com/nical/etagere
mod cache; mod cache;
mod custom_glyph;
mod error; mod error;
mod text_atlas; mod text_atlas;
mod text_render; mod text_render;
mod viewport; mod viewport;
pub use cache::Cache; pub use cache::Cache;
pub use custom_glyph::{
ContentType, CustomGlyph, CustomGlyphId, RasterizationRequest, RasterizedCustomGlyph,
};
pub use error::{PrepareError, RenderError}; pub use error::{PrepareError, RenderError};
pub use text_atlas::{ColorMode, TextAtlas}; pub use text_atlas::{ColorMode, TextAtlas};
pub use text_render::TextRenderer; pub use text_render::TextRenderer;
pub use viewport::Viewport; pub use viewport::Viewport;
use text_render::ContentType;
// Re-export all top-level types from `cosmic-text` for convenience. // Re-export all top-level types from `cosmic-text` for convenience.
#[doc(no_inline)] #[doc(no_inline)]
pub use cosmic_text::{ pub use cosmic_text::{
@ -117,4 +119,7 @@ pub struct TextArea<'a> {
pub bounds: TextBounds, pub bounds: TextBounds,
// The default color of the text area. // The default color of the text area.
pub default_color: Color, pub default_color: Color,
/// Additional custom glyphs to render
pub custom_glyphs: &'a [CustomGlyph],
} }

View file

@ -1,5 +1,6 @@
use crate::{ use crate::{
text_render::ContentType, Cache, CacheKey, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, text_render::GlyphonCacheKey, Cache, ContentType, RasterizationRequest, RasterizedCustomGlyph,
FontSystem, GlyphDetails, GpuCacheStatus, SwashCache,
}; };
use etagere::{size2, Allocation, BucketedAtlasAllocator}; use etagere::{size2, Allocation, BucketedAtlasAllocator};
use lru::LruCache; use lru::LruCache;
@ -20,8 +21,8 @@ pub(crate) struct InnerAtlas {
pub texture_view: TextureView, pub texture_view: TextureView,
pub packer: BucketedAtlasAllocator, pub packer: BucketedAtlasAllocator,
pub size: u32, pub size: u32,
pub glyph_cache: LruCache<CacheKey, GlyphDetails, Hasher>, pub glyph_cache: LruCache<GlyphonCacheKey, GlyphDetails, Hasher>,
pub glyphs_in_use: HashSet<CacheKey, Hasher>, pub glyphs_in_use: HashSet<GlyphonCacheKey, Hasher>,
pub max_texture_dimension_2d: u32, pub max_texture_dimension_2d: u32,
} }
@ -106,12 +107,12 @@ impl InnerAtlas {
self.kind.num_channels() 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.glyph_cache.promote(&glyph);
self.glyphs_in_use.insert(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.glyph_cache.put(glyph, details);
self.glyphs_in_use.insert(glyph); self.glyphs_in_use.insert(glyph);
} }
@ -122,6 +123,8 @@ impl InnerAtlas {
queue: &wgpu::Queue, queue: &wgpu::Queue,
font_system: &mut FontSystem, font_system: &mut FontSystem,
cache: &mut SwashCache, cache: &mut SwashCache,
scale_factor: f32,
mut rasterize_custom_glyph: impl FnMut(RasterizationRequest) -> Option<RasterizedCustomGlyph>,
) -> bool { ) -> bool {
if self.size >= self.max_texture_dimension_2d { if self.size >= self.max_texture_dimension_2d {
return false; return false;
@ -157,10 +160,38 @@ impl InnerAtlas {
GpuCacheStatus::SkipRasterization => continue, 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; (image.data, width, height)
let height = image.placement.height as usize; }
GlyphonCacheKey::Custom(cache_key) => {
let input = RasterizationRequest {
id: cache_key.glyph_id,
width: cache_key.width,
height: cache_key.height,
x_bin: cache_key.x_bin,
y_bin: cache_key.y_bin,
scale: scale_factor,
};
let Some(rasterized_glyph) = (rasterize_custom_glyph)(input) else {
panic!("Custom glyph rasterizer returned `None` when it previously returned `Some` for the same input {:?}", &input);
};
// Sanity checks on the rasterizer output
rasterized_glyph.validate(&input, Some(self.kind.as_content_type()));
(
rasterized_glyph.data,
cache_key.width as usize,
cache_key.height as usize,
)
}
};
queue.write_texture( queue.write_texture(
ImageCopyTexture { ImageCopyTexture {
@ -173,7 +204,7 @@ impl InnerAtlas {
}, },
aspect: TextureAspect::All, aspect: TextureAspect::All,
}, },
&image.data, &image_data,
ImageDataLayout { ImageDataLayout {
offset: 0, offset: 0,
bytes_per_row: Some(width as u32 * self.kind.num_channels() as u32), 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`]. /// The color mode of an [`Atlas`].
@ -313,10 +351,26 @@ impl TextAtlas {
font_system: &mut FontSystem, font_system: &mut FontSystem,
cache: &mut SwashCache, cache: &mut SwashCache,
content_type: ContentType, content_type: ContentType,
scale_factor: f32,
rasterize_custom_glyph: impl FnMut(RasterizationRequest) -> Option<RasterizedCustomGlyph>,
) -> bool { ) -> bool {
let did_grow = match content_type { let did_grow = match content_type {
ContentType::Mask => self.mask_atlas.grow(device, queue, font_system, cache), ContentType::Mask => self.mask_atlas.grow(
ContentType::Color => self.color_atlas.grow(device, queue, font_system, cache), 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 { if did_grow {
@ -326,7 +380,7 @@ impl TextAtlas {
did_grow did_grow
} }
pub(crate) fn glyph(&self, glyph: &CacheKey) -> Option<&GlyphDetails> { pub(crate) fn glyph(&self, glyph: &GlyphonCacheKey) -> Option<&GlyphDetails> {
self.mask_atlas self.mask_atlas
.glyph_cache .glyph_cache
.peek(glyph) .peek(glyph)

View file

@ -1,7 +1,9 @@
use crate::{ use crate::{
ColorMode, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError, custom_glyph::CustomGlyphCacheKey, ColorMode, ContentType, RasterizationRequest, RasterizedCustomGlyph,
SwashCache, SwashContent, TextArea, TextAtlas, Viewport, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError, SwashCache,
SwashContent, TextArea, TextAtlas, Viewport,
}; };
use cosmic_text::{Color, SubpixelBin};
use std::{slice, sync::Arc}; use std::{slice, sync::Arc};
use wgpu::{ use wgpu::{
Buffer, BufferDescriptor, BufferUsages, DepthStencilState, Device, Extent3d, ImageCopyTexture, Buffer, BufferDescriptor, BufferUsages, DepthStencilState, Device, Extent3d, ImageCopyTexture,
@ -43,8 +45,82 @@ impl TextRenderer {
} }
} }
/// Prepares all of the provided text areas for rendering.
pub fn prepare<'a>(
&mut self,
device: &Device,
queue: &Queue,
font_system: &mut FontSystem,
atlas: &mut TextAtlas,
viewport: &Viewport,
text_areas: impl IntoIterator<Item = TextArea<'a>>,
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. /// Prepares all of the provided text areas for rendering.
pub fn prepare_with_depth<'a>( 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<Item = TextArea<'a>>,
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<Item = TextArea<'a>>,
cache: &mut SwashCache,
rasterize_custom_glyph: impl FnMut(RasterizationRequest) -> Option<RasterizedCustomGlyph>,
) -> 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, &mut self,
device: &Device, device: &Device,
queue: &Queue, queue: &Queue,
@ -54,6 +130,7 @@ impl TextRenderer {
text_areas: impl IntoIterator<Item = TextArea<'a>>, text_areas: impl IntoIterator<Item = TextArea<'a>>,
cache: &mut SwashCache, cache: &mut SwashCache,
mut metadata_to_depth: impl FnMut(usize) -> f32, mut metadata_to_depth: impl FnMut(usize) -> f32,
mut rasterize_custom_glyph: impl FnMut(RasterizationRequest) -> Option<RasterizedCustomGlyph>,
) -> Result<(), PrepareError> { ) -> Result<(), PrepareError> {
self.glyph_vertices.clear(); self.glyph_vertices.clear();
@ -65,6 +142,88 @@ impl TextRenderer {
let bounds_max_x = text_area.bounds.right.min(resolution.width as i32); let bounds_max_x = text_area.bounds.right.min(resolution.width as i32);
let bounds_max_y = text_area.bounds.bottom.min(resolution.height as i32); let bounds_max_y = text_area.bounds.bottom.min(resolution.height as i32);
for glyph in text_area.custom_glyphs.iter() {
let x = text_area.left + (glyph.left * text_area.scale);
let y = text_area.top + (glyph.top * text_area.scale);
let width = (glyph.width * text_area.scale).round() as u16;
let height = (glyph.height * text_area.scale).round() as u16;
let (x, y, x_bin, y_bin) = if glyph.snap_to_physical_pixel {
(
x.round() as i32,
y.round() as i32,
SubpixelBin::Zero,
SubpixelBin::Zero,
)
} else {
let (x, x_bin) = SubpixelBin::new(x);
let (y, y_bin) = SubpixelBin::new(y);
(x, y, x_bin, y_bin)
};
let cache_key = GlyphonCacheKey::Custom(CustomGlyphCacheKey {
glyph_id: glyph.id,
width,
height,
x_bin,
y_bin,
});
let color = glyph.color.unwrap_or(text_area.default_color);
if let Some(glyph_to_render) = prepare_glyph(
x,
y,
0.0,
color,
glyph.metadata,
cache_key,
atlas,
device,
queue,
cache,
font_system,
text_area.scale,
bounds_min_x,
bounds_min_y,
bounds_max_x,
bounds_max_y,
|_cache, _font_system, rasterize_custom_glyph| -> Option<GetGlyphImageResult> {
if width == 0 || height == 0 {
return None;
}
let input = RasterizationRequest {
id: glyph.id,
width,
height,
x_bin,
y_bin,
scale: text_area.scale,
};
let Some(output) = (rasterize_custom_glyph)(input) else {
return None;
};
output.validate(&input, None);
Some(GetGlyphImageResult {
content_type: output.content_type,
top: 0,
left: 0,
width,
height,
data: output.data,
})
},
&mut metadata_to_depth,
&mut rasterize_custom_glyph,
)? {
self.glyph_vertices.push(glyph_to_render);
}
}
let is_run_visible = |run: &cosmic_text::LayoutRun| { let is_run_visible = |run: &cosmic_text::LayoutRun| {
let start_y = (text_area.top + run.line_top) as i32; 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; let end_y = (text_area.top + run.line_top + run.line_height) as i32;
@ -83,189 +242,61 @@ impl TextRenderer {
let physical_glyph = let physical_glyph =
glyph.physical((text_area.left, text_area.top), text_area.scale); glyph.physical((text_area.left, text_area.top), text_area.scale);
if atlas
.mask_atlas
.glyph_cache
.contains(&physical_glyph.cache_key)
{
atlas.mask_atlas.promote(physical_glyph.cache_key);
} else if atlas
.color_atlas
.glyph_cache
.contains(&physical_glyph.cache_key)
{
atlas.color_atlas.promote(physical_glyph.cache_key);
} else {
let Some(image) =
cache.get_image_uncached(font_system, physical_glyph.cache_key)
else {
continue;
};
let content_type = match image.content {
SwashContent::Color => ContentType::Color,
SwashContent::Mask => ContentType::Mask,
SwashContent::SubpixelMask => {
// Not implemented yet, but don't panic if this happens.
ContentType::Mask
}
};
let width = image.placement.width as usize;
let height = image.placement.height as usize;
let should_rasterize = width > 0 && height > 0;
let (gpu_cache, atlas_id, inner) = if should_rasterize {
let mut inner = atlas.inner_for_content_mut(content_type);
// Find a position in the packer
let allocation = loop {
match inner.try_allocate(width, height) {
Some(a) => break a,
None => {
if !atlas.grow(
device,
queue,
font_system,
cache,
content_type,
) {
return Err(PrepareError::AtlasFull);
}
inner = atlas.inner_for_content_mut(content_type);
}
}
};
let atlas_min = allocation.rectangle.min;
queue.write_texture(
ImageCopyTexture {
texture: &inner.texture,
mip_level: 0,
origin: Origin3d {
x: atlas_min.x as u32,
y: atlas_min.y as u32,
z: 0,
},
aspect: TextureAspect::All,
},
&image.data,
ImageDataLayout {
offset: 0,
bytes_per_row: Some(width as u32 * inner.num_channels() as u32),
rows_per_image: None,
},
Extent3d {
width: width as u32,
height: height as u32,
depth_or_array_layers: 1,
},
);
(
GpuCacheStatus::InAtlas {
x: atlas_min.x as u16,
y: atlas_min.y as u16,
content_type,
},
Some(allocation.id),
inner,
)
} else {
let inner = &mut atlas.color_atlas;
(GpuCacheStatus::SkipRasterization, None, inner)
};
inner.put(
physical_glyph.cache_key,
GlyphDetails {
width: width as u16,
height: height as u16,
gpu_cache,
atlas_id,
top: image.placement.top as i16,
left: image.placement.left as i16,
},
);
}
let details = atlas.glyph(&physical_glyph.cache_key).unwrap();
let mut x = physical_glyph.x + details.left as i32;
let mut y = (run.line_y * text_area.scale).round() as i32 + physical_glyph.y
- details.top as i32;
let (mut atlas_x, mut atlas_y, content_type) = match details.gpu_cache {
GpuCacheStatus::InAtlas { x, y, content_type } => (x, y, content_type),
GpuCacheStatus::SkipRasterization => continue,
};
let mut width = details.width as i32;
let mut height = details.height as i32;
// Starts beyond right edge or ends beyond left edge
let max_x = x + width;
if x > bounds_max_x || max_x < bounds_min_x {
continue;
}
// Starts beyond bottom edge or ends beyond top edge
let max_y = y + height;
if y > bounds_max_y || max_y < bounds_min_y {
continue;
}
// Clip left ege
if x < bounds_min_x {
let right_shift = bounds_min_x - x;
x = bounds_min_x;
width = max_x - bounds_min_x;
atlas_x += right_shift as u16;
}
// Clip right edge
if x + width > bounds_max_x {
width = bounds_max_x - x;
}
// Clip top edge
if y < bounds_min_y {
let bottom_shift = bounds_min_y - y;
y = bounds_min_y;
height = max_y - bounds_min_y;
atlas_y += bottom_shift as u16;
}
// Clip bottom edge
if y + height > bounds_max_y {
height = bounds_max_y - y;
}
let color = match glyph.color_opt { let color = match glyph.color_opt {
Some(some) => some, Some(some) => some,
None => text_area.default_color, None => text_area.default_color,
}; };
let depth = metadata_to_depth(glyph.metadata); if let Some(glyph_to_render) = prepare_glyph(
physical_glyph.x,
physical_glyph.y,
run.line_y,
color,
glyph.metadata,
GlyphonCacheKey::Text(physical_glyph.cache_key),
atlas,
device,
queue,
cache,
font_system,
text_area.scale,
bounds_min_x,
bounds_min_y,
bounds_max_x,
bounds_max_y,
|cache,
font_system,
_rasterize_custom_glyph|
-> Option<GetGlyphImageResult> {
let Some(image) =
cache.get_image_uncached(font_system, physical_glyph.cache_key)
else {
return None;
};
self.glyph_vertices.push(GlyphToRender { let content_type = match image.content {
pos: [x, y], SwashContent::Color => ContentType::Color,
dim: [width as u16, height as u16], SwashContent::Mask => ContentType::Mask,
uv: [atlas_x, atlas_y], SwashContent::SubpixelMask => {
color: color.0, // Not implemented yet, but don't panic if this happens.
content_type_with_srgb: [ ContentType::Mask
content_type as u16, }
match atlas.color_mode { };
ColorMode::Accurate => TextColorConversion::ConvertToLinear,
ColorMode::Web => TextColorConversion::None, Some(GetGlyphImageResult {
} as u16, content_type,
], top: image.placement.top as i16,
depth, left: image.placement.left as i16,
}); width: image.placement.width as u16,
height: image.placement.height as u16,
data: image.data,
})
},
&mut metadata_to_depth,
&mut rasterize_custom_glyph,
)? {
self.glyph_vertices.push(glyph_to_render);
}
} }
} }
} }
@ -302,28 +333,6 @@ impl TextRenderer {
Ok(()) Ok(())
} }
pub fn prepare<'a>(
&mut self,
device: &Device,
queue: &Queue,
font_system: &mut FontSystem,
atlas: &mut TextAtlas,
viewport: &Viewport,
text_areas: impl IntoIterator<Item = TextArea<'a>>,
cache: &mut SwashCache,
) -> Result<(), PrepareError> {
self.prepare_with_depth(
device,
queue,
font_system,
atlas,
viewport,
text_areas,
cache,
zero_depth,
)
}
/// Renders all layouts that were previously provided to `prepare`. /// Renders all layouts that were previously provided to `prepare`.
pub fn render<'pass>( pub fn render<'pass>(
&'pass self, &'pass self,
@ -345,13 +354,6 @@ impl TextRenderer {
} }
} }
#[repr(u16)]
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum ContentType {
Color = 0,
Mask = 1,
}
#[repr(u16)] #[repr(u16)]
#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum TextColorConversion { enum TextColorConversion {
@ -359,6 +361,12 @@ enum TextColorConversion {
ConvertToLinear = 1, 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 { fn next_copy_buffer_size(size: u64) -> u64 {
let align_mask = COPY_BUFFER_ALIGNMENT - 1; let align_mask = COPY_BUFFER_ALIGNMENT - 1;
((size.next_power_of_two() + align_mask) & !align_mask).max(COPY_BUFFER_ALIGNMENT) ((size.next_power_of_two() + align_mask) & !align_mask).max(COPY_BUFFER_ALIGNMENT)
@ -385,3 +393,199 @@ fn create_oversized_buffer(
fn zero_depth(_: usize) -> f32 { fn zero_depth(_: usize) -> f32 {
0f32 0f32
} }
struct GetGlyphImageResult {
content_type: ContentType,
top: i16,
left: i16,
width: u16,
height: u16,
data: Vec<u8>,
}
fn prepare_glyph<R>(
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<GetGlyphImageResult>,
mut metadata_to_depth: impl FnMut(usize) -> f32,
mut rasterize_custom_glyph: R,
) -> Result<Option<GlyphToRender>, PrepareError>
where
R: FnMut(RasterizationRequest) -> Option<RasterizedCustomGlyph>,
{
if atlas.mask_atlas.glyph_cache.contains(&cache_key) {
atlas.mask_atlas.promote(cache_key);
} else if atlas.color_atlas.glyph_cache.contains(&cache_key) {
atlas.color_atlas.promote(cache_key);
} else {
let Some(image) = (get_glyph_image)(cache, font_system, &mut rasterize_custom_glyph) else {
return Ok(None);
};
let should_rasterize = image.width > 0 && image.height > 0;
let (gpu_cache, atlas_id, inner) = if should_rasterize {
let mut inner = atlas.inner_for_content_mut(image.content_type);
// Find a position in the packer
let allocation = loop {
match inner.try_allocate(image.width as usize, image.height as usize) {
Some(a) => break a,
None => {
if !atlas.grow(
device,
queue,
font_system,
cache,
image.content_type,
scale_factor,
&mut rasterize_custom_glyph,
) {
return Err(PrepareError::AtlasFull);
}
inner = atlas.inner_for_content_mut(image.content_type);
}
}
};
let atlas_min = allocation.rectangle.min;
queue.write_texture(
ImageCopyTexture {
texture: &inner.texture,
mip_level: 0,
origin: Origin3d {
x: atlas_min.x as u32,
y: atlas_min.y as u32,
z: 0,
},
aspect: TextureAspect::All,
},
&image.data,
ImageDataLayout {
offset: 0,
bytes_per_row: Some(image.width as u32 * inner.num_channels() as u32),
rows_per_image: None,
},
Extent3d {
width: image.width as u32,
height: image.height as u32,
depth_or_array_layers: 1,
},
);
(
GpuCacheStatus::InAtlas {
x: atlas_min.x as u16,
y: atlas_min.y as u16,
content_type: image.content_type,
},
Some(allocation.id),
inner,
)
} else {
let inner = &mut atlas.color_atlas;
(GpuCacheStatus::SkipRasterization, None, inner)
};
inner.put(
cache_key,
GlyphDetails {
width: image.width,
height: image.height,
gpu_cache,
atlas_id,
top: image.top,
left: image.left,
},
);
}
let details = atlas.glyph(&cache_key).unwrap();
let mut x = x + details.left as i32;
let mut y = (line_y * scale_factor).round() as i32 + y - details.top as i32;
let (mut atlas_x, mut atlas_y, content_type) = match details.gpu_cache {
GpuCacheStatus::InAtlas { x, y, content_type } => (x, y, content_type),
GpuCacheStatus::SkipRasterization => return Ok(None),
};
let mut width = details.width as i32;
let mut height = details.height as i32;
// Starts beyond right edge or ends beyond left edge
let max_x = x + width;
if x > bounds_max_x || max_x < bounds_min_x {
return Ok(None);
}
// Starts beyond bottom edge or ends beyond top edge
let max_y = y + height;
if y > bounds_max_y || max_y < bounds_min_y {
return Ok(None);
}
// Clip left ege
if x < bounds_min_x {
let right_shift = bounds_min_x - x;
x = bounds_min_x;
width = max_x - bounds_min_x;
atlas_x += right_shift as u16;
}
// Clip right edge
if x + width > bounds_max_x {
width = bounds_max_x - x;
}
// Clip top edge
if y < bounds_min_y {
let bottom_shift = bounds_min_y - y;
y = bounds_min_y;
height = max_y - bounds_min_y;
atlas_y += bottom_shift as u16;
}
// Clip bottom edge
if y + height > bounds_max_y {
height = bounds_max_y - y;
}
let depth = metadata_to_depth(metadata);
Ok(Some(GlyphToRender {
pos: [x, y],
dim: [width as u16, height as u16],
uv: [atlas_x, atlas_y],
color: color.0,
content_type_with_srgb: [
content_type as u16,
match atlas.color_mode {
ColorMode::Accurate => TextColorConversion::ConvertToLinear,
ColorMode::Web => TextColorConversion::None,
} as u16,
],
depth,
}))
}