Generated reference › API — ICoreEssentials/UI/Values
kind: generated#api#icoreessentials-ui-values

API — ICoreEssentials/UI/Values

The public contract of 10 header(s) under src/ICoreEssentials/UI/Values — 10 class/struct definition(s), 164 declaration(s). Each section shows the header's banner and its public (and protected-virtual) surface exactly as the file writes it.

ICoreColor.h#

src/ICoreEssentials/UI/Values/ICoreColor.h

ICoreColor#

ICoreColor.h:22 · class · 32 declaration(s)

The client-facing name for a colour.

class ICoreColor {
public:
    ICoreColor();

    ICoreColor(int r, int g, int b, int a = 255);

    // Brings a Qt-free ICoreRgba back under the Qt-backed name at the seam.
    //
    // Implicit ON PURPOSE, and it is the hinge the styling module's migration
    // turns on: the theme's token tree holds ICoreRgba, and this is what lets
    // every file that reads those tokens as colours keep compiling.
    //
    // ⚠ An invalid ICoreRgba stays invalid rather than becoming opaque black.
    // QColor(0,0,0,255) is a perfectly valid colour, so forwarding the channels
    // unconditionally would launder "nobody assigned this token" into "somebody
    // chose black" -- exactly the failure the theme suite's
    // every_core_token_is_a_real_colour cases exist to catch.
    ICoreColor(const ICoreRgba& c);

    // ⚠ THE ONLY NAMED COLOUR, AND DELIBERATELY THE ONLY ONE. 45 of the sites
    // this conversion broke spelled `Qt::transparent`, reaching QColor through
    // the base that has now gone. The obvious fix -- an ICoreGlobalColor enum
    // mirroring Qt's 20 -- was rejected on census: of 71 Qt global-colour
    // spellings tree-wide, 60 are transparent and the rest are 7 black,
    // 2 white, 1 gray, 1 lightGray. Twenty names across the boundary to serve
    // one idiom is the trade P7.1 refused for composition modes (one of ~30 in
    // use), and P0.5, P3.4 and P7.3 each refused before that. The other eleven
    // sites spell their channels: ICoreColor(0, 0, 0) and so on.
    //
    // Fully transparent BLACK specifically -- Qt::transparent is #00000000, so
    // the RGB channels survive a later setAlpha(), which several sites rely on.
    // ⚠ Do NOT "simplify" this to a default-constructed ICoreColor: that one is
    // INVALID, not transparent, and the two differ everywhere isValid() is read.
    static ICoreColor transparent();

    // Parses "#rgb", "#rrggbb" and "#aarrggbb" -- the inverse of the two
    // persisted spellings below, and NAMED rather than a constructor because a
    // string that fails to parse yields an INVALID colour: an implicit
    // conversion would make that failure silent at the call site.
    static ICoreColor fromHexString(const ICoreString& text);

    ICoreColor(const ICoreColor& other);
    ICoreColor(ICoreColor&& other) noexcept;
    ICoreColor& operator=(const ICoreColor& other);
    ICoreColor& operator=(ICoreColor&& other) noexcept;
    ~ICoreColor();

    bool isValid() const;

    int red() const;
    int green() const;
    int blue() const;
    int alpha() const;

    double redF() const;
    double greenF() const;
    double blueF() const;
    double alphaF() const;

    void setRed(int v);
    void setGreen(int v);
    void setBlue(int v);
    void setAlpha(int v);
    void setAlphaF(double v);

    ICoreColor lighter(int factor = 150) const;
    ICoreColor darker(int factor = 200) const;

    // Named, never an operator: the layer's rule is that a type which hands out
    // its Qt representation implicitly can never be unhooked later.
    ICoreRgba toRgba() const;

    // "#rrggbb", or "#aarrggbb" when the colour is not fully opaque.
    //
    // ⚠ These two spellings are PERSISTED -- they land in project files and in
    // recipe scripts -- so they are pinned here rather than left to a caller to
    // format. Anything that changes the digits changes what an existing saved
    // project reads back as.
    ICoreString toHexString() const;

    // Always "#aarrggbb", alpha included even at 255.
    ICoreString toHexArgbString() const;

