detached 3f10f36

refactor(browser): simplify border API and improve deduplication logic

Changes:
- Remove determineBorderContext() in favor of direct applyActivePaneBorder(node)
- Update IsDuplicate() to only dedupe when target is already active
- Change CSS from outline to border for better rendering
- Update all callsites to use simplified border API

This simplifies the border management system and fixes focus request
deduplication to allow re-focusing a pane that lost focus.
commit
3f10f364355cc922bb646cfc3a7977a05db2daec
author
bamen · · 2025-10-09 20:29 UTC
committer
bamen · · 2025-10-09 19:09 UTC
parent
a6d29c6
signature
unsigned
7 files · +38 -91
M internal/app/browser/focus_state_machine.go +16 -11
@@ -159,7 +159,7 @@ func NewRequestDeduplicator(ttl time.Duration) *RequestDeduplicator {
159159 }
160160
161161 // IsDuplicate checks if a request is a duplicate within the TTL window
162-func (rd *RequestDeduplicator) IsDuplicate(req FocusRequest) bool {
162+func (rd *RequestDeduplicator) IsDuplicate(req FocusRequest, isActiveTarget bool) bool {
163163 rd.mu.Lock()
164164 defer rd.mu.Unlock()
165165
@@ -174,12 +174,16 @@ func (rd *RequestDeduplicator) IsDuplicate(req FocusRequest) bool {
174174 // Generate signature for this request
175175 sig := fmt.Sprintf("%p:%s", req.TargetNode, req.Source)
176176
177- // Check if this signature exists
178- if _, exists := rd.recentSigs[sig]; exists {
179- return true
177+ // Always record latest timestamp so future checks use fresh window
178+ // Prevent duplicates only when the target is already active; otherwise we
179+ // want to process the request to move focus back to that pane.
180+ if timestamp, exists := rd.recentSigs[sig]; exists {
181+ if isActiveTarget && now.Sub(timestamp) <= rd.ttl {
182+ rd.recentSigs[sig] = now
183+ return true
184+ }
180185 }
181186
182- // Record this signature
183187 rd.recentSigs[sig] = now
184188 return false
185189 }
@@ -367,8 +371,11 @@ func (fsm *FocusStateMachine) RequestFocus(node *paneNode, source FocusSource) e
367371 fsm.mu.Unlock()
368372 }
369373
370- // Check for duplicates
371- if fsm.deduplicator.IsDuplicate(request) {
374+ activePane := fsm.GetActivePane()
375+
376+ // Check for duplicates (only dedupe when the request targets the currently
377+ // active pane to avoid suppressing necessary focus transitions).
378+ if fsm.deduplicator.IsDuplicate(request, activePane == node) {
372379 if fsm.metricsEnabled {
373380 fsm.mu.Lock()
374381 fsm.metrics.DuplicateRequests++
@@ -589,8 +596,7 @@ func (fsm *FocusStateMachine) applyInitialFocus(node *paneNode) error {
589596
590597 // Apply initial visual border
591598 if fsm.wm != nil {
592- ctx := fsm.wm.determineBorderContext(node)
593- fsm.wm.applyActivePaneBorder(ctx)
599+ fsm.wm.applyActivePaneBorder(node)
594600 }
595601
596602 // Update state
@@ -756,8 +762,7 @@ func (fsm *FocusStateMachine) executeFocusChange(request FocusRequest) error {
756762 fsm.wm.removeActivePaneBorder(oldPane)
757763 }
758764 // Add border to new pane
759- ctx := fsm.wm.determineBorderContext(newPane)
760- fsm.wm.applyActivePaneBorder(ctx)
765+ fsm.wm.applyActivePaneBorder(newPane)
761766 }
762767
763768 // Update state
@@ -159,7 +159,7 @@ func NewRequestDeduplicator(ttl time.Duration) *RequestDeduplicator {
159 }159 }
160 160
161 // IsDuplicate checks if a request is a duplicate within the TTL window161 // IsDuplicate checks if a request is a duplicate within the TTL window
162-func (rd *RequestDeduplicator) IsDuplicate(req FocusRequest) bool {162+func (rd *RequestDeduplicator) IsDuplicate(req FocusRequest, isActiveTarget bool) bool {
163 rd.mu.Lock()163 rd.mu.Lock()
164 defer rd.mu.Unlock()164 defer rd.mu.Unlock()
165 165
@@ -174,12 +174,16 @@ func (rd *RequestDeduplicator) IsDuplicate(req FocusRequest) bool {
174 // Generate signature for this request174 // Generate signature for this request
175 sig := fmt.Sprintf("%p:%s", req.TargetNode, req.Source)175 sig := fmt.Sprintf("%p:%s", req.TargetNode, req.Source)
176 176
177- // Check if this signature exists177+ // Always record latest timestamp so future checks use fresh window
178- if _, exists := rd.recentSigs[sig]; exists {178+ // Prevent duplicates only when the target is already active; otherwise we
179- return true179+ // want to process the request to move focus back to that pane.
180+ if timestamp, exists := rd.recentSigs[sig]; exists {
181+ if isActiveTarget && now.Sub(timestamp) <= rd.ttl {
182+ rd.recentSigs[sig] = now
183+ return true
184+ }
180 }185 }
181 186
182- // Record this signature
183 rd.recentSigs[sig] = now187 rd.recentSigs[sig] = now
184 return false188 return false
185 }189 }
@@ -367,8 +371,11 @@ func (fsm *FocusStateMachine) RequestFocus(node *paneNode, source FocusSource) e
367 fsm.mu.Unlock()371 fsm.mu.Unlock()
368 }372 }
369 373
370- // Check for duplicates374+ activePane := fsm.GetActivePane()
371- if fsm.deduplicator.IsDuplicate(request) {375+
376+ // Check for duplicates (only dedupe when the request targets the currently
377+ // active pane to avoid suppressing necessary focus transitions).
378+ if fsm.deduplicator.IsDuplicate(request, activePane == node) {
372 if fsm.metricsEnabled {379 if fsm.metricsEnabled {
373 fsm.mu.Lock()380 fsm.mu.Lock()
374 fsm.metrics.DuplicateRequests++381 fsm.metrics.DuplicateRequests++
@@ -589,8 +596,7 @@ func (fsm *FocusStateMachine) applyInitialFocus(node *paneNode) error {
589 596
590 // Apply initial visual border597 // Apply initial visual border
591 if fsm.wm != nil {598 if fsm.wm != nil {
592- ctx := fsm.wm.determineBorderContext(node)599+ fsm.wm.applyActivePaneBorder(node)
593- fsm.wm.applyActivePaneBorder(ctx)
594 }600 }
595 601
596 // Update state602 // Update state
@@ -756,8 +762,7 @@ func (fsm *FocusStateMachine) executeFocusChange(request FocusRequest) error {
756 fsm.wm.removeActivePaneBorder(oldPane)762 fsm.wm.removeActivePaneBorder(oldPane)
757 }763 }
758 // Add border to new pane764 // Add border to new pane
759- ctx := fsm.wm.determineBorderContext(newPane)765+ fsm.wm.applyActivePaneBorder(newPane)
760- fsm.wm.applyActivePaneBorder(ctx)
761 }766 }
762 767
763 // Update state768 // Update state
M internal/app/browser/stacked_panes.go +3 -4
@@ -20,9 +20,9 @@ const (
2020
2121 // StackedPaneManager handles all stacked pane operations
2222 type StackedPaneManager struct {
23- wm *WorkspaceManager
24- titleBarToPane map[uint64]*paneNode
25- nextTitleBarID uint64
23+ wm *WorkspaceManager
24+ titleBarToPane map[uint64]*paneNode
25+ nextTitleBarID uint64
2626 }
2727
2828 // NewStackedPaneManager creates a new stacked pane manager
@@ -90,7 +90,6 @@ func (spm *StackedPaneManager) prepareNewStackedPane() (*paneNode, error) {
9090 }
9191 webkit.WidgetSetHExpand(newContainer, true)
9292 webkit.WidgetSetVExpand(newContainer, true)
93- webkit.WidgetRealize(newContainer)
9493
9594 // Create the new leaf node
9695 newLeaf := &paneNode{
@@ -20,9 +20,9 @@ const (
20 20
21 // StackedPaneManager handles all stacked pane operations21 // StackedPaneManager handles all stacked pane operations
22 type StackedPaneManager struct {22 type StackedPaneManager struct {
23- wm *WorkspaceManager23+ wm *WorkspaceManager
24- titleBarToPane map[uint64]*paneNode24+ titleBarToPane map[uint64]*paneNode
25- nextTitleBarID uint6425+ nextTitleBarID uint64
26 }26 }
27 27
28 // NewStackedPaneManager creates a new stacked pane manager28 // NewStackedPaneManager creates a new stacked pane manager
@@ -90,7 +90,6 @@ func (spm *StackedPaneManager) prepareNewStackedPane() (*paneNode, error) {
90 }90 }
91 webkit.WidgetSetHExpand(newContainer, true)91 webkit.WidgetSetHExpand(newContainer, true)
92 webkit.WidgetSetVExpand(newContainer, true)92 webkit.WidgetSetVExpand(newContainer, true)
93- webkit.WidgetRealize(newContainer)
94 93
95 // Create the new leaf node94 // Create the new leaf node
96 newLeaf := &paneNode{95 newLeaf := &paneNode{
M internal/app/browser/workspace_css.go +11 -12
@@ -79,25 +79,24 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string {
7979
8080 /* Base pane styling - subtle border for inactive panes */
8181 .workspace-pane, .stacked-pane-container {
82- outline: 2px solid %s;
83- outline-offset: -2px;
84- transition: outline-color %dms ease-in-out;
82+ border: 2px solid %s;
83+ border-radius: %dpx;
84+ transition: border-color %dms ease-in-out;
8585 }
8686
87- /* Active pane border styling using outline (no layout impact) */
87+ /* Active pane border styling */
8888 .workspace-pane-active {
89- outline-color: %s;
89+ border-color: %s;
9090 }
9191
9292 /* Stacked panes styling */
9393 .stacked-pane-container {
9494 background-color: %s;
95- border-radius: %dpx;
9695 }
9796
9897 /* Active stacked pane container gets the outline color */
9998 .stacked-pane-container.workspace-pane-active {
100- outline-color: %s;
99+ border-color: %s;
101100 }
102101
103102 .stacked-pane-title {
@@ -130,12 +129,12 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string {
130129 /* Collapsed panes are hidden - handled in code via widget visibility */
131130 }`,
132131 windowBackgroundColor, // window background
133- inactiveBorderColor, // base pane outline color (inactive)
134- styling.TransitionDuration, // base pane outline transition
135- activeBorderColor, // workspace-pane-active outline-color
132+ inactiveBorderColor, // base pane border color (inactive)
133+ styling.BorderRadius, // base pane border radius
134+ styling.TransitionDuration, // base pane border transition
135+ activeBorderColor, // workspace-pane-active border color
136136 windowBackgroundColor, // stacked-pane-container background
137- styling.BorderRadius, // stacked-pane-container border-radius
138- activeBorderColor, // stacked-pane-container.workspace-pane-active outline-color
137+ activeBorderColor, // stacked-pane-container.workspace-pane-active border color
139138 getStackTitleBg(isDark), // stacked-pane-title background
140139 inactiveBorderColor, // stacked-pane-title border-bottom
141140 styling.TransitionDuration, // stacked-pane-title transition
@@ -79,25 +79,24 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string {
79 79
80 /* Base pane styling - subtle border for inactive panes */80 /* Base pane styling - subtle border for inactive panes */
81 .workspace-pane, .stacked-pane-container {81 .workspace-pane, .stacked-pane-container {
82- outline: 2px solid %s;82+ border: 2px solid %s;
83- outline-offset: -2px;83+ border-radius: %dpx;
84- transition: outline-color %dms ease-in-out;84+ transition: border-color %dms ease-in-out;
85 }85 }
86 86
87- /* Active pane border styling using outline (no layout impact) */87+ /* Active pane border styling */
88 .workspace-pane-active {88 .workspace-pane-active {
89- outline-color: %s;89+ border-color: %s;
90 }90 }
91 91
92 /* Stacked panes styling */92 /* Stacked panes styling */
93 .stacked-pane-container {93 .stacked-pane-container {
94 background-color: %s;94 background-color: %s;
95- border-radius: %dpx;
96 }95 }
97 96
98 /* Active stacked pane container gets the outline color */97 /* Active stacked pane container gets the outline color */
99 .stacked-pane-container.workspace-pane-active {98 .stacked-pane-container.workspace-pane-active {
100- outline-color: %s;99+ border-color: %s;
101 }100 }
102 101
103 .stacked-pane-title {102 .stacked-pane-title {
@@ -130,12 +129,12 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string {
130 /* Collapsed panes are hidden - handled in code via widget visibility */129 /* Collapsed panes are hidden - handled in code via widget visibility */
131 }`,130 }`,
132 windowBackgroundColor, // window background131 windowBackgroundColor, // window background
133- inactiveBorderColor, // base pane outline color (inactive)132+ inactiveBorderColor, // base pane border color (inactive)
134- styling.TransitionDuration, // base pane outline transition133+ styling.BorderRadius, // base pane border radius
135- activeBorderColor, // workspace-pane-active outline-color134+ styling.TransitionDuration, // base pane border transition
135+ activeBorderColor, // workspace-pane-active border color
136 windowBackgroundColor, // stacked-pane-container background136 windowBackgroundColor, // stacked-pane-container background
137- styling.BorderRadius, // stacked-pane-container border-radius137+ activeBorderColor, // stacked-pane-container.workspace-pane-active border color
138- activeBorderColor, // stacked-pane-container.workspace-pane-active outline-color
139 getStackTitleBg(isDark), // stacked-pane-title background138 getStackTitleBg(isDark), // stacked-pane-title background
140 inactiveBorderColor, // stacked-pane-title border-bottom139 inactiveBorderColor, // stacked-pane-title border-bottom
141 styling.TransitionDuration, // stacked-pane-title transition140 styling.TransitionDuration, // stacked-pane-title transition
M internal/app/browser/workspace_manager.go +3 -0
@@ -166,6 +166,9 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag
166166 log.Printf("[workspace] Failed to initialize focus state machine: %v", err)
167167 }
168168
169+ // Ensure hover controllers are attached to all panes for mouse-driven focus changes
170+ manager.attachHoverHandlersToAllPanes()
171+
169172 if manager.debugPaneClose {
170173 manager.dumpTreeState("workspace_init")
171174 }
@@ -166,6 +166,9 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag
166 log.Printf("[workspace] Failed to initialize focus state machine: %v", err)166 log.Printf("[workspace] Failed to initialize focus state machine: %v", err)
167 }167 }
168 168
169+ // Ensure hover controllers are attached to all panes for mouse-driven focus changes
170+ manager.attachHoverHandlersToAllPanes()
171+
169 if manager.debugPaneClose {172 if manager.debugPaneClose {
170 manager.dumpTreeState("workspace_init")173 manager.dumpTreeState("workspace_init")
171 }174 }
M internal/app/browser/workspace_navigation.go +1 -41
@@ -89,47 +89,7 @@ func (wm *WorkspaceManager) focusByView(view *webkit.WebView) {
8989 }
9090 }
9191
92-// ensureHover sets up hover handlers for a pane node
93-func (wm *WorkspaceManager) ensureHover(node *paneNode) {
94- if wm == nil || node == nil || !node.isLeaf {
95- return
96- }
97- if node.container == nil || node.hoverToken != 0 {
98- return
99- }
100-
101- // Create motion controller for hover detection
102- controller := gtk.NewEventControllerMotion()
103- controller.ConnectEnter(func(x, y float64) {
104- if wm == nil {
105- return
106- }
107- wm.SetActivePane(node, SourceMouse)
108- })
109-
110- webkit.WidgetAddController(node.container, controller)
111- // Store controller pointer as token for later removal
112- node.hoverToken = uintptr(controller.Native())
113- if node.hoverToken == 0 {
114- log.Printf("[workspace] failed to attach hover handler")
115- }
116-}
117-
118-// detachHover removes hover handlers from a pane node
119-func (wm *WorkspaceManager) detachHover(node *paneNode) {
120- if wm == nil || node == nil || node.hoverToken == 0 {
121- return
122- }
123-
124- if node.container == nil {
125- node.hoverToken = 0
126- return
127- }
128-
129- // Note: In GTK4, controllers are automatically removed when widget is destroyed
130- // We just need to clear our token reference
131- node.hoverToken = 0
132-}
92+// Note: ensureHover and detachHover are now implemented in workspace_hover.go
13393
13494 // detachFocus removes the GTK focus controller attached to a pane, if any.
13595 func (wm *WorkspaceManager) detachFocus(node *paneNode) {
@@ -89,47 +89,7 @@ func (wm *WorkspaceManager) focusByView(view *webkit.WebView) {
89 }89 }
90 }90 }
91 91
92-// ensureHover sets up hover handlers for a pane node92+// Note: ensureHover and detachHover are now implemented in workspace_hover.go
93-func (wm *WorkspaceManager) ensureHover(node *paneNode) {
94- if wm == nil || node == nil || !node.isLeaf {
95- return
96- }
97- if node.container == nil || node.hoverToken != 0 {
98- return
99- }
100-
101- // Create motion controller for hover detection
102- controller := gtk.NewEventControllerMotion()
103- controller.ConnectEnter(func(x, y float64) {
104- if wm == nil {
105- return
106- }
107- wm.SetActivePane(node, SourceMouse)
108- })
109-
110- webkit.WidgetAddController(node.container, controller)
111- // Store controller pointer as token for later removal
112- node.hoverToken = uintptr(controller.Native())
113- if node.hoverToken == 0 {
114- log.Printf("[workspace] failed to attach hover handler")
115- }
116-}
117-
118-// detachHover removes hover handlers from a pane node
119-func (wm *WorkspaceManager) detachHover(node *paneNode) {
120- if wm == nil || node == nil || node.hoverToken == 0 {
121- return
122- }
123-
124- if node.container == nil {
125- node.hoverToken = 0
126- return
127- }
128-
129- // Note: In GTK4, controllers are automatically removed when widget is destroyed
130- // We just need to clear our token reference
131- node.hoverToken = 0
132-}
133 93
134 // detachFocus removes the GTK focus controller attached to a pane, if any.94 // detachFocus removes the GTK focus controller attached to a pane, if any.
135 func (wm *WorkspaceManager) detachFocus(node *paneNode) {95 func (wm *WorkspaceManager) detachFocus(node *paneNode) {
M internal/app/browser/workspace_pane_ops.go +3 -13
@@ -140,7 +140,7 @@ func (wm *WorkspaceManager) safelyDetachControllersBeforeReparent(node *paneNode
140140 return
141141 }
142142
143- if target.hoverToken != 0 {
143+ if target.hoverToken != nil {
144144 target.pendingHoverReattach = true
145145 wm.detachHover(target)
146146 }
@@ -319,13 +319,6 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa
319319
320320 webkit.WidgetSetHExpand(newContainer, true)
321321 webkit.WidgetSetVExpand(newContainer, true)
322- webkit.WidgetRealize(newContainer)
323-
324- // Also realize the WebView widget itself for proper popup rendering
325- webViewWidget := newPane.webView.Widget()
326- if webViewWidget != nil {
327- webkit.WidgetRealize(webViewWidget)
328- }
329322
330323 // CRITICAL: Handle stacked panes correctly - same logic as splitNode
331324 // When the target is inside a stacked pane, we need to split around the entire stack,
@@ -571,7 +564,7 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa
571564 }
572565 if candidate.pendingHoverReattach {
573566 wm.ensureHover(candidate)
574- if candidate.hoverToken != 0 {
567+ if candidate.hoverToken != nil {
575568 candidate.pendingHoverReattach = false
576569 }
577570 }
@@ -642,7 +635,6 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane
642635 }
643636 webkit.WidgetSetHExpand(newContainer, true)
644637 webkit.WidgetSetVExpand(newContainer, true)
645- webkit.WidgetRealize(newContainer)
646638
647639 // Use the determined split target container
648640 if splitTargetContainer == nil {
@@ -655,14 +647,12 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane
655647 if splitTargetContainer != nil {
656648 webkit.WidgetSetHExpand(splitTargetContainer, true)
657649 webkit.WidgetSetVExpand(splitTargetContainer, true)
658- webkit.WidgetRealize(splitTargetContainer)
659650 }
660651 } else if splitTarget.isStacked {
661652 // Stack containers need the same setup as regular panes for proper splitting
662653 if splitTargetContainer != nil {
663654 webkit.WidgetSetHExpand(splitTargetContainer, true)
664655 webkit.WidgetSetVExpand(splitTargetContainer, true)
665- webkit.WidgetRealize(splitTargetContainer)
666656 log.Printf("[workspace] configured stack wrapper for split: %p", splitTargetContainer)
667657 }
668658 }
@@ -871,7 +861,7 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane
871861 }
872862 if candidate.pendingHoverReattach {
873863 wm.ensureHover(candidate)
874- if candidate.hoverToken != 0 {
864+ if candidate.hoverToken != nil {
875865 candidate.pendingHoverReattach = false
876866 }
877867 }
@@ -140,7 +140,7 @@ func (wm *WorkspaceManager) safelyDetachControllersBeforeReparent(node *paneNode
140 return140 return
141 }141 }
142 142
143- if target.hoverToken != 0 {143+ if target.hoverToken != nil {
144 target.pendingHoverReattach = true144 target.pendingHoverReattach = true
145 wm.detachHover(target)145 wm.detachHover(target)
146 }146 }
@@ -319,13 +319,6 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa
319 319
320 webkit.WidgetSetHExpand(newContainer, true)320 webkit.WidgetSetHExpand(newContainer, true)
321 webkit.WidgetSetVExpand(newContainer, true)321 webkit.WidgetSetVExpand(newContainer, true)
322- webkit.WidgetRealize(newContainer)
323-
324- // Also realize the WebView widget itself for proper popup rendering
325- webViewWidget := newPane.webView.Widget()
326- if webViewWidget != nil {
327- webkit.WidgetRealize(webViewWidget)
328- }
329 322
330 // CRITICAL: Handle stacked panes correctly - same logic as splitNode323 // CRITICAL: Handle stacked panes correctly - same logic as splitNode
331 // When the target is inside a stacked pane, we need to split around the entire stack,324 // When the target is inside a stacked pane, we need to split around the entire stack,
@@ -571,7 +564,7 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa
571 }564 }
572 if candidate.pendingHoverReattach {565 if candidate.pendingHoverReattach {
573 wm.ensureHover(candidate)566 wm.ensureHover(candidate)
574- if candidate.hoverToken != 0 {567+ if candidate.hoverToken != nil {
575 candidate.pendingHoverReattach = false568 candidate.pendingHoverReattach = false
576 }569 }
577 }570 }
@@ -642,7 +635,6 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane
642 }635 }
643 webkit.WidgetSetHExpand(newContainer, true)636 webkit.WidgetSetHExpand(newContainer, true)
644 webkit.WidgetSetVExpand(newContainer, true)637 webkit.WidgetSetVExpand(newContainer, true)
645- webkit.WidgetRealize(newContainer)
646 638
647 // Use the determined split target container639 // Use the determined split target container
648 if splitTargetContainer == nil {640 if splitTargetContainer == nil {
@@ -655,14 +647,12 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane
655 if splitTargetContainer != nil {647 if splitTargetContainer != nil {
656 webkit.WidgetSetHExpand(splitTargetContainer, true)648 webkit.WidgetSetHExpand(splitTargetContainer, true)
657 webkit.WidgetSetVExpand(splitTargetContainer, true)649 webkit.WidgetSetVExpand(splitTargetContainer, true)
658- webkit.WidgetRealize(splitTargetContainer)
659 }650 }
660 } else if splitTarget.isStacked {651 } else if splitTarget.isStacked {
661 // Stack containers need the same setup as regular panes for proper splitting652 // Stack containers need the same setup as regular panes for proper splitting
662 if splitTargetContainer != nil {653 if splitTargetContainer != nil {
663 webkit.WidgetSetHExpand(splitTargetContainer, true)654 webkit.WidgetSetHExpand(splitTargetContainer, true)
664 webkit.WidgetSetVExpand(splitTargetContainer, true)655 webkit.WidgetSetVExpand(splitTargetContainer, true)
665- webkit.WidgetRealize(splitTargetContainer)
666 log.Printf("[workspace] configured stack wrapper for split: %p", splitTargetContainer)656 log.Printf("[workspace] configured stack wrapper for split: %p", splitTargetContainer)
667 }657 }
668 }658 }
@@ -871,7 +861,7 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane
871 }861 }
872 if candidate.pendingHoverReattach {862 if candidate.pendingHoverReattach {
873 wm.ensureHover(candidate)863 wm.ensureHover(candidate)
874- if candidate.hoverToken != 0 {864+ if candidate.hoverToken != nil {
875 candidate.pendingHoverReattach = false865 candidate.pendingHoverReattach = false
876 }866 }
877 }867 }
M pkg/webkit/widgets.go +1 -10
@@ -244,16 +244,7 @@ func getWidget(w gtk.Widgetter) *gtk.Widget {
244244 if w == nil {
245245 return nil
246246 }
247- // The Widgetter interface provides a way to get the base widget
248- // In gotk4, we can cast to get access to the underlying widget
249- if widget, ok := w.(interface{ Widget() *gtk.Widget }); ok {
250- return widget.Widget()
251- }
252- // Fallback: try to type assert directly
253- if widget, ok := w.(*gtk.Widget); ok {
254- return widget
255- }
256- return nil
247+ return gtk.BaseWidget(w)
257248 }
258249
259250 // WidgetShow makes a widget visible
@@ -244,16 +244,7 @@ func getWidget(w gtk.Widgetter) *gtk.Widget {
244 if w == nil {244 if w == nil {
245 return nil245 return nil
246 }246 }
247- // The Widgetter interface provides a way to get the base widget247+ return gtk.BaseWidget(w)
248- // In gotk4, we can cast to get access to the underlying widget
249- if widget, ok := w.(interface{ Widget() *gtk.Widget }); ok {
250- return widget.Widget()
251- }
252- // Fallback: try to type assert directly
253- if widget, ok := w.(*gtk.Widget); ok {
254- return widget
255- }
256- return nil
257 }248 }
258 249
259 // WidgetShow makes a widget visible250 // WidgetShow makes a widget visible

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close