breadcrumbs.rs

  1use editor::Editor;
  2use gpui::{
  3    Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
  4    ViewContext,
  5};
  6use itertools::Itertools;
  7use std::cmp;
  8use theme::ActiveTheme;
  9use ui::{prelude::*, ButtonLike, ButtonStyle, Label, Tooltip};
 10use workspace::{
 11    item::{BreadcrumbText, ItemEvent, ItemHandle},
 12    ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
 13};
 14
 15pub struct Breadcrumbs {
 16    pane_focused: bool,
 17    active_item: Option<Box<dyn ItemHandle>>,
 18    subscription: Option<Subscription>,
 19}
 20
 21impl Breadcrumbs {
 22    pub fn new() -> Self {
 23        Self {
 24            pane_focused: false,
 25            active_item: Default::default(),
 26            subscription: Default::default(),
 27        }
 28    }
 29}
 30
 31impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
 32
 33impl Render for Breadcrumbs {
 34    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 35        const MAX_SEGMENTS: usize = 12;
 36        let element = h_flex().text_ui();
 37        let Some(active_item) = self.active_item.as_ref() else {
 38            return element;
 39        };
 40        let Some(mut segments) = active_item.breadcrumbs(cx.theme(), cx) else {
 41            return element;
 42        };
 43
 44        let prefix_end_ix = cmp::min(segments.len(), MAX_SEGMENTS / 2);
 45        let suffix_start_ix = cmp::max(
 46            prefix_end_ix,
 47            segments.len().saturating_sub(MAX_SEGMENTS / 2),
 48        );
 49        if suffix_start_ix > prefix_end_ix {
 50            segments.splice(
 51                prefix_end_ix..suffix_start_ix,
 52                Some(BreadcrumbText {
 53                    text: "".into(),
 54                    highlights: None,
 55                }),
 56            );
 57        }
 58
 59        let highlighted_segments = segments.into_iter().map(|segment| {
 60            let mut text_style = cx.text_style();
 61            text_style.color = Color::Muted.color(cx);
 62
 63            StyledText::new(segment.text)
 64                .with_highlights(&text_style, segment.highlights.unwrap_or_default())
 65                .into_any()
 66        });
 67        let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
 68            Label::new("").color(Color::Muted).into_any_element()
 69        });
 70
 71        let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
 72        match active_item
 73            .downcast::<Editor>()
 74            .map(|editor| editor.downgrade())
 75        {
 76            Some(editor) => element.child(
 77                ButtonLike::new("toggle outline view")
 78                    .child(breadcrumbs_stack)
 79                    .style(ButtonStyle::Subtle)
 80                    .on_click(move |_, cx| {
 81                        if let Some(editor) = editor.upgrade() {
 82                            outline::toggle(editor, &outline::Toggle, cx)
 83                        }
 84                    })
 85                    .tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)),
 86            ),
 87            None => element
 88                // Match the height of the `ButtonLike` in the other arm.
 89                .h(rems_from_px(22.))
 90                .child(breadcrumbs_stack),
 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, |this, cx| {
110                            cx.notify();
111                            if let Some(active_item) = this.active_item.as_ref() {
112                                cx.emit(ToolbarItemEvent::ChangeLocation(
113                                    active_item.breadcrumb_location(cx),
114                                ))
115                            }
116                        })
117                        .ok();
118                    }
119                }),
120            ));
121            self.active_item = Some(item.boxed_clone());
122            item.breadcrumb_location(cx)
123        } else {
124            ToolbarItemLocation::Hidden
125        }
126    }
127
128    fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
129        self.pane_focused = pane_focused;
130    }
131}