improved text rendering, again

This commit is contained in:
Daniel Bulant 2024-02-24 23:44:39 +01:00
parent 24481bed2a
commit 455fad5269
7 changed files with 115 additions and 188 deletions

View file

@ -1,81 +0,0 @@
use std::sync::{Arc, Mutex, RwLock};
use mangui::{SharedNode, nodes::{layout::Layout, Style}, taffy::prelude::Size};
use rusalka::{component::Component, nodes::{primitives::{Rectangle, RectangleAttributes}, insert, detach}, SharedComponent, WeakSharedComponent, invalidator::Invalidator};
pub struct DemoComponent {
rect: SharedComponent<Rectangle>,
attrs: DemoComponentAttributes,
layout: Arc<RwLock<Layout>>,
selfref: WeakSharedComponent<Self>,
test: Arc<Mutex<Invalidator<bool>>>
}
#[derive(Default)]
pub struct DemoComponentAttributes {}
#[derive(Default)]
pub struct PartialDemoComponentAttributes {}
impl From<DemoComponentAttributes> for PartialDemoComponentAttributes {
fn from(_attrs: DemoComponentAttributes) -> Self {
Self {}
}
}
impl Component for DemoComponent {
type ComponentAttrs = DemoComponentAttributes;
type PartialComponentAttrs = PartialDemoComponentAttributes;
fn new(attrs: Self::ComponentAttrs, selfref: WeakSharedComponent<Self>) -> Self {
let test = Arc::new(Mutex::new(Invalidator::new(false)));
let this = Self {
rect: Arc::new_cyclic(|selfref| Mutex::new(Rectangle::new(RectangleAttributes { ..Default::default() }, selfref.clone()))),
layout: Arc::new(RwLock::new(Layout {
style: Style {
layout: mangui::nodes::TaffyStyle {
min_size: Size {
width: mangui::taffy::style::Dimension::Length( if **test.lock().unwrap() { 50. } else { 100. }),
height: mangui::taffy::style::Dimension::Length(100.)
},
..Default::default()
},
..Default::default()
},
..Default::default()
})),
attrs,
selfref,
test
};
let selfref = this.selfref.clone();
this.layout.write().unwrap().events.add_handler(Box::new(move |event| {
let selfref = selfref.upgrade().unwrap();
let this = selfref.lock().unwrap();
let attrs = &this.attrs;
let test = &this.test;
match event.event {
mangui::events::InnerEvent::MouseDown(_) => {
**test.lock().unwrap() = true;
},
mangui::events::InnerEvent::MouseUp(_) => {
**test.lock().unwrap() = false;
},
_ => {}
}
}));
this
}
fn set(&mut self, _attrs: Self::PartialComponentAttrs) { }
fn get(&self) -> &Self::ComponentAttrs { &self.attrs }
fn mount(&self, parent: &SharedNode, before: Option<&SharedNode>) {
insert(parent, &{self.layout.clone()}, before);
self.rect.lock().unwrap().mount(&{self.layout.clone()}, None);
}
fn unmount(&self) {
self.rect.lock().unwrap().unmount();
detach(&{self.layout.clone()});
}
fn update(&self, _bitmap: &[u32]) {}
}

View file

