mirror of
https://github.com/penpot/penpot.git
synced 2025-12-12 06:24:17 +01:00
🔧 Render strokes using paths
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
1
frontend/resources/wasm-playground/js/texts.js
Normal file
1
frontend/resources/wasm-playground/js/texts.js
Normal file
@@ -0,0 +1 @@
|
||||
// Placeholder for text-specific logic if needed in the future.
|
||||
94
frontend/resources/wasm-playground/texts.html
Normal file
94
frontend/resources/wasm-playground/texts.html
Normal file
@@ -0,0 +1,94 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>WASM + WebGL2 Texts</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import {
|
||||
init, assignCanvas, setupInteraction, useShape, setShapeChildren, addTextShape, hexToU32ARGB,getRandomInt, getRandomColor, getRandomFloat, addShapeSolidFill, addShapeSolidStroleFill
|
||||
} from './js/lib.js';
|
||||
|
||||
const canvas = document.getElementById("canvas");
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
const MIN_LINES = 1;
|
||||
const MAX_LINES = 5;
|
||||
const MIN_WORDS = 1;
|
||||
const MAX_WORDS = 10;
|
||||
|
||||
const params = new URLSearchParams(document.location.search);
|
||||
const texts = params.get("texts") || 100;
|
||||
|
||||
function getRandomText() {
|
||||
const words = ["Hello", "World", "Penpot", "Canvas", "Text", "Shape", "Random", "Line"];
|
||||
const lines = Math.floor(Math.random() * MAX_LINES) + MIN_LINES;
|
||||
let text = "";
|
||||
for (let i = 0; i < lines; i++) {
|
||||
const lineLength = Math.floor(Math.random() * MAX_WORDS) + MIN_WORDS;
|
||||
const line = Array.from({ length: lineLength }, () => words[Math.floor(Math.random() * words.length)]).join(" ");
|
||||
text += line;
|
||||
if (i < lines - 1) text += "\n";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
initWasmModule().then(Module => {
|
||||
init(Module);
|
||||
assignCanvas(canvas);
|
||||
Module._set_canvas_background(hexToU32ARGB("#FABADA", 1));
|
||||
Module._set_view(1, 0, 0);
|
||||
Module._init_shapes_pool(texts + 1);
|
||||
setupInteraction(canvas);
|
||||
|
||||
const children = [];
|
||||
for (let i = 0; i < texts; i++) {
|
||||
const uuid = crypto.randomUUID();
|
||||
children.push(uuid);
|
||||
|
||||
useShape(uuid);
|
||||
Module._set_parent(0, 0, 0, 0);
|
||||
Module._set_shape_type(5);
|
||||
|
||||
const x1 = getRandomInt(0, canvas.width);
|
||||
const y1 = getRandomInt(0, canvas.height);
|
||||
const width = getRandomInt(20, 500);
|
||||
const height = getRandomInt(20, 100);
|
||||
Module._set_shape_selrect(x1, y1, x1 + width, y1 + height);
|
||||
|
||||
const fontSize = Math.random() * 50 + 10;
|
||||
const text = getRandomText();
|
||||
addTextShape(x1, y1, fontSize, text);
|
||||
}
|
||||
|
||||
useShape("00000000-0000-0000-0000-000000000000");
|
||||
setShapeChildren(children);
|
||||
|
||||
performance.mark('render:begin');
|
||||
Module._render(Date.now());
|
||||
performance.mark('render:end');
|
||||
const { duration } = performance.measure('render', 'render:begin', 'render:end');
|
||||
console.log(`Render time: ${duration.toFixed(2)}ms`);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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| {
|
||||
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();
|
||||
|
||||
@@ -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<Font> {
|
||||
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 {
|
||||
|
||||
@@ -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<Paragraph>],
|
||||
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<Paragraph>],
|
||||
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<Paragraph>],
|
||||
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<Paragraph>],
|
||||
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),
|
||||
);
|
||||
|
||||
@@ -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<SurfaceId>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Paragraph>],
|
||||
paths: &Vec<(skia::Path, skia::Paint)>,
|
||||
surface_id: Option<SurfaceId>,
|
||||
paint: Option<skia::Paint>,
|
||||
) {
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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,47 +139,19 @@ 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
|
||||
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);
|
||||
}
|
||||
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
|
||||
paint
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_fill_shader(fill: &Fill, bounding_box: &Rect) -> Option<skia::Shader> {
|
||||
match fill {
|
||||
pub fn get_fill_shader(&self, bounding_box: &Rect) -> Option<skia::Shader> {
|
||||
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),
|
||||
@@ -187,8 +159,10 @@ pub fn get_fill_shader(fill: &Fill, bounding_box: &Rect) -> Option<skia::Shader>
|
||||
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);
|
||||
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);
|
||||
@@ -222,6 +196,7 @@ pub fn get_fill_shader(fill: &Fill, bounding_box: &Rect) -> Option<skia::Shader>
|
||||
image_shader
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge_fills(fills: &[Fill], bounding_box: Rect) -> skia::Paint {
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,8 +108,6 @@ pub fn propagate_modifiers(
|
||||
modifiers: &[TransformEntry],
|
||||
) -> (Vec<TransformEntry>, HashMap<Uuid, Bounds>) {
|
||||
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,
|
||||
|
||||
@@ -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<String, String>,
|
||||
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());
|
||||
|
||||
@@ -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,16 +44,12 @@ pub struct TextContent {
|
||||
grow_type: GrowType,
|
||||
}
|
||||
|
||||
pub fn set_paragraphs_width(width: f32, paragraphs: &mut Vec<Vec<skia::textlayout::Paragraph>>) {
|
||||
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.
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextContent {
|
||||
@@ -93,10 +94,11 @@ impl TextContent {
|
||||
self.paragraphs.push(paragraph);
|
||||
}
|
||||
|
||||
pub fn to_paragraphs(&self, fonts: &FontCollection) -> Vec<Vec<skia::textlayout::Paragraph>> {
|
||||
let mut paragraph_group = Vec::new();
|
||||
let paragraphs = self
|
||||
.paragraphs
|
||||
pub fn to_paragraphs(&self) -> Vec<ParagraphBuilder> {
|
||||
with_state!(state, {
|
||||
let fonts = state.render_state.fonts().font_collection();
|
||||
|
||||
self.paragraphs
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let paragraph_style = p.paragraph_to_style();
|
||||
@@ -108,78 +110,209 @@ impl TextContent {
|
||||
builder.add_text(&text);
|
||||
builder.pop();
|
||||
}
|
||||
builder.build()
|
||||
builder
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
paragraph_group.push(paragraphs);
|
||||
paragraph_group
|
||||
}
|
||||
|
||||
pub fn to_stroke_paragraphs(
|
||||
pub fn get_skia_paragraphs(&self) -> Vec<ParagraphBuilder> {
|
||||
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;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
line_offset_y = offset_y + line_baseline;
|
||||
}
|
||||
offset_y += skia_paragraph.height();
|
||||
}
|
||||
paths
|
||||
}
|
||||
|
||||
fn generate_text_path(
|
||||
&self,
|
||||
stroke: &Stroke,
|
||||
bounds: &Rect,
|
||||
fonts: &FontCollection,
|
||||
) -> Vec<Vec<skia::textlayout::Paragraph>> {
|
||||
let mut paragraph_group = Vec::new();
|
||||
let stroke_paints = get_text_stroke_paints(stroke, bounds);
|
||||
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;
|
||||
|
||||
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();
|
||||
}
|
||||
let p = builder.build();
|
||||
stroke_paragraphs.push(p);
|
||||
}
|
||||
paragraph_group.push(stroke_paragraphs);
|
||||
}
|
||||
paragraph_group
|
||||
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);
|
||||
}
|
||||
|
||||
pub fn collect_paragraphs(
|
||||
&self,
|
||||
mut paragraphs: Vec<Vec<skia::textlayout::Paragraph>>,
|
||||
) -> Vec<Vec<skia::textlayout::Paragraph>> {
|
||||
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);
|
||||
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<ParagraphBuilder>,
|
||||
) -> &'a mut Vec<ParagraphBuilder> {
|
||||
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<Vec<skia::textlayout::Paragraph>> {
|
||||
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<Rect> {
|
||||
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<Vec<skia::textlayout::Paragraph>> {
|
||||
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::<Vec<u16>>();
|
||||
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<u8>> for RawTextData {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn auto_width(paragraphs: &[Vec<skia::textlayout::Paragraph>]) -> 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<skia::textlayout::Paragraph>]) -> f32 {
|
||||
paragraphs
|
||||
.iter()
|
||||
.flatten()
|
||||
.fold(0.0, |auto_height, p| auto_height + p.height())
|
||||
}
|
||||
|
||||
fn get_text_stroke_paints(stroke: &Stroke, bounds: &Rect) -> Vec<Paint> {
|
||||
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()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user