Selkie.git | t/ | 89-modal-backdrop.rakutest
use Test;
use lib 'lib';
use Notcurses::Native::Types;
use Notcurses::Native::Channel;
use Selkie::Alpha;
use Selkie::Style;
use Selkie::Theme;
use Selkie::Widget::Modal;
=begin pod
C<Selkie::Widget::Modal>'s three backdrop modes, asserted where they are
actually decided: the plane base cells.
Two facts carry the whole feature. First, C<BackdropOpaque> must stay
B<byte-identical> to the pre-alpha Modal — it is what every existing
dialog in every downstream app renders with, and the base-cell word is
the single number that decides whether the screen behind is covered.
Second, the see-through modes need both halves of the mechanism at once:
transparent (or blended) channels B<and> an empty EGC. Channels alone
still paint blanks over the text underneath, because a space is a glyph;
gcluster 0 is the sentinel that makes notcurses's glyph search keep
looking further down the pile.
The expected channel words are hand-derived from the notcurses channel
layout and hard-coded, deliberately — computing them through the code
they are meant to guard would guard nothing.
Everything here is plane-free.
=end pod
plan 11;
# fchannel/bchannel for the default theme's `base` slot: 0x40000000
# ("this channel is an explicit RGB, not the terminal default") OR'd
# with the colour, fg in the high half and bg in the low half.
constant OPAQUE-BASE-CHANNELS = 0x40C0C0C0401A1A2E;
# Both halves 0x60000000: NCALPHA_TRANSPARENT (0x20000000) in the alpha
# field, plus the not-default bit that ncchannel_set_alpha sets for any
# non-opaque alpha. No colour is contributed at all, which is the point.
constant TRANSPARENT-BASE-CHANNELS = 0x6000000060000000;
# Both halves 0x50000000: NCALPHA_BLEND (0x10000000) over black
# (0x000000) with the not-default bit — a 50/50 mix with whatever the
# compositor has accumulated underneath, on both channels.
constant SCRIM-CHANNELS = 0x5000000050000000;
subtest "backdrop defaults to opaque" => {
plan 3;
my $m = Selkie::Widget::Modal.new;
is $m.backdrop, BackdropOpaque, "BackdropOpaque by default";
ok $m.dim-background, "dim-background reads True";
is BackdropMode.enums.elems, 3, "three modes, no more";
};
subtest "dim-background => False maps to BackdropNone" => {
plan 2;
my $m = Selkie::Widget::Modal.new(dim-background => False);
is $m.backdrop, BackdropNone, "legacy boolean lands on BackdropNone";
nok $m.dim-background, "and reads back False";
};
subtest "dim-background is derived, never stored alongside backdrop" => {
plan 3;
ok Selkie::Widget::Modal.new(backdrop => BackdropOpaque).dim-background,
"opaque dims";
ok Selkie::Widget::Modal.new(backdrop => BackdropScrim).dim-background,
"scrim still draws a backdrop plane";
nok Selkie::Widget::Modal.new(backdrop => BackdropNone).dim-background,
"none doesn't";
};
subtest "an explicit backdrop wins over dim-background" => {
plan 3;
my $m = Selkie::Widget::Modal.new(
dim-background => False, backdrop => BackdropScrim,
);
is $m.backdrop, BackdropScrim, "explicit mode kept";
ok $m.dim-background, "…and dim-background follows it";
my $t = Selkie::Widget::Modal.new(
dim-background => True, backdrop => BackdropNone,
);
is $t.backdrop, BackdropNone, "in both directions";
};
subtest "BYTE IDENTITY — the opaque base cell is unchanged" => {
plan 4;
# The modal's own plane is full-screen. If this word or this glyph
# moves, every existing dialog either stops covering the screen or
# covers it in the wrong colour.
my $m = Selkie::Widget::Modal.new;
is $m.base-channels, OPAQUE-BASE-CHANNELS,
"opaque base-channels is byte-identical to the pre-alpha value";
is $m.base-egc, ' ', "and the base glyph is still a space";
is-deeply ($m.base-style.fg, $m.base-style.bg),
(Selkie::Theme.default.base.fg, Selkie::Theme.default.base.bg),
"base-style is the theme's base slot, untouched";
is (ncchannels_fg_alpha($m.base-channels),
ncchannels_bg_alpha($m.base-channels)),
(NCALPHA_OPAQUE, NCALPHA_OPAQUE), "both channels opaque";
};
subtest "see-through modes make the modal's own plane contribute nothing" => {
plan 8;
for BackdropScrim, BackdropNone -> $mode {
my $m = Selkie::Widget::Modal.new(backdrop => $mode);
is $m.base-egc, '',
"$mode: empty EGC — gcluster 0, so the glyph search falls through";
is $m.base-style.effective-fg-alpha, AlphaTransparent,
"$mode: fg transparent";
is $m.base-style.effective-bg-alpha, AlphaTransparent,
"$mode: bg transparent";
is $m.base-channels, TRANSPARENT-BASE-CHANNELS,
"$mode: base-channels is the transparent word";
}
};
subtest "scrim-channels comes from the modal-scrim theme slot" => {
plan 5;
my $m = Selkie::Widget::Modal.new(backdrop => BackdropScrim);
is $m.scrim-channels, SCRIM-CHANNELS,
"default scrim is black, blended on both channels";
is ncchannels_fg_alpha($m.scrim-channels), NCALPHA_BLEND, "fg blends";
is ncchannels_bg_alpha($m.scrim-channels), NCALPHA_BLEND, "bg blends";
is ncchannels_fg_rgb($m.scrim-channels), 0x000000, "fg is black";
is ncchannels_bg_rgb($m.scrim-channels), 0x000000, "bg is black";
};
subtest "a darker scrim is a darker colour, not more alpha" => {
plan 3;
# The whole point of the "no fractional alpha" rule: the only knob
# is the slot's RGB. Alpha stays AlphaBlend either way.
my $theme = Selkie::Theme.default.clone(
modal-scrim => Selkie::Style.new(
fg => 0x101018, bg => 0x101018,
fg-alpha => AlphaBlend, bg-alpha => AlphaBlend,
),
);
my $m = Selkie::Widget::Modal.new(backdrop => BackdropScrim);
$m.set-theme($theme);
is ncchannels_bg_rgb($m.scrim-channels), 0x101018, "custom scrim colour lands";
is ncchannels_bg_alpha($m.scrim-channels), NCALPHA_BLEND, "still one blend layer";
isnt $m.scrim-channels, SCRIM-CHANNELS, "…and a different word than the default";
};
subtest "set-backdrop re-syncs the base cell and marks dirty" => {
plan 8;
my $m = Selkie::Widget::Modal.new;
$m.clear-dirty;
$m.set-backdrop(BackdropScrim);
is $m.backdrop, BackdropScrim, "mode switched";
ok $m.is-dirty, "marked dirty";
is $m.base-egc, '', "base EGC re-derived";
is $m.base-channels, TRANSPARENT-BASE-CHANNELS, "base channels re-derived";
$m.clear-dirty;
$m.set-backdrop(BackdropOpaque);
ok $m.is-dirty, "marked dirty going back";
is $m.base-egc, ' ', "base EGC back to a space";
is $m.base-channels, OPAQUE-BASE-CHANNELS, "base channels back to the theme base";
$m.clear-dirty;
$m.set-backdrop(BackdropOpaque);
nok $m.is-dirty, "no-op when the mode is already active";
};
subtest "backdrop follows a theme swap" => {
plan 2;
my $theme = Selkie::Theme.default.clone(
base => Selkie::Style.new(fg => 0x102030, bg => 0x405060),
);
my $m = Selkie::Widget::Modal.new;
$m.set-theme($theme);
is ncchannels_bg_rgb($m.base-channels), 0x405060,
"opaque base tracks the live theme";
$m.set-backdrop(BackdropNone);
is $m.base-channels, TRANSPARENT-BASE-CHANNELS,
"…and a see-through mode ignores it entirely";
};
subtest "theme slots default so framing changes nothing visually" => {
plan 4;
my $t = Selkie::Theme.default;
is-deeply ($t.modal-frame.fg, $t.modal-frame.bg),
($t.border.fg, $t.border.bg), "modal-frame matches border";
is-deeply ($t.modal-title.fg, $t.modal-title.bold),
($t.overlay-title.fg, $t.overlay-title.bold),
"modal-title matches overlay-title";
is-deeply ($t.modal-key.fg, $t.modal-key.bold),
($t.overlay-key.fg, $t.overlay-key.bold),
"modal-key matches overlay-key";
# A theme that predates these slots gets them for free.
my $legacy = Selkie::Theme.new(
base => Selkie::Style.new(fg => 0x111111, bg => 0x222222),
border => Selkie::Style.new(fg => 0x333333),
border-focused => Selkie::Style.new(fg => 0x444444),
text => Selkie::Style.new(fg => 0x555555),
text-dim => Selkie::Style.new(fg => 0x666666),
text-highlight => Selkie::Style.new(fg => 0x777777),
input => Selkie::Style.new(fg => 0x888888),
input-focused => Selkie::Style.new(fg => 0x999999),
input-placeholder => Selkie::Style.new(fg => 0xAAAAAA),
scrollbar-track => Selkie::Style.new(fg => 0xBBBBBB),
scrollbar-thumb => Selkie::Style.new(fg => 0xCCCCCC),
divider => Selkie::Style.new(fg => 0xDDDDDD),
tab-active => Selkie::Style.new(fg => 0xEEEEEE),
tab-inactive => Selkie::Style.new(fg => 0xFFFFFF),
);
is-deeply ($legacy.slot('modal-frame').fg,
$legacy.slot('modal-title').fg,
$legacy.slot('modal-key').fg,
$legacy.slot('modal-scrim').bg-alpha),
(0x333333, 0x444444, 0x777777, AlphaBlend),
"a pre-S4 theme derives all four slots";
};