Selkie.git | docs/api/ | Selkie--Style.md
NAME
====
Selkie::Style - Text styling (colors + bold/italic/underline/strikethrough)
SYNOPSIS
========
```raku
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](Selkie--Theme.md) — 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:
```raku
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`:
```raku
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](Selkie--Alpha.md)'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.)
```raku
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](Selkie--Alpha.md) 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:
```raku
Selkie::Style.new(bg-alpha => AlphaHighContrast); # dies
Selkie::Style.new(fg-alpha => AlphaHighContrast); # fine
```
Greyscale
---------
`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.
```raku
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 inherits
```
This is the same conversion notcurses's own `ncplane_greyscale` applies, reproduced bit-for-bit. Selkie doesn't call that function — see [Selkie::Widget](Selkie--Widget.md)'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:
```raku
Selkie::Style.greyscale-rgb(0x00FF00); # 0x959595 — 149, not 150
Selkie::Style.greyscale-rgb(0xFFFFFF); # 0xFFFFFF
Selkie::Style.greyscale-rgb(0x7AA2F7); # 0x9F9F9F — 159, not 160
```
The 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:
```raku
Selkie::Style.new(fg => 0xC0C0C0, bg => 0x1A1A2E).channels;
# 0x40C0C0C0401A1A2E
```
An 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](Selkie--Alpha.md) — the `AlphaMode` enum and what each mode actually does
* [Selkie::Theme](Selkie--Theme.md) — collects named styles into a palette
* [Selkie::Widget](Selkie--Widget.md) — every widget's `apply-style` method takes one of these; `base-channels` and `apply-disabled-effect` are built on `channels` and `greyscale`
### 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](Selkie--Alpha.md).
### 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](Selkie--Alpha.md).
### method effective-fg-alpha
```raku
method effective-fg-alpha() returns AlphaMode
```
This 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
```raku
method effective-bg-alpha() returns AlphaMode
```
This style's background alpha mode, resolving an undefined `bg-alpha` to `AlphaOpaque`.
### method styles
```raku
method styles() returns UInt
```
Return 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
```raku
method channels() returns UInt
```
The 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
```raku
method greyscale-rgb(
Int $rgb where { ... }
) returns UInt
```
Collapse 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
```raku
method greyscale-channel(
Int $channel where { ... }
) returns UInt
```
Grey 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
```raku
method greyscale-channels(
Int $channels where { ... }
) returns UInt
```
Grey 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
```raku
method greyscale() returns Selkie::Style
```
A 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
```raku
method merge(
Selkie::Style $override
) returns Selkie::Style
```
Combine 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.