Skip to content

feat: modernize launch splash#7210

Open
PastaPastaPasta wants to merge 4 commits intodashpay:developfrom
PastaPastaPasta:feat/improve-ui
Open

feat: modernize launch splash#7210
PastaPastaPasta wants to merge 4 commits intodashpay:developfrom
PastaPastaPasta:feat/improve-ui

Conversation

@PastaPastaPasta
Copy link
Member

Issue being fixed or feature implemented

  • Modernize the splash screen visual design and add a progress bar so users have feedback on initialization progress instead of only a static status message.

Old

Screenshot 2026-03-11 at 23 24 23

New

Screen.Recording.2026-03-11.at.23.21.01.mov

What was done?

Commit 1: Visual modernization

  • Replaced the flat rectangular splash with a modern card-style design featuring rounded corners and a translucent background
  • Redesigned layout: smaller centered logo, refined typography (28pt title, 12pt version), subtle text opacity
  • Replaced the rectangular network badge with a pill-shaped rounded badge
  • Added canvas padding around the card for compositor-based visual separation
  • Scoped QPainter lifetime to release the pixmap before timer/signal setup

Commit 2: Phase-based progress bar

  • Added a thin progress bar at the bottom of the splash card that tracks init phases
  • Built a phase weight table mapping init messages to progress ranges (e.g., "Loading block index…" = 3–75%)
  • Phases with ShowProgress callbacks (block verification, replaying, rescanning) interpolate linearly within their range
  • Phases without sub-progress use exponential time-based fill (slow curve for long phases like rescan)
  • "Verifying wallet(s)…" and "Loading wallet…" are batched as a single bar (verify = 0–20%, load = 20–100%)
  • "Rescanning…" resets the bar and uses a very slow exponential curve (tau=120s) appropriate for operations that can take hours
  • Snaps to 100% on "Done loading" since the splash is destroyed immediately after
  • Phase lookup translates keys via _() at runtime so matching works in all locales
  • messageColor is const and captured by value in signal lambdas for thread-safe cross-thread callbacks
  • Animation timer deferred until first message arrives to avoid idle repaints

How Has This Been Tested?

  • Manual testing on macOS: verified splash screen appearance, progress bar animation through full startup sequence including wallet verification and loading
  • Verified progress bar behavior with --rescan flag (bar resets and uses slow curve)
  • Confirmed "Done loading" snaps bar to 100% before splash dismissal
  • Verified no regressions in testnet/devnet badge rendering

Breaking Changes

None. Visual-only changes to the splash screen with no impact on consensus, RPC, or wallet behavior.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation
  • I have assigned this pull request to a milestone (for repository code-owners and collaborators only)

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fcf97a2ad4

ℹ️ 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".

