mirror of
https://github.com/penpot/penpot.git
synced 2025-05-16 18:56:10 +02:00
🎉 Render plain text
* 🎉 Serialize text content (wasm) * ♻️ Refactor functions in main to wasm module * 🎉 Stub rendering of paragraph text (wasm) * 📎 Clean up commented code
This commit is contained in:
parent
9e5de82967
commit
aa468e2153
9 changed files with 256 additions and 9 deletions
|
@ -657,6 +657,38 @@
|
||||||
(h/call internal-module "_add_shape_shadow" rgba blur spread x y (translate-shadow-style style) hidden)
|
(h/call internal-module "_add_shape_shadow" rgba blur spread x y (translate-shadow-style style) hidden)
|
||||||
(recur (inc index)))))))
|
(recur (inc index)))))))
|
||||||
|
|
||||||
|
(defn utf8->buffer [text]
|
||||||
|
(let [encoder (js/TextEncoder.)]
|
||||||
|
(.encode encoder text)))
|
||||||
|
|
||||||
|
(defn set-shape-text-content [content]
|
||||||
|
(h/call internal-module "_clear_shape_text")
|
||||||
|
(let [paragraph-set (first (dm/get-prop content :children))
|
||||||
|
paragraphs (dm/get-prop paragraph-set :children)
|
||||||
|
total-paragraphs (count paragraphs)]
|
||||||
|
|
||||||
|
(loop [index 0]
|
||||||
|
(when (< index total-paragraphs)
|
||||||
|
(let [paragraph (nth paragraphs index)
|
||||||
|
leaves (dm/get-prop paragraph :children)
|
||||||
|
total-leaves (count leaves)]
|
||||||
|
(h/call internal-module "_add_text_paragraph")
|
||||||
|
(loop [index-leaves 0]
|
||||||
|
(when (< index-leaves total-leaves)
|
||||||
|
(let [leaf (nth leaves index-leaves)
|
||||||
|
text (dm/get-prop leaf :text)
|
||||||
|
buffer (utf8->buffer text)
|
||||||
|
;; set up buffer array from
|
||||||
|
size (.-byteLength buffer)
|
||||||
|
ptr (h/call internal-module "_alloc_bytes" size)
|
||||||
|
heap (gobj/get ^js internal-module "HEAPU8")
|
||||||
|
mem (js/Uint8Array. (.-buffer heap) ptr size)]
|
||||||
|
|
||||||
|
(.set mem buffer)
|
||||||
|
(h/call internal-module "_add_text_leaf")
|
||||||
|
(recur (inc index-leaves))))))
|
||||||
|
(recur (inc index))))))
|
||||||
|
|
||||||
(defn set-view-box
|
(defn set-view-box
|
||||||
[zoom vbox]
|
[zoom vbox]
|
||||||
(h/call internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox)))
|
(h/call internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox)))
|
||||||
|
@ -727,6 +759,8 @@
|
||||||
(when (some? bool-content) (set-shape-bool-content bool-content))
|
(when (some? bool-content) (set-shape-bool-content bool-content))
|
||||||
(when (some? corners) (set-shape-corners corners))
|
(when (some? corners) (set-shape-corners corners))
|
||||||
(when (some? shadows) (set-shape-shadows shadows))
|
(when (some? shadows) (set-shape-shadows shadows))
|
||||||
|
(when (and (= type :text) (some? content))
|
||||||
|
(set-shape-text-content content))
|
||||||
|
|
||||||
(when (ctl/any-layout-immediate-child? objects shape)
|
(when (ctl/any-layout-immediate-child? objects shape)
|
||||||
(set-layout-child shape))
|
(set-layout-child shape))
|
||||||
|
|
|
@ -8,14 +8,15 @@ mod shapes;
|
||||||
mod state;
|
mod state;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod view;
|
mod view;
|
||||||
|
mod wasm;
|
||||||
|
|
||||||
use crate::mem::SerializableResult;
|
use crate::mem::SerializableResult;
|
||||||
use crate::shapes::{BoolType, ConstraintH, ConstraintV, TransformEntry, Type};
|
use crate::shapes::{BoolType, ConstraintH, ConstraintV, TransformEntry, Type};
|
||||||
|
|
||||||
use crate::state::State;
|
|
||||||
use crate::utils::uuid_from_u32_quartet;
|
use crate::utils::uuid_from_u32_quartet;
|
||||||
|
use state::State;
|
||||||
|
|
||||||
static mut STATE: Option<Box<State>> = None;
|
pub(crate) static mut STATE: Option<Box<State>> = None;
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
fn emscripten_GetProcAddress(
|
fn emscripten_GetProcAddress(
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use skia_safe::{self as skia, Contains, Matrix, RRect, Rect};
|
use skia_safe::{self as skia, Contains, FontMgr, Matrix, RRect, Rect};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ mod options;
|
||||||
mod shadows;
|
mod shadows;
|
||||||
mod strokes;
|
mod strokes;
|
||||||
mod surfaces;
|
mod surfaces;
|
||||||
|
mod text;
|
||||||
|
|
||||||
use crate::shapes::{Corners, Shape, Type};
|
use crate::shapes::{Corners, Shape, Type};
|
||||||
use cache::CachedSurfaceImage;
|
use cache::CachedSurfaceImage;
|
||||||
|
@ -56,7 +57,10 @@ pub(crate) struct RenderState {
|
||||||
gpu_state: GpuState,
|
gpu_state: GpuState,
|
||||||
pub options: RenderOptions,
|
pub options: RenderOptions,
|
||||||
pub surfaces: Surfaces,
|
pub surfaces: Surfaces,
|
||||||
|
// TODO: we should probably have only one of these
|
||||||
pub font_provider: skia::textlayout::TypefaceFontProvider,
|
pub font_provider: skia::textlayout::TypefaceFontProvider,
|
||||||
|
pub font_collection: skia::textlayout::FontCollection,
|
||||||
|
// ----
|
||||||
pub cached_surface_image: Option<CachedSurfaceImage>,
|
pub cached_surface_image: Option<CachedSurfaceImage>,
|
||||||
pub viewbox: Viewbox,
|
pub viewbox: Viewbox,
|
||||||
pub images: ImageStore,
|
pub images: ImageStore,
|
||||||
|
@ -84,6 +88,10 @@ impl RenderState {
|
||||||
.new_from_data(DEFAULT_FONT_BYTES, None)
|
.new_from_data(DEFAULT_FONT_BYTES, None)
|
||||||
.expect("Failed to load font");
|
.expect("Failed to load font");
|
||||||
font_provider.register_typeface(default_font, "robotomono-regular");
|
font_provider.register_typeface(default_font, "robotomono-regular");
|
||||||
|
let mut font_collection = skia::textlayout::FontCollection::new();
|
||||||
|
let font_manager = FontMgr::from(font_provider.clone());
|
||||||
|
font_collection.set_default_font_manager(FontMgr::default(), None);
|
||||||
|
font_collection.set_dynamic_font_manager(font_manager);
|
||||||
|
|
||||||
// This is used multiple times everywhere so instead of creating new instances every
|
// This is used multiple times everywhere so instead of creating new instances every
|
||||||
// time we reuse this one.
|
// time we reuse this one.
|
||||||
|
@ -93,6 +101,7 @@ impl RenderState {
|
||||||
surfaces,
|
surfaces,
|
||||||
cached_surface_image: None,
|
cached_surface_image: None,
|
||||||
font_provider,
|
font_provider,
|
||||||
|
font_collection,
|
||||||
options: RenderOptions::default(),
|
options: RenderOptions::default(),
|
||||||
viewbox: Viewbox::new(width as f32, height as f32),
|
viewbox: Viewbox::new(width as f32, height as f32),
|
||||||
images: ImageStore::new(),
|
images: ImageStore::new(),
|
||||||
|
@ -328,6 +337,9 @@ impl RenderState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Type::Text(text_content) => {
|
||||||
|
text::render(self, text_content);
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
self.surfaces
|
self.surfaces
|
||||||
.apply_mut(&[SurfaceId::Fills, SurfaceId::Strokes], |s| {
|
.apply_mut(&[SurfaceId::Fills, SurfaceId::Strokes], |s| {
|
||||||
|
|
|
@ -77,10 +77,8 @@ fn draw_image_fill_in_container(
|
||||||
Type::SVGRaw(_) => {
|
Type::SVGRaw(_) => {
|
||||||
canvas.clip_rect(container, skia::ClipOp::Intersect, true);
|
canvas.clip_rect(container, skia::ClipOp::Intersect, true);
|
||||||
}
|
}
|
||||||
Type::Text => {
|
|
||||||
// TODO: Text fill
|
|
||||||
}
|
|
||||||
Type::Group(_) => unreachable!("A group should not have fills"),
|
Type::Group(_) => unreachable!("A group should not have fills"),
|
||||||
|
Type::Text(_) => unimplemented!("TODO"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw the image with the calculated destination rectangle
|
// Draw the image with the calculated destination rectangle
|
||||||
|
|
12
render-wasm/src/render/text.rs
Normal file
12
render-wasm/src/render/text.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
use super::{RenderState, SurfaceId};
|
||||||
|
use crate::shapes::TextContent;
|
||||||
|
|
||||||
|
pub fn render(render_state: &mut RenderState, text: &TextContent) {
|
||||||
|
let mut offset_y = 0.0;
|
||||||
|
for mut skia_paragraph in text.to_paragraphs(&render_state.font_collection) {
|
||||||
|
skia_paragraph.layout(text.width());
|
||||||
|
let xy = (text.x(), text.y() + offset_y);
|
||||||
|
skia_paragraph.paint(render_state.surfaces.canvas(SurfaceId::Fills), xy);
|
||||||
|
offset_y += skia_paragraph.height();
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ mod rects;
|
||||||
mod shadows;
|
mod shadows;
|
||||||
mod strokes;
|
mod strokes;
|
||||||
mod svgraw;
|
mod svgraw;
|
||||||
|
mod text;
|
||||||
mod transform;
|
mod transform;
|
||||||
|
|
||||||
pub use blurs::*;
|
pub use blurs::*;
|
||||||
|
@ -33,6 +34,7 @@ pub use rects::*;
|
||||||
pub use shadows::*;
|
pub use shadows::*;
|
||||||
pub use strokes::*;
|
pub use strokes::*;
|
||||||
pub use svgraw::*;
|
pub use svgraw::*;
|
||||||
|
pub use text::*;
|
||||||
pub use transform::*;
|
pub use transform::*;
|
||||||
|
|
||||||
use crate::math;
|
use crate::math;
|
||||||
|
@ -45,9 +47,9 @@ pub enum Type {
|
||||||
Bool(Bool),
|
Bool(Bool),
|
||||||
Rect(Rect),
|
Rect(Rect),
|
||||||
Path(Path),
|
Path(Path),
|
||||||
Text,
|
|
||||||
Circle,
|
Circle,
|
||||||
SVGRaw(SVGRaw),
|
SVGRaw(SVGRaw),
|
||||||
|
Text(TextContent),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Type {
|
impl Type {
|
||||||
|
@ -58,7 +60,7 @@ impl Type {
|
||||||
2 => Type::Bool(Bool::default()),
|
2 => Type::Bool(Bool::default()),
|
||||||
3 => Type::Rect(Rect::default()),
|
3 => Type::Rect(Rect::default()),
|
||||||
4 => Type::Path(Path::default()),
|
4 => Type::Path(Path::default()),
|
||||||
5 => Type::Text,
|
5 => Type::Text(TextContent::default()),
|
||||||
6 => Type::Circle,
|
6 => Type::Circle,
|
||||||
7 => Type::SVGRaw(SVGRaw::default()),
|
7 => Type::SVGRaw(SVGRaw::default()),
|
||||||
_ => Type::Rect(Rect::default()),
|
_ => Type::Rect(Rect::default()),
|
||||||
|
@ -207,6 +209,12 @@ impl Shape {
|
||||||
|
|
||||||
pub fn set_selrect(&mut self, left: f32, top: f32, right: f32, bottom: f32) {
|
pub fn set_selrect(&mut self, left: f32, top: f32, right: f32, bottom: f32) {
|
||||||
self.selrect.set_ltrb(left, top, right, bottom);
|
self.selrect.set_ltrb(left, top, right, bottom);
|
||||||
|
match self.shape_type {
|
||||||
|
Type::Text(ref mut text) => {
|
||||||
|
text.set_xywh(left, top, right - left, bottom - top);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_masked(&mut self, masked: bool) {
|
pub fn set_masked(&mut self, masked: bool) {
|
||||||
|
@ -343,7 +351,7 @@ impl Shape {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_fill(&mut self, f: Fill) {
|
pub fn add_fill(&mut self, f: Fill) {
|
||||||
self.fills.push(f)
|
self.fills.push(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_fills(&mut self) {
|
pub fn clear_fills(&mut self) {
|
||||||
|
@ -580,6 +588,36 @@ impl Shape {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_text_leaf(&mut self, text_str: &str) -> Result<(), String> {
|
||||||
|
match self.shape_type {
|
||||||
|
Type::Text(ref mut text) => {
|
||||||
|
text.add_leaf(text_str)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
_ => Err("Shape is not a text".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_text_paragraph(&mut self) -> Result<(), String> {
|
||||||
|
match self.shape_type {
|
||||||
|
Type::Text(ref mut text) => {
|
||||||
|
text.add_paragraph();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
_ => Err("Shape is not a text".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_text(&mut self) {
|
||||||
|
match self.shape_type {
|
||||||
|
Type::Text(_) => {
|
||||||
|
let new_text_content = TextContent::new(self.selrect);
|
||||||
|
self.shape_type = Type::Text(new_text_content);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn transform_selrect(&mut self, transform: &Matrix) {
|
fn transform_selrect(&mut self, transform: &Matrix) {
|
||||||
let mut center = self.selrect.center();
|
let mut center = self.selrect.center();
|
||||||
center = transform.map_point(center);
|
center = transform.map_point(center);
|
||||||
|
|
114
render-wasm/src/shapes/text.rs
Normal file
114
render-wasm/src/shapes/text.rs
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
use crate::math::Rect;
|
||||||
|
use skia_safe::{
|
||||||
|
self as skia,
|
||||||
|
textlayout::{FontCollection, ParagraphBuilder},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub struct TextContent {
|
||||||
|
paragraphs: Vec<Paragraph>,
|
||||||
|
bounds: Rect,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextContent {
|
||||||
|
pub fn new(bounds: Rect) -> Self {
|
||||||
|
let mut res = Self::default();
|
||||||
|
res.bounds = bounds;
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_xywh(&mut self, x: f32, y: f32, w: f32, h: f32) {
|
||||||
|
self.bounds = Rect::from_xywh(x, y, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(&self) -> f32 {
|
||||||
|
self.bounds.width()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn x(&self) -> f32 {
|
||||||
|
self.bounds.x()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn y(&self) -> f32 {
|
||||||
|
self.bounds.y()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_paragraph(&mut self) {
|
||||||
|
let p = Paragraph::default();
|
||||||
|
self.paragraphs.push(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_leaf(&mut self, text: &str) -> Result<(), String> {
|
||||||
|
let paragraph = self
|
||||||
|
.paragraphs
|
||||||
|
.last_mut()
|
||||||
|
.ok_or("No paragraph to add text leaf to")?;
|
||||||
|
|
||||||
|
paragraph.add_leaf(TextLeaf {
|
||||||
|
text: text.to_owned(),
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_paragraphs(&self, fonts: &FontCollection) -> Vec<skia::textlayout::Paragraph> {
|
||||||
|
let mut paragraph_style = skia::textlayout::ParagraphStyle::default();
|
||||||
|
// TODO: read text direction, align, etc. from the shape
|
||||||
|
paragraph_style.set_text_direction(skia::textlayout::TextDirection::LTR);
|
||||||
|
|
||||||
|
self.paragraphs
|
||||||
|
.iter()
|
||||||
|
.map(|p| {
|
||||||
|
let mut builder = ParagraphBuilder::new(¶graph_style, fonts);
|
||||||
|
for leaf in &p.children {
|
||||||
|
builder.push_style(&leaf.to_style());
|
||||||
|
builder.add_text(&leaf.text);
|
||||||
|
builder.pop();
|
||||||
|
}
|
||||||
|
builder.build()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TextContent {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
paragraphs: vec![],
|
||||||
|
bounds: Rect::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub struct Paragraph {
|
||||||
|
children: Vec<TextLeaf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Paragraph {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { children: vec![] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Paragraph {
|
||||||
|
fn add_leaf(&mut self, leaf: TextLeaf) {
|
||||||
|
self.children.push(leaf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub struct TextLeaf {
|
||||||
|
text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextLeaf {
|
||||||
|
pub fn to_style(&self) -> skia::textlayout::TextStyle {
|
||||||
|
let mut style = skia::textlayout::TextStyle::default();
|
||||||
|
// TODO: read text style info from the shape
|
||||||
|
style.set_color(skia::Color::BLACK);
|
||||||
|
style.set_font_size(16.0);
|
||||||
|
|
||||||
|
style
|
||||||
|
}
|
||||||
|
}
|
1
render-wasm/src/wasm.rs
Normal file
1
render-wasm/src/wasm.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod text;
|
37
render-wasm/src/wasm/text.rs
Normal file
37
render-wasm/src/wasm/text.rs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
use crate::mem;
|
||||||
|
use crate::STATE;
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn clear_shape_text() {
|
||||||
|
let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer");
|
||||||
|
if let Some(shape) = state.current_shape() {
|
||||||
|
shape.clear_text();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn add_text_paragraph() {
|
||||||
|
let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer");
|
||||||
|
if let Some(shape) = state.current_shape() {
|
||||||
|
let res = shape.add_text_paragraph();
|
||||||
|
if let Err(err) = res {
|
||||||
|
eprintln!("{}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn add_text_leaf() {
|
||||||
|
let bytes = mem::bytes();
|
||||||
|
let text = unsafe {
|
||||||
|
String::from_utf8_unchecked(bytes) // TODO: handle this error
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
|
||||||
|
if let Some(shape) = state.current_shape() {
|
||||||
|
let res = shape.add_text_leaf(&text);
|
||||||
|
if let Err(err) = res {
|
||||||
|
eprintln!("{}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue