Selkie.git | lib/Selkie/App/Internal/ | OverlayTree.rakumod


=begin pod

=head1 NAME

Selkie::App::Internal::OverlayTree - internal toast and image-tree coordination role for Selkie::App

=head1 DESCRIPTION

Implementation detail composed by C<Selkie::App>. Use C<Selkie::App.toast>
and C<Selkie::App.force-refresh-sprixels> from application code.

=end pod

unit role Selkie::App::Internal::OverlayTree;

use Selkie::EffectiveBounds;
use Selkie::Tree;
use Selkie::Widget::Image;
use Selkie::Widget::Toast;

has Selkie::Widget::Toast $!toast;

method !toast-widget(--> Mu) {
    $!toast;
}

method !install-tree-providers(--> Nil) {
    my $self-ref = self;
    set-terminal-viewport-provider(-> { $self-ref!terminal-size });
    set-tree-roots-provider(-> { $self-ref!collect-tree-roots });
    set-modal-provider(-> { $self-ref!active-modal-or-nil });
}

method !collect-tree-roots(--> List) {
    my @roots;
    with self.root { @roots.push($_) }
    @roots.append: self!modal-stack-list;
    with $!toast { @roots.push($_) }
    @roots.List;
}

method !show-toast(Str:D $message, Num :$duration = 2e0 --> Nil) {
    my ($rows, $cols) = self!terminal-size;
    without $!toast {
        $!toast = Selkie::Widget::Toast.new;
        $!toast.attach(self.stdplane, rows => $rows, cols => $cols);
    }
    self!sync-toast-fade($!toast);
    $!toast.resize-screen($rows, $cols);
    $!toast.show($message, :$duration);

    # Park Image widgets that intersect the toast's bottom strip so
    # notcurses pixel graphics, which paint above cell content, do not
    # bleed through the toast background.
    my UInt $strip-rows = $rows >= 3 ?? 3 !! $rows;
    my Int  $strip-y    = ($rows - $strip-rows).Int max 0;
    self!park-images-in-rect(:abs-y($strip-y), :abs-x(0),
        :rows($strip-rows), :cols($cols));
}

# Bring `$toast`'s fade wiring in line with the app's `animate-toast`
# flag. Read on every show rather than once at construction so
# `set-animate-toast` is honoured in both directions without the widget
# having to watch the app.
#
# Takes the toast explicitly (rather than reading `$!toast`) so the
# decision is drivable from a test that has no terminal to attach a
# real toast to. With the flag off — the default — `!tween-group` is
# never touched, so an app that never animates allocates no group and
# registers no per-frame ticker.
method !sync-toast-fade(Selkie::Widget::Toast:D $toast --> Nil) {
    if self.animate-toast {
        $toast.enable-fade(self!tween-group) unless $toast.fade-enabled;
    } elsif $toast.fade-enabled {
        $toast.disable-fade;
    }
    Nil;
}

method !maybe-unpark-toast(Bool $just-hidden --> Nil) {
    return unless $just-hidden;
    self!mark-all-images-dirty;
}

method !force-refresh-sprixels(--> Nil) {
    sub visit($w) {
        return without $w;
        if $w ~~ Selkie::Widget::Image {
            $w.destroy-blit-plane;
            $w.mark-dirty unless $w.is-dirty;
        }
        if $w.^can('children') {
            visit($_) for $w.children;
        }
        if $w.^can('content') {
            my $c = $w.content;
            visit($c) if $c.defined;
        }
    }
    visit($_) for self!collect-tree-roots;
}

method !mark-all-images-dirty(--> Nil) {
    sub visit($w) {
        return without $w;
        $w.mark-dirty if $w ~~ Selkie::Widget::Image && !$w.is-dirty;
        if $w.^can('children') {
            visit($_) for $w.children;
        }
        if $w.^can('content') {
            my $c = $w.content;
            visit($c) if $c.defined;
        }
    }
    visit($_) for self!collect-tree-roots;
}

method !park-images-in-rect(
    Int  :$abs-y!,
    Int  :$abs-x!,
    UInt :$rows!,
    UInt :$cols!,
    --> Nil
) {
    return if $rows == 0 || $cols == 0;
    my Int $rect-bottom = $abs-y + $rows.Int;
    my Int $rect-right  = $abs-x + $cols.Int;
    sub visit($w) {
        return without $w;
        if $w ~~ Selkie::Widget::Image {
            my Int $w-bottom = $w.abs-y + $w.rows.Int;
            my Int $w-right  = $w.abs-x + $w.cols.Int;
            my Bool $intersects = !(
                   $w-right     <= $abs-x
                || $rect-right  <= $w.abs-x
                || $w-bottom    <= $abs-y
                || $rect-bottom <= $w.abs-y
            );
            $w.park if $intersects;
        }
        if $w.^can('children') {
            visit($_) for $w.children;
        }
        if $w.^can('content') {
            my $c = $w.content;
            visit($c) if $c.defined;
        }
    }
    visit($_) for self!collect-tree-roots;
}