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 @@ | |||
| 1 | 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 | 3 | <defs> | |
| 4 | 4 | <!-- Background gradient - deeper dark --> | |
| 5 | 5 | <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 | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "fmt" | |
| 6 | + | "strings" | |
| 6 | 7 | ||
| 8 | + | "github.com/bnema/dumber/assets" | |
| 7 | 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 | 18 | // Service implements the domain FaviconService interface. | |
| 11 | 19 | // It coordinates between the cache and fetcher components. | |
| 12 | 20 | type Service struct { | |
| @@ -30,6 +38,11 @@ func (s *Service) Get(ctx context.Context, domain string) ([]byte, error) { | |||
| 30 | 38 | 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 | 46 | // Check cache first (memory + disk) | |
| 34 | 47 | if data, ok := s.cache.Get(domain); ok { | |
| 35 | 48 | return data, nil | |
| @@ -49,6 +62,17 @@ func (s *Service) Get(ctx context.Context, domain string) ([]byte, error) { | |||
| 49 | 62 | 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 | 76 | // GetCached returns favicon bytes only if already cached (no external fetch). | |
| 53 | 77 | func (s *Service) GetCached(domain string) ([]byte, bool) { | |
| 54 | 78 | 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, nil | 38 | 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, nil | 48 | return data, nil | ||
| @@ -49,6 +62,17 @@ func (s *Service) Get(ctx context.Context, domain string) ([]byte, error) { | |||||
| 49 | return data, nil | 62 | 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 { | |||
| 46 | 46 | } | |
| 47 | 47 | ||
| 48 | 48 | // GetTextureByURL returns a cached texture by extracting domain from URL. | |
| 49 | + | // For internal dumb:// URLs, returns the app logo texture. | |
| 49 | 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 | 55 | domain := domainurl.ExtractDomain(pageURL) | |
| 51 | 56 | return a.GetTexture(domain) | |
| 52 | 57 | } | |
| 53 | 58 | ||
| 54 | 59 | // GetOrFetch retrieves a favicon texture, checking caches and fetching if needed. | |
| 55 | 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 | 62 | func (a *FaviconAdapter) GetOrFetch(ctx context.Context, pageURL string, callback func(*gdk.Texture)) { | |
| 57 | 63 | if callback == nil { | |
| 58 | 64 | return | |
| 59 | 65 | } | |
| 60 | 66 | ||
| 61 | 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 | 76 | domain := domainurl.ExtractDomain(pageURL) | |
| 63 | 77 | ||
| 64 | 78 | // 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 | 345 | // bytesToTexture converts raw favicon bytes to a GDK texture. | |
| 315 | 346 | func bytesToTexture(data []byte) *gdk.Texture { | |
| 316 | 347 | 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 | return | 64 | 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 first | 78 | // 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 @@ | |||
| 1 | 1 | package component | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "strings" | |
| 5 | - | ||
| 6 | 4 | "github.com/bnema/dumber/internal/ui/layout" | |
| 7 | 5 | "github.com/jwijenbergh/puregotk/v4/gtk" | |
| 8 | 6 | ) | |
| 9 | 7 | ||
| 10 | 8 | // LoadingSkeleton displays a themed loading placeholder. | |
| 11 | 9 | // It is intended to be embedded as an overlay while primary content is loading. | |
| 10 | + | // Shows a faded app logo and spinner. | |
| 12 | 11 | type LoadingSkeleton struct { | |
| 13 | 12 | container layout.BoxWidget | |
| 14 | 13 | content layout.BoxWidget | |
| 15 | 14 | spinner layout.SpinnerWidget | |
| 16 | - | label layout.LabelWidget | |
| 15 | + | logo layout.ImageWidget | |
| 17 | 16 | } | |
| 18 | 17 | ||
| 19 | - | const loadingSkeletonSpacing = 6 | |
| 18 | + | const ( | |
| 19 | + | loadingSkeletonSpacing = 6 | |
| 20 | + | loadingSkeletonLogoSize = 512 | |
| 21 | + | ) | |
| 20 | 22 | ||
| 21 | 23 | func NewLoadingSkeleton(factory layout.WidgetFactory) *LoadingSkeleton { | |
| 22 | 24 | container := factory.NewBox(layout.OrientationVertical, 0) | |
| @@ -43,23 +45,31 @@ func NewLoadingSkeleton(factory layout.WidgetFactory) *LoadingSkeleton { | |||
| 43 | 45 | spinner.SetSizeRequest(32, 32) | |
| 44 | 46 | spinner.AddCssClass("loading-skeleton-spinner") | |
| 45 | 47 | ||
| 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") | |
| 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 | 65 | content.Append(spinner) | |
| 55 | - | content.Append(label) | |
| 56 | 66 | container.Append(content) | |
| 57 | 67 | ||
| 58 | 68 | ls := &LoadingSkeleton{ | |
| 59 | 69 | container: container, | |
| 60 | 70 | content: content, | |
| 61 | 71 | spinner: spinner, | |
| 62 | - | label: label, | |
| 72 | + | logo: logo, | |
| 63 | 73 | } | |
| 64 | 74 | ls.SetVisible(true) | |
| 65 | 75 | return ls | |
| @@ -72,17 +82,6 @@ func (ls *LoadingSkeleton) Widget() layout.Widget { | |||
| 72 | 82 | 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 | 85 | func (ls *LoadingSkeleton) SetVisible(visible bool) { | |
| 87 | 86 | if ls == nil || ls.container == nil { | |
| 88 | 87 | return | |
| @@ -1,22 +1,24 @@ | |||||
| 1 | package component | 1 | 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.BoxWidget | 12 | container layout.BoxWidget | ||
| 14 | content layout.BoxWidget | 13 | content layout.BoxWidget | ||
| 15 | spinner layout.SpinnerWidget | 14 | spinner layout.SpinnerWidget | ||
| 16 | - | label layout.LabelWidget | 15 | + | logo layout.ImageWidget |
| 17 | } | 16 | } | ||
| 18 | 17 | ||||
| 19 | - | const loadingSkeletonSpacing = 6 | 18 | + | 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 ls | 75 | return ls | ||
| @@ -72,17 +82,6 @@ func (ls *LoadingSkeleton) Widget() layout.Widget { | |||||
| 72 | return ls.container | 82 | 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 | return | 87 | return | ||
M internal/ui/component/loading_skeleton_test.go +24 -73
| @@ -16,7 +16,7 @@ func TestNewLoadingSkeleton_ShowsAndStartsSpinner(t *testing.T) { | |||
| 16 | 16 | mockContainer := mocks.NewMockBoxWidget(t) | |
| 17 | 17 | mockContent := mocks.NewMockBoxWidget(t) | |
| 18 | 18 | mockSpinner := mocks.NewMockSpinnerWidget(t) | |
| 19 | - | mockLabel := mocks.NewMockLabelWidget(t) | |
| 19 | + | mockLogo := mocks.NewMockImageWidget(t) | |
| 20 | 20 | ||
| 21 | 21 | mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once() | |
| 22 | 22 | mockContainer.EXPECT().SetHexpand(true).Once() | |
| @@ -42,15 +42,18 @@ func TestNewLoadingSkeleton_ShowsAndStartsSpinner(t *testing.T) { | |||
| 42 | 42 | mockSpinner.EXPECT().SetSizeRequest(32, 32).Once() | |
| 43 | 43 | mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once() | |
| 44 | 44 | ||
| 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() | |
| 52 | 56 | mockContent.EXPECT().Append(mockSpinner).Once() | |
| 53 | - | mockContent.EXPECT().Append(mockLabel).Once() | |
| 54 | 57 | mockContainer.EXPECT().Append(mockContent).Once() | |
| 55 | 58 | ||
| 56 | 59 | // NewLoadingSkeleton ends by calling SetVisible(true). | |
| @@ -61,67 +64,12 @@ func TestNewLoadingSkeleton_ShowsAndStartsSpinner(t *testing.T) { | |||
| 61 | 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 | 67 | func TestLoadingSkeleton_SetVisible_StopsSpinnerWhenHidden(t *testing.T) { | |
| 120 | 68 | mockFactory := mocks.NewMockWidgetFactory(t) | |
| 121 | 69 | mockContainer := mocks.NewMockBoxWidget(t) | |
| 122 | 70 | mockContent := mocks.NewMockBoxWidget(t) | |
| 123 | 71 | mockSpinner := mocks.NewMockSpinnerWidget(t) | |
| 124 | - | mockLabel := mocks.NewMockLabelWidget(t) | |
| 72 | + | mockLogo := mocks.NewMockImageWidget(t) | |
| 125 | 73 | ||
| 126 | 74 | mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once() | |
| 127 | 75 | mockContainer.EXPECT().SetHexpand(true).Once() | |
| @@ -147,15 +95,18 @@ func TestLoadingSkeleton_SetVisible_StopsSpinnerWhenHidden(t *testing.T) { | |||
| 147 | 95 | mockSpinner.EXPECT().SetSizeRequest(32, 32).Once() | |
| 148 | 96 | mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once() | |
| 149 | 97 | ||
| 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() | |
| 157 | 109 | mockContent.EXPECT().Append(mockSpinner).Once() | |
| 158 | - | mockContent.EXPECT().Append(mockLabel).Once() | |
| 159 | 110 | mockContainer.EXPECT().Append(mockContent).Once() | |
| 160 | 111 | ||
| 161 | 112 | 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) { | |||
| 14 | 14 | mockContainer := mocks.NewMockBoxWidget(t) | |
| 15 | 15 | mockContent := mocks.NewMockBoxWidget(t) | |
| 16 | 16 | mockSpinner := mocks.NewMockSpinnerWidget(t) | |
| 17 | - | mockLabel := mocks.NewMockLabelWidget(t) | |
| 17 | + | mockLogo := mocks.NewMockImageWidget(t) | |
| 18 | 18 | ||
| 19 | 19 | mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once() | |
| 20 | 20 | mockContainer.EXPECT().SetHexpand(true).Once() | |
| @@ -40,15 +40,18 @@ func TestPaneView_HideLoadingSkeleton_HidesWhenPresent(t *testing.T) { | |||
| 40 | 40 | mockSpinner.EXPECT().SetSizeRequest(32, 32).Once() | |
| 41 | 41 | mockSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Once() | |
| 42 | 42 | ||
| 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() | |
| 49 | 52 | ||
| 53 | + | mockContent.EXPECT().Append(mockLogo).Once() | |
| 50 | 54 | mockContent.EXPECT().Append(mockSpinner).Once() | |
| 51 | - | mockContent.EXPECT().Append(mockLabel).Once() | |
| 52 | 55 | mockContainer.EXPECT().Append(mockContent).Once() | |
| 53 | 56 | ||
| 54 | 57 | 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) { | |||
| 23 | 23 | mockLoadingContainer := mocks.NewMockBoxWidget(t) | |
| 24 | 24 | mockLoadingContent := mocks.NewMockBoxWidget(t) | |
| 25 | 25 | mockLoadingSpinner := mocks.NewMockSpinnerWidget(t) | |
| 26 | - | mockLoadingLabel := mocks.NewMockLabelWidget(t) | |
| 26 | + | mockLoadingLogo := mocks.NewMockImageWidget(t) | |
| 27 | 27 | ||
| 28 | 28 | paneID := entity.PaneID("pane-1") | |
| 29 | 29 | ||
| @@ -34,7 +34,7 @@ func TestNewPaneView_CreatesOverlay(t *testing.T) { | |||
| 34 | 34 | mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background | |
| 35 | 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 | 39 | mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once() | |
| 40 | 40 | mockBorderBox.EXPECT().SetCanFocus(false).Once() | |
| @@ -64,7 +64,7 @@ func TestNewPaneView_NilWebView(t *testing.T) { | |||
| 64 | 64 | mockLoadingContainer := mocks.NewMockBoxWidget(t) | |
| 65 | 65 | mockLoadingContent := mocks.NewMockBoxWidget(t) | |
| 66 | 66 | mockLoadingSpinner := mocks.NewMockSpinnerWidget(t) | |
| 67 | - | mockLoadingLabel := mocks.NewMockLabelWidget(t) | |
| 67 | + | mockLoadingLogo := mocks.NewMockImageWidget(t) | |
| 68 | 68 | ||
| 69 | 69 | paneID := entity.PaneID("pane-1") | |
| 70 | 70 | ||
| @@ -75,7 +75,7 @@ func TestNewPaneView_NilWebView(t *testing.T) { | |||
| 75 | 75 | mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background | |
| 76 | 76 | // 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 | 80 | mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once() | |
| 81 | 81 | mockBorderBox.EXPECT().SetCanFocus(false).Once() | |
| @@ -514,7 +514,7 @@ func setupLoadingSkeletonMocks( | |||
| 514 | 514 | mockLoadingContainer *mocks.MockBoxWidget, | |
| 515 | 515 | mockLoadingContent *mocks.MockBoxWidget, | |
| 516 | 516 | mockLoadingSpinner *mocks.MockSpinnerWidget, | |
| 517 | - | mockLoadingLabel *mocks.MockLabelWidget, | |
| 517 | + | mockLoadingLogo *mocks.MockImageWidget, | |
| 518 | 518 | ) { | |
| 519 | 519 | mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockLoadingContainer).Once() | |
| 520 | 520 | mockLoadingContainer.EXPECT().SetHexpand(true).Maybe() | |
| @@ -542,15 +542,18 @@ func setupLoadingSkeletonMocks( | |||
| 542 | 542 | mockLoadingSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Maybe() | |
| 543 | 543 | mockLoadingSpinner.EXPECT().Start().Maybe() | |
| 544 | 544 | ||
| 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() | |
| 552 | 556 | mockLoadingContent.EXPECT().Append(mockLoadingSpinner).Maybe() | |
| 553 | - | mockLoadingContent.EXPECT().Append(mockLoadingLabel).Maybe() | |
| 554 | 557 | mockLoadingContainer.EXPECT().Append(mockLoadingContent).Maybe() | |
| 555 | 558 | ||
| 556 | 559 | mockOverlay.EXPECT().AddOverlay(mockLoadingContainer).Once() | |
| @@ -569,7 +572,7 @@ func setupPaneViewMocks( | |||
| 569 | 572 | mockLoadingContainer := mocks.NewMockBoxWidget(t) | |
| 570 | 573 | mockLoadingContent := mocks.NewMockBoxWidget(t) | |
| 571 | 574 | mockLoadingSpinner := mocks.NewMockSpinnerWidget(t) | |
| 572 | - | mockLoadingLabel := mocks.NewMockLabelWidget(t) | |
| 575 | + | mockLoadingLogo := mocks.NewMockImageWidget(t) | |
| 573 | 576 | ||
| 574 | 577 | mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once() | |
| 575 | 578 | mockOverlay.EXPECT().SetHexpand(true).Once() | |
| @@ -578,7 +581,7 @@ func setupPaneViewMocks( | |||
| 578 | 581 | mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background | |
| 579 | 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 | 586 | mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once() | |
| 584 | 587 | mockBorderBox.EXPECT().SetCanFocus(false).Once() | |
| @@ -600,7 +603,7 @@ func setupPaneViewMocksNoWebView( | |||
| 600 | 603 | mockLoadingContainer := mocks.NewMockBoxWidget(t) | |
| 601 | 604 | mockLoadingContent := mocks.NewMockBoxWidget(t) | |
| 602 | 605 | mockLoadingSpinner := mocks.NewMockSpinnerWidget(t) | |
| 603 | - | mockLoadingLabel := mocks.NewMockLabelWidget(t) | |
| 606 | + | mockLoadingLogo := mocks.NewMockImageWidget(t) | |
| 604 | 607 | ||
| 605 | 608 | mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once() | |
| 606 | 609 | mockOverlay.EXPECT().SetHexpand(true).Once() | |
| @@ -608,7 +611,7 @@ func setupPaneViewMocksNoWebView( | |||
| 608 | 611 | mockOverlay.EXPECT().SetVisible(true).Once() | |
| 609 | 612 | 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 | 616 | mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once() | |
| 614 | 617 | 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 background | 34 | 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 background | 75 | mockOverlay.EXPECT().AddCssClass("pane-overlay").Once() // Theme background | ||
| 76 | // SetChild should NOT be called when webview is nil | 76 | // 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 background | 581 | 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 background | 612 | 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( | |||
| 20 | 20 | mockLoadingContainer *mocks.MockBoxWidget, | |
| 21 | 21 | mockLoadingContent *mocks.MockBoxWidget, | |
| 22 | 22 | mockLoadingSpinner *mocks.MockSpinnerWidget, | |
| 23 | - | mockLoadingLabel *mocks.MockLabelWidget, | |
| 23 | + | mockLoadingLogo *mocks.MockImageWidget, | |
| 24 | 24 | ) { | |
| 25 | 25 | mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockLoadingContainer).Once() | |
| 26 | 26 | mockLoadingContainer.EXPECT().SetHexpand(true).Maybe() | |
| @@ -48,15 +48,18 @@ func setupLoadingSkeletonMocksWorkspace( | |||
| 48 | 48 | mockLoadingSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Maybe() | |
| 49 | 49 | mockLoadingSpinner.EXPECT().Start().Maybe() | |
| 50 | 50 | ||
| 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() | |
| 58 | 62 | mockLoadingContent.EXPECT().Append(mockLoadingSpinner).Maybe() | |
| 59 | - | mockLoadingContent.EXPECT().Append(mockLoadingLabel).Maybe() | |
| 60 | 63 | mockLoadingContainer.EXPECT().Append(mockLoadingContent).Maybe() | |
| 61 | 64 | ||
| 62 | 65 | mockOverlay.EXPECT().AddOverlay(mockLoadingContainer).Once() | |
| @@ -90,7 +93,7 @@ func setupWorkspacePaneViewMocks(t *testing.T, mockFactory *mocks.MockWidgetFact | |||
| 90 | 93 | mockLoadingContainer := mocks.NewMockBoxWidget(t) | |
| 91 | 94 | mockLoadingContent := mocks.NewMockBoxWidget(t) | |
| 92 | 95 | mockLoadingSpinner := mocks.NewMockSpinnerWidget(t) | |
| 93 | - | mockLoadingLabel := mocks.NewMockLabelWidget(t) | |
| 96 | + | mockLoadingLogo := mocks.NewMockImageWidget(t) | |
| 94 | 97 | ||
| 95 | 98 | mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once() | |
| 96 | 99 | mockOverlay.EXPECT().SetHexpand(true).Once() | |
| @@ -98,7 +101,7 @@ func setupWorkspacePaneViewMocks(t *testing.T, mockFactory *mocks.MockWidgetFact | |||
| 98 | 101 | mockOverlay.EXPECT().SetVisible(true).Once() | |
| 99 | 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 | 106 | mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once() | |
| 104 | 107 | 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 { | |||
| 512 | 512 | -gtk-icon-size: 32px; | |
| 513 | 513 | min-width: 32px; | |
| 514 | 514 | min-height: 32px; | |
| 515 | + | color: var(--muted); | |
| 515 | 516 | } | |
| 516 | 517 | ||
| 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; | |
| 522 | 522 | } | |
| 523 | 523 | ||
| 524 | 524 | /* 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 */ | ||