    bool operator==(const ICoreColor& other) const;
    bool operator!=(const ICoreColor& other) const;

    // The seam, per §1. Call icoreQt() from a .cpp rather than reaching in;
    // icoreColor(const QColor&) in ICoreNativeHandleAccess.h is the way back.
    const void* nativeStorage() const;

    // §7 measured QColor at 16 bytes / align 4 on this tree's Qt 6.10.2,
    // macOS arm64 -- a small inline value, not a d-pointer.
    static constexpr std::size_t kNativeStorageSize = 16;
    static constexpr std::size_t kNativeStorageAlign = 4;

};

ICoreCursor.h#

src/ICoreEssentials/UI/Values/ICoreCursor.h

ICoreCursor#

ICoreCursor.h:20 · class · 10 declaration(s)

class ICoreCursor {
public:
    ICoreCursor();

    ICoreCursor(ICoreCursorShape shape);
    ICoreCursor(const ICorePixmap& pixmap, int hotX = -1, int hotY = -1);

    ICoreCursor(const ICoreCursor& other);
    ICoreCursor(ICoreCursor&& other) noexcept;
    ICoreCursor& operator=(const ICoreCursor& other);
    ICoreCursor& operator=(ICoreCursor&& other) noexcept;
    ~ICoreCursor();

    // ⚠ Was an INHERITED STATIC (QCursor::pos()) and had to be re-declared by
    // hand -- the first inherited *static* this migration has had to carry
    // across, as opposed to an inherited instance method. Nothing about the
    // pImpl shape brings it along, and its one caller
    // (ICoreNotificationCenterMessageBox, testing whether the pointer is
    // inside the bubble) would otherwise fail with a plain "no member named
    // pos", which reads like a typo rather than like a missing forwarder.
    //
    // Returns the pointer position in GLOBAL screen coordinates, unchanged
    // from QCursor::pos().
    static ICorePoint pos();

    // The seam, per §1. Call icoreQt() from a .cpp rather than reaching in.
    const void* nativeStorage() const;

    // Public so the .cpp can pin them; §7 measured QCursor at 8 bytes (plain
    // d-pointer) against this tree's Qt 6.10.2 on macOS arm64.
    static constexpr std::size_t kNativeStorageSize = 8;
    static constexpr std::size_t kNativeStorageAlign = 8;

};

ICoreFont.h#

src/ICoreEssentials/UI/Values/ICoreFont.h

ICoreFont#

ICoreFont.h:30 · class · 37 declaration(s)

class ICoreFont {
public:
    ICoreFont();

    ICoreFont(const ICoreString& family, int pointSize, bool bold = false);

    // Resolves a Qt-free ICoreFontSpec into a real toolkit font. This is the
    // seam: the styling module stores what was ASKED FOR, and the platform
    // query that turns an unset family into an actual typeface happens in the
    // .cpp, at the moment the font is built, rather than while a theme is
    // derived.
    //
    // ⚠ IMPLICIT, AND THAT IS LOAD-BEARING RATHER THAN CONVENIENT. Call sites
    // write ICoreFontMetrics(theme.type.mono) with a SPEC, which works only
    // because converting the argument spends the single user-defined
    // conversion C++ allows and the ICoreFontMetrics constructor is then an
    // ordinary direct-initialisation. Make this explicit and every one of
    // those sites breaks with a message that does not mention this line.
    ICoreFont(const ICoreFontSpec& spec);

    // Implicit inbound seam, kept PERMANENTLY -- the same decision P7.4 made
    // for ICorePixmap(const QPixmap&) and ICoreIcon(const QIcon&), and for the
    // same reason: QWidget::font(), QPainter::font() and QGraphicsItem::font()
    // all hand back a raw QFont and will keep doing so until those wrappers
    // convert. ~8 sites write `ICoreFont f = someWidget->font();`. Only the
    // OUTBOUND direction becomes icoreQt(); inbound stays.
    ICoreFont(const QFont& font);

    ICoreFont(const ICoreFont& other);
    ICoreFont(ICoreFont&& other) noexcept;
    ICoreFont& operator=(const ICoreFont& other);
    ICoreFont& operator=(ICoreFont&& other) noexcept;
    ~ICoreFont();

