mirror of
https://github.com/danbulant/mangui
synced 2026-06-19 22:31:03 +00:00
add text rendering
This commit is contained in:
parent
8a63a219c5
commit
50c7e5f474
5 changed files with 296 additions and 4 deletions
|
|
@ -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(_) => {
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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
55
ui/src/nodes/text.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
210
ui/src/nodes/text_render_cache.rs
Normal file
210
ui/src/nodes/text_render_cache.rs
Normal 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue