v0.27.2 5960dd8

modal_sizing.go RawHistoryFolder

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}

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