diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 8297738804..4f8e84e645 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -602,8 +602,11 @@ shadows)) (defn set-shape-text-content + "This function sets shape text content and returns a stream that loads the needed fonts asynchronously" [shape-id content] + (h/call wasm/internal-module "_clear_shape_text") + (set-shape-vertical-align (get content :vertical-align)) (let [paragraph-set (first (get content :children)) @@ -635,8 +638,12 @@ (let [updated-fonts (-> fonts (cond-> ^boolean emoji? (f/add-emoji-font)) - (f/add-noto-fonts langs))] - (f/store-fonts shape-id updated-fonts)))))) + (f/add-noto-fonts langs)) + result (f/store-fonts shape-id updated-fonts)] + + (h/call wasm/internal-module "_update_shape_text_layout") + + result))))) (defn set-shape-text [shape-id content] diff --git a/frontend/text-editor/src/index.html b/frontend/text-editor/src/index.html index b53cd0a7d5..7ef89d3591 100644 --- a/frontend/text-editor/src/index.html +++ b/frontend/text-editor/src/index.html @@ -530,6 +530,7 @@ storeFonts(fonts) + console.log("text shape", uuid); useShape(uuid); Module._set_parent(0, 0, 0, 0); Module._set_shape_type(5); diff --git a/frontend/text-editor/src/wasm/lib.js b/frontend/text-editor/src/wasm/lib.js index 76289e1c78..dc64c5951a 100644 --- a/frontend/text-editor/src/wasm/lib.js +++ b/frontend/text-editor/src/wasm/lib.js @@ -471,6 +471,7 @@ export function updateTextShape(root, fonts) { Module._set_shape_text_content(); } + Module._update_shape_text_layout(); } export function addTextShape(text, fonts) { diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index aa061da8af..12ceffc180 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -7,7 +7,6 @@ mod performance; mod render; mod shapes; mod state; -mod textlayout; mod tiles; mod utils; mod uuid; @@ -372,21 +371,6 @@ pub extern "C" fn set_shape_corners(r1: f32, r2: f32, r3: f32, r4: f32) { }); } -#[no_mangle] -pub extern "C" fn propagate_modifiers(pixel_precision: bool) -> *mut u8 { - let bytes = mem::bytes(); - - let entries: Vec<_> = bytes - .chunks(size_of::<::BytesType>()) - .map(|data| TransformEntry::from_bytes(data.try_into().unwrap())) - .collect(); - - with_state!(state, { - let result = shapes::propagate_modifiers(state, &entries, pixel_precision); - mem::write_vec(result) - }) -} - #[no_mangle] pub extern "C" fn get_selection_rect() -> *mut u8 { let bytes = mem::bytes(); @@ -483,6 +467,21 @@ pub extern "C" fn clean_modifiers() { }); } +#[no_mangle] +pub extern "C" fn propagate_modifiers(pixel_precision: bool) -> *mut u8 { + let bytes = mem::bytes(); + + let entries: Vec<_> = bytes + .chunks(size_of::<::BytesType>()) + .map(|data| TransformEntry::from_bytes(data.try_into().unwrap())) + .collect(); + + with_state!(state, { + let result = shapes::propagate_modifiers(state, &entries, pixel_precision); + mem::write_vec(result) + }) +} + #[no_mangle] pub extern "C" fn set_modifiers() { let bytes = mem::bytes(); diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index f45bb3ef2f..92c7251278 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -25,9 +25,6 @@ use crate::shapes::{ Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, Stroke, StructureEntry, Type, }; use crate::state::ShapesPool; -use crate::textlayout::{ - paragraph_builder_group_from_text, stroke_paragraph_builder_group_from_text, -}; use crate::tiles::{self, PendingTiles, TileRect}; use crate::uuid::Uuid; use crate::view::Viewbox; @@ -622,13 +619,13 @@ impl RenderState { let inner_shadows = shape.inner_shadow_paints(); let blur_filter = shape.image_filter(1.); let count_inner_strokes = shape.count_visible_inner_strokes(); - let mut paragraphs = paragraph_builder_group_from_text(&text_content, None); + let mut paragraphs = text_content.paragraph_builder_group_from_text(None); let mut paragraphs_with_shadows = - paragraph_builder_group_from_text(&text_content, Some(true)); + text_content.paragraph_builder_group_from_text(Some(true)); let mut stroke_paragraphs_list = shape .visible_strokes() .map(|stroke| { - stroke_paragraph_builder_group_from_text( + text::stroke_paragraph_builder_group_from_text( &text_content, stroke, &shape.selrect(), @@ -641,7 +638,7 @@ impl RenderState { let mut stroke_paragraphs_with_shadows_list = shape .visible_strokes() .map(|stroke| { - stroke_paragraph_builder_group_from_text( + text::stroke_paragraph_builder_group_from_text( &text_content, stroke, &shape.selrect(), diff --git a/render-wasm/src/render/shadows.rs b/render-wasm/src/render/shadows.rs index 7569c77180..ff5e9f2cee 100644 --- a/render-wasm/src/render/shadows.rs +++ b/render-wasm/src/render/shadows.rs @@ -1,10 +1,9 @@ use super::{RenderState, SurfaceId}; use crate::render::strokes; -use crate::shapes::{Shadow, Shape, Stroke, Type}; +use crate::shapes::{ParagraphBuilderGroup, Shadow, Shape, Stroke, Type}; use skia_safe::{canvas::SaveLayerRec, Paint, Path}; use crate::render::text; -use crate::textlayout::ParagraphBuilderGroup; // Fill Shadows pub fn render_fill_inner_shadows( diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs index f44ccecac1..c435344441 100644 --- a/render-wasm/src/render/text.rs +++ b/render-wasm/src/render/text.rs @@ -1,6 +1,14 @@ use super::{RenderState, Shape, SurfaceId}; -use crate::shapes::VerticalAlign; +use crate::{ + math::Rect, + shapes::{ + merge_fills, set_paint_fill, ParagraphBuilderGroup, Stroke, StrokeKind, TextContent, + VerticalAlign, + }, + utils::{get_fallback_fonts, get_font_collection}, +}; use skia_safe::{ + self as skia, canvas::SaveLayerRec, textlayout::{ LineMetrics, Paragraph, ParagraphBuilder, RectHeightStyle, RectWidthStyle, StyleMetrics, @@ -9,11 +17,149 @@ use skia_safe::{ Canvas, ImageFilter, Paint, Path, }; +pub fn stroke_paragraph_builder_group_from_text( + text_content: &TextContent, + stroke: &Stroke, + bounds: &Rect, + count_inner_strokes: usize, + use_shadow: Option, +) -> Vec { + let fallback_fonts = get_fallback_fonts(); + let fonts = get_font_collection(); + let mut paragraph_group = Vec::new(); + let remove_stroke_alpha = use_shadow.unwrap_or(false) && !stroke.is_transparent(); + + for paragraph in text_content.paragraphs() { + let mut stroke_paragraphs_map: std::collections::HashMap = + std::collections::HashMap::new(); + + for leaf in paragraph.children().iter() { + let text_paint: skia_safe::Handle<_> = merge_fills(leaf.fills(), *bounds); + let stroke_paints = get_text_stroke_paints( + stroke, + bounds, + &text_paint, + count_inner_strokes, + remove_stroke_alpha, + ); + + let text: String = leaf.apply_text_transform(); + + for (paint_idx, stroke_paint) in stroke_paints.iter().enumerate() { + let builder = stroke_paragraphs_map.entry(paint_idx).or_insert_with(|| { + let paragraph_style = paragraph.paragraph_to_style(); + ParagraphBuilder::new(¶graph_style, fonts) + }); + let stroke_paint = stroke_paint.clone(); + let remove_alpha = use_shadow.unwrap_or(false) && !leaf.is_transparent(); + let stroke_style = + leaf.to_stroke_style(&stroke_paint, fallback_fonts, remove_alpha); + builder.push_style(&stroke_style); + builder.add_text(&text); + } + } + + let stroke_paragraphs: Vec = (0..stroke_paragraphs_map.len()) + .map(|i| stroke_paragraphs_map.remove(&i).unwrap()) + .collect(); + + paragraph_group.push(stroke_paragraphs); + } + + paragraph_group +} + +fn get_text_stroke_paints( + stroke: &Stroke, + bounds: &Rect, + text_paint: &Paint, + count_inner_strokes: usize, + remove_stroke_alpha: bool, +) -> Vec { + let mut paints = Vec::new(); + + match stroke.kind { + StrokeKind::Inner => { + let shader = text_paint.shader(); + let mut is_opaque = true; + + if shader.is_some() { + is_opaque = shader.unwrap().is_opaque(); + } + + if is_opaque && count_inner_strokes == 1 { + let mut paint = text_paint.clone(); + paint.set_style(skia::PaintStyle::Fill); + paint.set_anti_alias(true); + paints.push(paint); + + let mut paint = skia::Paint::default(); + paint.set_style(skia::PaintStyle::Stroke); + paint.set_blend_mode(skia::BlendMode::SrcIn); + paint.set_anti_alias(true); + paint.set_stroke_width(stroke.width * 2.0); + set_paint_fill(&mut paint, &stroke.fill, bounds, remove_stroke_alpha); + paints.push(paint); + } else { + let mut paint = skia::Paint::default(); + if remove_stroke_alpha { + paint.set_color(skia::Color::BLACK); + paint.set_alpha(255); + } else { + paint = text_paint.clone(); + set_paint_fill(&mut paint, &stroke.fill, bounds, false); + } + + paint.set_style(skia::PaintStyle::Fill); + paint.set_anti_alias(false); + paints.push(paint); + + let mut paint = skia::Paint::default(); + let image_filter = + skia_safe::image_filters::erode((stroke.width, stroke.width), None, None); + + paint.set_image_filter(image_filter); + paint.set_anti_alias(false); + paint.set_color(skia::Color::BLACK); + paint.set_alpha(255); + paint.set_blend_mode(skia::BlendMode::DstOut); + paints.push(paint); + } + } + StrokeKind::Center => { + let mut paint = skia::Paint::default(); + paint.set_style(skia::PaintStyle::Stroke); + paint.set_anti_alias(true); + paint.set_stroke_width(stroke.width); + set_paint_fill(&mut paint, &stroke.fill, bounds, remove_stroke_alpha); + paints.push(paint); + } + StrokeKind::Outer => { + let mut paint = skia::Paint::default(); + paint.set_style(skia::PaintStyle::Stroke); + paint.set_blend_mode(skia::BlendMode::DstOver); + paint.set_anti_alias(true); + paint.set_stroke_width(stroke.width * 2.0); + set_paint_fill(&mut paint, &stroke.fill, bounds, remove_stroke_alpha); + paints.push(paint); + + let mut paint = skia::Paint::default(); + paint.set_style(skia::PaintStyle::Fill); + paint.set_blend_mode(skia::BlendMode::Clear); + paint.set_color(skia::Color::TRANSPARENT); + paint.set_anti_alias(true); + paints.push(paint); + } + } + + paints +} + pub fn render( render_state: Option<&mut RenderState>, canvas: Option<&Canvas>, shape: &Shape, - paragraphs: &mut [Vec], + paragraph_builders: &mut [Vec], surface_id: Option, shadow: Option<&Paint>, blur: Option<&ImageFilter>, @@ -36,10 +182,10 @@ pub fn render( if let Some(shadow_paint) = shadow { let layer_rec = SaveLayerRec::default().paint(shadow_paint); render_canvas.save_layer(&layer_rec); - draw_text(render_canvas, shape, paragraphs); + draw_text(render_canvas, shape, paragraph_builders); render_canvas.restore(); } else { - draw_text(render_canvas, shape, paragraphs); + draw_text(render_canvas, shape, paragraph_builders); } if blur.is_some() { @@ -49,7 +195,7 @@ pub fn render( render_canvas.restore(); } -fn draw_text(canvas: &Canvas, shape: &Shape, paragraphs: &mut [Vec]) { +fn draw_text(canvas: &Canvas, shape: &Shape, paragraph_builders: &mut [Vec]) { // Width let paragraph_width = if let crate::shapes::Type::Text(text_content) = &shape.shape_type { text_content.width() @@ -59,7 +205,7 @@ fn draw_text(canvas: &Canvas, shape: &Shape, paragraphs: &mut [Vec (container_height - total_content_height) / 2.0, VerticalAlign::Bottom => container_height - total_content_height, @@ -68,7 +214,7 @@ fn draw_text(canvas: &Canvas, shape: &Shape, paragraphs: &mut [Vec { - let paragraph_width = shape_bounds_after.width(); - let mut paragraphs = paragraph_builder_group_from_text(content, None); - let height = auto_height(&mut paragraphs, paragraph_width); + let height = text_content.size.height; let resize_transform = math::resize_matrix( &shape_bounds_after, &shape_bounds_after, @@ -212,15 +214,10 @@ fn propagate_transform( transform.post_concat(&resize_transform); } GrowType::AutoWidth => { - let paragraph_width = content.width(); - let mut paragraphs = paragraph_builder_group_from_text(content, None); - let height = auto_height(&mut paragraphs, paragraph_width); - let resize_transform = math::resize_matrix( - &shape_bounds_after, - &shape_bounds_after, - paragraph_width, - height, - ); + let width = text_content.width(); + let height = text_content.size.height; + let resize_transform = + math::resize_matrix(&shape_bounds_after, &shape_bounds_after, width, height); shape_bounds_after = shape_bounds_after.transform(&resize_transform); transform.post_concat(&resize_transform); } diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index 0e4a11b127..1b466d7001 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -1,28 +1,138 @@ use crate::{ math::{Matrix, Rect}, render::{default_font, DEFAULT_EMOJI_FONT}, - textlayout::paragraph_builder_group_from_text, }; +use core::f32; +use macros::ToJs; use skia_safe::{ self as skia, paint::{self, Paint}, + textlayout::ParagraphBuilder, textlayout::ParagraphStyle, }; use std::collections::HashSet; use super::FontFamily; use crate::shapes::{self, merge_fills}; -use crate::textlayout::{auto_height, auto_width}; use crate::utils::uuid_from_u32; +use crate::utils::{get_fallback_fonts, get_font_collection}; use crate::wasm::fills::parse_fills_from_bytes; use crate::Uuid; -#[derive(Debug, PartialEq, Clone, Copy)] +// TODO: maybe move this to the wasm module? +pub type ParagraphBuilderGroup = Vec; + +#[repr(u8)] +#[derive(Debug, PartialEq, Clone, Copy, ToJs)] pub enum GrowType { - Fixed, - AutoWidth, - AutoHeight, + Fixed = 0, + AutoWidth = 1, + AutoHeight = 2, +} + +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct TextContentSize { + pub width: f32, + pub height: f32, + pub max_width: f32, +} + +const DEFAULT_TEXT_CONTENT_SIZE: f32 = 0.01; + +impl TextContentSize { + pub fn default() -> Self { + Self { + width: DEFAULT_TEXT_CONTENT_SIZE, + height: DEFAULT_TEXT_CONTENT_SIZE, + max_width: DEFAULT_TEXT_CONTENT_SIZE, + } + } + + pub fn new(width: f32, height: f32, max_width: f32) -> Self { + Self { + width, + height, + max_width, + } + } + + pub fn new_with_size(width: f32, height: f32) -> Self { + Self { + width, + height, + max_width: DEFAULT_TEXT_CONTENT_SIZE, + } + } + + pub fn set_size(&mut self, width: f32, height: f32) { + self.width = width; + self.height = height; + } + + pub fn copy_finite_size(&mut self, size: TextContentSize) { + if f32::is_finite(size.width) { + self.width = size.width; + } + if f32::is_finite(size.max_width) { + self.max_width = size.max_width; + } else { + self.max_width = size.width; + } + if f32::is_finite(size.height) { + self.height = size.height; + } + } +} + +#[derive(Debug)] +pub struct TextContentLayoutResult( + Vec, + Vec>, + TextContentSize, +); + +#[derive(Debug)] +pub struct TextContentLayout { + pub paragraph_builders: Vec, + pub paragraphs: Vec>, +} + +impl Clone for TextContentLayout { + fn clone(&self) -> Self { + Self { + paragraph_builders: vec![], + paragraphs: vec![], + } + } +} + +impl PartialEq for TextContentLayout { + fn eq(&self, _other: &Self) -> bool { + true + } +} + +impl TextContentLayout { + pub fn new() -> Self { + Self { + paragraph_builders: vec![], + paragraphs: vec![], + } + } + + pub fn set( + &mut self, + paragraph_builders: Vec, + paragraphs: Vec>, + ) { + self.paragraph_builders = paragraph_builders; + self.paragraphs = paragraphs; + } + + pub fn needs_update(&self) -> bool { + self.paragraph_builders.is_empty() || self.paragraphs.is_empty() + } } #[derive(Debug, PartialEq, Clone)] @@ -30,6 +140,8 @@ pub struct TextContent { pub paragraphs: Vec, pub bounds: Rect, pub grow_type: GrowType, + pub size: TextContentSize, + pub layout: TextContentLayout, } impl TextContent { @@ -38,6 +150,8 @@ impl TextContent { paragraphs: Vec::new(), bounds, grow_type, + size: TextContentSize::default(), + layout: TextContentLayout::new(), } } @@ -48,6 +162,8 @@ impl TextContent { paragraphs, bounds, grow_type, + size: TextContentSize::new_with_size(bounds.width(), bounds.height()), + layout: TextContentLayout::new(), } } @@ -79,9 +195,7 @@ impl TextContent { pub fn width(&self) -> f32 { if self.grow_type() == GrowType::AutoWidth { - let temp_paragraphs = paragraph_builder_group_from_text(self, None); - let mut temp_paragraphs = temp_paragraphs; - auto_width(&mut temp_paragraphs, f32::MAX).ceil() + self.size.width } else { self.bounds.width() } @@ -96,10 +210,7 @@ impl TextContent { } pub fn visual_bounds(&self) -> (f32, f32) { - let paragraph_width = self.width(); - let mut paragraphs = paragraph_builder_group_from_text(self, None); - let paragraph_height = auto_height(&mut paragraphs, paragraph_width); - (paragraph_width, paragraph_height) + (self.size.width, self.size.height) } pub fn transform(&mut self, transform: &Matrix) { @@ -111,6 +222,138 @@ impl TextContent { let p2 = transform.map_point(skia::Point::new(right, bottom)); self.bounds = Rect::from_ltrb(p1.x, p1.y, p2.x, p2.y); } + + /// Builds the ParagraphBuilders necessary to render + /// this text. + pub fn paragraph_builder_group_from_text( + &self, + use_shadow: Option, + ) -> Vec { + let fonts = get_font_collection(); + let fallback_fonts = get_fallback_fonts(); + let mut paragraph_group = Vec::new(); + + for paragraph in self.paragraphs() { + let paragraph_style = paragraph.paragraph_to_style(); + let mut builder = ParagraphBuilder::new(¶graph_style, fonts); + for leaf in paragraph.children() { + let remove_alpha = use_shadow.unwrap_or(false) && !leaf.is_transparent(); + let text_style = leaf.to_style(&self.bounds(), fallback_fonts, remove_alpha); + let text = leaf.apply_text_transform(); + builder.push_style(&text_style); + builder.add_text(&text); + } + paragraph_group.push(vec![builder]); + } + + paragraph_group + } + + /// Performs a text auto layout without width limits. + /// This should be the same as text_auto_layout. + fn build_paragraphs_from_paragraph_builders( + &self, + paragraph_builders: &mut [ParagraphBuilderGroup], + width: f32, + ) -> Vec> { + let paragraphs = paragraph_builders + .iter_mut() + .map(|builders| { + builders + .iter_mut() + .map(|builder| { + let mut paragraph = builder.build(); + // For auto-width, always layout with infinite width first to get intrinsic width + paragraph.layout(width); + paragraph + }) + .collect() + }) + .collect(); + paragraphs + } + + /// Performs an Auto Width text layout. + fn text_layout_auto_width(&self) -> TextContentLayoutResult { + // TODO: Deberíamos comprobar primero que los párrafos + // no están generados. + let mut paragraph_builders = self.paragraph_builder_group_from_text(None); + let paragraphs = + self.build_paragraphs_from_paragraph_builders(&mut paragraph_builders, f32::MAX); + let (width, height) = + paragraphs + .iter() + .flatten() + .fold((0.0, 0.0), |(auto_width, auto_height), paragraph| { + ( + f32::max(paragraph.max_intrinsic_width(), auto_width), + auto_height + paragraph.height(), + ) + }); + + let size = TextContentSize::new(width.ceil(), height.ceil(), width.ceil()); + TextContentLayoutResult(paragraph_builders, paragraphs, size) + } + + /// Private function that performs + /// Performs an Auto Height text layout. + fn text_layout_auto_height(&self) -> TextContentLayoutResult { + let width = self.width(); + // TODO: Deberíamos primero comprobar si existen los + // paragraph builders para poder obtener el layout. + let mut paragraph_builders = self.paragraph_builder_group_from_text(None); + let paragraphs = + self.build_paragraphs_from_paragraph_builders(&mut paragraph_builders, f32::INFINITY); + let height = paragraphs + .iter() + .flatten() + .fold(0.0, |auto_height, paragraph| { + auto_height + paragraph.height() + }); + + let size = TextContentSize::new_with_size(width.ceil(), height.ceil()); + TextContentLayoutResult(paragraph_builders, paragraphs, size) + } + + /// Performs a Fixed text layout. + fn text_layout_fixed(&self) -> TextContentLayoutResult { + let width = self.width(); + let mut paragraph_builders = self.paragraph_builder_group_from_text(None); + let paragraphs = + self.build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width); + + let size = TextContentSize::new_with_size(width.ceil(), f32::INFINITY); + TextContentLayoutResult(paragraph_builders, paragraphs, size) + } + + pub fn needs_update_layout(&self) -> bool { + self.layout.needs_update() + } + + pub fn set_layout_from_result(&mut self, result: TextContentLayoutResult) { + self.layout.set(result.0, result.1); + self.size.copy_finite_size(result.2); + } + + pub fn update_layout(&mut self, selrect: Rect) -> TextContentSize { + self.size.set_size(selrect.width(), selrect.height()); + // 3. Width and Height Calculation + match self.grow_type() { + GrowType::AutoHeight => { + let result = self.text_layout_auto_height(); + self.set_layout_from_result(result); + } + GrowType::AutoWidth => { + let result = self.text_layout_auto_width(); + self.set_layout_from_result(result); + } + GrowType::Fixed => { + let result = self.text_layout_fixed(); + self.set_layout_from_result(result); + } + } + self.size + } } impl Default for TextContent { @@ -119,6 +362,8 @@ impl Default for TextContent { paragraphs: vec![], bounds: Rect::default(), grow_type: GrowType::Fixed, + size: TextContentSize::default(), + layout: TextContentLayout::new(), } } } diff --git a/render-wasm/src/shapes/text_paths.rs b/render-wasm/src/shapes/text_paths.rs index 35ba08563e..4bc1d1d628 100644 --- a/render-wasm/src/shapes/text_paths.rs +++ b/render-wasm/src/shapes/text_paths.rs @@ -1,4 +1,4 @@ -use crate::{shapes::text::TextContent, textlayout::paragraph_builder_group_from_text}; +use crate::shapes::text::TextContent; use skia_safe::{ self as skia, textlayout::Paragraph as SkiaParagraph, FontMetrics, Point, Rect, TextBlob, }; @@ -12,17 +12,16 @@ pub struct TextPaths(TextContent); // It's an example of how to convert texts to paths #[allow(dead_code)] impl TextPaths { - pub fn new(content: TextContent) -> Self { - Self(content) + pub fn new(text_content: TextContent) -> Self { + Self(text_content) } pub fn get_paths(&self, antialias: bool) -> Vec<(skia::Path, skia::Paint)> { let mut paths = Vec::new(); - let mut offset_y = self.bounds.y(); - let mut paragraphs = paragraph_builder_group_from_text(&self.0, None); + let mut paragraph_builders = self.0.paragraph_builder_group_from_text(None); - for paragraphs in paragraphs.iter_mut() { + for paragraphs in paragraph_builders.iter_mut() { for paragraph_builder in paragraphs.iter_mut() { // 1. Get paragraph and set the width layout let mut skia_paragraph = paragraph_builder.build(); diff --git a/render-wasm/src/textlayout.rs b/render-wasm/src/textlayout.rs deleted file mode 100644 index d43779e772..0000000000 --- a/render-wasm/src/textlayout.rs +++ /dev/null @@ -1,222 +0,0 @@ -use skia_safe::{self as skia, textlayout::ParagraphBuilder, Paint, Rect}; - -use crate::{ - shapes::{merge_fills, set_paint_fill, Stroke, StrokeKind, TextContent}, - utils::{get_fallback_fonts, get_font_collection}, -}; - -pub fn auto_width(paragraphs: &mut [Vec], width: f32) -> f32 { - let built_paragraphs = get_built_paragraphs(paragraphs, width); - - built_paragraphs - .iter() - .flatten() - .fold(0.0, |auto_width, p| { - f32::max(p.max_intrinsic_width(), auto_width) - }) -} - -pub fn auto_height(paragraphs: &mut [Vec], width: f32) -> f32 { - paragraphs.iter_mut().fold(0.0, |auto_height, p| { - p.iter_mut().fold(auto_height, |auto_height, paragraph| { - let mut paragraph = paragraph.build(); - paragraph.layout(width); - auto_height + paragraph.height() - }) - }) -} - -pub fn build_paragraphs_with_width( - paragraphs: &mut [Vec], - width: f32, -) -> Vec> { - paragraphs - .iter_mut() - .map(|builders| { - builders - .iter_mut() - .map(|builder| { - let mut paragraph = builder.build(); - // For auto-width, always layout with infinite width first to get intrinsic width - paragraph.layout(f32::MAX); - let intrinsic_width = paragraph.max_intrinsic_width().ceil(); - // Use the larger of the requested width or intrinsic width to prevent line breaks - let final_width = f32::max(width, intrinsic_width); - paragraph.layout(final_width); - paragraph - }) - .collect() - }) - .collect() -} - -pub type ParagraphBuilderGroup = Vec; - -pub fn paragraph_builder_group_from_text( - text_content: &TextContent, - use_shadow: Option, -) -> Vec { - let fonts = get_font_collection(); - let fallback_fonts = get_fallback_fonts(); - let mut paragraph_group = Vec::new(); - - for paragraph in text_content.paragraphs() { - let paragraph_style = paragraph.paragraph_to_style(); - let mut builder = ParagraphBuilder::new(¶graph_style, fonts); - for leaf in paragraph.children() { - let remove_alpha = use_shadow.unwrap_or(false) && !leaf.is_transparent(); - let text_style = leaf.to_style(&text_content.bounds(), fallback_fonts, remove_alpha); - let text = leaf.apply_text_transform(); - builder.push_style(&text_style); - builder.add_text(&text); - } - paragraph_group.push(vec![builder]); - } - - paragraph_group -} - -pub fn stroke_paragraph_builder_group_from_text( - text_content: &TextContent, - stroke: &Stroke, - bounds: &Rect, - count_inner_strokes: usize, - use_shadow: Option, -) -> Vec { - let fallback_fonts = get_fallback_fonts(); - let fonts = get_font_collection(); - let mut paragraph_group = Vec::new(); - let remove_stroke_alpha = use_shadow.unwrap_or(false) && !stroke.is_transparent(); - - for paragraph in text_content.paragraphs() { - let mut stroke_paragraphs_map: std::collections::HashMap = - std::collections::HashMap::new(); - - for leaf in paragraph.children().iter() { - let text_paint: skia_safe::Handle<_> = merge_fills(leaf.fills(), *bounds); - let stroke_paints = get_text_stroke_paints( - stroke, - bounds, - &text_paint, - count_inner_strokes, - remove_stroke_alpha, - ); - - let text: String = leaf.apply_text_transform(); - - for (paint_idx, stroke_paint) in stroke_paints.iter().enumerate() { - let builder = stroke_paragraphs_map.entry(paint_idx).or_insert_with(|| { - let paragraph_style = paragraph.paragraph_to_style(); - ParagraphBuilder::new(¶graph_style, fonts) - }); - let stroke_paint = stroke_paint.clone(); - let remove_alpha = use_shadow.unwrap_or(false) && !leaf.is_transparent(); - let stroke_style = - leaf.to_stroke_style(&stroke_paint, fallback_fonts, remove_alpha); - builder.push_style(&stroke_style); - builder.add_text(&text); - } - } - - let stroke_paragraphs: Vec = (0..stroke_paragraphs_map.len()) - .map(|i| stroke_paragraphs_map.remove(&i).unwrap()) - .collect(); - - paragraph_group.push(stroke_paragraphs); - } - - paragraph_group -} - -fn get_built_paragraphs( - paragraphs: &mut [Vec], - width: f32, -) -> Vec> { - build_paragraphs_with_width(paragraphs, width) -} - -fn get_text_stroke_paints( - stroke: &Stroke, - bounds: &Rect, - text_paint: &Paint, - count_inner_strokes: usize, - remove_stroke_alpha: bool, -) -> Vec { - let mut paints = Vec::new(); - - match stroke.kind { - StrokeKind::Inner => { - let shader = text_paint.shader(); - let mut is_opaque = true; - - if shader.is_some() { - is_opaque = shader.unwrap().is_opaque(); - } - - if is_opaque && count_inner_strokes == 1 { - let mut paint = text_paint.clone(); - paint.set_style(skia::PaintStyle::Fill); - paint.set_anti_alias(true); - paints.push(paint); - - let mut paint = skia::Paint::default(); - paint.set_style(skia::PaintStyle::Stroke); - paint.set_blend_mode(skia::BlendMode::SrcIn); - paint.set_anti_alias(true); - paint.set_stroke_width(stroke.width * 2.0); - set_paint_fill(&mut paint, &stroke.fill, bounds, remove_stroke_alpha); - paints.push(paint); - } else { - let mut paint = skia::Paint::default(); - if remove_stroke_alpha { - paint.set_color(skia::Color::BLACK); - paint.set_alpha(255); - } else { - paint = text_paint.clone(); - set_paint_fill(&mut paint, &stroke.fill, bounds, false); - } - - paint.set_style(skia::PaintStyle::Fill); - paint.set_anti_alias(false); - paints.push(paint); - - let mut paint = skia::Paint::default(); - let image_filter = - skia_safe::image_filters::erode((stroke.width, stroke.width), None, None); - - paint.set_image_filter(image_filter); - paint.set_anti_alias(false); - paint.set_color(skia::Color::BLACK); - paint.set_alpha(255); - paint.set_blend_mode(skia::BlendMode::DstOut); - paints.push(paint); - } - } - StrokeKind::Center => { - let mut paint = skia::Paint::default(); - paint.set_style(skia::PaintStyle::Stroke); - paint.set_anti_alias(true); - paint.set_stroke_width(stroke.width); - set_paint_fill(&mut paint, &stroke.fill, bounds, remove_stroke_alpha); - paints.push(paint); - } - StrokeKind::Outer => { - let mut paint = skia::Paint::default(); - paint.set_style(skia::PaintStyle::Stroke); - paint.set_blend_mode(skia::BlendMode::DstOver); - paint.set_anti_alias(true); - paint.set_stroke_width(stroke.width * 2.0); - set_paint_fill(&mut paint, &stroke.fill, bounds, remove_stroke_alpha); - paints.push(paint); - - let mut paint = skia::Paint::default(); - paint.set_style(skia::PaintStyle::Fill); - paint.set_blend_mode(skia::BlendMode::Clear); - paint.set_color(skia::Color::TRANSPARENT); - paint.set_anti_alias(true); - paints.push(paint); - } - } - - paints -} diff --git a/render-wasm/src/wasm/text.rs b/render-wasm/src/wasm/text.rs index ea8b6cb737..1d69ad70c0 100644 --- a/render-wasm/src/wasm/text.rs +++ b/render-wasm/src/wasm/text.rs @@ -2,10 +2,7 @@ use macros::ToJs; use crate::mem; use crate::shapes::{GrowType, RawTextData, Type}; -use crate::textlayout::{ - auto_height, build_paragraphs_with_width, paragraph_builder_group_from_text, -}; -use crate::{with_current_shape, with_current_shape_mut, STATE}; +use crate::{with_current_shape_mut, STATE}; #[derive(Debug, PartialEq, Clone, Copy, ToJs)] #[repr(u8)] @@ -57,52 +54,38 @@ pub extern "C" fn set_shape_grow_type(grow_type: u8) { with_current_shape_mut!(state, |shape: &mut Shape| { if let Type::Text(text_content) = &mut shape.shape_type { - text_content.set_grow_type(grow_type.into()); + text_content.set_grow_type(GrowType::from(grow_type)); + } else { + panic!("Trying to update grow type in a shape that it's not a text shape"); } }); } #[no_mangle] pub extern "C" fn get_text_dimensions() -> *mut u8 { - let mut width = 0.01; - let mut height = 0.01; - let mut m_width = 0.01; + let mut ptr = std::ptr::null_mut(); + with_current_shape_mut!(state, |shape: &mut Shape| { + if let Type::Text(content) = &mut shape.shape_type { + let text_content_size = content.update_layout(shape.selrect); - with_current_shape!(state, |shape: &Shape| { - width = shape.selrect.width(); - height = shape.selrect.height(); - - if let Type::Text(content) = &shape.shape_type { - // 1. Reset Paragraphs - let paragraph_width = content.width(); - let mut paragraphs = paragraph_builder_group_from_text(content, None); - let built_paragraphs = build_paragraphs_with_width(&mut paragraphs, paragraph_width); - - // 2. Max Width Calculation - m_width = built_paragraphs - .iter() - .flatten() - .fold(0.0, |max_width, p| f32::max(p.max_width(), max_width)); - - // 3. Width and Height Calculation - match content.grow_type() { - GrowType::AutoHeight => { - let mut paragraph_height = paragraph_builder_group_from_text(content, None); - height = auto_height(&mut paragraph_height, paragraph_width).ceil(); - } - GrowType::AutoWidth => { - width = paragraph_width; - let mut paragraph_height = paragraph_builder_group_from_text(content, None); - height = auto_height(&mut paragraph_height, paragraph_width).ceil(); - } - GrowType::Fixed => {} - } + let mut bytes = vec![0; 12]; + bytes[0..4].clone_from_slice(&text_content_size.width.to_le_bytes()); + bytes[4..8].clone_from_slice(&text_content_size.height.to_le_bytes()); + bytes[8..12].clone_from_slice(&text_content_size.max_width.to_le_bytes()); + ptr = mem::write_bytes(bytes) } }); - let mut bytes = vec![0; 12]; - bytes[0..4].clone_from_slice(&width.to_le_bytes()); - bytes[4..8].clone_from_slice(&height.to_le_bytes()); - bytes[8..12].clone_from_slice(&m_width.to_le_bytes()); - mem::write_bytes(bytes) + // FIXME: I think it should be better if instead of returning + // a NULL ptr we failed gracefully. + ptr +} + +#[no_mangle] +pub extern "C" fn update_shape_text_layout() { + with_current_shape_mut!(state, |shape: &mut Shape| { + if let Type::Text(text_content) = &mut shape.shape_type { + text_content.update_layout(shape.selrect); + } + }); }