Skip to content

Spec: Dark/Light Auto-Theme

Overview

Automatically switch between dark and light themes based on the Windows system theme preference, with smooth transitions and per-element theme awareness for the circuit canvas.

Motivation

  • Windows 10/11 users commonly switch between dark and light modes.
  • Many users enable scheduled dark mode (dark at night, light during day).
  • The app currently has multiple themes but requires manual switching.
  • Automatic theme-following is expected behavior for modern Windows apps.

Requirements

Functional Requirements

  1. System Theme Detection: Detect current Windows dark/light mode setting.

  2. Auto-Follow Mode: Option to automatically match system theme changes.

  3. Real-Time Switching: Theme changes immediately when system theme changes (no app restart required).

  4. Smooth Transition: Animate color transitions over ~200ms when switching.

  5. Canvas Theme Adaptation: Canvas background, grid, and wire colors adapt.

  6. Component Rendering: Component shapes render appropriately in both themes.

  7. Theme Preference Options: Auto (follow system), Always Light, Always Dark, or Custom (user-defined theme).

  8. Per-Monitor Theme: If system supports per-app theme, respect it.

  9. Accent Color Integration: Use Windows accent color for highlights/selection.

  10. Scheduled Switching: Optional built-in schedule (dark after sunset) independent of system setting.

  11. Theme-Aware Exports: Exported images use a consistent theme (configurable) regardless of current app theme.

  12. Signal Colors in Dark Mode: Ensure High/Low/Floating signal colors remain distinguishable and accessible in both themes.

Non-Functional Requirements

  • Theme switch completes in < 300ms (including canvas re-render).
  • No flickering or partial-theme states during transition.
  • All 5 existing custom themes continue to work (auto-theme is additive).
  • Dark mode colors meet WCAG 2.1 AA contrast requirements.
  • Memory: no duplicate resource allocation for unused theme.

Design

System Theme Detection (WinUI 3)

csharp
public class ThemeWatcher : IDisposable
{
    private readonly UISettings _uiSettings = new();

    public ThemeWatcher()
    {
        _uiSettings.ColorValuesChanged += OnSystemThemeChanged;
    }

    public AppTheme CurrentSystemTheme
    {
        get
        {
            var foreground = _uiSettings.GetColorValue(UIColorType.Foreground);
            // If foreground is light, system is in dark mode
            return IsColorLight(foreground) ? AppTheme.Dark : AppTheme.Light;
        }
    }

    private void OnSystemThemeChanged(UISettings sender, object args)
    {
        DispatcherQueue.TryEnqueue(() =>
        {
            if (ThemePreference == ThemePreference.Auto)
                ApplyTheme(CurrentSystemTheme);
        });
    }
}

Theme Resource Structure

csharp
public class ThemeColors
{
    // Canvas
    public Color CanvasBackground { get; set; }
    public Color GridLines { get; set; }
    public Color GridDots { get; set; }

    // Components
    public Color ComponentBody { get; set; }
    public Color ComponentBorder { get; set; }
    public Color ComponentLabel { get; set; }
    public Color PinColor { get; set; }

    // Wires
    public Color WireDefault { get; set; }
    public Color WireHigh { get; set; }
    public Color WireLow { get; set; }
    public Color WireFloating { get; set; }

    // Selection
    public Color SelectionHighlight { get; set; }
    public Color SelectionBox { get; set; }

    // UI Chrome
    public Color Foreground { get; set; }
    public Color Background { get; set; }
    public Color AccentColor { get; set; }
}

Transition Animation

csharp
public async Task TransitionTheme(ThemeColors from, ThemeColors to, TimeSpan duration)
{
    var animation = new CompositionAnimation(duration);
    // Interpolate all color properties over the duration
    foreach (var property in typeof(ThemeColors).GetProperties())
    {
        var fromColor = (Color)property.GetValue(from);
        var toColor = (Color)property.GetValue(to);
        animation.AddKeyFrame(property.Name, fromColor, toColor);
    }
    await animation.StartAsync();
}

Settings Integration

Theme Preference:
  ○ Auto (follow Windows)    ← default
  ○ Light
  ○ Dark
  ○ Custom: [Select theme ▼]

  [ ] Animate transitions
  [ ] Use Windows accent color
  [ ] Schedule: Dark after [18:00] until [06:00]

Implementation Tasks

  1. Implement ThemeWatcher to detect system dark/light mode changes.
  2. Register for UISettings.ColorValuesChanged event.
  3. Define ThemeColors for light and dark canvas rendering.
  4. Update canvas renderer to use theme-reactive color bindings.
  5. Implement smooth color transition animation.
  6. Add "Auto" option to existing theme preferences.
  7. Integrate Windows accent color for selection/highlights.
  8. Ensure all signal colors meet contrast requirements in both modes.
  9. Update component shape rendering for dark/light variants.
  10. Add optional scheduled theme switching (time-based).
  11. Update export to use configurable theme (not current app theme).
  12. Test transitions between all theme combinations.

Risks & Open Questions

  • Should custom themes also have a "dark variant" auto-generated?
  • How to handle third-party/plugin UI elements that don't adapt?
  • Performance of re-rendering entire canvas on theme change for large circuits?
  • Should the minimap also transition or switch instantly?
  • Accent color: use for selection only, or also for active signals?

Priority

Low — Nice polish feature; existing manual theme switching is adequate.