    ICoreString family() const;
    void setFamily(const ICoreString& family);

    int pointSize() const;
    void setPointSize(int pointSize);

    double pointSizeF() const;
    void setPointSizeF(double pointSize);

    int pixelSize() const;
    void setPixelSize(int pixelSize);

    // ⚠ Returns the ICore enum, and the numeric value is a FILE FORMAT, not an
    // internal detail: ICoreStudioStaticCommands::serializeFont writes
    // static_cast<int>(weight()) into saved projects. ICoreFontWeight is pinned
    // to QFont::Weight (400/500/600/700, ICoreInputEnumsVerify.cpp), so the
    // bytes are unchanged by this conversion and old projects still load.
    //
    // ⚠ A weight OUTSIDE the four enumerators round-trips correctly, which the
    // deserializer depends on: it casts an arbitrary int from a file. That is
    // well-defined only because ICoreFontWeight has a fixed underlying type
    // (`: int`). Do not remove that.
    ICoreFontWeight weight() const;
    void setWeight(ICoreFontWeight weight);

    bool bold() const;
    void setBold(bool bold);

    bool italic() const;
    void setItalic(bool italic);

    bool underline() const;
    void setUnderline(bool underline);

    bool strikeOut() const;
    void setStrikeOut(bool strikeOut);

    int stretch() const;
    void setStretch(int stretch);

    bool kerning() const;
    void setKerning(bool kerning);

    bool fixedPitch() const;
    void setFixedPitch(bool fixedPitch);

    // The three below replace inherited QFont ENUMERATORS that call sites were
    // reaching through the wrapper name -- ICoreFont::AllUppercase,
    // ICoreFont::PercentageSpacing, QFont::Monospace. Those spellings are a
    // re-export hole of exactly the kind §9 records: the Qt enumerator appears
    // only at the call site, where neither guard can see it. Each is named for
    // the one thing its callers ask for rather than given a general enum, per
    // §9 -- there are two, two and one call sites respectively.
    void setAllUppercase();
    void setPercentageLetterSpacing(double percent);
    void setMonospaceStyleHint();

    friend bool operator==(const ICoreFont& a, const ICoreFont& b);
    friend bool operator!=(const ICoreFont& a, const ICoreFont& b);

    // The seam, per §1: a void* rather than a forward-declared QFont*, so the
    // cast lives in one place. Call icoreQt() from a .cpp instead of reaching
    // in here.
    const void* nativeStorage() const;

    // Public only so the .cpp can pin them. Measured, not assumed: QFont is 16
    // bytes / align 8 on this tree's Qt 6.10.2, macOS arm64 -- a d-pointer
    // PLUS a uint resolve_mask, which is why it is not the plain 8 that most
    // of Group D uses. Non-polymorphic, so unlike QPixmap the buffer is not
    // additionally vptr-bound.
    static constexpr std::size_t kNativeStorageSize = 16;
    static constexpr std::size_t kNativeStorageAlign = 8;

};

ICoreFontMetrics.h#

src/ICoreEssentials/UI/Values/ICoreFontMetrics.h

ICoreFontMetrics#

ICoreFontMetrics.h:27 · class · 16 declaration(s)

class ICoreFontMetrics {
public:
    // ⚠ NOT explicit, deliberately. Call sites pass an ICoreFontSpec
    // (ICoreFontMetrics(theme.type.mono)) and rely on spec -> ICoreFont being
    // the single user-defined conversion C++ permits before this constructor
    // runs. Marking it explicit breaks those sites; marking ICoreFont's spec
    // constructor explicit breaks them too, from the other end.
    ICoreFontMetrics(const ICoreFont& font);

    ICoreFontMetrics(const ICoreFontMetrics& other);
    ICoreFontMetrics(ICoreFontMetrics&& other) noexcept;
    ICoreFontMetrics& operator=(const ICoreFontMetrics& other);
    ICoreFontMetrics& operator=(ICoreFontMetrics&& other) noexcept;
    ~ICoreFontMetrics();

    double textWidth(const ICoreString& text) const;

    ICoreRect textBoundingRect(const ICoreString& text) const;

