breadcrumbs.rs

  1use editor::Editor;
  2use gpui::{
  3    Context, Element, EventEmitter, Focusable, FontWeight, IntoElement, ParentElement, Render,
  4    StyledText, Subscription, Window,
  5};
  6use itertools::Itertools;
  7use settings::Settings;
  8use std::cmp;
  9use theme::ActiveTheme;
 10use ui::{ButtonLike, ButtonStyle, Label, Tooltip, prelude::*};
 11use workspace::{
 12    TabBarSettings, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
 13    item::{BreadcrumbText, ItemEvent, ItemHandle},
 14};
 15
 16pub struct Breadcrumbs {
 17    pane_focused: bool,
 18    active_item: Option<Box<dyn ItemHandle>>,
 19    subscription: Option<Subscription>,
 20}
 21
 22impl Default for Breadcrumbs {
 23    fn default() -> Self {
 24        Self::new()
 25    }
 26}
 27
 28impl Breadcrumbs {
 29    pub fn new() -> Self {
 30        Self {
 31            pane_focused: false,
 32            active_item: Default::default(),
 33            subscription: Default::default(),
 34        }
 35    }
 36}
 37
 38impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
 39
 40impl Render for Breadcrumbs {
 41    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 42        const MAX_SEGMENTS: usize = 12;
 43
 44        let element = h_flex()
 45            .id("breadcrumb-container")
 46            .flex_grow()
 47            .overflow_x_scroll()
 48            .text_ui(cx);
 49
 50        let Some(active_item) = self.active_item.as_ref() else {
 51            return element;
 52        };
 53
 54        let Some(mut segments) = active_item.breadcrumbs(cx.theme(), cx) else {
 55            return element;
 56        };
 57
 58        let prefix_end_ix = cmp::min(segments.len(), MAX_SEGMENTS / 2);
 59        let suffix_start_ix = cmp::max(
 60            prefix_end_ix,
 61            segments.len().saturating_sub(MAX_SEGMENTS / 2),
 62        );
 63
 64        if suffix_start_ix > prefix_end_ix {
 65            segments.splice(
 66                prefix_end_ix..suffix_start_ix,
 67                Some(BreadcrumbText {
 68                    text: "".into(),
 69                    highlights: None,
 70                    font: None,
 71                }),
 72            );
 73        }
 74
 75        let highlighted_segments = segments.into_iter().enumerate().map(|(index, segment)| {
 76            let mut text_style = window.text_style();
 77            if let Some(ref font) = segment.font {
 78                text_style.font_family = font.family.clone();
 79                text_style.font_features = font.features.clone();
 80                text_style.font_style = font.style;
 81                text_style.font_weight = font.weight;
 82            }
 83            text_style.color = Color::Muted.color(cx);
 84
 85            if index == 0
 86                && !TabBarSettings::get_global(cx).show
 87                && active_item.is_dirty(cx)
 88                && let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx)
 89            {
 90                return styled_element;
 91            }
 92
 93            StyledText::new(segment.text.replace('\n', ""))
 94                .with_default_highlights(&text_style, segment.highlights.unwrap_or_default())
 95                .into_any()
 96        });
 97        let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
 98            Label::new("").color(Color::Placeholder).into_any_element()
 99        });
100
101        let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
102
103        let prefix_element = active_item.breadcrumb_prefix(window, cx);
104
105        let breadcrumbs = if let Some(prefix) = prefix_element {
106            h_flex().gap_1p5().child(prefix).child(breadcrumbs_stack)
107        } else {
108            breadcrumbs_stack
109        };
110
111        match active_item
112            .downcast::<Editor>()
113            .map(|editor| editor.downgrade())
114        {
115            Some(editor) => element.child(
116                ButtonLike::new("toggle outline view")
117                    .child(breadcrumbs)
118                    .style(ButtonStyle::Transparent)
119                    .on_click({
120                        let editor = editor.clone();
121                        move |_, window, cx| {
122                            if let Some((editor, callback)) = editor
123                                .upgrade()
124                                .zip(zed_actions::outline::TOGGLE_OUTLINE.get())
125                            {
126                                callback(editor.to_any(), window, cx);
127                            }
128                        }
129                    })
130                    .tooltip(move |_window, cx| {
131                        if let Some(editor) = editor.upgrade() {
132                            let focus_handle = editor.read(cx).focus_handle(cx);
133                            Tooltip::for_action_in(
134                                "Show Symbol Outline",
135                                &zed_actions::outline::ToggleOutline,
136                                &focus_handle,
137                                cx,
138                            )
139                        } else {
140                            Tooltip::for_action(
141                                "Show Symbol Outline",
142                                &zed_actions::outline::ToggleOutline,
143                                cx,
144                            )
145                        }
146                    }),
147            ),
148            None => element
149                // Match the height and padding of the `ButtonLike` in the other arm.
150                .h(rems_from_px(22.))
151                .pl_1()
152                .child(breadcrumbs),
153        }
154    }
155}
156
157impl ToolbarItemView for Breadcrumbs {
158    fn set_active_pane_item(
159        &mut self,
160        active_pane_item: Option<&dyn ItemHandle>,
161        window: &mut Window,
162        cx: &mut Context<Self>,
163    ) -> ToolbarItemLocation {
164        cx.notify();
165        self.active_item = None;
166
167        let Some(item) = active_pane_item else {
168            return ToolbarItemLocation::Hidden;
169        };
170
171        let this = cx.entity().downgrade();
172        self.subscription = Some(item.subscribe_to_item_events(
173            window,
174            cx,
175            Box::new(move |event, _, cx| {
176                if let ItemEvent::UpdateBreadcrumbs = event {
177                    this.update(cx, |this, cx| {
178                        cx.notify();
179                        if let Some(active_item) = this.active_item.as_ref() {
180                            cx.emit(ToolbarItemEvent::ChangeLocation(
181                                active_item.breadcrumb_location(cx),
182                            ))
183                        }
184                    })
185                    .ok();
186                }
187            }),
188        ));
189        self.active_item = Some(item.boxed_clone());
190        item.breadcrumb_location(cx)
191    }
192
193    fn pane_focus_update(
194        &mut self,
195        pane_focused: bool,
196        _window: &mut Window,
197        _: &mut Context<Self>,
198    ) {
199        self.pane_focused = pane_focused;
200    }
201}
202
203fn apply_dirty_filename_style(
204    segment: &BreadcrumbText,
205    text_style: &gpui::TextStyle,
206    cx: &mut Context<Breadcrumbs>,
207) -> Option<gpui::AnyElement> {
208    let text = segment.text.replace('\n', "");
209
210    let filename_position = std::path::Path::new(&segment.text)
211        .file_name()
212        .and_then(|f| {
213            let filename_str = f.to_string_lossy();
214            segment.text.rfind(filename_str.as_ref())
215        })?;
216
217    let bold_weight = FontWeight::BOLD;
218    let default_color = Color::Default.color(cx);
219
220    if filename_position == 0 {
221        let mut filename_style = text_style.clone();
222        filename_style.font_weight = bold_weight;
223        filename_style.color = default_color;
224
225        return Some(
226            StyledText::new(text)
227                .with_default_highlights(&filename_style, [])
228                .into_any(),
229        );
230    }
231
232    let highlight_style = gpui::HighlightStyle {
233        font_weight: Some(bold_weight),
234        color: Some(default_color),
235        ..Default::default()
236    };
237
238    let highlight = vec![(filename_position..text.len(), highlight_style)];
239    Some(
240        StyledText::new(text)
241            .with_default_highlights(text_style, highlight)
242            .into_any(),
243    )
244}