Selkie.git | lib/Selkie/Widget/ | GradientFill.rakumod


=begin pod

=head1 NAME

Selkie::Widget::GradientFill - A pane filled edge to edge with a colour ramp

=head1 SYNOPSIS

=begin code :lang<raku>

use Selkie::Gradient;
use Selkie::Widget::GradientFill;
use Selkie::Sizing;

# A two-row dusk fade under a header.
my $band = Selkie::Widget::GradientFill.new(
    gradient => Gradient.vertical(0x2E1A4A, 0x1A1A2E),
    sizing   => Sizing.fixed(2),
);
$root.add($band);

# Re-theme it later — marks dirty, repaints on the next frame.
$band.set-gradient(Gradient.vertical($theme.accent, $theme.bg-base));

=end code

=head1 DESCRIPTION

The whole widget is one call to C<gradient-fill> over its own plane: a
rectangle of colour, no text, no focus, no input. Use it for decorative
bands, dividers, accent strips, and the empty half of a split.

The gradient's corners are collapsed for the widget's actual size on
every render (see L<Selkie::Gradient>'s C<for-region>), so the same
C<Gradient> value works whether the layout gives the widget twenty rows
or one.

=head2 This is not a backdrop

B<A C<GradientFill> sibling does not appear behind another widget's
text.> Every Selkie widget owns its own notcurses plane, gradients are
written into the cells of one plane, and the widget above wins each cell
it covers — including the blank ones, because its base cell is opaque
too. Putting a C<GradientFill> and a C<Text> in the same C<VBox> gives
you a gradient band I<and> a text row, not text on a gradient.

For text on a gradient, call C<gradient-fill> at the top of your own
widget's C<render> and C<putstr> over it — same plane, one pass. That
idiom, and the see-through-widget alternative, are written up in
L<Selkie::Gradient>.

=head2 Glyphs and the foreground ramp

C<egc> is what gets stamped into every cell, and it defaults to a space,
which is what you want for a background wash. Set it to a block or a
texture glyph and the cell's I<foreground> becomes the visible colour —
at which point you want C<fg-gradient> as well, since without one the
glyphs are drawn in the terminal's default colour:

=begin code :lang<raku>

# A ramp drawn as coloured blocks rather than as a background.
my $bar = Selkie::Widget::GradientFill.new(
    gradient    => Gradient.uniform($theme.base.bg),   # behind the blocks
    fg-gradient => Gradient.horizontal(0x2E1A4A, 0xC04A2E),
    egc         => '█',
    sizing      => Sizing.fixed(1),
);

=end code

Keep C<egc> to a single column — it is stamped into every cell, so a
double-width grapheme misaligns the whole pane.

=head1 EXAMPLES

=head2 An accent strip down the side of a layout

A one-column widget is a degenerate region for notcurses, and would
paint nothing at all without the corner collapse C<render> applies.

=begin code :lang<raku>

my $row = Selkie::Layout::HBox.new(sizing => Sizing.flex);
$row.add: Selkie::Widget::GradientFill.new(
    gradient => Gradient.vertical(0xC04A2E, 0x2E1A4A),
    sizing   => Sizing.fixed(1),
);
$row.add: $content;

=end code

=head2 Driving it from the store

Like any widget: subscribe, then swap the value in the callback.

=begin code :lang<raku>

$app.store.subscribe-with-callback(
    'banner-accent',
    -> $s { $s.get-in('theme', 'accent') // 0x4A2E6E },
    -> UInt $accent {
        $band.set-gradient(Gradient.horizontal($accent, 0x1A1A2E));
    },
    $band,
);

=end code

=head1 SEE ALSO

=item L<Selkie::Gradient> — the C<Gradient> value type and the three painting subs
=item L<Selkie::Widget> — the role, and the C<base-style> / C<base-egc> see-through hooks
=item L<Selkie::Alpha> — why a gradient cannot be faded with alpha

=end pod

use Notcurses::Native::Types;

use Selkie::Gradient;
use Selkie::Widget;

unit class Selkie::Widget::GradientFill does Selkie::Widget;

#| The background colour ramp painted across the whole pane. Required.
has Gradient $.gradient is required;

#|( An optional foreground ramp — the colour C<egc> is drawn in. Left
    undefined the glyphs take the terminal's default colour, which is
    invisible for the default blank C<egc> and almost certainly not what
    you want for any other. )
has Gradient $.fg-gradient;

#| The grapheme stamped into every cell. A space (the default) makes the
#| pane a pure background wash. Must be a single column wide.
has Str $.egc = ' ';

#| Constructor. Defaults C<focusable> to False — the widget is
#| decorative and takes no input. Accepts C<:gradient> (required),
#| C<:fg-gradient>, C<:egc>, and the usual C<Selkie::Widget> attributes.
method new(*%args --> Selkie::Widget::GradientFill) {
    %args<focusable> //= False;
    callwith(|%args);
}

#| Replace the background ramp and mark the widget dirty.
method set-gradient(Gradient:D $gradient --> Nil) {
    $!gradient = $gradient;
    self.mark-dirty;
    Nil
}

#| Replace the foreground ramp and mark the widget dirty. Pass the bare
#| C<Gradient> type object to clear it back to the terminal default.
method set-fg-gradient(Gradient $fg-gradient --> Nil) {
    $!fg-gradient = $fg-gradient;
    self.mark-dirty;
    Nil
}

#| Replace the per-cell grapheme and mark the widget dirty.
method set-egc(Str:D $egc --> Nil) {
    $!egc = $egc;
    self.mark-dirty;
    Nil
}

method render() {
    return without self.plane;

    # gradient-fill writes into every cell of the region, so it is its
    # own erase — no ncplane_erase, and nothing stale can survive. It
    # also collapses the corners for the pane's real shape, which is
    # what lets a one-row or one-column GradientFill paint at all.
    gradient-fill(self.plane, $!gradient, fg => $!fg-gradient, egc => $!egc);

    self.clear-dirty;
}