Environment Context
cwd: /Users/dj/Developer/taffy-canvas
shell: zsh
current date: 2026-04-10
timezone: America/TorontoThis is the beginning of a new project called "Taffy Canvas". It is to be written in Rust, using Skia: https://github.com/rust-skia/rust-skia and Taffy as a layout engine: https://github.com/dioxusLabs/taffy
The goal is simple: allow rendering powerful, complex images server-side for message-based games like those on Discord, or open graph images. Having powerful layout capabilities allows focusing more on design than fighting layout, so we want this to be declarative. For simplicity, this should be a simple XML in, image out library.
It will consist of two parts: a rust API/crate, and a wrapper which exposes it to JS/Node.js using napi-rs
It must run with or without GPU (macOS laptops, linux VPSes)
XML will be fairly simple: view, text, image nodes. and support for all CSS layouts that taffy supports (and also: absolute and fixed positions) + styles that are trivial to implement (borders, backgrounds, corner radiuses, etc). Text measurement should be integrated using Skia itself.
Look into https://github.com/chenglou/pretext this library for inspiration on how to implement flawless image in-line with text layouts.
You must follow test-driven development. Make sure everything is throughly tested including edge cases. What this library renders should be exactly what the layout/XML describes.
For what exactly we're trying to render, look at /Users/dj/Developer/PokeOne/src/engine/hud/ It will have many different kind of images to render.
Essentially, this library should be able to provide an API surface that can:
What's most important is you must benchmark this. Implement all sorts of performance tricks to make rendering as fast as possible, reuse stuff, etc.
I’m treating this as a greenfield scaffold with hard constraints: Rust core, rust-skia, taffy, napi-rs, CPU/GPU-capable rendering, XML-in/image-out, and TDD with benchmarks from the start. I’m going to inspect the repo state, then I’ll lock down an initial architecture and implement the first slice with tests before expanding.
>pwd>rg --files -n>git status --shortThe workspace is effectively empty and not initialized as a git repo yet, so I’m starting from first principles rather than adapting existing code. Next I’m checking the directory contents and then I’ll scaffold a core workspace with tests and a benchmark harness before wiring rendering.
>ls -la>cargo --version>rustc --version>node --version>npm --versionI have a clean slate and the toolchain is current. I’m verifying the current upstream APIs for rust-skia, taffy, and napi-rs before I commit to crate boundaries and feature flags, because those surfaces do change and I don’t want to scaffold against stale assumptions.
I have enough upstream context to pin the first design: CPU raster path first with a render abstraction that can later host GPU surfaces, Taffy for block layout, and a separate prepared-template hot path modeled after Pretext’s “prepare once, layout cheaply” split. I’m now inspecting the PokeOne HUD code to shape the XML schema and the first render cases around real usage rather than toy examples.
>find /Users/dj/Developer/PokeOne/src/engine/hud -maxdepth 2 -type f | sort>rg -n "HUD|hud|image|text|layout|position|sprite|bar|panel" /Users/dj/Developer/PokeOne/src/engine/hudI’ve got the usage shape: lots of image-heavy HUDs, precise text sizing, truncation, badges, layered panels, and sprite reuse. I’m now pinning crate versions and then I’ll write the first failing tests around XML parsing, layout, templating, and deterministic raster output so the implementation grows under coverage rather than by scaffolding alone.
>cargo search skia-safe --limit 1>cargo search taffy --limit 3>cargo search napi --limit 3>cargo search napi-derive --limit 3>cargo info skia-safe>cargo info taffy>cargo info napiThe dependency surface looks workable: skia-safe 0.93.1 with binary-cache and textlayout, taffy 0.10.0, and napi 3.8.x. I’m moving into scaffold mode now: workspace layout, benchmark target, and the first red tests for parser/layout/render/template caching.
>cargo info napi --verbose>cargo info napi-derive --verbosePlan for the first implementation slice:
taffy-canvas-core and taffy-canvas-node.core, define the document model, XML parser, style system, template compiler, asset/text measurement traits, and a CPU raster renderer using Skia.node, expose sync and async render/template APIs via napi-rs, with a thread-pool-backed renderer handle.I’m going to scaffold the workspace and write the first failing tests now.
I’m creating the workspace skeleton now, then I’ll immediately replace the defaults with test targets so the implementation starts red.
>cargo new --lib crates/taffy-canvas-core>cargo new --lib crates/taffy-canvas-node>find . -maxdepth 3 -type f | sort>sed -n '1,200p' crates/taffy-canvas-core/Cargo.toml>sed -n '1,200p' crates/taffy-canvas-node/Cargo.toml>sed -n '1,200p' crates/taffy-canvas-core/src/lib.rs>sed -n '1,200p' crates/taffy-canvas-node/src/lib.rsI’ve got the minimal viable slice defined tightly enough to code against: XML view/text/image, CSS-like layout props, template placeholders, CPU raster via Skia, and an async pool surface. I’m editing the workspace manifests and adding the first failing tests now.
>rg -n "pub enum Position|MeasureFunc|new_leaf_with_context|compute_layout_with_measure|compute_layout" ~/.cargo/registry/src -g '*/taffy-0.10.0/src/*.rs'>rg -n "pub struct Paragraph|layout\(|paint\(|FontCollection" ~/.cargo/registry/src -g '*/skia-safe-0.93.1/src/textlayout/*.rs'Tip for XML parsing: specialize it for our use-case to squeeze performance out of it.
I’m taking that as a requirement, not just an optimization note. I’ll keep the XML parser purpose-built for this schema instead of building a generic DOM parser, so we can avoid excess allocation and parse directly into our node arena/template segments.
>find ~/.cargo/registry/src -maxdepth 2 -type d | head -20>rg -n "new_leaf_with_context|compute_layout\(|MeasureFunc|AvailableSpace|TaffyTree" ~/.cargo/registry/src/*/taffy-0.10.0/src>rg -n "pub struct Paragraph|pub struct ParagraphBuilder|FontCollection|layout\(|paint\(" ~/.cargo/registry/src/*/skia-safe-0.93.1/src/textlayout>find ~/.cargo/registry/src -path '*skia-safe-0.93.1/src/textlayout*' -type f | sort | head -50>sed -n '1,220p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/taffy-0.10.0/src/tree/taffy_tree.rsAnd also initialize git repo as needed. Keep committing incrementally as you work
I’m incorporating both new constraints: the XML parser will stay specialized and I’ll keep the repo history incremental rather than dumping one large commit at the end. I’m initializing git now, then I’ll land the red-test scaffold as the first commit before moving green.
>git init -b main>git status --short>rg -n "pub struct FontCollection|pub struct ParagraphBuilder|pub struct ParagraphStyle|pub struct TextStyle|layout\(|paint\(" ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src -g '*.rs'>rg -n "encode_to_data|Image::from_encoded|Surface::new_raster_n32_premul|draw_str|RRect" ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src -g '*.rs'>sed -n '1,220p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/modules/paragraph/paragraph_builder.rs>sed -n '1,220p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/modules/paragraph/paragraph.rs>sed -n '1,240p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/modules/paragraph/font_collection.rs>rg -n "read_pixels\(|peek_pixels\(|Pixmap" ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/core -g '*.rs'>rg -n "draw_image_rect|SamplingOptions" ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/core/canvas.rs>rg -n "from_encoded\(|new_copy\(" ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/core/image.rs ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/core/data.rs>sed -n '1,140p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/core/data.rs>sed -n '140,240p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/core/image.rs>sed -n '820,870p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/core/surface.rs>sed -n '39,90p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/core/pixmap.rsI’m laying down the API and test targets before filling in behavior. The first test set is focused on the hot path you care about: compile once, substitute params, compute layout with absolute positioning, render deterministic pixels, and fan out work through a pool.
The red suite is in place. I’m running it now to surface the real compile and behavior gaps, then I’ll close them in the core crate before the first commit.
>cargo test -p taffy-canvas-core>git status --shortThe dependency build is still chewing through rust-skia, but I can already clean a few obvious compile issues in parallel so the next test run is signal rather than noise.
>rg -n "pub struct Style" ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/taffy-0.10.0/src/style -g '*.rs'>sed -n '1,260p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/taffy-0.10.0/src/style/mod.rs>sed -n '1,240p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/taffy-0.10.0/src/style/style.rs>rg -n "pub enum Position|pub enum FlexDirection|pub enum AlignItems|pub enum JustifyContent|pub enum LengthPercentageAuto|pub enum Dimension" ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/taffy-0.10.0/src/style -g '*.rs'>sed -n '430,620p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/taffy-0.10.0/src/style/mod.rs>rg -n "set_text_style|set_max_lines|set_text_align|set_height|set_font_families|set_font_size|set_font_style|set_text_style" ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/modules/paragraph -g '*.rs'>sed -n '1,220p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/modules/paragraph/paragraph_style.rs>sed -n '1,220p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/modules/paragraph/text_style.rs>rg -n "pub fn new_with_children|pub fn layout\(&self" ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/taffy-0.10.0/src/tree/taffy_tree.rs>sed -n '560,660p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/taffy-0.10.0/src/tree/taffy_tree.rs>sed -n '900,940p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/taffy-0.10.0/src/tree/taffy_tree.rs>rg -n "config_mut\(\).*trim_text|read_event_into|Reader::from_str" ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quick-xml-0.38.4 -g '*.rs' | head -40>sed -n '1,140p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quick-xml-0.38.4/README.md>sed -n '1,220p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/taffy-0.10.0/src/style/dimension.rs>sed -n '260,330p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/taffy-0.10.0/src/style/mod.rs>sed -n '1,220p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/taffy-0.10.0/src/style/alignment.rs>rg -n "struct Attribute|decode_and_unescape_value|pub fn decoder" ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quick-xml-0.38.4/src/events/attributes.rs>sed -n '1,220p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quick-xml-0.38.4/src/events/attributes.rs>rg -n "new_n32_premul|Color4f|Color::from_argb|FontMgr::default|set_color\(|set_anti_alias|Style::Stroke" ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src -g '*.rs' | head -50>sed -n '1,120p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/utils/text_utils.rs>rg -n "new_rect_xy|set_rect_xy|from_rect_xy" ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/core/rrect.rs>sed -n '50,120p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/core/rrect.rs>rg -n "from_argb|from_rgb" ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/core/color.rs>sed -n '1,120p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/core/color.rs>rg -n "new_n32_premul" ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/core/image_info.rs>sed -n '80,120p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/core/image_info.rs>sed -n '200,235p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/core/image_info.rs>sed -n '1590,1648p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/core/canvas.rs>rg -n "set_color\(|set_color_foreground|set_font_style\(" ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/modules/paragraph/text_style.rs | head -30>sed -n '284,490p' ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/skia-safe-0.93.1/src/modules/paragraph/text_style.rs>rg -n "pub fn children\(" ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/taffy-0.10.0/src/tree/taffy_tree.rsThe core implementation is now in place for the first slice: specialized template parsing, param substitution, style parsing, Taffy layout, Skia raster drawing, and pooled renders. I’m running the suite again with a longer timeout because rust-skia’s first build is the slow part.
>cargo test -p taffy-canvas-coreThe first post-implementation run is useful now: this is down to normal Rust API mismatches, not design gaps. I’m fixing the import/export issues and rerunning immediately.
>cargo test -p taffy-canvas-core>cargo test -p taffy-canvas-core>cargo test -p taffy-canvas-coreThe first green slice is stable now: core tests pass end to end with compile, instantiate, layout, render, and pooled rendering. I’m going to commit this checkpoint, then I’ll extend the Node wrapper and add a usable benchmark run on top of it.
>git status --short>git add . && git commit -m "Scaffold core template renderer"cargo new created nested git repos inside each crate, which blocks staging from the workspace root. I’m removing those nested .git directories because they’re just scaffold artifacts I created, then I’ll retry the commit from the actual project root.
>rm -rf crates/taffy-canvas-core/.git crates/taffy-canvas-node/.git>find crates -maxdepth 2 -name .git -type d>find crates -maxdepth 2 -name .git -type d