gitkraft_gui/features/staging/
view.rs1use 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
14pub 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
31fn 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
143fn 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
240fn 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}