v0.30.0 eb3068f

modal_sizing.go RawHistoryFolder

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 = int(float64(parentWidth) * cfg.WidthPct)
169		if width > cfg.MaxWidth {
170			width = cfg.MaxWidth
171		}
172	}
173
174	if cfg.UseFixedTopMargin {
175		marginTop = cfg.FixedTopMargin
176	} else {
177		marginTop = int(float64(parentHeight) * cfg.TopMarginPct)
178	}
179	return width, marginTop
180}
181
182// CalculateOverlayDimensions computes width and height from overlay allocation percentages.
183func CalculateOverlayDimensions(
184	parent layout.OverlayWidget,
185	widthPct, heightPct float64,
186	fallbackWidth, fallbackHeight int,
187) (width, height int) {
188	parentWidth := fallbackWidth
189	parentHeight := fallbackHeight
190
191	if parent != nil {
192		if allocatedWidth := parent.GetAllocatedWidth(); allocatedWidth >= 100 {
193			parentWidth = allocatedWidth
194		}
195		if allocatedHeight := parent.GetAllocatedHeight(); allocatedHeight >= 100 {
196			parentHeight = allocatedHeight
197		}
198	}
199
200	if widthPct <= 0 || widthPct > 1 {
201		widthPct = 1.0
202	}
203	if heightPct <= 0 || heightPct > 1 {
204		heightPct = 1.0
205	}
206
207	width = int(float64(parentWidth) * widthPct)
208	height = int(float64(parentHeight) * heightPct)
209	return width, height
210}
211
212// SetScrolledWindowHeight safely sets min/max content height on a ScrolledWindow.
213// Resets min to -1 first to avoid GTK assertion (min <= max) when shrinking.
214func SetScrolledWindowHeight(sw *gtk.ScrolledWindow, height int) {
215	if sw == nil {
216		return
217	}
218	sw.SetMinContentHeight(-1)
219	sw.SetMaxContentHeight(height)
220	sw.SetMinContentHeight(height)
221}
222
223// MeasureWidgetHeight returns the natural height of a widget for a given width.
224// Returns 0 if widget is nil or measurement fails.
225func MeasureWidgetHeight(widget *gtk.Widget, forWidth int) int {
226	if widget == nil {
227		return 0
228	}
229	var minH, natH int
230	widget.Measure(gtk.OrientationVerticalValue, forWidth, &minH, &natH, nil, nil)
231	return natH
232}
233
234// ScaleValue scales a base pixel value by UI scale factor.
235// Returns the base value if scale is <= 0.
236func ScaleValue(base int, uiScale float64) int {
237	if uiScale <= 0 {
238		uiScale = 1.0
239	}
240	return int(float64(base) * uiScale)
241}

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