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 && !TabBarSettings::get_global(cx).show && active_item.is_dirty(cx) {
86 if let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx)
87 {
88 return styled_element;
89 }
90 }
91
92 StyledText::new(segment.text.replace('\n', "⏎"))
93 .with_default_highlights(&text_style, segment.highlights.unwrap_or_default())
94 .into_any()
95 });
96 let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
97 Label::new("›").color(Color::Placeholder).into_any_element()
98 });
99
100 let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
101
102 match active_item
103 .downcast::<Editor>()
104 .map(|editor| editor.downgrade())
105 {
106 Some(editor) => element.child(
107 ButtonLike::new("toggle outline view")
108 .child(breadcrumbs_stack)
109 .style(ButtonStyle::Transparent)
110 .on_click({
111 let editor = editor.clone();
112 move |_, window, cx| {
113 if let Some((editor, callback)) = editor
114 .upgrade()
115 .zip(zed_actions::outline::TOGGLE_OUTLINE.get())
116 {
117 callback(editor.to_any(), window, cx);
118 }
119 }
120 })
121 .tooltip(move |window, cx| {
122 if let Some(editor) = editor.upgrade() {
123 let focus_handle = editor.read(cx).focus_handle(cx);
124 Tooltip::for_action_in(
125 "Show Symbol Outline",
126 &zed_actions::outline::ToggleOutline,
127 &focus_handle,
128 window,
129 cx,
130 )
131 } else {
132 Tooltip::for_action(
133 "Show Symbol Outline",
134 &zed_actions::outline::ToggleOutline,
135 window,
136 cx,
137 )
138 }
139 }),
140 ),
141 None => element
142 // Match the height and padding of the `ButtonLike` in the other arm.
143 .h(rems_from_px(22.))
144 .pl_1()
145 .child(breadcrumbs_stack),
146 }
147 }
148}
149
150impl ToolbarItemView for Breadcrumbs {
151 fn set_active_pane_item(
152 &mut self,
153 active_pane_item: Option<&dyn ItemHandle>,
154 window: &mut Window,
155 cx: &mut Context<Self>,
156 ) -> ToolbarItemLocation {
157 cx.notify();
158 self.active_item = None;
159
160 let Some(item) = active_pane_item else {
161 return ToolbarItemLocation::Hidden;
162 };
163
164 let this = cx.entity().downgrade();
165 self.subscription = Some(item.subscribe_to_item_events(
166 window,
167 cx,
168 Box::new(move |event, _, cx| {
169 if let ItemEvent::UpdateBreadcrumbs = event {
170 this.update(cx, |this, cx| {
171 cx.notify();
172 if let Some(active_item) = this.active_item.as_ref() {
173 cx.emit(ToolbarItemEvent::ChangeLocation(
174 active_item.breadcrumb_location(cx),
175 ))
176 }
177 })
178 .ok();
179 }
180 }),
181 ));
182 self.active_item = Some(item.boxed_clone());
183 item.breadcrumb_location(cx)
184 }
185
186 fn pane_focus_update(
187 &mut self,
188 pane_focused: bool,
189 _window: &mut Window,
190 _: &mut Context<Self>,
191 ) {
192 self.pane_focused = pane_focused;
193 }
194}
195
196fn apply_dirty_filename_style(
197 segment: &BreadcrumbText,
198 text_style: &gpui::TextStyle,
199 cx: &mut Context<Breadcrumbs>,
200) -> Option<gpui::AnyElement> {
201 let text = segment.text.replace('\n', "⏎");
202
203 let filename_position = std::path::Path::new(&segment.text)
204 .file_name()
205 .and_then(|f| {
206 let filename_str = f.to_string_lossy();
207 segment.text.rfind(filename_str.as_ref())
208 })?;
209
210 let bold_weight = FontWeight::BOLD;
211 let default_color = Color::Default.color(cx);
212
213 if filename_position == 0 {
214 let mut filename_style = text_style.clone();
215 filename_style.font_weight = bold_weight;
216 filename_style.color = default_color;
217
218 return Some(
219 StyledText::new(text)
220 .with_default_highlights(&filename_style, [])
221 .into_any(),
222 );
223 }
224
225 let highlight_style = gpui::HighlightStyle {
226 font_weight: Some(bold_weight),
227 color: Some(default_color),
228 ..Default::default()
229 };
230
231 let highlight = vec![(filename_position..text.len(), highlight_style)];
232 Some(
233 StyledText::new(text)
234 .with_default_highlights(text_style, highlight)
235 .into_any(),
236 )
237}