From afab8815598ee0cd00f063a1323ca8b02b620e4d Mon Sep 17 00:00:00 2001 From: grovesNL Date: Tue, 13 Sep 2022 16:35:20 -0230 Subject: [PATCH] Handle text overflow Cull glyphs fully outside the bounds, and clip glyphs intersecting the bounds. This is all done on the CPU for now. Fixes #2 --- Cargo.toml | 2 +- examples/hello-world.rs | 33 ++++++++++++--- src/lib.rs | 89 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 110 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a8afab5..8207fa5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0 OR Zlib" [dependencies] wgpu = "0.13.1" -fontdue = "0.7.2" +fontdue = { git = "https://github.com/mooman219/fontdue", rev = "828b4f4" } etagere = "0.2.6" [dev-dependencies] diff --git a/examples/hello-world.rs b/examples/hello-world.rs index b66c9ed..47df576 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -1,9 +1,10 @@ +use fontdue::layout::{HorizontalAlign, VerticalAlign}; use glyphon::{ fontdue::{ layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle}, Font, FontSettings, }, - Color, HasColor, Resolution, TextAtlas, TextRenderer, + Color, HasColor, Resolution, TextAtlas, TextOverflow, TextRenderer, }; use wgpu::{ Backends, CommandEncoderDescriptor, DeviceDescriptor, Features, Instance, Limits, LoadOp, @@ -88,15 +89,15 @@ async fn run() { window.request_redraw(); } Event::RedrawRequested(_) => { - let mut layout = Layout::new(CoordinateSystem::PositiveYDown); + let mut layout1 = Layout::new(CoordinateSystem::PositiveYDown); - layout.reset(&LayoutSettings { + layout1.reset(&LayoutSettings { x: 0.0, y: 0.0, ..LayoutSettings::default() }); - layout.append( + layout1.append( fonts.as_slice(), &TextStyle::with_user_data( "Hello world!\nI'm on a new line!", @@ -106,6 +107,28 @@ async fn run() { ), ); + let mut layout2 = Layout::new(CoordinateSystem::PositiveYDown); + + layout2.reset(&LayoutSettings { + x: 0.0, + y: 200.0, + max_width: Some(200.0), + max_height: Some(190.0), + horizontal_align: HorizontalAlign::Center, + vertical_align: VerticalAlign::Middle, + ..LayoutSettings::default() + }); + + layout2.append( + fonts.as_slice(), + &TextStyle::with_user_data( + "abcdefghijklmnopqrstuvwxyz\nThis should be partially clipped!\nabcdefghijklmnopqrstuvwxyz", + 25.0, + 0, + GlyphUserData, + ), + ); + text_renderer .prepare( &device, @@ -116,7 +139,7 @@ async fn run() { height: config.height, }, &fonts, - &[layout], + &[(layout1, TextOverflow::Hide), (layout2, TextOverflow::Hide)], ) .unwrap(); diff --git a/src/lib.rs b/src/lib.rs index bc8bac5..7d7dc50 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -316,6 +316,15 @@ impl TextAtlas { } } +/// Controls the overflow behavior of any glyphs that are outside of the layout bounds. +pub enum TextOverflow { + /// Glyphs can overflow the bounds. + Overflow, + /// Hide any glyphs outside the bounds. If a glyph is partially outside the bounds, it will be + /// clipped to the bounds. + Hide, +} + pub struct TextRenderer { vertex_buffer: Buffer, vertex_buffer_size: u64, @@ -365,7 +374,7 @@ impl TextRenderer { atlas: &mut TextAtlas, screen_resolution: Resolution, fonts: &[Font], - layouts: &[Layout], + layouts: &[(Layout, TextOverflow)], ) -> Result<(), PrepareError> { self.screen_resolution = screen_resolution; @@ -391,7 +400,7 @@ impl TextRenderer { self.glyphs_in_use.clear(); - for layout in layouts.iter() { + for (layout, _) in layouts.iter() { for glyph in layout.glyphs() { self.glyphs_in_use.insert(glyph.key); @@ -494,22 +503,86 @@ impl TextRenderer { let mut glyph_indices = Vec::new(); let mut glyphs_added = 0; - for layout in layouts.iter() { + for (layout, overflow) in layouts.iter() { + let settings = layout.settings(); + + // Note: subpixel positioning is not currently handled, so we always use the nearest + // pixel. + let bounds_min_x = settings.x.round() as u32; + let bounds_max_x = settings + .max_width + .map(|w| bounds_min_x + w.round() as u32) + .unwrap_or(u32::MAX); + let bounds_min_y = settings.y.round() as u32; + let bounds_max_y = settings + .max_height + .map(|h| bounds_min_y + h.round() as u32) + .unwrap_or(u32::MAX); + for glyph in layout.glyphs() { + let mut x = glyph.x.round() as u32; + let mut y = glyph.y.round() as u32; + let details = atlas.glyph_cache.get(&glyph.key).unwrap(); - let (atlas_x, atlas_y) = match details.gpu_cache { + let (mut atlas_x, mut atlas_y) = match details.gpu_cache { GpuCache::InAtlas { x, y } => (x, y), GpuCache::SkipRasterization => continue, }; + let mut width = details.width as u32; + let mut height = details.height as u32; + + match overflow { + TextOverflow::Overflow => {} + TextOverflow::Hide => { + // 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 = glyph.user_data.color(); glyph_vertices.extend( iter::repeat(GlyphToRender { - // Note: subpixel positioning is not currently handled, so we always use - // the nearest pixel. - pos: [glyph.x.round() as u32, glyph.y.round() as u32], - dim: [details.width, details.height], + pos: [x, y], + dim: [width as u16, height as u16], uv: [atlas_x, atlas_y], color: [color.r, color.g, color.b, color.a], })