    double height() const;
    double ascent() const;
    double descent() const;
    double lineSpacing() const;

    // Shorten `text` with an ellipsis to fit `maxWidth`, middle-elided like
    // the file-path labels this exists for.
    ICoreString elidedMiddle(const ICoreString& text, double maxWidth) const;

    ICoreString elidedRight(const ICoreString& text, double maxWidth) const;

    // Elides the HEAD, so the tail stays readable -- what a file path
    // wants, since its last component is the informative one.
    ICoreString elidedLeft(const ICoreString& text, double maxWidth) const;

    // The seam, per §1. Call icoreQt() from a .cpp rather than reaching here.
    const void* nativeStorage() const;

    // Public only so the .cpp can pin them. Measured, not assumed:
    // QFontMetricsF is 8 bytes / align 8 on this tree's Qt 6.10.2, macOS
    // arm64 -- a plain d-pointer, §7's "everything else" row. Non-polymorphic.
    static constexpr std::size_t kNativeStorageSize = 8;
    static constexpr std::size_t kNativeStorageAlign = 8;

};

ICoreFontSpec.h#

src/ICoreEssentials/UI/Values/ICoreFontSpec.h

ICoreFontSpec -- a Qt-free DESCRIPTION of a font request.

ICoreFont is a QFont subclass, so a token tree holding ICoreFont holds Qt. This is the value the styling module stores instead: what was asked for, not a resolved toolkit object.

The distinction matters beyond the boundary. Resolving a family against the installed-font database is a PLATFORM QUERY, and doing it while deriving a theme meant the theme could not be derived without a live font system -- which is also why the derivation could not be unit-tested off-screen. Here an unset family stays unset, and the seam that builds the ICoreFont resolves it against the platform at the moment it is actually needed.

DO NOT let a bulk QFont->ICoreFont rename touch this file, and do not add an

ICoreFontSpec#

ICoreFontSpec.h:37 · struct · 0 declaration(s)

struct ICoreFontSpec {
public:
    // Empty = use the fallback below. UTF-8; std::string rather than
    // ICoreString because ICoreString forwards to QString, which would put Qt
    // back into every file that includes a theme token.
    std::string family;

    int pixelSize = 0;

    ICoreFontWeight weight = ICoreFontWeight::Normal;

    // Percent of character width, 100 = normal, matching the scale the old
    // percentage-spacing calls used. Exactly 100 means "do not set letter
    // spacing at all", which is not the same as setting it to 100 -- the old
    // code branched on that and some platforms differ.
    double letterSpacingPercent = 100.0;

    ICoreFontFallback fallback = ICoreFontFallback::PlatformUi;

    // Hidden friends: declared here so ADL finds them, DEFINED in
    // ICoreFontSpec.cpp -- a body in a header is a body wherever it sits.
    friend bool operator==(const ICoreFontSpec& a, const ICoreFontSpec& b);
    friend bool operator!=(const ICoreFontSpec& a, const ICoreFontSpec& b);
};
};

File-scope declarations#

// What an EMPTY family should fall back to. The old derivation resolved these
// two cases differently -- a plain default-constructed font for UI text, but an
// explicit system fixed-font lookup for the mono token -- so collapsing them
// into "empty means default" would silently give the code editor the
// proportional UI font.
enum class ICoreFontFallback {
    PlatformUi,
    PlatformFixed,
};

ICoreIcon.h#

src/ICoreEssentials/UI/Values/ICoreIcon.h

ICoreIcon#

ICoreIcon.h:29 · class · 12 declaration(s)

class ICoreIcon {
public:
    ICoreIcon();

    ICoreIcon(const ICoreString& path);
    ICoreIcon(const ICorePixmap& pixmap);

    // The rendered raster at width x height -- inherited from QIcon before
    // this conversion, and still the only way to get from "an icon" to
    // "a pixmap ready to hand a QLabel-shaped setPixmap()".
    [[nodiscard]] ICorePixmap pixmap(int width, int height) const;

    // Whether this icon carries no art at all. Added at Q2.1: a widget that
    // paints its own glyph rather than parking it in a child (ICoreHBoxButton)
    // has to skip the draw entirely, and QIcon::isNull() was what it used to
    // ask before the parameter swap. Without this the converted member would
    // have had to keep a QIcon beside it purely to answer the question.
    [[nodiscard]] bool isNull() const;

