Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for SVG icons #102

Merged
merged 22 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ homepage = "https://github.com/grovesNL/glyphon.git"
repository = "https://github.com/grovesNL/glyphon"
license = "MIT OR Apache-2.0 OR Zlib"

[features]
custom-glyphs = []
BillyDM marked this conversation as resolved.
Show resolved Hide resolved

[dependencies]
wgpu = { version = "22", default-features = false, features = ["wgsl"] }
etagere = "0.2.10"
Expand All @@ -17,4 +20,10 @@ rustc-hash = "2.0"
[dev-dependencies]
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"]
BillyDM marked this conversation as resolved.
Show resolved Hide resolved
331 changes: 331 additions & 0 deletions examples/custom-glyphs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
use glyphon::{
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::{
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(CustomGlyphInput) -> Option<CustomGlyphOutput>>,

// 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: CustomGlyphInput| -> Option<CustomGlyphOutput> {
// 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;
};

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<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(CustomGlyphOutput {
data,
width,
height,
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(
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,
},
})],
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(),
_ => {}
}
}
}
Loading