mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
🎉 Add internal TextContent layout data
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -471,6 +471,7 @@ export function updateTextShape(root, fonts) {
|
||||
|
||||
Module._set_shape_text_content();
|
||||
}
|
||||
Module._update_shape_text_layout();
|
||||
}
|
||||
|
||||
export function addTextShape(text, fonts) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(¶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<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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(¶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<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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(¶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<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(¶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<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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user