🎉 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))
(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]

View File

@@ -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);

View File

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

View File

@@ -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::<<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]
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::<<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]
pub extern "C" fn set_modifiers() {
let bytes = mem::bytes();

View File

@@ -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(),

View File

@@ -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(

View File

@@ -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<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(
render_state: Option<&mut RenderState>,
canvas: Option<&Canvas>,
shape: &Shape,
paragraphs: &mut [Vec<ParagraphBuilder>],
paragraph_builders: &mut [Vec<ParagraphBuilder>],
surface_id: Option<SurfaceId>,
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<ParagraphBuilder>]) {
fn draw_text(canvas: &Canvas, shape: &Shape, paragraph_builders: &mut [Vec<ParagraphBuilder>]) {
// 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<ParagraphBuil
// 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() {
VerticalAlign::Center => (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<ParagraphBuil
let layer_rec = SaveLayerRec::default();
canvas.save_layer(&layer_rec);
for group in paragraphs {
for group in paragraph_builders {
let mut group_offset_y = global_offset_y;
let group_len = group.len();

View File

@@ -769,7 +769,7 @@ impl Shape {
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();
rect.right = rect.left + width;
rect.bottom = rect.top + height;

View File

@@ -14,7 +14,6 @@ use crate::shapes::{
TransformEntry, Type,
};
use crate::state::{ShapesPool, State};
use crate::textlayout::{auto_height, paragraph_builder_group_from_text};
use crate::uuid::Uuid;
#[allow(clippy::too_many_arguments)]
@@ -196,12 +195,15 @@ fn propagate_transform(
let mut transform = entry.transform;
if let Type::Text(content) = &shape.shape_type {
match content.grow_type() {
// NOTA: No puedo utilizar un clone porque entonces estaríamos
// 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 => {
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);
}

View File

@@ -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<ParagraphBuilder>;
#[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<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)]
@@ -30,6 +140,8 @@ pub struct TextContent {
pub paragraphs: Vec<Paragraph>,
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<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 {
@@ -119,6 +362,8 @@ impl Default for TextContent {
paragraphs: vec![],
bounds: Rect::default(),
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::{
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();

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::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);
}
});
}