main 5041353

modal_sizing.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close