Selkie.git | examples/ | typed-counter.raku


#!/usr/bin/env raku
#
# typed-counter.raku — counter.raku, rebuilt on the typed store.
#
# Same app as examples/counter.raku, with the state tree declared as a
# Selkie::Store::Schema class instead of a bare Hash. Demonstrates:
#   - Declaring a root schema (including the required `ui` slot)
#   - Opting a Store in via :state-class, and handing it to App via :store
#   - Handlers using the db effect (validated) and db-update (typed)
#   - Typed reads through $store.state in computes and handlers
#   - What the schema buys you: uncomment the typo'd handler at the
#     bottom to watch a bad key become a loud __effect-error instead of
#     a silently ignored state write.
#
# Run with:  raku -I lib examples/typed-counter.raku

use Selkie::App;
use Selkie::Store;
use Selkie::Store::Schema;
use Selkie::Layout::VBox;
use Selkie::Layout::HBox;
use Selkie::Widget::Text;
use Selkie::Widget::Button;
use Selkie::Style;
use Selkie::Sizing;

# --- The state tree, as a class -------------------------------------------
# Every root schema declares the framework's `ui` slot; the rest is the
# app's own state. Slots are typed where it helps — a handler that tries
# to write a Str into `count` now fails at dispatch time.

class Counter::Db is Selkie::Store::Schema {
    has Selkie::Store::Schema::UI $.ui .= new;
    has Int $.count = 0;
}

my $store = Selkie::Store.new(state-class => Counter::Db);
my $app   = Selkie::App.new(:$store);

# --- State handlers --------------------------------------------------------
# Same pure (store, payload) -> effects shape as the Hash store. The db
# effect still works (now validated against the slots); db-update is the
# typed alternative — the fn receives the schema instance and returns a
# patched clone.

$store.register-handler('counter/inc', -> $st, %ev {
    (db-update => { fn => -> Counter::Db $db { $db.with(count => $db.count + 1) } },);
});

$store.register-handler('counter/dec', -> $st, %ev {
    (db-update => { fn => -> Counter::Db $db { $db.with(count => $db.count - 1) } },);
});

$store.register-handler('counter/reset', -> $st, %ev {
    (db => { count => 0 },);
});

# --- Widget tree ----------------------------------------------------------

my $root = Selkie::Layout::VBox.new(sizing => Sizing.flex);
$app.add-screen('main', $root);
$app.switch-screen('main');

$root.add: Selkie::Widget::Text.new(
    text   => '  Selkie Typed Counter  —  Tab switches focus, Ctrl+Q quits',
    sizing => Sizing.fixed(1),
    style  => Selkie::Style.new(fg => 0x7AA2F7, bold => True),
);

$root.add: Selkie::Widget::Text.new(text => '', sizing => Sizing.flex);

my $count-text = Selkie::Widget::Text.new(
    text   => '',
    sizing => Sizing.fixed(3),
    style  => Selkie::Style.new(fg => 0xFFFFFF, bold => True),
);
$root.add($count-text);

my $row = Selkie::Layout::HBox.new(sizing => Sizing.fixed(3));
my $dec   = Selkie::Widget::Button.new(label => ' - ',     sizing => Sizing.flex);
my $inc   = Selkie::Widget::Button.new(label => ' + ',     sizing => Sizing.flex);
my $reset = Selkie::Widget::Button.new(label => ' reset ', sizing => Sizing.flex);
$row.add($dec);
$row.add($inc);
$row.add($reset);
$root.add($row);

$root.add: Selkie::Widget::Text.new(text => '', sizing => Sizing.flex);

# --- Wire buttons to dispatch ---------------------------------------------

$dec.on-press.tap:   -> $ { $store.dispatch('counter/dec') };
$inc.on-press.tap:   -> $ { $store.dispatch('counter/inc') };
$reset.on-press.tap: -> $ { $store.dispatch('counter/reset') };

# --- Subscription ---------------------------------------------------------
# The compute reads through the typed accessor — no path strings, and a
# typo here is a compile-adjacent method-not-found, not a silent Nil.

$store.subscribe-with-callback(
    'count-display',
    -> $s { $s.state.count.Str },
    -> $text { $count-text.set-text("       Count: $text") },
    $count-text,
);

# No app/init needed: the schema's declared defaults ARE the initial
# state. One synchronous tick primes the display before first render.
$store.tick;

# --- Global keybinds ------------------------------------------------------

$app.on-key('ctrl+q', -> $ { $app.quit });

# --- What the schema catches ----------------------------------------------
# Uncomment to see validation in action: the typo'd key routes through
# __effect-error (naming event + slot) instead of silently growing a
# 'cout' key the UI never reads. Try it, press '+', watch stderr after
# quitting (or register a __effect-error handler that toasts).
#
# $store.register-handler('counter/inc', -> $st, %ev {
#     (db => { cout => $st.state.count + 1 },);
# });

# --- Go -------------------------------------------------------------------

$app.focus($inc);
$app.run;