🎉 Add internal TextContent layout data

This commit is contained in:
Aitor Moreno
2025-09-04 14:17:47 +02:00
parent e4d610d503
commit f505fcfa0d
13 changed files with 485 additions and 333 deletions

View File

@@ -602,8 +602,11 @@
shadows)) shadows))
(defn set-shape-text-content (defn set-shape-text-content
"This function sets shape text content and returns a stream that loads the needed fonts asynchronously"
[shape-id content] [shape-id content]
(h/call wasm/internal-module "_clear_shape_text") (h/call wasm/internal-module "_clear_shape_text")
(set-shape-vertical-align (get content :vertical-align)) (set-shape-vertical-align (get content :vertical-align))
(let [paragraph-set (first (get content :children)) (let [paragraph-set (first (get content :children))
@@ -635,8 +638,12 @@
(let [updated-fonts (let [updated-fonts
(-> fonts (-> fonts
(cond-> ^boolean emoji? (f/add-emoji-font)) (cond-> ^boolean emoji? (f/add-emoji-font))
(f/add-noto-fonts langs))] (f/add-noto-fonts langs))
(f/store-fonts shape-id updated-fonts)))))) result (f/store-fonts shape-id updated-fonts)]
(h/call wasm/internal-module "_update_shape_text_layout")
result)))))
(defn set-shape-text (defn set-shape-text
[shape-id content] [shape-id content]

View File

@@ -530,6 +530,7 @@
storeFonts(fonts) storeFonts(fonts)
console.log("text shape", uuid);
useShape(uuid); useShape(uuid);
Module._set_parent(0, 0, 0, 0); Module._set_parent(0, 0, 0, 0);
Module._set_shape_type(5); Module._set_shape_type(5);

View File

@@ -471,6 +471,7 @@ export function updateTextShape(root, fonts) {
Module._set_shape_text_content(); Module._set_shape_text_content();
} }
Module._update_shape_text_layout();
} }
export function addTextShape(text, fonts) { export function addTextShape(text, fonts) {

View File

@@ -7,7 +7,6 @@ mod performance;
mod render; mod render;
mod shapes; mod shapes;
mod state; mod state;
mod textlayout;
mod tiles; mod tiles;
mod utils; mod utils;
mod uuid; 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::<<TransformEntry as SerializableResult>::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] #[no_mangle]
pub extern "C" fn get_selection_rect() -> *mut u8 { pub extern "C" fn get_selection_rect() -> *mut u8 {
let bytes = mem::bytes(); 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::<<TransformEntry as SerializableResult>::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] #[no_mangle]
pub extern "C" fn set_modifiers() { pub extern "C" fn set_modifiers() {
let bytes = mem::bytes(); let bytes = mem::bytes();

View File

@@ -25,9 +25,6 @@ use crate::shapes::{
Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, Stroke, StructureEntry, Type, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, Stroke, StructureEntry, Type,
}; };
use crate::state::ShapesPool; 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::tiles::{self, PendingTiles, TileRect};
use crate::uuid::Uuid; use crate::uuid::Uuid;
use crate::view::Viewbox; use crate::view::Viewbox;
@@ -622,13 +619,13 @@ impl RenderState {
let inner_shadows = shape.inner_shadow_paints(); let inner_shadows = shape.inner_shadow_paints();
let blur_filter = shape.image_filter(1.); let blur_filter = shape.image_filter(1.);
let count_inner_strokes = shape.count_visible_inner_strokes(); 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 = 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 let mut stroke_paragraphs_list = shape
.visible_strokes() .visible_strokes()
.map(|stroke| { .map(|stroke| {
stroke_paragraph_builder_group_from_text( text::stroke_paragraph_builder_group_from_text(
&text_content, &text_content,
stroke, stroke,
&shape.selrect(), &shape.selrect(),
@@ -641,7 +638,7 @@ impl RenderState {
let mut stroke_paragraphs_with_shadows_list = shape let mut stroke_paragraphs_with_shadows_list = shape
.visible_strokes() .visible_strokes()
.map(|stroke| { .map(|stroke| {
stroke_paragraph_builder_group_from_text( text::stroke_paragraph_builder_group_from_text(
&text_content, &text_content,
stroke, stroke,
&shape.selrect(), &shape.selrect(),

View File

@@ -1,10 +1,9 @@
use super::{RenderState, SurfaceId}; use super::{RenderState, SurfaceId};
use crate::render::strokes; 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 skia_safe::{canvas::SaveLayerRec, Paint, Path};
use crate::render::text; use crate::render::text;
use crate::textlayout::ParagraphBuilderGroup;
// Fill Shadows // Fill Shadows
pub fn render_fill_inner_shadows( pub fn render_fill_inner_shadows(

View File

@@ -1,6 +1,14 @@
use super::{RenderState, Shape, SurfaceId}; 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::{ use skia_safe::{
self as skia,
canvas::SaveLayerRec, canvas::SaveLayerRec,
textlayout::{ textlayout::{
LineMetrics, Paragraph, ParagraphBuilder, RectHeightStyle, RectWidthStyle, StyleMetrics, LineMetrics, Paragraph, ParagraphBuilder, RectHeightStyle, RectWidthStyle, StyleMetrics,
@@ -9,11 +17,149 @@ use skia_safe::{
Canvas, ImageFilter, Paint, Path, 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<bool>,
) -> Vec<ParagraphBuilderGroup> {
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<usize, ParagraphBuilder> =
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(&paragraph_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<ParagraphBuilder> = (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<Paint> {
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( pub fn render(
render_state: Option<&mut RenderState>, render_state: Option<&mut RenderState>,
canvas: Option<&Canvas>, canvas: Option<&Canvas>,
shape: &Shape, shape: &Shape,
paragraphs: &mut [Vec<ParagraphBuilder>], paragraph_builders: &mut [Vec<ParagraphBuilder>],
surface_id: Option<SurfaceId>, surface_id: Option<SurfaceId>,
shadow: Option<&Paint>, shadow: Option<&Paint>,
blur: Option<&ImageFilter>, blur: Option<&ImageFilter>,
@@ -36,10 +182,10 @@ pub fn render(
if let Some(shadow_paint) = shadow { if let Some(shadow_paint) = shadow {
let layer_rec = SaveLayerRec::default().paint(shadow_paint); let layer_rec = SaveLayerRec::default().paint(shadow_paint);
render_canvas.save_layer(&layer_rec); render_canvas.save_layer(&layer_rec);
draw_text(render_canvas, shape, paragraphs); draw_text(render_canvas, shape, paragraph_builders);
render_canvas.restore(); render_canvas.restore();
} else { } else {
draw_text(render_canvas, shape, paragraphs); draw_text(render_canvas, shape, paragraph_builders);
} }
if blur.is_some() { if blur.is_some() {
@@ -49,7 +195,7 @@ pub fn render(
render_canvas.restore(); render_canvas.restore();
} }
fn draw_text(canvas: &Canvas, shape: &Shape, paragraphs: &mut [Vec<ParagraphBuilder>]) { fn draw_text(canvas: &Canvas, shape: &Shape, paragraph_builders: &mut [Vec<ParagraphBuilder>]) {
// Width // Width
let paragraph_width = if let crate::shapes::Type::Text(text_content) = &shape.shape_type { let paragraph_width = if let crate::shapes::Type::Text(text_content) = &shape.shape_type {
text_content.width() text_content.width()
@@ -59,7 +205,7 @@ fn draw_text(canvas: &Canvas, shape: &Shape, paragraphs: &mut [Vec<ParagraphBuil
// Height // Height
let container_height = shape.selrect().height(); let container_height = shape.selrect().height();
let total_content_height = calculate_all_paragraphs_height(paragraphs, paragraph_width); let total_content_height = calculate_all_paragraphs_height(paragraph_builders, paragraph_width);
let mut global_offset_y = match shape.vertical_align() { let mut global_offset_y = match shape.vertical_align() {
VerticalAlign::Center => (container_height - total_content_height) / 2.0, VerticalAlign::Center => (container_height - total_content_height) / 2.0,
VerticalAlign::Bottom => container_height - total_content_height, VerticalAlign::Bottom => container_height - total_content_height,
@@ -68,7 +214,7 @@ fn draw_text(canvas: &Canvas, shape: &Shape, paragraphs: &mut [Vec<ParagraphBuil
let layer_rec = SaveLayerRec::default(); let layer_rec = SaveLayerRec::default();
canvas.save_layer(&layer_rec); canvas.save_layer(&layer_rec);
for group in paragraphs { for group in paragraph_builders {
let mut group_offset_y = global_offset_y; let mut group_offset_y = global_offset_y;
let group_len = group.len(); let group_len = group.len();

View File

@@ -769,7 +769,7 @@ impl Shape {
bounds_rect bounds_rect
}; };
if let Type::Text(ref text_content) = self.shape_type { if let Type::Text(text_content) = &self.shape_type {
let (width, height) = text_content.visual_bounds(); let (width, height) = text_content.visual_bounds();
rect.right = rect.left + width; rect.right = rect.left + width;
rect.bottom = rect.top + height; rect.bottom = rect.top + height;

View File

@@ -14,7 +14,6 @@ use crate::shapes::{
TransformEntry, Type, TransformEntry, Type,
}; };
use crate::state::{ShapesPool, State}; use crate::state::{ShapesPool, State};
use crate::textlayout::{auto_height, paragraph_builder_group_from_text};
use crate::uuid::Uuid; use crate::uuid::Uuid;
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@@ -196,12 +195,15 @@ fn propagate_transform(
let mut transform = entry.transform; let mut transform = entry.transform;
if let Type::Text(content) = &shape.shape_type { // NOTA: No puedo utilizar un clone porque entonces estaríamos
match content.grow_type() { // perdiendo la referencia al contenido del layout...
if let Type::Text(text_content) = &mut shape.shape_type.clone() {
if text_content.needs_update_layout() {
text_content.update_layout(shape.selrect);
}
match text_content.grow_type() {
GrowType::AutoHeight => { GrowType::AutoHeight => {
let paragraph_width = shape_bounds_after.width(); let height = text_content.size.height;
let mut paragraphs = paragraph_builder_group_from_text(content, None);
let height = auto_height(&mut paragraphs, paragraph_width);
let resize_transform = math::resize_matrix( let resize_transform = math::resize_matrix(
&shape_bounds_after, &shape_bounds_after,
&shape_bounds_after, &shape_bounds_after,
@@ -212,15 +214,10 @@ fn propagate_transform(
transform.post_concat(&resize_transform); transform.post_concat(&resize_transform);
} }
GrowType::AutoWidth => { GrowType::AutoWidth => {
let paragraph_width = content.width(); let width = text_content.width();
let mut paragraphs = paragraph_builder_group_from_text(content, None); let height = text_content.size.height;
let height = auto_height(&mut paragraphs, paragraph_width); let resize_transform =
let resize_transform = math::resize_matrix( math::resize_matrix(&shape_bounds_after, &shape_bounds_after, width, height);
&shape_bounds_after,
&shape_bounds_after,
paragraph_width,
height,
);
shape_bounds_after = shape_bounds_after.transform(&resize_transform); shape_bounds_after = shape_bounds_after.transform(&resize_transform);
transform.post_concat(&resize_transform); transform.post_concat(&resize_transform);
} }

View File

@@ -1,28 +1,138 @@
use crate::{ use crate::{
math::{Matrix, Rect}, math::{Matrix, Rect},
render::{default_font, DEFAULT_EMOJI_FONT}, render::{default_font, DEFAULT_EMOJI_FONT},
textlayout::paragraph_builder_group_from_text,
}; };
use core::f32;
use macros::ToJs;
use skia_safe::{ use skia_safe::{
self as skia, self as skia,
paint::{self, Paint}, paint::{self, Paint},
textlayout::ParagraphBuilder,
textlayout::ParagraphStyle, textlayout::ParagraphStyle,
}; };
use std::collections::HashSet; use std::collections::HashSet;
use super::FontFamily; use super::FontFamily;
use crate::shapes::{self, merge_fills}; use crate::shapes::{self, merge_fills};
use crate::textlayout::{auto_height, auto_width};
use crate::utils::uuid_from_u32; 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::wasm::fills::parse_fills_from_bytes;
use crate::Uuid; use crate::Uuid;
#[derive(Debug, PartialEq, Clone, Copy)] // TODO: maybe move this to the wasm module?
pub type ParagraphBuilderGroup = Vec<ParagraphBuilder>;
#[repr(u8)]
#[derive(Debug, PartialEq, Clone, Copy, ToJs)]
pub enum GrowType { pub enum GrowType {
Fixed, Fixed = 0,
AutoWidth, AutoWidth = 1,
AutoHeight, 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<ParagraphBuilderGroup>,
Vec<Vec<skia::textlayout::Paragraph>>,
TextContentSize,
);
#[derive(Debug)]
pub struct TextContentLayout {
pub paragraph_builders: Vec<ParagraphBuilderGroup>,
pub paragraphs: Vec<Vec<skia_safe::textlayout::Paragraph>>,
}
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<ParagraphBuilderGroup>,
paragraphs: Vec<Vec<skia::textlayout::Paragraph>>,
) {
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)] #[derive(Debug, PartialEq, Clone)]
@@ -30,6 +140,8 @@ pub struct TextContent {
pub paragraphs: Vec<Paragraph>, pub paragraphs: Vec<Paragraph>,
pub bounds: Rect, pub bounds: Rect,
pub grow_type: GrowType, pub grow_type: GrowType,
pub size: TextContentSize,
pub layout: TextContentLayout,
} }
impl TextContent { impl TextContent {
@@ -38,6 +150,8 @@ impl TextContent {
paragraphs: Vec::new(), paragraphs: Vec::new(),
bounds, bounds,
grow_type, grow_type,
size: TextContentSize::default(),
layout: TextContentLayout::new(),
} }
} }
@@ -48,6 +162,8 @@ impl TextContent {
paragraphs, paragraphs,
bounds, bounds,
grow_type, 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 { pub fn width(&self) -> f32 {
if self.grow_type() == GrowType::AutoWidth { if self.grow_type() == GrowType::AutoWidth {
let temp_paragraphs = paragraph_builder_group_from_text(self, None); self.size.width
let mut temp_paragraphs = temp_paragraphs;
auto_width(&mut temp_paragraphs, f32::MAX).ceil()
} else { } else {
self.bounds.width() self.bounds.width()
} }
@@ -96,10 +210,7 @@ impl TextContent {
} }
pub fn visual_bounds(&self) -> (f32, f32) { pub fn visual_bounds(&self) -> (f32, f32) {
let paragraph_width = self.width(); (self.size.width, self.size.height)
let mut paragraphs = paragraph_builder_group_from_text(self, None);
let paragraph_height = auto_height(&mut paragraphs, paragraph_width);
(paragraph_width, paragraph_height)
} }
pub fn transform(&mut self, transform: &Matrix) { pub fn transform(&mut self, transform: &Matrix) {
@@ -111,6 +222,138 @@ impl TextContent {
let p2 = transform.map_point(skia::Point::new(right, bottom)); let p2 = transform.map_point(skia::Point::new(right, bottom));
self.bounds = Rect::from_ltrb(p1.x, p1.y, p2.x, p2.y); 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<bool>,
) -> Vec<ParagraphBuilderGroup> {
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(&paragraph_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<Vec<skia::textlayout::Paragraph>> {
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 { impl Default for TextContent {
@@ -119,6 +362,8 @@ impl Default for TextContent {
paragraphs: vec![], paragraphs: vec![],
bounds: Rect::default(), bounds: Rect::default(),
grow_type: GrowType::Fixed, grow_type: GrowType::Fixed,
size: TextContentSize::default(),
layout: TextContentLayout::new(),
} }
} }
} }

View File

@@ -1,4 +1,4 @@
use crate::{shapes::text::TextContent, textlayout::paragraph_builder_group_from_text}; use crate::shapes::text::TextContent;
use skia_safe::{ use skia_safe::{
self as skia, textlayout::Paragraph as SkiaParagraph, FontMetrics, Point, Rect, TextBlob, 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 // It's an example of how to convert texts to paths
#[allow(dead_code)] #[allow(dead_code)]
impl TextPaths { impl TextPaths {
pub fn new(content: TextContent) -> Self { pub fn new(text_content: TextContent) -> Self {
Self(content) Self(text_content)
} }
pub fn get_paths(&self, antialias: bool) -> Vec<(skia::Path, skia::Paint)> { pub fn get_paths(&self, antialias: bool) -> Vec<(skia::Path, skia::Paint)> {
let mut paths = Vec::new(); let mut paths = Vec::new();
let mut offset_y = self.bounds.y(); 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() { for paragraph_builder in paragraphs.iter_mut() {
// 1. Get paragraph and set the width layout // 1. Get paragraph and set the width layout
let mut skia_paragraph = paragraph_builder.build(); let mut skia_paragraph = paragraph_builder.build();

View File

@@ -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<ParagraphBuilder>], 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<ParagraphBuilder>], 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<ParagraphBuilder>],
width: f32,
) -> Vec<Vec<skia_safe::textlayout::Paragraph>> {
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<ParagraphBuilder>;
pub fn paragraph_builder_group_from_text(
text_content: &TextContent,
use_shadow: Option<bool>,
) -> Vec<ParagraphBuilderGroup> {
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(&paragraph_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<bool>,
) -> Vec<ParagraphBuilderGroup> {
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<usize, ParagraphBuilder> =
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(&paragraph_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<ParagraphBuilder> = (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<ParagraphBuilder>],
width: f32,
) -> Vec<Vec<skia_safe::textlayout::Paragraph>> {
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<Paint> {
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
}

View File

@@ -2,10 +2,7 @@ use macros::ToJs;
use crate::mem; use crate::mem;
use crate::shapes::{GrowType, RawTextData, Type}; use crate::shapes::{GrowType, RawTextData, Type};
use crate::textlayout::{ use crate::{with_current_shape_mut, STATE};
auto_height, build_paragraphs_with_width, paragraph_builder_group_from_text,
};
use crate::{with_current_shape, with_current_shape_mut, STATE};
#[derive(Debug, PartialEq, Clone, Copy, ToJs)] #[derive(Debug, PartialEq, Clone, Copy, ToJs)]
#[repr(u8)] #[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| { with_current_shape_mut!(state, |shape: &mut Shape| {
if let Type::Text(text_content) = &mut shape.shape_type { 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] #[no_mangle]
pub extern "C" fn get_text_dimensions() -> *mut u8 { pub extern "C" fn get_text_dimensions() -> *mut u8 {
let mut width = 0.01; let mut ptr = std::ptr::null_mut();
let mut height = 0.01; with_current_shape_mut!(state, |shape: &mut Shape| {
let mut m_width = 0.01; if let Type::Text(content) = &mut shape.shape_type {
let text_content_size = content.update_layout(shape.selrect);
with_current_shape!(state, |shape: &Shape| { let mut bytes = vec![0; 12];
width = shape.selrect.width(); bytes[0..4].clone_from_slice(&text_content_size.width.to_le_bytes());
height = shape.selrect.height(); 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());
if let Type::Text(content) = &shape.shape_type { ptr = mem::write_bytes(bytes)
// 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]; // FIXME: I think it should be better if instead of returning
bytes[0..4].clone_from_slice(&width.to_le_bytes()); // a NULL ptr we failed gracefully.
bytes[4..8].clone_from_slice(&height.to_le_bytes()); ptr
bytes[8..12].clone_from_slice(&m_width.to_le_bytes()); }
mem::write_bytes(bytes)
#[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);
}
});
} }