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.