Selkie--Widget

NAME

Selkie::Widget - Base role composed by every Selkie widget

SYNOPSIS

A minimal custom widget that renders a fixed string:

use Notcurses::Native;
use Notcurses::Native::Plane;
use Selkie::Widget;

unit class My::Hello does Selkie::Widget;

method render() {
    return without self.plane;
    ncplane_erase(self.plane);
    self.apply-style(self.theme.text);
    ncplane_putstr_yx(self.plane, 0, 0, 'Hello, Selkie!');
    self.clear-dirty;
}

Add it to any layout and it just works:

use Selkie::Sizing;

$vbox.add: My::Hello.new(sizing => Sizing.fixed(1));

DESCRIPTION

Selkie::Widget is the role at the bottom of every widget in the framework. Compose it to create your own widget; Selkie handles the tree integration, rendering cycle, focus routing, theme inheritance, store plumbing, and memory management.

You almost never construct a Selkie::Widget directly — it's a role, so you does Selkie::Widget on your own class. The framework itself composes it to build every built-in widget (Text, Button, ListView, and so on).

What you get for free

  • A notcurses plane to render into, created and destroyed for you

  • Theme inheritance from the widget tree

  • Themed plane-base painting so erase / unwritten cells show the theme background rather than the terminal default — applied on init-plane, set-store, and set-theme

  • Keybind registration and event bubbling

  • Dirty tracking so your render method only runs when needed

  • Per-widget subscription to the reactive store

  • Clean shutdown when the widget goes out of scope

What you must provide

At minimum, a render method. That's it.

What you may provide

  • handle-event — to react to keyboard or mouse input when focused

  • on-store-attached — to wire up subscriptions when the store appears

  • destroy — to clean up anything beyond the plane (e.g. extra notcurses handles, file descriptors, subscriptions)

LIFECYCLE

Construction happens in normal Raku fashion: My::Widget.new(...). At this point the widget has no plane and no size. It's safe to store configuration on the object but not to call notcurses functions.

When the widget is added to a parent layout (via $parent.add($child)), the parent calls init-plane to create a notcurses plane sized to its share of the layout. After this point self.plane returns a valid handle, and self.rows / self.cols reflect the plane's dimensions.

Each frame, the framework walks the tree and calls render on any widget whose is-dirty is true. Your render method should:

  • Erase the plane with ncplane_erase(self.plane)

  • Apply styles with self.apply-style($style)

  • Write to the plane with notcurses calls

  • Call self.clear-dirty at the end

When the widget is removed or the program exits, destroy is called and the plane is freed.

OVERRIDE POINTS

The public API is organised into three buckets.

Required override

  • render — draw yourself onto self.plane. Must be defined by the composing class.

Optional overrides

  • handle-event($ev -- Bool)> — return True if you consumed the event

  • destroy — call self.destroy-plane and clean up any extras

  • on-store-attached($store) — implement this (no inherited default) to register subscriptions

Do not override

  • init-plane, adopt-plane — called by layout containers

  • mark-dirty, clear-dirty — called by the render cycle

  • set-viewport — called by parent layouts

EXAMPLES

Example 1 — A static colored bar

The simplest useful widget. A solid block of color spanning its full size. Good for spacers or visual dividers.

use Notcurses::Native;
use Notcurses::Native::Plane;
use Selkie::Widget;

unit class My::ColorBar does Selkie::Widget;

has UInt $.color is required;   # 0xRRGGBB

method render() {
    return without self.plane;
    ncplane_set_bg_rgb(self.plane, $!color);
    ncplane_erase(self.plane);
    self.clear-dirty;
}

Use it like:

$vbox.add: My::ColorBar.new(color => 0xFF5555, sizing => Sizing.fixed(1));

Example 2 — A focusable toggle that emits on change

A box that flips a boolean when the user presses Space or Enter. The state is owned by the widget; interested app code subscribes by tapping the on-toggle Supply. This is the canonical leaf-widget pattern in Selkie — widgets emit, app code dispatches to the store.

use Notcurses::Native;
use Notcurses::Native::Plane;
use Notcurses::Native::Types;
use Selkie::Widget;
use Selkie::Event;

unit class My::Toggle does Selkie::Widget;

has Bool $.state = False;
has Supplier $!toggle-supplier = Supplier.new;

method new(*%args --> My::Toggle) {
    # Focusable by default, so Tab can reach us
    %args<focusable> //= True;
    callwith(|%args);
}

method on-toggle(--> Supply) { $!toggle-supplier.Supply }

method toggle() {
    $!state = !$!state;
    $!toggle-supplier.emit($!state);
    self.mark-dirty;
}

method render() {
    return without self.plane;
    my $style = self.theme.text;
    self.apply-style($style);
    ncplane_erase(self.plane);
    my $glyph = $!state ?? 'ā—' !! 'ā—‹';
    ncplane_putstr_yx(self.plane, 0, 0, $glyph);
    self.clear-dirty;
}

method handle-event(Selkie::Event $ev --> Bool) {
    # Only respond when we have focus — the app routes events to the
    # focused widget and up the parent chain.
    return False unless $ev.event-type ~~ KeyEvent;
    if $ev.id == NCKEY_ENTER || $ev.id == NCKEY_SPACE {
        self.toggle;
        return True;
    }
    # Pass through to any registered custom keybinds
    self!check-keybinds($ev);
}

Consuming app code:

my $toggle = My::Toggle.new(sizing => Sizing.fixed(1));
$vbox.add($toggle);
$toggle.on-toggle.tap: -> Bool $on {
    # Tap fires whenever state flips. Dispatch to the store from here.
    $app.store.dispatch('setting/changed', value => $on);
};

Example 3 — Registering a custom keybind

Widgets can register per-instance keybinds with on-key. These fire when the widget is focused (or, if unfocused, are available for the parent chain to delegate to). Useful for shortcuts scoped to a specific view.

my $list-view = Selkie::Widget::ListView.new(sizing => Sizing.flex);

# 'a' on the list triggers "add"
$list-view.on-key: 'a', -> $ {
    open-add-dialog();
};

# 'd' with the list focused deletes the cursor item
$list-view.on-key: 'd', -> $ {
    delete-current-item();
};

Keybinds with a modifier (ctrl+, alt+, super+) work even when a text input is focused — the input lets modified keys bubble up. Bare character keybinds get consumed by text inputs, so reserve them for list-style widgets.

Example 4 — A widget that reacts to store state

When a widget's appearance depends on shared application state, subscribe to the store from on-store-attached. The framework calls this once per set-store call, so use once-subscribe / once-subscribe-computed to avoid duplicate registrations across repeated calls.

use Selkie::Widget;

unit class My::UnreadBadge does Selkie::Widget;

has UInt $!count = 0;

method on-store-attached($store) {
    # Idempotent: won't double-register if on-store-attached is called
    # again (e.g. if this widget is reparented).
    self.once-subscribe-computed('unread-count', -> $s {
        $s.get-in('inbox', 'unread') // 0;
    });
}

method render() {
    return without self.plane;
    # Re-read fresh from the store each render; the subscription just
    # ensures we're re-rendered when the value changes.
    $!count = self.store.get-in('inbox', 'unread') // 0 if self.store;
    my $style = self.theme.text-highlight;
    self.apply-style($style);
    ncplane_erase(self.plane);
    my $badge = $!count > 0 ?? "($!count)" !! '';
    ncplane_putstr_yx(self.plane, 0, 0, $badge);
    self.clear-dirty;
}

SEE ALSO

has Int $.widget-id

A monotonically-increasing integer identifier unique to each widget instance. Assigned at construction and never changes. Useful as a key when you need identity-stable references in subscriptions or debug output.

has Selkie::Widget $.parent

The containing widget, set by the parent layout when this widget is added to it. Read-only in practice — layouts manage this — but exposed as is rw so internal helpers can reparent.

has Selkie::Sizing::Sizing $.sizing

