breadcrumbs.rs

  1use editor::Editor;
  2use gpui::{
  3    Element, EventEmitter, FocusableView, IntoElement, ParentElement, Render, StyledText,
  4    Subscription, 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({
 94                        let editor = editor.clone();
 95                        move |_, cx| {
 96                            if let Some(editor) = editor.upgrade() {
 97                                outline::toggle(editor, &editor::actions::ToggleOutline, cx)
 98                            }
 99                        }
100                    })
101                    .tooltip(move |cx| {
102                        if let Some(editor) = editor.upgrade() {
103                            let focus_handle = editor.read(cx).focus_handle(cx);
104                            Tooltip::for_action_in(
105                                "Show symbol outline",
106                                &editor::actions::ToggleOutline,
107                                &focus_handle,
108                                cx,
109                            )
110                        } else {
111                            Tooltip::for_action(
112                                "Show symbol outline",
113                                &editor::actions::ToggleOutline,
114                                cx,
115                            )
116                        }
117                    }),
118            ),
119            None => element
120                // Match the height of the `ButtonLike` in the other arm.
121                .h(rems_from_px(22.))
122                .child(breadcrumbs_stack),
123        }
124    }
125}
126
127impl ToolbarItemView for Breadcrumbs {
128    fn set_active_pane_item(
129        &mut self,
130        active_pane_item: Option<&dyn ItemHandle>,
131        cx: &mut ViewContext<Self>,
132    ) -> ToolbarItemLocation {
133        cx.notify();
134        self.active_item = None;
135
136        let Some(item) = active_pane_item else {
137            return ToolbarItemLocation::Hidden;
138        };
139
140        let this = cx.view().downgrade();
141        self.subscription = Some(item.subscribe_to_item_events(
142            cx,
143            Box::new(move |event, cx| {
144                if let ItemEvent::UpdateBreadcrumbs = event {
145                    this.update(cx, |this, cx| {
146                        cx.notify();
147                        if let Some(active_item) = this.active_item.as_ref() {
148                            cx.emit(ToolbarItemEvent::ChangeLocation(
149                                active_item.breadcrumb_location(cx),
150                            ))
151                        }
152                    })
153                    .ok();
154                }
155            }),
156        ));
157        self.active_item = Some(item.boxed_clone());
158        item.breadcrumb_location(cx)
159    }
160
161    fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
162        self.pane_focused = pane_focused;
163    }
164}