1use editor::Editor;
2use gpui::{
3 Context, Element, EventEmitter, Focusable, IntoElement, ParentElement, Render, StyledText,
4 Subscription, Window,
5};
6use itertools::Itertools;
7use std::cmp;
8use theme::ActiveTheme;
9use ui::{ButtonLike, ButtonStyle, Label, Tooltip, prelude::*};
10use workspace::{
11 ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
12 item::{BreadcrumbText, ItemEvent, ItemHandle},
13};
14
15pub struct Breadcrumbs {
16 pane_focused: bool,
17 active_item: Option<Box<dyn ItemHandle>>,
18 subscription: Option<Subscription>,
19}
20
21impl Default for Breadcrumbs {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl Breadcrumbs {
28 pub fn new() -> Self {
29 Self {
30 pane_focused: false,
31 active_item: Default::default(),
32 subscription: Default::default(),
33 }
34 }
35}
36
37impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
38
39impl Render for Breadcrumbs {
40 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
41 const MAX_SEGMENTS: usize = 12;
42
43 let element = h_flex()
44 .id("breadcrumb-container")
45 .flex_grow()
46 .overflow_x_scroll()
47 .text_ui(cx);
48
49 let Some(active_item) = self.active_item.as_ref() else {
50 return element;
51 };
52
53 let Some(mut segments) = active_item.breadcrumbs(cx.theme(), cx) else {
54 return element;
55 };
56
57 let prefix_end_ix = cmp::min(segments.len(), MAX_SEGMENTS / 2);
58 let suffix_start_ix = cmp::max(
59 prefix_end_ix,
60 segments.len().saturating_sub(MAX_SEGMENTS / 2),
61 );
62
63 if suffix_start_ix > prefix_end_ix {
64 segments.splice(
65 prefix_end_ix..suffix_start_ix,
66 Some(BreadcrumbText {
67 text: "⋯".into(),
68 highlights: None,
69 font: None,
70 }),
71 );
72 }
73
74 let highlighted_segments = segments.into_iter().map(|segment| {
75 let mut text_style = window.text_style();
76 if let Some(font) = segment.font {
77 text_style.font_family = font.family;
78 text_style.font_features = font.features;
79 text_style.font_style = font.style;
80 text_style.font_weight = font.weight;
81 }
82 text_style.color = Color::Muted.color(cx);
83
84 StyledText::new(segment.text.replace('\n', "⏎"))
85 .with_default_highlights(&text_style, segment.highlights.unwrap_or_default())
86 .into_any()
87 });
88 let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
89 Label::new("›").color(Color::Placeholder).into_any_element()
90 });
91
92 let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
93
94 match active_item
95 .downcast::<Editor>()
96 .map(|editor| editor.downgrade())
97 {
98 Some(editor) => element.child(
99 ButtonLike::new("toggle outline view")
100 .child(breadcrumbs_stack)
101 .style(ButtonStyle::Transparent)
102 .on_click({
103 let editor = editor.clone();
104 move |_, window, cx| {
105 if let Some((editor, callback)) = editor
106 .upgrade()
107 .zip(zed_actions::outline::TOGGLE_OUTLINE.get())
108 {
109 callback(editor.to_any(), window, cx);
110 }
111 }
112 })
113 .tooltip(move |window, cx| {
114 if let Some(editor) = editor.upgrade() {
115 let focus_handle = editor.read(cx).focus_handle(cx);
116 Tooltip::for_action_in(
117 "Show Symbol Outline",
118 &zed_actions::outline::ToggleOutline,
119 &focus_handle,
120 window,
121 cx,
122 )
123 } else {
124 Tooltip::for_action(
125 "Show Symbol Outline",
126 &zed_actions::outline::ToggleOutline,
127 window,
128 cx,
129 )
130 }
131 }),
132 ),
133 None => element
134 // Match the height and padding of the `ButtonLike` in the other arm.
135 .h(rems_from_px(22.))
136 .pl_1()
137 .child(breadcrumbs_stack),
138 }
139 }
140}
141
142impl ToolbarItemView for Breadcrumbs {
143 fn set_active_pane_item(
144 &mut self,
145 active_pane_item: Option<&dyn ItemHandle>,
146 window: &mut Window,
147 cx: &mut Context<Self>,
148 ) -> ToolbarItemLocation {
149 cx.notify();
150 self.active_item = None;
151
152 let Some(item) = active_pane_item else {
153 return ToolbarItemLocation::Hidden;
154 };
155
156 let this = cx.entity().downgrade();
157 self.subscription = Some(item.subscribe_to_item_events(
158 window,
159 cx,
160 Box::new(move |event, _, cx| {
161 if let ItemEvent::UpdateBreadcrumbs = event {
162 this.update(cx, |this, cx| {
163 cx.notify();
164 if let Some(active_item) = this.active_item.as_ref() {
165 cx.emit(ToolbarItemEvent::ChangeLocation(
166 active_item.breadcrumb_location(cx),
167 ))
168 }
169 })
170 .ok();
171 }
172 }),
173 ));
174 self.active_item = Some(item.boxed_clone());
175 item.breadcrumb_location(cx)
176 }
177
178 fn pane_focus_update(
179 &mut self,
180 pane_focused: bool,
181 _window: &mut Window,
182 _: &mut Context<Self>,
183 ) {
184 self.pane_focused = pane_focused;
185 }
186}