From 455fad5269cd98269cb46f88b3a511a615e19c47 Mon Sep 17 00:00:00 2001 From: Daniel Bulant Date: Sat, 24 Feb 2024 23:44:39 +0100 Subject: [PATCH] improved text rendering, again --- mangades/src/component_demo.rs | 81 ------------------ mangades/src/component_demo_syntax.rs | 29 +++++-- mangades/src/main.rs | 36 +------- ui/src/lib.rs | 7 +- ui/src/nodes/mod.rs | 1 + ui/src/nodes/text.rs | 34 ++++++-- ui/src/nodes/text_render_cache.rs | 115 ++++++++++++++------------ 7 files changed, 115 insertions(+), 188 deletions(-) delete mode 100644 mangades/src/component_demo.rs diff --git a/mangades/src/component_demo.rs b/mangades/src/component_demo.rs deleted file mode 100644 index 8361923..0000000 --- a/mangades/src/component_demo.rs +++ /dev/null @@ -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, - attrs: DemoComponentAttributes, - layout: Arc>, - selfref: WeakSharedComponent, - test: Arc>> -} - -#[derive(Default)] -pub struct DemoComponentAttributes {} -#[derive(Default)] -pub struct PartialDemoComponentAttributes {} - -impl From 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 { - 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]) {} -} diff --git a/mangades/src/component_demo_syntax.rs b/mangades/src/component_demo_syntax.rs index 7d2ba05..8eaa8ce 100644 --- a/mangades/src/component_demo_syntax.rs +++ b/mangades/src/component_demo_syntax.rs @@ -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() diff --git a/mangades/src/main.rs b/mangades/src/main.rs index a138b7f..20babe6 100644 --- a/mangades/src/main.rs +++ b/mangades/src/main.rs @@ -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> = Arc::new_cyclic(|cself| diff --git a/ui/src/lib.rs b/ui/src/lib.rs index 0bbd836..2760abf 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -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(); diff --git a/ui/src/nodes/mod.rs b/ui/src/nodes/mod.rs index 8fc3afc..d567abe 100644 --- a/ui/src/nodes/mod.rs +++ b/ui/src/nodes/mod.rs @@ -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) diff --git a/ui/src/nodes/text.rs b/ui/src/nodes/text.rs index f60c890..7c25443 100644 --- a/ui/src/nodes/text.rs +++ b/ui/src/nodes/text.rs @@ -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>, available_space: Size) -> Size { @@ -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; diff --git a/ui/src/nodes/text_render_cache.rs b/ui/src/nodes/text_render_cache.rs index 58a515a..bebc349 100644 --- a/ui/src/nodes/text_render_cache.rs +++ b/ui/src/nodes/text_render_cache.rs @@ -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 { 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); } }