Plushie.App behaviour (Plushie v0.7.0)

Copy Markdown View Source

Behaviour for Plushie apps following the Elm architecture.

Implement this behaviour to define a desktop application. The runtime calls init/1 once at startup, then repeatedly calls update/2 and view/1 as events arrive from the renderer and subscriptions.

Minimal example

defmodule MyApp do
  use Plushie.App

  alias Plushie.Event.WidgetEvent

  def init(_opts), do: %{count: 0}

  def update(model, %WidgetEvent{type: :click, id: "increment"}),
    do: %{model | count: model.count + 1}

  def update(model, _event), do: model

  def view(model) do
    import Plushie.UI

    window "counter", title: "Counter" do
      column do
        text("count", "Count: #{model.count}")
        button("increment", "Increment")
      end
    end
  end
end

Return values

init/1 and update/2 both accept two return shapes:

  • Bare model: no side effects needed.
  • {model, command} or {model, [command]}: schedule async work, widget operations, window management, or timers. See Plushie.Command for the full command API.

Commands

Return a {model, command} tuple from update/2 or init/1 to schedule side effects:

def init(_opts) do
  model = %{data: nil, loading: true}
  {model, Command.task(fn -> load_initial_data() end, :loaded)}
end

def update(model, %WidgetEvent{type: :click, id: "save"}) do
  {model, Command.task(fn -> save_to_disk(model) end, :save_result)}
end

See Plushie.Command for the full command API and result delivery.

Subscriptions

Override subscribe/1 to receive ongoing events. The runtime diffs the subscription list after each update and starts or stops subscriptions as needed.

def subscribe(model) do
  subs = [Plushie.Subscription.on_key(:key_events)]

  if model.auto_refresh do
    [Plushie.Subscription.every(5000, :refresh) | subs]
  else
    subs
  end
end

See Plushie.Subscription for all subscription types.

Error handling

Exceptions in update/2 and view/1 are caught by the runtime and logged; the model reverts to its pre-exception state. You do not need to wrap your callbacks in try/rescue. After 100 consecutive errors, the runtime suppresses further log messages to prevent log flooding.

Optional callbacks

subscribe/1, handle_renderer_exit/2, window_config/1, and settings/0 are optional. use Plushie.App provides default implementations that can be overridden.

Summary

Types

Events delivered to update/2.

Callbacks

Called when the renderer process exits unexpectedly. Receives a %Plushie.RendererExitError{} struct with :type, :message, and :details. Return the model to use when the renderer restarts. Default: return model unchanged.

Called once at startup. Returns the initial model, optionally with commands for loading initial data or configuring the UI.

Called once at startup to provide application-level settings to the renderer.

Called after every update. Returns the list of active subscriptions. The runtime diffs this list and starts or stops subscriptions as needed. Default: [].

Called on every event. Returns the next model, optionally with commands.

Called after every update. Returns a window node or a list of window nodes.

Called on runtime startup to configure window properties. Also called on renderer restart whenever windows need re-opening. Per-window props set in the view tree override these defaults.

Types

command()

@type command() :: Plushie.Command.t() | [Plushie.Command.t()]

event()

@type event() :: Plushie.Event.t() | term()

Events delivered to update/2.

Most events are Plushie.Event structs from the renderer and subscriptions. The term() case covers bare values delivered by Command.send_after/2 and Command.dispatch/2.

model()

@type model() :: term()

window_view()

@type window_view() ::
  Plushie.Widget.Window.t() | map() | [Plushie.Widget.Window.t() | map()] | nil

Callbacks

handle_renderer_exit(model, reason)

(optional)
@callback handle_renderer_exit(model(), reason :: Plushie.RendererExitError.t()) ::
  model()

Called when the renderer process exits unexpectedly. Receives a %Plushie.RendererExitError{} struct with :type, :message, and :details. Return the model to use when the renderer restarts. Default: return model unchanged.

init(opts)

@callback init(opts :: keyword()) :: model() | {model(), command()}

Called once at startup. Returns the initial model, optionally with commands for loading initial data or configuring the UI.

settings()

(optional)
@callback settings() :: map()

Called once at startup to provide application-level settings to the renderer.

Typography

  • default_font: font specification map (same format as font props)
  • default_text_size: base text size in pixels
  • fonts: list of font file paths to load at startup

Theming

  • theme: built-in theme atom (e.g. :dark, :nord), :system to follow the OS light/dark preference, or a custom palette map. See Plushie.Type.Theme.

Rendering

  • antialiasing: boolean
  • vsync: boolean (default true). Synchronizes frame presentation with the display refresh rate. Disabling can improve performance on some platforms.
  • scale_factor: number (default 1.0). Multiplier on top of OS DPI scaling. Per-window overrides via the scale_factor window prop.

Performance

  • default_event_rate: integer, max events per second for coalescable event types (mouse moves, scroll, slider drags, etc.). Omit for unlimited. Set to 60 for most apps, lower for dashboards or remote rendering. Per-subscription max_rate and per-widget event_rate override this default.

Native widgets

  • widget_config: map of namespace -> config for native widgets. Each key matches a widget's namespace() on the Rust side. The config is passed to PlushieWidget::init() via InitCtx.config. Also configurable via config :plushie, :widget_config.
  • required_widgets: list of native widget type names this app requires the renderer to have registered. The renderer emits a required_widgets_missing diagnostic on the Settings handshake for any names it does not recognize (non-fatal; the app keeps running).

Default: %{} (renderer uses its own defaults).

subscribe(model)

(optional)
@callback subscribe(model()) :: [Plushie.Subscription.t()]

Called after every update. Returns the list of active subscriptions. The runtime diffs this list and starts or stops subscriptions as needed. Default: [].

update(model, event)

@callback update(model(), event()) :: model() | {model(), command()}

Called on every event. Returns the next model, optionally with commands.

Events are Plushie.Event structs (widget interactions, key presses, window lifecycle, async results, timer ticks, etc.) or bare terms from Command.send_after/2. Include a catch-all clause for unhandled events.

view(model)

@callback view(model()) :: window_view()

Called after every update. Returns a window node or a list of window nodes.

The top level must be explicit windows. Single-window apps return one window(...) node. Multi-window apps return a list of window(...) nodes. nil or [] is allowed when the app intentionally has no open windows.

window_config(model)

(optional)
@callback window_config(model()) :: map()

Called on runtime startup to configure window properties. Also called on renderer restart whenever windows need re-opening. Per-window props set in the view tree override these defaults.

Supported keys

title, size, width, height, position, min_size, max_size, maximized, fullscreen, visible, resizable, closeable, minimizable, decorations, transparent, blur, level, exit_on_close_request.

See Plushie.Widget.Window for details on each property.

Example

def window_config(_model) do
  %{title: "My App", width: 800, height: 600, min_size: {400, 300}}
end

Default: empty config (the renderer uses its own defaults).