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}