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);   # 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.

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'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 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:

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 — the AlphaMode enum and what each mode actually does

  • Selkie::Theme — collects named styles into a palette

  • Selkie::Widget — 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.

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

method effective-bg-alpha() returns AlphaMode

This style's background alpha mode, resolving an undefined bg-alpha to AlphaOpaque.

method styles

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

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

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

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

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

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

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.

Selkie v0.11.1

High-level TUI framework built on Notcurses

Authors

  • Matt Doughty

License

Artistic-2.0

Dependencies

Notcurses::Native:ver<0.4.1+>:auth<zef:apogee>

Test Dependencies

Provides

  • Selkie
  • Selkie::Align
  • Selkie::Alpha
  • Selkie::App
  • Selkie::App::Internal::Animation
  • Selkie::App::Internal::Dispatch
  • Selkie::App::Internal::ErrorLog
  • Selkie::App::Internal::FocusTree
  • Selkie::App::Internal::HitTest
  • Selkie::App::Internal::IdleBudget
  • Selkie::App::Internal::OverlayTree
  • Selkie::App::Internal::RenderLoop
  • Selkie::App::Internal::ScreenModalLifecycle
  • Selkie::App::Internal::Terminal
  • Selkie::App::Internal::TerminalSequences
  • Selkie::BorderStyle
  • Selkie::Container
  • Selkie::EffectiveBounds
  • Selkie::Event
  • Selkie::Gradient
  • Selkie::Layout::Allocate
  • Selkie::Layout::HBox
  • Selkie::Layout::Split
  • Selkie::Layout::VBox
  • Selkie::Plot::Palette
  • Selkie::Plot::Scaler
  • Selkie::Plot::Ticks
  • Selkie::ScreenManager
  • Selkie::Sizing
  • Selkie::Store
  • Selkie::Style
  • Selkie::Test::Focus
  • Selkie::Test::Keys
  • Selkie::Test::Snapshot
  • Selkie::Test::Snapshot::Harness
  • Selkie::Test::Store
  • Selkie::Test::Supply
  • Selkie::Test::Tree
  • Selkie::Theme
  • Selkie::Trace
  • Selkie::Tree
  • Selkie::Tween
  • Selkie::Widget
  • Selkie::Widget::Axis
  • Selkie::Widget::BarChart
  • Selkie::Widget::Border
  • Selkie::Widget::Button
  • Selkie::Widget::CardList
  • Selkie::Widget::Checkbox
  • Selkie::Widget::CommandPalette
  • Selkie::Widget::ConfirmModal
  • Selkie::Widget::FileBrowser
  • Selkie::Widget::FocusableByDefault
  • Selkie::Widget::GradientFill
  • Selkie::Widget::Heatmap
  • Selkie::Widget::HelpOverlay
  • Selkie::Widget::Histogram
  • Selkie::Widget::Image
  • Selkie::Widget::Legend
  • Selkie::Widget::LineChart
  • Selkie::Widget::ListView
  • Selkie::Widget::Modal
  • Selkie::Widget::MultiLineInput
  • Selkie::Widget::PasswordStrength
  • Selkie::Widget::Plot
  • Selkie::Widget::ProgressBar
  • Selkie::Widget::RadioGroup
  • Selkie::Widget::RichText
  • Selkie::Widget::RichText::Span
  • Selkie::Widget::ScatterPlot
  • Selkie::Widget::ScrollView
  • Selkie::Widget::Select
  • Selkie::Widget::Sparkline
  • Selkie::Widget::Spinner
  • Selkie::Widget::TabBar
  • Selkie::Widget::Table
  • Selkie::Widget::Text
  • Selkie::Widget::TextInput
  • Selkie::Widget::TextInput::HighlightSpan
  • Selkie::Widget::TextStream
  • Selkie::Widget::Toast
  • Selkie::Widget::ViewportedCardList

Documentation

The Camelia image is copyright 2009 by Larry Wall. "Raku" is trademark of the Yet Another Society. All rights reserved.

Built with Podlite — the markup and publishing tools behind this site.