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 .overflow_hidden()
198 .rounded(cx.theme().radius_2xl())
199 .border_1()
200 .border_color(cx.theme().transparent)
201 .px_3()
202 .py_2()
203 .text_sm()
204 .line_height(relative(1.625))
205 .when_some(self.alignment, |this, alignment| match alignment {
206 MessageAlignment::Start => this.self_start(),
207 MessageAlignment::End => this.self_end(),
208 })
209 .map(|this| match self.variant {
210 BubbleVariant::Filled => this
211 .bg(tokens.colors.primary)
212 .text_color(tokens.colors.primary_foreground),
213 BubbleVariant::Secondary => this
218 .bg(tokens.colors.muted)
219 .text_color(tokens.colors.secondary_foreground),
220 BubbleVariant::Muted => this
221 .bg(tokens.colors.muted)
222 .text_color(tokens.colors.foreground),
223 BubbleVariant::Tinted => this
224 .bg(tokens.colors.primary.mix_oklab(
225 tokens.colors.background,
226 if cx.theme().is_dark() { 0.24 } else { 0.12 },
227 ))
228 .text_color(tokens.colors.foreground),
229 BubbleVariant::Outline => this
230 .border_color(tokens.colors.border)
231 .bg(tokens.colors.background)
232 .text_color(tokens.colors.foreground),
233 BubbleVariant::Ghost => this
234 .rounded(tokens.radius.none)
235 .border_0()
236 .bg(cx.theme().transparent)
237 .text_color(tokens.colors.foreground)
238 .p_0(),
239 BubbleVariant::Destructive => this
240 .bg(tokens.colors.destructive.opacity(if cx.theme().is_dark() {
241 0.2
242 } else {
243 0.1
244 }))
245 .text_color(tokens.colors.destructive),
246 })
247 .refine_style(&self.style)
248 .children(self.children)
249 }
250}
251
252#[derive(IntoElement)]
254pub struct BubbleGroup {
255 style: StyleRefinement,
256 children: Vec<AnyElement>,
257}
258
259impl BubbleGroup {
260 pub fn new() -> Self {
262 Self {
263 style: StyleRefinement::default(),
264 children: Vec::new(),
265 }
266 }
267}
268
269impl Default for BubbleGroup {
270 fn default() -> Self {
271 Self::new()
272 }
273}
274
275impl ParentElement for BubbleGroup {
276 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
277 self.children.extend(elements);
278 }
279}
280
281impl Styled for BubbleGroup {
282 fn style(&mut self) -> &mut StyleRefinement {
283 &mut self.style
284 }
285}
286
287impl RenderOnce for BubbleGroup {
288 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
289 v_flex()
290 .min_w_0()
291 .gap_2()
292 .refine_style(&self.style)
293 .children(self.children)
294 }
295}
296
297enum BubbleReactionChild {
298 Action(Box<Button>),
299 Element(AnyElement),
300}
301
302#[derive(IntoElement)]
307pub struct BubbleReactions {
308 style: StyleRefinement,
309 side: BubbleReactionSide,
310 alignment: MessageAlignment,
311 children: Vec<BubbleReactionChild>,
312}
313
314impl BubbleReactions {
315 pub fn new() -> Self {
317 Self {
318 style: StyleRefinement::default(),
319 side: BubbleReactionSide::Bottom,
320 alignment: MessageAlignment::End,
321 children: Vec::new(),
322 }
323 }
324
325 pub fn side(mut self, side: BubbleReactionSide) -> Self {
327 self.side = side;
328 self
329 }
330
331 pub fn alignment(mut self, alignment: MessageAlignment) -> Self {
333 self.alignment = alignment;
334 self
335 }
336
337 pub fn action(mut self, action: Button) -> Self {
345 self.children
346 .push(BubbleReactionChild::Action(Box::new(action)));
347 self
348 }
349}
350
351impl Default for BubbleReactions {
352 fn default() -> Self {
353 Self::new()
354 }
355}
356
357impl ParentElement for BubbleReactions {
358 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
359 self.children
360 .extend(elements.into_iter().map(BubbleReactionChild::Element));
361 }
362}
363
364impl Styled for BubbleReactions {
365 fn style(&mut self) -> &mut StyleRefinement {
366 &mut self.style
367 }
368}
369
370impl RenderOnce for BubbleReactions {
371 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
372 let tokens = cx.theme().semantic_tokens();
373 let has_action = self
374 .children
375 .iter()
376 .any(|child| matches!(child, BubbleReactionChild::Action(_)));
377 let action_radius = cx.theme().radius_full();
378 let children = self.children.into_iter().map(move |child| match child {
379 BubbleReactionChild::Action(action) => {
380 (*action).rounded(action_radius).into_any_element()
381 }
382 BubbleReactionChild::Element(element) => element,
383 });
384
385 div()
386 .absolute()
387 .flex()
388 .flex_none()
389 .items_center()
390 .justify_center()
391 .gap_1()
392 .rounded(cx.theme().radius_full())
393 .border_3()
394 .border_color(tokens.colors.background)
395 .bg(tokens.colors.muted)
396 .text_color(tokens.colors.foreground)
397 .when(!has_action, |this| this.px_1p5().py_0p5())
398 .text_sm()
399 .when(self.side == BubbleReactionSide::Top, |this| {
403 this.top(-rems(1.25))
404 })
405 .when(self.side == BubbleReactionSide::Bottom, |this| {
406 this.bottom(-rems(1.25))
407 })
408 .when(self.alignment == MessageAlignment::Start, |this| {
409 this.left_3()
410 })
411 .when(self.alignment == MessageAlignment::End, |this| {
412 this.right_3()
413 })
414 .refine_style(&self.style)
415 .children(children)
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422
423 #[test]
424 fn test_bubble_builder() {
425 let bubble = Bubble::new()
426 .alignment(MessageAlignment::End)
427 .with_variant(BubbleVariant::Outline)
428 .content(BubbleContent::new().child("Hello"))
429 .reactions(BubbleReactions::new().child("👍"));
430
431 assert_eq!(bubble.alignment, Some(MessageAlignment::End));
432 assert_eq!(bubble.variant, BubbleVariant::Outline);
433 assert_eq!(bubble.content.children.len(), 1);
434 assert!(bubble.reactions.is_some());
435
436 let reordered = Bubble::new()
439 .child("Existing")
440 .content(BubbleContent::new().child("Configured"));
441 assert_eq!(reordered.content.children.len(), 2);
442
443 let group = BubbleGroup::new().child("First").child("Second");
444 assert_eq!(group.children.len(), 2);
445
446 let reactions = BubbleReactions::new()
447 .side(BubbleReactionSide::Top)
448 .alignment(MessageAlignment::Start)
449 .child("👍 2");
450
451 assert_eq!(reactions.side, BubbleReactionSide::Top);
452 assert_eq!(reactions.alignment, MessageAlignment::Start);
453 assert_eq!(reactions.children.len(), 1);
454
455 let reactions = BubbleReactions::new()
456 .action(Button::new("reaction-action"))
457 .child("👍");
458 assert_eq!(reactions.children.len(), 2);
459 assert!(matches!(
460 reactions.children.first(),
461 Some(BubbleReactionChild::Action(_))
462 ));
463 assert!(matches!(
464 reactions.children.get(1),
465 Some(BubbleReactionChild::Element(_))
466 ));
467 }
468}