Selkie.git | lib/X/Selkie/ | WidgetCycle.rakumod


=begin pod

=head1 NAME

X::Selkie::WidgetCycle - thrown when a widget's parent chain loops back on itself

=head1 SYNOPSIS

=begin code :lang<raku>

use X::Selkie::WidgetCycle;

CATCH {
    when X::Selkie::WidgetCycle {
        note "widget tree is cyclic: {.chain}";
    }
}

=end code

=head1 DESCRIPTION

A Selkie widget tree is a tree by convention, not by construction:
C<parent> is a writable attribute that layout containers set when they
adopt a child. One mis-ordered reparent — a container adopting a widget
that is already one of its own ancestors — makes the "tree" cyclic, and
every walk up a parent chain then runs forever.

Selkie's parent-chain walks all hop through
C<Selkie::Tree>'s C<next-ancestor>, which throws this exception once a
single walk exceeds C<PARENT-CHAIN-LIMIT> hops. Catching it is rarely
useful: it is always an upstream bug in whatever set C<parent>, and the
whole point of throwing is that the alternative — a render thread
spinning on an endless chain, with a frozen screen and no keyboard — is
undiagnosable from the outside.

C<chain> holds a bounded, human-readable rendering of the widgets
involved (class name and C<widget-id>, joined by arrows), which is
normally enough to identify the offending container directly. C<hops>
is the budget that was exceeded.

=head1 SEE ALSO

=item L<Selkie::Tree> — C<next-ancestor>, C<PARENT-CHAIN-LIMIT>, and the walk contract
=item L<Selkie::Widget> — where C<parent> lives

=end pod

#| Thrown when a parent-chain walk exceeds Selkie::Tree's
#| C<PARENT-CHAIN-LIMIT>, i.e. when the widget tree contains a cycle.
unit class X::Selkie::WidgetCycle is Exception;

#| Bounded description of the widgets on the offending chain, as
#| C<Class#id -> Class#id -> …>.
has Str $.chain is required;

#| The hop budget that was exceeded.
has Int $.hops is required;

method message(--> Str) {
    "Selkie: parent-chain walk exceeded $!hops hops — the widget tree "
    ~ "contains a cycle (a widget is reachable from itself through "
    ~ "`parent`). Chain from the walk's current node: $!chain. This is "
    ~ "always a reparenting bug in the code that set `parent`: a "
    ~ "container adopted a widget that is already one of its own "
    ~ "ancestors.";
}