v0.14.0 76d1892

workspace_geometry_validator.go RawHistoryFolder

1// workspace_geometry_validator.go - Simplified geometry validation for split operations
2package browser
3
4import (
5	"fmt"
6	"log"
7
8	"github.com/bnema/dumber/pkg/webkit"
9)
10
11// GeometryValidator validates pane geometry constraints for splits and operations
12type GeometryValidator struct {
13	minPaneWidth  int
14	minPaneHeight int
15	debugMode     bool
16}
17
18// PaneGeometry represents the geometry of a pane
19type PaneGeometry struct {
20	X       int
21	Y       int
22	Width   int
23	Height  int
24	IsValid bool
25}
26
27// ValidationResult represents the result of a geometry validation
28type ValidationResult struct {
29	IsValid              bool
30	Reason               string
31	Geometry             PaneGeometry
32	RequiresRevalidation bool
33}
34
35// NewGeometryValidator creates a new geometry validator
36func NewGeometryValidator() *GeometryValidator {
37	return &GeometryValidator{
38		minPaneWidth:  300, // Minimum width for usable pane
39		minPaneHeight: 200, // Minimum height for usable pane
40		debugMode:     false,
41	}
42}
43
44// SetDebugMode enables or disables debug logging
45func (gv *GeometryValidator) SetDebugMode(debug bool) {
46	gv.debugMode = debug
47}
48
49// GetPaneGeometry extracts geometry information from a pane node
50func (gv *GeometryValidator) GetPaneGeometry(node *paneNode) PaneGeometry {
51	geometry := PaneGeometry{IsValid: false}
52
53	if node == nil || node.container == nil {
54		if gv.debugMode {
55			log.Printf("[geometry] Invalid node or missing container")
56		}
57		return geometry
58	}
59
60	// Get widget allocation directly - GTK operations are always on main thread
61	x, y, width, height := webkit.WidgetGetAllocation(node.container)
62
63	geometry.X = x
64	geometry.Y = y
65	geometry.Width = width
66	geometry.Height = height
67	geometry.IsValid = true
68
69	if gv.debugMode {
70		log.Printf("[geometry] Pane geometry: %dx%d at (%d,%d)",
71			geometry.Width, geometry.Height, geometry.X, geometry.Y)
72	}
73
74	return geometry
75}
76
77// prepareGeometryResult centralizes the common geometry validation logic used by
78// split/stack checks. It returns the computed geometry, a populated
79// ValidationResult (for early exits), and a boolean that indicates whether the
80// caller should continue with additional constraints.
81func (gv *GeometryValidator) prepareGeometryResult(node *paneNode) (PaneGeometry, ValidationResult, bool) {
82	geometry := gv.GetPaneGeometry(node)
83	result := ValidationResult{Geometry: geometry}
84
85	if !geometry.IsValid {
86		if geometry.Width == 0 && geometry.Height == 0 {
87			result.IsValid = true
88			result.Reason = "geometry pending allocation"
89			result.RequiresRevalidation = true
90		} else {
91			result.IsValid = false
92			result.Reason = "could not determine pane geometry"
93		}
94		return geometry, result, false
95	}
96
97	return geometry, result, true
98}
99
100// ValidateHorizontalSplit validates if a pane can be split horizontally
101func (gv *GeometryValidator) ValidateHorizontalSplit(node *paneNode) ValidationResult {
102	geometry, result, ok := gv.prepareGeometryResult(node)
103	if !ok {
104		return result
105	}
106
107	requiredWidth := gv.minPaneWidth * 2
108	if geometry.Width < requiredWidth {
109		result.IsValid = false
110		result.Reason = fmt.Sprintf("pane width %d < required %d for horizontal split",
111			geometry.Width, requiredWidth)
112		return result
113	}
114
115	if geometry.Height < gv.minPaneHeight {
116		result.IsValid = false
117		result.Reason = fmt.Sprintf("pane height %d < minimum %d",
118			geometry.Height, gv.minPaneHeight)
119		return result
120	}
121
122	result.IsValid = true
123	result.Reason = "horizontal split is valid"
124	return result
125}
126
127// ValidateVerticalSplit validates if a pane can be split vertically
128func (gv *GeometryValidator) ValidateVerticalSplit(node *paneNode) ValidationResult {
129	geometry, result, ok := gv.prepareGeometryResult(node)
130	if !ok {
131		return result
132	}
133
134	requiredHeight := gv.minPaneHeight * 2
135	if geometry.Height < requiredHeight {
136		result.IsValid = false
137		result.Reason = fmt.Sprintf("pane height %d < required %d for vertical split",
138			geometry.Height, requiredHeight)
139		return result
140	}
141
142	if geometry.Width < gv.minPaneWidth {
143		result.IsValid = false
144		result.Reason = fmt.Sprintf("pane width %d < minimum %d",
145			geometry.Width, gv.minPaneWidth)
146		return result
147	}
148
149	result.IsValid = true
150	result.Reason = "vertical split is valid"
151	return result
152}
153
154// ValidateSplit validates a split operation based on direction
155func (gv *GeometryValidator) ValidateSplit(node *paneNode, direction string) ValidationResult {
156	if node == nil {
157		return ValidationResult{IsValid: false, Reason: "node is nil"}
158	}
159
160	if node.isStacked {
161		return ValidationResult{IsValid: false, Reason: "cannot split stacked pane container"}
162	}
163
164	if !node.isLeaf {
165		return ValidationResult{IsValid: false, Reason: "can only split leaf panes"}
166	}
167
168	switch direction {
169	case DirectionLeft, DirectionRight:
170		return gv.ValidateHorizontalSplit(node)
171	case DirectionUp, DirectionDown:
172		return gv.ValidateVerticalSplit(node)
173	default:
174		return ValidationResult{IsValid: false, Reason: fmt.Sprintf("unknown split direction: %s", direction)}
175	}
176}
177
178// ValidateStackOperation validates if a pane can be converted to a stack
179func (gv *GeometryValidator) ValidateStackOperation(node *paneNode) ValidationResult {
180	geometry, result, ok := gv.prepareGeometryResult(node)
181	if !ok {
182		return result
183	}
184
185	if node.isStacked {
186		result.IsValid = false
187		result.Reason = "pane is already stacked"
188		return result
189	}
190
191	minStackHeight := gv.minPaneHeight + 30 // Extra space for title bar
192	if geometry.Height < minStackHeight {
193		result.IsValid = false
194		result.Reason = fmt.Sprintf("pane height %d < required %d for stacking",
195			geometry.Height, minStackHeight)
196		return result
197	}
198
199	if geometry.Width < gv.minPaneWidth {
200		result.IsValid = false
201		result.Reason = fmt.Sprintf("pane width %d < minimum %d",
202			geometry.Width, gv.minPaneWidth)
203		return result
204	}
205
206	result.IsValid = true
207	result.Reason = "stack operation is valid"
208	return result
209}

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