How this widget wants to be sized by its parent layout. See Selkie::Sizing: Sizing.fixed($n), Sizing.percent($n), or Sizing.flex($n = 1). Defaults to Sizing.flex.

has Bool $.focusable

Whether this widget can receive focus via Tab / Shift-Tab cycling or direct $app.focus($widget) calls. Leaf input widgets typically override this to True in their new method: method new(*%args) { %args //= True; callwith(|%args); }

has Bool $.clip-to-ancestors

When True (the default), this widget's Selkie::EffectiveBounds is computed as the intersection of its plane with every ancestor's plane and the terminal viewport. Sprixel-bearing widgets (Selkie::Widget::Image and any custom widget that allocates its own blit plane) use this to size their blit-plane to the visible region only — pixels never paint outside an ancestor's bounds, even though notcurses doesn't enforce that itself. Set False on a widget that intentionally paints outside its parent's visible area (e.g. a popup / dropdown / portal-style overlay that escapes its container's footprint). The intersection-with-terminal-viewport step still applies — pixels never paint past the terminal's edge regardless.

method plane

method plane() returns Notcurses::Native::Types::NcplaneHandle

Returns the notcurses plane this widget renders to, or the type object NcplaneHandle if the widget has not been added to a parent yet. Always guard with return without self.plane; at the top of render.

method rows

method rows() returns UInt

Current row height of the widget's plane.

method cols

method cols() returns UInt

Current column width of the widget's plane.

method y

method y() returns UInt

Y offset relative to the parent plane.

method x

method x() returns UInt

X offset relative to the parent plane.

method is-dirty

method is-dirty() returns Bool

True if this widget needs to be re-rendered on the next frame.

method abs-y

method abs-y() returns Int

Absolute Y position on the screen — the parent layout computes this by accumulating its own abs-y with this widget's local offset. Useful for overlay positioning.

method abs-x

method abs-x() returns Int

Absolute X position on the screen. See abs-y.

method viewport-rows

method viewport-rows() returns UInt

Number of rows actually visible on screen — may be smaller than rows if a parent ScrollView is clipping us.

method viewport-cols

method viewport-cols() returns UInt

Number of columns actually visible on screen. See viewport-rows.

method effective-bounds

method effective-bounds() returns Selkie::EffectiveBounds

Compute this widget's Selkie::EffectiveBounds — the rectangular intersection of its plane with every ancestor's plane and the terminal viewport. This is the on-screen rectangle into which the widget may safely paint pixels; anything outside would bleed past an ancestor's visible region (notcurses doesn't clip child planes to parents). When $!clip-to-ancestors is False, the ancestor walk is skipped and only the terminal-viewport intersection applies — useful for portal-style overlays that intentionally escape their container. Cheap by construction: O(depth) attribute reads + intersections, no allocations beyond the returned value class. Called per frame by Selkie::Widget::Image's blit-plane sizing path.

method set-viewport

method set-viewport(
    :$abs-y! is raw,
    :$abs-x! is raw,
    Int :$rows! where { ... },
    Int :$cols! where { ... }
) returns Mu

Called by parent layouts during layout. Propagates absolute screen position and visible bounds to this widget. You don't call this yourself unless you're implementing a layout container. Marks the widget dirty when its absolute position changes. Most widgets render position-independent cells, so this is redundant for them — but Image needs it: notcurses sprixels don't follow plane moves, and Image's blit-plane teardown only happens inside its render. If a parent shifts a card around (CardList scroll, screen layout reflow) without independently dirtying the subtree, the Image's render won't fire and the sprixel ghosts at the old screen coordinates. Marking dirty here ensures the next pass re-runs every affected widget; Image's cache check then short-circuits the re-blit when its own state didn't change.

method theme

method theme() returns Selkie::Theme

The effective theme for this widget. Walks up the parent chain until it finds a widget with an explicit theme, falling back to Selkie::Theme.default. Use this in render rather than caching a theme reference, so theme changes propagate correctly.