    // Implicit on purpose -- see the class comment. This is the seam that
    // keeps ICoreIcons' engine-backed icons flowing through ICoreIcon-typed
    // locals without naming QIcon at the call site.
    ICoreIcon(const QIcon& icon);

    ICoreIcon(const ICoreIcon& other);
    ICoreIcon(ICoreIcon&& other) noexcept;
    ICoreIcon& operator=(const ICoreIcon& other);
    ICoreIcon& operator=(ICoreIcon&& other) noexcept;
    ~ICoreIcon();

    // The seam, per §1. Call icoreQt() from a .cpp rather than reaching in
    // here.
    const void* nativeStorage() const;

    // Public so the .cpp can pin them; §7 measured QIcon at 8 bytes (plain
    // d-pointer) against this tree's Qt 6.10.2 on macOS arm64.
    static constexpr std::size_t kNativeStorageSize = 8;
    static constexpr std::size_t kNativeStorageAlign = 8;

};

ICoreKeySequence.h#

src/ICoreEssentials/UI/Values/ICoreKeySequence.h

ICoreKeySequence#

ICoreKeySequence.h:27 · class · 16 declaration(s)

The client-facing name for a shortcut chord.

class ICoreKeySequence {
public:
    ICoreKeySequence();
    ICoreKeySequence(const ICoreString& text);

    ICoreKeySequence(const ICoreKeySequence& other);
    ICoreKeySequence(ICoreKeySequence&& other) noexcept;
    ICoreKeySequence& operator=(const ICoreKeySequence& other);
    ICoreKeySequence& operator=(ICoreKeySequence&& other) noexcept;
    ~ICoreKeySequence();

    static ICoreKeySequence standardCopy();
    static ICoreKeySequence standardCut();
    static ICoreKeySequence standardPaste();
    static ICoreKeySequence standardSelectAll();
    static ICoreKeySequence standardFind();

    // Added by Q2.3, which needed them: ICoreMenu's shortcut parameter stopped
    // taking QKeySequence, and the File/Edit menus were spelling these two as
    // QKeySequence::Save / QKeySequence::Delete at the call site. Built here
    // rather than as portable text so they keep following each OS's convention
    // -- Delete in particular is Backspace on macOS and Del elsewhere.
    static ICoreKeySequence standardSave();
    static ICoreKeySequence standardDelete();

    // The chord as the platform renders it for display (⌘O on macOS, Ctrl+O
    // elsewhere) -- toString(NativeText) behind the boundary. For showing a
    // shortcut in UI text; not a serialization (that would be PortableText,
    // which no caller needs yet).
    [[nodiscard]] ICoreString nativeText() const;

    // The seam, per §1: a void* rather than a forward-declared QKeySequence*,
    // because a forward declaration would put the toolkit name in this public
    // header, which is the thing the rule forbids. Call icoreQt() from a .cpp
    // instead of reaching in here -- the cast lives in one place on purpose.
    const void* nativeStorage() const;

    // Public only so the .cpp can pin them; §7 measured QKeySequence at 8
    // bytes (plain d-pointer) against this tree's Qt 6.10.2 on macOS arm64.
    static constexpr std::size_t kNativeStorageSize = 8;
    static constexpr std::size_t kNativeStorageAlign = 8;

};

ICorePalette.h#

src/ICoreEssentials/UI/Values/ICorePalette.h

ICorePalette#

ICorePalette.h:40 · class · pImpl · 9 declaration(s)

ICorePalette -- the colour roles an application hands the toolkit.

class ICorePalette {
public:
    ICorePalette();
    ~ICorePalette();

    ICorePalette(const ICorePalette& other);
    ICorePalette& operator=(const ICorePalette& other);
    ICorePalette(ICorePalette&& other) noexcept;
    ICorePalette& operator=(ICorePalette&& other) noexcept;

    void setColor(ICoreColorRole role, const ICoreColor& colour);
    [[nodiscard]] ICoreColor color(ICoreColorRole role) const;

