Skip to content

fix(ui): force full repaint when re-showing settings from tray - #3

Merged
frankieg33 merged 4 commits into
mainfrom
claude/fix-stale-redraw-on-show
May 13, 2026
Merged

fix(ui): force full repaint when re-showing settings from tray#3
frankieg33 merged 4 commits into
mainfrom
claude/fix-stale-redraw-on-show

Conversation

@frankieg33

Copy link
Copy Markdown
Owner

Summary

  • After re-opening the settings window from the tray following a long hide, the Slint software renderer was painting a stale framebuffer — leaving partially-rendered regions until the user moved the mouse over the window or the 2-second GUI refresh timer ticked.
  • request_redraw() alone wasn't enough; only a property-change on the redraw_tick sentinel marks the window dirty for that renderer. Bump it immediately after w.show(), then bump it again 50ms later in case winit hadn't finished realizing the window on the first attempt.

Test plan

  • Open Fade, leave it hidden in the tray for several minutes.
  • Click the tray icon → settings window opens cleanly with no blank regions.
  • Repeat across a few hide/show cycles with the app idle for a while between cycles.
  • Confirm normal interactive use is unaffected (no visible double-paint flicker).

Note: I couldn't reproduce live (Linux dev environment, no Windows GUI). If artifacts still appear after this lands, the next tries are a longer follow-up delay (250ms) or a brief hide/show kick.


Generated by Claude Code

The settings window backed by the Slint software renderer can paint a
stale framebuffer after a long hide-to-tray, leaving partially-rendered
regions until the user moves the mouse over the window. request_redraw
alone does not suffice — only a property-change on a sentinel marks the
window dirty.

Bump the redraw_tick sentinel (same mechanism the 2s gui refresh timer
uses) immediately after w.show(), and schedule a second bump 50ms
later in case the first redraw was processed before winit had finished
realizing the window.

@gemini-code-assist gemini-code-assist 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.

Code Review

The pull request introduces a fix for stale framebuffers when restoring a Slint window from the system tray by forcing a repaint and scheduling a delayed secondary redraw. The reviewer identified that the redraw_tick property used for opacity calculations could exceed valid ranges over time, and suggested using a modulo operation to keep the value within a safe range, ensuring long-term stability and consistency across all redraw triggers.

Comment thread src/main.rs Outdated
// can otherwise paint a stale framebuffer until the user
// moves the mouse over it — only the property change marks
// the window dirty; request_redraw alone is not enough.
w.set_redraw_tick(w.get_redraw_tick().wrapping_add(1));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The redraw_tick sentinel is used in ui/main.slint to calculate an opacity value (root.redraw-tick * 0.0001 + 0.0001). Incrementing it indefinitely will eventually cause the opacity to exceed 1.0 (after ~10,000 increments, or roughly 5.5 hours of uptime at the 2s refresh rate). If the Slint property system or renderer clamps this value, the property evaluation result may become constant, preventing change detection from triggering further repaints. Using a modulo keeps the value in a safe range while still ensuring every call marks the property as dirty. Consider applying this same change to the periodic refresh timer at line 251 for consistency.

Suggested change
w.set_redraw_tick(w.get_redraw_tick().wrapping_add(1));
w.set_redraw_tick((w.get_redraw_tick() + 1) % 100);

