pulpkit-reactive SolidJS-inspired reactivity: Signal<T>, Computed<T>, Effect, batch()
|
pulpkit-render Skia CPU rendering: Canvas, TextRenderer, image loading + caching
|
pulpkit-wayland Layer-shell surfaces, pointer/keyboard input, output tracking
|
pulpkit-layout Widget tree (Node enum), Taffy flexbox layout, Tailwind-style tokens
|
pulpkit-lua Lua VM, widget constructors, signal/timer/system/stream APIs
|
pulpkit-core Runtime orchestration, event loop, dirty tracking, popup state machine, IPC
Every bug fix and improvement must work for ALL users, not just our specific shell config. If popup centering is broken, fix the positioning system — don't hardcode pixel offsets. If a widget has inconsistent padding, fix the widget's default style — don't override it in one shell.lua.
Every dynamic property is a Signal<T> or Prop<T>. Never use RefCell for widget state. The reactive graph tracks dependencies automatically. Signal::set() skips notification when value is unchanged (PartialEq).
Every API decision asks "could a user with a different shell use this?" The Lua API should be general-purpose. Shell-specific logic lives in examples/hello/shell.lua and lib.lua, not in Rust.
Target: <1% idle CPU in release builds. The framework itself is 0.5% — any excess comes from shell.lua polling. Prefer exec_stream() (push-based) over set_interval() + exec_output() (pull-based).
No file over 300 lines. Extract when it grows.
- pulpkit-reactive: KEEP AS-IS. Self-contained, well-tested. Only touch for bug fixes.
- pulpkit-wayland: KEEP AS-IS. Thin sctk wrapper. Exposes AppState, InputEvent, LayerSurface, OutputInfo.
- pulpkit-render: KEEP AS-IS. Skia canvas, text, image, color. Thread-local font/image caches.
- pulpkit-layout: Widget types (Node enum), Taffy integration, paint pipeline. Add new Node variants here.
- pulpkit-lua: Lua ↔ Rust bridge. Widget constructors, signal API, timer API, system API, stream API.
- pulpkit-core: Runtime, event loop, dirty tracking, popup state machine, IPC, setup.
Node::Container { style, direction, children } // row/col
Node::Text { style, content } // static or reactive text
Node::Image { style, path, width, height } // PNG image from file
Node::Spacer // flex-grow: 1
Node::DynamicList { style, direction, resolve, cached_children } // each() with key reconciliation
Node::Interactive { style, kind, children } // button/slider/togglerow, col, text, spacer, image, button, slider, toggle, each
signal(initial), computed(fn), effect(fn)
set_interval(fn, ms) -> id, set_timeout(fn, ms) -> id, clear_interval(id), clear_timeout(id)
exec(cmd), exec_output(cmd), exec_stream(cmd, callback) -> id, cancel_stream(id), env(name), resolve_icon(name)
window(name, opts, widget_fn), popup(name, opts, widget_fn)
Popups use layer-shell surfaces with margins for positioning. All coordinates must be in LOGICAL pixels (physical / scale). Use OutputInfo::logical_width() / logical_height().
anchor = "top left"/"top right"— positioned below bar via top margin + left/right margin from clickanchor = "center"— centered on screen using logical output dimensionsdismiss_on_outside = true— creates a transparent backdrop surface that catches clicks
1. calloop dispatch (waits for wayland events, IPC, stream output, timers)
2. Handle configure events (surface resize)
3. Dispatch input events (pointer motion, click, scroll, keyboard)
4. Process IPC commands (Lua eval)
5. Dispatch stream events (exec_stream callbacks)
6. Process timer cancellations
7. Fire due timers
8. Flush reactive effects
9. Check popup visibility signals (show/hide)
10. Tick popup animations
11. Mark dirty surfaces if handlers fired
12. Render dirty surfaces (single pass)
cargo test # all unit tests
cargo build --release # release build for performance testingSpacing: p-2, px-3, py-1, m-2, mx-3, gap-2
Colors: bg-base, bg-surface, text-fg, text-muted, text-primary
Typography: text-xs, text-sm, text-base, text-lg, text-xl, font-bold
Layout: flex-1, items-center, justify-end, justify-between, w-full, h-10
- Add variant to
Nodeenum inpulpkit-layout/src/tree.rs - Handle in
build_taffy_node()inflex.rs(layout) - Handle in
paint_tree()inpaint.rs(rendering) - Handle in
count_descendants()inflex.rs - Handle in
wire_node()inpulpkit-core/src/dirty.rs(dirty tracking) - Register Lua constructor in
pulpkit-lua/src/widgets.rs
- Add to the appropriate module in
pulpkit-lua/src/(system.rs, timers.rs, etc.) - Register in the
register_*_api()function - Export from
pulpkit-lua/src/lib.rsif types need to cross crate boundaries - Wire in
pulpkit-core/src/runtime.rsif it needs runtime integration