Selkie--Style
NAME
Selkie::Style - Text styling (colors + bold/italic/underline/strikethrough)
SYNOPSIS
use Selkie::Style;
# Bright cyan bold text
my $s1 = Selkie::Style.new(fg => 0x7AA2F7, bold => True);
# White on dark background, italic
my $s2 = Selkie::Style.new(fg => 0xFFFFFF, bg => 0x1A1A2E, italic => True);
# Apply to a widget
my $text = Selkie::Widget::Text.new(
text => 'hello',
style => $s1,
sizing => Sizing.fixed(1),
);DESCRIPTION
Selkie::Style represents the visual attributes of rendered text: foreground color, background color, and a set of text-style flags (bold, italic, underline, strikethrough). Colors are 24-bit RGB integers in the form 0xRRGGBB.
Widgets apply styles to their planes via self.apply-style($style) in their render method. The framework provides sensible defaults through Selkie::Theme ā you usually get a style from the theme rather than constructing one directly.
EXAMPLES
Using theme-provided styles
Most widgets should pull styles from the theme so the app's palette stays consistent:
method render() {
return without self.plane;
self.apply-style(self.theme.text); # default text
ncplane_putstr_yx(self.plane, 0, 0, 'normal');
self.apply-style(self.theme.text-highlight); # emphasised
ncplane_putstr_yx(self.plane, 1, 0, 'selected');
self.clear-dirty;
}Overlaying an override on a theme style
Combine a base theme style with widget-local tweaks via merge:
my $base = self.theme.text;
my $warning-variant = $base.merge(Selkie::Style.new(fg => 0xFF5555, bold => True));
self.apply-style($warning-variant);merge takes the non-null values of the override, falling back to the base for anything the override doesn't set. Bold/italic/underline/strike are logical-OR ā if either side has the flag, the result has it.
Alpha
A style also carries an alpha mode per channel: fg-alpha and bg-alpha, both Selkie::Alpha's AlphaMode. They follow the same "undefined means don't-care" convention the colours do ā an undefined alpha resolves to AlphaOpaque, which is what every widget has always rendered with, so a style that ignores them is unchanged.
The enum lives in its own module, so bring it into scope alongside Selkie::Style. (use Selkie loads it but does not import the enum's names ā same as Sizing and the BorderKind family; reach them as Selkie::Alpha::AlphaBlend or use Selkie::Alpha for the short names.)
use Selkie::Alpha;
use Selkie::Style;
# 50/50 mix with whatever is on the planes underneath.
my $scrim = Selkie::Style.new(
fg => 0x000000, bg => 0x000000,
fg-alpha => AlphaBlend, bg-alpha => AlphaBlend,
);
$scrim.effective-fg-alpha; # AlphaBlend
Selkie::Style.new.effective-bg-alpha; # AlphaOpaque (undefined ā opaque)Read Selkie::Alpha before reaching for these: alpha is a four-value enum with no intermediate states, so "40% opacity" and "fade the alpha out" are not expressible. Fades interpolate colour.
AlphaHighContrast is a foreground-only mode; notcurses refuses it on a background channel. Constructing a style with bg-alpha =E<gt> AlphaHighContrast throws rather than handing notcurses a write it will silently drop:
Selkie::Style.new(bg-alpha => AlphaHighContrast); # dies
Selkie::Style.new(fg-alpha => AlphaHighContrast); # fineGreyscale
greyscale returns a desaturated copy: both colours collapse onto the grey axis, everything else ā flags, alpha modes, and the "undefined means inherit" state of a colour that was never set ā is carried through untouched.
my $s = Selkie::Style.new(fg => 0x00FF00, bg => 0x1A1A2E, bold => True);
my $g = $s.greyscale;
$g.fg; # 0x959595
$g.bg; # 0x1C1C1C
$g.bold; # True ā flags survive
$g.fg-alpha; # whatever $s had
Selkie::Style.new.greyscale.fg; # UInt type object ā still inheritsThis is the same conversion notcurses's own ncplane_greyscale applies, reproduced bit-for-bit. Selkie doesn't call that function ā see Selkie::Widget's greyscale-plane for why ā but matching it exactly is still the right target: it is the reference implementation every terminal-side expectation is written against, and staying on it means a style greyed here and a plane greyed by C land on identical RGB values.
Matching it means matching its arithmetic. The weights are Rec. 601 luma (0.299 R + 0.587 G + 0.114 B) and the result is truncated, not rounded, because C's (int) cast truncates:
Selkie::Style.greyscale-rgb(0x00FF00); # 0x959595 ā 149, not 150
Selkie::Style.greyscale-rgb(0xFFFFFF); # 0xFFFFFF
Selkie::Style.greyscale-rgb(0x7AA2F7); # 0x9F9F9F ā 159, not 160The packed channel word
channels renders a style into the single 64-bit channels word notcurses passes around ā both colours and both alpha modes, in the layout ncplane_set_base and friends expect. It is pure, so it is the thing to assert against when you want to know what a style will actually produce on a plane without having a plane:
Selkie::Style.new(fg => 0xC0C0C0, bg => 0x1A1A2E).channels;
# 0x40C0C0C0401A1A2EAn undefined colour leaves its half of the word alone (notcurses reads that as "use the terminal default"), and AlphaOpaque is skipped entirely because a zeroed channel already reads as opaque.
SEE ALSO
Selkie::Alpha ā the
AlphaModeenum and what each mode actually doesSelkie::Theme ā collects named styles into a palette
Selkie::Widget ā every widget's
apply-stylemethod takes one of these;base-channelsandapply-disabled-effectare built onchannelsandgreyscale
has UInt $.fg
Foreground color as a 24-bit RGB integer (0xRRGGBB). Leave undefined to inherit from the surrounding context.
has UInt $.bg
Background color as a 24-bit RGB integer (0xRRGGBB). Leave undefined to inherit.
has Bool $.bold
Render text in bold.
has Bool $.italic
Render text in italic.
has Bool $.underline
Render text underlined.
has Bool $.strikethrough
Render text with strikethrough.
has AlphaMode $.fg-alpha
How this style's foreground composites against the planes beneath it. Leave undefined for AlphaOpaque ā the historical, and overwhelmingly common, behaviour. See Selkie::Alpha.
has AlphaMode $.bg-alpha
How this style's background composites against the planes beneath it. Leave undefined for AlphaOpaque. AlphaHighContrast is not a legal value here and throws at construction ā notcurses defines it for foregrounds only. See Selkie::Alpha.
method effective-fg-alpha
method effective-fg-alpha() returns AlphaModeThis style's foreground alpha mode, resolving an undefined fg-alpha to AlphaOpaque. Use this rather than the raw attribute anywhere you're about to act on the value.
method effective-bg-alpha
method effective-bg-alpha() returns AlphaModeThis style's background alpha mode, resolving an undefined bg-alpha to AlphaOpaque.
method styles
method styles() returns UIntReturn the notcurses style bitmask for the set of boolean flags enabled on this style. Widgets use this internally via apply-style; you don't normally need to call it.
method channels
method channels() returns UIntThe packed 64-bit notcurses channels word for this style: both colours and both alpha modes, in the layout every ncplane_* call that takes a channels argument expects. Pure ā no plane needed, nothing written ā so it's what to assert against when you want to know what a style will actually put on a plane. Colours and alpha share the word. ncchannel_set preserves the two alpha bits and ncchannel_set_alpha preserves the RGB bits, so the writes compose in either order; the opaque case is skipped entirely because a zeroed channel already reads as NCALPHA_OPAQUE, which keeps the produced word bit-identical to what Selkie emitted before alpha existed. An undefined fg or bg leaves that channel's "use the default colour" bit clear, exactly as before. This is the single implementation behind Selkie::Widget's base-channels and Selkie::Widget::Modal's scrim-channels ā anywhere Selkie primes a plane's base cell, the word comes from here.
method greyscale-rgb
method greyscale-rgb(
Int $rgb where { ... }
) returns UIntCollapse a 24-bit 0xRRGGBB colour onto the grey axis, returning another 0xRRGGBB with all three components equal. Deliberately reproduces ncplane_greyscale's arithmetic exactly: Rec. 601 luma weights (0.299 R + 0.587 G + 0.114 B) and a truncating conversion to an integer, because the C side ends in an (int) cast. Rounding instead would be off by one on most colours ā 0x00FF00 greys to 149, not 150 ā and the two paths would visibly disagree wherever Selkie greys a base cell in Raku next to cells notcurses greyed in C. Computed in exact integer arithmetic (299R + 587G + 114B over 1000) rather than floating point. That is not just tidier: it also keeps the achromatic identity grey(v,v,v) == v that a naive 0.299 + 0.587 + 0.114 in binary floating point loses, since the three weights sum to a hair under 1.0 and truncation then eats the last unit. Bits above the low 24 are ignored, matching notcurses's own component extraction. Invocant-independent ā call it on the class: Selkie::Style.greyscale-rgb(0x7AA2F7).
method greyscale-channel
method greyscale-channel(
Int $channel where { ... }
) returns UIntGrey one 32-bit channel ā half of a channels word ā leaving everything that is not a colour exactly as it was. Three cases, and the two that do nothing are the important ones: =item A channel marked "use the default colour" is returned untouched. A default channel is not a colour; notcurses resolves it against the planes underneath, which is how a widget inherits its background from its own base cell (and how see-through widgets work at all). Greying it would mean picking a colour ā and since a default channel reads back as 0, 0, 0, that colour would be black. notcurses's own ncplane_greyscale does exactly that, which is why Selkie does not use it. =item A palette-indexed channel is returned untouched: an index into a terminal-defined palette has no RGB to average, and rewriting it as RGB would silently opt the cell out of the palette. =item Anything else has its RGB greyed and its flag bits ā alpha included ā copied straight through. Pure and invocant-independent: Selkie::Style.greyscale-channel($c).
method greyscale-channels
method greyscale-channels(
Int $channels where { ... }
) returns UIntGrey a full 64-bit channels word ā foreground in the high half, background in the low half ā through greyscale-channel. This is what turns a rendered cell grey. Selkie::Widget's disabled support reads each cell's channels, runs them through here, and stains the result back; a word that comes out equal to what went in (already grey, or nothing but defaults) skips the write entirely.
method greyscale
method greyscale() returns Selkie::StyleA desaturated copy of this style: both colours run through greyscale-rgb, everything else preserved. Flags (bold / italic / underline / strikethrough) and both alpha modes carry over unchanged ā greying is a colour operation, and a blended overlay that goes grey must stay blended or it would suddenly occlude what it was tinting. An undefined colour stays undefined rather than greying to black: "inherit from the surrounding context" is not a colour, and turning it into one would silently opt a widget out of its parent's palette. Used by Selkie::Widget.apply-disabled-effect to re-stamp a greyed base cell, which the framebuffer walk cannot reach.
method merge
method merge(
Selkie::Style $override
) returns Selkie::StyleCombine this style with an override, producing a new style. Any color on the override takes precedence; any flag set on either side is set on the result (logical OR). Useful for producing variants of a theme style without replacing the whole thing. Alpha modes follow the colours, not the flags: an alpha the override sets wins outright, an alpha it leaves undefined inherits from the base. Because AlphaOpaque is a value like any other, an override that explicitly asks for AlphaOpaque pins the result opaque even over a blended base ā that's the escape hatch for opting a single element out of a translucent parent style.