mirror of
https://github.com/penpot/penpot.git
synced 2025-12-23 22:48:40 -05:00
Merge remote-tracking branch 'origin/staging-render' into develop
This commit is contained in:
@@ -230,20 +230,62 @@ pub extern "C" fn resize_viewbox(width: i32, height: i32) {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_view(zoom: f32, x: f32, y: f32) {
|
||||
with_state_mut!(state, {
|
||||
performance::begin_measure!("set_view");
|
||||
let render_state = state.render_state_mut();
|
||||
render_state.set_view(zoom, x, y);
|
||||
performance::end_measure!("set_view");
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "profile-macros")]
|
||||
static mut VIEW_INTERACTION_START: i32 = 0;
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_view_start() {
|
||||
with_state_mut!(state, {
|
||||
#[cfg(feature = "profile-macros")]
|
||||
unsafe {
|
||||
VIEW_INTERACTION_START = performance::get_time();
|
||||
}
|
||||
performance::begin_measure!("set_view_start");
|
||||
state.render_state.options.set_fast_mode(true);
|
||||
performance::end_measure!("set_view_start");
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_view_end() {
|
||||
with_state_mut!(state, {
|
||||
// We can have renders in progress
|
||||
let _end_start = performance::begin_timed_log!("set_view_end");
|
||||
performance::begin_measure!("set_view_end");
|
||||
state.render_state.options.set_fast_mode(false);
|
||||
state.render_state.cancel_animation_frame();
|
||||
if state.render_state.options.is_profile_rebuild_tiles() {
|
||||
state.rebuild_tiles();
|
||||
} else {
|
||||
state.rebuild_tiles_shallow();
|
||||
|
||||
let zoom_changed = state.render_state.zoom_changed();
|
||||
// Only rebuild tile indices when zoom has changed.
|
||||
// During pan-only operations, shapes stay in the same tiles
|
||||
// because tile_size = 1/scale * TILE_SIZE (depends only on zoom).
|
||||
if zoom_changed {
|
||||
let _rebuild_start = performance::begin_timed_log!("rebuild_tiles");
|
||||
performance::begin_measure!("set_view_end::rebuild_tiles");
|
||||
if state.render_state.options.is_profile_rebuild_tiles() {
|
||||
state.rebuild_tiles();
|
||||
} else {
|
||||
state.rebuild_tiles_shallow();
|
||||
}
|
||||
performance::end_measure!("set_view_end::rebuild_tiles");
|
||||
performance::end_timed_log!("rebuild_tiles", _rebuild_start);
|
||||
}
|
||||
performance::end_measure!("set_view_end");
|
||||
performance::end_timed_log!("set_view_end", _end_start);
|
||||
#[cfg(feature = "profile-macros")]
|
||||
{
|
||||
let total_time = performance::get_time() - unsafe { VIEW_INTERACTION_START };
|
||||
performance::console_log!(
|
||||
"[PERF] view_interaction (zoom_changed={}): {}ms",
|
||||
zoom_changed,
|
||||
total_time
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -261,7 +303,7 @@ pub extern "C" fn set_focus_mode() {
|
||||
|
||||
let entries: Vec<Uuid> = bytes
|
||||
.chunks(size_of::<<Uuid as SerializableResult>::BytesType>())
|
||||
.map(|data| Uuid::from_bytes(data.try_into().unwrap()))
|
||||
.map(|data| Uuid::try_from(data).unwrap())
|
||||
.collect();
|
||||
|
||||
with_state_mut!(state, {
|
||||
@@ -481,7 +523,7 @@ pub extern "C" fn set_children() {
|
||||
|
||||
let entries: Vec<Uuid> = bytes
|
||||
.chunks(size_of::<<Uuid as SerializableResult>::BytesType>())
|
||||
.map(|data| Uuid::from_bytes(data.try_into().unwrap()))
|
||||
.map(|data| Uuid::try_from(data).unwrap())
|
||||
.collect();
|
||||
|
||||
set_children_set(entries);
|
||||
@@ -637,7 +679,7 @@ pub extern "C" fn propagate_modifiers(pixel_precision: bool) -> *mut u8 {
|
||||
|
||||
let entries: Vec<_> = bytes
|
||||
.chunks(size_of::<<TransformEntry as SerializableResult>::BytesType>())
|
||||
.map(|data| TransformEntry::from_bytes(data.try_into().unwrap()))
|
||||
.map(|data| TransformEntry::try_from(data).unwrap())
|
||||
.collect();
|
||||
|
||||
with_state!(state, {
|
||||
@@ -652,7 +694,7 @@ pub extern "C" fn set_modifiers() {
|
||||
|
||||
let entries: Vec<_> = bytes
|
||||
.chunks(size_of::<<TransformEntry as SerializableResult>::BytesType>())
|
||||
.map(|data| TransformEntry::from_bytes(data.try_into().unwrap()))
|
||||
.map(|data| TransformEntry::try_from(data).unwrap())
|
||||
.collect();
|
||||
|
||||
let mut modifiers = HashMap::new();
|
||||
|
||||
@@ -57,10 +57,8 @@ pub fn bytes_or_empty() -> Vec<u8> {
|
||||
guard.take().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub trait SerializableResult {
|
||||
pub trait SerializableResult: From<Self::BytesType> + Into<Self::BytesType> {
|
||||
type BytesType;
|
||||
fn from_bytes(bytes: Self::BytesType) -> Self;
|
||||
fn as_bytes(&self) -> Self::BytesType;
|
||||
fn clone_to_slice(&self, slice: &mut [u8]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub const DEBUG_VISIBLE: u32 = 0x01;
|
||||
pub const PROFILE_REBUILD_TILES: u32 = 0x02;
|
||||
pub const FAST_MODE: u32 = 0x04;
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
#[allow(unused_imports)]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use crate::get_now;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn get_time() -> i32 {
|
||||
@@ -15,6 +11,68 @@ pub fn get_time() -> i32 {
|
||||
now.elapsed().as_millis() as i32
|
||||
}
|
||||
|
||||
/// Log a message to the browser console (only when profile-macros feature is enabled)
|
||||
#[macro_export]
|
||||
macro_rules! console_log {
|
||||
($($arg:tt)*) => {
|
||||
#[cfg(all(feature = "profile-macros", target_arch = "wasm32"))]
|
||||
{
|
||||
use $crate::run_script;
|
||||
run_script!(format!("console.log('{}')", format!($($arg)*)));
|
||||
}
|
||||
#[cfg(all(feature = "profile-macros", not(target_arch = "wasm32")))]
|
||||
{
|
||||
println!($($arg)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Begin a timed section with logging (only when profile-macros feature is enabled)
|
||||
/// Returns the start time - store it and pass to end_timed_log!
|
||||
#[macro_export]
|
||||
macro_rules! begin_timed_log {
|
||||
($name:expr) => {{
|
||||
#[cfg(feature = "profile-macros")]
|
||||
{
|
||||
$crate::performance::get_time()
|
||||
}
|
||||
#[cfg(not(feature = "profile-macros"))]
|
||||
{
|
||||
0.0
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
/// End a timed section and log the duration (only when profile-macros feature is enabled)
|
||||
#[macro_export]
|
||||
macro_rules! end_timed_log {
|
||||
($name:expr, $start:expr) => {{
|
||||
#[cfg(all(feature = "profile-macros", target_arch = "wasm32"))]
|
||||
{
|
||||
let duration = $crate::performance::get_time() - $start;
|
||||
use $crate::run_script;
|
||||
run_script!(format!(
|
||||
"console.log('[PERF] {}: {:.2}ms')",
|
||||
$name, duration
|
||||
));
|
||||
}
|
||||
#[cfg(all(feature = "profile-macros", not(target_arch = "wasm32")))]
|
||||
{
|
||||
let duration = $crate::performance::get_time() - $start;
|
||||
println!("[PERF] {}: {:.2}ms", $name, duration);
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use console_log;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use begin_timed_log;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use end_timed_log;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! mark {
|
||||
($name:expr) => {
|
||||
|
||||
@@ -9,7 +9,8 @@ mod options;
|
||||
mod shadows;
|
||||
mod strokes;
|
||||
mod surfaces;
|
||||
mod text;
|
||||
pub mod text;
|
||||
|
||||
mod ui;
|
||||
|
||||
use skia_safe::{self as skia, Matrix, RRect, Rect};
|
||||
@@ -928,6 +929,8 @@ impl RenderState {
|
||||
}
|
||||
|
||||
pub fn render_from_cache(&mut self, shapes: ShapesPoolRef) {
|
||||
let _start = performance::begin_timed_log!("render_from_cache");
|
||||
performance::begin_measure!("render_from_cache");
|
||||
let scale = self.get_cached_scale();
|
||||
if let Some(snapshot) = &self.cached_target_snapshot {
|
||||
let canvas = self.surfaces.canvas(SurfaceId::Target);
|
||||
@@ -965,6 +968,8 @@ impl RenderState {
|
||||
|
||||
self.flush_and_submit();
|
||||
}
|
||||
performance::end_measure!("render_from_cache");
|
||||
performance::end_timed_log!("render_from_cache", _start);
|
||||
}
|
||||
|
||||
pub fn start_render_loop(
|
||||
@@ -974,6 +979,7 @@ impl RenderState {
|
||||
timestamp: i32,
|
||||
sync_render: bool,
|
||||
) -> Result<(), String> {
|
||||
let _start = performance::begin_timed_log!("start_render_loop");
|
||||
let scale = self.get_scale();
|
||||
self.tile_viewbox.update(self.viewbox, scale);
|
||||
|
||||
@@ -1004,10 +1010,12 @@ impl RenderState {
|
||||
// FIXME - review debug
|
||||
// debug::render_debug_tiles_for_viewbox(self);
|
||||
|
||||
let _tile_start = performance::begin_timed_log!("tile_cache_update");
|
||||
performance::begin_measure!("tile_cache");
|
||||
self.pending_tiles
|
||||
.update(&self.tile_viewbox, &self.surfaces);
|
||||
performance::end_measure!("tile_cache");
|
||||
performance::end_timed_log!("tile_cache_update", _tile_start);
|
||||
|
||||
self.pending_nodes.clear();
|
||||
if self.pending_nodes.capacity() < tree.len() {
|
||||
@@ -1031,6 +1039,7 @@ impl RenderState {
|
||||
}
|
||||
|
||||
performance::end_measure!("start_render_loop");
|
||||
performance::end_timed_log!("start_render_loop", _start);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1479,8 +1488,11 @@ impl RenderState {
|
||||
.surfaces
|
||||
.get_render_context_translation(self.render_area, scale);
|
||||
|
||||
// Skip expensive drop shadow rendering in fast mode (during pan/zoom)
|
||||
let skip_shadows = self.options.is_fast_mode();
|
||||
|
||||
// For text shapes, render drop shadow using text rendering logic
|
||||
if !matches!(element.shape_type, Type::Text(_)) {
|
||||
if !skip_shadows && !matches!(element.shape_type, Type::Text(_)) {
|
||||
// Shadow rendering technique: Two-pass approach for proper opacity handling
|
||||
//
|
||||
// The shadow rendering uses a two-pass technique to ensure that overlapping
|
||||
@@ -2054,6 +2066,10 @@ impl RenderState {
|
||||
self.cached_viewbox.zoom() * self.options.dpr()
|
||||
}
|
||||
|
||||
pub fn zoom_changed(&self) -> bool {
|
||||
(self.viewbox.zoom - self.cached_viewbox.zoom).abs() > f32::EPSILON
|
||||
}
|
||||
|
||||
pub fn mark_touched(&mut self, uuid: Uuid) {
|
||||
self.touched_ids.insert(uuid);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,19 @@ impl RenderOptions {
|
||||
self.flags & options::PROFILE_REBUILD_TILES == options::PROFILE_REBUILD_TILES
|
||||
}
|
||||
|
||||
/// Use fast mode to enable / disable expensive operations
|
||||
pub fn is_fast_mode(&self) -> bool {
|
||||
self.flags & options::FAST_MODE == options::FAST_MODE
|
||||
}
|
||||
|
||||
pub fn set_fast_mode(&mut self, enabled: bool) {
|
||||
if enabled {
|
||||
self.flags |= options::FAST_MODE;
|
||||
} else {
|
||||
self.flags &= !options::FAST_MODE;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dpr(&self) -> f32 {
|
||||
self.dpr.unwrap_or(1.0)
|
||||
}
|
||||
|
||||
@@ -2,18 +2,15 @@ use super::{filters, RenderState, Shape, SurfaceId};
|
||||
use crate::{
|
||||
math::Rect,
|
||||
shapes::{
|
||||
merge_fills, set_paint_fill, ParagraphBuilderGroup, Stroke, StrokeKind, TextContent,
|
||||
VerticalAlign,
|
||||
calculate_position_data, calculate_text_layout_data, merge_fills, set_paint_fill,
|
||||
ParagraphBuilderGroup, Stroke, StrokeKind, TextContent,
|
||||
},
|
||||
utils::{get_fallback_fonts, get_font_collection},
|
||||
};
|
||||
use skia_safe::{
|
||||
self as skia,
|
||||
canvas::SaveLayerRec,
|
||||
textlayout::{
|
||||
LineMetrics, Paragraph, ParagraphBuilder, RectHeightStyle, RectWidthStyle, StyleMetrics,
|
||||
TextDecoration, TextStyle,
|
||||
},
|
||||
textlayout::{ParagraphBuilder, StyleMetrics, TextDecoration, TextStyle},
|
||||
Canvas, ImageFilter, Paint, Path,
|
||||
};
|
||||
|
||||
@@ -241,48 +238,24 @@ fn draw_text(
|
||||
paragraph_builder_groups: &mut [Vec<ParagraphBuilder>],
|
||||
) {
|
||||
let text_content = shape.get_text_content();
|
||||
let selrect_width = shape.selrect().width();
|
||||
let text_width = text_content.get_width(selrect_width);
|
||||
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 layout_info =
|
||||
calculate_text_layout_data(shape, text_content, paragraph_builder_groups, true);
|
||||
|
||||
let layer_rec = SaveLayerRec::default();
|
||||
canvas.save_layer(&layer_rec);
|
||||
let mut previous_line_height = text_content.normalized_line_height();
|
||||
|
||||
for paragraph_builder_group in paragraph_builder_groups {
|
||||
let group_offset_y = global_offset_y;
|
||||
let group_len = paragraph_builder_group.len();
|
||||
let mut paragraph_offset_y = previous_line_height;
|
||||
|
||||
for (paragraph_index, paragraph_builder) in paragraph_builder_group.iter_mut().enumerate() {
|
||||
let mut paragraph = paragraph_builder.build();
|
||||
paragraph.layout(text_width);
|
||||
let xy = (shape.selrect().x(), shape.selrect().y() + group_offset_y);
|
||||
paragraph.paint(canvas, xy);
|
||||
|
||||
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, ¶graph, paragraph_builder, line_metrics, xy);
|
||||
}
|
||||
for para in &layout_info.paragraphs {
|
||||
para.paragraph.paint(canvas, (para.x, para.y));
|
||||
for deco in ¶.decorations {
|
||||
draw_text_decorations(
|
||||
canvas,
|
||||
&deco.text_style,
|
||||
Some(deco.y),
|
||||
deco.thickness,
|
||||
deco.left,
|
||||
deco.width,
|
||||
);
|
||||
}
|
||||
|
||||
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)>,
|
||||
line_baseline: 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)]
|
||||
fn calculate_total_paragraphs_height(paragraphs: &mut [ParagraphBuilder], width: f32) -> f32 {
|
||||
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?
|
||||
// Type::Text(text_content) => {
|
||||
// self.surfaces
|
||||
|
||||
@@ -384,7 +384,7 @@ pub fn propagate_modifiers(
|
||||
if math::identitish(&entry.transform) {
|
||||
Modifier::Reflow(entry.id)
|
||||
} else {
|
||||
Modifier::Transform(entry.clone())
|
||||
Modifier::Transform(*entry)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -63,10 +63,50 @@ fn make_corner(
|
||||
Segment::CurveTo((h1, h2, to))
|
||||
}
|
||||
|
||||
// Calculates the minimum of five f32 values
|
||||
fn min_5(a: f32, b: f32, c: f32, d: f32, e: f32) -> f32 {
|
||||
f32::min(a, f32::min(b, f32::min(c, f32::min(d, e))))
|
||||
}
|
||||
|
||||
/*
|
||||
https://www.w3.org/TR/css-backgrounds-3/#corner-overlap
|
||||
|
||||
> Corner curves must not overlap: When the sum of any two adjacent border radii exceeds the size of the border box,
|
||||
> UAs must proportionally reduce the used values of all border radii until none of them overlap.
|
||||
|
||||
> The algorithm for reducing radii is as follows: Let f = min(Li/Si), where i ∈ {top, right, bottom, left}, Si is
|
||||
> the sum of the two corresponding radii of the corners on side i, and Ltop = Lbottom = the width of the box, and
|
||||
> Lleft = Lright = the height of the box. If f < 1, then all corner radii are reduced by multiplying them by f.
|
||||
*/
|
||||
fn fix_radius(
|
||||
r1: math::Point,
|
||||
r2: math::Point,
|
||||
r3: math::Point,
|
||||
r4: math::Point,
|
||||
width: f32,
|
||||
height: f32,
|
||||
) -> (math::Point, math::Point, math::Point, math::Point) {
|
||||
let f = min_5(
|
||||
1.0,
|
||||
width / (r1.x + r2.x),
|
||||
height / (r2.y + r3.y),
|
||||
width / (r3.x + r4.x),
|
||||
height / (r4.y + r1.y),
|
||||
);
|
||||
|
||||
if f < 1.0 {
|
||||
(r1 * f, r2 * f, r3 * f, r4 * f)
|
||||
} else {
|
||||
(r1, r2, r3, r4)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rect_segments(shape: &Shape, corners: Option<Corners>) -> Vec<Segment> {
|
||||
let sr = shape.selrect;
|
||||
|
||||
let segments = if let Some([r1, r2, r3, r4]) = corners {
|
||||
let (r1, r2, r3, r4) = fix_radius(r1, r2, r3, r4, sr.width(), sr.height());
|
||||
|
||||
let p1 = (sr.x(), sr.y() + r1.y);
|
||||
let p2 = (sr.x() + r1.x, sr.y());
|
||||
let p3 = (sr.x() + sr.width() - r2.x, sr.y());
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::render::text::calculate_decoration_metrics;
|
||||
use crate::{
|
||||
math::{Bounds, Matrix, Rect},
|
||||
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
|
||||
* 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)))
|
||||
}
|
||||
|
||||
// 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)]
|
||||
pub struct TextContent {
|
||||
pub paragraphs: Vec<Paragraph>,
|
||||
@@ -440,59 +494,15 @@ impl TextContent {
|
||||
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.
|
||||
fn text_layout_auto_width(&self) -> TextContentLayoutResult {
|
||||
let mut paragraph_builders = self.paragraph_builder_group_from_text(None);
|
||||
|
||||
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 =
|
||||
self.build_paragraphs_from_paragraph_builders(&mut paragraph_builders, f32::MAX);
|
||||
build_paragraphs_from_paragraph_builders(&mut paragraph_builders, f32::MAX);
|
||||
|
||||
let (width, height) =
|
||||
paragraphs
|
||||
@@ -521,10 +531,9 @@ impl TextContent {
|
||||
let mut paragraph_builders = self.paragraph_builder_group_from_text(None);
|
||||
|
||||
let normalized_line_height =
|
||||
self.calculate_normalized_line_height(&mut paragraph_builders, width);
|
||||
calculate_normalized_line_height(&mut paragraph_builders, width);
|
||||
|
||||
let paragraphs =
|
||||
self.build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width);
|
||||
let paragraphs = build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width);
|
||||
let height = paragraphs
|
||||
.iter()
|
||||
.flatten()
|
||||
@@ -546,10 +555,9 @@ impl TextContent {
|
||||
let mut paragraph_builders = self.paragraph_builder_group_from_text(None);
|
||||
|
||||
let normalized_line_height =
|
||||
self.calculate_normalized_line_height(&mut paragraph_builders, width);
|
||||
calculate_normalized_line_height(&mut paragraph_builders, width);
|
||||
|
||||
let paragraphs =
|
||||
self.build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width);
|
||||
let paragraphs = build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width);
|
||||
let paragraph_height = paragraphs
|
||||
.iter()
|
||||
.flatten()
|
||||
@@ -576,8 +584,7 @@ impl TextContent {
|
||||
|
||||
pub fn get_height(&self, width: f32) -> f32 {
|
||||
let mut paragraph_builders = self.paragraph_builder_group_from_text(None);
|
||||
let paragraphs =
|
||||
self.build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width);
|
||||
let paragraphs = build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width);
|
||||
let paragraph_height = paragraphs
|
||||
.iter()
|
||||
.flatten()
|
||||
@@ -733,8 +740,7 @@ impl TextContent {
|
||||
|
||||
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 paragraphs = build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width);
|
||||
|
||||
paragraphs
|
||||
.iter()
|
||||
@@ -863,17 +869,17 @@ impl Paragraph {
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct TextSpan {
|
||||
text: String,
|
||||
font_family: FontFamily,
|
||||
font_size: f32,
|
||||
line_height: f32,
|
||||
letter_spacing: f32,
|
||||
font_weight: i32,
|
||||
font_variant_id: Uuid,
|
||||
text_decoration: Option<TextDecoration>,
|
||||
text_transform: Option<TextTransform>,
|
||||
text_direction: TextDirection,
|
||||
fills: Vec<shapes::Fill>,
|
||||
pub text: String,
|
||||
pub font_family: FontFamily,
|
||||
pub font_size: f32,
|
||||
pub line_height: f32,
|
||||
pub letter_spacing: f32,
|
||||
pub font_weight: i32,
|
||||
pub font_variant_id: Uuid,
|
||||
pub text_decoration: Option<TextDecoration>,
|
||||
pub text_transform: Option<TextTransform>,
|
||||
pub text_direction: TextDirection,
|
||||
pub fills: Vec<shapes::Fill>,
|
||||
}
|
||||
|
||||
impl TextSpan {
|
||||
@@ -1045,3 +1051,251 @@ impl TextSpan {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
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
|
||||
}
|
||||
|
||||
@@ -23,13 +23,13 @@ impl Modifier {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
#[derive(PartialEq, Debug, Clone, Copy)]
|
||||
pub enum TransformEntrySource {
|
||||
Input,
|
||||
Propagate,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
#[derive(PartialEq, Debug, Clone, Copy)]
|
||||
#[repr(C)]
|
||||
pub struct TransformEntry {
|
||||
pub id: Uuid,
|
||||
@@ -65,10 +65,8 @@ impl TransformEntry {
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializableResult for TransformEntry {
|
||||
type BytesType = [u8; 40];
|
||||
|
||||
fn from_bytes(bytes: Self::BytesType) -> Self {
|
||||
impl From<[u8; 40]> for TransformEntry {
|
||||
fn from(bytes: [u8; 40]) -> Self {
|
||||
let id = uuid_from_u32_quartet(
|
||||
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
|
||||
u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
|
||||
@@ -89,29 +87,46 @@ impl SerializableResult for TransformEntry {
|
||||
);
|
||||
TransformEntry::from_input(id, transform)
|
||||
}
|
||||
}
|
||||
|
||||
fn as_bytes(&self) -> Self::BytesType {
|
||||
let mut result: Self::BytesType = [0; 40];
|
||||
let (a, b, c, d) = uuid_to_u32_quartet(&self.id);
|
||||
impl TryFrom<&[u8]> for TransformEntry {
|
||||
type Error = String;
|
||||
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
let bytes: [u8; 40] = bytes
|
||||
.try_into()
|
||||
.map_err(|_| "Invalid transform entry bytes".to_string())?;
|
||||
Ok(TransformEntry::from(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TransformEntry> for [u8; 40] {
|
||||
fn from(value: TransformEntry) -> Self {
|
||||
let mut result = [0; 40];
|
||||
let (a, b, c, d) = uuid_to_u32_quartet(&value.id);
|
||||
result[0..4].clone_from_slice(&a.to_le_bytes());
|
||||
result[4..8].clone_from_slice(&b.to_le_bytes());
|
||||
result[8..12].clone_from_slice(&c.to_le_bytes());
|
||||
result[12..16].clone_from_slice(&d.to_le_bytes());
|
||||
|
||||
result[16..20].clone_from_slice(&self.transform[0].to_le_bytes());
|
||||
result[20..24].clone_from_slice(&self.transform[3].to_le_bytes());
|
||||
result[24..28].clone_from_slice(&self.transform[1].to_le_bytes());
|
||||
result[28..32].clone_from_slice(&self.transform[4].to_le_bytes());
|
||||
result[32..36].clone_from_slice(&self.transform[2].to_le_bytes());
|
||||
result[36..40].clone_from_slice(&self.transform[5].to_le_bytes());
|
||||
result[16..20].clone_from_slice(&value.transform[0].to_le_bytes());
|
||||
result[20..24].clone_from_slice(&value.transform[3].to_le_bytes());
|
||||
result[24..28].clone_from_slice(&value.transform[1].to_le_bytes());
|
||||
result[28..32].clone_from_slice(&value.transform[4].to_le_bytes());
|
||||
result[32..36].clone_from_slice(&value.transform[2].to_le_bytes());
|
||||
result[36..40].clone_from_slice(&value.transform[5].to_le_bytes());
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializableResult for TransformEntry {
|
||||
type BytesType = [u8; 40];
|
||||
|
||||
// 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());
|
||||
let bytes = Self::BytesType::from(*self);
|
||||
slice.clone_from_slice(&bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,8 +213,8 @@ mod tests {
|
||||
Matrix::new_all(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 0.0, 0.0, 1.0),
|
||||
);
|
||||
|
||||
let bytes = entry.as_bytes();
|
||||
let bytes: [u8; 40] = entry.into();
|
||||
|
||||
assert_eq!(entry, TransformEntry::from_bytes(bytes));
|
||||
assert_eq!(entry, TransformEntry::from(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,10 +49,8 @@ impl fmt::Display for Uuid {
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializableResult for Uuid {
|
||||
type BytesType = [u8; 16];
|
||||
|
||||
fn from_bytes(bytes: Self::BytesType) -> Self {
|
||||
impl From<[u8; 16]> for Uuid {
|
||||
fn from(bytes: [u8; 16]) -> Self {
|
||||
Self(*uuid_from_u32_quartet(
|
||||
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
|
||||
u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
|
||||
@@ -60,10 +58,22 @@ impl SerializableResult for Uuid {
|
||||
u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn as_bytes(&self) -> Self::BytesType {
|
||||
let mut result: Self::BytesType = [0; 16];
|
||||
let (a, b, c, d) = uuid_to_u32_quartet(self);
|
||||
impl TryFrom<&[u8]> for Uuid {
|
||||
type Error = String;
|
||||
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
let bytes: [u8; 16] = bytes
|
||||
.try_into()
|
||||
.map_err(|_| "Invalid UUID bytes".to_string())?;
|
||||
Ok(Self::from(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Uuid> for [u8; 16] {
|
||||
fn from(value: Uuid) -> Self {
|
||||
let mut result = [0; 16];
|
||||
let (a, b, c, d) = uuid_to_u32_quartet(&value);
|
||||
result[0..4].clone_from_slice(&a.to_le_bytes());
|
||||
result[4..8].clone_from_slice(&b.to_le_bytes());
|
||||
result[8..12].clone_from_slice(&c.to_le_bytes());
|
||||
@@ -71,10 +81,15 @@ impl SerializableResult for Uuid {
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializableResult for Uuid {
|
||||
type BytesType = [u8; 16];
|
||||
|
||||
// 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());
|
||||
let bytes = Self::BytesType::from(*self);
|
||||
slice.clone_from_slice(&bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::mem;
|
||||
use crate::mem::SerializableResult;
|
||||
// use crate::mem::SerializableResult;
|
||||
use crate::uuid::Uuid;
|
||||
use crate::with_state_mut;
|
||||
use crate::STATE;
|
||||
@@ -48,8 +48,8 @@ pub struct ShapeImageIds {
|
||||
|
||||
impl From<[u8; IMAGE_IDS_SIZE]> for ShapeImageIds {
|
||||
fn from(bytes: [u8; IMAGE_IDS_SIZE]) -> Self {
|
||||
let shape_id = Uuid::from_bytes(bytes[0..16].try_into().unwrap());
|
||||
let image_id = Uuid::from_bytes(bytes[16..32].try_into().unwrap());
|
||||
let shape_id = Uuid::try_from(&bytes[0..16]).unwrap();
|
||||
let image_id = Uuid::try_from(&bytes[16..32]).unwrap();
|
||||
ShapeImageIds { shape_id, image_id }
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,7 @@ pub extern "C" fn store_image() {
|
||||
/// Stores an image from an existing WebGL texture, avoiding re-decoding
|
||||
/// Expected memory layout:
|
||||
/// - bytes 0-15: shape UUID
|
||||
/// - bytes 16-31: image UUID
|
||||
/// - bytes 16-31: image UUID
|
||||
/// - bytes 32-35: is_thumbnail flag (u32)
|
||||
/// - bytes 36-39: GL texture ID (u32)
|
||||
/// - bytes 40-43: width (i32)
|
||||
|
||||
@@ -40,7 +40,7 @@ pub extern "C" fn clear_shape_layout() {
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_layout_child_data(
|
||||
pub extern "C" fn set_layout_data(
|
||||
margin_top: f32,
|
||||
margin_right: f32,
|
||||
margin_bottom: f32,
|
||||
|
||||
@@ -51,25 +51,20 @@ impl TryFrom<&[u8]> for RawSegmentData {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RawSegmentData> for [u8; RAW_SEGMENT_DATA_SIZE] {
|
||||
fn from(value: RawSegmentData) -> Self {
|
||||
unsafe { std::mem::transmute(value) }
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializableResult for RawSegmentData {
|
||||
type BytesType = [u8; RAW_SEGMENT_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 RawSegmentData as *const u8;
|
||||
let bytes: &[u8] = unsafe { std::slice::from_raw_parts(ptr, RAW_SEGMENT_DATA_SIZE) };
|
||||
let mut result = [0; RAW_SEGMENT_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());
|
||||
let bytes = Self::BytesType::from(*self);
|
||||
slice.clone_from_slice(&bytes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ pub extern "C" fn calculate_bool(raw_bool_type: u8) -> *mut u8 {
|
||||
|
||||
let entries: Vec<Uuid> = bytes
|
||||
.chunks(size_of::<<Uuid as SerializableResult>::BytesType>())
|
||||
.map(|data| Uuid::from_bytes(data.try_into().unwrap()))
|
||||
.map(|data| Uuid::try_from(data).unwrap())
|
||||
.collect();
|
||||
|
||||
mem::free_bytes();
|
||||
|
||||
@@ -2,13 +2,14 @@ use macros::ToJs;
|
||||
|
||||
use super::{fills::RawFillData, fonts::RawFontStyle};
|
||||
use crate::math::{Matrix, Point};
|
||||
use crate::mem;
|
||||
use crate::mem::{self, SerializableResult};
|
||||
use crate::shapes::{
|
||||
self, GrowType, Shape, TextAlign, TextDecoration, TextDirection, TextTransform, Type,
|
||||
};
|
||||
use crate::utils::{uuid_from_u32, uuid_from_u32_quartet};
|
||||
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>();
|
||||
@@ -411,3 +412,39 @@ pub extern "C" fn get_caret_position_at(x: f32, y: f32) -> i32 {
|
||||
});
|
||||
-1
|
||||
}
|
||||
|
||||
const RAW_POSITION_DATA_SIZE: usize = size_of::<shapes::PositionData>();
|
||||
|
||||
impl From<[u8; RAW_POSITION_DATA_SIZE]> for shapes::PositionData {
|
||||
fn from(bytes: [u8; RAW_POSITION_DATA_SIZE]) -> Self {
|
||||
unsafe { std::mem::transmute(bytes) }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<shapes::PositionData> for [u8; RAW_POSITION_DATA_SIZE] {
|
||||
fn from(value: shapes::PositionData) -> Self {
|
||||
unsafe { std::mem::transmute(value) }
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializableResult for shapes::PositionData {
|
||||
type BytesType = [u8; RAW_POSITION_DATA_SIZE];
|
||||
|
||||
// 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]) {
|
||||
let bytes = Self::BytesType::from(*self);
|
||||
slice.clone_from_slice(&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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user