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}
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}

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