diff --git a/Cargo.toml b/Cargo.toml index c3f8b89..f140402 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,8 @@ license = "MIT OR Apache-2.0 OR Zlib" wgpu = "0.14.0" fontdue = { git = "https://github.com/mooman219/fontdue", rev = "828b4f4" } etagere = "0.2.6" +#cosmic-text = { git = "https://github.com/pop-os/cosmic-text", rev = "ef686f8" } +cosmic-text = { path = "../cosmic-text" } [dev-dependencies] winit = "0.27.0" diff --git a/examples/hello-world.rs b/examples/hello-world.rs index ac854de..34aa0cb 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -1,11 +1,5 @@ -use fontdue::layout::{HorizontalAlign, VerticalAlign}; -use glyphon::{ - fontdue::{ - layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle}, - Font, FontSettings, - }, - Color, HasColor, Resolution, TextAtlas, TextOverflow, TextRenderer, -}; +use cosmic_text::{fontdb, FontSystem, TextBuffer, TextMetrics}; +use glyphon::{Color, HasColor, Resolution, TextAtlas, TextRenderer}; use wgpu::{ Backends, CommandEncoderDescriptor, CompositeAlphaMode, DeviceDescriptor, Features, Instance, Limits, LoadOp, Operations, PresentMode, RenderPassColorAttachment, RenderPassDescriptor, @@ -35,6 +29,8 @@ impl HasColor for GlyphUserData { } } +static mut FONT_SYSTEM: Option = None; + async fn run() { let instance = Instance::new(Backends::all()); let adapter = instance @@ -70,10 +66,14 @@ async fn run() { let mut atlas = TextAtlas::new(&device, &queue, swapchain_format); let mut text_renderer = TextRenderer::new(&device, &queue); - - let font = include_bytes!("./Inter-Bold.ttf") as &[u8]; - let font = Font::from_bytes(font, FontSettings::default()).unwrap(); - let fonts = vec![font]; + unsafe { FONT_SYSTEM = Some(FontSystem::new()) }; + let font_matches = unsafe { + FONT_SYSTEM.as_ref().unwrap().matches(|info| { + info.style == fontdb::Style::Normal + && info.weight == fontdb::Weight::NORMAL + && info.stretch == fontdb::Stretch::Normal + }) + }; event_loop.run(move |event, _, control_flow| { let _ = (&instance, &adapter); @@ -90,45 +90,11 @@ async fn run() { window.request_redraw(); } Event::RedrawRequested(_) => { - let mut layout1 = Layout::new(CoordinateSystem::PositiveYDown); + let mut buffer = + TextBuffer::new(font_matches.as_ref().unwrap(), TextMetrics::new(20, 50)); - layout1.reset(&LayoutSettings { - x: 0.0, - y: 0.0, - ..LayoutSettings::default() - }); - - layout1.append( - fonts.as_slice(), - &TextStyle::with_user_data( - "Hello world!\nI'm on a new line!", - 50.0, - 0, - GlyphUserData, - ), - ); - - 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, - ), - ); + buffer.set_text("HELLO_FROM_COSMIC_INSIDE_OF_GLYPHON_WGPU"); + buffer.shape_until_cursor(); text_renderer .prepare( @@ -139,8 +105,7 @@ async fn run() { width: config.width, height: config.height, }, - &fonts, - &[(layout1, TextOverflow::Hide), (layout2, TextOverflow::Hide)], + &mut buffer, ) .unwrap(); diff --git a/examples/highlight-mouse-over.rs b/examples/highlight-mouse-over.rs deleted file mode 100644 index 72d4107..0000000 --- a/examples/highlight-mouse-over.rs +++ /dev/null @@ -1,204 +0,0 @@ -use fontdue::layout::{HorizontalAlign, VerticalAlign}; -use glyphon::{ - fontdue::{ - layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle}, - Font, FontSettings, - }, - Color, HasColor, Resolution, TextAtlas, TextOverflow, TextRenderer, -}; -use wgpu::{ - Backends, CommandEncoderDescriptor, CompositeAlphaMode, DeviceDescriptor, Features, Instance, - Limits, LoadOp, Operations, PresentMode, RenderPassColorAttachment, RenderPassDescriptor, - RequestAdapterOptions, SurfaceConfiguration, TextureUsages, TextureViewDescriptor, -}; -use winit::{ - event::{Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::Window, -}; - -fn main() { - pollster::block_on(run()); -} - -#[derive(Clone, Copy)] -struct GlyphUserData { - color: Color, -} - -impl HasColor for GlyphUserData { - fn color(&self) -> Color { - self.color - } -} - -async fn run() { - let instance = Instance::new(Backends::all()); - let adapter = instance - .request_adapter(&RequestAdapterOptions::default()) - .await - .unwrap(); - let (device, queue) = adapter - .request_device( - &DeviceDescriptor { - label: None, - features: Features::empty(), - limits: Limits::downlevel_defaults(), - }, - None, - ) - .await - .unwrap(); - - let event_loop = EventLoop::new(); - let window = Window::new(&event_loop).unwrap(); - let surface = unsafe { instance.create_surface(&window) }; - let size = window.inner_size(); - let swapchain_format = surface.get_supported_formats(&adapter)[0]; - let mut config = SurfaceConfiguration { - usage: TextureUsages::RENDER_ATTACHMENT, - format: swapchain_format, - width: size.width, - height: size.height, - present_mode: PresentMode::Mailbox, - alpha_mode: CompositeAlphaMode::Opaque, - }; - surface.configure(&device, &config); - - let min_x = 100f32; - let width = 250f32; - let min_y = 100f32; - let height = 250f32; - - let mut atlas = TextAtlas::new(&device, &queue, swapchain_format); - let mut text_renderer = TextRenderer::new(&device, &queue); - - let font = include_bytes!("./Inter-Bold.ttf") as &[u8]; - let font = Font::from_bytes(font, FontSettings::default()).unwrap(); - let fonts = vec![font]; - - let mut layout = Layout::new(CoordinateSystem::PositiveYDown); - - const YELLOW: Color = Color { - r: 255, - g: 255, - b: 0, - a: 255, - }; - const BLUE: Color = Color { - r: 0, - g: 0, - b: 255, - a: 255, - }; - let mut color = YELLOW; - let mut color_changed = true; - - event_loop.run(move |event, _, control_flow| { - let _ = (&instance, &adapter); - - *control_flow = ControlFlow::Poll; - match event { - Event::WindowEvent { - event: WindowEvent::Resized(size), - .. - } => { - config.width = size.width; - config.height = size.height; - surface.configure(&device, &config); - window.request_redraw(); - } - Event::WindowEvent { - event: WindowEvent::CursorMoved { position, .. }, - .. - } => { - let cursor_in_layout = min_x <= position.x as f32 - && position.x as f32 <= min_x + width - && min_y < position.y as f32 - && position.y as f32 <= min_y + height; - if cursor_in_layout { - if color != BLUE { - color = BLUE; - color_changed = true; - } - } else { - if color != YELLOW { - color = YELLOW; - color_changed = true; - } - } - } - Event::RedrawRequested(_) => { - if color_changed { - color_changed = false; - println!("Recreating layout because color changed"); - layout.reset(&LayoutSettings { - x: min_x, - y: min_y, - max_width: Some(width), - max_height: Some(height), - horizontal_align: HorizontalAlign::Center, - vertical_align: VerticalAlign::Middle, - ..LayoutSettings::default() - }); - - layout.append( - fonts.as_slice(), - &TextStyle::with_user_data( - "Move your mouse over this region to make it blue!", - 20.0, - 0, - GlyphUserData { color }, - ), - ); - text_renderer - .prepare( - &device, - &queue, - &mut atlas, - Resolution { - width: config.width, - height: config.height, - }, - &fonts, - &[(&layout, TextOverflow::Hide)], - ) - .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: true, - }, - })], - depth_stencil_attachment: None, - }); - - text_renderer.render(&atlas, &mut pass).unwrap(); - } - - queue.submit(Some(encoder.finish())); - frame.present(); - } - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - Event::MainEventsCleared => { - window.request_redraw(); - } - _ => {} - } - }); -} diff --git a/src/lib.rs b/src/lib.rs index 86d70e0..2924f9e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,11 +6,12 @@ mod text_atlas; mod text_render; pub use error::{PrepareError, RenderError}; -pub use fontdue; use recently_used::RecentlyUsedMap; pub use text_atlas::TextAtlas; pub use text_render::TextRenderer; +pub use cosmic_text; + /// The color to use when rendering text. #[repr(C)] #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/src/text_atlas.rs b/src/text_atlas.rs index e6804b6..b7acca0 100644 --- a/src/text_atlas.rs +++ b/src/text_atlas.rs @@ -1,5 +1,5 @@ +use cosmic_text::CacheKey; use etagere::{size2, BucketedAtlasAllocator}; -use fontdue::layout::GlyphRasterConfig; use std::{borrow::Cow, mem::size_of, num::NonZeroU64, sync::Arc}; use wgpu::{ BindGroup, BindGroupEntry, BindGroupLayoutEntry, BindingResource, BindingType, BlendState, @@ -20,7 +20,7 @@ pub struct TextAtlas { pub(crate) packer: BucketedAtlasAllocator, pub(crate) width: u32, pub(crate) height: u32, - pub(crate) glyph_cache: RecentlyUsedMap, + pub(crate) glyph_cache: RecentlyUsedMap, pub(crate) params: Params, pub(crate) params_buffer: Buffer, pub(crate) pipeline: Arc, diff --git a/src/text_render.rs b/src/text_render.rs index 59a6c91..df73a82 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -1,16 +1,14 @@ +use cosmic_text::{CacheKey, TextBuffer}; use etagere::{size2, Allocation}; -use fontdue::{ - layout::{GlyphRasterConfig, Layout}, - Font, -}; -use std::{borrow::Borrow, collections::HashSet, iter, mem::size_of, num::NonZeroU32, slice}; + +use std::{collections::HashSet, iter, mem::size_of, num::NonZeroU32, slice}; use wgpu::{ Buffer, BufferDescriptor, BufferUsages, Device, Extent3d, ImageCopyTexture, ImageDataLayout, IndexFormat, Origin3d, Queue, RenderPass, TextureAspect, COPY_BUFFER_ALIGNMENT, }; use crate::{ - GlyphDetails, GlyphToRender, GpuCache, HasColor, Params, PrepareError, RenderError, Resolution, + GlyphDetails, GlyphToRender, GpuCache, Params, PrepareError, RenderError, Resolution, TextAtlas, TextOverflow, }; @@ -21,7 +19,7 @@ pub struct TextRenderer { index_buffer: Buffer, index_buffer_size: u64, vertices_to_render: u32, - glyphs_in_use: HashSet, + glyphs_in_use: HashSet, screen_resolution: Resolution, } @@ -59,14 +57,13 @@ impl TextRenderer { } /// Prepares all of the provided layouts for rendering. - pub fn prepare( + pub fn prepare<'a>( &mut self, device: &Device, queue: &Queue, atlas: &mut TextAtlas, screen_resolution: Resolution, - fonts: &[Font], - layouts: &[(impl Borrow>, TextOverflow)], + buffer: &mut TextBuffer<'a>, ) -> Result<(), PrepareError> { self.screen_resolution = screen_resolution; @@ -92,75 +89,91 @@ impl TextRenderer { self.glyphs_in_use.clear(); - for (layout, _) in layouts.iter() { - for glyph in layout.borrow().glyphs() { - self.glyphs_in_use.insert(glyph.key); + let mut buffers = [(buffer, TextOverflow::Hide)]; - let already_on_gpu = atlas.glyph_cache.contains_key(&glyph.key); - - if already_on_gpu { - continue; - } - - let font = &fonts[glyph.font_index]; - let (metrics, bitmap) = font.rasterize_config(glyph.key); - - let (gpu_cache, atlas_id) = if glyph.char_data.rasterize() { - // Find a position in the packer - let allocation = match try_allocate(atlas, metrics.width, metrics.height) { - Some(a) => a, - None => return Err(PrepareError::AtlasFull), - }; - let atlas_min = allocation.rectangle.min; - let atlas_max = allocation.rectangle.max; - - for row in 0..metrics.height { - let y_offset = atlas_min.y as usize; - let x_offset = - (y_offset + row) * atlas.width as usize + atlas_min.x as usize; - let bitmap_row = &bitmap[row * metrics.width..(row + 1) * metrics.width]; - atlas.texture_pending[x_offset..x_offset + metrics.width] - .copy_from_slice(bitmap_row); - } - - match upload_bounds.as_mut() { - Some(ub) => { - ub.x_min = ub.x_min.min(atlas_min.x as usize); - ub.x_max = ub.x_max.max(atlas_max.x as usize); - ub.y_min = ub.y_min.min(atlas_min.y as usize); - ub.y_max = ub.y_max.max(atlas_max.y as usize); - } - None => { - upload_bounds = Some(UploadBounds { - x_min: atlas_min.x as usize, - x_max: atlas_max.x as usize, - y_min: atlas_min.y as usize, - y_max: atlas_max.y as usize, - }); - } - } - - ( - GpuCache::InAtlas { - x: atlas_min.x as u16, - y: atlas_min.y as u16, - }, - Some(allocation.id), - ) - } else { - (GpuCache::SkipRasterization, None) + for (buffer, _) in buffers.iter_mut() { + for line in buffer.lines.iter() { + let layout = match line.layout_opt.as_ref() { + Some(l) => l, + None => continue, }; + for layout_line in layout { + for glyph in layout_line.glyphs.iter() { + let key = glyph.inner.0; - if !atlas.glyph_cache.contains_key(&glyph.key) { - atlas.glyph_cache.insert( - glyph.key, - GlyphDetails { - width: metrics.width as u16, - height: metrics.height as u16, - gpu_cache, - atlas_id, - }, - ); + self.glyphs_in_use.insert(key); + + let already_on_gpu = atlas.glyph_cache.contains_key(&key); + + if already_on_gpu { + continue; + } + + let image = layout_line.rasterize_glyph(glyph).unwrap(); + let bitmap = image.data.clone(); + 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) = if should_rasterize { + // Find a position in the packer + let allocation = match try_allocate(atlas, width, height) { + Some(a) => a, + None => return Err(PrepareError::AtlasFull), + }; + let atlas_min = allocation.rectangle.min; + let atlas_max = allocation.rectangle.max; + + for row in 0..height { + let y_offset = atlas_min.y as usize; + let x_offset = + (y_offset + row) * atlas.width as usize + atlas_min.x as usize; + let bitmap_row = &bitmap[row * width..(row + 1) * width]; + atlas.texture_pending[x_offset..x_offset + width] + .copy_from_slice(bitmap_row); + } + + match upload_bounds.as_mut() { + Some(ub) => { + ub.x_min = ub.x_min.min(atlas_min.x as usize); + ub.x_max = ub.x_max.max(atlas_max.x as usize); + ub.y_min = ub.y_min.min(atlas_min.y as usize); + ub.y_max = ub.y_max.max(atlas_max.y as usize); + } + None => { + upload_bounds = Some(UploadBounds { + x_min: atlas_min.x as usize, + x_max: atlas_max.x as usize, + y_min: atlas_min.y as usize, + y_max: atlas_max.y as usize, + }); + } + } + + ( + GpuCache::InAtlas { + x: atlas_min.x as u16, + y: atlas_min.y as u16, + }, + Some(allocation.id), + ) + } else { + (GpuCache::SkipRasterization, None) + }; + + if !atlas.glyph_cache.contains_key(&key) { + atlas.glyph_cache.insert( + key, + GlyphDetails { + width: width as u16, + height: height as u16, + gpu_cache, + atlas_id, + }, + ); + } + } } } } @@ -191,101 +204,111 @@ impl TextRenderer { ); } - let mut glyph_vertices = Vec::new(); - let mut glyph_indices = Vec::new(); + let mut glyph_vertices: Vec = Vec::new(); + let mut glyph_indices: Vec = Vec::new(); let mut glyphs_added = 0; - for (layout, overflow) in layouts.iter() { - let layout: &Layout = layout.borrow(); - let settings = layout.settings(); - + for (buffer, overflow) in buffers.iter() { // Note: subpixel positioning is not currently handled, so we always truncate down to // the nearest pixel. - let bounds_min_x = settings.x.trunc(); - let bounds_max_x = settings - .max_width - .map(|w| bounds_min_x + w.trunc()) - .unwrap_or(f32::MAX); - let bounds_min_y = settings.y.trunc(); - let bounds_max_y = settings - .max_height - .map(|h| bounds_min_y + h.trunc()) - .unwrap_or(f32::MAX); + let bounds_min_x = 0u32; + let bounds_max_x = u32::MAX; + let bounds_min_y = 0u32; + let bounds_max_y = u32::MAX; - for glyph in layout.glyphs() { - let mut x = glyph.x; - let mut y = glyph.y; - - let details = atlas.glyph_cache.get(&glyph.key).unwrap(); - let (mut atlas_x, mut atlas_y) = match details.gpu_cache { - GpuCache::InAtlas { x, y } => (x, y), - GpuCache::SkipRasterization => continue, + for line in buffer.lines.iter() { + let layout = match line.layout_opt.as_ref() { + Some(l) => l, + None => continue, }; + for layout_line in layout { + for glyph in layout_line.glyphs.iter() { + let key = glyph.inner.0; - let mut width = details.width as f32; - let mut height = details.height as f32; + let details = atlas.glyph_cache.get(&key).unwrap(); - 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; + let mut x = glyph.x.trunc() as u32; + let mut y: u32 = (buffer.metrics.line_height as i32 + - details.height as i32) + .try_into() + .unwrap(); + + 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; + } + } } - // 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; - } + glyph_vertices.extend( + iter::repeat(GlyphToRender { + pos: [x as i32, y as i32], + dim: [width as u16, height as u16], + uv: [atlas_x, atlas_y], + color: [255, 0, 255, 255], + }) + .take(4), + ); - // Clip left ege - if x < bounds_min_x { - let right_shift = bounds_min_x - x; + let start = 4 * glyphs_added as u32; + glyph_indices.extend([ + start, + start + 1, + start + 2, + start, + start + 2, + start + 3, + ]); - 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; - } + glyphs_added += 1; } } - - let color = glyph.user_data.color(); - - glyph_vertices.extend( - iter::repeat(GlyphToRender { - pos: [x as i32, y as i32], - dim: [width as u16, height as u16], - uv: [atlas_x, atlas_y], - color: [color.r, color.g, color.b, color.a], - }) - .take(4), - ); - - let start = 4 * glyphs_added as u32; - glyph_indices.extend([start, start + 1, start + 2, start, start + 2, start + 3]); - - glyphs_added += 1; } }