    // The seam, the same shape ICoreColor's is: a .cpp inside a sanctioned zone
    // casts this back to the toolkit type. Nobody else touches it, and the
    // header stays free of the name it points at.
    [[nodiscard]] const void* nativeStorage() const;

private:
    class Impl;                    // the two-line residue; state lives here
    std::unique_ptr<Impl> impl;
};

File-scope declarations#

// The application-wide colour roles, pinned to the toolkit's own values in the
// .cpp with static_asserts -- the Q0.1 shape, so an or-ed or switched value
// keeps meaning what it meant before the wrapper existed.
// 
// ⚠ FIVE ROLES, not the toolkit's twenty. These are the ones this application
// actually sets (Initialization::configureApplication). Adding a role is one
enum class ICoreColorRole {
    WindowText      = 0,
    Text            = 6,
    ButtonText      = 8,
    HighlightedText = 13,
    PlaceholderText = 20,
};

ICorePixmap.h#

src/ICoreEssentials/UI/Values/ICorePixmap.h

ICorePixmap#

ICorePixmap.h:33 · class · 19 declaration(s)

class ICorePixmap {
public:
    ICorePixmap();

    ICorePixmap(const ICoreString& path);

    // Matches the two shapes call sites actually build with: a widget's own
    // size() cast to ints, and a devicePixelRatio-scaled size computed by
    // hand. No QSize overload -- naming QSize here would put a Qt type back
    // in this header for a shape no caller needs; both existing call sites
    // already have (or can trivially get) the two ints.
    ICorePixmap(int width, int height);

    // Implicit on purpose: theme and wrapper APIs (ICoreSvg, QIcon::pixmap())
    // still hand back a raw QPixmap, and this is the one conversion point
    // that lets ICorePainter::drawPixmap and friends accept either without
    // every producer having to spell the wrapper name.
    ICorePixmap(const QPixmap& pixmap);

    ICorePixmap(const ICorePixmap& other);
    ICorePixmap(ICorePixmap&& other) noexcept;
    ICorePixmap& operator=(const ICorePixmap& other);
    ICorePixmap& operator=(ICorePixmap&& other) noexcept;
    ~ICorePixmap();

    bool isNull() const;

    void fill(const ICoreColor& color);
    void setDevicePixelRatio(double ratio);

    // Scaled to exactly width x height, distorting the aspect ratio if it
    // does not match the source's -- the one shape a call site in this tree
    // asks for. Add a KeepAspectRatio sibling when a second caller needs one;
    // do not add a mode parameter for a single caller (§9).
    [[nodiscard]] ICorePixmap scaledIgnoringAspect(int width, int height) const;

    // The KeepAspectRatio sibling the comment above promised, added at E3 for
    // the block-frame renderer: as large as fits inside width x height with
    // the source's aspect ratio kept, smooth like its sibling.
    [[nodiscard]] ICorePixmap scaledToFit(int width, int height) const;

    // The raster's extent in device pixels (QPixmap::width/height). Added at
    // E3: the block frame's icon math reads the loaded art's size before
    // deciding how to scale it.
    [[nodiscard]] int width() const;
    [[nodiscard]] int height() const;

    // The 32-bit ARGB view of this raster, for hashing and pixel walks.
    // Returns the toolkit type rather than a wrapper: there is no ICore image
    // value type (ICoreRasterImage was deleted at P7.4 as dead surface).
    // Every use of this accessor is `const auto`, so the name never appears
    // at a call site.
    QImage toImageArgb32() const;

    // The seam, per §1. Call icoreQt() from a .cpp rather than reaching in
    // here -- and prefer the const overload; see the note above this class
    // for when the mutable one is actually the right call.
    const void* nativeStorage() const;

    void* nativeStorageMut();

    // Public so the .cpp can pin them; §7 measured QPixmap at 24 bytes
    // (QPaintDevice vptr + d-pointer, polymorphic and therefore ABI-bound)
    // against this tree's Qt 6.10.2 on macOS arm64. This is the type that
    // proves the 24-byte buffer, a role ICoreRasterImage would have carried
    // had it not turned out to be dead.
    static constexpr std::size_t kNativeStorageSize = 24;
    static constexpr std::size_t kNativeStorageAlign = 8;

};

