From a2102e5f17d24545a9a01ca59bb64d1c6c15d34b Mon Sep 17 00:00:00 2001 From: Benedikt Lezius Date: Fri, 10 Jul 2026 10:32:40 -0400 Subject: [PATCH 1/4] Fix invisible menu bar text in Windows dark mode On Windows 11, Qt's native style paints native chrome - most visibly the QMenuBar's top-level items - using the OS color scheme, ignoring the QSS `color` and the application palette. When the OS is in dark mode but Optiverse is in light mode, the menu bar renders white text on its light background, making "File/Edit/Insert/..." invisible. Pin Qt's color scheme to match the applied theme via QStyleHints.setColorScheme (Qt 6.8+) in apply_theme, so native rendering stays consistent with the theme we actually apply. Guarded with getattr so older Qt builds degrade to a no-op. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/optiverse/ui/theme_manager.py | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/optiverse/ui/theme_manager.py b/src/optiverse/ui/theme_manager.py index 5a504f65..0f6819a9 100644 --- a/src/optiverse/ui/theme_manager.py +++ b/src/optiverse/ui/theme_manager.py @@ -208,6 +208,31 @@ def _create_light_palette() -> QtGui.QPalette: return palette +def _set_color_scheme(dark_mode: bool) -> None: + """ + Pin Qt's color scheme so native styling matches the applied theme. + + Uses ``QStyleHints.setColorScheme`` (Qt 6.8+). On older Qt versions the + setter is unavailable and this is a no-op - the QSS/palette still apply, + only the native-chrome override (e.g. Windows dark-mode menu bar) is + unavailable to correct. + + Args: + dark_mode: True to request the dark color scheme, False for light. + """ + style_hints = QtGui.QGuiApplication.styleHints() + setter = getattr(style_hints, "setColorScheme", None) + if setter is None: + return + scheme = ( + QtCore.Qt.ColorScheme.Dark if dark_mode else QtCore.Qt.ColorScheme.Light + ) + try: + setter(scheme) + except (AttributeError, TypeError) as e: + _logger.debug("Could not set color scheme: %s", e) + + def apply_theme(dark_mode: bool) -> None: """ Apply the appropriate theme (stylesheet + palette) based on dark mode setting. @@ -225,6 +250,16 @@ def apply_theme(dark_mode: bool) -> None: _logger.warning("No QApplication instance - cannot apply theme") return + # Pin Qt's color scheme to match the app theme. + # + # On Windows 11, Qt's native style paints native chrome (most visibly the + # QMenuBar's top-level items) using the OS color scheme, ignoring the QSS + # `color` and the application palette. When the OS is in dark mode but the + # app is in light mode, this renders white menu-bar text on the light + # menu-bar background - i.e. invisible. Forcing the color scheme keeps the + # native rendering consistent with the theme we actually applied. + _set_color_scheme(dark_mode) + # Apply stylesheet and palette if dark_mode: app.setStyleSheet(get_dark_stylesheet()) From 3f365f50516351c6d37a2b2fde5c98dae4b64049 Mon Sep 17 00:00:00 2001 From: Benedikt Lezius Date: Fri, 10 Jul 2026 10:40:14 -0400 Subject: [PATCH 2/4] Fix pre-existing mypy errors blocking CI CI's mypy (unpinned, newer than the last green run on main) flags three errors unrelated to the menu-bar fix but which block this PR's pipeline: - raytracing_math.py: the numba-fallback `def jit` re-defines the name imported in the try-branch -> [no-redef]. Suppress with type: ignore. - refractive.py (x2): refract_vector_snell is annotated to return `np.ndarray | None`, but both return paths return a real ndarray (the TIR branch returns the reflected direction, not None). The lone caller never handles None. Narrow the return type to `tuple[np.ndarray, bool]` and correct the docstring, which resolves both call-site errors. Verified locally: `ruff check .` and `mypy src/` both pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/optiverse/core/raytracing_math.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/optiverse/core/raytracing_math.py b/src/optiverse/core/raytracing_math.py index eb3acae3..d9f325fb 100644 --- a/src/optiverse/core/raytracing_math.py +++ b/src/optiverse/core/raytracing_math.py @@ -13,7 +13,7 @@ NUMBA_AVAILABLE = True except ImportError: # Fallback: no-op decorator if numba isn't available - def jit(*args, **kwargs): + def jit(*args, **kwargs): # type: ignore[no-redef] def decorator(func): return func @@ -561,7 +561,7 @@ def compute_dichroic_reflectance( def refract_vector_snell( v_in: np.ndarray, n_hat: np.ndarray, n1: float, n2: float -) -> tuple[np.ndarray | None, bool]: +) -> tuple[np.ndarray, bool]: """ Apply Snell's law to refract a ray at an interface. @@ -572,9 +572,9 @@ def refract_vector_snell( n2: Refractive index of transmitted medium Returns: - Tuple of (refracted_direction, is_total_reflection) - - refracted_direction: Refracted ray direction (normalized), - or None if total internal reflection + Tuple of (direction, is_total_reflection) + - direction: Refracted ray direction (normalized), or the reflected + direction if total internal reflection occurs - is_total_reflection: True if total internal reflection occurs Physics: From 8615a6f890b96c351dfc682389fc9f8de6ae519a Mon Sep 17 00:00:00 2001 From: Benedikt Lezius Date: Fri, 10 Jul 2026 10:50:34 -0400 Subject: [PATCH 3/4] Sort standard component folders for deterministic order _iter_component_json_files relied on Path.iterdir(), whose order is filesystem dependent, so get_standard_components() returned components in a different order per OS. This made test_standard_lens_definition pass on macOS but fail on Linux/Windows (the "first lens" was Microscope Objective instead of Standard Lens). Sort the discovered component folders by name so the order is stable across platforms. Alphabetical folder order puts lens_standard_1in ("Standard Lens") ahead of objective_standard ("Microscope Objective"), matching the test's expectation. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/optiverse/objects/definitions_loader.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/optiverse/objects/definitions_loader.py b/src/optiverse/objects/definitions_loader.py index aa826908..8410803e 100644 --- a/src/optiverse/objects/definitions_loader.py +++ b/src/optiverse/objects/definitions_loader.py @@ -25,12 +25,17 @@ def _iter_component_json_files(library_path: Path | None = None) -> list[Path]: library_path: Optional custom library path. If None, uses built-in library. Returns: - List of Path objects to component folders + List of Path objects to component folders, sorted by folder name for a + deterministic order across platforms (iterdir() order is filesystem + dependent and differs between Linux/Windows/macOS). """ root = library_path if library_path else _library_root() if not root.exists(): return [] - return [p for p in root.iterdir() if p.is_dir() and (p / "component.json").exists()] + folders = [ + p for p in root.iterdir() if p.is_dir() and (p / "component.json").exists() + ] + return sorted(folders, key=lambda p: p.name) def load_component_records( From b111fe246166e0b5b6bfae935561f399ed2fa4f4 Mon Sep 17 00:00:00 2001 From: Benedikt Lezius Date: Fri, 10 Jul 2026 10:54:01 -0400 Subject: [PATCH 4/4] Cover both mypy error codes for numba jit fallback Windows CI's mypy flags the numba-fallback `def jit` with code `misc` ("All conditional function variants must have identical signatures") rather than `no-redef` seen on Linux/macOS, because numba's importability during analysis differs per platform. Suppress both codes; warn_unused_ignores is off so the inapplicable one is harmless. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/optiverse/core/raytracing_math.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/optiverse/core/raytracing_math.py b/src/optiverse/core/raytracing_math.py index d9f325fb..6601edf9 100644 --- a/src/optiverse/core/raytracing_math.py +++ b/src/optiverse/core/raytracing_math.py @@ -12,8 +12,12 @@ NUMBA_AVAILABLE = True except ImportError: - # Fallback: no-op decorator if numba isn't available - def jit(*args, **kwargs): # type: ignore[no-redef] + # Fallback: no-op decorator if numba isn't available. mypy flags the + # redefinition differently depending on whether numba is importable in the + # analysis environment (no-redef when it is, misc when it isn't), so both + # codes are suppressed for cross-platform CI. warn_unused_ignores is off, + # so the code that doesn't apply on a given platform is harmless. + def jit(*args, **kwargs): # type: ignore[no-redef,misc] def decorator(func): return func