🎉 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:
Belén Albeza 2025-03-04 11:54:52 +01:00 committed by GitHub
parent 9e5de82967
commit aa468e2153
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 256 additions and 9 deletions

View file

@ -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))

View file

@ -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<Box<State>> = None;
pub(crate) static mut STATE: Option<Box<State>> = None;
extern "C" {
fn emscripten_GetProcAddress(

View file

@ -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<CachedSurfaceImage>,
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| {

View file

@ -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

View 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();
}
}

View file

@ -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);

View 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(&paragraph_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
View file

@ -0,0 +1 @@
pub mod text;

View 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);
}
}
}