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