diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e39b36..580c434 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,8 +14,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Build - run: cargo build --verbose + run: cargo build --all-targets --verbose - name: Run tests run: cargo test --verbose diff --git a/Cargo.toml b/Cargo.toml index 8b158ed..ea27abb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,21 @@ [package] name = "glyphon" description = "Fast, simple 2D text rendering for wgpu" -version = "0.5.0" +version = "0.6.0" edition = "2021" homepage = "https://github.com/grovesNL/glyphon.git" repository = "https://github.com/grovesNL/glyphon" license = "MIT OR Apache-2.0 OR Zlib" [dependencies] -wgpu = "0.19" +wgpu = { version = "22", default-features = false, features = ["wgsl"] } etagere = "0.2.10" -lru = "0.12.1" -cosmic-text = "0.11.2" +cosmic-text = "0.12" +lru = { version = "0.12.1", default-features = false } +rustc-hash = "2.0" [dev-dependencies] -winit = { version = "0.29.10", features = ["rwh_05"] } +winit = "0.30.3" +wgpu = "22" +resvg = { version = "0.42", default-features = false } pollster = "0.3.0" diff --git a/examples/custom-glyphs.rs b/examples/custom-glyphs.rs new file mode 100644 index 0000000..98f6616 --- /dev/null +++ b/examples/custom-glyphs.rs @@ -0,0 +1,330 @@ +use glyphon::{ + Attrs, Buffer, Cache, Color, ContentType, CustomGlyph, Family, FontSystem, Metrics, + RasterizeCustomGlyphRequest, RasterizedCustomGlyph, Resolution, Shaping, SwashCache, TextArea, + TextAtlas, TextBounds, TextRenderer, Viewport, +}; +use std::sync::Arc; +use wgpu::{ + CommandEncoderDescriptor, CompositeAlphaMode, DeviceDescriptor, Instance, InstanceDescriptor, + LoadOp, MultisampleState, Operations, PresentMode, RenderPassColorAttachment, + RenderPassDescriptor, RequestAdapterOptions, SurfaceConfiguration, TextureFormat, + TextureUsages, TextureViewDescriptor, +}; +use winit::{dpi::LogicalSize, event::WindowEvent, event_loop::EventLoop, window::Window}; + +// Example SVG icons are from https://publicdomainvectors.org/ +static LION_SVG: &[u8] = include_bytes!("./lion.svg"); +static EAGLE_SVG: &[u8] = include_bytes!("./eagle.svg"); + +fn main() { + let event_loop = EventLoop::new().unwrap(); + event_loop + .run_app(&mut Application { window_state: None }) + .unwrap(); +} + +struct WindowState { + device: wgpu::Device, + queue: wgpu::Queue, + surface: wgpu::Surface<'static>, + surface_config: SurfaceConfiguration, + font_system: FontSystem, + swash_cache: SwashCache, + viewport: glyphon::Viewport, + atlas: glyphon::TextAtlas, + text_renderer: glyphon::TextRenderer, + text_buffer: glyphon::Buffer, + rasterize_svg: Box Option>, + // Make sure that the winit window is last in the struct so that + // it is dropped after the wgpu surface is dropped, otherwise the + // program may crash when closed. This is probably a bug in wgpu. + window: Arc, +} + +impl WindowState { + async fn new(window: Arc) -> Self { + let physical_size = window.inner_size(); + let scale_factor = window.scale_factor(); + + // Set up surface + let instance = Instance::new(InstanceDescriptor::default()); + let adapter = instance + .request_adapter(&RequestAdapterOptions::default()) + .await + .unwrap(); + let (device, queue) = adapter + .request_device(&DeviceDescriptor::default(), None) + .await + .unwrap(); + + let surface = instance + .create_surface(window.clone()) + .expect("Create surface"); + let swapchain_format = TextureFormat::Bgra8UnormSrgb; + let surface_config = SurfaceConfiguration { + usage: TextureUsages::RENDER_ATTACHMENT, + format: swapchain_format, + width: physical_size.width, + height: physical_size.height, + present_mode: PresentMode::Fifo, + alpha_mode: CompositeAlphaMode::Opaque, + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + surface.configure(&device, &surface_config); + + // Set up text renderer + let mut font_system = FontSystem::new(); + let swash_cache = SwashCache::new(); + let cache = Cache::new(&device); + let viewport = Viewport::new(&device, &cache); + let mut atlas = TextAtlas::new(&device, &queue, &cache, swapchain_format); + let text_renderer = + TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None); + let mut text_buffer = Buffer::new(&mut font_system, Metrics::new(30.0, 42.0)); + + let physical_width = (physical_size.width as f64 * scale_factor) as f32; + let physical_height = (physical_size.height as f64 * scale_factor) as f32; + + text_buffer.set_size( + &mut font_system, + Some(physical_width), + Some(physical_height), + ); + text_buffer.set_text( + &mut font_system, + "SVG icons! --->\n\nThe icons below should be partially clipped.", + Attrs::new().family(Family::SansSerif), + Shaping::Advanced, + ); + text_buffer.shape_until_scroll(&mut font_system, false); + + // Set up custom svg renderer + let svg_0 = resvg::usvg::Tree::from_data(LION_SVG, &Default::default()).unwrap(); + let svg_1 = resvg::usvg::Tree::from_data(EAGLE_SVG, &Default::default()).unwrap(); + + let rasterize_svg = + move |input: RasterizeCustomGlyphRequest| -> Option { + // Select the svg data based on the custom glyph ID. + let (svg, content_type) = match input.id { + 0 => (&svg_0, ContentType::Mask), + 1 => (&svg_1, ContentType::Color), + _ => return None, + }; + + // Calculate the scale based on the "glyph size". + let svg_size = svg.size(); + let scale_x = input.width as f32 / svg_size.width(); + let scale_y = input.height as f32 / svg_size.height(); + + let Some(mut pixmap) = + resvg::tiny_skia::Pixmap::new(input.width as u32, input.height as u32) + else { + return None; + }; + + let mut transform = resvg::usvg::Transform::from_scale(scale_x, scale_y); + + // Offset the glyph by the subpixel amount. + let offset_x = input.x_bin.as_float(); + let offset_y = input.y_bin.as_float(); + if offset_x != 0.0 || offset_y != 0.0 { + transform = transform.post_translate(offset_x, offset_y); + } + + resvg::render(svg, transform, &mut pixmap.as_mut()); + + let data: Vec = if let ContentType::Mask = content_type { + // Only use the alpha channel for symbolic icons. + pixmap.data().iter().skip(3).step_by(4).copied().collect() + } else { + pixmap.data().to_vec() + }; + + Some(RasterizedCustomGlyph { data, content_type }) + }; + + Self { + device, + queue, + surface, + surface_config, + font_system, + swash_cache, + viewport, + atlas, + text_renderer, + text_buffer, + rasterize_svg: Box::new(rasterize_svg), + window, + } + } +} + +struct Application { + window_state: Option, +} + +impl winit::application::ApplicationHandler for Application { + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + if self.window_state.is_some() { + return; + } + + // Set up window + let (width, height) = (800, 600); + let window_attributes = Window::default_attributes() + .with_inner_size(LogicalSize::new(width as f64, height as f64)) + .with_title("glyphon hello world"); + let window = Arc::new(event_loop.create_window(window_attributes).unwrap()); + + self.window_state = Some(pollster::block_on(WindowState::new(window))); + } + + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + _window_id: winit::window::WindowId, + event: WindowEvent, + ) { + let Some(state) = &mut self.window_state else { + return; + }; + + let WindowState { + window, + device, + queue, + surface, + surface_config, + font_system, + swash_cache, + viewport, + atlas, + text_renderer, + text_buffer, + rasterize_svg, + .. + } = state; + + match event { + WindowEvent::Resized(size) => { + surface_config.width = size.width; + surface_config.height = size.height; + surface.configure(&device, &surface_config); + window.request_redraw(); + } + WindowEvent::RedrawRequested => { + viewport.update( + &queue, + Resolution { + width: surface_config.width, + height: surface_config.height, + }, + ); + + text_renderer + .prepare_with_custom( + device, + queue, + font_system, + atlas, + viewport, + [TextArea { + buffer: &text_buffer, + left: 10.0, + top: 10.0, + scale: 1.0, + bounds: TextBounds { + left: 0, + top: 0, + right: 650, + bottom: 180, + }, + default_color: Color::rgb(255, 255, 255), + custom_glyphs: &[ + CustomGlyph { + id: 0, + left: 300.0, + top: 5.0, + width: 64.0, + height: 64.0, + color: Some(Color::rgb(200, 200, 255)), + snap_to_physical_pixel: true, + metadata: 0, + }, + CustomGlyph { + id: 1, + left: 400.0, + top: 5.0, + width: 64.0, + height: 64.0, + color: None, + snap_to_physical_pixel: true, + metadata: 0, + }, + CustomGlyph { + id: 0, + left: 300.0, + top: 130.0, + width: 64.0, + height: 64.0, + color: Some(Color::rgb(200, 255, 200)), + snap_to_physical_pixel: true, + metadata: 0, + }, + CustomGlyph { + id: 1, + left: 400.0, + top: 130.0, + width: 64.0, + height: 64.0, + color: None, + snap_to_physical_pixel: true, + metadata: 0, + }, + ], + }], + swash_cache, + rasterize_svg, + ) + .unwrap(); + + let frame = surface.get_current_texture().unwrap(); + let view = frame.texture.create_view(&TextureViewDescriptor::default()); + let mut encoder = + device.create_command_encoder(&CommandEncoderDescriptor { label: None }); + { + let mut pass = encoder.begin_render_pass(&RenderPassDescriptor { + label: None, + color_attachments: &[Some(RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(wgpu::Color { + r: 0.02, + g: 0.02, + b: 0.02, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + text_renderer.render(&atlas, &viewport, &mut pass).unwrap(); + } + + queue.submit(Some(encoder.finish())); + frame.present(); + + atlas.trim(); + } + WindowEvent::CloseRequested => event_loop.exit(), + _ => {} + } + } +} diff --git a/examples/eagle.svg b/examples/eagle.svg new file mode 100644 index 0000000..53ad249 --- /dev/null +++ b/examples/eagle.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Openclipart + + + Eagle + 2007-01-24T06:25:54 + animal, animal, bird, bird, clip art, clipart, eagle, eagle, head, head, image, media, nature, nature, public domain, svg, + http://openclipart.org/detail/2962/eagle-by-nfroidure + + + nfroidure + + + + + animal + bird + clip art + clipart + eagle + head + image + media + nature + public domain + svg + + + + + + + + + + + diff --git a/examples/hello-world.rs b/examples/hello-world.rs index daafbb3..51c5c49 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -1,159 +1,227 @@ use glyphon::{ - Attrs, Buffer, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, TextArea, - TextAtlas, TextBounds, TextRenderer, + Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, + TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, }; -use wgpu::{ - CommandEncoderDescriptor, CompositeAlphaMode, DeviceDescriptor, Features, Instance, - InstanceDescriptor, Limits, LoadOp, MultisampleState, Operations, PresentMode, - RenderPassColorAttachment, RenderPassDescriptor, RequestAdapterOptions, SurfaceConfiguration, - TextureFormat, TextureUsages, TextureViewDescriptor, -}; -use winit::{ - dpi::LogicalSize, - event::{Event, WindowEvent}, - event_loop::EventLoop, - window::WindowBuilder, -}; - 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}; 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 hello world") - .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 { - label: None, - required_features: Features::empty(), - required_limits: Limits::downlevel_defaults(), - }, - 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 cache = SwashCache::new(); - let mut atlas = TextAtlas::new(&device, &queue, 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, "Hello world! 👋\nThis is rendered with 🦅 glyphon 🦁\nThe text below should be partially clipped.\na b c d e f g h i j k l m n o p q r s t u v w x y z", Attrs::new().family(Family::SansSerif), Shaping::Advanced); - buffer.shape_until_scroll(&mut font_system); - 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 => { - text_renderer - .prepare( - &device, - &queue, - &mut font_system, - &mut atlas, - Resolution { - width: config.width, - height: config.height, - }, - [TextArea { - buffer: &buffer, - left: 10.0, - top: 10.0, - scale: 1.0, - bounds: TextBounds { - left: 0, - top: 0, - right: 600, - bottom: 160, - }, - default_color: Color::rgb(255, 255, 255), - }], - &mut 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::BLACK), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - text_renderer.render(&atlas, &mut pass).unwrap(); - } - - queue.submit(Some(encoder.finish())); - frame.present(); - - atlas.trim(); - } - WindowEvent::CloseRequested => target.exit(), - _ => {} - } - } - }) + .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, + + // 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, "Hello world! 👋\nThis is rendered with 🦅 glyphon 🦁\nThe text below should be partially clipped.\na b c d e f g h i j k l m n o p q r s t u v w x y z", Attrs::new().family(Family::SansSerif), Shaping::Advanced); + text_buffer.shape_until_scroll(&mut font_system, false); + + Self { + device, + queue, + surface, + surface_config, + font_system, + swash_cache, + viewport, + atlas, + text_renderer, + text_buffer, + window, + } + } +} + +struct Application { + window_state: Option, +} + +impl winit::application::ApplicationHandler for Application { + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + if self.window_state.is_some() { + return; + } + + // Set up window + let (width, height) = (800, 600); + let window_attributes = Window::default_attributes() + .with_inner_size(LogicalSize::new(width as f64, height as f64)) + .with_title("glyphon hello world"); + let window = Arc::new(event_loop.create_window(window_attributes).unwrap()); + + self.window_state = Some(pollster::block_on(WindowState::new(window))); + } + + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + _window_id: winit::window::WindowId, + event: WindowEvent, + ) { + let Some(state) = &mut self.window_state else { + return; + }; + + let WindowState { + window, + device, + queue, + surface, + surface_config, + font_system, + swash_cache, + viewport, + atlas, + text_renderer, + text_buffer, + .. + } = 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( + 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: 600, + bottom: 160, + }, + default_color: Color::rgb(255, 255, 255), + custom_glyphs: &[], + }], + 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::BLACK), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + text_renderer.render(&atlas, &viewport, &mut pass).unwrap(); + } + + queue.submit(Some(encoder.finish())); + frame.present(); + + atlas.trim(); + } + WindowEvent::CloseRequested => event_loop.exit(), + _ => {} + } + } +} diff --git a/examples/lion.svg b/examples/lion.svg new file mode 100644 index 0000000..6fbde49 --- /dev/null +++ b/examples/lion.svg @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Openclipart + + + + + + + + + + + diff --git a/examples/text-sizes.rs b/examples/text-sizes.rs new file mode 100644 index 0000000..6296a75 --- /dev/null +++ b/examples/text-sizes.rs @@ -0,0 +1,304 @@ +use glyphon::{ + Attrs, Buffer, Cache, Color, ColorMode, Family, FontSystem, Metrics, Resolution, Shaping, + SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Weight, +}; +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, PhysicalSize}, + event::WindowEvent, + event_loop::EventLoop, + window::Window, +}; + +const TEXT: &str = "The quick brown fox jumped over the lazy doggo. 🐕"; +const WEIGHT: Weight = Weight::NORMAL; +const SIZES: [f32; 16] = [ + 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 18.0, 20.0, 22.0, 24.0, 28.0, 32.0, 48.0, +]; +const LINE_HEIGHT: f32 = 1.15; +const BG_COLOR: wgpu::Color = wgpu::Color::WHITE; +const FONT_COLOR: Color = Color::rgb(0, 0, 0); +//const BG_COLOR: wgpu::Color = wgpu::Color::BLACK; +//const FONT_COLOR: Color = Color::rgb(255, 255, 255); +const USE_WEB_COLORS: bool = true; + +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, + physical_size: PhysicalSize, + scale_factor: f32, + + font_system: FontSystem, + swash_cache: SwashCache, + viewport: glyphon::Viewport, + atlas: glyphon::TextAtlas, + text_renderer: glyphon::TextRenderer, + buffers: Vec, + + // 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() as f32; + + // 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 (color_mode, swapchain_format) = if USE_WEB_COLORS { + (ColorMode::Web, TextureFormat::Bgra8Unorm) + } else { + (ColorMode::Accurate, TextureFormat::Bgra8UnormSrgb) + }; + + let surface = instance + .create_surface(window.clone()) + .expect("Create surface"); + 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); + + let logical_width = physical_size.width as f32 / scale_factor; + + // 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::with_color_mode(&device, &queue, &cache, swapchain_format, color_mode); + let text_renderer = + TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None); + + let attrs = Attrs::new().family(Family::SansSerif).weight(WEIGHT); + let shaping = Shaping::Advanced; + + let buffers: Vec = SIZES + .iter() + .copied() + .map(|s| { + let mut text_buffer = + Buffer::new(&mut font_system, Metrics::relative(s, LINE_HEIGHT)); + + text_buffer.set_size(&mut font_system, Some(logical_width - 20.0), None); + + text_buffer.set_text( + &mut font_system, + &format!("size {s}: {TEXT}"), + attrs, + shaping, + ); + + text_buffer.shape_until_scroll(&mut font_system, false); + + text_buffer + }) + .collect(); + + Self { + device, + queue, + surface, + surface_config, + physical_size: physical_size.cast(), + scale_factor: scale_factor as f32, + font_system, + swash_cache, + viewport, + atlas, + text_renderer, + buffers, + window, + } + } +} + +struct Application { + window_state: Option, +} + +impl winit::application::ApplicationHandler for Application { + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + if self.window_state.is_some() { + return; + } + + // Set up window + let (width, height) = (800, 600); + let window_attributes = Window::default_attributes() + .with_inner_size(LogicalSize::new(width as f64, height as f64)) + .with_title("glyphon text sizes test"); + 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, + buffers, + scale_factor, + physical_size, + .. + } = state; + + match event { + WindowEvent::Resized(size) => { + surface_config.width = size.width; + surface_config.height = size.height; + surface.configure(&device, &surface_config); + window.request_redraw(); + + *scale_factor = window.scale_factor() as f32; + *physical_size = size.cast(); + + let logical_width = size.width as f32 / *scale_factor; + + for b in buffers.iter_mut() { + b.set_size(font_system, Some(logical_width - 20.0), None); + b.shape_until_scroll(font_system, false); + } + } + WindowEvent::RedrawRequested => { + viewport.update( + &queue, + Resolution { + width: surface_config.width, + height: surface_config.height, + }, + ); + + let scale_factor = *scale_factor; + + let left = 10.0 * scale_factor; + let mut top = 10.0 * scale_factor; + + let bounds_left = left.floor() as i32; + let bounds_right = physical_size.width - 10; + + let text_areas: Vec