method set-theme

method set-theme(
    Selkie::Theme $t
) returns Mu

Override the theme for this widget and its subtree. Repaints this widget's plane base, marks it dirty, then recurses into .children and .content so every descendant's plane base is repainted too. The recursion matters: ncplane_erase on a child plane fills with that child's base cell, which was set the first time set-theme or set-store ran on it. Without the cascade here, only the root on which the caller invoked set-theme would repaint, and any cell a descendant didn't explicitly write would keep showing the OLD theme background — the visible symptom is "I changed theme and the tab bar / hint footer kept the old colour". Equivalent shape to set-store just below — same self.can detection so containers (children) and decorators (content) are both reached without coupling Widget to either role.

method sync-plane-base

method sync-plane-base() returns Mu

Paint this widget's plane base cell using the active theme's base style so ncplane_erase and any cell the widget doesn't explicitly write will carry the theme's background / foreground rather than notcurses's default-empty state (which renders as the terminal's own default). Safe to call repeatedly and before the plane or theme are ready — no-op in those cases.

method set-sizing

method set-sizing(
    Selkie::Sizing::Sizing $s
) returns Mu

Replace the widget's sizing constraint after construction. The parent layout picks up the new value on its next reflow. Useful for conditional UI — a form field that should disappear under one mode can be set to Sizing.fixed(0) to collapse out of the flow without removing it from the widget tree. Subclasses with height-driven content (e.g. MultiLineInput growing as the user types) call this from inside their own re-measure logic.

method store

method store() returns Mu

The Selkie::Store attached to this widget, or Nil if no store has been set yet. Propagates automatically from parent to child.

method set-store

method set-store(
    $store
) returns Mu

Attach a store to this widget. Called automatically by parent containers when a widget is added to the tree and a store exists. Recursively propagates to children and Border/Modal content. Fires on-store-attached on the widget if implemented. You shouldn't need to call this directly — just add the widget to a tree that has a store.

method dispatch

method dispatch(
    Str:D $event,
    *%payload
) returns Mu

Convenience for dispatching a store event. Equivalent to self.store.dispatch($event, |%payload) but gracefully no-ops if no store is attached. Most widgets shouldn't dispatch directly — prefer emitting on a Supply and letting app code dispatch.

method subscribe

method subscribe(
    Str:D $id,
    *@path
) returns Mu

Subscribe this widget to a path in the store. When the value at that path changes, the widget is marked dirty and re-renders. See Selkie::Store for details. Typically called from on-store-attached.

method subscribe-computed

method subscribe-computed(
    Str:D $id,
    &compute
) returns Mu

Subscribe to a computed value derived from the store. The compute function receives the store and should return the value; the widget is marked dirty whenever that value changes. See Selkie::Store.

method once-subscribe

method once-subscribe(
    Str:D $id,
    *@path
) returns Mu

Idempotent version of subscribe. Tracks per-id registration for the currently attached store so repeated same-store set-store calls don't create duplicate subscriptions. Prefer this over subscribe when registering from on-store-attached.

method once-subscribe-computed

method once-subscribe-computed(
    Str:D $id,
    &compute
) returns Mu

Idempotent version of subscribe-computed. See once-subscribe.

method update-sizing

method update-sizing(
    Selkie::Sizing::Sizing $s
) returns Mu

Update the widget's sizing declaration at runtime and request a re-layout. Use this when a widget's desired size changes — for example, a MultiLineInput growing as the user types more lines.

method init-plane

method init-plane(
    Notcurses::Native::Types::NcplaneHandle $parent-plane,
    Int :$y where { ... } = 0,
    Int :$x where { ... } = 0,
    Int :$rows where { ... } = 1,
    Int :$cols where { ... } = 1
) returns Mu

Create and take ownership of a notcurses plane, sized and positioned as specified. Called by parent layouts when they mount this widget. Override-safe: layout containers call this, leaf widgets never do.

method adopt-plane

