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}