diff --git a/examples/hello-world.rs b/examples/hello-world.rs index 738f061..88afb29 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -1,6 +1,7 @@ use cryoglyph::{ - Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, - TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, + cosmic_text::LetterSpacing, Attrs, AttrsOwned, Buffer, Cache, Color, Family, FontSystem, + Metrics, Resolution, Shaping, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, + Viewport, }; use std::sync::Arc; use wgpu::{ @@ -169,6 +170,21 @@ impl winit::application::ApplicationHandler for Application { let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor { label: None }); + let text_area = TextArea { + buffer: text_buffer, + left: 10.0, + top: 10.0, + scale: 3.5, + bounds: TextBounds { + left: 0, + top: 0, + right: 600, + bottom: 160, + }, + default_color: Color::rgb(255, 255, 255), + stroke_size: 5.5, + stroke_color: Color::rgb(50, 50, 255), + }; text_renderer .prepare( @@ -178,19 +194,7 @@ impl winit::application::ApplicationHandler for Application { 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(0, 0, 0), - }], + [text_area], swash_cache, ) .unwrap(); @@ -205,7 +209,7 @@ impl winit::application::ApplicationHandler for Application { view: &view, resolve_target: None, ops: Operations { - load: LoadOp::Clear(wgpu::Color::WHITE), + load: LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store, }, })], diff --git a/src/cache.rs b/src/cache.rs index 61d2e4d..9a1e1dc 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -87,6 +87,16 @@ impl Cache { offset: mem::size_of::() as u64 * 6, shader_location: 5, }, + wgpu::VertexAttribute { + format: VertexFormat::Uint32, + offset: mem::size_of::() as u64 * 7, + shader_location: 6, + }, + wgpu::VertexAttribute { + format: VertexFormat::Float32, + offset: mem::size_of::() as u64 * 8, + shader_location: 7, + }, ], }; diff --git a/src/lib.rs b/src/lib.rs index 9360b30..31f5ef7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,11 +21,11 @@ use text_render::ContentType; // Re-export all top-level types from `cosmic-text` for convenience. #[doc(no_inline)] pub use cosmic_text::{ - self, Action, Affinity, Attrs, AttrsList, AttrsOwned, Buffer, BufferLine, CacheKey, Color, - Command, Cursor, Edit, Editor, Family, FamilyOwned, Font, FontSystem, LayoutCursor, + self, fontdb, Action, Affinity, Attrs, AttrsList, AttrsOwned, Buffer, BufferLine, CacheKey, + Color, Command, Cursor, Edit, Editor, Family, FamilyOwned, Font, FontSystem, LayoutCursor, LayoutGlyph, LayoutLine, LayoutRun, LayoutRunIter, Metrics, ShapeGlyph, ShapeLine, ShapeSpan, ShapeWord, Shaping, Stretch, Style, SubpixelBin, SwashCache, SwashContent, SwashImage, Weight, - Wrap, fontdb, + Wrap, }; use etagere::AllocId; @@ -57,6 +57,8 @@ pub(crate) struct GlyphToRender { color: u32, content_type_with_srgb: [u16; 2], depth: f32, + stroke_color: u32, + stroke_size: f32, } /// The screen resolution to use when rendering text. @@ -117,4 +119,8 @@ pub struct TextArea<'a> { pub bounds: TextBounds, // The default color of the text area. pub default_color: Color, + /// The stroke (outline) size + pub stroke_size: f32, + /// The stroke (outline) color + pub stroke_color: Color, } diff --git a/src/shader.wgsl b/src/shader.wgsl index 1813a66..a88e9f6 100644 --- a/src/shader.wgsl +++ b/src/shader.wgsl @@ -6,6 +6,8 @@ struct VertexInput { @location(3) color: u32, @location(4) content_type_with_srgb: u32, @location(5) depth: f32, + @location(6) stroke_color: u32, + @location(7) stroke_size: f32, } struct VertexOutput { @@ -13,6 +15,8 @@ struct VertexOutput { @location(0) color: vec4, @location(1) uv: vec2, @location(2) @interpolate(flat) content_type: u32, + @location(3) stroke_color: vec4, + @location(4) stroke_size: f32, }; struct Params { @@ -42,11 +46,13 @@ fn srgb_to_linear(c: f32) -> f32 { @vertex fn vs_main(in_vert: VertexInput) -> VertexOutput { - var pos = in_vert.pos; - let width = in_vert.dim & 0xffffu; - let height = (in_vert.dim & 0xffff0000u) >> 16u; + var stroke = in_vert.stroke_size; + var width = in_vert.dim & 0xffffu; + var height = (in_vert.dim & 0xffff0000u) >> 16u; + var pos = in_vert.pos; + var uv = vec2(in_vert.uv & 0xffffu, (in_vert.uv & 0xffff0000u) >> 16u); let color = in_vert.color; - var uv = vec2(in_vert.uv & 0xffffu, (in_vert.uv & 0xffff0000u) >> 16u); + let stroke_color = in_vert.stroke_color; let v = in_vert.vertex_idx; let corner_position = vec2( @@ -80,6 +86,12 @@ fn vs_main(in_vert: VertexInput) -> VertexOutput { f32(color & 0x000000ffu) / 255.0, f32((color & 0xff000000u) >> 24u) / 255.0, ); + vert_output.stroke_color = vec4( + f32((stroke_color & 0x00ff0000u) >> 16u) / 255.0, + f32((stroke_color & 0x0000ff00u) >> 8u) / 255.0, + f32(stroke_color & 0x000000ffu) / 255.0, + f32((stroke_color & 0xff000000u) >> 24u) / 255.0, + ); } case 1u: { vert_output.color = vec4( @@ -88,6 +100,12 @@ fn vs_main(in_vert: VertexInput) -> VertexOutput { srgb_to_linear(f32(color & 0x000000ffu) / 255.0), f32((color & 0xff000000u) >> 24u) / 255.0, ); + vert_output.stroke_color = vec4( + srgb_to_linear(f32((stroke_color & 0x00ff0000u) >> 16u) / 255.0), + srgb_to_linear(f32((stroke_color & 0x0000ff00u) >> 8u) / 255.0), + srgb_to_linear(f32(stroke_color & 0x000000ffu) / 255.0), + f32((stroke_color & 0xff000000u) >> 24u) / 255.0, + ); } default: {} } @@ -108,20 +126,54 @@ fn vs_main(in_vert: VertexInput) -> VertexOutput { vert_output.content_type = content_type; vert_output.uv = vec2(uv) / vec2(dim); + vert_output.stroke_size = in_vert.stroke_size; return vert_output; } @fragment fn fs_main(in_frag: VertexOutput) -> @location(0) vec4 { + let dims = vec2(textureDimensions(mask_atlas_texture)); + var bit: u32 = 1; + + let current = textureSample(mask_atlas_texture, atlas_sampler, dims); + var closest_dist = distance(vec2(in_frag.position.xy), vec2(current.xy)); + var result = current; + + for (var step: u32 = 0; step <= 32; step++) { + bit = 1u << step; + for (var dy = -1; dy <= 1; dy++) { + for (var dx = -1; dx <= 1; dx++) { + if (dx == 0 && dy == 0) { + continue; + } + + let neighbour_coord = in_frag.position.xy + vec2(f32(dx * bit), f32(dy * bit)); + let neighbour = textureSample(mask_atlas_texture, atlas_sampler, neighbour_coord / dims); + let dist = distance(vec2(in_frag.position.xy), vec2(neighbour.xy)); + + if (dist < closest_dist) { + closest_dist = dist; + result = vec4(in_frag.stroke_color.rgb, in_frag.stroke_color.a * textureSampleLevel(mask_atlas_texture, atlas_sampler, neighbour.xy, 0.0).x); + } + } + } + } + + switch in_frag.content_type { case 0u: { return textureSampleLevel(color_atlas_texture, atlas_sampler, in_frag.uv, 0.0); } case 1u: { + if in_frag.stroke_size > 0.0 { + return result; + } else { return vec4(in_frag.color.rgb, in_frag.color.a * textureSampleLevel(mask_atlas_texture, atlas_sampler, in_frag.uv, 0.0).x); + } } default: { + // return result; return vec4(0.0); } } diff --git a/src/text_atlas.rs b/src/text_atlas.rs index 54a0536..8313672 100644 --- a/src/text_atlas.rs +++ b/src/text_atlas.rs @@ -1,7 +1,7 @@ use crate::{ - Cache, CacheKey, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, text_render::ContentType, + text_render::ContentType, Cache, CacheKey, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, }; -use etagere::{Allocation, BucketedAtlasAllocator, size2}; +use etagere::{size2, Allocation, BucketedAtlasAllocator}; use lru::LruCache; use rustc_hash::FxHasher; use std::{collections::HashSet, hash::BuildHasherDefault}; diff --git a/src/text_render.rs b/src/text_render.rs index 8b73fe7..39cadc6 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -2,12 +2,13 @@ use crate::{ ColorMode, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError, SwashCache, SwashContent, TextArea, TextAtlas, Viewport, }; +use cosmic_text::LayoutRun; use std::{num::NonZeroU64, slice}; use wgpu::util::StagingBelt; use wgpu::{ - Buffer, BufferDescriptor, BufferUsages, COPY_BUFFER_ALIGNMENT, CommandEncoder, - DepthStencilState, Device, Extent3d, MultisampleState, Origin3d, Queue, RenderPass, - RenderPipeline, TexelCopyBufferLayout, TexelCopyTextureInfo, TextureAspect, + Buffer, BufferDescriptor, BufferUsages, CommandEncoder, DepthStencilState, Device, Extent3d, + MultisampleState, Origin3d, Queue, RenderPass, RenderPipeline, TexelCopyBufferLayout, + TexelCopyTextureInfo, TextureAspect, COPY_BUFFER_ALIGNMENT, }; /// A text renderer that uses cached glyphs to render text into an existing render pass. @@ -87,188 +88,40 @@ impl TextRenderer { .take_while(is_run_visible); for run in layout_runs { - for glyph in run.glyphs.iter() { - let physical_glyph = - glyph.physical((text_area.left, text_area.top), text_area.scale); - - let cache_key = physical_glyph.cache_key; - - let details = if let Some(details) = - atlas.mask_atlas.glyph_cache.get(&cache_key) - { - atlas.mask_atlas.glyphs_in_use.insert(cache_key); - details - } else if let Some(details) = atlas.color_atlas.glyph_cache.get(&cache_key) { - atlas.color_atlas.glyphs_in_use.insert(cache_key); - details - } 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( - TexelCopyTextureInfo { - 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, - TexelCopyBufferLayout { - 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.glyphs_in_use.insert(cache_key); - // Insert the glyph into the cache and return the details reference - inner.glyph_cache.get_or_insert(cache_key, || GlyphDetails { - width: image.placement.width as u16, - height: image.placement.height as u16, - gpu_cache, - atlas_id, - top: image.placement.top as i16, - left: image.placement.left as i16, - }) - }; - - 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; - - // 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; + if text_area.stroke_size > 0.0 { + for i in 0..2 { + self.prepare_glyphs( + &run, + &text_area, + atlas, + cache, + font_system, + device, + queue, + bounds_max_x, + bounds_max_y, + bounds_min_y, + bounds_min_x, + i == 0, + &mut metadata_to_depth, + )?; } - - // 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 { - Some(some) => some, - None => text_area.default_color, - }; - - let depth = metadata_to_depth(glyph.metadata); - - self.glyph_vertices.push(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, - }); + } else { + self.prepare_glyphs( + &run, + &text_area, + atlas, + cache, + font_system, + device, + queue, + bounds_max_x, + bounds_max_y, + bounds_min_y, + bounds_min_x, + false, + &mut metadata_to_depth, + )?; } } } @@ -363,6 +216,204 @@ impl TextRenderer { Ok(()) } + + fn prepare_glyphs( + &mut self, + run: &LayoutRun, + text_area: &TextArea, + atlas: &mut TextAtlas, + cache: &mut SwashCache, + font_system: &mut FontSystem, + device: &Device, + queue: &Queue, + bounds_max_x: i32, + bounds_max_y: i32, + bounds_min_y: i32, + bounds_min_x: i32, + stroke: bool, + mut metadata_to_depth: impl FnMut(usize) -> f32, + ) -> Result<(), PrepareError> { + for glyph in run.glyphs.iter() { + let physical_glyph = glyph.physical((text_area.left, text_area.top), text_area.scale); + + let cache_key = physical_glyph.cache_key; + + let details = if let Some(details) = atlas.mask_atlas.glyph_cache.get(&cache_key) { + atlas.mask_atlas.glyphs_in_use.insert(cache_key); + details + } else if let Some(details) = atlas.color_atlas.glyph_cache.get(&cache_key) { + atlas.color_atlas.glyphs_in_use.insert(cache_key); + details + } 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( + TexelCopyTextureInfo { + 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, + TexelCopyBufferLayout { + 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.glyphs_in_use.insert(cache_key); + // Insert the glyph into the cache and return the details reference + inner.glyph_cache.get_or_insert(cache_key, || GlyphDetails { + width: image.placement.width as u16, + height: image.placement.height as u16, + gpu_cache, + atlas_id, + top: image.placement.top as i16, + left: image.placement.left as i16, + }) + }; + + 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; + + // 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 = if stroke { + text_area.stroke_color + } else { + match glyph.color_opt { + Some(some) => some, + None => text_area.default_color, + } + }; + + let depth = metadata_to_depth(glyph.metadata); + + self.glyph_vertices.push(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, + stroke_color: text_area.stroke_color.0, + stroke_size: if stroke { text_area.stroke_size } else { 0.0 }, + }); + } + Ok(()) + } } #[repr(u16)]