Source code for simvx.core.ui.theme

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