Skip to main content

gitkraft_gui/features/staging/
view.rs

1//! Staging area view — shows unstaged changes, staged changes, and commit
2//! message input side-by-side as three columns at the bottom of the main
3//! layout.
4
5use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
6use iced::{Alignment, Element, Font, Length};
7
8use crate::icons;
9use crate::message::Message;
10use crate::state::GitKraft;
11use crate::theme;
12use crate::view_utils;
13
14/// Render the full staging area panel (unstaged | staged | commit input).
15pub fn view(state: &GitKraft) -> Element<'_, Message> {
16    let unstaged_panel = unstaged_view(state);
17    let staged_panel = staged_view(state);
18    let commit_panel = commit_view(state);
19
20    let content = row![unstaged_panel, staged_panel, commit_panel]
21        .spacing(1)
22        .height(Length::Fill)
23        .width(Length::Fill);
24
25    container(content)
26        .width(Length::Fill)
27        .style(theme::surface_style)
28        .into()
29}
30
31/// Render the "Unstaged Changes" file list.
32fn unstaged_view(state: &GitKraft) -> Element<'_, Message> {
33    let tab = state.active_tab();
34    let c = state.colors();
35
36    let header_icon = icon!(icons::FILE_DIFF, 13, c.yellow);
37
38    let header_label = text("Unstaged").size(13).color(c.text_primary);
39
40    let count_label = text(format!("({})", tab.unstaged_changes.len()))
41        .size(11)
42        .color(c.muted);
43
44    let stage_msg = (!tab.unstaged_changes.is_empty()).then_some(Message::StageAll);
45    let stage_all_btn = view_utils::on_press_maybe(
46        button(text("Stage All").size(11))
47            .padding([2, 8])
48            .style(theme::toolbar_button),
49        stage_msg,
50    );
51
52    let header_row = row![
53        header_icon,
54        Space::with_width(4),
55        header_label,
56        Space::with_width(4),
57        count_label,
58        Space::with_width(Length::Fill),
59        stage_all_btn,
60    ]
61    .align_y(Alignment::Center)
62    .padding([6, 8]);
63
64    let file_rows: Vec<Element<'_, Message>> = tab
65        .unstaged_changes
66        .iter()
67        .map(|diff| {
68            let file_path_display = diff.display_path();
69
70            let status_color = theme::status_color(&diff.status, &c);
71
72            let status_badge = text(format!("{}", diff.status))
73                .size(11)
74                .font(Font::MONOSPACE)
75                .color(status_color);
76
77            let file_label = text(file_path_display).size(12).color(c.text_primary);
78
79            let view_btn = button(icon!(icons::CLOUD_UPLOAD, 11, c.accent))
80                .padding([2, 4])
81                .style(theme::icon_button)
82                .on_press(Message::SelectDiff(diff.clone()));
83
84            let stage_btn = button(icon!(icons::PLUS_CIRCLE, 11, c.green))
85                .padding([2, 4])
86                .style(theme::icon_button)
87                .on_press(Message::StageFile(file_path_display.to_string()));
88
89            let is_pending_discard = tab.pending_discard.as_deref() == Some(file_path_display);
90            let discard_btn = if is_pending_discard {
91                button(text("Confirm?").size(10).color(c.red))
92                    .padding([2, 6])
93                    .style(theme::toolbar_button)
94                    .on_press(Message::ConfirmDiscard(file_path_display.to_string()))
95            } else {
96                button(icon!(icons::TRASH, 11, c.red))
97                    .padding([2, 4])
98                    .style(theme::icon_button)
99                    .on_press(Message::DiscardFile(file_path_display.to_string()))
100            };
101
102            let file_row = row![
103                status_badge,
104                Space::with_width(6),
105                file_label,
106                Space::with_width(Length::Fill),
107                view_btn,
108                Space::with_width(2),
109                stage_btn,
110                Space::with_width(2),
111                discard_btn,
112            ]
113            .align_y(Alignment::Center)
114            .padding([2, 8]);
115
116            container(file_row).width(Length::Fill).into()
117        })
118        .collect();
119
120    let mut list_col = column![].spacing(1).width(Length::Fill);
121
122    if file_rows.is_empty() {
123        list_col = list_col.push(view_utils::empty_list_hint("No unstaged changes", c.muted));
124    } else {
125        for row_el in file_rows {
126            list_col = list_col.push(row_el);
127        }
128    }
129
130    let content = column![
131        header_row,
132        scrollable(list_col)
133            .height(Length::Fill)
134            .direction(view_utils::thin_scrollbar())
135            .style(crate::theme::overlay_scrollbar)
136    ]
137    .width(Length::Fill)
138    .height(Length::Fill);
139
140    view_utils::surface_panel(content, Length::FillPortion(3))
141}
142
143/// Render the "Staged Changes" file list.
144fn staged_view(state: &GitKraft) -> Element<'_, Message> {
145    let tab = state.active_tab();
146    let c = state.colors();
147
148    let header_icon = icon!(icons::CHECK_CIRCLE_FILL, 13, c.green);
149
150    let header_label = text("Staged").size(13).color(c.text_primary);
151
152    let count_label = text(format!("({})", tab.staged_changes.len()))
153        .size(11)
154        .color(c.muted);
155
156    let unstage_msg = (!tab.staged_changes.is_empty()).then_some(Message::UnstageAll);
157    let unstage_all_btn = view_utils::on_press_maybe(
158        button(text("Unstage All").size(11))
159            .padding([2, 8])
160            .style(theme::toolbar_button),
161        unstage_msg,
162    );
163
164    let header_row = row![
165        header_icon,
166        Space::with_width(4),
167        header_label,
168        Space::with_width(4),
169        count_label,
170        Space::with_width(Length::Fill),
171        unstage_all_btn,
172    ]
173    .align_y(Alignment::Center)
174    .padding([6, 8]);
175
176    let file_rows: Vec<Element<'_, Message>> = tab
177        .staged_changes
178        .iter()
179        .map(|diff| {
180            let file_path_display = diff.display_path();
181
182            let status_color = theme::status_color(&diff.status, &c);
183
184            let status_badge = text(format!("{}", diff.status))
185                .size(11)
186                .font(Font::MONOSPACE)
187                .color(status_color);
188
189            let file_label = text(file_path_display).size(12).color(c.text_primary);
190
191            let view_btn = button(icon!(icons::CLOUD_UPLOAD, 11, c.accent))
192                .padding([2, 4])
193                .style(theme::icon_button)
194                .on_press(Message::SelectDiff(diff.clone()));
195
196            let unstage_btn = button(icon!(icons::DASH_CIRCLE, 11, c.yellow))
197                .padding([2, 4])
198                .style(theme::icon_button)
199                .on_press(Message::UnstageFile(file_path_display.to_string()));
200
201            let file_row = row![
202                status_badge,
203                Space::with_width(6),
204                file_label,
205                Space::with_width(Length::Fill),
206                view_btn,
207                Space::with_width(2),
208                unstage_btn,
209            ]
210            .align_y(Alignment::Center)
211            .padding([2, 8]);
212
213            container(file_row).width(Length::Fill).into()
214        })
215        .collect();
216
217    let mut list_col = column![].spacing(1).width(Length::Fill);
218
219    if file_rows.is_empty() {
220        list_col = list_col.push(view_utils::empty_list_hint("No staged changes", c.muted));
221    } else {
222        for row_el in file_rows {
223            list_col = list_col.push(row_el);
224        }
225    }
226
227    let content = column![
228        header_row,
229        scrollable(list_col)
230            .height(Length::Fill)
231            .direction(view_utils::thin_scrollbar())
232            .style(crate::theme::overlay_scrollbar)
233    ]
234    .width(Length::Fill)
235    .height(Length::Fill);
236
237    view_utils::surface_panel(content, Length::FillPortion(3))
238}
239
240/// Render the commit message input and "Commit" button.
241fn commit_view(state: &GitKraft) -> Element<'_, Message> {
242    let tab = state.active_tab();
243    let c = state.colors();
244
245    let header_icon = icon!(icons::COMMIT, 13, c.accent);
246
247    let header_label = text("Commit").size(13).color(c.text_primary);
248
249    let header_row = row![header_icon, Space::with_width(4), header_label,]
250        .align_y(Alignment::Center)
251        .padding([6, 8]);
252
253    let input = text_input("Commit message…", &tab.commit_message)
254        .on_input(Message::CommitMessageChanged)
255        .padding(8)
256        .size(13);
257
258    let can_commit = !tab.commit_message.trim().is_empty() && !tab.staged_changes.is_empty();
259
260    let commit_icon = icon!(
261        icons::CHECK_CIRCLE_FILL,
262        14,
263        if can_commit { c.green } else { c.muted }
264    );
265
266    let commit_btn_content = row![commit_icon, Space::with_width(6), text("Commit").size(13),]
267        .align_y(Alignment::Center);
268
269    let commit_msg = can_commit.then_some(Message::CreateCommit);
270    let commit_btn = view_utils::on_press_maybe(
271        button(commit_btn_content)
272            .padding([8, 16])
273            .width(Length::Fill)
274            .style(theme::toolbar_button),
275        commit_msg,
276    );
277
278    let staged_hint = if tab.staged_changes.is_empty() {
279        text("Stage files before committing")
280            .size(11)
281            .color(c.muted)
282    } else {
283        text(format!("{} file(s) staged", tab.staged_changes.len()))
284            .size(11)
285            .color(c.text_secondary)
286    };
287
288    let content = column![
289        header_row,
290        container(
291            column![
292                input,
293                Space::with_height(6),
294                commit_btn,
295                Space::with_height(4),
296                staged_hint,
297            ]
298            .spacing(2)
299            .width(Length::Fill),
300        )
301        .padding([4, 8]),
302    ]
303    .width(Length::Fill)
304    .height(Length::Fill);
305
306    view_utils::surface_panel(content, Length::FillPortion(2))
307}