Skip to content

Fix faint caret in edit box after system dark-to-light theme switch (color tags)#11604

Merged
niksedk merged 2 commits into
SubtitleEdit:mainfrom
mjuhasz:fix/caret-system-theme-switch
Jun 15, 2026
Merged

Fix faint caret in edit box after system dark-to-light theme switch (color tags)#11604
niksedk merged 2 commits into
SubtitleEdit:mainfrom
mjuhasz:fix/caret-system-theme-switch

Conversation

@mjuhasz

@mjuhasz mjuhasz commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Problem

When Theme is set to System and macOS switches from dark to light mode, the text cursor (caret) in the subtitle edit box remains faint, hardly visible. This only occurs when color tags are enabled in Settings, because that activates the AvaloniaEdit TextEditor instead of a plain TextBox.

Root cause

The system theme auto-switch and the Settings theme change follow different code paths and are not equivalent:

Settings theme change System auto-switch
RequestedThemeVariant Changes (e.g. DefaultLight) Stays at Default
Avalonia full style re-evaluation ✅ triggered by property-change event ❌ not triggered
Layout rebuild (SetLayout) ✅ recreates all controls fresh ❌ existing controls reused

When the dark-mode style is removed by RemoveLighterDark(), TextArea.CaretBrushProperty drops from an explicitly-set white brush to null (unset). AvaloniaEdit's caret renderer retains a stale bright brush — it does not re-read the Foreground fallback cleanly when the property goes to null rather than to an explicit new value. Because RequestedThemeVariant does not change, Avalonia never fires the re-evaluation that would correct it.

Note: the earlier toolbar icon fix I made is a different class of problem — toolbar icons are PNG files loaded from a theme-specific folder at construction time, so no amount of style re-evaluation helps; RebuildToolbar() is the correct fix for that.

Fix

Apply a minimal style via ApplyLightThemeCaretFix() that explicitly sets TextArea.CaretBrushProperty = Black when the system variant switches to light. This fires a clean White → Black property-changed notification that the caret renderer responds to. The style is stored in _lighterDarkStyle so RemoveLighterDark() removes it automatically on the next theme switch.

Architectural note

This is a targeted, per-component fix. If more components exhibit stale state after a system theme switch, the correct long-term solution is the heavy approach: hook OnActualThemeVariantChanged to also call SetLayout() after SetCurrentTheme(), mirroring what ApplySettings() does. This rebuilds the entire layout — DataGrid, TextEditors, toolbar — creating all controls fresh with the correct theme active. It eliminates the stale-state class of bug entirely, at the cost of a brief visual reconstruction on every OS theme switch. Further per-component patches should be avoided in favor of that approach if a third component turns up with the same problem.

If you prefer to go straight for the heavy approach, let me know, we close this PR, and I'll submit one for the full layout rebuild to unify the Settings and System theme switch paths.

When Theme is set to System and the OS switches from dark to light,
RequestedThemeVariant stays at Default so Avalonia does not trigger a
full style re-evaluation. The dark-mode style that set TextArea
CaretBrushProperty to white is removed by RemoveLighterDark(), but
AvaloniaEdit's caret renderer retains a stale bright brush because
CaretBrushProperty falls to null rather than to an explicit value.

Apply a minimal style that sets CaretBrushProperty to black when the
system variant is light, stored in _lighterDarkStyle so RemoveLighterDark
cleans it up on the next theme switch. This fires a clean white-to-black
property-changed notification that the caret renderer responds to.

@niksedk niksedk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: looks correct and well-scoped

The diagnosis holds up against the code. ApplyLighterDark() sets TextArea.CaretBrushProperty to the white foreground (UiTheme.cs:514), and the System path keeps RequestedThemeVariant = Default on an OS switch (:72), so the full re-evaluation that fixes explicit theme changes never fires. Forcing an explicit White → Black transition is a sound, minimal way to kick the AvaloniaEdit caret renderer.

What's right

  • Symmetry is complete. dark→light now sets caret Black (new else branch); light→dark sets caret White (existing ApplyLighterDark). Both directions fire an explicit property change, so re-toggling works repeatedly.
  • No style leak. SetCurrentTheme always calls RemoveLighterDark() first (:67), which removes whatever is in _lighterDarkStyle, so the caret-fix style is cleaned up on the next switch like any other.
  • Correctly scoped. The style targets TextArea (AvaloniaEdit only), so the plain-TextBox path (color tags off) is an untouched no-op — matching the "only with color tags" repro.
  • Targets only CaretBrush. In light mode the default Fluent theme already supplies the right foreground; only the caret was stale, so forcing just the caret is the right call.

Minor nits (non-blocking)

  1. Field name is misleading. Storing a light-theme caret fix in _lighterDarkStyle reads oddly. Functionally fine because removal is centralized, but a short comment on the assignment (or renaming the field to e.g. _themeOverrideStyle) would stop a future reader assuming it only ever holds dark styling.
  2. Prefer Brushes.Black. new SolidColorBrush(Colors.Black) allocates each switch; Brushes.Black is a cached immutable brush. Trivial.
  3. Runs on first startup too. In System+light at launch the fix is applied even though there's no stale state yet — harmless (caret is black anyway), just slightly redundant.

On the architectural note

Agree with the framing. Take this targeted fix now; if a third component shows stale state after a system switch, do the unified SetLayout()-on-OnActualThemeVariantChanged rebuild rather than accumulating more per-component patches.

Suggested manual check

No tests, which is acceptable for theme/visual behavior. Worth a manual pass on Windows as well as macOS: toggle OS dark→light→dark with color tags on and confirm the caret stays visible each direction, and confirm switching Theme away from System to explicit Light/Dark still leaves a correct caret (should be fine, since RemoveLighterDark runs first).

The field now holds light-theme overrides too, so the old name was
misleading. Also replace new SolidColorBrush(Colors.Black) with the
cached Brushes.Black singleton in ApplyLightThemeCaretFix.
@mjuhasz

mjuhasz commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Addressed both nits in a follow-up commit:

  • Renamed _lighterDarkStyle → _themeOverrideStyle so the field name doesn't imply it only ever holds dark styling.
  • Switched to Brushes.Black in ApplyLightThemeCaretFix.

Agreed on the startup redundancy and the architectural note — leaving those for a future pass if a third component ends up needing the same treatment.

@niksedk niksedk merged commit 9689db5 into SubtitleEdit:main Jun 15, 2026
2 checks passed
@mjuhasz mjuhasz deleted the fix/caret-system-theme-switch branch June 15, 2026 07:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants