Search documentation of Plushie

# Getting Started

Copy Markdown View Source

## Prerequisites

You need Elixir 1.15 or later (with Erlang/OTP 25+). Plushie works on Linux, macOS, and Windows.

## Creating a project

We will build the pad application from scratch. Start with a new Mix project:

    mix new plushie_pad
    cd plushie_pad

Open `mix.exs` and add `:plushie` to your dependencies:

    defp deps do
      [
        {:plushie, "== 0.6.0"}
      ]
    end

Pin to an exact version pre-1.0. The API may change between minor releases. Check the CHANGELOG when upgrading.

Next, configure the formatter so the Plushie DSL macros are formatted correctly. In `.formatter.exs`:

    [
      import_deps: [:plushie],
      inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
    ]

Fetch dependencies:

    mix deps.get

## Installing the renderer

Plushie apps communicate with a Rust binary (built on Iced) that handles rendering and platform input. Download the precompiled binary:

    mix plushie.download

The binary is placed under `_build/` and Plushie resolves it automatically at runtime.

If you prefer to build the renderer yourself (or need to for native widgets), see the build instructions. You will need a Rust toolchain installed.

## Your first window

Create `lib/plushie_pad/hello.ex`:

    defmodule PlushiePad.Hello do
      use Plushie.App

      import Plushie.UI

      def init(_opts), do: %{}

      def update(model, _event), do: model

      def view(_model) do
        window "main", title: "Plushie Pad" do
          text("greeting", "Hello from Plushie")
        end
      end
    end

Run it:

    mix plushie.gui PlushiePad.Hello

A native window appears with the text "Hello from Plushie". Close the window or press Ctrl+C in the terminal to stop.

Here is what each piece does:

- `use Plushie.App` - declares that this module implements the `Plushie.App` behaviour. This gives you the `init/1`, `update/2`, and `view/1` callbacks that the runtime calls to drive your application.
- `import Plushie.UI` - brings the widget DSL into scope. Every widget you place in a view (`window`, `text`, `button`, `column`, `row`, and the rest) comes from this import.
- `window` - creates a native OS window. The first argument is the window's ID (here `"main"`). The `title:` option sets the title bar text. Every view must return at least one window.
- `text` - displays a read-only string. The first argument is the widget ID, the second is the content to display.

## The Elm loop: a counter

Let us add interactivity. Replace the contents of `lib/plushie_pad/hello.ex` with a counter:

    defmodule PlushiePad.Hello do
      use Plushie.App

      import Plushie.UI

      alias Plushie.Event.WidgetEvent

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

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

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

      def update(model, _event), do: model

      def view(model) do
        window "main", title: "Counter" do
          column padding: 16, spacing: 8 do
            text("count", "Count: #{model.count}")

            row spacing: 8 do
              button("increment", "+")
              button("decrement", "-")
            end
          end
        end
      end
    end

Run it:

    mix plushie.gui PlushiePad.Hello

Click the "+" and "-" buttons. The count updates on every click.

Here is what is new:

- `alias Plushie.Event.WidgetEvent` - `Plushie.Event.WidgetEvent` is the struct delivered when a user interacts with a widget. It carries a `type` (`:click`, `:toggle`, `:submit`, etc.) and the `id` of the widget that emitted it.
- `column` - a vertical layout container. Children stack top to bottom. `padding:` adds space around the edges, `spacing:` adds space between children.
- `row` - a horizontal layout container. Children flow left to right.
- `button` - a clickable button. The first argument is the widget ID, the second is the label text. When clicked, the runtime delivers a `%WidgetEvent{type: :click, id: "increment"}` to your `update/2`.

The cycle works like this: you click "+". The renderer sends a click event. The runtime calls `update/2` with your current model and the event. Your function pattern-matches on the ID, increments the count, and returns the new model. The runtime calls `view/1` with that new model, diffs the resulting tree against the previous one, and sends patches to the renderer. The renderer updates the display. The whole round trip happens in milliseconds.

One thing worth knowing early: if `update/2` raises an exception, the runtime catches it, logs the error, and reverts to the previous model. Your app keeps running. This makes it safe to experiment. A bad pattern match or a missing function clause will not crash the window.

## Your first test

Plushie apps are easy to test. Set up the test helper first. In `test/test_helper.exs`:

    Plushie.Test.setup!()  # starts the shared renderer backend for tests
    ExUnit.start()

Then write a test for the counter in `test/hello_test.exs`:

    defmodule PlushiePad.HelloTest do
      use Plushie.Test.Case, app: PlushiePad.Hello

      test "clicking + increments the count" do
        click("#increment")
        assert_text("#count", "Count: 1")
      end
    end

Run it:

    mix test

The test starts a real app instance, clicks the increment button, and verifies the display text changed. We will add tests throughout the guide -- just enough to verify each chapter's work. The full testing framework is covered in chapter 15.

## Enabling hot reload

During development, you want to see changes reflected immediately without restarting the application. Pass `--watch` to enable hot code reloading:

    mix plushie.gui PlushiePad.Hello --watch

Try it now: start the counter, then change the `padding:` value in `view/1` from `16` to `32`. Save the file. The window updates with the new spacing, and the count stays where it was.

This is how we will develop throughout the guide. Keep the app running, edit code, save, and see the result. In chapter 4 we will make hot reload the permanent default via a config file.

## Try it

With the counter running and hot reload active, try these changes one at a time. Save after each one and watch the window update:

- Add `size: 24` to the `text` call to make the count display larger: `text("count", "Count: #{model.count}", size: 24)`
- Add a reset button. Put `button("reset", "Reset")` in the row next to the other buttons, and add a matching `update/2` clause that sets `count` back to `0`.
- Change `column` to `row` and `row` to `column` to flip the layout. See how the same widgets rearrange with a single keyword change.

When you are comfortable with the init/update/view cycle and hot reload, you are ready for the next chapter where we start building the pad.

Next: Your First App

← Previous Page Introduction
Next Page → Your First App