Add full application source
This commit is contained in:
265
src/gui/theme.py
Normal file
265
src/gui/theme.py
Normal file
@@ -0,0 +1,265 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user