gitkraft_gui/features/branches/
view.rs1use gitkraft_core::BranchType;
5use iced::widget::{
6 button, column, container, mouse_area, row, scrollable, text, text_input, Space,
7};
8use iced::{Alignment, Element, Length};
9
10use crate::icons;
11use crate::message::Message;
12use crate::state::GitKraft;
13use crate::theme;
14use crate::view_utils;
15use crate::view_utils::truncate_to_fit;
16
17pub fn view(state: &GitKraft) -> Element<'_, Message> {
19 let tab = state.active_tab();
20 let c = state.colors();
21 let sidebar_width = state.sidebar_width;
22
23 let header_icon = icon!(icons::GIT_BRANCH, 14, c.accent);
24
25 let header_text = text("Branches").size(14).color(c.text_primary);
26
27 let toggle_icon_char = if tab.show_branch_create {
28 icons::DASH_CIRCLE
29 } else {
30 icons::PLUS_CIRCLE
31 };
32 let toggle_icon = icon!(toggle_icon_char, 14, c.accent);
33
34 let toggle_btn = button(toggle_icon)
35 .padding([2, 6])
36 .style(theme::icon_button)
37 .on_press(Message::ToggleBranchCreate);
38
39 let header_row = row![
40 header_icon,
41 Space::new().width(6),
42 header_text,
43 Space::new().width(Length::Fill),
44 toggle_btn,
45 ]
46 .align_y(Alignment::Center)
47 .padding([8, 10]);
48
49 let create_form: Element<'_, Message> = if tab.show_branch_create {
51 let input = text_input("new-branch-name", &tab.new_branch_name)
52 .on_input(Message::NewBranchNameChanged)
53 .padding(6)
54 .size(13);
55
56 let create_msg = (!tab.new_branch_name.trim().is_empty()).then_some(Message::CreateBranch);
57 let create_btn = view_utils::on_press_maybe(
58 button(text("Create").size(13))
59 .padding([4, 10])
60 .style(theme::toolbar_button),
61 create_msg,
62 );
63
64 container(column![input, create_btn,].spacing(4).width(Length::Fill))
65 .padding([4, 10])
66 .into()
67 } else {
68 Space::new().into()
69 };
70
71 let rename_form: Element<'_, Message> = if let Some(ref orig) = tab.rename_branch_target {
73 let input = iced::widget::text_input("new branch name", &tab.rename_branch_input)
74 .on_input(Message::RenameBranchInputChanged)
75 .on_submit(Message::ConfirmRenameBranch)
76 .padding(6)
77 .size(13);
78
79 let rename_enabled = !tab.rename_branch_input.trim().is_empty()
80 && tab.rename_branch_input.trim() != orig.as_str();
81 let confirm_btn = view_utils::on_press_maybe(
82 button(text("Rename").size(13))
83 .padding([4, 10])
84 .style(theme::toolbar_button),
85 rename_enabled.then_some(Message::ConfirmRenameBranch),
86 );
87
88 let cancel_btn = button(text("Cancel").size(13))
89 .padding([4, 10])
90 .style(theme::toolbar_button)
91 .on_press(Message::CancelRename);
92
93 let hint = text(format!("Renaming '{orig}'")).size(11).color(c.muted);
94
95 container(
96 column![
97 hint,
98 input,
99 row![confirm_btn, Space::new().width(4), cancel_btn],
100 ]
101 .spacing(4)
102 .width(Length::Fill),
103 )
104 .padding([4, 10])
105 .into()
106 } else {
107 Space::new().into()
108 };
109
110 let tag_form: Element<'_, Message> = if let Some(ref oid) = tab.create_tag_target_oid {
112 let short_oid = gitkraft_core::utils::short_oid_str(oid);
113 let label = if tab.create_tag_annotated {
114 format!("Creating annotated tag at {short_oid}")
115 } else {
116 format!("Creating lightweight tag at {short_oid}")
117 };
118 let hint = text(label).size(11).color(c.muted);
119
120 let name_input = iced::widget::text_input("tag-name", &tab.create_tag_name)
121 .on_input(Message::TagNameChanged)
122 .on_submit(Message::ConfirmCreateTag)
123 .padding(6)
124 .size(13);
125
126 let tag_msg = (!tab.create_tag_name.trim().is_empty()).then_some(Message::ConfirmCreateTag);
127 let confirm_btn = view_utils::on_press_maybe(
128 button(text("Create tag").size(13))
129 .padding([4, 10])
130 .style(theme::toolbar_button),
131 tag_msg,
132 );
133
134 let cancel_btn = button(text("Cancel").size(13))
135 .padding([4, 10])
136 .style(theme::toolbar_button)
137 .on_press(Message::CancelCreateTag);
138
139 let mut form_col = column![hint, name_input].spacing(4).width(Length::Fill);
140
141 if tab.create_tag_annotated {
142 let msg_input = iced::widget::text_input("tag message", &tab.create_tag_message)
143 .on_input(Message::TagMessageChanged)
144 .padding(6)
145 .size(13);
146 form_col = form_col.push(msg_input);
147 }
148
149 form_col = form_col.push(row![confirm_btn, Space::new().width(4), cancel_btn]);
150
151 container(form_col).padding([4, 10]).into()
152 } else {
153 Space::new().into()
154 };
155
156 let create_branch_at_form: Element<'_, Message> =
158 if let Some(ref oid) = tab.create_branch_at_oid {
159 let short_oid = gitkraft_core::utils::short_oid_str(oid);
160 let hint = text(format!("Creating branch at {short_oid}"))
161 .size(11)
162 .color(c.muted);
163
164 let name_input = iced::widget::text_input("branch-name", &tab.new_branch_name)
165 .on_input(Message::NewBranchNameChanged)
166 .on_submit(Message::ConfirmCreateBranchAtCommit)
167 .padding(6)
168 .size(13);
169
170 let confirm_msg = (!tab.new_branch_name.trim().is_empty())
171 .then_some(Message::ConfirmCreateBranchAtCommit);
172 let confirm_btn = view_utils::on_press_maybe(
173 button(text("Create").size(13))
174 .padding([4, 10])
175 .style(theme::toolbar_button),
176 confirm_msg,
177 );
178
179 let cancel_btn = button(text("Cancel").size(13))
180 .padding([4, 10])
181 .style(theme::toolbar_button)
182 .on_press(Message::CancelCreateBranchAtCommit);
183
184 container(
185 column![
186 hint,
187 name_input,
188 row![confirm_btn, Space::new().width(4), cancel_btn],
189 ]
190 .spacing(4)
191 .width(Length::Fill),
192 )
193 .padding([4, 10])
194 .into()
195 } else {
196 Space::new().into()
197 };
198
199 let local_branches: Vec<Element<'_, Message>> = tab
201 .branches
202 .iter()
203 .filter(|b| b.branch_type == BranchType::Local)
204 .enumerate()
205 .map(|(local_index, branch)| {
206 let is_current = branch.is_head;
207
208 let indicator: Element<'_, Message> = if is_current {
209 icon!(icons::CHECK_CIRCLE_FILL, 12, c.green).into()
210 } else {
211 icon!(icons::GIT_BRANCH, 12, c.muted).into()
212 };
213
214 let name_color = if is_current { c.green } else { c.text_primary };
215
216 let name_available = (sidebar_width - 66.0).max(20.0);
219 let display_name = truncate_to_fit(branch.name.as_str(), name_available, 7.5);
220
221 let name_label = text(display_name)
222 .size(13)
223 .color(name_color)
224 .wrapping(iced::widget::text::Wrapping::None);
225
226 let checkout_msg =
227 (!is_current).then_some(Message::CheckoutBranch(branch.name.clone()));
228 let checkout_btn = view_utils::on_press_maybe(
229 button(
230 row![indicator, Space::new().width(6), name_label].align_y(Alignment::Center),
231 )
232 .padding([4, 8])
233 .width(Length::Fill)
234 .style(theme::ghost_button),
235 checkout_msg,
236 );
237
238 let delete_icon = icon!(icons::TRASH, 12, c.red);
239
240 let delete_msg = (!is_current).then_some(Message::DeleteBranch(branch.name.clone()));
241 let delete_btn = view_utils::on_press_maybe(
242 button(delete_icon)
243 .padding([4, 6])
244 .style(theme::icon_button),
245 delete_msg,
246 );
247
248 let branch_row = row![checkout_btn, delete_btn]
249 .spacing(2)
250 .align_y(Alignment::Center)
251 .width(Length::Fill);
252
253 mouse_area(
254 container(branch_row)
255 .width(Length::Fill)
256 .height(Length::Fixed(28.0))
257 .clip(true),
258 )
259 .on_right_press(Message::OpenBranchContextMenu(
260 branch.name.clone(),
261 local_index,
262 branch.is_head,
263 ))
264 .into()
265 })
266 .collect();
267
268 let remote_branches: Vec<Element<'_, Message>> = tab
270 .branches
271 .iter()
272 .filter(|b| b.branch_type == BranchType::Remote)
273 .map(|branch| {
274 let icon = icon!(icons::CLOUD, 12, c.muted);
275
276 let label_available = (sidebar_width - 36.0).max(20.0);
279 let display_remote = truncate_to_fit(branch.name.as_str(), label_available, 7.0);
280
281 let label = text(display_remote)
282 .size(12)
283 .color(c.text_secondary)
284 .wrapping(iced::widget::text::Wrapping::None);
285
286 let branch_btn =
287 button(row![icon, Space::new().width(6), label].align_y(Alignment::Center))
288 .padding([2, 8])
289 .width(Length::Fill)
290 .style(theme::ghost_button)
291 .on_press(Message::CheckoutRemoteBranch(branch.name.clone()));
292
293 mouse_area(
294 container(branch_btn)
295 .width(Length::Fill)
296 .height(Length::Fixed(22.0))
297 .clip(true),
298 )
299 .on_right_press(Message::OpenRemoteBranchContextMenu(branch.name.clone()))
300 .into()
301 })
302 .collect();
303
304 let mut list_col = column![].spacing(1).width(Length::Fill);
305
306 if !local_branches.is_empty() || tab.local_branches_expanded {
307 let local_count = tab
308 .branches
309 .iter()
310 .filter(|b| b.branch_type == BranchType::Local)
311 .count();
312
313 let local_header_btn = view_utils::collapsible_header(
314 tab.local_branches_expanded,
315 "Local",
316 local_count,
317 Message::ToggleLocalBranches,
318 c.muted,
319 );
320 list_col = list_col.push(local_header_btn);
321
322 if tab.local_branches_expanded {
323 for item in local_branches {
324 list_col = list_col.push(item);
325 }
326 }
327 }
328
329 if !remote_branches.is_empty() || tab.remote_branches_expanded {
330 let remote_count = tab
331 .branches
332 .iter()
333 .filter(|b| b.branch_type == BranchType::Remote)
334 .count();
335
336 list_col = list_col.push(Space::new().height(4));
337
338 let remote_header_btn = view_utils::collapsible_header(
339 tab.remote_branches_expanded,
340 "Remote",
341 remote_count,
342 Message::ToggleRemoteBranches,
343 c.muted,
344 );
345 list_col = list_col.push(remote_header_btn);
346
347 if tab.remote_branches_expanded {
348 for item in remote_branches {
349 list_col = list_col.push(item);
350 }
351 }
352 }
353
354 let content = column![
355 header_row,
356 create_form,
357 rename_form,
358 tag_form,
359 create_branch_at_form,
360 scrollable(list_col)
361 .height(Length::Fill)
362 .direction(view_utils::thin_scrollbar())
363 .style(crate::theme::overlay_scrollbar),
364 ]
365 .width(Length::Fill)
366 .height(Length::Fill);
367
368 container(content)
369 .width(Length::Fill)
370 .height(Length::Fill)
371 .style(theme::sidebar_style)
372 .into()
373}