method adopt-plane(
    Notcurses::Native::Types::NcplaneHandle $plane,
    Int :$rows where { ... },
    Int :$cols where { ... }
) returns Mu

Borrow an existing plane (owned elsewhere) as this widget's plane. Used by Selkie::App to adopt the notcurses stdplane as the root screen's plane. This widget will not destroy the plane on cleanup. Rarely used outside the framework itself.

method mark-dirty

method mark-dirty() returns Mu

Mark this widget dirty so it re-renders on the next frame. Also propagates dirty upwards to the parent chain so the render walk reaches it. Cheap — short-circuits if already dirty. Call this whenever your widget's visual state changes.

method mark-dirty-tree

method mark-dirty-tree() returns Mu

Recursively mark this widget and every descendant dirty. Use when a state change has layout implications that the default up-propagating mark-dirty can't fully express — for example, a widget resizing itself causes every sibling's allocation to shift, and you want every descendant (not just the ancestors) to re-render fresh on the next frame. Pairs with mark-screen-dirty for the common "start from the root of the attached tree" case.

method mark-screen-dirty

method mark-screen-dirty() returns Mu

Walk up to the root of the attached tree and flag the whole screen for a full render pass (via mark-dirty-tree). Use when a local state change should invalidate every widget's layout — typically a dynamically-sized widget whose height or width just changed in a way that shifts its siblings' allocations. Cheap for rare events (rare meaning: not per-keystroke). For high-frequency triggers, prefer the default mark-dirty propagation and let each render walk figure out what actually needs redrawing.

method clear-dirty

method clear-dirty() returns Mu

Clear the dirty flag. Call this as the last line of your render method so the widget is skipped on subsequent frames until something changes.

method apply-style

method apply-style(
    Selkie::Style $style
) returns Mu

Apply a Selkie::Style (fg, bg, bold/italic/underline) to the widget's plane so subsequent ncplane_putstr_yx calls pick up those attributes. Handles the three distinct notcurses calls (styles, fg, bg) in one shot.

method resize

method resize(
    Int $rows where { ... },
    Int $cols where { ... }
) returns Mu

Resize the widget's plane to new dimensions. No-ops if the size is unchanged. Called by parent layouts — you shouldn't call this directly from a leaf widget.

method handle-resize

method handle-resize(
    Int $rows where { ... },
    Int $cols where { ... }
) returns Mu

The framework's explicit terminal-resize protocol. Called when the terminal is resized; cascades through containers so every widget learns its new dimensions before the next render. Short-circuits when dims are unchanged — safe to call redundantly. Default implementation just delegates to resize(); that's enough for leaf widgets. Containers override to cascade to their own children/content. Prefer this over resize() when propagating a resize event from outside the layout pass. The built-in containers call handle-resize on children from their layout-children pass so the on-resize hook fires for any widget whose dims actually changed. Custom containers should override to cascade to their own children/content. If you hold child widgets in something other than self.children (e.g. CardList's item hashes, Border's content), your override is the only way the cascade reaches them.

method on-resize

method on-resize() returns Mu

Optional hook called from handle-resize when dimensions actually changed. Use for widget-specific bookkeeping that must update at the moment of resize rather than on the next render — recomputing cached wrap tables, invalidating pre-rendered buffers, resetting scroll offsets that no longer make sense, etc. Default is a no-op. Called after the plane has been resized and mark-dirty has fired.

method reposition

method reposition(
    Int $y where { ... },
    Int $x where { ... }
) returns Mu

Move the widget's plane to new coordinates (relative to the parent plane). No-ops if the position is unchanged. Called by parent layouts.

method render

method render() returns Mu

Render this widget to its plane. Required override: the composing class must provide a body. Always guard with return without self.plane, and call self.clear-dirty at the end.

method park-y

method park-y() returns Int