ICoreRgba.h#

src/ICoreEssentials/UI/Values/ICoreRgba.h

ICoreRgba#

ICoreRgba.h:35 · class · 13 declaration(s)

ICoreRgba -- an 8-bit-per-channel colour VALUE that owns its storage.

class ICoreRgba {
public:
    // An invalid colour, matching the toolkit's default-constructed state: the
    // channels read back as 0 but alpha reads 255, and isValid() is false.
    // Reproduced rather than simplified because ICoreVisualIdentity relies on
    // value-initialised members behaving the way the old ones did.
    constexpr ICoreRgba() = default;

    constexpr ICoreRgba(int r, int g, int b, int a = 255)
        : m_r(clampChannel(r))
        , m_g(clampChannel(g))
        , m_b(clampChannel(b))
        , m_a(clampChannel(a))
        , m_valid(true) {
    }

    // Accepts "#rgb", "#rrggbb" and "#aarrggbb" -- the three spellings the
    // toolkit parses that this project actually writes. ALPHA COMES FIRST in
    // the 8-digit form, matching HexArgb and ICoreColor::toHexString(), which
    // is a PERSISTED format: it lands in project files and recipe scripts.
    // Anything that reorders those digits changes what an existing saved
    // project reads back as.
    //
    // A string that does not parse yields an invalid colour rather than
    // throwing, which is what the toolkit does with an unrecognised name.
    constexpr explicit ICoreRgba(std::string_view hex) {
        parseHex(hex);
    }

    constexpr bool isValid() const { return m_valid; }

    constexpr int red() const { return m_r; }
    constexpr int green() const { return m_g; }
    constexpr int blue() const { return m_b; }
    constexpr int alpha() const { return m_a; }

    constexpr void setAlpha(int a) { m_a = clampChannel(a); }

    // Returns a copy with the alpha replaced. The free-standing withAlpha()
    // helper in ICoreTheme.cpp did exactly this, by value.
    constexpr ICoreRgba withAlpha(int a) const {
        ICoreRgba c = *this;
        c.setAlpha(a);
        return c;
    }

    // Linear interpolation between two colours, t in [0..1].
    //
    // Two behaviours here are inherited verbatim from the toolkit-backed
    // version this replaces, and both are load-bearing for the derived themes:
    //   1. The RESULT IS ALWAYS OPAQUE. The original built its result from a
    //      three-argument constructor, which sets alpha to 255 regardless of
    //      what the operands' alphas were. Alpha is NOT interpolated.
    //   2. The channel maths TRUNCATES toward zero (static_cast<int>), it does
    //      not round. Rounding would shift derived tokens by a unit and move
    //      every golden image.
    static constexpr ICoreRgba mix(const ICoreRgba& a, const ICoreRgba& b, double t) {
        return ICoreRgba(
            static_cast<int>(a.m_r + (b.m_r - a.m_r) * t),
            static_cast<int>(a.m_g + (b.m_g - a.m_g) * t),
            static_cast<int>(a.m_b + (b.m_b - a.m_b) * t));
    }

    // "#rrggbb", or "#aarrggbb" when the colour is not fully opaque.
    // Mirrors ICoreColor::toHexString() exactly -- see the persistence note on
    // the hex constructor above.
    std::string toHexString() const {
        return m_a == 255 ? toHexRgbString() : toHexArgbString();
    }

    std::string toHexArgbString() const {
        std::string s = "#";
        appendByte(s, m_a);
        appendByte(s, m_r);
        appendByte(s, m_g);
        appendByte(s, m_b);
        return s;
    }

    std::string toHexRgbString() const {
        std::string s = "#";
        appendByte(s, m_r);
        appendByte(s, m_g);
        appendByte(s, m_b);
        return s;
    }

    friend constexpr bool operator==(const ICoreRgba& a, const ICoreRgba& b) {
        return a.m_valid == b.m_valid && a.m_r == b.m_r && a.m_g == b.m_g
               && a.m_b == b.m_b && a.m_a == b.m_a;
    }
    friend constexpr bool operator!=(const ICoreRgba& a, const ICoreRgba& b) {
        return !(a == b);
    }

};