"""Shared application theme -- single source of truth for colours and layout.
``AppTheme`` extends the base ``Theme`` with named attributes covering
backgrounds, text, accents, semantic colours, buttons, editor viewports,
gizmos, IDE minimap, autocomplete, and scrollbar styling.
``SyntaxTheme`` provides syntax highlighting colours for code editors.
Factory classmethods ``dark()``, ``light()``, and ``monokai()`` return
pre-configured instances. Module-level ``get_theme()`` / ``set_theme()``
manage a runtime-swappable singleton.
Usage::
from simvx.core.ui.theme import AppTheme, get_theme, set_theme
theme = get_theme() # module-level singleton (dark by default)
bg = theme.bg # direct attribute access
set_theme(AppTheme.monokai()) # runtime theme switch
"""
from __future__ import annotations
from __future__ import annotations
import logging
from .types import Theme
log = logging.getLogger(__name__)
Colour4 = tuple[float, float, float, float]
# ---------------------------------------------------------------------------
# StyleBox — rich background with gradients and per-side borders
# ---------------------------------------------------------------------------
[docs]
class StyleBox:
"""Themed background with optional gradient and per-side embossed borders.
Use ``draw()`` to render the background and borders. Use ``inset`` to
offset child content past the border + content margin.
"""
__slots__ = (
"bg_colour", "bg_gradient",
"border_colour", "border_top", "border_bottom", "border_left", "border_right",
"border_width", "content_margin",
)
def __init__(
self,
bg_colour: Colour4 = (0.2, 0.2, 0.2, 1.0),
bg_gradient: tuple[Colour4, Colour4] | None = None,
border_colour: Colour4 = (0.3, 0.3, 0.3, 1.0),
border_top: Colour4 | None = None,
border_bottom: Colour4 | None = None,
border_left: Colour4 | None = None,
border_right: Colour4 | None = None,
border_width: float = 1.0,
content_margin: float = 2.0,
):
self.bg_colour = bg_colour
self.bg_gradient = bg_gradient
self.border_colour = border_colour
self.border_top = border_top
self.border_bottom = border_bottom
self.border_left = border_left
self.border_right = border_right
self.border_width = border_width
self.content_margin = content_margin
@property
def inset(self) -> float:
"""Total inward offset (border + content margin) for child positioning."""
return self.border_width + self.content_margin
[docs]
def draw(self, renderer, x: float, y: float, w: float, h: float):
"""Render background and borders into *renderer* at the given rect."""
bw = self.border_width
# Background (inset by border width)
if self.bg_gradient is not None:
renderer.fill_rect_gradient(x + bw, y + bw, w - 2 * bw, h - 2 * bw, *self.bg_gradient)
else:
renderer.draw_filled_rect(x + bw, y + bw, w - 2 * bw, h - 2 * bw, self.bg_colour)
# Borders — four thin filled rects with per-side colour overrides
if bw > 0:
ct = self.border_top or self.border_colour
cb = self.border_bottom or self.border_colour
cl = self.border_left or self.border_colour
cr = self.border_right or self.border_colour
renderer.draw_filled_rect(x, y, w, bw, ct) # top
renderer.draw_filled_rect(x, y + h - bw, w, bw, cb) # bottom
renderer.draw_filled_rect(x, y + bw, bw, h - 2 * bw, cl) # left
renderer.draw_filled_rect(x + w - bw, y + bw, bw, h - 2 * bw, cr) # right
# ---------------------------------------------------------------------------
# SyntaxTheme -- colours for code/syntax highlighting
# ---------------------------------------------------------------------------
[docs]
class SyntaxTheme:
"""Syntax highlighting colour set for code editors.
Each attribute is a Colour4 tuple used to colour a token category.
"""
__slots__ = ("keyword", "string", "comment", "number", "decorator", "builtin", "normal")
def __init__(
self,
keyword: Colour4 = (0.4, 0.6, 1.0, 1.0),
string: Colour4 = (0.5, 0.9, 0.5, 1.0),
comment: Colour4 = (0.5, 0.5, 0.5, 1.0),
number: Colour4 = (1.0, 0.7, 0.3, 1.0),
decorator: Colour4 = (1.0, 0.9, 0.4, 1.0),
builtin: Colour4 = (0.4, 0.9, 0.9, 1.0),
normal: Colour4 = (0.9, 0.9, 0.9, 1.0),
):
self.keyword = keyword
self.string = string
self.comment = comment
self.number = number
self.decorator = decorator
self.builtin = builtin
self.normal = normal
# ---------------------------------------------------------------------------
# AppTheme
# ---------------------------------------------------------------------------
[docs]
class AppTheme(Theme):
"""Full application theme with named colour and layout attributes.
Subclasses :class:`Theme` so ``Control.get_theme()`` still works.
All attributes are also written into ``self.colours`` / ``self.sizes``
so the dict-based ``get_colour()`` / ``get_size()`` API stays valid.
"""
def __init__(self) -> None:
super().__init__()
# -- Background hierarchy (dark defaults) ----------------------------
self.bg_black: Colour4 = (0.0, 0.0, 0.0, 1.0)
self.bg_darkest: Colour4 = (0.02, 0.02, 0.025, 1.0)
self.bg_darker: Colour4 = (0.03, 0.03, 0.035, 1.0)
self.bg_dark: Colour4 = (0.045, 0.045, 0.05, 1.0)
self.bg: Colour4 = (0.055, 0.055, 0.06, 1.0)
self.bg_light: Colour4 = (0.075, 0.075, 0.085, 1.0)
self.bg_lighter: Colour4 = (0.095, 0.095, 0.105, 1.0)
self.bg_input: Colour4 = (0.025, 0.025, 0.03, 1.0)
# -- Panel roles -----------------------------------------------------
self.panel_bg: Colour4 = self.bg
self.header_bg: Colour4 = self.bg_light
self.toolbar_bg: Colour4 = (0.04, 0.04, 0.045, 1.0)
self.status_bar_bg: Colour4 = (0.03, 0.03, 0.035, 1.0)
self.section_bg: Colour4 = (0.07, 0.07, 0.08, 1.0)
# -- Text ------------------------------------------------------------
self.text: Colour4 = (0.86, 0.86, 0.88, 1.0)
self.text_bright: Colour4 = (0.95, 0.95, 0.97, 1.0)
self.text_label: Colour4 = (0.75, 0.75, 0.78, 1.0)
self.text_dim: Colour4 = (0.46, 0.46, 0.48, 1.0)
self.text_muted: Colour4 = (0.56, 0.56, 0.58, 1.0)
self.text_faint: Colour4 = (0.62, 0.62, 0.64, 1.0)
# -- Accent / semantic -----------------------------------------------
self.accent: Colour4 = (0.28, 0.58, 0.98, 1.0)
self.error: Colour4 = (0.94, 0.33, 0.31, 1.0)
self.warning: Colour4 = (0.95, 0.76, 0.19, 1.0)
self.success: Colour4 = (0.36, 0.72, 0.36, 1.0)
self.info: Colour4 = (0.4, 0.6, 0.9, 1.0)
# -- Selection / highlight -------------------------------------------
self.selection: Colour4 = (0.2, 0.4, 0.7, 0.5)
self.selection_bg: Colour4 = (0.2, 0.45, 0.8, 1.0)
self.hover_bg: Colour4 = (0.08, 0.08, 0.095, 1.0)
self.highlight: Colour4 = (0.3, 0.3, 0.0, 0.3)
# -- Border / separator ----------------------------------------------
self.border: Colour4 = (0.125, 0.125, 0.14, 1.0)
self.border_light: Colour4 = (0.15, 0.15, 0.165, 1.0)
# -- Button ----------------------------------------------------------
self.btn_bg: Colour4 = (0.10, 0.10, 0.115, 1.0)
self.btn_hover: Colour4 = (0.14, 0.14, 0.16, 1.0)
self.btn_pressed: Colour4 = (0.03, 0.03, 0.035, 1.0)
self.btn_primary: Colour4 = self.accent
self.btn_danger: Colour4 = (0.75, 0.25, 0.25, 1.0)
# -- Input fields ----------------------------------------------------
self.input_border: Colour4 = self.border
self.input_focus: Colour4 = self.accent
self.placeholder: Colour4 = self.text_dim
# -- Scrollbar -------------------------------------------------------
self.scrollbar_hover: Colour4 = (0.26, 0.26, 0.29, 0.8)
self.scrollbar_track: Colour4 = (0.04, 0.04, 0.045, 0.3)
# -- Tabs ------------------------------------------------------------
self.tab_bg: Colour4 = (0.04, 0.04, 0.045, 1.0)
self.tab_active: Colour4 = (0.09, 0.09, 0.105, 1.0)
self.tab_hover: Colour4 = (0.07, 0.07, 0.085, 1.0)
self.tab_text: Colour4 = self.text_label
self.tab_active_text: Colour4 = (1.0, 1.0, 1.0, 1.0)
# -- Tree view -------------------------------------------------------
self.tree_bg: Colour4 = self.bg_darker
self.tree_select: Colour4 = (0.2, 0.4, 0.7, 1.0)
self.tree_hover: Colour4 = self.hover_bg
self.tree_arrow: Colour4 = self.text_muted
# -- Check / spin ----------------------------------------------------
self.check_colour: Colour4 = self.accent
self.check_box: Colour4 = self.border
# -- Slider ----------------------------------------------------------
self.slider_fill: Colour4 = self.accent
self.slider_handle: Colour4 = (0.8, 0.8, 0.8, 1.0)
# -- Dock panels -----------------------------------------------------
self.dock_title_bg: Colour4 = self.bg_darker
self.dock_title_text: Colour4 = (0.85, 0.85, 0.85, 1.0)
# -- Popup menus -----------------------------------------------------
self.popup_bg: Colour4 = self.bg
self.popup_hover: Colour4 = self.selection_bg
self.popup_separator: Colour4 = self.border_light
# -- Split divider ---------------------------------------------------
self.divider: Colour4 = self.border_light
self.divider_hover: Colour4 = self.accent
# -- Code editor -----------------------------------------------------
self.current_line: Colour4 = (0.07, 0.07, 0.085, 1.0)
self.bracket_match: Colour4 = (0.4, 0.7, 0.4, 0.5)
self.bracket_mismatch: Colour4 = (0.9, 0.2, 0.2, 0.5)
self.line_number: Colour4 = self.text_dim
self.gutter_bg: Colour4 = self.bg_darker
# -- Syntax highlighting ---------------------------------------------
self.syntax: SyntaxTheme = SyntaxTheme()
# -- Editor-specific (viewport, gizmo, grid) ------------------------
self.viewport_bg: Colour4 = (0.05, 0.05, 0.06, 1.0)
self.gizmo_x: Colour4 = (0.96, 0.26, 0.28, 1.0)
self.gizmo_y: Colour4 = (0.40, 0.84, 0.36, 1.0)
self.gizmo_z: Colour4 = (0.26, 0.52, 0.96, 1.0)
self.selection_outline: Colour4 = (1.0, 0.6, 0.0, 1.0)
self.grid_major: Colour4 = (0.38, 0.38, 0.40, 1.0)
self.grid_minor: Colour4 = (0.26, 0.26, 0.28, 0.6)
# -- IDE-specific (minimap, autocomplete, scrollbar) -----------------
self.minimap_text: Colour4 = (0.42, 0.42, 0.47, 1.0)
self.minimap_keyword: Colour4 = (0.55, 0.40, 0.70, 1.0)
self.minimap_string: Colour4 = (0.55, 0.70, 0.40, 1.0)
self.minimap_comment: Colour4 = (0.32, 0.32, 0.36, 1.0)
self.autocomplete_bg: Colour4 = self.bg_lighter
self.autocomplete_selected: Colour4 = (0.28, 0.58, 0.98, 0.30)
self.autocomplete_border: Colour4 = (0.38, 0.38, 0.42, 1.0)
self.autocomplete_hover: Colour4 = (0.26, 0.56, 0.96, 0.12)
self.autocomplete_dim: Colour4 = (0.46, 0.46, 0.52, 1.0)
self.autocomplete_kind: Colour4 = (0.55, 0.75, 0.95, 1.0)
self.scrollbar_bg: Colour4 = (0.06, 0.06, 0.07, 1.0)
self.scrollbar_fg: Colour4 = (0.175, 0.175, 0.20, 1.0)
# -- Layout sizes ----------------------------------------------------
self.header_h: float = 28.0
self.row_h: float = 22.0
self.tab_h: float = 24.0
self.font_size: float = 11.0
self.ui_scale: float = 1.0
self.scrollbar_width: float = 8.0
self.dock_title_h: float = 24.0
# -- StyleBoxes (dark defaults) --------------------------------------
self._init_styleboxes_dark()
# Sync dicts so dict-based API works
self._sync_dicts()
# -- StyleBox helpers ----------------------------------------------------
@staticmethod
def _lighten(c: Colour4, amount: float = 0.08) -> Colour4:
return (min(c[0] + amount, 1.0), min(c[1] + amount, 1.0), min(c[2] + amount, 1.0), c[3])
@staticmethod
def _darken(c: Colour4, amount: float = 0.06) -> Colour4:
return (max(c[0] - amount, 0.0), max(c[1] - amount, 0.0), max(c[2] - amount, 0.0), c[3])
def _init_styleboxes_dark(self) -> None:
"""Initialise StyleBox slots with dark-theme defaults (embossed buttons, flat panels)."""
L, D = self._lighten, self._darken
# Button states
self.btn_style_normal = StyleBox(
bg_colour=self.btn_bg, border_width=1.0, content_margin=2.0,
border_top=L(self.btn_bg), border_left=L(self.btn_bg),
border_bottom=D(self.btn_bg), border_right=D(self.btn_bg),
)
self.btn_style_hover = StyleBox(
bg_colour=self.btn_hover, border_width=1.0, content_margin=2.0,
border_top=L(self.btn_hover), border_left=L(self.btn_hover),
border_bottom=D(self.btn_hover), border_right=D(self.btn_hover),
)
self.btn_style_pressed = StyleBox(
bg_colour=self.btn_pressed, border_width=1.0, content_margin=2.0,
border_top=D(self.btn_pressed), border_left=D(self.btn_pressed),
border_bottom=L(self.btn_pressed), border_right=L(self.btn_pressed),
)
self.btn_style_disabled = StyleBox(
bg_colour=D(self.btn_bg, 0.04), border_width=1.0, content_margin=2.0,
border_colour=D(self.border, 0.08),
)
self.btn_style_focused = StyleBox(
bg_colour=self.btn_bg, border_width=1.0, content_margin=2.0,
border_colour=self.accent,
)
# Panel
self.panel_style = StyleBox(
bg_colour=self.bg, border_width=1.0, content_margin=0.0,
border_colour=self.border,
)
# TextEdit
self.input_style_normal = StyleBox(
bg_colour=self.bg_input, border_width=1.0, content_margin=4.0,
border_colour=self.input_border,
)
self.input_style_focused = StyleBox(
bg_colour=self.bg_input, border_width=1.0, content_margin=4.0,
border_colour=self.accent,
)
self.input_style_disabled = StyleBox(
bg_colour=D(self.bg_input, 0.02), border_width=1.0, content_margin=4.0,
border_colour=D(self.border, 0.08),
)
# TabContainer
self.tab_style_normal = StyleBox(
bg_colour=self.tab_bg, border_width=0.0, content_margin=0.0,
)
self.tab_style_active = StyleBox(
bg_colour=self.tab_active, border_width=0.0, content_margin=0.0,
)
self.tab_style_hover = StyleBox(
bg_colour=self.tab_hover, border_width=0.0, content_margin=0.0,
)
# PopupMenu
self.popup_style = StyleBox(
bg_colour=self.popup_bg, border_width=1.0, content_margin=0.0,
border_colour=self.border_light,
)
self.popup_style_hover = StyleBox(
bg_colour=self.popup_hover, border_width=0.0, content_margin=0.0,
)
# DockPanel title
self.dock_title_style = StyleBox(
bg_colour=self.dock_title_bg, border_width=0.0, content_margin=0.0,
)
# -- Dict synchronisation ------------------------------------------------
def _sync_dicts(self) -> None:
"""Populate ``self.colours`` and ``self.sizes`` from named attributes."""
self.colours.update(
{
"bg_black": self.bg_black,
"bg_darkest": self.bg_darkest,
"bg_darker": self.bg_darker,
"bg_dark": self.bg_dark,
"bg": self.bg,
"bg_light": self.bg_light,
"bg_lighter": self.bg_lighter,
"bg_input": self.bg_input,
"panel_bg": self.panel_bg,
"header_bg": self.header_bg,
"toolbar_bg": self.toolbar_bg,
"status_bar_bg": self.status_bar_bg,
"section_bg": self.section_bg,
"text": self.text,
"text_bright": self.text_bright,
"text_label": self.text_label,
"text_dim": self.text_dim,
"text_muted": self.text_muted,
"text_faint": self.text_faint,
"text_disabled": self.text_dim,
"accent": self.accent,
"error": self.error,
"warning": self.warning,
"success": self.success,
"info": self.info,
"selection": self.selection,
"selection_bg": self.selection_bg,
"hover_bg": self.hover_bg,
"highlight": self.highlight,
"border": self.border,
"border_light": self.border_light,
"btn_bg": self.btn_bg,
"btn_hover": self.btn_hover,
"btn_pressed": self.btn_pressed,
"btn_primary": self.btn_primary,
"btn_danger": self.btn_danger,
"viewport_bg": self.viewport_bg,
"gizmo_x": self.gizmo_x,
"gizmo_y": self.gizmo_y,
"gizmo_z": self.gizmo_z,
"selection_outline": self.selection_outline,
"grid_major": self.grid_major,
"grid_minor": self.grid_minor,
"minimap_text": self.minimap_text,
"minimap_keyword": self.minimap_keyword,
"minimap_string": self.minimap_string,
"minimap_comment": self.minimap_comment,
"autocomplete_bg": self.autocomplete_bg,
"autocomplete_selected": self.autocomplete_selected,
"autocomplete_border": self.autocomplete_border,
"autocomplete_hover": self.autocomplete_hover,
"autocomplete_dim": self.autocomplete_dim,
"autocomplete_kind": self.autocomplete_kind,
"scrollbar_bg": self.scrollbar_bg,
"scrollbar_fg": self.scrollbar_fg,
# Keep legacy Theme keys alive
"accent_hover": (self.accent[0] + 0.1, self.accent[1] + 0.1, min(self.accent[2] + 0.04, 1.0), 1.0),
"accent_pressed": (self.accent[0] - 0.06, self.accent[1] - 0.06, self.accent[2] - 0.06, 1.0),
"focus": self.accent,
}
)
self.sizes.update(
{
"header_h": self.header_h,
"row_h": self.row_h,
"tab_h": self.tab_h,
"font_size": self.font_size,
"ui_scale": self.ui_scale,
"scrollbar_width": self.scrollbar_width,
"dock_title_h": self.dock_title_h,
}
)
# -- Factory presets -----------------------------------------------------
[docs]
@classmethod
def dark(cls) -> AppTheme:
"""Dark theme (default)."""
return cls()
[docs]
@classmethod
def abyss(cls) -> AppTheme:
"""Abyss — near-black with a subtle cool-blue tint. OLED-friendly."""
t = cls()
# Backgrounds — true-dark with faint blue bias
t.bg_black = (0.0, 0.0, 0.01, 1.0)
t.bg_darkest = (0.02, 0.02, 0.035, 1.0)
t.bg_darker = (0.03, 0.03, 0.05, 1.0)
t.bg_dark = (0.045, 0.045, 0.065, 1.0)
t.bg = (0.055, 0.055, 0.08, 1.0)
t.bg_light = (0.08, 0.08, 0.11, 1.0)
t.bg_lighter = (0.10, 0.10, 0.14, 1.0)
t.bg_input = (0.025, 0.025, 0.04, 1.0)
t.panel_bg = t.bg
t.header_bg = t.bg_light
t.toolbar_bg = t.bg_darker
t.status_bar_bg = t.bg_darkest
t.section_bg = t.bg_light
# Text — cool grey with slight blue
t.text = (0.78, 0.80, 0.86, 1.0)
t.text_bright = (0.90, 0.92, 0.97, 1.0)
t.text_label = (0.58, 0.60, 0.68, 1.0)
t.text_dim = (0.36, 0.38, 0.46, 1.0)
t.text_muted = (0.44, 0.46, 0.54, 1.0)
t.text_faint = (0.50, 0.52, 0.60, 1.0)
# Accent — electric blue
t.accent = (0.30, 0.55, 1.0, 1.0)
t.error = (0.90, 0.28, 0.30, 1.0)
t.warning = (0.92, 0.72, 0.16, 1.0)
t.success = (0.30, 0.72, 0.40, 1.0)
t.info = (0.35, 0.60, 0.95, 1.0)
t.selection = (0.20, 0.40, 0.80, 0.35)
t.selection_bg = (0.20, 0.40, 0.80, 1.0)
t.hover_bg = t.bg_light
t.highlight = (0.30, 0.45, 0.80, 0.2)
t.border = (0.12, 0.12, 0.18, 1.0)
t.border_light = (0.16, 0.16, 0.24, 1.0)
t.btn_bg = (0.08, 0.08, 0.12, 1.0)
t.btn_hover = (0.12, 0.12, 0.18, 1.0)
t.btn_pressed = (0.03, 0.03, 0.05, 1.0)
t.btn_primary = t.accent
t.btn_danger = (0.75, 0.22, 0.25, 1.0)
t.input_border = t.border
t.input_focus = t.accent
t.placeholder = t.text_dim
t.scrollbar_hover = (0.35, 0.38, 0.50, 0.7)
t.scrollbar_track = (0.03, 0.03, 0.05, 0.3)
t.tab_bg = t.bg_darker
t.tab_active = t.bg
t.tab_hover = t.bg_light
t.tab_text = t.text_label
t.tab_active_text = t.text_bright
t.tree_bg = t.bg_darkest
t.tree_select = t.selection_bg
t.tree_hover = t.hover_bg
t.tree_arrow = t.text_dim
t.check_colour = t.accent
t.check_box = t.border
t.slider_fill = t.accent
t.slider_handle = t.text_label
t.dock_title_bg = t.bg_darker
t.dock_title_text = t.text
t.popup_bg = t.bg_dark
t.popup_hover = t.selection_bg
t.popup_separator = t.border_light
t.divider = t.border
t.divider_hover = t.accent
t.current_line = (0.06, 0.06, 0.09, 1.0)
t.bracket_match = (0.25, 0.55, 0.35, 0.4)
t.bracket_mismatch = (0.80, 0.20, 0.25, 0.4)
t.line_number = t.text_dim
t.gutter_bg = t.bg_darkest
t.syntax = SyntaxTheme(
keyword=(0.45, 0.62, 1.0, 1.0), string=(0.40, 0.82, 0.55, 1.0),
comment=t.text_dim, number=(0.82, 0.58, 1.0, 1.0),
decorator=(0.90, 0.70, 0.30, 1.0), builtin=(0.40, 0.80, 0.95, 1.0),
normal=t.text,
)
t.viewport_bg = t.bg_darkest
t.selection_outline = (0.90, 0.55, 0.0, 1.0)
t.grid_major = t.border_light
t.grid_minor = (0.08, 0.08, 0.12, 0.5)
t.minimap_text = t.text_dim
t.minimap_keyword = (0.45, 0.35, 0.65, 1.0)
t.minimap_string = (0.40, 0.60, 0.45, 1.0)
t.minimap_comment = t.text_dim
t.autocomplete_bg = t.bg_lighter
t.autocomplete_selected = (0.30, 0.55, 1.0, 0.25)
t.autocomplete_border = t.border_light
t.autocomplete_hover = (0.30, 0.55, 1.0, 0.10)
t.autocomplete_dim = t.text_muted
t.autocomplete_kind = (0.55, 0.75, 0.95, 1.0)
t.scrollbar_bg = t.bg_darkest
t.scrollbar_fg = t.text_dim
# StyleBoxes — subtle emboss in the dark
L, D = cls._lighten, cls._darken
t.btn_style_normal = StyleBox(
bg_colour=t.btn_bg, border_width=1.0, content_margin=2.0,
border_top=L(t.btn_bg, 0.04), border_left=L(t.btn_bg, 0.04),
border_bottom=D(t.btn_bg, 0.02), border_right=D(t.btn_bg, 0.02),
)
t.btn_style_hover = StyleBox(
bg_colour=t.btn_hover, border_width=1.0, content_margin=2.0,
border_colour=t.accent,
)
t.btn_style_pressed = StyleBox(bg_colour=t.btn_pressed, border_width=1.0, content_margin=2.0, border_colour=t.accent)
t.btn_style_disabled = StyleBox(bg_colour=D(t.btn_bg, 0.02), border_width=1.0, content_margin=2.0, border_colour=D(t.border, 0.04))
t.btn_style_focused = StyleBox(bg_colour=t.btn_bg, border_width=1.0, content_margin=2.0, border_colour=t.accent)
t.panel_style = StyleBox(bg_colour=t.panel_bg, border_width=1.0, border_colour=t.border)
t.input_style_normal = StyleBox(bg_colour=t.bg_input, border_width=1.0, content_margin=4.0, border_colour=t.border)
t.input_style_focused = StyleBox(bg_colour=t.bg_input, border_width=1.0, content_margin=4.0, border_colour=t.accent)
t.input_style_disabled = StyleBox(bg_colour=D(t.bg_input, 0.01), border_width=1.0, content_margin=4.0, border_colour=D(t.border, 0.04))
t.tab_style_normal = StyleBox(bg_colour=t.tab_bg, border_width=0.0)
t.tab_style_active = StyleBox(bg_colour=t.tab_active, border_width=0.0)
t.tab_style_hover = StyleBox(bg_colour=t.tab_hover, border_width=0.0)
t.popup_style = StyleBox(bg_colour=t.popup_bg, border_width=1.0, border_colour=t.border)
t.popup_style_hover = StyleBox(bg_colour=t.popup_hover, border_width=0.0)
t.dock_title_style = StyleBox(bg_colour=t.dock_title_bg, border_width=0.0)
t._sync_dicts()
return t
[docs]
@classmethod
def midnight(cls) -> AppTheme:
"""Midnight — near-black with a subtle warm-green tint. OLED-friendly."""
t = cls()
# Backgrounds — true-dark with faint green bias
t.bg_black = (0.0, 0.01, 0.0, 1.0)
t.bg_darkest = (0.02, 0.03, 0.02, 1.0)
t.bg_darker = (0.03, 0.04, 0.03, 1.0)
t.bg_dark = (0.045, 0.06, 0.045, 1.0)
t.bg = (0.06, 0.07, 0.055, 1.0)
t.bg_light = (0.08, 0.10, 0.08, 1.0)
t.bg_lighter = (0.11, 0.13, 0.10, 1.0)
t.bg_input = (0.025, 0.035, 0.025, 1.0)
t.panel_bg = t.bg
t.header_bg = t.bg_light
t.toolbar_bg = t.bg_darker
t.status_bar_bg = t.bg_darkest
t.section_bg = t.bg_light
# Text — warm grey with green tint
t.text = (0.78, 0.84, 0.76, 1.0)
t.text_bright = (0.90, 0.95, 0.88, 1.0)
t.text_label = (0.58, 0.65, 0.55, 1.0)
t.text_dim = (0.36, 0.42, 0.34, 1.0)
t.text_muted = (0.44, 0.50, 0.42, 1.0)
t.text_faint = (0.50, 0.56, 0.48, 1.0)
# Accent — muted teal-green
t.accent = (0.28, 0.72, 0.56, 1.0)
t.error = (0.88, 0.30, 0.28, 1.0)
t.warning = (0.90, 0.75, 0.20, 1.0)
t.success = (0.35, 0.78, 0.35, 1.0)
t.info = (0.35, 0.68, 0.65, 1.0)
t.selection = (0.18, 0.45, 0.35, 0.35)
t.selection_bg = (0.18, 0.45, 0.35, 1.0)
t.hover_bg = t.bg_light
t.highlight = (0.35, 0.50, 0.20, 0.2)
t.border = (0.10, 0.14, 0.10, 1.0)
t.border_light = (0.14, 0.19, 0.14, 1.0)
t.btn_bg = (0.07, 0.09, 0.065, 1.0)
t.btn_hover = (0.10, 0.14, 0.10, 1.0)
t.btn_pressed = (0.03, 0.04, 0.03, 1.0)
t.btn_primary = t.accent
t.btn_danger = (0.75, 0.24, 0.22, 1.0)
t.input_border = t.border
t.input_focus = t.accent
t.placeholder = t.text_dim
t.scrollbar_hover = (0.32, 0.42, 0.32, 0.7)
t.scrollbar_track = (0.03, 0.04, 0.03, 0.3)
t.tab_bg = t.bg_darker
t.tab_active = t.bg
t.tab_hover = t.bg_light
t.tab_text = t.text_label
t.tab_active_text = t.text_bright
t.tree_bg = t.bg_darkest
t.tree_select = t.selection_bg
t.tree_hover = t.hover_bg
t.tree_arrow = t.text_dim
t.check_colour = t.accent
t.check_box = t.border
t.slider_fill = t.accent
t.slider_handle = t.text_label
t.dock_title_bg = t.bg_darker
t.dock_title_text = t.text
t.popup_bg = t.bg_dark
t.popup_hover = t.selection_bg
t.popup_separator = t.border_light
t.divider = t.border
t.divider_hover = t.accent
t.current_line = (0.05, 0.07, 0.05, 1.0)
t.bracket_match = (0.25, 0.55, 0.30, 0.4)
t.bracket_mismatch = (0.80, 0.22, 0.20, 0.4)
t.line_number = t.text_dim
t.gutter_bg = t.bg_darkest
t.syntax = SyntaxTheme(
keyword=(0.55, 0.85, 0.65, 1.0), string=(0.85, 0.82, 0.45, 1.0),
comment=t.text_dim, number=(0.75, 0.60, 0.90, 1.0),
decorator=(0.90, 0.65, 0.30, 1.0), builtin=(0.45, 0.82, 0.80, 1.0),
normal=t.text,
)
t.viewport_bg = t.bg_darkest
t.selection_outline = (0.85, 0.60, 0.0, 1.0)
t.grid_major = t.border_light
t.grid_minor = (0.07, 0.09, 0.07, 0.5)
t.minimap_text = t.text_dim
t.minimap_keyword = (0.45, 0.65, 0.50, 1.0)
t.minimap_string = (0.65, 0.60, 0.35, 1.0)
t.minimap_comment = t.text_dim
t.autocomplete_bg = t.bg_lighter
t.autocomplete_selected = (0.28, 0.72, 0.56, 0.25)
t.autocomplete_border = t.border_light
t.autocomplete_hover = (0.28, 0.72, 0.56, 0.10)
t.autocomplete_dim = t.text_muted
t.autocomplete_kind = (0.45, 0.82, 0.70, 1.0)
t.scrollbar_bg = t.bg_darkest
t.scrollbar_fg = t.text_dim
# StyleBoxes — subtle emboss
L, D = cls._lighten, cls._darken
t.btn_style_normal = StyleBox(
bg_colour=t.btn_bg, border_width=1.0, content_margin=2.0,
border_top=L(t.btn_bg, 0.03), border_left=L(t.btn_bg, 0.03),
border_bottom=D(t.btn_bg, 0.02), border_right=D(t.btn_bg, 0.02),
)
t.btn_style_hover = StyleBox(
bg_colour=t.btn_hover, border_width=1.0, content_margin=2.0,
border_colour=t.accent,
)
t.btn_style_pressed = StyleBox(bg_colour=t.btn_pressed, border_width=1.0, content_margin=2.0, border_colour=t.accent)
t.btn_style_disabled = StyleBox(bg_colour=D(t.btn_bg, 0.02), border_width=1.0, content_margin=2.0, border_colour=D(t.border, 0.03))
t.btn_style_focused = StyleBox(bg_colour=t.btn_bg, border_width=1.0, content_margin=2.0, border_colour=t.accent)
t.panel_style = StyleBox(bg_colour=t.panel_bg, border_width=1.0, border_colour=t.border)
t.input_style_normal = StyleBox(bg_colour=t.bg_input, border_width=1.0, content_margin=4.0, border_colour=t.border)
t.input_style_focused = StyleBox(bg_colour=t.bg_input, border_width=1.0, content_margin=4.0, border_colour=t.accent)
t.input_style_disabled = StyleBox(bg_colour=D(t.bg_input, 0.01), border_width=1.0, content_margin=4.0, border_colour=D(t.border, 0.03))
t.tab_style_normal = StyleBox(bg_colour=t.tab_bg, border_width=0.0)
t.tab_style_active = StyleBox(bg_colour=t.tab_active, border_width=0.0)
t.tab_style_hover = StyleBox(bg_colour=t.tab_hover, border_width=0.0)
t.popup_style = StyleBox(bg_colour=t.popup_bg, border_width=1.0, border_colour=t.border)
t.popup_style_hover = StyleBox(bg_colour=t.popup_hover, border_width=0.0)
t.dock_title_style = StyleBox(bg_colour=t.dock_title_bg, border_width=0.0)
t._sync_dicts()
return t
[docs]
@classmethod
def light(cls) -> AppTheme:
"""Light theme with bright backgrounds."""
t = cls()
# Backgrounds
t.bg_black = (0.80, 0.80, 0.80, 1.0)
t.bg_darkest = (0.82, 0.82, 0.82, 1.0)
t.bg_darker = (0.85, 0.85, 0.85, 1.0)
t.bg_dark = (0.88, 0.88, 0.88, 1.0)
t.bg = (0.92, 0.92, 0.92, 1.0)
t.bg_light = (0.96, 0.96, 0.96, 1.0)
t.bg_lighter = (0.98, 0.98, 0.98, 1.0)
t.bg_input = (1.0, 1.0, 1.0, 1.0)
# Panel roles
t.panel_bg = (0.92, 0.92, 0.93, 1.0)
t.header_bg = (0.88, 0.88, 0.88, 1.0)
t.toolbar_bg = (0.82, 0.82, 0.82, 1.0)
t.status_bar_bg = (0.78, 0.78, 0.8, 1.0)
t.section_bg = (0.90, 0.90, 0.90, 1.0)
# Text
t.text = (0.15, 0.15, 0.15, 1.0)
t.text_bright = (0.1, 0.1, 0.1, 1.0)
t.text_label = (0.35, 0.35, 0.35, 1.0)
t.text_dim = (0.45, 0.45, 0.45, 1.0)
t.text_muted = (0.50, 0.50, 0.50, 1.0)
t.text_faint = (0.55, 0.55, 0.55, 1.0)
# Accent / semantic
t.accent = (0.0, 0.45, 0.85, 1.0)
t.error = (0.85, 0.18, 0.15, 1.0)
t.warning = (0.8, 0.6, 0.0, 1.0)
t.success = (0.2, 0.6, 0.2, 1.0)
t.info = (0.2, 0.45, 0.8, 1.0)
# Selection / highlight
t.selection = (0.2, 0.5, 0.9, 0.3)
t.selection_bg = (0.3, 0.55, 0.9, 1.0)
t.hover_bg = (0.85, 0.85, 0.88, 1.0)
t.highlight = (1.0, 1.0, 0.0, 0.2)
# Border
t.border = (0.72, 0.72, 0.72, 1.0)
t.border_light = (0.80, 0.80, 0.80, 1.0)
# Button
t.btn_bg = (0.85, 0.85, 0.87, 1.0)
t.btn_hover = (0.78, 0.78, 0.82, 1.0)
t.btn_pressed = (0.70, 0.70, 0.74, 1.0)
t.btn_primary = t.accent
t.btn_danger = (0.80, 0.20, 0.20, 1.0)
# Input
t.input_border = t.border
t.input_focus = t.accent
t.placeholder = t.text_dim
# Scrollbar
t.scrollbar_hover = (0.55, 0.55, 0.58, 0.8)
t.scrollbar_track = (0.80, 0.80, 0.82, 0.3)
# Tabs
t.tab_bg = (0.85, 0.85, 0.85, 1.0)
t.tab_active = (0.92, 0.92, 0.93, 1.0)
t.tab_hover = (0.88, 0.88, 0.90, 1.0)
t.tab_text = t.text_label
t.tab_active_text = t.text_bright
# Tree
t.tree_bg = t.bg_lighter
t.tree_select = (0.3, 0.55, 0.9, 1.0)
t.tree_hover = t.hover_bg
t.tree_arrow = t.text_muted
# Check / spin
t.check_colour = t.accent
t.check_box = t.border
# Slider
t.slider_fill = t.accent
t.slider_handle = (0.4, 0.4, 0.42, 1.0)
# Dock
t.dock_title_bg = t.header_bg
t.dock_title_text = t.text
# Popup
t.popup_bg = t.bg_lighter
t.popup_hover = t.selection_bg
t.popup_separator = t.border
# Divider
t.divider = t.border
t.divider_hover = t.accent
# Code editor
t.current_line = (0.88, 0.90, 0.95, 1.0)
t.bracket_match = (0.2, 0.6, 0.2, 0.4)
t.bracket_mismatch = (0.85, 0.2, 0.2, 0.4)
t.line_number = t.text_dim
t.gutter_bg = t.bg_darker
# Syntax
t.syntax = SyntaxTheme(
keyword=(0.0, 0.0, 0.8, 1.0),
string=(0.0, 0.5, 0.0, 1.0),
comment=(0.5, 0.5, 0.5, 1.0),
number=(0.8, 0.4, 0.0, 1.0),
decorator=(0.6, 0.5, 0.0, 1.0),
builtin=(0.0, 0.5, 0.5, 1.0),
normal=(0.1, 0.1, 0.1, 1.0),
)
# Editor
t.viewport_bg = (0.75, 0.75, 0.78, 1.0)
t.selection_outline = (0.0, 0.45, 1.0, 1.0)
t.grid_major = (0.6, 0.6, 0.6, 1.0)
t.grid_minor = (0.7, 0.7, 0.7, 0.5)
# IDE
t.minimap_text = (0.55, 0.55, 0.58, 1.0)
t.minimap_keyword = (0.0, 0.0, 0.6, 0.7)
t.minimap_string = (0.0, 0.4, 0.0, 0.7)
t.minimap_comment = (0.55, 0.55, 0.58, 1.0)
t.autocomplete_bg = t.bg_lighter
t.autocomplete_selected = (0.0, 0.45, 0.85, 0.25)
t.autocomplete_border = t.border
t.autocomplete_hover = (0.0, 0.45, 0.85, 0.10)
t.autocomplete_dim = t.text_muted
t.autocomplete_kind = (0.2, 0.45, 0.7, 1.0)
t.scrollbar_bg = (0.82, 0.82, 0.84, 1.0)
t.scrollbar_fg = (0.60, 0.60, 0.62, 1.0)
# StyleBoxes — light: gradient buttons, clean borders
L, D = cls._lighten, cls._darken
t.btn_style_normal = StyleBox(
bg_colour=t.btn_bg, border_width=1.0, content_margin=2.0,
bg_gradient=(L(t.btn_bg, 0.04), D(t.btn_bg, 0.04)),
border_colour=t.border,
)
t.btn_style_hover = StyleBox(
bg_colour=t.btn_hover, border_width=1.0, content_margin=2.0,
bg_gradient=(L(t.btn_hover, 0.04), D(t.btn_hover, 0.04)),
border_colour=t.border,
)
t.btn_style_pressed = StyleBox(
bg_colour=t.btn_pressed, border_width=1.0, content_margin=2.0,
border_colour=t.border,
)
t.btn_style_disabled = StyleBox(
bg_colour=D(t.btn_bg, 0.04), border_width=1.0, content_margin=2.0,
border_colour=D(t.border, 0.08),
)
t.btn_style_focused = StyleBox(
bg_colour=t.btn_bg, border_width=1.0, content_margin=2.0,
border_colour=t.accent,
)
t.panel_style = StyleBox(bg_colour=t.panel_bg, border_width=1.0, border_colour=t.border)
t.input_style_normal = StyleBox(bg_colour=t.bg_input, border_width=1.0, content_margin=4.0, border_colour=t.input_border)
t.input_style_focused = StyleBox(bg_colour=t.bg_input, border_width=1.0, content_margin=4.0, border_colour=t.accent)
t.input_style_disabled = StyleBox(bg_colour=D(t.bg_input, 0.02), border_width=1.0, content_margin=4.0, border_colour=D(t.border, 0.08))
t.tab_style_normal = StyleBox(bg_colour=t.tab_bg, border_width=0.0)
t.tab_style_active = StyleBox(bg_colour=t.tab_active, border_width=0.0)
t.tab_style_hover = StyleBox(bg_colour=t.tab_hover, border_width=0.0)
t.popup_style = StyleBox(bg_colour=t.popup_bg, border_width=1.0, border_colour=t.border)
t.popup_style_hover = StyleBox(bg_colour=t.popup_hover, border_width=0.0)
t.dock_title_style = StyleBox(bg_colour=t.dock_title_bg, border_width=0.0)
t._sync_dicts()
return t
[docs]
@classmethod
def monokai(cls) -> AppTheme:
"""Monokai-inspired theme."""
t = cls()
# Backgrounds
t.bg_black = (0.06, 0.07, 0.04, 1.0)
t.bg_darkest = (0.09, 0.10, 0.07, 1.0)
t.bg_darker = (0.11, 0.12, 0.09, 1.0)
t.bg_dark = (0.13, 0.14, 0.11, 1.0)
t.bg = (0.15, 0.16, 0.13, 1.0)
t.bg_light = (0.22, 0.23, 0.19, 1.0)
t.bg_lighter = (0.25, 0.26, 0.22, 1.0)
t.bg_input = (0.12, 0.13, 0.10, 1.0)
# Panel roles
t.panel_bg = t.bg
t.header_bg = t.bg_light
t.toolbar_bg = (0.14, 0.15, 0.12, 1.0)
t.status_bar_bg = (0.12, 0.13, 0.1, 1.0)
t.section_bg = (0.20, 0.21, 0.17, 1.0)
# Text
t.text = (0.97, 0.97, 0.95, 1.0)
t.text_bright = (1.0, 1.0, 0.98, 1.0)
t.text_label = (0.75, 0.73, 0.66, 1.0)
t.text_dim = (0.46, 0.44, 0.37, 1.0)
t.text_muted = (0.55, 0.53, 0.46, 1.0)
t.text_faint = (0.60, 0.58, 0.50, 1.0)
# Accent / semantic
t.accent = (0.4, 0.85, 0.94, 1.0)
t.error = (0.98, 0.15, 0.45, 1.0)
t.warning = (0.9, 0.86, 0.45, 1.0)
t.success = (0.65, 0.89, 0.18, 1.0)
t.info = (0.4, 0.85, 0.94, 1.0)
# Selection / highlight
t.selection = (0.3, 0.45, 0.2, 0.5)
t.selection_bg = (0.4, 0.6, 0.2, 1.0)
t.hover_bg = (0.20, 0.21, 0.17, 1.0)
t.highlight = (0.4, 0.4, 0.0, 0.3)
# Border
t.border = (0.3, 0.31, 0.27, 1.0)
t.border_light = (0.38, 0.39, 0.34, 1.0)
# Button
t.btn_bg = (0.22, 0.23, 0.19, 1.0)
t.btn_hover = (0.30, 0.31, 0.26, 1.0)
t.btn_pressed = (0.12, 0.13, 0.10, 1.0)
t.btn_primary = t.accent
t.btn_danger = (0.80, 0.15, 0.35, 1.0)
# Input
t.input_border = t.border
t.input_focus = t.accent
t.placeholder = t.text_dim
# Scrollbar
t.scrollbar_hover = (0.50, 0.48, 0.42, 0.8)
t.scrollbar_track = (0.11, 0.12, 0.09, 0.3)
# Tabs
t.tab_bg = t.bg_dark
t.tab_active = t.btn_bg
t.tab_hover = t.hover_bg
t.tab_text = t.text_label
t.tab_active_text = t.text_bright
# Tree
t.tree_bg = t.bg_darker
t.tree_select = t.selection_bg
t.tree_hover = t.hover_bg
t.tree_arrow = t.text_muted
# Check / spin
t.check_colour = t.accent
t.check_box = t.border
# Slider
t.slider_fill = t.accent
t.slider_handle = (0.75, 0.73, 0.66, 1.0)
# Dock
t.dock_title_bg = t.bg_darker
t.dock_title_text = t.text
# Popup
t.popup_bg = t.bg
t.popup_hover = t.selection_bg
t.popup_separator = t.border_light
# Divider
t.divider = t.border_light
t.divider_hover = t.accent
# Code editor
t.current_line = (0.18, 0.19, 0.15, 1.0)
t.bracket_match = (0.4, 0.6, 0.2, 0.5)
t.bracket_mismatch = (0.9, 0.15, 0.35, 0.5)
t.line_number = t.text_dim
t.gutter_bg = t.bg_darker
# Syntax (monokai palette)
t.syntax = SyntaxTheme(
keyword=(0.98, 0.15, 0.45, 1.0),
string=(0.9, 0.86, 0.45, 1.0),
comment=(0.46, 0.44, 0.37, 1.0),
number=(0.68, 0.51, 1.0, 1.0),
decorator=(0.65, 0.89, 0.18, 1.0),
builtin=(0.4, 0.85, 0.94, 1.0),
normal=(0.97, 0.97, 0.95, 1.0),
)
# Editor
t.viewport_bg = (0.16, 0.17, 0.14, 1.0)
t.selection_outline = (0.65, 0.89, 0.18, 1.0)
t.grid_major = (0.3, 0.31, 0.27, 1.0)
t.grid_minor = (0.22, 0.23, 0.19, 0.5)
# IDE
t.minimap_text = (0.42, 0.40, 0.35, 1.0)
t.minimap_keyword = (0.75, 0.12, 0.35, 0.8)
t.minimap_string = (0.70, 0.66, 0.35, 0.8)
t.minimap_comment = (0.42, 0.40, 0.35, 1.0)
t.autocomplete_bg = t.bg_lighter
t.autocomplete_selected = (0.4, 0.85, 0.94, 0.25)
t.autocomplete_border = t.border_light
t.autocomplete_hover = (0.4, 0.85, 0.94, 0.10)
t.autocomplete_dim = t.text_muted
t.autocomplete_kind = (0.4, 0.85, 0.94, 0.8)
t.scrollbar_bg = (0.20, 0.21, 0.17, 1.0)
t.scrollbar_fg = (0.40, 0.38, 0.32, 1.0)
# StyleBoxes — monokai: flat buttons with accent highlights
D = cls._darken
t.btn_style_normal = StyleBox(
bg_colour=t.btn_bg, border_width=1.0, content_margin=2.0,
border_colour=t.border,
)
t.btn_style_hover = StyleBox(
bg_colour=t.btn_hover, border_width=1.0, content_margin=2.0,
border_colour=t.accent,
)
t.btn_style_pressed = StyleBox(
bg_colour=t.btn_pressed, border_width=1.0, content_margin=2.0,
border_colour=t.accent,
)
t.btn_style_disabled = StyleBox(
bg_colour=D(t.btn_bg, 0.04), border_width=1.0, content_margin=2.0,
border_colour=D(t.border, 0.08),
)
t.btn_style_focused = StyleBox(
bg_colour=t.btn_bg, border_width=1.0, content_margin=2.0,
border_colour=t.accent,
)
t.panel_style = StyleBox(bg_colour=t.panel_bg, border_width=1.0, border_colour=t.border)
t.input_style_normal = StyleBox(bg_colour=t.bg_input, border_width=1.0, content_margin=4.0, border_colour=t.input_border)
t.input_style_focused = StyleBox(bg_colour=t.bg_input, border_width=1.0, content_margin=4.0, border_colour=t.accent)
t.input_style_disabled = StyleBox(bg_colour=D(t.bg_input, 0.02), border_width=1.0, content_margin=4.0, border_colour=D(t.border, 0.08))
t.tab_style_normal = StyleBox(bg_colour=t.tab_bg, border_width=0.0)
t.tab_style_active = StyleBox(bg_colour=t.tab_active, border_width=0.0)
t.tab_style_hover = StyleBox(bg_colour=t.tab_hover, border_width=0.0)
t.popup_style = StyleBox(bg_colour=t.popup_bg, border_width=1.0, border_colour=t.border_light)
t.popup_style_hover = StyleBox(bg_colour=t.popup_hover, border_width=0.0)
t.dock_title_style = StyleBox(bg_colour=t.dock_title_bg, border_width=0.0)
t._sync_dicts()
return t
[docs]
@classmethod
def solarised_dark(cls) -> AppTheme:
"""Solarised Dark theme — Ethan Schoonover's warm-tinted dark palette."""
t = cls()
base03 = (0.0, 0.17, 0.21, 1.0)
base02 = (0.03, 0.21, 0.26, 1.0)
base01 = (0.35, 0.43, 0.46, 1.0)
base00 = (0.40, 0.48, 0.51, 1.0)
base0 = (0.51, 0.58, 0.59, 1.0)
base1 = (0.58, 0.63, 0.63, 1.0)
yellow = (0.71, 0.54, 0.0, 1.0)
orange = (0.80, 0.29, 0.09, 1.0)
red = (0.86, 0.20, 0.18, 1.0)
magenta = (0.83, 0.21, 0.51, 1.0)
blue = (0.15, 0.55, 0.82, 1.0)
cyan = (0.16, 0.63, 0.60, 1.0)
green = (0.52, 0.60, 0.0, 1.0)
t.bg_black = base03
t.bg_darkest = base03
t.bg_darker = base03
t.bg_dark = base02
t.bg = base02
t.bg_light = (0.06, 0.25, 0.30, 1.0)
t.bg_lighter = (0.09, 0.29, 0.34, 1.0)
t.bg_input = base03
t.panel_bg = base02
t.header_bg = t.bg_light
t.toolbar_bg = base03
t.status_bar_bg = base03
t.section_bg = t.bg_light
t.text = base0
t.text_bright = base1
t.text_label = base00
t.text_dim = base01
t.text_muted = base01
t.text_faint = base01
t.accent = blue
t.error = red
t.warning = yellow
t.success = green
t.info = cyan
t.selection = (0.15, 0.55, 0.82, 0.3)
t.selection_bg = blue
t.hover_bg = t.bg_light
t.highlight = (0.71, 0.54, 0.0, 0.2)
t.border = (0.10, 0.30, 0.36, 1.0)
t.border_light = (0.14, 0.34, 0.40, 1.0)
t.btn_bg = t.bg_light
t.btn_hover = t.bg_lighter
t.btn_pressed = base03
t.btn_primary = blue
t.btn_danger = red
t.input_border = t.border
t.input_focus = blue
t.placeholder = base01
t.scrollbar_hover = (0.40, 0.48, 0.51, 0.7)
t.scrollbar_track = (0.0, 0.17, 0.21, 0.3)
t.tab_bg = base03
t.tab_active = base02
t.tab_hover = t.bg_light
t.tab_text = base00
t.tab_active_text = base1
t.tree_bg = base03
t.tree_select = blue
t.tree_hover = t.hover_bg
t.tree_arrow = base01
t.check_colour = blue
t.check_box = t.border
t.slider_fill = blue
t.slider_handle = base1
t.dock_title_bg = base03
t.dock_title_text = base0
t.popup_bg = base02
t.popup_hover = blue
t.popup_separator = t.border
t.divider = t.border
t.divider_hover = blue
t.current_line = (0.04, 0.22, 0.27, 1.0)
t.bracket_match = (0.16, 0.63, 0.60, 0.4)
t.bracket_mismatch = (0.86, 0.20, 0.18, 0.4)
t.line_number = base01
t.gutter_bg = base03
t.syntax = SyntaxTheme(
keyword=green, string=cyan, comment=base01, number=magenta,
decorator=orange, builtin=yellow, normal=base0,
)
t.viewport_bg = base03
t.selection_outline = orange
t.grid_major = (0.10, 0.30, 0.36, 1.0)
t.grid_minor = (0.06, 0.25, 0.30, 0.5)
t.minimap_text = base01
t.minimap_keyword = green
t.minimap_string = cyan
t.minimap_comment = base01
t.autocomplete_bg = t.bg_lighter
t.autocomplete_selected = (0.15, 0.55, 0.82, 0.25)
t.autocomplete_border = t.border_light
t.autocomplete_hover = (0.15, 0.55, 0.82, 0.10)
t.autocomplete_dim = base00
t.autocomplete_kind = blue
t.scrollbar_bg = base03
t.scrollbar_fg = base01
# StyleBoxes
D = cls._darken
t.btn_style_normal = StyleBox(bg_colour=t.btn_bg, border_width=1.0, content_margin=2.0, border_colour=t.border)
t.btn_style_hover = StyleBox(bg_colour=t.btn_hover, border_width=1.0, content_margin=2.0, border_colour=blue)
t.btn_style_pressed = StyleBox(bg_colour=t.btn_pressed, border_width=1.0, content_margin=2.0, border_colour=blue)
t.btn_style_disabled = StyleBox(bg_colour=D(t.btn_bg, 0.04), border_width=1.0, content_margin=2.0, border_colour=D(t.border, 0.04))
t.btn_style_focused = StyleBox(bg_colour=t.btn_bg, border_width=1.0, content_margin=2.0, border_colour=blue)
t.panel_style = StyleBox(bg_colour=t.panel_bg, border_width=1.0, border_colour=t.border)
t.input_style_normal = StyleBox(bg_colour=t.bg_input, border_width=1.0, content_margin=4.0, border_colour=t.border)
t.input_style_focused = StyleBox(bg_colour=t.bg_input, border_width=1.0, content_margin=4.0, border_colour=blue)
t.input_style_disabled = StyleBox(bg_colour=D(t.bg_input, 0.02), border_width=1.0, content_margin=4.0, border_colour=D(t.border, 0.04))
t.tab_style_normal = StyleBox(bg_colour=t.tab_bg, border_width=0.0)
t.tab_style_active = StyleBox(bg_colour=t.tab_active, border_width=0.0)
t.tab_style_hover = StyleBox(bg_colour=t.tab_hover, border_width=0.0)
t.popup_style = StyleBox(bg_colour=t.popup_bg, border_width=1.0, border_colour=t.border)
t.popup_style_hover = StyleBox(bg_colour=t.popup_hover, border_width=0.0)
t.dock_title_style = StyleBox(bg_colour=t.dock_title_bg, border_width=0.0)
t._sync_dicts()
return t
[docs]
@classmethod
def nord(cls) -> AppTheme:
"""Nord theme — Arctic, north-bluish palette by Arctic Ice Studio."""
t = cls()
# Polar Night
n0 = (0.18, 0.20, 0.25, 1.0)
n1 = (0.23, 0.26, 0.32, 1.0)
n2 = (0.26, 0.30, 0.37, 1.0)
n3 = (0.30, 0.34, 0.42, 1.0)
# Snow Storm
n4 = (0.85, 0.87, 0.91, 1.0)
n6 = (0.93, 0.94, 0.96, 1.0)
# Frost
frost0 = (0.56, 0.74, 0.73, 1.0) # teal
frost1 = (0.53, 0.75, 0.82, 1.0) # light blue
frost2 = (0.51, 0.63, 0.76, 1.0) # blue
frost3 = (0.37, 0.51, 0.67, 1.0) # dark blue
# Aurora
a_red = (0.75, 0.38, 0.42, 1.0)
a_orange = (0.82, 0.53, 0.44, 1.0)
a_yellow = (0.92, 0.80, 0.55, 1.0)
a_green = (0.64, 0.75, 0.55, 1.0)
a_purple = (0.71, 0.56, 0.68, 1.0)
t.bg_black = n0
t.bg_darkest = n0
t.bg_darker = n0
t.bg_dark = n1
t.bg = n1
t.bg_light = n2
t.bg_lighter = n3
t.bg_input = n0
t.panel_bg = n1
t.header_bg = n2
t.toolbar_bg = n0
t.status_bar_bg = n0
t.section_bg = n2
t.text = n4
t.text_bright = n6
t.text_label = (0.72, 0.75, 0.80, 1.0)
t.text_dim = n3
t.text_muted = (0.42, 0.46, 0.54, 1.0)
t.text_faint = (0.50, 0.54, 0.62, 1.0)
t.accent = frost1
t.error = a_red
t.warning = a_yellow
t.success = a_green
t.info = frost1
t.selection = (0.53, 0.75, 0.82, 0.25)
t.selection_bg = frost3
t.hover_bg = n2
t.highlight = (0.92, 0.80, 0.55, 0.2)
t.border = n3
t.border_light = (0.36, 0.40, 0.48, 1.0)
t.btn_bg = n2
t.btn_hover = n3
t.btn_pressed = n0
t.btn_primary = frost1
t.btn_danger = a_red
t.input_border = n3
t.input_focus = frost1
t.placeholder = t.text_dim
t.scrollbar_hover = (0.56, 0.74, 0.73, 0.7)
t.scrollbar_track = (0.18, 0.20, 0.25, 0.3)
t.tab_bg = n0
t.tab_active = n1
t.tab_hover = n2
t.tab_text = t.text_label
t.tab_active_text = n6
t.tree_bg = n0
t.tree_select = frost3
t.tree_hover = n2
t.tree_arrow = t.text_muted
t.check_colour = frost1
t.check_box = n3
t.slider_fill = frost1
t.slider_handle = n4
t.dock_title_bg = n0
t.dock_title_text = n4
t.popup_bg = n1
t.popup_hover = frost3
t.popup_separator = n3
t.divider = n3
t.divider_hover = frost1
t.current_line = (0.24, 0.27, 0.33, 1.0)
t.bracket_match = (0.64, 0.75, 0.55, 0.4)
t.bracket_mismatch = (0.75, 0.38, 0.42, 0.4)
t.line_number = t.text_muted
t.gutter_bg = n0
t.syntax = SyntaxTheme(
keyword=frost2, string=a_green, comment=n3, number=a_purple,
decorator=a_orange, builtin=frost0, normal=n4,
)
t.viewport_bg = n0
t.selection_outline = a_orange
t.grid_major = n3
t.grid_minor = (0.26, 0.30, 0.37, 0.5)
t.minimap_text = t.text_muted
t.minimap_keyword = frost2
t.minimap_string = a_green
t.minimap_comment = n3
t.autocomplete_bg = n3
t.autocomplete_selected = (0.53, 0.75, 0.82, 0.25)
t.autocomplete_border = (0.36, 0.40, 0.48, 1.0)
t.autocomplete_hover = (0.53, 0.75, 0.82, 0.10)
t.autocomplete_dim = t.text_muted
t.autocomplete_kind = frost1
t.scrollbar_bg = n0
t.scrollbar_fg = t.text_muted
# StyleBoxes — nord: subtle gradient buttons
L, D = cls._lighten, cls._darken
t.btn_style_normal = StyleBox(
bg_colour=t.btn_bg, border_width=1.0, content_margin=2.0,
bg_gradient=(L(t.btn_bg, 0.02), D(t.btn_bg, 0.02)),
border_colour=t.border,
)
t.btn_style_hover = StyleBox(
bg_colour=t.btn_hover, border_width=1.0, content_margin=2.0,
bg_gradient=(L(t.btn_hover, 0.02), D(t.btn_hover, 0.02)),
border_colour=frost1,
)
t.btn_style_pressed = StyleBox(bg_colour=t.btn_pressed, border_width=1.0, content_margin=2.0, border_colour=frost1)
t.btn_style_disabled = StyleBox(bg_colour=D(t.btn_bg, 0.04), border_width=1.0, content_margin=2.0, border_colour=D(n3, 0.04))
t.btn_style_focused = StyleBox(bg_colour=t.btn_bg, border_width=1.0, content_margin=2.0, border_colour=frost1)
t.panel_style = StyleBox(bg_colour=t.panel_bg, border_width=1.0, border_colour=n3)
t.input_style_normal = StyleBox(bg_colour=t.bg_input, border_width=1.0, content_margin=4.0, border_colour=n3)
t.input_style_focused = StyleBox(bg_colour=t.bg_input, border_width=1.0, content_margin=4.0, border_colour=frost1)
t.input_style_disabled = StyleBox(bg_colour=D(t.bg_input, 0.02), border_width=1.0, content_margin=4.0, border_colour=D(n3, 0.04))
t.tab_style_normal = StyleBox(bg_colour=t.tab_bg, border_width=0.0)
t.tab_style_active = StyleBox(bg_colour=t.tab_active, border_width=0.0)
t.tab_style_hover = StyleBox(bg_colour=t.tab_hover, border_width=0.0)
t.popup_style = StyleBox(bg_colour=t.popup_bg, border_width=1.0, border_colour=n3)
t.popup_style_hover = StyleBox(bg_colour=t.popup_hover, border_width=0.0)
t.dock_title_style = StyleBox(bg_colour=t.dock_title_bg, border_width=0.0)
t._sync_dicts()
return t
# ---------------------------------------------------------------------------
# Module-level singleton
# ---------------------------------------------------------------------------
_current_theme: AppTheme = AppTheme()
_theme_generation: int = 0
[docs]
def get_theme() -> AppTheme:
"""Return the current application theme singleton."""
return _current_theme
[docs]
def theme_generation() -> int:
"""Return the current theme generation counter.
Incremented by ``set_theme()``. Draw caches use this to detect
global theme changes without walking the widget tree.
"""
return _theme_generation
[docs]
def set_theme(theme: AppTheme) -> None:
"""Swap the module-level theme singleton and bump the generation counter.
All ``ThemeColour`` / ``ThemeStyleBox`` descriptors resolve on access,
so widgets pick up the new theme on their next draw. The generation
counter invalidates draw caches so that next draw actually happens.
"""
global _current_theme, _theme_generation
_current_theme = theme
_theme_generation += 1
[docs]
def em(multiple: float) -> float:
"""Return *multiple* of the theme font size in logical pixels.
Use for layout dimensions that should scale with the user's font-size
preference. Does NOT multiply by ``ui_scale`` — UI layout works in
logical (window) coordinates; the GPU rendering pipeline handles the
logical-to-physical conversion separately via Draw2DPass::
row_height = em(2.18) # ~24 px at default 11pt
padding = em(0.55) # ~6 px
label_w = em(7.27) # ~80 px
"""
return multiple * _current_theme.font_size