From 50c7e5f474fde5bfc156021804091cb4401751c0 Mon Sep 17 00:00:00 2001 From: Daniel Bulant Date: Thu, 22 Feb 2024 13:27:10 +0100 Subject: [PATCH] add text rendering --- mangades/src/component_demo_syntax.rs | 18 ++- ui/src/lib.rs | 12 +- ui/src/nodes/mod.rs | 5 +- ui/src/nodes/text.rs | 55 +++++++ ui/src/nodes/text_render_cache.rs | 210 ++++++++++++++++++++++++++ 5 files changed, 296 insertions(+), 4 deletions(-) create mode 100644 ui/src/nodes/text.rs create mode 100644 ui/src/nodes/text_render_cache.rs diff --git a/mangades/src/component_demo_syntax.rs b/mangades/src/component_demo_syntax.rs index 260fca3..fa2e9ab 100644 --- a/mangades/src/component_demo_syntax.rs +++ b/mangades/src/component_demo_syntax.rs @@ -1,6 +1,6 @@ use rusalka_macro::make_component; use std::default::Default; -use mangui::{femtovg::ImageFlags, nodes::{layout::Layout, Style}, nodes::image::Image, taffy::prelude::Size}; +use mangui::{femtovg::{ImageFlags, Color, Paint}, cosmic_text::Metrics, nodes::{layout::Layout, Style}, nodes::text::Text, nodes::image::Image, taffy::prelude::Size}; use rusalka::nodes::primitives::{Rectangle, RectangleAttributes, PartialRectangleAttributes}; @@ -46,6 +46,22 @@ make_component!( events: Default::default(), parent: None } + @text { + text: String::from("Hello, World!"), + metrics: Metrics::new(20., 25.), + paint: Paint::color(Color::rgb(0, 255, 0)), + style: Style { + layout: mangui::nodes::TaffyStyle { + min_size: Size { + width: mangui::taffy::style::Dimension::Points(200.), + height: mangui::taffy::style::Dimension::Points(40.) + }, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + } $|event| { match event.event { mangui::events::InnerEvent::MouseDown(_) => { diff --git a/ui/src/lib.rs b/ui/src/lib.rs index d191c3b..03f7547 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -1,9 +1,10 @@ use std::collections::HashMap; use std::num::NonZeroU32; use std::ops::Deref; -use std::sync::{Arc, RwLock, Weak}; +use std::sync::{Arc, Mutex, RwLock, Weak}; use std::time::Duration; +use cosmic_text::FontSystem; use events::{Location, MouseValue, NodeEvent, MouseEvent}; use femtovg::renderer::OpenGl; use femtovg::{Canvas, Color}; @@ -34,6 +35,7 @@ pub mod nodes; pub mod events; pub use taffy; pub use femtovg; +pub use cosmic_text; pub type CurrentRenderer = OpenGl; pub type SharedNode = Arc>; @@ -41,6 +43,10 @@ type WeakNode = Weak>; type NodePtr = Option>; type NodeLayoutMap = PtrWeakKeyHashMap>, taffy::node::Node>; +lazy_static::lazy_static! { + pub static ref FONT_SYSTEM: Mutex = Mutex::new(FontSystem::new()); +} + /// The entry point of the UI. pub struct MainEntry { /// The root node of the UI @@ -88,7 +94,8 @@ pub fn run_event_loop(entry: MainEntry) -> () { node_layout: taffy_map, taffy, mouse: None, - keyboard_focus: None + keyboard_focus: None, + scale_factor: window.scale_factor() as f32 }; let root = entry.root.clone(); @@ -238,6 +245,7 @@ pub fn run_event_loop(entry: MainEntry) -> () { groot.resize(size.width as f32, size.height as f32); drop(groot); window.request_redraw(); + context.scale_factor = window.scale_factor() as f32; should_recompute = true; }, WindowEvent::RedrawRequested => { diff --git a/ui/src/nodes/mod.rs b/ui/src/nodes/mod.rs index 5c30187..30101f3 100644 --- a/ui/src/nodes/mod.rs +++ b/ui/src/nodes/mod.rs @@ -1,6 +1,8 @@ pub mod layout; pub mod primitives; pub mod image; +pub mod text; +pub mod text_render_cache; use std::fmt::Debug; use std::sync::Arc; @@ -20,7 +22,8 @@ pub struct RenderContext { pub node_layout: NodeLayoutMap, pub taffy: Taffy, pub mouse: NodePtr, - pub keyboard_focus: NodePtr + pub keyboard_focus: NodePtr, + pub scale_factor: f32 } impl RenderContext { diff --git a/ui/src/nodes/text.rs b/ui/src/nodes/text.rs new file mode 100644 index 0000000..9654fa9 --- /dev/null +++ b/ui/src/nodes/text.rs @@ -0,0 +1,55 @@ +use std::fmt::Debug; +use crate::{events::handler::EventHandlerDatabase, SharedNode, WeakNode, FONT_SYSTEM}; +use super::{text_render_cache::RENDER_CACHE, Node, NodeChildren, Style}; +use cosmic_text::{Attrs, Buffer, Metrics, Shaping}; +use femtovg::Paint; + +#[derive(Debug, Default)] +pub struct Text { + pub style: Style, + pub text: String, + pub events: EventHandlerDatabase, + pub parent: Option, + pub metrics: Metrics, + pub buffer: Option, + pub paint: Paint +} + +impl Node for Text { + fn style(&self) -> &Style { + &self.style + } + + fn children(&self) -> Option<&NodeChildren> { + None + } + + fn render_pre_children(&mut self, context: &mut super::RenderContext, layout: taffy::prelude::Layout) { + if let None = self.buffer { + self.buffer = Some(Buffer::new(&mut FONT_SYSTEM.lock().unwrap(), self.metrics)); + } + let buf = self.buffer.as_mut().unwrap(); + let mut font = FONT_SYSTEM.lock().unwrap(); + buf.set_text(&mut font, &self.text, Attrs::new(), Shaping::Advanced); + buf.set_size(&mut font, layout.size.width, layout.size.height); + buf.set_metrics(&mut font, self.metrics.scale(context.scale_factor)); + drop(font); + let cmds = RENDER_CACHE.lock().unwrap() + .fill_to_cmds(&mut context.canvas, buf, (0.0, 0.0), context.scale_factor) + .unwrap(); + context.canvas.draw_glyph_commands(cmds, &self.paint, 1.0); + } + + fn event_handlers(&self) -> Option { + Some(self.events.handlers.clone()) + } + fn set_parent(&mut self, parent: Option) { + self.parent = parent; + } + fn parent(&self) -> Option { + match &self.parent { + Some(parent) => parent.upgrade(), + None => None + } + } +} \ No newline at end of file diff --git a/ui/src/nodes/text_render_cache.rs b/ui/src/nodes/text_render_cache.rs new file mode 100644 index 0000000..6cb30dd --- /dev/null +++ b/ui/src/nodes/text_render_cache.rs @@ -0,0 +1,210 @@ +use cosmic_text::{Buffer, CacheKey, SubpixelBin}; +use femtovg::{ + Atlas, DrawCommand, ErrorKind, GlyphDrawCommands, ImageFlags, ImageId, + ImageSource, Quad +}; +use std::{collections::HashMap, sync::Mutex}; +use femtovg::imgref::{Img, ImgRef}; +use femtovg::rgb::RGBA8; +use swash::scale::image::Content; +use swash::scale::{Render, ScaleContext, Source, StrikeWith}; +use swash::zeno::{Format, Vector}; + +use crate::FONT_SYSTEM; + +use super::CanvasRenderer; + + +const GLYPH_PADDING: u32 = 1; +const GLYPH_MARGIN: u32 = 1; +const TEXTURE_SIZE: usize = 512; + +pub struct FontTexture { + atlas: Atlas, + image_id: ImageId +} + +#[derive(Copy, Clone, Debug)] +pub struct RenderedGlyph { + texture_index: usize, + width: u32, + height: u32, + offset_x: i32, + offset_y: i32, + atlas_x: u32, + atlas_y: u32, + color_glyph: bool, +} + +#[derive(Default)] +pub struct RenderCache { + scale_context: ScaleContext, + rendered_glyphs: HashMap>, + glyph_textures: Vec, +} + + +lazy_static::lazy_static! { + pub static ref RENDER_CACHE: Mutex = Mutex::new(RenderCache::default()); +} + +impl RenderCache { + pub(crate) fn fill_to_cmds( + &mut self, + canvas: &mut CanvasRenderer, + buffer: &Buffer, + position: (f32, f32), + scale: f32, + ) -> Result { + let mut alpha_cmd_map = HashMap::new(); + let mut color_cmd_map = HashMap::new(); + + //let total_height = buffer.layout_runs().len() as i32 * buffer.metrics().line_height; + for run in buffer.layout_runs() { + for glyph in run.glyphs.iter() { + let physical = glyph.physical(position, scale); + let mut cache_key = physical.cache_key; + let position_x = position.0 + cache_key.x_bin.as_float(); + let position_y = position.1 + cache_key.y_bin.as_float(); + //let position_x = position_x - run.line_w * justify.0; + //let position_y = position_y - total_height as f32 * justify.1; + let (position_x, subpixel_x) = SubpixelBin::new(position_x); + let (position_y, subpixel_y) = SubpixelBin::new(position_y); + cache_key.x_bin = subpixel_x; + cache_key.y_bin = subpixel_y; + // perform cache lookup for rendered glyph + if let Some(rendered) = self.rendered_glyphs.entry(cache_key).or_insert_with(|| { + // ...or insert it + + // do the actual rasterization + let font = FONT_SYSTEM.lock().unwrap() + .get_font(cache_key.font_id) + .expect("Shaped a nonexistent font. What?"); + let mut scaler = self + .scale_context + .builder(font.as_swash()) + .size(f32::from_bits(cache_key.font_size_bits)) + .hint(true) + .build(); + let offset = Vector::new(cache_key.x_bin.as_float(), cache_key.y_bin.as_float()); + let rendered = Render::new(&[ + Source::ColorOutline(0), + Source::ColorBitmap(StrikeWith::BestFit), + Source::Outline, + ]) + .format(Format::Alpha) + .offset(offset) + .render(&mut scaler, cache_key.glyph_id); + + // upload it to the GPU + rendered.map(|rendered| { + // pick an atlas texture for our glyph + let content_w = rendered.placement.width as usize; + let content_h = rendered.placement.height as usize; + let alloc_w = rendered.placement.width + (GLYPH_MARGIN + GLYPH_PADDING) * 2; + let alloc_h = rendered.placement.height + (GLYPH_MARGIN + GLYPH_PADDING) * 2; + let used_w = rendered.placement.width + GLYPH_PADDING * 2; + let used_h = rendered.placement.height + GLYPH_PADDING * 2; + let mut found = None; + for (texture_index, glyph_atlas) in self.glyph_textures.iter_mut().enumerate() { + if let Some((x, y)) = glyph_atlas.atlas.add_rect(alloc_w as usize, alloc_h as usize) { + found = Some((texture_index, x, y)); + break; + } + } + let (texture_index, atlas_alloc_x, atlas_alloc_y) = found.unwrap_or_else(|| { + // if no atlas could fit the texture, make a new atlas tyvm + // TODO error handling + let mut atlas = Atlas::new(TEXTURE_SIZE, TEXTURE_SIZE); + let image_id = canvas + .create_image( + Img::new( + vec![RGBA8::new(0, 0, 0, 0); TEXTURE_SIZE * TEXTURE_SIZE], + TEXTURE_SIZE, + TEXTURE_SIZE, + ) + .as_ref(), + ImageFlags::empty(), + ) + .unwrap(); + let texture_index = self.glyph_textures.len(); + let (x, y) = atlas.add_rect(alloc_w as usize, alloc_h as usize).unwrap(); + self.glyph_textures.push(FontTexture { atlas, image_id }); + (texture_index, x, y) + }); + + let atlas_used_x = atlas_alloc_x as u32 + GLYPH_MARGIN; + let atlas_used_y = atlas_alloc_y as u32 + GLYPH_MARGIN; + let atlas_content_x = atlas_alloc_x as u32 + GLYPH_MARGIN + GLYPH_PADDING; + let atlas_content_y = atlas_alloc_y as u32 + GLYPH_MARGIN + GLYPH_PADDING; + + let mut src_buf = Vec::with_capacity(content_w * content_h); + match rendered.content { + Content::Mask => { + for chunk in rendered.data.chunks_exact(1) { + src_buf.push(RGBA8::new(chunk[0], 0, 0, 0)); + } + } + Content::Color => { + for chunk in rendered.data.chunks_exact(4) { + src_buf.push(RGBA8::new(chunk[0], chunk[1], chunk[2], chunk[3])); + } + } + Content::SubpixelMask => unreachable!(), + } + canvas + .update_image::( + self.glyph_textures[texture_index].image_id, + ImgRef::new(&src_buf, content_w, content_h).into(), + atlas_content_x as usize, + atlas_content_y as usize, + ) + .unwrap(); + + RenderedGlyph { + texture_index, + width: used_w, + height: used_h, + offset_x: rendered.placement.left, + offset_y: rendered.placement.top, + atlas_x: atlas_used_x, + atlas_y: atlas_used_y, + color_glyph: matches!(rendered.content, Content::Color), + } + }) + }) { + let cmd_map = if rendered.color_glyph { + &mut color_cmd_map + } else { + &mut alpha_cmd_map + }; + + let cmd = cmd_map.entry(rendered.texture_index).or_insert_with(|| DrawCommand { + image_id: self.glyph_textures[rendered.texture_index].image_id, + quads: Vec::new(), + }); + + let mut q = Quad::default(); + let it = 1.0 / TEXTURE_SIZE as f32; + + q.x0 = (position_x + glyph.x as i32 + rendered.offset_x - GLYPH_PADDING as i32) as f32; + q.y0 = (position_y + run.line_y as i32 + glyph.y as i32 - rendered.offset_y - GLYPH_PADDING as i32) as f32; + q.x1 = q.x0 + rendered.width as f32; + q.y1 = q.y0 + rendered.height as f32; + + q.s0 = rendered.atlas_x as f32 * it; + q.t0 = rendered.atlas_y as f32 * it; + q.s1 = (rendered.atlas_x + rendered.width) as f32 * it; + q.t1 = (rendered.atlas_y + rendered.height) as f32 * it; + + cmd.quads.push(q); + } + } + } + + Ok(GlyphDrawCommands { + alpha_glyphs: alpha_cmd_map.into_values().collect(), + color_glyphs: color_cmd_map.into_values().collect(), + }) + } +} \ No newline at end of file