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