Park the widget off-screen — used by container swap operations (e.g. Border.set-content(:!destroy)) when an outgoing widget needs to keep its state but stop appearing on the terminal. Default implementation repositions the widget's plane to a far-off Y so notcurses clips it. Override in widgets that own other notcurses resources whose visibility doesn't follow plane position — most importantly Image, where the blit-plane carries a sprixel (Sixel/Kitty pixel image) that the terminal renders at an absolute on-screen position and won't clear just because the parent moved. Such widgets need to destroy their auxiliary plane(s) here so the sprixel gets removed from the terminal. Containers should override to recurse: park self + each descendant. Off-screen Y coordinate used by park to clip a widget's plane out of the visible terminal. Notcurses clips planes whose origin falls beyond the rendered bounds, so any sufficiently large value works; we standardise on 10,000 across Container, CardList, and any custom container override so the parked-Y is greppable and predictable in snapshot tests. Implemented as a method (rather than an our constant) because Raku doesn't allow our-scoped symbols inside a role — the role is parametric, so there's no single package to install the symbol in.

method park-children

method park-children(
    @kids
) returns Nil

Park each widget in @kids. Used by Container.park and Selkie::Widget::CardList.park — both walk a list of child widgets calling .park on each, but they store their children differently (Container in @!children, CardList in @!items), so the iteration is the only thing they share. Private to the Widget role; consumers compose Widget so they can call self!park-children(@kids).

method on-key

method on-key(
    Str:D $spec,
    &handler,
    Str :$description = ""
) returns Mu

Register a keybind for this widget. Fires when the widget has focus and an unconsumed event matches the spec. See Selkie::Event for the spec syntax ('a', 'ctrl+q', 'shift+tab', etc). Pass :description to surface the bind in Selkie::Widget::HelpOverlay. Binds without a description still work — they just don't appear in the help listing. Example: { delete-item }, :description('Delete'); { refresh }, :description('Refresh');

method on-click

method on-click(
    &handler,
    Int :$button where { ... } = 1,
    Str :$description = ""
) returns Mu

Register a click handler. Fires on a mouse button press whose cell falls within this widget's on-screen rectangle (per abs-y, abs-x, viewport extents). Default button is 1 (primary). The handler receives the Selkie::Event; use self.local-row($ev) and self.local-col($ev) for widget-local coordinates. Click handlers receive press events only — release is delivered via on-mouse-up if you need it. The click-count field on the event distinguishes single (1), double (2), and triple (3) clicks within the framework's 300 ms window.

method on-scroll

method on-scroll(
    &handler,
    Str :$description = ""
) returns Mu

Register a scroll-wheel handler. Fires on scroll-up (NCKEY_SCROLL_UP) and scroll-down (NCKEY_SCROLL_DOWN) events whose cell falls within this widget's on-screen rectangle. The handler receives the Selkie::Event; check $ev.id for direction.

method on-drag

method on-drag(
    &handler,
    Int :$button where { ... } = 1,
    Str :$description = ""
) returns Mu

Register a drag handler. Fires on motion events while the given button is held — the press that started the drag is delivered to on-click (or on-mouse-down); subsequent motion-while-held events come here regardless of whether the cursor has left the widget's bounds. Release is delivered via on-mouse-up and automatically clears the drag capture.

method on-mouse-down

method on-mouse-down(
    &handler,
    Int :$button where { ... } = 1,
    Str :$description = ""
) returns Mu

Register a low-level mouse-down handler. Fires on every press, regardless of button (defaults to 1 — pass :button(0) to listen on any button). Use this when you need to react to the press itself rather than the higher-level "click" abstraction.

method on-mouse-up

method on-mouse-up(
    &handler,
    Int :$button where { ... } = 1,
    Str :$description = ""
) returns Mu

Register a mouse-up (release) handler. Fires on every release, including releases that end a drag (in which case it fires after the drag capture has already been cleared). Default button is 1.

method mouse-handlers

method mouse-handlers() returns List

Read-only access to this widget's registered mouse handlers. Used by the framework's mouse dispatcher.

method local-row

method local-row(
    Selkie::Event $ev
) returns Int

Translate an absolute-screen mouse event into this widget's local Y coordinate (0-based, top-down). Returns -1 when the event falls outside the widget's viewport, so callers can guard with a single check.

method local-col

method local-col(
    Selkie::Event $ev
) returns Int

Translate an absolute-screen mouse event into this widget's local X coordinate. See local-row.

method contains-point

method contains-point(
    Int $y,
    Int $x
) returns Bool

True iff the given absolute-screen cell falls within this widget's on-screen rectangle (taking viewport clipping into account). The framework uses this for mouse hit-testing; widgets rarely need to call it directly. A widget with zero viewport dimensions never contains any point — that's how we filter out unmounted widgets and parked-off-screen widgets without needing to consult the plane handle.

method claims-overlay-at

method claims-overlay-at(
    Int $y,
    Int $x
) returns Bool

True iff this widget paints an overlay region that extends past its nominal rect (per contains-point) AND the given cell falls within that overlay. The framework's mouse dispatcher does an overlay-pass against the entire tree before the normal containment walk, so widgets that paint over the layout flow can still claim clicks the layout-aware walk would miss. The canonical consumer is Selkie::Widget::Select: an open dropdown is rendered as a notcurses child plane that paints over whatever widget sits below the Select in its layout, and the widget tree doesn't know about that overdraw. By overriding claims-overlay-at, the Select can capture clicks on the dropdown rows even though its parent layout's bounds end at the Select's closed-display row. Default returns False; overlay widgets opt in.

method dispatch-mouse-handlers

method dispatch-mouse-handlers(
    Selkie::Event $ev
) returns Bool

Internal: dispatch a MouseEvent to any registered handlers on this widget. Returns True if a handler consumed the event, False to let it bubble up. The framework calls this from the default handle-event when the event is a MouseEvent; widgets that override handle-event with their own mouse switch can skip this and handle the event raw, or call it explicitly to mix the registration API with their own logic. A press event fans out to both 'click' and 'mouse-down' handlers in registration order; first to return True consumes. Release events fire 'mouse-up'. Drag motion (buttons held) and pure motion (when a drag capture is active upstream) fire 'drag'. Scroll wheel fires 'scroll'.

method keybinds

method keybinds() returns List

Read-only access to this widget's registered keybinds. Used by HelpOverlay to render a listing for the focused widget chain.

method keybind-chain

method keybind-chain() returns List

Walk this widget's ancestor chain (self → parent → … → root) collecting every keybind whose :description is non-empty. Returns a list of { spec = Str, description => Str }> hashes in focused-leaf-first order. Identical specs further up the chain are deduplicated — the most-specific (leaf-closest) binding wins, mirroring the order in which event dispatch would actually invoke handlers. Cycle-safe: if the parent chain loops back on itself (pathological reparenting bug, but observed in test rigs), the walk stops at the first repeat. Useful for any "what shortcuts are reachable from here" UI — keybind footers, status bars, tooltips, command palettes. The grouped-by-class shape used by Selkie::Widget::HelpOverlay is a separate private helper because the overlay wants section headers per widget class; flat consumers want a flat list.

method handle-event

method handle-event(
    Selkie::Event $ev
) returns Bool

