Getting Started
Prerequisites
You need Gleam 1.x and Erlang/OTP 26 or later. Plushie runs on Linux, macOS, and Windows.
Creating a project
We will build the pad application from scratch. Start with a new Gleam project:
gleam new plushie_pad
cd plushie_pad
Open gleam.toml and add plushie_gleam under [dependencies]:
[dependencies]
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
plushie_gleam = ">= 0.6.0 and < 1.0.0"
Pin the range tightly pre-1.0. The API may change between minor releases. Check the CHANGELOG when upgrading.
Fetch dependencies:
gleam deps download
Installing the renderer
Plushie apps communicate with a Rust binary (built on Iced) that handles rendering and platform input. Download the precompiled binary:
gleam run -m plushie/download
The binary lands under build/plushie/bin/ and the SDK resolves it
automatically at runtime. The download is pinned to the
plushie_rust_version key in gleam.toml, so the binary and the SDK
always match.
If you prefer to build the renderer yourself (or need to for
native widgets), see the
CLI Commands reference. You will need
a Rust toolchain and cargo-plushie installed.
Your first window
Create src/hello.gleam:
import plushie/app
import plushie/command
import plushie/event.{type Event}
import plushie/gui
import plushie/node.{type Node}
import plushie/ui
import plushie/widget/window
fn init() {
#(Nil, command.none())
}
fn update(model: Nil, _event: Event) {
#(model, command.none())
}
fn view(_model: Nil) -> List(Node) {
[
ui.window("main", [window.Title("Plushie Pad")], [
ui.text_("greeting", "Hello from Plushie"),
]),
]
}
pub fn main() {
gui.run(app.simple(init, update, view), gui.default_opts())
}
Run it:
gleam run -m 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:
app.simple(init, update, view)bundles the three callbacks that drive the Elm loop. This shape covers apps whose message type is the built-inEvent. For a custom message type, seeapp.applicationin the App Lifecycle reference.gui.runresolves the renderer binary, starts the runtime, and blocks until the app exits.ui.windowcreates a native OS window. The first argument is the window’s ID ("main"), the second is a list of options (window.Title(...), etc.), and the third is the list of child widgets. Everyviewreturns a list of windows; the empty list renders nothing.- The
-> List(Node)annotation onviewis optional. Gleam can infer the return type. ui.text_displays a read-only string. The trailing underscore marks the no-options form.ui.texttakes a third argument for typed options.
The Elm loop: a counter
Let us add interactivity. Replace src/hello.gleam with a counter:
import gleam/int
import plushie/app
import plushie/command
import plushie/event.{type Event, Click, EventTarget, Widget}
import plushie/gui
import plushie/node.{type Node}
import plushie/prop/padding
import plushie/ui
import plushie/widget/column
import plushie/widget/row
import plushie/widget/window
pub type Model {
Model(count: Int)
}
fn init() {
#(Model(count: 0), command.none())
}
fn update(model: Model, event: Event) {
case event {
Widget(Click(target: EventTarget(id: "increment", ..))) -> #(
Model(count: model.count + 1),
command.none(),
)
Widget(Click(target: EventTarget(id: "decrement", ..))) -> #(
Model(count: model.count - 1),
command.none(),
)
_ -> #(model, command.none())
}
}
fn view(model: Model) -> List(Node) {
[
ui.window("main", [window.Title("Counter")], [
ui.column(
"content",
[column.Padding(padding.all(16.0)), column.Spacing(8.0)],
[
ui.text_("count", "Count: " <> int.to_string(model.count)),
ui.row("buttons", [row.Spacing(8.0)], [
ui.button_("increment", "+"),
ui.button_("decrement", "-"),
]),
],
),
]),
]
}
pub fn main() {
gui.run(app.simple(init, update, view), gui.default_opts())
}
Run it again with gleam run -m hello. Click “+” and “-”. The count
updates on every click.
Here is what is new:
Modelis a custom type with a singlecountfield. The init function returns the initial model and acommand.none()(no side effect on startup).updatepattern-matches onWidget(Click(target: EventTarget(id: ...))). The..elides the otherEventTargetfields (scope,window_id,full); this is standard Gleam record pattern syntax. See the Events reference for the full event taxonomy.ui.columnis a vertical layout container.column.Paddingadds space around the edges,column.Spacingadds space between children.padding.all(16.0)sets equal padding on all sides; see the Built-in Widgets reference for the full prop list.ui.rowis the horizontal counterpart, same opt shape.ui.button_is the no-options button builder. First argument is the widget ID, second is the label. Clicking it emitsWidget(Click(target: EventTarget(id: "increment", ..))).
The cycle: you click “+”. The renderer sends a click event. The
runtime calls update with the current model and the event. Your
function pattern-matches on the ID, increments the count, and returns
a new model. The runtime calls view with that model, diffs the
resulting tree against the previous one, and sends patches to the
renderer. The renderer updates the display. The round trip happens in
milliseconds.
If update raises an exception, the runtime catches it, logs the
error, and reverts to the previous model. Your app keeps running.
Experimenting is safe.
Your first test
Plushie apps are easy to test. Create test/hello_test.gleam:
import gleeunit
import plushie/testing
import hello
pub fn main() {
gleeunit.main()
}
pub fn clicking_increment_updates_count_test() {
let ctx = testing.start(hello.app())
let ctx = testing.click(ctx, "increment")
testing.assert_text(ctx, "count", "Count: 1")
testing.stop(ctx)
}
For the test to call hello.app(), expose it from src/hello.gleam:
pub fn app() {
app.simple(init, update, view)
}
Keep main calling gui.run(app(), gui.default_opts()).
Run it:
gleam test
The test starts the app against the real renderer binary (in --mock
mode by default), clicks the increment button, and asserts the
display text. We will add tests throughout the guide to verify each
chapter’s work. The full testing story is covered in
chapter 15 and the
Testing reference.
Enabling hot reload
Hot reload requires the file_system Hex package and Elixir installed.
Add to your gleam.toml:
[dependencies]
file_system = ">= 1.0.0 and < 2.0.0"
Then run gleam deps download. Elixir must be installed so Gleam can
compile the file_system package (sudo apt install elixir or see
elixir-lang.org/install).
During development you want changes reflected without restarting the
app. Set dev: True on GuiOpts:
pub fn main() {
gui.run(app(), gui.GuiOpts(..gui.default_opts(), dev: True))
}
Start the app with gleam run -m hello, then change the column.Spacing
value from 8.0 to 32.0 and save. The window updates with the new
spacing and the count stays where it was. Under the hood, a dev
server watches src/ for .gleam changes, runs gleam build, and
hot-loads the changed BEAM modules without tearing down the app.
This is how we will develop throughout the guide. Keep the app running, edit code, save, and watch the window update. In chapter 4 we wire hot reload into a longer-lived development loop.
Try it
With the counter running and hot reload active, try these changes one at a time:
- Enlarge the count display: add
[text.Size(24.0)]as the third argument toui.text, replacing theui.text_call:ui.text("count", "Count: " <> int.to_string(model.count), [text.Size(24.0)]). Addimport plushie/widget/textat the top of the file. - Add a reset button. Add
ui.button_("reset", "Reset")to the row, and add a matchingupdateclause that returnsModel(count: 0). - Flip the layout. Swap
ui.columnandui.row(and their respective opt modules) to rearrange the widgets horizontally and vertically.
When you are comfortable with the init / update / view cycle and hot reload, move on to the next chapter and start building the pad.
Next: Your First App