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