All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog.

[0.7.1] - 2026-05-09

Fixed

  • Screenshot requests now include the required id field, matching the renderer 0.7.1 wire protocol. Without it the renderer logged a decode error and Bridge.screenshot timed out after 30 seconds.
  • Screenshot assertions now save a PNG alongside the .sha256 hash. The PNG is written when rgba_data is present (headless and windowed backends). Pass png: false to assert_screenshot/2 or assert_screenshot_match/3 to write the hash only. The .sha256 file is the actual CI assertion; the PNG is a human convenience for visual inspection.
  • SocketAdapter JSON buffer accumulation is now O(n) instead of O(n^2). Incoming TCP chunks are accumulated in an iolist and only materialized to binary when a newline arrives. The overflow check now runs against the tracked size before any allocation, preventing timeouts on slow networks or tight socket buffers.

[0.7.0] - 2026-05-09

Breaking

  • BINARY_VERSION renamed to PLUSHIE_RUST_VERSION. The version file now makes its purpose explicit: it pins the plushie-rust release this SDK targets, independent of the SDK's own semver. Plushie.Binary.plushie_rust_version/0 replaces the previous binary_version/0.
  • PLUSHIE_SOURCE_PATH renamed to PLUSHIE_RUST_SOURCE_PATH. The application config key :source_path is unchanged.
  • Command.async/1,2 renamed to Command.task/1,2. Converges with Python, TypeScript, Gleam, and Ruby SDKs. The internal :async command type atom becomes :task. AsyncEvent is unchanged.
  • grid.columns renamed to grid.num_columns. Matches the renderer field name and avoids the collision with table.columns (column spec lists).
  • Window query functions drop the get_ prefix. Command.get_window_size -> Command.window_size, Command.get_window_position -> Command.window_position, Command.get_mode -> Command.window_mode, Command.get_scale_factor -> Command.scale_factor, Command.get_system_theme -> Command.system_theme, Command.get_system_info -> Command.system_info.
  • WidgetCommandError renamed to CommandError. Fields renamed: node_id -> id, op -> family. Update pattern matches and any @impl Plushie.App clauses that matched the old struct.
  • SystemEvent.data renamed to SystemEvent.value. Consistent with WidgetEvent.value and StreamEvent.value.
  • Angles throughout the canvas API are now in degrees. Previously Plushie.Canvas.Angle normalized to radians; it now normalizes to degrees. The arc, ellipse, and rotate builders and the image.rotation field all send degrees to the renderer. Use Plushie.Canvas.Angle.to_radians/1 when a radian value is needed.
  • Renderer subscriptions drop the tag parameter. Key, pointer, window, and touch subscriptions now take keyword opts only: Subscription.on_key_press(window: "main", max_rate: 60). The old tag: opt was management-only and never appeared in events. Timer subscriptions retain their positional tag.
  • Command renames. Command.request_user_attention/1 -> Command.request_attention/1, Command.done/2 -> Command.dispatch/2, Command.widget_commands/1 -> Command.widget_batch/1.
  • ime_purpose renamed to input_purpose on TextInput and TextEditor, matching the renderer field rename.
  • data: renamed to fields: in inline widget event declarations. The widget DSL event :name, data: [field: type] form now uses fields:. Do-block event declarations are unaffected.
  • LinearGradient renamed to Plushie.Canvas.Gradient. The module was Plushie.Canvas.Shape.LinearGradient. The Plushie.Type.Gradient module (widget background gradients) is unchanged.
  • Command.scroll_to signature changed. (id, offset) -> (id, x, y) with separate typed float parameters. snap_to/2 and scroll_by/2 lose their default-argument overloads.
  • Command.select_range field names changed to start_pos and end_pos (the old names shadow Elixir reserved words in pattern matching contexts).
  • Table rows are now children, not a prop. The rows: keyword prop is removed. Use table_row "id" do cell "col", content end DSL macros in a do-block. The separator field changed from boolean to float (thickness in pixels). Table gains selected, striped, and height fields and a row_click event.
  • gain_focus/1 renamed to focus_window/1. The deprecated alias will be removed in a future release.
  • Renderer binary version bumped to 0.7.0. Run mix plushie.download --force after updating.

