defmodule Counter do
use Plushie.App
alias Plushie.Event.WidgetEvent
def init(_opts), do: %{count: 0}
def update(model, %WidgetEvent{type: :click, id: "inc"}),
do: %{model | count: model.count + 1}
def update(model, %WidgetEvent{type: :click, id: "dec"}),
do: %{model | count: model.count - 1}
def update(model, _event), do: model
def view(model) do
import Plushie.UI
window "main", title: "Counter" do
column padding: 16, spacing: 8 do
text("count", "Count: #{model.count}")
row spacing: 8 do
button("inc", "+")
button("dec", "-")
end
end
end
end
enddefmodule CounterTest do
use Plushie.Test.Case, app: Counter
test "count to two and screenshot" do
click("#inc")
click("#inc")
assert_text("#count", "Count: 2")
assert_screenshot("counter")
end
endimport gleam/int
import plushie/command
import plushie/event.{type Event, WidgetClick}
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 {
WidgetClick(window_id: "main", id: "inc", ..) ->
#(Model(count: model.count + 1), command.none())
WidgetClick(window_id: "main", id: "dec", ..) ->
#(Model(count: model.count - 1), command.none())
_ -> #(model, command.none())
}
}
fn view(model: Model) -> Node {
ui.window("main", [window.Title("Counter")], [
ui.column("content",
[column.Padding(padding.all(16.0)), column.Spacing(8)], [
ui.text_("count",
"Count: " <> int.to_string(model.count)),
ui.row("buttons", [row.Spacing(8)], [
ui.button_("inc", "+"),
ui.button_("dec", "-"),
]),
]),
])
}import gleam/option
import gleeunit/should
import plushie/testing
import plushie/testing/element
pub fn count_to_two_test() {
let ctx = testing.start(counter.app())
let ctx = testing.click(ctx, "inc")
let ctx = testing.click(ctx, "inc")
let assert option.Some(el) = testing.find(ctx, "count")
should.equal(element.text(el), option.Some("Count: 2"))
testing.screenshot(ctx, "counter")
}from dataclasses import dataclass, replace
import plushie
from plushie import ui
from plushie.events import Click
@dataclass(frozen=True, slots=True)
class Model:
count: int = 0
class Counter(plushie.App[Model]):
def init(self) -> Model:
return Model()
def update(self, model, event):
match event:
case Click(id="inc"):
return replace(model, count=model.count + 1)
case Click(id="dec"):
return replace(model, count=model.count - 1)
case _:
return model
def view(self, model):
return ui.window("main",
ui.column(
ui.text("count", f"Count: {model.count}"),
ui.row(
ui.button("inc", "+"),
ui.button("dec", "-"),
spacing=8,
),
padding=16, spacing=8,
),
title="Counter",
)from plushie.testing import AppFixture
def test_count_to_two(plushie_pool):
with AppFixture(Counter, plushie_pool) as app:
app.click("#inc")
app.click("#inc")
assert app.text("#count") == "Count: 2"
app.save_screenshot("counter")require "plushie"
class Counter
include Plushie::App
Model = Plushie::Model.define(:count)
def init(_opts) = Model.new(count: 0)
def update(model, event)
case event
in Event::Widget[type: :click, id: "inc"]
model.with(count: model.count + 1)
in Event::Widget[type: :click, id: "dec"]
model.with(count: model.count - 1)
else
model
end
end
def view(model)
window("main", title: "Counter") do
column(padding: 16, spacing: 8) do
text("count", "Count: #{model.count}")
row(spacing: 8) do
button("inc", "+")
button("dec", "-")
end
end
end
end
endclass CounterTest < Plushie::Test::Case
app Counter
test "count to two and screenshot" do
click("#inc")
click("#inc")
assert_text("#count", "Count: 2")
screenshot("counter")
end
endimport { app } from "plushie"
import { window, column, row, text, button } from "plushie/ui"
type Model = { count: number }
const inc = (s: Model): Model => ({ ...s, count: s.count + 1 })
const dec = (s: Model): Model => ({ ...s, count: s.count - 1 })
export default app<Model>({
init: { count: 0 },
view: (s) =>
window("main", { title: "Counter" }, [
column({ padding: 16, spacing: 8 }, [
text("count", `Count: ${s.count}`),
row({ spacing: 8 }, [
button("inc", "+", { onClick: inc }),
button("dec", "-", { onClick: dec }),
]),
]),
]),
})import { testWith } from "plushie/testing"
import Counter from "./counter"
testWith(Counter)("count to two", async ({ session }) => {
await session.click("#inc")
await session.click("#inc")
await session.assertText("#count", "Count: 2")
await session.screenshot("counter")
})Your app describes the UI; the renderer draws it. No Rust toolchain required — the renderer is a pre-compiled binary you download, like a database driver for native GUI.
Multiple users can connect to the same server-side app for shared dashboards, collaborative sessions, and live mirroring — no synchronization code required.
.plushie scripts for automation, demos, and smoke flows.