1use collections::HashMap;
2use std::{
3 cell::OnceCell,
4 collections::HashSet,
5 ffi::OsStr,
6 ops::Range,
7 path::{Path, PathBuf},
8 sync::Arc,
9 time::Duration,
10};
11
12use git::repository::GitFileStatus;
13
14use util::{ResultExt, TryFutureExt};
15
16use db::kvp::KEY_VALUE_STORE;
17use gpui::*;
18use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, WorktreeId};
19use serde::{Deserialize, Serialize};
20use settings::Settings as _;
21use ui::{
22 prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Scrollbar, ScrollbarState, Tooltip,
23};
24use workspace::dock::{DockPosition, Panel, PanelEvent};
25use workspace::Workspace;
26
27use crate::{git_status_icon, settings::GitPanelSettings};
28use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
29
30actions!(git_panel, [ToggleFocus]);
31
32const GIT_PANEL_KEY: &str = "GitPanel";
33
34pub fn init(cx: &mut AppContext) {
35 cx.observe_new_views(
36 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
37 workspace.register_action(|workspace, _: &ToggleFocus, cx| {
38 workspace.toggle_panel_focus::<GitPanel>(cx);
39 });
40 },
41 )
42 .detach();
43}
44
45#[derive(Debug)]
46pub enum Event {
47 Focus,
48}
49
50pub struct GitStatusEntry {}
51
52#[derive(Debug, PartialEq, Eq, Clone)]
53struct EntryDetails {
54 filename: String,
55 display_name: String,
56 path: Arc<Path>,
57 kind: EntryKind,
58 depth: usize,
59 is_expanded: bool,
60 status: Option<GitFileStatus>,
61}
62
63impl EntryDetails {
64 pub fn is_dir(&self) -> bool {
65 self.kind.is_dir()
66 }
67}
68
69#[derive(Serialize, Deserialize)]
70struct SerializedGitPanel {
71 width: Option<Pixels>,
72}
73
74pub struct GitPanel {
75 _workspace: WeakView<Workspace>,
76 current_modifiers: Modifiers,
77 focus_handle: FocusHandle,
78 fs: Arc<dyn Fs>,
79 hide_scrollbar_task: Option<Task<()>>,
80 pending_serialization: Task<Option<()>>,
81 project: Model<Project>,
82 scroll_handle: UniformListScrollHandle,
83 scrollbar_state: ScrollbarState,
84 selected_item: Option<usize>,
85 show_scrollbar: bool,
86 expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
87
88 // The entries that are currently shown in the panel, aka
89 // not hidden by folding or such
90 visible_entries: Vec<(WorktreeId, Vec<Entry>, OnceCell<HashSet<Arc<Path>>>)>,
91 width: Option<Pixels>,
92}
93
94impl GitPanel {
95 pub fn load(
96 workspace: WeakView<Workspace>,
97 cx: AsyncWindowContext,
98 ) -> Task<Result<View<Self>>> {
99 cx.spawn(|mut cx| async move {
100 // Clippy incorrectly classifies this as a redundant closure
101 #[allow(clippy::redundant_closure)]
102 workspace.update(&mut cx, |workspace, cx| Self::new(workspace, cx))
103 })
104 }
105
106 pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
107 let fs = workspace.app_state().fs.clone();
108 let weak_workspace = workspace.weak_handle();
109 let project = workspace.project().clone();
110
111 let git_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
112 let focus_handle = cx.focus_handle();
113 cx.on_focus(&focus_handle, Self::focus_in).detach();
114 cx.on_focus_out(&focus_handle, |this, _, cx| {
115 this.hide_scrollbar(cx);
116 })
117 .detach();
118 cx.subscribe(&project, |this, _project, event, cx| match event {
119 project::Event::WorktreeRemoved(id) => {
120 this.expanded_dir_ids.remove(id);
121 this.update_visible_entries(None, cx);
122 cx.notify();
123 }
124 project::Event::WorktreeUpdatedEntries(_, _)
125 | project::Event::WorktreeAdded(_)
126 | project::Event::WorktreeOrderChanged => {
127 this.update_visible_entries(None, cx);
128 cx.notify();
129 }
130 _ => {}
131 })
132 .detach();
133
134 let scroll_handle = UniformListScrollHandle::new();
135
136 let mut this = Self {
137 _workspace: weak_workspace,
138 focus_handle: cx.focus_handle(),
139 fs,
140 pending_serialization: Task::ready(None),
141 project,
142 visible_entries: Vec::new(),
143 current_modifiers: cx.modifiers(),
144 expanded_dir_ids: Default::default(),
145
146 width: Some(px(360.)),
147 scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
148 scroll_handle,
149 selected_item: None,
150 show_scrollbar: !Self::should_autohide_scrollbar(cx),
151 hide_scrollbar_task: None,
152 };
153 this.update_visible_entries(None, cx);
154 this
155 });
156
157 git_panel
158 }
159
160 fn serialize(&mut self, cx: &mut ViewContext<Self>) {
161 let width = self.width;
162 self.pending_serialization = cx.background_executor().spawn(
163 async move {
164 KEY_VALUE_STORE
165 .write_kvp(
166 GIT_PANEL_KEY.into(),
167 serde_json::to_string(&SerializedGitPanel { width })?,
168 )
169 .await?;
170 anyhow::Ok(())
171 }
172 .log_err(),
173 );
174 }
175
176 fn dispatch_context(&self) -> KeyContext {
177 let mut dispatch_context = KeyContext::new_with_defaults();
178 dispatch_context.add("GitPanel");
179 dispatch_context.add("menu");
180
181 dispatch_context
182 }
183
184 fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
185 if !self.focus_handle.contains_focused(cx) {
186 cx.emit(Event::Focus);
187 }
188 }
189
190 fn should_show_scrollbar(_cx: &AppContext) -> bool {
191 // TODO: plug into settings
192 true
193 }
194
195 fn should_autohide_scrollbar(_cx: &AppContext) -> bool {
196 // TODO: plug into settings
197 true
198 }
199
200 fn hide_scrollbar(&mut self, cx: &mut ViewContext<Self>) {
201 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
202 if !Self::should_autohide_scrollbar(cx) {
203 return;
204 }
205 self.hide_scrollbar_task = Some(cx.spawn(|panel, mut cx| async move {
206 cx.background_executor()
207 .timer(SCROLLBAR_SHOW_INTERVAL)
208 .await;
209 panel
210 .update(&mut cx, |panel, cx| {
211 panel.show_scrollbar = false;
212 cx.notify();
213 })
214 .log_err();
215 }))
216 }
217
218 fn handle_modifiers_changed(
219 &mut self,
220 event: &ModifiersChangedEvent,
221 cx: &mut ViewContext<Self>,
222 ) {
223 self.current_modifiers = event.modifiers;
224 cx.notify();
225 }
226
227 fn calculate_depth_and_difference(
228 entry: &Entry,
229 visible_worktree_entries: &HashSet<Arc<Path>>,
230 ) -> (usize, usize) {
231 let (depth, difference) = entry
232 .path
233 .ancestors()
234 .skip(1) // Skip the entry itself
235 .find_map(|ancestor| {
236 if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
237 let entry_path_components_count = entry.path.components().count();
238 let parent_path_components_count = parent_entry.components().count();
239 let difference = entry_path_components_count - parent_path_components_count;
240 let depth = parent_entry
241 .ancestors()
242 .skip(1)
243 .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
244 .count();
245 Some((depth + 1, difference))
246 } else {
247 None
248 }
249 })
250 .unwrap_or((0, 0));
251
252 (depth, difference)
253 }
254}
255
256impl GitPanel {
257 fn stage_all(&mut self, _: &StageAll, _cx: &mut ViewContext<Self>) {
258 // TODO: Implement stage all
259 println!("Stage all triggered");
260 }
261
262 fn unstage_all(&mut self, _: &UnstageAll, _cx: &mut ViewContext<Self>) {
263 // TODO: Implement unstage all
264 println!("Unstage all triggered");
265 }
266
267 fn discard_all(&mut self, _: &DiscardAll, _cx: &mut ViewContext<Self>) {
268 // TODO: Implement discard all
269 println!("Discard all triggered");
270 }
271
272 /// Commit all staged changes
273 fn commit_staged_changes(&mut self, _: &CommitStagedChanges, _cx: &mut ViewContext<Self>) {
274 // TODO: Implement commit all staged
275 println!("Commit staged changes triggered");
276 }
277
278 /// Commit all changes, regardless of whether they are staged or not
279 fn commit_all_changes(&mut self, _: &CommitAllChanges, _cx: &mut ViewContext<Self>) {
280 // TODO: Implement commit all changes
281 println!("Commit all changes triggered");
282 }
283
284 fn all_staged(&self) -> bool {
285 // TODO: Implement all_staged
286 true
287 }
288
289 fn no_entries(&self) -> bool {
290 self.visible_entries.is_empty()
291 }
292
293 fn entry_count(&self) -> usize {
294 self.visible_entries
295 .iter()
296 .map(|(_, entries, _)| {
297 entries
298 .iter()
299 .filter(|entry| entry.git_status.is_some())
300 .count()
301 })
302 .sum()
303 }
304
305 fn for_each_visible_entry(
306 &self,
307 range: Range<usize>,
308 cx: &mut ViewContext<Self>,
309 mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<Self>),
310 ) {
311 let mut ix = 0;
312 for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {
313 if ix >= range.end {
314 return;
315 }
316
317 if ix + visible_worktree_entries.len() <= range.start {
318 ix += visible_worktree_entries.len();
319 continue;
320 }
321
322 let end_ix = range.end.min(ix + visible_worktree_entries.len());
323 // let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
324 if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
325 let snapshot = worktree.read(cx).snapshot();
326 let root_name = OsStr::new(snapshot.root_name());
327 let expanded_entry_ids = self
328 .expanded_dir_ids
329 .get(&snapshot.id())
330 .map(Vec::as_slice)
331 .unwrap_or(&[]);
332
333 let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
334 let entries = entries_paths.get_or_init(|| {
335 visible_worktree_entries
336 .iter()
337 .map(|e| (e.path.clone()))
338 .collect()
339 });
340
341 for entry in visible_worktree_entries[entry_range].iter() {
342 let status = entry.git_status;
343 let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
344
345 let (depth, difference) = Self::calculate_depth_and_difference(entry, entries);
346
347 let filename = match difference {
348 diff if diff > 1 => entry
349 .path
350 .iter()
351 .skip(entry.path.components().count() - diff)
352 .collect::<PathBuf>()
353 .to_str()
354 .unwrap_or_default()
355 .to_string(),
356 _ => entry
357 .path
358 .file_name()
359 .map(|name| name.to_string_lossy().into_owned())
360 .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
361 };
362
363 let display_name = entry.path.to_string_lossy().into_owned();
364
365 let details = EntryDetails {
366 filename,
367 display_name,
368 kind: entry.kind,
369 is_expanded,
370 path: entry.path.clone(),
371 status,
372 depth,
373 };
374 callback(entry.id, details, cx);
375 }
376 }
377 ix = end_ix;
378 }
379 }
380
381 // TODO: Update expanded directory state
382 fn update_visible_entries(
383 &mut self,
384 new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
385 cx: &mut ViewContext<Self>,
386 ) {
387 let project = self.project.read(cx);
388 self.visible_entries.clear();
389 for worktree in project.visible_worktrees(cx) {
390 let snapshot = worktree.read(cx).snapshot();
391 let worktree_id = snapshot.id();
392
393 let mut visible_worktree_entries = Vec::new();
394 let mut entry_iter = snapshot.entries(true, 0);
395 while let Some(entry) = entry_iter.entry() {
396 // Only include entries with a git status
397 if entry.git_status.is_some() {
398 visible_worktree_entries.push(entry.clone());
399 }
400 entry_iter.advance();
401 }
402
403 snapshot.propagate_git_statuses(&mut visible_worktree_entries);
404 project::sort_worktree_entries(&mut visible_worktree_entries);
405
406 if !visible_worktree_entries.is_empty() {
407 self.visible_entries
408 .push((worktree_id, visible_worktree_entries, OnceCell::new()));
409 }
410 }
411
412 if let Some((worktree_id, entry_id)) = new_selected_entry {
413 self.selected_item = self.visible_entries.iter().enumerate().find_map(
414 |(worktree_index, (id, entries, _))| {
415 if *id == worktree_id {
416 entries
417 .iter()
418 .position(|entry| entry.id == entry_id)
419 .map(|entry_index| worktree_index * entries.len() + entry_index)
420 } else {
421 None
422 }
423 },
424 );
425 }
426
427 cx.notify();
428 }
429}
430
431impl GitPanel {
432 pub fn panel_button(
433 &self,
434 id: impl Into<SharedString>,
435 label: impl Into<SharedString>,
436 ) -> Button {
437 let id = id.into().clone();
438 let label = label.into().clone();
439
440 Button::new(id, label)
441 .label_size(LabelSize::Small)
442 .layer(ElevationIndex::ElevatedSurface)
443 .size(ButtonSize::Compact)
444 .style(ButtonStyle::Filled)
445 }
446
447 pub fn render_divider(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
448 h_flex()
449 .items_center()
450 .h(px(8.))
451 .child(Divider::horizontal_dashed().color(DividerColor::Border))
452 }
453
454 pub fn render_panel_header(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
455 let focus_handle = self.focus_handle(cx).clone();
456
457 let changes_string = format!("{} changes", self.entry_count());
458
459 h_flex()
460 .h(px(32.))
461 .items_center()
462 .px_3()
463 .bg(ElevationIndex::Surface.bg(cx))
464 .child(
465 h_flex()
466 .gap_2()
467 .child(Checkbox::new("all-changes", true.into()).disabled(true))
468 .child(div().text_buffer(cx).text_ui_sm(cx).child(changes_string)),
469 )
470 .child(div().flex_grow())
471 .child(
472 h_flex()
473 .gap_2()
474 .child(
475 IconButton::new("discard-changes", IconName::Undo)
476 .tooltip(move |cx| {
477 let focus_handle = focus_handle.clone();
478
479 Tooltip::for_action_in(
480 "Discard all changes",
481 &DiscardAll,
482 &focus_handle,
483 cx,
484 )
485 })
486 .icon_size(IconSize::Small)
487 .disabled(true),
488 )
489 .child(if self.all_staged() {
490 self.panel_button("unstage-all", "Unstage All").on_click(
491 cx.listener(move |_, _, cx| cx.dispatch_action(Box::new(DiscardAll))),
492 )
493 } else {
494 self.panel_button("stage-all", "Stage All").on_click(
495 cx.listener(move |_, _, cx| cx.dispatch_action(Box::new(StageAll))),
496 )
497 }),
498 )
499 }
500
501 pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
502 let focus_handle_1 = self.focus_handle(cx).clone();
503 let focus_handle_2 = self.focus_handle(cx).clone();
504
505 let commit_staged_button = self
506 .panel_button("commit-staged-changes", "Commit")
507 .tooltip(move |cx| {
508 let focus_handle = focus_handle_1.clone();
509 Tooltip::for_action_in(
510 "Commit all staged changes",
511 &CommitStagedChanges,
512 &focus_handle,
513 cx,
514 )
515 })
516 .on_click(cx.listener(|this, _: &ClickEvent, cx| {
517 this.commit_staged_changes(&CommitStagedChanges, cx)
518 }));
519
520 let commit_all_button = self
521 .panel_button("commit-all-changes", "Commit All")
522 .tooltip(move |cx| {
523 let focus_handle = focus_handle_2.clone();
524 Tooltip::for_action_in(
525 "Commit all changes, including unstaged changes",
526 &CommitAllChanges,
527 &focus_handle,
528 cx,
529 )
530 })
531 .on_click(cx.listener(|this, _: &ClickEvent, cx| {
532 this.commit_all_changes(&CommitAllChanges, cx)
533 }));
534
535 div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
536 v_flex()
537 .h_full()
538 .py_2p5()
539 .px_3()
540 .bg(cx.theme().colors().editor_background)
541 .font_buffer(cx)
542 .text_ui_sm(cx)
543 .text_color(cx.theme().colors().text_muted)
544 .child("Add a message")
545 .gap_1()
546 .child(div().flex_grow())
547 .child(h_flex().child(div().gap_1().flex_grow()).child(
548 if self.current_modifiers.alt {
549 commit_all_button
550 } else {
551 commit_staged_button
552 },
553 ))
554 .cursor(CursorStyle::OperationNotAllowed)
555 .opacity(0.5),
556 )
557 }
558
559 fn render_empty_state(&self, cx: &ViewContext<Self>) -> impl IntoElement {
560 h_flex()
561 .h_full()
562 .flex_1()
563 .justify_center()
564 .items_center()
565 .child(
566 v_flex()
567 .gap_3()
568 .child("No changes to commit")
569 .text_ui_sm(cx)
570 .mx_auto()
571 .text_color(Color::Placeholder.color(cx)),
572 )
573 }
574
575 fn render_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {
576 if !Self::should_show_scrollbar(cx)
577 || !(self.show_scrollbar || self.scrollbar_state.is_dragging())
578 {
579 return None;
580 }
581 Some(
582 div()
583 .occlude()
584 .id("project-panel-vertical-scroll")
585 .on_mouse_move(cx.listener(|_, _, cx| {
586 cx.notify();
587 cx.stop_propagation()
588 }))
589 .on_hover(|_, cx| {
590 cx.stop_propagation();
591 })
592 .on_any_mouse_down(|_, cx| {
593 cx.stop_propagation();
594 })
595 .on_mouse_up(
596 MouseButton::Left,
597 cx.listener(|this, _, cx| {
598 if !this.scrollbar_state.is_dragging()
599 && !this.focus_handle.contains_focused(cx)
600 {
601 this.hide_scrollbar(cx);
602 cx.notify();
603 }
604
605 cx.stop_propagation();
606 }),
607 )
608 .on_scroll_wheel(cx.listener(|_, _, cx| {
609 cx.notify();
610 }))
611 .h_full()
612 .absolute()
613 .right_1()
614 .top_1()
615 .bottom_1()
616 .w(px(12.))
617 .cursor_default()
618 .children(Scrollbar::vertical(
619 // percentage as f32..end_offset as f32,
620 self.scrollbar_state.clone(),
621 )),
622 )
623 }
624
625 fn render_entries(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
626 let item_count = self
627 .visible_entries
628 .iter()
629 .map(|(_, worktree_entries, _)| worktree_entries.len())
630 .sum();
631 h_flex()
632 .size_full()
633 .overflow_hidden()
634 .child(
635 uniform_list(cx.view().clone(), "entries", item_count, {
636 |this, range, cx| {
637 let mut items = Vec::with_capacity(range.end - range.start);
638 this.for_each_visible_entry(range, cx, |id, details, cx| {
639 items.push(this.render_entry(id, details, cx));
640 });
641 items
642 }
643 })
644 .size_full()
645 .with_sizing_behavior(ListSizingBehavior::Infer)
646 .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
647 // .with_width_from_item(self.max_width_item_index)
648 .track_scroll(self.scroll_handle.clone()),
649 )
650 .children(self.render_scrollbar(cx))
651 }
652
653 fn render_entry(
654 &self,
655 id: ProjectEntryId,
656 details: EntryDetails,
657 cx: &ViewContext<Self>,
658 ) -> impl IntoElement {
659 let id = id.to_proto() as usize;
660 let checkbox_id = ElementId::Name(format!("checkbox_{}", id).into());
661 let is_staged = ToggleState::Selected;
662
663 h_flex()
664 .id(id)
665 .h(px(28.))
666 .w_full()
667 .pl(px(12. + 12. * details.depth as f32))
668 .pr(px(4.))
669 .items_center()
670 .gap_2()
671 .font_buffer(cx)
672 .text_ui_sm(cx)
673 .when(!details.is_dir(), |this| {
674 this.child(Checkbox::new(checkbox_id, is_staged))
675 })
676 .when_some(details.status, |this, status| {
677 this.child(git_status_icon(status))
678 })
679 .child(h_flex().gap_1p5().child(details.display_name.clone()))
680 }
681}
682
683impl Render for GitPanel {
684 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
685 let project = self.project.read(cx);
686
687 v_flex()
688 .id("git_panel")
689 .key_context(self.dispatch_context())
690 .track_focus(&self.focus_handle)
691 .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
692 .when(!project.is_read_only(cx), |this| {
693 this.on_action(cx.listener(|this, &StageAll, cx| this.stage_all(&StageAll, cx)))
694 .on_action(
695 cx.listener(|this, &UnstageAll, cx| this.unstage_all(&UnstageAll, cx)),
696 )
697 .on_action(
698 cx.listener(|this, &DiscardAll, cx| this.discard_all(&DiscardAll, cx)),
699 )
700 .on_action(cx.listener(|this, &CommitStagedChanges, cx| {
701 this.commit_staged_changes(&CommitStagedChanges, cx)
702 }))
703 .on_action(cx.listener(|this, &CommitAllChanges, cx| {
704 this.commit_all_changes(&CommitAllChanges, cx)
705 }))
706 })
707 .on_hover(cx.listener(|this, hovered, cx| {
708 if *hovered {
709 this.show_scrollbar = true;
710 this.hide_scrollbar_task.take();
711 cx.notify();
712 } else if !this.focus_handle.contains_focused(cx) {
713 this.hide_scrollbar(cx);
714 }
715 }))
716 .size_full()
717 .overflow_hidden()
718 .font_buffer(cx)
719 .py_1()
720 .bg(ElevationIndex::Surface.bg(cx))
721 .child(self.render_panel_header(cx))
722 .child(self.render_divider(cx))
723 .child(if !self.no_entries() {
724 self.render_entries(cx).into_any_element()
725 } else {
726 self.render_empty_state(cx).into_any_element()
727 })
728 .child(self.render_divider(cx))
729 .child(self.render_commit_editor(cx))
730 }
731}
732
733impl FocusableView for GitPanel {
734 fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
735 self.focus_handle.clone()
736 }
737}
738
739impl EventEmitter<Event> for GitPanel {}
740
741impl EventEmitter<PanelEvent> for GitPanel {}
742
743impl Panel for GitPanel {
744 fn persistent_name() -> &'static str {
745 "GitPanel"
746 }
747
748 fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
749 GitPanelSettings::get_global(cx).dock
750 }
751
752 fn position_is_valid(&self, position: DockPosition) -> bool {
753 matches!(position, DockPosition::Left | DockPosition::Right)
754 }
755
756 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
757 settings::update_settings_file::<GitPanelSettings>(
758 self.fs.clone(),
759 cx,
760 move |settings, _| settings.dock = Some(position),
761 );
762 }
763
764 fn size(&self, cx: &gpui::WindowContext) -> Pixels {
765 self.width
766 .unwrap_or_else(|| GitPanelSettings::get_global(cx).default_width)
767 }
768
769 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
770 self.width = size;
771 self.serialize(cx);
772 cx.notify();
773 }
774
775 fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
776 Some(ui::IconName::GitBranch).filter(|_| GitPanelSettings::get_global(cx).button)
777 }
778
779 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
780 Some("Git Panel")
781 }
782
783 fn toggle_action(&self) -> Box<dyn Action> {
784 Box::new(ToggleFocus)
785 }
786}