Added

  • memo/2 DSL primitive for subtree memoization. Wraps a widget view with a cache_key; the subtree is not re-rendered when the key is unchanged. Combine with cache_key state declaration for widget-level view caching.
  • Typed RichText.Span builder. Plushie.RichText.Span provides a typed struct instead of raw maps.
  • :link_click event type on rich_text. Clicked links deliver a WidgetEvent with type: :link_click.
  • on_focus/on_blur props on text_input and text_editor; :focused and :blurred event types delivered on focus change.
  • pointer_captured, pointer_lost, coords fields on pointer events where the renderer provides them.
  • Typed effect result variants. EffectEvent.result is a tagged tuple per kind (e.g., {:file_open, {:ok, path}}).
  • Typed diagnostic exceptions. Plushie.Runtime.BufferOverflowError and Plushie.Runtime.VersionMismatchError raised as structured exceptions.
  • rule.thickness prop - direction-agnostic line thickness; avoids choosing between width and height based on orientation.
  • Full input_purpose set on text_input and text_editor. Supports all nine renderer purposes: normal, secure, terminal, number, decimal, phone, email, url, search.
  • A11y improvements. resolved_a11y/1 helper for scoped ref resolution; builder defaults populate role automatically; canvas radio groups auto-infer position_in_set/size_of_set; focus commands scoped to canvas elements; announce politeness on a11y props.
  • Plushie.Canvas.Angle type for explicit angle values with to_degrees/1 and to_radians/1 converters.
  • Bridge heartbeat watchdog. Detects unresponsive renderers and triggers restart rather than hanging indefinitely.
  • use Plushie.Command standalone DSL for defining command modules with the same command/field macro infrastructure as native widgets.
  • :page scroll unit. Scroll events now carry unit: :page for page-delta events from the renderer.
  • Telemetry counters on normalize, diff, command execution, and subscription lifecycle.

Fixed

  • default_font always emitted as a family object.
  • Image list/clear sent through the typed image_op channel.
  • op_query_response decoded from the canonical "data" field.
  • window_opened decoded from the top-level x/y fields.
  • prop_validation events from debug renderer builds silently dropped rather than logged as protocol errors.

Changed

  • mix plushie.build delegates to cargo-plushie. Workspace generation, widget collision checks, main.rs emission, and Cargo.lock shepherding moved to the cargo-plushie Cargo subcommand in the plushie-rust workspace. The Mix task writes a "renderer spec" Cargo.toml listing native widget crates and shells out. Widget crates must declare [package.metadata.plushie.widget] in their own Cargo.toml for discovery. Install with cargo install cargo-plushie --version 0.7.0 --locked. See docs/versioning.md for the version correspondence table.
  • Plushie.Dev.DevServer now triggers incremental Rust builds through cargo plushie build instead of driving cargo build directly.

Removed

  • native/plushie/Cargo.lock stash. cargo-plushie preserves its own lockfile across runs; no SDK-level stash is needed.

[0.6.0] - 2026-04-02

Breaking

  • Unified pointer events. 14 device/widget-specific event types replaced with 8 generic types: :press, :release, :move, :scroll, :enter, :exit, :double_click, :resize. All carry pointer type (:mouse/:touch/:pen), modifiers state, and optional finger ID for touch. Removed types: canvas_press, canvas_release, canvas_move, canvas_scroll, mouse_right_press, mouse_right_release, mouse_middle_press, mouse_middle_release, mouse_move, mouse_scroll, mouse_enter, mouse_exit, mouse_double_click, sensor_resize.

  • Canvas element events unified. canvas_element_enter/leave/ focused/blurred/drag/drag_end/key_press/key_release and canvas_element_click replaced with standard types using scoped IDs. Canvas elements look like regular widgets from the SDK's perspective.

  • MouseEvent and TouchEvent removed. Subscription pointer events are now delivered as WidgetEvent structs with id set to the window ID and scope of [].

  • mouse_area renamed to pointer_area. The widget, DSL macro, and wire type all use the new name.

  • Window ID in scope chain. Window IDs are appended to the end of the scope list. Pattern matching with | _ at the end of scope naturally ignores the window for single-window apps.

  • :scroll vs :scrolled. :scroll is pointer wheel input (with coordinates and deltas). :scrolled is scrollable container viewport state change. Previously both used :scroll.

  • :start/:end alignment aliases removed. Use :left/:right/ :top/:bottom/:center.

  • Subscription functions renamed. on_mouse_move -> on_pointer_move, on_mouse_button -> on_pointer_button, on_mouse_scroll -> on_pointer_scroll, on_touch -> on_pointer_touch.

  • Canvas auto-consumption removed. Canvas background pointer events now reach update/2 when opted in via on_press/on_move/etc.

  • Renderer binary version bumped to 0.6.0.

