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(6, 0),
42 header_text,
43 Space::new(Length::Fill, 0),
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(0, 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![hint, input, row![confirm_btn, Space::new(4, 0), cancel_btn],]
97 .spacing(4)
98 .width(Length::Fill),
99 )
100 .padding([4, 10])
101 .into()
102 } else {
103 Space::new(0, 0).into()
104 };
105
106 let tag_form: Element<'_, Message> = if let Some(ref oid) = tab.create_tag_target_oid {
108 let short_oid = gitkraft_core::utils::short_oid_str(oid);
109 let label = if tab.create_tag_annotated {
110 format!("Creating annotated tag at {short_oid}")
111 } else {
112 format!("Creating lightweight tag at {short_oid}")
113 };
114 let hint = text(label).size(11).color(c.muted);
115
116 let name_input = iced::widget::text_input("tag-name", &tab.create_tag_name)
117 .on_input(Message::TagNameChanged)
118 .on_submit(Message::ConfirmCreateTag)
119 .padding(6)
120 .size(13);
121
122 let tag_msg = (!tab.create_tag_name.trim().is_empty()).then_some(Message::ConfirmCreateTag);
123 let confirm_btn = view_utils::on_press_maybe(
124 button(text("Create tag").size(13))
125 .padding([4, 10])
126 .style(theme::toolbar_button),
127 tag_msg,
128 );
129
130 let cancel_btn = button(text("Cancel").size(13))
131 .padding([4, 10])
132 .style(theme::toolbar_button)
133 .on_press(Message::CancelCreateTag);
134
135 let mut form_col = column![hint, name_input].spacing(4).width(Length::Fill);
136
137 if tab.create_tag_annotated {
138 let msg_input = iced::widget::text_input("tag message", &tab.create_tag_message)
139 .on_input(Message::TagMessageChanged)
140 .padding(6)
141 .size(13);
142 form_col = form_col.push(msg_input);
143 }
144
145 form_col = form_col.push(row![confirm_btn, Space::new(4, 0), cancel_btn]);
146
147 container(form_col).padding([4, 10]).into()
148 } else {
149 Space::new(0, 0).into()
150 };
151
152 let local_branches: Vec<Element<'_, Message>> = tab
154 .branches
155 .iter()
156 .filter(|b| b.branch_type == BranchType::Local)
157 .enumerate()
158 .map(|(local_index, branch)| {
159 let is_current = branch.is_head;
160
161 let indicator: Element<'_, Message> = if is_current {
162 icon!(icons::CHECK_CIRCLE_FILL, 12, c.green).into()
163 } else {
164 icon!(icons::GIT_BRANCH, 12, c.muted).into()
165 };
166
167 let name_color = if is_current { c.green } else { c.text_primary };
168
169 let name_available = (sidebar_width - 66.0).max(20.0);
172 let display_name = truncate_to_fit(branch.name.as_str(), name_available, 7.5);
173
174 let name_label = text(display_name)
175 .size(13)
176 .color(name_color)
177 .wrapping(iced::widget::text::Wrapping::None);
178
179 let checkout_msg =
180 (!is_current).then_some(Message::CheckoutBranch(branch.name.clone()));
181 let checkout_btn = view_utils::on_press_maybe(
182 button(row![indicator, Space::new(6, 0), name_label].align_y(Alignment::Center))
183 .padding([4, 8])
184 .width(Length::Fill)
185 .style(theme::ghost_button),
186 checkout_msg,
187 );
188
189 let delete_icon = icon!(icons::TRASH, 12, c.red);
190
191 let delete_msg = (!is_current).then_some(Message::DeleteBranch(branch.name.clone()));
192 let delete_btn = view_utils::on_press_maybe(
193 button(delete_icon)
194 .padding([4, 6])
195 .style(theme::icon_button),
196 delete_msg,
197 );
198
199 let branch_row = row![checkout_btn, delete_btn]
200 .spacing(2)
201 .align_y(Alignment::Center)
202 .width(Length::Fill);
203
204 mouse_area(
205 container(branch_row)
206 .width(Length::Fill)
207 .height(Length::Fixed(28.0))
208 .clip(true),
209 )
210 .on_right_press(Message::OpenBranchContextMenu(
211 branch.name.clone(),
212 local_index,
213 branch.is_head,
214 ))
215 .into()
216 })
217 .collect();
218
219 let remote_branches: Vec<Element<'_, Message>> = tab
221 .branches
222 .iter()
223 .filter(|b| b.branch_type == BranchType::Remote)
224 .map(|branch| {
225 let icon = icon!(icons::CLOUD, 12, c.muted);
226
227 let label_available = (sidebar_width - 36.0).max(20.0);
230 let display_remote = truncate_to_fit(branch.name.as_str(), label_available, 7.0);
231
232 let label = text(display_remote)
233 .size(12)
234 .color(c.text_secondary)
235 .wrapping(iced::widget::text::Wrapping::None);
236
237 let branch_btn = button(row![icon, Space::new(6, 0), label].align_y(Alignment::Center))
238 .padding([2, 8])
239 .width(Length::Fill)
240 .style(theme::ghost_button)
241 .on_press(Message::CheckoutRemoteBranch(branch.name.clone()));
242
243 mouse_area(
244 container(branch_btn)
245 .width(Length::Fill)
246 .height(Length::Fixed(22.0))
247 .clip(true),
248 )
249 .on_right_press(Message::OpenRemoteBranchContextMenu(branch.name.clone()))
250 .into()
251 })
252 .collect();
253
254 let mut list_col = column![].spacing(1).width(Length::Fill);
255
256 if !local_branches.is_empty() || tab.local_branches_expanded {
257 let local_count = tab
258 .branches
259 .iter()
260 .filter(|b| b.branch_type == BranchType::Local)
261 .count();
262
263 let local_header_btn = view_utils::collapsible_header(
264 tab.local_branches_expanded,
265 "Local",
266 local_count,
267 Message::ToggleLocalBranches,
268 c.muted,
269 );
270 list_col = list_col.push(local_header_btn);
271
272 if tab.local_branches_expanded {
273 for item in local_branches {
274 list_col = list_col.push(item);
275 }
276 }
277 }
278
279 if !remote_branches.is_empty() || tab.remote_branches_expanded {
280 let remote_count = tab
281 .branches
282 .iter()
283 .filter(|b| b.branch_type == BranchType::Remote)
284 .count();
285
286 list_col = list_col.push(Space::new(0, 4));
287
288 let remote_header_btn = view_utils::collapsible_header(
289 tab.remote_branches_expanded,
290 "Remote",
291 remote_count,
292 Message::ToggleRemoteBranches,
293 c.muted,
294 );
295 list_col = list_col.push(remote_header_btn);
296
297 if tab.remote_branches_expanded {
298 for item in remote_branches {
299 list_col = list_col.push(item);
300 }
301 }
302 }
303
304 let content = column![
305 header_row,
306 create_form,
307 rename_form,
308 tag_form,
309 scrollable(list_col)
310 .height(Length::Fill)
311 .direction(view_utils::thin_scrollbar())
312 .style(crate::theme::overlay_scrollbar),
313 ]
314 .width(Length::Fill)
315 .height(Length::Fill);
316
317 container(content)
318 .width(Length::Fill)
319 .height(Length::Fill)
320 .style(theme::sidebar_style)
321 .into()
322}