* 🎉 Add UI for toggling bullet lists (under config flag) * 🎉 Store list-style-type as a paragraph attribute The bullet list toggle now saves list-style-type ("none" or "disc") on the selected paragraphs and reads it back. A paragraph without the attr counts as "none", so a bullet paragraph next to an older one shows as mixed. WASM keeps the value on each paragraph: the paragraph bytes grow from 12 to 16, Enter keeps it on the new paragraph, and the v3 editor reports it in the current styles, so the sidebar shows it while editing too. Refs #12630 AI-assisted-by: claude-opus-5-5 * ✨ Draw bullets for list paragraphs A paragraph with list-style-type "disc" now gets a solid round bullet, sized as Chrome draws a CSS disc marker (measured with Source Sans Pro): a radius of 0.16 font sizes, centered 0.32 font sizes above the first baseline, with a gap of 0.33 font sizes before the text. The bullet hangs outside the text box, before its start edge (after it for RTL), like CSS list-style-position: outside, so the text layout, caret and selection stay as they were. The bullet takes the paint of its line's first style run, so fills, strokes, shadows and masks apply to it as they do to the glyphs. An empty item, which has no run, borrows the previous item's paint. The cached paint, the text extrect and text-to-path share one walker over the paragraphs, so they always agree: the extrect grows to hold the bullets, like shadows and blur, and flattening a text keeps them. Refs #12630 AI-assisted-by: claude-opus-5-5 * ✨ Show the list style in Inspect Text paragraphs with list-style-type "disc" now show a "List Style" row in the Inspect Styles and Computed views, after Text Transform. Its copy action gives "list-style-type: disc;". Text without a list shows no row. The list item check lives in common as list-paragraph?, so every place that needs it shares one rule. Refs #12630 AI-assisted-by: claude-opus-5-5 * ✨ Generate list HTML and CSS for bullet paragraphs In the Inspect code, neighbouring paragraphs with list-style-type "disc" become one <ul> with an <li> each, keeping the paragraph's class and dir. Other paragraphs stay <p>. Only code mode groups them; text rendering still uses <p>. The CSS sets list-style-type on the items and resets the list's margin and padding, so the browser hangs the bullet before the text as the canvas does. Paragraph rules set font-size: 0, so each item also gets a ::marker rule with its first span's font size and text color. A ::marker can't paint a gradient, so a gradient fill gives its first stop. Refs #12630 AI-assisted-by: claude-opus-5-5
Penpot WASM render
This is the canvas-based WebAssembly render engine for Penpot.
Rust & Emscripten
This project is a Rust crate that targets Emscripten (wasm32-unknown-emscripten).
We use wasm32-unknown-emscripten compilation target:
- It compiles Rust code into WASM
- It generates the JavaScript code (“glue”) to load and run the WASM code
Skia
We use Skia, an Open Source 2D graphics library. In particular, the render engine uses Skia via custom binaries of the rust-skia crate.
How to build
With the Penpot Development Environment running, create a new tab in the tmux.
cd penpot/render-wasm
./build
You can also use ./watch to run the build on every change.
The build script compiles the project and copies the .js and .wasm files to the app that uses each target.
Render targets
The same Rust source produces two artifacts, which differ only in compiler options:
| Target | Tuned for | Cargo profile | Consumed by |
|---|---|---|---|
frontend |
speed | release (-O3) |
frontend/resources/public/js |
export |
size | size (-Oz) |
exporter/resources/wasm |
./build # both targets, frontend first
./build frontend # workspace / viewer renderer
./build export # headless exporter renderer
./watch still follows a single target (frontend unless you pass one),
since watching both would rebuild twice on every keystroke.
Each target keeps its own CARGO_TARGET_DIR (target/<target>), so switching
between them does not invalidate the other's cache. Set BUILD_MODE=release
(or NODE_ENV=production) for an optimized build; the default is debug.
Set PENPOT_WASM_PREPARED=1 when the pinned Rust toolchain is selected, its
Emscripten target is installed, and this package's Node dependencies are
installed. ./build, ./test, ./lint and ./fmt then skip rustup setup
and pnpm install. Benchmark preparation sets this flag for each retained
build.
Serialize builds in one checkout
All targets in one checkout share one flock lock stored at
render-wasm/.render-wasm-build.lock. The lock covers dependency setup, the
Cargo build, artifact copy, and target cleanup. If another build or cleanup
already holds the lock, the new process prints a waiting message and starts
only after the first process exits.
The watch command takes the lock for each build, then releases it while it waits for source changes. The lock file stays in the checkout after a build, but the operating system releases its lock when the process exits, including after an error or signal.
The lock applies to one checkout and only to commands that use these scripts.
A manual Cargo build can still write to the same target without taking the
lock. Set RENDER_WASM_LOCK_FILE to the same path in each process when you
make different checkouts share a CARGO_TARGET_DIR. The supported Linux build
environment must provide flock from util-linux; flock --version checks this
dependency.
Use the target cleanup commands instead of running cargo clean on the shared
target/ directory:
./clean frontend # remove target/frontend only
./clean export # remove target/export only
Each target writes its own generated shared.js (the enum discriminants the
CLJS side compiles against) next to the code that imports it — respectively
frontend/src/app/render_wasm/api/shared.js and
exporter/src/app/wasm/shared.js. Neither build writes to the other's paths.
Edit your local frontend/resources/public/js/config.js to add the following flags:
enable-feature-render-wasmto enable this render engine.enable-render-wasm-dpr(optional), to enable using the device pixel ratio.
How to test
We currently have two types of tests:
- Unit tests
cd penpot/render-wasm
./test

