detached ba8a210

feat: use app logo for loading skeleton and dumb:// favicons

commit
ba8a210b3956001b3ce18fcefe900b54370e91d1
author
brice · · 2025-12-28 15:14 UTC
committer
brice · · 2025-12-28 16:16 UTC
parent
3b55bde
signature
unsigned
10 files · +196 -139
M assets/logo.svg +1 -1
@@ -1,5 +1,5 @@
11 <?xml version="1.0" encoding="UTF-8"?>
2-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="512" height="512">
33 <defs>
44 <!-- Background gradient - deeper dark -->
55 <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
@@ -1,5 +1,5 @@
1 <?xml version="1.0" encoding="UTF-8"?>1 <?xml version="1.0" encoding="UTF-8"?>
2-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">2+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="512" height="512">
3 <defs>3 <defs>
4 <!-- Background gradient - deeper dark -->4 <!-- Background gradient - deeper dark -->
5 <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">5 <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
M internal/infrastructure/favicon/service.go +24 -0
@@ -3,10 +3,18 @@ package favicon
33 import (
44 "context"
55 "fmt"
6+ "strings"
67
8+ "github.com/bnema/dumber/assets"
79 "github.com/bnema/dumber/internal/logging"
810 )
911
12+// InternalScheme is the URL scheme used for internal browser pages.
13+const InternalScheme = "dumb://"
14+
15+// InternalDomain is the pseudo-domain used for caching internal page favicons.
16+const InternalDomain = "dumb"
17+
1018 // Service implements the domain FaviconService interface.
1119 // It coordinates between the cache and fetcher components.
1220 type Service struct {
@@ -30,6 +38,11 @@ func (s *Service) Get(ctx context.Context, domain string) ([]byte, error) {
3038 return nil, nil
3139 }
3240
41+ // Internal domain returns the app logo
42+ if domain == InternalDomain {
43+ return assets.LogoSVG, nil
44+ }
45+
3346 // Check cache first (memory + disk)
3447 if data, ok := s.cache.Get(domain); ok {
3548 return data, nil
@@ -49,6 +62,17 @@ func (s *Service) Get(ctx context.Context, domain string) ([]byte, error) {
4962 return data, nil
5063 }
5164
65+// IsInternalURL checks if a URL uses the internal dumb:// scheme.
66+func IsInternalURL(url string) bool {
67+ return strings.HasPrefix(url, InternalScheme)
68+}
69+
70+// GetLogoBytes returns the raw SVG bytes of the app logo.
71+// Used for internal pages that need the logo as a favicon.
72+func GetLogoBytes() []byte {
73+ return assets.LogoSVG
74+}
75+
5276 // GetCached returns favicon bytes only if already cached (no external fetch).
5377 func (s *Service) GetCached(domain string) ([]byte, bool) {
5478 return s.cache.Get(domain)
@@ -3,10 +3,18 @@ package favicon
3 import (3 import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6+ "strings"
6 7
8+ "github.com/bnema/dumber/assets"
7 "github.com/bnema/dumber/internal/logging"9 "github.com/bnema/dumber/internal/logging"
8 )10 )
9 11
12+// InternalScheme is the URL scheme used for internal browser pages.
13+const InternalScheme = "dumb://"
14+
15+// InternalDomain is the pseudo-domain used for caching internal page favicons.
16+const InternalDomain = "dumb"
17+
10 // Service implements the domain FaviconService interface.18 // Service implements the domain FaviconService interface.
11 // It coordinates between the cache and fetcher components.19 // It coordinates between the cache and fetcher components.
12 type Service struct {20 type Service struct {
@@ -30,6 +38,11 @@ func (s *Service) Get(ctx context.Context, domain string) ([]byte, error) {
30 return nil, nil38 return nil, nil
31 }39 }
32 40
41+ // Internal domain returns the app logo
42+ if domain == InternalDomain {
43+ return assets.LogoSVG, nil
44+ }
45+
33 // Check cache first (memory + disk)46 // Check cache first (memory + disk)
34 if data, ok := s.cache.Get(domain); ok {47 if data, ok := s.cache.Get(domain); ok {
35 return data, nil48 return data, nil
@@ -49,6 +62,17 @@ func (s *Service) Get(ctx context.Context, domain string) ([]byte, error) {
49 return data, nil62 return data, nil
50 }63 }
51 64
65+// IsInternalURL checks if a URL uses the internal dumb:// scheme.
66+func IsInternalURL(url string) bool {
67+ return strings.HasPrefix(url, InternalScheme)
68+}
69+
70+// GetLogoBytes returns the raw SVG bytes of the app logo.
71+// Used for internal pages that need the logo as a favicon.
72+func GetLogoBytes() []byte {
73+ return assets.LogoSVG
74+}
75+
52 // GetCached returns favicon bytes only if already cached (no external fetch).76 // GetCached returns favicon bytes only if already cached (no external fetch).
53 func (s *Service) GetCached(domain string) ([]byte, bool) {77 func (s *Service) GetCached(domain string) ([]byte, bool) {
54 return s.cache.Get(domain)78 return s.cache.Get(domain)
M internal/ui/adapter/favicon.go +31 -0
@@ -46,19 +46,33 @@ func (a *FaviconAdapter) GetTexture(domain string) *gdk.Texture {
4646 }
4747
4848 // GetTextureByURL returns a cached texture by extracting domain from URL.
49+// For internal dumb:// URLs, returns the app logo texture.
4950 func (a *FaviconAdapter) GetTextureByURL(pageURL string) *gdk.Texture {
51+ // Handle internal dumb:// scheme URLs
52+ if favicon.IsInternalURL(pageURL) {
53+ return a.GetTexture(favicon.InternalDomain)
54+ }
5055 domain := domainurl.ExtractDomain(pageURL)
5156 return a.GetTexture(domain)
5257 }
5358
5459 // GetOrFetch retrieves a favicon texture, checking caches and fetching if needed.
5560 // The callback is invoked on the GTK main thread with the texture (or nil).
61+// For internal dumb:// URLs, returns the app logo texture.
5662 func (a *FaviconAdapter) GetOrFetch(ctx context.Context, pageURL string, callback func(*gdk.Texture)) {
5763 if callback == nil {
5864 return
5965 }
6066
6167 log := logging.FromContext(ctx)
68+
69+ // Handle internal dumb:// scheme URLs - use app logo
70+ if favicon.IsInternalURL(pageURL) {
71+ texture := a.getOrCreateLogoTexture()
72+ callback(texture)
73+ return
74+ }
75+
6276 domain := domainurl.ExtractDomain(pageURL)
6377
6478 // Check texture cache first
@@ -311,6 +325,23 @@ func (a *FaviconAdapter) ensureSizedPNG(ctx context.Context, domain string) {
311325 }
312326 }
313327
328+// getOrCreateLogoTexture returns the app logo texture, creating it if needed.
329+// The texture is cached under the InternalDomain key.
330+func (a *FaviconAdapter) getOrCreateLogoTexture() *gdk.Texture {
331+ // Check cache first
332+ if texture := a.GetTexture(favicon.InternalDomain); texture != nil {
333+ return texture
334+ }
335+
336+ // Create texture from logo bytes
337+ logoBytes := favicon.GetLogoBytes()
338+ texture := bytesToTexture(logoBytes)
339+ if texture != nil {
340+ a.setTexture(favicon.InternalDomain, texture)
341+ }
342+ return texture
343+}
344+
314345 // bytesToTexture converts raw favicon bytes to a GDK texture.
315346 func bytesToTexture(data []byte) *gdk.Texture {
316347 if len(data) == 0 {
@@ -46,19 +46,33 @@ func (a *FaviconAdapter) GetTexture(domain string) *gdk.Texture {
46 }46 }
47 47
48 // GetTextureByURL returns a cached texture by extracting domain from URL.48 // GetTextureByURL returns a cached texture by extracting domain from URL.
49+// For internal dumb:// URLs, returns the app logo texture.
49 func (a *FaviconAdapter) GetTextureByURL(pageURL string) *gdk.Texture {50 func (a *FaviconAdapter) GetTextureByURL(pageURL string) *gdk.Texture {
51+ // Handle internal dumb:// scheme URLs
52+ if favicon.IsInternalURL(pageURL) {
53+ return a.GetTexture(favicon.InternalDomain)
54+ }
50 domain := domainurl.ExtractDomain(pageURL)55 domain := domainurl.ExtractDomain(pageURL)
51 return a.GetTexture(domain)56 return a.GetTexture(domain)
52 }57 }
53 58
54 // GetOrFetch retrieves a favicon texture, checking caches and fetching if needed.59 // GetOrFetch retrieves a favicon texture, checking caches and fetching if needed.
55 // The callback is invoked on the GTK main thread with the texture (or nil).60 // The callback is invoked on the GTK main thread with the texture (or nil).
61+// For internal dumb:// URLs, returns the app logo texture.
56 func (a *FaviconAdapter) GetOrFetch(ctx context.Context, pageURL string, callback func(*gdk.Texture)) {62 func (a *FaviconAdapter) GetOrFetch(ctx context.Context, pageURL string, callback func(*gdk.Texture)) {
57 if callback == nil {63 if callback == nil {
58 return64 return
59 }65 }
60 66
61 log := logging.FromContext(ctx)67 log := logging.FromContext(ctx)
68+
69+ // Handle internal dumb:// scheme URLs - use app logo
70+ if favicon.IsInternalURL(pageURL) {
71+ texture := a.getOrCreateLogoTexture()
72+ callback(texture)
73+ return
74+ }
75+
62 domain := domainurl.ExtractDomain(pageURL)76 domain := domainurl.ExtractDomain(pageURL)
63 77
64 // Check texture cache first78 // Check texture cache first
@@ -311,6 +325,23 @@ func (a *FaviconAdapter) ensureSizedPNG(ctx context.Context, domain string) {
311 }325 }
312 }326 }
313 327
328+// getOrCreateLogoTexture returns the app logo texture, creating it if needed.
329+// The texture is cached under the InternalDomain key.
330+func (a *FaviconAdapter) getOrCreateLogoTexture() *gdk.Texture {
331+ // Check cache first
332+ if texture := a.GetTexture(favicon.InternalDomain); texture != nil {
333+ return texture
334+ }
335+
336+ // Create texture from logo bytes
337+ logoBytes := favicon.GetLogoBytes()
338+ texture := bytesToTexture(logoBytes)
339+ if texture != nil {
340+ a.setTexture(favicon.InternalDomain, texture)
341+ }
342+ return texture
343+}
344+
314 // bytesToTexture converts raw favicon bytes to a GDK texture.345 // bytesToTexture converts raw favicon bytes to a GDK texture.
315 func bytesToTexture(data []byte) *gdk.Texture {346 func bytesToTexture(data []byte) *gdk.Texture {
316 if len(data) == 0 {347 if len(data) == 0 {
M internal/ui/component/loading_skeleton.go +23 -24
@@ -1,22 +1,24 @@
11 package component
22
33 import (
4- "strings"
5-
64 "github.com/bnema/dumber/internal/ui/layout"
75 "github.com/jwijenbergh/puregotk/v4/gtk"
86 )
97
108 // LoadingSkeleton displays a themed loading placeholder.
119 // It is intended to be embedded as an overlay while primary content is loading.
10+// Shows a faded app logo and spinner.
1211 type LoadingSkeleton struct {
1312 container layout.BoxWidget
1413 content layout.BoxWidget
1514 spinner layout.SpinnerWidget
16- label layout.LabelWidget
15+ logo layout.ImageWidget
1716 }
1817
19-const loadingSkeletonSpacing = 6
18+const (
19+ loadingSkeletonSpacing = 6
20+ loadingSkeletonLogoSize = 512
21+)
2022
2123 func NewLoadingSkeleton(factory layout.WidgetFactory) *LoadingSkeleton {
2224 container := factory.NewBox(layout.OrientationVertical, 0)
@@ -43,23 +45,31 @@ func NewLoadingSkeleton(factory layout.WidgetFactory) *LoadingSkeleton {
4345 spinner.SetSizeRequest(32, 32)
4446 spinner.AddCssClass("loading-skeleton-spinner")
4547
46- text := "Loading..."
47- label := factory.NewLabel(text)
48- label.SetHalign(gtk.AlignCenterValue)
49- label.SetValign(gtk.AlignCenterValue)
50- label.SetCanFocus(false)
51- label.SetCanTarget(false)
52- label.AddCssClass("loading-skeleton-text")
48+ // Faded app logo
49+ logo := factory.NewImage()
50+ logo.SetHalign(gtk.AlignCenterValue)
51+ logo.SetValign(gtk.AlignCenterValue)
52+ logo.SetCanFocus(false)
53+ logo.SetCanTarget(false)
54+ logo.SetSizeRequest(loadingSkeletonLogoSize, loadingSkeletonLogoSize)
55+ logo.SetPixelSize(loadingSkeletonLogoSize)
56+ logo.AddCssClass("loading-skeleton-logo")
5357
58+ // Set logo texture if available
59+ if logoTexture := GetLogoTexture(); logoTexture != nil {
60+ logo.SetFromPaintable(logoTexture)
61+ }
62+
63+ // Layout: logo, spinner (vertically centered)
64+ content.Append(logo)
5465 content.Append(spinner)
55- content.Append(label)
5666 container.Append(content)
5767
5868 ls := &LoadingSkeleton{
5969 container: container,
6070 content: content,
6171 spinner: spinner,
62- label: label,
72+ logo: logo,
6373 }
6474 ls.SetVisible(true)
6575 return ls
@@ -72,17 +82,6 @@ func (ls *LoadingSkeleton) Widget() layout.Widget {
7282 return ls.container
7383 }
7484
75-func (ls *LoadingSkeleton) SetText(text string) {
76- if ls == nil || ls.label == nil {
77- return
78- }
79- trimmed := strings.TrimSpace(text)
80- if trimmed == "" {
81- trimmed = "Loading..."
82- }
83- ls.label.SetText(trimmed)
84-}
85-
8685 func (ls *LoadingSkeleton) SetVisible(visible bool) {
8786 if ls == nil || ls.container == nil {
8887 return
@@ -1,22 +1,24 @@
1 package component1 package component
2 2
3 import (3 import (
4- "strings"
5-
6 "github.com/bnema/dumber/internal/ui/layout"4 "github.com/bnema/dumber/internal/ui/layout"
7 "github.com/jwijenbergh/puregotk/v4/gtk"5 "github.com/jwijenbergh/puregotk/v4/gtk"
8 )6 )
9 7
10 // LoadingSkeleton displays a themed loading placeholder.8 // LoadingSkeleton displays a themed loading placeholder.
11 // 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.
12 type LoadingSkeleton struct {11 type LoadingSkeleton struct {
13 container layout.BoxWidget12 container layout.BoxWidget
14 content layout.BoxWidget13 content layout.BoxWidget
15 spinner layout.SpinnerWidget14 spinner layout.SpinnerWidget
16- label layout.LabelWidget15+ logo layout.ImageWidget
17 }16 }
18 17
19-const loadingSkeletonSpacing = 618+const (
19+ loadingSkeletonSpacing = 6
20+ loadingSkeletonLogoSize = 512
21+)
20 22
21 func NewLoadingSkeleton(factory layout.WidgetFactory) *LoadingSkeleton {23 func NewLoadingSkeleton(factory layout.WidgetFactory) *LoadingSkeleton {
22 container := factory.NewBox(layout.OrientationVertical, 0)24 container := factory.NewBox(layout.OrientationVertical, 0)
@@ -43,23 +45,31 @@ func NewLoadingSkeleton(factory layout.WidgetFactory) *LoadingSkeleton {
43 spinner.SetSizeRequest(32, 32)45 spinner.SetSizeRequest(32, 32)
44 spinner.AddCssClass("loading-skeleton-spinner")46 spinner.AddCssClass("loading-skeleton-spinner")
45 47
46- text := "Loading..."48+ // Faded app logo
47- label := factory.NewLabel(text)49+ logo := factory.NewImage()
48- label.SetHalign(gtk.AlignCenterValue)50+ logo.SetHalign(gtk.AlignCenterValue)
49- label.SetValign(gtk.AlignCenterValue)51+ logo.SetValign(gtk.AlignCenterValue)
50- label.SetCanFocus(false)52+ logo.SetCanFocus(false)
51- label.SetCanTarget(false)53+ logo.SetCanTarget(false)
52- label.AddCssClass("loading-skeleton-text")54+ logo.SetSizeRequest(loadingSkeletonLogoSize, loadingSkeletonLogoSize)
55+ logo.SetPixelSize(loadingSkeletonLogoSize)
56+ logo.AddCssClass("loading-skeleton-logo")
53 57
58+ // Set logo texture if available
59+ if logoTexture := GetLogoTexture(); logoTexture != nil {
60+ logo.SetFromPaintable(logoTexture)
61+ }
62+
63+ // Layout: logo, spinner (vertically centered)
64+ content.Append(logo)
54 content.Append(spinner)65 content.Append(spinner)
55- content.Append(label)
56 container.Append(content)66 container.Append(content)
57 67
58 ls := &LoadingSkeleton{68 ls := &LoadingSkeleton{
59 container: container,69 container: container,
60 content: content,70 content: content,
61 spinner: spinner,71 spinner: spinner,
62- label: label,72+ logo: logo,
63 }73 }
64 ls.SetVisible(true)74 ls.SetVisible(true)
65 return ls75 return ls
@@ -72,17 +82,6 @@ func (ls *LoadingSkeleton) Widget() layout.Widget {
72 return ls.container82 return ls.container
73 }83 }
74 84
75-func (ls *LoadingSkeleton) SetText(text string) {
76- if ls == nil || ls.label == nil {
77- return
78- }
79- trimmed := strings.TrimSpace(text)
80- if trimmed == "" {
81- trimmed = "Loading..."
82- }
83- ls.label.SetText(trimmed)
84-}
85-
86 func (ls *LoadingSkeleton) SetVisible(visible bool) {85 func (ls *LoadingSkeleton) SetVisible(visible bool) {
87 if ls == nil || ls.container == nil {86 if ls == nil || ls.container == nil {
88 return87 return
M internal/ui/component/loading_skeleton_test.go +24 -73
@@ -16,7 +16,7 @@ func TestNewLoadingSkeleton_ShowsAndStartsSpinner(t *testing.T) {
1616 mockContainer := mocks.NewMockBoxWidget(t)
1717 mockContent := mocks.NewMockBoxWidget(t)
1818 mockSpinner := mocks.NewMockSpinnerWidget(t)
19- mockLabel := mocks.NewMockLabelWidget(t)
19+ mockLogo := mocks.NewMockImageWidget(t)
2020
2121 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
2222 mockContainer.EXPECT().SetHexpand(true).Once()
@@ -42,15 +42,18 @@ func TestNewLoadingSkeleton_ShowsAndStartsSpinner(t *testing.T) {
4242 mockSpinner.EXPECT().SetSizeRequest(32, 32).Once()
4343 mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
4444
45- mockFactory.EXPECT().NewLabel("Loading...").Return(mockLabel).Once()
46- mockLabel.EXPECT().SetHalign(mock.Anything).Once()
47- mockLabel.EXPECT().SetValign(mock.Anything).Once()
48- mockLabel.EXPECT().SetCanFocus(false).Once()
49- mockLabel.EXPECT().SetCanTarget(false).Once()
50- mockLabel.EXPECT().AddCssClass("loading-skeleton-text").Once()
51-
45+ mockFactory.EXPECT().NewImage().Return(mockLogo).Once()
46+ mockLogo.EXPECT().SetHalign(mock.Anything).Once()
47+ mockLogo.EXPECT().SetValign(mock.Anything).Once()
48+ mockLogo.EXPECT().SetCanFocus(false).Once()
49+ mockLogo.EXPECT().SetCanTarget(false).Once()
50+ mockLogo.EXPECT().SetSizeRequest(512, 512).Once()
51+ mockLogo.EXPECT().SetPixelSize(512).Once()
52+ mockLogo.EXPECT().AddCssClass("loading-skeleton-logo").Once()
53+ mockLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
54+
55+ mockContent.EXPECT().Append(mockLogo).Once()
5256 mockContent.EXPECT().Append(mockSpinner).Once()
53- mockContent.EXPECT().Append(mockLabel).Once()
5457 mockContainer.EXPECT().Append(mockContent).Once()
5558
5659 // NewLoadingSkeleton ends by calling SetVisible(true).
@@ -61,67 +64,12 @@ func TestNewLoadingSkeleton_ShowsAndStartsSpinner(t *testing.T) {
6164 require.NotNil(t, ls)
6265 }
6366
64-func TestLoadingSkeleton_SetText_TrimsAndDefaults(t *testing.T) {
65- mockFactory := mocks.NewMockWidgetFactory(t)
66- mockContainer := mocks.NewMockBoxWidget(t)
67- mockContent := mocks.NewMockBoxWidget(t)
68- mockSpinner := mocks.NewMockSpinnerWidget(t)
69- mockLabel := mocks.NewMockLabelWidget(t)
70-
71- mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
72- mockContainer.EXPECT().SetHexpand(true).Once()
73- mockContainer.EXPECT().SetVexpand(true).Once()
74- mockContainer.EXPECT().SetHalign(mock.Anything).Once()
75- mockContainer.EXPECT().SetValign(mock.Anything).Once()
76- mockContainer.EXPECT().SetCanFocus(false).Once()
77- mockContainer.EXPECT().SetCanTarget(false).Once()
78- mockContainer.EXPECT().AddCssClass("loading-skeleton").Once()
79-
80- mockFactory.EXPECT().NewBox(layout.OrientationVertical, 6).Return(mockContent).Once()
81- mockContent.EXPECT().SetHalign(mock.Anything).Once()
82- mockContent.EXPECT().SetValign(mock.Anything).Once()
83- mockContent.EXPECT().SetCanFocus(false).Once()
84- mockContent.EXPECT().SetCanTarget(false).Once()
85- mockContent.EXPECT().AddCssClass("loading-skeleton-content").Once()
86-
87- mockFactory.EXPECT().NewSpinner().Return(mockSpinner).Once()
88- mockSpinner.EXPECT().SetHalign(mock.Anything).Once()
89- mockSpinner.EXPECT().SetValign(mock.Anything).Once()
90- mockSpinner.EXPECT().SetCanFocus(false).Once()
91- mockSpinner.EXPECT().SetCanTarget(false).Once()
92- mockSpinner.EXPECT().SetSizeRequest(32, 32).Once()
93- mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
94-
95- mockFactory.EXPECT().NewLabel("Loading...").Return(mockLabel).Once()
96- mockLabel.EXPECT().SetHalign(mock.Anything).Once()
97- mockLabel.EXPECT().SetValign(mock.Anything).Once()
98- mockLabel.EXPECT().SetCanFocus(false).Once()
99- mockLabel.EXPECT().SetCanTarget(false).Once()
100- mockLabel.EXPECT().AddCssClass("loading-skeleton-text").Once()
101-
102- mockContent.EXPECT().Append(mockSpinner).Once()
103- mockContent.EXPECT().Append(mockLabel).Once()
104- mockContainer.EXPECT().Append(mockContent).Once()
105-
106- mockContainer.EXPECT().SetVisible(true).Once()
107- mockSpinner.EXPECT().Start().Once()
108-
109- ls := component.NewLoadingSkeleton(mockFactory)
110- require.NotNil(t, ls)
111-
112- mockLabel.EXPECT().SetText("Hello").Once()
113- ls.SetText(" Hello\n")
114-
115- mockLabel.EXPECT().SetText("Loading...").Once()
116- ls.SetText(" \t ")
117-}
118-
11967 func TestLoadingSkeleton_SetVisible_StopsSpinnerWhenHidden(t *testing.T) {
12068 mockFactory := mocks.NewMockWidgetFactory(t)
12169 mockContainer := mocks.NewMockBoxWidget(t)
12270 mockContent := mocks.NewMockBoxWidget(t)
12371 mockSpinner := mocks.NewMockSpinnerWidget(t)
124- mockLabel := mocks.NewMockLabelWidget(t)
72+ mockLogo := mocks.NewMockImageWidget(t)
12573
12674 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
12775 mockContainer.EXPECT().SetHexpand(true).Once()
@@ -147,15 +95,18 @@ func TestLoadingSkeleton_SetVisible_StopsSpinnerWhenHidden(t *testing.T) {
14795 mockSpinner.EXPECT().SetSizeRequest(32, 32).Once()
14896 mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
14997
150- mockFactory.EXPECT().NewLabel("Loading...").Return(mockLabel).Once()
151- mockLabel.EXPECT().SetHalign(mock.Anything).Once()
152- mockLabel.EXPECT().SetValign(mock.Anything).Once()
153- mockLabel.EXPECT().SetCanFocus(false).Once()
154- mockLabel.EXPECT().SetCanTarget(false).Once()
155- mockLabel.EXPECT().AddCssClass("loading-skeleton-text").Once()
156-
98+ mockFactory.EXPECT().NewImage().Return(mockLogo).Once()
99+ mockLogo.EXPECT().SetHalign(mock.Anything).Once()
100+ mockLogo.EXPECT().SetValign(mock.Anything).Once()
101+ mockLogo.EXPECT().SetCanFocus(false).Once()
102+ mockLogo.EXPECT().SetCanTarget(false).Once()
103+ mockLogo.EXPECT().SetSizeRequest(512, 512).Once()
104+ mockLogo.EXPECT().SetPixelSize(512).Once()
105+ mockLogo.EXPECT().AddCssClass("loading-skeleton-logo").Once()
106+ mockLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
107+
108+ mockContent.EXPECT().Append(mockLogo).Once()
157109 mockContent.EXPECT().Append(mockSpinner).Once()
158- mockContent.EXPECT().Append(mockLabel).Once()
159110 mockContainer.EXPECT().Append(mockContent).Once()
160111
161112 mockContainer.EXPECT().SetVisible(true).Once()
@@ -16,7 +16,7 @@ func TestNewLoadingSkeleton_ShowsAndStartsSpinner(t *testing.T) {
16 mockContainer := mocks.NewMockBoxWidget(t)16 mockContainer := mocks.NewMockBoxWidget(t)
17 mockContent := mocks.NewMockBoxWidget(t)17 mockContent := mocks.NewMockBoxWidget(t)
18 mockSpinner := mocks.NewMockSpinnerWidget(t)18 mockSpinner := mocks.NewMockSpinnerWidget(t)
19- mockLabel := mocks.NewMockLabelWidget(t)19+ mockLogo := mocks.NewMockImageWidget(t)
20 20
21 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()21 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
22 mockContainer.EXPECT().SetHexpand(true).Once()22 mockContainer.EXPECT().SetHexpand(true).Once()
@@ -42,15 +42,18 @@ func TestNewLoadingSkeleton_ShowsAndStartsSpinner(t *testing.T) {
42 mockSpinner.EXPECT().SetSizeRequest(32, 32).Once()42 mockSpinner.EXPECT().SetSizeRequest(32, 32).Once()
43 mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()43 mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
44 44
45- mockFactory.EXPECT().NewLabel("Loading...").Return(mockLabel).Once()45+ mockFactory.EXPECT().NewImage().Return(mockLogo).Once()
46- mockLabel.EXPECT().SetHalign(mock.Anything).Once()46+ mockLogo.EXPECT().SetHalign(mock.Anything).Once()
47- mockLabel.EXPECT().SetValign(mock.Anything).Once()47+ mockLogo.EXPECT().SetValign(mock.Anything).Once()
48- mockLabel.EXPECT().SetCanFocus(false).Once()48+ mockLogo.EXPECT().SetCanFocus(false).Once()
49- mockLabel.EXPECT().SetCanTarget(false).Once()49+ mockLogo.EXPECT().SetCanTarget(false).Once()
50- mockLabel.EXPECT().AddCssClass("loading-skeleton-text").Once()50+ mockLogo.EXPECT().SetSizeRequest(512, 512).Once()
51-51+ mockLogo.EXPECT().SetPixelSize(512).Once()
52+ mockLogo.EXPECT().AddCssClass("loading-skeleton-logo").Once()
53+ mockLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
54+
55+ mockContent.EXPECT().Append(mockLogo).Once()
52 mockContent.EXPECT().Append(mockSpinner).Once()56 mockContent.EXPECT().Append(mockSpinner).Once()
53- mockContent.EXPECT().Append(mockLabel).Once()
54 mockContainer.EXPECT().Append(mockContent).Once()57 mockContainer.EXPECT().Append(mockContent).Once()
55 58
56 // NewLoadingSkeleton ends by calling SetVisible(true).59 // NewLoadingSkeleton ends by calling SetVisible(true).
@@ -61,67 +64,12 @@ func TestNewLoadingSkeleton_ShowsAndStartsSpinner(t *testing.T) {
61 require.NotNil(t, ls)64 require.NotNil(t, ls)
62 }65 }
63 66
64-func TestLoadingSkeleton_SetText_TrimsAndDefaults(t *testing.T) {
65- mockFactory := mocks.NewMockWidgetFactory(t)
66- mockContainer := mocks.NewMockBoxWidget(t)
67- mockContent := mocks.NewMockBoxWidget(t)
68- mockSpinner := mocks.NewMockSpinnerWidget(t)
69- mockLabel := mocks.NewMockLabelWidget(t)
70-
71- mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
72- mockContainer.EXPECT().SetHexpand(true).Once()
73- mockContainer.EXPECT().SetVexpand(true).Once()
74- mockContainer.EXPECT().SetHalign(mock.Anything).Once()
75- mockContainer.EXPECT().SetValign(mock.Anything).Once()
76- mockContainer.EXPECT().SetCanFocus(false).Once()
77- mockContainer.EXPECT().SetCanTarget(false).Once()
78- mockContainer.EXPECT().AddCssClass("loading-skeleton").Once()
79-
80- mockFactory.EXPECT().NewBox(layout.OrientationVertical, 6).Return(mockContent).Once()
81- mockContent.EXPECT().SetHalign(mock.Anything).Once()
82- mockContent.EXPECT().SetValign(mock.Anything).Once()
83- mockContent.EXPECT().SetCanFocus(false).Once()
84- mockContent.EXPECT().SetCanTarget(false).Once()
85- mockContent.EXPECT().AddCssClass("loading-skeleton-content").Once()
86-
87- mockFactory.EXPECT().NewSpinner().Return(mockSpinner).Once()
88- mockSpinner.EXPECT().SetHalign(mock.Anything).Once()
89- mockSpinner.EXPECT().SetValign(mock.Anything).Once()
90- mockSpinner.EXPECT().SetCanFocus(false).Once()
91- mockSpinner.EXPECT().SetCanTarget(false).Once()
92- mockSpinner.EXPECT().SetSizeRequest(32, 32).Once()
93- mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
94-
95- mockFactory.EXPECT().NewLabel("Loading...").Return(mockLabel).Once()
96- mockLabel.EXPECT().SetHalign(mock.Anything).Once()
97- mockLabel.EXPECT().SetValign(mock.Anything).Once()
98- mockLabel.EXPECT().SetCanFocus(false).Once()
99- mockLabel.EXPECT().SetCanTarget(false).Once()
100- mockLabel.EXPECT().AddCssClass("loading-skeleton-text").Once()
101-
102- mockContent.EXPECT().Append(mockSpinner).Once()
103- mockContent.EXPECT().Append(mockLabel).Once()
104- mockContainer.EXPECT().Append(mockContent).Once()
105-
106- mockContainer.EXPECT().SetVisible(true).Once()
107- mockSpinner.EXPECT().Start().Once()
108-
109- ls := component.NewLoadingSkeleton(mockFactory)
110- require.NotNil(t, ls)
111-
112- mockLabel.EXPECT().SetText("Hello").Once()
113- ls.SetText(" Hello\n")
114-
115- mockLabel.EXPECT().SetText("Loading...").Once()
116- ls.SetText(" \t ")
117-}
118-
119 func TestLoadingSkeleton_SetVisible_StopsSpinnerWhenHidden(t *testing.T) {67 func TestLoadingSkeleton_SetVisible_StopsSpinnerWhenHidden(t *testing.T) {
120 mockFactory := mocks.NewMockWidgetFactory(t)68 mockFactory := mocks.NewMockWidgetFactory(t)
121 mockContainer := mocks.NewMockBoxWidget(t)69 mockContainer := mocks.NewMockBoxWidget(t)
122 mockContent := mocks.NewMockBoxWidget(t)70 mockContent := mocks.NewMockBoxWidget(t)
123 mockSpinner := mocks.NewMockSpinnerWidget(t)71 mockSpinner := mocks.NewMockSpinnerWidget(t)
124- mockLabel := mocks.NewMockLabelWidget(t)72+ mockLogo := mocks.NewMockImageWidget(t)
125 73
126 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()74 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
127 mockContainer.EXPECT().SetHexpand(true).Once()75 mockContainer.EXPECT().SetHexpand(true).Once()
@@ -147,15 +95,18 @@ func TestLoadingSkeleton_SetVisible_StopsSpinnerWhenHidden(t *testing.T) {
147 mockSpinner.EXPECT().SetSizeRequest(32, 32).Once()95 mockSpinner.EXPECT().SetSizeRequest(32, 32).Once()
148 mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()96 mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
149 97
150- mockFactory.EXPECT().NewLabel("Loading...").Return(mockLabel).Once()98+ mockFactory.EXPECT().NewImage().Return(mockLogo).Once()
151- mockLabel.EXPECT().SetHalign(mock.Anything).Once()99+ mockLogo.EXPECT().SetHalign(mock.Anything).Once()
152- mockLabel.EXPECT().SetValign(mock.Anything).Once()100+ mockLogo.EXPECT().SetValign(mock.Anything).Once()
153- mockLabel.EXPECT().SetCanFocus(false).Once()101+ mockLogo.EXPECT().SetCanFocus(false).Once()
154- mockLabel.EXPECT().SetCanTarget(false).Once()102+ mockLogo.EXPECT().SetCanTarget(false).Once()
155- mockLabel.EXPECT().AddCssClass("loading-skeleton-text").Once()103+ mockLogo.EXPECT().SetSizeRequest(512, 512).Once()
156-104+ mockLogo.EXPECT().SetPixelSize(512).Once()
105+ mockLogo.EXPECT().AddCssClass("loading-skeleton-logo").Once()
106+ mockLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
107+
108+ mockContent.EXPECT().Append(mockLogo).Once()
157 mockContent.EXPECT().Append(mockSpinner).Once()109 mockContent.EXPECT().Append(mockSpinner).Once()
158- mockContent.EXPECT().Append(mockLabel).Once()
159 mockContainer.EXPECT().Append(mockContent).Once()110 mockContainer.EXPECT().Append(mockContent).Once()
160 111
161 mockContainer.EXPECT().SetVisible(true).Once()112 mockContainer.EXPECT().SetVisible(true).Once()
A internal/ui/component/logo.go +43 -0
@@ -0,0 +1,43 @@
1+// Package component provides reusable GTK UI components.
2+package component
3+
4+import (
5+ "sync"
6+
7+ "github.com/bnema/dumber/assets"
8+ "github.com/jwijenbergh/puregotk/v4/gdk"
9+ "github.com/jwijenbergh/puregotk/v4/glib"
10+)
11+
12+var (
13+ logoTexture *gdk.Texture
14+ logoTextureOnce sync.Once
15+)
16+
17+// GetLogoTexture returns a cached GDK texture of the dumber logo.
18+// Safe to call from any goroutine; lazily initializes on first call.
19+// Returns nil if the logo cannot be loaded.
20+func GetLogoTexture() *gdk.Texture {
21+ logoTextureOnce.Do(func() {
22+ data := assets.LogoSVG
23+ if len(data) == 0 {
24+ return
25+ }
26+ bytes := glib.NewBytes(data, uint(len(data)))
27+ if bytes == nil {
28+ return
29+ }
30+ texture, err := gdk.NewTextureFromBytes(bytes)
31+ if err != nil {
32+ return
33+ }
34+ logoTexture = texture
35+ })
36+ return logoTexture
37+}
38+
39+// LogoSVGBytes returns the raw SVG bytes of the dumber logo.
40+// Useful for infrastructure services that need the logo data without GTK dependencies.
41+func LogoSVGBytes() []byte {
42+ return assets.LogoSVG
43+}
@@ -0,0 +1,43 @@
1+// Package component provides reusable GTK UI components.
2+package component
3+
4+import (
5+ "sync"
6+
7+ "github.com/bnema/dumber/assets"
8+ "github.com/jwijenbergh/puregotk/v4/gdk"
9+ "github.com/jwijenbergh/puregotk/v4/glib"
10+)
11+
12+var (
13+ logoTexture *gdk.Texture
14+ logoTextureOnce sync.Once
15+)
16+
17+// GetLogoTexture returns a cached GDK texture of the dumber logo.
18+// Safe to call from any goroutine; lazily initializes on first call.
19+// Returns nil if the logo cannot be loaded.
20+func GetLogoTexture() *gdk.Texture {
21+ logoTextureOnce.Do(func() {
22+ data := assets.LogoSVG
23+ if len(data) == 0 {
24+ return
25+ }
26+ bytes := glib.NewBytes(data, uint(len(data)))
27+ if bytes == nil {
28+ return
29+ }
30+ texture, err := gdk.NewTextureFromBytes(bytes)
31+ if err != nil {
32+ return
33+ }
34+ logoTexture = texture
35+ })
36+ return logoTexture
37+}
38+
39+// LogoSVGBytes returns the raw SVG bytes of the dumber logo.
40+// Useful for infrastructure services that need the logo data without GTK dependencies.
41+func LogoSVGBytes() []byte {
42+ return assets.LogoSVG
43+}
M internal/ui/component/pane_view_internal_test.go +11 -8
@@ -14,7 +14,7 @@ func TestPaneView_HideLoadingSkeleton_HidesWhenPresent(t *testing.T) {
1414 mockContainer := mocks.NewMockBoxWidget(t)
1515 mockContent := mocks.NewMockBoxWidget(t)
1616 mockSpinner := mocks.NewMockSpinnerWidget(t)
17- mockLabel := mocks.NewMockLabelWidget(t)
17+ mockLogo := mocks.NewMockImageWidget(t)
1818
1919 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
2020 mockContainer.EXPECT().SetHexpand(true).Once()
@@ -40,15 +40,18 @@ func TestPaneView_HideLoadingSkeleton_HidesWhenPresent(t *testing.T) {
4040 mockSpinner.EXPECT().SetSizeRequest(32, 32).Once()
4141 mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
4242
43- mockFactory.EXPECT().NewLabel("Loading...").Return(mockLabel).Once()
44- mockLabel.EXPECT().SetHalign(mock.Anything).Once()
45- mockLabel.EXPECT().SetValign(mock.Anything).Once()
46- mockLabel.EXPECT().SetCanFocus(false).Once()
47- mockLabel.EXPECT().SetCanTarget(false).Once()
48- mockLabel.EXPECT().AddCssClass("loading-skeleton-text").Once()
43+ mockFactory.EXPECT().NewImage().Return(mockLogo).Once()
44+ mockLogo.EXPECT().SetHalign(mock.Anything).Once()
45+ mockLogo.EXPECT().SetValign(mock.Anything).Once()
46+ mockLogo.EXPECT().SetCanFocus(false).Once()
47+ mockLogo.EXPECT().SetCanTarget(false).Once()
48+ mockLogo.EXPECT().SetSizeRequest(512, 512).Once()
49+ mockLogo.EXPECT().SetPixelSize(512).Once()
50+ mockLogo.EXPECT().AddCssClass("loading-skeleton-logo").Once()
51+ mockLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
4952
53+ mockContent.EXPECT().Append(mockLogo).Once()
5054 mockContent.EXPECT().Append(mockSpinner).Once()
51- mockContent.EXPECT().Append(mockLabel).Once()
5255 mockContainer.EXPECT().Append(mockContent).Once()
5356
5457 mockContainer.EXPECT().SetVisible(true).Once()
@@ -14,7 +14,7 @@ func TestPaneView_HideLoadingSkeleton_HidesWhenPresent(t *testing.T) {
14 mockContainer := mocks.NewMockBoxWidget(t)14 mockContainer := mocks.NewMockBoxWidget(t)
15 mockContent := mocks.NewMockBoxWidget(t)15 mockContent := mocks.NewMockBoxWidget(t)
16 mockSpinner := mocks.NewMockSpinnerWidget(t)16 mockSpinner := mocks.NewMockSpinnerWidget(t)
17- mockLabel := mocks.NewMockLabelWidget(t)17+ mockLogo := mocks.NewMockImageWidget(t)
18 18
19 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()19 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
20 mockContainer.EXPECT().SetHexpand(true).Once()20 mockContainer.EXPECT().SetHexpand(true).Once()
@@ -40,15 +40,18 @@ func TestPaneView_HideLoadingSkeleton_HidesWhenPresent(t *testing.T) {
40 mockSpinner.EXPECT().SetSizeRequest(32, 32).Once()40 mockSpinner.EXPECT().SetSizeRequest(32, 32).Once()
41 mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()41 mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once()
42 42
43- mockFactory.EXPECT().NewLabel("Loading...").Return(mockLabel).Once()43+ mockFactory.EXPECT().NewImage().Return(mockLogo).Once()
44- mockLabel.EXPECT().SetHalign(mock.Anything).Once()44+ mockLogo.EXPECT().SetHalign(mock.Anything).Once()
45- mockLabel.EXPECT().SetValign(mock.Anything).Once()45+ mockLogo.EXPECT().SetValign(mock.Anything).Once()
46- mockLabel.EXPECT().SetCanFocus(false).Once()46+ mockLogo.EXPECT().SetCanFocus(false).Once()
47- mockLabel.EXPECT().SetCanTarget(false).Once()47+ mockLogo.EXPECT().SetCanTarget(false).Once()
48- mockLabel.EXPECT().AddCssClass("loading-skeleton-text").Once()48+ mockLogo.EXPECT().SetSizeRequest(512, 512).Once()
49+ mockLogo.EXPECT().SetPixelSize(512).Once()
50+ mockLogo.EXPECT().AddCssClass("loading-skeleton-logo").Once()
51+ mockLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
49 52
53+ mockContent.EXPECT().Append(mockLogo).Once()
50 mockContent.EXPECT().Append(mockSpinner).Once()54 mockContent.EXPECT().Append(mockSpinner).Once()
51- mockContent.EXPECT().Append(mockLabel).Once()
52 mockContainer.EXPECT().Append(mockContent).Once()55 mockContainer.EXPECT().Append(mockContent).Once()
53 56
54 mockContainer.EXPECT().SetVisible(true).Once()57 mockContainer.EXPECT().SetVisible(true).Once()
M internal/ui/component/pane_view_test.go +20 -17
@@ -23,7 +23,7 @@ func TestNewPaneView_CreatesOverlay(t *testing.T) {
2323 mockLoadingContainer := mocks.NewMockBoxWidget(t)
2424 mockLoadingContent := mocks.NewMockBoxWidget(t)
2525 mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)
26- mockLoadingLabel := mocks.NewMockLabelWidget(t)
26+ mockLoadingLogo := mocks.NewMockImageWidget(t)
2727
2828 paneID := entity.PaneID("pane-1")
2929
@@ -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, mockLoadingLabel)
37+ setupLoadingSkeletonMocks(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
3838
3939 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
4040 mockBorderBox.EXPECT().SetCanFocus(false).Once()
@@ -64,7 +64,7 @@ func TestNewPaneView_NilWebView(t *testing.T) {
6464 mockLoadingContainer := mocks.NewMockBoxWidget(t)
6565 mockLoadingContent := mocks.NewMockBoxWidget(t)
6666 mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)
67- mockLoadingLabel := mocks.NewMockLabelWidget(t)
67+ mockLoadingLogo := mocks.NewMockImageWidget(t)
6868
6969 paneID := entity.PaneID("pane-1")
7070
@@ -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, mockLoadingLabel)
78+ setupLoadingSkeletonMocks(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
7979
8080 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
8181 mockBorderBox.EXPECT().SetCanFocus(false).Once()
@@ -514,7 +514,7 @@ func setupLoadingSkeletonMocks(
514514 mockLoadingContainer *mocks.MockBoxWidget,
515515 mockLoadingContent *mocks.MockBoxWidget,
516516 mockLoadingSpinner *mocks.MockSpinnerWidget,
517- mockLoadingLabel *mocks.MockLabelWidget,
517+ mockLoadingLogo *mocks.MockImageWidget,
518518 ) {
519519 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockLoadingContainer).Once()
520520 mockLoadingContainer.EXPECT().SetHexpand(true).Maybe()
@@ -542,15 +542,18 @@ func setupLoadingSkeletonMocks(
542542 mockLoadingSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Maybe()
543543 mockLoadingSpinner.EXPECT().Start().Maybe()
544544
545- mockFactory.EXPECT().NewLabel("Loading...").Return(mockLoadingLabel).Once()
546- mockLoadingLabel.EXPECT().SetHalign(mock.Anything).Maybe()
547- mockLoadingLabel.EXPECT().SetValign(mock.Anything).Maybe()
548- mockLoadingLabel.EXPECT().SetCanFocus(false).Maybe()
549- mockLoadingLabel.EXPECT().SetCanTarget(false).Maybe()
550- mockLoadingLabel.EXPECT().AddCssClass("loading-skeleton-text").Maybe()
551-
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()
554+
555+ mockLoadingContent.EXPECT().Append(mockLoadingLogo).Maybe()
552556 mockLoadingContent.EXPECT().Append(mockLoadingSpinner).Maybe()
553- mockLoadingContent.EXPECT().Append(mockLoadingLabel).Maybe()
554557 mockLoadingContainer.EXPECT().Append(mockLoadingContent).Maybe()
555558
556559 mockOverlay.EXPECT().AddOverlay(mockLoadingContainer).Once()
@@ -569,7 +572,7 @@ func setupPaneViewMocks(
569572 mockLoadingContainer := mocks.NewMockBoxWidget(t)
570573 mockLoadingContent := mocks.NewMockBoxWidget(t)
571574 mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)
572- mockLoadingLabel := mocks.NewMockLabelWidget(t)
575+ mockLoadingLogo := mocks.NewMockImageWidget(t)
573576
574577 mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
575578 mockOverlay.EXPECT().SetHexpand(true).Once()
@@ -578,7 +581,7 @@ func setupPaneViewMocks(
578581 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background
579582 mockOverlay.EXPECT().SetChild(mockWebView).Once()
580583
581- setupLoadingSkeletonMocks(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLabel)
584+ setupLoadingSkeletonMocks(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
582585
583586 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
584587 mockBorderBox.EXPECT().SetCanFocus(false).Once()
@@ -600,7 +603,7 @@ func setupPaneViewMocksNoWebView(
600603 mockLoadingContainer := mocks.NewMockBoxWidget(t)
601604 mockLoadingContent := mocks.NewMockBoxWidget(t)
602605 mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)
603- mockLoadingLabel := mocks.NewMockLabelWidget(t)
606+ mockLoadingLogo := mocks.NewMockImageWidget(t)
604607
605608 mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
606609 mockOverlay.EXPECT().SetHexpand(true).Once()
@@ -608,7 +611,7 @@ func setupPaneViewMocksNoWebView(
608611 mockOverlay.EXPECT().SetVisible(true).Once()
609612 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background
610613
611- setupLoadingSkeletonMocks(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLabel)
614+ setupLoadingSkeletonMocks(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
612615
613616 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
614617 mockBorderBox.EXPECT().SetCanFocus(false).Once()
@@ -23,7 +23,7 @@ func TestNewPaneView_CreatesOverlay(t *testing.T) {
23 mockLoadingContainer := mocks.NewMockBoxWidget(t)23 mockLoadingContainer := mocks.NewMockBoxWidget(t)
24 mockLoadingContent := mocks.NewMockBoxWidget(t)24 mockLoadingContent := mocks.NewMockBoxWidget(t)
25 mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)25 mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)
26- mockLoadingLabel := mocks.NewMockLabelWidget(t)26+ mockLoadingLogo := mocks.NewMockImageWidget(t)
27 27
28 paneID := entity.PaneID("pane-1")28 paneID := entity.PaneID("pane-1")
29 29
@@ -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, mockLoadingLabel)37+ setupLoadingSkeletonMocks(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()
@@ -64,7 +64,7 @@ func TestNewPaneView_NilWebView(t *testing.T) {
64 mockLoadingContainer := mocks.NewMockBoxWidget(t)64 mockLoadingContainer := mocks.NewMockBoxWidget(t)
65 mockLoadingContent := mocks.NewMockBoxWidget(t)65 mockLoadingContent := mocks.NewMockBoxWidget(t)
66 mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)66 mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)
67- mockLoadingLabel := mocks.NewMockLabelWidget(t)67+ mockLoadingLogo := mocks.NewMockImageWidget(t)
68 68
69 paneID := entity.PaneID("pane-1")69 paneID := entity.PaneID("pane-1")
70 70
@@ -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, mockLoadingLabel)78+ setupLoadingSkeletonMocks(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()
@@ -514,7 +514,7 @@ func setupLoadingSkeletonMocks(
514 mockLoadingContainer *mocks.MockBoxWidget,514 mockLoadingContainer *mocks.MockBoxWidget,
515 mockLoadingContent *mocks.MockBoxWidget,515 mockLoadingContent *mocks.MockBoxWidget,
516 mockLoadingSpinner *mocks.MockSpinnerWidget,516 mockLoadingSpinner *mocks.MockSpinnerWidget,
517- mockLoadingLabel *mocks.MockLabelWidget,517+ mockLoadingLogo *mocks.MockImageWidget,
518 ) {518 ) {
519 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockLoadingContainer).Once()519 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockLoadingContainer).Once()
520 mockLoadingContainer.EXPECT().SetHexpand(true).Maybe()520 mockLoadingContainer.EXPECT().SetHexpand(true).Maybe()
@@ -542,15 +542,18 @@ func setupLoadingSkeletonMocks(
542 mockLoadingSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Maybe()542 mockLoadingSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Maybe()
543 mockLoadingSpinner.EXPECT().Start().Maybe()543 mockLoadingSpinner.EXPECT().Start().Maybe()
544 544
545- mockFactory.EXPECT().NewLabel("Loading...").Return(mockLoadingLabel).Once()545+ mockFactory.EXPECT().NewImage().Return(mockLoadingLogo).Once()
546- mockLoadingLabel.EXPECT().SetHalign(mock.Anything).Maybe()546+ mockLoadingLogo.EXPECT().SetHalign(mock.Anything).Maybe()
547- mockLoadingLabel.EXPECT().SetValign(mock.Anything).Maybe()547+ mockLoadingLogo.EXPECT().SetValign(mock.Anything).Maybe()
548- mockLoadingLabel.EXPECT().SetCanFocus(false).Maybe()548+ mockLoadingLogo.EXPECT().SetCanFocus(false).Maybe()
549- mockLoadingLabel.EXPECT().SetCanTarget(false).Maybe()549+ mockLoadingLogo.EXPECT().SetCanTarget(false).Maybe()
550- mockLoadingLabel.EXPECT().AddCssClass("loading-skeleton-text").Maybe()550+ mockLoadingLogo.EXPECT().SetSizeRequest(512, 512).Maybe()
551-551+ mockLoadingLogo.EXPECT().SetPixelSize(512).Maybe()
552+ mockLoadingLogo.EXPECT().AddCssClass("loading-skeleton-logo").Maybe()
553+ mockLoadingLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
554+
555+ mockLoadingContent.EXPECT().Append(mockLoadingLogo).Maybe()
552 mockLoadingContent.EXPECT().Append(mockLoadingSpinner).Maybe()556 mockLoadingContent.EXPECT().Append(mockLoadingSpinner).Maybe()
553- mockLoadingContent.EXPECT().Append(mockLoadingLabel).Maybe()
554 mockLoadingContainer.EXPECT().Append(mockLoadingContent).Maybe()557 mockLoadingContainer.EXPECT().Append(mockLoadingContent).Maybe()
555 558
556 mockOverlay.EXPECT().AddOverlay(mockLoadingContainer).Once()559 mockOverlay.EXPECT().AddOverlay(mockLoadingContainer).Once()
@@ -569,7 +572,7 @@ func setupPaneViewMocks(
569 mockLoadingContainer := mocks.NewMockBoxWidget(t)572 mockLoadingContainer := mocks.NewMockBoxWidget(t)
570 mockLoadingContent := mocks.NewMockBoxWidget(t)573 mockLoadingContent := mocks.NewMockBoxWidget(t)
571 mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)574 mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)
572- mockLoadingLabel := mocks.NewMockLabelWidget(t)575+ mockLoadingLogo := mocks.NewMockImageWidget(t)
573 576
574 mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()577 mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
575 mockOverlay.EXPECT().SetHexpand(true).Once()578 mockOverlay.EXPECT().SetHexpand(true).Once()
@@ -578,7 +581,7 @@ func setupPaneViewMocks(
578 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background581 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background
579 mockOverlay.EXPECT().SetChild(mockWebView).Once()582 mockOverlay.EXPECT().SetChild(mockWebView).Once()
580 583
581- setupLoadingSkeletonMocks(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLabel)584+ setupLoadingSkeletonMocks(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
582 585
583 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()586 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
584 mockBorderBox.EXPECT().SetCanFocus(false).Once()587 mockBorderBox.EXPECT().SetCanFocus(false).Once()
@@ -600,7 +603,7 @@ func setupPaneViewMocksNoWebView(
600 mockLoadingContainer := mocks.NewMockBoxWidget(t)603 mockLoadingContainer := mocks.NewMockBoxWidget(t)
601 mockLoadingContent := mocks.NewMockBoxWidget(t)604 mockLoadingContent := mocks.NewMockBoxWidget(t)
602 mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)605 mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)
603- mockLoadingLabel := mocks.NewMockLabelWidget(t)606+ mockLoadingLogo := mocks.NewMockImageWidget(t)
604 607
605 mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()608 mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
606 mockOverlay.EXPECT().SetHexpand(true).Once()609 mockOverlay.EXPECT().SetHexpand(true).Once()
@@ -608,7 +611,7 @@ func setupPaneViewMocksNoWebView(
608 mockOverlay.EXPECT().SetVisible(true).Once()611 mockOverlay.EXPECT().SetVisible(true).Once()
609 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background612 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background
610 613
611- setupLoadingSkeletonMocks(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLabel)614+ setupLoadingSkeletonMocks(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
612 615
613 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()616 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
614 mockBorderBox.EXPECT().SetCanFocus(false).Once()617 mockBorderBox.EXPECT().SetCanFocus(false).Once()
M internal/ui/component/workspace_view_test.go +14 -11
@@ -20,7 +20,7 @@ func setupLoadingSkeletonMocksWorkspace(
2020 mockLoadingContainer *mocks.MockBoxWidget,
2121 mockLoadingContent *mocks.MockBoxWidget,
2222 mockLoadingSpinner *mocks.MockSpinnerWidget,
23- mockLoadingLabel *mocks.MockLabelWidget,
23+ mockLoadingLogo *mocks.MockImageWidget,
2424 ) {
2525 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockLoadingContainer).Once()
2626 mockLoadingContainer.EXPECT().SetHexpand(true).Maybe()
@@ -48,15 +48,18 @@ func setupLoadingSkeletonMocksWorkspace(
4848 mockLoadingSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Maybe()
4949 mockLoadingSpinner.EXPECT().Start().Maybe()
5050
51- mockFactory.EXPECT().NewLabel("Loading...").Return(mockLoadingLabel).Once()
52- mockLoadingLabel.EXPECT().SetHalign(mock.Anything).Maybe()
53- mockLoadingLabel.EXPECT().SetValign(mock.Anything).Maybe()
54- mockLoadingLabel.EXPECT().SetCanFocus(false).Maybe()
55- mockLoadingLabel.EXPECT().SetCanTarget(false).Maybe()
56- mockLoadingLabel.EXPECT().AddCssClass("loading-skeleton-text").Maybe()
57-
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()
60+
61+ mockLoadingContent.EXPECT().Append(mockLoadingLogo).Maybe()
5862 mockLoadingContent.EXPECT().Append(mockLoadingSpinner).Maybe()
59- mockLoadingContent.EXPECT().Append(mockLoadingLabel).Maybe()
6063 mockLoadingContainer.EXPECT().Append(mockLoadingContent).Maybe()
6164
6265 mockOverlay.EXPECT().AddOverlay(mockLoadingContainer).Once()
@@ -90,7 +93,7 @@ func setupWorkspacePaneViewMocks(t *testing.T, mockFactory *mocks.MockWidgetFact
9093 mockLoadingContainer := mocks.NewMockBoxWidget(t)
9194 mockLoadingContent := mocks.NewMockBoxWidget(t)
9295 mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)
93- mockLoadingLabel := mocks.NewMockLabelWidget(t)
96+ mockLoadingLogo := mocks.NewMockImageWidget(t)
9497
9598 mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
9699 mockOverlay.EXPECT().SetHexpand(true).Once()
@@ -98,7 +101,7 @@ func setupWorkspacePaneViewMocks(t *testing.T, mockFactory *mocks.MockWidgetFact
98101 mockOverlay.EXPECT().SetVisible(true).Once()
99102 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once()
100103
101- setupLoadingSkeletonMocksWorkspace(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLabel)
104+ setupLoadingSkeletonMocksWorkspace(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
102105
103106 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
104107 mockBorderBox.EXPECT().SetCanFocus(false).Once()
@@ -20,7 +20,7 @@ func setupLoadingSkeletonMocksWorkspace(
20 mockLoadingContainer *mocks.MockBoxWidget,20 mockLoadingContainer *mocks.MockBoxWidget,
21 mockLoadingContent *mocks.MockBoxWidget,21 mockLoadingContent *mocks.MockBoxWidget,
22 mockLoadingSpinner *mocks.MockSpinnerWidget,22 mockLoadingSpinner *mocks.MockSpinnerWidget,
23- mockLoadingLabel *mocks.MockLabelWidget,23+ mockLoadingLogo *mocks.MockImageWidget,
24 ) {24 ) {
25 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockLoadingContainer).Once()25 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockLoadingContainer).Once()
26 mockLoadingContainer.EXPECT().SetHexpand(true).Maybe()26 mockLoadingContainer.EXPECT().SetHexpand(true).Maybe()
@@ -48,15 +48,18 @@ func setupLoadingSkeletonMocksWorkspace(
48 mockLoadingSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Maybe()48 mockLoadingSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Maybe()
49 mockLoadingSpinner.EXPECT().Start().Maybe()49 mockLoadingSpinner.EXPECT().Start().Maybe()
50 50
51- mockFactory.EXPECT().NewLabel("Loading...").Return(mockLoadingLabel).Once()51+ mockFactory.EXPECT().NewImage().Return(mockLoadingLogo).Once()
52- mockLoadingLabel.EXPECT().SetHalign(mock.Anything).Maybe()52+ mockLoadingLogo.EXPECT().SetHalign(mock.Anything).Maybe()
53- mockLoadingLabel.EXPECT().SetValign(mock.Anything).Maybe()53+ mockLoadingLogo.EXPECT().SetValign(mock.Anything).Maybe()
54- mockLoadingLabel.EXPECT().SetCanFocus(false).Maybe()54+ mockLoadingLogo.EXPECT().SetCanFocus(false).Maybe()
55- mockLoadingLabel.EXPECT().SetCanTarget(false).Maybe()55+ mockLoadingLogo.EXPECT().SetCanTarget(false).Maybe()
56- mockLoadingLabel.EXPECT().AddCssClass("loading-skeleton-text").Maybe()56+ mockLoadingLogo.EXPECT().SetSizeRequest(512, 512).Maybe()
57-57+ mockLoadingLogo.EXPECT().SetPixelSize(512).Maybe()
58+ mockLoadingLogo.EXPECT().AddCssClass("loading-skeleton-logo").Maybe()
59+ mockLoadingLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
60+
61+ mockLoadingContent.EXPECT().Append(mockLoadingLogo).Maybe()
58 mockLoadingContent.EXPECT().Append(mockLoadingSpinner).Maybe()62 mockLoadingContent.EXPECT().Append(mockLoadingSpinner).Maybe()
59- mockLoadingContent.EXPECT().Append(mockLoadingLabel).Maybe()
60 mockLoadingContainer.EXPECT().Append(mockLoadingContent).Maybe()63 mockLoadingContainer.EXPECT().Append(mockLoadingContent).Maybe()
61 64
62 mockOverlay.EXPECT().AddOverlay(mockLoadingContainer).Once()65 mockOverlay.EXPECT().AddOverlay(mockLoadingContainer).Once()
@@ -90,7 +93,7 @@ func setupWorkspacePaneViewMocks(t *testing.T, mockFactory *mocks.MockWidgetFact
90 mockLoadingContainer := mocks.NewMockBoxWidget(t)93 mockLoadingContainer := mocks.NewMockBoxWidget(t)
91 mockLoadingContent := mocks.NewMockBoxWidget(t)94 mockLoadingContent := mocks.NewMockBoxWidget(t)
92 mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)95 mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)
93- mockLoadingLabel := mocks.NewMockLabelWidget(t)96+ mockLoadingLogo := mocks.NewMockImageWidget(t)
94 97
95 mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()98 mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
96 mockOverlay.EXPECT().SetHexpand(true).Once()99 mockOverlay.EXPECT().SetHexpand(true).Once()
@@ -98,7 +101,7 @@ func setupWorkspacePaneViewMocks(t *testing.T, mockFactory *mocks.MockWidgetFact
98 mockOverlay.EXPECT().SetVisible(true).Once()101 mockOverlay.EXPECT().SetVisible(true).Once()
99 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once()102 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once()
100 103
101- setupLoadingSkeletonMocksWorkspace(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLabel)104+ setupLoadingSkeletonMocksWorkspace(mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
102 105
103 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()106 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
104 mockBorderBox.EXPECT().SetCanFocus(false).Once()107 mockBorderBox.EXPECT().SetCanFocus(false).Once()
M internal/ui/theme/css.go +5 -5
@@ -512,13 +512,13 @@ func generatePaneCSS(p Palette) string {
512512 -gtk-icon-size: 32px;
513513 min-width: 32px;
514514 min-height: 32px;
515+ color: var(--muted);
515516 }
516517
517-.loading-skeleton-text {
518- font-family: var(--font-mono);
519- font-size: 1.5em;
520- letter-spacing: 0.1em;
521- color: var(--muted);
518+.loading-skeleton-logo {
519+ opacity: 0.08;
520+ min-width: 512px;
521+ min-height: 512px;
522522 }
523523
524524 /* Pane border - default transparent */
@@ -512,13 +512,13 @@ func generatePaneCSS(p Palette) string {
512 -gtk-icon-size: 32px;512 -gtk-icon-size: 32px;
513 min-width: 32px;513 min-width: 32px;
514 min-height: 32px;514 min-height: 32px;
515+ color: var(--muted);
515 }516 }
516 517
517-.loading-skeleton-text {518+.loading-skeleton-logo {
518- font-family: var(--font-mono);519+ opacity: 0.08;
519- font-size: 1.5em;520+ min-width: 512px;
520- letter-spacing: 0.1em;521+ min-height: 512px;
521- color: var(--muted);
522 }522 }
523 523
524 /* Pane border - default transparent */524 /* 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