1use gpui::{
2 AnyElement, App, IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, Window, div,
3 prelude::FluentBuilder as _, relative, rems,
4};
5
6use crate::{
7 ActiveTheme as _, Colorize as _, StyledExt as _, button::Button, message::MessageAlignment,
8 v_flex,
9};
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
13pub enum BubbleVariant {
14 #[default]
16 Filled,
17 Secondary,
19 Muted,
21 Tinted,
23 Outline,
25 Ghost,
27 Destructive,
29}
30
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
33pub enum BubbleReactionSide {
34 Top,
36 #[default]
38 Bottom,
39}
40
41#[derive(IntoElement)]
46pub struct Bubble {
47 style: StyleRefinement,
48 alignment: Option<MessageAlignment>,
49 variant: BubbleVariant,
50 content: BubbleContent,
51 reactions: Option<BubbleReactions>,
52}
53
54impl Bubble {
55 pub fn new() -> Self {
57 Self {
58 style: StyleRefinement::default(),
59 alignment: None,
60 variant: BubbleVariant::Filled,
61 content: BubbleContent::new(),
62 reactions: None,
63 }
64 }
65
66 pub fn alignment(mut self, alignment: MessageAlignment) -> Self {
68 self.alignment = Some(alignment);
69 self
70 }
71
72 pub fn with_variant(mut self, variant: BubbleVariant) -> Self {
74 self.variant = variant;
75 self
76 }
77
78 pub(crate) fn is_ghost(&self) -> bool {
79 self.variant == BubbleVariant::Ghost
80 }
81
82 pub fn content(mut self, mut content: BubbleContent) -> Self {
88 let mut children = std::mem::take(&mut self.content.children);
89 children.append(&mut content.children);
90 content.children = children;
91 self.content = content;
92 self
93 }
94
95 pub fn reactions(mut self, reactions: BubbleReactions) -> Self {
97 self.reactions = Some(reactions);
98 self
99 }
100}
101
102impl Default for Bubble {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108impl ParentElement for Bubble {
109 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
110 self.content.children.extend(elements);
111 }
112}
113
114impl Styled for Bubble {
115 fn style(&mut self) -> &mut StyleRefinement {
116 &mut self.style
117 }
118}
119
120impl RenderOnce for Bubble {
121 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
122 let variant = self.variant;
123 let mut content = self.content;
124 content.variant = variant;
125 content.alignment = self.alignment;
126
127 div()
128 .relative()
129 .flex()
130 .min_w_0()
131 .flex_col()
132 .flex_none()
133 .gap_1()
134 .max_w(relative(0.8))
135 .when(variant == BubbleVariant::Ghost, |this| {
136 this.w_full().max_w_full()
137 })
138 .when_some(self.alignment, |this, alignment| match alignment {
139 MessageAlignment::Start => this.self_start().mr_auto(),
140 MessageAlignment::End => this.self_end().ml_auto(),
141 })
142 .refine_style(&self.style)
143 .child(content)
144 .when_some(self.reactions, |this, reactions| this.child(reactions))
145 }
146}
147
148#[derive(IntoElement)]
153pub struct BubbleContent {
154 style: StyleRefinement,
155 variant: BubbleVariant,
156 alignment: Option<MessageAlignment>,
157 children: Vec<AnyElement>,
158}
159
160impl BubbleContent {
161 pub fn new() -> Self {
163 Self {
164 style: StyleRefinement::default(),
165 variant: BubbleVariant::default(),
166 alignment: None,
167 children: Vec::new(),
168 }
169 }
170}
171
172impl Default for BubbleContent {
173 fn default() -> Self {
174 Self::new()
175 }
176}
177
178impl ParentElement for BubbleContent {
179 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
180 self.children.extend(elements);
181 }
182}
183
184impl Styled for BubbleContent {
185 fn style(&mut self) -> &mut StyleRefinement {
186 &mut self.style
187 }
188}
189
190impl RenderOnce for BubbleContent {
191 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
192 let tokens = cx.theme().semantic_tokens();
193
194 div()
195 .min_w_0()
196 .max_w_full()
197 .when(self.variant != BubbleVariant::Ghost, |this| {
200 this.overflow_hidden()
201 })
202 .rounded(cx.theme().radius_2xl())
203 .border_1()
204 .border_color(cx.theme().transparent)
205 .px_3()
206 .py_2()
207 .text_sm()
208 .line_height(relative(1.625))
209 .when_some(self.alignment, |this, alignment| match alignment {
210 MessageAlignment::Start => this.self_start(),
211 MessageAlignment::End => this.self_end(),
212 })
213 .map(|this| match self.variant {
214 BubbleVariant::Filled => this
215 .bg(tokens.colors.primary)
216 .text_color(tokens.colors.primary_foreground),
217 BubbleVariant::Secondary => this
222 .bg(tokens.colors.muted)
223 .text_color(tokens.colors.secondary_foreground),
224 BubbleVariant::Muted => this
225 .bg(tokens.colors.muted)
226 .text_color(tokens.colors.foreground),
227 BubbleVariant::Tinted => this
228 .bg(tokens.colors.primary.mix_oklab(
229 tokens.colors.background,
230 if cx.theme().is_dark() { 0.24 } else { 0.12 },
231 ))
232 .text_color(tokens.colors.foreground),
233 BubbleVariant::Outline => this
234 .border_color(tokens.colors.border)
235 .bg(tokens.colors.background)
236 .text_color(tokens.colors.foreground),
237 BubbleVariant::Ghost => this
238 .rounded(tokens.radius.none)
239 .border_0()
240 .bg(cx.theme().transparent)
241 .text_color(tokens.colors.foreground)
242 .p_0(),
243 BubbleVariant::Destructive => this
244 .bg(tokens.colors.destructive.opacity(if cx.theme().is_dark() {
245 0.2
246 } else {
247 0.1
248 }))
249 .text_color(tokens.colors.destructive),
250 })
251 .refine_style(&self.style)
252 .children(self.children)
253 }
254}
255
256#[derive(IntoElement)]
258pub struct BubbleGroup {
259 style: StyleRefinement,
260 children: Vec<AnyElement>,
261}
262
263impl BubbleGroup {
264 pub fn new() -> Self {
266 Self {
267 style: StyleRefinement::default(),
268 children: Vec::new(),
269 }
270 }
271}
272
273impl Default for BubbleGroup {
274 fn default() -> Self {
275 Self::new()
276 }
277}
278
279impl ParentElement for BubbleGroup {
280 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
281 self.children.extend(elements);
282 }
283}
284
285impl Styled for BubbleGroup {
286 fn style(&mut self) -> &mut StyleRefinement {
287 &mut self.style
288 }
289}
290
291impl RenderOnce for BubbleGroup {
292 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
293 v_flex()
294 .min_w_0()
295 .gap_2()
296 .refine_style(&self.style)
297 .children(self.children)
298 }
299}
300
301enum BubbleReactionChild {
302 Action(Box<Button>),
303 Element(AnyElement),
304}
305
306#[derive(IntoElement)]
311pub struct BubbleReactions {
312 style: StyleRefinement,
313 side: BubbleReactionSide,
314 alignment: MessageAlignment,
315 children: Vec<BubbleReactionChild>,
316}
317
318impl BubbleReactions {
319 pub fn new() -> Self {
321 Self {
322 style: StyleRefinement::default(),
323 side: BubbleReactionSide::Bottom,
324 alignment: MessageAlignment::End,
325 children: Vec::new(),
326 }
327 }
328
329 pub fn side(mut self, side: BubbleReactionSide) -> Self {
331 self.side = side;
332 self
333 }
334
335 pub fn alignment(mut self, alignment: MessageAlignment) -> Self {
337 self.alignment = alignment;
338 self
339 }
340
341 pub fn action(mut self, action: Button) -> Self {
349 self.children
350 .push(BubbleReactionChild::Action(Box::new(action)));
351 self
352 }
353}
354
355impl Default for BubbleReactions {
356 fn default() -> Self {
357 Self::new()
358 }
359}
360
361impl ParentElement for BubbleReactions {
362 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
363 self.children
364 .extend(elements.into_iter().map(BubbleReactionChild::Element));
365 }
366}
367
368impl Styled for BubbleReactions {
369 fn style(&mut self) -> &mut StyleRefinement {
370 &mut self.style
371 }
372}
373
374impl RenderOnce for BubbleReactions {
375 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
376 let tokens = cx.theme().semantic_tokens();
377 let has_action = self
378 .children
379 .iter()
380 .any(|child| matches!(child, BubbleReactionChild::Action(_)));
381 let action_radius = cx.theme().radius_full();
382 let children = self.children.into_iter().map(move |child| match child {
383 BubbleReactionChild::Action(action) => {
384 (*action).rounded(action_radius).into_any_element()
385 }
386 BubbleReactionChild::Element(element) => element,
387 });
388
389 div()
390 .absolute()
391 .flex()
392 .flex_none()
393 .items_center()
394 .justify_center()
395 .gap_1()
396 .rounded(cx.theme().radius_full())
397 .border_3()
398 .border_color(tokens.colors.background)
399 .bg(tokens.colors.muted)
400 .text_color(tokens.colors.foreground)
401 .when(!has_action, |this| this.px_1p5().py_0p5())
402 .text_sm()
403 .when(self.side == BubbleReactionSide::Top, |this| {
407 this.top(-rems(1.25))
408 })
409 .when(self.side == BubbleReactionSide::Bottom, |this| {
410 this.bottom(-rems(1.25))
411 })
412 .when(self.alignment == MessageAlignment::Start, |this| {
413 this.left_3()
414 })
415 .when(self.alignment == MessageAlignment::End, |this| {
416 this.right_3()
417 })
418 .refine_style(&self.style)
419 .children(children)
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426
427 #[test]
428 fn test_bubble_builder() {
429 let bubble = Bubble::new()
430 .alignment(MessageAlignment::End)
431 .with_variant(BubbleVariant::Outline)
432 .content(BubbleContent::new().child("Hello"))
433 .reactions(BubbleReactions::new().child("👍"));
434
435 assert_eq!(bubble.alignment, Some(MessageAlignment::End));
436 assert_eq!(bubble.variant, BubbleVariant::Outline);
437 assert_eq!(bubble.content.children.len(), 1);
438 assert!(bubble.reactions.is_some());
439
440 let reordered = Bubble::new()
443 .child("Existing")
444 .content(BubbleContent::new().child("Configured"));
445 assert_eq!(reordered.content.children.len(), 2);
446
447 let group = BubbleGroup::new().child("First").child("Second");
448 assert_eq!(group.children.len(), 2);
449
450 let reactions = BubbleReactions::new()
451 .side(BubbleReactionSide::Top)
452 .alignment(MessageAlignment::Start)
453 .child("👍 2");
454
455 assert_eq!(reactions.side, BubbleReactionSide::Top);
456 assert_eq!(reactions.alignment, MessageAlignment::Start);
457 assert_eq!(reactions.children.len(), 1);
458
459 let reactions = BubbleReactions::new()
460 .action(Button::new("reaction-action"))
461 .child("👍");
462 assert_eq!(reactions.children.len(), 2);
463 assert!(matches!(
464 reactions.children.first(),
465 Some(BubbleReactionChild::Action(_))
466 ));
467 assert!(matches!(
468 reactions.children.get(1),
469 Some(BubbleReactionChild::Element(_))
470 ));
471 }
472}