""" 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( "", lambda _: entry_widget.configure(border_color=CyberpunkTheme.PRIMARY), ) entry_widget.bind( "", 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("", lambda _: entry.configure(border_color=CyberpunkTheme.PRIMARY)) entry.bind("", 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