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