Added

  • Device awareness on pointer events. Plushie.Type.Pointer module with pointer_type (:mouse/:touch/:pen) and button types. Every pointer event includes pointer type, modifier state, and finger ID for touch.

  • Window-qualified selector syntax. "main#form/save" targets a widget in a specific window. Works in test selectors and commands (Command.focus, Command.scroll_to, etc.).

  • Widget state re-render. When a widget's handle_event/2 returns {:update_state, new_state}, the view is immediately re-rendered. Previously required an unrelated event to trigger the re-render.

  • Mock canvas element click. click("#canvas-id/element-id") works in mock mode tests by detecting scoped IDs and verifying element existence.

  • Mock sequential click fix. Sequential clicks on different widgets now work reliably in mock mode (synthetic event path replaces fragile focus+space approach).

  • Coalescing for pointer events. :move (Replace), :scroll (Accumulate deltas), :scrolled (Replace), :resize (Replace).

Fixed

  • Widget re-render on state change with window sync and error revert.
  • Pre-existing decoder bugs: transition_complete missing from specs and missing scope extraction, sort data shape mismatch, pane_focus_cycle spec/decoder mismatch.
  • plushie.build patches vendored iced subcrates when local source checkout exists. Handles file read errors in Cargo.toml parsing.

Changed

  • Documentation overhaul. README rewritten, docs/README.md index added for hexdocs, CONTRIBUTING.md created. All em-dashes replaced with single dashes. Comprehensive docs for pointer events, device awareness, canvas touch, modifier patterns, window scope, and selector syntax.

[0.5.0] - 2026-03-23

Breaking

  • Renderer binary renamed from plushie to plushie-renderer. The binary resolution chain, download task, and build task all use the new name. The bin/plushie-renderer symlink replaces the old bin/plushie.
  • WASM files renamed from plushie_wasm.* to plushie_renderer_wasm.*. Update any HTML script tags that reference the old names.

Added

  • --bin-file PATH and --wasm-dir PATH options on mix plushie.build, matching the existing options on mix plushie.download. Override output locations for both stock and extension builds.
  • Canvas group redesign - groups now support role and arrow_mode props for accessible tree/list patterns in canvas.
  • Block-form options on Canvas widget - role and arrow_mode can be set via the DSL block form.
  • Focus ring support - focus_ring_radius on canvas groups, focus ring padding on interactive canvas widgets.
  • Expanded test helpers - additional utility functions for test sessions.
  • Demo project links in docs - extensions guide, testing guide, running guide, getting-started guide, and examples README all link to the plushie-demos repo.

Changed

  • Renderer binary version bumped to 0.5.1.
  • Download URL updated from plushie-ui/plushie to plushie-ui/plushie-renderer releases.
  • Rust crate references updated throughout for the plushie-renderer workspace split (plushie-ext, plushie-core, plushie-renderer-lib).
  • Canvas element terminology updated across docs and code.

Fixed

  • Protocol dispatch warning in Canvas.build/1.
  • Credo string literal warnings in Canvas doc comments.
  • Star rating focus_style uses correct stroke object format.
  • Button style map removed from RatePlushie (uses default theme).
  • Review form theme contrast in RatePlushie.
  • Canvas scope walker wraps transform/clip directives as metadata.
  • Heading text colors restored for contrast.
  • Vestigial interactive field removed from leaf shape structs.

