mirror of
https://github.com/danbulant/mangui
synced 2026-07-08 04:10:44 +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 rusalka_macro::make_component;
|
||||||
use std::default::Default;
|
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};
|
use rusalka::nodes::primitives::{Rectangle, RectangleAttributes, PartialRectangleAttributes};
|
||||||
|
|
||||||
|
|
@ -46,6 +46,22 @@ make_component!(
|
||||||
events: Default::default(),
|
events: Default::default(),
|
||||||
parent: None
|
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| {
|
$|event| {
|
||||||
match event.event {
|
match event.event {
|
||||||
mangui::events::InnerEvent::MouseDown(_) => {
|
mangui::events::InnerEvent::MouseDown(_) => {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::num::NonZeroU32;
|
use std::num::NonZeroU32;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::sync::{Arc, RwLock, Weak};
|
use std::sync::{Arc, Mutex, RwLock, Weak};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use cosmic_text::FontSystem;
|
||||||
use events::{Location, MouseValue, NodeEvent, MouseEvent};
|
use events::{Location, MouseValue, NodeEvent, MouseEvent};
|
||||||
use femtovg::renderer::OpenGl;
|
use femtovg::renderer::OpenGl;
|
||||||
use femtovg::{Canvas, Color};
|
use femtovg::{Canvas, Color};
|
||||||
|
|
@ -34,6 +35,7 @@ pub mod nodes;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
pub use taffy;
|
pub use taffy;
|
||||||
pub use femtovg;
|
pub use femtovg;
|
||||||
|
pub use cosmic_text;
|
||||||
|
|
||||||
pub type CurrentRenderer = OpenGl;
|
pub type CurrentRenderer = OpenGl;
|
||||||
pub type SharedNode = Arc<RwLock<dyn Node>>;
|
pub type SharedNode = Arc<RwLock<dyn Node>>;
|
||||||
|
|
@ -41,6 +43,10 @@ type WeakNode = Weak<RwLock<dyn Node>>;
|
||||||
type NodePtr = Option<Vec<WeakNode>>;
|
type NodePtr = Option<Vec<WeakNode>>;
|
||||||
type NodeLayoutMap = PtrWeakKeyHashMap<Weak<RwLock<dyn Node>>, taffy::node::Node>;
|
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.
|
/// The entry point of the UI.
|
||||||
pub struct MainEntry {
|
pub struct MainEntry {
|
||||||
/// The root node of the UI
|
/// The root node of the UI
|
||||||
|
|
@ -88,7 +94,8 @@ pub fn run_event_loop(entry: MainEntry) -> () {
|
||||||
node_layout: taffy_map,
|
node_layout: taffy_map,
|
||||||
taffy,
|
taffy,
|
||||||
mouse: None,
|
mouse: None,
|
||||||
keyboard_focus: None
|
keyboard_focus: None,
|
||||||
|
scale_factor: window.scale_factor() as f32
|
||||||
};
|
};
|
||||||
let root = entry.root.clone();
|
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);
|
groot.resize(size.width as f32, size.height as f32);
|
||||||
drop(groot);
|
drop(groot);
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
|
context.scale_factor = window.scale_factor() as f32;
|
||||||
should_recompute = true;
|
should_recompute = true;
|
||||||
},
|
},
|
||||||
WindowEvent::RedrawRequested => {
|
WindowEvent::RedrawRequested => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
pub mod primitives;
|
pub mod primitives;
|
||||||
pub mod image;
|
pub mod image;
|
||||||
|
pub mod text;
|
||||||
|
pub mod text_render_cache;
|
||||||
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -20,7 +22,8 @@ pub struct RenderContext {
|
||||||
pub node_layout: NodeLayoutMap,
|
pub node_layout: NodeLayoutMap,
|
||||||
pub taffy: Taffy,
|
pub taffy: Taffy,
|
||||||
pub mouse: NodePtr,
|
pub mouse: NodePtr,
|
||||||
pub keyboard_focus: NodePtr
|
pub keyboard_focus: NodePtr,
|
||||||
|
pub scale_factor: f32
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderContext {
|
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