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