detached 99570ca

refactor(browser): migrate workspace_navigation.go to gotk4 API

commit
99570ca51e12d4df75d0cf2d5848a823c8e0100c
author
bamen · · 2025-10-09 05:36 UTC
parent
44a6914
signature
unsigned
1 file · +39 -36
M internal/app/browser/workspace_navigation.go +39 -36
@@ -9,6 +9,7 @@ import (
99 "time"
1010
1111 "github.com/bnema/dumber/pkg/webkit"
12+ "github.com/diamondburned/gotk4/pkg/gtk/v4"
1213 )
1314
1415 const (
@@ -93,18 +94,23 @@ func (wm *WorkspaceManager) ensureHover(node *paneNode) {
9394 if wm == nil || node == nil || !node.isLeaf {
9495 return
9596 }
96- if node.container == 0 || node.hoverToken != 0 {
97+ if node.container == nil || node.hoverToken != 0 {
9798 return
9899 }
99100
100- token := webkit.WidgetAddHoverHandler(node.container, func() {
101+ // Create motion controller for hover detection
102+ controller := gtk.NewEventControllerMotion()
103+ controller.ConnectEnter(func(x, y float64) {
101104 if wm == nil {
102105 return
103106 }
104107 wm.SetActivePane(node, SourceMouse)
105108 })
106- node.hoverToken = token
107- if token == 0 {
109+
110+ node.container.AddController(controller)
111+ // Store controller pointer as token for later removal
112+ node.hoverToken = uintptr(controller.Native())
113+ if node.hoverToken == 0 {
108114 log.Printf("[workspace] failed to attach hover handler")
109115 }
110116 }
@@ -115,17 +121,14 @@ func (wm *WorkspaceManager) detachHover(node *paneNode) {
115121 return
116122 }
117123
118- if node.container == 0 || !webkit.WidgetIsValid(node.container) {
124+ if node.container == nil {
119125 node.hoverToken = 0
120126 return
121127 }
122128
123- token := node.hoverToken
129+ // Note: In GTK4, controllers are automatically removed when widget is destroyed
130+ // We just need to clear our token reference
124131 node.hoverToken = 0
125-
126- if webkit.WidgetIsValid(node.container) {
127- webkit.WidgetRemoveHoverHandler(node.container, token)
128- }
129132 }
130133
131134 // detachFocus removes the GTK focus controller attached to a pane, if any.
@@ -240,7 +243,7 @@ func (wm *WorkspaceManager) navigateStack(direction string) bool {
240243 // focusAdjacent uses geometry-based calculations to find and focus adjacent panes
241244 func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
242245 currentFocused := wm.GetActiveNode()
243- if currentFocused == nil || !currentFocused.isLeaf || currentFocused.container == 0 {
246+ if currentFocused == nil || !currentFocused.isLeaf || currentFocused.container == nil {
244247 return false
245248 }
246249
@@ -249,14 +252,14 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
249252 return true
250253 }
251254
252- currentBounds, ok := webkit.WidgetGetBounds(currentFocused.container)
253- if !ok {
255+ currentBounds := currentFocused.container.Allocation()
256+ if currentBounds == nil {
254257 log.Printf("[workspace] unable to compute bounds for active pane")
255258 return false
256259 }
257260
258- cx := currentBounds.X + currentBounds.Width/2.0
259- cy := currentBounds.Y + currentBounds.Height/2.0
261+ cx := float64(currentBounds.X()) + float64(currentBounds.Width())/2.0
262+ cy := float64(currentBounds.Y()) + float64(currentBounds.Height())/2.0
260263
261264 leaves := wm.collectLeaves()
262265 bestScore := math.MaxFloat64
@@ -264,16 +267,16 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
264267 var debugCandidates []string
265268
266269 for _, candidate := range leaves {
267- if candidate == nil || candidate == currentFocused || candidate.container == 0 {
270+ if candidate == nil || candidate == currentFocused || candidate.container == nil {
268271 continue
269272 }
270273
271- bounds, ok := webkit.WidgetGetBounds(candidate.container)
272- if !ok {
274+ bounds := candidate.container.Allocation()
275+ if bounds == nil {
273276 continue
274277 }
275- tx := bounds.X + bounds.Width/2.0
276- ty := bounds.Y + bounds.Height/2.0
278+ tx := float64(bounds.X()) + float64(bounds.Width())/2.0
279+ ty := float64(bounds.Y()) + float64(bounds.Height())/2.0
277280
278281 dx := tx - cx
279282 dy := ty - cy
@@ -303,7 +306,7 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
303306 }
304307
305308 if direction == "up" || direction == "down" {
306- debugCandidates = append(debugCandidates, fmt.Sprintf("cand=%#x dx=%.2f dy=%.2f score=%.2f", candidate.container, dx, dy, score))
309+ debugCandidates = append(debugCandidates, fmt.Sprintf("cand=%p dx=%.2f dy=%.2f score=%.2f", candidate.container, dx, dy, score))
307310 }
308311
309312 if score < bestScore {
@@ -318,47 +321,47 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
318321 }
319322
320323 if len(debugCandidates) > 0 {
321- log.Printf("[workspace] focusAdjacent no candidate direction=%s current=%#x candidates=%s", direction, currentFocused.container, strings.Join(debugCandidates, "; "))
324+ log.Printf("[workspace] focusAdjacent no candidate direction=%s current=%p candidates=%s", direction, currentFocused.container, strings.Join(debugCandidates, "; "))
322325 }
323326 return false
324327 }
325328
326329 // structuralNeighbor finds neighbors based on the tree structure rather than geometry
327330 func (wm *WorkspaceManager) structuralNeighbor(node *paneNode, direction string) *paneNode {
328- if node == nil || node.container == 0 {
331+ if node == nil || node.container == nil {
329332 return nil
330333 }
331334
332- refBounds, ok := webkit.WidgetGetBounds(node.container)
333- if !ok {
335+ refBounds := node.container.Allocation()
336+ if refBounds == nil {
334337 return nil
335338 }
336- cx := refBounds.X + refBounds.Width/2.0
337- cy := refBounds.Y + refBounds.Height/2.0
339+ cx := float64(refBounds.X()) + float64(refBounds.Width())/2.0
340+ cy := float64(refBounds.Y()) + float64(refBounds.Height())/2.0
338341 axisVertical := direction == "up" || direction == "down"
339342
340343 for parent := node.parent; parent != nil; parent = parent.parent {
341344 switch direction {
342345 case "up":
343- if axisVertical && parent.orientation == webkit.OrientationVertical && parent.right == node {
346+ if axisVertical && parent.orientation == gtk.OrientationVertical && parent.right == node {
344347 if leaf := wm.closestLeafFromSubtree(parent.left, cx, cy, direction); leaf != nil {
345348 return leaf
346349 }
347350 }
348351 case "down":
349- if axisVertical && parent.orientation == webkit.OrientationVertical && parent.left == node {
352+ if axisVertical && parent.orientation == gtk.OrientationVertical && parent.left == node {
350353 if leaf := wm.closestLeafFromSubtree(parent.right, cx, cy, direction); leaf != nil {
351354 return leaf
352355 }
353356 }
354357 case "left":
355- if !axisVertical && parent.orientation == webkit.OrientationHorizontal && parent.right == node {
358+ if !axisVertical && parent.orientation == gtk.OrientationHorizontal && parent.right == node {
356359 if leaf := wm.closestLeafFromSubtree(parent.left, cx, cy, direction); leaf != nil {
357360 return leaf
358361 }
359362 }
360363 case "right":
361- if !axisVertical && parent.orientation == webkit.OrientationHorizontal && parent.left == node {
364+ if !axisVertical && parent.orientation == gtk.OrientationHorizontal && parent.left == node {
362365 if leaf := wm.closestLeafFromSubtree(parent.right, cx, cy, direction); leaf != nil {
363366 return leaf
364367 }
@@ -375,16 +378,16 @@ func (wm *WorkspaceManager) closestLeafFromSubtree(node *paneNode, cx, cy float6
375378 bestScore := math.MaxFloat64
376379 var best *paneNode
377380 for _, leaf := range leaves {
378- if leaf == nil || leaf.container == 0 {
381+ if leaf == nil || leaf.container == nil {
379382 continue
380383 }
381384
382- bounds, ok := webkit.WidgetGetBounds(leaf.container)
383- if !ok {
385+ bounds := leaf.container.Allocation()
386+ if bounds == nil {
384387 continue
385388 }
386- tx := bounds.X + bounds.Width/2.0
387- ty := bounds.Y + bounds.Height/2.0
389+ tx := float64(bounds.X()) + float64(bounds.Width())/2.0
390+ ty := float64(bounds.Y()) + float64(bounds.Height())/2.0
388391 dx := tx - cx
389392 dy := ty - cy
390393 var score float64
@@ -9,6 +9,7 @@ import (
9 "time"9 "time"
10 10
11 "github.com/bnema/dumber/pkg/webkit"11 "github.com/bnema/dumber/pkg/webkit"
12+ "github.com/diamondburned/gotk4/pkg/gtk/v4"
12 )13 )
13 14
14 const (15 const (
@@ -93,18 +94,23 @@ func (wm *WorkspaceManager) ensureHover(node *paneNode) {
93 if wm == nil || node == nil || !node.isLeaf {94 if wm == nil || node == nil || !node.isLeaf {
94 return95 return
95 }96 }
96- if node.container == 0 || node.hoverToken != 0 {97+ if node.container == nil || node.hoverToken != 0 {
97 return98 return
98 }99 }
99 100
100- token := webkit.WidgetAddHoverHandler(node.container, func() {101+ // Create motion controller for hover detection
102+ controller := gtk.NewEventControllerMotion()
103+ controller.ConnectEnter(func(x, y float64) {
101 if wm == nil {104 if wm == nil {
102 return105 return
103 }106 }
104 wm.SetActivePane(node, SourceMouse)107 wm.SetActivePane(node, SourceMouse)
105 })108 })
106- node.hoverToken = token109+
107- if token == 0 {110+ node.container.AddController(controller)
111+ // Store controller pointer as token for later removal
112+ node.hoverToken = uintptr(controller.Native())
113+ if node.hoverToken == 0 {
108 log.Printf("[workspace] failed to attach hover handler")114 log.Printf("[workspace] failed to attach hover handler")
109 }115 }
110 }116 }
@@ -115,17 +121,14 @@ func (wm *WorkspaceManager) detachHover(node *paneNode) {
115 return121 return
116 }122 }
117 123
118- if node.container == 0 || !webkit.WidgetIsValid(node.container) {124+ if node.container == nil {
119 node.hoverToken = 0125 node.hoverToken = 0
120 return126 return
121 }127 }
122 128
123- token := node.hoverToken129+ // Note: In GTK4, controllers are automatically removed when widget is destroyed
130+ // We just need to clear our token reference
124 node.hoverToken = 0131 node.hoverToken = 0
125-
126- if webkit.WidgetIsValid(node.container) {
127- webkit.WidgetRemoveHoverHandler(node.container, token)
128- }
129 }132 }
130 133
131 // detachFocus removes the GTK focus controller attached to a pane, if any.134 // detachFocus removes the GTK focus controller attached to a pane, if any.
@@ -240,7 +243,7 @@ func (wm *WorkspaceManager) navigateStack(direction string) bool {
240 // focusAdjacent uses geometry-based calculations to find and focus adjacent panes243 // focusAdjacent uses geometry-based calculations to find and focus adjacent panes
241 func (wm *WorkspaceManager) focusAdjacent(direction string) bool {244 func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
242 currentFocused := wm.GetActiveNode()245 currentFocused := wm.GetActiveNode()
243- if currentFocused == nil || !currentFocused.isLeaf || currentFocused.container == 0 {246+ if currentFocused == nil || !currentFocused.isLeaf || currentFocused.container == nil {
244 return false247 return false
245 }248 }
246 249
@@ -249,14 +252,14 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
249 return true252 return true
250 }253 }
251 254
252- currentBounds, ok := webkit.WidgetGetBounds(currentFocused.container)255+ currentBounds := currentFocused.container.Allocation()
253- if !ok {256+ if currentBounds == nil {
254 log.Printf("[workspace] unable to compute bounds for active pane")257 log.Printf("[workspace] unable to compute bounds for active pane")
255 return false258 return false
256 }259 }
257 260
258- cx := currentBounds.X + currentBounds.Width/2.0261+ cx := float64(currentBounds.X()) + float64(currentBounds.Width())/2.0
259- cy := currentBounds.Y + currentBounds.Height/2.0262+ cy := float64(currentBounds.Y()) + float64(currentBounds.Height())/2.0
260 263
261 leaves := wm.collectLeaves()264 leaves := wm.collectLeaves()
262 bestScore := math.MaxFloat64265 bestScore := math.MaxFloat64
@@ -264,16 +267,16 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
264 var debugCandidates []string267 var debugCandidates []string
265 268
266 for _, candidate := range leaves {269 for _, candidate := range leaves {
267- if candidate == nil || candidate == currentFocused || candidate.container == 0 {270+ if candidate == nil || candidate == currentFocused || candidate.container == nil {
268 continue271 continue
269 }272 }
270 273
271- bounds, ok := webkit.WidgetGetBounds(candidate.container)274+ bounds := candidate.container.Allocation()
272- if !ok {275+ if bounds == nil {
273 continue276 continue
274 }277 }
275- tx := bounds.X + bounds.Width/2.0278+ tx := float64(bounds.X()) + float64(bounds.Width())/2.0
276- ty := bounds.Y + bounds.Height/2.0279+ ty := float64(bounds.Y()) + float64(bounds.Height())/2.0
277 280
278 dx := tx - cx281 dx := tx - cx
279 dy := ty - cy282 dy := ty - cy
@@ -303,7 +306,7 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
303 }306 }
304 307
305 if direction == "up" || direction == "down" {308 if direction == "up" || direction == "down" {
306- debugCandidates = append(debugCandidates, fmt.Sprintf("cand=%#x dx=%.2f dy=%.2f score=%.2f", candidate.container, dx, dy, score))309+ debugCandidates = append(debugCandidates, fmt.Sprintf("cand=%p dx=%.2f dy=%.2f score=%.2f", candidate.container, dx, dy, score))
307 }310 }
308 311
309 if score < bestScore {312 if score < bestScore {
@@ -318,47 +321,47 @@ func (wm *WorkspaceManager) focusAdjacent(direction string) bool {
318 }321 }
319 322
320 if len(debugCandidates) > 0 {323 if len(debugCandidates) > 0 {
321- log.Printf("[workspace] focusAdjacent no candidate direction=%s current=%#x candidates=%s", direction, currentFocused.container, strings.Join(debugCandidates, "; "))324+ log.Printf("[workspace] focusAdjacent no candidate direction=%s current=%p candidates=%s", direction, currentFocused.container, strings.Join(debugCandidates, "; "))
322 }325 }
323 return false326 return false
324 }327 }
325 328
326 // structuralNeighbor finds neighbors based on the tree structure rather than geometry329 // structuralNeighbor finds neighbors based on the tree structure rather than geometry
327 func (wm *WorkspaceManager) structuralNeighbor(node *paneNode, direction string) *paneNode {330 func (wm *WorkspaceManager) structuralNeighbor(node *paneNode, direction string) *paneNode {
328- if node == nil || node.container == 0 {331+ if node == nil || node.container == nil {
329 return nil332 return nil
330 }333 }
331 334
332- refBounds, ok := webkit.WidgetGetBounds(node.container)335+ refBounds := node.container.Allocation()
333- if !ok {336+ if refBounds == nil {
334 return nil337 return nil
335 }338 }
336- cx := refBounds.X + refBounds.Width/2.0339+ cx := float64(refBounds.X()) + float64(refBounds.Width())/2.0
337- cy := refBounds.Y + refBounds.Height/2.0340+ cy := float64(refBounds.Y()) + float64(refBounds.Height())/2.0
338 axisVertical := direction == "up" || direction == "down"341 axisVertical := direction == "up" || direction == "down"
339 342
340 for parent := node.parent; parent != nil; parent = parent.parent {343 for parent := node.parent; parent != nil; parent = parent.parent {
341 switch direction {344 switch direction {
342 case "up":345 case "up":
343- if axisVertical && parent.orientation == webkit.OrientationVertical && parent.right == node {346+ if axisVertical && parent.orientation == gtk.OrientationVertical && parent.right == node {
344 if leaf := wm.closestLeafFromSubtree(parent.left, cx, cy, direction); leaf != nil {347 if leaf := wm.closestLeafFromSubtree(parent.left, cx, cy, direction); leaf != nil {
345 return leaf348 return leaf
346 }349 }
347 }350 }
348 case "down":351 case "down":
349- if axisVertical && parent.orientation == webkit.OrientationVertical && parent.left == node {352+ if axisVertical && parent.orientation == gtk.OrientationVertical && parent.left == node {
350 if leaf := wm.closestLeafFromSubtree(parent.right, cx, cy, direction); leaf != nil {353 if leaf := wm.closestLeafFromSubtree(parent.right, cx, cy, direction); leaf != nil {
351 return leaf354 return leaf
352 }355 }
353 }356 }
354 case "left":357 case "left":
355- if !axisVertical && parent.orientation == webkit.OrientationHorizontal && parent.right == node {358+ if !axisVertical && parent.orientation == gtk.OrientationHorizontal && parent.right == node {
356 if leaf := wm.closestLeafFromSubtree(parent.left, cx, cy, direction); leaf != nil {359 if leaf := wm.closestLeafFromSubtree(parent.left, cx, cy, direction); leaf != nil {
357 return leaf360 return leaf
358 }361 }
359 }362 }
360 case "right":363 case "right":
361- if !axisVertical && parent.orientation == webkit.OrientationHorizontal && parent.left == node {364+ if !axisVertical && parent.orientation == gtk.OrientationHorizontal && parent.left == node {
362 if leaf := wm.closestLeafFromSubtree(parent.right, cx, cy, direction); leaf != nil {365 if leaf := wm.closestLeafFromSubtree(parent.right, cx, cy, direction); leaf != nil {
363 return leaf366 return leaf
364 }367 }
@@ -375,16 +378,16 @@ func (wm *WorkspaceManager) closestLeafFromSubtree(node *paneNode, cx, cy float6
375 bestScore := math.MaxFloat64378 bestScore := math.MaxFloat64
376 var best *paneNode379 var best *paneNode
377 for _, leaf := range leaves {380 for _, leaf := range leaves {
378- if leaf == nil || leaf.container == 0 {381+ if leaf == nil || leaf.container == nil {
379 continue382 continue
380 }383 }
381 384
382- bounds, ok := webkit.WidgetGetBounds(leaf.container)385+ bounds := leaf.container.Allocation()
383- if !ok {386+ if bounds == nil {
384 continue387 continue
385 }388 }
386- tx := bounds.X + bounds.Width/2.0389+ tx := float64(bounds.X()) + float64(bounds.Width())/2.0
387- ty := bounds.Y + bounds.Height/2.0390+ ty := float64(bounds.Y()) + float64(bounds.Height())/2.0
388 dx := tx - cx391 dx := tx - cx
389 dy := ty - cy392 dy := ty - cy
390 var score float64393 var score float64

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