1pub const NARROW_WIDTH: f32 = 720.0;
6pub const COMPACT_WIDTH: f32 = 1000.0;
8pub const MINIMAL_WIDTH: f32 = 800.0;
10pub const TINY_WIDTH: f32 = 520.0;
12
13pub const SIDEBAR_MAX_FRAC: f32 = 0.35;
15pub const COMMIT_MAX_FRAC: f32 = 0.55;
17pub const STAGING_MAX_FRAC: f32 = 0.5;
19pub const STAGING_MAX_FRAC_SHORT: f32 = 0.4;
21pub const SHORT_HEIGHT: f32 = 640.0;
23pub const STAGING_MIN: f32 = 110.0;
25
26pub const DIFF_MIN_WIDTH: f32 = 280.0;
28pub const COMMIT_MIN_WIDTH: f32 = 160.0;
30pub const DIVIDER_W: f32 = 6.0;
32pub const DIFF_LIST_MAX_FRAC: f32 = 0.4;
34
35pub const AUTHOR_COLUMN_PX: f32 = 110.0;
37pub const TIME_COLUMN_PX: f32 = 72.0;
39pub const SHOW_AUTHOR_FROM: f32 = 380.0;
41pub const SHOW_TIME_FROM: f32 = 480.0;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum Breakpoint {
47 Narrow,
49 Medium,
51 Wide,
53}
54
55impl Breakpoint {
56 pub fn of(window_w: f32) -> Self {
57 if window_w < NARROW_WIDTH {
58 Self::Narrow
59 } else if window_w < COMPACT_WIDTH {
60 Self::Medium
61 } else {
62 Self::Wide
63 }
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq)]
69pub struct LayoutDims {
70 pub breakpoint: Breakpoint,
71 pub show_sidebar: bool,
73 pub overlay_sidebar: bool,
75 pub sidebar_width: f32,
76 pub commit_log_width: f32,
77 pub diff_width: f32,
79 pub diff_file_list_width: f32,
81 pub staging_height: f32,
82 pub staging_stacked: bool,
84 pub compact_toolbar: bool,
86 pub minimal_toolbar: bool,
88 pub tiny_toolbar: bool,
90 pub compact_vertical: bool,
92}
93
94impl LayoutDims {
95 pub fn compute(
96 window_w: f32,
97 window_h: f32,
98 sidebar_expanded: bool,
99 sidebar_w: f32,
100 commit_w: f32,
101 staging_h: f32,
102 diff_list_w: f32,
103 ) -> Self {
104 let window_w = window_w.max(0.0);
105 let window_h = window_h.max(0.0);
106 let show_sidebar = sidebar_expanded && window_w >= NARROW_WIDTH;
107 let overlay_sidebar = sidebar_expanded && !show_sidebar;
108 let sidebar_width = sidebar_w.min(window_w * SIDEBAR_MAX_FRAC).max(0.0);
109 let used = if show_sidebar {
110 sidebar_width + DIVIDER_W
111 } else {
112 0.0
113 };
114 let avail = (window_w - used).max(0.0);
115
116 let cap = (avail * COMMIT_MAX_FRAC).min(avail - DIVIDER_W - DIFF_MIN_WIDTH);
119 let floor = COMMIT_MIN_WIDTH.min(avail * 0.6);
120 let commit_log_width = commit_w.min(cap).max(floor).min(avail);
121 let diff_width = (avail - commit_log_width - DIVIDER_W).max(0.0);
122
123 let staging_frac = if window_h < SHORT_HEIGHT {
124 STAGING_MAX_FRAC_SHORT
125 } else {
126 STAGING_MAX_FRAC
127 };
128 let staging_cap = window_h * staging_frac;
129 let staging_height = staging_h
130 .min(staging_cap)
131 .max(STAGING_MIN.min(staging_cap))
132 .max(0.0);
133
134 Self {
135 breakpoint: Breakpoint::of(window_w),
136 show_sidebar,
137 overlay_sidebar,
138 sidebar_width,
139 commit_log_width,
140 diff_width,
141 diff_file_list_width: diff_list_w.min(diff_width * DIFF_LIST_MAX_FRAC).max(0.0),
142 staging_height,
143 staging_stacked: window_w < NARROW_WIDTH,
144 compact_toolbar: window_w < COMPACT_WIDTH,
145 minimal_toolbar: window_w < MINIMAL_WIDTH,
146 tiny_toolbar: window_w < TINY_WIDTH,
147 compact_vertical: window_h < SHORT_HEIGHT,
148 }
149 }
150}
151
152pub fn clamp_dialog(want: f32, window: f32) -> f32 {
154 want.min((window - 32.0).max(120.0))
155}
156
157#[derive(Debug, Clone, Copy, PartialEq)]
159pub struct CommitColumns {
160 pub ref_w: f32,
161 pub graph_w: f32,
162 pub show_author: bool,
163 pub show_time: bool,
164 pub msg_px: f32,
166}
167
168impl CommitColumns {
169 pub fn compute(commit_w: f32, ref_stored: f32, graph_stored: f32) -> Self {
170 let commit_w = commit_w.max(0.0);
171 let show_author = commit_w >= SHOW_AUTHOR_FROM;
172 let show_time = commit_w >= SHOW_TIME_FROM;
173 let ref_w = ref_stored.min(commit_w * 0.28).max(0.0);
174 let graph_w = graph_stored.min(commit_w * 0.22).max(0.0);
175 let mut fixed = ref_w + DIVIDER_W + graph_w + DIVIDER_W + 30.0;
176 if show_author {
177 fixed += AUTHOR_COLUMN_PX + 6.0;
178 }
179 if show_time {
180 fixed += TIME_COLUMN_PX + 6.0;
181 }
182 Self {
183 ref_w,
184 graph_w,
185 show_author,
186 show_time,
187 msg_px: (commit_w - fixed).max(60.0),
188 }
189 }
190}
191
192impl crate::state::GitKraft {
193 pub fn layout(&self) -> LayoutDims {
195 LayoutDims::compute(
196 self.window_width,
197 self.window_height,
198 self.sidebar_expanded,
199 self.sidebar_width,
200 self.commit_log_width,
201 self.staging_height,
202 self.diff_file_list_width,
203 )
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 #[test]
210 fn short_window_is_vertically_compact() {
211 let d = |h| LayoutDims::compute(1200.0, h, true, 250.0, 600.0, 300.0, 200.0);
212 assert!(d(SHORT_HEIGHT - 1.0).compact_vertical);
213 assert!(!d(SHORT_HEIGHT).compact_vertical);
214 }
215
216 #[test]
217 fn dialog_fits_window() {
218 assert_eq!(super::clamp_dialog(1100.0, 2000.0), 1100.0);
219 assert_eq!(super::clamp_dialog(1100.0, 600.0), 568.0);
220 assert_eq!(super::clamp_dialog(700.0, 50.0), 120.0);
221 }
222
223 use super::*;
224
225 fn dims(w: f32, h: f32, exp: bool) -> LayoutDims {
226 LayoutDims::compute(w, h, exp, 250.0, 600.0, 300.0, 180.0)
227 }
228
229 #[test]
230 fn wide_window_keeps_stored_sizes() {
231 let d = dims(1920.0, 1080.0, true);
232 assert!(d.show_sidebar);
233 assert_eq!(d.sidebar_width, 250.0);
234 assert_eq!(d.commit_log_width, 600.0);
235 assert_eq!(d.staging_height, 300.0);
236 assert_eq!(d.diff_file_list_width, 180.0);
237 assert!(!d.compact_toolbar && !d.staging_stacked);
238 }
239
240 #[test]
241 fn narrow_window_hides_sidebar() {
242 assert!(!dims(NARROW_WIDTH - 1.0, 800.0, true).show_sidebar);
243 assert!(dims(NARROW_WIDTH, 800.0, true).show_sidebar);
244 }
245
246 #[test]
247 fn narrow_expanded_sidebar_overlays() {
248 let d = dims(600.0, 800.0, true);
249 assert!(d.overlay_sidebar && !d.show_sidebar);
250 assert!(!dims(1200.0, 800.0, true).overlay_sidebar);
251 assert!(!dims(600.0, 800.0, false).overlay_sidebar);
252 }
253
254 #[test]
255 fn collapsed_sidebar_stays_hidden() {
256 assert!(!dims(1920.0, 1080.0, false).show_sidebar);
257 }
258
259 #[test]
260 fn sidebar_and_commit_clamped() {
261 let d = LayoutDims::compute(800.0, 800.0, true, 400.0, 900.0, 300.0, 180.0);
262 assert_eq!(d.sidebar_width, 280.0); assert!(d.commit_log_width <= (800.0 - d.sidebar_width) * COMMIT_MAX_FRAC);
264 }
265
266 #[test]
267 fn diff_always_keeps_minimum_width() {
268 for w in [721.0, 800.0, 1000.0, 1280.0, 1600.0] {
269 let d = dims(w, 900.0, true);
270 assert!(
271 d.diff_width >= DIFF_MIN_WIDTH - 0.01,
272 "w={w} diff={}",
273 d.diff_width
274 );
275 }
276 let d = dims(600.0, 800.0, true); assert!(d.diff_width >= DIFF_MIN_WIDTH - 0.01);
278 }
279
280 #[test]
281 fn panes_add_up_to_window() {
282 for w in [400.0, 600.0, 900.0, 1400.0] {
283 let d = dims(w, 800.0, true);
284 let sb = if d.show_sidebar {
285 d.sidebar_width + DIVIDER_W
286 } else {
287 0.0
288 };
289 let total = sb + d.commit_log_width + DIVIDER_W + d.diff_width;
290 assert!(total <= w + 0.01, "w={w} total={total}");
291 }
292 }
293
294 #[test]
295 fn diff_file_list_limited_to_share_of_diff() {
296 let d = LayoutDims::compute(800.0, 800.0, false, 250.0, 300.0, 300.0, 400.0);
297 assert!(d.diff_file_list_width <= d.diff_width * DIFF_LIST_MAX_FRAC + 0.01);
298 }
299
300 #[test]
301 fn staging_clamped_with_floor() {
302 assert_eq!(dims(1200.0, 400.0, true).staging_height, 160.0); assert_eq!(dims(1200.0, 800.0, true).staging_height, 300.0);
304 let d = LayoutDims::compute(1200.0, 1000.0, true, 250.0, 600.0, 20.0, 180.0);
305 assert_eq!(d.staging_height, STAGING_MIN);
306 }
307
308 #[test]
309 fn toolbar_modes_by_width() {
310 assert!(!dims(1200.0, 800.0, true).compact_toolbar);
311 let d = dims(900.0, 800.0, true);
312 assert!(d.compact_toolbar && !d.minimal_toolbar);
313 let d = dims(700.0, 800.0, true);
314 assert!(d.minimal_toolbar && d.staging_stacked && !d.tiny_toolbar);
315 assert!(dims(500.0, 800.0, true).tiny_toolbar);
316 }
317
318 #[test]
319 fn breakpoints() {
320 assert_eq!(Breakpoint::of(500.0), Breakpoint::Narrow);
321 assert_eq!(Breakpoint::of(900.0), Breakpoint::Medium);
322 assert_eq!(Breakpoint::of(1400.0), Breakpoint::Wide);
323 }
324
325 #[test]
326 fn tiny_window_never_negative() {
327 let d = dims(0.0, 0.0, true);
328 assert!(d.sidebar_width >= 0.0 && d.commit_log_width >= 0.0);
329 assert!(d.staging_height >= 0.0 && d.diff_width >= 0.0 && d.diff_file_list_width >= 0.0);
330 }
331
332 #[test]
333 fn commit_columns_drop_as_pane_shrinks() {
334 let wide = CommitColumns::compute(700.0, 120.0, 100.0);
335 assert!(wide.show_author && wide.show_time);
336 let mid = CommitColumns::compute(420.0, 120.0, 100.0);
337 assert!(mid.show_author && !mid.show_time);
338 let small = CommitColumns::compute(300.0, 120.0, 100.0);
339 assert!(!small.show_author && !small.show_time);
340 assert!(small.ref_w <= 300.0 * 0.28 + 0.01 && small.graph_w <= 300.0 * 0.22 + 0.01);
341 assert!(small.msg_px >= 60.0);
342 }
343
344 #[test]
345 fn commit_message_gets_room_when_wide() {
346 let c = CommitColumns::compute(900.0, 120.0, 100.0);
347 assert_eq!(c.ref_w, 120.0);
348 assert!(c.msg_px > 300.0);
349 }
350}