Skip to content

fix(ui): obwódka „systemowa" nie zostaje kobaltowa — dobicie po repaintach WPF-UI (regresja #183)#186

Merged
FilipB97 merged 1 commit into
masterfrom
claude/border-system-sticky
Jul 24, 2026
Merged

fix(ui): obwódka „systemowa" nie zostaje kobaltowa — dobicie po repaintach WPF-UI (regresja #183)#186
FilipB97 merged 1 commit into
masterfrom
claude/border-system-sticky

Conversation

@FilipB97

Copy link
Copy Markdown
Owner

Objaw (regresja po #183)

Obwódka okna zostaje kobaltowa na stałe mimo ustawienia „systemowa". Po zmianie ustawień przez chwilę jest poprawnie, potem wraca kobalt — i nie błyska, tylko zostaje.

Root cause

Sekwencja cyklu aktywacji okna:

  1. WM_NCACTIVATEnasz synchroniczny zapis obwódki (dodany w fix(ui): obwódka okna nie błyska na kobalt przy starcie i aktywacji #183),
  2. ActivatedWPF-UI przemalowuje krawędź na akcent (kobalt) później w tym samym cyklu.

#183 usunął jedyny mechanizm, który pisał po repaintach WPF-UI — handler Activated z odroczonym (ApplicationIdle) ponownym nałożeniem. Został tylko zapis synchroniczny, który leci przed zapisem WPF-UI → ich kobalt ma zawsze ostatnie słowo → obwódka zostaje kobaltowa na stałe. To dokładnie tłumaczy objawy:

  • „nie błyska, tylko zostaje" — nasza korekta w ogóle nie ląduje po ich zapisie,
  • „po zmianie ustawień chwilowo dobrze" — ReapplyAll ma własne odroczone dobicie; psuje się przy pierwszym kolejnym cyklu (de)aktywacji.

Że WPF-UI faktycznie maluje krawędź akcentem, potwierdzają i historia projektu (#49, stare komentarze: „Sam Apply w handlerze aktywacji bywa ZA wcześnie — repaint WPF-UI leci później"), i metadane Wpf.Ui.dll 4.3 (BORDER_COLOR + DwmSetWindowAttribute + OnActivated). W 4.3 nie ma przełącznika, by to wyłączyć u źródła.

Fix — obie krawędzie cyklu zamiast wyboru jednej

Koszt: kilka zapisów jednego atrybutu DWM (DwmSetWindowAttribute) na cykl aktywacji — pomijalny. Możliwy powrót krótkiego mgnienia akcentu na krawędzi w rzadkich przypadkach (odstęp między zapisem WPF-UI a naszym dobiciem) — to stan sprzed #183; trwałe zafałszowanie koloru jest naprawione, a zapis synchroniczny osłania wiodącą krawędź cyklu.

Zakres

Tylko WindowBorder.cs (+33/−9). Build 0 błędów, testy 347/347.

Weryfikacja ręczna (WPF / Windows 11)

  • Ustawienia → obwódka „systemowa" → przeklikuj między Waypoint a innymi oknami, otwórz/zamknij dialog (np. „O aplikacji", Środowiska) → obwódka pozostaje systemowa, bez trwałego kobaltu.
  • To samo dla „brak" i koloru własnego.
  • Zmiana motywu / presetu / akcentu nie przywraca kobaltu.
  • Start aplikacji: obwódka zgodna z ustawieniem od pierwszych klatek (ew. krótkie mgnienie akceptowalne — stan sprzed fix(ui): obwódka okna nie błyska na kobalt przy starcie i aktywacji #183).
  • Osobne okno sesji (tear-off) i dialogi zachowują się tak samo.

🤖 Generated with Claude Code

…ne dobicie po repaintach WPF-UI

Regresja z #183: usunięto handler Activated z odroczonym (ApplicationIdle)
ponownym nałożeniem obwódki, zostawiając wyłącznie SYNCHRONICZNY zapis
w hooku WM_NCACTIVATE. Problem w tym, że WM_NCACTIVATE leci PRZED
przemalowaniem krawędzi na akcent przez WPF-UI (dzieje się po Activated),
więc nasz zapis przegrywał „ostatnie słowo" — obwódka zostawała kobaltowa
NA STAŁE mimo ustawienia „systemowa"/„brak". Po zmianie ustawień było
chwilowo dobrze (ReapplyAll ma własne odroczone dobicie), do pierwszego
cyklu (de)aktywacji. Stąd objaw „nie błyska, tylko zostaje".

Fix — obie krawędzie cyklu, zamiast wyboru jednej:
- zapis synchroniczny w BorderHook zostaje (ogranicza błysk z #183),
- BorderHook dodatkowo kolejkuje dobicie na ApplicationIdle (pisze wprost
  po hwnd — działa też dla deaktywacji i okien w trakcie zamykania),
- Keep przywraca sprawdzony sprzed #183 handler Activated (Apply teraz
  + dobicie na ApplicationIdle) — to on latami gwarantował, że nasza
  obwódka ląduje PO repaintach WPF-UI.

Koszt: kilka zapisów jednego atrybutu DWM na cykl aktywacji — pomijalny.

Weryfikacja ręczna (WPF/Win11): ustaw obwódkę „systemowa" → klikaj między
oknami/otwórz i zamknij dialog — obwódka pozostaje systemowa (bez trwałego
kobaltu); to samo dla „brak" i koloru własnego; zmiana motywu/akcentu
nie przywraca kobaltu.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@llamapreview llamapreview Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AI Code Review by LlamaPReview

🎯 TL;DR & Recommendation

Recommendation: Approve with suggestions.
The fix correctly addresses the permanent accent color regression by re-introducing deferred DWM writes after WPF-UI repaints, ensuring the system border remains system.

💡 Suggestions (P2)

  • src/RdpManager/WindowBorder.cs: Each call to Keep attaches a new Activated handler without removing previous ones, causing duplicate subscriptions and redundant DWM writes on every activation. This results in unnecessary overhead and could lead to future behavioral confusion. Consider managing subscriptions with a ConditionalWeakTable to ensure a single handler per window.

💡 Have feedback? We'd love to hear it in our GitHub Discussions.
✨ This review was generated by LlamaPReview Advanced, which is free for all open-source projects. Learn more.

Comment on lines +74 to +79
window.Activated += (_, __) =>
{
Apply(window);
window.Dispatcher.BeginInvoke(new Action(() => Apply(window)),
System.Windows.Threading.DispatcherPriority.ApplicationIdle);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 | Confidence: High

Each call to Keep attaches a new Activated event handler without removing any previously attached one. If Keep is invoked multiple times for the same window (e.g., when border settings are reapplied via ReapplyAll), duplicate handlers accumulate. This results in redundant synchronous and deferred Apply calls on every activation, and a growing delegate invocation list. While the immediate effect is benign (idempotent DWM writes), it violates best practices for event subscription lifecycle, adds unnecessary overhead, and could lead to confusion or subtle behavioral changes if any side‑effect is added to the handler in the future. The current fix focuses on correctness of the border color; tackling this subscription pattern would improve long‑term maintainability.

Code Suggestion:

// In the class, store a WeakReference-based map to ensure single subscription per window.
private static readonly System.Runtime.CompilerServices.ConditionalWeakTable<Window, EventHandler> _activatedHandlers = new();

public static void Keep(Window window)
{
    if (window == null) return;
    Apply(window);

    // Remove any previously attached handler (safe if none).
    if (_activatedHandlers.TryGetValue(window, out var oldHandler))
        window.Activated -= oldHandler;

    EventHandler handler = (_, __) =>
    {
        Apply(window);
        window.Dispatcher.BeginInvoke(new Action(() => Apply(window)),
            System.Windows.Threading.DispatcherPriority.ApplicationIdle);
    };
    _activatedHandlers.AddOrUpdate(window, handler);
    window.Activated += handler;

    // ... rest of Keep unchanged
}

@FilipB97
FilipB97 merged commit 2796186 into master Jul 24, 2026
8 checks passed
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.

1 participant