Files
GOD-CWAK/src/gui/theme.py
2026-05-22 12:47:24 -07:00

266 lines
11 KiB
Python

"""
Cyberpunk theme — colors, fonts, and reusable widget factories.
"""
import customtkinter as ctk
from config import THEME
class CyberpunkTheme:
"""Applies the cyberpunk neon theme to the entire application."""
# Color palette
BG_DARK = THEME["bg_dark"]
BG_MEDIUM = THEME["bg_medium"]
BG_LIGHT = THEME["bg_light"]
PRIMARY = THEME["primary"]
SECONDARY = THEME["secondary"]
TERTIARY = THEME["tertiary"]
TEXT_PRIMARY = THEME["text_primary"]
TEXT_SECONDARY = THEME["text_secondary"]
TEXT_DIM = THEME["text_dim"]
SUCCESS = THEME["success"]
WARNING = THEME["warning"]
ERROR = THEME["error"]
BORDER = THEME["border"]
@classmethod
def _set(cls, widget: str, **props):
"""Set theme properties only when the widget key exists in customtkinter."""
theme = ctk.ThemeManager.theme
if widget not in theme:
return
for key, value in props.items():
if key in theme[widget]:
theme[widget][key] = value
@classmethod
def apply(cls):
"""Apply the cyberpunk theme globally to customtkinter."""
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("dark-blue")
# ── Frames ──────────────────────────────────────────────
cls._set("CTkFrame",
fg_color=[cls.BG_MEDIUM, cls.BG_MEDIUM],
border_color=[cls.BORDER, cls.BORDER],
top_fg_color=[cls.BG_DARK, cls.BG_DARK])
# ── Buttons ─────────────────────────────────────────────
cls._set("CTkButton",
fg_color=[cls.TERTIARY, cls.TERTIARY],
hover_color=[cls.PRIMARY, cls.PRIMARY],
text_color=[cls.TEXT_PRIMARY, cls.TEXT_PRIMARY],
border_color=[cls.PRIMARY, cls.PRIMARY])
# ── Entries ─────────────────────────────────────────────
cls._set("CTkEntry",
fg_color=[cls.BG_DARK, cls.BG_DARK],
text_color=[cls.TEXT_PRIMARY, cls.TEXT_PRIMARY],
border_color=[cls.BORDER, cls.BORDER],
placeholder_text_color=[cls.TEXT_DIM, cls.TEXT_DIM])
# ── Labels ──────────────────────────────────────────────
cls._set("CTkLabel",
text_color=[cls.TEXT_PRIMARY, cls.TEXT_PRIMARY],
fg_color=["transparent", "transparent"])
# ── Checkboxes (customtkinter uses CTkCheckBox, not CTkCheckbox) ──
cls._set("CTkCheckBox",
fg_color=[cls.TERTIARY, cls.TERTIARY],
hover_color=[cls.PRIMARY, cls.PRIMARY],
text_color=[cls.TEXT_PRIMARY, cls.TEXT_PRIMARY],
border_color=[cls.BORDER, cls.BORDER],
checkmark_color=[cls.BG_DARK, cls.BG_DARK])
# ── Progress bars ────────────────────────────────────────
cls._set("CTkProgressBar",
fg_color=[cls.BG_DARK, cls.BG_DARK],
progress_color=[cls.PRIMARY, cls.PRIMARY],
border_color=[cls.BORDER, cls.BORDER])
# ── Sliders ──────────────────────────────────────────────
cls._set("CTkSlider",
fg_color=[cls.BG_DARK, cls.BG_DARK],
progress_color=[cls.PRIMARY, cls.PRIMARY],
button_color=[cls.PRIMARY, cls.PRIMARY],
button_hover_color=[cls.SECONDARY, cls.SECONDARY])
# ── Option menus ─────────────────────────────────────────
cls._set("CTkOptionMenu",
fg_color=[cls.BG_DARK, cls.BG_DARK],
text_color=[cls.TEXT_PRIMARY, cls.TEXT_PRIMARY],
dropdown_fg_color=[cls.BG_MEDIUM, cls.BG_MEDIUM],
dropdown_hover_color=[cls.TERTIARY, cls.TERTIARY],
dropdown_text_color=[cls.TEXT_PRIMARY, cls.TEXT_PRIMARY],
border_color=[cls.BORDER, cls.BORDER])
# ── Scrollbars ───────────────────────────────────────────
cls._set("CTkScrollbar",
fg_color=[cls.BG_DARK, cls.BG_DARK],
button_color=[cls.TERTIARY, cls.TERTIARY],
button_hover_color=[cls.PRIMARY, cls.PRIMARY])
# ── Tab bar (CTkTabview uses CTkSegmentedButton internally) ──
cls._set("CTkSegmentedButton",
fg_color=[cls.BG_DARK, cls.BG_DARK],
selected_color=[cls.TERTIARY, cls.TERTIARY],
selected_hover_color=[cls.PRIMARY, cls.PRIMARY],
unselected_color=[cls.BG_DARK, cls.BG_DARK],
unselected_hover_color=[cls.BG_LIGHT, cls.BG_LIGHT],
text_color=[cls.TEXT_PRIMARY, cls.TEXT_PRIMARY])
# ── Textboxes ────────────────────────────────────────────
cls._set("CTkTextbox",
fg_color=[cls.BG_DARK, cls.BG_DARK],
text_color=[cls.TEXT_PRIMARY, cls.TEXT_PRIMARY],
border_color=[cls.BORDER, cls.BORDER])
# ── Scrollable frames ────────────────────────────────────
cls._set("CTkScrollableFrame",
fg_color=[cls.BG_MEDIUM, cls.BG_MEDIUM],
border_color=[cls.BORDER, cls.BORDER])
# ──────────────────────────────────────────────────────────────
# Widget factories
# ──────────────────────────────────────────────────────────────
@staticmethod
def create_neon_button(parent, text, command=None, color=None, **kwargs):
"""Neon button with colored border glow."""
fg = color or CyberpunkTheme.TERTIARY
# Hover brightens toward PRIMARY cyan
hover = CyberpunkTheme.PRIMARY if fg != CyberpunkTheme.PRIMARY else CyberpunkTheme.SECONDARY
font = kwargs.pop("font", ("Consolas", 13, "bold"))
btn = ctk.CTkButton(
parent,
text=text,
command=command,
fg_color=fg,
hover_color=hover,
border_width=1,
border_color=CyberpunkTheme.PRIMARY,
text_color=CyberpunkTheme.TEXT_PRIMARY,
font=font,
corner_radius=4,
**kwargs,
)
return btn
@staticmethod
def apply_focus_glow(entry_widget):
"""Add cyan glow on focus to any CTkEntry widget."""
entry_widget.bind(
"<FocusIn>",
lambda _: entry_widget.configure(border_color=CyberpunkTheme.PRIMARY),
)
entry_widget.bind(
"<FocusOut>",
lambda _: entry_widget.configure(border_color=CyberpunkTheme.BORDER),
)
return entry_widget
@staticmethod
def create_neon_entry(parent, placeholder="", width=200, **kwargs):
"""Entry with neon border that brightens on focus."""
entry = ctk.CTkEntry(
parent,
placeholder_text=placeholder,
width=width,
fg_color=CyberpunkTheme.BG_DARK,
border_color=CyberpunkTheme.BORDER,
text_color=CyberpunkTheme.TEXT_PRIMARY,
placeholder_text_color=CyberpunkTheme.TEXT_DIM,
font=("Consolas", 12),
corner_radius=4,
**kwargs,
)
# Subtle focus glow: swap border color on focus/unfocus
entry.bind("<FocusIn>", lambda _: entry.configure(border_color=CyberpunkTheme.PRIMARY))
entry.bind("<FocusOut>", lambda _: entry.configure(border_color=CyberpunkTheme.BORDER))
return entry
@staticmethod
def create_neon_label(parent, text, font_size=12, color=None, **kwargs):
return ctk.CTkLabel(
parent,
text=text,
text_color=color or CyberpunkTheme.TEXT_PRIMARY,
font=("Consolas", font_size),
**kwargs,
)
@staticmethod
def create_section_header(parent, text):
"""
Sleek section header: full-width rule with a neon left accent and
uppercase monospace label.
"""
outer = ctk.CTkFrame(parent, fg_color="transparent", height=34)
outer.pack(fill="x", pady=(14, 4))
outer.pack_propagate(False)
# Neon left accent bar
accent = ctk.CTkFrame(outer, fg_color=CyberpunkTheme.PRIMARY, width=3, corner_radius=0)
accent.pack(side="left", fill="y", padx=(0, 10))
label = ctk.CTkLabel(
outer,
text=text.upper(),
text_color=CyberpunkTheme.PRIMARY,
font=("Consolas", 11, "bold"),
)
label.pack(side="left", anchor="center")
# Trailing dim rule that fills the rest of the width
rule = ctk.CTkFrame(outer, fg_color=CyberpunkTheme.BORDER, height=1)
rule.pack(side="left", fill="x", expand=True, padx=(10, 0), pady=(1, 0))
return outer
@staticmethod
def create_status_bar(parent):
"""
Bottom status bar: dark strip with a thin colored top-border for depth.
"""
wrapper = ctk.CTkFrame(parent, fg_color=CyberpunkTheme.BG_DARK, height=30)
wrapper.pack(side="bottom", fill="x")
wrapper.pack_propagate(False)
# Thin top accent line
top_rule = ctk.CTkFrame(wrapper, fg_color=CyberpunkTheme.BORDER, height=1)
top_rule.pack(fill="x", side="top")
# Blinking status dot + text
dot = ctk.CTkLabel(
wrapper,
text="",
text_color=CyberpunkTheme.PRIMARY,
font=("Consolas", 10),
)
dot.pack(side="left", padx=(8, 2))
status_label = ctk.CTkLabel(
wrapper,
text="SYSTEM READY",
text_color=CyberpunkTheme.TEXT_DIM,
font=("Consolas", 10),
)
status_label.pack(side="left")
# Animate the dot — alternate PRIMARY ↔ dim every 900ms
def _blink():
try:
current = dot.cget("text_color")
next_color = (
CyberpunkTheme.TEXT_DIM
if current == CyberpunkTheme.PRIMARY
else CyberpunkTheme.PRIMARY
)
dot.configure(text_color=next_color)
wrapper.after(900, _blink)
except Exception:
pass
wrapper.after(900, _blink)
return wrapper, status_label