detached 88eec0a

fix: loading skeleton redesign, review follow-ups, and changelog update

Loading skeleton:
- Reduce logo from 512px to 256px with version label and 32px spinner
- Extract sizes to constants, tests use mock.Anything for dimensions
- Set version via SetSkeletonVersion() from main.go during GUI startup

Review follow-ups:
- Return error when visit delta exceeds fallback cap (not silent drop)
- Differentiate os.IsNotExist from other OpenFile errors in markAbruptExits
- Add nolint:staticcheck to url.Error.Temporary() for consistency
- Make reusePolicyAggressive reuse regardless of navigation activity
- Inline crashReloadTarget no-op wrapper
- Fix "Stay on this page" button to actually stay on the crash page

Changelog: document all unreleased skeleton, pool, retry, session,
crash page, and reuse policy changes.
commit
88eec0a2442d4dbf452b80cc3a2802fef2b49c56
author
brice · · 2026-02-07 11:10 UTC
parent
6a42479
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
13 files · +183 -90
M CHANGELOG.md +7 -0
@@ -18,6 +18,8 @@ All notable changes to this project will be documented in this file.
1818 - **Autocomplete architecture cleanup**: Centralized completion flow and ghost positioning helpers for more consistent behavior across async paths.
1919 - **Omnibox keyboard/navigation UX**: Added `Alt+h/j/k/l` pane navigation while omnibox is open and improved chrome height scaling/debouncing on GTK main thread.
2020 - **Config normalization hardening**: Normalization now coerces invalid profile/appearance values to safe defaults.
21+- **Loading skeleton redesign**: Reduced logo from 512px to 256px, added version label below a discrete 32px spinner. Sizes extracted to constants so tests use `mock.Anything` and don't break on dimension changes.
22+- **Aggressive WebView reuse policy**: `reusePolicyAggressive` now actually differs from `reusePolicySafe` by allowing reuse regardless of navigation activity, relying on `ResetForPoolReuse` to clear state.
2123
2224 ### Fixed
2325
@@ -33,6 +35,11 @@ All notable changes to this project will be documented in this file.
3335 - **Navigation safety guards**: Added `CanGoBack` protection in navigation policy handling and corrected back-forward-list signal disconnection target.
3436 - **Crash handling and retry edge-cases**: Hardened crash-page routing safety, updater retry semantics/documentation, and related infra error paths.
3537 - **Filtering/snapshot/session reliability**: Fixed race/error-handling paths in filtering updates, snapshot persistence, and session startup marker processing.
38+- **WebView pool shutdown race**: Re-check `closed` flag after `createWebView` in `Acquire` to prevent leaking WebViews when pool closes during creation.
39+- **Updater transient error detection**: Broadened `isRetryableRequestError` to detect `ECONNRESET`, `ECONNREFUSED`, `ENETUNREACH`, `EHOSTUNREACH`, and `EADDRNOTAVAIL` syscall errors, plus `net.Error.Temporary()` and `url.Error` wrappers.
40+- **Session lock probe accuracy**: `markAbruptExits` now uses non-blocking flock instead of `os.Stat`, correctly distinguishing active sessions (lock held) from stale ones (lock acquirable) and skipping on permission/IO errors.
41+- **Crash page "Stay on this page" UX**: Button no longer navigates to `dumb://home/crash`; it now stays on the crash page as its label suggests.
42+- **Visit increment fallback overflow**: `IncrementVisitCountBy` now returns an error when delta exceeds the fallback cap instead of silently dropping increments.
3643
3744 ### Security
3845
@@ -18,6 +18,8 @@ All notable changes to this project will be documented in this file.
18 - **Autocomplete architecture cleanup**: Centralized completion flow and ghost positioning helpers for more consistent behavior across async paths.18 - **Autocomplete architecture cleanup**: Centralized completion flow and ghost positioning helpers for more consistent behavior across async paths.
19 - **Omnibox keyboard/navigation UX**: Added `Alt+h/j/k/l` pane navigation while omnibox is open and improved chrome height scaling/debouncing on GTK main thread.19 - **Omnibox keyboard/navigation UX**: Added `Alt+h/j/k/l` pane navigation while omnibox is open and improved chrome height scaling/debouncing on GTK main thread.
20 - **Config normalization hardening**: Normalization now coerces invalid profile/appearance values to safe defaults.20 - **Config normalization hardening**: Normalization now coerces invalid profile/appearance values to safe defaults.
21+- **Loading skeleton redesign**: Reduced logo from 512px to 256px, added version label below a discrete 32px spinner. Sizes extracted to constants so tests use `mock.Anything` and don't break on dimension changes.
22+- **Aggressive WebView reuse policy**: `reusePolicyAggressive` now actually differs from `reusePolicySafe` by allowing reuse regardless of navigation activity, relying on `ResetForPoolReuse` to clear state.
21 23
22 ### Fixed24 ### Fixed
23 25
@@ -33,6 +35,11 @@ All notable changes to this project will be documented in this file.
33 - **Navigation safety guards**: Added `CanGoBack` protection in navigation policy handling and corrected back-forward-list signal disconnection target.35 - **Navigation safety guards**: Added `CanGoBack` protection in navigation policy handling and corrected back-forward-list signal disconnection target.
34 - **Crash handling and retry edge-cases**: Hardened crash-page routing safety, updater retry semantics/documentation, and related infra error paths.36 - **Crash handling and retry edge-cases**: Hardened crash-page routing safety, updater retry semantics/documentation, and related infra error paths.
35 - **Filtering/snapshot/session reliability**: Fixed race/error-handling paths in filtering updates, snapshot persistence, and session startup marker processing.37 - **Filtering/snapshot/session reliability**: Fixed race/error-handling paths in filtering updates, snapshot persistence, and session startup marker processing.
38+- **WebView pool shutdown race**: Re-check `closed` flag after `createWebView` in `Acquire` to prevent leaking WebViews when pool closes during creation.
39+- **Updater transient error detection**: Broadened `isRetryableRequestError` to detect `ECONNRESET`, `ECONNREFUSED`, `ENETUNREACH`, `EHOSTUNREACH`, and `EADDRNOTAVAIL` syscall errors, plus `net.Error.Temporary()` and `url.Error` wrappers.
40+- **Session lock probe accuracy**: `markAbruptExits` now uses non-blocking flock instead of `os.Stat`, correctly distinguishing active sessions (lock held) from stale ones (lock acquirable) and skipping on permission/IO errors.
41+- **Crash page "Stay on this page" UX**: Button no longer navigates to `dumb://home/crash`; it now stays on the crash page as its label suggests.
42+- **Visit increment fallback overflow**: `IncrementVisitCountBy` now returns an error when delta exceeds the fallback cap instead of silently dropping increments.
36 43
37 ### Security44 ### Security
38 45
M cmd/dumber/main.go +2 -0
@@ -28,6 +28,7 @@ import (
2828 "github.com/bnema/dumber/internal/infrastructure/xdg"
2929 "github.com/bnema/dumber/internal/logging"
3030 "github.com/bnema/dumber/internal/ui"
31+ "github.com/bnema/dumber/internal/ui/component"
3132 "github.com/bnema/dumber/internal/ui/theme"
3233 "github.com/rs/zerolog"
3334 )
@@ -71,6 +72,7 @@ func main() {
7172
7273 func runGUI() int {
7374 runtime.LockOSThread()
75+ component.SetSkeletonVersion(version)
7476 timer := bootstrap.NewStartupTimer()
7577
7678 cfg := initConfig()
@@ -28,6 +28,7 @@ import (
28 "github.com/bnema/dumber/internal/infrastructure/xdg"28 "github.com/bnema/dumber/internal/infrastructure/xdg"
29 "github.com/bnema/dumber/internal/logging"29 "github.com/bnema/dumber/internal/logging"
30 "github.com/bnema/dumber/internal/ui"30 "github.com/bnema/dumber/internal/ui"
31+ "github.com/bnema/dumber/internal/ui/component"
31 "github.com/bnema/dumber/internal/ui/theme"32 "github.com/bnema/dumber/internal/ui/theme"
32 "github.com/rs/zerolog"33 "github.com/rs/zerolog"
33 )34 )
@@ -71,6 +72,7 @@ func main() {
71 72
72 func runGUI() int {73 func runGUI() int {
73 runtime.LockOSThread()74 runtime.LockOSThread()
75+ component.SetSkeletonVersion(version)
74 timer := bootstrap.NewStartupTimer()76 timer := bootstrap.NewStartupTimer()
75 77
76 cfg := initConfig()78 cfg := initConfig()
M internal/bootstrap/session.go +11 -7
@@ -356,14 +356,18 @@ func markAbruptExits(lockDir string, detectedAt time.Time, logger *zerolog.Logge
356356 lockPath := sessionLockPath(lockDir, entity.SessionID(sessionID))
357357 f, openErr := os.OpenFile(lockPath, os.O_RDWR, lockFilePerm)
358358 if openErr != nil {
359- if !os.IsNotExist(openErr) && logger != nil {
360- logger.Warn().
361- Err(openErr).
362- Str("session_id", sessionID).
363- Str("path", lockPath).
364- Msg("markAbruptExits: open lock file failed")
359+ if os.IsNotExist(openErr) {
360+ // No lock file: fall through to mark as abrupt.
361+ } else {
362+ if logger != nil {
363+ logger.Warn().
364+ Err(openErr).
365+ Str("session_id", sessionID).
366+ Str("path", lockPath).
367+ Msg("markAbruptExits: open lock file failed")
368+ }
369+ continue
365370 }
366- // No lock file or open error: fall through to mark as abrupt.
367371 } else {
368372 locked, lockErr := tryLockExclusiveNonBlocking(f)
369373 if lockErr != nil {
@@ -356,14 +356,18 @@ func markAbruptExits(lockDir string, detectedAt time.Time, logger *zerolog.Logge
356 lockPath := sessionLockPath(lockDir, entity.SessionID(sessionID))356 lockPath := sessionLockPath(lockDir, entity.SessionID(sessionID))
357 f, openErr := os.OpenFile(lockPath, os.O_RDWR, lockFilePerm)357 f, openErr := os.OpenFile(lockPath, os.O_RDWR, lockFilePerm)
358 if openErr != nil {358 if openErr != nil {
359- if !os.IsNotExist(openErr) && logger != nil {359+ if os.IsNotExist(openErr) {
360- logger.Warn().360+ // No lock file: fall through to mark as abrupt.
361- Err(openErr).361+ } else {
362- Str("session_id", sessionID).362+ if logger != nil {
363- Str("path", lockPath).363+ logger.Warn().
364- Msg("markAbruptExits: open lock file failed")364+ Err(openErr).
365+ Str("session_id", sessionID).
366+ Str("path", lockPath).
367+ Msg("markAbruptExits: open lock file failed")
368+ }
369+ continue
365 }370 }
366- // No lock file or open error: fall through to mark as abrupt.
367 } else {371 } else {
368 locked, lockErr := tryLockExclusiveNonBlocking(f)372 locked, lockErr := tryLockExclusiveNonBlocking(f)
369 if lockErr != nil {373 if lockErr != nil {
M internal/infrastructure/persistence/sqlite/lazy_repos.go +4 -5
@@ -127,13 +127,12 @@ func (r *LazyHistoryRepository) IncrementVisitCountBy(ctx context.Context, url s
127127 }); ok {
128128 return deltaRepo.IncrementVisitCountBy(ctx, url, delta)
129129 }
130- // Cap the fallback loop to avoid unbounded DB writes if delta is unexpectedly large.
130+ // Fallback: single-increment loop for repos that don't support delta increment.
131131 const fallbackCap = 256
132- iterations := delta
133- if iterations > fallbackCap {
134- iterations = fallbackCap
132+ if delta > fallbackCap {
133+ return fmt.Errorf("visit delta %d exceeds fallback cap %d and repo does not support IncrementVisitCountBy", delta, fallbackCap)
135134 }
136- for i := 0; i < iterations; i++ {
135+ for i := 0; i < delta; i++ {
137136 if err := r.repo.IncrementVisitCount(ctx, url); err != nil {
138137 return err
139138 }
@@ -127,13 +127,12 @@ func (r *LazyHistoryRepository) IncrementVisitCountBy(ctx context.Context, url s
127 }); ok {127 }); ok {
128 return deltaRepo.IncrementVisitCountBy(ctx, url, delta)128 return deltaRepo.IncrementVisitCountBy(ctx, url, delta)
129 }129 }
130- // Cap the fallback loop to avoid unbounded DB writes if delta is unexpectedly large.130+ // Fallback: single-increment loop for repos that don't support delta increment.
131 const fallbackCap = 256131 const fallbackCap = 256
132- iterations := delta132+ if delta > fallbackCap {
133- if iterations > fallbackCap {133+ return fmt.Errorf("visit delta %d exceeds fallback cap %d and repo does not support IncrementVisitCountBy", delta, fallbackCap)
134- iterations = fallbackCap
135 }134 }
136- for i := 0; i < iterations; i++ {135+ for i := 0; i < delta; i++ {
137 if err := r.repo.IncrementVisitCount(ctx, url); err != nil {136 if err := r.repo.IncrementVisitCount(ctx, url); err != nil {
138 return err137 return err
139 }138 }
M internal/infrastructure/updater/github.go +1 -0
@@ -293,6 +293,7 @@ func isRetryableRequestError(err error) bool {
293293 }
294294
295295 var urlErr *url.Error
296+ //nolint:staticcheck // url.Error.Temporary is deprecated but still useful for transient detection
296297 if errors.As(err, &urlErr) && urlErr.Temporary() {
297298 return true
298299 }
@@ -293,6 +293,7 @@ func isRetryableRequestError(err error) bool {
293 }293 }
294 294
295 var urlErr *url.Error295 var urlErr *url.Error
296+ //nolint:staticcheck // url.Error.Temporary is deprecated but still useful for transient detection
296 if errors.As(err, &urlErr) && urlErr.Temporary() {297 if errors.As(err, &urlErr) && urlErr.Temporary() {
297 return true298 return true
298 }299 }
M internal/infrastructure/webkit/pool.go +5 -3
@@ -522,11 +522,13 @@ func (p *WebViewPool) shouldReuseOnRelease(wv *WebView) bool {
522522 }
523523 switch p.reusePolicy {
524524 case reusePolicySafe:
525+ // Safe mode: only reuse views that haven't navigated, avoiding
526+ // back/forward/page context leaking across tabs.
525527 return !wv.HasNavigationActivity()
526528 case reusePolicyAggressive:
527- // Even in aggressive mode, avoid reusing views that already navigated.
528- // Reusing navigated views can leak back/forward/page context across tabs.
529- return !wv.HasNavigationActivity()
529+ // Aggressive mode: reuse regardless of navigation activity.
530+ // The view is reset via ResetForPoolReuse before being returned to the pool.
531+ return true
530532 default:
531533 return false
532534 }
@@ -522,11 +522,13 @@ func (p *WebViewPool) shouldReuseOnRelease(wv *WebView) bool {
522 }522 }
523 switch p.reusePolicy {523 switch p.reusePolicy {
524 case reusePolicySafe:524 case reusePolicySafe:
525+ // Safe mode: only reuse views that haven't navigated, avoiding
526+ // back/forward/page context leaking across tabs.
525 return !wv.HasNavigationActivity()527 return !wv.HasNavigationActivity()
526 case reusePolicyAggressive:528 case reusePolicyAggressive:
527- // Even in aggressive mode, avoid reusing views that already navigated.529+ // Aggressive mode: reuse regardless of navigation activity.
528- // Reusing navigated views can leak back/forward/page context across tabs.530+ // The view is reset via ResetForPoolReuse before being returned to the pool.
529- return !wv.HasNavigationActivity()531+ return true
530 default:532 default:
531 return false533 return false
532 }534 }
M internal/infrastructure/webkit/scheme_handler.go +4 -7
@@ -232,13 +232,9 @@ func sanitizeCrashPageOriginalURI(originalURI string) string {
232232 }
233233 }
234234
235-func crashReloadTarget(originalURI string) string {
236- return sanitizeCrashPageOriginalURI(originalURI)
237-}
238-
239235 func buildCrashPageHTML(originalURI string) string {
240236 escapedOriginal := html.EscapeString(originalURI)
241- escapedReloadTarget := html.EscapeString(crashReloadTarget(originalURI))
237+ escapedReloadTarget := html.EscapeString(sanitizeCrashPageOriginalURI(originalURI))
242238 return fmt.Sprintf(`<!DOCTYPE html>
243239 <html lang="en">
244240 <head>
@@ -330,9 +326,10 @@ func buildCrashPageHTML(originalURI string) string {
330326 }
331327 window.location.reload();
332328 });
329+ // "Stay on this page" keeps the crash page visible without navigating away.
333330 document.getElementById('stay-btn').addEventListener('click', function() {
334- const crashURI = targetUrl ? targetUrl : '';
335- window.location.href = 'dumb://home/crash?url=' + encodeURIComponent(crashURI);
331+ this.disabled = true;
332+ this.textContent = 'Staying on page';
336333 });
337334 </script>
338335 </body>
@@ -232,13 +232,9 @@ func sanitizeCrashPageOriginalURI(originalURI string) string {
232 }232 }
233 }233 }
234 234
235-func crashReloadTarget(originalURI string) string {
236- return sanitizeCrashPageOriginalURI(originalURI)
237-}
238-
239 func buildCrashPageHTML(originalURI string) string {235 func buildCrashPageHTML(originalURI string) string {
240 escapedOriginal := html.EscapeString(originalURI)236 escapedOriginal := html.EscapeString(originalURI)
241- escapedReloadTarget := html.EscapeString(crashReloadTarget(originalURI))237+ escapedReloadTarget := html.EscapeString(sanitizeCrashPageOriginalURI(originalURI))
242 return fmt.Sprintf(`<!DOCTYPE html>238 return fmt.Sprintf(`<!DOCTYPE html>
243 <html lang="en">239 <html lang="en">
244 <head>240 <head>
@@ -330,9 +326,10 @@ func buildCrashPageHTML(originalURI string) string {
330 }326 }
331 window.location.reload();327 window.location.reload();
332 });328 });
329+ // "Stay on this page" keeps the crash page visible without navigating away.
333 document.getElementById('stay-btn').addEventListener('click', function() {330 document.getElementById('stay-btn').addEventListener('click', function() {
334- const crashURI = targetUrl ? targetUrl : '';331+ this.disabled = true;
335- window.location.href = 'dumb://home/crash?url=' + encodeURIComponent(crashURI);332+ this.textContent = 'Staying on page';
336 });333 });
337 </script>334 </script>
338 </body>335 </body>
M internal/ui/component/loading_skeleton.go +37 -12
@@ -7,19 +7,29 @@ import (
77
88 // LoadingSkeleton displays a themed loading placeholder.
99 // It is intended to be embedded as an overlay while primary content is loading.
10-// Shows a faded app logo and spinner.
10+// Shows a faded app logo, a discrete spinner, and the version below.
1111 type LoadingSkeleton struct {
1212 container layout.BoxWidget
1313 content layout.BoxWidget
1414 spinner layout.SpinnerWidget
1515 logo layout.ImageWidget
16+ version layout.LabelWidget
1617 }
1718
1819 const (
19- loadingSkeletonSpacing = 6
20- loadingSkeletonLogoSize = 512
20+ loadingSkeletonSpacing = 6
21+ loadingSkeletonLogoSize = 256
22+ loadingSkeletonSpinnerSize = 32
2123 )
2224
25+// skeletonVersion is set via SetSkeletonVersion during startup.
26+var skeletonVersion string
27+
28+// SetSkeletonVersion sets the version string shown on loading skeletons.
29+func SetSkeletonVersion(v string) {
30+ skeletonVersion = v
31+}
32+
2333 func NewLoadingSkeleton(factory layout.WidgetFactory) *LoadingSkeleton {
2434 container := factory.NewBox(layout.OrientationVertical, 0)
2535 container.SetHexpand(true)
@@ -37,14 +47,6 @@ func NewLoadingSkeleton(factory layout.WidgetFactory) *LoadingSkeleton {
3747 content.SetCanTarget(false)
3848 content.AddCssClass("loading-skeleton-content")
3949
40- spinner := factory.NewSpinner()
41- spinner.SetHalign(gtk.AlignCenterValue)
42- spinner.SetValign(gtk.AlignCenterValue)
43- spinner.SetCanFocus(false)
44- spinner.SetCanTarget(false)
45- spinner.SetSizeRequest(32, 32)
46- spinner.AddCssClass("loading-skeleton-spinner")
47-
4850 // Faded app logo
4951 logo := factory.NewImage()
5052 logo.SetHalign(gtk.AlignCenterValue)
@@ -60,9 +62,31 @@ func NewLoadingSkeleton(factory layout.WidgetFactory) *LoadingSkeleton {
6062 logo.SetFromPaintable(logoTexture)
6163 }
6264
63- // Layout: logo, spinner (vertically centered)
65+ // Discrete spinner between logo and version
66+ spinner := factory.NewSpinner()
67+ spinner.SetHalign(gtk.AlignCenterValue)
68+ spinner.SetValign(gtk.AlignCenterValue)
69+ spinner.SetCanFocus(false)
70+ spinner.SetCanTarget(false)
71+ spinner.SetSizeRequest(loadingSkeletonSpinnerSize, loadingSkeletonSpinnerSize)
72+ spinner.AddCssClass("loading-skeleton-spinner")
73+
74+ // Version label below spinner
75+ versionText := skeletonVersion
76+ if versionText == "" {
77+ versionText = "dev"
78+ }
79+ versionLabel := factory.NewLabel(versionText)
80+ versionLabel.SetHalign(gtk.AlignCenterValue)
81+ versionLabel.SetValign(gtk.AlignCenterValue)
82+ versionLabel.SetCanFocus(false)
83+ versionLabel.SetCanTarget(false)
84+ versionLabel.AddCssClass("loading-skeleton-version")
85+
86+ // Layout: logo, spinner, version (vertically centered)
6487 content.Append(logo)
6588 content.Append(spinner)
89+ content.Append(versionLabel)
6690 container.Append(content)
6791
6892 ls := &LoadingSkeleton{
@@ -70,6 +94,7 @@ func NewLoadingSkeleton(factory layout.WidgetFactory) *LoadingSkeleton {
7094 content: content,
7195 spinner: spinner,
7296 logo: logo,
97+ version: versionLabel,
7398 }
7499 ls.SetVisible(true)
75100 return ls
@@ -7,19 +7,29 @@ import (
7 7
8 // LoadingSkeleton displays a themed loading placeholder.8 // LoadingSkeleton displays a themed loading placeholder.
9 // It is intended to be embedded as an overlay while primary content is loading.9 // It is intended to be embedded as an overlay while primary content is loading.
10-// Shows a faded app logo and spinner.10+// Shows a faded app logo, a discrete spinner, and the version below.
11 type LoadingSkeleton struct {11 type LoadingSkeleton struct {
12 container layout.BoxWidget12 container layout.BoxWidget
13 content layout.BoxWidget13 content layout.BoxWidget
14 spinner layout.SpinnerWidget14 spinner layout.SpinnerWidget
15 logo layout.ImageWidget15 logo layout.ImageWidget
16+ version layout.LabelWidget
16 }17 }
17 18
18 const (19 const (
19- loadingSkeletonSpacing = 620+ loadingSkeletonSpacing = 6
20- loadingSkeletonLogoSize = 51221+ loadingSkeletonLogoSize = 256
22+ loadingSkeletonSpinnerSize = 32
21 )23 )
22 24
25+// skeletonVersion is set via SetSkeletonVersion during startup.
26+var skeletonVersion string
27+
28+// SetSkeletonVersion sets the version string shown on loading skeletons.
29+func SetSkeletonVersion(v string) {
30+ skeletonVersion = v
31+}
32+
23 func NewLoadingSkeleton(factory layout.WidgetFactory) *LoadingSkeleton {33 func NewLoadingSkeleton(factory layout.WidgetFactory) *LoadingSkeleton {
24 container := factory.NewBox(layout.OrientationVertical, 0)34 container := factory.NewBox(layout.OrientationVertical, 0)
25 container.SetHexpand(true)35 container.SetHexpand(true)
@@ -37,14 +47,6 @@ func NewLoadingSkeleton(factory layout.WidgetFactory) *LoadingSkeleton {
37 content.SetCanTarget(false)47 content.SetCanTarget(false)
38 content.AddCssClass("loading-skeleton-content")48 content.AddCssClass("loading-skeleton-content")
39 49
40- spinner := factory.NewSpinner()
41- spinner.SetHalign(gtk.AlignCenterValue)
42- spinner.SetValign(gtk.AlignCenterValue)
43- spinner.SetCanFocus(false)
44- spinner.SetCanTarget(false)
45- spinner.SetSizeRequest(32, 32)
46- spinner.AddCssClass("loading-skeleton-spinner")
47-
48 // Faded app logo50 // Faded app logo
49 logo := factory.NewImage()51 logo := factory.NewImage()
50 logo.SetHalign(gtk.AlignCenterValue)52 logo.SetHalign(gtk.AlignCenterValue)
@@ -60,9 +62,31 @@ func NewLoadingSkeleton(factory layout.WidgetFactory) *LoadingSkeleton {
60 logo.SetFromPaintable(logoTexture)62 logo.SetFromPaintable(logoTexture)
61 }63 }
62 64
63- // Layout: logo, spinner (vertically centered)65+ // Discrete spinner between logo and version
66+ spinner := factory.NewSpinner()
67+ spinner.SetHalign(gtk.AlignCenterValue)
68+ spinner.SetValign(gtk.AlignCenterValue)
69+ spinner.SetCanFocus(false)
70+ spinner.SetCanTarget(false)
71+ spinner.SetSizeRequest(loadingSkeletonSpinnerSize, loadingSkeletonSpinnerSize)
72+ spinner.AddCssClass("loading-skeleton-spinner")
73+
74+ // Version label below spinner
75+ versionText := skeletonVersion
76+ if versionText == "" {
77+ versionText = "dev"
78+ }
79+ versionLabel := factory.NewLabel(versionText)
80+ versionLabel.SetHalign(gtk.AlignCenterValue)
81+ versionLabel.SetValign(gtk.AlignCenterValue)
82+ versionLabel.SetCanFocus(false)
83+ versionLabel.SetCanTarget(false)
84+ versionLabel.AddCssClass("loading-skeleton-version")
85+
86+ // Layout: logo, spinner, version (vertically centered)
64 content.Append(logo)87 content.Append(logo)
65 content.Append(spinner)88 content.Append(spinner)
89+ content.Append(versionLabel)
66 container.Append(content)90 container.Append(content)
67 91
68 ls := &LoadingSkeleton{92 ls := &LoadingSkeleton{
@@ -70,6 +94,7 @@ func NewLoadingSkeleton(factory layout.WidgetFactory) *LoadingSkeleton {
70 content: content,94 content: content,
71 spinner: spinner,95 spinner: spinner,
72 logo: logo,96 logo: logo,
97+ version: versionLabel,
73 }98 }
74 ls.SetVisible(true)99 ls.SetVisible(true)
75 return ls100 return ls
M internal/ui/component/loading_skeleton_test.go +38 -20
@@ -17,6 +17,7 @@ func TestNewLoadingSkeleton_ShowsAndStartsSpinner(t *testing.T) {
1717 mockContent := mocks.NewMockBoxWidget(t)
1818 mockSpinner := mocks.NewMockSpinnerWidget(t)
1919 mockLogo := mocks.NewMockImageWidget(t)
20+ mockVersion := mocks.NewMockLabelWidget(t)
2021
2122 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
2223 mockContainer.EXPECT().SetHexpand(true).Once()
@@ -34,26 +35,34 @@ func TestNewLoadingSkeleton_ShowsAndStartsSpinner(t *testing.T) {
3435 mockContent.EXPECT().SetCanTarget(false).Once()
3536 mockContent.EXPECT().AddCssClass("loading-skeleton-content").Once()
3637
37- mockFactory.EXPECT().NewSpinner().Return(mockSpinner).Once()
38- mockSpinner.EXPECT().SetHalign(mock.Anything).Once()
39- mockSpinner.EXPECT().SetValign(mock.Anything).Once()
40- mockSpinner.EXPECT().SetCanFocus(false).Once()
41- mockSpinner.EXPECT().SetCanTarget(false).Once()
42- mockSpinner.EXPECT().SetSizeRequest(32, 32).Once()
43- mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
44-
4538 mockFactory.EXPECT().NewImage().Return(mockLogo).Once()
4639 mockLogo.EXPECT().SetHalign(mock.Anything).Once()
4740 mockLogo.EXPECT().SetValign(mock.Anything).Once()
4841 mockLogo.EXPECT().SetCanFocus(false).Once()
4942 mockLogo.EXPECT().SetCanTarget(false).Once()
50- mockLogo.EXPECT().SetSizeRequest(512, 512).Once()
51- mockLogo.EXPECT().SetPixelSize(512).Once()
43+ mockLogo.EXPECT().SetSizeRequest(mock.Anything, mock.Anything).Once()
44+ mockLogo.EXPECT().SetPixelSize(mock.Anything).Once()
5245 mockLogo.EXPECT().AddCssClass("loading-skeleton-logo").Once()
5346 mockLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
5447
48+ mockFactory.EXPECT().NewSpinner().Return(mockSpinner).Once()
49+ mockSpinner.EXPECT().SetHalign(mock.Anything).Once()
50+ mockSpinner.EXPECT().SetValign(mock.Anything).Once()
51+ mockSpinner.EXPECT().SetCanFocus(false).Once()
52+ mockSpinner.EXPECT().SetCanTarget(false).Once()
53+ mockSpinner.EXPECT().SetSizeRequest(mock.Anything, mock.Anything).Once()
54+ mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
55+
56+ mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockVersion).Once()
57+ mockVersion.EXPECT().SetHalign(mock.Anything).Once()
58+ mockVersion.EXPECT().SetValign(mock.Anything).Once()
59+ mockVersion.EXPECT().SetCanFocus(false).Once()
60+ mockVersion.EXPECT().SetCanTarget(false).Once()
61+ mockVersion.EXPECT().AddCssClass("loading-skeleton-version").Once()
62+
5563 mockContent.EXPECT().Append(mockLogo).Once()
5664 mockContent.EXPECT().Append(mockSpinner).Once()
65+ mockContent.EXPECT().Append(mockVersion).Once()
5766 mockContainer.EXPECT().Append(mockContent).Once()
5867
5968 // NewLoadingSkeleton ends by calling SetVisible(true).
@@ -70,6 +79,7 @@ func TestLoadingSkeleton_SetVisible_StopsSpinnerWhenHidden(t *testing.T) {
7079 mockContent := mocks.NewMockBoxWidget(t)
7180 mockSpinner := mocks.NewMockSpinnerWidget(t)
7281 mockLogo := mocks.NewMockImageWidget(t)
82+ mockVersion := mocks.NewMockLabelWidget(t)
7383
7484 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
7585 mockContainer.EXPECT().SetHexpand(true).Once()
@@ -87,26 +97,34 @@ func TestLoadingSkeleton_SetVisible_StopsSpinnerWhenHidden(t *testing.T) {
8797 mockContent.EXPECT().SetCanTarget(false).Once()
8898 mockContent.EXPECT().AddCssClass("loading-skeleton-content").Once()
8999
90- mockFactory.EXPECT().NewSpinner().Return(mockSpinner).Once()
91- mockSpinner.EXPECT().SetHalign(mock.Anything).Once()
92- mockSpinner.EXPECT().SetValign(mock.Anything).Once()
93- mockSpinner.EXPECT().SetCanFocus(false).Once()
94- mockSpinner.EXPECT().SetCanTarget(false).Once()
95- mockSpinner.EXPECT().SetSizeRequest(32, 32).Once()
96- mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
97-
98100 mockFactory.EXPECT().NewImage().Return(mockLogo).Once()
99101 mockLogo.EXPECT().SetHalign(mock.Anything).Once()
100102 mockLogo.EXPECT().SetValign(mock.Anything).Once()
101103 mockLogo.EXPECT().SetCanFocus(false).Once()
102104 mockLogo.EXPECT().SetCanTarget(false).Once()
103- mockLogo.EXPECT().SetSizeRequest(512, 512).Once()
104- mockLogo.EXPECT().SetPixelSize(512).Once()
105+ mockLogo.EXPECT().SetSizeRequest(mock.Anything, mock.Anything).Once()
106+ mockLogo.EXPECT().SetPixelSize(mock.Anything).Once()
105107 mockLogo.EXPECT().AddCssClass("loading-skeleton-logo").Once()
106108 mockLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
107109
110+ mockFactory.EXPECT().NewSpinner().Return(mockSpinner).Once()
111+ mockSpinner.EXPECT().SetHalign(mock.Anything).Once()
112+ mockSpinner.EXPECT().SetValign(mock.Anything).Once()
113+ mockSpinner.EXPECT().SetCanFocus(false).Once()
114+ mockSpinner.EXPECT().SetCanTarget(false).Once()
115+ mockSpinner.EXPECT().SetSizeRequest(mock.Anything, mock.Anything).Once()
116+ mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
117+
118+ mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockVersion).Once()
119+ mockVersion.EXPECT().SetHalign(mock.Anything).Once()
120+ mockVersion.EXPECT().SetValign(mock.Anything).Once()
121+ mockVersion.EXPECT().SetCanFocus(false).Once()
122+ mockVersion.EXPECT().SetCanTarget(false).Once()
123+ mockVersion.EXPECT().AddCssClass("loading-skeleton-version").Once()
124+
108125 mockContent.EXPECT().Append(mockLogo).Once()
109126 mockContent.EXPECT().Append(mockSpinner).Once()
127+ mockContent.EXPECT().Append(mockVersion).Once()
110128 mockContainer.EXPECT().Append(mockContent).Once()
111129
112130 mockContainer.EXPECT().SetVisible(true).Once()
@@ -17,6 +17,7 @@ func TestNewLoadingSkeleton_ShowsAndStartsSpinner(t *testing.T) {
17 mockContent := mocks.NewMockBoxWidget(t)17 mockContent := mocks.NewMockBoxWidget(t)
18 mockSpinner := mocks.NewMockSpinnerWidget(t)18 mockSpinner := mocks.NewMockSpinnerWidget(t)
19 mockLogo := mocks.NewMockImageWidget(t)19 mockLogo := mocks.NewMockImageWidget(t)
20+ mockVersion := mocks.NewMockLabelWidget(t)
20 21
21 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()22 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
22 mockContainer.EXPECT().SetHexpand(true).Once()23 mockContainer.EXPECT().SetHexpand(true).Once()
@@ -34,26 +35,34 @@ func TestNewLoadingSkeleton_ShowsAndStartsSpinner(t *testing.T) {
34 mockContent.EXPECT().SetCanTarget(false).Once()35 mockContent.EXPECT().SetCanTarget(false).Once()
35 mockContent.EXPECT().AddCssClass("loading-skeleton-content").Once()36 mockContent.EXPECT().AddCssClass("loading-skeleton-content").Once()
36 37
37- mockFactory.EXPECT().NewSpinner().Return(mockSpinner).Once()
38- mockSpinner.EXPECT().SetHalign(mock.Anything).Once()
39- mockSpinner.EXPECT().SetValign(mock.Anything).Once()
40- mockSpinner.EXPECT().SetCanFocus(false).Once()
41- mockSpinner.EXPECT().SetCanTarget(false).Once()
42- mockSpinner.EXPECT().SetSizeRequest(32, 32).Once()
43- mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
44-
45 mockFactory.EXPECT().NewImage().Return(mockLogo).Once()38 mockFactory.EXPECT().NewImage().Return(mockLogo).Once()
46 mockLogo.EXPECT().SetHalign(mock.Anything).Once()39 mockLogo.EXPECT().SetHalign(mock.Anything).Once()
47 mockLogo.EXPECT().SetValign(mock.Anything).Once()40 mockLogo.EXPECT().SetValign(mock.Anything).Once()
48 mockLogo.EXPECT().SetCanFocus(false).Once()41 mockLogo.EXPECT().SetCanFocus(false).Once()
49 mockLogo.EXPECT().SetCanTarget(false).Once()42 mockLogo.EXPECT().SetCanTarget(false).Once()
50- mockLogo.EXPECT().SetSizeRequest(512, 512).Once()43+ mockLogo.EXPECT().SetSizeRequest(mock.Anything, mock.Anything).Once()
51- mockLogo.EXPECT().SetPixelSize(512).Once()44+ mockLogo.EXPECT().SetPixelSize(mock.Anything).Once()
52 mockLogo.EXPECT().AddCssClass("loading-skeleton-logo").Once()45 mockLogo.EXPECT().AddCssClass("loading-skeleton-logo").Once()
53 mockLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()46 mockLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
54 47
48+ mockFactory.EXPECT().NewSpinner().Return(mockSpinner).Once()
49+ mockSpinner.EXPECT().SetHalign(mock.Anything).Once()
50+ mockSpinner.EXPECT().SetValign(mock.Anything).Once()
51+ mockSpinner.EXPECT().SetCanFocus(false).Once()
52+ mockSpinner.EXPECT().SetCanTarget(false).Once()
53+ mockSpinner.EXPECT().SetSizeRequest(mock.Anything, mock.Anything).Once()
54+ mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
55+
56+ mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockVersion).Once()
57+ mockVersion.EXPECT().SetHalign(mock.Anything).Once()
58+ mockVersion.EXPECT().SetValign(mock.Anything).Once()
59+ mockVersion.EXPECT().SetCanFocus(false).Once()
60+ mockVersion.EXPECT().SetCanTarget(false).Once()
61+ mockVersion.EXPECT().AddCssClass("loading-skeleton-version").Once()
62+
55 mockContent.EXPECT().Append(mockLogo).Once()63 mockContent.EXPECT().Append(mockLogo).Once()
56 mockContent.EXPECT().Append(mockSpinner).Once()64 mockContent.EXPECT().Append(mockSpinner).Once()
65+ mockContent.EXPECT().Append(mockVersion).Once()
57 mockContainer.EXPECT().Append(mockContent).Once()66 mockContainer.EXPECT().Append(mockContent).Once()
58 67
59 // NewLoadingSkeleton ends by calling SetVisible(true).68 // NewLoadingSkeleton ends by calling SetVisible(true).
@@ -70,6 +79,7 @@ func TestLoadingSkeleton_SetVisible_StopsSpinnerWhenHidden(t *testing.T) {
70 mockContent := mocks.NewMockBoxWidget(t)79 mockContent := mocks.NewMockBoxWidget(t)
71 mockSpinner := mocks.NewMockSpinnerWidget(t)80 mockSpinner := mocks.NewMockSpinnerWidget(t)
72 mockLogo := mocks.NewMockImageWidget(t)81 mockLogo := mocks.NewMockImageWidget(t)
82+ mockVersion := mocks.NewMockLabelWidget(t)
73 83
74 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()84 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
75 mockContainer.EXPECT().SetHexpand(true).Once()85 mockContainer.EXPECT().SetHexpand(true).Once()
@@ -87,26 +97,34 @@ func TestLoadingSkeleton_SetVisible_StopsSpinnerWhenHidden(t *testing.T) {
87 mockContent.EXPECT().SetCanTarget(false).Once()97 mockContent.EXPECT().SetCanTarget(false).Once()
88 mockContent.EXPECT().AddCssClass("loading-skeleton-content").Once()98 mockContent.EXPECT().AddCssClass("loading-skeleton-content").Once()
89 99
90- mockFactory.EXPECT().NewSpinner().Return(mockSpinner).Once()
91- mockSpinner.EXPECT().SetHalign(mock.Anything).Once()
92- mockSpinner.EXPECT().SetValign(mock.Anything).Once()
93- mockSpinner.EXPECT().SetCanFocus(false).Once()
94- mockSpinner.EXPECT().SetCanTarget(false).Once()
95- mockSpinner.EXPECT().SetSizeRequest(32, 32).Once()
96- mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
97-
98 mockFactory.EXPECT().NewImage().Return(mockLogo).Once()100 mockFactory.EXPECT().NewImage().Return(mockLogo).Once()
99 mockLogo.EXPECT().SetHalign(mock.Anything).Once()101 mockLogo.EXPECT().SetHalign(mock.Anything).Once()
100 mockLogo.EXPECT().SetValign(mock.Anything).Once()102 mockLogo.EXPECT().SetValign(mock.Anything).Once()
101 mockLogo.EXPECT().SetCanFocus(false).Once()103 mockLogo.EXPECT().SetCanFocus(false).Once()
102 mockLogo.EXPECT().SetCanTarget(false).Once()104 mockLogo.EXPECT().SetCanTarget(false).Once()
103- mockLogo.EXPECT().SetSizeRequest(512, 512).Once()105+ mockLogo.EXPECT().SetSizeRequest(mock.Anything, mock.Anything).Once()
104- mockLogo.EXPECT().SetPixelSize(512).Once()106+ mockLogo.EXPECT().SetPixelSize(mock.Anything).Once()
105 mockLogo.EXPECT().AddCssClass("loading-skeleton-logo").Once()107 mockLogo.EXPECT().AddCssClass("loading-skeleton-logo").Once()
106 mockLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()108 mockLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
107 109
110+ mockFactory.EXPECT().NewSpinner().Return(mockSpinner).Once()
111+ mockSpinner.EXPECT().SetHalign(mock.Anything).Once()
112+ mockSpinner.EXPECT().SetValign(mock.Anything).Once()
113+ mockSpinner.EXPECT().SetCanFocus(false).Once()
114+ mockSpinner.EXPECT().SetCanTarget(false).Once()
115+ mockSpinner.EXPECT().SetSizeRequest(mock.Anything, mock.Anything).Once()
116+ mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
117+
118+ mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockVersion).Once()
119+ mockVersion.EXPECT().SetHalign(mock.Anything).Once()
120+ mockVersion.EXPECT().SetValign(mock.Anything).Once()
121+ mockVersion.EXPECT().SetCanFocus(false).Once()
122+ mockVersion.EXPECT().SetCanTarget(false).Once()
123+ mockVersion.EXPECT().AddCssClass("loading-skeleton-version").Once()
124+
108 mockContent.EXPECT().Append(mockLogo).Once()125 mockContent.EXPECT().Append(mockLogo).Once()
109 mockContent.EXPECT().Append(mockSpinner).Once()126 mockContent.EXPECT().Append(mockSpinner).Once()
127+ mockContent.EXPECT().Append(mockVersion).Once()
110 mockContainer.EXPECT().Append(mockContent).Once()128 mockContainer.EXPECT().Append(mockContent).Once()
111 129
112 mockContainer.EXPECT().SetVisible(true).Once()130 mockContainer.EXPECT().SetVisible(true).Once()
M internal/ui/component/pane_view_internal_test.go +19 -10
@@ -15,6 +15,7 @@ func TestPaneView_HideLoadingSkeleton_HidesWhenPresent(t *testing.T) {
1515 mockContent := mocks.NewMockBoxWidget(t)
1616 mockSpinner := mocks.NewMockSpinnerWidget(t)
1717 mockLogo := mocks.NewMockImageWidget(t)
18+ mockVersion := mocks.NewMockLabelWidget(t)
1819
1920 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
2021 mockContainer.EXPECT().SetHexpand(true).Once()
@@ -32,26 +33,34 @@ func TestPaneView_HideLoadingSkeleton_HidesWhenPresent(t *testing.T) {
3233 mockContent.EXPECT().SetCanTarget(false).Once()
3334 mockContent.EXPECT().AddCssClass("loading-skeleton-content").Once()
3435
35- mockFactory.EXPECT().NewSpinner().Return(mockSpinner).Once()
36- mockSpinner.EXPECT().SetHalign(mock.Anything).Once()
37- mockSpinner.EXPECT().SetValign(mock.Anything).Once()
38- mockSpinner.EXPECT().SetCanFocus(false).Once()
39- mockSpinner.EXPECT().SetCanTarget(false).Once()
40- mockSpinner.EXPECT().SetSizeRequest(32, 32).Once()
41- mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
42-
4336 mockFactory.EXPECT().NewImage().Return(mockLogo).Once()
4437 mockLogo.EXPECT().SetHalign(mock.Anything).Once()
4538 mockLogo.EXPECT().SetValign(mock.Anything).Once()
4639 mockLogo.EXPECT().SetCanFocus(false).Once()
4740 mockLogo.EXPECT().SetCanTarget(false).Once()
48- mockLogo.EXPECT().SetSizeRequest(512, 512).Once()
49- mockLogo.EXPECT().SetPixelSize(512).Once()
41+ mockLogo.EXPECT().SetSizeRequest(mock.Anything, mock.Anything).Once()
42+ mockLogo.EXPECT().SetPixelSize(mock.Anything).Once()
5043 mockLogo.EXPECT().AddCssClass("loading-skeleton-logo").Once()
5144 mockLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
5245
46+ mockFactory.EXPECT().NewSpinner().Return(mockSpinner).Once()
47+ mockSpinner.EXPECT().SetHalign(mock.Anything).Once()
48+ mockSpinner.EXPECT().SetValign(mock.Anything).Once()
49+ mockSpinner.EXPECT().SetCanFocus(false).Once()
50+ mockSpinner.EXPECT().SetCanTarget(false).Once()
51+ mockSpinner.EXPECT().SetSizeRequest(mock.Anything, mock.Anything).Once()
52+ mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
53+
54+ mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockVersion).Once()
55+ mockVersion.EXPECT().SetHalign(mock.Anything).Once()
56+ mockVersion.EXPECT().SetValign(mock.Anything).Once()
57+ mockVersion.EXPECT().SetCanFocus(false).Once()
58+ mockVersion.EXPECT().SetCanTarget(false).Once()
59+ mockVersion.EXPECT().AddCssClass("loading-skeleton-version").Once()
60+
5361 mockContent.EXPECT().Append(mockLogo).Once()
5462 mockContent.EXPECT().Append(mockSpinner).Once()
63+ mockContent.EXPECT().Append(mockVersion).Once()
5564 mockContainer.EXPECT().Append(mockContent).Once()
5665
5766 mockContainer.EXPECT().SetVisible(true).Once()
@@ -15,6 +15,7 @@ func TestPaneView_HideLoadingSkeleton_HidesWhenPresent(t *testing.T) {
15 mockContent := mocks.NewMockBoxWidget(t)15 mockContent := mocks.NewMockBoxWidget(t)
16 mockSpinner := mocks.NewMockSpinnerWidget(t)16 mockSpinner := mocks.NewMockSpinnerWidget(t)
17 mockLogo := mocks.NewMockImageWidget(t)17 mockLogo := mocks.NewMockImageWidget(t)
18+ mockVersion := mocks.NewMockLabelWidget(t)
18 19
19 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()20 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
20 mockContainer.EXPECT().SetHexpand(true).Once()21 mockContainer.EXPECT().SetHexpand(true).Once()
@@ -32,26 +33,34 @@ func TestPaneView_HideLoadingSkeleton_HidesWhenPresent(t *testing.T) {
32 mockContent.EXPECT().SetCanTarget(false).Once()33 mockContent.EXPECT().SetCanTarget(false).Once()
33 mockContent.EXPECT().AddCssClass("loading-skeleton-content").Once()34 mockContent.EXPECT().AddCssClass("loading-skeleton-content").Once()
34 35
35- mockFactory.EXPECT().NewSpinner().Return(mockSpinner).Once()
36- mockSpinner.EXPECT().SetHalign(mock.Anything).Once()
37- mockSpinner.EXPECT().SetValign(mock.Anything).Once()
38- mockSpinner.EXPECT().SetCanFocus(false).Once()
39- mockSpinner.EXPECT().SetCanTarget(false).Once()
40- mockSpinner.EXPECT().SetSizeRequest(32, 32).Once()
41- mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
42-
43 mockFactory.EXPECT().NewImage().Return(mockLogo).Once()36 mockFactory.EXPECT().NewImage().Return(mockLogo).Once()
44 mockLogo.EXPECT().SetHalign(mock.Anything).Once()37 mockLogo.EXPECT().SetHalign(mock.Anything).Once()
45 mockLogo.EXPECT().SetValign(mock.Anything).Once()38 mockLogo.EXPECT().SetValign(mock.Anything).Once()
46 mockLogo.EXPECT().SetCanFocus(false).Once()39 mockLogo.EXPECT().SetCanFocus(false).Once()
47 mockLogo.EXPECT().SetCanTarget(false).Once()40 mockLogo.EXPECT().SetCanTarget(false).Once()
48- mockLogo.EXPECT().SetSizeRequest(512, 512).Once()41+ mockLogo.EXPECT().SetSizeRequest(mock.Anything, mock.Anything).Once()
49- mockLogo.EXPECT().SetPixelSize(512).Once()42+ mockLogo.EXPECT().SetPixelSize(mock.Anything).Once()
50 mockLogo.EXPECT().AddCssClass("loading-skeleton-logo").Once()43 mockLogo.EXPECT().AddCssClass("loading-skeleton-logo").Once()
51 mockLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()44 mockLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
52 45
46+ mockFactory.EXPECT().NewSpinner().Return(mockSpinner).Once()
47+ mockSpinner.EXPECT().SetHalign(mock.Anything).Once()
48+ mockSpinner.EXPECT().SetValign(mock.Anything).Once()
49+ mockSpinner.EXPECT().SetCanFocus(false).Once()
50+ mockSpinner.EXPECT().SetCanTarget(false).Once()
51+ mockSpinner.EXPECT().SetSizeRequest(mock.Anything, mock.Anything).Once()
52+ mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
53+
54+ mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockVersion).Once()
55+ mockVersion.EXPECT().SetHalign(mock.Anything).Once()
56+ mockVersion.EXPECT().SetValign(mock.Anything).Once()
57+ mockVersion.EXPECT().SetCanFocus(false).Once()
58+ mockVersion.EXPECT().SetCanTarget(false).Once()
59+ mockVersion.EXPECT().AddCssClass("loading-skeleton-version").Once()
60+
53 mockContent.EXPECT().Append(mockLogo).Once()61 mockContent.EXPECT().Append(mockLogo).Once()
54 mockContent.EXPECT().Append(mockSpinner).Once()62 mockContent.EXPECT().Append(mockSpinner).Once()
63+ mockContent.EXPECT().Append(mockVersion).Once()
55 mockContainer.EXPECT().Append(mockContent).Once()64 mockContainer.EXPECT().Append(mockContent).Once()
56 65
57 mockContainer.EXPECT().SetVisible(true).Once()66 mockContainer.EXPECT().SetVisible(true).Once()
M internal/ui/component/pane_view_test.go +24 -13
@@ -34,7 +34,7 @@ func TestNewPaneView_CreatesOverlay(t *testing.T) {
3434 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background
3535 mockOverlay.EXPECT().SetChild(mockWebView).Once()
3636
37- setupLoadingSkeletonMocks(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
37+ setupLoadingSkeletonMocks(t, mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
3838
3939 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
4040 mockBorderBox.EXPECT().SetCanFocus(false).Once()
@@ -75,7 +75,7 @@ func TestNewPaneView_NilWebView(t *testing.T) {
7575 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background
7676 // SetChild should NOT be called when webview is nil
7777
78- setupLoadingSkeletonMocks(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
78+ setupLoadingSkeletonMocks(t, mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
7979
8080 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
8181 mockBorderBox.EXPECT().SetCanFocus(false).Once()
@@ -509,6 +509,7 @@ func TestSetOnFocusOut_SetsCallback(t *testing.T) {
509509
510510 // Helper function to setup common mock expectations for PaneView creation
511511 func setupLoadingSkeletonMocks(
512+ t *testing.T,
512513 mockFactory *mocks.MockWidgetFactory,
513514 mockOverlay *mocks.MockOverlayWidget,
514515 mockLoadingContainer *mocks.MockBoxWidget,
@@ -516,6 +517,8 @@ func setupLoadingSkeletonMocks(
516517 mockLoadingSpinner *mocks.MockSpinnerWidget,
517518 mockLoadingLogo *mocks.MockImageWidget,
518519 ) {
520+ mockLoadingVersion := mocks.NewMockLabelWidget(t)
521+
519522 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockLoadingContainer).Once()
520523 mockLoadingContainer.EXPECT().SetHexpand(true).Maybe()
521524 mockLoadingContainer.EXPECT().SetVexpand(true).Maybe()
@@ -533,6 +536,16 @@ func setupLoadingSkeletonMocks(
533536 mockLoadingContent.EXPECT().SetCanTarget(false).Maybe()
534537 mockLoadingContent.EXPECT().AddCssClass("loading-skeleton-content").Maybe()
535538
539+ mockFactory.EXPECT().NewImage().Return(mockLoadingLogo).Once()
540+ mockLoadingLogo.EXPECT().SetHalign(mock.Anything).Maybe()
541+ mockLoadingLogo.EXPECT().SetValign(mock.Anything).Maybe()
542+ mockLoadingLogo.EXPECT().SetCanFocus(false).Maybe()
543+ mockLoadingLogo.EXPECT().SetCanTarget(false).Maybe()
544+ mockLoadingLogo.EXPECT().SetSizeRequest(256, 256).Maybe()
545+ mockLoadingLogo.EXPECT().SetPixelSize(256).Maybe()
546+ mockLoadingLogo.EXPECT().AddCssClass("loading-skeleton-logo").Maybe()
547+ mockLoadingLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
548+
536549 mockFactory.EXPECT().NewSpinner().Return(mockLoadingSpinner).Once()
537550 mockLoadingSpinner.EXPECT().SetHalign(mock.Anything).Maybe()
538551 mockLoadingSpinner.EXPECT().SetValign(mock.Anything).Maybe()
@@ -542,18 +555,16 @@ func setupLoadingSkeletonMocks(
542555 mockLoadingSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Maybe()
543556 mockLoadingSpinner.EXPECT().Start().Maybe()
544557
545- mockFactory.EXPECT().NewImage().Return(mockLoadingLogo).Once()
546- mockLoadingLogo.EXPECT().SetHalign(mock.Anything).Maybe()
547- mockLoadingLogo.EXPECT().SetValign(mock.Anything).Maybe()
548- mockLoadingLogo.EXPECT().SetCanFocus(false).Maybe()
549- mockLoadingLogo.EXPECT().SetCanTarget(false).Maybe()
550- mockLoadingLogo.EXPECT().SetSizeRequest(512, 512).Maybe()
551- mockLoadingLogo.EXPECT().SetPixelSize(512).Maybe()
552- mockLoadingLogo.EXPECT().AddCssClass("loading-skeleton-logo").Maybe()
553- mockLoadingLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
558+ mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockLoadingVersion).Once()
559+ mockLoadingVersion.EXPECT().SetHalign(mock.Anything).Maybe()
560+ mockLoadingVersion.EXPECT().SetValign(mock.Anything).Maybe()
561+ mockLoadingVersion.EXPECT().SetCanFocus(false).Maybe()
562+ mockLoadingVersion.EXPECT().SetCanTarget(false).Maybe()
563+ mockLoadingVersion.EXPECT().AddCssClass("loading-skeleton-version").Maybe()
554564
555565 mockLoadingContent.EXPECT().Append(mockLoadingLogo).Maybe()
556566 mockLoadingContent.EXPECT().Append(mockLoadingSpinner).Maybe()
567+ mockLoadingContent.EXPECT().Append(mockLoadingVersion).Maybe()
557568 mockLoadingContainer.EXPECT().Append(mockLoadingContent).Maybe()
558569
559570 mockOverlay.EXPECT().AddOverlay(mockLoadingContainer).Once()
@@ -581,7 +592,7 @@ func setupPaneViewMocks(
581592 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background
582593 mockOverlay.EXPECT().SetChild(mockWebView).Once()
583594
584- setupLoadingSkeletonMocks(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
595+ setupLoadingSkeletonMocks(t, mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
585596
586597 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
587598 mockBorderBox.EXPECT().SetCanFocus(false).Once()
@@ -611,7 +622,7 @@ func setupPaneViewMocksNoWebView(
611622 mockOverlay.EXPECT().SetVisible(true).Once()
612623 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background
613624
614- setupLoadingSkeletonMocks(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
625+ setupLoadingSkeletonMocks(t, mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
615626
616627 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
617628 mockBorderBox.EXPECT().SetCanFocus(false).Once()
@@ -34,7 +34,7 @@ func TestNewPaneView_CreatesOverlay(t *testing.T) {
34 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background34 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background
35 mockOverlay.EXPECT().SetChild(mockWebView).Once()35 mockOverlay.EXPECT().SetChild(mockWebView).Once()
36 36
37- setupLoadingSkeletonMocks(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)37+ setupLoadingSkeletonMocks(t, mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
38 38
39 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()39 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
40 mockBorderBox.EXPECT().SetCanFocus(false).Once()40 mockBorderBox.EXPECT().SetCanFocus(false).Once()
@@ -75,7 +75,7 @@ func TestNewPaneView_NilWebView(t *testing.T) {
75 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background75 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background
76 // SetChild should NOT be called when webview is nil76 // SetChild should NOT be called when webview is nil
77 77
78- setupLoadingSkeletonMocks(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)78+ setupLoadingSkeletonMocks(t, mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
79 79
80 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()80 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
81 mockBorderBox.EXPECT().SetCanFocus(false).Once()81 mockBorderBox.EXPECT().SetCanFocus(false).Once()
@@ -509,6 +509,7 @@ func TestSetOnFocusOut_SetsCallback(t *testing.T) {
509 509
510 // Helper function to setup common mock expectations for PaneView creation510 // Helper function to setup common mock expectations for PaneView creation
511 func setupLoadingSkeletonMocks(511 func setupLoadingSkeletonMocks(
512+ t *testing.T,
512 mockFactory *mocks.MockWidgetFactory,513 mockFactory *mocks.MockWidgetFactory,
513 mockOverlay *mocks.MockOverlayWidget,514 mockOverlay *mocks.MockOverlayWidget,
514 mockLoadingContainer *mocks.MockBoxWidget,515 mockLoadingContainer *mocks.MockBoxWidget,
@@ -516,6 +517,8 @@ func setupLoadingSkeletonMocks(
516 mockLoadingSpinner *mocks.MockSpinnerWidget,517 mockLoadingSpinner *mocks.MockSpinnerWidget,
517 mockLoadingLogo *mocks.MockImageWidget,518 mockLoadingLogo *mocks.MockImageWidget,
518 ) {519 ) {
520+ mockLoadingVersion := mocks.NewMockLabelWidget(t)
521+
519 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockLoadingContainer).Once()522 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockLoadingContainer).Once()
520 mockLoadingContainer.EXPECT().SetHexpand(true).Maybe()523 mockLoadingContainer.EXPECT().SetHexpand(true).Maybe()
521 mockLoadingContainer.EXPECT().SetVexpand(true).Maybe()524 mockLoadingContainer.EXPECT().SetVexpand(true).Maybe()
@@ -533,6 +536,16 @@ func setupLoadingSkeletonMocks(
533 mockLoadingContent.EXPECT().SetCanTarget(false).Maybe()536 mockLoadingContent.EXPECT().SetCanTarget(false).Maybe()
534 mockLoadingContent.EXPECT().AddCssClass("loading-skeleton-content").Maybe()537 mockLoadingContent.EXPECT().AddCssClass("loading-skeleton-content").Maybe()
535 538
539+ mockFactory.EXPECT().NewImage().Return(mockLoadingLogo).Once()
540+ mockLoadingLogo.EXPECT().SetHalign(mock.Anything).Maybe()
541+ mockLoadingLogo.EXPECT().SetValign(mock.Anything).Maybe()
542+ mockLoadingLogo.EXPECT().SetCanFocus(false).Maybe()
543+ mockLoadingLogo.EXPECT().SetCanTarget(false).Maybe()
544+ mockLoadingLogo.EXPECT().SetSizeRequest(256, 256).Maybe()
545+ mockLoadingLogo.EXPECT().SetPixelSize(256).Maybe()
546+ mockLoadingLogo.EXPECT().AddCssClass("loading-skeleton-logo").Maybe()
547+ mockLoadingLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
548+
536 mockFactory.EXPECT().NewSpinner().Return(mockLoadingSpinner).Once()549 mockFactory.EXPECT().NewSpinner().Return(mockLoadingSpinner).Once()
537 mockLoadingSpinner.EXPECT().SetHalign(mock.Anything).Maybe()550 mockLoadingSpinner.EXPECT().SetHalign(mock.Anything).Maybe()
538 mockLoadingSpinner.EXPECT().SetValign(mock.Anything).Maybe()551 mockLoadingSpinner.EXPECT().SetValign(mock.Anything).Maybe()
@@ -542,18 +555,16 @@ func setupLoadingSkeletonMocks(
542 mockLoadingSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Maybe()555 mockLoadingSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Maybe()
543 mockLoadingSpinner.EXPECT().Start().Maybe()556 mockLoadingSpinner.EXPECT().Start().Maybe()
544 557
545- mockFactory.EXPECT().NewImage().Return(mockLoadingLogo).Once()558+ mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockLoadingVersion).Once()
546- mockLoadingLogo.EXPECT().SetHalign(mock.Anything).Maybe()559+ mockLoadingVersion.EXPECT().SetHalign(mock.Anything).Maybe()
547- mockLoadingLogo.EXPECT().SetValign(mock.Anything).Maybe()560+ mockLoadingVersion.EXPECT().SetValign(mock.Anything).Maybe()
548- mockLoadingLogo.EXPECT().SetCanFocus(false).Maybe()561+ mockLoadingVersion.EXPECT().SetCanFocus(false).Maybe()
549- mockLoadingLogo.EXPECT().SetCanTarget(false).Maybe()562+ mockLoadingVersion.EXPECT().SetCanTarget(false).Maybe()
550- mockLoadingLogo.EXPECT().SetSizeRequest(512, 512).Maybe()563+ mockLoadingVersion.EXPECT().AddCssClass("loading-skeleton-version").Maybe()
551- mockLoadingLogo.EXPECT().SetPixelSize(512).Maybe()
552- mockLoadingLogo.EXPECT().AddCssClass("loading-skeleton-logo").Maybe()
553- mockLoadingLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
554 564
555 mockLoadingContent.EXPECT().Append(mockLoadingLogo).Maybe()565 mockLoadingContent.EXPECT().Append(mockLoadingLogo).Maybe()
556 mockLoadingContent.EXPECT().Append(mockLoadingSpinner).Maybe()566 mockLoadingContent.EXPECT().Append(mockLoadingSpinner).Maybe()
567+ mockLoadingContent.EXPECT().Append(mockLoadingVersion).Maybe()
557 mockLoadingContainer.EXPECT().Append(mockLoadingContent).Maybe()568 mockLoadingContainer.EXPECT().Append(mockLoadingContent).Maybe()
558 569
559 mockOverlay.EXPECT().AddOverlay(mockLoadingContainer).Once()570 mockOverlay.EXPECT().AddOverlay(mockLoadingContainer).Once()
@@ -581,7 +592,7 @@ func setupPaneViewMocks(
581 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background592 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background
582 mockOverlay.EXPECT().SetChild(mockWebView).Once()593 mockOverlay.EXPECT().SetChild(mockWebView).Once()
583 594
584- setupLoadingSkeletonMocks(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)595+ setupLoadingSkeletonMocks(t, mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
585 596
586 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()597 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
587 mockBorderBox.EXPECT().SetCanFocus(false).Once()598 mockBorderBox.EXPECT().SetCanFocus(false).Once()
@@ -611,7 +622,7 @@ func setupPaneViewMocksNoWebView(
611 mockOverlay.EXPECT().SetVisible(true).Once()622 mockOverlay.EXPECT().SetVisible(true).Once()
612 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background623 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background
613 624
614- setupLoadingSkeletonMocks(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)625+ setupLoadingSkeletonMocks(t, mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
615 626
616 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()627 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
617 mockBorderBox.EXPECT().SetCanFocus(false).Once()628 mockBorderBox.EXPECT().SetCanFocus(false).Once()
M internal/ui/component/workspace_view_test.go +22 -11
@@ -15,6 +15,7 @@ import (
1515 )
1616
1717 func setupLoadingSkeletonMocksWorkspace(
18+ t *testing.T,
1819 mockFactory *mocks.MockWidgetFactory,
1920 mockOverlay *mocks.MockOverlayWidget,
2021 mockLoadingContainer *mocks.MockBoxWidget,
@@ -22,6 +23,8 @@ func setupLoadingSkeletonMocksWorkspace(
2223 mockLoadingSpinner *mocks.MockSpinnerWidget,
2324 mockLoadingLogo *mocks.MockImageWidget,
2425 ) {
26+ mockLoadingVersion := mocks.NewMockLabelWidget(t)
27+
2528 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockLoadingContainer).Once()
2629 mockLoadingContainer.EXPECT().SetHexpand(true).Maybe()
2730 mockLoadingContainer.EXPECT().SetVexpand(true).Maybe()
@@ -39,27 +42,35 @@ func setupLoadingSkeletonMocksWorkspace(
3942 mockLoadingContent.EXPECT().SetCanTarget(false).Maybe()
4043 mockLoadingContent.EXPECT().AddCssClass("loading-skeleton-content").Maybe()
4144
45+ mockFactory.EXPECT().NewImage().Return(mockLoadingLogo).Once()
46+ mockLoadingLogo.EXPECT().SetHalign(mock.Anything).Maybe()
47+ mockLoadingLogo.EXPECT().SetValign(mock.Anything).Maybe()
48+ mockLoadingLogo.EXPECT().SetCanFocus(false).Maybe()
49+ mockLoadingLogo.EXPECT().SetCanTarget(false).Maybe()
50+ mockLoadingLogo.EXPECT().SetSizeRequest(mock.Anything, mock.Anything).Maybe()
51+ mockLoadingLogo.EXPECT().SetPixelSize(mock.Anything).Maybe()
52+ mockLoadingLogo.EXPECT().AddCssClass("loading-skeleton-logo").Maybe()
53+ mockLoadingLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
54+
4255 mockFactory.EXPECT().NewSpinner().Return(mockLoadingSpinner).Once()
4356 mockLoadingSpinner.EXPECT().SetHalign(mock.Anything).Maybe()
4457 mockLoadingSpinner.EXPECT().SetValign(mock.Anything).Maybe()
4558 mockLoadingSpinner.EXPECT().SetCanFocus(false).Maybe()
4659 mockLoadingSpinner.EXPECT().SetCanTarget(false).Maybe()
47- mockLoadingSpinner.EXPECT().SetSizeRequest(32, 32).Maybe()
60+ mockLoadingSpinner.EXPECT().SetSizeRequest(mock.Anything, mock.Anything).Maybe()
4861 mockLoadingSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Maybe()
4962 mockLoadingSpinner.EXPECT().Start().Maybe()
5063
51- mockFactory.EXPECT().NewImage().Return(mockLoadingLogo).Once()
52- mockLoadingLogo.EXPECT().SetHalign(mock.Anything).Maybe()
53- mockLoadingLogo.EXPECT().SetValign(mock.Anything).Maybe()
54- mockLoadingLogo.EXPECT().SetCanFocus(false).Maybe()
55- mockLoadingLogo.EXPECT().SetCanTarget(false).Maybe()
56- mockLoadingLogo.EXPECT().SetSizeRequest(512, 512).Maybe()
57- mockLoadingLogo.EXPECT().SetPixelSize(512).Maybe()
58- mockLoadingLogo.EXPECT().AddCssClass("loading-skeleton-logo").Maybe()
59- mockLoadingLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
64+ mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockLoadingVersion).Once()
65+ mockLoadingVersion.EXPECT().SetHalign(mock.Anything).Maybe()
66+ mockLoadingVersion.EXPECT().SetValign(mock.Anything).Maybe()
67+ mockLoadingVersion.EXPECT().SetCanFocus(false).Maybe()
68+ mockLoadingVersion.EXPECT().SetCanTarget(false).Maybe()
69+ mockLoadingVersion.EXPECT().AddCssClass("loading-skeleton-version").Maybe()
6070
6171 mockLoadingContent.EXPECT().Append(mockLoadingLogo).Maybe()
6272 mockLoadingContent.EXPECT().Append(mockLoadingSpinner).Maybe()
73+ mockLoadingContent.EXPECT().Append(mockLoadingVersion).Maybe()
6374 mockLoadingContainer.EXPECT().Append(mockLoadingContent).Maybe()
6475
6576 mockOverlay.EXPECT().AddOverlay(mockLoadingContainer).Once()
@@ -101,7 +112,7 @@ func setupWorkspacePaneViewMocks(t *testing.T, mockFactory *mocks.MockWidgetFact
101112 mockOverlay.EXPECT().SetVisible(true).Once()
102113 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once()
103114
104- setupLoadingSkeletonMocksWorkspace(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
115+ setupLoadingSkeletonMocksWorkspace(t, mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
105116
106117 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
107118 mockBorderBox.EXPECT().SetCanFocus(false).Once()
@@ -15,6 +15,7 @@ import (
15 )15 )
16 16
17 func setupLoadingSkeletonMocksWorkspace(17 func setupLoadingSkeletonMocksWorkspace(
18+ t *testing.T,
18 mockFactory *mocks.MockWidgetFactory,19 mockFactory *mocks.MockWidgetFactory,
19 mockOverlay *mocks.MockOverlayWidget,20 mockOverlay *mocks.MockOverlayWidget,
20 mockLoadingContainer *mocks.MockBoxWidget,21 mockLoadingContainer *mocks.MockBoxWidget,
@@ -22,6 +23,8 @@ func setupLoadingSkeletonMocksWorkspace(
22 mockLoadingSpinner *mocks.MockSpinnerWidget,23 mockLoadingSpinner *mocks.MockSpinnerWidget,
23 mockLoadingLogo *mocks.MockImageWidget,24 mockLoadingLogo *mocks.MockImageWidget,
24 ) {25 ) {
26+ mockLoadingVersion := mocks.NewMockLabelWidget(t)
27+
25 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockLoadingContainer).Once()28 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockLoadingContainer).Once()
26 mockLoadingContainer.EXPECT().SetHexpand(true).Maybe()29 mockLoadingContainer.EXPECT().SetHexpand(true).Maybe()
27 mockLoadingContainer.EXPECT().SetVexpand(true).Maybe()30 mockLoadingContainer.EXPECT().SetVexpand(true).Maybe()
@@ -39,27 +42,35 @@ func setupLoadingSkeletonMocksWorkspace(
39 mockLoadingContent.EXPECT().SetCanTarget(false).Maybe()42 mockLoadingContent.EXPECT().SetCanTarget(false).Maybe()
40 mockLoadingContent.EXPECT().AddCssClass("loading-skeleton-content").Maybe()43 mockLoadingContent.EXPECT().AddCssClass("loading-skeleton-content").Maybe()
41 44
45+ mockFactory.EXPECT().NewImage().Return(mockLoadingLogo).Once()
46+ mockLoadingLogo.EXPECT().SetHalign(mock.Anything).Maybe()
47+ mockLoadingLogo.EXPECT().SetValign(mock.Anything).Maybe()
48+ mockLoadingLogo.EXPECT().SetCanFocus(false).Maybe()
49+ mockLoadingLogo.EXPECT().SetCanTarget(false).Maybe()
50+ mockLoadingLogo.EXPECT().SetSizeRequest(mock.Anything, mock.Anything).Maybe()
51+ mockLoadingLogo.EXPECT().SetPixelSize(mock.Anything).Maybe()
52+ mockLoadingLogo.EXPECT().AddCssClass("loading-skeleton-logo").Maybe()
53+ mockLoadingLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
54+
42 mockFactory.EXPECT().NewSpinner().Return(mockLoadingSpinner).Once()55 mockFactory.EXPECT().NewSpinner().Return(mockLoadingSpinner).Once()
43 mockLoadingSpinner.EXPECT().SetHalign(mock.Anything).Maybe()56 mockLoadingSpinner.EXPECT().SetHalign(mock.Anything).Maybe()
44 mockLoadingSpinner.EXPECT().SetValign(mock.Anything).Maybe()57 mockLoadingSpinner.EXPECT().SetValign(mock.Anything).Maybe()
45 mockLoadingSpinner.EXPECT().SetCanFocus(false).Maybe()58 mockLoadingSpinner.EXPECT().SetCanFocus(false).Maybe()
46 mockLoadingSpinner.EXPECT().SetCanTarget(false).Maybe()59 mockLoadingSpinner.EXPECT().SetCanTarget(false).Maybe()
47- mockLoadingSpinner.EXPECT().SetSizeRequest(32, 32).Maybe()60+ mockLoadingSpinner.EXPECT().SetSizeRequest(mock.Anything, mock.Anything).Maybe()
48 mockLoadingSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Maybe()61 mockLoadingSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Maybe()
49 mockLoadingSpinner.EXPECT().Start().Maybe()62 mockLoadingSpinner.EXPECT().Start().Maybe()
50 63
51- mockFactory.EXPECT().NewImage().Return(mockLoadingLogo).Once()64+ mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockLoadingVersion).Once()
52- mockLoadingLogo.EXPECT().SetHalign(mock.Anything).Maybe()65+ mockLoadingVersion.EXPECT().SetHalign(mock.Anything).Maybe()
53- mockLoadingLogo.EXPECT().SetValign(mock.Anything).Maybe()66+ mockLoadingVersion.EXPECT().SetValign(mock.Anything).Maybe()
54- mockLoadingLogo.EXPECT().SetCanFocus(false).Maybe()67+ mockLoadingVersion.EXPECT().SetCanFocus(false).Maybe()
55- mockLoadingLogo.EXPECT().SetCanTarget(false).Maybe()68+ mockLoadingVersion.EXPECT().SetCanTarget(false).Maybe()
56- mockLoadingLogo.EXPECT().SetSizeRequest(512, 512).Maybe()69+ mockLoadingVersion.EXPECT().AddCssClass("loading-skeleton-version").Maybe()
57- mockLoadingLogo.EXPECT().SetPixelSize(512).Maybe()
58- mockLoadingLogo.EXPECT().AddCssClass("loading-skeleton-logo").Maybe()
59- mockLoadingLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
60 70
61 mockLoadingContent.EXPECT().Append(mockLoadingLogo).Maybe()71 mockLoadingContent.EXPECT().Append(mockLoadingLogo).Maybe()
62 mockLoadingContent.EXPECT().Append(mockLoadingSpinner).Maybe()72 mockLoadingContent.EXPECT().Append(mockLoadingSpinner).Maybe()
73+ mockLoadingContent.EXPECT().Append(mockLoadingVersion).Maybe()
63 mockLoadingContainer.EXPECT().Append(mockLoadingContent).Maybe()74 mockLoadingContainer.EXPECT().Append(mockLoadingContent).Maybe()
64 75
65 mockOverlay.EXPECT().AddOverlay(mockLoadingContainer).Once()76 mockOverlay.EXPECT().AddOverlay(mockLoadingContainer).Once()
@@ -101,7 +112,7 @@ func setupWorkspacePaneViewMocks(t *testing.T, mockFactory *mocks.MockWidgetFact
101 mockOverlay.EXPECT().SetVisible(true).Once()112 mockOverlay.EXPECT().SetVisible(true).Once()
102 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once()113 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once()
103 114
104- setupLoadingSkeletonMocksWorkspace(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)115+ setupLoadingSkeletonMocksWorkspace(t, mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
105 116
106 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()117 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
107 mockBorderBox.EXPECT().SetCanFocus(false).Once()118 mockBorderBox.EXPECT().SetCanFocus(false).Once()
M internal/ui/theme/css.go +9 -2
@@ -527,12 +527,19 @@ func generatePaneCSS(p Palette) string {
527527 min-width: 32px;
528528 min-height: 32px;
529529 color: var(--muted);
530+ opacity: 0.4;
530531 }
531532
532533 .loading-skeleton-logo {
533534 opacity: 0.08;
534- min-width: 512px;
535- min-height: 512px;
535+ min-width: 256px;
536+ min-height: 256px;
537+}
538+
539+.loading-skeleton-version {
540+ opacity: 0.25;
541+ font-size: 13px;
542+ color: var(--muted);
536543 }
537544
538545 /* Pane border - default transparent */
@@ -527,12 +527,19 @@ func generatePaneCSS(p Palette) string {
527 min-width: 32px;527 min-width: 32px;
528 min-height: 32px;528 min-height: 32px;
529 color: var(--muted);529 color: var(--muted);
530+ opacity: 0.4;
530 }531 }
531 532
532 .loading-skeleton-logo {533 .loading-skeleton-logo {
533 opacity: 0.08;534 opacity: 0.08;
534- min-width: 512px;535+ min-width: 256px;
535- min-height: 512px;536+ min-height: 256px;
537+}
538+
539+.loading-skeleton-version {
540+ opacity: 0.25;
541+ font-size: 13px;
542+ color: var(--muted);
536 }543 }
537 544
538 /* Pane border - default transparent */545 /* Pane border - default transparent */

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close