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 Default for Breadcrumbs {
 22    fn default() -> Self {
 23        Self::new()
 24    }
 25}
 26
 27impl Breadcrumbs {
 28    pub fn new() -> Self {
 29        Self {
 30            pane_focused: false,
 31            active_item: Default::default(),
 32            subscription: Default::default(),
 33        }
 34    }
 35}
 36
 37impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
 38
 39impl Render for Breadcrumbs {
 40    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 41        const MAX_SEGMENTS: usize = 12;
 42        let element = h_flex().text_ui(cx);
 43        let Some(active_item) = self.active_item.as_ref() else {
 44            return element;
 45        };
 46        let Some(mut segments) = active_item.breadcrumbs(cx.theme(), cx) else {
 47            return element;
 48        };
 49
 50        let prefix_end_ix = cmp::min(segments.len(), MAX_SEGMENTS / 2);
 51        let suffix_start_ix = cmp::max(
 52            prefix_end_ix,
 53            segments.len().saturating_sub(MAX_SEGMENTS / 2),
 54        );
 55        if suffix_start_ix > prefix_end_ix {
 56            segments.splice(
 57                prefix_end_ix..suffix_start_ix,
 58                Some(BreadcrumbText {
 59                    text: "".into(),
 60                    highlights: None,
 61                    font: None,
 62                }),
 63            );
 64        }
 65
 66        let highlighted_segments = segments.into_iter().map(|segment| {
 67            let mut text_style = cx.text_style();
 68            if let Some(font) = segment.font {
 69                text_style.font_family = font.family;
 70                text_style.font_features = font.features;
 71                text_style.font_style = font.style;
 72                text_style.font_weight = font.weight;
 73            }
 74            text_style.color = Color::Muted.color(cx);
 75
 76            StyledText::new(segment.text.replace('\n', ""))
 77                .with_highlights(&text_style, segment.highlights.unwrap_or_default())
 78                .into_any()
 79        });
 80        let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
 81            Label::new("").color(Color::Placeholder).into_any_element()
 82        });
 83
 84        let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
 85        match active_item
 86            .downcast::<Editor>()
 87            .map(|editor| editor.downgrade())
 88        {
 89            Some(editor) => element.child(
 90                ButtonLike::new("toggle outline view")
 91                    .child(breadcrumbs_stack)
 92                    .style(ButtonStyle::Transparent)
 93                    .on_click(move |_, cx| {
 94                        if let Some(editor) = editor.upgrade() {
 95                            outline::toggle(editor, &editor::actions::ToggleOutline, cx)
 96                        }
 97                    })
 98                    .tooltip(|cx| {
 99                        Tooltip::for_action(
100                            "Show symbol outline",
101                            &editor::actions::ToggleOutline,
102                            cx,
103                        )
104                    }),
105            ),
106            None => element
107                // Match the height of the `ButtonLike` in the other arm.
108                .h(rems_from_px(22.))
109                .child(breadcrumbs_stack),
110        }
111    }
112}
113
114impl ToolbarItemView for Breadcrumbs {
115    fn set_active_pane_item(
116        &mut self,
117        active_pane_item: Option<&dyn ItemHandle>,
118        cx: &mut ViewContext<Self>,
119    ) -> ToolbarItemLocation {
120        cx.notify();
121        self.active_item = None;
122
123        let Some(item) = active_pane_item else {
124            return ToolbarItemLocation::Hidden;
125        };
126
127        let this = cx.view().downgrade();
128        self.subscription = Some(item.subscribe_to_item_events(
129            cx,
130            Box::new(move |event, cx| {
131                if let ItemEvent::UpdateBreadcrumbs = event {
132                    this.update(cx, |this, cx| {
133                        cx.notify();
134                        if let Some(active_item) = this.active_item.as_ref() {
135                            cx.emit(ToolbarItemEvent::ChangeLocation(
136                                active_item.breadcrumb_location(cx),
137                            ))
138                        }
139                    })
140                    .ok();
141                }
142            }),
143        ));
144        self.active_item = Some(item.boxed_clone());
145        item.breadcrumb_location(cx)
146    }
147
148    fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
149        self.pane_focused = pane_focused;
150    }
151}