Skip to content

Commit b2129f1

Browse files
authored
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
1 parent ce6ede9 commit b2129f1

File tree

9 files changed

+1171
-220
lines changed

9 files changed

+1171
-220
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ rustc-hash = "2.0"
1717
[dev-dependencies]
1818
winit = "0.30.3"
1919
wgpu = "22"
20+
resvg = { version = "0.42", default-features = false }
2021
pollster = "0.3.0"

examples/custom-glyphs.rs

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
use glyphon::{
2+
Attrs, Buffer, Cache, Color, ContentType, CustomGlyph, RasterizationRequest, RasterizedCustomGlyph,
3+
Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, TextArea, TextAtlas, TextBounds,
4+
TextRenderer, Viewport,
5+
};
6+
use std::sync::Arc;
7+
use wgpu::{
8+
CommandEncoderDescriptor, CompositeAlphaMode, DeviceDescriptor, Instance, InstanceDescriptor,
9+
LoadOp, MultisampleState, Operations, PresentMode, RenderPassColorAttachment,
10+
RenderPassDescriptor, RequestAdapterOptions, SurfaceConfiguration, TextureFormat,
11+
TextureUsages, TextureViewDescriptor,
12+
};
13+
use winit::{dpi::LogicalSize, event::WindowEvent, event_loop::EventLoop, window::Window};
14+
15+
// Example SVG icons are from https://publicdomainvectors.org/
16+
static LION_SVG: &[u8] = include_bytes!("./lion.svg");
17+
static EAGLE_SVG: &[u8] = include_bytes!("./eagle.svg");
18+
19+
fn main() {
20+
let event_loop = EventLoop::new().unwrap();
21+
event_loop
22+
.run_app(&mut Application { window_state: None })
23+
.unwrap();
24+
}
25+
26+
struct WindowState {
27+
device: wgpu::Device,
28+
queue: wgpu::Queue,
29+
surface: wgpu::Surface<'static>,
30+
surface_config: SurfaceConfiguration,
31+
32+
font_system: FontSystem,
33+
swash_cache: SwashCache,
34+
viewport: glyphon::Viewport,
35+
atlas: glyphon::TextAtlas,
36+
text_renderer: glyphon::TextRenderer,
37+
text_buffer: glyphon::Buffer,
38+
39+
rasterize_svg: Box<dyn Fn(RasterizationRequest) -> Option<RasterizedCustomGlyph>>,
40+
41+
// Make sure that the winit window is last in the struct so that
42+
// it is dropped after the wgpu surface is dropped, otherwise the
43+
// program may crash when closed. This is probably a bug in wgpu.
44+
window: Arc<Window>,
45+
}
46+
47+
impl WindowState {
48+
async fn new(window: Arc<Window>) -> Self {
49+
let physical_size = window.inner_size();
50+
let scale_factor = window.scale_factor();
51+
52+
// Set up surface
53+
let instance = Instance::new(InstanceDescriptor::default());
54+
let adapter = instance
55+
.request_adapter(&RequestAdapterOptions::default())
56+
.await
57+
.unwrap();
58+
let (device, queue) = adapter
59+
.request_device(&DeviceDescriptor::default(), None)
60+
.await
61+
.unwrap();
62+
63+
let surface = instance
64+
.create_surface(window.clone())
65+
.expect("Create surface");
66+
let swapchain_format = TextureFormat::Bgra8UnormSrgb;
67+
let surface_config = SurfaceConfiguration {
68+
usage: TextureUsages::RENDER_ATTACHMENT,
69+
format: swapchain_format,
70+
width: physical_size.width,
71+
height: physical_size.height,
72+
present_mode: PresentMode::Fifo,
73+
alpha_mode: CompositeAlphaMode::Opaque,
74+
view_formats: vec![],
75+
desired_maximum_frame_latency: 2,
76+
};
77+
surface.configure(&device, &surface_config);
78+
79+
// Set up text renderer
80+
let mut font_system = FontSystem::new();
81+
let swash_cache = SwashCache::new();
82+
let cache = Cache::new(&device);
83+
let viewport = Viewport::new(&device, &cache);
84+
let mut atlas = TextAtlas::new(&device, &queue, &cache, swapchain_format);
85+
let text_renderer =
86+
TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None);
87+
let mut text_buffer = Buffer::new(&mut font_system, Metrics::new(30.0, 42.0));
88+
89+
let physical_width = (physical_size.width as f64 * scale_factor) as f32;
90+
let physical_height = (physical_size.height as f64 * scale_factor) as f32;
91+
92+
text_buffer.set_size(
93+
&mut font_system,
94+
Some(physical_width),
95+
Some(physical_height),
96+
);
97+
text_buffer.set_text(
98+
&mut font_system,
99+
"SVG icons! --->\n\nThe icons below should be partially clipped.",
100+
Attrs::new().family(Family::SansSerif),
101+
Shaping::Advanced,
102+
);
103+
text_buffer.shape_until_scroll(&mut font_system, false);
104+
105+
// Set up custom svg renderer
106+
let svg_0 = resvg::usvg::Tree::from_data(LION_SVG, &Default::default()).unwrap();
107+
let svg_1 = resvg::usvg::Tree::from_data(EAGLE_SVG, &Default::default()).unwrap();
108+
109+
let rasterize_svg = move |input: RasterizationRequest| -> Option<RasterizedCustomGlyph> {
110+
// Select the svg data based on the custom glyph ID.
111+
let (svg, content_type) = match input.id {
112+
0 => (&svg_0, ContentType::Mask),
113+
1 => (&svg_1, ContentType::Color),
114+
_ => return None,
115+
};
116+
117+
// Calculate the scale based on the "glyph size".
118+
let svg_size = svg.size();
119+
let scale_x = input.width as f32 / svg_size.width();
120+
let scale_y = input.height as f32 / svg_size.height();
121+
122+
let Some(mut pixmap) =
123+
resvg::tiny_skia::Pixmap::new(input.width as u32, input.height as u32)
124+
else {
125+
return None;
126+
};
127+
128+
let mut transform = resvg::usvg::Transform::from_scale(scale_x, scale_y);
129+
130+
// Offset the glyph by the subpixel amount.
131+
let offset_x = input.x_bin.as_float();
132+
let offset_y = input.y_bin.as_float();
133+
if offset_x != 0.0 || offset_y != 0.0 {
134+
transform = transform.post_translate(offset_x, offset_y);
135+
}
136+
137+
resvg::render(svg, transform, &mut pixmap.as_mut());
138+
139+
let data: Vec<u8> = if let ContentType::Mask = content_type {
140+
// Only use the alpha channel for symbolic icons.
141+
pixmap.data().iter().skip(3).step_by(4).copied().collect()
142+
} else {
143+
pixmap.data().to_vec()
144+
};
145+
146+
Some(RasterizedCustomGlyph { data, content_type })
147+
};
148+
149+
Self {
150+
device,
151+
queue,
152+
surface,
153+
surface_config,
154+
font_system,
155+
swash_cache,
156+
viewport,
157+
atlas,
158+
text_renderer,
159+
text_buffer,
160+
rasterize_svg: Box::new(rasterize_svg),
161+
window,
162+
}
163+
}
164+
}
165+
166+
struct Application {
167+
window_state: Option<WindowState>,
168+
}
169+
170+
impl winit::application::ApplicationHandler for Application {
171+
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
172+
if self.window_state.is_some() {
173+
return;
174+
}
175+
176+
// Set up window
177+
let (width, height) = (800, 600);
178+
let window_attributes = Window::default_attributes()
179+
.with_inner_size(LogicalSize::new(width as f64, height as f64))
180+
.with_title("glyphon hello world");
181+
let window = Arc::new(event_loop.create_window(window_attributes).unwrap());
182+
183+
self.window_state = Some(pollster::block_on(WindowState::new(window)));
184+
}
185+
186+
fn window_event(
187+
&mut self,
188+
event_loop: &winit::event_loop::ActiveEventLoop,
189+
_window_id: winit::window::WindowId,
190+
event: WindowEvent,
191+
) {
192+
let Some(state) = &mut self.window_state else {
193+
return;
194+
};
195+
196+
let WindowState {
197+
window,
198+
device,
199+
queue,
200+
surface,
201+
surface_config,
202+
font_system,
203+
swash_cache,
204+
viewport,
205+
atlas,
206+
text_renderer,
207+
text_buffer,
208+
rasterize_svg,
209+
..
210+
} = state;
211+
212+
match event {
213+
WindowEvent::Resized(size) => {
214+
surface_config.width = size.width;
215+
surface_config.height = size.height;
216+
surface.configure(&device, &surface_config);
217+
window.request_redraw();
218+
}
219+
WindowEvent::RedrawRequested => {
220+
viewport.update(
221+
&queue,
222+
Resolution {
223+
width: surface_config.width,
224+
height: surface_config.height,
225+
},
226+
);
227+
228+
text_renderer
229+
.prepare_with_custom(
230+
device,
231+
queue,
232+
font_system,
233+
atlas,
234+
viewport,
235+
[TextArea {
236+
buffer: &text_buffer,
237+
left: 10.0,
238+
top: 10.0,
239+
scale: 1.0,
240+
bounds: TextBounds {
241+
left: 0,
242+
top: 0,
243+
right: 650,
244+
bottom: 180,
245+
},
246+
default_color: Color::rgb(255, 255, 255),
247+
custom_glyphs: &[
248+
CustomGlyph {
249+
id: 0,
250+
left: 300.0,
251+
top: 5.0,
252+
width: 64.0,
253+
height: 64.0,
254+
color: Some(Color::rgb(200, 200, 255)),
255+
snap_to_physical_pixel: true,
256+
metadata: 0,
257+
},
258+
CustomGlyph {
259+
id: 1,
260+
left: 400.0,
261+
top: 5.0,
262+
width: 64.0,
263+
height: 64.0,
264+
color: None,
265+
snap_to_physical_pixel: true,
266+
metadata: 0,
267+
},
268+
CustomGlyph {
269+
id: 0,
270+
left: 300.0,
271+
top: 130.0,
272+
width: 64.0,
273+
height: 64.0,
274+
color: Some(Color::rgb(200, 255, 200)),
275+
snap_to_physical_pixel: true,
276+
metadata: 0,
277+
},
278+
CustomGlyph {
279+
id: 1,
280+
left: 400.0,
281+
top: 130.0,
282+
width: 64.0,
283+
height: 64.0,
284+
color: None,
285+
snap_to_physical_pixel: true,
286+
metadata: 0,
287+
},
288+
],
289+
}],
290+
swash_cache,
291+
rasterize_svg,
292+
)
293+
.unwrap();
294+
295+
let frame = surface.get_current_texture().unwrap();
296+
let view = frame.texture.create_view(&TextureViewDescriptor::default());
297+
let mut encoder =
298+
device.create_command_encoder(&CommandEncoderDescriptor { label: None });
299+
{
300+
let mut pass = encoder.begin_render_pass(&RenderPassDescriptor {
301+
label: None,
302+
color_attachments: &[Some(RenderPassColorAttachment {
303+
view: &view,
304+
resolve_target: None,
305+
ops: Operations {
306+
load: LoadOp::Clear(wgpu::Color {
307+
r: 0.02,
308+
g: 0.02,
309+
b: 0.02,
310+
a: 1.0,
311+
}),
312+
store: wgpu::StoreOp::Store,
313+
},
314+
})],
315+
depth_stencil_attachment: None,
316+
timestamp_writes: None,
317+
occlusion_query_set: None,
318+
});
319+
320+
text_renderer.render(&atlas, &viewport, &mut pass).unwrap();
321+
}
322+
323+
queue.submit(Some(encoder.finish()));
324+
frame.present();
325+
326+
atlas.trim();
327+
}
328+
WindowEvent::CloseRequested => event_loop.exit(),
329+
_ => {}
330+
}
331+
}
332+
}

0 commit comments

Comments
 (0)