266 lines
11 KiB
Python
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
|