Calculate position data in wasm

This commit is contained in:
alonso.torres
2025-12-11 13:29:43 +01:00
committed by Belén Albeza
parent b705cf953a
commit ea4d0e1238
11 changed files with 570 additions and 219 deletions

View File

@@ -24,6 +24,8 @@
(def revn-data (atom {})) (def revn-data (atom {}))
(def queue-conj (fnil conj #queue [])) (def queue-conj (fnil conj #queue []))
(def force-persist? #(= % ::force-persist))
(defn- update-status (defn- update-status
[status] [status]
(ptk/reify ::update-status (ptk/reify ::update-status

View File

@@ -32,7 +32,7 @@
[app.main.data.helpers :as dsh] [app.main.data.helpers :as dsh]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.notifications :as ntf] [app.main.data.notifications :as ntf]
[app.main.data.persistence :as-alias dps] [app.main.data.persistence :as dps]
[app.main.data.plugins :as dp] [app.main.data.plugins :as dp]
[app.main.data.profile :as du] [app.main.data.profile :as du]
[app.main.data.project :as dpj] [app.main.data.project :as dpj]
@@ -67,6 +67,7 @@
[app.main.errors] [app.main.errors]
[app.main.features :as features] [app.main.features :as features]
[app.main.features.pointer-map :as fpmap] [app.main.features.pointer-map :as fpmap]
[app.main.refs :as refs]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.router :as rt] [app.main.router :as rt]
[app.render-wasm :as wasm] [app.render-wasm :as wasm]
@@ -379,6 +380,59 @@
(->> (rx/from added) (->> (rx/from added)
(rx/map process-wasm-object))))))) (rx/map process-wasm-object)))))))
(when render-wasm?
(let [local-commits-s
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/filter #(and (= :local (:source %))
(not (contains? (:tags %) :position-data))))
(rx/filter (complement empty?)))
notifier-s
(rx/merge
(->> local-commits-s (rx/debounce 1000))
(->> stream (rx/filter dps/force-persist?)))
objects-s
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
current-page-id-s
(rx/from-atom refs/current-page-id {:emit-current-value? true})]
(->> local-commits-s
(rx/buffer-until notifier-s)
(rx/with-latest-from objects-s)
(rx/map
(fn [[commits objects]]
(->> commits
(mapcat :redo-changes)
(filter #(contains? #{:mod-obj :add-obj} (:type %)))
(filter #(cfh/text-shape? objects (:id %)))
(map #(vector
(:id %)
(wasm.api/calculate-position-data (get objects (:id %))))))))
(rx/with-latest-from current-page-id-s)
(rx/map
(fn [[text-position-data page-id]]
(let [changes
(->> text-position-data
(mapv (fn [[id position-data]]
{:type :mod-obj
:id id
:page-id page-id
:operations
[{:type :set
:attr :position-data
:val position-data
:ignore-touched true
:ignore-geometry true}]})))]
(dch/commit-changes
{:redo-changes changes :undo-changes []
:save-undo? false
:tags #{:position-data}})))))))
(->> stream (->> stream
(rx/filter dch/commit?) (rx/filter dch/commit?)
(rx/map deref) (rx/map deref)

View File

@@ -30,6 +30,9 @@
(def profile (def profile
(l/derived (l/key :profile) st/state)) (l/derived (l/key :profile) st/state))
(def current-page-id
(l/derived (l/key :current-page-id) st/state))
(def team (def team
(l/derived (fn [state] (l/derived (fn [state]
(let [team-id (:current-team-id state) (let [team-id (:current-team-id state)

View File

@@ -16,6 +16,7 @@
[app.common.geom.shapes.points :as gpo] [app.common.geom.shapes.points :as gpo]
[app.common.types.shape.layout :as ctl] [app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.render-wasm.api :as wasm.api]
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
@@ -275,3 +276,26 @@
:y2 (:y end-p) :y2 (:y end-p)
:style {:stroke "red" :style {:stroke "red"
:stroke-width (/ 1 zoom)}}]))])))) :stroke-width (/ 1 zoom)}}]))]))))
(mf/defc debug-text-wasm-position-data
{::mf/wrap-props false}
[props]
(let [zoom (unchecked-get props "zoom")
selected-shapes (unchecked-get props "selected-shapes")
selected-text
(when (and (= (count selected-shapes) 1) (= :text (-> selected-shapes first :type)))
(first selected-shapes))
position-data
(when selected-text
(wasm.api/calculate-position-data selected-text))]
(for [{:keys [x y width height]} position-data]
[:rect {:x x
:y (- y height)
:width width
:height height
:fill "none"
:strokeWidth (/ 1 zoom)
:stroke "red"}])))

View File

@@ -647,6 +647,12 @@
:hover-top-frame-id @hover-top-frame-id :hover-top-frame-id @hover-top-frame-id
:zoom zoom}]) :zoom zoom}])
(when (dbg/enabled? :text-outline)
[:& wvd/debug-text-wasm-position-data
{:selected-shapes selected-shapes
:objects base-objects
:zoom zoom}])
(when show-selection-handlers? (when show-selection-handlers?
[:g.selection-handlers {:clipPath "url(#clip-handlers)"} [:g.selection-handlers {:clipPath "url(#clip-handlers)"}
(when-not text-editing? (when-not text-editing?

View File

@@ -23,6 +23,7 @@
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.render :as render] [app.main.render :as render]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.shapes.text]
[app.main.worker :as mw] [app.main.worker :as mw]
[app.render-wasm.api.fonts :as f] [app.render-wasm.api.fonts :as f]
[app.render-wasm.api.texts :as t] [app.render-wasm.api.texts :as t]
@@ -1002,10 +1003,7 @@
(run! (run!
(fn [id] (fn [id]
(f/update-text-layout id) (f/update-text-layout id)
(mw/emit! {:cmd :index/update-text-rect (update-text-rect! id)))))
:page-id (:current-page-id @st/state)
:shape-id id
:dimensions (get-text-dimensions id)})))))
(defn process-pending (defn process-pending
([shapes thumbnails full on-complete] ([shapes thumbnails full on-complete]
@@ -1361,6 +1359,59 @@
(h/call wasm/internal-module "_end_temp_objects") (h/call wasm/internal-module "_end_temp_objects")
content))) content)))
(def POSITION-DATA-U8-SIZE 36)
(def POSITION-DATA-U32-SIZE (/ POSITION-DATA-U8-SIZE 4))
(defn calculate-position-data
[shape]
(when wasm/context-initialized?
(use-shape (:id shape))
(let [heapf32 (mem/get-heap-f32)
heapu32 (mem/get-heap-u32)
offset (-> (h/call wasm/internal-module "_calculate_position_data")
(mem/->offset-32))
length (aget heapu32 offset)
max-offset (+ offset 1 (* length POSITION-DATA-U32-SIZE))
result
(loop [result (transient [])
offset (inc offset)]
(if (< offset max-offset)
(let [entry (dr/read-position-data-entry heapu32 heapf32 offset)]
(recur (conj! result entry)
(+ offset POSITION-DATA-U32-SIZE)))
(persistent! result)))
result
(->> result
(mapv
(fn [{:keys [paragraph span start-pos end-pos direction x y width height]}]
(let [content (:content shape)
element (-> content :children
(get 0) :children ;; paragraph-set
(get paragraph) :children ;; paragraph
(get span))
text (subs (:text element) start-pos end-pos)]
{:x x
:y (+ y height)
:width width
:height height
:direction (dr/translate-direction direction)
:font-family (get element :font-family)
:font-size (get element :font-size)
:font-weight (get element :font-weight)
:text-transform (get element :text-transform)
:text-decoration (get element :text-decoration)
:letter-spacing (get element :letter-spacing)
:font-style (get element :font-style)
:fills (get element :fills)
:text text}))))]
(mem/free)
result)))
(defn init-wasm-module (defn init-wasm-module
[module] [module]
(let [default-fn (unchecked-get module "default") (let [default-fn (unchecked-get module "default")

View File

@@ -45,4 +45,29 @@
:center (gpt/point cx cy) :center (gpt/point cx cy)
:transform (gmt/matrix a b c d e f)})) :transform (gmt/matrix a b c d e f)}))
(defn read-position-data-entry
[heapu32 heapf32 offset]
(let [paragraph (aget heapu32 (+ offset 0))
span (aget heapu32 (+ offset 1))
start-pos (aget heapu32 (+ offset 2))
end-pos (aget heapu32 (+ offset 3))
x (aget heapf32 (+ offset 4))
y (aget heapf32 (+ offset 5))
width (aget heapf32 (+ offset 6))
height (aget heapf32 (+ offset 7))
direction (aget heapu32 (+ offset 8))]
{:paragraph paragraph
:span span
:start-pos start-pos
:end-pos end-pos
:x x
:y y
:width width
:height height
:direction direction}))
(defn translate-direction
[direction]
(case direction
0 "rtl"
"ltr"))