[0.4.0] - 2026-03-22

Breaking

  • Project renamed from toddy to plushie. All module names (Toddy.* -> Plushie.*), config keys (:toddy -> :plushie), environment variables (TODDY_* -> PLUSHIE_*), and mix tasks (toddy.* -> plushie.*) have changed.
  • Canvas shapes are now typed structs instead of plain maps. Builder functions (rect, circle, line, text, path, image, svg, stroke, linear_gradient) return struct instances (%Rect{}, %Circle{}, etc.) with Plushie.Encode protocol implementations. Code that pattern-matched on %{type: "rect"} must use %Rect{}.
  • import Plushie.Canvas.Shape no longer needed for group, layer, or interactive in canvas blocks. Use import Plushie.UI instead. Canvas text, image, and svg calls are automatically resolved inside canvas/layer/group blocks.
  • Test backend renamed: :pooled_mock -> :mock, Plushie.Test.Backend.Pooled -> Plushie.Test.Backend.MockRenderer. The env var value changes: PLUSHIE_TEST_BACKEND=mock (was pooled_mock). Plushie.Test.MockBridge renamed to Plushie.Test.InternalMockBridge (@moduledoc false).
  • Padding.encode/1 renamed to Padding.cast/1 (normalization, not wire encoding).

Added

  • Block-form options for all widgets. Every leaf widget and canvas shape supports a do-block syntax for declaring options:
    button "save", "Save" do
      style :primary
      padding %{top: 10, bottom: 10}
    end
  • Container inline props. Container widgets (column, row, container, etc.) accept option declarations directly in their do-blocks, mixed with children:
    column do
      spacing 8
      padding 16
      text("Hello")
    end
  • Nested do-blocks for struct-typed options. Options like padding, a11y, border, shadow, and style support nested do-blocks that construct typed structs:
    container "card" do
      border do
        width 1
        color "#ddd"
        rounded 8
      end
      shadow do
        color "#0000001a"
        offset_y 2
        blur_radius 8
      end
      text("Content")
    end
  • interactive directive with id-first syntax, keyword form, block form, and pipe form for canvas shape interactivity.
  • Plushie.DSL.Buildable behaviour - formal contract for types participating in the DSL block-form pattern (from_opts/1, __field_keys__/0, __field_types__/0).
  • Compile-time validation everywhere. All widget block forms validate option keys at compile time. Using an option that doesn't belong to the current widget produces a helpful error. Canvas blocks validate every call against its context (canvas/layer/group).
  • Context-aware canvas_scope walker validates and rewrites calls inside canvas blocks. Wrong-arity text/image/svg calls, widget macros, and misplaced shapes produce compile-time errors.
  • Context-aware container_scope walker validates container options. Using an option on the wrong container lists which containers support it.
  • New value structs - ShapeStyle (hover/pressed overrides), DragBounds, HitRect, Dash, plus Padding and Font converted from utility types to proper structs.
  • Extension DSL integration - extension widgets automatically generate Buildable callbacks and option metadata from prop declarations.
  • Tree normalizer leak detection - shape structs and DSL metadata tuples in the widget tree produce clear error messages.
  • Event coalescing - max_rate on subscriptions, event_rate on widgets, host-side pending coalesce buffer for mouse moves and sensor resizes.
  • Three transport modes - :spawn (default), :stdio (for plushie --exec), and {:iostream, pid} (for SSH/TCP/custom).
  • Canvas interactive shapes - renderer-side hit testing with click, hover, drag, focus events via the interactive field on shapes.
  • docs/dsl-internals.md - maintainer guide for the DSL architecture, Buildable behaviour, and scope walkers.
  • --wasm flag for mix plushie.download and mix plushie.build.
  • bin/plushie symlink created by mix plushie.download for stable path references without the platform-specific name.
  • mix plushie.connect replaces mix plushie.stdio. Connects to the renderer via Unix socket or TCP instead of stdin/stdout. Token auth via Settings message.
  • Doc-sync tests linking doc code blocks to test functions via HTML comment markers.

