use klyff::{
Color, EncoderContext, Features, Glow, GlyphColoring, Outline, Rect, Shadow, StyledText,
TextRenderer, TextStyle, TextureAtlas, TextureAtlasDescriptor,
};
const WIDTH: u32 = 512;
const HEIGHT: u32 = 128;
const SURFACE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
async fn request_device() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::None,
compatible_surface: None,
force_fallback_adapter: false,
})
.await
.ok()?;
adapter
.request_device(&wgpu::DeviceDescriptor::default())
.await
.ok()
}
fn make_font_system() -> klyff::cosmic_text::FontSystem {
let mut db = klyff::fontdb::Database::new();
db.load_font_data(include_bytes!("../../../fonts/OpenSans-Regular.ttf").to_vec());
db.load_font_data(include_bytes!("../../../fonts/NotoSansSC-Regular.ttf").to_vec());
db.load_font_data(include_bytes!("../../../fonts/NotoColorEmoji.ttf").to_vec());
klyff::cosmic_text::FontSystem::new_with_locale_and_db("en-US".to_string(), db)
}
fn make_text(font_system: &mut klyff::cosmic_text::FontSystem, style: TextStyle) -> StyledText {
use klyff::cosmic_text::{Attrs, Family, Metrics};
let default_attrs = Attrs::new().family(Family::Name("Open Sans"));
let mut text_builder = klyff::StyledTextBuilder::new(
Rect::from_xywh(0.0, 0.0, WIDTH as f32, HEIGHT as f32),
font_system,
Metrics::new(48.0, 60.0),
);
text_builder.push_text("klyff", &default_attrs, style);
text_builder.finish(font_system, &default_attrs)
}
fn render_frame(
device: &wgpu::Device,
queue: &wgpu::Queue,
renderer: &mut TextRenderer,
font_system: &mut klyff::cosmic_text::FontSystem,
texts: &[StyledText],
) {
let mut atlas = TextureAtlas::new(device, TextureAtlasDescriptor::default());
let target = device.create_texture(&wgpu::TextureDescriptor {
label: Some("render target"),
size: wgpu::Extent3d {
width: WIDTH,
height: HEIGHT,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: SURFACE_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = target.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
renderer.prepare(
EncoderContext {
atlas: &mut atlas,
device,
queue,
cmd_encoder: &mut encoder,
font_system,
},
(WIDTH, HEIGHT),
texts,
);
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
renderer.render(&mut pass, &atlas);
}
queue.submit(std::iter::once(encoder.finish()));
let _ = device.poll(wgpu::PollType::Wait {
submission_index: None,
timeout: None,
});
}
fn style() -> TextStyle {
TextStyle {
color: GlyphColoring::Gradient {
primary: Color::from_rgb(1.0, 0.5, 0.0),
secondary: Color::from_rgb(0.0, 0.5, 1.0),
direction: glam::vec2(0.0, 1.0),
},
stroke_out: Outline {
color: GlyphColoring::Gradient {
primary: Color::from_rgb(0.8, 0.0, 0.0),
secondary: Color::from_rgb(1.0, 0.4, 0.0),
direction: glam::vec2(0.0, 1.0),
},
width: 0.1,
roundness: 0.5,
},
stroke_in: Outline {
color: GlyphColoring::Solid(Color::from_rgba(0.1, 0.05, 0.0, 0.8)),
width: 0.05,
roundness: 0.5,
},
glow_out: Glow {
color: GlyphColoring::Solid(Color::from_rgba(0.0, 0.8, 1.0, 0.6)),
width: 0.3,
spread: 0.0,
roundness: 0.5,
},
glow_in: Glow {
color: GlyphColoring::Solid(Color::from_rgba(1.0, 0.95, 0.6, 0.4)),
width: 0.2,
spread: 0.1,
roundness: 0.5,
},
shadow: Shadow {
color: GlyphColoring::Solid(Color::from_rgba(0.0, 0.0, 0.0, 0.7)),
direction: glam::vec2(0.06, 0.06),
additional_width: 0.2,
spread: 0.0,
roundness: 0.5,
},
}
}
macro_rules! render_test {
($name:ident, $features:expr) => {
#[test]
fn $name() {
let Some((device, queue)) = pollster::block_on(request_device()) else {
return;
};
let mut font_system = make_font_system();
let text = make_text(&mut font_system, style());
let mut renderer = TextRenderer::new(&device, SURFACE_FORMAT);
render_frame(&device, &queue, &mut renderer, &mut font_system, &[text]);
}
};
}
render_test!(render_empty, Features::empty());
render_test!(render_stroke_out, Features::STROKE_OUT);
render_test!(render_stroke_in, Features::STROKE_IN);
render_test!(render_glow_out, Features::GLOW_OUT);
render_test!(render_glow_in, Features::GLOW_IN);
render_test!(render_shadow, Features::SHADOW);
render_test!(render_all, Features::all());
macro_rules! multipass_render_test {
($name:ident, $features:expr) => {
#[test]
fn $name() {
let Some((device, queue)) = pollster::block_on(request_device()) else {
return;
};
let features = $features;
let mut font_system = make_font_system();
let text = make_text(&mut font_system, style());
let mut renderer = TextRenderer::with_styling(features, &device, SURFACE_FORMAT);
render_frame(&device, &queue, &mut renderer, &mut font_system, &[text]);
}
};
}
multipass_render_test!(render_stroke_out_multipass, Features::STROKE_OUT);
multipass_render_test!(render_glow_out_multipass, Features::GLOW_OUT);
multipass_render_test!(render_shadow_multipass, Features::SHADOW);
multipass_render_test!(render_all_multipass, Features::all());
fn alt_style() -> TextStyle {
TextStyle {
color: GlyphColoring::Solid(Color::from_rgb(0.2, 0.9, 0.4)),
stroke_out: Outline {
color: GlyphColoring::Solid(Color::from_rgb(0.0, 0.3, 0.1)),
width: 0.08,
roundness: 0.5,
},
..Default::default()
}
}
fn make_multi_style_text(font_system: &mut klyff::cosmic_text::FontSystem) -> StyledText {
use klyff::cosmic_text::{Attrs, Family, Metrics};
let default_attrs = Attrs::new().family(Family::Name("Open Sans"));
let mut text_builder = klyff::StyledTextBuilder::new(
Rect::from_xywh(0.0, 0.0, WIDTH as f32, HEIGHT as f32),
font_system,
Metrics::new(48.0, 60.0),
);
text_builder.push_text("kly", &default_attrs, style());
text_builder.push_text("ff", &default_attrs, alt_style());
text_builder.finish(font_system, &default_attrs)
}
fn make_text_with_font(
font_system: &mut klyff::cosmic_text::FontSystem,
content: &str,
family: &str,
) -> StyledText {
use klyff::cosmic_text::{Attrs, Family, Metrics};
let default_attrs = Attrs::new().family(Family::Name(family));
let mut text_builder = klyff::StyledTextBuilder::new(
Rect::from_xywh(0.0, 0.0, WIDTH as f32, HEIGHT as f32),
font_system,
Metrics::new(48.0, 60.0),
);
text_builder.push_text(content, &default_attrs, style());
text_builder.finish(font_system, &default_attrs)
}
#[test]
fn render_multiple_styles() {
let Some((device, queue)) = pollster::block_on(request_device()) else {
return;
};
let mut font_system = make_font_system();
let text = make_multi_style_text(&mut font_system);
let mut renderer = TextRenderer::new(&device, SURFACE_FORMAT);
render_frame(&device, &queue, &mut renderer, &mut font_system, &[text]);
}
#[test]
fn render_cjk() {
let Some((device, queue)) = pollster::block_on(request_device()) else {
return;
};
let mut font_system = make_font_system();
let text = make_text_with_font(&mut font_system, "δ½ ε₯½δΈη", "Noto Sans SC");
let mut renderer = TextRenderer::new(&device, SURFACE_FORMAT);
render_frame(&device, &queue, &mut renderer, &mut font_system, &[text]);
}
#[test]
fn render_custom_glyphs() {
use klyff::cosmic_text::{Attrs, Family, Metrics};
let Some((device, queue)) = pollster::block_on(request_device()) else {
return;
};
let mut font_system = make_font_system();
let custom_font = klyff::setup_custom_glyph_font(&mut font_system);
let default_attrs = Attrs::new().family(Family::Name("Open Sans"));
let mut text_builder = klyff::StyledTextBuilder::new(
Rect::from_xywh(0.0, 0.0, WIDTH as f32, HEIGHT as f32),
&mut font_system,
Metrics::new(48.0, 60.0),
);
text_builder.push_text("ab", &default_attrs, style());
text_builder.push_custom_glyph(42, 40.0, 30.0);
text_builder.push_text("cd", &default_attrs, style());
text_builder.push_custom_glyph(7, 24.0, 24.0);
let text = text_builder.finish(&mut font_system, &default_attrs);
let mut renderer = TextRenderer::new(&device, SURFACE_FORMAT);
renderer.set_custom_glyph_font(Some(custom_font));
render_frame(&device, &queue, &mut renderer, &mut font_system, &[text]);
let custom = renderer.custom_glyphs();
assert_eq!(custom.len(), 2, "expected two detected custom glyphs");
let ids: Vec<u64> = custom.iter().map(|g| g.id).collect();
assert!(ids.contains(&42) && ids.contains(&7), "ids: {ids:?}");
for g in custom {
assert_eq!(g.text_id, 0);
assert!(g.rect.width() > 0.0, "custom glyph should reserve width");
assert!(g.rect.height() > 0.0, "custom glyph should reserve height");
}
let g42 = custom.iter().find(|g| g.id == 42).unwrap();
let g7 = custom.iter().find(|g| g.id == 7).unwrap();
assert!(g42.rect.width() > g7.rect.width());
}
#[test]
fn render_emoji() {
let Some((device, queue)) = pollster::block_on(request_device()) else {
return;
};
let mut font_system = make_font_system();
let text = make_text_with_font(&mut font_system, "πππ", "Noto Color Emoji");
let mut renderer = TextRenderer::new(&device, SURFACE_FORMAT);
render_frame(&device, &queue, &mut renderer, &mut font_system, &[text]);
}