Handle a keyboard or mouse event. Return True if the event was consumed (the event will stop bubbling to the parent); False to let it continue up the chain. The default implementation routes MouseEvents through any handlers registered via on-click, on-scroll, on-drag, on-mouse-down, on-mouse-up, and falls through to the keybind table (registered via on-key) for everything else. Override to implement cursor movement, character input, or widget-specific click handling. Overrides typically split the event into Mouse and non-Mouse branches: =begin code :lang method handle-event(Selkie::Event ev.event-type ~~ MouseEvent { return True if self!dispatch-mouse-handlers(!focused; # ...keyboard handling... } =end code That call to self!dispatch-mouse-handlers reuses the same registration API the base uses (handlers registered via on-click, on-scroll, on-drag, on-mouse-down, on-mouse-up); your override doesn't need to re-implement mouse-event classification. Override only the keyboard branch. A note on nextsame: it does not work for delegating Mouse handling back to this default. Selkie::Widget is a role, and Raku flattens role methods into the consuming class — so a subclass override of handle-event shadows the role version rather than inheriting it as a separate dispatch candidate, and nextsame from the override has no role-supplied candidate to fall through to. Always call self!dispatch-mouse-handlers($ev) explicitly when you need the registration-API behaviour.

method destroy

method destroy() returns Mu

Release any resources held by this widget. The default implementation destroys the plane. Override if your widget owns extra notcurses handles (e.g. ncvisual, child planes) or other resources — and always call self.destroy-plane (or self!destroy-plane if you're inside the same role/class) as the last step.

Selkie v0.10.0

High-level TUI framework built on Notcurses

Authors

  • Matt Doughty

License

Artistic-2.0

Dependencies

Notcurses::Native:ver<0.4.0+>:auth<zef:apogee>

Test Dependencies

Provides

  • Selkie
  • Selkie::App
  • Selkie::App::Internal::Dispatch
  • Selkie::App::Internal::ErrorLog
  • Selkie::App::Internal::FocusTree
  • Selkie::App::Internal::HitTest
  • Selkie::App::Internal::IdleBudget
  • Selkie::App::Internal::OverlayTree
  • Selkie::App::Internal::RenderLoop
  • Selkie::App::Internal::ScreenModalLifecycle
  • Selkie::App::Internal::Terminal
  • Selkie::App::Internal::TerminalSequences
  • Selkie::Container
  • Selkie::EffectiveBounds
  • Selkie::Event
  • Selkie::Layout::Allocate
  • Selkie::Layout::HBox
  • Selkie::Layout::Split
  • Selkie::Layout::VBox
  • Selkie::Plot::Palette
  • Selkie::Plot::Scaler
  • Selkie::Plot::Ticks
  • Selkie::ScreenManager
  • Selkie::Sizing
  • Selkie::Store
  • Selkie::Style
  • Selkie::Test::Focus
  • Selkie::Test::Keys
  • Selkie::Test::Snapshot
  • Selkie::Test::Snapshot::Harness
  • Selkie::Test::Store
  • Selkie::Test::Supply
  • Selkie::Test::Tree
  • Selkie::Theme
  • Selkie::Trace
  • Selkie::Tree
  • Selkie::Widget
  • Selkie::Widget::Axis
  • Selkie::Widget::BarChart
  • Selkie::Widget::Border
  • Selkie::Widget::Button
  • Selkie::Widget::CardList
  • Selkie::Widget::Checkbox
  • Selkie::Widget::CommandPalette
  • Selkie::Widget::ConfirmModal
  • Selkie::Widget::FileBrowser
  • Selkie::Widget::FocusableByDefault
  • Selkie::Widget::Heatmap
  • Selkie::Widget::HelpOverlay
  • Selkie::Widget::Histogram
  • Selkie::Widget::Image
  • Selkie::Widget::Legend
  • Selkie::Widget::LineChart
  • Selkie::Widget::ListView
  • Selkie::Widget::Modal
  • Selkie::Widget::MultiLineInput
  • Selkie::Widget::PasswordStrength
  • Selkie::Widget::Plot
  • Selkie::Widget::ProgressBar
  • Selkie::Widget::RadioGroup
  • Selkie::Widget::RichText
  • Selkie::Widget::RichText::Span
  • Selkie::Widget::ScatterPlot
  • Selkie::Widget::ScrollView
  • Selkie::Widget::Select
  • Selkie::Widget::Sparkline
  • Selkie::Widget::Spinner
  • Selkie::Widget::TabBar
  • Selkie::Widget::Table
  • Selkie::Widget::Text
  • Selkie::Widget::TextInput
  • Selkie::Widget::TextInput::HighlightSpan
  • Selkie::Widget::TextStream
  • Selkie::Widget::Toast
  • Selkie::Widget::ViewportedCardList

Documentation

The Camelia image is copyright 2009 by Larry Wall. "Raku" is trademark of the Yet Another Society. All rights reserved.