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