diff --git a/frontend/resources/wasm-playground/js/lib.js b/frontend/resources/wasm-playground/js/lib.js
index 0f27cf83a1..156b55f2ef 100644
--- a/frontend/resources/wasm-playground/js/lib.js
+++ b/frontend/resources/wasm-playground/js/lib.js
@@ -216,3 +216,86 @@ export function setupInteraction(canvas) {
canvas.addEventListener("mouseup", () => { isPanning = false; });
canvas.addEventListener("mouseout", () => { isPanning = false; });
}
+
+export function addTextShape(x, y, fontSize, text) {
+ const numLeaves = 1; // Single text leaf for simplicity
+ const paragraphAttrSize = 48;
+ const leafAttrSize = 56;
+ const fillSize = 160;
+ const textBuffer = new TextEncoder().encode(text);
+ const textSize = textBuffer.byteLength;
+
+ // Calculate fills
+ const fills = [
+ {
+ type: "solid",
+ color: getRandomColor(),
+ opacity: getRandomFloat(0.5, 1.0),
+ },
+ ];
+ const totalFills = fills.length;
+ const totalFillsSize = totalFills * fillSize;
+
+ // Calculate metadata and total buffer size
+ const metadataSize = paragraphAttrSize + leafAttrSize + totalFillsSize;
+ const totalSize = metadataSize + textSize;
+
+ // Allocate buffer
+ const bufferPtr = allocBytes(totalSize);
+ const heap = new Uint8Array(Module.HEAPU8.buffer, bufferPtr, totalSize);
+ const dview = new DataView(heap.buffer, bufferPtr, totalSize);
+
+ // Set number of leaves
+ dview.setUint32(0, numLeaves, true);
+
+ // Serialize paragraph attributes
+ dview.setUint8(4, 1); // text-align: left
+ dview.setUint8(5, 0); // text-direction: LTR
+ dview.setUint8(6, 0); // text-decoration: none
+ dview.setUint8(7, 0); // text-transform: none
+ dview.setFloat32(8, 1.2, true); // line-height
+ dview.setFloat32(12, 0, true); // letter-spacing
+ dview.setUint32(16, 0, true); // typography-ref-file (UUID part 1)
+ dview.setUint32(20, 0, true); // typography-ref-file (UUID part 2)
+ dview.setUint32(24, 0, true); // typography-ref-file (UUID part 3)
+ dview.setInt32(28, 0, true); // typography-ref-file (UUID part 4)
+ dview.setUint32(32, 0, true); // typography-ref-id (UUID part 1)
+ dview.setUint32(36, 0, true); // typography-ref-id (UUID part 2)
+ dview.setUint32(40, 0, true); // typography-ref-id (UUID part 3)
+ dview.setInt32(44, 0, true); // typography-ref-id (UUID part 4)
+
+ // Serialize leaf attributes
+ const leafOffset = paragraphAttrSize;
+ dview.setUint8(leafOffset, 0); // font-style: normal
+ dview.setFloat32(leafOffset + 4, fontSize, true); // font-size
+ dview.setUint32(leafOffset + 8, 400, true); // font-weight: normal
+ dview.setUint32(leafOffset + 12, 0, true); // font-id (UUID part 1)
+ dview.setUint32(leafOffset + 16, 0, true); // font-id (UUID part 2)
+ dview.setUint32(leafOffset + 20, 0, true); // font-id (UUID part 3)
+ dview.setInt32(leafOffset + 24, 0, true); // font-id (UUID part 4)
+ dview.setInt32(leafOffset + 28, 0, true); // font-family hash
+ dview.setUint32(leafOffset + 32, 0, true); // font-variant-id (UUID part 1)
+ dview.setUint32(leafOffset + 36, 0, true); // font-variant-id (UUID part 2)
+ dview.setUint32(leafOffset + 40, 0, true); // font-variant-id (UUID part 3)
+ dview.setInt32(leafOffset + 44, 0, true); // font-variant-id (UUID part 4)
+ dview.setInt32(leafOffset + 48, textSize, true); // text-length
+ dview.setInt32(leafOffset + 52, totalFills, true); // total fills count
+
+ // Serialize fills
+ let fillOffset = leafOffset + leafAttrSize;
+ fills.forEach((fill) => {
+ if (fill.type === "solid") {
+ const argb = hexToU32ARGB(fill.color, fill.opacity);
+ dview.setUint8(fillOffset, 0x00, true); // Fill type: solid
+ dview.setUint32(fillOffset + 4, argb, true);
+ fillOffset += fillSize; // Move to the next fill
+ }
+ });
+
+ // Add text content
+ const textOffset = metadataSize;
+ heap.set(textBuffer, textOffset);
+
+ // Call the WebAssembly function
+ Module._set_shape_text_content();
+}
diff --git a/frontend/resources/wasm-playground/js/texts.js b/frontend/resources/wasm-playground/js/texts.js
new file mode 100644
index 0000000000..aed1c48da3
--- /dev/null
+++ b/frontend/resources/wasm-playground/js/texts.js
@@ -0,0 +1 @@
+// Placeholder for text-specific logic if needed in the future.
diff --git a/frontend/resources/wasm-playground/texts.html b/frontend/resources/wasm-playground/texts.html
new file mode 100644
index 0000000000..77657be0e9
--- /dev/null
+++ b/frontend/resources/wasm-playground/texts.html
@@ -0,0 +1,94 @@
+
+
+
+
+ WASM + WebGL2 Texts
+
+
+
+
+
+
+
diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs
index 70ceea42dc..38a5df3d24 100644
--- a/render-wasm/src/render.rs
+++ b/render-wasm/src/render.rs
@@ -1,3 +1,7 @@
+use skia_safe::{self as skia, Matrix, RRect, Rect};
+use std::borrow::Cow;
+use std::collections::{HashMap, HashSet};
+
mod blend;
mod debug;
mod fills;
@@ -10,10 +14,6 @@ mod strokes;
mod surfaces;
mod text;
-use skia_safe::{self as skia, Matrix, RRect, Rect};
-use std::borrow::Cow;
-use std::collections::{HashMap, HashSet};
-
use gpu_state::GpuState;
use options::RenderOptions;
use surfaces::{SurfaceId, Surfaces};
@@ -416,34 +416,28 @@ impl RenderState {
}
Type::Text(text_content) => {
- self.surfaces.apply_mut(&[SurfaceId::Fills], |s| {
- s.canvas().concat(&matrix);
- });
+ self.surfaces
+ .apply_mut(&[SurfaceId::Fills, SurfaceId::Strokes], |s| {
+ s.canvas().concat(&matrix);
+ });
let text_content = text_content.new_bounds(shape.selrect());
- let paragraphs = text_content.get_skia_paragraphs(self.fonts.font_collection());
+ let paths = text_content.get_paths(antialias);
- shadows::render_text_drop_shadows(self, &shape, ¶graphs, antialias);
- text::render(self, &shape, ¶graphs, None, None);
+ shadows::render_text_drop_shadows(self, &shape, &paths, antialias);
+ text::render(self, &paths, None, None);
for stroke in shape.strokes().rev() {
- let stroke_paragraphs = text_content.get_skia_stroke_paragraphs(
- stroke,
- &shape.selrect(),
- self.fonts.font_collection(),
+ shadows::render_text_stroke_drop_shadows(
+ self, &shape, &paths, stroke, antialias,
);
- shadows::render_text_drop_shadows(self, &shape, &stroke_paragraphs, antialias);
- text::render(
- self,
- &shape,
- &stroke_paragraphs,
- Some(SurfaceId::Strokes),
- None,
+ strokes::render_text_paths(self, &shape, stroke, &paths, None, None, antialias);
+ shadows::render_text_stroke_inner_shadows(
+ self, &shape, &paths, stroke, antialias,
);
- shadows::render_text_inner_shadows(self, &shape, &stroke_paragraphs, antialias);
}
- shadows::render_text_inner_shadows(self, &shape, ¶graphs, antialias);
+ shadows::render_text_inner_shadows(self, &shape, &paths, antialias);
}
_ => {
self.surfaces.apply_mut(
@@ -688,7 +682,7 @@ impl RenderState {
// scaled offset of the viewbox, this method snaps the origin to the nearest
// lower multiple of `TILE_SIZE`. This ensures the tile bounds are aligned
// with the global tile grid, which is useful for rendering tiles in a
- /// consistent and predictable layout.
+ // consistent and predictable layout.
pub fn get_current_aligned_tile_bounds(&mut self) -> Rect {
let tiles::Tile(tile_x, tile_y) = self.current_tile.unwrap();
let scale = self.get_scale();
diff --git a/render-wasm/src/render/fonts.rs b/render-wasm/src/render/fonts.rs
index e21205f23d..9c0e8bbfb0 100644
--- a/render-wasm/src/render/fonts.rs
+++ b/render-wasm/src/render/fonts.rs
@@ -87,6 +87,16 @@ impl FontStore {
let serialized = format!("{}", family);
self.font_provider.family_names().any(|x| x == serialized)
}
+
+ pub fn get_emoji_font(&self, size: f32) -> Option {
+ if let Some(typeface) = self
+ .font_provider
+ .match_family_style(DEFAULT_EMOJI_FONT, skia::FontStyle::default())
+ {
+ return Some(Font::from_typeface(typeface, size));
+ }
+ None
+ }
}
fn load_default_provider(font_mgr: &FontMgr) -> skia::textlayout::TypefaceFontProvider {
diff --git a/render-wasm/src/render/shadows.rs b/render-wasm/src/render/shadows.rs
index d5819c8d7c..7708f6171b 100644
--- a/render-wasm/src/render/shadows.rs
+++ b/render-wasm/src/render/shadows.rs
@@ -2,7 +2,7 @@ use super::{RenderState, SurfaceId};
use crate::render::strokes;
use crate::render::text::{self};
use crate::shapes::{Shadow, Shape, Stroke, Type};
-use skia_safe::{textlayout::Paragraph, Paint};
+use skia_safe::{Paint, Path};
// Fill Shadows
pub fn render_fill_drop_shadows(render_state: &mut RenderState, shape: &Shape, antialias: bool) {
@@ -86,56 +86,92 @@ pub fn render_stroke_inner_shadows(
pub fn render_text_drop_shadows(
render_state: &mut RenderState,
shape: &Shape,
- paragraphs: &[Vec],
+ paths: &Vec<(Path, Paint)>,
antialias: bool,
) {
for shadow in shape.drop_shadows().rev().filter(|s| !s.hidden()) {
- render_text_drop_shadow(render_state, shape, shadow, paragraphs, antialias);
+ render_text_drop_shadow(render_state, shadow, paths, antialias);
}
}
pub fn render_text_drop_shadow(
render_state: &mut RenderState,
- shape: &Shape,
shadow: &Shadow,
- paragraphs: &[Vec],
+ paths: &Vec<(Path, Paint)>,
antialias: bool,
) {
let paint = shadow.get_drop_shadow_paint(antialias);
-
text::render(
render_state,
- shape,
- paragraphs,
+ paths,
Some(SurfaceId::DropShadows),
Some(paint),
);
}
+pub fn render_text_stroke_drop_shadows(
+ render_state: &mut RenderState,
+ shape: &Shape,
+ paths: &Vec<(Path, Paint)>,
+ stroke: &Stroke,
+ antialias: bool,
+) {
+ for shadow in shape.drop_shadows().rev().filter(|s| !s.hidden()) {
+ let stroke_shadow = shadow.get_drop_shadow_filter();
+ strokes::render_text_paths(
+ render_state,
+ shape,
+ stroke,
+ paths,
+ Some(SurfaceId::DropShadows),
+ stroke_shadow.as_ref(),
+ antialias,
+ );
+ }
+}
+
pub fn render_text_inner_shadows(
render_state: &mut RenderState,
shape: &Shape,
- paragraphs: &[Vec],
+ paths: &Vec<(Path, Paint)>,
antialias: bool,
) {
for shadow in shape.inner_shadows().rev().filter(|s| !s.hidden()) {
- render_text_inner_shadow(render_state, shape, shadow, paragraphs, antialias);
+ render_text_inner_shadow(render_state, shadow, paths, antialias);
+ }
+}
+
+pub fn render_text_stroke_inner_shadows(
+ render_state: &mut RenderState,
+ shape: &Shape,
+ paths: &Vec<(Path, Paint)>,
+ stroke: &Stroke,
+ antialias: bool,
+) {
+ for shadow in shape.inner_shadows().rev().filter(|s| !s.hidden()) {
+ let stroke_shadow = shadow.get_inner_shadow_filter();
+ strokes::render_text_paths(
+ render_state,
+ shape,
+ stroke,
+ paths,
+ Some(SurfaceId::InnerShadows),
+ stroke_shadow.as_ref(),
+ antialias,
+ );
}
}
pub fn render_text_inner_shadow(
render_state: &mut RenderState,
- shape: &Shape,
shadow: &Shadow,
- paragraphs: &[Vec],
+ paths: &Vec<(Path, Paint)>,
antialias: bool,
) {
let paint = shadow.get_inner_shadow_paint(antialias);
-
text::render(
render_state,
- shape,
- paragraphs,
+ paths,
Some(SurfaceId::InnerShadows),
Some(paint),
);
diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs
index 36748a8dff..17a8c45980 100644
--- a/render-wasm/src/render/strokes.rs
+++ b/render-wasm/src/render/strokes.rs
@@ -69,6 +69,39 @@ fn draw_stroke_on_circle(
canvas.draw_oval(stroke_rect, &paint);
}
+fn draw_inner_stroke_path(
+ canvas: &skia::Canvas,
+ path: &skia::Path,
+ paint: &skia::Paint,
+ antialias: bool,
+) {
+ canvas.save();
+ canvas.clip_path(path, skia::ClipOp::Intersect, antialias);
+ canvas.draw_path(path, paint);
+ canvas.restore();
+}
+
+fn draw_outer_stroke_path(
+ canvas: &skia::Canvas,
+ path: &skia::Path,
+ paint: &skia::Paint,
+ antialias: bool,
+) {
+ let mut outer_paint = skia::Paint::default();
+ outer_paint.set_blend_mode(skia::BlendMode::SrcOver);
+ outer_paint.set_anti_alias(antialias);
+ let layer_rec = skia::canvas::SaveLayerRec::default().paint(&outer_paint);
+ canvas.save_layer(&layer_rec);
+ canvas.draw_path(path, paint);
+
+ let mut clear_paint = skia::Paint::default();
+ clear_paint.set_blend_mode(skia::BlendMode::Clear);
+ clear_paint.set_anti_alias(antialias);
+ canvas.draw_path(path, &clear_paint);
+
+ canvas.restore();
+}
+
// FIXME: See if we can simplify these arguments
#[allow(clippy::too_many_arguments)]
pub fn draw_stroke_on_path(
@@ -93,34 +126,15 @@ pub fn draw_stroke_on_path(
paint.set_image_filter(filter.clone());
}
- // Draw the different kind of strokes for a path requires different strategies:
match stroke.render_kind(is_open) {
- // For inner stroke we draw a center stroke (with double width) and clip to the original path (that way the extra outer stroke is removed)
StrokeKind::Inner => {
- canvas.save(); // As we are using clear for surfaces we use save and restore here to still be able to clean the full surface
- canvas.clip_path(&skia_path, skia::ClipOp::Intersect, antialias);
- canvas.draw_path(&skia_path, &paint);
- canvas.restore();
+ draw_inner_stroke_path(canvas, &skia_path, &paint, antialias);
}
- // For center stroke we don't need to do anything extra
StrokeKind::Center => {
canvas.draw_path(&skia_path, &paint);
}
- // For outer stroke we draw a center stroke (with double width) and use another path with blend mode clear to remove the inner stroke added
StrokeKind::Outer => {
- let mut outer_paint = skia::Paint::default();
- outer_paint.set_blend_mode(skia::BlendMode::SrcOver);
- outer_paint.set_anti_alias(antialias);
- let layer_rec = skia::canvas::SaveLayerRec::default().paint(&outer_paint);
- canvas.save_layer(&layer_rec);
- canvas.draw_path(&skia_path, &paint);
-
- let mut clear_paint = skia::Paint::default();
- clear_paint.set_blend_mode(skia::BlendMode::Clear);
- clear_paint.set_anti_alias(antialias);
- canvas.draw_path(&skia_path, &clear_paint);
-
- canvas.restore();
+ draw_outer_stroke_path(canvas, &skia_path, &paint, antialias);
}
}
@@ -543,3 +557,44 @@ pub fn render(
}
}
}
+
+pub fn render_text_paths(
+ render_state: &mut RenderState,
+ shape: &Shape,
+ stroke: &Stroke,
+ paths: &Vec<(skia::Path, skia::Paint)>,
+ surface_id: Option,
+ shadow: Option<&ImageFilter>,
+ antialias: bool,
+) {
+ let scale = render_state.get_scale();
+ let canvas = render_state
+ .surfaces
+ .canvas(surface_id.unwrap_or(SurfaceId::Strokes));
+ let selrect = &shape.selrect;
+ let svg_attrs = &shape.svg_attrs;
+ let mut paint: skia_safe::Handle<_> =
+ stroke.to_text_stroked_paint(false, selrect, svg_attrs, scale, antialias);
+
+ if let Some(filter) = shadow {
+ paint.set_image_filter(filter.clone());
+ }
+
+ match stroke.render_kind(false) {
+ StrokeKind::Inner => {
+ for (path, _) in paths {
+ draw_inner_stroke_path(canvas, path, &paint, antialias);
+ }
+ }
+ StrokeKind::Center => {
+ for (path, _) in paths {
+ canvas.draw_path(path, &paint);
+ }
+ }
+ StrokeKind::Outer => {
+ for (path, _) in paths {
+ draw_outer_stroke_path(canvas, path, &paint, antialias);
+ }
+ }
+ }
+}
diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs
index 0bec6887d2..d460a7498a 100644
--- a/render-wasm/src/render/text.rs
+++ b/render-wasm/src/render/text.rs
@@ -1,10 +1,9 @@
-use super::{RenderState, Shape, SurfaceId};
-use skia_safe::{self as skia, canvas::SaveLayerRec, textlayout::Paragraph};
+use super::{RenderState, SurfaceId};
+use skia_safe::{self as skia, canvas::SaveLayerRec};
pub fn render(
render_state: &mut RenderState,
- shape: &Shape,
- paragraphs: &[Vec],
+ paths: &Vec<(skia::Path, skia::Paint)>,
surface_id: Option,
paint: Option,
) {
@@ -15,13 +14,12 @@ pub fn render(
.canvas(surface_id.unwrap_or(SurfaceId::Fills));
canvas.save_layer(&mask);
- for group in paragraphs {
- let mut offset_y = 0.0;
- for skia_paragraph in group {
- let xy = (shape.selrect().x(), shape.selrect.y() + offset_y);
- skia_paragraph.paint(canvas, xy);
- offset_y += skia_paragraph.height();
+
+ for (path, paint) in paths {
+ if path.is_empty() {
+ eprintln!("Warning: Empty path detected");
}
+ canvas.draw_path(path, paint);
}
canvas.restore();
}
diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs
index ea328266d2..97299877ea 100644
--- a/render-wasm/src/shapes.rs
+++ b/render-wasm/src/shapes.rs
@@ -602,6 +602,7 @@ impl Shape {
rect.join(shadow_rect);
}
+
if self.blur.blur_type != blurs::BlurType::None {
rect.left -= self.blur.value;
rect.top -= self.blur.value;
@@ -609,6 +610,18 @@ impl Shape {
rect.bottom += self.blur.value;
}
+ if let Some(max_stroke_width) = self
+ .strokes
+ .iter()
+ .map(|s| s.width)
+ .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
+ {
+ rect.left -= max_stroke_width / 2.0;
+ rect.top -= max_stroke_width / 2.0;
+ rect.right += max_stroke_width / 2.0;
+ rect.bottom += max_stroke_width / 2.0;
+ }
+
rect
}
@@ -704,6 +717,7 @@ impl Shape {
match self.shape_type {
Type::Text(ref mut text) => {
text.add_paragraph(paragraph);
+ text.new_bounds(self.selrect);
Ok(())
}
_ => Err("Shape is not a text".to_string()),
diff --git a/render-wasm/src/shapes/fills.rs b/render-wasm/src/shapes/fills.rs
index dd73f81ec8..93f2b053fa 100644
--- a/render-wasm/src/shapes/fills.rs
+++ b/render-wasm/src/shapes/fills.rs
@@ -1,4 +1,4 @@
-use skia_safe::{self as skia, Paint, Rect};
+use skia_safe::{self as skia, Rect};
pub use super::Color;
use crate::utils::get_image;
@@ -139,87 +139,62 @@ pub enum Fill {
impl Fill {
pub fn to_paint(&self, rect: &Rect, anti_alias: bool) -> skia::Paint {
- match self {
- Self::Solid(SolidColor(color)) => {
- let mut p = skia::Paint::default();
- p.set_color(*color);
- p.set_style(skia::PaintStyle::Fill);
- p.set_anti_alias(anti_alias);
- p.set_blend_mode(skia::BlendMode::SrcOver);
- p
- }
- Self::LinearGradient(gradient) => {
- let mut p = skia::Paint::default();
- p.set_shader(gradient.to_linear_shader(rect));
- p.set_alpha(gradient.opacity);
- p.set_style(skia::PaintStyle::Fill);
- p.set_anti_alias(anti_alias);
- p.set_blend_mode(skia::BlendMode::SrcOver);
- p
- }
- Self::RadialGradient(gradient) => {
- let mut p = skia::Paint::default();
- p.set_shader(gradient.to_radial_shader(rect));
- p.set_alpha(gradient.opacity);
- p.set_style(skia::PaintStyle::Fill);
- p.set_anti_alias(anti_alias);
- p.set_blend_mode(skia::BlendMode::SrcOver);
- p
- }
- Self::Image(image_fill) => {
- let mut p = skia::Paint::default();
- p.set_style(skia::PaintStyle::Fill);
- p.set_anti_alias(anti_alias);
- p.set_blend_mode(skia::BlendMode::SrcOver);
- p.set_alpha(image_fill.opacity);
- p
- }
+ let mut paint = skia::Paint::default();
+ paint.set_style(skia::PaintStyle::Fill);
+ paint.set_anti_alias(anti_alias);
+ paint.set_blend_mode(skia::BlendMode::SrcOver);
+ let shader = self.get_fill_shader(rect);
+ if let Some(shader) = shader {
+ paint.set_shader(shader);
}
+ paint
}
-}
-pub fn get_fill_shader(fill: &Fill, bounding_box: &Rect) -> Option {
- match fill {
- Fill::Solid(SolidColor(color)) => Some(skia::shaders::color(*color)),
- Fill::LinearGradient(gradient) => gradient.to_linear_shader(bounding_box),
- Fill::RadialGradient(gradient) => gradient.to_radial_shader(bounding_box),
- Fill::Image(image_fill) => {
- let mut image_shader = None;
- let image = get_image(&image_fill.id);
- if let Some(image) = image {
- let sampling_options =
- skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest);
+ pub fn get_fill_shader(&self, bounding_box: &Rect) -> Option {
+ match self {
+ Fill::Solid(SolidColor(color)) => Some(skia::shaders::color(*color)),
+ Fill::LinearGradient(gradient) => gradient.to_linear_shader(bounding_box),
+ Fill::RadialGradient(gradient) => gradient.to_radial_shader(bounding_box),
+ Fill::Image(image_fill) => {
+ let mut image_shader = None;
+ let image = get_image(&image_fill.id);
+ if let Some(image) = image {
+ let sampling_options = skia::SamplingOptions::new(
+ skia::FilterMode::Linear,
+ skia::MipmapMode::Nearest,
+ );
- // FIXME no image ratio applied, centered to the current rect
- let tile_modes = (skia::TileMode::Clamp, skia::TileMode::Clamp);
- let image_width = image_fill.width as f32;
- let image_height = image_fill.height as f32;
- let scale_x = bounding_box.width() / image_width;
- let scale_y = bounding_box.height() / image_height;
- let scale = scale_x.max(scale_y);
- let scaled_width = image_width * scale;
- let scaled_height = image_height * scale;
- let pos_x = bounding_box.left() - (scaled_width - bounding_box.width()) / 2.0;
- let pos_y = bounding_box.top() - (scaled_height - bounding_box.height()) / 2.0;
+ // FIXME no image ratio applied, centered to the current rect
+ let tile_modes = (skia::TileMode::Clamp, skia::TileMode::Clamp);
+ let image_width = image_fill.width as f32;
+ let image_height = image_fill.height as f32;
+ let scale_x = bounding_box.width() / image_width;
+ let scale_y = bounding_box.height() / image_height;
+ let scale = scale_x.max(scale_y);
+ let scaled_width = image_width * scale;
+ let scaled_height = image_height * scale;
+ let pos_x = bounding_box.left() - (scaled_width - bounding_box.width()) / 2.0;
+ let pos_y = bounding_box.top() - (scaled_height - bounding_box.height()) / 2.0;
- let mut matrix = skia::Matrix::new_identity();
- matrix.pre_translate((pos_x, pos_y));
- matrix.pre_scale((scale, scale), None);
+ let mut matrix = skia::Matrix::new_identity();
+ matrix.pre_translate((pos_x, pos_y));
+ matrix.pre_scale((scale, scale), None);
- let opacity = image_fill.opacity();
- let alpha_color = skia::Color4f::new(1.0, 1.0, 1.0, opacity as f32 / 255.0);
- let alpha_shader = skia::shaders::color(alpha_color.to_color());
+ let opacity = image_fill.opacity();
+ let alpha_color = skia::Color4f::new(1.0, 1.0, 1.0, opacity as f32 / 255.0);
+ let alpha_shader = skia::shaders::color(alpha_color.to_color());
- image_shader = image.to_shader(tile_modes, sampling_options, &matrix);
- if let Some(shader) = image_shader {
- image_shader = Some(skia::shaders::blend(
- skia::Blender::mode(skia::BlendMode::DstIn),
- shader,
- alpha_shader,
- ));
+ image_shader = image.to_shader(tile_modes, sampling_options, &matrix);
+ if let Some(shader) = image_shader {
+ image_shader = Some(skia::shaders::blend(
+ skia::Blender::mode(skia::BlendMode::DstIn),
+ shader,
+ alpha_shader,
+ ));
+ }
}
+ image_shader
}
- image_shader
}
}
}
@@ -229,7 +204,7 @@ pub fn merge_fills(fills: &[Fill], bounding_box: Rect) -> skia::Paint {
let mut fills_paint = skia::Paint::default();
for fill in fills {
- let shader = get_fill_shader(fill, &bounding_box);
+ let shader = fill.get_fill_shader(&bounding_box);
if let Some(shader) = shader {
combined_shader = match combined_shader {
@@ -246,10 +221,3 @@ pub fn merge_fills(fills: &[Fill], bounding_box: Rect) -> skia::Paint {
fills_paint.set_shader(combined_shader.clone());
fills_paint
}
-
-pub fn set_paint_fill(paint: &mut Paint, fill: &Fill, bounding_box: &Rect) {
- let shader = get_fill_shader(fill, bounding_box);
- if let Some(shader) = shader {
- paint.set_shader(shader);
- }
-}
diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs
index a1d5cd00d2..c757d8c00d 100644
--- a/render-wasm/src/shapes/modifiers.rs
+++ b/render-wasm/src/shapes/modifiers.rs
@@ -108,8 +108,6 @@ pub fn propagate_modifiers(
modifiers: &[TransformEntry],
) -> (Vec, HashMap) {
let shapes = &state.shapes;
-
- let font_col = state.render_state.fonts.font_collection();
let mut entries: VecDeque<_> = modifiers
.iter()
.map(|entry| Modifier::Transform(entry.clone()))
@@ -147,9 +145,9 @@ pub fn propagate_modifiers(
if let Type::Text(content) = &shape.shape_type {
if content.grow_type() == GrowType::AutoHeight {
- let mut paragraphs = content.get_skia_paragraphs(font_col);
+ let mut paragraphs = content.get_skia_paragraphs();
set_paragraphs_width(shape_bounds_after.width(), &mut paragraphs);
- let height = auto_height(¶graphs);
+ let height = auto_height(&mut paragraphs);
let resize_transform = math::resize_matrix(
&shape_bounds_after,
&shape_bounds_after,
diff --git a/render-wasm/src/shapes/strokes.rs b/render-wasm/src/shapes/strokes.rs
index e389b627cb..e93bb42d85 100644
--- a/render-wasm/src/shapes/strokes.rs
+++ b/render-wasm/src/shapes/strokes.rs
@@ -224,6 +224,29 @@ impl Stroke {
antialias: bool,
) -> skia::Paint {
let mut paint = self.to_paint(rect, svg_attrs, scale, antialias);
+
+ match self.render_kind(is_open) {
+ StrokeKind::Inner => {
+ paint.set_stroke_width(2. * paint.stroke_width());
+ }
+ StrokeKind::Center => {}
+ StrokeKind::Outer => {
+ paint.set_stroke_width(2. * paint.stroke_width());
+ }
+ }
+
+ paint
+ }
+
+ pub fn to_text_stroked_paint(
+ &self,
+ is_open: bool,
+ rect: &Rect,
+ svg_attrs: &HashMap,
+ scale: f32,
+ antialias: bool,
+ ) -> skia::Paint {
+ let mut paint = self.to_paint(rect, svg_attrs, scale, antialias);
match self.render_kind(is_open) {
StrokeKind::Inner => {
paint.set_stroke_width(2. * paint.stroke_width());
diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs
index 510406469e..764c999bef 100644
--- a/render-wasm/src/shapes/text.rs
+++ b/render-wasm/src/shapes/text.rs
@@ -1,15 +1,20 @@
+use crate::with_state;
+use crate::STATE;
+
use crate::{
math::Rect,
render::{default_font, DEFAULT_EMOJI_FONT},
};
use skia_safe::{
self as skia,
- paint::Paint,
- textlayout::{FontCollection, ParagraphBuilder, ParagraphStyle},
+ textlayout::{Paragraph as SkiaParagraph, ParagraphBuilder, ParagraphStyle},
+ Point, TextBlob,
};
+use crate::skia::FontMetrics;
+
use super::FontFamily;
-use crate::shapes::{self, merge_fills, set_paint_fill, Stroke, StrokeKind};
+use crate::shapes::{self, merge_fills};
use crate::utils::uuid_from_u32;
use crate::wasm::fills::parse_fills_from_bytes;
use crate::Uuid;
@@ -39,15 +44,11 @@ pub struct TextContent {
grow_type: GrowType,
}
-pub fn set_paragraphs_width(width: f32, paragraphs: &mut Vec>) {
- for group in paragraphs {
- for paragraph in group {
- // We first set max so we can get the min_intrinsic_width (this is the min word size)
- // then after we set either the real with or the min.
- // This is done this way so the words are not break into lines.
- paragraph.layout(f32::MAX);
- paragraph.layout(f32::max(width, paragraph.min_intrinsic_width().ceil()));
- }
+pub fn set_paragraphs_width(width: f32, paragraphs: &mut [ParagraphBuilder]) {
+ for paragraph_builder in paragraphs {
+ let mut paragraph = paragraph_builder.build();
+ paragraph.layout(f32::MAX);
+ paragraph.layout(f32::max(width, paragraph.min_intrinsic_width().ceil()));
}
}
@@ -93,93 +94,225 @@ impl TextContent {
self.paragraphs.push(paragraph);
}
- pub fn to_paragraphs(&self, fonts: &FontCollection) -> Vec> {
- let mut paragraph_group = Vec::new();
- let paragraphs = self
- .paragraphs
- .iter()
- .map(|p| {
- let paragraph_style = p.paragraph_to_style();
- let mut builder = ParagraphBuilder::new(¶graph_style, fonts);
- for leaf in &p.children {
- let text_style = leaf.to_style(p, &self.bounds); // FIXME
- let text = leaf.apply_text_transform(p.text_transform);
- builder.push_style(&text_style);
- builder.add_text(&text);
- builder.pop();
- }
- builder.build()
- })
- .collect();
- paragraph_group.push(paragraphs);
- paragraph_group
+ pub fn to_paragraphs(&self) -> Vec {
+ with_state!(state, {
+ let fonts = state.render_state.fonts().font_collection();
+
+ self.paragraphs
+ .iter()
+ .map(|p| {
+ let paragraph_style = p.paragraph_to_style();
+ let mut builder = ParagraphBuilder::new(¶graph_style, fonts);
+ for leaf in &p.children {
+ let text_style = leaf.to_style(p, &self.bounds); // FIXME
+ let text = leaf.apply_text_transform(p.text_transform);
+ builder.push_style(&text_style);
+ builder.add_text(&text);
+ builder.pop();
+ }
+ builder
+ })
+ .collect()
+ })
}
- pub fn to_stroke_paragraphs(
- &self,
- stroke: &Stroke,
- bounds: &Rect,
- fonts: &FontCollection,
- ) -> Vec> {
- let mut paragraph_group = Vec::new();
- let stroke_paints = get_text_stroke_paints(stroke, bounds);
+ pub fn get_skia_paragraphs(&self) -> Vec {
+ let mut paragraphs = self.to_paragraphs();
+ self.collect_paragraphs(&mut paragraphs);
+ paragraphs
+ }
+ pub fn grow_type(&self) -> GrowType {
+ self.grow_type
+ }
+ pub fn set_grow_type(&mut self, grow_type: GrowType) {
+ self.grow_type = grow_type;
+ }
- for stroke_paint in stroke_paints {
- let mut stroke_paragraphs = Vec::new();
- for paragraph in &self.paragraphs {
- let paragraph_style = paragraph.paragraph_to_style();
- let mut builder = ParagraphBuilder::new(¶graph_style, fonts);
- for leaf in ¶graph.children {
- let stroke_style = leaf.to_stroke_style(paragraph, &stroke_paint);
- let text: String = leaf.apply_text_transform(paragraph.text_transform);
- builder.push_style(&stroke_style);
- builder.add_text(&text);
- builder.pop();
+ pub fn get_paths(&self, antialias: bool) -> Vec<(skia::Path, skia::Paint)> {
+ let mut paths = Vec::new();
+
+ let mut offset_y = self.bounds.y();
+ let mut paragraphs = self.get_skia_paragraphs();
+ for paragraph_builder in paragraphs.iter_mut() {
+ // 1. Get paragraph and set the width layout
+ let mut skia_paragraph = paragraph_builder.build();
+ let text = paragraph_builder.get_text();
+ let paragraph_width = self.bounds.width();
+ skia_paragraph.layout(paragraph_width);
+
+ let mut line_offset_y = offset_y;
+
+ // 2. Iterate through each line in the paragraph
+ for line_metrics in skia_paragraph.get_line_metrics() {
+ let line_baseline = line_metrics.baseline as f32;
+ let start = line_metrics.start_index;
+ let end = line_metrics.end_index;
+
+ // 3. Get styles present in line for each text leaf
+ let style_metrics = line_metrics.get_style_metrics(start..end);
+
+ let mut offset_x = 0.0;
+
+ for (i, (start_index, style_metric)) in style_metrics.iter().enumerate() {
+ let end_index = style_metrics.get(i + 1).map_or(end, |next| next.0);
+
+ let start_byte = text
+ .char_indices()
+ .nth(*start_index)
+ .map(|(i, _)| i)
+ .unwrap_or(0);
+ let end_byte = text
+ .char_indices()
+ .nth(end_index)
+ .map(|(i, _)| i)
+ .unwrap_or(text.len());
+
+ let leaf_text = &text[start_byte..end_byte];
+
+ let font = skia_paragraph.get_font_at(*start_index);
+
+ let blob_offset_x = self.bounds.x() + line_metrics.left as f32 + offset_x;
+ let blob_offset_y = line_offset_y;
+
+ // 4. Get the path for each text leaf
+ if let Some((text_path, paint)) = self.generate_text_path(
+ leaf_text,
+ &font,
+ blob_offset_x,
+ blob_offset_y,
+ style_metric,
+ antialias,
+ ) {
+ let text_width = font.measure_text(leaf_text, None).0;
+ offset_x += text_width;
+ paths.push((text_path, paint));
+ }
}
- let p = builder.build();
- stroke_paragraphs.push(p);
+ line_offset_y = offset_y + line_baseline;
}
- paragraph_group.push(stroke_paragraphs);
+ offset_y += skia_paragraph.height();
}
- paragraph_group
+ paths
}
- pub fn collect_paragraphs(
+ fn generate_text_path(
&self,
- mut paragraphs: Vec>,
- ) -> Vec> {
- if self.grow_type() == GrowType::AutoWidth {
- set_paragraphs_width(f32::MAX, &mut paragraphs);
- let max_width = auto_width(¶graphs).ceil();
- set_paragraphs_width(max_width, &mut paragraphs);
+ leaf_text: &str,
+ font: &skia::Font,
+ blob_offset_x: f32,
+ blob_offset_y: f32,
+ style_metric: &skia::textlayout::StyleMetrics,
+ antialias: bool,
+ ) -> Option<(skia::Path, skia::Paint)> {
+ // Convert text to path, including text decoration
+ if let Some((text_blob_path, text_blob_bounds)) =
+ Self::get_text_blob_path(leaf_text, font, blob_offset_x, blob_offset_y)
+ {
+ let mut text_path = text_blob_path.clone();
+ let text_width = font.measure_text(leaf_text, None).0;
+
+ let decoration = style_metric.text_style.decoration();
+ let font_metrics = style_metric.font_metrics;
+
+ let blob_left = blob_offset_x;
+ let blob_top = blob_offset_y;
+ let blob_height = text_blob_bounds.height();
+
+ if let Some(decoration_rect) = self.calculate_text_decoration_rect(
+ decoration.ty,
+ font_metrics,
+ blob_left,
+ blob_top,
+ text_width,
+ blob_height,
+ ) {
+ text_path.add_rect(decoration_rect, None);
+ }
+
+ let mut paint = style_metric.text_style.foreground();
+ paint.set_anti_alias(antialias);
+
+ return Some((text_path, paint));
} else {
- set_paragraphs_width(self.width(), &mut paragraphs);
+ eprintln!("Failed to generate path for text.");
+ }
+ None
+ }
+
+ fn collect_paragraphs<'a>(
+ &self,
+ paragraphs: &'a mut Vec,
+ ) -> &'a mut Vec {
+ match self.grow_type() {
+ GrowType::AutoWidth => {
+ set_paragraphs_width(f32::MAX, paragraphs);
+ let max_width = auto_width(paragraphs).ceil();
+ set_paragraphs_width(max_width, paragraphs);
+ }
+ _ => {
+ set_paragraphs_width(self.width(), paragraphs);
+ }
}
paragraphs
}
- pub fn get_skia_paragraphs(
+ fn calculate_text_decoration_rect(
&self,
- fonts: &FontCollection,
- ) -> Vec> {
- self.collect_paragraphs(self.to_paragraphs(fonts))
+ decoration: skia::textlayout::TextDecoration,
+ font_metrics: FontMetrics,
+ blob_left: f32,
+ blob_offset_y: f32,
+ text_width: f32,
+ blob_height: f32,
+ ) -> Option {
+ match decoration {
+ skia::textlayout::TextDecoration::LINE_THROUGH => {
+ let underline_thickness = font_metrics.underline_thickness().unwrap_or(0.0);
+ let underline_position = blob_height / 2.0;
+ Some(Rect::new(
+ blob_left,
+ blob_offset_y + underline_position - underline_thickness / 2.0,
+ blob_left + text_width,
+ blob_offset_y + underline_position + underline_thickness / 2.0,
+ ))
+ }
+ skia::textlayout::TextDecoration::UNDERLINE => {
+ let underline_thickness = font_metrics.underline_thickness().unwrap_or(0.0);
+ let underline_position = blob_height - underline_thickness;
+ Some(Rect::new(
+ blob_left,
+ blob_offset_y + underline_position - underline_thickness / 2.0,
+ blob_left + text_width,
+ blob_offset_y + underline_position + underline_thickness / 2.0,
+ ))
+ }
+ _ => None,
+ }
}
- pub fn get_skia_stroke_paragraphs(
- &self,
- stroke: &Stroke,
- bounds: &Rect,
- fonts: &FontCollection,
- ) -> Vec> {
- self.collect_paragraphs(self.to_stroke_paragraphs(stroke, bounds, fonts))
- }
+ fn get_text_blob_path(
+ leaf_text: &str,
+ font: &skia::Font,
+ blob_offset_x: f32,
+ blob_offset_y: f32,
+ ) -> Option<(skia::Path, skia::Rect)> {
+ with_state!(state, {
+ let utf16_text = leaf_text.encode_utf16().collect::>();
+ let text = unsafe { skia_safe::as_utf16_unchecked(&utf16_text) };
+ let emoji_font = state.render_state.fonts().get_emoji_font(font.size());
+ let use_font = emoji_font.as_ref().unwrap_or(font);
- pub fn grow_type(&self) -> GrowType {
- self.grow_type
- }
+ if let Some(mut text_blob) = TextBlob::from_text(text, use_font) {
+ let path = SkiaParagraph::get_path(&mut text_blob);
+ let d = Point::new(blob_offset_x, blob_offset_y);
+ let offset_path = path.with_offset(d);
+ let bounds = text_blob.bounds();
+ return Some((offset_path, *bounds));
+ }
+ });
- pub fn set_grow_type(&mut self, grow_type: GrowType) {
- self.grow_type = grow_type;
+ eprintln!("Failed to create TextBlob for text.");
+ None
}
}
@@ -197,8 +330,8 @@ impl Default for TextContent {
pub struct Paragraph {
num_leaves: u32,
text_align: u8,
- text_decoration: u8,
text_direction: u8,
+ text_decoration: u8,
text_transform: u8,
line_height: f32,
letter_spacing: f32,
@@ -212,8 +345,8 @@ impl Default for Paragraph {
Self {
num_leaves: 0,
text_align: 0,
- text_decoration: 0,
text_direction: 0,
+ text_decoration: 0,
text_transform: 0,
line_height: 1.0,
letter_spacing: 0.0,
@@ -229,8 +362,8 @@ impl Paragraph {
pub fn new(
num_leaves: u32,
text_align: u8,
- text_decoration: u8,
text_direction: u8,
+ text_decoration: u8,
text_transform: u8,
line_height: f32,
letter_spacing: f32,
@@ -241,8 +374,8 @@ impl Paragraph {
Self {
num_leaves,
text_align,
- text_decoration,
text_direction,
+ text_decoration,
text_transform,
line_height,
letter_spacing,
@@ -345,7 +478,6 @@ impl TextLeaf {
3 => skia::textlayout::TextDecoration::OVERLINE,
_ => skia::textlayout::TextDecoration::NO_DECORATION,
});
-
style.set_font_families(&[
self.serialized_font_family(),
default_font(),
@@ -355,16 +487,6 @@ impl TextLeaf {
style
}
- pub fn to_stroke_style(
- &self,
- paragraph: &Paragraph,
- stroke_paint: &Paint,
- ) -> skia::textlayout::TextStyle {
- let mut style = self.to_style(paragraph, &Rect::default());
- style.set_foreground_paint(stroke_paint);
- style
- }
-
fn serialized_font_family(&self) -> String {
format!("{}", self.font_family)
}
@@ -445,7 +567,6 @@ impl From<&[u8]> for RawTextLeafData {
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");
@@ -588,66 +709,18 @@ impl From<&Vec> for RawTextData {
}
}
-pub fn auto_width(paragraphs: &[Vec]) -> f32 {
- paragraphs.iter().flatten().fold(0.0, |auto_width, p| {
- f32::max(p.max_intrinsic_width(), auto_width)
+pub fn auto_width(paragraphs: &mut [ParagraphBuilder]) -> f32 {
+ paragraphs.iter_mut().fold(0.0, |auto_width, p| {
+ let mut paragraph = p.build();
+ paragraph.layout(f32::MAX);
+ f32::max(paragraph.max_intrinsic_width(), auto_width)
})
}
-pub fn auto_height(paragraphs: &[Vec]) -> f32 {
- paragraphs
- .iter()
- .flatten()
- .fold(0.0, |auto_height, p| auto_height + p.height())
-}
-
-fn get_text_stroke_paints(stroke: &Stroke, bounds: &Rect) -> Vec {
- let mut paints = Vec::new();
-
- match stroke.kind {
- StrokeKind::Inner => {
- let mut paint = skia::Paint::default();
- paint.set_blend_mode(skia::BlendMode::DstOver);
- paint.set_anti_alias(true);
- paints.push(paint);
-
- let mut paint = skia::Paint::default();
- paint.set_style(skia::PaintStyle::Stroke);
- paint.set_blend_mode(skia::BlendMode::SrcATop);
- paint.set_anti_alias(true);
- paint.set_stroke_width(stroke.width * 2.0);
-
- set_paint_fill(&mut paint, &stroke.fill, bounds);
-
- paints.push(paint);
- }
- StrokeKind::Center => {
- let mut paint = skia::Paint::default();
- paint.set_style(skia::PaintStyle::Stroke);
- paint.set_anti_alias(true);
- paint.set_stroke_width(stroke.width);
-
- set_paint_fill(&mut paint, &stroke.fill, bounds);
-
- paints.push(paint);
- }
- StrokeKind::Outer => {
- let mut paint = skia::Paint::default();
- paint.set_style(skia::PaintStyle::Stroke);
- paint.set_blend_mode(skia::BlendMode::DstOver);
- paint.set_anti_alias(true);
- paint.set_stroke_width(stroke.width * 2.0);
-
- set_paint_fill(&mut paint, &stroke.fill, bounds);
-
- paints.push(paint);
-
- let mut paint = skia::Paint::default();
- paint.set_blend_mode(skia::BlendMode::Clear);
- paint.set_anti_alias(true);
- paints.push(paint);
- }
- }
-
- paints
+pub fn auto_height(paragraphs: &mut [ParagraphBuilder]) -> f32 {
+ paragraphs.iter_mut().fold(0.0, |auto_height, p| {
+ let mut paragraph = p.build();
+ paragraph.layout(f32::MAX);
+ auto_height + paragraph.height()
+ })
}
diff --git a/render-wasm/src/wasm/text.rs b/render-wasm/src/wasm/text.rs
index 7642216daa..3e2822164e 100644
--- a/render-wasm/src/wasm/text.rs
+++ b/render-wasm/src/wasm/text.rs
@@ -1,8 +1,8 @@
use crate::mem;
use crate::shapes::{auto_height, auto_width, GrowType, RawTextData, Type};
+use crate::with_current_shape;
use crate::STATE;
-use crate::{with_current_shape, with_state};
#[no_mangle]
pub extern "C" fn clear_shape_text() {
@@ -35,11 +35,6 @@ pub extern "C" fn set_shape_grow_type(grow_type: u8) {
#[no_mangle]
pub extern "C" fn get_text_dimensions() -> *mut u8 {
- let font_col;
- with_state!(state, {
- font_col = state.render_state.fonts.font_collection();
- });
-
let mut width = 0.01;
let mut height = 0.01;
with_current_shape!(state, |shape: &mut Shape| {
@@ -47,10 +42,10 @@ pub extern "C" fn get_text_dimensions() -> *mut u8 {
height = shape.selrect.height();
if let Type::Text(content) = &shape.shape_type {
- let paragraphs = content.get_skia_paragraphs(font_col);
- height = auto_height(¶graphs).ceil();
+ let mut paragraphs = content.to_paragraphs();
+ height = auto_height(&mut paragraphs).ceil();
if content.grow_type() == GrowType::AutoWidth {
- width = auto_width(¶graphs).ceil();
+ width = auto_width(&mut paragraphs).ceil();
}
}
});