diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 51f53c7bb..17679db57 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -657,6 +657,38 @@ (h/call internal-module "_add_shape_shadow" rgba blur spread x y (translate-shadow-style style) hidden) (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 [zoom 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? corners) (set-shape-corners corners)) (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) (set-layout-child shape)) diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index a40c867d6..6a427d7ef 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -8,14 +8,15 @@ mod shapes; mod state; mod utils; mod view; +mod wasm; use crate::mem::SerializableResult; use crate::shapes::{BoolType, ConstraintH, ConstraintV, TransformEntry, Type}; -use crate::state::State; use crate::utils::uuid_from_u32_quartet; +use state::State; -static mut STATE: Option> = None; +pub(crate) static mut STATE: Option> = None; extern "C" { fn emscripten_GetProcAddress( diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index e2e4a3be5..9c5e69611 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -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 uuid::Uuid; @@ -14,6 +14,7 @@ mod options; mod shadows; mod strokes; mod surfaces; +mod text; use crate::shapes::{Corners, Shape, Type}; use cache::CachedSurfaceImage; @@ -56,7 +57,10 @@ pub(crate) struct RenderState { gpu_state: GpuState, pub options: RenderOptions, pub surfaces: Surfaces, + // TODO: we should probably have only one of these pub font_provider: skia::textlayout::TypefaceFontProvider, + pub font_collection: skia::textlayout::FontCollection, + // ---- pub cached_surface_image: Option, pub viewbox: Viewbox, pub images: ImageStore, @@ -84,6 +88,10 @@ impl RenderState { .new_from_data(DEFAULT_FONT_BYTES, None) .expect("Failed to load font"); 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 // time we reuse this one. @@ -93,6 +101,7 @@ impl RenderState { surfaces, cached_surface_image: None, font_provider, + font_collection, options: RenderOptions::default(), viewbox: Viewbox::new(width as f32, height as f32), images: ImageStore::new(), @@ -328,6 +337,9 @@ impl RenderState { } } } + Type::Text(text_content) => { + text::render(self, text_content); + } _ => { self.surfaces .apply_mut(&[SurfaceId::Fills, SurfaceId::Strokes], |s| { diff --git a/render-wasm/src/render/fills.rs b/render-wasm/src/render/fills.rs index 78e295c81..781531d91 100644 --- a/render-wasm/src/render/fills.rs +++ b/render-wasm/src/render/fills.rs @@ -77,10 +77,8 @@ fn draw_image_fill_in_container( Type::SVGRaw(_) => { canvas.clip_rect(container, skia::ClipOp::Intersect, true); } - Type::Text => { - // TODO: Text fill - } Type::Group(_) => unreachable!("A group should not have fills"), + Type::Text(_) => unimplemented!("TODO"), } // Draw the image with the calculated destination rectangle diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs new file mode 100644 index 000000000..563775f9c --- /dev/null +++ b/render-wasm/src/render/text.rs @@ -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(); + } +} diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 40420a975..02dbcb710 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -18,6 +18,7 @@ mod rects; mod shadows; mod strokes; mod svgraw; +mod text; mod transform; pub use blurs::*; @@ -33,6 +34,7 @@ pub use rects::*; pub use shadows::*; pub use strokes::*; pub use svgraw::*; +pub use text::*; pub use transform::*; use crate::math; @@ -45,9 +47,9 @@ pub enum Type { Bool(Bool), Rect(Rect), Path(Path), - Text, Circle, SVGRaw(SVGRaw), + Text(TextContent), } impl Type { @@ -58,7 +60,7 @@ impl Type { 2 => Type::Bool(Bool::default()), 3 => Type::Rect(Rect::default()), 4 => Type::Path(Path::default()), - 5 => Type::Text, + 5 => Type::Text(TextContent::default()), 6 => Type::Circle, 7 => Type::SVGRaw(SVGRaw::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) { 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) { @@ -343,7 +351,7 @@ impl Shape { } pub fn add_fill(&mut self, f: Fill) { - self.fills.push(f) + self.fills.push(f); } 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) { let mut center = self.selrect.center(); center = transform.map_point(center); diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs new file mode 100644 index 000000000..69abfd4ed --- /dev/null +++ b/render-wasm/src/shapes/text.rs @@ -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, + 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 { + 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, +} + +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 + } +} diff --git a/render-wasm/src/wasm.rs b/render-wasm/src/wasm.rs new file mode 100644 index 000000000..481c63acc --- /dev/null +++ b/render-wasm/src/wasm.rs @@ -0,0 +1 @@ +pub mod text; diff --git a/render-wasm/src/wasm/text.rs b/render-wasm/src/wasm/text.rs new file mode 100644 index 000000000..57762a5c5 --- /dev/null +++ b/render-wasm/src/wasm/text.rs @@ -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); + } + } +}