feat(browser): add page load progress bar
- commit
- 6532eea697cc5504ba264e455ddd3412eaaa9946
- author
- bnema · · 2025-11-18 22:58 UTC
- parent
- e2080dd
- signature
- unsigned
7 files · +278 -11
M CHANGELOG.md +1 -0
| @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. | |||
| 5 | 5 | ## [Unreleased] | |
| 6 | 6 | ||
| 7 | 7 | ### Added | |
| 8 | + | - **Page load progress bar**: Thin animated indicator at window bottom showing web page loading progress. Tracks WebView load events with gradient animation, auto-hides when complete. | |
| 8 | 9 | - **Omnibox initial history display**: Configurable behavior when opening empty omnibox via `omnibox.initial_behavior` ("recent", "most_visited", "none"). Shows initial history on open and when input is cleared. Default: recent visits. | |
| 9 | 10 | - **Omnibox keyboard blocking**: Prevent page scripts from stealing focus while omnibox is active. | |
| 10 | 11 | ||
| @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. | |||||
| 5 | ## [Unreleased] | 5 | ## [Unreleased] | ||
| 6 | 6 | ||||
| 7 | ### Added | 7 | ### Added | ||
| 8 | + | - **Page load progress bar**: Thin animated indicator at window bottom showing web page loading progress. Tracks WebView load events with gradient animation, auto-hides when complete. | |||
| 8 | - **Omnibox initial history display**: Configurable behavior when opening empty omnibox via `omnibox.initial_behavior` ("recent", "most_visited", "none"). Shows initial history on open and when input is cleared. Default: recent visits. | 9 | - **Omnibox initial history display**: Configurable behavior when opening empty omnibox via `omnibox.initial_behavior` ("recent", "most_visited", "none"). Shows initial history on open and when input is cleared. Default: recent visits. | ||
| 9 | - **Omnibox keyboard blocking**: Prevent page scripts from stealing focus while omnibox is active. | 10 | - **Omnibox keyboard blocking**: Prevent page scripts from stealing focus while omnibox is active. | ||
| 10 | 11 | ||||
A internal/app/browser/progress_bar.go +24 -0
| @@ -0,0 +1,24 @@ | |||
| 1 | + | package browser | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "github.com/bnema/dumber/internal/logging" | |
| 5 | + | "github.com/bnema/dumber/pkg/webkit" | |
| 6 | + | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // createProgressBar builds the shared loading indicator shown at the bottom of the window. | |
| 10 | + | func (tm *TabManager) createProgressBar() gtk.Widgetter { | |
| 11 | + | bar := gtk.NewProgressBar() | |
| 12 | + | if bar == nil { | |
| 13 | + | logging.Error("[tabs] Failed to create tab progress bar") | |
| 14 | + | return nil | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | bar.SetHExpand(true) | |
| 18 | + | bar.SetVExpand(false) | |
| 19 | + | bar.SetShowText(false) | |
| 20 | + | bar.SetFraction(0.0) | |
| 21 | + | bar.AddCSSClass("tab-progress-bar") | |
| 22 | + | webkit.WidgetSetVisible(bar, false) | |
| 23 | + | return bar | |
| 24 | + | } | |
| @@ -0,0 +1,24 @@ | |||||
| 1 | + | package browser | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "github.com/bnema/dumber/internal/logging" | |||
| 5 | + | "github.com/bnema/dumber/pkg/webkit" | |||
| 6 | + | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // createProgressBar builds the shared loading indicator shown at the bottom of the window. | |||
| 10 | + | func (tm *TabManager) createProgressBar() gtk.Widgetter { | |||
| 11 | + | bar := gtk.NewProgressBar() | |||
| 12 | + | if bar == nil { | |||
| 13 | + | logging.Error("[tabs] Failed to create tab progress bar") | |||
| 14 | + | return nil | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | bar.SetHExpand(true) | |||
| 18 | + | bar.SetVExpand(false) | |||
| 19 | + | bar.SetShowText(false) | |||
| 20 | + | bar.SetFraction(0.0) | |||
| 21 | + | bar.AddCSSClass("tab-progress-bar") | |||
| 22 | + | webkit.WidgetSetVisible(bar, false) | |||
| 23 | + | return bar | |||
| 24 | + | } | |||
M internal/app/browser/tab_manager.go +141 -1
| @@ -24,10 +24,12 @@ type TabManager struct { | |||
| 24 | 24 | tabBar gtk.Widgetter // Horizontal tab bar container | |
| 25 | 25 | ContentArea gtk.Widgetter // Container for active workspace (exported for border styling) | |
| 26 | 26 | tabModeTarget gtk.Widgetter // Widget currently holding tab mode border margins | |
| 27 | + | progressBar gtk.Widgetter // Thin load progress indicator | |
| 27 | 28 | ||
| 28 | 29 | // Modal state | |
| 29 | 30 | tabModeActive bool | |
| 30 | 31 | tabModeTimer *time.Timer | |
| 32 | + | lastTabCreate time.Time // Debounce guard for rapid tab creation shortcuts | |
| 31 | 33 | ||
| 32 | 34 | // Rename state | |
| 33 | 35 | renameInProgress bool | |
| @@ -52,6 +54,9 @@ type Tab struct { | |||
| 52 | 54 | workspace *WorkspaceManager | |
| 53 | 55 | titleButton gtk.Widgetter | |
| 54 | 56 | isActive bool | |
| 57 | + | progress float64 | |
| 58 | + | isLoading bool | |
| 59 | + | webViews map[*webkit.WebView]struct{} | |
| 55 | 60 | } | |
| 56 | 61 | ||
| 57 | 62 | // NewTabManager creates a new tab manager instance. | |
| @@ -126,6 +131,9 @@ func (tm *TabManager) createRootContainer() error { | |||
| 126 | 131 | contentArea.SetVExpand(true) | |
| 127 | 132 | contentArea.AddCSSClass("tab-content-area") | |
| 128 | 133 | ||
| 134 | + | // Create progress bar (displayed at the bottom regardless of tab bar position) | |
| 135 | + | progressBar := tm.createProgressBar() | |
| 136 | + | ||
| 129 | 137 | // Add widgets in order based on position | |
| 130 | 138 | if position == "top" { | |
| 131 | 139 | rootBox.Append(tabBar) | |
| @@ -135,10 +143,27 @@ func (tm *TabManager) createRootContainer() error { | |||
| 135 | 143 | rootBox.Append(tabBar) | |
| 136 | 144 | } | |
| 137 | 145 | ||
| 146 | + | // Wrap everything in an overlay so the progress bar can float without resizing layout | |
| 147 | + | rootOverlay := gtk.NewOverlay() | |
| 148 | + | if rootOverlay == nil { | |
| 149 | + | return fmt.Errorf("failed to create root overlay") | |
| 150 | + | } | |
| 151 | + | rootOverlay.SetHExpand(true) | |
| 152 | + | rootOverlay.SetVExpand(true) | |
| 153 | + | rootOverlay.SetChild(rootBox) | |
| 154 | + | if progressBar != nil { | |
| 155 | + | if bar, ok := progressBar.(*gtk.ProgressBar); ok && bar != nil { | |
| 156 | + | bar.SetHAlign(gtk.AlignFill) | |
| 157 | + | bar.SetVAlign(gtk.AlignEnd) | |
| 158 | + | } | |
| 159 | + | rootOverlay.AddOverlay(progressBar) | |
| 160 | + | } | |
| 161 | + | ||
| 138 | 162 | // Store references | |
| 139 | - | tm.rootContainer = rootBox | |
| 163 | + | tm.rootContainer = rootOverlay | |
| 140 | 164 | tm.tabBar = tabBar | |
| 141 | 165 | tm.ContentArea = contentArea | |
| 166 | + | tm.progressBar = progressBar | |
| 142 | 167 | ||
| 143 | 168 | logging.Info(fmt.Sprintf("[tabs] Root container created with tab bar at %s", position)) | |
| 144 | 169 | return nil | |
| @@ -157,6 +182,12 @@ func (tm *TabManager) CreateTab(url string) error { | |||
| 157 | 182 | func (tm *TabManager) createTabInternal(url string) error { | |
| 158 | 183 | logging.Info(fmt.Sprintf("[tabs] Creating new tab with URL: %s", url)) | |
| 159 | 184 | ||
| 185 | + | const tabCreateDebounce = 200 * time.Millisecond | |
| 186 | + | if !tm.lastTabCreate.IsZero() && time.Since(tm.lastTabCreate) < tabCreateDebounce { | |
| 187 | + | logging.Warn("[tabs] Ignoring rapid tab creation request (debounced)") | |
| 188 | + | return nil | |
| 189 | + | } | |
| 190 | + | ||
| 160 | 191 | // Use default URL if empty | |
| 161 | 192 | if url == "" { | |
| 162 | 193 | url = "about:blank" | |
| @@ -172,6 +203,7 @@ func (tm *TabManager) createTabInternal(url string) error { | |||
| 172 | 203 | id: tabID, | |
| 173 | 204 | title: defaultTitle, | |
| 174 | 205 | isActive: false, | |
| 206 | + | webViews: make(map[*webkit.WebView]struct{}), | |
| 175 | 207 | } | |
| 176 | 208 | ||
| 177 | 209 | // Create WebView for this tab | |
| @@ -195,6 +227,7 @@ func (tm *TabManager) createTabInternal(url string) error { | |||
| 195 | 227 | // Create workspace for this tab | |
| 196 | 228 | workspace := NewWorkspaceManager(tm.app, pane) | |
| 197 | 229 | tab.workspace = workspace | |
| 230 | + | tm.registerWebViewLocked(tab, view) | |
| 198 | 231 | ||
| 199 | 232 | // Verify workspace root exists | |
| 200 | 233 | rootWidget := workspace.GetRootWidget() | |
| @@ -222,6 +255,7 @@ func (tm *TabManager) createTabInternal(url string) error { | |||
| 222 | 255 | ||
| 223 | 256 | // Add to tabs slice | |
| 224 | 257 | tm.tabs = append(tm.tabs, tab) | |
| 258 | + | tm.lastTabCreate = time.Now() | |
| 225 | 259 | ||
| 226 | 260 | // Attach click handler with unique ID (pattern from stacked panes) | |
| 227 | 261 | tm.attachTabClickHandler(tab.titleButton, tab) | |
| @@ -325,6 +359,9 @@ func (tm *TabManager) switchToTabInternal(index int) error { | |||
| 325 | 359 | newTab.workspace.RestoreFocus() | |
| 326 | 360 | } | |
| 327 | 361 | ||
| 362 | + | // Sync progress bar with the newly active tab | |
| 363 | + | tm.updateProgressBarLocked() | |
| 364 | + | ||
| 328 | 365 | logging.Info(fmt.Sprintf("[tabs] Switched to tab %d (%s)", index, newTab.id)) | |
| 329 | 366 | return nil | |
| 330 | 367 | } | |
| @@ -405,6 +442,9 @@ func (tm *TabManager) closeTabInternal(index int) error { | |||
| 405 | 442 | // Update tab bar visibility (hide if only 1 tab) | |
| 406 | 443 | tm.updateTabBarVisibility() | |
| 407 | 444 | ||
| 445 | + | // Update progress bar (active tab may have changed after close) | |
| 446 | + | tm.updateProgressBarLocked() | |
| 447 | + | ||
| 408 | 448 | logging.Info(fmt.Sprintf("[tabs] Tab closed (remaining: %d)", len(tm.tabs))) | |
| 409 | 449 | return nil | |
| 410 | 450 | } | |
| @@ -553,3 +593,103 @@ func (tm *TabManager) updateTabBarVisibility() { | |||
| 553 | 593 | func (tm *TabManager) getConfig() *config.Config { | |
| 554 | 594 | return config.Get() | |
| 555 | 595 | } | |
| 596 | + | ||
| 597 | + | // updateProgressForWebView updates progress state for the tab that owns the given WebView. | |
| 598 | + | func (tm *TabManager) updateProgressForWebView(view *webkit.WebView, progress float64, loading bool) { | |
| 599 | + | if view == nil || tm == nil { | |
| 600 | + | return | |
| 601 | + | } | |
| 602 | + | ||
| 603 | + | tm.mu.Lock() | |
| 604 | + | defer tm.mu.Unlock() | |
| 605 | + | ||
| 606 | + | tab := tm.findTabForViewLocked(view) | |
| 607 | + | if tab == nil { | |
| 608 | + | return | |
| 609 | + | } | |
| 610 | + | ||
| 611 | + | // Clamp progress | |
| 612 | + | if progress < 0 { | |
| 613 | + | progress = 0 | |
| 614 | + | } | |
| 615 | + | if progress > 1 { | |
| 616 | + | progress = 1 | |
| 617 | + | } | |
| 618 | + | ||
| 619 | + | // Only mark loading when progress is incomplete and the event indicates an active load. | |
| 620 | + | tab.isLoading = loading && progress < 1.0 | |
| 621 | + | tab.progress = progress | |
| 622 | + | ||
| 623 | + | tm.updateProgressBarLocked() | |
| 624 | + | } | |
| 625 | + | ||
| 626 | + | // findTabForViewLocked finds the tab owning the given WebView. Caller must hold tm.mu. | |
| 627 | + | func (tm *TabManager) findTabForViewLocked(view *webkit.WebView) *Tab { | |
| 628 | + | for _, tab := range tm.tabs { | |
| 629 | + | if _, ok := tab.webViews[view]; ok { | |
| 630 | + | return tab | |
| 631 | + | } | |
| 632 | + | } | |
| 633 | + | return nil | |
| 634 | + | } | |
| 635 | + | ||
| 636 | + | // registerWebViewLocked associates a WebView with a tab. Caller must hold tm.mu. | |
| 637 | + | func (tm *TabManager) registerWebViewLocked(tab *Tab, view *webkit.WebView) { | |
| 638 | + | if tab == nil || view == nil { | |
| 639 | + | return | |
| 640 | + | } | |
| 641 | + | if tab.webViews == nil { | |
| 642 | + | tab.webViews = make(map[*webkit.WebView]struct{}) | |
| 643 | + | } | |
| 644 | + | tab.webViews[view] = struct{}{} | |
| 645 | + | } | |
| 646 | + | ||
| 647 | + | // registerWebViewForWorkspace registers a webview to the tab that owns the workspace. | |
| 648 | + | func (tm *TabManager) registerWebViewForWorkspace(view *webkit.WebView, workspace *WorkspaceManager) { | |
| 649 | + | if tm == nil || view == nil || workspace == nil { | |
| 650 | + | return | |
| 651 | + | } | |
| 652 | + | ||
| 653 | + | tm.mu.Lock() | |
| 654 | + | defer tm.mu.Unlock() | |
| 655 | + | ||
| 656 | + | for _, tab := range tm.tabs { | |
| 657 | + | if tab.workspace == workspace { | |
| 658 | + | tm.registerWebViewLocked(tab, view) | |
| 659 | + | return | |
| 660 | + | } | |
| 661 | + | } | |
| 662 | + | } | |
| 663 | + | ||
| 664 | + | // updateProgressBarLocked syncs the shared progress bar to the active tab. Caller must hold tm.mu. | |
| 665 | + | func (tm *TabManager) updateProgressBarLocked() { | |
| 666 | + | if tm.progressBar == nil { | |
| 667 | + | return | |
| 668 | + | } | |
| 669 | + | ||
| 670 | + | bar, ok := tm.progressBar.(*gtk.ProgressBar) | |
| 671 | + | if !ok || bar == nil { | |
| 672 | + | return | |
| 673 | + | } | |
| 674 | + | ||
| 675 | + | // No active tab -> hide bar | |
| 676 | + | if tm.activeIndex < 0 || tm.activeIndex >= len(tm.tabs) { | |
| 677 | + | webkit.RunOnMainThread(func() { | |
| 678 | + | bar.SetFraction(0.0) | |
| 679 | + | webkit.WidgetSetVisible(bar, false) | |
| 680 | + | }) | |
| 681 | + | return | |
| 682 | + | } | |
| 683 | + | ||
| 684 | + | activeTab := tm.tabs[tm.activeIndex] | |
| 685 | + | visible := activeTab.isLoading | |
| 686 | + | fraction := activeTab.progress | |
| 687 | + | if !visible { | |
| 688 | + | fraction = 0.0 | |
| 689 | + | } | |
| 690 | + | ||
| 691 | + | webkit.RunOnMainThread(func() { | |
| 692 | + | bar.SetFraction(fraction) | |
| 693 | + | webkit.WidgetSetVisible(bar, visible) | |
| 694 | + | }) | |
| 695 | + | } | |
| @@ -24,10 +24,12 @@ type TabManager struct { | |||||
| 24 | tabBar gtk.Widgetter // Horizontal tab bar container | 24 | tabBar gtk.Widgetter // Horizontal tab bar container | ||
| 25 | ContentArea gtk.Widgetter // Container for active workspace (exported for border styling) | 25 | ContentArea gtk.Widgetter // Container for active workspace (exported for border styling) | ||
| 26 | tabModeTarget gtk.Widgetter // Widget currently holding tab mode border margins | 26 | tabModeTarget gtk.Widgetter // Widget currently holding tab mode border margins | ||
| 27 | + | progressBar gtk.Widgetter // Thin load progress indicator | |||
| 27 | 28 | ||||
| 28 | // Modal state | 29 | // Modal state | ||
| 29 | tabModeActive bool | 30 | tabModeActive bool | ||
| 30 | tabModeTimer *time.Timer | 31 | tabModeTimer *time.Timer | ||
| 32 | + | lastTabCreate time.Time // Debounce guard for rapid tab creation shortcuts | |||
| 31 | 33 | ||||
| 32 | // Rename state | 34 | // Rename state | ||
| 33 | renameInProgress bool | 35 | renameInProgress bool | ||
| @@ -52,6 +54,9 @@ type Tab struct { | |||||
| 52 | workspace *WorkspaceManager | 54 | workspace *WorkspaceManager | ||
| 53 | titleButton gtk.Widgetter | 55 | titleButton gtk.Widgetter | ||
| 54 | isActive bool | 56 | isActive bool | ||
| 57 | + | progress float64 | |||
| 58 | + | isLoading bool | |||
| 59 | + | webViews map[*webkit.WebView]struct{} | |||
| 55 | } | 60 | } | ||
| 56 | 61 | ||||
| 57 | // NewTabManager creates a new tab manager instance. | 62 | // NewTabManager creates a new tab manager instance. | ||
| @@ -126,6 +131,9 @@ func (tm *TabManager) createRootContainer() error { | |||||
| 126 | contentArea.SetVExpand(true) | 131 | contentArea.SetVExpand(true) | ||
| 127 | contentArea.AddCSSClass("tab-content-area") | 132 | contentArea.AddCSSClass("tab-content-area") | ||
| 128 | 133 | ||||
| 134 | + | // Create progress bar (displayed at the bottom regardless of tab bar position) | |||
| 135 | + | progressBar := tm.createProgressBar() | |||
| 136 | + | ||||
| 129 | // Add widgets in order based on position | 137 | // Add widgets in order based on position | ||
| 130 | if position == "top" { | 138 | if position == "top" { | ||
| 131 | rootBox.Append(tabBar) | 139 | rootBox.Append(tabBar) | ||
| @@ -135,10 +143,27 @@ func (tm *TabManager) createRootContainer() error { | |||||
| 135 | rootBox.Append(tabBar) | 143 | rootBox.Append(tabBar) | ||
| 136 | } | 144 | } | ||
| 137 | 145 | ||||
| 146 | + | // Wrap everything in an overlay so the progress bar can float without resizing layout | |||
| 147 | + | rootOverlay := gtk.NewOverlay() | |||
| 148 | + | if rootOverlay == nil { | |||
| 149 | + | return fmt.Errorf("failed to create root overlay") | |||
| 150 | + | } | |||
| 151 | + | rootOverlay.SetHExpand(true) | |||
| 152 | + | rootOverlay.SetVExpand(true) | |||
| 153 | + | rootOverlay.SetChild(rootBox) | |||
| 154 | + | if progressBar != nil { | |||
| 155 | + | if bar, ok := progressBar.(*gtk.ProgressBar); ok && bar != nil { | |||
| 156 | + | bar.SetHAlign(gtk.AlignFill) | |||
| 157 | + | bar.SetVAlign(gtk.AlignEnd) | |||
| 158 | + | } | |||
| 159 | + | rootOverlay.AddOverlay(progressBar) | |||
| 160 | + | } | |||
| 161 | + | ||||
| 138 | // Store references | 162 | // Store references | ||
| 139 | - | tm.rootContainer = rootBox | 163 | + | tm.rootContainer = rootOverlay |
| 140 | tm.tabBar = tabBar | 164 | tm.tabBar = tabBar | ||
| 141 | tm.ContentArea = contentArea | 165 | tm.ContentArea = contentArea | ||
| 166 | + | tm.progressBar = progressBar | |||
| 142 | 167 | ||||
| 143 | logging.Info(fmt.Sprintf("[tabs] Root container created with tab bar at %s", position)) | 168 | logging.Info(fmt.Sprintf("[tabs] Root container created with tab bar at %s", position)) | ||
| 144 | return nil | 169 | return nil | ||
| @@ -157,6 +182,12 @@ func (tm *TabManager) CreateTab(url string) error { | |||||
| 157 | func (tm *TabManager) createTabInternal(url string) error { | 182 | func (tm *TabManager) createTabInternal(url string) error { | ||
| 158 | logging.Info(fmt.Sprintf("[tabs] Creating new tab with URL: %s", url)) | 183 | logging.Info(fmt.Sprintf("[tabs] Creating new tab with URL: %s", url)) | ||
| 159 | 184 | ||||
| 185 | + | const tabCreateDebounce = 200 * time.Millisecond | |||
| 186 | + | if !tm.lastTabCreate.IsZero() && time.Since(tm.lastTabCreate) < tabCreateDebounce { | |||
| 187 | + | logging.Warn("[tabs] Ignoring rapid tab creation request (debounced)") | |||
| 188 | + | return nil | |||
| 189 | + | } | |||
| 190 | + | ||||
| 160 | // Use default URL if empty | 191 | // Use default URL if empty | ||
| 161 | if url == "" { | 192 | if url == "" { | ||
| 162 | url = "about:blank" | 193 | url = "about:blank" | ||
| @@ -172,6 +203,7 @@ func (tm *TabManager) createTabInternal(url string) error { | |||||
| 172 | id: tabID, | 203 | id: tabID, | ||
| 173 | title: defaultTitle, | 204 | title: defaultTitle, | ||
| 174 | isActive: false, | 205 | isActive: false, | ||
| 206 | + | webViews: make(map[*webkit.WebView]struct{}), | |||
| 175 | } | 207 | } | ||
| 176 | 208 | ||||
| 177 | // Create WebView for this tab | 209 | // Create WebView for this tab | ||
| @@ -195,6 +227,7 @@ func (tm *TabManager) createTabInternal(url string) error { | |||||
| 195 | // Create workspace for this tab | 227 | // Create workspace for this tab | ||
| 196 | workspace := NewWorkspaceManager(tm.app, pane) | 228 | workspace := NewWorkspaceManager(tm.app, pane) | ||
| 197 | tab.workspace = workspace | 229 | tab.workspace = workspace | ||
| 230 | + | tm.registerWebViewLocked(tab, view) | |||
| 198 | 231 | ||||
| 199 | // Verify workspace root exists | 232 | // Verify workspace root exists | ||
| 200 | rootWidget := workspace.GetRootWidget() | 233 | rootWidget := workspace.GetRootWidget() | ||
| @@ -222,6 +255,7 @@ func (tm *TabManager) createTabInternal(url string) error { | |||||
| 222 | 255 | ||||
| 223 | // Add to tabs slice | 256 | // Add to tabs slice | ||
| 224 | tm.tabs = append(tm.tabs, tab) | 257 | tm.tabs = append(tm.tabs, tab) | ||
| 258 | + | tm.lastTabCreate = time.Now() | |||
| 225 | 259 | ||||
| 226 | // Attach click handler with unique ID (pattern from stacked panes) | 260 | // Attach click handler with unique ID (pattern from stacked panes) | ||
| 227 | tm.attachTabClickHandler(tab.titleButton, tab) | 261 | tm.attachTabClickHandler(tab.titleButton, tab) | ||
| @@ -325,6 +359,9 @@ func (tm *TabManager) switchToTabInternal(index int) error { | |||||
| 325 | newTab.workspace.RestoreFocus() | 359 | newTab.workspace.RestoreFocus() | ||
| 326 | } | 360 | } | ||
| 327 | 361 | ||||
| 362 | + | // Sync progress bar with the newly active tab | |||
| 363 | + | tm.updateProgressBarLocked() | |||
| 364 | + | ||||
| 328 | logging.Info(fmt.Sprintf("[tabs] Switched to tab %d (%s)", index, newTab.id)) | 365 | logging.Info(fmt.Sprintf("[tabs] Switched to tab %d (%s)", index, newTab.id)) | ||
| 329 | return nil | 366 | return nil | ||
| 330 | } | 367 | } | ||
| @@ -405,6 +442,9 @@ func (tm *TabManager) closeTabInternal(index int) error { | |||||
| 405 | // Update tab bar visibility (hide if only 1 tab) | 442 | // Update tab bar visibility (hide if only 1 tab) | ||
| 406 | tm.updateTabBarVisibility() | 443 | tm.updateTabBarVisibility() | ||
| 407 | 444 | ||||
| 445 | + | // Update progress bar (active tab may have changed after close) | |||
| 446 | + | tm.updateProgressBarLocked() | |||
| 447 | + | ||||
| 408 | logging.Info(fmt.Sprintf("[tabs] Tab closed (remaining: %d)", len(tm.tabs))) | 448 | logging.Info(fmt.Sprintf("[tabs] Tab closed (remaining: %d)", len(tm.tabs))) | ||
| 409 | return nil | 449 | return nil | ||
| 410 | } | 450 | } | ||
| @@ -553,3 +593,103 @@ func (tm *TabManager) updateTabBarVisibility() { | |||||
| 553 | func (tm *TabManager) getConfig() *config.Config { | 593 | func (tm *TabManager) getConfig() *config.Config { | ||
| 554 | return config.Get() | 594 | return config.Get() | ||
| 555 | } | 595 | } | ||
| 596 | + | ||||
| 597 | + | // updateProgressForWebView updates progress state for the tab that owns the given WebView. | |||
| 598 | + | func (tm *TabManager) updateProgressForWebView(view *webkit.WebView, progress float64, loading bool) { | |||
| 599 | + | if view == nil || tm == nil { | |||
| 600 | + | return | |||
| 601 | + | } | |||
| 602 | + | ||||
| 603 | + | tm.mu.Lock() | |||
| 604 | + | defer tm.mu.Unlock() | |||
| 605 | + | ||||
| 606 | + | tab := tm.findTabForViewLocked(view) | |||
| 607 | + | if tab == nil { | |||
| 608 | + | return | |||
| 609 | + | } | |||
| 610 | + | ||||
| 611 | + | // Clamp progress | |||
| 612 | + | if progress < 0 { | |||
| 613 | + | progress = 0 | |||
| 614 | + | } | |||
| 615 | + | if progress > 1 { | |||
| 616 | + | progress = 1 | |||
| 617 | + | } | |||
| 618 | + | ||||
| 619 | + | // Only mark loading when progress is incomplete and the event indicates an active load. | |||
| 620 | + | tab.isLoading = loading && progress < 1.0 | |||
| 621 | + | tab.progress = progress | |||
| 622 | + | ||||
| 623 | + | tm.updateProgressBarLocked() | |||
| 624 | + | } | |||
| 625 | + | ||||
| 626 | + | // findTabForViewLocked finds the tab owning the given WebView. Caller must hold tm.mu. | |||
| 627 | + | func (tm *TabManager) findTabForViewLocked(view *webkit.WebView) *Tab { | |||
| 628 | + | for _, tab := range tm.tabs { | |||
| 629 | + | if _, ok := tab.webViews[view]; ok { | |||
| 630 | + | return tab | |||
| 631 | + | } | |||
| 632 | + | } | |||
| 633 | + | return nil | |||
| 634 | + | } | |||
| 635 | + | ||||
| 636 | + | // registerWebViewLocked associates a WebView with a tab. Caller must hold tm.mu. | |||
| 637 | + | func (tm *TabManager) registerWebViewLocked(tab *Tab, view *webkit.WebView) { | |||
| 638 | + | if tab == nil || view == nil { | |||
| 639 | + | return | |||
| 640 | + | } | |||
| 641 | + | if tab.webViews == nil { | |||
| 642 | + | tab.webViews = make(map[*webkit.WebView]struct{}) | |||
| 643 | + | } | |||
| 644 | + | tab.webViews[view] = struct{}{} | |||
| 645 | + | } | |||
| 646 | + | ||||
| 647 | + | // registerWebViewForWorkspace registers a webview to the tab that owns the workspace. | |||
| 648 | + | func (tm *TabManager) registerWebViewForWorkspace(view *webkit.WebView, workspace *WorkspaceManager) { | |||
| 649 | + | if tm == nil || view == nil || workspace == nil { | |||
| 650 | + | return | |||
| 651 | + | } | |||
| 652 | + | ||||
| 653 | + | tm.mu.Lock() | |||
| 654 | + | defer tm.mu.Unlock() | |||
| 655 | + | ||||
| 656 | + | for _, tab := range tm.tabs { | |||
| 657 | + | if tab.workspace == workspace { | |||
| 658 | + | tm.registerWebViewLocked(tab, view) | |||
| 659 | + | return | |||
| 660 | + | } | |||
| 661 | + | } | |||
| 662 | + | } | |||
| 663 | + | ||||
| 664 | + | // updateProgressBarLocked syncs the shared progress bar to the active tab. Caller must hold tm.mu. | |||
| 665 | + | func (tm *TabManager) updateProgressBarLocked() { | |||
| 666 | + | if tm.progressBar == nil { | |||
| 667 | + | return | |||
| 668 | + | } | |||
| 669 | + | ||||
| 670 | + | bar, ok := tm.progressBar.(*gtk.ProgressBar) | |||
| 671 | + | if !ok || bar == nil { | |||
| 672 | + | return | |||
| 673 | + | } | |||
| 674 | + | ||||
| 675 | + | // No active tab -> hide bar | |||
| 676 | + | if tm.activeIndex < 0 || tm.activeIndex >= len(tm.tabs) { | |||
| 677 | + | webkit.RunOnMainThread(func() { | |||
| 678 | + | bar.SetFraction(0.0) | |||
| 679 | + | webkit.WidgetSetVisible(bar, false) | |||
| 680 | + | }) | |||
| 681 | + | return | |||
| 682 | + | } | |||
| 683 | + | ||||
| 684 | + | activeTab := tm.tabs[tm.activeIndex] | |||
| 685 | + | visible := activeTab.isLoading | |||
| 686 | + | fraction := activeTab.progress | |||
| 687 | + | if !visible { | |||
| 688 | + | fraction = 0.0 | |||
| 689 | + | } | |||
| 690 | + | ||||
| 691 | + | webkit.RunOnMainThread(func() { | |||
| 692 | + | bar.SetFraction(fraction) | |||
| 693 | + | webkit.WidgetSetVisible(bar, visible) | |||
| 694 | + | }) | |||
| 695 | + | } | |||
M internal/app/browser/webview.go +21 -0
| @@ -191,6 +191,17 @@ func (app *BrowserApp) attachPaneHandlers(pane *BrowserPane) { | |||
| 191 | 191 | pane.messageHandler.Handle(payload) | |
| 192 | 192 | }) | |
| 193 | 193 | ||
| 194 | + | // Page load progress - drive tab-level progress bar | |
| 195 | + | pane.webView.RegisterLoadStartedHandler(func() { | |
| 196 | + | app.handleLoadProgress(pane.webView, 0.0, true) | |
| 197 | + | }) | |
| 198 | + | pane.webView.RegisterLoadFinishedHandler(func() { | |
| 199 | + | app.handleLoadProgress(pane.webView, 1.0, false) | |
| 200 | + | }) | |
| 201 | + | pane.webView.RegisterLoadProgressHandler(func(progress float64) { | |
| 202 | + | app.handleLoadProgress(pane.webView, progress, true) | |
| 203 | + | }) | |
| 204 | + | ||
| 194 | 205 | if pane.messageHandler != nil && pane.navigationController != nil { | |
| 195 | 206 | pane.messageHandler.SetNavigationController(pane.navigationController) | |
| 196 | 207 | } | |
| @@ -205,6 +216,16 @@ func (app *BrowserApp) attachPaneHandlers(pane *BrowserPane) { | |||
| 205 | 216 | } | |
| 206 | 217 | } | |
| 207 | 218 | ||
| 219 | + | // handleLoadProgress routes WebView load progress events to the tab manager for UI display. | |
| 220 | + | func (app *BrowserApp) handleLoadProgress(view *webkit.WebView, progress float64, loading bool) { | |
| 221 | + | if app == nil || view == nil || app.tabManager == nil { | |
| 222 | + | return | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | // Run asynchronously to avoid blocking the GTK/main thread during load-changed signals. | |
| 226 | + | go app.tabManager.updateProgressForWebView(view, progress, loading) | |
| 227 | + | } | |
| 228 | + | ||
| 208 | 229 | // shouldFocusForScriptMessage filters script messages and only allows focus handoff | |
| 209 | 230 | // for explicit user-driven actions (navigation, history commands, popup lifecycle). | |
| 210 | 231 | // Background bootstrap messages (palette sync, workspace bridge chatter, etc.) are | |
| @@ -191,6 +191,17 @@ func (app *BrowserApp) attachPaneHandlers(pane *BrowserPane) { | |||||
| 191 | pane.messageHandler.Handle(payload) | 191 | pane.messageHandler.Handle(payload) | ||
| 192 | }) | 192 | }) | ||
| 193 | 193 | ||||
| 194 | + | // Page load progress - drive tab-level progress bar | |||
| 195 | + | pane.webView.RegisterLoadStartedHandler(func() { | |||
| 196 | + | app.handleLoadProgress(pane.webView, 0.0, true) | |||
| 197 | + | }) | |||
| 198 | + | pane.webView.RegisterLoadFinishedHandler(func() { | |||
| 199 | + | app.handleLoadProgress(pane.webView, 1.0, false) | |||
| 200 | + | }) | |||
| 201 | + | pane.webView.RegisterLoadProgressHandler(func(progress float64) { | |||
| 202 | + | app.handleLoadProgress(pane.webView, progress, true) | |||
| 203 | + | }) | |||
| 204 | + | ||||
| 194 | if pane.messageHandler != nil && pane.navigationController != nil { | 205 | if pane.messageHandler != nil && pane.navigationController != nil { | ||
| 195 | pane.messageHandler.SetNavigationController(pane.navigationController) | 206 | pane.messageHandler.SetNavigationController(pane.navigationController) | ||
| 196 | } | 207 | } | ||
| @@ -205,6 +216,16 @@ func (app *BrowserApp) attachPaneHandlers(pane *BrowserPane) { | |||||
| 205 | } | 216 | } | ||
| 206 | } | 217 | } | ||
| 207 | 218 | ||||
| 219 | + | // handleLoadProgress routes WebView load progress events to the tab manager for UI display. | |||
| 220 | + | func (app *BrowserApp) handleLoadProgress(view *webkit.WebView, progress float64, loading bool) { | |||
| 221 | + | if app == nil || view == nil || app.tabManager == nil { | |||
| 222 | + | return | |||
| 223 | + | } | |||
| 224 | + | ||||
| 225 | + | // Run asynchronously to avoid blocking the GTK/main thread during load-changed signals. | |||
| 226 | + | go app.tabManager.updateProgressForWebView(view, progress, loading) | |||
| 227 | + | } | |||
| 228 | + | ||||
| 208 | // shouldFocusForScriptMessage filters script messages and only allows focus handoff | 229 | // shouldFocusForScriptMessage filters script messages and only allows focus handoff | ||
| 209 | // for explicit user-driven actions (navigation, history commands, popup lifecycle). | 230 | // for explicit user-driven actions (navigation, history commands, popup lifecycle). | ||
| 210 | // Background bootstrap messages (palette sync, workspace bridge chatter, etc.) are | 231 | // Background bootstrap messages (palette sync, workspace bridge chatter, etc.) are | ||
M internal/app/browser/workspace_css.go +43 -9
| @@ -241,6 +241,32 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||
| 241 | 241 | min-height: %dpx; | |
| 242 | 242 | } | |
| 243 | 243 | ||
| 244 | + | /* Tab progress bar */ | |
| 245 | + | progressbar.tab-progress-bar { | |
| 246 | + | min-height: 4px; | |
| 247 | + | padding: 0; | |
| 248 | + | margin: 0; | |
| 249 | + | border: none; | |
| 250 | + | transition: opacity 180ms ease-in-out; | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | progressbar.tab-progress-bar trough { | |
| 254 | + | min-height: 4px; | |
| 255 | + | background: transparent; | |
| 256 | + | border: none; | |
| 257 | + | padding: 0; | |
| 258 | + | } | |
| 259 | + | ||
| 260 | + | progressbar.tab-progress-bar progress { | |
| 261 | + | min-height: 4px; | |
| 262 | + | border-radius: 0; | |
| 263 | + | background-image: linear-gradient(90deg, %s 0%%, %s 50%%, %s 100%%); | |
| 264 | + | background-size: 240%% 100%%; | |
| 265 | + | background-position: 0%% 0%%; | |
| 266 | + | transition: width 180ms ease-in-out, background-position 420ms linear; | |
| 267 | + | animation: tab-progress-stripe 1.3s linear infinite; | |
| 268 | + | } | |
| 269 | + | ||
| 244 | 270 | /* Tab mode active - window background changes to border color (like pane mode) */ | |
| 245 | 271 | window.tab-mode-active { | |
| 246 | 272 | background-color: %s; | |
| @@ -312,6 +338,11 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||
| 312 | 338 | @keyframes pulse-border { | |
| 313 | 339 | 0%%, 100%% { opacity: 1.0; } | |
| 314 | 340 | 50%% { opacity: 0.6; } | |
| 341 | + | } | |
| 342 | + | ||
| 343 | + | @keyframes tab-progress-stripe { | |
| 344 | + | 0%% { background-position: 0%% 0%%; } | |
| 345 | + | 100%% { background-position: -100%% 0%%; } | |
| 315 | 346 | }`, | |
| 316 | 347 | windowBackgroundColor, // window background | |
| 317 | 348 | paneModeColor, // window.pane-mode-active background (border color) | |
| @@ -339,6 +370,9 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||
| 339 | 370 | windowBackgroundColor, // tab-bar background | |
| 340 | 371 | getStackTitleBorderColor(isDark), // tab-bar border-top | |
| 341 | 372 | int(32*uiScale), // tab-bar min-height | |
| 373 | + | "#606060", // tab progress gradient start (neutral gray) | |
| 374 | + | "#747474", // tab progress gradient middle (neutral gray) | |
| 375 | + | "#8a8a8a", // tab progress gradient end (neutral gray) | |
| 342 | 376 | tabModeColor, // window.tab-mode-active .tab-bar border-top-color (orange, distinct from pane mode blue) | |
| 343 | 377 | getStackTitleBg(isDark), // tab-button background (same as stacked title) | |
| 344 | 378 | getStackTitleBorderColor(isDark), // tab-button border-right | |
| @@ -351,15 +385,15 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||
| 351 | 385 | titleFontSize, // tab-title font-size | |
| 352 | 386 | getStackTitleTextColor(isDark), // tab-title color | |
| 353 | 387 | // Tab rename entry styles | |
| 354 | - | getEntryBg(isDark), // entry.tab-rename-entry background | |
| 355 | - | getStackTitleTextColor(isDark), // entry.tab-rename-entry color (text) | |
| 356 | - | titleFontSize, // entry.tab-rename-entry font-size | |
| 357 | - | titlePaddingHorizontal, // entry.tab-rename-entry padding horizontal (vertical is hardcoded 2px) | |
| 358 | - | getEntryBorderColor(isDark), // entry.tab-rename-entry border-color | |
| 359 | - | getEntryFocusBg(isDark), // entry.tab-rename-entry:focus background | |
| 360 | - | activeBorderColor, // entry.tab-rename-entry:focus border-color (use active pane color) | |
| 361 | - | windowBackgroundColor, // tab-content-area background | |
| 362 | - | windowBackgroundColor, // tab-workspace-container background | |
| 388 | + | getEntryBg(isDark), // entry.tab-rename-entry background | |
| 389 | + | getStackTitleTextColor(isDark), // entry.tab-rename-entry color (text) | |
| 390 | + | titleFontSize, // entry.tab-rename-entry font-size | |
| 391 | + | titlePaddingHorizontal, // entry.tab-rename-entry padding horizontal (vertical is hardcoded 2px) | |
| 392 | + | getEntryBorderColor(isDark), // entry.tab-rename-entry border-color | |
| 393 | + | getEntryFocusBg(isDark), // entry.tab-rename-entry:focus background | |
| 394 | + | activeBorderColor, // entry.tab-rename-entry:focus border-color (use active pane color) | |
| 395 | + | windowBackgroundColor, // tab-content-area background | |
| 396 | + | windowBackgroundColor, // tab-workspace-container background | |
| 363 | 397 | ) | |
| 364 | 398 | ||
| 365 | 399 | return css | |
| @@ -241,6 +241,32 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||||
| 241 | min-height: %dpx; | 241 | min-height: %dpx; | ||
| 242 | } | 242 | } | ||
| 243 | 243 | ||||
| 244 | + | /* Tab progress bar */ | |||
| 245 | + | progressbar.tab-progress-bar { | |||
| 246 | + | min-height: 4px; | |||
| 247 | + | padding: 0; | |||
| 248 | + | margin: 0; | |||
| 249 | + | border: none; | |||
| 250 | + | transition: opacity 180ms ease-in-out; | |||
| 251 | + | } | |||
| 252 | + | ||||
| 253 | + | progressbar.tab-progress-bar trough { | |||
| 254 | + | min-height: 4px; | |||
| 255 | + | background: transparent; | |||
| 256 | + | border: none; | |||
| 257 | + | padding: 0; | |||
| 258 | + | } | |||
| 259 | + | ||||
| 260 | + | progressbar.tab-progress-bar progress { | |||
| 261 | + | min-height: 4px; | |||
| 262 | + | border-radius: 0; | |||
| 263 | + | background-image: linear-gradient(90deg, %s 0%%, %s 50%%, %s 100%%); | |||
| 264 | + | background-size: 240%% 100%%; | |||
| 265 | + | background-position: 0%% 0%%; | |||
| 266 | + | transition: width 180ms ease-in-out, background-position 420ms linear; | |||
| 267 | + | animation: tab-progress-stripe 1.3s linear infinite; | |||
| 268 | + | } | |||
| 269 | + | ||||
| 244 | /* Tab mode active - window background changes to border color (like pane mode) */ | 270 | /* Tab mode active - window background changes to border color (like pane mode) */ | ||
| 245 | window.tab-mode-active { | 271 | window.tab-mode-active { | ||
| 246 | background-color: %s; | 272 | background-color: %s; | ||
| @@ -312,6 +338,11 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||||
| 312 | @keyframes pulse-border { | 338 | @keyframes pulse-border { | ||
| 313 | 0%%, 100%% { opacity: 1.0; } | 339 | 0%%, 100%% { opacity: 1.0; } | ||
| 314 | 50%% { opacity: 0.6; } | 340 | 50%% { opacity: 0.6; } | ||
| 341 | + | } | |||
| 342 | + | ||||
| 343 | + | @keyframes tab-progress-stripe { | |||
| 344 | + | 0%% { background-position: 0%% 0%%; } | |||
| 345 | + | 100%% { background-position: -100%% 0%%; } | |||
| 315 | }`, | 346 | }`, | ||
| 316 | windowBackgroundColor, // window background | 347 | windowBackgroundColor, // window background | ||
| 317 | paneModeColor, // window.pane-mode-active background (border color) | 348 | paneModeColor, // window.pane-mode-active background (border color) | ||
| @@ -339,6 +370,9 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||||
| 339 | windowBackgroundColor, // tab-bar background | 370 | windowBackgroundColor, // tab-bar background | ||
| 340 | getStackTitleBorderColor(isDark), // tab-bar border-top | 371 | getStackTitleBorderColor(isDark), // tab-bar border-top | ||
| 341 | int(32*uiScale), // tab-bar min-height | 372 | int(32*uiScale), // tab-bar min-height | ||
| 373 | + | "#606060", // tab progress gradient start (neutral gray) | |||
| 374 | + | "#747474", // tab progress gradient middle (neutral gray) | |||
| 375 | + | "#8a8a8a", // tab progress gradient end (neutral gray) | |||
| 342 | tabModeColor, // window.tab-mode-active .tab-bar border-top-color (orange, distinct from pane mode blue) | 376 | tabModeColor, // window.tab-mode-active .tab-bar border-top-color (orange, distinct from pane mode blue) | ||
| 343 | getStackTitleBg(isDark), // tab-button background (same as stacked title) | 377 | getStackTitleBg(isDark), // tab-button background (same as stacked title) | ||
| 344 | getStackTitleBorderColor(isDark), // tab-button border-right | 378 | getStackTitleBorderColor(isDark), // tab-button border-right | ||
| @@ -351,15 +385,15 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||||
| 351 | titleFontSize, // tab-title font-size | 385 | titleFontSize, // tab-title font-size | ||
| 352 | getStackTitleTextColor(isDark), // tab-title color | 386 | getStackTitleTextColor(isDark), // tab-title color | ||
| 353 | // Tab rename entry styles | 387 | // Tab rename entry styles | ||
| 354 | - | getEntryBg(isDark), // entry.tab-rename-entry background | 388 | + | getEntryBg(isDark), // entry.tab-rename-entry background |
| 355 | - | getStackTitleTextColor(isDark), // entry.tab-rename-entry color (text) | 389 | + | getStackTitleTextColor(isDark), // entry.tab-rename-entry color (text) |
| 356 | - | titleFontSize, // entry.tab-rename-entry font-size | 390 | + | titleFontSize, // entry.tab-rename-entry font-size |
| 357 | - | titlePaddingHorizontal, // entry.tab-rename-entry padding horizontal (vertical is hardcoded 2px) | 391 | + | titlePaddingHorizontal, // entry.tab-rename-entry padding horizontal (vertical is hardcoded 2px) |
| 358 | - | getEntryBorderColor(isDark), // entry.tab-rename-entry border-color | 392 | + | getEntryBorderColor(isDark), // entry.tab-rename-entry border-color |
| 359 | - | getEntryFocusBg(isDark), // entry.tab-rename-entry:focus background | 393 | + | getEntryFocusBg(isDark), // entry.tab-rename-entry:focus background |
| 360 | - | activeBorderColor, // entry.tab-rename-entry:focus border-color (use active pane color) | 394 | + | activeBorderColor, // entry.tab-rename-entry:focus border-color (use active pane color) |
| 361 | - | windowBackgroundColor, // tab-content-area background | 395 | + | windowBackgroundColor, // tab-content-area background |
| 362 | - | windowBackgroundColor, // tab-workspace-container background | 396 | + | windowBackgroundColor, // tab-workspace-container background |
| 363 | ) | 397 | ) | ||
| 364 | 398 | ||||
| 365 | return css | 399 | return css | ||
M internal/app/browser/workspace_pane_ops.go +11 -1
| @@ -164,7 +164,17 @@ func (wm *WorkspaceManager) createPane(view *webkit.WebView) (*BrowserPane, erro | |||
| 164 | 164 | if wm == nil || wm.createPaneFn == nil { | |
| 165 | 165 | return nil, errors.New("workspace manager missing pane factory") | |
| 166 | 166 | } | |
| 167 | - | return wm.createPaneFn(view) | |
| 167 | + | pane, err := wm.createPaneFn(view) | |
| 168 | + | if err != nil { | |
| 169 | + | return nil, err | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | // Register the new WebView with the tab manager for progress tracking. | |
| 173 | + | if wm.app != nil && wm.app.tabManager != nil { | |
| 174 | + | wm.app.tabManager.registerWebViewForWorkspace(view, wm) | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | return pane, nil | |
| 168 | 178 | } | |
| 169 | 179 | ||
| 170 | 180 | // clonePaneState sets up a new pane with cloned state from another pane | |
| @@ -164,7 +164,17 @@ func (wm *WorkspaceManager) createPane(view *webkit.WebView) (*BrowserPane, erro | |||||
| 164 | if wm == nil || wm.createPaneFn == nil { | 164 | if wm == nil || wm.createPaneFn == nil { | ||
| 165 | return nil, errors.New("workspace manager missing pane factory") | 165 | return nil, errors.New("workspace manager missing pane factory") | ||
| 166 | } | 166 | } | ||
| 167 | - | return wm.createPaneFn(view) | 167 | + | pane, err := wm.createPaneFn(view) |
| 168 | + | if err != nil { | |||
| 169 | + | return nil, err | |||
| 170 | + | } | |||
| 171 | + | ||||
| 172 | + | // Register the new WebView with the tab manager for progress tracking. | |||
| 173 | + | if wm.app != nil && wm.app.tabManager != nil { | |||
| 174 | + | wm.app.tabManager.registerWebViewForWorkspace(view, wm) | |||
| 175 | + | } | |||
| 176 | + | ||||
| 177 | + | return pane, nil | |||
| 168 | } | 178 | } | ||
| 169 | 179 | ||||
| 170 | // clonePaneState sets up a new pane with cloned state from another pane | 180 | // clonePaneState sets up a new pane with cloned state from another pane | ||
M pkg/webkit/webview.go +37 -0
| @@ -43,6 +43,9 @@ type WebView struct { | |||
| 43 | 43 | onFaviconURIChanged func(pageURI, faviconURI string) | |
| 44 | 44 | onZoomChanged func(float64) | |
| 45 | 45 | onLoadCommitted func(string) // Called when page load is committed (safe to apply zoom) | |
| 46 | + | onLoadStarted func() // Called when a load starts | |
| 47 | + | onLoadFinished func() // Called when a load finishes | |
| 48 | + | onLoadProgress func(float64) // Called when estimated-load-progress changes | |
| 46 | 49 | onPopupCreate func(*webkit.NavigationAction) *WebView // New WebKit create signal handler | |
| 47 | 50 | onReadyToShow func() // WebKit ready-to-show signal handler | |
| 48 | 51 | onClose func() | |
| @@ -246,10 +249,23 @@ func (w *WebView) setupEventHandlers() { | |||
| 246 | 249 | ||
| 247 | 250 | // Load committed - connect to load-changed signal for WEBKIT_LOAD_COMMITTED | |
| 248 | 251 | w.view.ConnectLoadChanged(func(loadEvent webkit.LoadEvent) { | |
| 252 | + | if loadEvent == webkit.LoadStarted && w.onLoadStarted != nil { | |
| 253 | + | w.onLoadStarted() | |
| 254 | + | } | |
| 249 | 255 | if loadEvent == webkit.LoadCommitted && w.onLoadCommitted != nil { | |
| 250 | 256 | uri := w.view.URI() | |
| 251 | 257 | w.onLoadCommitted(uri) | |
| 252 | 258 | } | |
| 259 | + | if loadEvent == webkit.LoadFinished && w.onLoadFinished != nil { | |
| 260 | + | w.onLoadFinished() | |
| 261 | + | } | |
| 262 | + | }) | |
| 263 | + | ||
| 264 | + | // Estimated load progress - notify::estimated-load-progress property | |
| 265 | + | w.view.Connect("notify::estimated-load-progress", func() { | |
| 266 | + | if w.onLoadProgress != nil { | |
| 267 | + | w.onLoadProgress(w.view.EstimatedLoadProgress()) | |
| 268 | + | } | |
| 253 | 269 | }) | |
| 254 | 270 | ||
| 255 | 271 | // Favicon changed - connect to FaviconDatabase | |
| @@ -722,6 +738,27 @@ func (w *WebView) RegisterLoadCommittedHandler(handler func(string)) { | |||
| 722 | 738 | w.onLoadCommitted = handler | |
| 723 | 739 | } | |
| 724 | 740 | ||
| 741 | + | // RegisterLoadStartedHandler registers a handler for load start events. | |
| 742 | + | func (w *WebView) RegisterLoadStartedHandler(handler func()) { | |
| 743 | + | w.mu.Lock() | |
| 744 | + | defer w.mu.Unlock() | |
| 745 | + | w.onLoadStarted = handler | |
| 746 | + | } | |
| 747 | + | ||
| 748 | + | // RegisterLoadFinishedHandler registers a handler for load finished events. | |
| 749 | + | func (w *WebView) RegisterLoadFinishedHandler(handler func()) { | |
| 750 | + | w.mu.Lock() | |
| 751 | + | defer w.mu.Unlock() | |
| 752 | + | w.onLoadFinished = handler | |
| 753 | + | } | |
| 754 | + | ||
| 755 | + | // RegisterLoadProgressHandler registers a handler for load progress updates (0.0 - 1.0). | |
| 756 | + | func (w *WebView) RegisterLoadProgressHandler(handler func(float64)) { | |
| 757 | + | w.mu.Lock() | |
| 758 | + | defer w.mu.Unlock() | |
| 759 | + | w.onLoadProgress = handler | |
| 760 | + | } | |
| 761 | + | ||
| 725 | 762 | // RegisterCloseHandler registers a handler for close requests | |
| 726 | 763 | func (w *WebView) RegisterCloseHandler(handler func()) { | |
| 727 | 764 | w.mu.Lock() | |
| @@ -43,6 +43,9 @@ type WebView struct { | |||||
| 43 | onFaviconURIChanged func(pageURI, faviconURI string) | 43 | onFaviconURIChanged func(pageURI, faviconURI string) | ||
| 44 | onZoomChanged func(float64) | 44 | onZoomChanged func(float64) | ||
| 45 | onLoadCommitted func(string) // Called when page load is committed (safe to apply zoom) | 45 | onLoadCommitted func(string) // Called when page load is committed (safe to apply zoom) | ||
| 46 | + | onLoadStarted func() // Called when a load starts | |||
| 47 | + | onLoadFinished func() // Called when a load finishes | |||
| 48 | + | onLoadProgress func(float64) // Called when estimated-load-progress changes | |||
| 46 | onPopupCreate func(*webkit.NavigationAction) *WebView // New WebKit create signal handler | 49 | onPopupCreate func(*webkit.NavigationAction) *WebView // New WebKit create signal handler | ||
| 47 | onReadyToShow func() // WebKit ready-to-show signal handler | 50 | onReadyToShow func() // WebKit ready-to-show signal handler | ||
| 48 | onClose func() | 51 | onClose func() | ||
| @@ -246,10 +249,23 @@ func (w *WebView) setupEventHandlers() { | |||||
| 246 | 249 | ||||
| 247 | // Load committed - connect to load-changed signal for WEBKIT_LOAD_COMMITTED | 250 | // Load committed - connect to load-changed signal for WEBKIT_LOAD_COMMITTED | ||
| 248 | w.view.ConnectLoadChanged(func(loadEvent webkit.LoadEvent) { | 251 | w.view.ConnectLoadChanged(func(loadEvent webkit.LoadEvent) { | ||
| 252 | + | if loadEvent == webkit.LoadStarted && w.onLoadStarted != nil { | |||
| 253 | + | w.onLoadStarted() | |||
| 254 | + | } | |||
| 249 | if loadEvent == webkit.LoadCommitted && w.onLoadCommitted != nil { | 255 | if loadEvent == webkit.LoadCommitted && w.onLoadCommitted != nil { | ||
| 250 | uri := w.view.URI() | 256 | uri := w.view.URI() | ||
| 251 | w.onLoadCommitted(uri) | 257 | w.onLoadCommitted(uri) | ||
| 252 | } | 258 | } | ||
| 259 | + | if loadEvent == webkit.LoadFinished && w.onLoadFinished != nil { | |||
| 260 | + | w.onLoadFinished() | |||
| 261 | + | } | |||
| 262 | + | }) | |||
| 263 | + | ||||
| 264 | + | // Estimated load progress - notify::estimated-load-progress property | |||
| 265 | + | w.view.Connect("notify::estimated-load-progress", func() { | |||
| 266 | + | if w.onLoadProgress != nil { | |||
| 267 | + | w.onLoadProgress(w.view.EstimatedLoadProgress()) | |||
| 268 | + | } | |||
| 253 | }) | 269 | }) | ||
| 254 | 270 | ||||
| 255 | // Favicon changed - connect to FaviconDatabase | 271 | // Favicon changed - connect to FaviconDatabase | ||
| @@ -722,6 +738,27 @@ func (w *WebView) RegisterLoadCommittedHandler(handler func(string)) { | |||||
| 722 | w.onLoadCommitted = handler | 738 | w.onLoadCommitted = handler | ||
| 723 | } | 739 | } | ||
| 724 | 740 | ||||
| 741 | + | // RegisterLoadStartedHandler registers a handler for load start events. | |||
| 742 | + | func (w *WebView) RegisterLoadStartedHandler(handler func()) { | |||
| 743 | + | w.mu.Lock() | |||
| 744 | + | defer w.mu.Unlock() | |||
| 745 | + | w.onLoadStarted = handler | |||
| 746 | + | } | |||
| 747 | + | ||||
| 748 | + | // RegisterLoadFinishedHandler registers a handler for load finished events. | |||
| 749 | + | func (w *WebView) RegisterLoadFinishedHandler(handler func()) { | |||
| 750 | + | w.mu.Lock() | |||
| 751 | + | defer w.mu.Unlock() | |||
| 752 | + | w.onLoadFinished = handler | |||
| 753 | + | } | |||
| 754 | + | ||||
| 755 | + | // RegisterLoadProgressHandler registers a handler for load progress updates (0.0 - 1.0). | |||
| 756 | + | func (w *WebView) RegisterLoadProgressHandler(handler func(float64)) { | |||
| 757 | + | w.mu.Lock() | |||
| 758 | + | defer w.mu.Unlock() | |||
| 759 | + | w.onLoadProgress = handler | |||
| 760 | + | } | |||
| 761 | + | ||||
| 725 | // RegisterCloseHandler registers a handler for close requests | 762 | // RegisterCloseHandler registers a handler for close requests | ||
| 726 | func (w *WebView) RegisterCloseHandler(handler func()) { | 763 | func (w *WebView) RegisterCloseHandler(handler func()) { | ||
| 727 | w.mu.Lock() | 764 | w.mu.Lock() | ||