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