|
@@ -22,7 +22,7 @@ use crate::{
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
use super::{
|
|
use super::{
|
|
|
- atlas::{Atlas, RenderedAtlas, RunIdx},
|
|
|
|
|
|
|
+ atlas::{Atlas, GlyphInfo, RenderedAtlas, RunIdx},
|
|
|
TextLayout,
|
|
TextLayout,
|
|
|
};
|
|
};
|
|
|
|
|
|
|
@@ -60,6 +60,15 @@ pub fn render_layout(
|
|
|
render_layout_with_opts(layout, DebugRenderOptions::OFF, renderer, tag)
|
|
render_layout_with_opts(layout, DebugRenderOptions::OFF, renderer, tag)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/// A text shadow/glow effect. `offset=(0,0)` + `blur>0` gives an outer
|
|
|
|
|
+/// glow, an offset gives a drop shadow; blur 0 gives a hard edge.
|
|
|
|
|
+#[derive(Copy, Clone, Debug, PartialEq)]
|
|
|
|
|
+pub struct Shadow {
|
|
|
|
|
+ pub color: Color,
|
|
|
|
|
+ pub offset: Point,
|
|
|
|
|
+ pub blur: f32,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/// Render a raw parley layout that was built with the given scale. Used
|
|
/// Render a raw parley layout that was built with the given scale. Used
|
|
|
/// by editors that own their layout internally (e.g. `PlainEditor`).
|
|
/// by editors that own their layout internally (e.g. `PlainEditor`).
|
|
|
#[cfg(not(target_os = "android"))]
|
|
#[cfg(not(target_os = "android"))]
|
|
@@ -69,7 +78,7 @@ pub fn render_raw_layout(
|
|
|
renderer: &Renderer,
|
|
renderer: &Renderer,
|
|
|
tag: DebugTag,
|
|
tag: DebugTag,
|
|
|
) -> Vec<DrawInstruction> {
|
|
) -> Vec<DrawInstruction> {
|
|
|
- render_raw_layout_impl(layout, scale, DebugRenderOptions::OFF, renderer, tag).0
|
|
|
|
|
|
|
+ render_raw_layout_impl(layout, scale, DebugRenderOptions::OFF, None, renderer, tag).0
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// Draw a filled (and optionally outlined) background box behind every glyph run
|
|
/// Draw a filled (and optionally outlined) background box behind every glyph run
|
|
@@ -135,7 +144,17 @@ pub fn render_layout_with_opts(
|
|
|
renderer: &Renderer,
|
|
renderer: &Renderer,
|
|
|
tag: DebugTag,
|
|
tag: DebugTag,
|
|
|
) -> Vec<DrawInstruction> {
|
|
) -> Vec<DrawInstruction> {
|
|
|
- render_raw_layout_impl(layout, layout.scale(), opts, renderer, tag).0
|
|
|
|
|
|
|
+ render_raw_layout_impl(layout, layout.scale(), opts, None, renderer, tag).0
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+pub fn render_layout_shadowed(
|
|
|
|
|
+ layout: &TextLayout,
|
|
|
|
|
+ shadow: Option<&Shadow>,
|
|
|
|
|
+ opts: DebugRenderOptions,
|
|
|
|
|
+ renderer: &Renderer,
|
|
|
|
|
+ tag: DebugTag,
|
|
|
|
|
+) -> Vec<DrawInstruction> {
|
|
|
|
|
+ render_raw_layout_impl(layout, layout.scale(), opts, shadow, renderer, tag).0
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// Layout coordinates are physical (scale is baked in by parley) while
|
|
/// Layout coordinates are physical (scale is baked in by parley) while
|
|
@@ -147,9 +166,13 @@ fn render_raw_layout_impl(
|
|
|
layout: &parley::Layout<Color>,
|
|
layout: &parley::Layout<Color>,
|
|
|
scale: f32,
|
|
scale: f32,
|
|
|
opts: DebugRenderOptions,
|
|
opts: DebugRenderOptions,
|
|
|
|
|
+ shadow: Option<&Shadow>,
|
|
|
renderer: &Renderer,
|
|
renderer: &Renderer,
|
|
|
tag: DebugTag,
|
|
tag: DebugTag,
|
|
|
) -> (Vec<DrawInstruction>, Rectangle) {
|
|
) -> (Vec<DrawInstruction>, Rectangle) {
|
|
|
|
|
+ let has_shadow = shadow.map_or(false, |s| s.color[3] > 0.);
|
|
|
|
|
+ let shadow_blur_px = shadow.map_or(0., |s| s.blur * scale);
|
|
|
|
|
+
|
|
|
// First pass to create atlas
|
|
// First pass to create atlas
|
|
|
let mut scale_ctx = swash::scale::ScaleContext::new();
|
|
let mut scale_ctx = swash::scale::ScaleContext::new();
|
|
|
let mut atlas = Atlas::new(renderer, tag);
|
|
let mut atlas = Atlas::new(renderer, tag);
|
|
@@ -159,6 +182,16 @@ fn render_raw_layout_impl(
|
|
|
match item {
|
|
match item {
|
|
|
parley::PositionedLayoutItem::GlyphRun(glyph_run) => {
|
|
parley::PositionedLayoutItem::GlyphRun(glyph_run) => {
|
|
|
push_glyphs(&mut atlas, &glyph_run, run_idx, &mut scale_ctx);
|
|
push_glyphs(&mut atlas, &glyph_run, run_idx, &mut scale_ctx);
|
|
|
|
|
+ // Skip shadow if it has a fully transparent color
|
|
|
|
|
+ if has_shadow {
|
|
|
|
|
+ push_shadow_glyphs(
|
|
|
|
|
+ &mut atlas,
|
|
|
|
|
+ &glyph_run,
|
|
|
|
|
+ run_idx,
|
|
|
|
|
+ &mut scale_ctx,
|
|
|
|
|
+ shadow_blur_px,
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
run_idx += 1;
|
|
run_idx += 1;
|
|
|
}
|
|
}
|
|
|
parley::PositionedLayoutItem::InlineBox(_) => {}
|
|
parley::PositionedLayoutItem::InlineBox(_) => {}
|
|
@@ -177,8 +210,16 @@ fn render_raw_layout_impl(
|
|
|
for item in line.items() {
|
|
for item in line.items() {
|
|
|
match item {
|
|
match item {
|
|
|
parley::PositionedLayoutItem::GlyphRun(glyph_run) => {
|
|
parley::PositionedLayoutItem::GlyphRun(glyph_run) => {
|
|
|
- let (mesh, run_bounds) =
|
|
|
|
|
- render_glyph_run(&glyph_run, run_idx, opts, &atlas, scale, renderer, tag);
|
|
|
|
|
|
|
+ let (mesh, run_bounds) = render_glyph_run(
|
|
|
|
|
+ &glyph_run,
|
|
|
|
|
+ run_idx,
|
|
|
|
|
+ opts,
|
|
|
|
|
+ shadow.filter(|_| has_shadow),
|
|
|
|
|
+ &atlas,
|
|
|
|
|
+ scale,
|
|
|
|
|
+ renderer,
|
|
|
|
|
+ tag,
|
|
|
|
|
+ );
|
|
|
bounds.join(run_bounds);
|
|
bounds.join(run_bounds);
|
|
|
instrs.push(DrawInstruction::Draw(mesh));
|
|
instrs.push(DrawInstruction::Draw(mesh));
|
|
|
run_idx += 1;
|
|
run_idx += 1;
|
|
@@ -214,10 +255,46 @@ pub(super) fn push_glyphs(
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// TODO: deduplicate code with push_glyphs()
|
|
|
|
|
+pub(super) fn push_shadow_glyphs(
|
|
|
|
|
+ atlas: &mut Atlas,
|
|
|
|
|
+ glyph_run: &parley::GlyphRun<'_, Color>,
|
|
|
|
|
+ run_idx: RunIdx,
|
|
|
|
|
+ scale_ctx: &mut swash::scale::ScaleContext,
|
|
|
|
|
+ blur_px: f32,
|
|
|
|
|
+) {
|
|
|
|
|
+ let run = glyph_run.run();
|
|
|
|
|
+ let font = run.font();
|
|
|
|
|
+ let font_size = run.font_size();
|
|
|
|
|
+ let normalized_coords = run.normalized_coords();
|
|
|
|
|
+ let font_ref = swash::FontRef::from_index(font.data.as_ref(), font.index as usize).unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ let mut scaler = scale_ctx
|
|
|
|
|
+ .builder(font_ref)
|
|
|
|
|
+ .size(font_size)
|
|
|
|
|
+ .hint(true)
|
|
|
|
|
+ .normalized_coords(normalized_coords)
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ for glyph in glyph_run.glyphs() {
|
|
|
|
|
+ atlas.push_shadow_glyph(glyph.id as u16, run_idx, &mut scaler, blur_px);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn glyph_rect(glyph_x: f32, glyph_y: f32, glyph_inf: &GlyphInfo, scale: f32) -> Rectangle {
|
|
|
|
|
+ Rectangle::new(
|
|
|
|
|
+ (glyph_x + glyph_inf.place.left as f32) / scale,
|
|
|
|
|
+ (glyph_y - glyph_inf.place.top as f32) / scale,
|
|
|
|
|
+ glyph_inf.place.width as f32 / scale,
|
|
|
|
|
+ glyph_inf.place.height as f32 / scale,
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
fn render_glyph_run(
|
|
fn render_glyph_run(
|
|
|
glyph_run: &parley::GlyphRun<'_, Color>,
|
|
glyph_run: &parley::GlyphRun<'_, Color>,
|
|
|
run_idx: usize,
|
|
run_idx: usize,
|
|
|
opts: DebugRenderOptions,
|
|
opts: DebugRenderOptions,
|
|
|
|
|
+ shadow: Option<&Shadow>,
|
|
|
atlas: &RenderedAtlas,
|
|
atlas: &RenderedAtlas,
|
|
|
scale: f32,
|
|
scale: f32,
|
|
|
renderer: &Renderer,
|
|
renderer: &Renderer,
|
|
@@ -236,6 +313,24 @@ fn render_glyph_run(
|
|
|
render_underline(underline, glyph_run, scale, &mut mesh);
|
|
render_underline(underline, glyph_run, scale, &mut mesh);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Shadow quads first so glyphs draw on top within the same mesh
|
|
|
|
|
+ if let Some(shadow) = shadow {
|
|
|
|
|
+ for glyph in glyph_run.glyphs() {
|
|
|
|
|
+ let glyph_x = run_x + glyph.x;
|
|
|
|
|
+ let glyph_y = run_y - glyph.y;
|
|
|
|
|
+ run_x += glyph.advance;
|
|
|
|
|
+
|
|
|
|
|
+ // Color glyphs have no alpha-mask silhouette in the atlas
|
|
|
|
|
+ let Some(glyph_inf) = atlas.fetch_uv_shadow(glyph.id as u16, run_idx) else { continue };
|
|
|
|
|
+
|
|
|
|
|
+ let shadow_rect = glyph_rect(glyph_x, glyph_y, glyph_inf, scale) + shadow.offset;
|
|
|
|
|
+ bounds.add(shadow_rect);
|
|
|
|
|
+ mesh.draw_box(&shadow_rect, shadow.color, &glyph_inf.uv_rect);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ run_x = glyph_run.offset();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
for glyph in glyph_run.glyphs() {
|
|
for glyph in glyph_run.glyphs() {
|
|
|
let glyph_inf = atlas.fetch_uv(glyph.id as u16, run_idx).expect("missing glyph UV rect");
|
|
let glyph_inf = atlas.fetch_uv(glyph.id as u16, run_idx).expect("missing glyph UV rect");
|
|
|
|
|
|
|
@@ -243,12 +338,7 @@ fn render_glyph_run(
|
|
|
let glyph_y = run_y - glyph.y;
|
|
let glyph_y = run_y - glyph.y;
|
|
|
run_x += glyph.advance;
|
|
run_x += glyph.advance;
|
|
|
|
|
|
|
|
- let glyph_rect = Rectangle::new(
|
|
|
|
|
- (glyph_x + glyph_inf.place.left as f32) / scale,
|
|
|
|
|
- (glyph_y - glyph_inf.place.top as f32) / scale,
|
|
|
|
|
- glyph_inf.place.width as f32 / scale,
|
|
|
|
|
- glyph_inf.place.height as f32 / scale,
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ let glyph_rect = glyph_rect(glyph_x, glyph_y, glyph_inf, scale);
|
|
|
|
|
|
|
|
bounds.add(glyph_rect);
|
|
bounds.add(glyph_rect);
|
|
|
|
|
|
|
@@ -301,3 +391,50 @@ fn render_underline(
|
|
|
|
|
|
|
|
mesh.draw_line(start, end, color, width / scale);
|
|
mesh.draw_line(start, end, color, width / scale);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+#[cfg(test)]
|
|
|
|
|
+mod tests {
|
|
|
|
|
+ use super::*;
|
|
|
|
|
+ use crate::{gfx::Renderer, text::make_layout};
|
|
|
|
|
+
|
|
|
|
|
+ /// Count quads (6 indices each) across all emitted draw instrs.
|
|
|
|
|
+ fn count_quads(instrs: &[DrawInstruction], num_elements_of: impl Fn(&DrawMesh) -> i32) -> i32 {
|
|
|
|
|
+ let mut quads = 0;
|
|
|
|
|
+ for instr in instrs {
|
|
|
|
|
+ let DrawInstruction::Draw(mesh) = instr else { continue };
|
|
|
|
|
+ quads += num_elements_of(mesh) / 6;
|
|
|
|
|
+ }
|
|
|
|
|
+ quads
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[test]
|
|
|
|
|
+ fn test_shadow_quads_double_and_alpha_zero_is_noop() {
|
|
|
|
|
+ let (method_send, _method_recv) = async_channel::unbounded();
|
|
|
|
|
+ let renderer = Renderer::new(method_send);
|
|
|
|
|
+
|
|
|
|
|
+ let layout = make_layout("Hello", COLOR_WHITE, 40., 1., 1., None, &[]);
|
|
|
|
|
+
|
|
|
|
|
+ let plain = render_layout(&layout, &renderer, None);
|
|
|
|
|
+ let plain_quads = count_quads(&plain, |m| m.num_elements);
|
|
|
|
|
+
|
|
|
|
|
+ let off_shadow = Shadow { color: [1., 0., 0., 0.], offset: Point::new(2., 2.), blur: 4. };
|
|
|
|
|
+ let off = render_layout_shadowed(
|
|
|
|
|
+ &layout,
|
|
|
|
|
+ Some(&off_shadow),
|
|
|
|
|
+ DebugRenderOptions::OFF,
|
|
|
|
|
+ &renderer,
|
|
|
|
|
+ None,
|
|
|
|
|
+ );
|
|
|
|
|
+ assert_eq!(count_quads(&off, |m| m.num_elements), plain_quads);
|
|
|
|
|
+
|
|
|
|
|
+ let shadow = Shadow { color: [0., 0., 0., 1.], offset: Point::new(2., 2.), blur: 4. };
|
|
|
|
|
+ let shadowed = render_layout_shadowed(
|
|
|
|
|
+ &layout,
|
|
|
|
|
+ Some(&shadow),
|
|
|
|
|
+ DebugRenderOptions::OFF,
|
|
|
|
|
+ &renderer,
|
|
|
|
|
+ None,
|
|
|
|
|
+ );
|
|
|
|
|
+ assert_eq!(count_quads(&shadowed, |m| m.num_elements), plain_quads * 2);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|