add text rendering

This commit is contained in:
Daniel Bulant 2024-02-22 13:27:10 +01:00
parent 8a63a219c5
commit 50c7e5f474
5 changed files with 296 additions and 4 deletions

View file

@ -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(_) => {

View file

@ -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<RwLock<dyn Node>>;
@ -41,6 +43,10 @@ type WeakNode = Weak<RwLock<dyn Node>>;
type NodePtr = Option<Vec<WeakNode>>;
type NodeLayoutMap = PtrWeakKeyHashMap<Weak<RwLock<dyn Node>>, taffy::node::Node>;
lazy_static::lazy_static! {
pub static ref FONT_SYSTEM: Mutex<FontSystem> = 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 => {

View file

@ -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 {

55
ui/src/nodes/text.rs Normal file
View file

@ -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<WeakNode>,
pub metrics: Metrics,
pub buffer: Option<Buffer>,
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<crate::events::handler::InnerEventHandlerDataset> {
Some(self.events.handlers.clone())
}
fn set_parent(&mut self, parent: Option<WeakNode>) {
self.parent = parent;
}
fn parent(&self) -> Option<SharedNode> {
match &self.parent {
Some(parent) => parent.upgrade(),
None => None
}
}
}

View file

@ -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<CacheKey, Option<RenderedGlyph>>,
glyph_textures: Vec<FontTexture>,
}
lazy_static::lazy_static! {
pub static ref RENDER_CACHE: Mutex<RenderCache> = 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<GlyphDrawCommands, ErrorKind> {
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::<ImageSource>(
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(),
})
}
}