Comment on lines +367 to +370
} else if (phase->resetsBar) {
// Long independent phases get their own full bar
displayProgress = 0.0;
phaseStart = phase->start;

Choose a reason for hiding this comment

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

P2 Badge Avoid resetting bar on each rescanning progress update

In showMessage, every message that matches a resetsBar phase unconditionally sets displayProgress = 0.0. During wallet rescans, ShowProgress emits repeated "Rescanning…" updates with increasing percentages (src/wallet/wallet.cpp emits this at startup and then periodically), so each update re-enters this block and forces the bar back to zero before it can advance, producing visible backward jumps and inaccurate progress reporting for long rescans. The reset should happen only when entering the phase, not on every progress callback.

Useful? React with 👍 / 👎.

@coderabbitai
Copy link

coderabbitai bot commented Mar 12, 2026

Walkthrough

Introduces a phase-driven, time- and progress-aware SplashScreen. Adds PhaseInfo, a phase lookup mapping init messages to phase ranges and flags (resetsBar, snapsToEnd, long phases). Adds thread-safe PostMessageAndProgress wiring to marshal messages/progress to the GUI thread, public setProgress(int) and private calcOverallProgress() APIs, an animation timer that interpolates displayProgress toward targets, and per-phase timing/state fields. Reworks painting to a rounded card layout with logo, title/version, optional network badge, and an integrated bottom progress bar; showMessage now triggers phase lookup and starts animation updates.

Sequence Diagram(s)

sequenceDiagram
  participant Core
  participant GUIThread as "GUI Thread / Event Queue"
  participant Splash as "SplashScreen"
  participant Anim as "Anim Timer"
  participant Painter as "Paint / QWidget"

  Core->>GUIThread: PostMessageAndProgress(msg, progress, color)
  GUIThread->>Splash: showMessage(msg, color)
  GUIThread->>Splash: setProgress(subProgress)
  Splash->>Splash: LookupPhase(msg) -> set phaseStart/phaseEnd/flags
  Splash->>Anim: ensure animTimer running
  Anim->>Splash: timeout tick
  Splash->>Splash: calcOverallProgress() -> update displayProgress (interpolate)
  Splash->>Painter: schedule repaint()
  Painter->>Splash: paintEvent -> draw card, logo, texts, progress bar (displayProgress)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: modernize launch splash' directly and accurately describes the main change: modernizing the splash screen's visual design and adding a progress bar.
Description check ✅ Passed The description comprehensively covers the pull request changes, including visual modernization details, phase-based progress bar implementation, testing approach, and breaking changes assessment.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan for PR comments
  • Generate coding plan

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/qt/splashscreen.cpp (1)

386-389: Assert the documented -1 or 0..100 contract in setProgress().

calcOverallProgress() assumes that range when it scales the current phase. Guarding it here makes bad signal values fail fast instead of pushing the bar outside its phase window.

Suggested fix
 void SplashScreen::setProgress(int value)
 {
+    assert(value == -1 || (value >= 0 && value <= 100));
     curProgress = value;
 }
Based on learnings, use fail-fast asserts in setters to enforce invariants and catch programmer errors early during development.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/qt/splashscreen.cpp` around lines 386 - 389, Add a fail-fast assertion in
SplashScreen::setProgress to enforce the documented contract (value == -1 or
0..100) before assigning curProgress; specifically, in the
SplashScreen::setProgress(int value) method assert that value == -1 || (value >=
0 && value <= 100) so invalid signal values are caught early
(calcOverallProgress relies on this invariant).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/qt/splashscreen.cpp`:
- Around line 357-381: The code currently reinitializes phase state every time
showMessage() is called (via PostMessageAndProgress()), causing timers and
progress bases to restart for repeated identical messages; modify the logic to
only reinitialize when the phase actually changes by comparing the
LookupPhase(message) result to a stored previous-phase pointer (e.g. add a
member like lastPhase or lastPhaseMessage and update it after changes). Only run
the block that sets displayProgress, phaseStart, phaseEnd, phaseIsLong,
animTimer.start/stop and phaseTimer.restart when the new phase pointer differs
from lastPhase; when LookupPhase(message) returns nullptr (message-only phase)
explicitly reset curProgress to 0 so exponential-progress handling isn’t
skipped, and update lastPhase appropriately. Ensure lastPhase is updated
whenever you perform a real transition.

---

Nitpick comments:
In `@src/qt/splashscreen.cpp`:
- Around line 386-389: Add a fail-fast assertion in SplashScreen::setProgress to
enforce the documented contract (value == -1 or 0..100) before assigning
curProgress; specifically, in the SplashScreen::setProgress(int value) method
assert that value == -1 || (value >= 0 && value <= 100) so invalid signal values
are caught early (calcOverallProgress relies on this invariant).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 43db32d3-f40c-47a0-b25d-c242a5afbff3

📥 Commits

Reviewing files that changed from the base of the PR and between 259cf95 and fcf97a2.

📒 Files selected for processing (2)
  • src/qt/splashscreen.cpp
  • src/qt/splashscreen.h

…yout

Redesign the splash screen with a modern card-style appearance featuring rounded corners, a translucent background with OS compositor support, and a refined visual layout with properly scoped QPainter lifetime.
Add a progress bar that tracks initialization phases with weighted time estimates. Includes smooth easing transitions, phase-aware progress for long operations like wallet loading and rescan, and snap-to-100% on completion.

- Phase lookup uses _() to translate keys at runtime for locale-safe matching

- messageColor is const, captured by value in lambdas for thread safety

- Animation timer deferred until first message to avoid idle repaints

- Time-based exponential fill capped at 95% (EXPONENTIAL_FILL_CAP)
@github-actions
Copy link

github-actions bot commented Mar 12, 2026

✅ No Merge Conflicts Detected

This PR currently has no conflicts with other open PRs.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 98333549c2

ℹ️ 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".

Comment on lines +361 to +362
if (phase != nullptr && phase != m_current_phase) {
m_current_phase = phase;

Choose a reason for hiding this comment

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

P2 Badge Reset rescanning bar when switching wallets

showMessage only reinitializes a phase when phase != m_current_phase, so resetsBar phases are not reset when the same phase text reappears later. Wallet rescans emit titles prefixed with each wallet name (src/wallet/wallet.cpp uses "<wallet> Rescanning…"), so rescanning wallet #2 still maps to the same PhaseInfo and skips this block; because the animation never moves backward unless displayProgress is explicitly zeroed, the bar remains near completion and reports incorrect progress for subsequent wallets during multi-wallet startup.

Useful? React with 👍 / 👎.

During wallet rescans, repeated ShowProgress callbacks with the same message re-entered the resetsBar block, slamming displayProgress back to 0 and restarting phaseTimer on every update. Track the current phase pointer and skip reinitialization when the phase hasn't changed.
When multiple wallets rescan at startup, each emits ShowProgress with a wallet-name-prefixed message (e.g. '[wallet2] Rescanning…'). Since these map to the same PhaseInfo entry, the phase \!= m_current_phase guard skipped reinitialization for subsequent wallets. Track the triggering message text and also reinitialize resetsBar phases when the message changes.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/qt/splashscreen.cpp (1)

394-397: Assert the setProgress() invariant at the boundary.

This slot is the single ingress for the documented -1 / 0..100 contract. Failing fast here is better than letting calcOverallProgress() accept impossible values and render misleading widths.

🛡️ Suggested change
 void SplashScreen::setProgress(int value)
 {
+    assert(value == -1 || (value >= 0 && value <= 100));
     curProgress = value;
 }
Based on learnings, use fail-fast asserts in setters to enforce invariants.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/qt/splashscreen.cpp` around lines 394 - 397, Add a fail-fast assertion to
SplashScreen::setProgress to enforce the documented contract that the incoming
value must be either -1 or within 0..100; validate the parameter at the boundary
(in SplashScreen::setProgress) and assert (or otherwise abort in debug builds)
when value is outside these allowed ranges before assigning curProgress so
calcOverallProgress() never receives impossible values.
src/qt/splashscreen.h (1)

71-80: Prefer the usual = initializer style for the new primitive members.

These POD defaults stand out from the surrounding Dash/Bitcoin Core header style, which generally uses simple = initializers for primitive members.

♻️ Suggested change
-    int curProgress{-1};
+    int curProgress = -1;
 
     // Phase-based progress tracking
-    qreal phaseStart{0.0};      // Overall progress at start of current phase
-    qreal phaseEnd{0.0};        // Overall progress at end of current phase
-    bool phaseIsLong{false};    // True for long independent phases (rescan, wallet load)
+    qreal phaseStart = 0.0;      // Overall progress at start of current phase
+    qreal phaseEnd = 0.0;        // Overall progress at end of current phase
+    bool phaseIsLong = false;    // True for long independent phases (rescan, wallet load)
     QElapsedTimer phaseTimer;    // Time since current phase started
-    const struct PhaseInfo* m_current_phase{nullptr}; // Current phase (defined in splashscreen.cpp)
+    const struct PhaseInfo* m_current_phase = nullptr; // Current phase (defined in splashscreen.cpp)
     QString m_current_phase_message;                   // Message that triggered current phase
-    qreal displayProgress{0.0}; // Smoothly animated display value (0.0-1.0)
+    qreal displayProgress = 0.0; // Smoothly animated display value (0.0-1.0)
Based on learnings, in header files prefer in-class initialization for POD types and use simple defaults for primitive members.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/qt/splashscreen.h` around lines 71 - 80, The header uses brace
initializers for primitive/POD members; change them to the usual equals style:
replace the brace initializers for curProgress, phaseStart, phaseEnd,
phaseIsLong, displayProgress and m_current_phase (and any other primitive/POD
members in the same block) to use = -1, = 0.0, = 0.0, = false, = 0.0 and =
nullptr respectively; leave complex types like QElapsedTimer and QString as-is
if desired. This keeps the class consistent with surrounding Dash/Bitcoin Core
header style and makes the defaults easier to read.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/qt/splashscreen.cpp`:
- Around line 217-230: The timeout lambda attached to animTimer never stops
until 100%, causing needless repaints when the target stalls; modify the lambda
(the QTimer::timeout connection using calcOverallProgress(), displayProgress,
and target) to stop animTimer once displayProgress reaches the current target
(use the same snap epsilon, e.g. when target - displayProgress < 0.002), i.e.
set displayProgress = target and call animTimer.stop() when close to target so
the timer halts even if overall progress < 0.999; keep update() behavior so the
final frame is painted before stopping.

---

Nitpick comments:
In `@src/qt/splashscreen.cpp`:
- Around line 394-397: Add a fail-fast assertion to SplashScreen::setProgress to
enforce the documented contract that the incoming value must be either -1 or
within 0..100; validate the parameter at the boundary (in
SplashScreen::setProgress) and assert (or otherwise abort in debug builds) when
value is outside these allowed ranges before assigning curProgress so
calcOverallProgress() never receives impossible values.

In `@src/qt/splashscreen.h`:
- Around line 71-80: The header uses brace initializers for primitive/POD
members; change them to the usual equals style: replace the brace initializers
for curProgress, phaseStart, phaseEnd, phaseIsLong, displayProgress and
m_current_phase (and any other primitive/POD members in the same block) to use =
-1, = 0.0, = 0.0, = false, = 0.0 and = nullptr respectively; leave complex types
like QElapsedTimer and QString as-is if desired. This keeps the class consistent
with surrounding Dash/Bitcoin Core header style and makes the defaults easier to
read.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 37802fed-e0b6-4484-accc-6eace07d8130

📥 Commits

Reviewing files that changed from the base of the PR and between 9833354 and 4c97eb5.

📒 Files selected for processing (2)
  • src/qt/splashscreen.cpp
  • src/qt/splashscreen.h

Comment on lines +217 to +230
connect(&animTimer, &QTimer::timeout, this, [this]() {
qreal target = calcOverallProgress();
// Smoothly animate toward target (never go backwards)
if (target > displayProgress) {
qreal gap = target - displayProgress;
// Faster easing for larger gaps so fast phases don't lag behind
qreal ease = gap > 0.1 ? 0.3 : 0.15;
displayProgress += gap * ease;
// Snap if very close
if (target - displayProgress < 0.002) displayProgress = target;
}
update();
// Stop timer once progress is complete
if (displayProgress >= 0.999) animTimer.stop();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Stop the animation timer once the current target is reached.

This lambda keeps calling update() every 30 ms and only stops at 100%. In a progress-backed phase—or if the first message does not resolve to a PHASE_TABLE entry so the target stays flat—the splash will repaint indefinitely with no visual change.

💡 Suggested change
 connect(&animTimer, &QTimer::timeout, this, [this]() {
-    qreal target = calcOverallProgress();
+    const qreal target = calcOverallProgress();
+    const bool time_driven = curProgress < 0 && phaseEnd > phaseStart;
     // Smoothly animate toward target (never go backwards)
     if (target > displayProgress) {
         qreal gap = target - displayProgress;
         // Faster easing for larger gaps so fast phases don't lag behind
         qreal ease = gap > 0.1 ? 0.3 : 0.15;
         displayProgress += gap * ease;
         // Snap if very close
         if (target - displayProgress < 0.002) displayProgress = target;
     }
-    update();
-    // Stop timer once progress is complete
-    if (displayProgress >= 0.999) animTimer.stop();
+    if (target > displayProgress || time_driven) {
+        update();
+    } else {
+        animTimer.stop();
+    }
+    if (displayProgress >= 0.999) animTimer.stop();
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/qt/splashscreen.cpp` around lines 217 - 230, The timeout lambda attached
to animTimer never stops until 100%, causing needless repaints when the target
stalls; modify the lambda (the QTimer::timeout connection using
calcOverallProgress(), displayProgress, and target) to stop animTimer once
displayProgress reaches the current target (use the same snap epsilon, e.g. when
target - displayProgress < 0.002), i.e. set displayProgress = target and call
animTimer.stop() when close to target so the timer halts even if overall
progress < 0.999; keep update() behavior so the final frame is painted before
stopping.

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