Skip to repository content365 lines · 13.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:43:39.651Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
callout.rs
1use gpui::AnyElement;
2
3use crate::prelude::*;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum CalloutBorderPosition {
7 Top,
8 Bottom,
9}
10
11/// A callout component for displaying important information that requires user attention.
12///
13/// # Usage Example
14///
15/// ```
16/// use ui::prelude::*;
17/// use ui::{Button, Callout, IconName, Label, Severity};
18///
19/// let callout = Callout::new()
20/// .severity(Severity::Warning)
21/// .icon(IconName::Warning)
22/// .title("Be aware of your subscription!")
23/// .description("Your subscription is about to expire. Renew now!")
24/// .actions_slot(Button::new("renew", "Renew Now"));
25/// ```
26///
27#[derive(IntoElement, RegisterComponent)]
28pub struct Callout {
29 severity: Severity,
30 icon: Option<IconName>,
31 title: Option<SharedString>,
32 description: Option<SharedString>,
33 description_slot: Option<AnyElement>,
34 actions_slot: Option<AnyElement>,
35 dismiss_action: Option<AnyElement>,
36 line_height: Option<Pixels>,
37 border_position: CalloutBorderPosition,
38}
39
40impl Callout {
41 /// Creates a new `Callout` component with default styling.
42 pub fn new() -> Self {
43 Self {
44 severity: Severity::Info,
45 icon: None,
46 title: None,
47 description: None,
48 description_slot: None,
49 actions_slot: None,
50 dismiss_action: None,
51 line_height: None,
52 border_position: CalloutBorderPosition::Top,
53 }
54 }
55
56 /// Sets the severity of the callout.
57 pub fn severity(mut self, severity: Severity) -> Self {
58 self.severity = severity;
59 self
60 }
61
62 /// Sets the icon to display in the callout.
63 pub fn icon(mut self, icon: IconName) -> Self {
64 self.icon = Some(icon);
65 self
66 }
67
68 /// Sets the title of the callout.
69 pub fn title(mut self, title: impl Into<SharedString>) -> Self {
70 self.title = Some(title.into());
71 self
72 }
73
74 /// Sets the description of the callout.
75 /// The description can be single or multi-line text.
76 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
77 self.description = Some(description.into());
78 self
79 }
80
81 /// Allows for any element—like markdown elements—to fill the description slot of the callout.
82 /// This method wins over `description` if both happen to be set.
83 pub fn description_slot(mut self, description: impl IntoElement) -> Self {
84 self.description_slot = Some(description.into_any_element());
85 self
86 }
87
88 /// Sets the primary call-to-action button.
89 pub fn actions_slot(mut self, action: impl IntoElement) -> Self {
90 self.actions_slot = Some(action.into_any_element());
91 self
92 }
93
94 /// Sets an optional dismiss button, which is usually an icon button with a close icon.
95 /// This button is always rendered as the last one to the far right.
96 pub fn dismiss_action(mut self, action: impl IntoElement) -> Self {
97 self.dismiss_action = Some(action.into_any_element());
98 self
99 }
100
101 /// Sets a custom line height for the callout content.
102 pub fn line_height(mut self, line_height: Pixels) -> Self {
103 self.line_height = Some(line_height);
104 self
105 }
106
107 /// Sets the border position in the callout.
108 pub fn border_position(mut self, border_position: CalloutBorderPosition) -> Self {
109 self.border_position = border_position;
110 self
111 }
112}
113
114impl RenderOnce for Callout {
115 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
116 let line_height = self.line_height.unwrap_or(window.line_height());
117
118 let has_actions = self.actions_slot.is_some() || self.dismiss_action.is_some();
119
120 let (icon, icon_color, bg_color) = match self.severity {
121 Severity::Info => (
122 IconName::Info,
123 Color::Muted,
124 cx.theme().status().info_background.opacity(0.1),
125 ),
126 Severity::Success => (
127 IconName::Check,
128 Color::Success,
129 cx.theme().status().success.opacity(0.1),
130 ),
131 Severity::Warning => (
132 IconName::Warning,
133 Color::Warning,
134 cx.theme().status().warning_background.opacity(0.2),
135 ),
136 Severity::Error => (
137 IconName::XCircle,
138 Color::Error,
139 cx.theme().status().error.opacity(0.08),
140 ),
141 };
142
143 h_flex()
144 .min_w_0()
145 .w_full()
146 .p_2()
147 .gap_2()
148 .items_start()
149 .map(|this| match self.border_position {
150 CalloutBorderPosition::Top => this.border_t_1(),
151 CalloutBorderPosition::Bottom => this.border_b_1(),
152 })
153 .border_color(cx.theme().colors().border)
154 .bg(bg_color)
155 .overflow_x_hidden()
156 .when(self.icon.is_some(), |this| {
157 this.child(
158 h_flex()
159 .h(line_height)
160 .justify_center()
161 .child(Icon::new(icon).size(IconSize::Small).color(icon_color)),
162 )
163 })
164 .child(
165 v_flex()
166 .min_w_0()
167 .min_h_0()
168 .w_full()
169 .child(
170 h_flex()
171 .min_h(line_height)
172 .w_full()
173 .gap_1()
174 .justify_between()
175 .flex_wrap()
176 .when_some(self.title, |this, title| {
177 this.child(
178 div()
179 .min_w_0()
180 .flex_1()
181 .child(Label::new(title).size(LabelSize::Small)),
182 )
183 })
184 .when(has_actions, |this| {
185 this.child(
186 h_flex()
187 .gap_0p5()
188 .when_some(self.actions_slot, |this, action| {
189 this.child(action)
190 })
191 .when_some(self.dismiss_action, |this, action| {
192 this.child(action)
193 }),
194 )
195 }),
196 )
197 .map(|this| {
198 let base_desc_container = div()
199 .id("callout-description-slot")
200 .w_full()
201 .max_h_32()
202 .flex_1()
203 .overflow_y_scroll()
204 .text_ui_sm(cx);
205
206 if let Some(description_slot) = self.description_slot {
207 this.child(base_desc_container.child(description_slot))
208 } else if let Some(description) = self.description {
209 this.child(
210 base_desc_container
211 .text_color(cx.theme().colors().text_muted)
212 .child(description),
213 )
214 } else {
215 this
216 }
217 }),
218 )
219 }
220}
221
222impl Component for Callout {
223 fn scope() -> ComponentScope {
224 ComponentScope::DataDisplay
225 }
226
227 fn description() -> &'static str {
228 "Used to display a callout for situations where the user \
229 needs to know some information, and likely make a decision. \
230 This might be a thread running out of tokens, \
231 or running out of prompts on a plan and needing to upgrade."
232 }
233
234 fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
235 let single_action = || Button::new("got-it", "Got it").label_size(LabelSize::Small);
236 let multiple_actions = || {
237 h_flex()
238 .gap_0p5()
239 .child(Button::new("update", "Backup & Update").label_size(LabelSize::Small))
240 .child(Button::new("dismiss", "Dismiss").label_size(LabelSize::Small))
241 };
242
243 let basic_examples = vec![
244 single_example(
245 "Simple with Title Only",
246 Callout::new()
247 .icon(IconName::Info)
248 .title("System maintenance scheduled for tonight")
249 .actions_slot(single_action())
250 .into_any_element(),
251 )
252 .width(px(580.)),
253 single_example(
254 "With Title and Description",
255 Callout::new()
256 .icon(IconName::Warning)
257 .title("Your settings contain deprecated values")
258 .description(
259 "We'll backup your current settings and update them to the new format.",
260 )
261 .actions_slot(single_action())
262 .into_any_element(),
263 )
264 .width(px(580.)),
265 single_example(
266 "Error with Multiple Actions",
267 Callout::new()
268 .icon(IconName::Close)
269 .title("Thread reached the token limit")
270 .description("Start a new thread from a summary to continue the conversation.")
271 .actions_slot(multiple_actions())
272 .into_any_element(),
273 )
274 .width(px(580.)),
275 single_example(
276 "Multi-line Description",
277 Callout::new()
278 .icon(IconName::Sparkle)
279 .title("Upgrade to Pro")
280 .description("• Unlimited threads\n• Priority support\n• Advanced analytics")
281 .actions_slot(multiple_actions())
282 .into_any_element(),
283 )
284 .width(px(580.)),
285 single_example(
286 "Scrollable Long Description",
287 Callout::new()
288 .severity(Severity::Error)
289 .icon(IconName::XCircle)
290 .title("Very Long API Error Description")
291 .description_slot(
292 v_flex().gap_1().children(
293 [
294 "You exceeded your current quota.",
295 "For more information, visit the docs.",
296 "Error details:",
297 "• Quota exceeded for metric",
298 "• Limit: 0",
299 "• Model: gemini-3.1-pro",
300 "Please retry in 26.33s.",
301 "Additional details:",
302 "- Request ID: abc123def456",
303 "- Timestamp: 2024-01-15T10:30:00Z",
304 "- Region: us-central1",
305 "- Service: generativelanguage.googleapis.com",
306 "- Error Code: RESOURCE_EXHAUSTED",
307 "- Retry After: 26s",
308 "This error occurs when you have exceeded your API quota.",
309 ]
310 .into_iter()
311 .map(|t| Label::new(t).size(LabelSize::Small).color(Color::Muted)),
312 ),
313 )
314 .actions_slot(single_action())
315 .into_any_element(),
316 )
317 .width(px(580.)),
318 ];
319
320 let severity_examples = vec![
321 single_example(
322 "Info",
323 Callout::new()
324 .icon(IconName::Info)
325 .title("System maintenance scheduled for tonight")
326 .actions_slot(single_action())
327 .into_any_element(),
328 ),
329 single_example(
330 "Warning",
331 Callout::new()
332 .severity(Severity::Warning)
333 .icon(IconName::Triangle)
334 .title("System maintenance scheduled for tonight")
335 .actions_slot(single_action())
336 .into_any_element(),
337 ),
338 single_example(
339 "Error",
340 Callout::new()
341 .severity(Severity::Error)
342 .icon(IconName::XCircle)
343 .title("System maintenance scheduled for tonight")
344 .actions_slot(single_action())
345 .into_any_element(),
346 ),
347 single_example(
348 "Success",
349 Callout::new()
350 .severity(Severity::Success)
351 .icon(IconName::Check)
352 .title("System maintenance scheduled for tonight")
353 .actions_slot(single_action())
354 .into_any_element(),
355 ),
356 ];
357
358 v_flex()
359 .gap_4()
360 .child(example_group(basic_examples).vertical())
361 .child(example_group_with_title("Severity", severity_examples).vertical())
362 .into_any_element()
363 }
364}
365