1use editor::Editor;
2use gpui::{
3 Element, EventEmitter, FocusableView, IntoElement, ParentElement, Render, StyledText,
4 Subscription, ViewContext,
5};
6use itertools::Itertools;
7use std::cmp;
8use theme::ActiveTheme;
9use ui::{prelude::*, ButtonLike, ButtonStyle, Label, Tooltip};
10use workspace::{
11 item::{BreadcrumbText, ItemEvent, ItemHandle},
12 ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
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, cx: &mut ViewContext<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 = cx.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_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 |_, cx| {
105 if let Some((editor, callback)) = editor
106 .upgrade()
107 .zip(zed_actions::outline::TOGGLE_OUTLINE.get())
108 {
109 callback(editor.to_any(), cx);
110 }
111 }
112 })
113 .tooltip(move |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 cx,
121 )
122 } else {
123 Tooltip::for_action(
124 "Show Symbol Outline",
125 &zed_actions::outline::ToggleOutline,
126 cx,
127 )
128 }
129 }),
130 ),
131 None => element
132 // Match the height of the `ButtonLike` in the other arm.
133 .h(rems_from_px(22.))
134 .child(breadcrumbs_stack),
135 }
136 }
137}
138
139impl ToolbarItemView for Breadcrumbs {
140 fn set_active_pane_item(
141 &mut self,
142 active_pane_item: Option<&dyn ItemHandle>,
143 cx: &mut ViewContext<Self>,
144 ) -> ToolbarItemLocation {
145 cx.notify();
146 self.active_item = None;
147
148 let Some(item) = active_pane_item else {
149 return ToolbarItemLocation::Hidden;
150 };
151
152 let this = cx.view().downgrade();
153 self.subscription = Some(item.subscribe_to_item_events(
154 cx,
155 Box::new(move |event, cx| {
156 if let ItemEvent::UpdateBreadcrumbs = event {
157 this.update(cx, |this, cx| {
158 cx.notify();
159 if let Some(active_item) = this.active_item.as_ref() {
160 cx.emit(ToolbarItemEvent::ChangeLocation(
161 active_item.breadcrumb_location(cx),
162 ))
163 }
164 })
165 .ok();
166 }
167 }),
168 ));
169 self.active_item = Some(item.boxed_clone());
170 item.breadcrumb_location(cx)
171 }
172
173 fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
174 self.pane_focused = pane_focused;
175 }
176}