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:
parent
ce6ede951c
commit
b2129f1765
9 changed files with 1173 additions and 222 deletions
332
examples/custom-glyphs.rs
Normal file
332
examples/custom-glyphs.rs
Normal 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
72
examples/eagle.svg
Normal 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 |
|
@ -186,6 +186,7 @@ impl winit::application::ApplicationHandler for Application {
|
|||
bottom: 160,
|
||||
},
|
||||
default_color: Color::rgb(255, 255, 255),
|
||||
custom_glyphs: &[],
|
||||
}],
|
||||
swash_cache,
|
||||
)
|
||||
|
|
169
examples/lion.svg
Normal file
169
examples/lion.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 23 KiB |
Loading…
Add table
Add a link
Reference in a new issue