From 361bdb4a04918ed7f275c647c144dde64e5abeb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Thu, 25 Sep 2025 16:54:07 +0200 Subject: [PATCH] :recycle: Decouple serialization from text/layout models" (#7360) * :recycle: Move text serialization code to wasm module * :recycle: Add serializer for TextAlign * :recycle: Add serializers for TextDirection and TextDecoration * :recycle: Add serializer for TextTransform * :recycle: Remove unused font_style from TextLeaf model * :recycle: Refactor parsing of TextLeaf from bytes * :recycle: Decouple tight serialization of Paragraph --- frontend/src/app/render_wasm/api.cljs | 4 + frontend/src/app/render_wasm/serializers.cljs | 43 +-- render-wasm/src/shapes/text.rs | 310 +++--------------- render-wasm/src/wasm/fills.rs | 1 + render-wasm/src/wasm/text.rs | 287 +++++++++++++++- 5 files changed, 348 insertions(+), 297 deletions(-) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 4f8e84e645..f0e6d2ca21 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -1068,6 +1068,10 @@ :sizing (unchecked-get module "RawSizing") :vertical-align (unchecked-get module "RawVerticalAlign") :fill-data (unchecked-get module "RawFillData") + :text-align (unchecked-get module "RawTextAlign") + :text-direction (unchecked-get module "RawTextDirection") + :text-decoration (unchecked-get module "RawTextDecoration") + :text-transform (unchecked-get module "RawTextTransform") :segment-data (unchecked-get module "RawSegmentData")}] (set! wasm/serializers serializers) (default)))) diff --git a/frontend/src/app/render_wasm/serializers.cljs b/frontend/src/app/render_wasm/serializers.cljs index 4c718c83fa..306a768f8e 100644 --- a/frontend/src/app/render_wasm/serializers.cljs +++ b/frontend/src/app/render_wasm/serializers.cljs @@ -205,46 +205,33 @@ default (unchecked-get values "top")] (d/nilv (unchecked-get values (d/name vertical-align)) default))) -;; TODO: Find/Create a Rust enum for this (defn translate-text-align [text-align] - (case text-align - "left" 0 - "center" 1 - "right" 2 - "justify" 3 - 0)) + (let [values (unchecked-get wasm/serializers "text-align") + default (unchecked-get values "left")] + (d/nilv (unchecked-get values (d/name text-align)) default))) + ;; TODO: Find/Create a Rust enum for this (defn translate-text-transform [text-transform] - (case text-transform - "none" 0 - "uppercase" 1 - "lowercase" 2 - "capitalize" 3 - nil 0 - 0)) + (let [values (unchecked-get wasm/serializers "text-transform") + default (unchecked-get values "none")] + (d/nilv (unchecked-get values (d/name text-transform)) default))) + -;; TODO: Find/Create a Rust enum for this (defn translate-text-decoration [text-decoration] - (case text-decoration - "none" 0 - "underline" 1 - "line-through" 2 - "overline" 3 - nil 0 - 0)) + (let [values (unchecked-get wasm/serializers "text-decoration") + default (unchecked-get values "none")] + (d/nilv (unchecked-get values (d/name text-decoration)) default))) -;; TODO: Find/Create a Rust enum for this (defn translate-text-direction [text-direction] - (case text-direction - "ltr" 0 - "rtl" 1 - nil 0 - 0)) + (let [values (unchecked-get wasm/serializers "text-direction") + default (unchecked-get values "ltr")] + (d/nilv (unchecked-get values (d/name text-direction)) default))) + (defn translate-font-style [font-style] diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index 1b466d7001..a659a7f1a7 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -15,9 +15,7 @@ use std::collections::HashSet; use super::FontFamily; use crate::shapes::{self, merge_fills}; -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; // TODO: maybe move this to the wasm module? @@ -368,15 +366,25 @@ impl Default for TextContent { } } +pub type TextAlign = skia::textlayout::TextAlign; +pub type TextDirection = skia::textlayout::TextDirection; +pub type TextDecoration = skia::textlayout::TextDecoration; + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum TextTransform { + Lowercase, + Uppercase, + Capitalize, +} + // FIXME: Rethink this type. We'll probably need to move the serialization to the -// wasm moduel and store here meaningful model values (and/or skia type aliases) +// wasm module and store here meaningful model values (and/or skia type aliases) #[derive(Debug, PartialEq, Clone)] pub struct Paragraph { - num_leaves: u32, - text_align: u8, - text_direction: u8, - text_decoration: u8, - text_transform: u8, + text_align: TextAlign, + text_direction: TextDirection, + text_decoration: Option, + text_transform: Option, line_height: f32, letter_spacing: f32, typography_ref_file: Uuid, @@ -387,11 +395,10 @@ pub struct Paragraph { impl Default for Paragraph { fn default() -> Self { Self { - num_leaves: 0, - text_align: 0, - text_direction: 0, - text_decoration: 0, - text_transform: 0, + text_align: TextAlign::default(), + text_direction: TextDirection::LTR, + text_decoration: None, + text_transform: None, line_height: 1.0, letter_spacing: 0.0, typography_ref_file: Uuid::nil(), @@ -404,11 +411,10 @@ impl Default for Paragraph { impl Paragraph { #[allow(clippy::too_many_arguments)] pub fn new( - num_leaves: u32, - text_align: u8, - text_direction: u8, - text_decoration: u8, - text_transform: u8, + text_align: TextAlign, + text_direction: TextDirection, + text_decoration: Option, + text_transform: Option, line_height: f32, letter_spacing: f32, typography_ref_file: Uuid, @@ -416,7 +422,6 @@ impl Paragraph { children: Vec, ) -> Self { Self { - num_leaves, text_align, text_direction, text_decoration, @@ -446,18 +451,8 @@ impl Paragraph { // FIXME: move serialization to wasm module pub fn paragraph_to_style(&self) -> ParagraphStyle { let mut style = ParagraphStyle::default(); - style.set_text_align(match self.text_align { - 0 => skia::textlayout::TextAlign::Left, - 1 => skia::textlayout::TextAlign::Center, - 2 => skia::textlayout::TextAlign::Right, - 3 => skia::textlayout::TextAlign::Justify, - _ => skia::textlayout::TextAlign::Left, - }); - style.set_text_direction(match self.text_direction { - 0 => skia::textlayout::TextDirection::LTR, - 1 => skia::textlayout::TextDirection::RTL, - _ => skia::textlayout::TextDirection::LTR, - }); + style.set_text_align(self.text_align); + style.set_text_direction(self.text_direction); if !self.children.is_empty() { let reference_child = self @@ -502,12 +497,11 @@ pub struct TextLeaf { font_family: FontFamily, font_size: f32, letter_spacing: f32, - font_style: u8, font_weight: i32, font_variant_id: Uuid, - text_decoration: u8, - text_transform: u8, - text_direction: u8, + text_decoration: Option, + text_transform: Option, + text_direction: TextDirection, fills: Vec, } @@ -518,10 +512,9 @@ impl TextLeaf { font_family: FontFamily, font_size: f32, letter_spacing: f32, - font_style: u8, - text_decoration: u8, - text_transform: u8, - text_direction: u8, + text_decoration: Option, + text_transform: Option, + text_direction: TextDirection, font_weight: i32, font_variant_id: Uuid, fills: Vec, @@ -531,7 +524,6 @@ impl TextLeaf { font_family, font_size, letter_spacing, - font_style, text_decoration, text_transform, text_direction, @@ -541,6 +533,10 @@ impl TextLeaf { } } + pub fn set_text(&mut self, text: String) { + self.text = text; + } + pub fn fills(&self) -> &[shapes::Fill] { &self.fills } @@ -568,11 +564,8 @@ impl TextLeaf { style.set_half_leading(false); style.set_decoration_type(match self.text_decoration { - 0 => skia::textlayout::TextDecoration::NO_DECORATION, - 1 => skia::textlayout::TextDecoration::UNDERLINE, - 2 => skia::textlayout::TextDecoration::LINE_THROUGH, - 3 => skia::textlayout::TextDecoration::OVERLINE, - _ => skia::textlayout::TextDecoration::NO_DECORATION, + Some(text_decoration) => text_decoration, + None => skia::textlayout::TextDecoration::NO_DECORATION, }); // Trick to avoid showing the text decoration @@ -611,11 +604,8 @@ impl TextLeaf { style.set_font_size(self.font_size); style.set_letter_spacing(self.letter_spacing); style.set_decoration_type(match self.text_decoration { - 0 => skia::textlayout::TextDecoration::NO_DECORATION, - 1 => skia::textlayout::TextDecoration::UNDERLINE, - 2 => skia::textlayout::TextDecoration::LINE_THROUGH, - 3 => skia::textlayout::TextDecoration::OVERLINE, - _ => skia::textlayout::TextDecoration::NO_DECORATION, + Some(text_decoration) => text_decoration, + None => skia::textlayout::TextDecoration::NO_DECORATION, }); style } @@ -626,9 +616,9 @@ impl TextLeaf { pub fn apply_text_transform(&self) -> String { match self.text_transform { - 1 => self.text.to_uppercase(), - 2 => self.text.to_lowercase(), - 3 => self + Some(TextTransform::Uppercase) => self.text.to_uppercase(), + Some(TextTransform::Lowercase) => self.text.to_lowercase(), + Some(TextTransform::Capitalize) => self .text .split_whitespace() .map(|word| { @@ -640,7 +630,7 @@ impl TextLeaf { }) .collect::>() .join(" "), - _ => self.text.clone(), + None => self.text.clone(), } } @@ -655,215 +645,3 @@ impl TextLeaf { }) } } - -const RAW_PARAGRAPH_DATA_SIZE: usize = std::mem::size_of::(); -const RAW_LEAF_DATA_SIZE: usize = std::mem::size_of::(); -pub const RAW_LEAF_FILLS_SIZE: usize = 160; - -#[repr(C)] -#[derive(Debug, Clone, Copy)] -pub struct RawTextLeaf { - font_style: u8, - text_decoration: u8, - text_transform: u8, - font_size: f32, - letter_spacing: f32, - font_weight: i32, - font_id: [u32; 4], - font_family: [u8; 4], - font_variant_id: [u32; 4], - text_length: u32, - total_fills: u32, -} - -impl From<[u8; RAW_LEAF_DATA_SIZE]> for RawTextLeaf { - fn from(bytes: [u8; RAW_LEAF_DATA_SIZE]) -> Self { - unsafe { std::mem::transmute(bytes) } - } -} - -impl TryFrom<&[u8]> for RawTextLeaf { - type Error = String; - fn try_from(bytes: &[u8]) -> Result { - let data: [u8; RAW_LEAF_DATA_SIZE] = bytes - .get(0..RAW_LEAF_DATA_SIZE) - .and_then(|slice| slice.try_into().ok()) - .ok_or("Invalid text leaf data".to_string())?; - Ok(RawTextLeaf::from(data)) - } -} - -#[allow(dead_code)] -#[repr(C)] -#[derive(Debug, Clone)] -pub struct RawTextLeafData { - font_style: u8, - text_decoration: u8, - text_transform: u8, - text_direction: u8, - font_size: f32, - letter_spacing: f32, - font_weight: i32, - font_id: [u32; 4], - font_family: [u8; 4], - font_variant_id: [u32; 4], - text_length: u32, - total_fills: u32, - fills: Vec, -} - -impl From<&[u8]> for RawTextLeafData { - fn from(bytes: &[u8]) -> Self { - let text_leaf: RawTextLeaf = RawTextLeaf::try_from(bytes).unwrap(); - let total_fills = text_leaf.total_fills as usize; - - // Use checked_mul to prevent overflow - let fills_size = total_fills - .checked_mul(RAW_LEAF_FILLS_SIZE) - .expect("Overflow occurred while calculating fills size"); - - let fills_start = RAW_LEAF_DATA_SIZE; - let fills_end = fills_start + fills_size; - let buffer = &bytes[fills_start..fills_end]; - let fills = parse_fills_from_bytes(buffer, total_fills); - - Self { - font_style: text_leaf.font_style, - text_decoration: text_leaf.text_decoration, - text_transform: text_leaf.text_transform, - text_direction: 0, // TODO: AƱadirlo - font_size: text_leaf.font_size, - letter_spacing: text_leaf.letter_spacing, - font_weight: text_leaf.font_weight, - font_id: text_leaf.font_id, - font_family: text_leaf.font_family, - font_variant_id: text_leaf.font_variant_id, - text_length: text_leaf.text_length, - total_fills: text_leaf.total_fills, - fills, - } - } -} - -#[repr(C)] -#[repr(align(4))] -#[derive(Debug, Clone, Copy)] -pub struct RawParagraphData { - num_leaves: u32, - text_align: u8, - text_direction: u8, - text_decoration: u8, - text_transform: u8, - line_height: f32, - letter_spacing: f32, - typography_ref_file: [u32; 4], - typography_ref_id: [u32; 4], -} - -impl From<[u8; RAW_PARAGRAPH_DATA_SIZE]> for RawParagraphData { - fn from(bytes: [u8; RAW_PARAGRAPH_DATA_SIZE]) -> Self { - unsafe { std::mem::transmute(bytes) } - } -} - -impl TryFrom<&[u8]> for RawParagraphData { - type Error = String; - fn try_from(bytes: &[u8]) -> Result { - let data: [u8; RAW_PARAGRAPH_DATA_SIZE] = bytes - .get(0..RAW_PARAGRAPH_DATA_SIZE) - .and_then(|slice| slice.try_into().ok()) - .ok_or("Invalid paragraph data".to_string())?; - Ok(RawParagraphData::from(data)) - } -} - -impl RawTextData { - fn text_from_bytes(buffer: &[u8], offset: usize, text_length: u32) -> (String, usize) { - let text_length = text_length as usize; - let text_end = offset + text_length; - - if text_end > buffer.len() { - panic!( - "Invalid text range: offset={}, text_end={}, buffer_len={}", - offset, - text_end, - buffer.len() - ); - } - - let text_utf8 = buffer[offset..text_end].to_vec(); - if text_utf8.is_empty() { - return (String::new(), text_end); - } - - let text = String::from_utf8_lossy(&text_utf8).to_string(); - (text, text_end) - } -} - -// TODO: maybe move this to the wasm module? -pub struct RawTextData { - pub paragraph: Paragraph, -} - -// TODO: maybe move this to the wasm module? -impl From<&Vec> for RawTextData { - fn from(bytes: &Vec) -> Self { - let paragraph = RawParagraphData::try_from(&bytes[..RAW_PARAGRAPH_DATA_SIZE]).unwrap(); - let mut offset = RAW_PARAGRAPH_DATA_SIZE; - let mut raw_text_leaves: Vec = Vec::new(); - let mut text_leaves: Vec = Vec::new(); - - for _ in 0..paragraph.num_leaves { - let text_leaf = RawTextLeafData::from(&bytes[offset..]); - raw_text_leaves.push(text_leaf.clone()); - offset += RAW_LEAF_DATA_SIZE + (text_leaf.total_fills as usize * RAW_LEAF_FILLS_SIZE); - } - - for text_leaf in raw_text_leaves.iter() { - let (text, new_offset) = - RawTextData::text_from_bytes(bytes, offset, text_leaf.text_length); - offset = new_offset; - - let font_id = uuid_from_u32(text_leaf.font_id); - let font_variant_id = uuid_from_u32(text_leaf.font_variant_id); - let font_style = crate::wasm::fonts::RawFontStyle::from(text_leaf.font_style); - - let font_family = - FontFamily::new(font_id, text_leaf.font_weight as u32, font_style.into()); - - let new_text_leaf = TextLeaf::new( - text, - font_family, - text_leaf.font_size, - text_leaf.letter_spacing, - text_leaf.font_style, - text_leaf.text_decoration, - text_leaf.text_transform, - text_leaf.text_direction, - text_leaf.font_weight, - font_variant_id, - text_leaf.fills.clone(), - ); - text_leaves.push(new_text_leaf); - } - - let typography_ref_file = uuid_from_u32(paragraph.typography_ref_file); - let typography_ref_id = uuid_from_u32(paragraph.typography_ref_id); - - let paragraph = Paragraph::new( - paragraph.num_leaves, - paragraph.text_align, - paragraph.text_direction, - paragraph.text_decoration, - paragraph.text_transform, - paragraph.line_height, - paragraph.letter_spacing, - typography_ref_file, - typography_ref_id, - text_leaves.clone(), - ); - - Self { paragraph } - } -} diff --git a/render-wasm/src/wasm/fills.rs b/render-wasm/src/wasm/fills.rs index dd57170555..24935e7491 100644 --- a/render-wasm/src/wasm/fills.rs +++ b/render-wasm/src/wasm/fills.rs @@ -53,6 +53,7 @@ impl TryFrom<&[u8]> for RawFillData { } } +// FIXME: return Result pub fn parse_fills_from_bytes(buffer: &[u8], num_fills: usize) -> Vec { buffer .chunks_exact(RAW_FILL_DATA_SIZE) diff --git a/render-wasm/src/wasm/text.rs b/render-wasm/src/wasm/text.rs index 1d69ad70c0..7af6a91235 100644 --- a/render-wasm/src/wasm/text.rs +++ b/render-wasm/src/wasm/text.rs @@ -1,9 +1,290 @@ use macros::ToJs; +use super::fonts::RawFontStyle; use crate::mem; -use crate::shapes::{GrowType, RawTextData, Type}; +use crate::shapes::{ + self, GrowType, TextAlign, TextDecoration, TextDirection, TextTransform, Type, +}; +use crate::utils::uuid_from_u32; use crate::{with_current_shape_mut, STATE}; +const RAW_LEAF_DATA_SIZE: usize = std::mem::size_of::(); +pub const RAW_LEAF_FILLS_SIZE: usize = 160; +const RAW_PARAGRAPH_DATA_SIZE: usize = std::mem::size_of::(); + +#[derive(Debug, PartialEq, Clone, Copy, ToJs)] +#[repr(u8)] +pub enum RawTextAlign { + Left = 0, + Center = 1, + Right = 2, + Justify = 3, +} + +impl From for TextAlign { + fn from(value: RawTextAlign) -> Self { + match value { + RawTextAlign::Left => TextAlign::Left, + RawTextAlign::Center => TextAlign::Center, + RawTextAlign::Right => TextAlign::Right, + RawTextAlign::Justify => TextAlign::Justify, + } + } +} + +#[derive(Debug, PartialEq, Clone, Copy, ToJs)] +#[repr(u8)] +pub enum RawTextDirection { + Ltr = 0, + Rtl = 1, +} + +impl From for TextDirection { + fn from(value: RawTextDirection) -> Self { + match value { + RawTextDirection::Ltr => TextDirection::LTR, + RawTextDirection::Rtl => TextDirection::RTL, + } + } +} + +#[derive(Debug, PartialEq, Clone, Copy, ToJs)] +#[repr(u8)] +pub enum RawTextDecoration { + None = 0, + Underline = 1, + LineThrough = 2, + Overline = 3, +} + +impl From for Option { + fn from(value: RawTextDecoration) -> Self { + match value { + RawTextDecoration::None => None, + RawTextDecoration::Underline => Some(TextDecoration::UNDERLINE), + RawTextDecoration::LineThrough => Some(TextDecoration::LINE_THROUGH), + RawTextDecoration::Overline => Some(TextDecoration::OVERLINE), + } + } +} + +#[derive(Debug, PartialEq, Clone, Copy, ToJs)] +#[repr(u8)] +pub enum RawTextTransform { + None = 0, + Uppercase = 1, + Lowercase = 2, + Capitalize = 3, +} + +impl From for Option { + fn from(value: RawTextTransform) -> Self { + match value { + RawTextTransform::None => None, + RawTextTransform::Uppercase => Some(TextTransform::Uppercase), + RawTextTransform::Lowercase => Some(TextTransform::Lowercase), + RawTextTransform::Capitalize => Some(TextTransform::Capitalize), + } + } +} + +#[repr(C)] +#[repr(align(4))] +#[derive(Debug, Clone, Copy)] +pub struct RawParagraphData { + leaf_count: u32, + text_align: RawTextAlign, + text_direction: RawTextDirection, + text_decoration: RawTextDecoration, + text_transform: RawTextTransform, + line_height: f32, + letter_spacing: f32, + typography_ref_file: [u32; 4], + typography_ref_id: [u32; 4], +} + +impl From<[u8; RAW_PARAGRAPH_DATA_SIZE]> for RawParagraphData { + fn from(bytes: [u8; RAW_PARAGRAPH_DATA_SIZE]) -> Self { + unsafe { std::mem::transmute(bytes) } + } +} + +impl TryFrom<&[u8]> for RawParagraphData { + type Error = String; + fn try_from(bytes: &[u8]) -> Result { + let data: [u8; RAW_PARAGRAPH_DATA_SIZE] = bytes + .get(0..RAW_PARAGRAPH_DATA_SIZE) + .and_then(|slice| slice.try_into().ok()) + .ok_or("Invalid paragraph data".to_string())?; + Ok(RawParagraphData::from(data)) + } +} + +// FIXME: Merge this struct with RawTextLeaf once we cap the amount of fills a text shape has +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct RawTextLeafAttrs { + font_style: RawFontStyle, + text_decoration: RawTextDecoration, + text_transform: RawTextTransform, + text_direction: RawTextDirection, + font_size: f32, + letter_spacing: f32, + font_weight: i32, + font_id: [u32; 4], + font_family: [u8; 4], + font_variant_id: [u32; 4], // TODO: maybe add RawUUID type + text_length: u32, + fill_count: u32, // FIXME: we should cap the amount of fills a text shape has +} + +impl From<[u8; RAW_LEAF_DATA_SIZE]> for RawTextLeafAttrs { + fn from(bytes: [u8; RAW_LEAF_DATA_SIZE]) -> Self { + unsafe { std::mem::transmute(bytes) } + } +} + +impl TryFrom<&[u8]> for RawTextLeafAttrs { + type Error = String; + fn try_from(bytes: &[u8]) -> Result { + let data: [u8; RAW_LEAF_DATA_SIZE] = bytes + .get(0..RAW_LEAF_DATA_SIZE) + .and_then(|slice| slice.try_into().ok()) + .ok_or("Invalid text leaf data".to_string())?; + Ok(RawTextLeafAttrs::from(data)) + } +} + +#[allow(dead_code)] +#[repr(C)] +#[derive(Debug, Clone)] +pub struct RawTextLeaf { + attrs: RawTextLeafAttrs, + raw_fills: Vec, // FIXME: remove this once we cap the amount of fills a text shape has +} + +impl TryFrom<&[u8]> for RawTextLeaf { + // TODO: use a proper error type + type Error = String; + + fn try_from(bytes: &[u8]) -> Result { + let raw_attrs: RawTextLeafAttrs = RawTextLeafAttrs::try_from(bytes)?; + let total_fills = raw_attrs.fill_count as usize; + + // Use checked_mul to prevent overflow + let fills_size = total_fills + .checked_mul(RAW_LEAF_FILLS_SIZE) + .ok_or("Overflow occurred while calculating fills size")?; + + let fills_start = RAW_LEAF_DATA_SIZE; + let fills_end = fills_start + fills_size; + let raw_fills = &bytes[fills_start..fills_end]; + + Ok(Self { + attrs: raw_attrs, + raw_fills: raw_fills.to_vec(), + }) + } +} + +impl From for shapes::TextLeaf { + fn from(value: RawTextLeaf) -> Self { + let text = String::default(); + + let font_family = shapes::FontFamily::new( + uuid_from_u32(value.attrs.font_id), + value.attrs.font_weight as u32, + value.attrs.font_style.into(), + ); + let fills = + super::fills::parse_fills_from_bytes(&value.raw_fills, value.attrs.fill_count as usize); + + Self::new( + text, + font_family, + value.attrs.font_size, + value.attrs.letter_spacing, + value.attrs.text_decoration.into(), + value.attrs.text_transform.into(), + value.attrs.text_direction.into(), + value.attrs.font_weight, + uuid_from_u32(value.attrs.font_variant_id), + fills, + ) + } +} + +#[repr(C)] +#[derive(Debug, Clone)] +pub struct RawParagraph { + attrs: RawParagraphData, + leaves: Vec, + text_buffer: Vec, +} + +impl TryFrom<&Vec> for RawParagraph { + // TODO: use a proper error type + type Error = String; + + fn try_from(bytes: &Vec) -> Result { + let attrs = RawParagraphData::try_from(&bytes[..RAW_PARAGRAPH_DATA_SIZE])?; + let mut offset = RAW_PARAGRAPH_DATA_SIZE; + let mut raw_text_leaves: Vec = Vec::new(); + + for _ in 0..attrs.leaf_count { + let text_leaf = RawTextLeaf::try_from(&bytes[offset..])?; + let leaf_size = + RAW_LEAF_DATA_SIZE + (text_leaf.attrs.fill_count as usize * RAW_LEAF_FILLS_SIZE); + + offset += leaf_size; + raw_text_leaves.push(text_leaf); + } + + let text_buffer = &bytes[offset..]; + + Ok(Self { + attrs, + leaves: raw_text_leaves, + text_buffer: text_buffer.to_vec(), + }) + } +} + +impl From for shapes::Paragraph { + fn from(value: RawParagraph) -> Self { + let typography_ref_file = uuid_from_u32(value.attrs.typography_ref_file); + let typography_ref_id = uuid_from_u32(value.attrs.typography_ref_id); + + let mut leaves = vec![]; + + let mut offset = 0; + for raw_leaf in value.leaves.into_iter() { + let delta = raw_leaf.attrs.text_length as usize; + let text_buffer = &value.text_buffer[offset..offset + delta]; + + let mut leaf = shapes::TextLeaf::from(raw_leaf); + if !text_buffer.is_empty() { + leaf.set_text(String::from_utf8_lossy(text_buffer).to_string()); + } + + leaves.push(leaf); + offset += delta; + } + + shapes::Paragraph::new( + value.attrs.text_align.into(), + value.attrs.text_direction.into(), + value.attrs.text_decoration.into(), + value.attrs.text_transform.into(), + value.attrs.line_height, + value.attrs.letter_spacing, + typography_ref_file, + typography_ref_id, + leaves, + ) + } +} + #[derive(Debug, PartialEq, Clone, Copy, ToJs)] #[repr(u8)] #[allow(dead_code)] @@ -40,9 +321,9 @@ pub extern "C" fn clear_shape_text() { pub extern "C" fn set_shape_text_content() { let bytes = mem::bytes(); with_current_shape_mut!(state, |shape: &mut Shape| { - let raw_text_data = RawTextData::from(&bytes); + let raw_text_data = RawParagraph::try_from(&bytes).unwrap(); shape - .add_paragraph(raw_text_data.paragraph) + .add_paragraph(raw_text_data.into()) .expect("Failed to add paragraph"); }); mem::free_bytes();