Skip to main content

gitkraft_tui/features/
skeleton.rs

1//! Shimmering skeleton loading screen shown while the initial repo data loads.
2//!
3//! Uses [`tui_skeleton`] widgets to render animated placeholders that match
4//! the exact structure of the main layout (sidebar | commit log | diff,
5//! staging at the bottom) so the transition to real content feels seamless.
6//!
7//! The skeleton is only shown on the **initial** load (when commits are still
8//! empty).  Background watcher refreshes use the real content with the
9//! status-bar spinner instead.
10
11use ratatui::layout::{Constraint, Direction, Layout, Rect};
12use ratatui::style::Style;
13use ratatui::widgets::{Block, Borders};
14use ratatui::Frame;
15use tui_skeleton::{AnimationMode, SkeletonBlock, SkeletonList, SkeletonTable};
16
17use crate::app::App;
18
19/// Render the full shimmering skeleton in place of the main view.
20///
21/// Called from [`crate::layout::render_main`] when `is_loading && commits.is_empty()`.
22pub fn render(app: &mut App, frame: &mut Frame) {
23    let theme = app.theme();
24
25    // `elapsed_ms` drives all animations — tick_count ≈ frame × 33 ms at 30 fps.
26    let elapsed_ms = app.tick_count.saturating_mul(33);
27
28    // Use theme colours so the skeleton looks at home in any colour scheme.
29    let base = theme.border_inactive; // dim background fill
30    let hi = theme.sel_bg; // brighter shimmer highlight
31
32    let area = frame.area();
33
34    // ── Outer layout: header | content | staging | status ─────────────────
35    let outer = Layout::default()
36        .direction(Direction::Vertical)
37        .constraints([
38            Constraint::Length(3),
39            Constraint::Percentage(60),
40            Constraint::Percentage(40),
41            Constraint::Length(1),
42        ])
43        .split(area);
44
45    // Reuse the real header (it is static and always available).
46    crate::widgets::header::render(app, frame, outer[0]);
47
48    // ── Main content: sidebar | commit log | diff ────────────────────────
49    let main_cols = Layout::default()
50        .direction(Direction::Horizontal)
51        .constraints([
52            Constraint::Length(24),
53            Constraint::Percentage(40),
54            Constraint::Min(20),
55        ])
56        .split(outer[1]);
57
58    // Sidebar is split into branches / stashes / remotes.
59    let sidebar = Layout::default()
60        .direction(Direction::Vertical)
61        .constraints([
62            Constraint::Min(6),
63            Constraint::Length(5),
64            Constraint::Length(5),
65        ])
66        .split(main_cols[0]);
67
68    let inner = pane(
69        frame,
70        sidebar[0],
71        " Branches ",
72        theme.border_inactive,
73        theme.bg,
74    );
75    frame.render_widget(
76        SkeletonList::new(elapsed_ms)
77            .mode(AnimationMode::Sweep)
78            .items(10)
79            .base(base)
80            .highlight(hi),
81        inner,
82    );
83
84    let inner = pane(
85        frame,
86        sidebar[1],
87        " Stashes ",
88        theme.border_inactive,
89        theme.bg,
90    );
91    frame.render_widget(
92        SkeletonList::new(elapsed_ms)
93            .mode(AnimationMode::Sweep)
94            .items(3)
95            .base(base)
96            .highlight(hi),
97        inner,
98    );
99
100    let inner = pane(
101        frame,
102        sidebar[2],
103        " Remotes ",
104        theme.border_inactive,
105        theme.bg,
106    );
107    frame.render_widget(
108        SkeletonList::new(elapsed_ms)
109            .mode(AnimationMode::Sweep)
110            .items(2)
111            .base(base)
112            .highlight(hi),
113        inner,
114    );
115
116    // Commit log — table with graph | OID | summary | author | time columns.
117    let inner = pane(
118        frame,
119        main_cols[1],
120        " Commit Log ",
121        theme.border_inactive,
122        theme.bg,
123    );
124    frame.render_widget(
125        SkeletonTable::new(elapsed_ms)
126            .mode(AnimationMode::Sweep)
127            .rows(30)
128            .columns(&[
129                Constraint::Length(2),  // graph
130                Constraint::Length(8),  // short OID
131                Constraint::Fill(1),    // commit summary
132                Constraint::Length(14), // author
133                Constraint::Length(9),  // relative time
134            ])
135            .zebra(true)
136            .base(base)
137            .highlight(hi),
138        inner,
139    );
140
141    // Diff pane — solid shimmer block.
142    let inner = pane(
143        frame,
144        main_cols[2],
145        " Diff ",
146        theme.border_inactive,
147        theme.bg,
148    );
149    frame.render_widget(
150        SkeletonBlock::new(elapsed_ms)
151            .mode(AnimationMode::Sweep)
152            .base(base)
153            .highlight(hi),
154        inner,
155    );
156
157    // ── Staging area: unstaged | staged ─────────────────────────────
158    let staging_cols = Layout::default()
159        .direction(Direction::Horizontal)
160        .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
161        .split(outer[2]);
162
163    let inner = pane(
164        frame,
165        staging_cols[0],
166        " Unstaged ",
167        theme.border_inactive,
168        theme.bg,
169    );
170    frame.render_widget(
171        SkeletonList::new(elapsed_ms)
172            .mode(AnimationMode::Sweep)
173            .items(5)
174            .base(base)
175            .highlight(hi),
176        inner,
177    );
178
179    let inner = pane(
180        frame,
181        staging_cols[1],
182        " Staged ",
183        theme.border_inactive,
184        theme.bg,
185    );
186    frame.render_widget(
187        SkeletonList::new(elapsed_ms)
188            .mode(AnimationMode::Sweep)
189            .items(5)
190            .base(base)
191            .highlight(hi),
192        inner,
193    );
194
195    // Status bar — show the real one (spinner + any status message).
196    crate::widgets::status_bar::render(app, frame, outer[3]);
197}
198
199/// Draw a titled bordered pane and return the inner [`Rect`] for the caller to fill.
200fn pane(
201    frame: &mut Frame,
202    area: Rect,
203    title: &str,
204    border_color: ratatui::style::Color,
205    bg_color: ratatui::style::Color,
206) -> Rect {
207    let block = Block::default()
208        .title(title)
209        .borders(Borders::ALL)
210        .border_style(Style::default().fg(border_color))
211        .style(Style::default().bg(bg_color));
212    let inner = block.inner(area);
213    frame.render_widget(block, area);
214    inner
215}