feat(omnibox): move overlay from window to active pane
Refactor omnibox to be positioned within the active workspace pane instead of the entire browser window. Add overlay widget management to PaneView, create WorkspaceView omnibox lifecycle management, and decouple omnibox from main window using OmniboxProvider interface.
- commit
- b106e602402524c22c3a8ad397ba499552c1225e
- author
- bnema · · 2025-12-16 22:30 UTC
- parent
- f367b16
- signature
- unsigned
13 files · +404 -49
A cmd/dumber/main.go +131 -0
| @@ -0,0 +1,131 @@ | |||
| 1 | + | package main | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 6 | + | "os" | |
| 7 | + | "path/filepath" | |
| 8 | + | "runtime" | |
| 9 | + | ||
| 10 | + | "github.com/bnema/dumber/internal/application/usecase" | |
| 11 | + | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 12 | + | "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite" | |
| 13 | + | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 14 | + | "github.com/bnema/dumber/internal/logging" | |
| 15 | + | "github.com/bnema/dumber/internal/ui" | |
| 16 | + | "github.com/bnema/dumber/internal/ui/theme" | |
| 17 | + | ) | |
| 18 | + | ||
| 19 | + | // Build-time variables (set via ldflags) | |
| 20 | + | var ( | |
| 21 | + | version = "dev" | |
| 22 | + | commit = "unknown" | |
| 23 | + | buildDate = "unknown" | |
| 24 | + | ) | |
| 25 | + | ||
| 26 | + | func main() { | |
| 27 | + | // GTK requires all GTK calls to be made from the main thread | |
| 28 | + | runtime.LockOSThread() | |
| 29 | + | ||
| 30 | + | // Initialize configuration | |
| 31 | + | if err := config.Init(); err != nil { | |
| 32 | + | fmt.Fprintf(os.Stderr, "Failed to initialize configuration: %v\n", err) | |
| 33 | + | os.Exit(1) | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | cfg := config.Get() | |
| 37 | + | ||
| 38 | + | // Initialize logger from config | |
| 39 | + | logger := logging.NewFromConfigValues(cfg.Logging.Level, cfg.Logging.Format) | |
| 40 | + | logger.Info(). | |
| 41 | + | Str("version", version). | |
| 42 | + | Str("commit", commit). | |
| 43 | + | Str("build_date", buildDate). | |
| 44 | + | Msg("starting dumber") | |
| 45 | + | ||
| 46 | + | // Create root context with logger | |
| 47 | + | ctx := logging.WithContext(context.Background(), logger) | |
| 48 | + | ||
| 49 | + | // Resolve data/cache directories for WebKit | |
| 50 | + | dataDir, err := config.GetDataDir() | |
| 51 | + | if err != nil { | |
| 52 | + | logger.Fatal().Err(err).Msg("failed to resolve data directory") | |
| 53 | + | } | |
| 54 | + | stateDir, err := config.GetStateDir() | |
| 55 | + | if err != nil { | |
| 56 | + | logger.Fatal().Err(err).Msg("failed to resolve state directory") | |
| 57 | + | } | |
| 58 | + | cacheDir := filepath.Join(stateDir, "webkit-cache") | |
| 59 | + | if mkErr := os.MkdirAll(cacheDir, 0o755); mkErr != nil { | |
| 60 | + | logger.Fatal().Err(mkErr).Str("path", cacheDir).Msg("failed to create cache directory") | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | // Initialize SQLite database | |
| 64 | + | dbPath := filepath.Join(stateDir, "dumber.db") | |
| 65 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |
| 66 | + | if err != nil { | |
| 67 | + | logger.Fatal().Err(err).Str("path", dbPath).Msg("failed to initialize database") | |
| 68 | + | } | |
| 69 | + | defer sqlite.Close(db) | |
| 70 | + | ||
| 71 | + | // Create repositories | |
| 72 | + | historyRepo := sqlite.NewHistoryRepository(db) | |
| 73 | + | favoriteRepo := sqlite.NewFavoriteRepository(db) | |
| 74 | + | folderRepo := sqlite.NewFolderRepository(db) | |
| 75 | + | tagRepo := sqlite.NewTagRepository(db) | |
| 76 | + | zoomRepo := sqlite.NewZoomRepository(db) | |
| 77 | + | ||
| 78 | + | // Theme management | |
| 79 | + | themeManager := theme.NewManager(ctx, cfg) | |
| 80 | + | ||
| 81 | + | // WebKit plumbing | |
| 82 | + | wkCtx, err := webkit.NewWebKitContext(ctx, dataDir, cacheDir) | |
| 83 | + | if err != nil { | |
| 84 | + | logger.Fatal().Err(err).Msg("failed to initialize WebKit context") | |
| 85 | + | } | |
| 86 | + | settings := webkit.NewSettingsManager(ctx, cfg) | |
| 87 | + | injector := webkit.NewContentInjector(themeManager.PrefersDark()) | |
| 88 | + | messageRouter := webkit.NewMessageRouter(ctx) | |
| 89 | + | poolCfg := webkit.DefaultPoolConfig() | |
| 90 | + | pool := webkit.NewWebViewPool(ctx, wkCtx, settings, poolCfg, injector, messageRouter) | |
| 91 | + | ||
| 92 | + | // Create use cases | |
| 93 | + | idCounter := uint64(0) | |
| 94 | + | idGenerator := func() string { | |
| 95 | + | idCounter++ | |
| 96 | + | return fmt.Sprintf("%c%d", 'a'+rune(idCounter%26), idCounter/26) | |
| 97 | + | } | |
| 98 | + | tabsUC := usecase.NewManageTabsUseCase(idGenerator) | |
| 99 | + | panesUC := usecase.NewManagePanesUseCase(idGenerator) | |
| 100 | + | historyUC := usecase.NewSearchHistoryUseCase(historyRepo) | |
| 101 | + | favoritesUC := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo) | |
| 102 | + | zoomUC := usecase.NewManageZoomUseCase(zoomRepo, cfg.DefaultWebpageZoom) | |
| 103 | + | navigateUC := usecase.NewNavigateUseCase(historyRepo, zoomRepo, cfg.DefaultWebpageZoom) | |
| 104 | + | ||
| 105 | + | // Build dependencies | |
| 106 | + | deps := &ui.Dependencies{ | |
| 107 | + | Ctx: ctx, | |
| 108 | + | Config: cfg, | |
| 109 | + | Theme: themeManager, | |
| 110 | + | WebContext: wkCtx, | |
| 111 | + | Pool: pool, | |
| 112 | + | Settings: settings, | |
| 113 | + | Injector: injector, | |
| 114 | + | MessageRouter: messageRouter, | |
| 115 | + | // Repositories | |
| 116 | + | HistoryRepo: historyRepo, | |
| 117 | + | FavoriteRepo: favoriteRepo, | |
| 118 | + | ZoomRepo: zoomRepo, | |
| 119 | + | // Use Cases | |
| 120 | + | TabsUC: tabsUC, | |
| 121 | + | PanesUC: panesUC, | |
| 122 | + | HistoryUC: historyUC, | |
| 123 | + | FavoritesUC: favoritesUC, | |
| 124 | + | ZoomUC: zoomUC, | |
| 125 | + | NavigateUC: navigateUC, | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | // Run the application | |
| 129 | + | exitCode := ui.RunWithArgs(ctx, deps) | |
| 130 | + | os.Exit(exitCode) | |
| 131 | + | } | |
| @@ -0,0 +1,131 @@ | |||||
| 1 | + | package main | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "fmt" | |||
| 6 | + | "os" | |||
| 7 | + | "path/filepath" | |||
| 8 | + | "runtime" | |||
| 9 | + | ||||
| 10 | + | "github.com/bnema/dumber/internal/application/usecase" | |||
| 11 | + | "github.com/bnema/dumber/internal/infrastructure/config" | |||
| 12 | + | "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite" | |||
| 13 | + | "github.com/bnema/dumber/internal/infrastructure/webkit" | |||
| 14 | + | "github.com/bnema/dumber/internal/logging" | |||
| 15 | + | "github.com/bnema/dumber/internal/ui" | |||
| 16 | + | "github.com/bnema/dumber/internal/ui/theme" | |||
| 17 | + | ) | |||
| 18 | + | ||||
| 19 | + | // Build-time variables (set via ldflags) | |||
| 20 | + | var ( | |||
| 21 | + | version = "dev" | |||
| 22 | + | commit = "unknown" | |||
| 23 | + | buildDate = "unknown" | |||
| 24 | + | ) | |||
| 25 | + | ||||
| 26 | + | func main() { | |||
| 27 | + | // GTK requires all GTK calls to be made from the main thread | |||
| 28 | + | runtime.LockOSThread() | |||
| 29 | + | ||||
| 30 | + | // Initialize configuration | |||
| 31 | + | if err := config.Init(); err != nil { | |||
| 32 | + | fmt.Fprintf(os.Stderr, "Failed to initialize configuration: %v\n", err) | |||
| 33 | + | os.Exit(1) | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | cfg := config.Get() | |||
| 37 | + | ||||
| 38 | + | // Initialize logger from config | |||
| 39 | + | logger := logging.NewFromConfigValues(cfg.Logging.Level, cfg.Logging.Format) | |||
| 40 | + | logger.Info(). | |||
| 41 | + | Str("version", version). | |||
| 42 | + | Str("commit", commit). | |||
| 43 | + | Str("build_date", buildDate). | |||
| 44 | + | Msg("starting dumber") | |||
| 45 | + | ||||
| 46 | + | // Create root context with logger | |||
| 47 | + | ctx := logging.WithContext(context.Background(), logger) | |||
| 48 | + | ||||
| 49 | + | // Resolve data/cache directories for WebKit | |||
| 50 | + | dataDir, err := config.GetDataDir() | |||
| 51 | + | if err != nil { | |||
| 52 | + | logger.Fatal().Err(err).Msg("failed to resolve data directory") | |||
| 53 | + | } | |||
| 54 | + | stateDir, err := config.GetStateDir() | |||
| 55 | + | if err != nil { | |||
| 56 | + | logger.Fatal().Err(err).Msg("failed to resolve state directory") | |||
| 57 | + | } | |||
| 58 | + | cacheDir := filepath.Join(stateDir, "webkit-cache") | |||
| 59 | + | if mkErr := os.MkdirAll(cacheDir, 0o755); mkErr != nil { | |||
| 60 | + | logger.Fatal().Err(mkErr).Str("path", cacheDir).Msg("failed to create cache directory") | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | // Initialize SQLite database | |||
| 64 | + | dbPath := filepath.Join(stateDir, "dumber.db") | |||
| 65 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |||
| 66 | + | if err != nil { | |||
| 67 | + | logger.Fatal().Err(err).Str("path", dbPath).Msg("failed to initialize database") | |||
| 68 | + | } | |||
| 69 | + | defer sqlite.Close(db) | |||
| 70 | + | ||||
| 71 | + | // Create repositories | |||
| 72 | + | historyRepo := sqlite.NewHistoryRepository(db) | |||
| 73 | + | favoriteRepo := sqlite.NewFavoriteRepository(db) | |||
| 74 | + | folderRepo := sqlite.NewFolderRepository(db) | |||
| 75 | + | tagRepo := sqlite.NewTagRepository(db) | |||
| 76 | + | zoomRepo := sqlite.NewZoomRepository(db) | |||
| 77 | + | ||||
| 78 | + | // Theme management | |||
| 79 | + | themeManager := theme.NewManager(ctx, cfg) | |||
| 80 | + | ||||
| 81 | + | // WebKit plumbing | |||
| 82 | + | wkCtx, err := webkit.NewWebKitContext(ctx, dataDir, cacheDir) | |||
| 83 | + | if err != nil { | |||
| 84 | + | logger.Fatal().Err(err).Msg("failed to initialize WebKit context") | |||
| 85 | + | } | |||
| 86 | + | settings := webkit.NewSettingsManager(ctx, cfg) | |||
| 87 | + | injector := webkit.NewContentInjector(themeManager.PrefersDark()) | |||
| 88 | + | messageRouter := webkit.NewMessageRouter(ctx) | |||
| 89 | + | poolCfg := webkit.DefaultPoolConfig() | |||
| 90 | + | pool := webkit.NewWebViewPool(ctx, wkCtx, settings, poolCfg, injector, messageRouter) | |||
| 91 | + | ||||
| 92 | + | // Create use cases | |||
| 93 | + | idCounter := uint64(0) | |||
| 94 | + | idGenerator := func() string { | |||
| 95 | + | idCounter++ | |||
| 96 | + | return fmt.Sprintf("%c%d", 'a'+rune(idCounter%26), idCounter/26) | |||
| 97 | + | } | |||
| 98 | + | tabsUC := usecase.NewManageTabsUseCase(idGenerator) | |||
| 99 | + | panesUC := usecase.NewManagePanesUseCase(idGenerator) | |||
| 100 | + | historyUC := usecase.NewSearchHistoryUseCase(historyRepo) | |||
| 101 | + | favoritesUC := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo) | |||
| 102 | + | zoomUC := usecase.NewManageZoomUseCase(zoomRepo, cfg.DefaultWebpageZoom) | |||
| 103 | + | navigateUC := usecase.NewNavigateUseCase(historyRepo, zoomRepo, cfg.DefaultWebpageZoom) | |||
| 104 | + | ||||
| 105 | + | // Build dependencies | |||
| 106 | + | deps := &ui.Dependencies{ | |||
| 107 | + | Ctx: ctx, | |||
| 108 | + | Config: cfg, | |||
| 109 | + | Theme: themeManager, | |||
| 110 | + | WebContext: wkCtx, | |||
| 111 | + | Pool: pool, | |||
| 112 | + | Settings: settings, | |||
| 113 | + | Injector: injector, | |||
| 114 | + | MessageRouter: messageRouter, | |||
| 115 | + | // Repositories | |||
| 116 | + | HistoryRepo: historyRepo, | |||
| 117 | + | FavoriteRepo: favoriteRepo, | |||
| 118 | + | ZoomRepo: zoomRepo, | |||
| 119 | + | // Use Cases | |||
| 120 | + | TabsUC: tabsUC, | |||
| 121 | + | PanesUC: panesUC, | |||
| 122 | + | HistoryUC: historyUC, | |||
| 123 | + | FavoritesUC: favoritesUC, | |||
| 124 | + | ZoomUC: zoomUC, | |||
| 125 | + | NavigateUC: navigateUC, | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | // Run the application | |||
| 129 | + | exitCode := ui.RunWithArgs(ctx, deps) | |||
| 130 | + | os.Exit(exitCode) | |||
| 131 | + | } | |||
M internal/ui/app.go +51 -14
| @@ -58,8 +58,8 @@ type App struct { | |||
| 58 | 58 | focusMgr *focus.Manager | |
| 59 | 59 | borderMgr *focus.BorderManager | |
| 60 | 60 | ||
| 61 | - | // Native omnibox | |
| 62 | - | omnibox *component.Omnibox | |
| 61 | + | // Omnibox configuration (omnibox is created per workspace view) | |
| 62 | + | omniboxCfg component.OmniboxConfig | |
| 63 | 63 | ||
| 64 | 64 | // Web content (managed by ContentCoordinator) | |
| 65 | 65 | pool *webkit.WebViewPool | |
| @@ -188,8 +188,8 @@ func (a *App) onActivate(ctx context.Context) { | |||
| 188 | 188 | }) | |
| 189 | 189 | a.keyboardHandler.AttachTo(a.mainWindow.Window()) | |
| 190 | 190 | ||
| 191 | - | // Create native omnibox | |
| 192 | - | a.omnibox = component.NewOmnibox(ctx, a.mainWindow.Window(), component.OmniboxConfig{ | |
| 191 | + | // Store omnibox config (omnibox is created per-pane via WorkspaceView) | |
| 192 | + | a.omniboxCfg = component.OmniboxConfig{ | |
| 193 | 193 | HistoryUC: a.deps.HistoryUC, | |
| 194 | 194 | FavoritesUC: a.deps.FavoritesUC, | |
| 195 | 195 | FaviconCache: a.faviconCache, | |
| @@ -197,18 +197,12 @@ func (a *App) onActivate(ctx context.Context) { | |||
| 197 | 197 | DefaultSearch: a.deps.Config.DefaultSearchEngine, | |
| 198 | 198 | InitialBehavior: a.deps.Config.Omnibox.InitialBehavior, | |
| 199 | 199 | UIScale: a.deps.Config.DefaultUIScale, | |
| 200 | - | }) | |
| 201 | - | if a.omnibox != nil { | |
| 202 | - | a.omnibox.SetOnNavigate(func(url string) { | |
| 200 | + | OnNavigate: func(url string) { | |
| 203 | 201 | a.navCoord.Navigate(ctx, url) | |
| 204 | - | }) | |
| 205 | - | a.navCoord.SetOmnibox(a.omnibox) | |
| 206 | - | // Add omnibox widget to the main window overlay | |
| 207 | - | a.mainWindow.AddOverlay(a.omnibox.Widget()) | |
| 208 | - | log.Debug().Msg("native omnibox created and added to overlay") | |
| 209 | - | } else { | |
| 210 | - | log.Warn().Msg("failed to create native omnibox") | |
| 202 | + | }, | |
| 211 | 203 | } | |
| 204 | + | a.navCoord.SetOmniboxProvider(a) | |
| 205 | + | log.Debug().Msg("omnibox config stored, provider set") | |
| 212 | 206 | ||
| 213 | 207 | // Create an initial tab using coordinator | |
| 214 | 208 | if _, err := a.tabCoord.Create(ctx, "https://duckduckgo.com"); err != nil { | |
| @@ -396,6 +390,9 @@ func (a *App) createWorkspaceView(ctx context.Context, tab *entity.Tab) { | |||
| 396 | 390 | wsView.SetModeBorderOverlay(a.borderMgr.Widget()) | |
| 397 | 391 | } | |
| 398 | 392 | ||
| 393 | + | // Set omnibox config for this workspace view | |
| 394 | + | wsView.SetOmniboxConfig(a.omniboxCfg) | |
| 395 | + | ||
| 399 | 396 | // Store in map | |
| 400 | 397 | a.workspaceViews[tab.ID] = wsView | |
| 401 | 398 | ||
| @@ -430,3 +427,43 @@ func (a *App) activeWorkspaceView() *component.WorkspaceView { | |||
| 430 | 427 | } | |
| 431 | 428 | return a.workspaceViews[activeTab.ID] | |
| 432 | 429 | } | |
| 430 | + | ||
| 431 | + | // ToggleOmnibox implements OmniboxProvider. | |
| 432 | + | // Toggles the omnibox visibility in the active workspace view. | |
| 433 | + | func (a *App) ToggleOmnibox(ctx context.Context) { | |
| 434 | + | log := logging.FromContext(ctx) | |
| 435 | + | ||
| 436 | + | wsView := a.activeWorkspaceView() | |
| 437 | + | if wsView == nil { | |
| 438 | + | log.Warn().Msg("no active workspace view for omnibox toggle") | |
| 439 | + | return | |
| 440 | + | } | |
| 441 | + | ||
| 442 | + | if wsView.IsOmniboxVisible() { | |
| 443 | + | wsView.HideOmnibox() | |
| 444 | + | } else { | |
| 445 | + | wsView.ShowOmnibox(ctx, "") | |
| 446 | + | } | |
| 447 | + | } | |
| 448 | + | ||
| 449 | + | // UpdateOmniboxZoom implements OmniboxProvider. | |
| 450 | + | // Updates the zoom indicator on the current omnibox if visible. | |
| 451 | + | func (a *App) UpdateOmniboxZoom(factor float64) { | |
| 452 | + | wsView := a.activeWorkspaceView() | |
| 453 | + | if wsView == nil { | |
| 454 | + | return | |
| 455 | + | } | |
| 456 | + | ||
| 457 | + | omnibox := wsView.GetOmnibox() | |
| 458 | + | if omnibox != nil { | |
| 459 | + | omnibox.UpdateZoomIndicator(factor) | |
| 460 | + | } | |
| 461 | + | } | |
| 462 | + | ||
| 463 | + | // SetOmniboxOnNavigate implements OmniboxProvider. | |
| 464 | + | // This is called to set the navigation callback on new omniboxes. | |
| 465 | + | // Since omniboxes are created per-pane, we store the config with the callback. | |
| 466 | + | func (a *App) SetOmniboxOnNavigate(fn func(url string)) { | |
| 467 | + | // The navigate callback is set when the omnibox is created via WorkspaceView.ShowOmnibox | |
| 468 | + | // The WorkspaceView uses the stored omniboxCfg and sets up navigation via the omnibox's SetOnNavigate | |
| 469 | + | } | |
| @@ -58,8 +58,8 @@ type App struct { | |||||
| 58 | focusMgr *focus.Manager | 58 | focusMgr *focus.Manager | ||
| 59 | borderMgr *focus.BorderManager | 59 | borderMgr *focus.BorderManager | ||
| 60 | 60 | ||||
| 61 | - | // Native omnibox | 61 | + | // Omnibox configuration (omnibox is created per workspace view) |
| 62 | - | omnibox *component.Omnibox | 62 | + | omniboxCfg component.OmniboxConfig |
| 63 | 63 | ||||
| 64 | // Web content (managed by ContentCoordinator) | 64 | // Web content (managed by ContentCoordinator) | ||
| 65 | pool *webkit.WebViewPool | 65 | pool *webkit.WebViewPool | ||
| @@ -188,8 +188,8 @@ func (a *App) onActivate(ctx context.Context) { | |||||
| 188 | }) | 188 | }) | ||
| 189 | a.keyboardHandler.AttachTo(a.mainWindow.Window()) | 189 | a.keyboardHandler.AttachTo(a.mainWindow.Window()) | ||
| 190 | 190 | ||||
| 191 | - | // Create native omnibox | 191 | + | // Store omnibox config (omnibox is created per-pane via WorkspaceView) |
| 192 | - | a.omnibox = component.NewOmnibox(ctx, a.mainWindow.Window(), component.OmniboxConfig{ | 192 | + | a.omniboxCfg = component.OmniboxConfig{ |
| 193 | HistoryUC: a.deps.HistoryUC, | 193 | HistoryUC: a.deps.HistoryUC, | ||
| 194 | FavoritesUC: a.deps.FavoritesUC, | 194 | FavoritesUC: a.deps.FavoritesUC, | ||
| 195 | FaviconCache: a.faviconCache, | 195 | FaviconCache: a.faviconCache, | ||
| @@ -197,18 +197,12 @@ func (a *App) onActivate(ctx context.Context) { | |||||
| 197 | DefaultSearch: a.deps.Config.DefaultSearchEngine, | 197 | DefaultSearch: a.deps.Config.DefaultSearchEngine, | ||
| 198 | InitialBehavior: a.deps.Config.Omnibox.InitialBehavior, | 198 | InitialBehavior: a.deps.Config.Omnibox.InitialBehavior, | ||
| 199 | UIScale: a.deps.Config.DefaultUIScale, | 199 | UIScale: a.deps.Config.DefaultUIScale, | ||
| 200 | - | }) | 200 | + | OnNavigate: func(url string) { |
| 201 | - | if a.omnibox != nil { | |||
| 202 | - | a.omnibox.SetOnNavigate(func(url string) { | |||
| 203 | a.navCoord.Navigate(ctx, url) | 201 | a.navCoord.Navigate(ctx, url) | ||
| 204 | - | }) | 202 | + | }, |
| 205 | - | a.navCoord.SetOmnibox(a.omnibox) | |||
| 206 | - | // Add omnibox widget to the main window overlay | |||
| 207 | - | a.mainWindow.AddOverlay(a.omnibox.Widget()) | |||
| 208 | - | log.Debug().Msg("native omnibox created and added to overlay") | |||
| 209 | - | } else { | |||
| 210 | - | log.Warn().Msg("failed to create native omnibox") | |||
| 211 | } | 203 | } | ||
| 204 | + | a.navCoord.SetOmniboxProvider(a) | |||
| 205 | + | log.Debug().Msg("omnibox config stored, provider set") | |||
| 212 | 206 | ||||
| 213 | // Create an initial tab using coordinator | 207 | // Create an initial tab using coordinator | ||
| 214 | if _, err := a.tabCoord.Create(ctx, "https://duckduckgo.com"); err != nil { | 208 | if _, err := a.tabCoord.Create(ctx, "https://duckduckgo.com"); err != nil { | ||
| @@ -396,6 +390,9 @@ func (a *App) createWorkspaceView(ctx context.Context, tab *entity.Tab) { | |||||
| 396 | wsView.SetModeBorderOverlay(a.borderMgr.Widget()) | 390 | wsView.SetModeBorderOverlay(a.borderMgr.Widget()) | ||
| 397 | } | 391 | } | ||
| 398 | 392 | ||||
| 393 | + | // Set omnibox config for this workspace view | |||
| 394 | + | wsView.SetOmniboxConfig(a.omniboxCfg) | |||
| 395 | + | ||||
| 399 | // Store in map | 396 | // Store in map | ||
| 400 | a.workspaceViews[tab.ID] = wsView | 397 | a.workspaceViews[tab.ID] = wsView | ||
| 401 | 398 | ||||
| @@ -430,3 +427,43 @@ func (a *App) activeWorkspaceView() *component.WorkspaceView { | |||||
| 430 | } | 427 | } | ||
| 431 | return a.workspaceViews[activeTab.ID] | 428 | return a.workspaceViews[activeTab.ID] | ||
| 432 | } | 429 | } | ||
| 430 | + | ||||
| 431 | + | // ToggleOmnibox implements OmniboxProvider. | |||
| 432 | + | // Toggles the omnibox visibility in the active workspace view. | |||
| 433 | + | func (a *App) ToggleOmnibox(ctx context.Context) { | |||
| 434 | + | log := logging.FromContext(ctx) | |||
| 435 | + | ||||
| 436 | + | wsView := a.activeWorkspaceView() | |||
| 437 | + | if wsView == nil { | |||
| 438 | + | log.Warn().Msg("no active workspace view for omnibox toggle") | |||
| 439 | + | return | |||
| 440 | + | } | |||
| 441 | + | ||||
| 442 | + | if wsView.IsOmniboxVisible() { | |||
| 443 | + | wsView.HideOmnibox() | |||
| 444 | + | } else { | |||
| 445 | + | wsView.ShowOmnibox(ctx, "") | |||
| 446 | + | } | |||
| 447 | + | } | |||
| 448 | + | ||||
| 449 | + | // UpdateOmniboxZoom implements OmniboxProvider. | |||
| 450 | + | // Updates the zoom indicator on the current omnibox if visible. | |||
| 451 | + | func (a *App) UpdateOmniboxZoom(factor float64) { | |||
| 452 | + | wsView := a.activeWorkspaceView() | |||
| 453 | + | if wsView == nil { | |||
| 454 | + | return | |||
| 455 | + | } | |||
| 456 | + | ||||
| 457 | + | omnibox := wsView.GetOmnibox() | |||
| 458 | + | if omnibox != nil { | |||
| 459 | + | omnibox.UpdateZoomIndicator(factor) | |||
| 460 | + | } | |||
| 461 | + | } | |||
| 462 | + | ||||
| 463 | + | // SetOmniboxOnNavigate implements OmniboxProvider. | |||
| 464 | + | // This is called to set the navigation callback on new omniboxes. | |||
| 465 | + | // Since omniboxes are created per-pane, we store the config with the callback. | |||
| 466 | + | func (a *App) SetOmniboxOnNavigate(fn func(url string)) { | |||
| 467 | + | // The navigate callback is set when the omnibox is created via WorkspaceView.ShowOmnibox | |||
| 468 | + | // The WorkspaceView uses the stored omniboxCfg and sets up navigation via the omnibox's SetOnNavigate | |||
| 469 | + | } | |||
M internal/ui/cache/favicon_cache.go +3 -3
| @@ -49,9 +49,9 @@ type FaviconCache struct { | |||
| 49 | 49 | cache map[string]*gdk.Texture // key: domain (e.g., "github.com") | |
| 50 | 50 | faviconDB *webkit.FaviconDatabase | |
| 51 | 51 | client *http.Client | |
| 52 | - | diskCacheDir string // path to favicon cache directory | |
| 53 | - | writeChan chan diskWrite // channel for async writes | |
| 54 | - | closeOnce sync.Once // ensures Close is called only once | |
| 52 | + | diskCacheDir string // path to favicon cache directory | |
| 53 | + | writeChan chan diskWrite // channel for async writes | |
| 54 | + | closeOnce sync.Once // ensures Close is called only once | |
| 55 | 55 | } | |
| 56 | 56 | ||
| 57 | 57 | // NewFaviconCache creates a new FaviconCache with the given FaviconDatabase. | |
| @@ -49,9 +49,9 @@ type FaviconCache struct { | |||||
| 49 | cache map[string]*gdk.Texture // key: domain (e.g., "github.com") | 49 | cache map[string]*gdk.Texture // key: domain (e.g., "github.com") | ||
| 50 | faviconDB *webkit.FaviconDatabase | 50 | faviconDB *webkit.FaviconDatabase | ||
| 51 | client *http.Client | 51 | client *http.Client | ||
| 52 | - | diskCacheDir string // path to favicon cache directory | 52 | + | diskCacheDir string // path to favicon cache directory |
| 53 | - | writeChan chan diskWrite // channel for async writes | 53 | + | writeChan chan diskWrite // channel for async writes |
| 54 | - | closeOnce sync.Once // ensures Close is called only once | 54 | + | closeOnce sync.Once // ensures Close is called only once |
| 55 | } | 55 | } | ||
| 56 | 56 | ||||
| 57 | // NewFaviconCache creates a new FaviconCache with the given FaviconDatabase. | 57 | // NewFaviconCache creates a new FaviconCache with the given FaviconDatabase. | ||
M internal/ui/component/omnibox.go +45 -18
| @@ -12,6 +12,7 @@ import ( | |||
| 12 | 12 | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 13 | 13 | "github.com/bnema/dumber/internal/logging" | |
| 14 | 14 | "github.com/bnema/dumber/internal/ui/cache" | |
| 15 | + | "github.com/bnema/dumber/internal/ui/layout" | |
| 15 | 16 | "github.com/jwijenbergh/puregotk/v4/gdk" | |
| 16 | 17 | "github.com/jwijenbergh/puregotk/v4/glib" | |
| 17 | 18 | "github.com/jwijenbergh/puregotk/v4/gtk" | |
| @@ -51,8 +52,8 @@ type Favorite struct { | |||
| 51 | 52 | // Omnibox is the native GTK4 address bar / command palette. | |
| 52 | 53 | type Omnibox struct { | |
| 53 | 54 | // GTK widgets | |
| 54 | - | outerBox *gtk.Box // Outer container for positioning | |
| 55 | - | mainBox *gtk.Box // Main content box | |
| 55 | + | outerBox *gtk.Box // Outer container for positioning | |
| 56 | + | mainBox *gtk.Box // Main content box | |
| 56 | 57 | headerBox *gtk.Box | |
| 57 | 58 | historyBtn *gtk.Button | |
| 58 | 59 | favoritesBtn *gtk.Button | |
| @@ -61,8 +62,8 @@ type Omnibox struct { | |||
| 61 | 62 | scrolledWin *gtk.ScrolledWindow | |
| 62 | 63 | listBox *gtk.ListBox | |
| 63 | 64 | ||
| 64 | - | // Parent window reference for sizing | |
| 65 | - | parentWindow *gtk.ApplicationWindow | |
| 65 | + | // Parent overlay reference for sizing (set via SetParentOverlay) | |
| 66 | + | parentOverlay layout.OverlayWidget | |
| 66 | 67 | ||
| 67 | 68 | // State | |
| 68 | 69 | mu sync.RWMutex | |
| @@ -110,11 +111,13 @@ type OmniboxConfig struct { | |||
| 110 | 111 | Shortcuts map[string]config.SearchShortcut | |
| 111 | 112 | DefaultSearch string | |
| 112 | 113 | InitialBehavior string | |
| 113 | - | UIScale float64 // UI scale for favicon sizing | |
| 114 | + | UIScale float64 // UI scale for favicon sizing | |
| 115 | + | OnNavigate func(url string) // Callback when user navigates via omnibox | |
| 114 | 116 | } | |
| 115 | 117 | ||
| 116 | 118 | // NewOmnibox creates a new native GTK4 omnibox widget. | |
| 117 | - | func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxConfig) *Omnibox { | |
| 119 | + | // Call SetParentOverlay() before Show() to set the parent for sizing. | |
| 120 | + | func NewOmnibox(ctx context.Context, cfg OmniboxConfig) *Omnibox { | |
| 118 | 121 | log := logging.FromContext(ctx) | |
| 119 | 122 | ||
| 120 | 123 | uiScale := cfg.UIScale | |
| @@ -123,7 +126,6 @@ func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxC | |||
| 123 | 126 | } | |
| 124 | 127 | ||
| 125 | 128 | o := &Omnibox{ | |
| 126 | - | parentWindow: parent, | |
| 127 | 129 | viewMode: ViewModeHistory, | |
| 128 | 130 | selectedIndex: -1, | |
| 129 | 131 | historyUC: cfg.HistoryUC, | |
| @@ -144,10 +146,32 @@ func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxC | |||
| 144 | 146 | o.setupKeyboardHandling() | |
| 145 | 147 | o.setupEntryChanged() | |
| 146 | 148 | ||
| 149 | + | // Set navigation callback if provided | |
| 150 | + | if cfg.OnNavigate != nil { | |
| 151 | + | o.onNavigate = cfg.OnNavigate | |
| 152 | + | } | |
| 153 | + | ||
| 147 | 154 | log.Debug().Msg("omnibox created") | |
| 148 | 155 | return o | |
| 149 | 156 | } | |
| 150 | 157 | ||
| 158 | + | // SetParentOverlay sets the overlay widget used for sizing calculations. | |
| 159 | + | // Must be called before Show(). | |
| 160 | + | func (o *Omnibox) SetParentOverlay(overlay layout.OverlayWidget) { | |
| 161 | + | o.mu.Lock() | |
| 162 | + | defer o.mu.Unlock() | |
| 163 | + | o.parentOverlay = overlay | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | // WidgetAsLayout returns the omnibox's outer widget as a layout.Widget. | |
| 167 | + | // This is useful for adding the omnibox to a PaneView overlay. | |
| 168 | + | func (o *Omnibox) WidgetAsLayout(factory layout.WidgetFactory) layout.Widget { | |
| 169 | + | if o.outerBox == nil { | |
| 170 | + | return nil | |
| 171 | + | } | |
| 172 | + | return factory.WrapWidget(&o.outerBox.Widget) | |
| 173 | + | } | |
| 174 | + | ||
| 151 | 175 | // updateSize sets the omnibox size based on parent window dimensions. | |
| 152 | 176 | // Deprecated: use resizeAndCenter with row count instead. | |
| 153 | 177 | func (o *Omnibox) updateSize() { | |
| @@ -157,20 +181,20 @@ func (o *Omnibox) updateSize() { | |||
| 157 | 181 | // resizeAndCenter adjusts the omnibox size based on content and centers it. | |
| 158 | 182 | // rowCount is the number of result rows to display (0 = no content, max 10). | |
| 159 | 183 | func (o *Omnibox) resizeAndCenter(rowCount int) { | |
| 160 | - | if o.parentWindow == nil || o.outerBox == nil || o.mainBox == nil { | |
| 184 | + | if o.parentOverlay == nil || o.outerBox == nil || o.mainBox == nil { | |
| 161 | 185 | return | |
| 162 | 186 | } | |
| 163 | 187 | ||
| 164 | 188 | // Use GetAllocatedWidth/Height for actual rendered size | |
| 165 | - | parentWidth := o.parentWindow.GetAllocatedWidth() | |
| 166 | - | parentHeight := o.parentWindow.GetAllocatedHeight() | |
| 189 | + | parentWidth := o.parentOverlay.GetAllocatedWidth() | |
| 190 | + | parentHeight := o.parentOverlay.GetAllocatedHeight() | |
| 167 | 191 | ||
| 168 | 192 | if parentWidth <= 0 || parentHeight <= 0 { | |
| 169 | 193 | log := logging.FromContext(o.ctx) | |
| 170 | 194 | log.Error(). | |
| 171 | 195 | Int("parent_width", parentWidth). | |
| 172 | 196 | Int("parent_height", parentHeight). | |
| 173 | - | Msg("omnibox: invalid parent window dimensions") | |
| 197 | + | Msg("omnibox: invalid parent overlay dimensions") | |
| 174 | 198 | return | |
| 175 | 199 | } | |
| 176 | 200 | ||
| @@ -251,9 +275,9 @@ func (o *Omnibox) createWidgets() error { | |||
| 251 | 275 | return errNilWidget("outerBox") | |
| 252 | 276 | } | |
| 253 | 277 | o.outerBox.AddCssClass("omnibox-outer") | |
| 254 | - | o.outerBox.SetHalign(gtk.AlignCenterValue) // Center horizontally | |
| 255 | - | o.outerBox.SetValign(gtk.AlignStartValue) // Align to top | |
| 256 | - | o.outerBox.SetVisible(false) // Hidden by default | |
| 278 | + | o.outerBox.SetHalign(gtk.AlignCenterValue) // Center horizontally | |
| 279 | + | o.outerBox.SetValign(gtk.AlignStartValue) // Align to top | |
| 280 | + | o.outerBox.SetVisible(false) // Hidden by default | |
| 257 | 281 | ||
| 258 | 282 | // Main content box | |
| 259 | 283 | o.mainBox = gtk.NewBox(gtk.OrientationVerticalValue, 0) | |
| @@ -679,9 +703,12 @@ func (o *Omnibox) rebuildList() { | |||
| 679 | 703 | // Schedule measurement if not yet cached and we have rows | |
| 680 | 704 | if !o.measuredHeights.valid && o.listBox.GetRowAtIndex(0) != nil { | |
| 681 | 705 | var cb glib.SourceFunc = func(uintptr) bool { | |
| 682 | - | width := o.parentWindow.GetAllocatedWidth() | |
| 706 | + | if o.parentOverlay == nil { | |
| 707 | + | return false | |
| 708 | + | } | |
| 709 | + | width := o.parentOverlay.GetAllocatedWidth() | |
| 683 | 710 | if width <= 0 { | |
| 684 | - | return false // Window not allocated yet, skip | |
| 711 | + | return false // Overlay not allocated yet, skip | |
| 685 | 712 | } | |
| 686 | 713 | forWidth := int(float64(width) * omniboxWidthPct) | |
| 687 | 714 | if o.measureComponentHeights(forWidth) { | |
| @@ -1104,8 +1131,8 @@ func (o *Omnibox) Show(ctx context.Context, query string) { | |||
| 1104 | 1131 | } | |
| 1105 | 1132 | ||
| 1106 | 1133 | // Set width and vertical positioning (20% from top) | |
| 1107 | - | parentWidth := o.parentWindow.GetAllocatedWidth() | |
| 1108 | - | parentHeight := o.parentWindow.GetAllocatedHeight() | |
| 1134 | + | parentWidth := o.parentOverlay.GetAllocatedWidth() | |
| 1135 | + | parentHeight := o.parentOverlay.GetAllocatedHeight() | |
| 1109 | 1136 | width := int(float64(parentWidth) * omniboxWidthPct) | |
| 1110 | 1137 | marginTop := int(float64(parentHeight) * 0.20) // 20% from top | |
| 1111 | 1138 | ||
| @@ -12,6 +12,7 @@ import ( | |||||
| 12 | "github.com/bnema/dumber/internal/infrastructure/config" | 12 | "github.com/bnema/dumber/internal/infrastructure/config" | ||
| 13 | "github.com/bnema/dumber/internal/logging" | 13 | "github.com/bnema/dumber/internal/logging" | ||
| 14 | "github.com/bnema/dumber/internal/ui/cache" | 14 | "github.com/bnema/dumber/internal/ui/cache" | ||
| 15 | + | "github.com/bnema/dumber/internal/ui/layout" | |||
| 15 | "github.com/jwijenbergh/puregotk/v4/gdk" | 16 | "github.com/jwijenbergh/puregotk/v4/gdk" | ||
| 16 | "github.com/jwijenbergh/puregotk/v4/glib" | 17 | "github.com/jwijenbergh/puregotk/v4/glib" | ||
| 17 | "github.com/jwijenbergh/puregotk/v4/gtk" | 18 | "github.com/jwijenbergh/puregotk/v4/gtk" | ||
| @@ -51,8 +52,8 @@ type Favorite struct { | |||||
| 51 | // Omnibox is the native GTK4 address bar / command palette. | 52 | // Omnibox is the native GTK4 address bar / command palette. | ||
| 52 | type Omnibox struct { | 53 | type Omnibox struct { | ||
| 53 | // GTK widgets | 54 | // GTK widgets | ||
| 54 | - | outerBox *gtk.Box // Outer container for positioning | 55 | + | outerBox *gtk.Box // Outer container for positioning |
| 55 | - | mainBox *gtk.Box // Main content box | 56 | + | mainBox *gtk.Box // Main content box |
| 56 | headerBox *gtk.Box | 57 | headerBox *gtk.Box | ||
| 57 | historyBtn *gtk.Button | 58 | historyBtn *gtk.Button | ||
| 58 | favoritesBtn *gtk.Button | 59 | favoritesBtn *gtk.Button | ||
| @@ -61,8 +62,8 @@ type Omnibox struct { | |||||
| 61 | scrolledWin *gtk.ScrolledWindow | 62 | scrolledWin *gtk.ScrolledWindow | ||
| 62 | listBox *gtk.ListBox | 63 | listBox *gtk.ListBox | ||
| 63 | 64 | ||||
| 64 | - | // Parent window reference for sizing | 65 | + | // Parent overlay reference for sizing (set via SetParentOverlay) |
| 65 | - | parentWindow *gtk.ApplicationWindow | 66 | + | parentOverlay layout.OverlayWidget |
| 66 | 67 | ||||
| 67 | // State | 68 | // State | ||
| 68 | mu sync.RWMutex | 69 | mu sync.RWMutex | ||
| @@ -110,11 +111,13 @@ type OmniboxConfig struct { | |||||
| 110 | Shortcuts map[string]config.SearchShortcut | 111 | Shortcuts map[string]config.SearchShortcut | ||
| 111 | DefaultSearch string | 112 | DefaultSearch string | ||
| 112 | InitialBehavior string | 113 | InitialBehavior string | ||
| 113 | - | UIScale float64 // UI scale for favicon sizing | 114 | + | UIScale float64 // UI scale for favicon sizing |
| 115 | + | OnNavigate func(url string) // Callback when user navigates via omnibox | |||
| 114 | } | 116 | } | ||
| 115 | 117 | ||||
| 116 | // NewOmnibox creates a new native GTK4 omnibox widget. | 118 | // NewOmnibox creates a new native GTK4 omnibox widget. | ||
| 117 | - | func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxConfig) *Omnibox { | 119 | + | // Call SetParentOverlay() before Show() to set the parent for sizing. |
| 120 | + | func NewOmnibox(ctx context.Context, cfg OmniboxConfig) *Omnibox { | |||
| 118 | log := logging.FromContext(ctx) | 121 | log := logging.FromContext(ctx) | ||
| 119 | 122 | ||||
| 120 | uiScale := cfg.UIScale | 123 | uiScale := cfg.UIScale | ||
| @@ -123,7 +126,6 @@ func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxC | |||||
| 123 | } | 126 | } | ||
| 124 | 127 | ||||
| 125 | o := &Omnibox{ | 128 | o := &Omnibox{ | ||
| 126 | - | parentWindow: parent, | |||
| 127 | viewMode: ViewModeHistory, | 129 | viewMode: ViewModeHistory, | ||
| 128 | selectedIndex: -1, | 130 | selectedIndex: -1, | ||
| 129 | historyUC: cfg.HistoryUC, | 131 | historyUC: cfg.HistoryUC, | ||
| @@ -144,10 +146,32 @@ func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxC | |||||
| 144 | o.setupKeyboardHandling() | 146 | o.setupKeyboardHandling() | ||
| 145 | o.setupEntryChanged() | 147 | o.setupEntryChanged() | ||
| 146 | 148 | ||||
| 149 | + | // Set navigation callback if provided | |||
| 150 | + | if cfg.OnNavigate != nil { | |||
| 151 | + | o.onNavigate = cfg.OnNavigate | |||
| 152 | + | } | |||
| 153 | + | ||||
| 147 | log.Debug().Msg("omnibox created") | 154 | log.Debug().Msg("omnibox created") | ||
| 148 | return o | 155 | return o | ||
| 149 | } | 156 | } | ||
| 150 | 157 | ||||
| 158 | + | // SetParentOverlay sets the overlay widget used for sizing calculations. | |||
| 159 | + | // Must be called before Show(). | |||
| 160 | + | func (o *Omnibox) SetParentOverlay(overlay layout.OverlayWidget) { | |||
| 161 | + | o.mu.Lock() | |||
| 162 | + | defer o.mu.Unlock() | |||
| 163 | + | o.parentOverlay = overlay | |||
| 164 | + | } | |||
| 165 | + | ||||
| 166 | + | // WidgetAsLayout returns the omnibox's outer widget as a layout.Widget. | |||
| 167 | + | // This is useful for adding the omnibox to a PaneView overlay. | |||
| 168 | + | func (o *Omnibox) WidgetAsLayout(factory layout.WidgetFactory) layout.Widget { | |||
| 169 | + | if o.outerBox == nil { | |||
| 170 | + | return nil | |||
| 171 | + | } | |||
| 172 | + | return factory.WrapWidget(&o.outerBox.Widget) | |||
| 173 | + | } | |||
| 174 | + | ||||
| 151 | // updateSize sets the omnibox size based on parent window dimensions. | 175 | // updateSize sets the omnibox size based on parent window dimensions. | ||
| 152 | // Deprecated: use resizeAndCenter with row count instead. | 176 | // Deprecated: use resizeAndCenter with row count instead. | ||
| 153 | func (o *Omnibox) updateSize() { | 177 | func (o *Omnibox) updateSize() { | ||
| @@ -157,20 +181,20 @@ func (o *Omnibox) updateSize() { | |||||
| 157 | // resizeAndCenter adjusts the omnibox size based on content and centers it. | 181 | // resizeAndCenter adjusts the omnibox size based on content and centers it. | ||
| 158 | // rowCount is the number of result rows to display (0 = no content, max 10). | 182 | // rowCount is the number of result rows to display (0 = no content, max 10). | ||
| 159 | func (o *Omnibox) resizeAndCenter(rowCount int) { | 183 | func (o *Omnibox) resizeAndCenter(rowCount int) { | ||
| 160 | - | if o.parentWindow == nil || o.outerBox == nil || o.mainBox == nil { | 184 | + | if o.parentOverlay == nil || o.outerBox == nil || o.mainBox == nil { |
| 161 | return | 185 | return | ||
| 162 | } | 186 | } | ||
| 163 | 187 | ||||
| 164 | // Use GetAllocatedWidth/Height for actual rendered size | 188 | // Use GetAllocatedWidth/Height for actual rendered size | ||
| 165 | - | parentWidth := o.parentWindow.GetAllocatedWidth() | 189 | + | parentWidth := o.parentOverlay.GetAllocatedWidth() |
| 166 | - | parentHeight := o.parentWindow.GetAllocatedHeight() | 190 | + | parentHeight := o.parentOverlay.GetAllocatedHeight() |
| 167 | 191 | ||||
| 168 | if parentWidth <= 0 || parentHeight <= 0 { | 192 | if parentWidth <= 0 || parentHeight <= 0 { | ||
| 169 | log := logging.FromContext(o.ctx) | 193 | log := logging.FromContext(o.ctx) | ||
| 170 | log.Error(). | 194 | log.Error(). | ||
| 171 | Int("parent_width", parentWidth). | 195 | Int("parent_width", parentWidth). | ||
| 172 | Int("parent_height", parentHeight). | 196 | Int("parent_height", parentHeight). | ||
| 173 | - | Msg("omnibox: invalid parent window dimensions") | 197 | + | Msg("omnibox: invalid parent overlay dimensions") |
| 174 | return | 198 | return | ||
| 175 | } | 199 | } | ||
| 176 | 200 | ||||
| @@ -251,9 +275,9 @@ func (o *Omnibox) createWidgets() error { | |||||
| 251 | return errNilWidget("outerBox") | 275 | return errNilWidget("outerBox") | ||
| 252 | } | 276 | } | ||
| 253 | o.outerBox.AddCssClass("omnibox-outer") | 277 | o.outerBox.AddCssClass("omnibox-outer") | ||
| 254 | - | o.outerBox.SetHalign(gtk.AlignCenterValue) // Center horizontally | 278 | + | o.outerBox.SetHalign(gtk.AlignCenterValue) // Center horizontally |
| 255 | - | o.outerBox.SetValign(gtk.AlignStartValue) // Align to top | 279 | + | o.outerBox.SetValign(gtk.AlignStartValue) // Align to top |
| 256 | - | o.outerBox.SetVisible(false) // Hidden by default | 280 | + | o.outerBox.SetVisible(false) // Hidden by default |
| 257 | 281 | ||||
| 258 | // Main content box | 282 | // Main content box | ||
| 259 | o.mainBox = gtk.NewBox(gtk.OrientationVerticalValue, 0) | 283 | o.mainBox = gtk.NewBox(gtk.OrientationVerticalValue, 0) | ||
| @@ -679,9 +703,12 @@ func (o *Omnibox) rebuildList() { | |||||
| 679 | // Schedule measurement if not yet cached and we have rows | 703 | // Schedule measurement if not yet cached and we have rows | ||
| 680 | if !o.measuredHeights.valid && o.listBox.GetRowAtIndex(0) != nil { | 704 | if !o.measuredHeights.valid && o.listBox.GetRowAtIndex(0) != nil { | ||
| 681 | var cb glib.SourceFunc = func(uintptr) bool { | 705 | var cb glib.SourceFunc = func(uintptr) bool { | ||
| 682 | - | width := o.parentWindow.GetAllocatedWidth() | 706 | + | if o.parentOverlay == nil { |
| 707 | + | return false | |||
| 708 | + | } | |||
| 709 | + | width := o.parentOverlay.GetAllocatedWidth() | |||
| 683 | if width <= 0 { | 710 | if width <= 0 { | ||
| 684 | - | return false // Window not allocated yet, skip | 711 | + | return false // Overlay not allocated yet, skip |
| 685 | } | 712 | } | ||
| 686 | forWidth := int(float64(width) * omniboxWidthPct) | 713 | forWidth := int(float64(width) * omniboxWidthPct) | ||
| 687 | if o.measureComponentHeights(forWidth) { | 714 | if o.measureComponentHeights(forWidth) { | ||
| @@ -1104,8 +1131,8 @@ func (o *Omnibox) Show(ctx context.Context, query string) { | |||||
| 1104 | } | 1131 | } | ||
| 1105 | 1132 | ||||
| 1106 | // Set width and vertical positioning (20% from top) | 1133 | // Set width and vertical positioning (20% from top) | ||
| 1107 | - | parentWidth := o.parentWindow.GetAllocatedWidth() | 1134 | + | parentWidth := o.parentOverlay.GetAllocatedWidth() |
| 1108 | - | parentHeight := o.parentWindow.GetAllocatedHeight() | 1135 | + | parentHeight := o.parentOverlay.GetAllocatedHeight() |
| 1109 | width := int(float64(parentWidth) * omniboxWidthPct) | 1136 | width := int(float64(parentWidth) * omniboxWidthPct) | ||
| 1110 | marginTop := int(float64(parentHeight) * 0.20) // 20% from top | 1137 | marginTop := int(float64(parentHeight) * 0.20) // 20% from top | ||
| 1111 | 1138 | ||||
M internal/ui/component/pane_view.go +18 -0
| @@ -210,3 +210,21 @@ func (pv *PaneView) AddCssClass(class string) { | |||
| 210 | 210 | func (pv *PaneView) RemoveCssClass(class string) { | |
| 211 | 211 | pv.overlay.RemoveCssClass(class) | |
| 212 | 212 | } | |
| 213 | + | ||
| 214 | + | // AddOverlayWidget adds a widget as an overlay on this pane. | |
| 215 | + | // The widget will appear above the WebView and border. | |
| 216 | + | func (pv *PaneView) AddOverlayWidget(widget layout.Widget) { | |
| 217 | + | pv.overlay.AddOverlay(widget) | |
| 218 | + | pv.overlay.SetClipOverlay(widget, false) | |
| 219 | + | pv.overlay.SetMeasureOverlay(widget, false) | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | // RemoveOverlayWidget removes an overlay widget from this pane. | |
| 223 | + | func (pv *PaneView) RemoveOverlayWidget(widget layout.Widget) { | |
| 224 | + | pv.overlay.RemoveOverlay(widget) | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | // GetContentDimensions returns the pane's allocated width and height. | |
| 228 | + | func (pv *PaneView) GetContentDimensions() (width, height int) { | |
| 229 | + | return pv.overlay.GetAllocatedWidth(), pv.overlay.GetAllocatedHeight() | |
| 230 | + | } | |
| @@ -210,3 +210,21 @@ func (pv *PaneView) AddCssClass(class string) { | |||||
| 210 | func (pv *PaneView) RemoveCssClass(class string) { | 210 | func (pv *PaneView) RemoveCssClass(class string) { | ||
| 211 | pv.overlay.RemoveCssClass(class) | 211 | pv.overlay.RemoveCssClass(class) | ||
| 212 | } | 212 | } | ||
| 213 | + | ||||
| 214 | + | // AddOverlayWidget adds a widget as an overlay on this pane. | |||
| 215 | + | // The widget will appear above the WebView and border. | |||
| 216 | + | func (pv *PaneView) AddOverlayWidget(widget layout.Widget) { | |||
| 217 | + | pv.overlay.AddOverlay(widget) | |||
| 218 | + | pv.overlay.SetClipOverlay(widget, false) | |||
| 219 | + | pv.overlay.SetMeasureOverlay(widget, false) | |||
| 220 | + | } | |||
| 221 | + | ||||
| 222 | + | // RemoveOverlayWidget removes an overlay widget from this pane. | |||
| 223 | + | func (pv *PaneView) RemoveOverlayWidget(widget layout.Widget) { | |||
| 224 | + | pv.overlay.RemoveOverlay(widget) | |||
| 225 | + | } | |||
| 226 | + | ||||
| 227 | + | // GetContentDimensions returns the pane's allocated width and height. | |||
| 228 | + | func (pv *PaneView) GetContentDimensions() (width, height int) { | |||
| 229 | + | return pv.overlay.GetAllocatedWidth(), pv.overlay.GetAllocatedHeight() | |||
| 230 | + | } | |||
M internal/ui/component/workspace_view.go +118 -0
| @@ -31,6 +31,12 @@ type WorkspaceView struct { | |||
| 31 | 31 | // Mode border overlay slot | |
| 32 | 32 | modeBorderWidget layout.Widget | |
| 33 | 33 | ||
| 34 | + | // Omnibox lifecycle | |
| 35 | + | omnibox *Omnibox // Current omnibox (nil if not shown) | |
| 36 | + | omniboxWidget layout.Widget // Wrapped omnibox widget for overlay | |
| 37 | + | omniboxPaneID entity.PaneID // Which pane has the omnibox | |
| 38 | + | omniboxCfg OmniboxConfig // Stored config for creating omniboxes | |
| 39 | + | ||
| 34 | 40 | workspace *entity.Workspace | |
| 35 | 41 | paneViews map[entity.PaneID]*PaneView | |
| 36 | 42 | activePaneID entity.PaneID | |
| @@ -154,6 +160,11 @@ func (wv *WorkspaceView) SetActivePaneID(paneID entity.PaneID) error { | |||
| 154 | 160 | ||
| 155 | 161 | // setActivePaneIDInternal updates active pane without locking. | |
| 156 | 162 | func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error { | |
| 163 | + | // Destroy omnibox if active pane is changing | |
| 164 | + | if wv.activePaneID != paneID && wv.omnibox != nil { | |
| 165 | + | wv.hideOmniboxInternal() | |
| 166 | + | } | |
| 167 | + | ||
| 157 | 168 | // Deactivate current active pane | |
| 158 | 169 | if wv.activePaneID != "" { | |
| 159 | 170 | if oldPV, ok := wv.paneViews[wv.activePaneID]; ok { | |
| @@ -401,3 +412,110 @@ func (wv *WorkspaceView) GetStackContainerWidget(paneID entity.PaneID) layout.Wi | |||
| 401 | 412 | func (wv *WorkspaceView) ContainerWidget() layout.Widget { | |
| 402 | 413 | return wv.container | |
| 403 | 414 | } | |
| 415 | + | ||
| 416 | + | // SetOmniboxConfig stores the omnibox configuration for later use. | |
| 417 | + | func (wv *WorkspaceView) SetOmniboxConfig(cfg OmniboxConfig) { | |
| 418 | + | wv.mu.Lock() | |
| 419 | + | defer wv.mu.Unlock() | |
| 420 | + | wv.omniboxCfg = cfg | |
| 421 | + | } | |
| 422 | + | ||
| 423 | + | // ShowOmnibox creates and shows the omnibox in the active pane. | |
| 424 | + | func (wv *WorkspaceView) ShowOmnibox(ctx context.Context, query string) { | |
| 425 | + | wv.mu.Lock() | |
| 426 | + | defer wv.mu.Unlock() | |
| 427 | + | ||
| 428 | + | // If omnibox already exists, just show it | |
| 429 | + | if wv.omnibox != nil { | |
| 430 | + | wv.omnibox.Show(ctx, query) | |
| 431 | + | return | |
| 432 | + | } | |
| 433 | + | ||
| 434 | + | // Get the active pane view | |
| 435 | + | pv := wv.paneViews[wv.activePaneID] | |
| 436 | + | if pv == nil { | |
| 437 | + | wv.logger.Warn().Str("paneID", string(wv.activePaneID)).Msg("cannot show omnibox: active pane not found") | |
| 438 | + | return | |
| 439 | + | } | |
| 440 | + | ||
| 441 | + | // Create omnibox | |
| 442 | + | omnibox := NewOmnibox(ctx, wv.omniboxCfg) | |
| 443 | + | if omnibox == nil { | |
| 444 | + | wv.logger.Error().Msg("failed to create omnibox") | |
| 445 | + | return | |
| 446 | + | } | |
| 447 | + | ||
| 448 | + | // Set parent overlay for sizing | |
| 449 | + | omnibox.SetParentOverlay(pv.Overlay()) | |
| 450 | + | ||
| 451 | + | // Wrap widget for layout system | |
| 452 | + | omniboxWidget := omnibox.WidgetAsLayout(wv.factory) | |
| 453 | + | if omniboxWidget == nil { | |
| 454 | + | wv.logger.Error().Msg("failed to wrap omnibox widget") | |
| 455 | + | return | |
| 456 | + | } | |
| 457 | + | ||
| 458 | + | // Add to pane overlay | |
| 459 | + | pv.AddOverlayWidget(omniboxWidget) | |
| 460 | + | ||
| 461 | + | // Store references | |
| 462 | + | wv.omnibox = omnibox | |
| 463 | + | wv.omniboxWidget = omniboxWidget | |
| 464 | + | wv.omniboxPaneID = wv.activePaneID | |
| 465 | + | ||
| 466 | + | // Show the omnibox | |
| 467 | + | omnibox.Show(ctx, query) | |
| 468 | + | ||
| 469 | + | wv.logger.Debug().Str("paneID", string(wv.activePaneID)).Msg("omnibox shown") | |
| 470 | + | } | |
| 471 | + | ||
| 472 | + | // HideOmnibox hides and destroys the current omnibox. | |
| 473 | + | func (wv *WorkspaceView) HideOmnibox() { | |
| 474 | + | wv.mu.Lock() | |
| 475 | + | defer wv.mu.Unlock() | |
| 476 | + | ||
| 477 | + | wv.hideOmniboxInternal() | |
| 478 | + | } | |
| 479 | + | ||
| 480 | + | // hideOmniboxInternal destroys the omnibox without locking. | |
| 481 | + | func (wv *WorkspaceView) hideOmniboxInternal() { | |
| 482 | + | if wv.omnibox == nil { | |
| 483 | + | return | |
| 484 | + | } | |
| 485 | + | ||
| 486 | + | // Hide the omnibox first (use Background context since this is internal cleanup) | |
| 487 | + | wv.omnibox.Hide(context.Background()) | |
| 488 | + | ||
| 489 | + | // Remove from pane overlay | |
| 490 | + | if pv := wv.paneViews[wv.omniboxPaneID]; pv != nil && wv.omniboxWidget != nil { | |
| 491 | + | pv.RemoveOverlayWidget(wv.omniboxWidget) | |
| 492 | + | } | |
| 493 | + | ||
| 494 | + | // Clear references | |
| 495 | + | wv.omnibox = nil | |
| 496 | + | wv.omniboxWidget = nil | |
| 497 | + | wv.omniboxPaneID = "" | |
| 498 | + | ||
| 499 | + | wv.logger.Debug().Msg("omnibox hidden and destroyed") | |
| 500 | + | } | |
| 501 | + | ||
| 502 | + | // GetOmnibox returns the current omnibox if visible. | |
| 503 | + | func (wv *WorkspaceView) GetOmnibox() *Omnibox { | |
| 504 | + | wv.mu.RLock() | |
| 505 | + | defer wv.mu.RUnlock() | |
| 506 | + | return wv.omnibox | |
| 507 | + | } | |
| 508 | + | ||
| 509 | + | // IsOmniboxVisible returns whether the omnibox is currently visible. | |
| 510 | + | func (wv *WorkspaceView) IsOmniboxVisible() bool { | |
| 511 | + | wv.mu.RLock() | |
| 512 | + | defer wv.mu.RUnlock() | |
| 513 | + | return wv.omnibox != nil && wv.omnibox.IsVisible() | |
| 514 | + | } | |
| 515 | + | ||
| 516 | + | // GetActivePaneView returns the PaneView for the current active pane. | |
| 517 | + | func (wv *WorkspaceView) GetActivePaneView() *PaneView { | |
| 518 | + | wv.mu.RLock() | |
| 519 | + | defer wv.mu.RUnlock() | |
| 520 | + | return wv.paneViews[wv.activePaneID] | |
| 521 | + | } | |
| @@ -31,6 +31,12 @@ type WorkspaceView struct { | |||||
| 31 | // Mode border overlay slot | 31 | // Mode border overlay slot | ||
| 32 | modeBorderWidget layout.Widget | 32 | modeBorderWidget layout.Widget | ||
| 33 | 33 | ||||
| 34 | + | // Omnibox lifecycle | |||
| 35 | + | omnibox *Omnibox // Current omnibox (nil if not shown) | |||
| 36 | + | omniboxWidget layout.Widget // Wrapped omnibox widget for overlay | |||
| 37 | + | omniboxPaneID entity.PaneID // Which pane has the omnibox | |||
| 38 | + | omniboxCfg OmniboxConfig // Stored config for creating omniboxes | |||
| 39 | + | ||||
| 34 | workspace *entity.Workspace | 40 | workspace *entity.Workspace | ||
| 35 | paneViews map[entity.PaneID]*PaneView | 41 | paneViews map[entity.PaneID]*PaneView | ||
| 36 | activePaneID entity.PaneID | 42 | activePaneID entity.PaneID | ||
| @@ -154,6 +160,11 @@ func (wv *WorkspaceView) SetActivePaneID(paneID entity.PaneID) error { | |||||
| 154 | 160 | ||||
| 155 | // setActivePaneIDInternal updates active pane without locking. | 161 | // setActivePaneIDInternal updates active pane without locking. | ||
| 156 | func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error { | 162 | func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error { | ||
| 163 | + | // Destroy omnibox if active pane is changing | |||
| 164 | + | if wv.activePaneID != paneID && wv.omnibox != nil { | |||
| 165 | + | wv.hideOmniboxInternal() | |||
| 166 | + | } | |||
| 167 | + | ||||
| 157 | // Deactivate current active pane | 168 | // Deactivate current active pane | ||
| 158 | if wv.activePaneID != "" { | 169 | if wv.activePaneID != "" { | ||
| 159 | if oldPV, ok := wv.paneViews[wv.activePaneID]; ok { | 170 | if oldPV, ok := wv.paneViews[wv.activePaneID]; ok { | ||
| @@ -401,3 +412,110 @@ func (wv *WorkspaceView) GetStackContainerWidget(paneID entity.PaneID) layout.Wi | |||||
| 401 | func (wv *WorkspaceView) ContainerWidget() layout.Widget { | 412 | func (wv *WorkspaceView) ContainerWidget() layout.Widget { | ||
| 402 | return wv.container | 413 | return wv.container | ||
| 403 | } | 414 | } | ||
| 415 | + | ||||
| 416 | + | // SetOmniboxConfig stores the omnibox configuration for later use. | |||
| 417 | + | func (wv *WorkspaceView) SetOmniboxConfig(cfg OmniboxConfig) { | |||
| 418 | + | wv.mu.Lock() | |||
| 419 | + | defer wv.mu.Unlock() | |||
| 420 | + | wv.omniboxCfg = cfg | |||
| 421 | + | } | |||
| 422 | + | ||||
| 423 | + | // ShowOmnibox creates and shows the omnibox in the active pane. | |||
| 424 | + | func (wv *WorkspaceView) ShowOmnibox(ctx context.Context, query string) { | |||
| 425 | + | wv.mu.Lock() | |||
| 426 | + | defer wv.mu.Unlock() | |||
| 427 | + | ||||
| 428 | + | // If omnibox already exists, just show it | |||
| 429 | + | if wv.omnibox != nil { | |||
| 430 | + | wv.omnibox.Show(ctx, query) | |||
| 431 | + | return | |||
| 432 | + | } | |||
| 433 | + | ||||
| 434 | + | // Get the active pane view | |||
| 435 | + | pv := wv.paneViews[wv.activePaneID] | |||
| 436 | + | if pv == nil { | |||
| 437 | + | wv.logger.Warn().Str("paneID", string(wv.activePaneID)).Msg("cannot show omnibox: active pane not found") | |||
| 438 | + | return | |||
| 439 | + | } | |||
| 440 | + | ||||
| 441 | + | // Create omnibox | |||
| 442 | + | omnibox := NewOmnibox(ctx, wv.omniboxCfg) | |||
| 443 | + | if omnibox == nil { | |||
| 444 | + | wv.logger.Error().Msg("failed to create omnibox") | |||
| 445 | + | return | |||
| 446 | + | } | |||
| 447 | + | ||||
| 448 | + | // Set parent overlay for sizing | |||
| 449 | + | omnibox.SetParentOverlay(pv.Overlay()) | |||
| 450 | + | ||||
| 451 | + | // Wrap widget for layout system | |||
| 452 | + | omniboxWidget := omnibox.WidgetAsLayout(wv.factory) | |||
| 453 | + | if omniboxWidget == nil { | |||
| 454 | + | wv.logger.Error().Msg("failed to wrap omnibox widget") | |||
| 455 | + | return | |||
| 456 | + | } | |||
| 457 | + | ||||
| 458 | + | // Add to pane overlay | |||
| 459 | + | pv.AddOverlayWidget(omniboxWidget) | |||
| 460 | + | ||||
| 461 | + | // Store references | |||
| 462 | + | wv.omnibox = omnibox | |||
| 463 | + | wv.omniboxWidget = omniboxWidget | |||
| 464 | + | wv.omniboxPaneID = wv.activePaneID | |||
| 465 | + | ||||
| 466 | + | // Show the omnibox | |||
| 467 | + | omnibox.Show(ctx, query) | |||
| 468 | + | ||||
| 469 | + | wv.logger.Debug().Str("paneID", string(wv.activePaneID)).Msg("omnibox shown") | |||
| 470 | + | } | |||
| 471 | + | ||||
| 472 | + | // HideOmnibox hides and destroys the current omnibox. | |||
| 473 | + | func (wv *WorkspaceView) HideOmnibox() { | |||
| 474 | + | wv.mu.Lock() | |||
| 475 | + | defer wv.mu.Unlock() | |||
| 476 | + | ||||
| 477 | + | wv.hideOmniboxInternal() | |||
| 478 | + | } | |||
| 479 | + | ||||
| 480 | + | // hideOmniboxInternal destroys the omnibox without locking. | |||
| 481 | + | func (wv *WorkspaceView) hideOmniboxInternal() { | |||
| 482 | + | if wv.omnibox == nil { | |||
| 483 | + | return | |||
| 484 | + | } | |||
| 485 | + | ||||
| 486 | + | // Hide the omnibox first (use Background context since this is internal cleanup) | |||
| 487 | + | wv.omnibox.Hide(context.Background()) | |||
| 488 | + | ||||
| 489 | + | // Remove from pane overlay | |||
| 490 | + | if pv := wv.paneViews[wv.omniboxPaneID]; pv != nil && wv.omniboxWidget != nil { | |||
| 491 | + | pv.RemoveOverlayWidget(wv.omniboxWidget) | |||
| 492 | + | } | |||
| 493 | + | ||||
| 494 | + | // Clear references | |||
| 495 | + | wv.omnibox = nil | |||
| 496 | + | wv.omniboxWidget = nil | |||
| 497 | + | wv.omniboxPaneID = "" | |||
| 498 | + | ||||
| 499 | + | wv.logger.Debug().Msg("omnibox hidden and destroyed") | |||
| 500 | + | } | |||
| 501 | + | ||||
| 502 | + | // GetOmnibox returns the current omnibox if visible. | |||
| 503 | + | func (wv *WorkspaceView) GetOmnibox() *Omnibox { | |||
| 504 | + | wv.mu.RLock() | |||
| 505 | + | defer wv.mu.RUnlock() | |||
| 506 | + | return wv.omnibox | |||
| 507 | + | } | |||
| 508 | + | ||||
| 509 | + | // IsOmniboxVisible returns whether the omnibox is currently visible. | |||
| 510 | + | func (wv *WorkspaceView) IsOmniboxVisible() bool { | |||
| 511 | + | wv.mu.RLock() | |||
| 512 | + | defer wv.mu.RUnlock() | |||
| 513 | + | return wv.omnibox != nil && wv.omnibox.IsVisible() | |||
| 514 | + | } | |||
| 515 | + | ||||
| 516 | + | // GetActivePaneView returns the PaneView for the current active pane. | |||
| 517 | + | func (wv *WorkspaceView) GetActivePaneView() *PaneView { | |||
| 518 | + | wv.mu.RLock() | |||
| 519 | + | defer wv.mu.RUnlock() | |||
| 520 | + | return wv.paneViews[wv.activePaneID] | |||
| 521 | + | } | |||
M internal/ui/coordinator/navigation.go +19 -13
| @@ -8,14 +8,20 @@ import ( | |||
| 8 | 8 | "github.com/bnema/dumber/internal/domain/entity" | |
| 9 | 9 | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 10 | 10 | "github.com/bnema/dumber/internal/logging" | |
| 11 | - | "github.com/bnema/dumber/internal/ui/component" | |
| 12 | 11 | ) | |
| 13 | 12 | ||
| 13 | + | // OmniboxProvider provides access to omnibox operations. | |
| 14 | + | type OmniboxProvider interface { | |
| 15 | + | ToggleOmnibox(ctx context.Context) | |
| 16 | + | UpdateOmniboxZoom(factor float64) | |
| 17 | + | SetOmniboxOnNavigate(fn func(url string)) | |
| 18 | + | } | |
| 19 | + | ||
| 14 | 20 | // NavigationCoordinator handles URL navigation, history, and browser controls. | |
| 15 | 21 | type NavigationCoordinator struct { | |
| 16 | - | navigateUC *usecase.NavigateUseCase | |
| 17 | - | contentCoord *ContentCoordinator | |
| 18 | - | omnibox *component.Omnibox | |
| 22 | + | navigateUC *usecase.NavigateUseCase | |
| 23 | + | contentCoord *ContentCoordinator | |
| 24 | + | omniboxProvider OmniboxProvider | |
| 19 | 25 | } | |
| 20 | 26 | ||
| 21 | 27 | // NewNavigationCoordinator creates a new NavigationCoordinator. | |
| @@ -33,9 +39,9 @@ func NewNavigationCoordinator( | |||
| 33 | 39 | } | |
| 34 | 40 | } | |
| 35 | 41 | ||
| 36 | - | // SetOmnibox sets the omnibox reference for toggle operations. | |
| 37 | - | func (c *NavigationCoordinator) SetOmnibox(omnibox *component.Omnibox) { | |
| 38 | - | c.omnibox = omnibox | |
| 42 | + | // SetOmniboxProvider sets the omnibox provider for toggle/zoom operations. | |
| 43 | + | func (c *NavigationCoordinator) SetOmniboxProvider(provider OmniboxProvider) { | |
| 44 | + | c.omniboxProvider = provider | |
| 39 | 45 | } | |
| 40 | 46 | ||
| 41 | 47 | // Navigate loads a URL in the active pane using NavigateUseCase. | |
| @@ -164,13 +170,13 @@ func (c *NavigationCoordinator) GoForward(ctx context.Context) error { | |||
| 164 | 170 | func (c *NavigationCoordinator) OpenOmnibox(ctx context.Context) error { | |
| 165 | 171 | log := logging.FromContext(ctx) | |
| 166 | 172 | ||
| 167 | - | if c.omnibox == nil { | |
| 168 | - | log.Error().Msg("omnibox not initialized") | |
| 169 | - | return fmt.Errorf("omnibox not initialized") | |
| 173 | + | if c.omniboxProvider == nil { | |
| 174 | + | log.Error().Msg("omnibox provider not initialized") | |
| 175 | + | return fmt.Errorf("omnibox provider not initialized") | |
| 170 | 176 | } | |
| 171 | 177 | ||
| 172 | 178 | log.Debug().Msg("toggling omnibox") | |
| 173 | - | c.omnibox.Toggle(ctx) | |
| 179 | + | c.omniboxProvider.ToggleOmnibox(ctx) | |
| 174 | 180 | return nil | |
| 175 | 181 | } | |
| 176 | 182 | ||
| @@ -214,7 +220,7 @@ func (c *NavigationCoordinator) ActiveWebView(ctx context.Context) *webkit.WebVi | |||
| 214 | 220 | ||
| 215 | 221 | // NotifyZoomChanged updates the omnibox zoom indicator. | |
| 216 | 222 | func (c *NavigationCoordinator) NotifyZoomChanged(ctx context.Context, factor float64) { | |
| 217 | - | if c.omnibox != nil { | |
| 218 | - | c.omnibox.UpdateZoomIndicator(factor) | |
| 223 | + | if c.omniboxProvider != nil { | |
| 224 | + | c.omniboxProvider.UpdateOmniboxZoom(factor) | |
| 219 | 225 | } | |
| 220 | 226 | } | |
| @@ -8,14 +8,20 @@ import ( | |||||
| 8 | "github.com/bnema/dumber/internal/domain/entity" | 8 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 9 | "github.com/bnema/dumber/internal/infrastructure/webkit" | 9 | "github.com/bnema/dumber/internal/infrastructure/webkit" | ||
| 10 | "github.com/bnema/dumber/internal/logging" | 10 | "github.com/bnema/dumber/internal/logging" | ||
| 11 | - | "github.com/bnema/dumber/internal/ui/component" | |||
| 12 | ) | 11 | ) | ||
| 13 | 12 | ||||
| 13 | + | // OmniboxProvider provides access to omnibox operations. | |||
| 14 | + | type OmniboxProvider interface { | |||
| 15 | + | ToggleOmnibox(ctx context.Context) | |||
| 16 | + | UpdateOmniboxZoom(factor float64) | |||
| 17 | + | SetOmniboxOnNavigate(fn func(url string)) | |||
| 18 | + | } | |||
| 19 | + | ||||
| 14 | // NavigationCoordinator handles URL navigation, history, and browser controls. | 20 | // NavigationCoordinator handles URL navigation, history, and browser controls. | ||
| 15 | type NavigationCoordinator struct { | 21 | type NavigationCoordinator struct { | ||
| 16 | - | navigateUC *usecase.NavigateUseCase | 22 | + | navigateUC *usecase.NavigateUseCase |
| 17 | - | contentCoord *ContentCoordinator | 23 | + | contentCoord *ContentCoordinator |
| 18 | - | omnibox *component.Omnibox | 24 | + | omniboxProvider OmniboxProvider |
| 19 | } | 25 | } | ||
| 20 | 26 | ||||
| 21 | // NewNavigationCoordinator creates a new NavigationCoordinator. | 27 | // NewNavigationCoordinator creates a new NavigationCoordinator. | ||
| @@ -33,9 +39,9 @@ func NewNavigationCoordinator( | |||||
| 33 | } | 39 | } | ||
| 34 | } | 40 | } | ||
| 35 | 41 | ||||
| 36 | - | // SetOmnibox sets the omnibox reference for toggle operations. | 42 | + | // SetOmniboxProvider sets the omnibox provider for toggle/zoom operations. |
| 37 | - | func (c *NavigationCoordinator) SetOmnibox(omnibox *component.Omnibox) { | 43 | + | func (c *NavigationCoordinator) SetOmniboxProvider(provider OmniboxProvider) { |
| 38 | - | c.omnibox = omnibox | 44 | + | c.omniboxProvider = provider |
| 39 | } | 45 | } | ||
| 40 | 46 | ||||
| 41 | // Navigate loads a URL in the active pane using NavigateUseCase. | 47 | // Navigate loads a URL in the active pane using NavigateUseCase. | ||
| @@ -164,13 +170,13 @@ func (c *NavigationCoordinator) GoForward(ctx context.Context) error { | |||||
| 164 | func (c *NavigationCoordinator) OpenOmnibox(ctx context.Context) error { | 170 | func (c *NavigationCoordinator) OpenOmnibox(ctx context.Context) error { | ||
| 165 | log := logging.FromContext(ctx) | 171 | log := logging.FromContext(ctx) | ||
| 166 | 172 | ||||
| 167 | - | if c.omnibox == nil { | 173 | + | if c.omniboxProvider == nil { |
| 168 | - | log.Error().Msg("omnibox not initialized") | 174 | + | log.Error().Msg("omnibox provider not initialized") |
| 169 | - | return fmt.Errorf("omnibox not initialized") | 175 | + | return fmt.Errorf("omnibox provider not initialized") |
| 170 | } | 176 | } | ||
| 171 | 177 | ||||
| 172 | log.Debug().Msg("toggling omnibox") | 178 | log.Debug().Msg("toggling omnibox") | ||
| 173 | - | c.omnibox.Toggle(ctx) | 179 | + | c.omniboxProvider.ToggleOmnibox(ctx) |
| 174 | return nil | 180 | return nil | ||
| 175 | } | 181 | } | ||
| 176 | 182 | ||||
| @@ -214,7 +220,7 @@ func (c *NavigationCoordinator) ActiveWebView(ctx context.Context) *webkit.WebVi | |||||
| 214 | 220 | ||||
| 215 | // NotifyZoomChanged updates the omnibox zoom indicator. | 221 | // NotifyZoomChanged updates the omnibox zoom indicator. | ||
| 216 | func (c *NavigationCoordinator) NotifyZoomChanged(ctx context.Context, factor float64) { | 222 | func (c *NavigationCoordinator) NotifyZoomChanged(ctx context.Context, factor float64) { | ||
| 217 | - | if c.omnibox != nil { | 223 | + | if c.omniboxProvider != nil { |
| 218 | - | c.omnibox.UpdateZoomIndicator(factor) | 224 | + | c.omniboxProvider.UpdateOmniboxZoom(factor) |
| 219 | } | 225 | } | ||
| 220 | } | 226 | } | ||
M internal/ui/coordinator/workspace.go +16 -0
| @@ -136,6 +136,10 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli | |||
| 136 | 136 | } | |
| 137 | 137 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | |
| 138 | 138 | } | |
| 139 | + | // Update workspace view's active pane tracking | |
| 140 | + | if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil { | |
| 141 | + | log.Warn().Err(err).Msg("failed to set active pane in workspace view") | |
| 142 | + | } | |
| 139 | 143 | wsView.FocusPane(ws.ActivePaneID) | |
| 140 | 144 | } else if wsView != nil { | |
| 141 | 145 | // No existing widget (shouldn't happen), fallback to rebuild | |
| @@ -143,6 +147,10 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli | |||
| 143 | 147 | log.Error().Err(err).Msg("failed to rebuild workspace view") | |
| 144 | 148 | } | |
| 145 | 149 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | |
| 150 | + | // Update workspace view's active pane tracking | |
| 151 | + | if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil { | |
| 152 | + | log.Warn().Err(err).Msg("failed to set active pane in workspace view") | |
| 153 | + | } | |
| 146 | 154 | wsView.FocusPane(ws.ActivePaneID) | |
| 147 | 155 | } | |
| 148 | 156 | ||
| @@ -542,6 +550,10 @@ func (c *WorkspaceCoordinator) ClosePane(ctx context.Context) error { | |||
| 542 | 550 | c.contentCoord.ReleaseWebView(ctx, closingPaneID) | |
| 543 | 551 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | |
| 544 | 552 | } | |
| 553 | + | // Update workspace view's active pane tracking | |
| 554 | + | if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil { | |
| 555 | + | log.Warn().Err(err).Msg("failed to set active pane in workspace view") | |
| 556 | + | } | |
| 545 | 557 | wsView.FocusPane(ws.ActivePaneID) | |
| 546 | 558 | } else if wsView != nil { | |
| 547 | 559 | // Fallback to rebuild if we couldn't get context | |
| @@ -550,6 +562,10 @@ func (c *WorkspaceCoordinator) ClosePane(ctx context.Context) error { | |||
| 550 | 562 | } | |
| 551 | 563 | c.contentCoord.ReleaseWebView(ctx, closingPaneID) | |
| 552 | 564 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | |
| 565 | + | // Update workspace view's active pane tracking | |
| 566 | + | if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil { | |
| 567 | + | log.Warn().Err(err).Msg("failed to set active pane in workspace view") | |
| 568 | + | } | |
| 553 | 569 | wsView.FocusPane(ws.ActivePaneID) | |
| 554 | 570 | } | |
| 555 | 571 | ||
| @@ -136,6 +136,10 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli | |||||
| 136 | } | 136 | } | ||
| 137 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | 137 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | ||
| 138 | } | 138 | } | ||
| 139 | + | // Update workspace view's active pane tracking | |||
| 140 | + | if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil { | |||
| 141 | + | log.Warn().Err(err).Msg("failed to set active pane in workspace view") | |||
| 142 | + | } | |||
| 139 | wsView.FocusPane(ws.ActivePaneID) | 143 | wsView.FocusPane(ws.ActivePaneID) | ||
| 140 | } else if wsView != nil { | 144 | } else if wsView != nil { | ||
| 141 | // No existing widget (shouldn't happen), fallback to rebuild | 145 | // No existing widget (shouldn't happen), fallback to rebuild | ||
| @@ -143,6 +147,10 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli | |||||
| 143 | log.Error().Err(err).Msg("failed to rebuild workspace view") | 147 | log.Error().Err(err).Msg("failed to rebuild workspace view") | ||
| 144 | } | 148 | } | ||
| 145 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | 149 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | ||
| 150 | + | // Update workspace view's active pane tracking | |||
| 151 | + | if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil { | |||
| 152 | + | log.Warn().Err(err).Msg("failed to set active pane in workspace view") | |||
| 153 | + | } | |||
| 146 | wsView.FocusPane(ws.ActivePaneID) | 154 | wsView.FocusPane(ws.ActivePaneID) | ||
| 147 | } | 155 | } | ||
| 148 | 156 | ||||
| @@ -542,6 +550,10 @@ func (c *WorkspaceCoordinator) ClosePane(ctx context.Context) error { | |||||
| 542 | c.contentCoord.ReleaseWebView(ctx, closingPaneID) | 550 | c.contentCoord.ReleaseWebView(ctx, closingPaneID) | ||
| 543 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | 551 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | ||
| 544 | } | 552 | } | ||
| 553 | + | // Update workspace view's active pane tracking | |||
| 554 | + | if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil { | |||
| 555 | + | log.Warn().Err(err).Msg("failed to set active pane in workspace view") | |||
| 556 | + | } | |||
| 545 | wsView.FocusPane(ws.ActivePaneID) | 557 | wsView.FocusPane(ws.ActivePaneID) | ||
| 546 | } else if wsView != nil { | 558 | } else if wsView != nil { | ||
| 547 | // Fallback to rebuild if we couldn't get context | 559 | // Fallback to rebuild if we couldn't get context | ||
| @@ -550,6 +562,10 @@ func (c *WorkspaceCoordinator) ClosePane(ctx context.Context) error { | |||||
| 550 | } | 562 | } | ||
| 551 | c.contentCoord.ReleaseWebView(ctx, closingPaneID) | 563 | c.contentCoord.ReleaseWebView(ctx, closingPaneID) | ||
| 552 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | 564 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | ||
| 565 | + | // Update workspace view's active pane tracking | |||
| 566 | + | if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil { | |||
| 567 | + | log.Warn().Err(err).Msg("failed to set active pane in workspace view") | |||
| 568 | + | } | |||
| 553 | wsView.FocusPane(ws.ActivePaneID) | 569 | wsView.FocusPane(ws.ActivePaneID) | ||
| 554 | } | 570 | } | ||
| 555 | 571 | ||||
M internal/ui/window/main_window.go +1 -1
| @@ -20,7 +20,7 @@ const ( | |||
| 20 | 20 | // MainWindow represents the main browser window. | |
| 21 | 21 | type MainWindow struct { | |
| 22 | 22 | window *gtk.ApplicationWindow | |
| 23 | - | rootBox *gtk.Box // Vertical: tab bar + content | |
| 23 | + | rootBox *gtk.Box // Vertical: tab bar + content | |
| 24 | 24 | tabBar *component.TabBar | |
| 25 | 25 | contentOverlay *gtk.Overlay // Overlay for content + omnibox | |
| 26 | 26 | contentArea *gtk.Box // Container for workspace content | |
| @@ -20,7 +20,7 @@ const ( | |||||
| 20 | // MainWindow represents the main browser window. | 20 | // MainWindow represents the main browser window. | ||
| 21 | type MainWindow struct { | 21 | type MainWindow struct { | ||
| 22 | window *gtk.ApplicationWindow | 22 | window *gtk.ApplicationWindow | ||
| 23 | - | rootBox *gtk.Box // Vertical: tab bar + content | 23 | + | rootBox *gtk.Box // Vertical: tab bar + content |
| 24 | tabBar *component.TabBar | 24 | tabBar *component.TabBar | ||
| 25 | contentOverlay *gtk.Overlay // Overlay for content + omnibox | 25 | contentOverlay *gtk.Overlay // Overlay for content + omnibox | ||
| 26 | contentArea *gtk.Box // Container for workspace content | 26 | contentArea *gtk.Box // Container for workspace content | ||
A storage/AvMqFGIIrr6_1j389SL02pSAz612eCesZHRPox5jdX0/AvMqFGIIrr6_1j389SL02pSAz612eCesZHRPox5jdX0/LocalStorage/localstorage.sqlite3-shm +0 -0 binary file: no text diff
binary file: no text diff
A storage/AvMqFGIIrr6_1j389SL02pSAz612eCesZHRPox5jdX0/AvMqFGIIrr6_1j389SL02pSAz612eCesZHRPox5jdX0/LocalStorage/localstorage.sqlite3-wal +0 -0 binary file: no text diff
binary file: no text diff
A storage/AvMqFGIIrr6_1j389SL02pSAz612eCesZHRPox5jdX0/AvMqFGIIrr6_1j389SL02pSAz612eCesZHRPox5jdX0/origin +0 -0 binary file: no text diff
binary file: no text diff
A storage/salt +2 -0
| @@ -0,0 +1,2 @@ | |||
| 1 | + | FT | |
| 2 | + | a�x$#⏎̸ | |
| @@ -0,0 +1,2 @@ | |||||
| 1 | + | FT | |||
| 2 | + | a�x$#⏎̸ | |||