Comment thread src/main.rs Outdated
std::time::Duration::from_millis(50),
move || {
if let Some(w) = weak.upgrade() {
w.set_redraw_tick(w.get_redraw_tick().wrapping_add(1));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Applying the same modulo logic here for consistency and to ensure the sentinel remains effective over long periods of application uptime.

Suggested change
w.set_redraw_tick(w.get_redraw_tick().wrapping_add(1));
w.set_redraw_tick((w.get_redraw_tick() + 1) % 100);

Copy link
Copy Markdown
Owner Author

@codex review
@gemini-code-assist review


Generated by Claude Code

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request adds logic to force a full repaint when the application window is shown from the tray, addressing an issue where the software renderer might display a stale framebuffer. This is done by incrementing a 'redraw_tick' property and requesting a redraw immediately and after a 50ms delay. The review feedback suggests using a modulo operation for the tick increment to ensure it remains effective after the value exceeds the threshold where Slint's opacity clamping would prevent further UI updates.

Comment thread src/main.rs Outdated
// can otherwise paint a stale framebuffer until the user
// moves the mouse over it — only the property change marks
// the window dirty; request_redraw alone is not enough.
w.set_redraw_tick(w.get_redraw_tick().wrapping_add(1));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The redraw_tick sentinel is used in ui/main.slint to calculate opacity via root.redraw-tick * 0.0001 + 0.0001. Since Slint clamps opacity to the [0, 1] range, this hack will stop forcing repaints once the tick exceeds 10,000 (as the resulting opacity will remain 1.0). For a background app like Fade, this limit is reached in about 5.5 hours due to the 2-second refresh timer. Using a modulo ensures the property change always results in a unique visual value within the valid range.

Suggested change
w.set_redraw_tick(w.get_redraw_tick().wrapping_add(1));
w.set_redraw_tick((w.get_redraw_tick() + 1) % 1000);

Comment thread src/main.rs Outdated
std::time::Duration::from_millis(50),
move || {
if let Some(w) = weak.upgrade() {
w.set_redraw_tick(w.get_redraw_tick().wrapping_add(1));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

As noted above, using a modulo here ensures the redraw_tick remains effective for forcing repaints over long periods of time.

Suggested change
w.set_redraw_tick(w.get_redraw_tick().wrapping_add(1));
w.set_redraw_tick((w.get_redraw_tick() + 1) % 1000);

- rustfmt collapsed the single_shot closure onto one line.
- The redraw_tick property feeds an opacity expression in main.slint
  ('tick * 0.0001 + 0.0001'). Past ~10k bumps the resulting opacity
  saturates at 1.0 and Slint may stop treating it as a real change,
  defeating the sentinel. Modulo 1000 keeps the value bounded well
  below saturation while every bump still produces a unique input value.

Addresses gemini-code-assist review on PR #3.

Copy link
Copy Markdown
Owner Author

Addressed the modulo feedback in 013f912. Could you take another look?

@codex review
@gemini-code-assist review


Generated by Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request implements a mechanism to force a full repaint of the Slint window when it is restored from the system tray, addressing an issue where the software renderer might display a stale framebuffer. The implementation involves incrementing a bounded redraw_tick property and requesting a redraw both immediately and after a 50ms delay. Feedback from the review suggests refactoring this logic to avoid code duplication and replacing magic numbers with named constants to improve maintainability.

Comment thread src/main.rs Outdated
Comment on lines +192 to +203
w.set_redraw_tick((w.get_redraw_tick() + 1) % 1000);
w.window().request_redraw();
// Schedule a second redraw shortly after the OS has
// actually realized/sized the window, in case the first
// one happened before winit finished its show sequence.
let weak = w.as_weak();
slint::Timer::single_shot(std::time::Duration::from_millis(50), move || {
if let Some(w) = weak.upgrade() {
w.set_redraw_tick((w.get_redraw_tick() + 1) % 1000);
w.window().request_redraw();
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

There's some repeated logic and magic numbers here. To improve maintainability and readability, you could:

  • Extract the repaint logic into a local closure to avoid duplication.
  • Define constants for the magic numbers 1000 and 50.

This repaint logic is also used in the gui_refresh_timer. A single helper function defined at a higher scope would be an even better solution to avoid duplication across the file.

                    const REDRAW_TICK_MODULO: i32 = 1000;
                    const REPAINT_DELAY_MS: u64 = 50;

                    let force_repaint = |w: &SettingsWindow| {
                        w.set_redraw_tick((w.get_redraw_tick() + 1) % REDRAW_TICK_MODULO);
                        w.window().request_redraw();
                    };
                    force_repaint(&w);
                    // Schedule a second redraw shortly after the OS has
                    // actually realized/sized the window, in case the first
                    // one happened before winit finished its show sequence.
                    let weak = w.as_weak();
                    slint::Timer::single_shot(std::time::Duration::from_millis(REPAINT_DELAY_MS), move || {
                        if let Some(w) = weak.upgrade() {
                            force_repaint(&w);
                        }
                    });

Per gemini-code-assist review on PR #3:
- promote the redraw_tick bump + request_redraw pair into force_repaint().
- name the magic numbers (REDRAW_TICK_MODULO = 1000, POST_SHOW_REPAINT_DELAY_MS = 50)
  with one-line rationales for why each value is what it is.

No behavior change — three call sites now share the same code path.

Copy link
Copy Markdown
Owner Author

Refactor applied in 0217070 (force_repaint helper + named constants). One more look?

@codex review
@gemini-code-assist review


Generated by Claude Code

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a mechanism to address stale framebuffer issues in the Slint software renderer by implementing a force_repaint function. This function increments a redraw_tick property and requests a window redraw, ensuring the UI is marked dirty. The logic is applied immediately when showing the window and again after a 50ms delay. Feedback was provided to use wrapping_add when incrementing the tick to prevent potential integer overflow panics in debug builds.

Comment thread src/main.rs Outdated
/// property change is what marks the window dirty for the Slint software
/// renderer — request_redraw on its own is not enough after a long hide.
fn force_repaint(w: &SettingsWindow) {
w.set_redraw_tick((w.get_redraw_tick() + 1) % REDRAW_TICK_MODULO);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

To prevent a potential panic in debug builds due to integer overflow if get_redraw_tick() were to reach i32::MAX, it's safer to use wrapping_add. This makes the wrapping behavior explicit and consistent with release builds, where integer overflow wraps by default.

Suggested change
w.set_redraw_tick((w.get_redraw_tick() + 1) % REDRAW_TICK_MODULO);
w.set_redraw_tick(w.get_redraw_tick().wrapping_add(1) % REDRAW_TICK_MODULO);

The modulo invariant already keeps the tick in [0, 999], so + 1 cannot
overflow. wrapping_add costs nothing and makes the helper safe even if
someone later sets redraw_tick out of band.

Addresses gemini-code-assist review on PR #3.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@frankieg33
frankieg33 merged commit 5ccfa0c into main May 13, 2026
1 check passed
@frankieg33
frankieg33 deleted the claude/fix-stale-redraw-on-show branch May 13, 2026 20:23
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