1use editor::Editor;
2use gpui::{
3 Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
4 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 Breadcrumbs {
22 pub fn new() -> Self {
23 Self {
24 pane_focused: false,
25 active_item: Default::default(),
26 subscription: Default::default(),
27 }
28 }
29}
30
31impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
32
33impl Render for Breadcrumbs {
34 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
35 const MAX_SEGMENTS: usize = 12;
36 let element = h_flex().text_ui(cx);
37 let Some(active_item) = self.active_item.as_ref() else {
38 return element;
39 };
40 let Some(mut segments) = active_item.breadcrumbs(cx.theme(), cx) else {
41 return element;
42 };
43
44 let prefix_end_ix = cmp::min(segments.len(), MAX_SEGMENTS / 2);
45 let suffix_start_ix = cmp::max(
46 prefix_end_ix,
47 segments.len().saturating_sub(MAX_SEGMENTS / 2),
48 );
49 if suffix_start_ix > prefix_end_ix {
50 segments.splice(
51 prefix_end_ix..suffix_start_ix,
52 Some(BreadcrumbText {
53 text: "⋯".into(),
54 highlights: None,
55 font: None,
56 }),
57 );
58 }
59
60 let highlighted_segments = segments.into_iter().map(|segment| {
61 let mut text_style = cx.text_style();
62 if let Some(font) = segment.font {
63 text_style.font_family = font.family;
64 text_style.font_features = font.features;
65 text_style.font_style = font.style;
66 text_style.font_weight = font.weight;
67 }
68 text_style.color = Color::Muted.color(cx);
69
70 StyledText::new(segment.text.replace('\n', ""))
71 .with_highlights(&text_style, segment.highlights.unwrap_or_default())
72 .into_any()
73 });
74 let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
75 Label::new("›").color(Color::Muted).into_any_element()
76 });
77
78 let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
79 match active_item
80 .downcast::<Editor>()
81 .map(|editor| editor.downgrade())
82 {
83 Some(editor) => element.child(
84 ButtonLike::new("toggle outline view")
85 .child(breadcrumbs_stack)
86 .style(ButtonStyle::Subtle)
87 .on_click(move |_, cx| {
88 if let Some(editor) = editor.upgrade() {
89 outline::toggle(editor, &editor::actions::ToggleOutline, cx)
90 }
91 })
92 .tooltip(|cx| {
93 Tooltip::for_action(
94 "Show symbol outline",
95 &editor::actions::ToggleOutline,
96 cx,
97 )
98 }),
99 ),
100 None => element
101 // Match the height of the `ButtonLike` in the other arm.
102 .h(rems_from_px(22.))
103 .child(breadcrumbs_stack),
104 }
105 }
106}
107
108impl ToolbarItemView for Breadcrumbs {
109 fn set_active_pane_item(
110 &mut self,
111 active_pane_item: Option<&dyn ItemHandle>,
112 cx: &mut ViewContext<Self>,
113 ) -> ToolbarItemLocation {
114 cx.notify();
115 self.active_item = None;
116 if let Some(item) = active_pane_item {
117 let this = cx.view().downgrade();
118 self.subscription = Some(item.subscribe_to_item_events(
119 cx,
120 Box::new(move |event, cx| {
121 if let ItemEvent::UpdateBreadcrumbs = event {
122 this.update(cx, |this, cx| {
123 cx.notify();
124 if let Some(active_item) = this.active_item.as_ref() {
125 cx.emit(ToolbarItemEvent::ChangeLocation(
126 active_item.breadcrumb_location(cx),
127 ))
128 }
129 })
130 .ok();
131 }
132 }),
133 ));
134 self.active_item = Some(item.boxed_clone());
135 item.breadcrumb_location(cx)
136 } else {
137 ToolbarItemLocation::Hidden
138 }
139 }
140
141 fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
142 self.pane_focused = pane_focused;
143 }
144}