"""bfos/ui_theme.py -- design tokens for the device console. The values in this file MIRROR ``web/static/tokens.css``, which is the single source of truth for both operator surfaces (the device console and the central panel). Nothing here invents a colour: a colour means a device state, a severity, or the action accent, and a fifth hue would be a bug (docs/DESIGN.md, "Rules"). The mirror is not a comment-level promise -- ``TOKENS`` carries the CSS custom-property name of every value, and ``tests/test_bfos_gui_static.py`` parses ``tokens.css`` and compares the two files, so a token changed on one surface and not the other fails the suite instead of drifting. Why light, not dark ------------------- The console runs in workshops, dealer service areas and customer sites: bright overhead light, daylight from a roller door, a 14" laptop panel. A near-black console loses its own body text there and its state colours collapse into equal-brightness smudges. So the ground is light (``BG_APP``), the panels are white (``BG_PANEL``), the one dark region is the header (``BG_HEADER``), and saturation is spent only where it carries meaning. Faces ----- No webfont, and no font download: the fleet installs offline, so the UI face is whatever the platform ships and the data face is monospace (ids, counts, exit codes, timestamps line up in a column). ``apply_theme`` picks the first family the platform actually has, from a candidate list -- never a missing font name, which is a silently different layout. Sizes and the 4px rhythm ------------------------ Type sizes and space steps are kept in the token's own unit (CSS px) so the mirror is exact. Tk font sizes are points, so ``point_size`` converts with the 96 dpi relation the tokens assume; padding and row heights are already pixels in Tk, so they are used as ``SP_*`` values directly. Usage ----- theme = ui_theme.apply_theme(root) # once, before any widget is built ttk.Label(parent, style=theme.panel_label) """ from __future__ import annotations from typing import Dict, Mapping, Sequence try: # pragma: no cover - environment dependent from tkinter import ttk HAVE_TKINTER = True except Exception: # pragma: no cover - environment dependent ttk = None # type: ignore[assignment] HAVE_TKINTER = False # --------------------------------------------------------------------------- # Ground and structure -- tokens.css: "Ground and structure" # --------------------------------------------------------------------------- BG_APP = "#f6f7f8" # window ground BG_PANEL = "#ffffff" # raised surfaces, cards, table body BG_SUNKEN = "#eef0f2" # wells: log output, code, inactive rows BG_HEADER = "#1d2530" # app header; the one dark region BORDER = "#d5d9de" # hairline between regions BORDER_STRONG = "#b6bcc4" # input borders, table rules # --------------------------------------------------------------------------- # Text # --------------------------------------------------------------------------- TEXT = "#1a1f26" # body TEXT_MUTED = "#5c6570" # secondary lines, labels TEXT_FAINT = "#8b939c" # timestamps, hints, placeholders TEXT_ON_DARK = "#e8ebee" # header text TEXT_ON_ACCENT = "#ffffff" # --------------------------------------------------------------------------- # Action -- one accent: the action the operator came to take, and the # selected row. Nothing else. # --------------------------------------------------------------------------- ACCENT = "#1d4ed8" ACCENT_HOVER = "#1a43bd" ACCENT_ACTIVE = "#16389e" ACCENT_SOFT = "#e8eeff" # selected row, focus ring fill FOCUS_RING = "#1d4ed8" # --------------------------------------------------------------------------- # State severities -- four, each with a strong form for text and a soft form # for fills, so a badge is built from one palette without inventing a second. # --------------------------------------------------------------------------- OK = "#15803d" OK_SOFT = "#e6f4ea" WARN = "#b45309" WARN_SOFT = "#fdf3e3" DANGER = "#b91c1c" DANGER_SOFT = "#fdecec" INFO = "#1d4ed8" INFO_SOFT = "#e8eeff" IDLE = "#5c6570" # not yet acted on: not a failure IDLE_SOFT = "#eef0f2" # --------------------------------------------------------------------------- # Device state machine -- PROVISIONED_OFFLINE -> ENROLLED -> READY, plus the # failure and the absence that sit outside the sequence. Named so no surface # repaints a stage by accident. # # PROVISIONED is deliberately NOT the action accent (tokens.css states the # reason): the accent means "the action you came to take" and "the selected # row", so a KURULDU badge built from it made a state and a selection the same # blue. It is a slate of its own, one step along the path and distinct from the # neutral --idle. # --------------------------------------------------------------------------- STATE_PROVISIONED = "#475569" STATE_ENROLLED = WARN STATE_READY = OK STATE_FAILED = DANGER STATE_ABSENT = IDLE # --------------------------------------------------------------------------- # Type (px, as the tokens declare them) # --------------------------------------------------------------------------- SIZE_XS = 12 SIZE_SM = 13 SIZE_MD = 15 SIZE_LG = 18 SIZE_XL = 22 SIZE_2XL = 30 LEADING_TIGHT = 1.25 LEADING_NORMAL = 1.5 # --------------------------------------------------------------------------- # Space -- a 4px rhythm, named so a component never hand-picks 7px # --------------------------------------------------------------------------- SP_1 = 4 SP_2 = 8 SP_3 = 12 SP_4 = 16 SP_5 = 24 SP_6 = 32 SP_7 = 48 RADIUS = 6 RADIUS_LG = 10 # --------------------------------------------------------------------------- # Metrics # --------------------------------------------------------------------------- ROW_H = 44 # fleet table row: comfortable at arm's length CONTROL_H = 36 # buttons and inputs share one height HEADER_H = 56 SIDEBAR_W = 220 # Elevation is a token with no Tk equivalent (Tk has no box-shadow); the names # are kept so the two token sets stay comparable and the absence is explicit. SHADOW_SM = "0 1px 2px rgba(16, 24, 40, .06)" SHADOW_MD = "0 4px 12px rgba(16, 24, 40, .10)" # --------------------------------------------------------------------------- # Fonts -- candidates in preference order, resolved against the platform # --------------------------------------------------------------------------- FONT_UI_CANDIDATES: Sequence[str] = ( "Noto Sans", "DejaVu Sans", "Ubuntu", "Liberation Sans", "Segoe UI", "Helvetica", ) FONT_DATA_CANDIDATES: Sequence[str] = ( "DejaVu Sans Mono", "Liberation Mono", "Noto Sans Mono", "Consolas", "Menlo", "Courier New", ) FONT_UI_FALLBACK = "TkDefaultFont" FONT_DATA_FALLBACK = "TkFixedFont" #: CSS px -> Tk points, at the 96 dpi the tokens assume. Table, not a formula: #: rounding is per size so the console keeps the token's steps instead of #: collapsing two of them onto the same point size. POINT_SIZES: Mapping[int, int] = { SIZE_XS: 9, SIZE_SM: 10, SIZE_MD: 11, SIZE_LG: 14, SIZE_XL: 16, SIZE_2XL: 22, } # --------------------------------------------------------------------------- # The mirror table -- CSS custom property -> value. Kept as data so the parity # test reads the same list the code uses. # --------------------------------------------------------------------------- TOKENS: Mapping[str, str] = { "--bg-app": BG_APP, "--bg-panel": BG_PANEL, "--bg-sunken": BG_SUNKEN, "--bg-header": BG_HEADER, "--border": BORDER, "--border-strong": BORDER_STRONG, "--text": TEXT, "--text-muted": TEXT_MUTED, "--text-faint": TEXT_FAINT, "--text-on-dark": TEXT_ON_DARK, "--text-on-accent": TEXT_ON_ACCENT, "--accent": ACCENT, "--accent-hover": ACCENT_HOVER, "--accent-active": ACCENT_ACTIVE, "--accent-soft": ACCENT_SOFT, "--focus-ring": FOCUS_RING, "--ok": OK, "--ok-soft": OK_SOFT, "--warn": WARN, "--warn-soft": WARN_SOFT, "--danger": DANGER, "--danger-soft": DANGER_SOFT, "--info": INFO, "--info-soft": INFO_SOFT, "--idle": IDLE, "--idle-soft": IDLE_SOFT, "--state-provisioned": STATE_PROVISIONED, "--state-enrolled": STATE_ENROLLED, "--state-ready": STATE_READY, "--state-failed": STATE_FAILED, "--state-absent": STATE_ABSENT, } #: Space tokens, mirrored by name (the parity test compares these too). SPACE_TOKENS: Mapping[str, int] = { "--sp-1": SP_1, "--sp-2": SP_2, "--sp-3": SP_3, "--sp-4": SP_4, "--sp-5": SP_5, "--sp-6": SP_6, "--sp-7": SP_7, } #: Type-size tokens, mirrored by name. SIZE_TOKENS: Mapping[str, int] = { "--size-xs": SIZE_XS, "--size-sm": SIZE_SM, "--size-md": SIZE_MD, "--size-lg": SIZE_LG, "--size-xl": SIZE_XL, "--size-2xl": SIZE_2XL, } #: Metric tokens, mirrored by name. METRIC_TOKENS: Mapping[str, int] = { "--row-h": ROW_H, "--control-h": CONTROL_H, "--header-h": HEADER_H, "--sidebar-w": SIDEBAR_W, } #: Every mirrored value, by CSS token name (used by the parity test). #: Lengths are stored WITHOUT their unit: the unit is the token's (px at #: 96 dpi), and the console passes the number straight to Tk padding, which is #: already in pixels. The parity test strips the CSS unit to compare. ALL_TOKENS: Mapping[str, str] = { **TOKENS, **{name: str(value) for name, value in SPACE_TOKENS.items()}, **{name: str(value) for name, value in SIZE_TOKENS.items()}, **{name: str(value) for name, value in METRIC_TOKENS.items()}, } # --------------------------------------------------------------------------- # Severity vocabulary -- the fill/text pair a badge or a row is built from # --------------------------------------------------------------------------- SEVERITIES: Sequence[str] = ("ok", "warn", "danger", "info", "idle") SEVERITY_FILL: Mapping[str, str] = { "ok": OK_SOFT, "warn": WARN_SOFT, "danger": DANGER_SOFT, "info": INFO_SOFT, "idle": IDLE_SOFT, } SEVERITY_TEXT: Mapping[str, str] = { "ok": OK, "warn": WARN, "danger": DANGER, "info": INFO, "idle": IDLE, } #: State vocabulary, exactly the words both surfaces use (docs/DESIGN.md). STATE_WORDS: Mapping[str, str] = { "provisioned": "KURULDU", "enrolled": "KAYITLI", "ready": "HAZIR", "failed": "HATA", "absent": "YOK", } STATE_FILL: Mapping[str, str] = { # The tokens give no soft form for provisioned; the console borrows the # sunken well fill rather than inventing a hue, so a KURULDU badge cannot be # mistaken for the accent-soft selection (see tokens.css, device state). "provisioned": BG_SUNKEN, "enrolled": WARN_SOFT, "ready": OK_SOFT, "failed": DANGER_SOFT, "absent": IDLE_SOFT, } STATE_TEXT_COLOUR: Mapping[str, str] = { "provisioned": STATE_PROVISIONED, "enrolled": STATE_ENROLLED, "ready": STATE_READY, "failed": STATE_FAILED, "absent": STATE_ABSENT, } #: Privilege is a state, not a log line (docs/DESIGN.md, "Privilege"). PRIVILEGE_STYLE: Mapping[bool, str] = {True: "ok", False: "warn"} #: ttk style names, resolved once by apply_theme and read by the view. _FAMILIES: Dict[str, str] = {} # --------------------------------------------------------------------------- # Small helpers # --------------------------------------------------------------------------- def point_size(px: int) -> int: """Token px -> Tk point size (see POINT_SIZES).""" return POINT_SIZES.get(px, max(7, round(px * 0.75))) def ui_family() -> str: """The UI face resolved by :func:`apply_theme` (platform default before).""" return _FAMILIES.get("ui", FONT_UI_FALLBACK) def data_family() -> str: """The monospace face resolved by :func:`apply_theme`.""" return _FAMILIES.get("data", FONT_DATA_FALLBACK) def ui_font(size: int = SIZE_MD, weight: str = "normal") -> tuple: """A Tk font tuple in the platform UI face.""" if weight == "normal": return (ui_family(), point_size(size)) return (ui_family(), point_size(size), weight) def data_font(size: int = SIZE_SM, weight: str = "normal") -> tuple: """A Tk font tuple in the data (monospace) face.""" if weight == "normal": return (data_family(), point_size(size)) return (data_family(), point_size(size), weight) #: State key -> badge style, built from STATE_WORDS so the word and the colour #: can never be paired up wrongly on one surface. STATE_BADGE_STYLE: Mapping[str, str] = { key: f"State{key.capitalize()}.Badge.TLabel" for key in STATE_WORDS } #: Privilege level -> badge style ("Privilege" for full, "ReadOnly" else). PRIVILEGE_BADGE_STYLE: Mapping[str, str] = { "full": "Privilege.Badge.TLabel", "read_only": "ReadOnly.Badge.TLabel", } def severity_style(severity: str) -> str: """ttk style name for a severity pill (ok / warn / danger / info / idle).""" return f"{severity.capitalize()}.Status.TLabel" def _resolve_family(root, candidates: Sequence[str], fallback: str) -> str: """First family the platform actually has; the Tk default otherwise.""" try: from tkinter import font as tkfont available = {name.lower() for name in tkfont.families(root)} except Exception: # pragma: no cover - font database unavailable return fallback for name in candidates: if name.lower() in available: return name return fallback def _configure(style, name: str, **options) -> None: """Configure a ttk style, ignoring options the platform's theme rejects. ttk style elements differ between Tk builds (clam vs default, Tk 8.6 vs 9.x): a missing element option raises TclError. Losing one border colour is a cosmetic difference; crashing the console on startup is not. """ try: style.configure(name, **options) except Exception: for key, value in options.items(): try: style.configure(name, **{key: value}) except Exception: continue def _map(style, name: str, **options) -> None: """State map for a ttk style, same tolerance as :func:`_configure`.""" try: style.map(name, **options) except Exception: for key, value in options.items(): try: style.map(name, **{key: value}) except Exception: continue # --------------------------------------------------------------------------- # The one setup function, called once at startup # --------------------------------------------------------------------------- def apply_theme(root) -> Mapping[str, str]: """Install the token set on ``root``'s ttk styles; returns the style names. Idempotent and cheap: the GUI calls it once before building any widget, and a second call simply re-applies the same values (tests may build several roots in one process). """ if not HAVE_TKINTER: # pragma: no cover - environment dependent raise RuntimeError("tkinter yok: tema uygulanamaz") assert ttk is not None # narrowing for the type checker _FAMILIES["ui"] = _resolve_family(root, FONT_UI_CANDIDATES, FONT_UI_FALLBACK) _FAMILIES["data"] = _resolve_family(root, FONT_DATA_CANDIDATES, FONT_DATA_FALLBACK) try: root.configure(bg=BG_APP) except Exception: # pragma: no cover - non-Tk root pass style = ttk.Style(root) # clam honours background/bordercolor on the widget elements we restyle; # the 'default' theme on Linux ignores most of them, which is how a # console ends up grey whatever the tokens say. try: if "clam" in style.theme_names(): style.theme_use("clam") except Exception: # pragma: no cover - theme list unavailable pass base_font = ui_font(SIZE_MD) _configure( style, ".", background=BG_APP, foreground=TEXT, font=base_font, bordercolor=BORDER, lightcolor=BORDER, darkcolor=BORDER, troughcolor=BG_SUNKEN, focuscolor=FOCUS_RING, ) # -- frames: the ground, the raised panel, the well, the one dark band -- _configure(style, "TFrame", background=BG_APP, borderwidth=0) _configure(style, "Panel.TFrame", background=BG_PANEL, borderwidth=1, relief="solid", bordercolor=BORDER, lightcolor=BORDER, darkcolor=BORDER) _configure(style, "Sunken.TFrame", background=BG_SUNKEN, borderwidth=1, relief="solid", bordercolor=BORDER, lightcolor=BORDER, darkcolor=BORDER) _configure(style, "Header.TFrame", background=BG_HEADER, borderwidth=0) _configure(style, "Warn.TFrame", background=WARN_SOFT, borderwidth=1, relief="solid", bordercolor=WARN, lightcolor=WARN_SOFT, darkcolor=WARN_SOFT) _configure(style, "Info.TFrame", background=INFO_SOFT, borderwidth=1, relief="solid", bordercolor=INFO, lightcolor=INFO_SOFT, darkcolor=INFO_SOFT) _configure(style, "Accent.TFrame", background=ACCENT_SOFT, borderwidth=1, relief="solid", bordercolor=ACCENT, lightcolor=ACCENT_SOFT, darkcolor=ACCENT_SOFT) # -- labels ------------------------------------------------------------- _configure(style, "TLabel", background=BG_APP, foreground=TEXT, font=base_font) _configure(style, "Panel.TLabel", background=BG_PANEL, foreground=TEXT) _configure(style, "Sunken.TLabel", background=BG_SUNKEN, foreground=TEXT) _configure(style, "Muted.TLabel", background=BG_APP, foreground=TEXT_MUTED, font=ui_font(SIZE_SM)) _configure(style, "PanelMuted.TLabel", background=BG_PANEL, foreground=TEXT_MUTED, font=ui_font(SIZE_SM)) _configure(style, "Faint.TLabel", background=BG_APP, foreground=TEXT_FAINT, font=ui_font(SIZE_XS)) _configure(style, "PanelFaint.TLabel", background=BG_PANEL, foreground=TEXT_FAINT, font=ui_font(SIZE_XS)) _configure(style, "FieldLabel.TLabel", background=BG_PANEL, foreground=TEXT_MUTED, font=ui_font(SIZE_SM), anchor="e") _configure(style, "Section.TLabel", background=BG_APP, foreground=TEXT, font=ui_font(SIZE_LG, "bold")) _configure(style, "PanelTitle.TLabel", background=BG_PANEL, foreground=TEXT_MUTED, font=ui_font(SIZE_XS, "bold")) # The one loud figure per view. _configure(style, "Figure.TLabel", background=BG_APP, foreground=TEXT, font=ui_font(SIZE_2XL, "bold")) _configure(style, "Ok.TLabel", background=BG_PANEL, foreground=OK, font=ui_font(SIZE_SM)) _configure(style, "Warn.TLabel", background=WARN_SOFT, foreground=WARN, font=ui_font(SIZE_SM)) _configure(style, "InfoText.TLabel", background=INFO_SOFT, foreground=INFO, font=ui_font(SIZE_SM)) _configure(style, "Danger.TLabel", background=BG_PANEL, foreground=DANGER, font=ui_font(SIZE_SM)) _configure(style, "AppWarn.TLabel", background=BG_APP, foreground=WARN, font=ui_font(SIZE_SM, "bold")) _configure(style, "PanelOk.TLabel", background=BG_PANEL, foreground=OK, font=ui_font(SIZE_SM, "bold")) _configure(style, "PanelDanger.TLabel", background=BG_PANEL, foreground=DANGER, font=ui_font(SIZE_SM, "bold")) # Dialogs sit on the app ground, not on a panel. _configure(style, "Dialog.TLabel", background=BG_APP, foreground=TEXT, font=ui_font(SIZE_MD)) _configure(style, "DialogTitle.TLabel", background=BG_APP, foreground=TEXT, font=ui_font(SIZE_LG, "bold")) _configure(style, "DialogMuted.TLabel", background=BG_APP, foreground=TEXT_MUTED, font=ui_font(SIZE_SM)) # Header (dark) region. _configure(style, "HeaderTitle.TLabel", background=BG_HEADER, foreground=TEXT_ON_DARK, font=ui_font(SIZE_XL, "bold")) _configure(style, "HeaderMeta.TLabel", background=BG_HEADER, foreground=TEXT_FAINT, font=ui_font(SIZE_SM)) _configure(style, "HeaderTarget.TLabel", background=BG_HEADER, foreground=TEXT_ON_DARK, font=data_font(SIZE_SM)) _configure(style, "HeaderFigure.TLabel", background=BG_HEADER, foreground=TEXT_ON_DARK, font=data_font(SIZE_MD, "bold")) # -- badges: state word + colour, never colour alone ------------------- _configure(style, "Badge.TLabel", background=IDLE_SOFT, foreground=IDLE, font=ui_font(SIZE_SM, "bold"), padding=(SP_2, SP_1)) for key, word_style in (("provisioned", "Provisioned"), ("enrolled", "Enrolled"), ("ready", "Ready"), ("failed", "Failed"), ("absent", "Absent")): _configure( style, f"State{word_style}.Badge.TLabel", background=STATE_FILL[key], foreground=STATE_TEXT_COLOUR[key], font=ui_font(SIZE_SM, "bold"), padding=(SP_2, SP_1), ) _configure(style, "Privilege.Badge.TLabel", background=OK_SOFT, foreground=OK, font=ui_font(SIZE_SM, "bold"), padding=(SP_3, SP_1)) _configure(style, "ReadOnly.Badge.TLabel", background=WARN_SOFT, foreground=WARN, font=ui_font(SIZE_SM, "bold"), padding=(SP_3, SP_1)) _configure(style, "Type.Destructive.Badge.TLabel", background=DANGER_SOFT, foreground=DANGER, font=ui_font(SIZE_XS, "bold"), padding=(SP_2, SP_1)) _configure(style, "Type.ReadOnly.Badge.TLabel", background=IDLE_SOFT, foreground=IDLE, font=ui_font(SIZE_XS, "bold"), padding=(SP_2, SP_1)) _configure(style, "Open.Badge.TLabel", background=OK_SOFT, foreground=OK, font=ui_font(SIZE_XS, "bold"), padding=(SP_2, SP_1)) _configure(style, "Closed.Badge.TLabel", background=WARN_SOFT, foreground=WARN, font=ui_font(SIZE_XS, "bold"), padding=(SP_2, SP_1)) _configure(style, "Info.Badge.TLabel", background=INFO_SOFT, foreground=INFO, font=ui_font(SIZE_SM, "bold"), padding=(SP_3, SP_1)) _configure(style, "Neutral.Badge.TLabel", background=BG_SUNKEN, foreground=IDLE, font=ui_font(SIZE_XS, "bold"), padding=(SP_2, SP_1)) # -- status pill (footer): the console's own state --------------------- _configure(style, "Status.TLabel", background=IDLE_SOFT, foreground=IDLE, font=ui_font(SIZE_SM, "bold"), padding=(SP_3, SP_2)) for severity in SEVERITIES: _configure( style, f"{severity.capitalize()}.Status.TLabel", background=SEVERITY_FILL[severity], foreground=SEVERITY_TEXT[severity], font=ui_font(SIZE_SM, "bold"), padding=(SP_3, SP_2), ) # -- inputs: one height with the buttons --------------------------------- entry_opts = dict( fieldbackground=BG_PANEL, background=BG_PANEL, foreground=TEXT, bordercolor=BORDER_STRONG, lightcolor=BORDER_STRONG, darkcolor=BORDER_STRONG, insertcolor=TEXT, padding=(SP_2, SP_1), font=ui_font(SIZE_MD), ) _configure(style, "TEntry", **entry_opts) _map(style, "TEntry", bordercolor=[("focus", FOCUS_RING), ("disabled", BORDER)], foreground=[("disabled", TEXT_FAINT)], fieldbackground=[("disabled", BG_SUNKEN)]) _configure(style, "Invalid.TEntry", **entry_opts) _configure(style, "Invalid.TEntry", bordercolor=DANGER, lightcolor=DANGER, darkcolor=DANGER) _map(style, "Invalid.TEntry", bordercolor=[("focus", DANGER)]) _configure(style, "TCombobox", **entry_opts) _configure(style, "TCombobox", arrowcolor=TEXT_MUTED, selectbackground=ACCENT_SOFT, selectforeground=TEXT, padding=(SP_2, SP_1)) _map(style, "TCombobox", fieldbackground=[("readonly", BG_PANEL), ("disabled", BG_SUNKEN)], foreground=[("disabled", TEXT_FAINT)], bordercolor=[("focus", FOCUS_RING)]) _configure(style, "TCheckbutton", background=BG_APP, foreground=TEXT, font=ui_font(SIZE_SM), focuscolor=BG_APP, padding=(SP_1, SP_1)) _map(style, "TCheckbutton", background=[("active", BG_APP)], foreground=[("disabled", TEXT_FAINT)], indicatorcolor=[("selected", ACCENT), ("pressed", ACCENT_ACTIVE)]) _configure(style, "Panel.TCheckbutton", background=BG_PANEL, foreground=TEXT, font=ui_font(SIZE_SM), padding=(SP_1, SP_1)) _map(style, "Panel.TCheckbutton", background=[("active", BG_PANEL), ("disabled", BG_PANEL)], foreground=[("disabled", TEXT_FAINT)]) # -- buttons: hairline secondary, one accent primary, danger for # destructive work ------------------------------------------------ _configure(style, "TButton", background=BG_PANEL, foreground=TEXT, bordercolor=BORDER_STRONG, lightcolor=BG_PANEL, darkcolor=BG_PANEL, font=ui_font(SIZE_MD), padding=(SP_3, SP_2), relief="solid", focusthickness=1, focuscolor=FOCUS_RING, anchor="center") _map(style, "TButton", background=[("disabled", BG_SUNKEN), ("pressed", BG_SUNKEN), ("active", ACCENT_SOFT)], foreground=[("disabled", TEXT_FAINT), ("pressed", ACCENT_ACTIVE)], bordercolor=[("focus", FOCUS_RING)], lightcolor=[("active", ACCENT_SOFT), ("pressed", BG_SUNKEN)], darkcolor=[("active", ACCENT_SOFT), ("pressed", BG_SUNKEN)]) _configure(style, "Accent.TButton", background=ACCENT, foreground=TEXT_ON_ACCENT, bordercolor=ACCENT, lightcolor=ACCENT, darkcolor=ACCENT) _map(style, "Accent.TButton", background=[("disabled", IDLE_SOFT), ("pressed", ACCENT_ACTIVE), ("active", ACCENT_HOVER)], foreground=[("disabled", TEXT_FAINT)], bordercolor=[("focus", ACCENT_ACTIVE), ("active", ACCENT_HOVER)], lightcolor=[("active", ACCENT_HOVER), ("pressed", ACCENT_ACTIVE)], darkcolor=[("active", ACCENT_HOVER), ("pressed", ACCENT_ACTIVE)]) _configure(style, "Danger.TButton", background=DANGER, foreground=TEXT_ON_ACCENT, bordercolor=DANGER, lightcolor=DANGER, darkcolor=DANGER) _map(style, "Danger.TButton", background=[("disabled", IDLE_SOFT), ("pressed", DANGER_SOFT), ("active", DANGER_SOFT)], foreground=[("disabled", TEXT_FAINT), ("active", DANGER), ("pressed", DANGER)], bordercolor=[("focus", DANGER)], lightcolor=[("active", DANGER_SOFT), ("pressed", DANGER_SOFT)], darkcolor=[("active", DANGER_SOFT), ("pressed", DANGER_SOFT)]) # Simple mode: the three big buttons, one of them the action accent. _configure(style, "Big.TButton", font=ui_font(SIZE_LG), padding=(SP_5, SP_4), anchor="w") _configure(style, "Accent.Big.TButton", background=ACCENT, foreground=TEXT_ON_ACCENT, bordercolor=ACCENT, lightcolor=ACCENT, darkcolor=ACCENT) _map(style, "Accent.Big.TButton", background=[("disabled", IDLE_SOFT), ("pressed", ACCENT_ACTIVE), ("active", ACCENT_HOVER)], foreground=[("disabled", TEXT_FAINT)], lightcolor=[("active", ACCENT_HOVER), ("pressed", ACCENT_ACTIVE)], darkcolor=[("active", ACCENT_HOVER), ("pressed", ACCENT_ACTIVE)]) _configure(style, "Quiet.TButton", background=BG_SUNKEN, bordercolor=BORDER) # -- table: the fleet/operation list ------------------------------------ _configure(style, "Treeview", background=BG_PANEL, fieldbackground=BG_PANEL, foreground=TEXT, bordercolor=BORDER, lightcolor=BORDER, darkcolor=BORDER, font=ui_font(SIZE_SM), rowheight=ROW_H) _configure(style, "Treeview.Heading", background=BG_SUNKEN, foreground=TEXT_MUTED, font=ui_font(SIZE_XS, "bold"), relief="flat", padding=(SP_2, SP_2)) _map(style, "Treeview", background=[("selected", ACCENT_SOFT)], foreground=[("selected", TEXT)]) _map(style, "Treeview.Heading", background=[("active", BG_SUNKEN)]) # -- tabs, separators, scrollbars -------------------------------------- _configure(style, "TNotebook", background=BG_APP, bordercolor=BORDER, lightcolor=BORDER, darkcolor=BORDER, tabmargins=(0, 0, 0, 0)) _configure(style, "TNotebook.Tab", background=BG_SUNKEN, foreground=TEXT_MUTED, font=ui_font(SIZE_SM), padding=(SP_3, SP_2), bordercolor=BORDER) _map(style, "TNotebook.Tab", background=[("selected", BG_PANEL), ("active", BG_PANEL)], foreground=[("selected", TEXT), ("active", TEXT)], expand=[("selected", (0, 0, 0, 0))]) _configure(style, "TSeparator", background=BORDER) for orient in ("Vertical", "Horizontal"): _configure(style, f"{orient}.TScrollbar", background=BG_SUNKEN, troughcolor=BG_APP, bordercolor=BORDER, arrowcolor=TEXT_MUTED, lightcolor=BG_SUNKEN, darkcolor=BG_SUNKEN) _map(style, f"{orient}.TScrollbar", background=[("active", BORDER_STRONG)]) return { "frame": "TFrame", "panel": "Panel.TFrame", "sunken": "Sunken.TFrame", "header": "Header.TFrame", "warn_band": "Warn.TFrame", "info_band": "Info.TFrame", "label": "TLabel", "panel_label": "Panel.TLabel", "panel_title": "PanelTitle.TLabel", "muted": "Muted.TLabel", "panel_muted": "PanelMuted.TLabel", "faint": "Faint.TLabel", "panel_faint": "PanelFaint.TLabel", "field_label": "FieldLabel.TLabel", "section": "Section.TLabel", "figure": "Figure.TLabel", "ok_label": "Ok.TLabel", "warn_label": "Warn.TLabel", "info_label": "InfoText.TLabel", "danger_label": "Danger.TLabel", "app_warn": "AppWarn.TLabel", "panel_ok": "PanelOk.TLabel", "panel_danger": "PanelDanger.TLabel", "header_title": "HeaderTitle.TLabel", "header_meta": "HeaderMeta.TLabel", "header_target": "HeaderTarget.TLabel", "header_figure": "HeaderFigure.TLabel", "badge": "Badge.TLabel", "badge_info": "Info.Badge.TLabel", "badge_neutral": "Neutral.Badge.TLabel", "badge_destructive": "Type.Destructive.Badge.TLabel", "badge_readonly": "Type.ReadOnly.Badge.TLabel", "badge_open": "Open.Badge.TLabel", "badge_closed": "Closed.Badge.TLabel", "privilege": "Privilege.Badge.TLabel", "readonly": "ReadOnly.Badge.TLabel", "status": "Status.TLabel", "button": "TButton", "accent_button": "Accent.TButton", "danger_button": "Danger.TButton", "big_button": "Big.TButton", "big_accent_button": "Accent.Big.TButton", "quiet_button": "Quiet.TButton", "entry": "TEntry", "invalid_entry": "Invalid.TEntry", "panel_check": "Panel.TCheckbutton", "check": "TCheckbutton", "dialog": "Dialog.TLabel", "dialog_title": "DialogTitle.TLabel", "dialog_muted": "DialogMuted.TLabel", } def DANGER_ACTIVE_SAFE() -> str: # pragma: no cover - trivial """The danger colour used for a focused destructive control. A named function (rather than a bare literal) so the fifth-hue rule stays visible: focus on a dangerous action reuses the danger token. """ return DANGER