mirror of
https://github.com/penpot/penpot.git
synced 2026-05-08 09:38:43 +00:00
Merge pull request #7247 from penpot/ladybenko-11983-textlayout-module
♻️ Refactor into new textlayout module
This commit is contained in:
commit
f60b6a4869
@ -7,6 +7,7 @@ mod performance;
|
||||
mod render;
|
||||
mod shapes;
|
||||
mod state;
|
||||
mod textlayout;
|
||||
mod tiles;
|
||||
mod utils;
|
||||
mod uuid;
|
||||
|
||||
@ -24,6 +24,9 @@ pub use surfaces::{SurfaceId, Surfaces};
|
||||
use crate::performance;
|
||||
use crate::shapes::{Blur, BlurType, Corners, Fill, Shape, 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;
|
||||
@ -541,13 +544,18 @@ impl RenderState {
|
||||
let inner_shadows = shape.inner_shadow_paints();
|
||||
let blur_filter = shape.image_filter(1.);
|
||||
let blur_mask = shape.mask_filter(1.);
|
||||
let mut paragraphs =
|
||||
text_content.to_paragraphs(blur_filter.as_ref(), blur_mask.as_ref(), None);
|
||||
let mut paragraphs = paragraph_builder_group_from_text(
|
||||
&text_content,
|
||||
blur_filter.as_ref(),
|
||||
blur_mask.as_ref(),
|
||||
None,
|
||||
);
|
||||
|
||||
// Render all drop shadows if there are no visible strokes
|
||||
if !shape.has_visible_strokes() && !drop_shadows.is_empty() {
|
||||
for drop_shadow in &drop_shadows {
|
||||
let mut paragraphs_with_drop_shadows = text_content.to_paragraphs(
|
||||
let mut paragraphs_with_drop_shadows = paragraph_builder_group_from_text(
|
||||
&text_content,
|
||||
blur_filter.as_ref(),
|
||||
blur_mask.as_ref(),
|
||||
Some(drop_shadow),
|
||||
@ -564,8 +572,9 @@ impl RenderState {
|
||||
text::render(self, &shape, &mut paragraphs, None);
|
||||
for stroke in shape.visible_strokes().rev() {
|
||||
for drop_shadow in &drop_shadows {
|
||||
let mut stroke_paragraphs_with_drop_shadows = text_content
|
||||
.to_stroke_paragraphs(
|
||||
let mut stroke_paragraphs_with_drop_shadows =
|
||||
stroke_paragraph_builder_group_from_text(
|
||||
&text_content,
|
||||
stroke,
|
||||
&shape.selrect(),
|
||||
blur_filter.as_ref(),
|
||||
@ -580,7 +589,8 @@ impl RenderState {
|
||||
);
|
||||
}
|
||||
|
||||
let mut stroke_paragraphs = text_content.to_stroke_paragraphs(
|
||||
let mut stroke_paragraphs = stroke_paragraph_builder_group_from_text(
|
||||
&text_content,
|
||||
stroke,
|
||||
&shape.selrect(),
|
||||
blur_filter.as_ref(),
|
||||
@ -600,8 +610,9 @@ impl RenderState {
|
||||
);
|
||||
|
||||
for inner_shadow in &inner_shadows {
|
||||
let mut stroke_paragraphs_with_inner_shadows = text_content
|
||||
.to_stroke_paragraphs(
|
||||
let mut stroke_paragraphs_with_inner_shadows =
|
||||
stroke_paragraph_builder_group_from_text(
|
||||
&text_content,
|
||||
stroke,
|
||||
&shape.selrect(),
|
||||
blur_filter.as_ref(),
|
||||
@ -618,7 +629,8 @@ impl RenderState {
|
||||
}
|
||||
|
||||
for inner_shadow in &inner_shadows {
|
||||
let mut paragraphs_with_inner_shadows = text_content.to_paragraphs(
|
||||
let mut paragraphs_with_inner_shadows = paragraph_builder_group_from_text(
|
||||
&text_content,
|
||||
blur_filter.as_ref(),
|
||||
blur_mask.as_ref(),
|
||||
Some(inner_shadow),
|
||||
|
||||
@ -19,7 +19,7 @@ pub fn render(
|
||||
|
||||
// Width
|
||||
let paragraph_width = if let crate::shapes::Type::Text(text_content) = &shape.shape_type {
|
||||
text_content.get_width()
|
||||
text_content.width()
|
||||
} else {
|
||||
shape.width()
|
||||
};
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
pub mod common;
|
||||
|
||||
mod constraints;
|
||||
mod flex_layout;
|
||||
|
||||
pub mod common;
|
||||
pub mod grid_layout;
|
||||
|
||||
use crate::math::{self as math, bools, identitish, Bounds, Matrix, Point};
|
||||
use common::GetBounds;
|
||||
|
||||
use crate::math::bools;
|
||||
use crate::math::{self as math, identitish, Bounds, Matrix, Point};
|
||||
|
||||
use crate::shapes::{
|
||||
auto_height, ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, Shape,
|
||||
StructureEntry, TransformEntry, Type,
|
||||
ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, Shape, StructureEntry,
|
||||
TransformEntry, Type,
|
||||
};
|
||||
use crate::state::ShapesPool;
|
||||
use crate::state::State;
|
||||
use crate::state::{ShapesPool, State};
|
||||
use crate::textlayout::{auto_height, paragraph_builder_group_from_text};
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@ -200,7 +200,7 @@ fn propagate_transform(
|
||||
match content.grow_type() {
|
||||
GrowType::AutoHeight => {
|
||||
let paragraph_width = shape_bounds_after.width();
|
||||
let mut paragraphs = content.to_paragraphs(None, None, None);
|
||||
let mut paragraphs = paragraph_builder_group_from_text(content, None, None, None);
|
||||
let height = auto_height(&mut paragraphs, paragraph_width);
|
||||
let resize_transform = math::resize_matrix(
|
||||
&shape_bounds_after,
|
||||
@ -212,8 +212,8 @@ fn propagate_transform(
|
||||
transform.post_concat(&resize_transform);
|
||||
}
|
||||
GrowType::AutoWidth => {
|
||||
let paragraph_width = content.get_width();
|
||||
let mut paragraphs = content.to_paragraphs(None, None, None);
|
||||
let paragraph_width = content.width();
|
||||
let mut paragraphs = paragraph_builder_group_from_text(content, None, None, None);
|
||||
let height = auto_height(&mut paragraphs, paragraph_width);
|
||||
let resize_transform = math::resize_matrix(
|
||||
&shape_bounds_after,
|
||||
|
||||
@ -1,19 +1,16 @@
|
||||
use crate::{
|
||||
math::{Matrix, Rect},
|
||||
render::{default_font, filters::compose_filters, DEFAULT_EMOJI_FONT},
|
||||
render::{default_font, DEFAULT_EMOJI_FONT},
|
||||
textlayout::paragraph_builder_group_from_text,
|
||||
};
|
||||
|
||||
use skia_safe::{
|
||||
self as skia,
|
||||
paint::Paint,
|
||||
textlayout::{ParagraphBuilder, ParagraphStyle},
|
||||
ImageFilter, MaskFilter,
|
||||
};
|
||||
use skia_safe::{self as skia, paint::Paint, textlayout::ParagraphStyle, ImageFilter, MaskFilter};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::FontFamily;
|
||||
use crate::shapes::{self, merge_fills, set_paint_fill, Stroke, StrokeKind};
|
||||
use crate::utils::{get_fallback_fonts, get_font_collection, uuid_from_u32};
|
||||
use crate::shapes::{self, merge_fills};
|
||||
use crate::textlayout::{auto_height, auto_width};
|
||||
use crate::utils::uuid_from_u32;
|
||||
use crate::wasm::fills::parse_fills_from_bytes;
|
||||
use crate::Uuid;
|
||||
|
||||
@ -42,30 +39,6 @@ pub struct TextContent {
|
||||
pub grow_type: GrowType,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
impl TextContent {
|
||||
pub fn new(bounds: Rect, grow_type: GrowType) -> Self {
|
||||
Self {
|
||||
@ -85,13 +58,12 @@ impl TextContent {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_xywh(&mut self, x: f32, y: f32, w: f32, h: f32) {
|
||||
self.bounds = Rect::from_xywh(x, y, w, h);
|
||||
pub fn bounds(&self) -> Rect {
|
||||
self.bounds
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn width(&self) -> f32 {
|
||||
self.bounds.width()
|
||||
pub fn set_xywh(&mut self, x: f32, y: f32, w: f32, h: f32) {
|
||||
self.bounds = Rect::from_xywh(x, y, w, h);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@ -108,106 +80,17 @@ impl TextContent {
|
||||
self.paragraphs.push(paragraph);
|
||||
}
|
||||
|
||||
pub fn to_paragraphs(
|
||||
&self,
|
||||
blur: Option<&ImageFilter>,
|
||||
blur_mask: Option<&MaskFilter>,
|
||||
shadow: Option<&Paint>,
|
||||
) -> Vec<Vec<ParagraphBuilder>> {
|
||||
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 ¶graph.children {
|
||||
let text_style =
|
||||
leaf.to_style(&self.bounds, fallback_fonts, blur, blur_mask, shadow);
|
||||
let text = leaf.apply_text_transform();
|
||||
builder.push_style(&text_style);
|
||||
builder.add_text(&text);
|
||||
}
|
||||
paragraph_group.push(vec![builder]);
|
||||
}
|
||||
|
||||
paragraph_group
|
||||
pub fn paragraphs(&self) -> &[Paragraph] {
|
||||
&self.paragraphs
|
||||
}
|
||||
|
||||
pub fn to_stroke_paragraphs(
|
||||
&self,
|
||||
stroke: &Stroke,
|
||||
bounds: &Rect,
|
||||
blur: Option<&ImageFilter>,
|
||||
blur_mask: Option<&MaskFilter>,
|
||||
shadow: Option<&Paint>,
|
||||
count_inner_strokes: usize,
|
||||
) -> Vec<Vec<ParagraphBuilder>> {
|
||||
let fallback_fonts = get_fallback_fonts();
|
||||
let fonts = get_font_collection();
|
||||
let mut paragraph_group = Vec::new();
|
||||
|
||||
for paragraph in &self.paragraphs {
|
||||
let mut stroke_paragraphs_map: std::collections::HashMap<usize, ParagraphBuilder> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
for leaf in paragraph.children.iter() {
|
||||
let mut text_paint = merge_fills(&leaf.fills, *bounds);
|
||||
if let Some(blur_mask) = blur_mask {
|
||||
text_paint.set_mask_filter(blur_mask.clone());
|
||||
}
|
||||
|
||||
let stroke_paints = if shadow.is_some() {
|
||||
get_text_stroke_paints_with_shadows(
|
||||
stroke,
|
||||
blur,
|
||||
blur_mask,
|
||||
shadow,
|
||||
leaf.is_transparent(),
|
||||
)
|
||||
} else {
|
||||
get_text_stroke_paints(
|
||||
stroke,
|
||||
bounds,
|
||||
&text_paint,
|
||||
blur,
|
||||
blur_mask,
|
||||
count_inner_strokes,
|
||||
)
|
||||
};
|
||||
|
||||
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 stroke_style =
|
||||
leaf.to_stroke_style(&stroke_paint, fallback_fonts, blur, blur_mask, None);
|
||||
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
|
||||
}
|
||||
|
||||
pub fn get_width(&self) -> f32 {
|
||||
pub fn width(&self) -> f32 {
|
||||
if self.grow_type() == GrowType::AutoWidth {
|
||||
let temp_paragraphs = self.to_paragraphs(None, None, None);
|
||||
let temp_paragraphs = paragraph_builder_group_from_text(self, None, None, None);
|
||||
let mut temp_paragraphs = temp_paragraphs;
|
||||
auto_width(&mut temp_paragraphs, f32::MAX).ceil()
|
||||
} else {
|
||||
self.width()
|
||||
self.bounds.width()
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,8 +103,8 @@ impl TextContent {
|
||||
}
|
||||
|
||||
pub fn visual_bounds(&self) -> (f32, f32) {
|
||||
let paragraph_width = self.get_width();
|
||||
let mut paragraphs = self.to_paragraphs(None, None, None);
|
||||
let paragraph_width = self.width();
|
||||
let mut paragraphs = paragraph_builder_group_from_text(self, None, None, None);
|
||||
let paragraph_height = auto_height(&mut paragraphs, paragraph_width);
|
||||
(paragraph_width, paragraph_height)
|
||||
}
|
||||
@ -311,8 +194,7 @@ impl Paragraph {
|
||||
self.children = children;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_children(&self) -> &Vec<TextLeaf> {
|
||||
pub fn children(&self) -> &[TextLeaf] {
|
||||
&self.children
|
||||
}
|
||||
|
||||
@ -418,6 +300,10 @@ impl TextLeaf {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fills(&self) -> &[shapes::Fill] {
|
||||
&self.fills
|
||||
}
|
||||
|
||||
pub fn to_style(
|
||||
&self,
|
||||
content_bounds: &Rect,
|
||||
@ -741,211 +627,3 @@ impl From<&Vec<u8>> for RawTextData {
|
||||
Self { paragraph }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_built_paragraphs(
|
||||
paragraphs: &mut [Vec<ParagraphBuilder>],
|
||||
width: f32,
|
||||
) -> Vec<Vec<skia_safe::textlayout::Paragraph>> {
|
||||
build_paragraphs_with_width(paragraphs, width)
|
||||
}
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn get_text_stroke_paints_with_shadows(
|
||||
stroke: &Stroke,
|
||||
blur: Option<&ImageFilter>,
|
||||
blur_mask: Option<&MaskFilter>,
|
||||
shadow: Option<&Paint>,
|
||||
is_transparent: bool,
|
||||
) -> Vec<Paint> {
|
||||
let mut paints = Vec::new();
|
||||
|
||||
match stroke.kind {
|
||||
StrokeKind::Inner => {
|
||||
let mut paint = skia_safe::Paint::default();
|
||||
paint.set_style(skia::PaintStyle::Fill);
|
||||
paint.set_anti_alias(true);
|
||||
|
||||
if let Some(blur) = blur {
|
||||
paint.set_image_filter(blur.clone());
|
||||
}
|
||||
|
||||
if let Some(shadow) = shadow {
|
||||
paint.set_image_filter(shadow.image_filter());
|
||||
}
|
||||
|
||||
paints.push(paint.clone());
|
||||
|
||||
if is_transparent {
|
||||
let image_filter = skia_safe::image_filters::erode(
|
||||
(stroke.width, stroke.width),
|
||||
paint.image_filter(),
|
||||
None,
|
||||
);
|
||||
paint.set_image_filter(image_filter);
|
||||
paint.set_blend_mode(skia::BlendMode::DstOut);
|
||||
paints.push(paint.clone());
|
||||
}
|
||||
}
|
||||
StrokeKind::Center => {
|
||||
let mut paint = skia_safe::Paint::default();
|
||||
paint.set_anti_alias(true);
|
||||
paint.set_stroke_width(stroke.width);
|
||||
|
||||
if let Some(blur) = blur {
|
||||
paint.set_image_filter(blur.clone());
|
||||
}
|
||||
|
||||
if let Some(shadow) = shadow {
|
||||
paint.set_image_filter(shadow.image_filter());
|
||||
}
|
||||
|
||||
if is_transparent {
|
||||
paint.set_style(skia::PaintStyle::Stroke);
|
||||
} else {
|
||||
paint.set_style(skia::PaintStyle::StrokeAndFill);
|
||||
}
|
||||
|
||||
paints.push(paint);
|
||||
}
|
||||
StrokeKind::Outer => {
|
||||
let mut paint = skia_safe::Paint::default();
|
||||
paint.set_style(skia::PaintStyle::StrokeAndFill);
|
||||
paint.set_anti_alias(true);
|
||||
paint.set_stroke_width(stroke.width * 2.0);
|
||||
|
||||
if let Some(blur_mask) = blur_mask {
|
||||
paint.set_mask_filter(blur_mask.clone());
|
||||
}
|
||||
|
||||
if let Some(shadow) = shadow {
|
||||
paint.set_image_filter(shadow.image_filter());
|
||||
}
|
||||
|
||||
paints.push(paint.clone());
|
||||
|
||||
if is_transparent {
|
||||
let image_filter = skia_safe::image_filters::erode(
|
||||
(stroke.width, stroke.width),
|
||||
paint.image_filter(),
|
||||
None,
|
||||
);
|
||||
paint.set_image_filter(image_filter);
|
||||
paint.set_blend_mode(skia::BlendMode::DstOut);
|
||||
paints.push(paint.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
paints
|
||||
}
|
||||
|
||||
fn get_text_stroke_paints(
|
||||
stroke: &Stroke,
|
||||
bounds: &Rect,
|
||||
text_paint: &Paint,
|
||||
blur: Option<&ImageFilter>,
|
||||
blur_mask: Option<&MaskFilter>,
|
||||
count_inner_strokes: usize,
|
||||
) -> 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);
|
||||
if let Some(blur) = blur {
|
||||
paint.set_image_filter(blur.clone());
|
||||
}
|
||||
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);
|
||||
if let Some(blur) = blur {
|
||||
paint.set_image_filter(blur.clone());
|
||||
}
|
||||
paints.push(paint);
|
||||
} else {
|
||||
let mut paint = text_paint.clone();
|
||||
paint.set_style(skia::PaintStyle::Fill);
|
||||
paint.set_anti_alias(false);
|
||||
set_paint_fill(&mut paint, &stroke.fill, bounds);
|
||||
paints.push(paint);
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
let image_filter =
|
||||
skia_safe::image_filters::erode((stroke.width, stroke.width), None, None);
|
||||
|
||||
let filter = compose_filters(blur, image_filter.as_ref());
|
||||
paint.set_image_filter(filter);
|
||||
paint.set_anti_alias(false);
|
||||
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);
|
||||
if let Some(blur) = blur {
|
||||
paint.set_image_filter(blur.clone());
|
||||
}
|
||||
|
||||
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);
|
||||
if let Some(blur_mask) = blur_mask {
|
||||
paint.set_mask_filter(blur_mask.clone());
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use crate::shapes::text::TextContent;
|
||||
use crate::{shapes::text::TextContent, textlayout::paragraph_builder_group_from_text};
|
||||
use skia_safe::{
|
||||
self as skia, textlayout::Paragraph as SkiaParagraph, FontMetrics, Point, Rect, TextBlob,
|
||||
};
|
||||
@ -20,7 +20,7 @@ impl TextPaths {
|
||||
let mut paths = Vec::new();
|
||||
|
||||
let mut offset_y = self.bounds.y();
|
||||
let mut paragraphs = self.to_paragraphs(None, None, None);
|
||||
let mut paragraphs = paragraph_builder_group_from_text(&self.0, None, None, None);
|
||||
|
||||
for paragraphs in paragraphs.iter_mut() {
|
||||
for paragraph_builder in paragraphs.iter_mut() {
|
||||
|
||||
339
render-wasm/src/textlayout.rs
Normal file
339
render-wasm/src/textlayout.rs
Normal file
@ -0,0 +1,339 @@
|
||||
use skia_safe::{self as skia, textlayout::ParagraphBuilder, ImageFilter, MaskFilter, Paint, Rect};
|
||||
|
||||
use crate::{
|
||||
render::filters::compose_filters,
|
||||
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()
|
||||
}
|
||||
|
||||
type ParagraphBuilderGroup = Vec<ParagraphBuilder>;
|
||||
|
||||
pub fn paragraph_builder_group_from_text(
|
||||
text_content: &TextContent,
|
||||
blur: Option<&ImageFilter>,
|
||||
blur_mask: Option<&MaskFilter>,
|
||||
shadow: Option<&Paint>,
|
||||
) -> 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 text_style = leaf.to_style(
|
||||
&text_content.bounds(),
|
||||
fallback_fonts,
|
||||
blur,
|
||||
blur_mask,
|
||||
shadow,
|
||||
);
|
||||
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,
|
||||
blur: Option<&ImageFilter>,
|
||||
blur_mask: Option<&MaskFilter>,
|
||||
shadow: Option<&Paint>,
|
||||
count_inner_strokes: usize,
|
||||
) -> Vec<ParagraphBuilderGroup> {
|
||||
let fallback_fonts = get_fallback_fonts();
|
||||
let fonts = get_font_collection();
|
||||
let mut paragraph_group = Vec::new();
|
||||
|
||||
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 mut text_paint = merge_fills(leaf.fills(), *bounds);
|
||||
if let Some(blur_mask) = blur_mask {
|
||||
text_paint.set_mask_filter(blur_mask.clone());
|
||||
}
|
||||
|
||||
let stroke_paints = if shadow.is_some() {
|
||||
get_text_stroke_paints_with_shadows(
|
||||
stroke,
|
||||
blur,
|
||||
blur_mask,
|
||||
shadow,
|
||||
leaf.is_transparent(),
|
||||
)
|
||||
} else {
|
||||
get_text_stroke_paints(
|
||||
stroke,
|
||||
bounds,
|
||||
&text_paint,
|
||||
blur,
|
||||
blur_mask,
|
||||
count_inner_strokes,
|
||||
)
|
||||
};
|
||||
|
||||
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 stroke_style =
|
||||
leaf.to_stroke_style(&stroke_paint, fallback_fonts, blur, blur_mask, None);
|
||||
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_with_shadows(
|
||||
stroke: &Stroke,
|
||||
blur: Option<&ImageFilter>,
|
||||
blur_mask: Option<&MaskFilter>,
|
||||
shadow: Option<&Paint>,
|
||||
is_transparent: bool,
|
||||
) -> Vec<Paint> {
|
||||
let mut paints = Vec::new();
|
||||
|
||||
match stroke.kind {
|
||||
StrokeKind::Inner => {
|
||||
let mut paint = Paint::default();
|
||||
paint.set_style(skia::PaintStyle::Fill);
|
||||
paint.set_anti_alias(true);
|
||||
|
||||
if let Some(blur) = blur {
|
||||
paint.set_image_filter(blur.clone());
|
||||
}
|
||||
|
||||
if let Some(shadow) = shadow {
|
||||
paint.set_image_filter(shadow.image_filter());
|
||||
}
|
||||
|
||||
paints.push(paint.clone());
|
||||
|
||||
if is_transparent {
|
||||
let image_filter = skia_safe::image_filters::erode(
|
||||
(stroke.width, stroke.width),
|
||||
paint.image_filter(),
|
||||
None,
|
||||
);
|
||||
paint.set_image_filter(image_filter);
|
||||
paint.set_blend_mode(skia::BlendMode::DstOut);
|
||||
paints.push(paint.clone());
|
||||
}
|
||||
}
|
||||
StrokeKind::Center => {
|
||||
let mut paint = skia_safe::Paint::default();
|
||||
paint.set_anti_alias(true);
|
||||
paint.set_stroke_width(stroke.width);
|
||||
|
||||
if let Some(blur) = blur {
|
||||
paint.set_image_filter(blur.clone());
|
||||
}
|
||||
|
||||
if let Some(shadow) = shadow {
|
||||
paint.set_image_filter(shadow.image_filter());
|
||||
}
|
||||
|
||||
if is_transparent {
|
||||
paint.set_style(skia::PaintStyle::Stroke);
|
||||
} else {
|
||||
paint.set_style(skia::PaintStyle::StrokeAndFill);
|
||||
}
|
||||
|
||||
paints.push(paint);
|
||||
}
|
||||
StrokeKind::Outer => {
|
||||
let mut paint = skia_safe::Paint::default();
|
||||
paint.set_style(skia::PaintStyle::StrokeAndFill);
|
||||
paint.set_anti_alias(true);
|
||||
paint.set_stroke_width(stroke.width * 2.0);
|
||||
|
||||
if let Some(blur_mask) = blur_mask {
|
||||
paint.set_mask_filter(blur_mask.clone());
|
||||
}
|
||||
|
||||
if let Some(shadow) = shadow {
|
||||
paint.set_image_filter(shadow.image_filter());
|
||||
}
|
||||
|
||||
paints.push(paint.clone());
|
||||
|
||||
if is_transparent {
|
||||
let image_filter = skia_safe::image_filters::erode(
|
||||
(stroke.width, stroke.width),
|
||||
paint.image_filter(),
|
||||
None,
|
||||
);
|
||||
paint.set_image_filter(image_filter);
|
||||
paint.set_blend_mode(skia::BlendMode::DstOut);
|
||||
paints.push(paint.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
paints
|
||||
}
|
||||
|
||||
fn get_text_stroke_paints(
|
||||
stroke: &Stroke,
|
||||
bounds: &Rect,
|
||||
text_paint: &Paint,
|
||||
blur: Option<&ImageFilter>,
|
||||
blur_mask: Option<&MaskFilter>,
|
||||
count_inner_strokes: usize,
|
||||
) -> 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);
|
||||
if let Some(blur) = blur {
|
||||
paint.set_image_filter(blur.clone());
|
||||
}
|
||||
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);
|
||||
if let Some(blur) = blur {
|
||||
paint.set_image_filter(blur.clone());
|
||||
}
|
||||
paints.push(paint);
|
||||
} else {
|
||||
let mut paint = text_paint.clone();
|
||||
paint.set_style(skia::PaintStyle::Fill);
|
||||
paint.set_anti_alias(false);
|
||||
set_paint_fill(&mut paint, &stroke.fill, bounds);
|
||||
paints.push(paint);
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
let image_filter =
|
||||
skia_safe::image_filters::erode((stroke.width, stroke.width), None, None);
|
||||
|
||||
let filter = compose_filters(blur, image_filter.as_ref());
|
||||
paint.set_image_filter(filter);
|
||||
paint.set_anti_alias(false);
|
||||
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);
|
||||
if let Some(blur) = blur {
|
||||
paint.set_image_filter(blur.clone());
|
||||
}
|
||||
|
||||
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);
|
||||
if let Some(blur_mask) = blur_mask {
|
||||
paint.set_mask_filter(blur_mask.clone());
|
||||
}
|
||||
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
|
||||
}
|
||||
@ -1,8 +1,9 @@
|
||||
use crate::mem;
|
||||
use crate::shapes::{auto_height, build_paragraphs_with_width, GrowType, RawTextData, Type};
|
||||
|
||||
use crate::STATE;
|
||||
use crate::{with_current_shape, with_current_shape_mut};
|
||||
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};
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn clear_shape_text() {
|
||||
@ -44,8 +45,8 @@ pub extern "C" fn get_text_dimensions() -> *mut u8 {
|
||||
|
||||
if let Type::Text(content) = &shape.shape_type {
|
||||
// 1. Reset Paragraphs
|
||||
let paragraph_width = content.get_width();
|
||||
let mut paragraphs = content.to_paragraphs(None, None, None);
|
||||
let paragraph_width = content.width();
|
||||
let mut paragraphs = paragraph_builder_group_from_text(content, None, None, None);
|
||||
let built_paragraphs = build_paragraphs_with_width(&mut paragraphs, paragraph_width);
|
||||
|
||||
// 2. Max Width Calculation
|
||||
@ -57,12 +58,14 @@ pub extern "C" fn get_text_dimensions() -> *mut u8 {
|
||||
// 3. Width and Height Calculation
|
||||
match content.grow_type() {
|
||||
GrowType::AutoHeight => {
|
||||
let mut paragraph_height = content.to_paragraphs(None, None, None);
|
||||
let mut paragraph_height =
|
||||
paragraph_builder_group_from_text(content, None, None, None);
|
||||
height = auto_height(&mut paragraph_height, paragraph_width).ceil();
|
||||
}
|
||||
GrowType::AutoWidth => {
|
||||
width = paragraph_width;
|
||||
let mut paragraph_height = content.to_paragraphs(None, None, None);
|
||||
let mut paragraph_height =
|
||||
paragraph_builder_group_from_text(content, None, None, None);
|
||||
height = auto_height(&mut paragraph_height, paragraph_width).ceil();
|
||||
}
|
||||
GrowType::Fixed => {}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user