# theme.py """ Theming support for manual_slop GUI. Palettes -------- Each palette is a dict mapping semantic names to (R,G,B) or (R,G,B,A) tuples. The names correspond to dpg theme colour / style constants. Font handling ------------- Call apply_font(path, size) to load a TTF and bind it as the global default. Call set_scale(factor) to set the global font scale (DPI scaling). Usage ----- import theme theme.apply("10x") # apply a named palette theme.apply_font("C:/Windows/Fonts/CascadiaCode.ttf", 15) theme.set_scale(1.25) """ import dearpygui.dearpygui as dpg from pathlib import Path # ------------------------------------------------------------------ palettes # Colour key names match the DPG mvThemeCol_* constants (string lookup below). # Only keys that differ from DPG defaults need to be listed. _PALETTES: dict[str, dict] = { "DPG Default": {}, # empty = reset to DPG built-in defaults "10x Dark": { # Window / frame chrome "WindowBg": ( 34, 32, 28), "ChildBg": ( 30, 28, 24), "PopupBg": ( 35, 30, 20), "Border": ( 60, 55, 50), "BorderShadow": ( 0, 0, 0, 0), "FrameBg": ( 45, 42, 38), "FrameBgHovered": ( 60, 56, 50), "FrameBgActive": ( 75, 70, 62), # Title bars "TitleBg": ( 40, 35, 25), "TitleBgActive": ( 60, 45, 15), "TitleBgCollapsed": ( 30, 27, 20), # Menu bar "MenuBarBg": ( 35, 30, 20), # Scrollbar "ScrollbarBg": ( 30, 28, 24), "ScrollbarGrab": ( 80, 78, 72), "ScrollbarGrabHovered": (100, 100, 92), "ScrollbarGrabActive": (120, 118, 110), # Check marks / radio buttons "CheckMark": (194, 164, 74), # Sliders "SliderGrab": (126, 78, 14), "SliderGrabActive": (194, 140, 30), # Buttons "Button": ( 83, 76, 60), "ButtonHovered": (126, 78, 14), "ButtonActive": (115, 90, 70), # Headers (collapsing headers, selectables, listbox items) "Header": ( 83, 76, 60), "HeaderHovered": (126, 78, 14), "HeaderActive": (115, 90, 70), # Separator "Separator": ( 70, 65, 55), "SeparatorHovered": (126, 78, 14), "SeparatorActive": (194, 164, 74), # Resize grip "ResizeGrip": ( 60, 55, 44), "ResizeGripHovered": (126, 78, 14), "ResizeGripActive": (194, 164, 74), # Tab bar "Tab": ( 83, 83, 70), "TabHovered": (126, 77, 25), "TabActive": (126, 77, 25), "TabUnfocused": ( 60, 58, 50), "TabUnfocusedActive": ( 90, 80, 55), # Docking "DockingPreview": (126, 78, 14, 180), "DockingEmptyBg": ( 20, 20, 20), # Text "Text": (200, 200, 200), "TextDisabled": (130, 130, 120), # Input text cursor / selection "TextSelectedBg": ( 59, 86, 142, 180), # Plot / table lines "TableHeaderBg": ( 55, 50, 38), "TableBorderStrong": ( 70, 65, 55), "TableBorderLight": ( 50, 47, 42), "TableRowBg": ( 0, 0, 0, 0), "TableRowBgAlt": ( 40, 38, 34, 40), # Misc "NavHighlight": (126, 78, 14), "NavWindowingHighlight":(194, 164, 74, 180), "NavWindowingDimBg": ( 20, 20, 20, 80), "ModalWindowDimBg": ( 10, 10, 10, 100), }, "Nord Dark": { "WindowBg": ( 36, 41, 49), "ChildBg": ( 30, 34, 42), "PopupBg": ( 36, 41, 49), "Border": ( 59, 66, 82), "BorderShadow": ( 0, 0, 0, 0), "FrameBg": ( 46, 52, 64), "FrameBgHovered": ( 59, 66, 82), "FrameBgActive": ( 67, 76, 94), "TitleBg": ( 36, 41, 49), "TitleBgActive": ( 59, 66, 82), "TitleBgCollapsed": ( 30, 34, 42), "MenuBarBg": ( 46, 52, 64), "ScrollbarBg": ( 30, 34, 42), "ScrollbarGrab": ( 76, 86, 106), "ScrollbarGrabHovered": ( 94, 129, 172), "ScrollbarGrabActive": (129, 161, 193), "CheckMark": (136, 192, 208), "SliderGrab": ( 94, 129, 172), "SliderGrabActive": (129, 161, 193), "Button": ( 59, 66, 82), "ButtonHovered": ( 94, 129, 172), "ButtonActive": (129, 161, 193), "Header": ( 59, 66, 82), "HeaderHovered": ( 94, 129, 172), "HeaderActive": (129, 161, 193), "Separator": ( 59, 66, 82), "SeparatorHovered": ( 94, 129, 172), "SeparatorActive": (136, 192, 208), "ResizeGrip": ( 59, 66, 82), "ResizeGripHovered": ( 94, 129, 172), "ResizeGripActive": (136, 192, 208), "Tab": ( 46, 52, 64), "TabHovered": ( 94, 129, 172), "TabActive": ( 76, 86, 106), "TabUnfocused": ( 36, 41, 49), "TabUnfocusedActive": ( 59, 66, 82), "DockingPreview": ( 94, 129, 172, 180), "DockingEmptyBg": ( 20, 22, 28), "Text": (216, 222, 233), "TextDisabled": (116, 128, 150), "TextSelectedBg": ( 94, 129, 172, 180), "TableHeaderBg": ( 59, 66, 82), "TableBorderStrong": ( 76, 86, 106), "TableBorderLight": ( 59, 66, 82), "TableRowBg": ( 0, 0, 0, 0), "TableRowBgAlt": ( 46, 52, 64, 40), "NavHighlight": (136, 192, 208), "ModalWindowDimBg": ( 10, 12, 16, 100), }, "Monokai": { "WindowBg": ( 39, 40, 34), "ChildBg": ( 34, 35, 29), "PopupBg": ( 39, 40, 34), "Border": ( 60, 61, 52), "BorderShadow": ( 0, 0, 0, 0), "FrameBg": ( 50, 51, 44), "FrameBgHovered": ( 65, 67, 56), "FrameBgActive": ( 80, 82, 68), "TitleBg": ( 39, 40, 34), "TitleBgActive": ( 73, 72, 62), "TitleBgCollapsed": ( 30, 31, 26), "MenuBarBg": ( 50, 51, 44), "ScrollbarBg": ( 34, 35, 29), "ScrollbarGrab": ( 80, 80, 72), "ScrollbarGrabHovered": (102, 217, 39), "ScrollbarGrabActive": (166, 226, 46), "CheckMark": (166, 226, 46), "SliderGrab": (102, 217, 39), "SliderGrabActive": (166, 226, 46), "Button": ( 73, 72, 62), "ButtonHovered": (249, 38, 114), "ButtonActive": (198, 30, 92), "Header": ( 73, 72, 62), "HeaderHovered": (249, 38, 114), "HeaderActive": (198, 30, 92), "Separator": ( 60, 61, 52), "SeparatorHovered": (249, 38, 114), "SeparatorActive": (166, 226, 46), "ResizeGrip": ( 73, 72, 62), "ResizeGripHovered": (249, 38, 114), "ResizeGripActive": (166, 226, 46), "Tab": ( 73, 72, 62), "TabHovered": (249, 38, 114), "TabActive": (249, 38, 114), "TabUnfocused": ( 50, 51, 44), "TabUnfocusedActive": ( 90, 88, 76), "DockingPreview": (249, 38, 114, 180), "DockingEmptyBg": ( 20, 20, 18), "Text": (248, 248, 242), "TextDisabled": (117, 113, 94), "TextSelectedBg": (249, 38, 114, 150), "TableHeaderBg": ( 60, 61, 52), "TableBorderStrong": ( 73, 72, 62), "TableBorderLight": ( 55, 56, 48), "TableRowBg": ( 0, 0, 0, 0), "TableRowBgAlt": ( 50, 51, 44, 40), "NavHighlight": (166, 226, 46), "ModalWindowDimBg": ( 10, 10, 8, 100), }, } PALETTE_NAMES: list[str] = list(_PALETTES.keys()) # ------------------------------------------------------------------ colour key -> mvThemeCol_* mapping # Maps our friendly name -> dpg constant name _COL_MAP: dict[str, str] = { "Text": "mvThemeCol_Text", "TextDisabled": "mvThemeCol_TextDisabled", "WindowBg": "mvThemeCol_WindowBg", "ChildBg": "mvThemeCol_ChildBg", "PopupBg": "mvThemeCol_PopupBg", "Border": "mvThemeCol_Border", "BorderShadow": "mvThemeCol_BorderShadow", "FrameBg": "mvThemeCol_FrameBg", "FrameBgHovered": "mvThemeCol_FrameBgHovered", "FrameBgActive": "mvThemeCol_FrameBgActive", "TitleBg": "mvThemeCol_TitleBg", "TitleBgActive": "mvThemeCol_TitleBgActive", "TitleBgCollapsed": "mvThemeCol_TitleBgCollapsed", "MenuBarBg": "mvThemeCol_MenuBarBg", "ScrollbarBg": "mvThemeCol_ScrollbarBg", "ScrollbarGrab": "mvThemeCol_ScrollbarGrab", "ScrollbarGrabHovered": "mvThemeCol_ScrollbarGrabHovered", "ScrollbarGrabActive": "mvThemeCol_ScrollbarGrabActive", "CheckMark": "mvThemeCol_CheckMark", "SliderGrab": "mvThemeCol_SliderGrab", "SliderGrabActive": "mvThemeCol_SliderGrabActive", "Button": "mvThemeCol_Button", "ButtonHovered": "mvThemeCol_ButtonHovered", "ButtonActive": "mvThemeCol_ButtonActive", "Header": "mvThemeCol_Header", "HeaderHovered": "mvThemeCol_HeaderHovered", "HeaderActive": "mvThemeCol_HeaderActive", "Separator": "mvThemeCol_Separator", "SeparatorHovered": "mvThemeCol_SeparatorHovered", "SeparatorActive": "mvThemeCol_SeparatorActive", "ResizeGrip": "mvThemeCol_ResizeGrip", "ResizeGripHovered": "mvThemeCol_ResizeGripHovered", "ResizeGripActive": "mvThemeCol_ResizeGripActive", "Tab": "mvThemeCol_Tab", "TabHovered": "mvThemeCol_TabHovered", "TabActive": "mvThemeCol_TabActive", "TabUnfocused": "mvThemeCol_TabUnfocused", "TabUnfocusedActive": "mvThemeCol_TabUnfocusedActive", "DockingPreview": "mvThemeCol_DockingPreview", "DockingEmptyBg": "mvThemeCol_DockingEmptyBg", "TextSelectedBg": "mvThemeCol_TextSelectedBg", "TableHeaderBg": "mvThemeCol_TableHeaderBg", "TableBorderStrong": "mvThemeCol_TableBorderStrong", "TableBorderLight": "mvThemeCol_TableBorderLight", "TableRowBg": "mvThemeCol_TableRowBg", "TableRowBgAlt": "mvThemeCol_TableRowBgAlt", "NavHighlight": "mvThemeCol_NavHighlight", "NavWindowingHighlight": "mvThemeCol_NavWindowingHighlight", "NavWindowingDimBg": "mvThemeCol_NavWindowingDimBg", "ModalWindowDimBg": "mvThemeCol_ModalWindowDimBg", } # ------------------------------------------------------------------ state _current_theme_tag: str | None = None _current_font_tag: str | None = None _font_registry_tag: str | None = None _current_palette: str = "DPG Default" _current_font_path: str = "" _current_font_size: float = 14.0 _current_scale: float = 1.0 # ------------------------------------------------------------------ public API def get_palette_names() -> list[str]: return list(_PALETTES.keys()) def get_current_palette() -> str: return _current_palette def get_current_font_path() -> str: return _current_font_path def get_current_font_size() -> float: return _current_font_size def get_current_scale() -> float: return _current_scale def get_palette_colours(name: str) -> dict: """Return a copy of the colour dict for the named palette.""" return dict(_PALETTES.get(name, {})) def apply(palette_name: str, overrides: dict | None = None): """ Build a global DPG theme from the named palette plus optional per-colour overrides, and bind it as the default theme. overrides: {colour_key: (R,G,B) or (R,G,B,A)} — merged on top of palette. """ global _current_theme_tag, _current_palette _current_palette = palette_name colours = dict(_PALETTES.get(palette_name, {})) if overrides: colours.update(overrides) # Delete the old theme if one exists if _current_theme_tag is not None: try: dpg.delete_item(_current_theme_tag) except Exception: pass _current_theme_tag = None if palette_name == "DPG Default" and not overrides: # Bind an empty theme to reset to DPG defaults with dpg.theme() as t: with dpg.theme_component(dpg.mvAll): pass dpg.bind_theme(t) _current_theme_tag = t return with dpg.theme() as t: with dpg.theme_component(dpg.mvAll): for name, colour in colours.items(): const_name = _COL_MAP.get(name) if const_name is None: continue const = getattr(dpg, const_name, None) if const is None: continue # Ensure 4-tuple if len(colour) == 3: colour = (*colour, 255) dpg.add_theme_color(const, colour) dpg.bind_theme(t) _current_theme_tag = t def apply_font(font_path: str, size: float = 14.0): """ Load the TTF at font_path at the given point size and bind it globally. Safe to call multiple times. Uses a single persistent font_registry; only the font *item* tag is tracked. Passing an empty path or a missing file resets to the DPG built-in font. """ global _current_font_tag, _current_font_path, _current_font_size, _font_registry_tag _current_font_path = font_path _current_font_size = size if not font_path or not Path(font_path).exists(): # Reset to default built-in font dpg.bind_font(0) _current_font_tag = None return # Create the registry once if _font_registry_tag is None or not dpg.does_item_exist(_font_registry_tag): with dpg.font_registry() as reg: _font_registry_tag = reg # Delete previous custom font item only (not the registry) if _current_font_tag is not None: try: dpg.delete_item(_current_font_tag) except Exception: pass _current_font_tag = None font = dpg.add_font(font_path, size, parent=_font_registry_tag) _current_font_tag = font dpg.bind_font(font) def set_scale(factor: float): """Set the global Dear PyGui font/UI scale factor.""" global _current_scale _current_scale = factor dpg.set_global_font_scale(factor) def save_to_config(config: dict): """Persist theme settings into the config dict under [theme].""" config.setdefault("theme", {}) config["theme"]["palette"] = _current_palette config["theme"]["font_path"] = _current_font_path config["theme"]["font_size"] = _current_font_size config["theme"]["scale"] = _current_scale def load_from_config(config: dict): """Read [theme] from config and apply everything.""" t = config.get("theme", {}) palette = t.get("palette", "DPG Default") font_path = t.get("font_path", "") font_size = float(t.get("font_size", 14.0)) scale = float(t.get("scale", 1.0)) apply(palette) if font_path: apply_font(font_path, font_size) set_scale(scale)