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/app
import plushie/command
import plushie/event.{type Event, Click, EventTarget, Widget}
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: "inc", ..))) ->
#(Model(count: model.count + 1), command.none())
Widget(Click(target: EventTarget(id: "dec", ..))) ->
#(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_("inc", "+"),
ui.button_("dec", "-"),
]),
]),
]),
]
}
pub fn app() { app.simple(init, update, view) }import gleam/option
import gleeunit/should
import plushie/testing
import plushie/testing/element
import my_app/counter
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"))
}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
enduse plushie::prelude::*;
struct Counter {
count: i32,
}
impl App for Counter {
type Model = Self;
fn init() -> (Self, Command) {
(Counter { count: 0 }, Command::none())
}
fn update(model: &mut Self, event: Event) -> Command {
match event.widget_match() {
Some(Click("inc")) => model.count += 1,
Some(Click("dec")) => model.count -= 1,
_ => {}
}
Command::none()
}
fn view(model: &Self, _widgets: &mut WidgetRegistrar) -> ViewList {
window("main")
.title("Counter")
.child(
column()
.padding(16)
.spacing(8.0)
.child(text(&format!("Count: {}", model.count)).id("count"))
.child(
row()
.spacing(8.0)
.children([button("inc", "+"), button("dec", "-")]),
),
)
.into()
}
}
fn main() -> plushie::Result {
plushie::run::<Counter>()
}#[cfg(test)]
mod tests {
use super::*;
use plushie::test::TestSession;
#[test]
fn count_to_two() {
let mut session = TestSession::<Counter>::start();
session.click("inc");
session.click("inc");
session.assert_text("count", "Count: 2");
}
}import { 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 },
update: (s) => s as Model,
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.assertScreenshot("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.