From 15eee0d8d8ad033fa7d2f58f87af0af78894847b Mon Sep 17 00:00:00 2001 From: Aitor Moreno Date: Tue, 19 Aug 2025 16:10:10 +0200 Subject: [PATCH 1/2] :tada: Add Text Editor WASM Playground --- frontend/text-editor/.gitignore | 2 + frontend/text-editor/package.json | 1 + frontend/text-editor/src/wasm.html | 502 +++++++++++++++++++++++++ frontend/text-editor/src/wasm/lib.js | 541 +++++++++++++++++++++++++++ render-wasm/src/shapes/text.rs | 3 + render-wasm/src/wasm/text.rs | 1 - 6 files changed, 1049 insertions(+), 1 deletion(-) create mode 100644 frontend/text-editor/src/wasm.html create mode 100644 frontend/text-editor/src/wasm/lib.js diff --git a/frontend/text-editor/.gitignore b/frontend/text-editor/.gitignore index db3424bb9f..ad94b410e1 100644 --- a/frontend/text-editor/.gitignore +++ b/frontend/text-editor/.gitignore @@ -340,3 +340,5 @@ $RECYCLE.BIN/ /playwright/.cache/ vite.config.js.timestamp* + +render_wasm.* diff --git a/frontend/text-editor/package.json b/frontend/text-editor/package.json index 865bc5d5a1..b31fc91b7f 100644 --- a/frontend/text-editor/package.json +++ b/frontend/text-editor/package.json @@ -7,6 +7,7 @@ "scripts": { "dev": "vite", "coverage": "vitest run --coverage", + "wasm:update": "cp ../resources/public/js/render_wasm.wasm ./src/wasm/render_wasm.wasm && cp ../resources/public/js/render_wasm.js ./src/wasm/render_wasm.js", "test": "vitest --run", "test:watch": "vitest", "test:watch:ui": "vitest --ui", diff --git a/frontend/text-editor/src/wasm.html b/frontend/text-editor/src/wasm.html new file mode 100644 index 0000000000..700d38a363 --- /dev/null +++ b/frontend/text-editor/src/wasm.html @@ -0,0 +1,502 @@ + + + + + + + + + + Penpot - Text Editor Playground + + + +
+
+ Styles + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ Debug +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
+
+ +
+ + + + + diff --git a/frontend/text-editor/src/wasm/lib.js b/frontend/text-editor/src/wasm/lib.js new file mode 100644 index 0000000000..022a163bf6 --- /dev/null +++ b/frontend/text-editor/src/wasm/lib.js @@ -0,0 +1,541 @@ +let Module = null; + +let scale = 1; +let offsetX = 0; +let offsetY = 0; + +let isPanning = false; +let lastX = 0; +let lastY = 0; + +export function init(moduleInstance) { + Module = moduleInstance; +} + +export function assignCanvas(canvas) { + const glModule = Module.GL; + const context = canvas.getContext("webgl2", { + antialias: true, + depth: true, + alpha: false, + stencil: true, + preserveDrawingBuffer: true, + }); + + const handle = glModule.registerContext(context, { majorVersion: 2 }); + glModule.makeContextCurrent(handle); + context.getExtension("WEBGL_debug_renderer_info"); + + Module._init(canvas.width, canvas.height); + Module._set_render_options(0, 1); +} + +export function hexToU32ARGB(hex, opacity = 1) { + const rgb = parseInt(hex.slice(1), 16); + const a = Math.floor(opacity * 0xFF); + const argb = (a << 24) | rgb; + return argb >>> 0; +} + +export function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min)) + min; +} + +export function getRandomColor() { + const r = getRandomInt(0, 256).toString(16).padStart(2, '0'); + const g = getRandomInt(0, 256).toString(16).padStart(2, '0'); + const b = getRandomInt(0, 256).toString(16).padStart(2, '0'); + return `#${r}${g}${b}`; +} + +export function getRandomFloat(min, max) { + return Math.random() * (max - min) + min; +} + +function getU32(id) { + const hex = id.replace(/-/g, ""); + const buffer = new Uint32Array(4); + for (let i = 0; i < 4; i++) { + buffer[i] = parseInt(hex.slice(i * 8, (i + 1) * 8), 16); + } + return buffer; +} + +function heapU32SetUUID(id, heap, offset) { + const buffer = getU32(id); + heap.set(buffer, offset); + return buffer; +} + +function ptr8ToPtr32(ptr8) { + return ptr8 >>> 2; +} + +export function allocBytes(size) { + return Module._alloc_bytes(size); +} + +export function getHeapU32() { + return Module.HEAPU32; +} + +export function clearShapeFills() { + Module._clear_shape_fills(); +} + +export function addShapeSolidFill(argb) { + const ptr = allocBytes(160); + const heap = getHeapU32(); + const dv = new DataView(heap.buffer); + dv.setUint8(ptr, 0x00, true); + dv.setUint32(ptr + 4, argb, true); + Module._add_shape_fill(); +} + +export function addShapeSolidStrokeFill(argb) { + const ptr = allocBytes(160); + const heap = getHeapU32(); + const dv = new DataView(heap.buffer); + dv.setUint8(ptr, 0x00, true); + dv.setUint32(ptr + 4, argb, true); + Module._add_shape_stroke_fill(); +} + +function serializePathAttrs(svgAttrs) { + return Object.entries(svgAttrs).reduce((acc, [key, value]) => { + return acc + key + '\0' + value + '\0'; + }, ''); +} + +export function draw_star(x, y, width, height) { + const len = 11; // 1 MOVE + 9 LINE + 1 CLOSE + const ptr = allocBytes(len * 28); + const heap = getHeapU32(); + const dv = new DataView(heap.buffer); + + const cx = x + width / 2; + const cy = y + height / 2; + const outerRadius = Math.min(width, height) / 2; + const innerRadius = outerRadius * 0.4; + + const star = []; + for (let i = 0; i < 10; i++) { + const angle = Math.PI / 5 * i - Math.PI / 2; + const r = i % 2 === 0 ? outerRadius : innerRadius; + const px = cx + r * Math.cos(angle); + const py = cy + r * Math.sin(angle); + star.push([px, py]); + } + + let offset = 0; + + // MOVE to first point + dv.setUint16(ptr + offset + 0, 1, true); // MOVE + dv.setFloat32(ptr + offset + 20, star[0][0], true); + dv.setFloat32(ptr + offset + 24, star[0][1], true); + offset += 28; + + // LINE to remaining points + for (let i = 1; i < star.length; i++) { + dv.setUint16(ptr + offset + 0, 2, true); // LINE + dv.setFloat32(ptr + offset + 20, star[i][0], true); + dv.setFloat32(ptr + offset + 24, star[i][1], true); + offset += 28; + } + + // CLOSE the path + dv.setUint16(ptr + offset + 0, 4, true); // CLOSE + + Module._set_shape_path_content(); + + const str = serializePathAttrs({ + "fill": "none", + "stroke-linecap": "round", + "stroke-linejoin": "round", + }); + const size = str.length; + offset = allocBytes(size); + Module.stringToUTF8(str, offset, size); + Module._set_shape_path_attrs(3); +} + + +export function setShapeChildren(shapeIds) { + const offset = allocBytes(shapeIds.length * 16); + const heap = getHeapU32(); + let currentOffset = offset; + for (const id of shapeIds) { + heapU32SetUUID(id, heap, ptr8ToPtr32(currentOffset)); + currentOffset += 16; + } + return Module._set_children(); +} + +export function useShape(id) { + const buffer = getU32(id); + Module._use_shape(...buffer); +} + +export function set_parent(id) { + const buffer = getU32(id); + Module._set_parent(...buffer); +} + +export function render() { + console.log('render') + Module._set_view(1, 0, 0); + Module._render_from_cache(); + debouncedRender(); +} + +function debounce(fn, delay) { + let timeout; + return (...args) => { + clearTimeout(timeout); + timeout = setTimeout(() => fn(...args), delay); + }; +} + +const debouncedRender = debounce(() => { + Module._render(Date.now()); +}, 100); + +export function setupInteraction(canvas) { + canvas.addEventListener("wheel", (e) => { + e.preventDefault(); + const zoomFactor = e.deltaY < 0 ? 1.1 : 0.9; + scale *= zoomFactor; + const mouseX = e.offsetX; + const mouseY = e.offsetY; + offsetX -= (mouseX - offsetX) * (zoomFactor - 1); + offsetY -= (mouseY - offsetY) * (zoomFactor - 1); + Module._set_view(scale, offsetX, offsetY); + Module._render_from_cache(); + debouncedRender(); + }); + + canvas.addEventListener("mousedown", (e) => { + isPanning = true; + lastX = e.offsetX; + lastY = e.offsetY; + }); + + canvas.addEventListener("mousemove", (e) => { + if (isPanning) { + const dx = e.offsetX - lastX; + const dy = e.offsetY - lastY; + offsetX += dx; + offsetY += dy; + lastX = e.offsetX; + lastY = e.offsetY; + Module._set_view(scale, offsetX, offsetY); + Module._render_from_cache(); + debouncedRender(); + } + }); + + canvas.addEventListener("mouseup", () => { isPanning = false; }); + canvas.addEventListener("mouseout", () => { isPanning = false; }); +} + +const TextAlign = { + 'left': 0, + 'center': 1, + 'right': 2, + 'justify': 3, +} + +function getTextAlign(textAlign) { + if (textAlign in TextAlign) { + return TextAlign[textAlign]; + } + return 0; +} + +function getTextDirection(textDirection) { + switch (textDirection) { + default: + case 'LTR': return 0; + case 'RTL': return 1; + } +} + +function getTextDecoration(textDecoration) { + switch (textDecoration) { + default: + case 'none': return 0; + case 'underline': return 1; + case 'line-through': return 2; + case 'overline': return 3; + } +} + +function getTextTransform(textTransform) { + switch (textTransform) { + default: + case 'none': return 0; + case 'uppercase': return 1; + case 'lowercase': return 2; + case 'capitalize': return 3; + } +} + +function getFontStyle(fontStyle) { + switch (fontStyle) { + default: + case 'normal': + case 'oblique': + case 'italic': + return 0; + } +} + +export function updateTextShape(fontSize, root) { + const paragraphAttrSize = 48; + const leafAttrSize = 56; + const fillSize = 160; + + // Calculate fills + const fills = [ + { + type: "solid", + color: "#ff00ff", + opacity: 1.0, + }, + ]; + + const totalFills = fills.length; + const totalFillsSize = totalFills * fillSize; + + const paragraphs = root.children; + console.log("paragraphs", paragraphs.length); + + Module._clear_shape_text(); + for (const paragraph of paragraphs) { + let totalSize = paragraphAttrSize; + + const leaves = paragraph.children; + const numLeaves = leaves.length; + console.log("leaves", numLeaves); + + for (const leaf of leaves) { + const text = leaf.textContent; + const textBuffer = new TextEncoder().encode(text); + const textSize = textBuffer.byteLength; + console.log("text", text, textSize); + totalSize += leafAttrSize + totalFillsSize; + } + + totalSize += paragraph.textContent.length; + + console.log("Total Size", totalSize); + // Allocate buffer + const bufferPtr = allocBytes(totalSize); + const heap = new Uint8Array(Module.HEAPU8.buffer, bufferPtr, totalSize); + const dview = new DataView(heap.buffer, bufferPtr, totalSize); + + const textAlign = getTextAlign( + paragraph.style.getPropertyValue("text-align"), + ); + console.log("text-align", textAlign); + const textDirection = getTextDirection( + paragraph.style.getPropertyValue("text-direction"), + ); + console.log("text-direction", textDirection); + const textDecoration = getTextDecoration( + paragraph.style.getPropertyValue("text-decoration"), + ); + console.log("text-decoration", textDecoration); + const textTransform = getTextTransform( + paragraph.style.getPropertyValue("text-transform"), + ); + console.log("text-transform", textTransform); + const lineHeight = parseFloat( + paragraph.style.getPropertyValue("line-height"), + ); + console.log("line-height", lineHeight); + const letterSpacing = parseFloat( + paragraph.style.getPropertyValue("letter-spacing"), + ); + console.log("letter-spacing", letterSpacing); + + /* + num_leaves: u32, + text_align: u8, + text_direction: u8, + text_decoration: u8, + text_transform: u8, + line_height: f32, + letter_spacing: f32, + typography_ref_file: [u32; 4], + typography_ref_id: [u32; 4], + */ + + // Set number of leaves + dview.setUint32(0, numLeaves, true); + + // Serialize paragraph attributes + dview.setUint8(4, textAlign, true); // text-align: left + dview.setUint8(5, textDirection, true); // text-direction: LTR + dview.setUint8(6, textDecoration, true); // text-decoration: none + dview.setUint8(7, textTransform, true); // text-transform: none + dview.setFloat32(8, lineHeight, true); // line-height + dview.setFloat32(12, letterSpacing, 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.setUint32(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.setUint32(44, 0, true); // typography-ref-id (UUID part 4) + + let leafOffset = paragraphAttrSize; + for (const leaf of leaves) { + console.log( + "leafOffset", + leafOffset, + paragraphAttrSize, + leafAttrSize, + fillSize, + totalFills, + totalFillsSize, + ); + const fontStyle = getFontStyle(leaf.style.getPropertyValue("font-style")); + const fontSize = parseFloat(leaf.style.getPropertyValue("font-size")); + console.log("font-size", fontSize); + const fontWeight = parseInt( + leaf.style.getPropertyValue("font-weight"), + 10, + ); + console.log("font-weight", fontWeight); + + const text = leaf.textContent; + const textBuffer = new TextEncoder().encode(text); + const textSize = textBuffer.byteLength; + + // Serialize leaf attributes + dview.setUint8(leafOffset + 0, fontStyle, true); // font-style: normal + dview.setUint8(leafOffset + 1, 0, true); // text-decoration: none + dview.setUint8(leafOffset + 2, 0, true); // text-transform: none + dview.setFloat32(leafOffset + 4, fontSize, true); // font-size + dview.setInt32(leafOffset + 8, fontWeight, 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.setUint32(leafOffset + 24, 0, true); // font-id (UUID part 4) + dview.setUint32(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.setUint32(leafOffset + 44, 0, true); // font-variant-id (UUID part 4) + dview.setUint32(leafOffset + 48, textSize, true); // text-length + dview.setUint32(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 + 0, 0x00, true); // Fill type: solid + dview.setUint32(fillOffset + 4, argb, true); + fillOffset += fillSize; // Move to the next fill + } + }); + leafOffset += leafAttrSize + totalFillsSize; + } + + const text = paragraph.textContent; + const textBuffer = new TextEncoder().encode(text); + + // Add text content + const textOffset = leafOffset; + console.log('textOffset', textOffset); + heap.set(textBuffer, textOffset); + + Module._set_shape_text_content(); + } +} + +export function addTextShape(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: "#ff00ff", + opacity: 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/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index c232e2a34b..8310fe0b3b 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -529,11 +529,13 @@ impl TryFrom<&[u8]> for RawTextLeaf { } #[allow(dead_code)] +#[repr(C)] #[derive(Debug, Clone)] pub struct RawTextLeafData { font_style: u8, text_decoration: u8, text_transform: u8, + byte_padding: u8, font_size: f32, font_weight: i32, font_id: [u32; 4], @@ -563,6 +565,7 @@ impl From<&[u8]> for RawTextLeafData { font_style: text_leaf.font_style, text_decoration: text_leaf.text_decoration, text_transform: text_leaf.text_transform, + byte_padding: 0, font_size: text_leaf.font_size, font_weight: text_leaf.font_weight, font_id: text_leaf.font_id, diff --git a/render-wasm/src/wasm/text.rs b/render-wasm/src/wasm/text.rs index fdfec34f05..f581d06933 100644 --- a/render-wasm/src/wasm/text.rs +++ b/render-wasm/src/wasm/text.rs @@ -20,7 +20,6 @@ pub extern "C" fn set_shape_text_content() { .add_paragraph(raw_text_data.paragraph) .expect("Failed to add paragraph"); }); - mem::free_bytes(); } From 596193d34ddc465b3b64157f958e20f0c8d12ac3 Mon Sep 17 00:00:00 2001 From: Aitor Moreno Date: Thu, 21 Aug 2025 12:22:39 +0200 Subject: [PATCH 2/2] :tada: Add missing styles on text leaves --- frontend/src/app/render_wasm/api/texts.cljs | 13 +- frontend/src/app/render_wasm/serializers.cljs | 3 +- frontend/text-editor/src/style.css | 16 +- frontend/text-editor/src/wasm.html | 438 +++++++++--------- frontend/text-editor/src/wasm/lib.js | 201 ++++---- render-wasm/src/shapes/text.rs | 19 +- 6 files changed, 358 insertions(+), 332 deletions(-) diff --git a/frontend/src/app/render_wasm/api/texts.cljs b/frontend/src/app/render_wasm/api/texts.cljs index 9f6dbd4575..eb0d961491 100644 --- a/frontend/src/app/render_wasm/api/texts.cljs +++ b/frontend/src/app/render_wasm/api/texts.cljs @@ -16,7 +16,7 @@ [app.render-wasm.wasm :as wasm])) (def ^:const PARAGRAPH-ATTR-U8-SIZE 44) -(def ^:const LEAF-ATTR-U8-SIZE 56) +(def ^:const LEAF-ATTR-U8-SIZE 60) (defn- encode-text "Into an UTF8 buffer. Returns an ArrayBuffer instance" @@ -79,6 +79,7 @@ (reduce (fn [offset leaf] (let [font-style (sr/translate-font-style (get leaf :font-style)) font-size (get leaf :font-size) + letter-spacing (get leaf :letter-spacing) font-weight (get leaf :font-weight) font-id (f/normalize-font-id (get leaf :font-id)) font-family (hash (get leaf :font-family)) @@ -104,15 +105,21 @@ text-transform (or (sr/translate-text-transform (:text-transform leaf)) (sr/translate-text-transform (:text-transform paragraph)) - (sr/translate-text-transform "none"))] + (sr/translate-text-transform "none")) + + text-direction + (or (sr/translate-text-direction (:text-direction leaf)) + (sr/translate-text-direction (:text-direction paragraph)) + (sr/translate-text-direction "ltr"))] (-> offset (mem/write-u8 dview font-style) (mem/write-u8 dview text-decoration) (mem/write-u8 dview text-transform) - (+ 1) ;;padding + (mem/write-u8 dview text-direction) (mem/write-f32 dview font-size) + (mem/write-f32 dview letter-spacing) (mem/write-u32 dview font-weight) (mem/write-uuid dview font-id) diff --git a/frontend/src/app/render_wasm/serializers.cljs b/frontend/src/app/render_wasm/serializers.cljs index 13fc6a62c3..32c5e20854 100644 --- a/frontend/src/app/render_wasm/serializers.cljs +++ b/frontend/src/app/render_wasm/serializers.cljs @@ -310,7 +310,8 @@ [text-direction] (case text-direction "ltr" 0 - "rtl" 1)) + "rtl" 1 + 0)) (defn translate-font-style [font-style] diff --git a/frontend/text-editor/src/style.css b/frontend/text-editor/src/style.css index a2e71ddda1..659dbc2e16 100644 --- a/frontend/text-editor/src/style.css +++ b/frontend/text-editor/src/style.css @@ -3,12 +3,20 @@ color: #eee; } +html, body { + margin: 0; + padding: 0; +} + +canvas { + width: 100cqw; +} + .text-editor-container { background-color: white; } -#output { - font-family: monospace; - padding: 1rem; - border: 1px solid #333; +.playground { + display: grid; + max-width: 1280px; } diff --git a/frontend/text-editor/src/wasm.html b/frontend/text-editor/src/wasm.html index 700d38a363..cb3d0dafa0 100644 --- a/frontend/text-editor/src/wasm.html +++ b/frontend/text-editor/src/wasm.html @@ -8,220 +8,216 @@ Penpot - Text Editor Playground - -
-
- Styles - -
- - -
-
- - -
-
- - -
-
- - -
- -
- - -
-
- - -
-
- - -
-
- - -
- -
- - -
-
- - -
-
- - -
-
- - -
- -
- - -
-
- - -
-
- - -
-
- - -
-
-
- Debug -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ Debug +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + -
-
-
+ --> +
+
+
+
+ +
- -
-