breadcrumbs.rs

  1use gpui::{
  2    Div, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
  3    ViewContext, WeakView,
  4};
  5use itertools::Itertools;
  6use theme::ActiveTheme;
  7use ui::{prelude::*, ButtonLike, ButtonStyle, Label, Tooltip};
  8use workspace::{
  9    item::{ItemEvent, ItemHandle},
 10    ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
 11};
 12
 13pub enum Event {
 14    UpdateLocation,
 15}
 16
 17pub struct Breadcrumbs {
 18    pane_focused: bool,
 19    active_item: Option<Box<dyn ItemHandle>>,
 20    subscription: Option<Subscription>,
 21    workspace: WeakView<Workspace>,
 22}
 23
 24impl Breadcrumbs {
 25    pub fn new(workspace: &Workspace) -> Self {
 26        Self {
 27            pane_focused: false,
 28            active_item: Default::default(),
 29            subscription: Default::default(),
 30            workspace: workspace.weak_handle(),
 31        }
 32    }
 33}
 34
 35impl EventEmitter<Event> for Breadcrumbs {}
 36impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
 37
 38impl Render for Breadcrumbs {
 39    type Element = Div;
 40
 41    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
 42        let element = h_stack().text_ui();
 43
 44        let Some(active_item) = &self
 45            .active_item
 46            .as_ref()
 47            .filter(|item| item.downcast::<editor::Editor>().is_some())
 48        else {
 49            return element;
 50        };
 51
 52        let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else {
 53            return element;
 54        };
 55
 56        let highlighted_segments = segments.into_iter().map(|segment| {
 57            StyledText::new(segment.text)
 58                .with_highlights(&cx.text_style(), segment.highlights.unwrap_or_default())
 59                .into_any()
 60        });
 61        let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
 62            Label::new("").into_any_element()
 63        });
 64
 65        element.child(
 66            ButtonLike::new("toggle outline view")
 67                .style(ButtonStyle::Subtle)
 68                .child(h_stack().gap_1().children(breadcrumbs))
 69                // We disable the button when the containing pane is not focused:
 70                //    Because right now all the breadcrumb does is open the outline view, which is an
 71                //    action which operates on the active editor, clicking the breadcrumbs of another
 72                //    editor could cause weirdness. I remember that at one point it actually caused a
 73                //    panic weirdly.
 74                //
 75                //    It might be possible that with changes around how focus is managed that we
 76                //    might be able to update the active editor to the one with the breadcrumbs
 77                //    clicked on? That or we could just add a code path for being able to open the
 78                //    outline for a specific editor. Long term we'd like for it to be an actual
 79                //    breadcrumb bar so that problem goes away
 80                //
 81                //   — Julia (https://github.com/zed-industries/zed/pull/3505#pullrequestreview-1766198050)
 82                .disabled(!self.pane_focused)
 83                .on_click(cx.listener(|breadcrumbs, _, cx| {
 84                    if let Some(workspace) = breadcrumbs.workspace.upgrade() {
 85                        workspace.update(cx, |workspace, cx| {
 86                            outline::toggle(workspace, &outline::Toggle, cx)
 87                        })
 88                    }
 89                }))
 90                .tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)),
 91        )
 92    }
 93}
 94
 95impl ToolbarItemView for Breadcrumbs {
 96    fn set_active_pane_item(
 97        &mut self,
 98        active_pane_item: Option<&dyn ItemHandle>,
 99        cx: &mut ViewContext<Self>,
100    ) -> ToolbarItemLocation {
101        cx.notify();
102        self.active_item = None;
103        if let Some(item) = active_pane_item {
104            let this = cx.view().downgrade();
105            self.subscription = Some(item.subscribe_to_item_events(
106                cx,
107                Box::new(move |event, cx| {
108                    if let ItemEvent::UpdateBreadcrumbs = event {
109                        this.update(cx, |_, cx| {
110                            cx.emit(Event::UpdateLocation);
111                            cx.notify();
112                        })
113                        .ok();
114                    }
115                }),
116            ));
117            self.active_item = Some(item.boxed_clone());
118            item.breadcrumb_location(cx)
119        } else {
120            ToolbarItemLocation::Hidden
121        }
122    }
123
124    // fn location_for_event(
125    //     &self,
126    //     _: &Event,
127    //     current_location: ToolbarItemLocation,
128    //     cx: &AppContext,
129    // ) -> ToolbarItemLocation {
130    //     if let Some(active_item) = self.active_item.as_ref() {
131    //         active_item.breadcrumb_location(cx)
132    //     } else {
133    //         current_location
134    //     }
135    // }
136
137    fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
138        self.pane_focused = pane_focused;
139    }
140}