1use crate::{
2 ActiveTheme as _, RoleOverride, Sizable as _, StyledExt as _, h_flex,
3 shimmer::{ShimmerStyle, ShimmerText},
4 spinner::Spinner,
5};
6use gpui::{
7 AnimationExt as _, AnyElement, App, ElementId, InteractiveElement as _, IntoElement,
8 ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement,
9 Styled, StyledText, Window, div, prelude::FluentBuilder as _, px, relative, rems,
10};
11
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
14pub enum MarkerVariant {
15 #[default]
17 Plain,
18 Separator,
20 Border,
22}
23
24#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
26pub enum MarkerLoadingStyle {
27 #[default]
29 Spinner,
30 Shimmer,
32}
33
34enum MarkerChild {
35 Icon(MarkerIcon),
36 Content(MarkerContent),
37 Element(AnyElement),
38}
39
40#[derive(IntoElement)]
48pub struct Marker {
49 id: Option<ElementId>,
50 style: StyleRefinement,
51 separator_style: StyleRefinement,
52 variant: MarkerVariant,
53 loading: bool,
54 loading_style: MarkerLoadingStyle,
55 shimmer_style: ShimmerStyle,
56 role: RoleOverride,
57 children: Vec<MarkerChild>,
58}
59
60impl Marker {
61 pub fn new() -> Self {
63 Self {
64 id: None,
65 style: StyleRefinement::default(),
66 separator_style: StyleRefinement::default(),
67 variant: MarkerVariant::default(),
68 loading: false,
69 loading_style: MarkerLoadingStyle::default(),
70 shimmer_style: ShimmerStyle::default(),
71 role: RoleOverride::default(),
72 children: Vec::new(),
73 }
74 }
75
76 pub fn id(mut self, id: impl Into<ElementId>) -> Self {
78 self.id = Some(id.into());
79 self
80 }
81
82 pub fn role(mut self, role: impl Into<RoleOverride>) -> Self {
89 self.role = role.into();
90 self
91 }
92
93 pub fn with_variant(mut self, variant: MarkerVariant) -> Self {
95 self.variant = variant;
96 self
97 }
98
99 pub fn loading(mut self, loading: bool) -> Self {
101 self.loading = loading;
102 self
103 }
104
105 pub fn with_loading_style(mut self, loading_style: MarkerLoadingStyle) -> Self {
107 self.loading_style = loading_style;
108 self
109 }
110
111 pub fn with_shimmer_style(mut self, shimmer_style: ShimmerStyle) -> Self {
113 self.shimmer_style = shimmer_style;
114 self
115 }
116
117 pub fn separator_style(mut self, style: StyleRefinement) -> Self {
119 self.separator_style = style;
120 self
121 }
122
123 pub fn icon(mut self, icon: MarkerIcon) -> Self {
125 self.children.push(MarkerChild::Icon(icon));
126 self
127 }
128
129 pub fn content(mut self, content: MarkerContent) -> Self {
131 self.children.push(MarkerChild::Content(content));
132 self
133 }
134}
135
136impl Default for Marker {
137 fn default() -> Self {
138 Self::new()
139 }
140}
141
142impl ParentElement for Marker {
143 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
144 self.children
145 .extend(elements.into_iter().map(MarkerChild::Element));
146 }
147}
148
149impl Styled for Marker {
150 fn style(&mut self) -> &mut StyleRefinement {
151 &mut self.style
152 }
153}
154
155impl RenderOnce for Marker {
156 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
157 let tokens = cx.theme().semantic_tokens();
158 let variant = self.variant;
159 let loading = self.loading;
160 let loading_style = self.loading_style;
161 let shimmer_style = self.shimmer_style;
162 let has_icon = self
163 .children
164 .iter()
165 .any(|child| matches!(child, MarkerChild::Icon(_)));
166 let role = self.role;
167 let separator_style = self.separator_style;
168 let children = self.children.into_iter().map(move |child| match child {
169 MarkerChild::Icon(icon) => icon.into_any_element(),
170 MarkerChild::Content(mut content) => {
171 content.shimmer = loading && loading_style == MarkerLoadingStyle::Shimmer;
172 content.shimmer_style = shimmer_style;
173 content.separator = variant == MarkerVariant::Separator;
174 content.into_any_element()
175 }
176 MarkerChild::Element(element) => element,
177 });
178
179 let row = h_flex()
180 .w_full()
181 .min_h(rems(1.))
182 .gap_2()
183 .text_sm()
184 .line_height(relative(1.5))
185 .text_color(tokens.colors.muted_foreground)
186 .text_left()
187 .when(variant == MarkerVariant::Separator, |this| {
188 this.justify_center()
189 })
190 .when(variant == MarkerVariant::Border, |this| {
191 this.border_b_1().border_color(tokens.colors.border).pb_2()
192 })
193 .when(variant == MarkerVariant::Separator, |this| {
194 this.child(
195 div()
196 .flex_1()
197 .min_w_0()
198 .h(px(1.))
199 .mr_1()
200 .bg(tokens.colors.border)
201 .refine_style(&separator_style),
202 )
203 })
204 .when(
205 loading && loading_style == MarkerLoadingStyle::Spinner && !has_icon,
206 |this| this.child(MarkerIcon::new().child(Spinner::new().xsmall())),
207 )
208 .children(children)
209 .when(variant == MarkerVariant::Separator, |this| {
210 this.child(
211 div()
212 .flex_1()
213 .min_w_0()
214 .h(px(1.))
215 .ml_1()
216 .bg(tokens.colors.border)
217 .refine_style(&separator_style),
218 )
219 })
220 .refine_style(&self.style);
221
222 match (self.id, role) {
225 (Some(id), RoleOverride::Role(role)) => row.id(id).role(role).into_any_element(),
226 (Some(id), _) => row.id(id).into_any_element(),
227 (None, _) => row.into_any_element(),
228 }
229 }
230}
231
232#[derive(IntoElement)]
234pub struct MarkerIcon {
235 style: StyleRefinement,
236 children: Vec<AnyElement>,
237}
238
239impl MarkerIcon {
240 pub fn new() -> Self {
242 Self {
243 style: StyleRefinement::default(),
244 children: Vec::new(),
245 }
246 }
247}
248
249impl Default for MarkerIcon {
250 fn default() -> Self {
251 Self::new()
252 }
253}
254
255impl ParentElement for MarkerIcon {
256 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
257 self.children.extend(elements);
258 }
259}
260
261impl Styled for MarkerIcon {
262 fn style(&mut self) -> &mut StyleRefinement {
263 &mut self.style
264 }
265}
266
267impl RenderOnce for MarkerIcon {
268 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
269 h_flex()
270 .size_4()
271 .flex_none()
272 .items_center()
273 .justify_center()
274 .refine_style(&self.style)
275 .children(self.children)
276 }
277}
278
279#[derive(IntoElement)]
281pub struct MarkerContent {
282 style: StyleRefinement,
283 shimmer: bool,
284 shimmer_style: ShimmerStyle,
285 separator: bool,
286 children: Vec<MarkerContentChild>,
287}
288
289enum MarkerContentChild {
290 Text(SharedString),
291 Element(AnyElement),
292}
293
294impl MarkerContent {
295 pub fn new() -> Self {
297 Self {
298 style: StyleRefinement::default(),
299 shimmer: false,
300 shimmer_style: ShimmerStyle::default(),
301 separator: false,
302 children: Vec::new(),
303 }
304 }
305
306 pub fn text(mut self, text: impl Into<SharedString>) -> Self {
310 self.children.push(MarkerContentChild::Text(text.into()));
311 self
312 }
313}
314
315impl Default for MarkerContent {
316 fn default() -> Self {
317 Self::new()
318 }
319}
320
321impl ParentElement for MarkerContent {
322 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
323 self.children
324 .extend(elements.into_iter().map(MarkerContentChild::Element));
325 }
326}
327
328impl Styled for MarkerContent {
329 fn style(&mut self) -> &mut StyleRefinement {
330 &mut self.style
331 }
332}
333
334impl RenderOnce for MarkerContent {
335 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
336 let animate = self.shimmer && !cx.reduce_motion();
337 let has_text = self
338 .children
339 .iter()
340 .any(|child| matches!(child, MarkerContentChild::Text(_)));
341 let base_opacity = self.style.opacity.unwrap_or(1.);
342 let shimmer_style = self.shimmer_style;
343 let children =
344 self.children
345 .into_iter()
346 .enumerate()
347 .map(move |(index, child)| match child {
348 MarkerContentChild::Text(text) if animate => ShimmerText::new(text)
349 .id(("marker-loading-text", index))
350 .with_shimmer_style(shimmer_style)
351 .into_any_element(),
352 MarkerContentChild::Text(text) => StyledText::new(text).into_any_element(),
353 MarkerContentChild::Element(element) => element,
354 });
355
356 let content = div()
357 .min_w_0()
358 .when(self.separator, |this| this.flex_none().text_center())
359 .refine_style(&self.style)
360 .children(children);
361
362 if animate && !has_text {
363 content
364 .with_animation(
365 "marker-loading-content",
366 shimmer_style.animation(),
367 move |this, phase| {
368 let highlight = (phase * std::f32::consts::TAU).cos().mul_add(0.5, 0.5);
369 this.opacity(base_opacity * highlight.mul_add(0.4, 0.6))
370 },
371 )
372 .into_any_element()
373 } else {
374 content.into_any_element()
375 }
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 #[test]
384 fn test_marker_builder() {
385 let marker = Marker::new()
386 .with_variant(MarkerVariant::Separator)
387 .loading(true)
388 .with_loading_style(MarkerLoadingStyle::Shimmer)
389 .with_shimmer_style(ShimmerStyle::new().reverse(true))
390 .separator_style(StyleRefinement::default())
391 .content(MarkerContent::new().child("Today"));
392
393 assert_eq!(marker.variant, MarkerVariant::Separator);
394 assert!(marker.loading);
395 assert_eq!(marker.loading_style, MarkerLoadingStyle::Shimmer);
396 assert_eq!(marker.children.len(), 1);
397 assert_eq!(Marker::default().variant, MarkerVariant::Plain);
398 assert!(!Marker::default().loading);
399 assert_eq!(Marker::default().loading_style, MarkerLoadingStyle::Spinner);
400
401 let content_first = Marker::new()
402 .content(MarkerContent::new().text("Thinking"))
403 .with_loading_style(MarkerLoadingStyle::Shimmer)
404 .loading(true);
405 assert!(content_first.loading);
406 assert_eq!(content_first.loading_style, MarkerLoadingStyle::Shimmer);
407 assert!(matches!(
408 &content_first.children[0],
409 MarkerChild::Content(_)
410 ));
411
412 let custom_icon = Marker::new()
413 .loading(true)
414 .icon(MarkerIcon::new().child("custom"))
415 .content(MarkerContent::new().text("Loading"));
416 assert_eq!(custom_icon.children.len(), 2);
417 assert!(matches!(&custom_icon.children[0], MarkerChild::Icon(_)));
418
419 assert_eq!(Marker::default().role, RoleOverride::default());
420 assert!(Marker::default().id.is_none());
421 let status = Marker::new().id("sync-status").role(gpui::Role::Status);
422 assert_eq!(status.id, Some("sync-status".into()));
423 assert_eq!(status.role, RoleOverride::Role(gpui::Role::Status));
424
425 let styled = Marker::new().opacity(0.37).child("Status").child("Details");
426
427 assert_eq!(styled.style.opacity, Some(0.37));
428 assert_eq!(styled.children.len(), 2);
429
430 let icon = MarkerIcon::new().child("icon");
431 assert_eq!(icon.children.len(), 1);
432
433 let content = MarkerContent::new()
434 .text("Thinking")
435 .child("…")
436 .text("正在思考");
437 assert_eq!(content.children.len(), 3);
438 assert!(matches!(&content.children[0], MarkerContentChild::Text(_)));
439 assert!(matches!(
440 &content.children[1],
441 MarkerContentChild::Element(_)
442 ));
443 assert!(matches!(&content.children[2], MarkerContentChild::Text(_)));
444 }
445}