View File

@@ -9,7 +9,8 @@ mod options;
mod shadows; mod shadows;
mod strokes; mod strokes;
mod surfaces; mod surfaces;
mod text; pub mod text;
mod ui; mod ui;
use skia_safe::{self as skia, Matrix, RRect, Rect}; use skia_safe::{self as skia, Matrix, RRect, Rect};

View File

@@ -2,18 +2,15 @@ use super::{filters, RenderState, Shape, SurfaceId};
use crate::{ use crate::{
math::Rect, math::Rect,
shapes::{ shapes::{
merge_fills, set_paint_fill, ParagraphBuilderGroup, Stroke, StrokeKind, TextContent, calculate_position_data, calculate_text_layout_data, merge_fills, set_paint_fill,
VerticalAlign, ParagraphBuilderGroup, Stroke, StrokeKind, TextContent,
}, },
utils::{get_fallback_fonts, get_font_collection}, utils::{get_fallback_fonts, get_font_collection},
}; };
use skia_safe::{ use skia_safe::{
self as skia, self as skia,
canvas::SaveLayerRec, canvas::SaveLayerRec,
textlayout::{ textlayout::{ParagraphBuilder, StyleMetrics, TextDecoration, TextStyle},
LineMetrics, Paragraph, ParagraphBuilder, RectHeightStyle, RectWidthStyle, StyleMetrics,
TextDecoration, TextStyle,
},
Canvas, ImageFilter, Paint, Path, Canvas, ImageFilter, Paint, Path,
}; };
@@ -241,48 +238,24 @@ fn draw_text(
paragraph_builder_groups: &mut [Vec<ParagraphBuilder>], paragraph_builder_groups: &mut [Vec<ParagraphBuilder>],
) { ) {
let text_content = shape.get_text_content(); let text_content = shape.get_text_content();
let selrect_width = shape.selrect().width(); let layout_info =
let text_width = text_content.get_width(selrect_width); calculate_text_layout_data(shape, text_content, paragraph_builder_groups, true);
let text_height = text_content.get_height(selrect_width);
let selrect_height = shape.selrect().height();
let mut global_offset_y = match shape.vertical_align() {
VerticalAlign::Center => (selrect_height - text_height) / 2.0,
VerticalAlign::Bottom => selrect_height - text_height,
_ => 0.0,
};
let layer_rec = SaveLayerRec::default(); let layer_rec = SaveLayerRec::default();
canvas.save_layer(&layer_rec); canvas.save_layer(&layer_rec);
let mut previous_line_height = text_content.normalized_line_height();
for paragraph_builder_group in paragraph_builder_groups { for para in &layout_info.paragraphs {
let group_offset_y = global_offset_y; para.paragraph.paint(canvas, (para.x, para.y));
let group_len = paragraph_builder_group.len(); for deco in &para.decorations {
let mut paragraph_offset_y = previous_line_height; draw_text_decorations(
canvas,
for (paragraph_index, paragraph_builder) in paragraph_builder_group.iter_mut().enumerate() { &deco.text_style,
let mut paragraph = paragraph_builder.build(); Some(deco.y),
paragraph.layout(text_width); deco.thickness,
let xy = (shape.selrect().x(), shape.selrect().y() + group_offset_y); deco.left,
paragraph.paint(canvas, xy); deco.width,
);
let line_metrics = paragraph.get_line_metrics();
if paragraph_index == group_len - 1 {
if line_metrics.is_empty() {
paragraph_offset_y = paragraph.ideographic_baseline();
} else {
paragraph_offset_y = paragraph.height();
previous_line_height = paragraph.ideographic_baseline();
}
}
for line_metrics in paragraph.get_line_metrics().iter() {
render_text_decoration(canvas, &paragraph, paragraph_builder, line_metrics, xy);
}
} }
global_offset_y += paragraph_offset_y;
} }
} }
@@ -307,7 +280,7 @@ fn draw_text_decorations(
} }
} }
fn calculate_decoration_metrics( pub fn calculate_decoration_metrics(
style_metrics: &Vec<(usize, &StyleMetrics)>, style_metrics: &Vec<(usize, &StyleMetrics)>,
line_baseline: f32, line_baseline: f32,
) -> (f32, Option<f32>, f32, Option<f32>) { ) -> (f32, Option<f32>, f32, Option<f32>) {
@@ -357,106 +330,6 @@ fn calculate_decoration_metrics(
) )
} }
fn render_text_decoration(
canvas: &Canvas,
skia_paragraph: &Paragraph,
builder: &mut ParagraphBuilder,
line_metrics: &LineMetrics,
xy: (f32, f32),
) {
let style_metrics: Vec<_> = line_metrics
.get_style_metrics(line_metrics.start_index..line_metrics.end_index)
.into_iter()
.collect();
let mut current_x_offset = 0.0;
let total_chars = line_metrics.end_index - line_metrics.start_index;
let line_start_offset = line_metrics.left as f32;
if total_chars == 0 || style_metrics.is_empty() {
return;
}
let line_baseline = xy.1 + line_metrics.baseline as f32;
let full_text = builder.get_text();
// Calculate decoration metrics
let (max_underline_thickness, underline_y, max_strike_thickness, strike_y) =
calculate_decoration_metrics(&style_metrics, line_baseline);
// Draw decorations per segment (text span)
for (i, (style_start, style_metric)) in style_metrics.iter().enumerate() {
let text_style = &style_metric.text_style;
let style_end = style_metrics
.get(i + 1)
.map(|(next_i, _)| *next_i)
.unwrap_or(line_metrics.end_index);
let seg_start = (*style_start).max(line_metrics.start_index);
let seg_end = style_end.min(line_metrics.end_index);
if seg_start >= seg_end {
continue;
}
let start_byte = full_text
.char_indices()
.nth(seg_start)
.map(|(i, _)| i)
.unwrap_or(0);
let end_byte = full_text
.char_indices()
.nth(seg_end)
.map(|(i, _)| i)
.unwrap_or(full_text.len());
let segment_text = &full_text[start_byte..end_byte];
let rects = skia_paragraph.get_rects_for_range(
seg_start..seg_end,
RectHeightStyle::Tight,
RectWidthStyle::Tight,
);
let (segment_width, actual_x_offset) = if !rects.is_empty() {
let total_width: f32 = rects.iter().map(|r| r.rect.width()).sum();
let skia_x_offset = rects
.first()
.map(|r| r.rect.left - line_start_offset)
.unwrap_or(0.0);
(total_width, skia_x_offset)
} else {
let font = skia_paragraph.get_font_at(seg_start);
let measured_width = font.measure_text(segment_text, None).0;
(measured_width, current_x_offset)
};
let text_left = xy.0 + line_start_offset + actual_x_offset;
let text_width = segment_width;
// Underline
if text_style.decoration().ty == TextDecoration::UNDERLINE {
draw_text_decorations(
canvas,
text_style,
underline_y,
max_underline_thickness,
text_left,
text_width,
);
}
// Strikethrough
if text_style.decoration().ty == TextDecoration::LINE_THROUGH {
draw_text_decorations(
canvas,
text_style,
strike_y,
max_strike_thickness,
text_left,
text_width,
);
}
current_x_offset += segment_width;
}
}
#[allow(dead_code)] #[allow(dead_code)]
fn calculate_total_paragraphs_height(paragraphs: &mut [ParagraphBuilder], width: f32) -> f32 { fn calculate_total_paragraphs_height(paragraphs: &mut [ParagraphBuilder], width: f32) -> f32 {
paragraphs paragraphs
@@ -506,6 +379,29 @@ pub fn render_as_path(
} }
} }
#[allow(dead_code)]
pub fn render_position_data(
render_state: &mut RenderState,
surface_id: SurfaceId,
shape: &Shape,
text_content: &TextContent,
) {
let position_data = calculate_position_data(shape, text_content, false);
let mut paint = skia::Paint::default();
paint.set_style(skia::PaintStyle::Stroke);
paint.set_color(skia::Color::from_argb(255, 255, 0, 0));
paint.set_stroke_width(2.);
for pd in position_data {
let rect = Rect::from_xywh(pd.x, pd.y, pd.width, pd.height);
render_state
.surfaces
.canvas(surface_id)
.draw_rect(rect, &paint);
}
}
// How to use it? // How to use it?
// Type::Text(text_content) => { // Type::Text(text_content) => {
// self.surfaces // self.surfaces

View File

@@ -1,3 +1,4 @@
use crate::render::text::calculate_decoration_metrics;
use crate::{ use crate::{
math::{Bounds, Matrix, Rect}, math::{Bounds, Matrix, Rect},
render::{default_font, DEFAULT_EMOJI_FONT}, render::{default_font, DEFAULT_EMOJI_FONT},
@@ -185,6 +186,17 @@ impl TextContentLayout {
} }
} }
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct TextDecorationSegment {
pub kind: skia::textlayout::TextDecoration,
pub text_style: skia::textlayout::TextStyle,
pub y: f32,
pub thickness: f32,
pub left: f32,
pub width: f32,
}
/* /*
* Check if the current x,y (in paragraph relative coordinates) is inside * Check if the current x,y (in paragraph relative coordinates) is inside
* the paragraph * the paragraph
@@ -204,6 +216,48 @@ fn intersects(paragraph: &skia_safe::textlayout::Paragraph, x: f32, y: f32) -> b
rects.iter().any(|r| r.rect.contains(&Point::new(x, y))) rects.iter().any(|r| r.rect.contains(&Point::new(x, y)))
} }
// Performs a text auto layout without width limits.
// This should be the same as text_auto_layout.
pub fn build_paragraphs_from_paragraph_builders(
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
}
/// Calculate the normalized line height from paragraph builders
pub fn calculate_normalized_line_height(
paragraph_builders: &mut [ParagraphBuilderGroup],
width: f32,
) -> f32 {
let mut normalized_line_height = 0.0;
for paragraph_builder_group in paragraph_builders.iter_mut() {
for paragraph_builder in paragraph_builder_group.iter_mut() {
let mut paragraph = paragraph_builder.build();
paragraph.layout(width);
let baseline = paragraph.ideographic_baseline();
if baseline > normalized_line_height {
normalized_line_height = baseline;
}
}
}
normalized_line_height
}
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub struct TextContent { pub struct TextContent {
pub paragraphs: Vec<Paragraph>, pub paragraphs: Vec<Paragraph>,
@@ -440,59 +494,15 @@ impl TextContent {
paragraph_group 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
}
/// Calculate the normalized line height from paragraph builders
fn calculate_normalized_line_height(
&self,
paragraph_builders: &mut [ParagraphBuilderGroup],
width: f32,
) -> f32 {
let mut normalized_line_height = 0.0;
for paragraph_builder_group in paragraph_builders.iter_mut() {
for paragraph_builder in paragraph_builder_group.iter_mut() {
let mut paragraph = paragraph_builder.build();
paragraph.layout(width);
let baseline = paragraph.ideographic_baseline();
if baseline > normalized_line_height {
normalized_line_height = baseline;
}
}
}
normalized_line_height
}
/// Performs an Auto Width text layout. /// Performs an Auto Width text layout.
fn text_layout_auto_width(&self) -> TextContentLayoutResult { fn text_layout_auto_width(&self) -> TextContentLayoutResult {
let mut paragraph_builders = self.paragraph_builder_group_from_text(None); let mut paragraph_builders = self.paragraph_builder_group_from_text(None);
let normalized_line_height = let normalized_line_height =
self.calculate_normalized_line_height(&mut paragraph_builders, f32::MAX); calculate_normalized_line_height(&mut paragraph_builders, f32::MAX);
let paragraphs = let paragraphs =
self.build_paragraphs_from_paragraph_builders(&mut paragraph_builders, f32::MAX); build_paragraphs_from_paragraph_builders(&mut paragraph_builders, f32::MAX);
let (width, height) = let (width, height) =
paragraphs paragraphs
@@ -521,10 +531,9 @@ impl TextContent {
let mut paragraph_builders = self.paragraph_builder_group_from_text(None); let mut paragraph_builders = self.paragraph_builder_group_from_text(None);
let normalized_line_height = let normalized_line_height =
self.calculate_normalized_line_height(&mut paragraph_builders, width); calculate_normalized_line_height(&mut paragraph_builders, width);
let paragraphs = let paragraphs = build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width);
self.build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width);
let height = paragraphs let height = paragraphs
.iter() .iter()
.flatten() .flatten()
@@ -546,10 +555,9 @@ impl TextContent {
let mut paragraph_builders = self.paragraph_builder_group_from_text(None); let mut paragraph_builders = self.paragraph_builder_group_from_text(None);
let normalized_line_height = let normalized_line_height =
self.calculate_normalized_line_height(&mut paragraph_builders, width); calculate_normalized_line_height(&mut paragraph_builders, width);
let paragraphs = let paragraphs = build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width);
self.build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width);
let paragraph_height = paragraphs let paragraph_height = paragraphs
.iter() .iter()
.flatten() .flatten()
@@ -576,8 +584,7 @@ impl TextContent {
pub fn get_height(&self, width: f32) -> f32 { pub fn get_height(&self, width: f32) -> f32 {
let mut paragraph_builders = self.paragraph_builder_group_from_text(None); let mut paragraph_builders = self.paragraph_builder_group_from_text(None);
let paragraphs = let paragraphs = build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width);
self.build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width);
let paragraph_height = paragraphs let paragraph_height = paragraphs
.iter() .iter()
.flatten() .flatten()
@@ -733,8 +740,7 @@ impl TextContent {
let width = self.width(); let width = self.width();
let mut paragraph_builders = self.paragraph_builder_group_from_text(None); let mut paragraph_builders = self.paragraph_builder_group_from_text(None);
let paragraphs = let paragraphs = build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width);
self.build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width);
paragraphs paragraphs
.iter() .iter()
@@ -863,17 +869,17 @@ impl Paragraph {
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub struct TextSpan { pub struct TextSpan {
text: String, pub text: String,
font_family: FontFamily, pub font_family: FontFamily,
font_size: f32, pub font_size: f32,
line_height: f32, pub line_height: f32,
letter_spacing: f32, pub letter_spacing: f32,
font_weight: i32, pub font_weight: i32,
font_variant_id: Uuid, pub font_variant_id: Uuid,
text_decoration: Option<TextDecoration>, pub text_decoration: Option<TextDecoration>,
text_transform: Option<TextTransform>, pub text_transform: Option<TextTransform>,
text_direction: TextDirection, pub text_direction: TextDirection,
fills: Vec<shapes::Fill>, pub fills: Vec<shapes::Fill>,
} }
impl TextSpan { impl TextSpan {
@@ -1045,3 +1051,251 @@ impl TextSpan {
}) })
} }
} }
#[allow(dead_code)]
#[derive(Debug)]
pub struct PositionData {
pub paragraph: u32,
pub span: u32,
pub start_pos: u32,
pub end_pos: u32,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub direction: u32,
}
#[allow(dead_code)]
#[derive(Debug)]
pub struct ParagraphLayout {
pub paragraph: skia::textlayout::Paragraph,
pub x: f32,
pub y: f32,
pub spans: Vec<crate::shapes::TextSpan>,
pub decorations: Vec<TextDecorationSegment>,
}
#[allow(dead_code)]
#[derive(Debug)]
pub struct TextLayoutData {
pub position_data: Vec<PositionData>,
pub content_rect: Rect,
pub paragraphs: Vec<ParagraphLayout>,
}
fn direction_to_int(direction: TextDirection) -> u32 {
match direction {
TextDirection::RTL => 0,
TextDirection::LTR => 1,
}
}
pub fn calculate_text_layout_data(
shape: &Shape,
text_content: &TextContent,
paragraph_builder_groups: &mut [ParagraphBuilderGroup],
skip_position_data: bool,
) -> TextLayoutData {
let selrect_width = shape.selrect().width();
let text_width = text_content.get_width(selrect_width);
let selrect_height = shape.selrect().height();
let x = shape.selrect.x();
let base_y = shape.selrect.y();
let mut position_data: Vec<PositionData> = Vec::new();
let mut previous_line_height = text_content.normalized_line_height();
let text_paragraphs = text_content.paragraphs();
// 1. Calculate paragraph heights
let mut paragraph_heights: Vec<f32> = Vec::new();
for paragraph_builder_group in paragraph_builder_groups.iter_mut() {
let group_len = paragraph_builder_group.len();
let mut paragraph_offset_y = previous_line_height;
for (builder_index, paragraph_builder) in paragraph_builder_group.iter_mut().enumerate() {
let mut skia_paragraph = paragraph_builder.build();
skia_paragraph.layout(text_width);
if builder_index == group_len - 1 {
if skia_paragraph.get_line_metrics().is_empty() {
paragraph_offset_y = skia_paragraph.ideographic_baseline();
} else {
paragraph_offset_y = skia_paragraph.height();
}
}
if builder_index == 0 {
paragraph_heights.push(skia_paragraph.height());
}
}
previous_line_height = paragraph_offset_y;
}
// 2. Calculate vertical offset and build paragraphs with positions
let total_text_height: f32 = paragraph_heights.iter().sum();
let vertical_offset = match shape.vertical_align() {
VerticalAlign::Center => (selrect_height - total_text_height) / 2.0,
VerticalAlign::Bottom => selrect_height - total_text_height,
_ => 0.0,
};
let mut paragraph_layouts: Vec<ParagraphLayout> = Vec::new();
let mut y_accum = base_y + vertical_offset;
for (i, paragraph_builder_group) in paragraph_builder_groups.iter_mut().enumerate() {
// For each paragraph in the group (e.g., fill, stroke, etc.)
for paragraph_builder in paragraph_builder_group.iter_mut() {
let mut skia_paragraph = paragraph_builder.build();
skia_paragraph.layout(text_width);
let spans = if let Some(text_para) = text_paragraphs.get(i) {
text_para.children().to_vec()
} else {
Vec::new()
};
// Calculate text decorations for this paragraph
let mut decorations = Vec::new();
let line_metrics = skia_paragraph.get_line_metrics();
for line in &line_metrics {
let style_metrics: Vec<_> = line
.get_style_metrics(line.start_index..line.end_index)
.into_iter()
.collect();
let line_baseline = y_accum + line.baseline as f32;
let (max_underline_thickness, underline_y, max_strike_thickness, strike_y) =
calculate_decoration_metrics(&style_metrics, line_baseline);
for (i, (style_start, style_metric)) in style_metrics.iter().enumerate() {
let text_style = &style_metric.text_style;
let style_end = style_metrics
.get(i + 1)
.map(|(next_i, _)| *next_i)
.unwrap_or(line.end_index);
let seg_start = (*style_start).max(line.start_index);
let seg_end = style_end.min(line.end_index);
if seg_start >= seg_end {
continue;
}
let rects = skia_paragraph.get_rects_for_range(
seg_start..seg_end,
skia::textlayout::RectHeightStyle::Tight,
skia::textlayout::RectWidthStyle::Tight,
);
let (segment_width, actual_x_offset) = if !rects.is_empty() {
let total_width: f32 = rects.iter().map(|r| r.rect.width()).sum();
let skia_x_offset = rects
.first()
.map(|r| r.rect.left - line.left as f32)
.unwrap_or(0.0);
(total_width, skia_x_offset)
} else {
(0.0, 0.0)
};
let text_left = x + line.left as f32 + actual_x_offset;
let text_width = segment_width;
use skia::textlayout::TextDecoration;
if text_style.decoration().ty == TextDecoration::UNDERLINE {
decorations.push(TextDecorationSegment {
kind: TextDecoration::UNDERLINE,
text_style: (*text_style).clone(),
y: underline_y.unwrap_or(line_baseline),
thickness: max_underline_thickness,
left: text_left,
width: text_width,
});
}
if text_style.decoration().ty == TextDecoration::LINE_THROUGH {
decorations.push(TextDecorationSegment {
kind: TextDecoration::LINE_THROUGH,
text_style: (*text_style).clone(),
y: strike_y.unwrap_or(line_baseline),
thickness: max_strike_thickness,
left: text_left,
width: text_width,
});
}
}
}
paragraph_layouts.push(ParagraphLayout {
paragraph: skia_paragraph,
x,
y: y_accum,
spans: spans.clone(),
decorations,
});
}
y_accum += paragraph_heights[i];
}
// Calculate position data from paragraph_layouts
if !skip_position_data {
for (paragraph_index, para_layout) in paragraph_layouts.iter().enumerate() {
let current_y = para_layout.y;
let text_paragraph = text_paragraphs.get(paragraph_index);
if let Some(text_para) = text_paragraph {
let mut span_ranges: Vec<(usize, usize, usize)> = vec![];
let mut cur = 0;
for (span_index, span) in text_para.children().iter().enumerate() {
let text: String = span.apply_text_transform();
span_ranges.push((cur, cur + text.len(), span_index));
cur += text.len();
}
for (start, end, span_index) in span_ranges {
let rects = para_layout.paragraph.get_rects_for_range(
start..end,
RectHeightStyle::Tight,
RectWidthStyle::Tight,
);
for textbox in rects {
let direction = textbox.direct;
let mut rect = textbox.rect;
let cy = rect.top + rect.height() / 2.0;
let start_pos = para_layout
.paragraph
.get_glyph_position_at_coordinate((rect.left + 0.1, cy))
.position as usize;
let end_pos = para_layout
.paragraph
.get_glyph_position_at_coordinate((rect.right - 0.1, cy))
.position as usize;
let start_pos = start_pos.saturating_sub(start);
let end_pos = end_pos.saturating_sub(start);
rect.offset((x, current_y));
position_data.push(PositionData {
paragraph: paragraph_index as u32,
span: span_index as u32,
start_pos: start_pos as u32,
end_pos: end_pos as u32,
x: rect.x(),
y: rect.y(),
width: rect.width(),
height: rect.height(),
direction: direction_to_int(direction),
});
}
}
}
}
}
let content_rect = Rect::from_xywh(x, base_y + vertical_offset, text_width, total_text_height);
TextLayoutData {
position_data,
content_rect,
paragraphs: paragraph_layouts,
}
}
pub fn calculate_position_data(
shape: &Shape,
text_content: &TextContent,
skip_position_data: bool,
) -> Vec<PositionData> {
let mut text_content = text_content.clone();
text_content.update_layout(shape.selrect);
let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None);
let layout_info = calculate_text_layout_data(
shape,
&text_content,
&mut paragraph_builders,
skip_position_data,
);
layout_info.position_data
}

View File

@@ -2,13 +2,14 @@ use macros::ToJs;
use super::{fills::RawFillData, fonts::RawFontStyle}; use super::{fills::RawFillData, fonts::RawFontStyle};
use crate::math::{Matrix, Point}; use crate::math::{Matrix, Point};
use crate::mem; use crate::mem::{self, SerializableResult};
use crate::shapes::{ use crate::shapes::{
self, GrowType, Shape, TextAlign, TextDecoration, TextDirection, TextTransform, Type, self, GrowType, Shape, TextAlign, TextDecoration, TextDirection, TextTransform, Type,
}; };
use crate::utils::{uuid_from_u32, uuid_from_u32_quartet}; use crate::utils::{uuid_from_u32, uuid_from_u32_quartet};
use crate::{ use crate::{
with_current_shape_mut, with_state, with_state_mut, with_state_mut_current_shape, STATE, with_current_shape, with_current_shape_mut, with_state, with_state_mut,
with_state_mut_current_shape, STATE,
}; };
const RAW_SPAN_DATA_SIZE: usize = std::mem::size_of::<RawTextSpan>(); const RAW_SPAN_DATA_SIZE: usize = std::mem::size_of::<RawTextSpan>();
@@ -411,3 +412,37 @@ pub extern "C" fn get_caret_position_at(x: f32, y: f32) -> i32 {
}); });
-1 -1
} }
const RAW_POSITION_DATA_SIZE: usize = size_of::<shapes::PositionData>();
impl SerializableResult for shapes::PositionData {
type BytesType = [u8; RAW_POSITION_DATA_SIZE];
fn from_bytes(bytes: Self::BytesType) -> Self {
unsafe { std::mem::transmute(bytes) }
}
fn as_bytes(&self) -> Self::BytesType {
let ptr = self as *const shapes::PositionData as *const u8;
let bytes: &[u8] = unsafe { std::slice::from_raw_parts(ptr, RAW_POSITION_DATA_SIZE) };
let mut result = [0; RAW_POSITION_DATA_SIZE];
result.copy_from_slice(bytes);
result
}
// The generic trait doesn't know the size of the array. This is why the
// clone needs to be here even if it could be generic.
fn clone_to_slice(&self, slice: &mut [u8]) {
slice.clone_from_slice(&self.as_bytes());
}
}
#[no_mangle]
pub extern "C" fn calculate_position_data() -> *mut u8 {
let mut result = Vec::<shapes::PositionData>::default();
with_current_shape!(state, |shape: &Shape| {
if let Type::Text(text_content) = &shape.shape_type {
result = shapes::calculate_position_data(shape, text_content, false);
}
});
mem::write_vec(result)
}