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;
}