Compare commits
23 commits
01ab64704b
...
487b7bbe3c
Author | SHA1 | Date | |
---|---|---|---|
487b7bbe3c | |||
|
0115197751 | ||
|
e39fa92462 | ||
|
744f239841 | ||
|
692e23ca33 | ||
|
2daf883cad | ||
|
b2129f1765 | ||
|
ce6ede951c | ||
|
3dab1f2dc4 | ||
|
f82094703e | ||
|
719d69a1ee | ||
|
9def853042 | ||
|
47f4126f99 | ||
|
2a45708767 | ||
|
0fdbd90c55 | ||
|
b411ea71e7 | ||
|
5aed9e1477 | ||
|
670140e2a1 | ||
|
3e281d1828 | ||
|
4f24305ac5 | ||
|
c16b6eb957 | ||
|
f95e66f612 | ||
|
4700e54f16 |
14 changed files with 2115 additions and 735 deletions
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
@ -14,8 +14,8 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --verbose
|
run: cargo build --all-targets --verbose
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo test --verbose
|
run: cargo test --verbose
|
||||||
|
|
13
Cargo.toml
13
Cargo.toml
|
@ -1,18 +1,21 @@
|
||||||
[package]
|
[package]
|
||||||
name = "glyphon"
|
name = "glyphon"
|
||||||
description = "Fast, simple 2D text rendering for wgpu"
|
description = "Fast, simple 2D text rendering for wgpu"
|
||||||
version = "0.5.0"
|
version = "0.6.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
homepage = "https://github.com/grovesNL/glyphon.git"
|
homepage = "https://github.com/grovesNL/glyphon.git"
|
||||||
repository = "https://github.com/grovesNL/glyphon"
|
repository = "https://github.com/grovesNL/glyphon"
|
||||||
license = "MIT OR Apache-2.0 OR Zlib"
|
license = "MIT OR Apache-2.0 OR Zlib"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
wgpu = "0.19"
|
wgpu = { version = "22", default-features = false, features = ["wgsl"] }
|
||||||
etagere = "0.2.10"
|
etagere = "0.2.10"
|
||||||
cosmic-text = "0.10"
|
cosmic-text = "0.12"
|
||||||
lru = "0.12.1"
|
lru = { version = "0.12.1", default-features = false }
|
||||||
|
rustc-hash = "2.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[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"
|
pollster = "0.3.0"
|
||||||
|
|
330
examples/custom-glyphs.rs
Normal file
330
examples/custom-glyphs.rs
Normal file
|
@ -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<dyn Fn(RasterizeCustomGlyphRequest) -> 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: RasterizeCustomGlyphRequest| -> 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 |
|
@ -1,159 +1,227 @@
|
||||||
use glyphon::{
|
use glyphon::{
|
||||||
Attrs, Buffer, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, TextArea,
|
Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache,
|
||||||
TextAtlas, TextBounds, TextRenderer,
|
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 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() {
|
fn main() {
|
||||||
pollster::block_on(run());
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run() {
|
|
||||||
// Set up window
|
|
||||||
let (width, height) = (800, 600);
|
|
||||||
let event_loop = EventLoop::new().unwrap();
|
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
|
event_loop
|
||||||
.run(move |event, target| {
|
.run_app(&mut Application { window_state: None })
|
||||||
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(),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap();
|
.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<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, "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<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,
|
||||||
|
..
|
||||||
|
} = 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(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
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 |
304
examples/text-sizes.rs
Normal file
304
examples/text-sizes.rs
Normal file
|
@ -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<i32>,
|
||||||
|
scale_factor: f32,
|
||||||
|
|
||||||
|
font_system: FontSystem,
|
||||||
|
swash_cache: SwashCache,
|
||||||
|
viewport: glyphon::Viewport,
|
||||||
|
atlas: glyphon::TextAtlas,
|
||||||
|
text_renderer: glyphon::TextRenderer,
|
||||||
|
buffers: Vec<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<Window>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowState {
|
||||||
|
async fn new(window: Arc<Window>) -> 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<glyphon::Buffer> = 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<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 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<TextArea> = buffers
|
||||||
|
.iter()
|
||||||
|
.map(|b| {
|
||||||
|
let a = TextArea {
|
||||||
|
buffer: b,
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
scale: scale_factor,
|
||||||
|
bounds: TextBounds {
|
||||||
|
left: bounds_left,
|
||||||
|
top: top.floor() as i32,
|
||||||
|
right: bounds_right,
|
||||||
|
bottom: top.floor() as i32 + physical_size.height,
|
||||||
|
},
|
||||||
|
default_color: FONT_COLOR,
|
||||||
|
custom_glyphs: &[],
|
||||||
|
};
|
||||||
|
|
||||||
|
let total_lines = b
|
||||||
|
.layout_runs()
|
||||||
|
.fold(0usize, |total_lines, _| total_lines + 1);
|
||||||
|
|
||||||
|
top += (total_lines as f32 * b.metrics().line_height + 5.0) * scale_factor;
|
||||||
|
|
||||||
|
a
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
text_renderer
|
||||||
|
.prepare(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
font_system,
|
||||||
|
atlas,
|
||||||
|
viewport,
|
||||||
|
text_areas,
|
||||||
|
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(BG_COLOR),
|
||||||
|
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(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
254
src/cache.rs
Normal file
254
src/cache.rs
Normal file
|
@ -0,0 +1,254 @@
|
||||||
|
use crate::{GlyphToRender, Params};
|
||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
mem,
|
||||||
|
num::NonZeroU64,
|
||||||
|
ops::Deref,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
};
|
||||||
|
use wgpu::{
|
||||||
|
BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutEntry,
|
||||||
|
BindingResource, BindingType, BlendState, Buffer, BufferBindingType, ColorTargetState,
|
||||||
|
ColorWrites, DepthStencilState, Device, FilterMode, FragmentState, MultisampleState,
|
||||||
|
PipelineCompilationOptions, PipelineLayout, PipelineLayoutDescriptor, PrimitiveState,
|
||||||
|
PrimitiveTopology, RenderPipeline, RenderPipelineDescriptor, Sampler, SamplerBindingType,
|
||||||
|
SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages,
|
||||||
|
TextureFormat, TextureSampleType, TextureView, TextureViewDimension, VertexFormat, VertexState,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A cache to share common resources (e.g., pipelines, layouts, shaders) between multiple text
|
||||||
|
/// renderers.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Cache(Arc<Inner>);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Inner {
|
||||||
|
sampler: Sampler,
|
||||||
|
shader: ShaderModule,
|
||||||
|
vertex_buffers: [wgpu::VertexBufferLayout<'static>; 1],
|
||||||
|
atlas_layout: BindGroupLayout,
|
||||||
|
uniforms_layout: BindGroupLayout,
|
||||||
|
pipeline_layout: PipelineLayout,
|
||||||
|
cache: RwLock<
|
||||||
|
Vec<(
|
||||||
|
TextureFormat,
|
||||||
|
MultisampleState,
|
||||||
|
Option<DepthStencilState>,
|
||||||
|
Arc<RenderPipeline>,
|
||||||
|
)>,
|
||||||
|
>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cache {
|
||||||
|
/// Creates a new `Cache` with the given `device`.
|
||||||
|
pub fn new(device: &Device) -> Self {
|
||||||
|
let sampler = device.create_sampler(&SamplerDescriptor {
|
||||||
|
label: Some("glyphon sampler"),
|
||||||
|
min_filter: FilterMode::Nearest,
|
||||||
|
mag_filter: FilterMode::Nearest,
|
||||||
|
mipmap_filter: FilterMode::Nearest,
|
||||||
|
lod_min_clamp: 0f32,
|
||||||
|
lod_max_clamp: 0f32,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let shader = device.create_shader_module(ShaderModuleDescriptor {
|
||||||
|
label: Some("glyphon shader"),
|
||||||
|
source: ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))),
|
||||||
|
});
|
||||||
|
|
||||||
|
let vertex_buffer_layout = wgpu::VertexBufferLayout {
|
||||||
|
array_stride: mem::size_of::<GlyphToRender>() as wgpu::BufferAddress,
|
||||||
|
step_mode: wgpu::VertexStepMode::Instance,
|
||||||
|
attributes: &[
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
format: VertexFormat::Sint32x2,
|
||||||
|
offset: 0,
|
||||||
|
shader_location: 0,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
format: VertexFormat::Uint32,
|
||||||
|
offset: mem::size_of::<u32>() as u64 * 2,
|
||||||
|
shader_location: 1,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
format: VertexFormat::Uint32,
|
||||||
|
offset: mem::size_of::<u32>() as u64 * 3,
|
||||||
|
shader_location: 2,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
format: VertexFormat::Uint32,
|
||||||
|
offset: mem::size_of::<u32>() as u64 * 4,
|
||||||
|
shader_location: 3,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
format: VertexFormat::Uint32,
|
||||||
|
offset: mem::size_of::<u32>() as u64 * 5,
|
||||||
|
shader_location: 4,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
format: VertexFormat::Float32,
|
||||||
|
offset: mem::size_of::<u32>() as u64 * 6,
|
||||||
|
shader_location: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let atlas_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
entries: &[
|
||||||
|
BindGroupLayoutEntry {
|
||||||
|
binding: 0,
|
||||||
|
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
||||||
|
ty: BindingType::Texture {
|
||||||
|
multisampled: false,
|
||||||
|
view_dimension: TextureViewDimension::D2,
|
||||||
|
sample_type: TextureSampleType::Float { filterable: true },
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
BindGroupLayoutEntry {
|
||||||
|
binding: 1,
|
||||||
|
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
||||||
|
ty: BindingType::Texture {
|
||||||
|
multisampled: false,
|
||||||
|
view_dimension: TextureViewDimension::D2,
|
||||||
|
sample_type: TextureSampleType::Float { filterable: true },
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
BindGroupLayoutEntry {
|
||||||
|
binding: 2,
|
||||||
|
visibility: ShaderStages::FRAGMENT,
|
||||||
|
ty: BindingType::Sampler(SamplerBindingType::Filtering),
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
label: Some("glyphon atlas bind group layout"),
|
||||||
|
});
|
||||||
|
|
||||||
|
let uniforms_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
entries: &[BindGroupLayoutEntry {
|
||||||
|
binding: 0,
|
||||||
|
visibility: ShaderStages::VERTEX,
|
||||||
|
ty: BindingType::Buffer {
|
||||||
|
ty: BufferBindingType::Uniform,
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: NonZeroU64::new(mem::size_of::<Params>() as u64),
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
}],
|
||||||
|
label: Some("glyphon uniforms bind group layout"),
|
||||||
|
});
|
||||||
|
|
||||||
|
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||||
|
label: None,
|
||||||
|
bind_group_layouts: &[&atlas_layout, &uniforms_layout],
|
||||||
|
push_constant_ranges: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
Self(Arc::new(Inner {
|
||||||
|
sampler,
|
||||||
|
shader,
|
||||||
|
vertex_buffers: [vertex_buffer_layout],
|
||||||
|
uniforms_layout,
|
||||||
|
atlas_layout,
|
||||||
|
pipeline_layout,
|
||||||
|
cache: RwLock::new(Vec::new()),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn create_atlas_bind_group(
|
||||||
|
&self,
|
||||||
|
device: &Device,
|
||||||
|
color_atlas: &TextureView,
|
||||||
|
mask_atlas: &TextureView,
|
||||||
|
) -> BindGroup {
|
||||||
|
device.create_bind_group(&BindGroupDescriptor {
|
||||||
|
layout: &self.0.atlas_layout,
|
||||||
|
entries: &[
|
||||||
|
BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: BindingResource::TextureView(color_atlas),
|
||||||
|
},
|
||||||
|
BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: BindingResource::TextureView(mask_atlas),
|
||||||
|
},
|
||||||
|
BindGroupEntry {
|
||||||
|
binding: 2,
|
||||||
|
resource: BindingResource::Sampler(&self.0.sampler),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
label: Some("glyphon atlas bind group"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn create_uniforms_bind_group(&self, device: &Device, buffer: &Buffer) -> BindGroup {
|
||||||
|
device.create_bind_group(&BindGroupDescriptor {
|
||||||
|
layout: &self.0.uniforms_layout,
|
||||||
|
entries: &[BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: buffer.as_entire_binding(),
|
||||||
|
}],
|
||||||
|
label: Some("glyphon uniforms bind group"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_or_create_pipeline(
|
||||||
|
&self,
|
||||||
|
device: &Device,
|
||||||
|
format: TextureFormat,
|
||||||
|
multisample: MultisampleState,
|
||||||
|
depth_stencil: Option<DepthStencilState>,
|
||||||
|
) -> Arc<RenderPipeline> {
|
||||||
|
let Inner {
|
||||||
|
cache,
|
||||||
|
pipeline_layout,
|
||||||
|
shader,
|
||||||
|
vertex_buffers,
|
||||||
|
..
|
||||||
|
} = self.0.deref();
|
||||||
|
|
||||||
|
let mut cache = cache.write().expect("Write pipeline cache");
|
||||||
|
|
||||||
|
cache
|
||||||
|
.iter()
|
||||||
|
.find(|(fmt, ms, ds, _)| fmt == &format && ms == &multisample && ds == &depth_stencil)
|
||||||
|
.map(|(_, _, _, p)| Arc::clone(p))
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let pipeline = Arc::new(device.create_render_pipeline(&RenderPipelineDescriptor {
|
||||||
|
label: Some("glyphon pipeline"),
|
||||||
|
layout: Some(pipeline_layout),
|
||||||
|
vertex: VertexState {
|
||||||
|
module: shader,
|
||||||
|
entry_point: "vs_main",
|
||||||
|
buffers: vertex_buffers,
|
||||||
|
compilation_options: PipelineCompilationOptions::default(),
|
||||||
|
},
|
||||||
|
fragment: Some(FragmentState {
|
||||||
|
module: shader,
|
||||||
|
entry_point: "fs_main",
|
||||||
|
targets: &[Some(ColorTargetState {
|
||||||
|
format,
|
||||||
|
blend: Some(BlendState::ALPHA_BLENDING),
|
||||||
|
write_mask: ColorWrites::default(),
|
||||||
|
})],
|
||||||
|
compilation_options: PipelineCompilationOptions::default(),
|
||||||
|
}),
|
||||||
|
primitive: PrimitiveState {
|
||||||
|
topology: PrimitiveTopology::TriangleStrip,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: depth_stencil.clone(),
|
||||||
|
multisample,
|
||||||
|
multiview: None,
|
||||||
|
cache: None,
|
||||||
|
}));
|
||||||
|
|
||||||
|
cache.push((format, multisample, depth_stencil, pipeline.clone()));
|
||||||
|
|
||||||
|
pipeline
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
}
|
117
src/custom_glyph.rs
Normal file
117
src/custom_glyph.rs
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
use crate::Color;
|
||||||
|
use cosmic_text::SubpixelBin;
|
||||||
|
|
||||||
|
pub type CustomGlyphId = u16;
|
||||||
|
|
||||||
|
/// A custom glyph to render
|
||||||
|
#[derive(Default, Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct CustomGlyph {
|
||||||
|
/// The unique identifier for this glyph
|
||||||
|
pub id: CustomGlyphId,
|
||||||
|
/// The position of the left edge of the glyph
|
||||||
|
pub left: f32,
|
||||||
|
/// The position of the top edge of the glyph
|
||||||
|
pub top: f32,
|
||||||
|
/// The width of the glyph
|
||||||
|
pub width: f32,
|
||||||
|
/// The height of the glyph
|
||||||
|
pub height: f32,
|
||||||
|
/// The color of this glyph (only relevant if the glyph is rendered with the
|
||||||
|
/// type [`ContentType::Mask`])
|
||||||
|
///
|
||||||
|
/// Set to `None` to use [`crate::TextArea::default_color`].
|
||||||
|
pub color: Option<Color>,
|
||||||
|
/// If `true`, then this glyph will be snapped to the nearest whole physical
|
||||||
|
/// pixel and the resulting `SubpixelBin`'s in `RasterizationRequest` will always
|
||||||
|
/// be `Zero` (useful for images and other large glyphs).
|
||||||
|
pub snap_to_physical_pixel: bool,
|
||||||
|
/// Additional metadata about the glyph
|
||||||
|
pub metadata: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A request to rasterize a custom glyph
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct RasterizeCustomGlyphRequest {
|
||||||
|
/// The unique identifier of the glyph
|
||||||
|
pub id: CustomGlyphId,
|
||||||
|
/// The width of the glyph in physical pixels
|
||||||
|
pub width: u16,
|
||||||
|
/// The height of the glyph in physical pixels
|
||||||
|
pub height: u16,
|
||||||
|
/// Binning of fractional X offset
|
||||||
|
///
|
||||||
|
/// If `CustomGlyph::snap_to_physical_pixel` was set to `true`, then this
|
||||||
|
/// will always be `Zero`.
|
||||||
|
pub x_bin: SubpixelBin,
|
||||||
|
/// Binning of fractional Y offset
|
||||||
|
///
|
||||||
|
/// If `CustomGlyph::snap_to_physical_pixel` was set to `true`, then this
|
||||||
|
/// will always be `Zero`.
|
||||||
|
pub y_bin: SubpixelBin,
|
||||||
|
/// The scaling factor applied to the text area (Note that `width` and
|
||||||
|
/// `height` are already scaled by this factor.)
|
||||||
|
pub scale: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A rasterized custom glyph
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RasterizedCustomGlyph {
|
||||||
|
/// The raw image data
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
/// The type of image data contained in `data`
|
||||||
|
pub content_type: ContentType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RasterizedCustomGlyph {
|
||||||
|
pub(crate) fn validate(
|
||||||
|
&self,
|
||||||
|
input: &RasterizeCustomGlyphRequest,
|
||||||
|
expected_type: Option<ContentType>,
|
||||||
|
) {
|
||||||
|
if let Some(expected_type) = expected_type {
|
||||||
|
assert_eq!(self.content_type, expected_type, "Custom glyph rasterizer must always produce the same content type for a given input. Expected {:?}, got {:?}. Input: {:?}", expected_type, self.content_type, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
self.data.len(),
|
||||||
|
input.width as usize * input.height as usize * self.content_type.bytes_per_pixel(),
|
||||||
|
"Invalid custom glyph rasterizer output. Expected data of length {}, got length {}. Input: {:?}",
|
||||||
|
input.width as usize * input.height as usize * self.content_type.bytes_per_pixel(),
|
||||||
|
self.data.len(),
|
||||||
|
input,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct CustomGlyphCacheKey {
|
||||||
|
/// Font ID
|
||||||
|
pub glyph_id: CustomGlyphId,
|
||||||
|
/// Glyph width
|
||||||
|
pub width: u16,
|
||||||
|
/// Glyph height
|
||||||
|
pub height: u16,
|
||||||
|
/// Binning of fractional X offset
|
||||||
|
pub x_bin: SubpixelBin,
|
||||||
|
/// Binning of fractional Y offset
|
||||||
|
pub y_bin: SubpixelBin,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type of image data contained in a rasterized glyph
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
|
pub enum ContentType {
|
||||||
|
/// Each pixel contains 32 bits of rgba data
|
||||||
|
Color,
|
||||||
|
/// Each pixel contains a single 8 bit channel
|
||||||
|
Mask,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContentType {
|
||||||
|
/// The number of bytes per pixel for this content type
|
||||||
|
pub fn bytes_per_pixel(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::Color => 4,
|
||||||
|
Self::Mask => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
src/lib.rs
16
src/lib.rs
|
@ -4,15 +4,21 @@
|
||||||
//! [cosmic-text]: https://github.com/pop-os/cosmic-text
|
//! [cosmic-text]: https://github.com/pop-os/cosmic-text
|
||||||
//! [etagere]: https://github.com/nical/etagere
|
//! [etagere]: https://github.com/nical/etagere
|
||||||
|
|
||||||
|
mod cache;
|
||||||
|
mod custom_glyph;
|
||||||
mod error;
|
mod error;
|
||||||
mod text_atlas;
|
mod text_atlas;
|
||||||
mod text_render;
|
mod text_render;
|
||||||
|
mod viewport;
|
||||||
|
|
||||||
|
pub use cache::Cache;
|
||||||
|
pub use custom_glyph::{
|
||||||
|
ContentType, CustomGlyph, CustomGlyphId, RasterizeCustomGlyphRequest, RasterizedCustomGlyph,
|
||||||
|
};
|
||||||
pub use error::{PrepareError, RenderError};
|
pub use error::{PrepareError, RenderError};
|
||||||
pub use text_atlas::{ColorMode, TextAtlas};
|
pub use text_atlas::{ColorMode, TextAtlas};
|
||||||
pub use text_render::TextRenderer;
|
pub use text_render::TextRenderer;
|
||||||
|
pub use viewport::Viewport;
|
||||||
use text_render::ContentType;
|
|
||||||
|
|
||||||
// Re-export all top-level types from `cosmic-text` for convenience.
|
// Re-export all top-level types from `cosmic-text` for convenience.
|
||||||
#[doc(no_inline)]
|
#[doc(no_inline)]
|
||||||
|
@ -111,6 +117,10 @@ pub struct TextArea<'a> {
|
||||||
/// The visible bounds of the text area. This is used to clip the text and doesn't have to
|
/// The visible bounds of the text area. This is used to clip the text and doesn't have to
|
||||||
/// match the `left` and `top` values.
|
/// match the `left` and `top` values.
|
||||||
pub bounds: TextBounds,
|
pub bounds: TextBounds,
|
||||||
// The default color of the text area.
|
/// The default color of the text area.
|
||||||
pub default_color: Color,
|
pub default_color: Color,
|
||||||
|
/// The opacity of the text
|
||||||
|
pub opacity: f32,
|
||||||
|
/// Additional custom glyphs to render.
|
||||||
|
pub custom_glyphs: &'a [CustomGlyph],
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,17 +21,17 @@ struct Params {
|
||||||
};
|
};
|
||||||
|
|
||||||
@group(0) @binding(0)
|
@group(0) @binding(0)
|
||||||
var<uniform> params: Params;
|
|
||||||
|
|
||||||
@group(0) @binding(1)
|
|
||||||
var color_atlas_texture: texture_2d<f32>;
|
var color_atlas_texture: texture_2d<f32>;
|
||||||
|
|
||||||
@group(0) @binding(2)
|
@group(0) @binding(1)
|
||||||
var mask_atlas_texture: texture_2d<f32>;
|
var mask_atlas_texture: texture_2d<f32>;
|
||||||
|
|
||||||
@group(0) @binding(3)
|
@group(0) @binding(2)
|
||||||
var atlas_sampler: sampler;
|
var atlas_sampler: sampler;
|
||||||
|
|
||||||
|
@group(1) @binding(0)
|
||||||
|
var<uniform> params: Params;
|
||||||
|
|
||||||
fn srgb_to_linear(c: f32) -> f32 {
|
fn srgb_to_linear(c: f32) -> f32 {
|
||||||
if c <= 0.04045 {
|
if c <= 0.04045 {
|
||||||
return c / 12.92;
|
return c / 12.92;
|
||||||
|
@ -47,25 +47,17 @@ fn vs_main(in_vert: VertexInput) -> VertexOutput {
|
||||||
let height = (in_vert.dim & 0xffff0000u) >> 16u;
|
let height = (in_vert.dim & 0xffff0000u) >> 16u;
|
||||||
let color = in_vert.color;
|
let color = in_vert.color;
|
||||||
var uv = vec2<u32>(in_vert.uv & 0xffffu, (in_vert.uv & 0xffff0000u) >> 16u);
|
var uv = vec2<u32>(in_vert.uv & 0xffffu, (in_vert.uv & 0xffff0000u) >> 16u);
|
||||||
let v = in_vert.vertex_idx % 4u;
|
let v = in_vert.vertex_idx;
|
||||||
|
|
||||||
switch v {
|
let corner_position = vec2<u32>(
|
||||||
case 1u: {
|
in_vert.vertex_idx & 1u,
|
||||||
pos.x += i32(width);
|
(in_vert.vertex_idx >> 1u) & 1u,
|
||||||
uv.x += width;
|
);
|
||||||
}
|
|
||||||
case 2u: {
|
let corner_offset = vec2<u32>(width, height) * corner_position;
|
||||||
pos.x += i32(width);
|
|
||||||
pos.y += i32(height);
|
uv = uv + corner_offset;
|
||||||
uv.x += width;
|
pos = pos + vec2<i32>(corner_offset);
|
||||||
uv.y += height;
|
|
||||||
}
|
|
||||||
case 3u: {
|
|
||||||
pos.y += i32(height);
|
|
||||||
uv.y += height;
|
|
||||||
}
|
|
||||||
default: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
var vert_output: VertexOutput;
|
var vert_output: VertexOutput;
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,19 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
text_render::ContentType, CacheKey, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus,
|
text_render::GlyphonCacheKey, Cache, ContentType, RasterizeCustomGlyphRequest, FontSystem,
|
||||||
Params, Resolution, SwashCache,
|
GlyphDetails, GpuCacheStatus, RasterizedCustomGlyph, SwashCache,
|
||||||
};
|
};
|
||||||
use etagere::{size2, Allocation, BucketedAtlasAllocator};
|
use etagere::{size2, Allocation, BucketedAtlasAllocator};
|
||||||
use lru::LruCache;
|
use lru::LruCache;
|
||||||
use std::{borrow::Cow, collections::HashSet, mem::size_of, num::NonZeroU64, sync::Arc};
|
use rustc_hash::FxHasher;
|
||||||
|
use std::{collections::HashSet, hash::BuildHasherDefault, sync::Arc};
|
||||||
use wgpu::{
|
use wgpu::{
|
||||||
BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutEntry,
|
BindGroup, DepthStencilState, Device, Extent3d, ImageCopyTexture, ImageDataLayout,
|
||||||
BindingResource, BindingType, BlendState, Buffer, BufferBindingType, BufferDescriptor,
|
MultisampleState, Origin3d, Queue, RenderPipeline, Texture, TextureAspect, TextureDescriptor,
|
||||||
BufferUsages, ColorTargetState, ColorWrites, DepthStencilState, Device, Extent3d, FilterMode,
|
TextureDimension, TextureFormat, TextureUsages, TextureView, TextureViewDescriptor,
|
||||||
FragmentState, ImageCopyTexture, ImageDataLayout, MultisampleState, Origin3d, PipelineLayout,
|
|
||||||
PipelineLayoutDescriptor, PrimitiveState, Queue, RenderPipeline, RenderPipelineDescriptor,
|
|
||||||
Sampler, SamplerBindingType, SamplerDescriptor, ShaderModule, ShaderModuleDescriptor,
|
|
||||||
ShaderSource, ShaderStages, Texture, TextureAspect, TextureDescriptor, TextureDimension,
|
|
||||||
TextureFormat, TextureSampleType, TextureUsages, TextureView, TextureViewDescriptor,
|
|
||||||
TextureViewDimension, VertexFormat, VertexState,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Hasher = BuildHasherDefault<FxHasher>;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) struct InnerAtlas {
|
pub(crate) struct InnerAtlas {
|
||||||
pub kind: Kind,
|
pub kind: Kind,
|
||||||
|
@ -24,8 +21,8 @@ pub(crate) struct InnerAtlas {
|
||||||
pub texture_view: TextureView,
|
pub texture_view: TextureView,
|
||||||
pub packer: BucketedAtlasAllocator,
|
pub packer: BucketedAtlasAllocator,
|
||||||
pub size: u32,
|
pub size: u32,
|
||||||
pub glyph_cache: LruCache<CacheKey, GlyphDetails>,
|
pub glyph_cache: LruCache<GlyphonCacheKey, GlyphDetails, Hasher>,
|
||||||
pub glyphs_in_use: HashSet<CacheKey>,
|
pub glyphs_in_use: HashSet<GlyphonCacheKey, Hasher>,
|
||||||
pub max_texture_dimension_2d: u32,
|
pub max_texture_dimension_2d: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,8 +53,8 @@ impl InnerAtlas {
|
||||||
|
|
||||||
let texture_view = texture.create_view(&TextureViewDescriptor::default());
|
let texture_view = texture.create_view(&TextureViewDescriptor::default());
|
||||||
|
|
||||||
let glyph_cache = LruCache::unbounded();
|
let glyph_cache = LruCache::unbounded_with_hasher(Hasher::default());
|
||||||
let glyphs_in_use = HashSet::new();
|
let glyphs_in_use = HashSet::with_hasher(Hasher::default());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
kind,
|
kind,
|
||||||
|
@ -110,12 +107,12 @@ impl InnerAtlas {
|
||||||
self.kind.num_channels()
|
self.kind.num_channels()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn promote(&mut self, glyph: CacheKey) {
|
pub(crate) fn promote(&mut self, glyph: GlyphonCacheKey) {
|
||||||
self.glyph_cache.promote(&glyph);
|
self.glyph_cache.promote(&glyph);
|
||||||
self.glyphs_in_use.insert(glyph);
|
self.glyphs_in_use.insert(glyph);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn put(&mut self, glyph: CacheKey, details: GlyphDetails) {
|
pub(crate) fn put(&mut self, glyph: GlyphonCacheKey, details: GlyphDetails) {
|
||||||
self.glyph_cache.put(glyph, details);
|
self.glyph_cache.put(glyph, details);
|
||||||
self.glyphs_in_use.insert(glyph);
|
self.glyphs_in_use.insert(glyph);
|
||||||
}
|
}
|
||||||
|
@ -126,6 +123,10 @@ impl InnerAtlas {
|
||||||
queue: &wgpu::Queue,
|
queue: &wgpu::Queue,
|
||||||
font_system: &mut FontSystem,
|
font_system: &mut FontSystem,
|
||||||
cache: &mut SwashCache,
|
cache: &mut SwashCache,
|
||||||
|
scale_factor: f32,
|
||||||
|
mut rasterize_custom_glyph: impl FnMut(
|
||||||
|
RasterizeCustomGlyphRequest,
|
||||||
|
) -> Option<RasterizedCustomGlyph>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if self.size >= self.max_texture_dimension_2d {
|
if self.size >= self.max_texture_dimension_2d {
|
||||||
return false;
|
return false;
|
||||||
|
@ -161,10 +162,38 @@ impl InnerAtlas {
|
||||||
GpuCacheStatus::SkipRasterization => continue,
|
GpuCacheStatus::SkipRasterization => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
let image = cache.get_image_uncached(font_system, cache_key).unwrap();
|
let (image_data, width, height) = match cache_key {
|
||||||
|
GlyphonCacheKey::Text(cache_key) => {
|
||||||
|
let image = cache.get_image_uncached(font_system, cache_key).unwrap();
|
||||||
|
let width = image.placement.width as usize;
|
||||||
|
let height = image.placement.height as usize;
|
||||||
|
|
||||||
let width = image.placement.width as usize;
|
(image.data, width, height)
|
||||||
let height = image.placement.height as usize;
|
}
|
||||||
|
GlyphonCacheKey::Custom(cache_key) => {
|
||||||
|
let input = RasterizeCustomGlyphRequest {
|
||||||
|
id: cache_key.glyph_id,
|
||||||
|
width: cache_key.width,
|
||||||
|
height: cache_key.height,
|
||||||
|
x_bin: cache_key.x_bin,
|
||||||
|
y_bin: cache_key.y_bin,
|
||||||
|
scale: scale_factor,
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(rasterized_glyph) = (rasterize_custom_glyph)(input) else {
|
||||||
|
panic!("Custom glyph rasterizer returned `None` when it previously returned `Some` for the same input {:?}", &input);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sanity checks on the rasterizer output
|
||||||
|
rasterized_glyph.validate(&input, Some(self.kind.as_content_type()));
|
||||||
|
|
||||||
|
(
|
||||||
|
rasterized_glyph.data,
|
||||||
|
cache_key.width as usize,
|
||||||
|
cache_key.height as usize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
queue.write_texture(
|
queue.write_texture(
|
||||||
ImageCopyTexture {
|
ImageCopyTexture {
|
||||||
|
@ -177,7 +206,7 @@ impl InnerAtlas {
|
||||||
},
|
},
|
||||||
aspect: TextureAspect::All,
|
aspect: TextureAspect::All,
|
||||||
},
|
},
|
||||||
&image.data,
|
&image_data,
|
||||||
ImageDataLayout {
|
ImageDataLayout {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
bytes_per_row: Some(width as u32 * self.kind.num_channels() as u32),
|
bytes_per_row: Some(width as u32 * self.kind.num_channels() as u32),
|
||||||
|
@ -228,9 +257,16 @@ impl Kind {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_content_type(&self) -> ContentType {
|
||||||
|
match self {
|
||||||
|
Self::Mask => ContentType::Mask,
|
||||||
|
Self::Color { .. } => ContentType::Color,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The color mode of an [`Atlas`].
|
/// The color mode of a [`TextAtlas`].
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ColorMode {
|
pub enum ColorMode {
|
||||||
/// Accurate color management.
|
/// Accurate color management.
|
||||||
|
@ -255,148 +291,28 @@ pub enum ColorMode {
|
||||||
|
|
||||||
/// An atlas containing a cache of rasterized glyphs that can be rendered.
|
/// An atlas containing a cache of rasterized glyphs that can be rendered.
|
||||||
pub struct TextAtlas {
|
pub struct TextAtlas {
|
||||||
pub(crate) params: Params,
|
cache: Cache,
|
||||||
pub(crate) params_buffer: Buffer,
|
pub(crate) bind_group: BindGroup,
|
||||||
pub(crate) cached_pipelines: Vec<(
|
|
||||||
MultisampleState,
|
|
||||||
Option<DepthStencilState>,
|
|
||||||
Arc<RenderPipeline>,
|
|
||||||
)>,
|
|
||||||
pub(crate) bind_group: Arc<BindGroup>,
|
|
||||||
pub(crate) bind_group_layout: BindGroupLayout,
|
|
||||||
pub(crate) sampler: Sampler,
|
|
||||||
pub(crate) color_atlas: InnerAtlas,
|
pub(crate) color_atlas: InnerAtlas,
|
||||||
pub(crate) mask_atlas: InnerAtlas,
|
pub(crate) mask_atlas: InnerAtlas,
|
||||||
pub(crate) pipeline_layout: PipelineLayout,
|
|
||||||
pub(crate) shader: ShaderModule,
|
|
||||||
pub(crate) vertex_buffers: [wgpu::VertexBufferLayout<'static>; 1],
|
|
||||||
pub(crate) format: TextureFormat,
|
pub(crate) format: TextureFormat,
|
||||||
pub(crate) color_mode: ColorMode,
|
pub(crate) color_mode: ColorMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextAtlas {
|
impl TextAtlas {
|
||||||
/// Creates a new [`TextAtlas`].
|
/// Creates a new [`TextAtlas`].
|
||||||
pub fn new(device: &Device, queue: &Queue, format: TextureFormat) -> Self {
|
pub fn new(device: &Device, queue: &Queue, cache: &Cache, format: TextureFormat) -> Self {
|
||||||
Self::with_color_mode(device, queue, format, ColorMode::Accurate)
|
Self::with_color_mode(device, queue, cache, format, ColorMode::Accurate)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new [`TextAtlas`] with the given [`ColorMode`].
|
/// Creates a new [`TextAtlas`] with the given [`ColorMode`].
|
||||||
pub fn with_color_mode(
|
pub fn with_color_mode(
|
||||||
device: &Device,
|
device: &Device,
|
||||||
queue: &Queue,
|
queue: &Queue,
|
||||||
|
cache: &Cache,
|
||||||
format: TextureFormat,
|
format: TextureFormat,
|
||||||
color_mode: ColorMode,
|
color_mode: ColorMode,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let sampler = device.create_sampler(&SamplerDescriptor {
|
|
||||||
label: Some("glyphon sampler"),
|
|
||||||
min_filter: FilterMode::Nearest,
|
|
||||||
mag_filter: FilterMode::Nearest,
|
|
||||||
mipmap_filter: FilterMode::Nearest,
|
|
||||||
lod_min_clamp: 0f32,
|
|
||||||
lod_max_clamp: 0f32,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create a render pipeline to use for rendering later
|
|
||||||
let shader = device.create_shader_module(ShaderModuleDescriptor {
|
|
||||||
label: Some("glyphon shader"),
|
|
||||||
source: ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))),
|
|
||||||
});
|
|
||||||
|
|
||||||
let vertex_buffers = [wgpu::VertexBufferLayout {
|
|
||||||
array_stride: size_of::<GlyphToRender>() as wgpu::BufferAddress,
|
|
||||||
step_mode: wgpu::VertexStepMode::Vertex,
|
|
||||||
attributes: &[
|
|
||||||
wgpu::VertexAttribute {
|
|
||||||
format: VertexFormat::Sint32x2,
|
|
||||||
offset: 0,
|
|
||||||
shader_location: 0,
|
|
||||||
},
|
|
||||||
wgpu::VertexAttribute {
|
|
||||||
format: VertexFormat::Uint32,
|
|
||||||
offset: size_of::<u32>() as u64 * 2,
|
|
||||||
shader_location: 1,
|
|
||||||
},
|
|
||||||
wgpu::VertexAttribute {
|
|
||||||
format: VertexFormat::Uint32,
|
|
||||||
offset: size_of::<u32>() as u64 * 3,
|
|
||||||
shader_location: 2,
|
|
||||||
},
|
|
||||||
wgpu::VertexAttribute {
|
|
||||||
format: VertexFormat::Uint32,
|
|
||||||
offset: size_of::<u32>() as u64 * 4,
|
|
||||||
shader_location: 3,
|
|
||||||
},
|
|
||||||
wgpu::VertexAttribute {
|
|
||||||
format: VertexFormat::Uint32,
|
|
||||||
offset: size_of::<u32>() as u64 * 5,
|
|
||||||
shader_location: 4,
|
|
||||||
},
|
|
||||||
wgpu::VertexAttribute {
|
|
||||||
format: VertexFormat::Float32,
|
|
||||||
offset: size_of::<u32>() as u64 * 6,
|
|
||||||
shader_location: 5,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}];
|
|
||||||
|
|
||||||
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
||||||
entries: &[
|
|
||||||
BindGroupLayoutEntry {
|
|
||||||
binding: 0,
|
|
||||||
visibility: ShaderStages::VERTEX,
|
|
||||||
ty: BindingType::Buffer {
|
|
||||||
ty: BufferBindingType::Uniform,
|
|
||||||
has_dynamic_offset: false,
|
|
||||||
min_binding_size: NonZeroU64::new(size_of::<Params>() as u64),
|
|
||||||
},
|
|
||||||
count: None,
|
|
||||||
},
|
|
||||||
BindGroupLayoutEntry {
|
|
||||||
binding: 1,
|
|
||||||
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
|
||||||
ty: BindingType::Texture {
|
|
||||||
multisampled: false,
|
|
||||||
view_dimension: TextureViewDimension::D2,
|
|
||||||
sample_type: TextureSampleType::Float { filterable: true },
|
|
||||||
},
|
|
||||||
count: None,
|
|
||||||
},
|
|
||||||
BindGroupLayoutEntry {
|
|
||||||
binding: 2,
|
|
||||||
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
|
||||||
ty: BindingType::Texture {
|
|
||||||
multisampled: false,
|
|
||||||
view_dimension: TextureViewDimension::D2,
|
|
||||||
sample_type: TextureSampleType::Float { filterable: true },
|
|
||||||
},
|
|
||||||
count: None,
|
|
||||||
},
|
|
||||||
BindGroupLayoutEntry {
|
|
||||||
binding: 3,
|
|
||||||
visibility: ShaderStages::FRAGMENT,
|
|
||||||
ty: BindingType::Sampler(SamplerBindingType::Filtering),
|
|
||||||
count: None,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
label: Some("glyphon bind group layout"),
|
|
||||||
});
|
|
||||||
|
|
||||||
let params = Params {
|
|
||||||
screen_resolution: Resolution {
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
},
|
|
||||||
_pad: [0, 0],
|
|
||||||
};
|
|
||||||
|
|
||||||
let params_buffer = device.create_buffer(&BufferDescriptor {
|
|
||||||
label: Some("glyphon params"),
|
|
||||||
size: size_of::<Params>() as u64,
|
|
||||||
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
|
||||||
mapped_at_creation: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
let color_atlas = InnerAtlas::new(
|
let color_atlas = InnerAtlas::new(
|
||||||
device,
|
device,
|
||||||
queue,
|
queue,
|
||||||
|
@ -409,47 +325,17 @@ impl TextAtlas {
|
||||||
);
|
);
|
||||||
let mask_atlas = InnerAtlas::new(device, queue, Kind::Mask);
|
let mask_atlas = InnerAtlas::new(device, queue, Kind::Mask);
|
||||||
|
|
||||||
let bind_group = Arc::new(device.create_bind_group(&BindGroupDescriptor {
|
let bind_group = cache.create_atlas_bind_group(
|
||||||
layout: &bind_group_layout,
|
device,
|
||||||
entries: &[
|
&color_atlas.texture_view,
|
||||||
BindGroupEntry {
|
&mask_atlas.texture_view,
|
||||||
binding: 0,
|
);
|
||||||
resource: params_buffer.as_entire_binding(),
|
|
||||||
},
|
|
||||||
BindGroupEntry {
|
|
||||||
binding: 1,
|
|
||||||
resource: BindingResource::TextureView(&color_atlas.texture_view),
|
|
||||||
},
|
|
||||||
BindGroupEntry {
|
|
||||||
binding: 2,
|
|
||||||
resource: BindingResource::TextureView(&mask_atlas.texture_view),
|
|
||||||
},
|
|
||||||
BindGroupEntry {
|
|
||||||
binding: 3,
|
|
||||||
resource: BindingResource::Sampler(&sampler),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
label: Some("glyphon bind group"),
|
|
||||||
}));
|
|
||||||
|
|
||||||
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
|
||||||
label: None,
|
|
||||||
bind_group_layouts: &[&bind_group_layout],
|
|
||||||
push_constant_ranges: &[],
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
params,
|
cache: cache.clone(),
|
||||||
params_buffer,
|
|
||||||
cached_pipelines: Vec::new(),
|
|
||||||
bind_group,
|
bind_group,
|
||||||
bind_group_layout,
|
|
||||||
sampler,
|
|
||||||
color_atlas,
|
color_atlas,
|
||||||
mask_atlas,
|
mask_atlas,
|
||||||
pipeline_layout,
|
|
||||||
shader,
|
|
||||||
vertex_buffers,
|
|
||||||
format,
|
format,
|
||||||
color_mode,
|
color_mode,
|
||||||
}
|
}
|
||||||
|
@ -467,10 +353,28 @@ impl TextAtlas {
|
||||||
font_system: &mut FontSystem,
|
font_system: &mut FontSystem,
|
||||||
cache: &mut SwashCache,
|
cache: &mut SwashCache,
|
||||||
content_type: ContentType,
|
content_type: ContentType,
|
||||||
|
scale_factor: f32,
|
||||||
|
rasterize_custom_glyph: impl FnMut(
|
||||||
|
RasterizeCustomGlyphRequest,
|
||||||
|
) -> Option<RasterizedCustomGlyph>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let did_grow = match content_type {
|
let did_grow = match content_type {
|
||||||
ContentType::Mask => self.mask_atlas.grow(device, queue, font_system, cache),
|
ContentType::Mask => self.mask_atlas.grow(
|
||||||
ContentType::Color => self.color_atlas.grow(device, queue, font_system, cache),
|
device,
|
||||||
|
queue,
|
||||||
|
font_system,
|
||||||
|
cache,
|
||||||
|
scale_factor,
|
||||||
|
rasterize_custom_glyph,
|
||||||
|
),
|
||||||
|
ContentType::Color => self.color_atlas.grow(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
font_system,
|
||||||
|
cache,
|
||||||
|
scale_factor,
|
||||||
|
rasterize_custom_glyph,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
if did_grow {
|
if did_grow {
|
||||||
|
@ -480,7 +384,7 @@ impl TextAtlas {
|
||||||
did_grow
|
did_grow
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn glyph(&self, glyph: &CacheKey) -> Option<&GlyphDetails> {
|
pub(crate) fn glyph(&self, glyph: &GlyphonCacheKey) -> Option<&GlyphDetails> {
|
||||||
self.mask_atlas
|
self.mask_atlas
|
||||||
.glyph_cache
|
.glyph_cache
|
||||||
.peek(glyph)
|
.peek(glyph)
|
||||||
|
@ -495,67 +399,20 @@ impl TextAtlas {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_or_create_pipeline(
|
pub(crate) fn get_or_create_pipeline(
|
||||||
&mut self,
|
&self,
|
||||||
device: &Device,
|
device: &Device,
|
||||||
multisample: MultisampleState,
|
multisample: MultisampleState,
|
||||||
depth_stencil: Option<DepthStencilState>,
|
depth_stencil: Option<DepthStencilState>,
|
||||||
) -> Arc<RenderPipeline> {
|
) -> Arc<RenderPipeline> {
|
||||||
self.cached_pipelines
|
self.cache
|
||||||
.iter()
|
.get_or_create_pipeline(device, self.format, multisample, depth_stencil)
|
||||||
.find(|(ms, ds, _)| ms == &multisample && ds == &depth_stencil)
|
|
||||||
.map(|(_, _, p)| Arc::clone(p))
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
let pipeline = Arc::new(device.create_render_pipeline(&RenderPipelineDescriptor {
|
|
||||||
label: Some("glyphon pipeline"),
|
|
||||||
layout: Some(&self.pipeline_layout),
|
|
||||||
vertex: VertexState {
|
|
||||||
module: &self.shader,
|
|
||||||
entry_point: "vs_main",
|
|
||||||
buffers: &self.vertex_buffers,
|
|
||||||
},
|
|
||||||
fragment: Some(FragmentState {
|
|
||||||
module: &self.shader,
|
|
||||||
entry_point: "fs_main",
|
|
||||||
targets: &[Some(ColorTargetState {
|
|
||||||
format: self.format,
|
|
||||||
blend: Some(BlendState::ALPHA_BLENDING),
|
|
||||||
write_mask: ColorWrites::default(),
|
|
||||||
})],
|
|
||||||
}),
|
|
||||||
primitive: PrimitiveState::default(),
|
|
||||||
depth_stencil: depth_stencil.clone(),
|
|
||||||
multisample,
|
|
||||||
multiview: None,
|
|
||||||
}));
|
|
||||||
|
|
||||||
self.cached_pipelines
|
|
||||||
.push((multisample, depth_stencil, pipeline.clone()));
|
|
||||||
pipeline
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rebind(&mut self, device: &wgpu::Device) {
|
fn rebind(&mut self, device: &wgpu::Device) {
|
||||||
self.bind_group = Arc::new(device.create_bind_group(&BindGroupDescriptor {
|
self.bind_group = self.cache.create_atlas_bind_group(
|
||||||
layout: &self.bind_group_layout,
|
device,
|
||||||
entries: &[
|
&self.color_atlas.texture_view,
|
||||||
BindGroupEntry {
|
&self.mask_atlas.texture_view,
|
||||||
binding: 0,
|
);
|
||||||
resource: self.params_buffer.as_entire_binding(),
|
|
||||||
},
|
|
||||||
BindGroupEntry {
|
|
||||||
binding: 1,
|
|
||||||
resource: BindingResource::TextureView(&self.color_atlas.texture_view),
|
|
||||||
},
|
|
||||||
BindGroupEntry {
|
|
||||||
binding: 2,
|
|
||||||
resource: BindingResource::TextureView(&self.mask_atlas.texture_view),
|
|
||||||
},
|
|
||||||
BindGroupEntry {
|
|
||||||
binding: 3,
|
|
||||||
resource: BindingResource::Sampler(&self.sampler),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
label: Some("glyphon bind group"),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,22 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
ColorMode, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, Params, PrepareError,
|
custom_glyph::CustomGlyphCacheKey, ColorMode, ContentType, FontSystem, GlyphDetails,
|
||||||
RenderError, Resolution, SwashCache, SwashContent, TextArea, TextAtlas,
|
GlyphToRender, GpuCacheStatus, PrepareError, RasterizeCustomGlyphRequest,
|
||||||
|
RasterizedCustomGlyph, RenderError, SwashCache, SwashContent, TextArea, TextAtlas, Viewport,
|
||||||
};
|
};
|
||||||
use std::{iter, mem::size_of, slice, sync::Arc};
|
use cosmic_text::{Color, SubpixelBin};
|
||||||
|
use std::{slice, sync::Arc};
|
||||||
use wgpu::{
|
use wgpu::{
|
||||||
Buffer, BufferDescriptor, BufferUsages, DepthStencilState, Device, Extent3d, ImageCopyTexture,
|
Buffer, BufferDescriptor, BufferUsages, DepthStencilState, Device, Extent3d, ImageCopyTexture,
|
||||||
ImageDataLayout, IndexFormat, MultisampleState, Origin3d, Queue, RenderPass, RenderPipeline,
|
ImageDataLayout, MultisampleState, Origin3d, Queue, RenderPass, RenderPipeline, TextureAspect,
|
||||||
TextureAspect, COPY_BUFFER_ALIGNMENT,
|
COPY_BUFFER_ALIGNMENT,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A text renderer that uses cached glyphs to render text into an existing render pass.
|
/// A text renderer that uses cached glyphs to render text into an existing render pass.
|
||||||
pub struct TextRenderer {
|
pub struct TextRenderer {
|
||||||
vertex_buffer: Buffer,
|
vertex_buffer: Buffer,
|
||||||
vertex_buffer_size: u64,
|
vertex_buffer_size: u64,
|
||||||
index_buffer: Buffer,
|
|
||||||
index_buffer_size: u64,
|
|
||||||
vertices_to_render: u32,
|
|
||||||
screen_resolution: Resolution,
|
|
||||||
pipeline: Arc<RenderPipeline>,
|
pipeline: Arc<RenderPipeline>,
|
||||||
|
glyph_vertices: Vec<GlyphToRender>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextRenderer {
|
impl TextRenderer {
|
||||||
|
@ -36,30 +35,40 @@ impl TextRenderer {
|
||||||
mapped_at_creation: false,
|
mapped_at_creation: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
let index_buffer_size = next_copy_buffer_size(4096);
|
|
||||||
let index_buffer = device.create_buffer(&BufferDescriptor {
|
|
||||||
label: Some("glyphon indices"),
|
|
||||||
size: index_buffer_size,
|
|
||||||
usage: BufferUsages::INDEX | BufferUsages::COPY_DST,
|
|
||||||
mapped_at_creation: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
let pipeline = atlas.get_or_create_pipeline(device, multisample, depth_stencil);
|
let pipeline = atlas.get_or_create_pipeline(device, multisample, depth_stencil);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
vertex_buffer,
|
vertex_buffer,
|
||||||
vertex_buffer_size,
|
vertex_buffer_size,
|
||||||
index_buffer,
|
|
||||||
index_buffer_size,
|
|
||||||
vertices_to_render: 0,
|
|
||||||
screen_resolution: Resolution {
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
},
|
|
||||||
pipeline,
|
pipeline,
|
||||||
|
glyph_vertices: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prepares all of the provided text areas for rendering.
|
||||||
|
pub fn prepare<'a>(
|
||||||
|
&mut self,
|
||||||
|
device: &Device,
|
||||||
|
queue: &Queue,
|
||||||
|
font_system: &mut FontSystem,
|
||||||
|
atlas: &mut TextAtlas,
|
||||||
|
viewport: &Viewport,
|
||||||
|
text_areas: impl IntoIterator<Item = TextArea<'a>>,
|
||||||
|
cache: &mut SwashCache,
|
||||||
|
) -> Result<(), PrepareError> {
|
||||||
|
self.prepare_with_depth_and_custom(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
font_system,
|
||||||
|
atlas,
|
||||||
|
viewport,
|
||||||
|
text_areas,
|
||||||
|
cache,
|
||||||
|
zero_depth,
|
||||||
|
|_| None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Prepares all of the provided text areas for rendering.
|
/// Prepares all of the provided text areas for rendering.
|
||||||
pub fn prepare_with_depth<'a>(
|
pub fn prepare_with_depth<'a>(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -67,251 +76,240 @@ impl TextRenderer {
|
||||||
queue: &Queue,
|
queue: &Queue,
|
||||||
font_system: &mut FontSystem,
|
font_system: &mut FontSystem,
|
||||||
atlas: &mut TextAtlas,
|
atlas: &mut TextAtlas,
|
||||||
screen_resolution: Resolution,
|
viewport: &Viewport,
|
||||||
|
text_areas: impl IntoIterator<Item = TextArea<'a>>,
|
||||||
|
cache: &mut SwashCache,
|
||||||
|
metadata_to_depth: impl FnMut(usize) -> f32,
|
||||||
|
) -> Result<(), PrepareError> {
|
||||||
|
self.prepare_with_depth_and_custom(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
font_system,
|
||||||
|
atlas,
|
||||||
|
viewport,
|
||||||
|
text_areas,
|
||||||
|
cache,
|
||||||
|
metadata_to_depth,
|
||||||
|
|_| None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepares all of the provided text areas for rendering.
|
||||||
|
pub fn prepare_with_custom<'a>(
|
||||||
|
&mut self,
|
||||||
|
device: &Device,
|
||||||
|
queue: &Queue,
|
||||||
|
font_system: &mut FontSystem,
|
||||||
|
atlas: &mut TextAtlas,
|
||||||
|
viewport: &Viewport,
|
||||||
|
text_areas: impl IntoIterator<Item = TextArea<'a>>,
|
||||||
|
cache: &mut SwashCache,
|
||||||
|
rasterize_custom_glyph: impl FnMut(RasterizeCustomGlyphRequest) -> Option<RasterizedCustomGlyph>,
|
||||||
|
) -> Result<(), PrepareError> {
|
||||||
|
self.prepare_with_depth_and_custom(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
font_system,
|
||||||
|
atlas,
|
||||||
|
viewport,
|
||||||
|
text_areas,
|
||||||
|
cache,
|
||||||
|
zero_depth,
|
||||||
|
rasterize_custom_glyph,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepares all of the provided text areas for rendering.
|
||||||
|
pub fn prepare_with_depth_and_custom<'a>(
|
||||||
|
&mut self,
|
||||||
|
device: &Device,
|
||||||
|
queue: &Queue,
|
||||||
|
font_system: &mut FontSystem,
|
||||||
|
atlas: &mut TextAtlas,
|
||||||
|
viewport: &Viewport,
|
||||||
text_areas: impl IntoIterator<Item = TextArea<'a>>,
|
text_areas: impl IntoIterator<Item = TextArea<'a>>,
|
||||||
cache: &mut SwashCache,
|
cache: &mut SwashCache,
|
||||||
mut metadata_to_depth: impl FnMut(usize) -> f32,
|
mut metadata_to_depth: impl FnMut(usize) -> f32,
|
||||||
|
mut rasterize_custom_glyph: impl FnMut(
|
||||||
|
RasterizeCustomGlyphRequest,
|
||||||
|
) -> Option<RasterizedCustomGlyph>,
|
||||||
) -> Result<(), PrepareError> {
|
) -> Result<(), PrepareError> {
|
||||||
self.screen_resolution = screen_resolution;
|
self.glyph_vertices.clear();
|
||||||
|
|
||||||
let atlas_current_resolution = { atlas.params.screen_resolution };
|
let resolution = viewport.resolution();
|
||||||
|
|
||||||
if screen_resolution != atlas_current_resolution {
|
|
||||||
atlas.params.screen_resolution = screen_resolution;
|
|
||||||
queue.write_buffer(&atlas.params_buffer, 0, unsafe {
|
|
||||||
slice::from_raw_parts(
|
|
||||||
&atlas.params as *const Params as *const u8,
|
|
||||||
size_of::<Params>(),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut glyph_vertices: Vec<GlyphToRender> = Vec::new();
|
|
||||||
let mut glyph_indices: Vec<u32> = Vec::new();
|
|
||||||
let mut glyphs_added = 0;
|
|
||||||
|
|
||||||
for text_area in text_areas {
|
for text_area in text_areas {
|
||||||
for run in text_area.buffer.layout_runs() {
|
let bounds_min_x = text_area.bounds.left.max(0);
|
||||||
|
let bounds_min_y = text_area.bounds.top.max(0);
|
||||||
|
let bounds_max_x = text_area.bounds.right.min(resolution.width as i32);
|
||||||
|
let bounds_max_y = text_area.bounds.bottom.min(resolution.height as i32);
|
||||||
|
|
||||||
|
for glyph in text_area.custom_glyphs.iter() {
|
||||||
|
let x = text_area.left + (glyph.left * text_area.scale);
|
||||||
|
let y = text_area.top + (glyph.top * text_area.scale);
|
||||||
|
let width = (glyph.width * text_area.scale).round() as u16;
|
||||||
|
let height = (glyph.height * text_area.scale).round() as u16;
|
||||||
|
|
||||||
|
let (x, y, x_bin, y_bin) = if glyph.snap_to_physical_pixel {
|
||||||
|
(
|
||||||
|
x.round() as i32,
|
||||||
|
y.round() as i32,
|
||||||
|
SubpixelBin::Zero,
|
||||||
|
SubpixelBin::Zero,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let (x, x_bin) = SubpixelBin::new(x);
|
||||||
|
let (y, y_bin) = SubpixelBin::new(y);
|
||||||
|
(x, y, x_bin, y_bin)
|
||||||
|
};
|
||||||
|
|
||||||
|
let cache_key = GlyphonCacheKey::Custom(CustomGlyphCacheKey {
|
||||||
|
glyph_id: glyph.id,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
x_bin,
|
||||||
|
y_bin,
|
||||||
|
});
|
||||||
|
|
||||||
|
let color = glyph.color.unwrap_or(text_area.default_color);
|
||||||
|
|
||||||
|
if let Some(glyph_to_render) = prepare_glyph(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
0.0,
|
||||||
|
color,
|
||||||
|
glyph.metadata,
|
||||||
|
cache_key,
|
||||||
|
atlas,
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
cache,
|
||||||
|
font_system,
|
||||||
|
text_area.scale,
|
||||||
|
bounds_min_x,
|
||||||
|
bounds_min_y,
|
||||||
|
bounds_max_x,
|
||||||
|
bounds_max_y,
|
||||||
|
|_cache, _font_system, rasterize_custom_glyph| -> Option<GetGlyphImageResult> {
|
||||||
|
if width == 0 || height == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let input = RasterizeCustomGlyphRequest {
|
||||||
|
id: glyph.id,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
x_bin,
|
||||||
|
y_bin,
|
||||||
|
scale: text_area.scale,
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = (rasterize_custom_glyph)(input)?;
|
||||||
|
|
||||||
|
output.validate(&input, None);
|
||||||
|
|
||||||
|
Some(GetGlyphImageResult {
|
||||||
|
content_type: output.content_type,
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
data: output.data,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
&mut metadata_to_depth,
|
||||||
|
&mut rasterize_custom_glyph,
|
||||||
|
)? {
|
||||||
|
self.glyph_vertices.push(glyph_to_render);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_run_visible = |run: &cosmic_text::LayoutRun| {
|
||||||
|
let start_y = (text_area.top + run.line_top) as i32;
|
||||||
|
let end_y = (text_area.top + run.line_top + run.line_height) as i32;
|
||||||
|
|
||||||
|
start_y <= bounds_max_y && bounds_min_y <= end_y
|
||||||
|
};
|
||||||
|
|
||||||
|
let layout_runs = text_area
|
||||||
|
.buffer
|
||||||
|
.layout_runs()
|
||||||
|
.skip_while(|run| !is_run_visible(run))
|
||||||
|
.take_while(is_run_visible);
|
||||||
|
|
||||||
|
for run in layout_runs {
|
||||||
for glyph in run.glyphs.iter() {
|
for glyph in run.glyphs.iter() {
|
||||||
let physical_glyph =
|
let physical_glyph =
|
||||||
glyph.physical((text_area.left, text_area.top), text_area.scale);
|
glyph.physical((text_area.left, text_area.top), text_area.scale);
|
||||||
|
|
||||||
if atlas
|
|
||||||
.mask_atlas
|
|
||||||
.glyph_cache
|
|
||||||
.contains(&physical_glyph.cache_key)
|
|
||||||
{
|
|
||||||
atlas.mask_atlas.promote(physical_glyph.cache_key);
|
|
||||||
} else if atlas
|
|
||||||
.color_atlas
|
|
||||||
.glyph_cache
|
|
||||||
.contains(&physical_glyph.cache_key)
|
|
||||||
{
|
|
||||||
atlas.color_atlas.promote(physical_glyph.cache_key);
|
|
||||||
} else {
|
|
||||||
let Some(image) =
|
|
||||||
cache.get_image_uncached(font_system, physical_glyph.cache_key)
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let content_type = match image.content {
|
|
||||||
SwashContent::Color => ContentType::Color,
|
|
||||||
SwashContent::Mask => ContentType::Mask,
|
|
||||||
SwashContent::SubpixelMask => {
|
|
||||||
// Not implemented yet, but don't panic if this happens.
|
|
||||||
ContentType::Mask
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let width = image.placement.width as usize;
|
|
||||||
let height = image.placement.height as usize;
|
|
||||||
|
|
||||||
let should_rasterize = width > 0 && height > 0;
|
|
||||||
|
|
||||||
let (gpu_cache, atlas_id, inner) = if should_rasterize {
|
|
||||||
let mut inner = atlas.inner_for_content_mut(content_type);
|
|
||||||
|
|
||||||
// Find a position in the packer
|
|
||||||
let allocation = loop {
|
|
||||||
match inner.try_allocate(width, height) {
|
|
||||||
Some(a) => break a,
|
|
||||||
None => {
|
|
||||||
if !atlas.grow(
|
|
||||||
device,
|
|
||||||
queue,
|
|
||||||
font_system,
|
|
||||||
cache,
|
|
||||||
content_type,
|
|
||||||
) {
|
|
||||||
return Err(PrepareError::AtlasFull);
|
|
||||||
}
|
|
||||||
|
|
||||||
inner = atlas.inner_for_content_mut(content_type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let atlas_min = allocation.rectangle.min;
|
|
||||||
|
|
||||||
queue.write_texture(
|
|
||||||
ImageCopyTexture {
|
|
||||||
texture: &inner.texture,
|
|
||||||
mip_level: 0,
|
|
||||||
origin: Origin3d {
|
|
||||||
x: atlas_min.x as u32,
|
|
||||||
y: atlas_min.y as u32,
|
|
||||||
z: 0,
|
|
||||||
},
|
|
||||||
aspect: TextureAspect::All,
|
|
||||||
},
|
|
||||||
&image.data,
|
|
||||||
ImageDataLayout {
|
|
||||||
offset: 0,
|
|
||||||
bytes_per_row: Some(width as u32 * inner.num_channels() as u32),
|
|
||||||
rows_per_image: None,
|
|
||||||
},
|
|
||||||
Extent3d {
|
|
||||||
width: width as u32,
|
|
||||||
height: height as u32,
|
|
||||||
depth_or_array_layers: 1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
(
|
|
||||||
GpuCacheStatus::InAtlas {
|
|
||||||
x: atlas_min.x as u16,
|
|
||||||
y: atlas_min.y as u16,
|
|
||||||
content_type,
|
|
||||||
},
|
|
||||||
Some(allocation.id),
|
|
||||||
inner,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
let inner = &mut atlas.color_atlas;
|
|
||||||
(GpuCacheStatus::SkipRasterization, None, inner)
|
|
||||||
};
|
|
||||||
|
|
||||||
inner.put(
|
|
||||||
physical_glyph.cache_key,
|
|
||||||
GlyphDetails {
|
|
||||||
width: width as u16,
|
|
||||||
height: height as u16,
|
|
||||||
gpu_cache,
|
|
||||||
atlas_id,
|
|
||||||
top: image.placement.top as i16,
|
|
||||||
left: image.placement.left as i16,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let details = atlas.glyph(&physical_glyph.cache_key).unwrap();
|
|
||||||
|
|
||||||
let mut x = physical_glyph.x + details.left as i32;
|
|
||||||
let mut y = (run.line_y * text_area.scale).round() as i32 + physical_glyph.y
|
|
||||||
- details.top as i32;
|
|
||||||
|
|
||||||
let (mut atlas_x, mut atlas_y, content_type) = match details.gpu_cache {
|
|
||||||
GpuCacheStatus::InAtlas { x, y, content_type } => (x, y, content_type),
|
|
||||||
GpuCacheStatus::SkipRasterization => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut width = details.width as i32;
|
|
||||||
let mut height = details.height as i32;
|
|
||||||
|
|
||||||
let bounds_min_x = text_area.bounds.left.max(0);
|
|
||||||
let bounds_min_y = text_area.bounds.top.max(0);
|
|
||||||
let bounds_max_x = text_area.bounds.right.min(screen_resolution.width as i32);
|
|
||||||
let bounds_max_y = text_area.bounds.bottom.min(screen_resolution.height as i32);
|
|
||||||
|
|
||||||
// Starts beyond right edge or ends beyond left edge
|
|
||||||
let max_x = x + width;
|
|
||||||
if x > bounds_max_x || max_x < bounds_min_x {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Starts beyond bottom edge or ends beyond top edge
|
|
||||||
let max_y = y + height;
|
|
||||||
if y > bounds_max_y || max_y < bounds_min_y {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clip left ege
|
|
||||||
if x < bounds_min_x {
|
|
||||||
let right_shift = bounds_min_x - x;
|
|
||||||
|
|
||||||
x = bounds_min_x;
|
|
||||||
width = max_x - bounds_min_x;
|
|
||||||
atlas_x += right_shift as u16;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clip right edge
|
|
||||||
if x + width > bounds_max_x {
|
|
||||||
width = bounds_max_x - x;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clip top edge
|
|
||||||
if y < bounds_min_y {
|
|
||||||
let bottom_shift = bounds_min_y - y;
|
|
||||||
|
|
||||||
y = bounds_min_y;
|
|
||||||
height = max_y - bounds_min_y;
|
|
||||||
atlas_y += bottom_shift as u16;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clip bottom edge
|
|
||||||
if y + height > bounds_max_y {
|
|
||||||
height = bounds_max_y - y;
|
|
||||||
}
|
|
||||||
|
|
||||||
let color = match glyph.color_opt {
|
let color = match glyph.color_opt {
|
||||||
Some(some) => some,
|
Some(some) => some,
|
||||||
None => text_area.default_color,
|
None => text_area.default_color,
|
||||||
};
|
};
|
||||||
|
let color = Color::rgba(
|
||||||
let depth = metadata_to_depth(glyph.metadata);
|
(color.r() as f32 * text_area.opacity + 0.5) as u8,
|
||||||
|
(color.g() as f32 * text_area.opacity + 0.5) as u8,
|
||||||
glyph_vertices.extend(
|
(color.b() as f32 * text_area.opacity + 0.5) as u8,
|
||||||
iter::repeat(GlyphToRender {
|
(color.a() as f32 * text_area.opacity + 0.5) as u8,
|
||||||
pos: [x, y],
|
|
||||||
dim: [width as u16, height as u16],
|
|
||||||
uv: [atlas_x, atlas_y],
|
|
||||||
color: color.0,
|
|
||||||
content_type_with_srgb: [
|
|
||||||
content_type as u16,
|
|
||||||
match atlas.color_mode {
|
|
||||||
ColorMode::Accurate => TextColorConversion::ConvertToLinear,
|
|
||||||
ColorMode::Web => TextColorConversion::None,
|
|
||||||
} as u16,
|
|
||||||
],
|
|
||||||
depth,
|
|
||||||
})
|
|
||||||
.take(4),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let start = 4 * glyphs_added as u32;
|
if let Some(glyph_to_render) = prepare_glyph(
|
||||||
glyph_indices.extend([
|
physical_glyph.x,
|
||||||
start,
|
physical_glyph.y,
|
||||||
start + 1,
|
run.line_y,
|
||||||
start + 2,
|
color,
|
||||||
start,
|
glyph.metadata,
|
||||||
start + 2,
|
GlyphonCacheKey::Text(physical_glyph.cache_key),
|
||||||
start + 3,
|
atlas,
|
||||||
]);
|
device,
|
||||||
|
queue,
|
||||||
|
cache,
|
||||||
|
font_system,
|
||||||
|
text_area.scale,
|
||||||
|
bounds_min_x,
|
||||||
|
bounds_min_y,
|
||||||
|
bounds_max_x,
|
||||||
|
bounds_max_y,
|
||||||
|
|cache,
|
||||||
|
font_system,
|
||||||
|
_rasterize_custom_glyph|
|
||||||
|
-> Option<GetGlyphImageResult> {
|
||||||
|
let image =
|
||||||
|
cache.get_image_uncached(font_system, physical_glyph.cache_key)?;
|
||||||
|
|
||||||
glyphs_added += 1;
|
let content_type = match image.content {
|
||||||
|
SwashContent::Color => ContentType::Color,
|
||||||
|
SwashContent::Mask => ContentType::Mask,
|
||||||
|
SwashContent::SubpixelMask => {
|
||||||
|
// Not implemented yet, but don't panic if this happens.
|
||||||
|
ContentType::Mask
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(GetGlyphImageResult {
|
||||||
|
content_type,
|
||||||
|
top: image.placement.top as i16,
|
||||||
|
left: image.placement.left as i16,
|
||||||
|
width: image.placement.width as u16,
|
||||||
|
height: image.placement.height as u16,
|
||||||
|
data: image.data,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
&mut metadata_to_depth,
|
||||||
|
&mut rasterize_custom_glyph,
|
||||||
|
)? {
|
||||||
|
self.glyph_vertices.push(glyph_to_render);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const VERTICES_PER_GLYPH: u32 = 6;
|
let will_render = !self.glyph_vertices.is_empty();
|
||||||
self.vertices_to_render = glyphs_added as u32 * VERTICES_PER_GLYPH;
|
|
||||||
|
|
||||||
let will_render = glyphs_added > 0;
|
|
||||||
if !will_render {
|
if !will_render {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let vertices = glyph_vertices.as_slice();
|
let vertices = self.glyph_vertices.as_slice();
|
||||||
let vertices_raw = unsafe {
|
let vertices_raw = unsafe {
|
||||||
slice::from_raw_parts(
|
slice::from_raw_parts(
|
||||||
vertices as *const _ as *const u8,
|
vertices as *const _ as *const u8,
|
||||||
|
@ -335,89 +333,30 @@ impl TextRenderer {
|
||||||
self.vertex_buffer_size = buffer_size;
|
self.vertex_buffer_size = buffer_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
let indices = glyph_indices.as_slice();
|
|
||||||
let indices_raw = unsafe {
|
|
||||||
slice::from_raw_parts(
|
|
||||||
indices as *const _ as *const u8,
|
|
||||||
std::mem::size_of_val(indices),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.index_buffer_size >= indices_raw.len() as u64 {
|
|
||||||
queue.write_buffer(&self.index_buffer, 0, indices_raw);
|
|
||||||
} else {
|
|
||||||
self.index_buffer.destroy();
|
|
||||||
|
|
||||||
let (buffer, buffer_size) = create_oversized_buffer(
|
|
||||||
device,
|
|
||||||
Some("glyphon indices"),
|
|
||||||
indices_raw,
|
|
||||||
BufferUsages::INDEX | BufferUsages::COPY_DST,
|
|
||||||
);
|
|
||||||
|
|
||||||
self.index_buffer = buffer;
|
|
||||||
self.index_buffer_size = buffer_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prepare<'a>(
|
|
||||||
&mut self,
|
|
||||||
device: &Device,
|
|
||||||
queue: &Queue,
|
|
||||||
font_system: &mut FontSystem,
|
|
||||||
atlas: &mut TextAtlas,
|
|
||||||
screen_resolution: Resolution,
|
|
||||||
text_areas: impl IntoIterator<Item = TextArea<'a>>,
|
|
||||||
cache: &mut SwashCache,
|
|
||||||
) -> Result<(), PrepareError> {
|
|
||||||
self.prepare_with_depth(
|
|
||||||
device,
|
|
||||||
queue,
|
|
||||||
font_system,
|
|
||||||
atlas,
|
|
||||||
screen_resolution,
|
|
||||||
text_areas,
|
|
||||||
cache,
|
|
||||||
zero_depth,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders all layouts that were previously provided to `prepare`.
|
/// Renders all layouts that were previously provided to `prepare`.
|
||||||
pub fn render<'pass>(
|
pub fn render(
|
||||||
&'pass self,
|
&self,
|
||||||
atlas: &'pass TextAtlas,
|
atlas: &TextAtlas,
|
||||||
pass: &mut RenderPass<'pass>,
|
viewport: &Viewport,
|
||||||
|
pass: &mut RenderPass<'_>,
|
||||||
) -> Result<(), RenderError> {
|
) -> Result<(), RenderError> {
|
||||||
if self.vertices_to_render == 0 {
|
if self.glyph_vertices.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
// Validate that screen resolution hasn't changed since `prepare`
|
|
||||||
if self.screen_resolution != atlas.params.screen_resolution {
|
|
||||||
return Err(RenderError::ScreenResolutionChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pass.set_pipeline(&self.pipeline);
|
pass.set_pipeline(&self.pipeline);
|
||||||
pass.set_bind_group(0, &atlas.bind_group, &[]);
|
pass.set_bind_group(0, &atlas.bind_group, &[]);
|
||||||
|
pass.set_bind_group(1, &viewport.bind_group, &[]);
|
||||||
pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
|
pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
|
||||||
pass.set_index_buffer(self.index_buffer.slice(..), IndexFormat::Uint32);
|
pass.draw(0..4, 0..self.glyph_vertices.len() as u32);
|
||||||
pass.draw_indexed(0..self.vertices_to_render, 0, 0..1);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(u16)]
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
|
||||||
pub enum ContentType {
|
|
||||||
Color = 0,
|
|
||||||
Mask = 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(u16)]
|
#[repr(u16)]
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
enum TextColorConversion {
|
enum TextColorConversion {
|
||||||
|
@ -425,6 +364,12 @@ enum TextColorConversion {
|
||||||
ConvertToLinear = 1,
|
ConvertToLinear = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub(crate) enum GlyphonCacheKey {
|
||||||
|
Text(cosmic_text::CacheKey),
|
||||||
|
Custom(CustomGlyphCacheKey),
|
||||||
|
}
|
||||||
|
|
||||||
fn next_copy_buffer_size(size: u64) -> u64 {
|
fn next_copy_buffer_size(size: u64) -> u64 {
|
||||||
let align_mask = COPY_BUFFER_ALIGNMENT - 1;
|
let align_mask = COPY_BUFFER_ALIGNMENT - 1;
|
||||||
((size.next_power_of_two() + align_mask) & !align_mask).max(COPY_BUFFER_ALIGNMENT)
|
((size.next_power_of_two() + align_mask) & !align_mask).max(COPY_BUFFER_ALIGNMENT)
|
||||||
|
@ -451,3 +396,199 @@ fn create_oversized_buffer(
|
||||||
fn zero_depth(_: usize) -> f32 {
|
fn zero_depth(_: usize) -> f32 {
|
||||||
0f32
|
0f32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct GetGlyphImageResult {
|
||||||
|
content_type: ContentType,
|
||||||
|
top: i16,
|
||||||
|
left: i16,
|
||||||
|
width: u16,
|
||||||
|
height: u16,
|
||||||
|
data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_glyph<R>(
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
line_y: f32,
|
||||||
|
color: Color,
|
||||||
|
metadata: usize,
|
||||||
|
cache_key: GlyphonCacheKey,
|
||||||
|
atlas: &mut TextAtlas,
|
||||||
|
device: &Device,
|
||||||
|
queue: &Queue,
|
||||||
|
cache: &mut SwashCache,
|
||||||
|
font_system: &mut FontSystem,
|
||||||
|
scale_factor: f32,
|
||||||
|
bounds_min_x: i32,
|
||||||
|
bounds_min_y: i32,
|
||||||
|
bounds_max_x: i32,
|
||||||
|
bounds_max_y: i32,
|
||||||
|
get_glyph_image: impl FnOnce(
|
||||||
|
&mut SwashCache,
|
||||||
|
&mut FontSystem,
|
||||||
|
&mut R,
|
||||||
|
) -> Option<GetGlyphImageResult>,
|
||||||
|
mut metadata_to_depth: impl FnMut(usize) -> f32,
|
||||||
|
mut rasterize_custom_glyph: R,
|
||||||
|
) -> Result<Option<GlyphToRender>, PrepareError>
|
||||||
|
where
|
||||||
|
R: FnMut(RasterizeCustomGlyphRequest) -> Option<RasterizedCustomGlyph>,
|
||||||
|
{
|
||||||
|
if atlas.mask_atlas.glyph_cache.contains(&cache_key) {
|
||||||
|
atlas.mask_atlas.promote(cache_key);
|
||||||
|
} else if atlas.color_atlas.glyph_cache.contains(&cache_key) {
|
||||||
|
atlas.color_atlas.promote(cache_key);
|
||||||
|
} else {
|
||||||
|
let Some(image) = (get_glyph_image)(cache, font_system, &mut rasterize_custom_glyph) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let should_rasterize = image.width > 0 && image.height > 0;
|
||||||
|
|
||||||
|
let (gpu_cache, atlas_id, inner) = if should_rasterize {
|
||||||
|
let mut inner = atlas.inner_for_content_mut(image.content_type);
|
||||||
|
|
||||||
|
// Find a position in the packer
|
||||||
|
let allocation = loop {
|
||||||
|
match inner.try_allocate(image.width as usize, image.height as usize) {
|
||||||
|
Some(a) => break a,
|
||||||
|
None => {
|
||||||
|
if !atlas.grow(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
font_system,
|
||||||
|
cache,
|
||||||
|
image.content_type,
|
||||||
|
scale_factor,
|
||||||
|
&mut rasterize_custom_glyph,
|
||||||
|
) {
|
||||||
|
return Err(PrepareError::AtlasFull);
|
||||||
|
}
|
||||||
|
|
||||||
|
inner = atlas.inner_for_content_mut(image.content_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let atlas_min = allocation.rectangle.min;
|
||||||
|
|
||||||
|
queue.write_texture(
|
||||||
|
ImageCopyTexture {
|
||||||
|
texture: &inner.texture,
|
||||||
|
mip_level: 0,
|
||||||
|
origin: Origin3d {
|
||||||
|
x: atlas_min.x as u32,
|
||||||
|
y: atlas_min.y as u32,
|
||||||
|
z: 0,
|
||||||
|
},
|
||||||
|
aspect: TextureAspect::All,
|
||||||
|
},
|
||||||
|
&image.data,
|
||||||
|
ImageDataLayout {
|
||||||
|
offset: 0,
|
||||||
|
bytes_per_row: Some(image.width as u32 * inner.num_channels() as u32),
|
||||||
|
rows_per_image: None,
|
||||||
|
},
|
||||||
|
Extent3d {
|
||||||
|
width: image.width as u32,
|
||||||
|
height: image.height as u32,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
(
|
||||||
|
GpuCacheStatus::InAtlas {
|
||||||
|
x: atlas_min.x as u16,
|
||||||
|
y: atlas_min.y as u16,
|
||||||
|
content_type: image.content_type,
|
||||||
|
},
|
||||||
|
Some(allocation.id),
|
||||||
|
inner,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let inner = &mut atlas.color_atlas;
|
||||||
|
(GpuCacheStatus::SkipRasterization, None, inner)
|
||||||
|
};
|
||||||
|
|
||||||
|
inner.put(
|
||||||
|
cache_key,
|
||||||
|
GlyphDetails {
|
||||||
|
width: image.width,
|
||||||
|
height: image.height,
|
||||||
|
gpu_cache,
|
||||||
|
atlas_id,
|
||||||
|
top: image.top,
|
||||||
|
left: image.left,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let details = atlas.glyph(&cache_key).unwrap();
|
||||||
|
|
||||||
|
let mut x = x + details.left as i32;
|
||||||
|
let mut y = (line_y * scale_factor).round() as i32 + y - details.top as i32;
|
||||||
|
|
||||||
|
let (mut atlas_x, mut atlas_y, content_type) = match details.gpu_cache {
|
||||||
|
GpuCacheStatus::InAtlas { x, y, content_type } => (x, y, content_type),
|
||||||
|
GpuCacheStatus::SkipRasterization => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut width = details.width as i32;
|
||||||
|
let mut height = details.height as i32;
|
||||||
|
|
||||||
|
// Starts beyond right edge or ends beyond left edge
|
||||||
|
let max_x = x + width;
|
||||||
|
if x > bounds_max_x || max_x < bounds_min_x {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts beyond bottom edge or ends beyond top edge
|
||||||
|
let max_y = y + height;
|
||||||
|
if y > bounds_max_y || max_y < bounds_min_y {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clip left ege
|
||||||
|
if x < bounds_min_x {
|
||||||
|
let right_shift = bounds_min_x - x;
|
||||||
|
|
||||||
|
x = bounds_min_x;
|
||||||
|
width = max_x - bounds_min_x;
|
||||||
|
atlas_x += right_shift as u16;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clip right edge
|
||||||
|
if x + width > bounds_max_x {
|
||||||
|
width = bounds_max_x - x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clip top edge
|
||||||
|
if y < bounds_min_y {
|
||||||
|
let bottom_shift = bounds_min_y - y;
|
||||||
|
|
||||||
|
y = bounds_min_y;
|
||||||
|
height = max_y - bounds_min_y;
|
||||||
|
atlas_y += bottom_shift as u16;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clip bottom edge
|
||||||
|
if y + height > bounds_max_y {
|
||||||
|
height = bounds_max_y - y;
|
||||||
|
}
|
||||||
|
|
||||||
|
let depth = metadata_to_depth(metadata);
|
||||||
|
|
||||||
|
Ok(Some(GlyphToRender {
|
||||||
|
pos: [x, y],
|
||||||
|
dim: [width as u16, height as u16],
|
||||||
|
uv: [atlas_x, atlas_y],
|
||||||
|
color: color.0,
|
||||||
|
content_type_with_srgb: [
|
||||||
|
content_type as u16,
|
||||||
|
match atlas.color_mode {
|
||||||
|
ColorMode::Accurate => TextColorConversion::ConvertToLinear,
|
||||||
|
ColorMode::Web => TextColorConversion::None,
|
||||||
|
} as u16,
|
||||||
|
],
|
||||||
|
depth,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
63
src/viewport.rs
Normal file
63
src/viewport.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
use crate::{Cache, Params, Resolution};
|
||||||
|
use std::{mem, slice};
|
||||||
|
use wgpu::{BindGroup, Buffer, BufferDescriptor, BufferUsages, Device, Queue};
|
||||||
|
|
||||||
|
/// Controls the visible area of all text for a given renderer. Any text outside of the visible
|
||||||
|
/// area will be clipped.
|
||||||
|
///
|
||||||
|
/// Many projects will only ever need a single `Viewport`, but it is possible to create multiple
|
||||||
|
/// `Viewport`s if you want to render text to specific areas within a window (without having to)
|
||||||
|
/// bound each `TextArea`).
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Viewport {
|
||||||
|
params: Params,
|
||||||
|
params_buffer: Buffer,
|
||||||
|
pub(crate) bind_group: BindGroup,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Viewport {
|
||||||
|
/// Creates a new `Viewport` with the given `device` and `cache`.
|
||||||
|
pub fn new(device: &Device, cache: &Cache) -> Self {
|
||||||
|
let params = Params {
|
||||||
|
screen_resolution: Resolution {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
|
_pad: [0, 0],
|
||||||
|
};
|
||||||
|
|
||||||
|
let params_buffer = device.create_buffer(&BufferDescriptor {
|
||||||
|
label: Some("glyphon params"),
|
||||||
|
size: mem::size_of::<Params>() as u64,
|
||||||
|
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let bind_group = cache.create_uniforms_bind_group(device, ¶ms_buffer);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
params,
|
||||||
|
params_buffer,
|
||||||
|
bind_group,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the `Viewport` with the given `resolution`.
|
||||||
|
pub fn update(&mut self, queue: &Queue, resolution: Resolution) {
|
||||||
|
if self.params.screen_resolution != resolution {
|
||||||
|
self.params.screen_resolution = resolution;
|
||||||
|
|
||||||
|
queue.write_buffer(&self.params_buffer, 0, unsafe {
|
||||||
|
slice::from_raw_parts(
|
||||||
|
&self.params as *const Params as *const u8,
|
||||||
|
mem::size_of::<Params>(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current resolution of the `Viewport`.
|
||||||
|
pub fn resolution(&self) -> Resolution {
|
||||||
|
self.params.screen_resolution
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue