feat(workspace): add favicon display and click navigation to stacked pane title bars
- commit
- 5a2eb4637d3e2a3c36f997ae9a392c661c806f21
- author
- bamen · · 2025-10-06 16:18 UTC
- committer
- bamen · · 2025-10-06 19:18 UTC
- parent
- b184813
- signature
- unsigned
3 files · +165 -37
M internal/app/browser/stacked_panes.go +152 -37
| @@ -1,23 +1,29 @@ | |||
| 1 | 1 | package browser | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "context" | |
| 4 | 5 | "errors" | |
| 5 | 6 | "fmt" | |
| 6 | 7 | "log" | |
| 8 | + | "sync/atomic" | |
| 7 | 9 | "time" | |
| 8 | 10 | ||
| 11 | + | "github.com/bnema/dumber/internal/cache" | |
| 9 | 12 | "github.com/bnema/dumber/pkg/webkit" | |
| 10 | 13 | ) | |
| 11 | 14 | ||
| 12 | 15 | // StackedPaneManager handles all stacked pane operations | |
| 13 | 16 | type StackedPaneManager struct { | |
| 14 | - | wm *WorkspaceManager | |
| 17 | + | wm *WorkspaceManager | |
| 18 | + | titleBarToPane map[uintptr]*paneNode | |
| 19 | + | nextTitleBarID uint64 | |
| 15 | 20 | } | |
| 16 | 21 | ||
| 17 | 22 | // NewStackedPaneManager creates a new stacked pane manager | |
| 18 | 23 | func NewStackedPaneManager(wm *WorkspaceManager) *StackedPaneManager { | |
| 19 | 24 | return &StackedPaneManager{ | |
| 20 | - | wm: wm, | |
| 25 | + | wm: wm, | |
| 26 | + | titleBarToPane: make(map[uintptr]*paneNode), | |
| 21 | 27 | } | |
| 22 | 28 | } | |
| 23 | 29 | ||
| @@ -409,6 +415,15 @@ func (spm *StackedPaneManager) UpdateStackVisibility(stackNode *paneNode) { | |||
| 409 | 415 | } | |
| 410 | 416 | if pane.titleBar != 0 { | |
| 411 | 417 | webkit.WidgetSetVisible(pane.titleBar, true) | |
| 418 | + | ||
| 419 | + | // Refresh title bar to pick up any newly downloaded favicons | |
| 420 | + | if pane.pane != nil && pane.pane.webView != nil { | |
| 421 | + | title := pane.pane.webView.GetTitle() | |
| 422 | + | if title == "" { | |
| 423 | + | title = "New Tab" | |
| 424 | + | } | |
| 425 | + | spm.updateTitleBarLabel(pane, title) | |
| 426 | + | } | |
| 412 | 427 | } | |
| 413 | 428 | } | |
| 414 | 429 | } | |
| @@ -494,43 +509,31 @@ func (spm *StackedPaneManager) NavigateStack(direction string) bool { | |||
| 494 | 509 | ||
| 495 | 510 | // createTitleBar creates a title bar widget for a pane in a stack | |
| 496 | 511 | func (spm *StackedPaneManager) createTitleBar(pane *paneNode) uintptr { | |
| 497 | - | titleBox := webkit.NewBox(webkit.OrientationHorizontal, 8) | |
| 498 | - | if titleBox == 0 { | |
| 499 | - | log.Printf("[workspace] failed to create title box") | |
| 500 | - | return 0 | |
| 501 | - | } | |
| 502 | - | ||
| 503 | - | webkit.WidgetAddCSSClass(titleBox, "stacked-pane-title") | |
| 504 | - | webkit.WidgetSetHExpand(titleBox, true) | |
| 505 | - | webkit.WidgetSetVExpand(titleBox, false) | |
| 506 | - | ||
| 507 | - | // Get the actual title from the WebView instead of hardcoding "New Tab" | |
| 508 | - | var titleText string | |
| 512 | + | // Get the actual title and URL from the WebView | |
| 513 | + | var titleText, pageURL string | |
| 509 | 514 | if pane.pane != nil && pane.pane.webView != nil { | |
| 510 | 515 | titleText = pane.pane.webView.GetTitle() | |
| 511 | 516 | if titleText == "" { | |
| 512 | 517 | titleText = "New Tab" // Fallback only when title is actually empty | |
| 513 | 518 | } | |
| 519 | + | pageURL = pane.pane.webView.GetCurrentURL() | |
| 514 | 520 | } else { | |
| 515 | 521 | titleText = "New Tab" | |
| 516 | 522 | } | |
| 517 | 523 | ||
| 518 | - | // Create title label with the actual title | |
| 519 | - | titleLabel := webkit.NewLabel(titleText) | |
| 520 | - | if titleLabel == 0 { | |
| 521 | - | log.Printf("[workspace] failed to create title label") | |
| 522 | - | return titleBox | |
| 523 | - | } | |
| 524 | - | ||
| 525 | - | webkit.WidgetAddCSSClass(titleLabel, "stacked-pane-title-text") | |
| 526 | - | webkit.LabelSetMaxWidthChars(titleLabel, 50) | |
| 527 | - | webkit.LabelSetEllipsize(titleLabel, webkit.EllipsizeEnd) | |
| 524 | + | titleBar := spm.createTitleBarWithTitle(titleText, pageURL) | |
| 525 | + | if titleBar != 0 { | |
| 526 | + | // Store the mapping from titleBar ID to pane | |
| 527 | + | titleBarID := atomic.AddUint64(&spm.nextTitleBarID, 1) | |
| 528 | + | spm.titleBarToPane[uintptr(titleBarID)] = pane | |
| 528 | 529 | ||
| 529 | - | webkit.BoxAppend(titleBox, titleLabel) | |
| 530 | - | webkit.WidgetShow(titleBox) | |
| 531 | - | webkit.WidgetShow(titleLabel) | |
| 530 | + | // Attach click handler | |
| 531 | + | webkit.WidgetAttachClickHandler(titleBar, func() { | |
| 532 | + | spm.handleTitleBarClick(uintptr(titleBarID)) | |
| 533 | + | }) | |
| 534 | + | } | |
| 532 | 535 | ||
| 533 | - | return titleBox | |
| 536 | + | return titleBar | |
| 534 | 537 | } | |
| 535 | 538 | ||
| 536 | 539 | // UpdateTitleBar updates the title bar label for a WebView in stacked panes | |
| @@ -578,19 +581,28 @@ func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string) | |||
| 578 | 581 | return | |
| 579 | 582 | } | |
| 580 | 583 | ||
| 581 | - | // Format the title for display | |
| 582 | - | displayTitle := title | |
| 583 | - | if len(displayTitle) > 50 { | |
| 584 | - | displayTitle = displayTitle[:47] + "..." | |
| 584 | + | // Get the current URL from the WebView for favicon | |
| 585 | + | var pageURL string | |
| 586 | + | if node.pane != nil && node.pane.webView != nil { | |
| 587 | + | pageURL = node.pane.webView.GetCurrentURL() | |
| 585 | 588 | } | |
| 586 | 589 | ||
| 587 | - | // Create new title bar with updated title | |
| 588 | - | newTitleBar := spm.createTitleBarWithTitle(displayTitle) | |
| 590 | + | // Create new title bar with updated title and favicon | |
| 591 | + | newTitleBar := spm.createTitleBarWithTitle(title, pageURL) | |
| 589 | 592 | if newTitleBar == 0 { | |
| 590 | 593 | log.Printf("[workspace] failed to create new title bar") | |
| 591 | 594 | return | |
| 592 | 595 | } | |
| 593 | 596 | ||
| 597 | + | // Store the mapping from titleBar ID to pane and attach click handler | |
| 598 | + | titleBarID := atomic.AddUint64(&spm.nextTitleBarID, 1) | |
| 599 | + | spm.titleBarToPane[uintptr(titleBarID)] = node | |
| 600 | + | ||
| 601 | + | // Attach click handler | |
| 602 | + | webkit.WidgetAttachClickHandler(newTitleBar, func() { | |
| 603 | + | spm.handleTitleBarClick(uintptr(titleBarID)) | |
| 604 | + | }) | |
| 605 | + | ||
| 594 | 606 | // Replace the old title bar in the stack | |
| 595 | 607 | if node.parent != nil && node.parent.isStacked && node.parent.stackWrapper != 0 { | |
| 596 | 608 | // Find the correct insertion position based on the pane's index in the stack | |
| @@ -628,8 +640,8 @@ func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string) | |||
| 628 | 640 | } | |
| 629 | 641 | } | |
| 630 | 642 | ||
| 631 | - | // createTitleBarWithTitle creates a title bar with a specific title | |
| 632 | - | func (spm *StackedPaneManager) createTitleBarWithTitle(title string) uintptr { | |
| 643 | + | // createTitleBarWithTitle creates a title bar with a specific title and optional favicon | |
| 644 | + | func (spm *StackedPaneManager) createTitleBarWithTitle(title string, pageURL string) uintptr { | |
| 633 | 645 | titleBox := webkit.NewBox(webkit.OrientationHorizontal, 8) | |
| 634 | 646 | if titleBox == 0 { | |
| 635 | 647 | return 0 | |
| @@ -639,6 +651,75 @@ func (spm *StackedPaneManager) createTitleBarWithTitle(title string) uintptr { | |||
| 639 | 651 | webkit.WidgetSetHExpand(titleBox, true) | |
| 640 | 652 | webkit.WidgetSetVExpand(titleBox, false) | |
| 641 | 653 | ||
| 654 | + | // Add favicon placeholder for debugging (always visible) | |
| 655 | + | var faviconImg uintptr | |
| 656 | + | var debugReason string | |
| 657 | + | ||
| 658 | + | if pageURL == "" { | |
| 659 | + | debugReason = "empty pageURL" | |
| 660 | + | } else if spm.wm == nil { | |
| 661 | + | debugReason = "nil workspace manager" | |
| 662 | + | } else if spm.wm.app == nil { | |
| 663 | + | debugReason = "nil app" | |
| 664 | + | } else if spm.wm.app.queries == nil { | |
| 665 | + | debugReason = "nil queries" | |
| 666 | + | } else { | |
| 667 | + | // Query database for favicon URL | |
| 668 | + | ctx := context.Background() | |
| 669 | + | entry, err := spm.wm.app.queries.GetHistoryEntry(ctx, pageURL) | |
| 670 | + | if err != nil { | |
| 671 | + | debugReason = fmt.Sprintf("DB query failed: %v", err) | |
| 672 | + | log.Printf("[favicon] Failed to get history entry for %s: %v", pageURL, err) | |
| 673 | + | } else if !entry.FaviconUrl.Valid || entry.FaviconUrl.String == "" { | |
| 674 | + | debugReason = "no favicon URL in DB" | |
| 675 | + | log.Printf("[favicon] No favicon URL in DB for %s", pageURL) | |
| 676 | + | } else { | |
| 677 | + | faviconURL := entry.FaviconUrl.String | |
| 678 | + | log.Printf("[favicon] Found favicon URL in DB for %s: %s", pageURL, faviconURL) | |
| 679 | + | ||
| 680 | + | // Use favicon cache with favicon URL (same as dmenu) | |
| 681 | + | faviconCache, err := cache.NewFaviconCache() | |
| 682 | + | if err != nil { | |
| 683 | + | debugReason = fmt.Sprintf("cache init failed: %v", err) | |
| 684 | + | log.Printf("[favicon] Failed to create cache: %v", err) | |
| 685 | + | } else { | |
| 686 | + | faviconPath := faviconCache.GetCachedPath(faviconURL) | |
| 687 | + | if faviconPath != "" { | |
| 688 | + | log.Printf("[favicon] Loading cached favicon from: %s", faviconPath) | |
| 689 | + | faviconImg = webkit.ImageNewFromFile(faviconPath) | |
| 690 | + | if faviconImg != 0 { | |
| 691 | + | webkit.ImageSetPixelSize(faviconImg, 16) | |
| 692 | + | webkit.WidgetAddCSSClass(faviconImg, "stacked-pane-favicon") | |
| 693 | + | log.Printf("[favicon] Successfully created favicon image widget") | |
| 694 | + | } else { | |
| 695 | + | debugReason = "image widget creation failed" | |
| 696 | + | log.Printf("[favicon] Failed to create image widget from %s", faviconPath) | |
| 697 | + | } | |
| 698 | + | } else { | |
| 699 | + | debugReason = "not cached yet" | |
| 700 | + | log.Printf("[favicon] Favicon not cached yet, starting download: %s", faviconURL) | |
| 701 | + | // Start async download for next time (same as dmenu) | |
| 702 | + | faviconCache.CacheAsync(faviconURL) | |
| 703 | + | } | |
| 704 | + | } | |
| 705 | + | } | |
| 706 | + | } | |
| 707 | + | ||
| 708 | + | // Add favicon or placeholder | |
| 709 | + | if faviconImg != 0 { | |
| 710 | + | webkit.BoxAppend(titleBox, faviconImg) | |
| 711 | + | webkit.WidgetShow(faviconImg) | |
| 712 | + | } else { | |
| 713 | + | // DEBUG: Add visible placeholder label to show code is running | |
| 714 | + | placeholderLabel := webkit.NewLabel("🌐") | |
| 715 | + | if placeholderLabel != 0 { | |
| 716 | + | webkit.WidgetAddCSSClass(placeholderLabel, "stacked-pane-favicon-placeholder") | |
| 717 | + | webkit.BoxAppend(titleBox, placeholderLabel) | |
| 718 | + | webkit.WidgetShow(placeholderLabel) | |
| 719 | + | log.Printf("[favicon] Using placeholder for %s (reason: %s)", pageURL, debugReason) | |
| 720 | + | } | |
| 721 | + | } | |
| 722 | + | ||
| 642 | 723 | // Create title label with the specified title | |
| 643 | 724 | titleLabel := webkit.NewLabel(title) | |
| 644 | 725 | if titleLabel == 0 { | |
| @@ -646,7 +727,6 @@ func (spm *StackedPaneManager) createTitleBarWithTitle(title string) uintptr { | |||
| 646 | 727 | } | |
| 647 | 728 | ||
| 648 | 729 | webkit.WidgetAddCSSClass(titleLabel, "stacked-pane-title-text") | |
| 649 | - | webkit.LabelSetMaxWidthChars(titleLabel, 50) | |
| 650 | 730 | webkit.LabelSetEllipsize(titleLabel, webkit.EllipsizeEnd) | |
| 651 | 731 | ||
| 652 | 732 | webkit.BoxAppend(titleBox, titleLabel) | |
| @@ -852,3 +932,38 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error { | |||
| 852 | 932 | return nil | |
| 853 | 933 | } | |
| 854 | 934 | } | |
| 935 | + | ||
| 936 | + | // handleTitleBarClick handles clicks on title bars to switch the active pane | |
| 937 | + | func (spm *StackedPaneManager) handleTitleBarClick(titleBarID uintptr) { | |
| 938 | + | pane, exists := spm.titleBarToPane[titleBarID] | |
| 939 | + | if !exists || pane == nil || pane.parent == nil || !pane.parent.isStacked { | |
| 940 | + | return | |
| 941 | + | } | |
| 942 | + | ||
| 943 | + | stackNode := pane.parent | |
| 944 | + | ||
| 945 | + | // Find the index of the clicked pane | |
| 946 | + | for i, p := range stackNode.stackedPanes { | |
| 947 | + | if p == pane { | |
| 948 | + | // Only switch if it's not already active | |
| 949 | + | if i != stackNode.activeStackIndex { | |
| 950 | + | log.Printf("[workspace] title bar clicked: switching from pane %d to pane %d", | |
| 951 | + | stackNode.activeStackIndex, i) | |
| 952 | + | ||
| 953 | + | // Update title bar for the pane transitioning from ACTIVE to INACTIVE | |
| 954 | + | currentActivePane := stackNode.stackedPanes[stackNode.activeStackIndex] | |
| 955 | + | if currentActivePane.pane != nil && currentActivePane.pane.webView != nil && currentActivePane.titleBar != 0 { | |
| 956 | + | currentTitle := currentActivePane.pane.webView.GetTitle() | |
| 957 | + | if currentTitle != "" { | |
| 958 | + | spm.updateTitleBarLabel(currentActivePane, currentTitle) | |
| 959 | + | } | |
| 960 | + | } | |
| 961 | + | ||
| 962 | + | stackNode.activeStackIndex = i | |
| 963 | + | spm.UpdateStackVisibility(stackNode) | |
| 964 | + | spm.wm.SetActivePane(pane, SourceStackNav) | |
| 965 | + | } | |
| 966 | + | return | |
| 967 | + | } | |
| 968 | + | } | |
| 969 | + | } | |
| @@ -1,23 +1,29 @@ | |||||
| 1 | package browser | 1 | package browser | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "context" | |||
| 4 | "errors" | 5 | "errors" | ||
| 5 | "fmt" | 6 | "fmt" | ||
| 6 | "log" | 7 | "log" | ||
| 8 | + | "sync/atomic" | |||
| 7 | "time" | 9 | "time" | ||
| 8 | 10 | ||||
| 11 | + | "github.com/bnema/dumber/internal/cache" | |||
| 9 | "github.com/bnema/dumber/pkg/webkit" | 12 | "github.com/bnema/dumber/pkg/webkit" | ||
| 10 | ) | 13 | ) | ||
| 11 | 14 | ||||
| 12 | // StackedPaneManager handles all stacked pane operations | 15 | // StackedPaneManager handles all stacked pane operations | ||
| 13 | type StackedPaneManager struct { | 16 | type StackedPaneManager struct { | ||
| 14 | - | wm *WorkspaceManager | 17 | + | wm *WorkspaceManager |
| 18 | + | titleBarToPane map[uintptr]*paneNode | |||
| 19 | + | nextTitleBarID uint64 | |||
| 15 | } | 20 | } | ||
| 16 | 21 | ||||
| 17 | // NewStackedPaneManager creates a new stacked pane manager | 22 | // NewStackedPaneManager creates a new stacked pane manager | ||
| 18 | func NewStackedPaneManager(wm *WorkspaceManager) *StackedPaneManager { | 23 | func NewStackedPaneManager(wm *WorkspaceManager) *StackedPaneManager { | ||
| 19 | return &StackedPaneManager{ | 24 | return &StackedPaneManager{ | ||
| 20 | - | wm: wm, | 25 | + | wm: wm, |
| 26 | + | titleBarToPane: make(map[uintptr]*paneNode), | |||
| 21 | } | 27 | } | ||
| 22 | } | 28 | } | ||
| 23 | 29 | ||||
| @@ -409,6 +415,15 @@ func (spm *StackedPaneManager) UpdateStackVisibility(stackNode *paneNode) { | |||||
| 409 | } | 415 | } | ||
| 410 | if pane.titleBar != 0 { | 416 | if pane.titleBar != 0 { | ||
| 411 | webkit.WidgetSetVisible(pane.titleBar, true) | 417 | webkit.WidgetSetVisible(pane.titleBar, true) | ||
| 418 | + | ||||
| 419 | + | // Refresh title bar to pick up any newly downloaded favicons | |||
| 420 | + | if pane.pane != nil && pane.pane.webView != nil { | |||
| 421 | + | title := pane.pane.webView.GetTitle() | |||
| 422 | + | if title == "" { | |||
| 423 | + | title = "New Tab" | |||
| 424 | + | } | |||
| 425 | + | spm.updateTitleBarLabel(pane, title) | |||
| 426 | + | } | |||
| 412 | } | 427 | } | ||
| 413 | } | 428 | } | ||
| 414 | } | 429 | } | ||
| @@ -494,43 +509,31 @@ func (spm *StackedPaneManager) NavigateStack(direction string) bool { | |||||
| 494 | 509 | ||||
| 495 | // createTitleBar creates a title bar widget for a pane in a stack | 510 | // createTitleBar creates a title bar widget for a pane in a stack | ||
| 496 | func (spm *StackedPaneManager) createTitleBar(pane *paneNode) uintptr { | 511 | func (spm *StackedPaneManager) createTitleBar(pane *paneNode) uintptr { | ||
| 497 | - | titleBox := webkit.NewBox(webkit.OrientationHorizontal, 8) | 512 | + | // Get the actual title and URL from the WebView |
| 498 | - | if titleBox == 0 { | 513 | + | var titleText, pageURL string |
| 499 | - | log.Printf("[workspace] failed to create title box") | |||
| 500 | - | return 0 | |||
| 501 | - | } | |||
| 502 | - | ||||
| 503 | - | webkit.WidgetAddCSSClass(titleBox, "stacked-pane-title") | |||
| 504 | - | webkit.WidgetSetHExpand(titleBox, true) | |||
| 505 | - | webkit.WidgetSetVExpand(titleBox, false) | |||
| 506 | - | ||||
| 507 | - | // Get the actual title from the WebView instead of hardcoding "New Tab" | |||
| 508 | - | var titleText string | |||
| 509 | if pane.pane != nil && pane.pane.webView != nil { | 514 | if pane.pane != nil && pane.pane.webView != nil { | ||
| 510 | titleText = pane.pane.webView.GetTitle() | 515 | titleText = pane.pane.webView.GetTitle() | ||
| 511 | if titleText == "" { | 516 | if titleText == "" { | ||
| 512 | titleText = "New Tab" // Fallback only when title is actually empty | 517 | titleText = "New Tab" // Fallback only when title is actually empty | ||
| 513 | } | 518 | } | ||
| 519 | + | pageURL = pane.pane.webView.GetCurrentURL() | |||
| 514 | } else { | 520 | } else { | ||
| 515 | titleText = "New Tab" | 521 | titleText = "New Tab" | ||
| 516 | } | 522 | } | ||
| 517 | 523 | ||||
| 518 | - | // Create title label with the actual title | 524 | + | titleBar := spm.createTitleBarWithTitle(titleText, pageURL) |
| 519 | - | titleLabel := webkit.NewLabel(titleText) | 525 | + | if titleBar != 0 { |
| 520 | - | if titleLabel == 0 { | 526 | + | // Store the mapping from titleBar ID to pane |
| 521 | - | log.Printf("[workspace] failed to create title label") | 527 | + | titleBarID := atomic.AddUint64(&spm.nextTitleBarID, 1) |
| 522 | - | return titleBox | 528 | + | spm.titleBarToPane[uintptr(titleBarID)] = pane |
| 523 | - | } | |||
| 524 | - | ||||
| 525 | - | webkit.WidgetAddCSSClass(titleLabel, "stacked-pane-title-text") | |||
| 526 | - | webkit.LabelSetMaxWidthChars(titleLabel, 50) | |||
| 527 | - | webkit.LabelSetEllipsize(titleLabel, webkit.EllipsizeEnd) | |||
| 528 | 529 | ||||
| 529 | - | webkit.BoxAppend(titleBox, titleLabel) | 530 | + | // Attach click handler |
| 530 | - | webkit.WidgetShow(titleBox) | 531 | + | webkit.WidgetAttachClickHandler(titleBar, func() { |
| 531 | - | webkit.WidgetShow(titleLabel) | 532 | + | spm.handleTitleBarClick(uintptr(titleBarID)) |
| 533 | + | }) | |||
| 534 | + | } | |||
| 532 | 535 | ||||
| 533 | - | return titleBox | 536 | + | return titleBar |
| 534 | } | 537 | } | ||
| 535 | 538 | ||||
| 536 | // UpdateTitleBar updates the title bar label for a WebView in stacked panes | 539 | // UpdateTitleBar updates the title bar label for a WebView in stacked panes | ||
| @@ -578,19 +581,28 @@ func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string) | |||||
| 578 | return | 581 | return | ||
| 579 | } | 582 | } | ||
| 580 | 583 | ||||
| 581 | - | // Format the title for display | 584 | + | // Get the current URL from the WebView for favicon |
| 582 | - | displayTitle := title | 585 | + | var pageURL string |
| 583 | - | if len(displayTitle) > 50 { | 586 | + | if node.pane != nil && node.pane.webView != nil { |
| 584 | - | displayTitle = displayTitle[:47] + "..." | 587 | + | pageURL = node.pane.webView.GetCurrentURL() |
| 585 | } | 588 | } | ||
| 586 | 589 | ||||
| 587 | - | // Create new title bar with updated title | 590 | + | // Create new title bar with updated title and favicon |
| 588 | - | newTitleBar := spm.createTitleBarWithTitle(displayTitle) | 591 | + | newTitleBar := spm.createTitleBarWithTitle(title, pageURL) |
| 589 | if newTitleBar == 0 { | 592 | if newTitleBar == 0 { | ||
| 590 | log.Printf("[workspace] failed to create new title bar") | 593 | log.Printf("[workspace] failed to create new title bar") | ||
| 591 | return | 594 | return | ||
| 592 | } | 595 | } | ||
| 593 | 596 | ||||
| 597 | + | // Store the mapping from titleBar ID to pane and attach click handler | |||
| 598 | + | titleBarID := atomic.AddUint64(&spm.nextTitleBarID, 1) | |||
| 599 | + | spm.titleBarToPane[uintptr(titleBarID)] = node | |||
| 600 | + | ||||
| 601 | + | // Attach click handler | |||
| 602 | + | webkit.WidgetAttachClickHandler(newTitleBar, func() { | |||
| 603 | + | spm.handleTitleBarClick(uintptr(titleBarID)) | |||
| 604 | + | }) | |||
| 605 | + | ||||
| 594 | // Replace the old title bar in the stack | 606 | // Replace the old title bar in the stack | ||
| 595 | if node.parent != nil && node.parent.isStacked && node.parent.stackWrapper != 0 { | 607 | if node.parent != nil && node.parent.isStacked && node.parent.stackWrapper != 0 { | ||
| 596 | // Find the correct insertion position based on the pane's index in the stack | 608 | // Find the correct insertion position based on the pane's index in the stack | ||
| @@ -628,8 +640,8 @@ func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string) | |||||
| 628 | } | 640 | } | ||
| 629 | } | 641 | } | ||
| 630 | 642 | ||||
| 631 | - | // createTitleBarWithTitle creates a title bar with a specific title | 643 | + | // createTitleBarWithTitle creates a title bar with a specific title and optional favicon |
| 632 | - | func (spm *StackedPaneManager) createTitleBarWithTitle(title string) uintptr { | 644 | + | func (spm *StackedPaneManager) createTitleBarWithTitle(title string, pageURL string) uintptr { |
| 633 | titleBox := webkit.NewBox(webkit.OrientationHorizontal, 8) | 645 | titleBox := webkit.NewBox(webkit.OrientationHorizontal, 8) | ||
| 634 | if titleBox == 0 { | 646 | if titleBox == 0 { | ||
| 635 | return 0 | 647 | return 0 | ||
| @@ -639,6 +651,75 @@ func (spm *StackedPaneManager) createTitleBarWithTitle(title string) uintptr { | |||||
| 639 | webkit.WidgetSetHExpand(titleBox, true) | 651 | webkit.WidgetSetHExpand(titleBox, true) | ||
| 640 | webkit.WidgetSetVExpand(titleBox, false) | 652 | webkit.WidgetSetVExpand(titleBox, false) | ||
| 641 | 653 | ||||
| 654 | + | // Add favicon placeholder for debugging (always visible) | |||
| 655 | + | var faviconImg uintptr | |||
| 656 | + | var debugReason string | |||
| 657 | + | ||||
| 658 | + | if pageURL == "" { | |||
| 659 | + | debugReason = "empty pageURL" | |||
| 660 | + | } else if spm.wm == nil { | |||
| 661 | + | debugReason = "nil workspace manager" | |||
| 662 | + | } else if spm.wm.app == nil { | |||
| 663 | + | debugReason = "nil app" | |||
| 664 | + | } else if spm.wm.app.queries == nil { | |||
| 665 | + | debugReason = "nil queries" | |||
| 666 | + | } else { | |||
| 667 | + | // Query database for favicon URL | |||
| 668 | + | ctx := context.Background() | |||
| 669 | + | entry, err := spm.wm.app.queries.GetHistoryEntry(ctx, pageURL) | |||
| 670 | + | if err != nil { | |||
| 671 | + | debugReason = fmt.Sprintf("DB query failed: %v", err) | |||
| 672 | + | log.Printf("[favicon] Failed to get history entry for %s: %v", pageURL, err) | |||
| 673 | + | } else if !entry.FaviconUrl.Valid || entry.FaviconUrl.String == "" { | |||
| 674 | + | debugReason = "no favicon URL in DB" | |||
| 675 | + | log.Printf("[favicon] No favicon URL in DB for %s", pageURL) | |||
| 676 | + | } else { | |||
| 677 | + | faviconURL := entry.FaviconUrl.String | |||
| 678 | + | log.Printf("[favicon] Found favicon URL in DB for %s: %s", pageURL, faviconURL) | |||
| 679 | + | ||||
| 680 | + | // Use favicon cache with favicon URL (same as dmenu) | |||
| 681 | + | faviconCache, err := cache.NewFaviconCache() | |||
| 682 | + | if err != nil { | |||
| 683 | + | debugReason = fmt.Sprintf("cache init failed: %v", err) | |||
| 684 | + | log.Printf("[favicon] Failed to create cache: %v", err) | |||
| 685 | + | } else { | |||
| 686 | + | faviconPath := faviconCache.GetCachedPath(faviconURL) | |||
| 687 | + | if faviconPath != "" { | |||
| 688 | + | log.Printf("[favicon] Loading cached favicon from: %s", faviconPath) | |||
| 689 | + | faviconImg = webkit.ImageNewFromFile(faviconPath) | |||
| 690 | + | if faviconImg != 0 { | |||
| 691 | + | webkit.ImageSetPixelSize(faviconImg, 16) | |||
| 692 | + | webkit.WidgetAddCSSClass(faviconImg, "stacked-pane-favicon") | |||
| 693 | + | log.Printf("[favicon] Successfully created favicon image widget") | |||
| 694 | + | } else { | |||
| 695 | + | debugReason = "image widget creation failed" | |||
| 696 | + | log.Printf("[favicon] Failed to create image widget from %s", faviconPath) | |||
| 697 | + | } | |||
| 698 | + | } else { | |||
| 699 | + | debugReason = "not cached yet" | |||
| 700 | + | log.Printf("[favicon] Favicon not cached yet, starting download: %s", faviconURL) | |||
| 701 | + | // Start async download for next time (same as dmenu) | |||
| 702 | + | faviconCache.CacheAsync(faviconURL) | |||
| 703 | + | } | |||
| 704 | + | } | |||
| 705 | + | } | |||
| 706 | + | } | |||
| 707 | + | ||||
| 708 | + | // Add favicon or placeholder | |||
| 709 | + | if faviconImg != 0 { | |||
| 710 | + | webkit.BoxAppend(titleBox, faviconImg) | |||
| 711 | + | webkit.WidgetShow(faviconImg) | |||
| 712 | + | } else { | |||
| 713 | + | // DEBUG: Add visible placeholder label to show code is running | |||
| 714 | + | placeholderLabel := webkit.NewLabel("🌐") | |||
| 715 | + | if placeholderLabel != 0 { | |||
| 716 | + | webkit.WidgetAddCSSClass(placeholderLabel, "stacked-pane-favicon-placeholder") | |||
| 717 | + | webkit.BoxAppend(titleBox, placeholderLabel) | |||
| 718 | + | webkit.WidgetShow(placeholderLabel) | |||
| 719 | + | log.Printf("[favicon] Using placeholder for %s (reason: %s)", pageURL, debugReason) | |||
| 720 | + | } | |||
| 721 | + | } | |||
| 722 | + | ||||
| 642 | // Create title label with the specified title | 723 | // Create title label with the specified title | ||
| 643 | titleLabel := webkit.NewLabel(title) | 724 | titleLabel := webkit.NewLabel(title) | ||
| 644 | if titleLabel == 0 { | 725 | if titleLabel == 0 { | ||
| @@ -646,7 +727,6 @@ func (spm *StackedPaneManager) createTitleBarWithTitle(title string) uintptr { | |||||
| 646 | } | 727 | } | ||
| 647 | 728 | ||||
| 648 | webkit.WidgetAddCSSClass(titleLabel, "stacked-pane-title-text") | 729 | webkit.WidgetAddCSSClass(titleLabel, "stacked-pane-title-text") | ||
| 649 | - | webkit.LabelSetMaxWidthChars(titleLabel, 50) | |||
| 650 | webkit.LabelSetEllipsize(titleLabel, webkit.EllipsizeEnd) | 730 | webkit.LabelSetEllipsize(titleLabel, webkit.EllipsizeEnd) | ||
| 651 | 731 | ||||
| 652 | webkit.BoxAppend(titleBox, titleLabel) | 732 | webkit.BoxAppend(titleBox, titleLabel) | ||
| @@ -852,3 +932,38 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error { | |||||
| 852 | return nil | 932 | return nil | ||
| 853 | } | 933 | } | ||
| 854 | } | 934 | } | ||
| 935 | + | ||||
| 936 | + | // handleTitleBarClick handles clicks on title bars to switch the active pane | |||
| 937 | + | func (spm *StackedPaneManager) handleTitleBarClick(titleBarID uintptr) { | |||
| 938 | + | pane, exists := spm.titleBarToPane[titleBarID] | |||
| 939 | + | if !exists || pane == nil || pane.parent == nil || !pane.parent.isStacked { | |||
| 940 | + | return | |||
| 941 | + | } | |||
| 942 | + | ||||
| 943 | + | stackNode := pane.parent | |||
| 944 | + | ||||
| 945 | + | // Find the index of the clicked pane | |||
| 946 | + | for i, p := range stackNode.stackedPanes { | |||
| 947 | + | if p == pane { | |||
| 948 | + | // Only switch if it's not already active | |||
| 949 | + | if i != stackNode.activeStackIndex { | |||
| 950 | + | log.Printf("[workspace] title bar clicked: switching from pane %d to pane %d", | |||
| 951 | + | stackNode.activeStackIndex, i) | |||
| 952 | + | ||||
| 953 | + | // Update title bar for the pane transitioning from ACTIVE to INACTIVE | |||
| 954 | + | currentActivePane := stackNode.stackedPanes[stackNode.activeStackIndex] | |||
| 955 | + | if currentActivePane.pane != nil && currentActivePane.pane.webView != nil && currentActivePane.titleBar != 0 { | |||
| 956 | + | currentTitle := currentActivePane.pane.webView.GetTitle() | |||
| 957 | + | if currentTitle != "" { | |||
| 958 | + | spm.updateTitleBarLabel(currentActivePane, currentTitle) | |||
| 959 | + | } | |||
| 960 | + | } | |||
| 961 | + | ||||
| 962 | + | stackNode.activeStackIndex = i | |||
| 963 | + | spm.UpdateStackVisibility(stackNode) | |||
| 964 | + | spm.wm.SetActivePane(pane, SourceStackNav) | |||
| 965 | + | } | |||
| 966 | + | return | |||
| 967 | + | } | |||
| 968 | + | } | |||
| 969 | + | } | |||
M internal/app/browser/webview.go +9 -0
| @@ -144,6 +144,15 @@ func (app *BrowserApp) buildPane(view *webkit.WebView) (*BrowserPane, error) { | |||
| 144 | 144 | app.workspace.RegisterNavigationHandler(view) | |
| 145 | 145 | } | |
| 146 | 146 | ||
| 147 | + | // Register favicon handlers for this webview (needed for each pane) | |
| 148 | + | // This ensures favicons are detected and cached for all panes, not just the main webview | |
| 149 | + | view.RegisterFaviconURIChangedHandler(func(pageURI, faviconURI string) { | |
| 150 | + | // Call the private handler through a public wrapper or directly via browserService | |
| 151 | + | // We can't call handleFaviconURIChanged directly as it's private, so we need to expose it | |
| 152 | + | // For now, duplicate the minimal logic here | |
| 153 | + | app.browserService.ProcessFaviconURI(pageURI, faviconURI) | |
| 154 | + | }) | |
| 155 | + | ||
| 147 | 156 | return pane, nil | |
| 148 | 157 | } | |
| 149 | 158 | ||
| @@ -144,6 +144,15 @@ func (app *BrowserApp) buildPane(view *webkit.WebView) (*BrowserPane, error) { | |||||
| 144 | app.workspace.RegisterNavigationHandler(view) | 144 | app.workspace.RegisterNavigationHandler(view) | ||
| 145 | } | 145 | } | ||
| 146 | 146 | ||||
| 147 | + | // Register favicon handlers for this webview (needed for each pane) | |||
| 148 | + | // This ensures favicons are detected and cached for all panes, not just the main webview | |||
| 149 | + | view.RegisterFaviconURIChangedHandler(func(pageURI, faviconURI string) { | |||
| 150 | + | // Call the private handler through a public wrapper or directly via browserService | |||
| 151 | + | // We can't call handleFaviconURIChanged directly as it's private, so we need to expose it | |||
| 152 | + | // For now, duplicate the minimal logic here | |||
| 153 | + | app.browserService.ProcessFaviconURI(pageURI, faviconURI) | |||
| 154 | + | }) | |||
| 155 | + | ||||
| 147 | return pane, nil | 156 | return pane, nil | ||
| 148 | } | 157 | } | ||
| 149 | 158 | ||||
M internal/app/browser/workspace_css.go +4 -0
| @@ -117,6 +117,10 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||
| 117 | 117 | font-weight: 500; | |
| 118 | 118 | } | |
| 119 | 119 | ||
| 120 | + | .stacked-pane-favicon { | |
| 121 | + | margin-right: 4px; | |
| 122 | + | } | |
| 123 | + | ||
| 120 | 124 | .stacked-pane-active { | |
| 121 | 125 | /* Active pane is fully visible */ | |
| 122 | 126 | } | |
| @@ -117,6 +117,10 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||||
| 117 | font-weight: 500; | 117 | font-weight: 500; | ||
| 118 | } | 118 | } | ||
| 119 | 119 | ||||
| 120 | + | .stacked-pane-favicon { | |||
| 121 | + | margin-right: 4px; | |||
| 122 | + | } | |||
| 123 | + | ||||
| 120 | .stacked-pane-active { | 124 | .stacked-pane-active { | ||
| 121 | /* Active pane is fully visible */ | 125 | /* Active pane is fully visible */ | ||
| 122 | } | 126 | } | ||