1package component
2
3import (
4 "math"
5
6 "github.com/bnema/dumber/internal/ui/layout"
7 "github.com/bnema/puregotk/v4/gtk"
8)
9
10// ModalSizeConfig holds configuration for modal sizing calculations.
11type ModalSizeConfig struct {
12 WidthPct float64 // Percentage of parent width (e.g., 0.6)
13 MaxWidth int // Maximum width in pixels
14 TopMarginPct float64 // Top margin as percentage of parent height
15 FallbackWidth int // Fallback when parent not allocated
16 FallbackHeight int // Fallback height
17 FixedWidth int // Optional fixed width in pixels (overrides WidthPct/MaxWidth)
18 FixedTopMargin int // Optional fixed top margin in pixels
19 UseFixedTopMargin bool // When true, FixedTopMargin overrides TopMarginPct
20 UseMonitorHeight bool // When true, row limits use the monitor height (standalone window sized by its content)
21}
22
23// RowHeightDefaults holds default (unscaled) row heights for list-based modals.
24// These are base pixel values that should be multiplied by UI scale.
25type RowHeightDefaults struct {
26 Standard int // Standard list row (e.g., session row, history row)
27 Compact int // Compact row (e.g., tree row, sub-item)
28 Divider int // Divider/separator row
29}
30
31// ListDisplayDefaults holds display limits for list-based modals.
32type ListDisplayDefaults struct {
33 MaxVisibleRows int // Maximum rows visible before scrolling
34 MaxResults int // Maximum results to fetch/display
35 SmallMaxVisibleRows int // Reduced row count when rows don't fit (0 = no adaptation)
36}
37
38// Package-level defaults for row heights.
39var DefaultRowHeights = RowHeightDefaults{
40 Standard: 50,
41 Compact: 28,
42 Divider: 30,
43}
44
45// OmniboxSizeDefaults provides default sizing for omnibox modal.
46// The in-pane omnibox is vertically centered, so it has no top margin.
47var OmniboxSizeDefaults = ModalSizeConfig{
48 WidthPct: 0.8,
49 MaxWidth: 800,
50 FallbackWidth: 800,
51 FallbackHeight: 600,
52}
53
54// OmniboxListDefaults provides display limits for omnibox modal:
55// MaxResults rows are listed and navigable, MaxVisibleRows fit before scrolling.
56var OmniboxListDefaults = ListDisplayDefaults{
57 MaxVisibleRows: 5,
58 MaxResults: 10,
59 SmallMaxVisibleRows: 3,
60}
61
62// SessionManagerSizeDefaults provides default sizing for session manager modal.
63var SessionManagerSizeDefaults = ModalSizeConfig{
64 WidthPct: 0.6,
65 MaxWidth: 600,
66 TopMarginPct: 0.15,
67 FallbackWidth: 600,
68 FallbackHeight: 600,
69}
70
71// SessionManagerListDefaults provides display limits for session manager modal.
72var SessionManagerListDefaults = ListDisplayDefaults{
73 MaxVisibleRows: 6,
74 MaxResults: 50,
75}
76
77// TabPickerSizeDefaults provides default sizing for tab picker modal.
78var TabPickerSizeDefaults = ModalSizeConfig{
79 WidthPct: 0.6,
80 MaxWidth: 600,
81 TopMarginPct: 0.15,
82 FallbackWidth: 600,
83 FallbackHeight: 600,
84}
85
86// TabPickerListDefaults provides display limits for tab picker modal.
87var TabPickerListDefaults = ListDisplayDefaults{
88 MaxVisibleRows: 8,
89 MaxResults: 20,
90}
91
92// PermissionPopupSizeDefaults provides default sizing for permission popup modal.
93var PermissionPopupSizeDefaults = ModalSizeConfig{
94 WidthPct: 0.4,
95 MaxWidth: 400,
96 TopMarginPct: 0.3,
97 FallbackWidth: 400,
98 FallbackHeight: 300,
99}
100
101// EffectiveMaxRows returns the maximum number of visible rows that fit
102// in the available space. It computes available height by subtracting the
103// top margin (TopMarginPct of parentHeight) and chrome height (header tabs +
104// search entry, estimated as 2× rowHeight to scale with the UI). If
105// MaxVisibleRows worth of rows don't fit, it falls back to SmallMaxVisibleRows.
106func EffectiveMaxRows(parentHeight, rowHeight int, sizeCfg ModalSizeConfig, defaults ListDisplayDefaults) int {
107 if parentHeight <= 0 || rowHeight <= 0 || defaults.SmallMaxVisibleRows <= 0 {
108 return defaults.MaxVisibleRows
109 }
110 topMargin := int(float64(parentHeight) * sizeCfg.TopMarginPct)
111 chromeHeight := 2 * rowHeight // header tabs + search entry, scales with UI
112 available := parentHeight - topMargin - chromeHeight
113 needed := defaults.MaxVisibleRows * rowHeight
114 if available < needed {
115 return defaults.SmallMaxVisibleRows
116 }
117 return defaults.MaxVisibleRows
118}
119
120// ResolveModalSizeConfig merges a partial override with defaults.
121func ResolveModalSizeConfig(cfg, defaults ModalSizeConfig) ModalSizeConfig {
122 resolved := defaults
123
124 if cfg.WidthPct > 0 {
125 resolved.WidthPct = cfg.WidthPct
126 }
127 if cfg.MaxWidth > 0 {
128 resolved.MaxWidth = cfg.MaxWidth
129 }
130 if cfg.TopMarginPct > 0 {
131 resolved.TopMarginPct = cfg.TopMarginPct
132 }
133 if cfg.FallbackWidth > 0 {
134 resolved.FallbackWidth = cfg.FallbackWidth
135 }
136 if cfg.FallbackHeight > 0 {
137 resolved.FallbackHeight = cfg.FallbackHeight
138 }
139 if cfg.FixedWidth > 0 {
140 resolved.FixedWidth = cfg.FixedWidth
141 }
142 if cfg.UseFixedTopMargin {
143 resolved.FixedTopMargin = cfg.FixedTopMargin
144 resolved.UseFixedTopMargin = true
145 }
146 if cfg.UseMonitorHeight {
147 resolved.UseMonitorHeight = true
148 }
149
150 return resolved
151}
152
153// CalculateModalDimensions computes width and top margin based on parent overlay.
154// Returns calculated width and top margin in pixels.
155func CalculateModalDimensions(parent layout.OverlayWidget, cfg ModalSizeConfig) (width, marginTop int) {
156 return CalculateModalDimensionsWithScale(parent, cfg, 1)
157}
158
159// normalizeUIScale maps nonpositive or nonfinite scale factors to 1.
160func normalizeUIScale(uiScale float64) float64 {
161 if math.IsNaN(uiScale) || math.IsInf(uiScale, 0) || uiScale <= 0 {
162 return 1.0
163 }
164 return uiScale
165}
166
167// CalculateModalDimensionsWithScale computes width and top margin based on
168// parent overlay, scaling the requested width by the UI scale factor while
169// preserving scale=1 geometry, top placement, and fallback height.
170// Returns calculated width and top margin in pixels.
171func CalculateModalDimensionsWithScale(parent layout.OverlayWidget, cfg ModalSizeConfig, uiScale float64) (width, marginTop int) {
172 scale := normalizeUIScale(uiScale)
173
174 var parentWidth, parentHeight int
175
176 if parent != nil {
177 parentWidth = parent.GetAllocatedWidth()
178 parentHeight = parent.GetAllocatedHeight()
179 }
180
181 // Use fallback if parent not allocated or too small to be useful
182 // A width < 100 is too small for any meaningful modal
183 if parentWidth < 100 {
184 parentWidth = ScaleValue(cfg.FallbackWidth, scale)
185 }
186 if parentHeight < 100 {
187 parentHeight = cfg.FallbackHeight
188 }
189
190 if cfg.FixedWidth > 0 {
191 // Fixed widths (e.g. standalone omnibox) are content-sized requests:
192 // scale them directly without clamping to the host allocation, which
193 // is itself driven by the child and would prevent growth.
194 width = ScaleValue(cfg.FixedWidth, scale)
195 } else {
196 width = min(int(float64(parentWidth)*cfg.WidthPct), ScaleValue(cfg.MaxWidth, scale))
197 }
198
199 if cfg.UseFixedTopMargin {
200 marginTop = cfg.FixedTopMargin
201 } else {
202 marginTop = int(float64(parentHeight) * cfg.TopMarginPct)
203 }
204 return width, marginTop
205}
206
207// CalculateOverlayDimensions computes width and height from overlay allocation percentages.
208func CalculateOverlayDimensions(
209 parent layout.OverlayWidget,
210 widthPct, heightPct float64,
211 fallbackWidth, fallbackHeight int,
212) (width, height int) {
213 parentWidth := fallbackWidth
214 parentHeight := fallbackHeight
215
216 if parent != nil {
217 if allocatedWidth := parent.GetAllocatedWidth(); allocatedWidth >= 100 {
218 parentWidth = allocatedWidth
219 }
220 if allocatedHeight := parent.GetAllocatedHeight(); allocatedHeight >= 100 {
221 parentHeight = allocatedHeight
222 }
223 }
224
225 if widthPct <= 0 || widthPct > 1 {
226 widthPct = 1.0
227 }
228 if heightPct <= 0 || heightPct > 1 {
229 heightPct = 1.0
230 }
231
232 width = int(float64(parentWidth) * widthPct)
233 height = int(float64(parentHeight) * heightPct)
234 return width, height
235}
236
237// SetScrolledWindowHeight safely sets min/max content height on a ScrolledWindow.
238// Resets min to -1 first to avoid GTK assertion (min <= max) when shrinking.
239func SetScrolledWindowHeight(sw *gtk.ScrolledWindow, height int) {
240 if sw == nil {
241 return
242 }
243 sw.SetMinContentHeight(-1)
244 sw.SetMaxContentHeight(height)
245 sw.SetMinContentHeight(height)
246}
247
248// MeasureWidgetHeight returns the natural height of a widget for a given width.
249// Returns 0 if widget is nil or measurement fails.
250func MeasureWidgetHeight(widget *gtk.Widget, forWidth int) int {
251 if widget == nil {
252 return 0
253 }
254 var minH, natH int
255 widget.Measure(gtk.OrientationVerticalValue, forWidth, &minH, &natH, nil, nil)
256 return natH
257}
258
259// ScaleValue scales a base pixel value by UI scale factor.
260// Returns the base value if scale is <= 0.
261func ScaleValue(base int, uiScale float64) int {
262 if uiScale <= 0 {
263 uiScale = 1.0
264 }
265 return int(float64(base) * uiScale)
266}