breadcrumbs.rs

  1use editor::Editor;
  2use gpui::{
  3    Context, Element, EventEmitter, Focusable, IntoElement, ParentElement, Render, StyledText,
  4    Subscription, Window,
  5};
  6use itertools::Itertools;
  7use std::cmp;
  8use theme::ActiveTheme;
  9use ui::{ButtonLike, ButtonStyle, Label, Tooltip, prelude::*};
 10use workspace::{
 11    ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
 12    item::{BreadcrumbText, ItemEvent, ItemHandle},
 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, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 41        const MAX_SEGMENTS: usize = 12;
 42
 43        let element = h_flex()
 44            .id("breadcrumb-container")
 45            .flex_grow()
 46            .overflow_x_scroll()
 47            .text_ui(cx);
 48
 49        let Some(active_item) = self.active_item.as_ref() else {
 50            return element;
 51        };
 52
 53        let Some(mut segments) = active_item.breadcrumbs(cx.theme(), cx) else {
 54            return element;
 55        };
 56
 57        let prefix_end_ix = cmp::min(segments.len(), MAX_SEGMENTS / 2);
 58        let suffix_start_ix = cmp::max(
 59            prefix_end_ix,
 60            segments.len().saturating_sub(MAX_SEGMENTS / 2),
 61        );
 62
 63        if suffix_start_ix > prefix_end_ix {
 64            segments.splice(
 65                prefix_end_ix..suffix_start_ix,
 66                Some(BreadcrumbText {
 67                    text: "".into(),
 68                    highlights: None,
 69                    font: None,
 70                }),
 71            );
 72        }
 73
 74        let highlighted_segments = segments.into_iter().map(|segment| {
 75            let mut text_style = window.text_style();
 76            if let Some(font) = segment.font {
 77                text_style.font_family = font.family;
 78                text_style.font_features = font.features;
 79                text_style.font_style = font.style;
 80                text_style.font_weight = font.weight;
 81            }
 82            text_style.color = Color::Muted.color(cx);
 83
 84            StyledText::new(segment.text.replace('\n', ""))
 85                .with_default_highlights(&text_style, segment.highlights.unwrap_or_default())
 86                .into_any()
 87        });
 88        let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
 89            Label::new("").color(Color::Placeholder).into_any_element()
 90        });
 91
 92        let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
 93
 94        match active_item
 95            .downcast::<Editor>()
 96            .map(|editor| editor.downgrade())
 97        {
 98            Some(editor) => element.child(
 99                ButtonLike::new("toggle outline view")
100                    .child(breadcrumbs_stack)
101                    .style(ButtonStyle::Transparent)
102                    .on_click({
103                        let editor = editor.clone();
104                        move |_, window, cx| {
105                            if let Some((editor, callback)) = editor
106                                .upgrade()
107                                .zip(zed_actions::outline::TOGGLE_OUTLINE.get())
108                            {
109                                callback(editor.to_any(), window, cx);
110                            }
111                        }
112                    })
113                    .tooltip(move |window, cx| {
114                        if let Some(editor) = editor.upgrade() {
115                            let focus_handle = editor.read(cx).focus_handle(cx);
116                            Tooltip::for_action_in(
117                                "Show Symbol Outline",
118                                &zed_actions::outline::ToggleOutline,
119                                &focus_handle,
120                                window,
121                                cx,
122                            )
123                        } else {
124                            Tooltip::for_action(
125                                "Show Symbol Outline",
126                                &zed_actions::outline::ToggleOutline,
127                                window,
128                                cx,
129                            )
130                        }
131                    }),
132            ),
133            None => element
134                // Match the height and padding of the `ButtonLike` in the other arm.
135                .h(rems_from_px(22.))
136                .pl_1()
137                .child(breadcrumbs_stack),
138        }
139    }
140}
141
142impl ToolbarItemView for Breadcrumbs {
143    fn set_active_pane_item(
144        &mut self,
145        active_pane_item: Option<&dyn ItemHandle>,
146        window: &mut Window,
147        cx: &mut Context<Self>,
148    ) -> ToolbarItemLocation {
149        cx.notify();
150        self.active_item = None;
151
152        let Some(item) = active_pane_item else {
153            return ToolbarItemLocation::Hidden;
154        };
155
156        let this = cx.entity().downgrade();
157        self.subscription = Some(item.subscribe_to_item_events(
158            window,
159            cx,
160            Box::new(move |event, _, cx| {
161                if let ItemEvent::UpdateBreadcrumbs = event {
162                    this.update(cx, |this, cx| {
163                        cx.notify();
164                        if let Some(active_item) = this.active_item.as_ref() {
165                            cx.emit(ToolbarItemEvent::ChangeLocation(
166                                active_item.breadcrumb_location(cx),
167                            ))
168                        }
169                    })
170                    .ok();
171                }
172            }),
173        ));
174        self.active_item = Some(item.boxed_clone());
175        item.breadcrumb_location(cx)
176    }
177
178    fn pane_focus_update(
179        &mut self,
180        pane_focused: bool,
181        _window: &mut Window,
182        _: &mut Context<Self>,
183    ) {
184        self.pane_focused = pane_focused;
185    }
186}