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
 40// Potential idea:
 41// - Rename this to "BreadcrumbToolbar" or something
 42// - Create a wrapping "Breadcrumb" struct for Vec<BreadcrumbText>
 43// - Implement render for _that_ breadcrumb struct.
 44// - Call that from here to eliminate much of the logic.
 45// - This will change the Item interface, so do it only after you're happy with the features thus far
 46impl Render for Breadcrumbs {
 47    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 48        const MAX_SEGMENTS: usize = 12;
 49
 50        let element = h_flex()
 51            .id("breadcrumb-container")
 52            .flex_grow()
 53            .h_8()
 54            .overflow_x_scroll()
 55            .text_ui(cx);
 56
 57        let Some(active_item) = self.active_item.as_ref() else {
 58            return element;
 59        };
 60
 61        // Begin - logic we should copy/move
 62        let Some(mut segments) = active_item.breadcrumbs(cx.theme(), cx) else {
 63            return element;
 64        };
 65
 66        let prefix_end_ix = cmp::min(segments.len(), MAX_SEGMENTS / 2);
 67        let suffix_start_ix = cmp::max(
 68            prefix_end_ix,
 69            segments.len().saturating_sub(MAX_SEGMENTS / 2),
 70        );
 71
 72        if suffix_start_ix > prefix_end_ix {
 73            segments.splice(
 74                prefix_end_ix..suffix_start_ix,
 75                Some(BreadcrumbText {
 76                    text: "".into(),
 77                    highlights: None,
 78                    font: None,
 79                }),
 80            );
 81        }
 82
 83        let highlighted_segments = segments.into_iter().enumerate().map(|(index, segment)| {
 84            let mut text_style = window.text_style();
 85            if let Some(ref font) = segment.font {
 86                text_style.font_family = font.family.clone();
 87                text_style.font_features = font.features.clone();
 88                text_style.font_style = font.style;
 89                text_style.font_weight = font.weight;
 90            }
 91            text_style.color = Color::Muted.color(cx);
 92
 93            if index == 0
 94                && !TabBarSettings::get_global(cx).show
 95                && active_item.is_dirty(cx)
 96                && let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx)
 97            {
 98                return styled_element;
 99            }
100
101            StyledText::new(segment.text.replace('\n', ""))
102                .with_default_highlights(&text_style, segment.highlights.unwrap_or_default())
103                .into_any()
104        });
105        let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
106            Label::new("").color(Color::Placeholder).into_any_element()
107        });
108
109        let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
110
111        let prefix_element = active_item.breadcrumb_prefix(window, cx);
112
113        let breadcrumbs = if let Some(prefix) = prefix_element {
114            h_flex().gap_1p5().child(prefix).child(breadcrumbs_stack)
115        } else {
116            breadcrumbs_stack
117        };
118
119        match active_item
120            .downcast::<Editor>()
121            .map(|editor| editor.downgrade())
122        {
123            Some(editor) => element.child(
124                ButtonLike::new("toggle outline view")
125                    .child(breadcrumbs)
126                    .style(ButtonStyle::Transparent)
127                    .on_click({
128                        let editor = editor.clone();
129                        move |_, window, cx| {
130                            if let Some((editor, callback)) = editor
131                                .upgrade()
132                                .zip(zed_actions::outline::TOGGLE_OUTLINE.get())
133                            {
134                                callback(editor.to_any_view(), window, cx);
135                            }
136                        }
137                    })
138                    .tooltip(move |_window, cx| {
139                        if let Some(editor) = editor.upgrade() {
140                            let focus_handle = editor.read(cx).focus_handle(cx);
141                            Tooltip::for_action_in(
142                                "Show Symbol Outline",
143                                &zed_actions::outline::ToggleOutline,
144                                &focus_handle,
145                                cx,
146                            )
147                        } else {
148                            Tooltip::for_action(
149                                "Show Symbol Outline",
150                                &zed_actions::outline::ToggleOutline,
151                                cx,
152                            )
153                        }
154                    }),
155            ),
156            None => element
157                // Match the height and padding of the `ButtonLike` in the other arm.
158                .h(rems_from_px(22.))
159                .pl_1()
160                .child(breadcrumbs),
161        }
162        // End
163    }
164}
165
166impl ToolbarItemView for Breadcrumbs {
167    fn set_active_pane_item(
168        &mut self,
169        active_pane_item: Option<&dyn ItemHandle>,
170        window: &mut Window,
171        cx: &mut Context<Self>,
172    ) -> ToolbarItemLocation {
173        cx.notify();
174        self.active_item = None;
175
176        let Some(item) = active_pane_item else {
177            return ToolbarItemLocation::Hidden;
178        };
179
180        let this = cx.entity().downgrade();
181        self.subscription = Some(item.subscribe_to_item_events(
182            window,
183            cx,
184            Box::new(move |event, _, cx| {
185                if let ItemEvent::UpdateBreadcrumbs = event {
186                    this.update(cx, |this, cx| {
187                        cx.notify();
188                        if let Some(active_item) = this.active_item.as_ref() {
189                            cx.emit(ToolbarItemEvent::ChangeLocation(
190                                active_item.breadcrumb_location(cx),
191                            ))
192                        }
193                    })
194                    .ok();
195                }
196            }),
197        ));
198        self.active_item = Some(item.boxed_clone());
199        item.breadcrumb_location(cx)
200    }
201
202    fn pane_focus_update(
203        &mut self,
204        pane_focused: bool,
205        _window: &mut Window,
206        _: &mut Context<Self>,
207    ) {
208        self.pane_focused = pane_focused;
209    }
210}
211
212fn apply_dirty_filename_style(
213    segment: &BreadcrumbText,
214    text_style: &gpui::TextStyle,
215    cx: &mut Context<Breadcrumbs>,
216) -> Option<gpui::AnyElement> {
217    let text = segment.text.replace('\n', "");
218
219    let filename_position = std::path::Path::new(&segment.text)
220        .file_name()
221        .and_then(|f| {
222            let filename_str = f.to_string_lossy();
223            segment.text.rfind(filename_str.as_ref())
224        })?;
225
226    let bold_weight = FontWeight::BOLD;
227    let default_color = Color::Default.color(cx);
228
229    if filename_position == 0 {
230        let mut filename_style = text_style.clone();
231        filename_style.font_weight = bold_weight;
232        filename_style.color = default_color;
233
234        return Some(
235            StyledText::new(text)
236                .with_default_highlights(&filename_style, [])
237                .into_any(),
238        );
239    }
240
241    let highlight_style = gpui::HighlightStyle {
242        font_weight: Some(bold_weight),
243        color: Some(default_color),
244        ..Default::default()
245    };
246
247    let highlight = vec![(filename_position..text.len(), highlight_style)];
248    Some(
249        StyledText::new(text)
250            .with_default_highlights(text_style, highlight)
251            .into_any(),
252    )
253}