@ -3,6 +3,7 @@ use std::default::Default;
use mangui::{femtovg::{ImageFlags, Color, Paint}, cosmic_text::Metrics, nodes::{layout::Layout, Style}, nodes::text::Text, nodes::image::Image, taffy::prelude::Size};
use mangui::nodes::TaffyStyle;
use mangui::taffy::Display::Block;
use mangui::taffy::{FlexDirection, LengthPercentage, Rect};
use rusalka::nodes::primitives::{Rectangle, RectangleAttributes, PartialRectangleAttributes};
@ -27,10 +28,27 @@ make_component!(
Component {
@layout {
@Rectangle {
radius: if $test_ { attrs.radius } else { 0. },
..Default::default()
radius: if $test_ { attrs.radius } else { 0. }
}
@layout {
@text {
text: String::from("Hello, World 🌎! And there's more text in here, as a single line"),
metrics: Metrics::new(20., 25.),
paint: Paint::color(Color::rgb(0, 255, 0)),
style: Style {
layout: TaffyStyle {
padding: Rect {
top: LengthPercentage::Length(10.),
left: LengthPercentage::Length(10.),
right: LengthPercentage::Length(10.),
bottom: LengthPercentage::Length(10.)
},
..Default::default()
},
..Default::default()
},
..Default::default()
}
@image {
style: Style {
layout: TaffyStyle {
@ -46,16 +64,11 @@ make_component!(
radius: 5.,
..Default::default()
}
@text {
text: String::from("Hello, World!"),
metrics: Metrics::new(20., 25.),
paint: Paint::color(Color::rgb(0, 255, 0)),
..Default::default()
}
style: Style {
layout: TaffyStyle {
display: Block,
flex_direction: FlexDirection::Column,
..Default::default()
},
..Default::default()

View file

@ -1,8 +1,7 @@
use std::sync::{RwLock, Arc, mpsc, Mutex};
use mangui::{self, nodes::{layout::Layout, self, Style, TaffyStyle}, taffy::{self, prelude::Size, style::Dimension}, femtovg::{Paint, Color}, SharedNode, MainEntry, events::NodeEvent};
use mangui::{self, nodes::layout::Layout, SharedNode, MainEntry};
mod component_demo;
mod component_demo_syntax;
mod anilist;
@ -11,38 +10,7 @@ use rusalka::component::Component;
fn main() {
let (tx, rx) = mpsc::channel();
let _tx = Arc::new(tx);
let mut root = Layout::default();
root.style.layout.display = taffy::style::Display::Flex;
root.style.layout.flex_direction = taffy::style::FlexDirection::Row;
let right_node = Arc::new(RwLock::new(nodes::primitives::Rectangle {
style: Style {
layout: TaffyStyle {
min_size: Size {
width: Dimension::Length(50.),
height: Dimension::Length(100.)
},
..Default::default()
},
cursor: Default::default()
},
fill: Paint::color(Color::rgb(0, 0, 255)),
radius: 0.,
events: Default::default(),
parent: None
}));
root.children.push(right_node.clone());
right_node.clone().write().unwrap().events.add_handler(Box::new(move |event| {
let NodeEvent { target, path, event } = event;
match event {
mangui::events::InnerEvent::MouseDown(_) => {
right_node.write().unwrap().fill = Paint::color(Color::rgb(255, 0, 255));
},
mangui::events::InnerEvent::MouseUp(_) => {
right_node.write().unwrap().fill = Paint::color(Color::rgb(0, 0, 255));
},
_ => {}
}
}));
let root = Layout::default();
let groot: SharedNode = Arc::new(RwLock::new(root));
let cdemo: Arc<Mutex<component_demo_syntax::ComponentDemo>> = Arc::new_cyclic(|cself|

View file

@ -2,7 +2,6 @@ use std::collections::HashMap;
use std::num::NonZeroU32;
use std::ops::Deref;
use std::sync::{Arc, Mutex, RwLock, Weak};
use std::time::Duration;
use cosmic_text::FontSystem;
use events::{Location, MouseValue, NodeEvent, MouseEvent};
@ -14,7 +13,7 @@ use glutin_winit::DisplayBuilder;
use nodes::{get_element_at, run_event_handlers, run_single_event_handlers};
use raw_window_handle::HasRawWindowHandle;
use winit::event::{Event, WindowEvent, Modifiers, DeviceId};
use winit::event_loop::{ControlFlow, EventLoop};
use winit::event_loop::EventLoop;
use winit::window::WindowBuilder;
use winit::{dpi::PhysicalSize, window::Window};
@ -79,8 +78,8 @@ pub fn run_event_loop(entry: MainEntry) -> () {
let mut taffy = TaffyTree::new();
let mut taffy_map = NodeLayoutMap::new();
{
let clonned = entry.root.clone();
let root = clonned.read().unwrap();
let cloned = entry.root.clone();
let root = cloned.read().unwrap();
let root_style = root.deref().style();
let root_layout = root_style.layout.to_owned();
let taffy_root_node = taffy.new_leaf(root_layout).unwrap();

View file

@ -122,6 +122,7 @@ pub trait Node: Debug {
/// Render the node, called before rendering it's children
/// Canvas considers 0, 0 to be top left corner (for location after layouting happens)
/// Top left corner is after margin and similar, but before padding and border.
fn render_pre_children(&mut self, _context: &mut RenderContext, _layout: Layout) {}
/// Render the node, called after rendering it's children
/// Canvas considers 0, 0 to be top left corner (for location after layouting happens)

View file

@ -1,9 +1,10 @@
use std::fmt::Debug;
use crate::{events::handler::EventHandlerDatabase, SharedNode, WeakNode, FONT_SYSTEM};
use super::{text_render_cache::RENDER_CACHE, Node, NodeChildren, Style, MeasureContext, RenderContext};
use cosmic_text::{Attrs, Buffer, Metrics, Shaping};
use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Stretch};
use femtovg::{Color, Paint, Path};
use taffy::{AvailableSpace, Size};
use crate::nodes::text_render_cache::TextConfig;
#[derive(Debug, Default)]
pub struct Text {
@ -38,7 +39,12 @@ impl Node for Text {
// this can crash, but it should crash earlier during measure -> see the comment there.
let buf = self.buffer.as_mut().unwrap();
let mut font = FONT_SYSTEM.lock().unwrap();
buf.set_size(&mut font, layout.size.width, layout.size.height);
let offset_size = (
layout.padding.left + layout.padding.right + layout.border.left + layout.border.right,
layout.padding.top + layout.padding.bottom + layout.border.top + layout.border.bottom
);
// the height * scale factor is an ugly hack to fix height of the text... not sure why it's wrong in the first place
buf.set_size(&mut font, layout.content_size.width - offset_size.0, (layout.content_size.height * context.scale_factor) - offset_size.1);
buf.set_metrics(&mut font, self.metrics.scale(context.scale_factor));
// fill_to_cmds requires FONT_SYSTEM lock.
drop(font);
@ -54,10 +60,26 @@ impl Node for Text {
&path,
&Paint::color(Color::rgb(255, 0, 0))
);
let position = (
layout.padding.left + layout.border.left,
layout.padding.top + layout.border.top
);
let mut path = Path::new();
path.rounded_rect(
position.0,
position.1,
layout.content_size.width - offset_size.0,
layout.content_size.height - offset_size.1,
0.
);
context.canvas.fill_path(
&path,
&Paint::color(Color::rgb(0, 0, 255))
);
let cmds = RENDER_CACHE.lock().unwrap()
.fill_to_cmds(&mut context.canvas, buf, (0.0, 0.0), context.scale_factor)
.fill_to_cmds(&mut context.canvas, buf, position, context.scale_factor, TextConfig { hint: false, subpixel: false })
.unwrap();
context.canvas.draw_glyph_commands(cmds, &self.paint, 1.0);
context.canvas.draw_glyph_commands(cmds, &self.paint, context.scale_factor);
}
fn measure(&mut self, context: &mut MeasureContext, known_dimensions: Size<Option<f32>>, available_space: Size<AvailableSpace>) -> Size<f32> {
@ -74,7 +96,7 @@ impl Node for Text {
buf.set_metrics(&mut font, self.metrics.scale(context.scale_factor));
// Compute layout
buf.shape_until_scroll(&mut font, false);
buf.shape_until_scroll(&mut font, true);
drop(font);
// Determine measured size of text
@ -82,7 +104,7 @@ impl Node for Text {
.layout_runs()
.fold((0.0, 0usize), |(width, total_lines), run| (run.line_w.max(width), total_lines + 1));
// fixes text not rendering in some cases (??????)
let height = total_lines as f32 * buf.metrics().line_height + 1.0;
let height = (total_lines as f32 * buf.metrics().line_height + 1.0) / context.scale_factor;
// fixes flickering of text on devices with non-integer scale factors due to loss of precision
let width = width + 0.5;

View file

@ -24,6 +24,12 @@ pub struct FontTexture {
image_id: ImageId
}
#[derive(Default, Debug, Clone, Copy)]
pub struct TextConfig {
pub hint: bool,
pub subpixel: bool,
}
#[derive(Copy, Clone, Debug)]
pub struct RenderedGlyph {
texture_index: usize,
@ -57,25 +63,19 @@ impl RenderCache {
buffer: &Buffer,
position: (f32, f32),
scale: f32,
config: TextConfig
) -> 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;
let lines = buffer.layout_runs().filter(|run| run.line_w != 0.0).count();
let total_height = lines as f32 * 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;
let physical_glyph = glyph.physical(position, scale);
let mut cache_key = physical_glyph.cache_key;
// perform cache lookup for rendered glyph
if let Some(rendered) = self.rendered_glyphs.entry(cache_key).or_insert_with(|| {
let Some(rendered) = self.rendered_glyphs.entry(cache_key).or_insert_with(|| {
// ...or insert it
// do the actual rasterization
@ -86,7 +86,7 @@ impl RenderCache {
.scale_context
.builder(font.as_swash())
.size(f32::from_bits(cache_key.font_size_bits))
.hint(true)
.hint(config.hint)
.build();
let offset = Vector::new(cache_key.x_bin.as_float(), cache_key.y_bin.as_float());
let rendered = Render::new(&[
@ -94,7 +94,7 @@ impl RenderCache {
Source::ColorBitmap(StrikeWith::BestFit),
Source::Outline,
])
.format(Format::Alpha)
.format(if config.subpixel { Format::Subpixel } else { Format::Alpha })
.offset(offset)
.render(&mut scaler, cache_key.glyph_id);
@ -114,26 +114,32 @@ impl RenderCache {
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,
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(),
)
.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)
});
.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;
@ -174,33 +180,32 @@ impl RenderCache {
color_glyph: matches!(rendered.content, Content::Color),
}
})
}) {
let cmd_map = if rendered.color_glyph {
&mut color_cmd_map
} else {
&mut alpha_cmd_map
};
}) else { continue };
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 cmd_map = if rendered.color_glyph {
&mut color_cmd_map
} else {
&mut alpha_cmd_map
};
let mut q = Quad::default();
let it = 1.0 / TEXTURE_SIZE as f32;
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(),
});
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;
let mut q = Quad::default();
let it = 1.0 / TEXTURE_SIZE as f32;
q.x0 = (physical_glyph.x + rendered.offset_x - GLYPH_PADDING as i32) as f32;
q.y0 = (physical_glyph.y - rendered.offset_y - GLYPH_PADDING as i32) as f32 + run.line_y;
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;
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);
}
cmd.quads.push(q);
}
}