Skip to main content

journey/widgets/
layout.rs

1//! Pure geometry for journey's two screens.
2//!
3//! The generic [`Shell`](crate::widgets::Shell) places each child by calling
4//! one of these functions with the container bounds, so the browse (gitk) and
5//! commit (git-gui) layouts are simply two sets of rectangles.
6
7use saudade::Rect;
8
9// ---- browse (gitk) layout -------------------------------------------------
10
11/// Height of the menu bar (shared by both screens).
12pub const MENU_H: i32 = 20;
13/// Height of the search toolbar below the menu.
14pub const TOOLBAR_H: i32 = 26;
15/// Width of the changed-files pane on the lower left.
16pub const FILES_W: i32 = 300;
17/// Fraction of the content height given to the history pane.
18pub const HISTORY_FRAC: f32 = 0.46;
19/// Slight breathing room around — and between — the three browse panes
20/// (history, files, diff), so they don't run into each other or the window
21/// edge.
22pub const BROWSE_PAD: i32 = 4;
23/// At or below this logical window width the left pane (the browse files list
24/// and the commit file lists) is capped at a third of the width, so the diff
25/// and the other right-hand panes keep the lion's share of a cramped window.
26pub const NARROW_W: i32 = 800;
27
28/// A third of the window width — the cap applied to the left pane on narrow
29/// windows. `None` above [`NARROW_W`], where the fixed widths stand.
30fn narrow_left_cap(b: Rect) -> Option<i32> {
31    (b.w <= NARROW_W).then_some(b.w / 3)
32}
33
34pub fn browse_menu(b: Rect) -> Rect {
35    Rect::new(b.x, b.y, b.w, MENU_H)
36}
37
38pub fn browse_toolbar(b: Rect) -> Rect {
39    Rect::new(b.x, b.y + MENU_H, b.w, TOOLBAR_H)
40}
41
42/// Top of the padded three-pane content area, below the menu and toolbar.
43fn content_y(b: Rect) -> i32 {
44    b.y + MENU_H + TOOLBAR_H + BROWSE_PAD
45}
46
47/// Height available to the three panes, after top and bottom padding.
48fn content_h(b: Rect) -> i32 {
49    (b.h - MENU_H - TOOLBAR_H - 2 * BROWSE_PAD).max(0)
50}
51
52/// Height of the history pane: a fraction of the content area, leaving a gap
53/// above the lower (files + diff) band.
54fn history_h(b: Rect) -> i32 {
55    let avail = (content_h(b) - BROWSE_PAD).max(0);
56    (avail as f32 * HISTORY_FRAC).round() as i32
57}
58
59pub fn browse_history(b: Rect) -> Rect {
60    Rect::new(
61        b.x + BROWSE_PAD,
62        content_y(b),
63        (b.w - 2 * BROWSE_PAD).max(0),
64        history_h(b),
65    )
66}
67
68pub fn browse_files(b: Rect) -> Rect {
69    let (lower_y, lower_h) = lower_band(b);
70    Rect::new(b.x + BROWSE_PAD, lower_y, clamp_files_w(b), lower_h)
71}
72
73pub fn browse_diff(b: Rect) -> Rect {
74    let (lower_y, lower_h) = lower_band(b);
75    let files_w = clamp_files_w(b);
76    let diff_x = b.x + 2 * BROWSE_PAD + files_w;
77    let diff_w = (b.w - files_w - 3 * BROWSE_PAD).max(0);
78    Rect::new(diff_x, lower_y, diff_w, lower_h)
79}
80
81/// The (top, height) of the lower band holding the files and diff panes, below
82/// the history pane and the gap under it.
83fn lower_band(b: Rect) -> (i32, i32) {
84    let lower_y = content_y(b) + history_h(b) + BROWSE_PAD;
85    let lower_h = (content_h(b) - history_h(b) - BROWSE_PAD).max(0);
86    (lower_y, lower_h)
87}
88
89/// Width of the files pane, clamped so the diff pane keeps a usable minimum
90/// once the inter-pane gap is accounted for, and capped to a third of the
91/// window on narrow layouts (see [`narrow_left_cap`]).
92fn clamp_files_w(b: Rect) -> i32 {
93    let w = FILES_W.min((b.w - 3 * BROWSE_PAD - 80).max(0));
94    match narrow_left_cap(b) {
95        Some(cap) => w.min(cap),
96        None => w,
97    }
98}
99
100// ---- review (branch review) layout ------------------------------------------
101//
102// The same shape as the browse screen minus the search toolbar: the branch
103// list takes the upper band, the aggregated file list and the diff share the
104// lower one.
105
106pub fn review_menu(b: Rect) -> Rect {
107    browse_menu(b)
108}
109
110/// Top of the padded review content area, directly below the menu.
111fn review_content_y(b: Rect) -> i32 {
112    b.y + MENU_H + BROWSE_PAD
113}
114
115/// Height available to the review panes, after top and bottom padding.
116fn review_content_h(b: Rect) -> i32 {
117    (b.h - MENU_H - 2 * BROWSE_PAD).max(0)
118}
119
120/// Height of the branch list: the same fraction of the content area the
121/// browse history pane gets.
122fn review_branches_h(b: Rect) -> i32 {
123    let avail = (review_content_h(b) - BROWSE_PAD).max(0);
124    (avail as f32 * HISTORY_FRAC).round() as i32
125}
126
127pub fn review_branches(b: Rect) -> Rect {
128    Rect::new(
129        b.x + BROWSE_PAD,
130        review_content_y(b),
131        (b.w - 2 * BROWSE_PAD).max(0),
132        review_branches_h(b),
133    )
134}
135
136/// The (top, height) of the lower band holding the file and diff panes.
137fn review_lower_band(b: Rect) -> (i32, i32) {
138    let lower_y = review_content_y(b) + review_branches_h(b) + BROWSE_PAD;
139    let lower_h = (review_content_h(b) - review_branches_h(b) - BROWSE_PAD).max(0);
140    (lower_y, lower_h)
141}
142
143pub fn review_files(b: Rect) -> Rect {
144    let (lower_y, lower_h) = review_lower_band(b);
145    Rect::new(b.x + BROWSE_PAD, lower_y, clamp_files_w(b), lower_h)
146}
147
148pub fn review_diff(b: Rect) -> Rect {
149    let (lower_y, lower_h) = review_lower_band(b);
150    let files_w = clamp_files_w(b);
151    let diff_x = b.x + 2 * BROWSE_PAD + files_w;
152    let diff_w = (b.w - files_w - 3 * BROWSE_PAD).max(0);
153    Rect::new(diff_x, lower_y, diff_w, lower_h)
154}
155
156// ---- commit (git-gui) layout ----------------------------------------------
157//
158// Both columns share one vertical grid: a top section (heading over a pane) and
159// a bottom section of the same shape, above a button band. So the left lists
160// line up row-for-row with the right diff and editor — "Unstaged Changes" with
161// "Diff", "Staged Changes" with "Commit Message" — and the action buttons share
162// one baseline.
163
164const LEFT_W: i32 = 320;
165/// Margin between the panes and the window edges.
166const PAD: i32 = 6;
167/// Space between the two columns, split evenly across the `LEFT_W` divider.
168/// Narrower than the outer `PAD` so the lists and the diff sit close together.
169const GUTTER: i32 = 6;
170const HEADING_H: i32 = 18;
171/// Height of the bottom band reserved for the action buttons on both columns.
172const BTN_BAND_H: i32 = 34;
173const BTN_GAP: i32 = 4;
174const LEFT_BTN_H: i32 = 24;
175const AMEND_H: i32 = 24;
176const COMMIT_BTN_H: i32 = 26;
177
178pub fn commit_menu(b: Rect) -> Rect {
179    Rect::new(b.x, b.y, b.w, MENU_H)
180}
181
182fn content_top(b: Rect) -> i32 {
183    b.y + MENU_H
184}
185fn content_height(b: Rect) -> i32 {
186    (b.h - MENU_H).max(0)
187}
188
189// Shared vertical grid -------------------------------------------------------
190
191/// Combined height of the two stacked sections, above the button band.
192fn sections_h(b: Rect) -> i32 {
193    (content_height(b) - BTN_BAND_H).max(0)
194}
195/// Height of a single section (heading + pane).
196fn section_h(b: Rect) -> i32 {
197    sections_h(b) / 2
198}
199fn top_label_y(b: Rect) -> i32 {
200    content_top(b) + 2
201}
202fn top_pane_y(b: Rect) -> i32 {
203    top_label_y(b) + HEADING_H
204}
205fn top_pane_h(b: Rect) -> i32 {
206    (section_h(b) - HEADING_H - 4).max(0)
207}
208fn bottom_label_y(b: Rect) -> i32 {
209    content_top(b) + section_h(b) + 2
210}
211fn bottom_pane_y(b: Rect) -> i32 {
212    bottom_label_y(b) + HEADING_H
213}
214fn bottom_pane_h(b: Rect) -> i32 {
215    (sections_h(b) - section_h(b) - HEADING_H - 4).max(0)
216}
217/// Top of a button of height `bh`, vertically centered in the bottom band so
218/// the left and right rows line up regardless of each button's height.
219fn btn_y(b: Rect, bh: i32) -> i32 {
220    content_top(b) + sections_h(b) + ((BTN_BAND_H - bh) / 2).max(0)
221}
222
223// Left column (unstaged / staged file lists) ---------------------------------
224
225fn left_x(b: Rect) -> i32 {
226    b.x + PAD
227}
228/// Width of the left column (the file lists). Fixed at [`LEFT_W`], but capped
229/// to a third of the window on narrow layouts (see [`narrow_left_cap`]) so the
230/// diff and message editor keep the majority of the space.
231fn left_col_w(b: Rect) -> i32 {
232    match narrow_left_cap(b) {
233        Some(cap) => LEFT_W.min(cap),
234        None => LEFT_W,
235    }
236}
237fn left_w(b: Rect) -> i32 {
238    (left_col_w(b) - PAD - GUTTER / 2).max(0)
239}
240
241pub fn commit_unstaged_label(b: Rect) -> Rect {
242    Rect::new(left_x(b), top_label_y(b), left_w(b), HEADING_H)
243}
244
245pub fn commit_unstaged_list(b: Rect) -> Rect {
246    Rect::new(left_x(b), top_pane_y(b), left_w(b), top_pane_h(b))
247}
248
249pub fn commit_staged_label(b: Rect) -> Rect {
250    Rect::new(left_x(b), bottom_label_y(b), left_w(b), HEADING_H)
251}
252
253pub fn commit_staged_list(b: Rect) -> Rect {
254    Rect::new(left_x(b), bottom_pane_y(b), left_w(b), bottom_pane_h(b))
255}
256
257/// Width of each of the three left-column action buttons: they split the column
258/// width evenly (minus the two gaps) so the row spans the full list width above
259/// it at any window size.
260fn left_btn_w(b: Rect) -> i32 {
261    ((left_w(b) - 2 * BTN_GAP) / 3).max(0)
262}
263
264/// Left edge of the left-column button in `slot` (0..3), packed left-to-right.
265fn left_btn_x(b: Rect, slot: i32) -> i32 {
266    left_x(b) + slot * (left_btn_w(b) + BTN_GAP)
267}
268
269pub fn commit_stage_btn(b: Rect) -> Rect {
270    Rect::new(
271        left_btn_x(b, 0),
272        btn_y(b, LEFT_BTN_H),
273        left_btn_w(b),
274        LEFT_BTN_H,
275    )
276}
277
278pub fn commit_unstage_btn(b: Rect) -> Rect {
279    Rect::new(
280        left_btn_x(b, 1),
281        btn_y(b, LEFT_BTN_H),
282        left_btn_w(b),
283        LEFT_BTN_H,
284    )
285}
286
287pub fn commit_rescan_btn(b: Rect) -> Rect {
288    Rect::new(
289        left_btn_x(b, 2),
290        btn_y(b, LEFT_BTN_H),
291        left_btn_w(b),
292        LEFT_BTN_H,
293    )
294}
295
296// Right column (diff view / commit message editor) ---------------------------
297
298/// Left edge of the right column: a half-gutter past the divider, which moves
299/// in with the left column on narrow layouts.
300fn right_inner_x(b: Rect) -> i32 {
301    b.x + left_col_w(b) + GUTTER / 2
302}
303/// Width of the right column: from `right_inner_x` to a `PAD` window margin.
304fn right_inner_w(b: Rect) -> i32 {
305    (b.x + b.w - PAD - right_inner_x(b)).max(0)
306}
307
308pub fn commit_diff_label(b: Rect) -> Rect {
309    Rect::new(
310        right_inner_x(b),
311        top_label_y(b),
312        right_inner_w(b),
313        HEADING_H,
314    )
315}
316
317pub fn commit_diff(b: Rect) -> Rect {
318    Rect::new(
319        right_inner_x(b),
320        top_pane_y(b),
321        right_inner_w(b),
322        top_pane_h(b),
323    )
324}
325
326pub fn commit_msg_label(b: Rect) -> Rect {
327    Rect::new(
328        right_inner_x(b),
329        bottom_label_y(b),
330        right_inner_w(b),
331        HEADING_H,
332    )
333}
334
335pub fn commit_editor(b: Rect) -> Rect {
336    Rect::new(
337        right_inner_x(b),
338        bottom_pane_y(b),
339        right_inner_w(b),
340        bottom_pane_h(b),
341    )
342}
343
344pub fn commit_amend(b: Rect) -> Rect {
345    Rect::new(right_inner_x(b), btn_y(b, AMEND_H), 180, AMEND_H)
346}
347
348pub fn commit_commit_btn(b: Rect) -> Rect {
349    let w = 110;
350    Rect::new(
351        right_inner_x(b) + right_inner_w(b) - w,
352        btn_y(b, COMMIT_BTN_H),
353        w,
354        COMMIT_BTN_H,
355    )
356}