1use gpui::ClickEvent;
2
3use crate::prelude::*;
4
5#[derive(IntoElement, RegisterComponent)]
6pub struct Callout {
7 title: SharedString,
8 message: Option<SharedString>,
9 icon: Icon,
10 cta_label: SharedString,
11 cta_action: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
12 line_height: Option<Pixels>,
13}
14
15impl Callout {
16 pub fn single_line(
17 title: impl Into<SharedString>,
18 icon: Icon,
19 cta_label: impl Into<SharedString>,
20 cta_action: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
21 ) -> Self {
22 Self {
23 title: title.into(),
24 message: None,
25 icon,
26 cta_label: cta_label.into(),
27 cta_action,
28 line_height: None,
29 }
30 }
31
32 pub fn multi_line(
33 title: impl Into<SharedString>,
34 message: impl Into<SharedString>,
35 icon: Icon,
36 cta_label: impl Into<SharedString>,
37 cta_action: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
38 ) -> Self {
39 Self {
40 title: title.into(),
41 message: Some(message.into()),
42 icon,
43 cta_label: cta_label.into(),
44 cta_action,
45 line_height: None,
46 }
47 }
48
49 pub fn line_height(mut self, line_height: Pixels) -> Self {
50 self.line_height = Some(line_height);
51 self
52 }
53}
54
55impl RenderOnce for Callout {
56 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
57 let line_height = self.line_height.unwrap_or(window.line_height());
58
59 h_flex()
60 .p_2()
61 .gap_2()
62 .w_full()
63 .items_center()
64 .justify_between()
65 .bg(cx.theme().colors().panel_background)
66 .border_t_1()
67 .border_color(cx.theme().colors().border)
68 .overflow_x_hidden()
69 .child(
70 h_flex()
71 .flex_shrink()
72 .overflow_hidden()
73 .gap_2()
74 .items_start()
75 .child(
76 h_flex()
77 .h(line_height)
78 .items_center()
79 .justify_center()
80 .child(self.icon),
81 )
82 .child(
83 v_flex()
84 .flex_shrink()
85 .overflow_hidden()
86 .child(
87 h_flex()
88 .h(line_height)
89 .items_center()
90 .child(Label::new(self.title).size(LabelSize::Small)),
91 )
92 .when_some(self.message, |this, message| {
93 this.child(
94 div()
95 .w_full()
96 .flex_1()
97 .child(message)
98 .text_ui_sm(cx)
99 .text_color(cx.theme().colors().text_muted),
100 )
101 }),
102 ),
103 )
104 .child(
105 div().flex_none().child(
106 Button::new("cta", self.cta_label)
107 .on_click(self.cta_action)
108 .style(ButtonStyle::Filled)
109 .label_size(LabelSize::Small),
110 ),
111 )
112 }
113}
114
115impl Component for Callout {
116 fn scope() -> ComponentScope {
117 ComponentScope::Notification
118 }
119
120 fn description() -> Option<&'static str> {
121 Some(
122 "Used to display a callout for situations where the user needs to know some information, and likely make a decision. This might be a thread running out of tokens, or running out of prompts on a plan and needing to upgrade.",
123 )
124 }
125
126 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
127 let callout_examples = vec![
128 single_example(
129 "Single Line",
130 Callout::single_line(
131 "Your settings contain deprecated values, please update them.",
132 Icon::new(IconName::Warning)
133 .color(Color::Warning)
134 .size(IconSize::Small),
135 "Backup & Update",
136 Box::new(|_, _, _| {}),
137 )
138 .into_any_element(),
139 )
140 .width(px(580.)),
141 single_example(
142 "Multi Line",
143 Callout::multi_line(
144 "Thread reached the token limit",
145 "Start a new thread from a summary to continue the conversation.",
146 Icon::new(IconName::X)
147 .color(Color::Error)
148 .size(IconSize::Small),
149 "Start New Thread",
150 Box::new(|_, _, _| {}),
151 )
152 .into_any_element(),
153 )
154 .width(px(580.)),
155 ];
156
157 Some(
158 example_group(callout_examples)
159 .vertical()
160 .into_any_element(),
161 )
162 }
163}