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::with_width(6),
42 header_text,
43 Space::with_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::with_height(0).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::with_width(4), cancel_btn],
100 ]
101 .spacing(4)
102 .width(Length::Fill),
103 )
104 .padding([4, 10])
105 .into()
106 } else {
107 Space::with_height(0).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::with_width(4), cancel_btn]);
150
151 container(form_col).padding([4, 10]).into()
152 } else {
153 Space::with_height(0).into()
154 };
155
156 let local_branches: Vec<Element<'_, Message>> = tab
158 .branches
159 .iter()
160 .filter(|b| b.branch_type == BranchType::Local)
161 .enumerate()
162 .map(|(local_index, branch)| {
163 let is_current = branch.is_head;
164
165 let indicator: Element<'_, Message> = if is_current {
166 icon!(icons::CHECK_CIRCLE_FILL, 12, c.green).into()
167 } else {
168 icon!(icons::GIT_BRANCH, 12, c.muted).into()
169 };
170
171 let name_color = if is_current { c.green } else { c.text_primary };
172
173 let name_available = (sidebar_width - 66.0).max(20.0);
176 let display_name = truncate_to_fit(branch.name.as_str(), name_available, 7.5);
177
178 let name_label = text(display_name)
179 .size(13)
180 .color(name_color)
181 .wrapping(iced::widget::text::Wrapping::None);
182
183 let checkout_msg =
184 (!is_current).then_some(Message::CheckoutBranch(branch.name.clone()));
185 let checkout_btn = view_utils::on_press_maybe(
186 button(
187 row![indicator, Space::with_width(6), name_label].align_y(Alignment::Center),
188 )
189 .padding([4, 8])
190 .width(Length::Fill)
191 .style(theme::ghost_button),
192 checkout_msg,
193 );
194
195 let delete_icon = icon!(icons::TRASH, 12, c.red);
196
197 let delete_msg = (!is_current).then_some(Message::DeleteBranch(branch.name.clone()));
198 let delete_btn = view_utils::on_press_maybe(
199 button(delete_icon)
200 .padding([4, 6])
201 .style(theme::icon_button),
202 delete_msg,
203 );
204
205 let branch_row = row![checkout_btn, delete_btn]
206 .spacing(2)
207 .align_y(Alignment::Center)
208 .width(Length::Fill);
209
210 mouse_area(
211 container(branch_row)
212 .width(Length::Fill)
213 .height(Length::Fixed(28.0))
214 .clip(true),
215 )
216 .on_right_press(Message::OpenBranchContextMenu(
217 branch.name.clone(),
218 local_index,
219 branch.is_head,
220 ))
221 .into()
222 })
223 .collect();
224
225 let remote_branches: Vec<Element<'_, Message>> = tab
227 .branches
228 .iter()
229 .filter(|b| b.branch_type == BranchType::Remote)
230 .map(|branch| {
231 let icon = icon!(icons::CLOUD, 12, c.muted);
232
233 let label_available = (sidebar_width - 36.0).max(20.0);
236 let display_remote = truncate_to_fit(branch.name.as_str(), label_available, 7.0);
237
238 let label = text(display_remote)
239 .size(12)
240 .color(c.text_secondary)
241 .wrapping(iced::widget::text::Wrapping::None);
242
243 let branch_btn =
244 button(row![icon, Space::with_width(6), label].align_y(Alignment::Center))
245 .padding([2, 8])
246 .width(Length::Fill)
247 .style(theme::ghost_button)
248 .on_press(Message::CheckoutRemoteBranch(branch.name.clone()));
249
250 mouse_area(
251 container(branch_btn)
252 .width(Length::Fill)
253 .height(Length::Fixed(22.0))
254 .clip(true),
255 )
256 .on_right_press(Message::OpenRemoteBranchContextMenu(branch.name.clone()))
257 .into()
258 })
259 .collect();
260
261 let mut list_col = column![].spacing(1).width(Length::Fill);
262
263 if !local_branches.is_empty() || tab.local_branches_expanded {
264 let local_count = tab
265 .branches
266 .iter()
267 .filter(|b| b.branch_type == BranchType::Local)
268 .count();
269
270 let local_header_btn = view_utils::collapsible_header(
271 tab.local_branches_expanded,
272 "Local",
273 local_count,
274 Message::ToggleLocalBranches,
275 c.muted,
276 );
277 list_col = list_col.push(local_header_btn);
278
279 if tab.local_branches_expanded {
280 for item in local_branches {
281 list_col = list_col.push(item);
282 }
283 }
284 }
285
286 if !remote_branches.is_empty() || tab.remote_branches_expanded {
287 let remote_count = tab
288 .branches
289 .iter()
290 .filter(|b| b.branch_type == BranchType::Remote)
291 .count();
292
293 list_col = list_col.push(Space::with_height(4));
294
295 let remote_header_btn = view_utils::collapsible_header(
296 tab.remote_branches_expanded,
297 "Remote",
298 remote_count,
299 Message::ToggleRemoteBranches,
300 c.muted,
301 );
302 list_col = list_col.push(remote_header_btn);
303
304 if tab.remote_branches_expanded {
305 for item in remote_branches {
306 list_col = list_col.push(item);
307 }
308 }
309 }
310
311 let content = column![
312 header_row,
313 create_form,
314 rename_form,
315 tag_form,
316 scrollable(list_col)
317 .height(Length::Fill)
318 .direction(view_utils::thin_scrollbar())
319 .style(crate::theme::overlay_scrollbar),
320 ]
321 .width(Length::Fill)
322 .height(Length::Fill);
323
324 container(content)
325 .width(Length::Fill)
326 .height(Length::Fill)
327 .style(theme::sidebar_style)
328 .into()
329}