Changed

  • Downloaded binaries moved from priv/bin/ to _build/plushie/bin/. Build artifacts belong in _build/ where mix clean removes them.

  • mix plushie.stdio renamed to mix plushie.connect. The old stdin/stdout transport is still available as a fallback when PLUSHIE_SOCKET is not set.

  • Plushie.UI is now the single macro/DSL layer. All shape macros (rect, circle, group, layer, etc.), path commands, transforms, clips, and gradients are available via import Plushie.UI.

  • Plushie.Canvas.Shape is now a pure-function module (no macros). Import it directly only for helper functions outside canvas blocks.

  • All Encode protocol implementations moved to their respective struct module files. Plushie.Encode contains only the protocol definition and primitive implementations.

  • Widget struct field types tightened to reference specific type modules (e.g., Plushie.Type.Padding.t() instead of term()).

  • Canvas widget type annotations use canvas_shape() union type.

  • @widget_calls derived from component lists instead of manually maintained.

  • Doc code examples use use Plushie.App instead of @behaviour Plushie.App (the latter misses default implementations of optional callbacks like window_config/1).

[0.3.0] - 2026-03-19

Initial public release.

Added

  • Elm architecture - init/1, update/2, view/1, optional subscribe/1 callbacks via the Plushie.App behaviour.
  • 38 built-in widget types - layout (column, row, container, scrollable, stack, grid, pane_grid), display (text, rich_text, markdown, image, svg, progress_bar, qr_code, rule, canvas), input (button, text_input, text_editor, checkbox, radio, toggler, slider, vertical_slider, pick_list, combo_box, table), and wrappers (tooltip, pointer_area, sensor, overlay, responsive, themer, keyed_column, space, floating, pin, window).
  • 22 built-in themes - light, dark, dracula, nord, solarized, gruvbox, catppuccin, tokyo night, kanagawa, moonfly, nightfly, oxocarbon, ferra. Custom palettes and per-widget style overrides via Plushie.Type.StyleMap.
  • Multi-window - declare window nodes in the widget tree; the framework manages open/close/update automatically.
  • Platform effects - native file dialogs, clipboard (text, HTML, primary selection), OS notifications.
  • Accessibility - screen reader support via accesskit on all platforms. A11y props on all widgets.
  • Commands - async work, streaming, timers, widget ops (focus, scroll, select), window management (25+ operations), image management, platform effects, extension commands.
  • Subscriptions - timers, keyboard, mouse, touch, IME, window lifecycle, animation frames, system theme changes.
  • 16 typed event structs - Widget, Key, Mouse, Touch, Ime, Window, Canvas, PointerArea, Pane, Sensor, Effect, System, Timer, Async, Stream, Modifiers.
  • Scoped widget IDs - containers namespace children's IDs automatically. Pattern match on local ID or scope chain.
  • Three-backend test framework - mocked (fast, no display), headless (real rendering via tiny-skia, screenshots), windowed (real GPU windows). Same API across all three.
  • Extension system - pure Elixir composite widgets or Rust-backed native widgets via Plushie.Extension macro DSL.
  • Live reload - file watching in dev mode, enabled by default via mix plushie.gui. State preserved across reloads.
  • Daemon mode - Plushie.start_link(MyApp, daemon: true) keeps the process running after the last window closes.
  • Precompiled binaries - mix plushie.download fetches platform-specific binaries with mandatory SHA256 verification.
  • Build from source - mix plushie.build compiles the plushie binary, with optional extension workspace generation.
  • State helpers - Plushie.State (revision tracking), Plushie.Undo (undo/redo), Plushie.Selection (single/multi/range), Plushie.Route (navigation), Plushie.Data (query pipeline), Plushie.Animation (easing functions).
  • Canvas drawing - shape primitives (rect, circle, arc, path, text, image) with layers, gradients, opacity, and caching.
  • 8 example apps - Counter, Todo, Notes, Clock, Shortcuts, AsyncFetch, ColorPicker, Catalog.