v0.15.0 9006957

workspace_geometry_validator.go RawHistoryFolder

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

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