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(cx);
 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                    font: None,
 56                }),
 57            );
 58        }
 59
 60        let highlighted_segments = segments.into_iter().map(|segment| {
 61            let mut text_style = cx.text_style();
 62            if let Some(font) = segment.font {
 63                text_style.font_family = font.family;
 64                text_style.font_features = font.features;
 65                text_style.font_style = font.style;
 66                text_style.font_weight = font.weight;
 67            }
 68            text_style.color = Color::Muted.color(cx);
 69
 70            StyledText::new(segment.text.replace('\n', ""))
 71                .with_highlights(&text_style, segment.highlights.unwrap_or_default())
 72                .into_any()
 73        });
 74        let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
 75            Label::new("").color(Color::Placeholder).into_any_element()
 76        });
 77
 78        let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
 79        match active_item
 80            .downcast::<Editor>()
 81            .map(|editor| editor.downgrade())
 82        {
 83            Some(editor) => element.child(
 84                ButtonLike::new("toggle outline view")
 85                    .child(breadcrumbs_stack)
 86                    .style(ButtonStyle::Transparent)
 87                    .on_click(move |_, cx| {
 88                        if let Some(editor) = editor.upgrade() {
 89                            outline::toggle(editor, &editor::actions::ToggleOutline, cx)
 90                        }
 91                    })
 92                    .tooltip(|cx| {
 93                        Tooltip::for_action(
 94                            "Show symbol outline",
 95                            &editor::actions::ToggleOutline,
 96                            cx,
 97                        )
 98                    }),
 99            ),
100            None => element
101                // Match the height of the `ButtonLike` in the other arm.
102                .h(rems_from_px(22.))
103                .child(breadcrumbs_stack),
104        }
105    }
106}
107
108impl ToolbarItemView for Breadcrumbs {
109    fn set_active_pane_item(
110        &mut self,
111        active_pane_item: Option<&dyn ItemHandle>,
112        cx: &mut ViewContext<Self>,
113    ) -> ToolbarItemLocation {
114        cx.notify();
115        self.active_item = None;
116        if let Some(item) = active_pane_item {
117            let this = cx.view().downgrade();
118            self.subscription = Some(item.subscribe_to_item_events(
119                cx,
120                Box::new(move |event, cx| {
121                    if let ItemEvent::UpdateBreadcrumbs = event {
122                        this.update(cx, |this, cx| {
123                            cx.notify();
124                            if let Some(active_item) = this.active_item.as_ref() {
125                                cx.emit(ToolbarItemEvent::ChangeLocation(
126                                    active_item.breadcrumb_location(cx),
127                                ))
128                            }
129                        })
130                        .ok();
131                    }
132                }),
133            ));
134            self.active_item = Some(item.boxed_clone());
135            item.breadcrumb_location(cx)
136        } else {
137            ToolbarItemLocation::Hidden
138        }
139    }
140
141    fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
142        self.pane_focused = pane_focused;
143    }
144}