spacr.qt.theme¶
Provide palettes, geometry tokens, and QSS for the spaCR Qt interface.
Use active_palette() for colors shown by a live widget and
stylesheet() for the application stylesheet. THEMES contains
the selectable palettes "dark", "light", "cell", "glass",
"high_contrast" and the night and data-art presets of
spacr.qt.night_themes; the "system"
preference resolves to dark or light before palette lookup. A legacy
"space" palette can still be read from persisted settings but is not
selectable.
Warning
theme.PALETTE is a deprecated, read-only alias for the dark palette and
does not follow runtime theme changes. Use active_palette(), or
DARK_PALETTE only when dark colors are explicitly required.
Cell and Glass are IMAGE_THEMES. Their panels use translucent scrims
so the backdrop remains visible without sacrificing text contrast.
contrast_failures() validates roles painted on scrims, while
image_contrast_failures() validates roles painted directly over image
content. solve_scrim_alpha() balances those contrast constraints against
MIN_PICTURE_CONTRAST, and scrim_report() exposes the result.
enable_spaceout() dresses the process in the rainbow palette the
spaceout entry point launches into. It re-hues whichever theme is
resolved rather than adding a fifth one, so THEMES and the light/dark
handling are untouched; it is process state and is never persisted.
Functions¶
|
Serve the deprecated |
|
|
|
The palette for the theme that is on screen right now. |
|
Advance the spaceout clock and return the resulting hue rotation. |
|
Apply the shared close-mark glyph, styling, and hit-target size. |
|
Apply the palette to the QApplication so native controls (menu |
|
Install |
|
|
|
The colour for TEXT drawn in the button accent straight on the page. |
|
Tag every layout container under |
|
Remove screen-local late-QSS suffixes before a global theme rebuild. |
|
Create a standalone close mark as a flat |
|
Return normal, hover, and disabled close-mark colours for a theme. |
|
Return the close-mark font size in pixels. |
|
Return the shared Qt style-sheet rules for close marks. |
|
Return the required side length for a close-mark hit target. |
|
Alpha-composite |
|
Human-readable description of every rule |
|
WCAG contrast ratio between two colours — 1.0 (same) to 21.0. |
|
Measured contrast for every rule in |
|
Render a colour for QSS — plain hex, or |
|
Disable process-local spaceout rendering and reset its clock. |
|
The left dock's background. Never translucent, in any theme. |
|
The colour a surface role actually presents to the eye. |
|
Enable process-local spaceout rendering. |
|
Install late registered blocks on |
|
Return theme colors and geometry for faded field containers. |
|
Alpha of a field's container at fraction |
|
The sampled ramp as |
|
Return a font size in px with the user's Zoom preference applied. |
|
Return a neutral, layered QSS brush that suggests optical depth. |
|
Tell the platform which scheme spaCR draws in, so it cannot differ. |
|
Every rule |
|
Measured contrast for every rule, judged over a real image colour. |
|
Install shared close marks on closable tabs below |
|
Return whether a widget uses the shared close-mark styling. |
|
Whether |
|
Thinnest scrim for |
|
CIE L* of |
|
Import |
|
Stop |
|
Have |
|
Declare that |
|
Replace existing tab close buttons with the shared close mark. |
|
Brightest a background image may be before |
|
The QSS colour the menu bar and its corner chrome both paint. |
|
The flat colour the page is painted with under |
|
Human-readable description of every separation |
|
How far each resting panel role sits from the page in |
|
Home's tab treatment, for a tab strip that IS the page. |
|
Draw a rounded, translucent panel filling |
|
Return the palette dict for |
|
The alpha a user-controlled page surface is actually painted at. |
|
Thinnest the Home pane may be painted and stay readable. |
|
A page-surface colour, already carrying the user's page opacity. |
|
Apply the page-opacity preference to a shared UI surface role. |
|
|
|
How much of the wallpaper survives |
|
Thickest scrim for |
|
Return |
|
Register a QSS block appended to every generated stylesheet. |
|
Render every registered block into one QSS fragment. |
|
WCAG relative luminance of a |
|
Reapply Qt styling after a widget property changes. |
|
The meaningful hairline on outlined tiles — the theme's own ink. |
|
|
|
Opacity of surface |
|
Every role of |
|
Both bounds, the solved alpha and what it buys, for every role. |
|
Brightest colour |
|
Text colour for a row selected with the accent behind it. |
|
Replace |
|
Set the spaceout animation clock, clamped to zero or greater. |
|
Record the exact live preference inputs for late screen blocks. |
|
Resize a close-mark button for its current font and interface scale. |
|
The opacity |
|
Return the continuous spaceout hue rotation in degrees. |
|
Return the elapsed spaceout animation time in seconds. |
|
Return |
|
Return whether spaceout rendering is enabled for this process. |
|
Re-hue a theme palette while preserving accessible luminance. |
|
The lowest alpha at which |
|
Hover colour for |
|
Return the QSS string that styles every custom widget in the app. |
|
What the operating system's own light/dark setting is, if Qt knows. |
|
Disable overflow buttons for every tab bar below |
|
Drop a registered block. |
Put the application on Fusion unless somebody asked for a style. |
|
|
Every registered block name, in registration order. |
|
The sheet |
Module Contents¶
- spacr.qt.theme.active_page_colour() str[source]¶
page_colour()for the theme that is on screen right now.Falls back to dark, like
active_palette(), and for the same reason: a backdrop must never be the thing that stops a screen opening.
- spacr.qt.theme.active_palette() dict[source]¶
The palette for the theme that is on screen right now.
This is what a widget wants.
DARK_PALETTEis a constant; the theme is a preference, and it changes while the process is running. A widget that inlines colours — anything that builds its ownsetStyleSheetstring, and anything that paints in apaintEvent— must resolve them through here, per instance, at construction or paint time.Screens are rebuilt on a theme change (
MainWindow._rebuild_startup_pagefor Home, and the stylesheet is re-applied to everything else), so one call per widget build is enough; there is no need to cache the result across constructions.Falls back to dark if preferences cannot be read — headless, no
QApplication, a corrupt settings file — because that is what the app looked like before this function existed.
- spacr.qt.theme.advance_spaceout_drift(dt: float) float[source]¶
Advance the spaceout clock and return the resulting hue rotation.
Non-positive intervals and calls made while spaceout is disabled do not modify the clock.
- Parameters:
dt – seconds to add to the spaceout clock; ignored when not positive or when spaceout is off.
- spacr.qt.theme.apply_close_mark(button, *, tooltip: str | None = None, body_px: int | None = None)[source]¶
Apply the shared close-mark glyph, styling, and hit-target size.
- Parameters:
button – Qt button to configure.
tooltip – Replacement tooltip.
Nonepreserves the existing tooltip.body_px – Optional resolved body-text size.
- Returns:
The configured button.
- spacr.qt.theme.apply_qpalette(app: PySide6.QtWidgets.QApplication, theme: str = 'dark', *, follow_system: bool = False) None[source]¶
Apply the palette to the QApplication so native controls (menu bars, tooltips, dialogs) match the QSS-styled widgets.
The platform is told the theme’s scheme first (see
hold_the_colour_scheme()), so a theme change the operating system reports afterwards cannot repaint the roles set here. Disabled text is the dim ink, so a disabled control reads as disabled on every theme.- Parameters:
app – the running QApplication.
theme – one of
THEMES; unknown values fall back to dark.follow_system – release the scheme to the operating system instead of pinning it; the explicit “Follow system” choice.
- spacr.qt.theme.apply_stylesheet_per_window(app, sheet: str) int[source]¶
Install
sheeton every top-level window instead of onapp.WHY, WITH THE NUMBER.
QApplication.setStyleSheetrepolishes every widget the process owns, and a session that has opened a few modules owns thousands it cannot see –MainWindowbuilds a module screen on first navigation and keeps it in the stack afterwards. Measured on this box, offscreen, four modules open, 9,045 live widgets of which 6,111 are on screens nobody is looking at:app.setStyleSheet 2,836 ms first, ~7,500 ms thereafter every top-level window 1,684 ms first, ~1,900 ms thereafter the visible screen alone 222 ms
A QUARTER OF THE COST FOR THE SAME PICTURE. The floor is lower still – 222 ms is what the visible screen costs on its own – and reaching it means not sheeting the hidden screens either, which is a bigger change than this one.
- Parameters:
app – the
QApplicationwhose windows wear the sheet, and where the sheet itself is parked so a window created later can find it.Noneis accepted and does nothing, so a caller running without an application does not have to check first.sheet – the complete application stylesheet, as
stylesheet()composes it.
- Returns:
the number of windows the sheet was put on.
A settings category’s body that is waiting off the page is parentless, so Qt lists it among the top-level widgets, but it is not a window: it goes back under its page before anybody sees it and wears the page’s sheet from there. Sheeting it here would leave it carrying a copy of the sheet of the moment when it went back, which the next theme change would not reach. See
spacr.qt.widgets.section.Section._detach_body_while_hidden().
- spacr.qt.theme.block_surface(role: str = 'surface_alt', theme: str | None = None, opacity: float | None = None) str[source]¶
pane_surface()for a registered QSS block:NoneIS the scrim.The two differ in exactly one place, and it matters only there. A block registered with
register_widget_qss()is handed theopacitystylesheet()was called with, and that isNonewhen the caller asked for the theme’s designed scrim rather than any user’s setting — the documented meaning ofsurface_opacity=None, and what every built-in rule in this file honours throughpanel_alpha().pane_surface()cannot honour it. ItsNonemeans “nobody told me, go and look”, so it reads the live page-opacity preference. That is right for the inline and paint-time callers it was written for — Home’s aside panels have nostylesheetargument to plumb through — and wrong inside a block, whereNonewas already an answer. The consequence was thatstylesheet("dark")emittedrgba(22, 23, 25, 0.600)for every block that passed it through: a function of its arguments quietly depending on a QSettings value, which made the assertion that the opaque themes emit plain hex pass or fail on module import order.No user-visible change. The live path calls
stylesheet()with the preference already in hand, soopacityis a number there and the two functions return the same string; the emitted sheet was compared byte for byte across every theme at 30 %, 60 % and 100 %.- Parameters:
role – a palette key, normally
surface/surface_alt/tile.theme – theme name;
Noneresolves the effective one.opacity – 0..1, straight from the block’s own argument.
Nonemeans the theme’s designed scrim and is NOT looked up anywhere.
- Returns:
a QSS colour — plain hex when opaque,
rgba()when not.
- spacr.qt.theme.button_accent_text(palette: dict | None = None) str[source]¶
The colour for TEXT drawn in the button accent straight on the page.
button_accentis the same blue on every theme (seeCONSTANT_ROLES), and as a fill or an outline it is. As the ink of an outlined button’s caption it is not readable on a light page: #4A9EFF on the light theme’s page is about 2.2:1, and on Glass’s lightest panel it is 3.9:1. So the caption takes the first of a short list that clears 4.5:1 on every panel the theme has: on a dark theme the constant blue, then its lighterbutton_accent_hi; on a light theme the theme’s darkeraccent_hi, thenaccent_lo. The ink itself is the last resort, which clears it on any theme that passes its own contrast rules.Derived on request rather than stored as a palette role, so the spaceout dressing (which re-hues every stored role) needs no entry for it.
- Parameters:
palette – a palette carrying
bg,fgand the accent roles;active_palette()when omitted.- Returns:
a hex colour.
- spacr.qt.theme.clear_container_surfaces(root) int[source]¶
Tag every layout container under
rootso it paints nothing.Most spaCR screens are plain
QWidgettrees. AQWidgetwith no QSS rule of its own inherits the blanketQWidget {{ background-color: bg }}and paints the WINDOW colour — which is not a surface, so no value of the page-opacity preference can reach it. That is why a screen could sit as a black slab over the animated background no matter what the slider said.The rule, and it is a heuristic worth stating plainly:
an anonymous
QWidget(noobjectName) is scaffolding — it exists to hold a layout, so it should show whatever is behind it;a named widget is something the designer styled on purpose — a
Card, aSection, aConsoleBox— and keeps its fill, at the page opacity.
Scroll areas, their viewports and splitters are always containers whatever they are called, so they are tagged by type — unless the screen has declared one a surface with
mark_surface(). That is the one opt-out, and it exists because the type test cannot tell a table that sits ON a pane from a table that IS the page. Seemark_surface().- Parameters:
root – the screen (or any subtree) to sweep.
- Returns:
how many widgets were tagged, which is what a test asserts on.
- spacr.qt.theme.clear_widget_qss_overlays(app=None) int[source]¶
Remove screen-local late-QSS suffixes before a global theme rebuild.
The rebuilt application sheet contains every block registered so far, with the new theme, opacity and font scale. Leaving an older local copy in place would give it precedence and strand the screen on the previous preference values.
- Returns:
number of screen roots whose owned suffix was removed.
- spacr.qt.theme.close_mark_button(parent=None, *, tooltip: str | None = None, body_px: int | None = None)[source]¶
Create a standalone close mark as a flat
QToolButton.
- spacr.qt.theme.close_mark_colours(theme: str = 'dark') Dict[str, str][source]¶
Return normal, hover, and disabled close-mark colours for a theme.
- spacr.qt.theme.close_mark_font_px(body_px: int | None = None) int[source]¶
Return the close-mark font size in pixels.
- Parameters:
body_px – Resolved body-text size.
Noneusesfont_px().
- spacr.qt.theme.close_mark_rules(theme: str = 'dark', body_px: int | None = None) str[source]¶
Return the shared Qt style-sheet rules for close marks.
- spacr.qt.theme.close_mark_side(widget=None, body_px: int | None = None) int[source]¶
Return the required side length for a close-mark hit target.
The result accounts for the rendered glyph, current interface scale, and
CLOSE_MARK_HIT_PXminimum. The glyph’s ink box is measured once per font:tightBoundingRectrasterises the glyph, and a module screen re-measures each of its close marks at every style change.
- spacr.qt.theme.composite(top: str, alpha: float, under: str = WORST_CASE_UNDER) str[source]¶
Alpha-composite
topatalphaoverunder, as hex.- Parameters:
top – the upper colour, a
#rgbor#rrggbbcolour string.alpha – opacity of
top, clamped to [0, 1].
- spacr.qt.theme.contrast_failures(theme: str) List[str][source]¶
Human-readable description of every rule
themefails.- Parameters:
theme – the theme name, as passed to
palette_for().
- spacr.qt.theme.contrast_ratio(a: str, b: str) float[source]¶
WCAG contrast ratio between two colours — 1.0 (same) to 21.0.
- Parameters:
a – one colour, a
#rgbor#rrggbbcolour string.b – the other colour; the order of
aandbdoes not matter.
- spacr.qt.theme.contrast_report(theme: str) List[dict][source]¶
Measured contrast for every rule in
CONTRAST_RULES.Each entry is
{"fg", "bg", "fg_color", "bg_color", "ratio", "required", "passes"}. Surfaces are resolved througheffective_surface(), so Space is judged on the composited scrim rather than on a colour the user never actually sees.- Parameters:
theme – the theme name, as passed to
palette_for().
- spacr.qt.theme.css_color(color: str, alpha: float = 1.0) str[source]¶
Render a colour for QSS — plain hex, or
rgba()when translucent.- Parameters:
color – the colour, a
#rgbor#rrggbbcolour string. Returned unchanged whenalphais 1 or more.
- spacr.qt.theme.disable_spaceout() None[source]¶
Disable process-local spaceout rendering and reset its clock.
- spacr.qt.theme.dock_colour(theme: str = 'dark') str[source]¶
The left dock’s background. Never translucent, in any theme.
The dock used to paint
surface, which the image themes re-render throughscrim_alpha()— so on Space and Cell the app list was a ghost with a galaxy behind every row. A navigation column is chrome: it is the thing you look at when you have lost your place, and it has to be a solid edge for the page to end at.White under the light theme (its
surface), a dark grey everywhere else (surface_alt, one step up from the near-black window so the dock reads as a separate plane rather than as more page).
- spacr.qt.theme.effective_surface(theme: str, role: str, under: str | None = None) str[source]¶
The colour a surface role actually presents to the eye.
For opaque themes that is just the palette entry —
undercannot reach through an alpha of 1.0. For an image theme it is the scrim composited overunder, which defaults toscrim_under(): the brightest thing that theme’s wallpaper pipeline can put behind a panel. White for Space, whose sky blows its sun out on purpose; the exposure ceiling for Cell, whose every wallpaper is solved down to it.- Parameters:
theme – the theme name, as passed to
palette_for().role – the surface role, a key of the theme’s palette.
- spacr.qt.theme.enable_spaceout() None[source]¶
Enable process-local spaceout rendering.
This operation is idempotent and does not modify saved preferences.
- spacr.qt.theme.ensure_widget_qss_applied(*names: str, root=None) bool[source]¶
Install late registered blocks on
rootwithout restyling the app.The production application stylesheet is composed before unopened screen modules are imported.
Replacing that whole sheet for each import closes the first-paint race, but it also makes Qt parse the sheet and re-polish every widget accumulated in every cached screen.
A screen root is a QSS scope: rules installed there reach that screen and its descendants, and applying them before the root is shown preserves the same first-paint contract without touching Home or any previously opened module.
The suffix contains every registered block absent from the application sheet, in registry order, rather than only
names. This keeps blocks imported by a screen’s dependencies together and means a later call can replace one complete suffix instead of stacking fragments with different preference values.namesremains the caller’s documentation of the blocks it requires; omitting it is the MainWindow screen-host path.It is a no-op with no
root, noQApplication, or no spaCR sheet in force. A caller that never opted into spaCR styling is not opted in merely by constructing one of its widgets.THE SHEET IN FORCE IS NOT
app.styleSheet()ANY MORE. Per-window sheeting takes the application sheet DOWN on purpose and gives each window its own, soapp.styleSheet()is empty in every production run – and this function read that as “nobody opted in” and returned before doing anything. Measured: opening one module registers four blocks (SettingsBox,ClassEditor,SettingAlphabetChip,TableChip) and NONE of the four reached the screen that had just imported them. That is the exact defect this function was written for, reintroduced by the change that made the window the sheet’s owner.A ROOT WHOSE SHEET IS STILL OWED KEEPS THE BLOCKS AND IS NOT RESTYLED. A module screen built after the theme is in force carries no sheet until its first show, and
_sheet_one_window()puts this suffix on the end of the sheet it applies then. Applying it here as well would repolish the whole screen once for the suffix and again when Qt reparents it into the stack – 829 ms and about as much again on a Regression open, for a screen nobody could see yet.- Returns:
Trueonly whenroot.setStyleSheetwas called.
- spacr.qt.theme.field_chrome(theme: str = 'dark') Dict[str, object][source]¶
Return theme colors and geometry for faded field containers.
- Parameters:
theme (str, default="dark") – Theme name accepted by
palette_for().- Returns:
dict – Radius, fill, border, focus, and disabled-state tokens. Each color is represented as
(hex_color, alpha).
Notes
The fade multiplies each token’s alpha so translucent themes retain their material. Field chrome is independent of the page-opacity preference.
- spacr.qt.theme.field_fade_alpha(t: float) float[source]¶
Alpha of a field’s container at fraction
tacross its width.tis clamped to [0, 1].field_fade_alpha(0.0)is 1.0 — a field is fully opaque where its value begins, no matter what the page-opacity preference is set to — andfield_fade_alpha(1.0)is 0.0.This is the container and its outline only. The text is drawn after the ramp, at full alpha, and never passes through this function.
- Parameters:
t – fraction of the way across the field, clamped to [0, 1].
- spacr.qt.theme.field_fade_profile(stops: int = FIELD_FADE_STOPS)[source]¶
The sampled ramp as
((t, alpha), ...), left edge first.What a
QLinearGradientis built from, and what a test asserts the shape of without needing a QApplication.
- spacr.qt.theme.font_px(role_or_px, scale: float | None = None) int[source]¶
Return a font size in px with the user’s Zoom preference applied.
The application stylesheet scales
FONT_SIZEitself (seestylesheet()), so anything styled by it already tracks Zoom. What does not is a widget that sets its own sheet — a per-widgetsetStyleSheetbeats the application sheet whatever the selector says — or one that paints text with aQPainter. Those surfaces hard-coded a pixel number and so stayed 13 px at 150 %: the tab strips, the Home aside, the hover tooltip, the Live/AI toggles.Route every such number through here instead of writing a literal.
- Parameters:
role_or_px – a
FONT_SIZEkey ("body","small"…) or a raw base pixel size.scale – override the preference — used by
stylesheet(), which is generating a sheet for a scale that may not be the saved one yet.Nonereads the preference.
- Returns:
at least 1 px, because Qt refuses a pixel size of zero or less; no larger floor is imposed.
- spacr.qt.theme.glass_material(color: str, alpha: float) str[source]¶
Return a neutral, layered QSS brush that suggests optical depth.
QSS cannot sample and refract pixels behind a widget. A thin bright upper layer, translucent neutral body, and slightly denser lower edge provide the stable cross-platform cues of glass without pretending opacity alone is a material.
- Parameters:
color – the body colour of the glass, a
#rgbor#rrggbbcolour string.alpha – base opacity of the body, clamped to [0, 1]; the highlight and lower edge are drawn slightly denser.
- spacr.qt.theme.hold_the_colour_scheme(app, theme: str | None) bool[source]¶
Tell the platform which scheme spaCR draws in, so it cannot differ.
Qt 6.8 added
QStyleHints.setColorScheme. Without it a Mac in light appearance draws spaCR’s title bar, native menus and file dialogs light around a dark window, and Windows does the same with its title bar; on Linux the GTK and KDE platform themes feed their own palette to the style. Setting it pins those to the theme in force.Nonereleases the request, which is what the explicit “Follow system” choice wants.A no-op returning
Falseon a Qt older than 6.8, where the palette and stylesheet (and the Fusion styleuse_a_style_that_honours_the_palette()installs) still carry the colours.- Parameters:
app – the running application.
theme – a theme from
THEMES, orNoneto follow the system again.
- Returns:
Truewhen the request reached Qt.
- spacr.qt.theme.image_contrast_failures(theme: str, under: str) List[str][source]¶
Every rule
themefails over a wallpaper colourunder.- Parameters:
theme – the theme name, as passed to
palette_for().under – the wallpaper colour sampled behind the text, a
#rgbor#rrggbbcolour string.
- spacr.qt.theme.image_contrast_report(theme: str, under: str) List[dict][source]¶
Measured contrast for every rule, judged over a real image colour.
underis a colour sampled from the wallpaper — in practice the mean of its brightest text-line-sized region, which is whatspacr.qt.imagery.brightest_window()returns. Rules naming thebgsurface are judged against it directly, because in an image theme nothing is painted between the photograph and the text. Everything else is judged against its scrim composited over it.- Parameters:
theme – the theme name, as passed to
palette_for().under – the wallpaper colour sampled behind the text, a
#rgbor#rrggbbcolour string.
- spacr.qt.theme.install_close_marks(root, *, tooltip: str | None = None) int[source]¶
Install shared close marks on closable tabs below
root.rootmay be a tab widget, tab bar, or containing widget. Event filters also style close buttons added later. Repeated calls are idempotent.- Parameters:
root – a
QTabWidget, aQTabBar, or any widget whose child tab widgets and tab bars are marked.- Returns:
Number of close marks installed during this call.
- spacr.qt.theme.is_close_mark(widget) bool[source]¶
Return whether a widget uses the shared close-mark styling.
- Parameters:
widget – the widget to test; None gives False.
- spacr.qt.theme.is_surface(widget) bool[source]¶
Whether
widgetwas declared a page surface bymark_surface().- Parameters:
widget – the widget to test; None gives False.
- spacr.qt.theme.legible_scrim_floor(theme: str, role: str, colour_role: str | None = None, under: str | None = None) float[source]¶
Thinnest scrim for
rolethat text is still readable over.Every rule in
CONTRAST_RULESthat paints text on this surface must clear its WCAG minimum (timesSCRIM_HEADROOM) with the surface composited overscrim_under()— the brightest thing the theme’s wallpaper pipeline can put behind it. Below this number the panel stops being readable; it is a hard lower bound.- Parameters:
theme – name of the palette whose surface is being evaluated.
role – palette surface role on which the text is painted.
colour_role – palette entry the surface is painted with, when it differs from
role—tileis painted withsurface.under – what is actually behind the surface, when it is not the wallpaper.
pane_alpha_floor()passes the flat window colour for the opaque themes; judging a dark panel over a dark window against a white worst case would report a 0.92 floor for a surface that is legible at any alpha at all.
- spacr.qt.theme.lightness(color: str) float[source]¶
CIE L* of
color, 0 (black) to 100 (white).Perceptually uniform, which
relative_luminance()is not andcontrast_ratio()is not: a ratio of 1.09:1 means something very different between two near-blacks and between two near-whites, and the page/panel question lives at both ends.- Parameters:
color – a
#rgbor#rrggbbcolour string.
- spacr.qt.theme.load_widget_qss_registrars() Tuple[str, ...][source]¶
Import
WIDGET_QSS_MODULESso their blocks are registered.Called by the exhaustive/default
stylesheet()path before it composes anything. The production preference path opts out so unopened data screens do not import their scientific dependencies merely to contribute decoration.Idempotent, and the flag is set BEFORE the imports rather than after: several of these modules call
stylesheet()while being imported, and without that ordering the first one would recurse.One module’s failure costs that module’s rules and nothing else. A widget QSS block is decoration; it must never be the thing that stops the GUI from starting.
- Returns:
the module names that imported cleanly.
- spacr.qt.theme.make_transparent(*widgets) None[source]¶
Stop
widgetspainting a background of their own.A backdrop — the theme’s wallpaper, or the DNA rain on the sequencing screen — is behind the page, and in the opaque themes every container between it and the eye is an opaque
bgby virtue of the blanketQWidgetrule. One container is enough to bury it: a screen’s header widget, its splitter, a scroll area and that scroll area’s viewport are each a QWidget, and each one used to paint solid black over the animation the screen had just installed.Tag the layout containers with this and the backdrop reaches the eye; leave the cards, panels and inputs alone and they stay the readable surface on top of it. Safe to call on a widget that is already visible — the style is re-polished so the change takes effect immediately rather than at the next theme switch.
A
QScrollArea’sviewport()is tagged automatically along with it: they are two widgets, the viewport is the one that actually paints, and forgetting it is the obvious way to get this wrong.
- spacr.qt.theme.mark_as_a_sheet_target(widget) None[source]¶
Have
widgetcarry the application sheet in its own right.FOR A WIDGET A WINDOW DOES NOT WANT TO SHEET THROUGH. A module screen is the case: sheeting the window reaches every hidden screen with it, and with four modules open that is 7,595 of the window’s 8,002 widgets repolished so that one screen can change colour.
A marked widget is sheeted when the sheet changes IF IT IS VISIBLE, and on its next
showEventotherwise – which is what makes not sheeting it now safe.- Parameters:
widget – the widget to sheet in its own right. A window may be passed and the mark is then redundant – the event filter sheets every window on sight – but it is not an error.
- spacr.qt.theme.mark_surface(*widgets) None[source]¶
Declare that
widgetsARE the page surface, not passengers on one.clear_container_surfaces()tags everyQAbstractScrollAreaby type, andQAbstractItemViewandQPlainTextEditare both one. So the shippedQTableView/QTreeView {{ background-color: surface_alt }}rule never landed on any view in the application: the attribute selector forTRANSPARENT_PROPERTYoutranks a bare type selector, and the view painted nothing at all.Where a view sits on a tab pane or inside a card that is right, and it is right by accident — the container behind supplies the surface and the view shows it through. Where a view sits straight on the page there is nothing behind it, and the backdrop arrives untouched: a measured 1.000 transmission, which over a near-black window colour reads as a black box with the text floating on it.
The sweep cannot be narrowed to exact types. Doing that flips every view in the application at once, and the ones already sitting on a pane would then stack two translucent greys and read about 0.49 — a shade no position of the page-opacity slider can produce. Nor can a type test make the distinction: Hit List’s
QTreeWidgetis the page and Control Chart’sQListWidgetis a passenger, and the pair after them is the other way round. Only the screen that built the layout knows which it is, so the screen is asked, once, per view.Opt-in rather than opt-out on purpose. Today every view in the application is swept, so opting in changes nothing except where a screen says so, and a view nobody has looked at keeps the behaviour it was written against.
Two screens said this before the mechanism existed, by giving the view an object name and registering a whole QSS block for it — Model Compare’s result tables, Model Zoo’s listing and provenance box. An ID selector outranks the transparent tag the same way. This is that without the block: one call, and either the shipped table rule or the
*[spacrSurface="true"]rule supplies the fill at the user’s page opacity. The second is not redundant: nothing in the sheet covers a bareQListWidget, which would otherwise fall through to the blanketQWidgetrule and paint the WINDOW colour, which is not a surface.Safe to call before or after the sweep, and safe to call twice: the transparent tag is cleared as well as the surface tag set, and the style is re-polished so a visible widget changes immediately.
One Qt rule has to be paid on the way. A subclass of
QWidgetignores a QSS background entirely unlessWA_StyledBackgroundis set, which is why Power’s caveat panel still measured the backdrop untouched with a matching rule sitting in the sheet. The attribute is set here for those, and deliberately NOT for aQAbstractScrollArea: a view already paints its background through its viewport, and a second styled fill on top of that is the two-surfaces-stacked fault again. Measured both ways rather than reasoned about.
- spacr.qt.theme.mark_tab_bar(bar, tooltip: str | None = None) int[source]¶
Replace existing tab close buttons with the shared close mark.
Tabs without a close button remain unchanged, and hidden buttons remain hidden.
- Parameters:
bar – the
QTabBarwhose close buttons are replaced.- Returns:
Number of close marks installed.
- spacr.qt.theme.max_background_luma(theme: str) float[source]¶
Brightest a background image may be before
themefails AA.Closed form, not a search: for a foreground of relative luminance
Lfand a required ratior, WCAG allows a background up to(Lf + 0.05) / r - 0.05. The answer is the tightest of those overBARE_IMAGE_RULES, and it is whatspacr.qt.imagery.solve_dim()expects as its target.- Parameters:
theme – the theme name, as passed to
palette_for().
The QSS colour the menu bar and its corner chrome both paint.
ONE FUNCTION FOR BOTH so they cannot drift. The bar is styled from the generated stylesheet and the window chrome is styled in
spacr.qt.appwith a stylesheet of its own; two hard-coded colours that have to match is one of them going stale.- Parameters:
theme – theme name; the active theme when omitted.
- Returns:
a QSS colour string.
- spacr.qt.theme.page_colour(theme: str = 'dark') str[source]¶
The flat colour the page is painted with under
theme.Prefer this over
palette_for(theme)["bg"]anywhere the question is “what is behind the panels”.bganswers a different question — see the block above — and on the dark theme it answers it#000000.Falls back to
bgfor a palette that has nopage, so an older or third-party palette dict still resolves rather than raising.
- spacr.qt.theme.page_separation_failures(theme: str) List[str][source]¶
Human-readable description of every separation
themefails.- Parameters:
theme – the theme name, as passed to
palette_for().
- spacr.qt.theme.page_separation_report(theme: str) List[dict][source]¶
How far each resting panel role sits from the page in
theme.One entry per role in
PAGE_PANEL_ROLESper opacity in(1.0, PAGE_FADED_OPACITY), carrying{"role", "opacity", "page", "panel", "ratio", "delta_lstar", "min_ratio", "min_delta_lstar", "passes"}.The faded rows composite the panel over the page rather than over anything else, because the page is what is behind it — that is the whole subject.
- Parameters:
theme – the theme name, as passed to
palette_for().
- spacr.qt.theme.page_tabs_qss(object_name: str, palette: dict, opacity=None) str[source]¶
Home’s tab treatment, for a tab strip that IS the page.
The shipped
QTabBar/QTabWidget::panerules paintP["surface"]andP["surface_alt"]— raw hex, so a tab strip that is the main content of a screen (Classifier Evaluation, Run History) sat there as a flat opaque slab while the cards beside it thinned with the slider.This is the same shape Home uses, at the page opacity: rounded top corners, a dark-grey tab by default, the accent blue under the pointer, and a rounded translucent pane below it.
Register it per screen rather than making it a blanket rule — a tab strip inside a card is on a surface already and must keep the shipped look, or it double-fills.
- Parameters:
object_name –
objectNameof theQTabWidget.palette – the palette handed to a registered block, including the reserved
themekey.opacity – the page-opacity preference, passed straight through —
Nonehere means the theme’s designed scrim, which is why this readsblock_surface()and notpane_surface().
- spacr.qt.theme.paint_panel(painter, widget, *, role: str = 'surface', radius: int | None = None, border: bool = True, inset: float = 0.0, theme: str | None = None, opacity: float | None = None) None[source]¶
Draw a rounded, translucent panel filling
widget.The
paintEventcounterpart of the QSS panel rules, for the regions QSS cannot reach. Call it first in apaintEvent, in place ofpainter.fillRect(self.rect(), QColor(palette["surface"]))— that call is opaque by construction and is what makes a custom-painted canvas read as a bare dark hole punched through the page.Composites rather than replaces: the widget must be
WA_TranslucentBackgroundor otherwise unfilled for the backdrop to reach this, which is what tagging the parents withmake_transparent()arranges.- Parameters:
painter – an active
QPainteronwidget.widget – the widget being painted; its
rect()is the panel.role – palette key for the fill.
radius – corner radius in px;
NoneusesRADIUS["md"].border – draw the theme’s soft hairline around the panel.
inset – shrink the panel by this many px on every side, so a 1 px border lands inside the widget instead of being clipped.
- spacr.qt.theme.palette_for(theme: str = 'dark') dict[source]¶
Return the palette dict for
theme.themeis one ofTHEMES; anything else (including"system", which the caller is expected to have resolved) falls back to the dark palette. The returned dict always carries every theme-invariant key fromCONSTANT_ROLESso callers can hit e.g.palette_for(t)["button_accent"]and know the value is the same across themes.Under the
spaceoutdressing (spaceout_enabled()) the result is re-hued onto the spectrum on the way out, at whatever offset the drift has reached. The keys and the count are unchanged, and so is every surface role’s relative luminance, so callers, contrast rules and the light/dark distinction all go on working. The three ink roles are the exception and are solved rather than carried — seeSPACEOUT_INK_ROLES.
- spacr.qt.theme.pane_alpha(theme: str, opacity: float | None = None) float[source]¶
The alpha a user-controlled page surface is actually painted at.
The user’s
opacity(0..1), clamped up topane_alpha_floor().NonemeansDEFAULT_PANE_OPACITY.- Parameters:
theme – the theme name, as passed to
palette_for(). Glass scales the opacity by its designed surface scrim.
- spacr.qt.theme.pane_alpha_floor(theme: str) float[source]¶
Thinnest the Home pane may be painted and stay readable.
An image theme is judged against
scrim_under(), the brightest thing its wallpaper can put behind the panel. Everything else is judged against its own flat window colour, which is where the answer goes to (near) zero: a dark panel fading into a dark window cannot make white text harder to read, so those themes let the user take the box away entirely.- Parameters:
theme – the theme name, as passed to
palette_for(). Image themes are judged overscrim_under(), others over their ownbgcolour.
- spacr.qt.theme.pane_surface(role: str = 'surface_alt', theme: str | None = None, opacity: float | None = None) str[source]¶
A page-surface colour, already carrying the user’s page opacity.
The single accessor every container should use, including the ones styled inline rather than through
stylesheet(). Those were the gap: Home’s aside panels, the dock and the tile boxes all readactive_palette()directly, which returns raw hex, so they stayed fully opaque no matter what the preference said and the setting looked broken from the page the user lands on.Reads the live preference when
opacityis not given, so a caller does not have to plumb it through — and falls back to the theme’s designed scrim if preferences cannot be read at all, which is what a first run mid-generation gets.- Parameters:
role – a palette key, normally
surface/surface_alt/tile.theme – theme name;
Noneresolves the effective one.opacity – 0..1 override;
Nonereads the preference.
- Returns:
a QSS colour — plain hex when opaque,
rgba()when not.
- spacr.qt.theme.panel_alpha(theme: str, role: str, opacity: float | None = None) float[source]¶
Apply the page-opacity preference to a shared UI surface role.
Nonepreserves the theme’s designed scrim, which keepsstylesheet()useful to callers that have no preferences store. A numeric value is the user’s requested alpha and is honoured for every card, settings section, console and preview surface, clamped only where going thinner would make that role’s text illegible. Glass treats it as relative material strength, because making 100% an opaque fill would remove the defining property of the theme.Popups stay opaque because they are separate native windows; making those translucent reveals the desktop rather than the spaCR backdrop.
- Parameters:
theme – the theme name, as passed to
palette_for().role – the surface role;
elevatedis always opaque.
- spacr.qt.theme.panel_qcolor(role: str = 'surface', theme: str | None = None, opacity: float | None = None) PySide6.QtGui.QColor[source]¶
pane_surface()as aQColor, alpha included.The QSS accessor is no use to a widget that draws itself: a custom-painted canvas has no stylesheet to put
rgba(...)in, and the obviousQColor(active_palette()["surface"])it reaches for instead is raw hex — fully opaque, whatever the page-opacity preference says. That is how a screen ends up with one flat black rectangle in the middle of a page of translucent panels.- Parameters:
role – palette key, normally
surface/surface_alt.theme – theme name;
Noneresolves the effective one.opacity – 0..1 override;
Nonereads the preference.
- spacr.qt.theme.picture_contrast(theme: str, role: str, alpha: float, colour_role: str | None = None) float[source]¶
How much of the wallpaper survives
roleatalpha.The WCAG ratio between the panel sitting over the brightest thing the theme can put behind it and the same panel over black: the dynamic range of the picture as seen through the panel. 1.0 is an opaque panel — no picture at all.
- Parameters:
theme – the theme name, as passed to
palette_for().role – the surface role, a key of the theme’s palette.
alpha – the panel’s opacity, 0 to 1.
- spacr.qt.theme.present_scrim_ceiling(theme: str, role: str, colour_role: str | None = None) float[source]¶
Thickest scrim for
rolethat the picture still reads through.The largest alpha whose
picture_contrast()is still at leastMIN_PICTURE_CONTRAST. Above this number the wallpaper is a ghost — which is the bug this whole solver exists to close.- Parameters:
theme – the theme name, as passed to
palette_for().role – the surface role, a key of the theme’s palette.
- spacr.qt.theme.preserve_widget_qss_overlay(root, stylesheet: str) str[source]¶
Return
stylesheetwithroot’s owned late-QSS suffix intact.A screen may legitimately replace its own base stylesheet after its late widget blocks were installed. Folding the suffix into that existing assignment avoids a second
setStyleSheetcall (and its palette-change cascade) while keeping the blocks available for the next paint.- Parameters:
root – the widget whose stored late-QSS suffix is appended; a widget without one adds nothing.
stylesheet – the new base stylesheet, converted to a string.
- spacr.qt.theme.register_widget_qss(name: str, fn, *, replace: bool = False)[source]¶
Register a QSS block appended to every generated stylesheet.
Registration itself never re-applies the
QApplicationstylesheet. Qt re-polishes every live widget on a globalsetStyleSheetcall; doing that once for every screen imported on demand made later module opens progressively slower.ensure_widget_qss_applied()installs the missing blocks on the new screen’s root before it can paint instead.- Parameters:
name – stable identifier, normally the widget’s
objectName. It is what the block is reported and unregistered by; it does not appear in the QSS.fn –
fn(palette, opacity) -> str, called once perstylesheet()call.paletteis the theme’s palette with the three surface roles (surface,surface_alt,surface_hi) already rendered through the user’s page opacity — the same values the built-in rules interpolate, so a registered block matches the app without doing the alpha maths. Two reserved non-colour keys ride along:theme(the theme name, whichpane_surface(),pane_alpha()andpalette_for()all want) andfont_scale.opacityis the user’s page-opacity preference, orNonefor “use the theme’s designed scrim”. Pass it straight through toblock_surface()/pane_alpha()/panel_alpha()rather than interpreting it:Noneis not 1.0, and the legibility floor is theirs to apply.block_surface()and notpane_surface(), which is the near-identical accessor for inline and paint-time callers. ItsNonemeans “nobody told me” and reads the live preference, so a block using it turnsstylesheet(theme)into a function of a QSettings value rather than of its arguments.replace – allow re-registering
name. Off by default so two widgets cannot quietly claim one name.
- Raises:
ValueError – on a duplicate name without
replace.TypeError – if
fnis not callable.
- spacr.qt.theme.registered_widget_qss(palette: dict, opacity: float | None = None, *, names=None) str[source]¶
Render every registered block into one QSS fragment.
Empty (not even a newline) while nothing is registered, which is what keeps the shipped stylesheet byte-identical to the one that had no seam at all.
A block that raises, or returns something that is not a string, is dropped with a logged traceback rather than taking the stylesheet down: an unstyled widget is a cosmetic fault, and an exception here would leave the whole application unstyled — black text on a black window — because one contributed widget had a typo.
- Parameters:
palette – the palette dict passed, with
opacity, to every registered block function.
- spacr.qt.theme.relative_luminance(color: str) float[source]¶
WCAG relative luminance of a
#rrggbbcolour, in [0, 1].- Parameters:
color – a
#rgbor#rrggbbcolour string.
- spacr.qt.theme.repolish(widget) None[source]¶
Reapply Qt styling after a widget property changes.
- Parameters:
widget – the widget to unpolish, polish and update.
- spacr.qt.theme.rim_colour(theme: str = 'dark') str[source]¶
The meaningful hairline on outlined tiles — the theme’s own ink.
White in the dark themes, near-black in the light one, because that is what
fgalready is. Horizontal cards and interactive hover states still use it; resting Home module tiles deliberately do not. Deriving it fromfgrather than writing#ffffffmeans every palette gets the right ink without a raw colour drifting out of sync.
- spacr.qt.theme.scheme_of(theme: str) str[source]¶
"light"or"dark": which waythemedraws its text.Read off the palette rather than listed, so a new theme needs no entry here: a theme whose page is brighter than its ink is a light theme.
- Parameters:
theme – one of
THEMES.- Returns:
"light"when the page outshines the ink, else"dark".
- spacr.qt.theme.scrim_alpha(theme: str, role: str) float[source]¶
Opacity of surface
roleintheme. 1.0 unless translucent.- Parameters:
theme – the theme name, as passed to
palette_for().role – the surface role; roles and themes not in
SCRIM_ALPHAgive 1.0.
- spacr.qt.theme.scrim_failures(theme: str) List[str][source]¶
Every role of
themethat cannot be both legible and see-through.Empty when the theme manages both. A non-empty result is not a crash — legibility wins and the entry says by how much the picture misses — but it means the wallpaper is a ghost under that role and something upstream (the palette, or the exposure the imagery is solved to) has to give.
- Parameters:
theme – the theme name, as passed to
palette_for().
- spacr.qt.theme.scrim_report(theme: str) List[dict][source]¶
Both bounds, the solved alpha and what it buys, for every role.
Each entry is
{"role", "colour_role", "alpha", "floor", "ceiling", "picture", "worst_fg", "worst_ratio", "required", "legible", "shows_picture"}. This is the audit trail forSCRIM_ALPHA— the numbers a reviewer would otherwise have to re-derive to check that a solved alpha is the right one.- Parameters:
theme – the theme name, as passed to
palette_for().
- spacr.qt.theme.scrim_under(theme: str) str[source]¶
Brightest colour
theme’s wallpaper can put behind a panel.This is the whole reason the two image themes do not end up with the same alphas, and it is a property of the pipeline that produces the wallpaper, not of the palette:
Cell wallpapers always come out of
spacr.qt.imagery.render(), which exposure-solves every frame it returns — the shipped masters and the user’s own drop-in alike. No text-line-sized region of a Cell background can therefore exceedmax_background_luma(), and that ceiling, not white, is the worst case a Cell panel has to survive. Measured: the shippedmicrotubulesmaster peaks at 0.098 against a 0.109 limit,filopodiaat 0.073.Space can be the procedurally generated sky, whose exposure is anchored on the 40th percentile precisely so a sun stays white-hot (
spacr.qt.space.TARGET_SKY_PERCENTILE). That is a deliberate look, and it means the sky really does present a near-white region the size of a line of text: the 1440x900 galaxy sky measures 0.49 over a text window, colour#bab9b9. Only a strong scrim saves text over that, so Space is judged against white and its panels stay much more opaque than Cell’s.
Anything that is not an image theme gets white; its alphas are 1.0 and the answer is never used.
- Parameters:
theme – the theme name.
- spacr.qt.theme.selection_ink(theme: str = 'dark') str[source]¶
Text colour for a row selected with the accent behind it.
Derived, not chosen.
accentis a mid blue on both themes, and the ink that reads on it flips: measured, black is 7.63:1 on the dark accent and white is 5.84:1 on the light one, whilebutton_accent_ink– the obvious-looking role – is 6.96:1 on dark and 3.28:1 on light, which is below the 4.5 minimum and was shipped by picking a role instead of measuring.So it picks whichever of the theme’s own two extremes contrasts better, which keeps the colour inside the palette rather than reaching for a raw #ffffff that belongs to no theme.
- spacr.qt.theme.set_a_sheeted_widgets_own_rule(widget, rule: str) None[source]¶
Replace
widget’s own QSS without losing the sheet it carries.FOR A WIDGET THAT IS A SHEET ROOT. Before per-screen sheeting, a module screen’s own stylesheet held only its own rules and the APPLICATION carried the theme, so
setStyleSheeton the screen was a safe, local thing to do. Now the screen may be carrying all ~73 KB of the window sheet, and a plainsetStyleSheetthrows the theme away.Measured before this existed: wiping a shown page’s sheet the way
AppScreen._sync_page_palettedoes left a probe under it resolving to#000000on the dark theme, and it stayed that way until the next theme change – the digest check repairs a wipe at the widget’s next polish, and a page already on show does not get one.Falls back to a plain assignment when there is no window sheet to preserve, which is every caller that never opted into spaCR styling and every test that does not apply a theme.
A WIDGET STILL BEING BUILT IS NOT SHEETED HERE. It is marked as a sheet target and sheeted on its first
Polishas a page or its firstShow, which is before its first paint; see_the_sheet_can_wait_for_the_show()for why, and for the three repolishes of a whole module screen that saves.- Parameters:
widget – the widget whose own rules are being replaced.
rule – the QSS the widget owns, or
""to own none.
- spacr.qt.theme.set_spaceout_drift_seconds(seconds: float) None[source]¶
Set the spaceout animation clock, clamped to zero or greater.
- Parameters:
seconds – the new clock value, in seconds; negative values become 0.
- spacr.qt.theme.set_widget_qss_context(app, theme: str, font_scale: float, surface_opacity: float | None) None[source]¶
Record the exact live preference inputs for late screen blocks.
- Parameters:
app – the application object the context is stored on; None does nothing.
theme – the active theme name.
font_scale – the active font scale, stored as a float.
surface_opacity – the page-opacity preference, or None for the theme’s designed scrim.
- spacr.qt.theme.size_close_mark(button, body_px: int | None = None) None[source]¶
Resize a close-mark button for its current font and interface scale.
THE FLOORS ARE CONVERTED, and that is the whole of the GUI scale in here.
minimumWidthanswers in 100 % units (the scaling layer inspacr.qt.gui_scaleremembers what the code asked for) while the glyph is measured in the pixels actually being drawn, so comparing them raw made the box 12 px wider than the mark at 50 % and left the chrome shifted when the scale came back.- Parameters:
button – the close-mark button to give a fixed size.
- spacr.qt.theme.solve_scrim_alpha(theme: str, role: str, colour_role: str | None = None) float[source]¶
The opacity
roleshould be painted at intheme.Two bounds, pulling opposite ways:
legible_scrim_floor()is a lower bound — thinner than that and text on the panel stops clearing AA over the worst thing the wallpaper can present.present_scrim_ceiling()is an upper bound — thicker than that and the picture stops reading through the panel.
The answer is the ceiling, clamped up to the floor: as solid a panel as the picture can afford, and never thinner than legibility allows. Every alpha in that window satisfies both constraints, so the choice within it is which one to spend the slack on, and it goes to the panel: the settings form sits on this surface, and preserving its grey category structure is more important than exposing additional wallpaper. Taking the floor instead would show more picture — Cell’s floor is 0.05, a nearly transparent panel — at the cost of the form dissolving into the wallpaper.
When the floor lands above the ceiling the theme cannot do both, legibility wins, and the shortfall is visible in
scrim_report().- Parameters:
theme – name of the palette whose surface is being solved.
role – palette surface role whose opacity is being chosen.
colour_role – palette entry the surface is painted with, when it differs from
role—tileis painted withsurface.
- spacr.qt.theme.spaceout_drift(at: float | None = None) float[source]¶
Return the continuous spaceout hue rotation in degrees.
- Parameters:
at – Elapsed animation time in seconds.
Noneuses the current drift clock.- Returns:
Hue rotation in
[0, 360), or zero when spaceout is disabled.
- spacr.qt.theme.spaceout_drift_seconds() float[source]¶
Return the elapsed spaceout animation time in seconds.
- spacr.qt.theme.spaceout_drift_step(at: float | None = None) float[source]¶
Return
spaceout_drift()quantised to its solved palette grid.
- spacr.qt.theme.spaceout_enabled() bool[source]¶
Return whether spaceout rendering is enabled for this process.
- spacr.qt.theme.spaceout_palette(palette: dict, drift: float = 0.0, theme: str | None = None) dict[source]¶
Re-hue a theme palette while preserving accessible luminance.
Roles absent from
SPACEOUT_HUESare returned unchanged. Named ink roles are constrained to contrast-safe luminance bands fortheme.- Parameters:
palette – Mapping from theme roles to colour values.
drift – Hue rotation in degrees applied to all mapped roles.
theme – Theme used to resolve contrast-safe ink bands, or
Nonefor a direct hue shift.
- Returns:
A new role-to-colour mapping.
- spacr.qt.theme.splash_dim_alpha(ink: str, bg: str, *, target: float = 3.0, floor: int = 110) int[source]¶
The lowest alpha at which
inkstill readstargetoverbg.Unlit phases are meant to look unreached, so this searches UP from
floorrather than starting bright: dim enough to read as pending, legible enough to read at all.- Parameters:
ink – the text colour, a
#rgbor#rrggbbcolour string.bg – the background colour it is composited over, a
#rgbor#rrggbbcolour string.
- spacr.qt.theme.stage_hover(stage: str) str[source]¶
Hover colour for
stage; unknown stages read as stable.- Parameters:
stage – the app stage, a key of
STAGE_HOVER.
- spacr.qt.theme.stylesheet(theme: str = 'dark', font_scale: float = 1.0, background: str | None = None, surface_opacity: float | None = None, *, load_widget_registrars: bool = True) str[source]¶
Return the QSS string that styles every custom widget in the app.
Blocks registered with
register_widget_qss()are appended after everything below, so a widget’s own rules win a specificity tie against the general ones.- Parameters:
theme – one of
THEMES; unknown values fall back to dark.font_scale – multiplier applied to every font size in
FONT_SIZE. 1.0 = 100 %.background – path to a background image. Only the themes in
IMAGE_THEMESuse it;None(the default, and what a first run mid-generation gets) falls back to a flat gradient.surface_opacity – optional user-requested alpha for all shared module surfaces.
Noneuses the theme’s designed scrims.load_widget_registrars – import every module that contributes a widget block before composing. This remains the public default for exhaustive callers and tests. Application startup passes
Falseso an unopened data screen cannot pull the scientific stack into the first frame;MainWindowscopes late blocks to a new screen before inserting that screen into the visible stack.
- spacr.qt.theme.system_colour_scheme(app=None) str | None[source]¶
What the operating system’s own light/dark setting is, if Qt knows.
Any scheme spaCR asked for earlier is released first (
QStyleHints.unsetColorScheme, Qt 6.8+), so the answer is the platform’s – macOS appearance, the Windows app mode, or the GTK/KDE preference on Linux – and not an echo of what spaCR requested. Only the"system"theme calls this.- Parameters:
app – the running application;
QApplication.instance()when omitted.- Returns:
"dark","light", orNonewhen Qt cannot tell (the offscreen platform, a desktop with no preference, Qt < 6.5).
- spacr.qt.theme.take_the_scroll_arrows_off(root) int[source]¶
Disable overflow buttons for every tab bar below
root.This changes only the visibility of the scroll buttons. Qt’s keyboard and mouse-wheel tab navigation remain available.
- Parameters:
root (PySide6.QtWidgets.QWidget) – Widget, tab widget, or tab bar to inspect recursively.
- Returns:
int – Number of distinct tab bars found.
- spacr.qt.theme.unregister_widget_qss(name: str) bool[source]¶
Drop a registered block.
Trueif there was one.- Parameters:
name – the name the block was registered under, converted to a string.
- spacr.qt.theme.use_a_style_that_honours_the_palette(app=None, environ=None) str[source]¶
Put the application on Fusion unless somebody asked for a style.
spaCR’s stylesheet is written against Fusion, which draws every control from the palette. The native macOS and Windows styles draw some of them – combo boxes, spin-box buttons, scroll bars, the parts of a control no rule reaches – from the operating system’s own light or dark setting, which is how a dark spaCR came out with light fields on a light Mac.
Left alone when
QT_STYLE_OVERRIDEis set: that is somebody choosing a style on purpose. (launchhands Qt only the program name, so a-styleargument never reaches Qt and needs no exception here.)- Parameters:
app – the application;
QApplication.instance()when omitted.environ – environment to consult;
os.environwhen omitted.
- Returns:
the name of the style in force afterwards, lower case.
- spacr.qt.theme.widget_qss_names() Tuple[str, ...][source]¶
Every registered block name, in registration order.
- spacr.qt.theme.window_stylesheet(app=None) str | None[source]¶
The sheet
apply_stylesheet_per_window()last installed.The replacement for reading
app.styleSheet()back: that is empty now and says nothing about what the windows are wearing.- Parameters:
app – the application to read it off; the running one by default.
- Returns:
the sheet, or
Noneif no per-window sheet is installed.
Nested helpers¶
- _hue_shift.tint(step: int) Tuple[int, int, int]¶
The base hue at
step, lightened toward white.spacr/qt/theme.py:1134
- _hue_shift.value(level: int) Tuple[int, int, int]¶
The base hue at
level, darkened toward black.spacr/qt/theme.py:1128
- _relative_luminance.channel(value: int) float¶
One sRGB channel linearised, per WCAG’s own definition.
spacr/qt/theme.py:953
- _scrim_bounds.over(alpha: float, beneath: Tuple[int, int, int]) float¶
The luminance of the scrim at
alphaover one background.spacr/qt/theme.py:1652
- _scrim_bounds.shows(step: int) bool¶
Whether text still meets the contrast floor at this scrim strength.
Checked against BOTH the lightest and the darkest thing the scrim can sit on, because a picture backdrop is neither – an alpha that reads against one and not the other is not usable.
spacr/qt/theme.py:1668