Map: follow the app's light/dark color scheme, live
The rest of the UI already adapted to system theme automatically via libadwaita, only the hand-drawn Cairo map (grid lines, markers, overlays, everything in grid_widget.py) was hardcoded to the dark palette. Added a parallel light palette (first-pass guesses, same as the Shell descriptions were, flagged for correction) and hooked Adw.StyleManager's dark/light state, including its own live-update signal, so switching the system theme while the app is running repaints the map with the other palette immediately, not just at startup. Verified: rendered both palettes side by side with the same board state (legible in both), and a live theme-switch test confirming the module-level color names actually change value when the StyleManager signal fires, not just once at construction. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
a92aff5e06
commit
084764aa9b
@ -14,7 +14,8 @@ import gi
|
|||||||
|
|
||||||
gi.require_version("Gtk", "4.0")
|
gi.require_version("Gtk", "4.0")
|
||||||
gi.require_version("Gdk", "4.0")
|
gi.require_version("Gdk", "4.0")
|
||||||
from gi.repository import Gdk, Gtk # noqa: E402
|
gi.require_version("Adw", "1")
|
||||||
|
from gi.repository import Adw, Gdk, Gtk # noqa: E402
|
||||||
|
|
||||||
from . import ballistics, icons, solver
|
from . import ballistics, icons, solver
|
||||||
from .models import LARGE_X, Board, Target
|
from .models import LARGE_X, Board, Target
|
||||||
@ -52,6 +53,19 @@ ICON_MIN_CELL_PX = 42
|
|||||||
# unless zoomed in and panned).
|
# unless zoomed in and panned).
|
||||||
_View = namedtuple("_View", "cell_w cell_h grid_w grid_h pad_x pad_y ox oy vis_cols vis_rows")
|
_View = namedtuple("_View", "cell_w cell_h grid_w grid_h pad_x pad_y ox oy vis_cols vis_rows")
|
||||||
|
|
||||||
|
# Everything below (CATEGORY_COLOR through PLACEMENT_PREVIEW) is a
|
||||||
|
# module-level name deliberately kept mutable: _apply_palette() below
|
||||||
|
# reassigns all of them via `global`, in place, whenever the app's
|
||||||
|
# light/dark scheme changes (see GridCanvas.__init__, which hooks
|
||||||
|
# Adw.StyleManager's own dark/light detection, including live updates
|
||||||
|
# if the system theme changes while running). Every draw method
|
||||||
|
# references these bare names directly (`cr.set_source_rgb(*BG)` etc.)
|
||||||
|
# rather than threading a palette object through every call, reassigning
|
||||||
|
# the names in place is what makes that keep working without touching
|
||||||
|
# every call site. The values set here at import time are the dark
|
||||||
|
# palette, _apply_palette(is_dark=True) (called from __init__) reapplies
|
||||||
|
# the same values, it's the light branch that actually changes anything
|
||||||
|
# the first time it runs.
|
||||||
CATEGORY_COLOR = {
|
CATEGORY_COLOR = {
|
||||||
"nest": (0.35, 0.60, 0.95),
|
"nest": (0.35, 0.60, 0.95),
|
||||||
"spotter": (0.35, 0.78, 0.40),
|
"spotter": (0.35, 0.78, 0.40),
|
||||||
@ -63,17 +77,89 @@ SCOUT_FLIGHT = (0.70, 0.45, 0.92)
|
|||||||
|
|
||||||
BG = (0.13, 0.12, 0.10)
|
BG = (0.13, 0.12, 0.10)
|
||||||
GRID_LINE = (1.0, 1.0, 1.0, 0.20)
|
GRID_LINE = (1.0, 1.0, 1.0, 0.20)
|
||||||
SUBGRID_LINE = (0.72, 0.70, 0.65, 0.35) # a shade between BG and GRID_LINE's white, not a hue change
|
SUBGRID_LINE = (0.72, 0.70, 0.65, 0.15) # verified by actually computing the blended-over-BG
|
||||||
|
# pixel values, not eyeballing it: alpha 0.35 (a previous version) blended this same RGB out to
|
||||||
|
# (86, 83, 75), BRIGHTER than GRID_LINE's own blended (77, 76, 72), backwards from the intent.
|
||||||
|
# 0.15 blends to (56, 53, 47): sits between BG (33, 31, 26) and GRID_LINE (77, 76, 72), the RGB
|
||||||
|
# tint stays visible but the line itself reads as genuinely fainter, not louder.
|
||||||
HOVER_LEGEND = (0.45, 0.65, 0.95) # blue, not yellow, for the highlighted X/Y legend label
|
HOVER_LEGEND = (0.45, 0.65, 0.95) # blue, not yellow, for the highlighted X/Y legend label
|
||||||
LABEL = (0.88, 0.86, 0.80)
|
LABEL = (0.88, 0.86, 0.80)
|
||||||
COORD_LABEL = (0.60, 0.58, 0.54)
|
COORD_LABEL = (0.60, 0.58, 0.54)
|
||||||
YELLOW = (0.95, 0.85, 0.20)
|
YELLOW = (0.95, 0.85, 0.20)
|
||||||
WHITE = (1.0, 1.0, 1.0)
|
WHITE = (1.0, 1.0, 1.0) # not literally "white" any more in the light palette, see _LIGHT_PALETTE:
|
||||||
|
# its role is "a neutral that maximally contrasts with BG", the name stuck around from when this
|
||||||
|
# only ever ran on a dark background.
|
||||||
FIRING_ARROW = (0.95, 0.15, 0.15)
|
FIRING_ARROW = (0.95, 0.15, 0.15)
|
||||||
SELECTION_RING = (1.0, 1.0, 1.0)
|
SELECTION_RING = (1.0, 1.0, 1.0)
|
||||||
BLAST_RADIUS = (0.95, 0.40, 0.10)
|
BLAST_RADIUS = (0.95, 0.40, 0.10)
|
||||||
PLACEMENT_PREVIEW = (0.95, 0.85, 0.20)
|
PLACEMENT_PREVIEW = (0.95, 0.85, 0.20)
|
||||||
|
|
||||||
|
_DARK_PALETTE = dict(
|
||||||
|
CATEGORY_COLOR={
|
||||||
|
"nest": (0.35, 0.60, 0.95), "spotter": (0.35, 0.78, 0.40),
|
||||||
|
"rp": (0.95, 0.78, 0.20), "target": (0.92, 0.30, 0.28), "ally": (0.30, 0.85, 0.85),
|
||||||
|
},
|
||||||
|
SCOUT_FLIGHT=(0.70, 0.45, 0.92),
|
||||||
|
BG=(0.13, 0.12, 0.10),
|
||||||
|
GRID_LINE=(1.0, 1.0, 1.0, 0.20),
|
||||||
|
SUBGRID_LINE=(0.72, 0.70, 0.65, 0.15),
|
||||||
|
HOVER_LEGEND=(0.45, 0.65, 0.95),
|
||||||
|
LABEL=(0.88, 0.86, 0.80),
|
||||||
|
COORD_LABEL=(0.60, 0.58, 0.54),
|
||||||
|
YELLOW=(0.95, 0.85, 0.20),
|
||||||
|
WHITE=(1.0, 1.0, 1.0),
|
||||||
|
FIRING_ARROW=(0.95, 0.15, 0.15),
|
||||||
|
SELECTION_RING=(1.0, 1.0, 1.0),
|
||||||
|
BLAST_RADIUS=(0.95, 0.40, 0.10),
|
||||||
|
PLACEMENT_PREVIEW=(0.95, 0.85, 0.20),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Same relative brightness relationships as the dark palette (main grid
|
||||||
|
# line vs. the fainter subgrid one, category colors distinct from each
|
||||||
|
# other), just inverted for a light background: every color that needs
|
||||||
|
# to contrast against BG got darkened instead of brightened. First-pass
|
||||||
|
# guesses, flagged the same way the Shell descriptions were, correct
|
||||||
|
# whichever look off once actually seen on a real light-themed desktop.
|
||||||
|
_LIGHT_PALETTE = dict(
|
||||||
|
CATEGORY_COLOR={
|
||||||
|
"nest": (0.15, 0.35, 0.75), "spotter": (0.10, 0.50, 0.15),
|
||||||
|
"rp": (0.65, 0.50, 0.05), "target": (0.75, 0.12, 0.10), "ally": (0.05, 0.45, 0.45),
|
||||||
|
},
|
||||||
|
SCOUT_FLIGHT=(0.45, 0.20, 0.65),
|
||||||
|
BG=(0.96, 0.95, 0.93),
|
||||||
|
GRID_LINE=(0.08, 0.08, 0.08, 0.20),
|
||||||
|
SUBGRID_LINE=(0.08, 0.08, 0.08, 0.15),
|
||||||
|
HOVER_LEGEND=(0.10, 0.35, 0.75),
|
||||||
|
LABEL=(0.15, 0.14, 0.12),
|
||||||
|
COORD_LABEL=(0.42, 0.40, 0.37),
|
||||||
|
YELLOW=(0.65, 0.48, 0.02),
|
||||||
|
WHITE=(0.10, 0.10, 0.10),
|
||||||
|
FIRING_ARROW=(0.80, 0.10, 0.10),
|
||||||
|
SELECTION_RING=(0.05, 0.05, 0.05),
|
||||||
|
BLAST_RADIUS=(0.80, 0.35, 0.05),
|
||||||
|
PLACEMENT_PREVIEW=(0.65, 0.48, 0.02),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_palette(is_dark: bool) -> None:
|
||||||
|
global CATEGORY_COLOR, SCOUT_FLIGHT, BG, GRID_LINE, SUBGRID_LINE, HOVER_LEGEND, LABEL, \
|
||||||
|
COORD_LABEL, YELLOW, WHITE, FIRING_ARROW, SELECTION_RING, BLAST_RADIUS, PLACEMENT_PREVIEW
|
||||||
|
p = _DARK_PALETTE if is_dark else _LIGHT_PALETTE
|
||||||
|
CATEGORY_COLOR = p["CATEGORY_COLOR"]
|
||||||
|
SCOUT_FLIGHT = p["SCOUT_FLIGHT"]
|
||||||
|
BG = p["BG"]
|
||||||
|
GRID_LINE = p["GRID_LINE"]
|
||||||
|
SUBGRID_LINE = p["SUBGRID_LINE"]
|
||||||
|
HOVER_LEGEND = p["HOVER_LEGEND"]
|
||||||
|
LABEL = p["LABEL"]
|
||||||
|
COORD_LABEL = p["COORD_LABEL"]
|
||||||
|
YELLOW = p["YELLOW"]
|
||||||
|
WHITE = p["WHITE"]
|
||||||
|
FIRING_ARROW = p["FIRING_ARROW"]
|
||||||
|
SELECTION_RING = p["SELECTION_RING"]
|
||||||
|
BLAST_RADIUS = p["BLAST_RADIUS"]
|
||||||
|
PLACEMENT_PREVIEW = p["PLACEMENT_PREVIEW"]
|
||||||
|
|
||||||
# path -> loaded cairo.ImageSurface (or None for a path that failed to
|
# path -> loaded cairo.ImageSurface (or None for a path that failed to
|
||||||
# load, so a missing/bad icon file only ever gets one failed attempt,
|
# load, so a missing/bad icon file only ever gets one failed attempt,
|
||||||
# not one per frame). Module-level, not per-canvas: the icon set is
|
# not one per frame). Module-level, not per-canvas: the icon set is
|
||||||
@ -109,6 +195,16 @@ class GridCanvas(Gtk.DrawingArea):
|
|||||||
def __init__(self, board: Board) -> None:
|
def __init__(self, board: Board) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.board = board
|
self.board = board
|
||||||
|
|
||||||
|
# Follow the app's light/dark scheme (system setting, or an
|
||||||
|
# in-app override if one's ever added later) for every color
|
||||||
|
# this canvas draws with, live: if the scheme changes while
|
||||||
|
# running, redraw with the other palette rather than staying
|
||||||
|
# stuck on whichever was active at startup.
|
||||||
|
style_manager = Adw.StyleManager.get_default()
|
||||||
|
_apply_palette(style_manager.get_dark())
|
||||||
|
style_manager.connect("notify::dark", self._on_style_changed)
|
||||||
|
|
||||||
self.hovered = None
|
self.hovered = None
|
||||||
self.hovered_point = None # which candidate, when obj has more than one point
|
self.hovered_point = None # which candidate, when obj has more than one point
|
||||||
self.selected = None
|
self.selected = None
|
||||||
@ -200,6 +296,10 @@ class GridCanvas(Gtk.DrawingArea):
|
|||||||
def refresh(self) -> None:
|
def refresh(self) -> None:
|
||||||
self.queue_draw()
|
self.queue_draw()
|
||||||
|
|
||||||
|
def _on_style_changed(self, style_manager, _pspec) -> None:
|
||||||
|
_apply_palette(style_manager.get_dark())
|
||||||
|
self.queue_draw()
|
||||||
|
|
||||||
# -- placement mode -----------------------------------------------------------
|
# -- placement mode -----------------------------------------------------------
|
||||||
def start_placement(self, callback, preview_radius_km=None) -> None:
|
def start_placement(self, callback, preview_radius_km=None) -> None:
|
||||||
self.placement_callback = callback
|
self.placement_callback = callback
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user