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
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum MarkerAlignment {
37 Start,
39 Center,
41 End,
43}
44
45enum MarkerChild {
46 Icon(MarkerIcon),
47 Content(MarkerContent),
48 Element(AnyElement),
49}
50
51#[derive(IntoElement)]
59pub struct Marker {
60 id: Option<ElementId>,
61 style: StyleRefinement,
62 separator_style: StyleRefinement,
63 variant: MarkerVariant,
64 alignment: Option<MarkerAlignment>,
65 loading: bool,
66 loading_style: MarkerLoadingStyle,
67 shimmer_style: ShimmerStyle,
68 role: RoleOverride,
69 children: Vec<MarkerChild>,
70}
71
72impl Marker {
73 pub fn new() -> Self {
75 Self {
76 id: None,
77 style: StyleRefinement::default(),
78 separator_style: StyleRefinement::default(),
79 variant: MarkerVariant::default(),
80 alignment: None,
81 loading: false,
82 loading_style: MarkerLoadingStyle::default(),
83 shimmer_style: ShimmerStyle::default(),
84 role: RoleOverride::default(),
85 children: Vec::new(),
86 }
87 }
88
89 pub fn id(mut self, id: impl Into<ElementId>) -> Self {
91 self.id = Some(id.into());
92 self
93 }
94
95 pub fn role(mut self, role: impl Into<RoleOverride>) -> Self {
102 self.role = role.into();
103 self
104 }
105
106 pub fn with_variant(mut self, variant: MarkerVariant) -> Self {
108 self.variant = variant;
109 self
110 }
111
112 pub fn alignment(mut self, alignment: MarkerAlignment) -> Self {
120 self.alignment = Some(alignment);
121 self
122 }
123
124 fn resolved_alignment(&self) -> MarkerAlignment {
125 self.alignment.unwrap_or(match self.variant {
126 MarkerVariant::Separator => MarkerAlignment::Center,
127 MarkerVariant::Plain | MarkerVariant::Border => MarkerAlignment::Start,
128 })
129 }
130
131 pub fn loading(mut self, loading: bool) -> Self {
133 self.loading = loading;
134 self
135 }
136
137 pub fn with_loading_style(mut self, loading_style: MarkerLoadingStyle) -> Self {
139 self.loading_style = loading_style;
140 self
141 }
142
143 pub fn with_shimmer_style(mut self, shimmer_style: ShimmerStyle) -> Self {
145 self.shimmer_style = shimmer_style;
146 self
147 }
148
149 pub fn separator_style(mut self, style: StyleRefinement) -> Self {
151 self.separator_style = style;
152 self
153 }
154
155 pub fn icon(mut self, icon: MarkerIcon) -> Self {
157 self.children.push(MarkerChild::Icon(icon));
158 self
159 }
160
161 pub fn content(mut self, content: MarkerContent) -> Self {
163 self.children.push(MarkerChild::Content(content));
164 self
165 }
166}
167
168impl Default for Marker {
169 fn default() -> Self {
170 Self::new()
171 }
172}
173
174impl ParentElement for Marker {
175 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
176 self.children
177 .extend(elements.into_iter().map(MarkerChild::Element));
178 }
179}
180
181impl Styled for Marker {
182 fn style(&mut self) -> &mut StyleRefinement {
183 &mut self.style
184 }
185}
186
187impl RenderOnce for Marker {
188 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
189 let tokens = cx.theme().semantic_tokens();
190 let variant = self.variant;
191 let alignment = self.resolved_alignment();
192 let loading = self.loading;
193 let loading_style = self.loading_style;
194 let shimmer_style = self.shimmer_style;
195 let has_icon = self
196 .children
197 .iter()
198 .any(|child| matches!(child, MarkerChild::Icon(_)));
199 let role = self.role;
200 let separator_style = self.separator_style;
201 let children = self.children.into_iter().map(move |child| match child {
202 MarkerChild::Icon(icon) => icon.into_any_element(),
203 MarkerChild::Content(mut content) => {
204 content.shimmer = loading && loading_style == MarkerLoadingStyle::Shimmer;
205 content.shimmer_style = shimmer_style;
206 content.separator = variant == MarkerVariant::Separator;
207 content.alignment = alignment;
208 content.into_any_element()
209 }
210 MarkerChild::Element(element) => element,
211 });
212
213 let row = h_flex()
214 .w_full()
215 .min_h(rems(1.))
216 .gap_2()
217 .text_sm()
218 .line_height(relative(1.5))
219 .text_color(tokens.colors.muted_foreground)
220 .map(|this| match alignment {
221 MarkerAlignment::Start => this.justify_start().text_left(),
222 MarkerAlignment::Center => this.justify_center().text_center(),
223 MarkerAlignment::End => this.justify_end().text_right(),
224 })
225 .when(variant == MarkerVariant::Border, |this| {
226 this.border_b_1().border_color(tokens.colors.border).pb_2()
227 })
228 .when(
229 variant == MarkerVariant::Separator && alignment != MarkerAlignment::Start,
230 |this| {
231 this.child(
232 div()
233 .flex_1()
234 .min_w_0()
235 .h(px(1.))
236 .mr_1()
237 .bg(tokens.colors.border)
238 .refine_style(&separator_style),
239 )
240 },
241 )
242 .when(
243 loading && loading_style == MarkerLoadingStyle::Spinner && !has_icon,
244 |this| this.child(MarkerIcon::new().child(Spinner::new().xsmall())),
245 )
246 .children(children)
247 .when(
248 variant == MarkerVariant::Separator && alignment != MarkerAlignment::End,
249 |this| {
250 this.child(
251 div()
252 .flex_1()
253 .min_w_0()
254 .h(px(1.))
255 .ml_1()
256 .bg(tokens.colors.border)
257 .refine_style(&separator_style),
258 )
259 },
260 )
261 .refine_style(&self.style);
262
263 match (self.id, role) {
266 (Some(id), RoleOverride::Role(role)) => row.id(id).role(role).into_any_element(),
267 (Some(id), _) => row.id(id).into_any_element(),
268 (None, _) => row.into_any_element(),
269 }
270 }
271}
272
273#[derive(IntoElement)]
275pub struct MarkerIcon {
276 style: StyleRefinement,
277 children: Vec<AnyElement>,
278}
279
280impl MarkerIcon {
281 pub fn new() -> Self {
283 Self {
284 style: StyleRefinement::default(),
285 children: Vec::new(),
286 }
287 }
288}
289
290impl Default for MarkerIcon {
291 fn default() -> Self {
292 Self::new()
293 }
294}
295
296impl ParentElement for MarkerIcon {
297 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
298 self.children.extend(elements);
299 }
300}
301
302impl Styled for MarkerIcon {
303 fn style(&mut self) -> &mut StyleRefinement {
304 &mut self.style
305 }
306}
307
308impl RenderOnce for MarkerIcon {
309 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
310 h_flex()
311 .size_4()
312 .flex_none()
313 .items_center()
314 .justify_center()
315 .refine_style(&self.style)
316 .children(self.children)
317 }
318}
319
320#[derive(IntoElement)]
322pub struct MarkerContent {
323 style: StyleRefinement,
324 shimmer: bool,
325 shimmer_style: ShimmerStyle,
326 separator: bool,
327 alignment: MarkerAlignment,
328 children: Vec<MarkerContentChild>,
329}
330
331enum MarkerContentChild {
332 Text(SharedString),
333 Element(AnyElement),
334}
335
336impl MarkerContent {
337 pub fn new() -> Self {
339 Self {
340 style: StyleRefinement::default(),
341 shimmer: false,
342 shimmer_style: ShimmerStyle::default(),
343 separator: false,
344 alignment: MarkerAlignment::Start,
345 children: Vec::new(),
346 }
347 }
348
349 pub fn text(mut self, text: impl Into<SharedString>) -> Self {
353 self.children.push(MarkerContentChild::Text(text.into()));
354 self
355 }
356}
357
358impl Default for MarkerContent {
359 fn default() -> Self {
360 Self::new()
361 }
362}
363
364impl ParentElement for MarkerContent {
365 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
366 self.children
367 .extend(elements.into_iter().map(MarkerContentChild::Element));
368 }
369}
370
371impl Styled for MarkerContent {
372 fn style(&mut self) -> &mut StyleRefinement {
373 &mut self.style
374 }
375}
376
377impl RenderOnce for MarkerContent {
378 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
379 let animate = self.shimmer && !cx.reduce_motion();
380 let has_text = self
381 .children
382 .iter()
383 .any(|child| matches!(child, MarkerContentChild::Text(_)));
384 let base_opacity = self.style.opacity.unwrap_or(1.);
385 let shimmer_style = self.shimmer_style;
386 let children =
387 self.children
388 .into_iter()
389 .enumerate()
390 .map(move |(index, child)| match child {
391 MarkerContentChild::Text(text) if animate => ShimmerText::new(text)
392 .id(("marker-loading-text", index))
393 .with_shimmer_style(shimmer_style)
394 .into_any_element(),
395 MarkerContentChild::Text(text) => StyledText::new(text).into_any_element(),
396 MarkerContentChild::Element(element) => element,
397 });
398
399 let content = div()
400 .min_w_0()
401 .when(self.separator, |this| this.flex_none())
404 .map(|this| match self.alignment {
405 MarkerAlignment::Start => this.text_left(),
406 MarkerAlignment::Center => this.text_center(),
407 MarkerAlignment::End => this.text_right(),
408 })
409 .refine_style(&self.style)
410 .children(children);
411
412 if animate && !has_text {
413 content
414 .with_animation(
415 "marker-loading-content",
416 shimmer_style.animation(),
417 move |this, phase| {
418 let highlight = (phase * std::f32::consts::TAU).cos().mul_add(0.5, 0.5);
419 this.opacity(base_opacity * highlight.mul_add(0.4, 0.6))
420 },
421 )
422 .into_any_element()
423 } else {
424 content.into_any_element()
425 }
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn test_marker_builder() {
435 let marker = Marker::new()
436 .with_variant(MarkerVariant::Separator)
437 .loading(true)
438 .with_loading_style(MarkerLoadingStyle::Shimmer)
439 .with_shimmer_style(ShimmerStyle::new().reverse(true))
440 .separator_style(StyleRefinement::default())
441 .content(MarkerContent::new().child("Today"));
442
443 assert_eq!(marker.variant, MarkerVariant::Separator);
444 assert!(marker.loading);
445 assert_eq!(marker.loading_style, MarkerLoadingStyle::Shimmer);
446 assert_eq!(marker.children.len(), 1);
447 assert_eq!(Marker::default().variant, MarkerVariant::Plain);
448 assert!(Marker::default().alignment.is_none());
449 assert!(!Marker::default().loading);
450 assert_eq!(Marker::default().loading_style, MarkerLoadingStyle::Spinner);
451
452 let centered = Marker::new().alignment(MarkerAlignment::Center);
453 assert_eq!(centered.alignment, Some(MarkerAlignment::Center));
454
455 let content_first = Marker::new()
456 .content(MarkerContent::new().text("Thinking"))
457 .with_loading_style(MarkerLoadingStyle::Shimmer)
458 .loading(true);
459 assert!(content_first.loading);
460 assert_eq!(content_first.loading_style, MarkerLoadingStyle::Shimmer);
461 assert!(matches!(
462 &content_first.children[0],
463 MarkerChild::Content(_)
464 ));
465
466 let custom_icon = Marker::new()
467 .loading(true)
468 .icon(MarkerIcon::new().child("custom"))
469 .content(MarkerContent::new().text("Loading"));
470 assert_eq!(custom_icon.children.len(), 2);
471 assert!(matches!(&custom_icon.children[0], MarkerChild::Icon(_)));
472
473 assert_eq!(Marker::default().role, RoleOverride::default());
474 assert!(Marker::default().id.is_none());
475 let status = Marker::new().id("sync-status").role(gpui::Role::Status);
476 assert_eq!(status.id, Some("sync-status".into()));
477 assert_eq!(status.role, RoleOverride::Role(gpui::Role::Status));
478
479 let styled = Marker::new().opacity(0.37).child("Status").child("Details");
480
481 assert_eq!(styled.style.opacity, Some(0.37));
482 assert_eq!(styled.children.len(), 2);
483
484 let icon = MarkerIcon::new().child("icon");
485 assert_eq!(icon.children.len(), 1);
486
487 let content = MarkerContent::new()
488 .text("Thinking")
489 .child("…")
490 .text("正在思考");
491 assert_eq!(content.children.len(), 3);
492 assert!(matches!(&content.children[0], MarkerContentChild::Text(_)));
493 assert!(matches!(
494 &content.children[1],
495 MarkerContentChild::Element(_)
496 ));
497 assert!(matches!(&content.children[2], MarkerContentChild::Text(_)));
498 }
499
500 #[test]
501 fn test_marker_resolved_alignment() {
502 assert_eq!(Marker::new().resolved_alignment(), MarkerAlignment::Start);
504 assert_eq!(
505 Marker::new()
506 .with_variant(MarkerVariant::Border)
507 .resolved_alignment(),
508 MarkerAlignment::Start
509 );
510 assert_eq!(
511 Marker::new()
512 .with_variant(MarkerVariant::Separator)
513 .resolved_alignment(),
514 MarkerAlignment::Center
515 );
516
517 assert_eq!(
519 Marker::new()
520 .alignment(MarkerAlignment::End)
521 .with_variant(MarkerVariant::Separator)
522 .resolved_alignment(),
523 MarkerAlignment::End
524 );
525 assert_eq!(
526 Marker::new()
527 .with_variant(MarkerVariant::Plain)
528 .alignment(MarkerAlignment::Center)
529 .resolved_alignment(),
530 MarkerAlignment::Center
531 );
532 }
533}