Selkie.git | docs/api/ | Selkie--Tree.md


NAME
====

Selkie::Tree - Tree-walking helpers used by widgets that need to reach beyond their own subtree

SYNOPSIS
========

```raku
use Selkie::Tree;

# Mark every widget whose plane intersects this absolute screen rect
# as dirty — used by Image.destroy-blit-plane to repaint cells under
# the removed sprixel.
mark-widgets-in-rect-dirty(
    abs-y => 5,  abs-x => 10,
    rows  => 4,  cols  => 16,
);

# The active modal (or Nil), used by widgets that need to skip
# rendering when occluded.
my $modal = current-active-modal;
```

DESCRIPTION
===========

A small set of free subs that bridge between a widget and the wider tree it lives in, without requiring the widget to walk up to the [Selkie::App](Selkie--App.md) instance manually. [Selkie::App](Selkie--App.md) at init populates two class-level provider closures — one returning the live list of tree roots (active screen + modal stack + toast), the other returning the active modal — and the helpers here read through them.

This pattern keeps widgets like [Selkie::Widget::Image](Selkie--Widget--Image.md) from needing a circular import on Selkie::App while still letting them participate in app-level coordination (cell cleanup after sprixel destroy, modal occlusion checks, etc.).

`mark-widgets-in-rect-dirty` walks whole trees, so it is written to stay cheap on the trees it actually meets: it prunes any subtree whose root is parked (see `Selkie::Widget.is-parked` — notcurses carries bound child planes with their parent, so a parked subtree owns no on-screen cells), and it duck-types `children` / `content` through `nqp::can`. Both details matter for correctness as much as speed; see the sub's own documentation.

### sub set-tree-roots-provider

```raku
sub set-tree-roots-provider(
    &p
) returns Nil
```

Set the tree-roots provider — a closure returning an iterable of widget roots. `Selkie::App` calls this during init so tree-walking helpers can find the live trees without each helper needing a direct reference to the app.

### sub current-tree-roots

```raku
sub current-tree-roots() returns List
```

The current list of widget tree roots. Used internally by helpers in this module; apps don't typically call this directly.

### sub set-modal-provider

```raku
sub set-modal-provider(
    &p
) returns Nil
```

Set the active-modal provider — a closure returning the topmost open modal widget or Nil. `Selkie::App` calls this on init.

### sub current-active-modal

```raku
sub current-active-modal() returns Mu
```

The topmost open modal widget, or Nil if no modal is open.

### sub widget-occluded-by-active-modal

```raku
sub widget-occluded-by-active-modal(
    Mu $widget
) returns Bool
```

True when `$widget` is outside the active modal tree and therefore should suppress out-of-band painting such as sprixels. Widgets inside the active modal, including descendants of its content tree, are not occluded.

### sub mark-widgets-in-rect-dirty

```raku
sub mark-widgets-in-rect-dirty(
    Int :$abs-y!,
    Int :$abs-x!,
    Int :$rows! where { ... },
    Int :$cols! where { ... }
) returns Nil
```

Walk every tree root and mark dirty any widget whose absolute screen bounds intersect the given rectangle. Used by sprixel-bearing widgets after they destroy a blit-plane: the cells under the removed sprixel may belong to a widget that has nothing else changing this frame, so without an explicit dirty mark the widget won't repaint and the cells will continue to show whatever was cached pre-sprixel-removal. Called once per blit teardown. Two properties keep this affordable on the trees it actually runs against (a long chat transcript in a consumer app is ~10k widgets): =item **Parked subtrees are pruned.** A widget whose `is-parked` latch is set has been moved to `park-y`, and notcurses carries every bound descendant plane along with it — so neither it nor anything beneath it owns an on-screen cell, and none of them can intersect an on-screen rect. Skipping the subtree is not an approximation. It also breaks a feedback loop: without the prune, tearing a sprixel down re-dirties the parked cards whose stale `abs-y` still overlaps the rect, which re-renders them, which tears more sprixels down. =item **Capability tests go through `nqp::can`.** `children` and `content` are duck-typed here on purpose — `Selkie::Widget::CardList` and `ViewportedCardList` expose `children` without composing `Selkie::Container`, so a `~~ Selkie::Container` test would walk past every card in the list. `nqp::can` answers the same question as `.^can` from the method cache, without building the candidate list `.^can` returns.