"""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.
Variants are driven by module-level palette dicts and ``StyleBoxConfig``
strategies. Factory classmethods (``dark()``, ``light()``, ``monokai()``,
``abyss()``, ``midnight()``, ``solarised_dark()``, ``nord()``) 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
import logging
from dataclasses import dataclass
from typing import Any
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
[docs]
@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
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_rect((x + bw, y + bw), (w - 2 * bw, h - 2 * bw), colour=self.bg_colour, filled=True)
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_rect((x, y), (w, bw), colour=ct, filled=True)
renderer.draw_rect((x, y + h - bw), (w, bw), colour=cb, filled=True)
renderer.draw_rect((x, y + bw), (bw, h - 2 * bw), colour=cl, filled=True)
renderer.draw_rect((x + w - bw, y + bw), (bw, h - 2 * bw), colour=cr, filled=True)
# ---------------------------------------------------------------------------
# SyntaxTheme -- colours for code/syntax highlighting
# ---------------------------------------------------------------------------
[docs]
class SyntaxTheme:
"""Syntax highlighting colour set for code editors."""
__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
# ---------------------------------------------------------------------------
# StyleBox configuration -- strategy for building per-state StyleBoxes
# ---------------------------------------------------------------------------
[docs]
@dataclass(frozen=True)
class StyleBoxConfig:
"""Strategy for building the per-state StyleBoxes from a colour palette.
Defaults match the dark theme (classic emboss, tight margins). Variants
override only the knobs that differ.
"""
button_style: str = "embossed_classic" # embossed_classic | embossed_subtle | gradient | flat
emboss_light: float = 0.08
emboss_dark: float = 0.06
gradient_amount: float = 0.0
hover_uses_accent: bool = False
pressed_uses_accent: bool = False
disabled_bg_darken: float = 0.04
disabled_border_darken: float = 0.08
input_disabled_darken: float = 0.02
flat_content_margin: float = 0.0
popup_border_attr: str = "border_light"
# ---------------------------------------------------------------------------
# Layout sizes (shared across all variants)
# ---------------------------------------------------------------------------
_DEFAULT_SIZES: dict[str, float] = {
"header_h": 28.0,
"row_h": 22.0,
"tab_h": 24.0,
"font_size": 11.0,
"ui_scale": 1.0,
"scrollbar_width": 8.0,
"dock_title_h": 24.0,
}
# ---------------------------------------------------------------------------
# Palettes
# ---------------------------------------------------------------------------
# Each palette is a flat dict mapping attribute name -> value (Colour4 or
# SyntaxTheme). Values may be aliased via local variables for clarity.
def _build_dark_palette() -> dict[str, Any]:
bg_darker = (0.03, 0.03, 0.035, 1.0)
bg = (0.055, 0.055, 0.06, 1.0)
bg_light = (0.075, 0.075, 0.085, 1.0)
bg_lighter = (0.095, 0.095, 0.105, 1.0)
bg_input = (0.025, 0.025, 0.03, 1.0)
accent = (0.28, 0.58, 0.98, 1.0)
border = (0.125, 0.125, 0.14, 1.0)
border_light = (0.15, 0.15, 0.165, 1.0)
text_dim = (0.46, 0.46, 0.48, 1.0)
selection_bg = (0.2, 0.45, 0.8, 1.0)
hover_bg = (0.08, 0.08, 0.095, 1.0)
btn_bg = (0.10, 0.10, 0.115, 1.0)
return {
"bg_black": (0.0, 0.0, 0.0, 1.0),
"bg_darkest": (0.02, 0.02, 0.025, 1.0),
"bg_darker": bg_darker,
"bg_dark": (0.045, 0.045, 0.05, 1.0),
"bg": bg,
"bg_light": bg_light,
"bg_lighter": bg_lighter,
"bg_input": bg_input,
"panel_bg": bg,
"header_bg": bg_light,
"toolbar_bg": (0.04, 0.04, 0.045, 1.0),
"status_bar_bg": (0.03, 0.03, 0.035, 1.0),
"section_bg": (0.07, 0.07, 0.08, 1.0),
"text": (0.86, 0.86, 0.88, 1.0),
"text_bright": (0.95, 0.95, 0.97, 1.0),
"text_label": (0.75, 0.75, 0.78, 1.0),
"text_dim": text_dim,
"text_muted": (0.56, 0.56, 0.58, 1.0),
"text_faint": (0.62, 0.62, 0.64, 1.0),
"accent": accent,
"error": (0.94, 0.33, 0.31, 1.0),
"warning": (0.95, 0.76, 0.19, 1.0),
"success": (0.36, 0.72, 0.36, 1.0),
"info": (0.4, 0.6, 0.9, 1.0),
"selection": (0.2, 0.4, 0.7, 0.5),
"selection_bg": selection_bg,
"hover_bg": hover_bg,
"highlight": (0.3, 0.3, 0.0, 0.3),
"border": border,
"border_light": border_light,
"btn_bg": btn_bg,
"btn_hover": (0.14, 0.14, 0.16, 1.0),
"btn_pressed": (0.03, 0.03, 0.035, 1.0),
"btn_border": border,
"btn_primary": accent,
"btn_danger": (0.75, 0.25, 0.25, 1.0),
"input_border": border,
"input_focus": accent,
"placeholder": text_dim,
"scrollbar_hover": (0.26, 0.26, 0.29, 0.8),
"scrollbar_track": (0.04, 0.04, 0.045, 0.3),
"tab_bg": (0.04, 0.04, 0.045, 1.0),
"tab_active": (0.09, 0.09, 0.105, 1.0),
"tab_hover": (0.07, 0.07, 0.085, 1.0),
"tab_text": (0.75, 0.75, 0.78, 1.0),
"tab_active_text": (1.0, 1.0, 1.0, 1.0),
"tree_bg": bg_darker,
"tree_select": (0.2, 0.4, 0.7, 1.0),
"tree_hover": hover_bg,
"tree_arrow": (0.56, 0.56, 0.58, 1.0),
"check_colour": accent,
"check_box": border,
"slider_fill": accent,
"slider_handle": (0.8, 0.8, 0.8, 1.0),
"dock_title_bg": bg_darker,
"dock_title_text": (0.85, 0.85, 0.85, 1.0),
"popup_bg": bg,
"popup_hover": selection_bg,
"popup_separator": border_light,
"divider": border_light,
"divider_hover": accent,
"current_line": (0.07, 0.07, 0.085, 1.0),
"bracket_match": (0.4, 0.7, 0.4, 0.5),
"bracket_mismatch": (0.9, 0.2, 0.2, 0.5),
"line_number": text_dim,
"gutter_bg": bg_darker,
"syntax": SyntaxTheme(),
"viewport_bg": (0.05, 0.05, 0.06, 1.0),
"gizmo_x": (0.96, 0.26, 0.28, 1.0),
"gizmo_y": (0.40, 0.84, 0.36, 1.0),
"gizmo_z": (0.26, 0.52, 0.96, 1.0),
"selection_outline": (1.0, 0.6, 0.0, 1.0),
"grid_major": (0.38, 0.38, 0.40, 1.0),
"grid_minor": (0.26, 0.26, 0.28, 0.6),
"minimap_text": (0.42, 0.42, 0.47, 1.0),
"minimap_keyword": (0.55, 0.40, 0.70, 1.0),
"minimap_string": (0.55, 0.70, 0.40, 1.0),
"minimap_comment": (0.32, 0.32, 0.36, 1.0),
"autocomplete_bg": bg_lighter,
"autocomplete_selected": (0.28, 0.58, 0.98, 0.30),
"autocomplete_border": (0.38, 0.38, 0.42, 1.0),
"autocomplete_hover": (0.26, 0.56, 0.96, 0.12),
"autocomplete_dim": (0.46, 0.46, 0.52, 1.0),
"autocomplete_kind": (0.55, 0.75, 0.95, 1.0),
"scrollbar_bg": (0.06, 0.06, 0.07, 1.0),
"scrollbar_fg": (0.175, 0.175, 0.20, 1.0),
}
def _build_abyss_palette() -> dict[str, Any]:
bg_darkest = (0.02, 0.02, 0.035, 1.0)
bg_darker = (0.03, 0.03, 0.05, 1.0)
bg_dark = (0.045, 0.045, 0.065, 1.0)
bg = (0.055, 0.055, 0.08, 1.0)
bg_light = (0.08, 0.08, 0.11, 1.0)
bg_lighter = (0.10, 0.10, 0.14, 1.0)
bg_input = (0.025, 0.025, 0.04, 1.0)
accent = (0.30, 0.55, 1.0, 1.0)
border = (0.12, 0.12, 0.18, 1.0)
border_light = (0.16, 0.16, 0.24, 1.0)
text = (0.78, 0.80, 0.86, 1.0)
text_bright = (0.90, 0.92, 0.97, 1.0)
text_label = (0.58, 0.60, 0.68, 1.0)
text_dim = (0.36, 0.38, 0.46, 1.0)
text_muted = (0.44, 0.46, 0.54, 1.0)
selection_bg = (0.20, 0.40, 0.80, 1.0)
return {
"bg_black": (0.0, 0.0, 0.01, 1.0),
"bg_darkest": bg_darkest,
"bg_darker": bg_darker,
"bg_dark": bg_dark,
"bg": bg,
"bg_light": bg_light,
"bg_lighter": bg_lighter,
"bg_input": bg_input,
"panel_bg": bg,
"header_bg": bg_light,
"toolbar_bg": bg_darker,
"status_bar_bg": bg_darkest,
"section_bg": bg_light,
"text": text,
"text_bright": text_bright,
"text_label": text_label,
"text_dim": text_dim,
"text_muted": text_muted,
"text_faint": (0.50, 0.52, 0.60, 1.0),
"accent": accent,
"error": (0.90, 0.28, 0.30, 1.0),
"warning": (0.92, 0.72, 0.16, 1.0),
"success": (0.30, 0.72, 0.40, 1.0),
"info": (0.35, 0.60, 0.95, 1.0),
"selection": (0.20, 0.40, 0.80, 0.35),
"selection_bg": selection_bg,
"hover_bg": bg_light,
"highlight": (0.30, 0.45, 0.80, 0.2),
"border": border,
"border_light": border_light,
"btn_bg": (0.08, 0.08, 0.12, 1.0),
"btn_hover": (0.12, 0.12, 0.18, 1.0),
"btn_pressed": (0.03, 0.03, 0.05, 1.0),
"btn_border": border,
"btn_primary": accent,
"btn_danger": (0.75, 0.22, 0.25, 1.0),
"input_border": border,
"input_focus": accent,
"placeholder": text_dim,
"scrollbar_hover": (0.35, 0.38, 0.50, 0.7),
"scrollbar_track": (0.03, 0.03, 0.05, 0.3),
"tab_bg": bg_darker,
"tab_active": bg,
"tab_hover": bg_light,
"tab_text": text_label,
"tab_active_text": text_bright,
"tree_bg": bg_darkest,
"tree_select": selection_bg,
"tree_hover": bg_light,
"tree_arrow": text_dim,
"check_colour": accent,
"check_box": border,
"slider_fill": accent,
"slider_handle": text_label,
"dock_title_bg": bg_darker,
"dock_title_text": text,
"popup_bg": bg_dark,
"popup_hover": selection_bg,
"popup_separator": border_light,
"divider": border,
"divider_hover": accent,
"current_line": (0.06, 0.06, 0.09, 1.0),
"bracket_match": (0.25, 0.55, 0.35, 0.4),
"bracket_mismatch": (0.80, 0.20, 0.25, 0.4),
"line_number": text_dim,
"gutter_bg": bg_darkest,
"syntax": SyntaxTheme(
keyword=(0.45, 0.62, 1.0, 1.0), string=(0.40, 0.82, 0.55, 1.0),
comment=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=text,
),
"viewport_bg": bg_darkest,
"gizmo_x": (0.96, 0.26, 0.28, 1.0),
"gizmo_y": (0.40, 0.84, 0.36, 1.0),
"gizmo_z": (0.26, 0.52, 0.96, 1.0),
"selection_outline": (0.90, 0.55, 0.0, 1.0),
"grid_major": border_light,
"grid_minor": (0.08, 0.08, 0.12, 0.5),
"minimap_text": text_dim,
"minimap_keyword": (0.45, 0.35, 0.65, 1.0),
"minimap_string": (0.40, 0.60, 0.45, 1.0),
"minimap_comment": text_dim,
"autocomplete_bg": bg_lighter,
"autocomplete_selected": (0.30, 0.55, 1.0, 0.25),
"autocomplete_border": border_light,
"autocomplete_hover": (0.30, 0.55, 1.0, 0.10),
"autocomplete_dim": text_muted,
"autocomplete_kind": (0.55, 0.75, 0.95, 1.0),
"scrollbar_bg": bg_darkest,
"scrollbar_fg": text_dim,
}
def _build_midnight_palette() -> dict[str, Any]:
bg_darkest = (0.02, 0.03, 0.02, 1.0)
bg_darker = (0.03, 0.04, 0.03, 1.0)
bg_dark = (0.045, 0.06, 0.045, 1.0)
bg = (0.06, 0.07, 0.055, 1.0)
bg_light = (0.08, 0.10, 0.08, 1.0)
bg_lighter = (0.11, 0.13, 0.10, 1.0)
bg_input = (0.025, 0.035, 0.025, 1.0)
accent = (0.28, 0.72, 0.56, 1.0)
border = (0.10, 0.14, 0.10, 1.0)
border_light = (0.14, 0.19, 0.14, 1.0)
text = (0.78, 0.84, 0.76, 1.0)
text_bright = (0.90, 0.95, 0.88, 1.0)
text_label = (0.58, 0.65, 0.55, 1.0)
text_dim = (0.36, 0.42, 0.34, 1.0)
text_muted = (0.44, 0.50, 0.42, 1.0)
selection_bg = (0.18, 0.45, 0.35, 1.0)
return {
"bg_black": (0.0, 0.01, 0.0, 1.0),
"bg_darkest": bg_darkest,
"bg_darker": bg_darker,
"bg_dark": bg_dark,
"bg": bg,
"bg_light": bg_light,
"bg_lighter": bg_lighter,
"bg_input": bg_input,
"panel_bg": bg,
"header_bg": bg_light,
"toolbar_bg": bg_darker,
"status_bar_bg": bg_darkest,
"section_bg": bg_light,
"text": text,
"text_bright": text_bright,
"text_label": text_label,
"text_dim": text_dim,
"text_muted": text_muted,
"text_faint": (0.50, 0.56, 0.48, 1.0),
"accent": accent,
"error": (0.88, 0.30, 0.28, 1.0),
"warning": (0.90, 0.75, 0.20, 1.0),
"success": (0.35, 0.78, 0.35, 1.0),
"info": (0.35, 0.68, 0.65, 1.0),
"selection": (0.18, 0.45, 0.35, 0.35),
"selection_bg": selection_bg,
"hover_bg": bg_light,
"highlight": (0.35, 0.50, 0.20, 0.2),
"border": border,
"border_light": border_light,
"btn_bg": (0.07, 0.09, 0.065, 1.0),
"btn_hover": (0.10, 0.14, 0.10, 1.0),
"btn_pressed": (0.03, 0.04, 0.03, 1.0),
"btn_border": border,
"btn_primary": accent,
"btn_danger": (0.75, 0.24, 0.22, 1.0),
"input_border": border,
"input_focus": accent,
"placeholder": text_dim,
"scrollbar_hover": (0.32, 0.42, 0.32, 0.7),
"scrollbar_track": (0.03, 0.04, 0.03, 0.3),
"tab_bg": bg_darker,
"tab_active": bg,
"tab_hover": bg_light,
"tab_text": text_label,
"tab_active_text": text_bright,
"tree_bg": bg_darkest,
"tree_select": selection_bg,
"tree_hover": bg_light,
"tree_arrow": text_dim,
"check_colour": accent,
"check_box": border,
"slider_fill": accent,
"slider_handle": text_label,
"dock_title_bg": bg_darker,
"dock_title_text": text,
"popup_bg": bg_dark,
"popup_hover": selection_bg,
"popup_separator": border_light,
"divider": border,
"divider_hover": accent,
"current_line": (0.05, 0.07, 0.05, 1.0),
"bracket_match": (0.25, 0.55, 0.30, 0.4),
"bracket_mismatch": (0.80, 0.22, 0.20, 0.4),
"line_number": text_dim,
"gutter_bg": bg_darkest,
"syntax": SyntaxTheme(
keyword=(0.55, 0.85, 0.65, 1.0), string=(0.85, 0.82, 0.45, 1.0),
comment=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=text,
),
"viewport_bg": bg_darkest,
"gizmo_x": (0.96, 0.26, 0.28, 1.0),
"gizmo_y": (0.40, 0.84, 0.36, 1.0),
"gizmo_z": (0.26, 0.52, 0.96, 1.0),
"selection_outline": (0.85, 0.60, 0.0, 1.0),
"grid_major": border_light,
"grid_minor": (0.07, 0.09, 0.07, 0.5),
"minimap_text": text_dim,
"minimap_keyword": (0.45, 0.65, 0.50, 1.0),
"minimap_string": (0.65, 0.60, 0.35, 1.0),
"minimap_comment": text_dim,
"autocomplete_bg": bg_lighter,
"autocomplete_selected": (0.28, 0.72, 0.56, 0.25),
"autocomplete_border": border_light,
"autocomplete_hover": (0.28, 0.72, 0.56, 0.10),
"autocomplete_dim": text_muted,
"autocomplete_kind": (0.45, 0.82, 0.70, 1.0),
"scrollbar_bg": bg_darkest,
"scrollbar_fg": text_dim,
}
def _build_light_palette() -> dict[str, Any]:
bg_darker = (0.85, 0.85, 0.85, 1.0)
bg = (0.92, 0.92, 0.92, 1.0)
bg_lighter = (0.98, 0.98, 0.98, 1.0)
bg_input = (1.0, 1.0, 1.0, 1.0)
accent = (0.0, 0.45, 0.85, 1.0)
border = (0.72, 0.72, 0.72, 1.0)
text = (0.15, 0.15, 0.15, 1.0)
text_bright = (0.1, 0.1, 0.1, 1.0)
text_label = (0.35, 0.35, 0.35, 1.0)
text_dim = (0.45, 0.45, 0.45, 1.0)
text_muted = (0.50, 0.50, 0.50, 1.0)
selection_bg = (0.3, 0.55, 0.9, 1.0)
hover_bg = (0.85, 0.85, 0.88, 1.0)
header_bg = (0.88, 0.88, 0.88, 1.0)
return {
"bg_black": (0.80, 0.80, 0.80, 1.0),
"bg_darkest": (0.82, 0.82, 0.82, 1.0),
"bg_darker": bg_darker,
"bg_dark": (0.88, 0.88, 0.88, 1.0),
"bg": bg,
"bg_light": (0.96, 0.96, 0.96, 1.0),
"bg_lighter": bg_lighter,
"bg_input": bg_input,
"panel_bg": (0.92, 0.92, 0.93, 1.0),
"header_bg": header_bg,
"toolbar_bg": (0.82, 0.82, 0.82, 1.0),
"status_bar_bg": (0.78, 0.78, 0.8, 1.0),
"section_bg": (0.90, 0.90, 0.90, 1.0),
"text": text,
"text_bright": text_bright,
"text_label": text_label,
"text_dim": text_dim,
"text_muted": text_muted,
"text_faint": (0.55, 0.55, 0.55, 1.0),
"accent": accent,
"error": (0.85, 0.18, 0.15, 1.0),
"warning": (0.8, 0.6, 0.0, 1.0),
"success": (0.2, 0.6, 0.2, 1.0),
"info": (0.2, 0.45, 0.8, 1.0),
"selection": (0.2, 0.5, 0.9, 0.3),
"selection_bg": selection_bg,
"hover_bg": hover_bg,
"highlight": (1.0, 1.0, 0.0, 0.2),
"border": border,
"border_light": (0.80, 0.80, 0.80, 1.0),
"btn_bg": (0.85, 0.85, 0.87, 1.0),
"btn_hover": (0.78, 0.78, 0.82, 1.0),
"btn_pressed": (0.70, 0.70, 0.74, 1.0),
"btn_border": border,
"btn_primary": accent,
"btn_danger": (0.80, 0.20, 0.20, 1.0),
"input_border": border,
"input_focus": accent,
"placeholder": text_dim,
"scrollbar_hover": (0.55, 0.55, 0.58, 0.8),
"scrollbar_track": (0.80, 0.80, 0.82, 0.3),
"tab_bg": (0.85, 0.85, 0.85, 1.0),
"tab_active": (0.92, 0.92, 0.93, 1.0),
"tab_hover": (0.88, 0.88, 0.90, 1.0),
"tab_text": text_label,
"tab_active_text": text_bright,
"tree_bg": bg_lighter,
"tree_select": (0.3, 0.55, 0.9, 1.0),
"tree_hover": hover_bg,
"tree_arrow": text_muted,
"check_colour": accent,
"check_box": border,
"slider_fill": accent,
"slider_handle": (0.4, 0.4, 0.42, 1.0),
"dock_title_bg": header_bg,
"dock_title_text": text,
"popup_bg": bg_lighter,
"popup_hover": selection_bg,
"popup_separator": border,
"divider": border,
"divider_hover": accent,
"current_line": (0.88, 0.90, 0.95, 1.0),
"bracket_match": (0.2, 0.6, 0.2, 0.4),
"bracket_mismatch": (0.85, 0.2, 0.2, 0.4),
"line_number": text_dim,
"gutter_bg": bg_darker,
"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),
),
"viewport_bg": (0.75, 0.75, 0.78, 1.0),
"gizmo_x": (0.96, 0.26, 0.28, 1.0),
"gizmo_y": (0.40, 0.84, 0.36, 1.0),
"gizmo_z": (0.26, 0.52, 0.96, 1.0),
"selection_outline": (0.0, 0.45, 1.0, 1.0),
"grid_major": (0.6, 0.6, 0.6, 1.0),
"grid_minor": (0.7, 0.7, 0.7, 0.5),
"minimap_text": (0.55, 0.55, 0.58, 1.0),
"minimap_keyword": (0.0, 0.0, 0.6, 0.7),
"minimap_string": (0.0, 0.4, 0.0, 0.7),
"minimap_comment": (0.55, 0.55, 0.58, 1.0),
"autocomplete_bg": bg_lighter,
"autocomplete_selected": (0.0, 0.45, 0.85, 0.25),
"autocomplete_border": border,
"autocomplete_hover": (0.0, 0.45, 0.85, 0.10),
"autocomplete_dim": text_muted,
"autocomplete_kind": (0.2, 0.45, 0.7, 1.0),
"scrollbar_bg": (0.82, 0.82, 0.84, 1.0),
"scrollbar_fg": (0.60, 0.60, 0.62, 1.0),
}
def _build_monokai_palette() -> dict[str, Any]:
bg_darker = (0.11, 0.12, 0.09, 1.0)
bg = (0.15, 0.16, 0.13, 1.0)
bg_light = (0.22, 0.23, 0.19, 1.0)
bg_lighter = (0.25, 0.26, 0.22, 1.0)
bg_input = (0.12, 0.13, 0.10, 1.0)
accent = (0.4, 0.85, 0.94, 1.0)
border = (0.3, 0.31, 0.27, 1.0)
border_light = (0.38, 0.39, 0.34, 1.0)
text = (0.97, 0.97, 0.95, 1.0)
text_bright = (1.0, 1.0, 0.98, 1.0)
text_label = (0.75, 0.73, 0.66, 1.0)
text_dim = (0.46, 0.44, 0.37, 1.0)
text_muted = (0.55, 0.53, 0.46, 1.0)
hover_bg = (0.20, 0.21, 0.17, 1.0)
selection_bg = (0.4, 0.6, 0.2, 1.0)
btn_bg = (0.22, 0.23, 0.19, 1.0)
return {
"bg_black": (0.06, 0.07, 0.04, 1.0),
"bg_darkest": (0.09, 0.10, 0.07, 1.0),
"bg_darker": bg_darker,
"bg_dark": (0.13, 0.14, 0.11, 1.0),
"bg": bg,
"bg_light": bg_light,
"bg_lighter": bg_lighter,
"bg_input": bg_input,
"panel_bg": bg,
"header_bg": bg_light,
"toolbar_bg": (0.14, 0.15, 0.12, 1.0),
"status_bar_bg": (0.12, 0.13, 0.1, 1.0),
"section_bg": (0.20, 0.21, 0.17, 1.0),
"text": text,
"text_bright": text_bright,
"text_label": text_label,
"text_dim": text_dim,
"text_muted": text_muted,
"text_faint": (0.60, 0.58, 0.50, 1.0),
"accent": accent,
"error": (0.98, 0.15, 0.45, 1.0),
"warning": (0.9, 0.86, 0.45, 1.0),
"success": (0.65, 0.89, 0.18, 1.0),
"info": (0.4, 0.85, 0.94, 1.0),
"selection": (0.3, 0.45, 0.2, 0.5),
"selection_bg": selection_bg,
"hover_bg": hover_bg,
"highlight": (0.4, 0.4, 0.0, 0.3),
"border": border,
"border_light": border_light,
"btn_bg": btn_bg,
"btn_hover": (0.30, 0.31, 0.26, 1.0),
"btn_pressed": (0.12, 0.13, 0.10, 1.0),
"btn_border": border,
"btn_primary": accent,
"btn_danger": (0.80, 0.15, 0.35, 1.0),
"input_border": border,
"input_focus": accent,
"placeholder": text_dim,
"scrollbar_hover": (0.50, 0.48, 0.42, 0.8),
"scrollbar_track": (0.11, 0.12, 0.09, 0.3),
"tab_bg": (0.13, 0.14, 0.11, 1.0),
"tab_active": btn_bg,
"tab_hover": hover_bg,
"tab_text": text_label,
"tab_active_text": text_bright,
"tree_bg": bg_darker,
"tree_select": selection_bg,
"tree_hover": hover_bg,
"tree_arrow": text_muted,
"check_colour": accent,
"check_box": border,
"slider_fill": accent,
"slider_handle": (0.75, 0.73, 0.66, 1.0),
"dock_title_bg": bg_darker,
"dock_title_text": text,
"popup_bg": bg,
"popup_hover": selection_bg,
"popup_separator": border_light,
"divider": border_light,
"divider_hover": accent,
"current_line": (0.18, 0.19, 0.15, 1.0),
"bracket_match": (0.4, 0.6, 0.2, 0.5),
"bracket_mismatch": (0.9, 0.15, 0.35, 0.5),
"line_number": text_dim,
"gutter_bg": bg_darker,
"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),
),
"viewport_bg": (0.16, 0.17, 0.14, 1.0),
"gizmo_x": (0.96, 0.26, 0.28, 1.0),
"gizmo_y": (0.40, 0.84, 0.36, 1.0),
"gizmo_z": (0.26, 0.52, 0.96, 1.0),
"selection_outline": (0.65, 0.89, 0.18, 1.0),
"grid_major": (0.3, 0.31, 0.27, 1.0),
"grid_minor": (0.22, 0.23, 0.19, 0.5),
"minimap_text": (0.42, 0.40, 0.35, 1.0),
"minimap_keyword": (0.75, 0.12, 0.35, 0.8),
"minimap_string": (0.70, 0.66, 0.35, 0.8),
"minimap_comment": (0.42, 0.40, 0.35, 1.0),
"autocomplete_bg": bg_lighter,
"autocomplete_selected": (0.4, 0.85, 0.94, 0.25),
"autocomplete_border": border_light,
"autocomplete_hover": (0.4, 0.85, 0.94, 0.10),
"autocomplete_dim": text_muted,
"autocomplete_kind": (0.4, 0.85, 0.94, 0.8),
"scrollbar_bg": (0.20, 0.21, 0.17, 1.0),
"scrollbar_fg": (0.40, 0.38, 0.32, 1.0),
}
def _build_solarised_dark_palette() -> dict[str, Any]:
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)
bg_light = (0.06, 0.25, 0.30, 1.0)
bg_lighter = (0.09, 0.29, 0.34, 1.0)
border = (0.10, 0.30, 0.36, 1.0)
border_light = (0.14, 0.34, 0.40, 1.0)
return {
"bg_black": base03,
"bg_darkest": base03,
"bg_darker": base03,
"bg_dark": base02,
"bg": base02,
"bg_light": bg_light,
"bg_lighter": bg_lighter,
"bg_input": base03,
"panel_bg": base02,
"header_bg": bg_light,
"toolbar_bg": base03,
"status_bar_bg": base03,
"section_bg": bg_light,
"text": base0,
"text_bright": base1,
"text_label": base00,
"text_dim": base01,
"text_muted": base01,
"text_faint": base01,
"accent": blue,
"error": red,
"warning": yellow,
"success": green,
"info": cyan,
"selection": (0.15, 0.55, 0.82, 0.3),
"selection_bg": blue,
"hover_bg": bg_light,
"highlight": (0.71, 0.54, 0.0, 0.2),
"border": border,
"border_light": border_light,
"btn_bg": bg_light,
"btn_hover": bg_lighter,
"btn_pressed": base03,
"btn_border": border,
"btn_primary": blue,
"btn_danger": red,
"input_border": border,
"input_focus": blue,
"placeholder": base01,
"scrollbar_hover": (0.40, 0.48, 0.51, 0.7),
"scrollbar_track": (0.0, 0.17, 0.21, 0.3),
"tab_bg": base03,
"tab_active": base02,
"tab_hover": bg_light,
"tab_text": base00,
"tab_active_text": base1,
"tree_bg": base03,
"tree_select": blue,
"tree_hover": bg_light,
"tree_arrow": base01,
"check_colour": blue,
"check_box": border,
"slider_fill": blue,
"slider_handle": base1,
"dock_title_bg": base03,
"dock_title_text": base0,
"popup_bg": base02,
"popup_hover": blue,
"popup_separator": border,
"divider": border,
"divider_hover": blue,
"current_line": (0.04, 0.22, 0.27, 1.0),
"bracket_match": (0.16, 0.63, 0.60, 0.4),
"bracket_mismatch": (0.86, 0.20, 0.18, 0.4),
"line_number": base01,
"gutter_bg": base03,
"syntax": SyntaxTheme(
keyword=green, string=cyan, comment=base01, number=magenta,
decorator=orange, builtin=yellow, normal=base0,
),
"viewport_bg": base03,
"gizmo_x": (0.96, 0.26, 0.28, 1.0),
"gizmo_y": (0.40, 0.84, 0.36, 1.0),
"gizmo_z": (0.26, 0.52, 0.96, 1.0),
"selection_outline": orange,
"grid_major": border,
"grid_minor": (0.06, 0.25, 0.30, 0.5),
"minimap_text": base01,
"minimap_keyword": green,
"minimap_string": cyan,
"minimap_comment": base01,
"autocomplete_bg": bg_lighter,
"autocomplete_selected": (0.15, 0.55, 0.82, 0.25),
"autocomplete_border": border_light,
"autocomplete_hover": (0.15, 0.55, 0.82, 0.10),
"autocomplete_dim": base00,
"autocomplete_kind": blue,
"scrollbar_bg": base03,
"scrollbar_fg": base01,
}
def _build_nord_palette() -> dict[str, Any]:
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)
n4 = (0.85, 0.87, 0.91, 1.0)
n6 = (0.93, 0.94, 0.96, 1.0)
frost0 = (0.56, 0.74, 0.73, 1.0)
frost1 = (0.53, 0.75, 0.82, 1.0)
frost2 = (0.51, 0.63, 0.76, 1.0)
frost3 = (0.37, 0.51, 0.67, 1.0)
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)
border_light = (0.36, 0.40, 0.48, 1.0)
text_label = (0.72, 0.75, 0.80, 1.0)
text_dim = n3
text_muted = (0.42, 0.46, 0.54, 1.0)
return {
"bg_black": n0,
"bg_darkest": n0,
"bg_darker": n0,
"bg_dark": n1,
"bg": n1,
"bg_light": n2,
"bg_lighter": n3,
"bg_input": n0,
"panel_bg": n1,
"header_bg": n2,
"toolbar_bg": n0,
"status_bar_bg": n0,
"section_bg": n2,
"text": n4,
"text_bright": n6,
"text_label": text_label,
"text_dim": text_dim,
"text_muted": text_muted,
"text_faint": (0.50, 0.54, 0.62, 1.0),
"accent": frost1,
"error": a_red,
"warning": a_yellow,
"success": a_green,
"info": frost1,
"selection": (0.53, 0.75, 0.82, 0.25),
"selection_bg": frost3,
"hover_bg": n2,
"highlight": (0.92, 0.80, 0.55, 0.2),
"border": n3,
"border_light": border_light,
"btn_bg": n2,
"btn_hover": n3,
"btn_pressed": n0,
"btn_border": n3,
"btn_primary": frost1,
"btn_danger": a_red,
"input_border": n3,
"input_focus": frost1,
"placeholder": text_dim,
"scrollbar_hover": (0.56, 0.74, 0.73, 0.7),
"scrollbar_track": (0.18, 0.20, 0.25, 0.3),
"tab_bg": n0,
"tab_active": n1,
"tab_hover": n2,
"tab_text": text_label,
"tab_active_text": n6,
"tree_bg": n0,
"tree_select": frost3,
"tree_hover": n2,
"tree_arrow": text_muted,
"check_colour": frost1,
"check_box": n3,
"slider_fill": frost1,
"slider_handle": n4,
"dock_title_bg": n0,
"dock_title_text": n4,
"popup_bg": n1,
"popup_hover": frost3,
"popup_separator": n3,
"divider": n3,
"divider_hover": frost1,
"current_line": (0.24, 0.27, 0.33, 1.0),
"bracket_match": (0.64, 0.75, 0.55, 0.4),
"bracket_mismatch": (0.75, 0.38, 0.42, 0.4),
"line_number": text_muted,
"gutter_bg": n0,
"syntax": SyntaxTheme(
keyword=frost2, string=a_green, comment=n3, number=a_purple,
decorator=a_orange, builtin=frost0, normal=n4,
),
"viewport_bg": n0,
"gizmo_x": (0.96, 0.26, 0.28, 1.0),
"gizmo_y": (0.40, 0.84, 0.36, 1.0),
"gizmo_z": (0.26, 0.52, 0.96, 1.0),
"selection_outline": a_orange,
"grid_major": n3,
"grid_minor": (0.26, 0.30, 0.37, 0.5),
"minimap_text": text_muted,
"minimap_keyword": frost2,
"minimap_string": a_green,
"minimap_comment": n3,
"autocomplete_bg": n3,
"autocomplete_selected": (0.53, 0.75, 0.82, 0.25),
"autocomplete_border": border_light,
"autocomplete_hover": (0.53, 0.75, 0.82, 0.10),
"autocomplete_dim": text_muted,
"autocomplete_kind": frost1,
"scrollbar_bg": n0,
"scrollbar_fg": text_muted,
}
_DARK_PALETTE: dict[str, Any] = _build_dark_palette()
_ABYSS_PALETTE: dict[str, Any] = _build_abyss_palette()
_MIDNIGHT_PALETTE: dict[str, Any] = _build_midnight_palette()
_LIGHT_PALETTE: dict[str, Any] = _build_light_palette()
_MONOKAI_PALETTE: dict[str, Any] = _build_monokai_palette()
_SOLARISED_DARK_PALETTE: dict[str, Any] = _build_solarised_dark_palette()
_NORD_PALETTE: dict[str, Any] = _build_nord_palette()
# ---------------------------------------------------------------------------
# Per-variant StyleBox configurations (defaults match dark)
# ---------------------------------------------------------------------------
_DARK_STYLEBOX = StyleBoxConfig()
_ABYSS_STYLEBOX = StyleBoxConfig(
button_style="embossed_subtle", emboss_light=0.04, emboss_dark=0.02,
hover_uses_accent=True, pressed_uses_accent=True,
disabled_bg_darken=0.02, disabled_border_darken=0.04, input_disabled_darken=0.01,
flat_content_margin=2.0, popup_border_attr="border",
)
_MIDNIGHT_STYLEBOX = StyleBoxConfig(
button_style="embossed_subtle", emboss_light=0.03, emboss_dark=0.02,
hover_uses_accent=True, pressed_uses_accent=True,
disabled_bg_darken=0.02, disabled_border_darken=0.03, input_disabled_darken=0.01,
flat_content_margin=2.0, popup_border_attr="border",
)
_LIGHT_STYLEBOX = StyleBoxConfig(
button_style="gradient", gradient_amount=0.04,
flat_content_margin=2.0, popup_border_attr="border",
)
_MONOKAI_STYLEBOX = StyleBoxConfig(
button_style="flat", hover_uses_accent=True, pressed_uses_accent=True,
flat_content_margin=2.0,
)
_SOLARISED_DARK_STYLEBOX = StyleBoxConfig(
button_style="flat", hover_uses_accent=True, pressed_uses_accent=True,
disabled_border_darken=0.04, flat_content_margin=2.0, popup_border_attr="border",
)
_NORD_STYLEBOX = StyleBoxConfig(
button_style="gradient", gradient_amount=0.02,
hover_uses_accent=True, pressed_uses_accent=True,
disabled_border_darken=0.04, flat_content_margin=2.0, popup_border_attr="border",
)
# ---------------------------------------------------------------------------
# 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.
"""
# Class-level annotations so static type checkers see the palette-driven attributes.
# The values themselves are populated in __init__ from the active palette.
bg_black: Colour4
bg_darkest: Colour4
bg_darker: Colour4
bg_dark: Colour4
bg: Colour4
bg_light: Colour4
bg_lighter: Colour4
bg_input: Colour4
panel_bg: Colour4
header_bg: Colour4
toolbar_bg: Colour4
status_bar_bg: Colour4
section_bg: Colour4
text: Colour4
text_bright: Colour4
text_label: Colour4
text_dim: Colour4
text_muted: Colour4
text_faint: Colour4
accent: Colour4
error: Colour4
warning: Colour4
success: Colour4
info: Colour4
selection: Colour4
selection_bg: Colour4
hover_bg: Colour4
highlight: Colour4
border: Colour4
border_light: Colour4
btn_bg: Colour4
btn_hover: Colour4
btn_pressed: Colour4
btn_border: Colour4
btn_primary: Colour4
btn_danger: Colour4
input_border: Colour4
input_focus: Colour4
placeholder: Colour4
scrollbar_hover: Colour4
scrollbar_track: Colour4
tab_bg: Colour4
tab_active: Colour4
tab_hover: Colour4
tab_text: Colour4
tab_active_text: Colour4
tree_bg: Colour4
tree_select: Colour4
tree_hover: Colour4
tree_arrow: Colour4
check_colour: Colour4
check_box: Colour4
slider_fill: Colour4
slider_handle: Colour4
dock_title_bg: Colour4
dock_title_text: Colour4
popup_bg: Colour4
popup_hover: Colour4
popup_separator: Colour4
divider: Colour4
divider_hover: Colour4
current_line: Colour4
bracket_match: Colour4
bracket_mismatch: Colour4
line_number: Colour4
gutter_bg: Colour4
syntax: SyntaxTheme
viewport_bg: Colour4
gizmo_x: Colour4
gizmo_y: Colour4
gizmo_z: Colour4
selection_outline: Colour4
grid_major: Colour4
grid_minor: Colour4
minimap_text: Colour4
minimap_keyword: Colour4
minimap_string: Colour4
minimap_comment: Colour4
autocomplete_bg: Colour4
autocomplete_selected: Colour4
autocomplete_border: Colour4
autocomplete_hover: Colour4
autocomplete_dim: Colour4
autocomplete_kind: Colour4
scrollbar_bg: Colour4
scrollbar_fg: Colour4
header_h: float
row_h: float
tab_h: float
font_size: float
ui_scale: float
scrollbar_width: float
dock_title_h: float
btn_style_normal: StyleBox
btn_style_hover: StyleBox
btn_style_pressed: StyleBox
btn_style_disabled: StyleBox
btn_style_focused: StyleBox
panel_style: StyleBox
input_style_normal: StyleBox
input_style_focused: StyleBox
input_style_disabled: StyleBox
tab_style_normal: StyleBox
tab_style_active: StyleBox
tab_style_hover: StyleBox
popup_style: StyleBox
popup_style_hover: StyleBox
dock_title_style: StyleBox
def __init__(
self,
palette: dict[str, Any] | None = None,
stylebox_cfg: StyleBoxConfig | None = None,
) -> None:
super().__init__()
self._palette = palette if palette is not None else _DARK_PALETTE
for key, value in self._palette.items():
setattr(self, key, value)
for key, value in _DEFAULT_SIZES.items():
setattr(self, key, value)
self._init_styleboxes(stylebox_cfg if stylebox_cfg is not None else _DARK_STYLEBOX)
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(self, cfg: StyleBoxConfig) -> None:
"""Build all StyleBox attributes from the colour palette and the supplied strategy."""
L, D = self._lighten, self._darken
bb, bh, bp = self.btn_bg, self.btn_hover, self.btn_pressed
EL, ED = cfg.emboss_light, cfg.emboss_dark
ga = cfg.gradient_amount
hover_b = self.accent if cfg.hover_uses_accent else self.btn_border
pressed_b = self.accent if cfg.pressed_uses_accent else self.btn_border
if cfg.button_style == "embossed_classic":
for state, bg in (("normal", bb), ("hover", bh)):
setattr(self, f"btn_style_{state}", StyleBox(
bg_colour=bg, border_width=1.0, content_margin=2.0,
border_top=L(bg, EL), border_left=L(bg, EL),
border_bottom=D(bg, ED), border_right=D(bg, ED),
))
self.btn_style_pressed = StyleBox(
bg_colour=bp, border_width=1.0, content_margin=2.0,
border_top=D(bp, ED), border_left=D(bp, ED),
border_bottom=L(bp, EL), border_right=L(bp, EL),
)
elif cfg.button_style == "embossed_subtle":
self.btn_style_normal = StyleBox(
bg_colour=bb, border_width=1.0, content_margin=2.0,
border_top=L(bb, EL), border_left=L(bb, EL),
border_bottom=D(bb, ED), border_right=D(bb, ED),
)
self.btn_style_hover = StyleBox(
bg_colour=bh, border_width=1.0, content_margin=2.0, border_colour=hover_b,
)
self.btn_style_pressed = StyleBox(
bg_colour=bp, border_width=1.0, content_margin=2.0, border_colour=pressed_b,
)
elif cfg.button_style == "gradient":
self.btn_style_normal = StyleBox(
bg_colour=bb, border_width=1.0, content_margin=2.0,
bg_gradient=(L(bb, ga), D(bb, ga)), border_colour=self.btn_border,
)
self.btn_style_hover = StyleBox(
bg_colour=bh, border_width=1.0, content_margin=2.0,
bg_gradient=(L(bh, ga), D(bh, ga)), border_colour=hover_b,
)
self.btn_style_pressed = StyleBox(
bg_colour=bp, border_width=1.0, content_margin=2.0, border_colour=pressed_b,
)
else: # "flat"
self.btn_style_normal = StyleBox(
bg_colour=bb, border_width=1.0, content_margin=2.0, border_colour=self.btn_border,
)
self.btn_style_hover = StyleBox(
bg_colour=bh, border_width=1.0, content_margin=2.0, border_colour=hover_b,
)
self.btn_style_pressed = StyleBox(
bg_colour=bp, border_width=1.0, content_margin=2.0, border_colour=pressed_b,
)
self.btn_style_disabled = StyleBox(
bg_colour=D(bb, cfg.disabled_bg_darken), border_width=1.0, content_margin=2.0,
border_colour=D(self.btn_border, cfg.disabled_border_darken),
)
self.btn_style_focused = StyleBox(
bg_colour=bb, border_width=1.0, content_margin=2.0, border_colour=self.accent,
)
fcm = cfg.flat_content_margin
self.panel_style = StyleBox(
bg_colour=self.panel_bg, border_width=1.0, content_margin=fcm,
border_colour=self.border,
)
bi = self.bg_input
self.input_style_normal = StyleBox(
bg_colour=bi, border_width=1.0, content_margin=4.0, border_colour=self.input_border,
)
self.input_style_focused = StyleBox(
bg_colour=bi, border_width=1.0, content_margin=4.0, border_colour=self.accent,
)
self.input_style_disabled = StyleBox(
bg_colour=D(bi, cfg.input_disabled_darken), border_width=1.0, content_margin=4.0,
border_colour=D(self.input_border, cfg.disabled_border_darken),
)
self.tab_style_normal = StyleBox(bg_colour=self.tab_bg, border_width=0.0, content_margin=fcm)
self.tab_style_active = StyleBox(bg_colour=self.tab_active, border_width=0.0, content_margin=fcm)
self.tab_style_hover = StyleBox(bg_colour=self.tab_hover, border_width=0.0, content_margin=fcm)
self.popup_style = StyleBox(
bg_colour=self.popup_bg, border_width=1.0, content_margin=fcm,
border_colour=getattr(self, cfg.popup_border_attr),
)
self.popup_style_hover = StyleBox(bg_colour=self.popup_hover, border_width=0.0, content_margin=fcm)
self.dock_title_style = StyleBox(bg_colour=self.dock_title_bg, border_width=0.0, content_margin=fcm)
# -- Dict synchronisation ------------------------------------------------
def _sync_dicts(self) -> None:
"""Populate ``self.colours`` and ``self.sizes`` from the palette and sizes."""
self.colours.update({k: v for k, v in self._palette.items() if k != "syntax"})
self.sizes.update(_DEFAULT_SIZES)
a = self.accent
# Legacy / computed entries kept for API compatibility
self.colours["text_disabled"] = self.text_dim
self.colours["accent_hover"] = (a[0] + 0.1, a[1] + 0.1, min(a[2] + 0.04, 1.0), 1.0)
self.colours["accent_pressed"] = (a[0] - 0.06, a[1] - 0.06, a[2] - 0.06, 1.0)
self.colours["focus"] = a
# -- 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."""
return cls(palette=_ABYSS_PALETTE, stylebox_cfg=_ABYSS_STYLEBOX)
[docs]
@classmethod
def midnight(cls) -> AppTheme:
"""Midnight — near-black with a subtle warm-green tint. OLED-friendly."""
return cls(palette=_MIDNIGHT_PALETTE, stylebox_cfg=_MIDNIGHT_STYLEBOX)
[docs]
@classmethod
def light(cls) -> AppTheme:
"""Light theme with bright backgrounds and gradient buttons."""
return cls(palette=_LIGHT_PALETTE, stylebox_cfg=_LIGHT_STYLEBOX)
[docs]
@classmethod
def monokai(cls) -> AppTheme:
"""Monokai-inspired theme."""
return cls(palette=_MONOKAI_PALETTE, stylebox_cfg=_MONOKAI_STYLEBOX)
[docs]
@classmethod
def solarised_dark(cls) -> AppTheme:
"""Solarised Dark — Ethan Schoonover's warm-tinted dark palette."""
return cls(palette=_SOLARISED_DARK_PALETTE, stylebox_cfg=_SOLARISED_DARK_STYLEBOX)
[docs]
@classmethod
def nord(cls) -> AppTheme:
"""Nord — Arctic, north-bluish palette by Arctic Ice Studio."""
return cls(palette=_NORD_PALETTE, stylebox_cfg=_NORD_STYLEBOX)
# ---------------------------------------------------------------------------
# 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."""
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.
"""
return multiple * _current_theme.font_size