1use gpui::{
2 AnyElement, App, IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, Window,
3 prelude::FluentBuilder as _, relative, rems,
4};
5
6use crate::{ActiveTheme as _, StyledExt as _, bubble::Bubble, h_flex, v_flex};
7
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
10pub enum MessageAlignment {
11 #[default]
13 Start,
14 End,
16}
17
18#[derive(IntoElement)]
20pub struct MessageGroup {
21 style: StyleRefinement,
22 children: Vec<AnyElement>,
23}
24
25impl MessageGroup {
26 pub fn new() -> Self {
28 Self {
29 style: StyleRefinement::default(),
30 children: Vec::new(),
31 }
32 }
33}
34
35impl Default for MessageGroup {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41impl ParentElement for MessageGroup {
42 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
43 self.children.extend(elements);
44 }
45}
46
47impl Styled for MessageGroup {
48 fn style(&mut self) -> &mut StyleRefinement {
49 &mut self.style
50 }
51}
52
53impl RenderOnce for MessageGroup {
54 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
55 v_flex()
56 .min_w_0()
57 .gap_2()
58 .refine_style(&self.style)
59 .children(self.children)
60 }
61}
62
63#[derive(IntoElement)]
68pub struct Message {
69 style: StyleRefinement,
70 stack_style: StyleRefinement,
71 alignment: MessageAlignment,
72 avatar: Option<MessageAvatar>,
73 header: Option<MessageHeader>,
74 content: Option<MessageContent>,
75 footer: Option<MessageFooter>,
76}
77
78impl Message {
79 pub fn new() -> Self {
81 Self {
82 style: StyleRefinement::default(),
83 stack_style: StyleRefinement::default(),
84 alignment: MessageAlignment::Start,
85 avatar: None,
86 header: None,
87 content: None,
88 footer: None,
89 }
90 }
91
92 pub fn alignment(mut self, alignment: MessageAlignment) -> Self {
94 self.alignment = alignment;
95 self
96 }
97
98 pub fn with_stack_style(mut self, style: StyleRefinement) -> Self {
100 self.stack_style = style;
101 self
102 }
103
104 pub fn avatar(mut self, avatar: impl IntoElement) -> Self {
106 self.avatar = Some(MessageAvatar::new().child(avatar));
107 self
108 }
109
110 pub fn avatar_slot(mut self, avatar: MessageAvatar) -> Self {
112 self.avatar = Some(avatar);
113 self
114 }
115
116 pub fn header(mut self, header: MessageHeader) -> Self {
118 self.header = Some(header);
119 self
120 }
121
122 pub fn content(mut self, content: MessageContent) -> Self {
124 self.content = Some(content);
125 self
126 }
127
128 pub fn footer(mut self, footer: MessageFooter) -> Self {
130 self.footer = Some(footer);
131 self
132 }
133}
134
135impl Default for Message {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141impl Styled for Message {
142 fn style(&mut self) -> &mut StyleRefinement {
143 &mut self.style
144 }
145}
146
147impl RenderOnce for Message {
148 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
149 let alignment = self.alignment;
150 let has_avatar = self.avatar.is_some();
151 let has_ghost_bubble = self
152 .content
153 .as_ref()
154 .is_some_and(|content| content.has_ghost_bubble);
155 let stack_style = self.stack_style;
156
157 v_flex()
158 .relative()
159 .w_full()
160 .min_w_0()
161 .gap(rems(0.625))
162 .text_sm()
163 .line_height(relative(1.25))
164 .map(|this| match alignment {
165 MessageAlignment::Start => this.items_start(),
166 MessageAlignment::End => this.items_end(),
167 })
168 .refine_style(&self.style)
169 .child(
170 h_flex()
174 .w_full()
175 .min_w_0()
176 .items_end()
177 .gap_2()
178 .when(alignment == MessageAlignment::End, |this| {
179 this.flex_row_reverse()
180 })
181 .when_some(self.avatar, |this, avatar| this.child(avatar))
182 .child(
183 v_flex()
184 .w_full()
185 .min_w_0()
186 .gap(rems(0.625))
187 .map(|this| match alignment {
188 MessageAlignment::Start => this.items_start(),
189 MessageAlignment::End => this.items_end(),
190 })
191 .refine_style(&stack_style)
192 .when_some(self.header, |this, header| {
193 this.child(header.with_inherited_content_inset(!has_ghost_bubble))
194 })
195 .when_some(self.content, |this, content| {
196 this.child(content.aligned(alignment))
197 }),
198 ),
199 )
200 .when_some(self.footer, |this, footer| {
201 this.child(
202 footer
203 .with_inherited_content_inset(!has_ghost_bubble)
204 .when(has_avatar && alignment == MessageAlignment::Start, |this| {
207 this.ml(rems(2.5))
208 })
209 .when(has_avatar && alignment == MessageAlignment::End, |this| {
210 this.mr(rems(2.5))
211 }),
212 )
213 })
214 }
215}
216
217#[derive(IntoElement)]
222pub struct MessageAvatar {
223 style: StyleRefinement,
224 children: Vec<AnyElement>,
225}
226
227impl MessageAvatar {
228 pub fn new() -> Self {
230 Self {
231 style: StyleRefinement::default(),
232 children: Vec::new(),
233 }
234 }
235}
236
237impl Default for MessageAvatar {
238 fn default() -> Self {
239 Self::new()
240 }
241}
242
243impl ParentElement for MessageAvatar {
244 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
245 self.children.extend(elements);
246 }
247}
248
249impl Styled for MessageAvatar {
250 fn style(&mut self) -> &mut StyleRefinement {
251 &mut self.style
252 }
253}
254
255impl RenderOnce for MessageAvatar {
256 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
257 let tokens = cx.theme().semantic_tokens();
258
259 h_flex()
260 .relative()
261 .min_w_8()
262 .flex_none()
263 .items_center()
264 .justify_center()
265 .self_end()
266 .overflow_hidden()
267 .rounded(cx.theme().radius_full())
268 .bg(tokens.colors.muted)
269 .refine_style(&self.style)
270 .children(self.children)
271 }
272}
273
274#[derive(IntoElement)]
276pub struct MessageHeader {
277 style: StyleRefinement,
278 content_inset: Option<bool>,
279 children: Vec<AnyElement>,
280}
281
282impl MessageHeader {
283 pub fn new() -> Self {
285 Self {
286 style: StyleRefinement::default(),
287 content_inset: None,
288 children: Vec::new(),
289 }
290 }
291
292 pub fn content_inset(mut self, content_inset: bool) -> Self {
294 self.content_inset = Some(content_inset);
295 self
296 }
297
298 fn with_inherited_content_inset(mut self, content_inset: bool) -> Self {
299 self.content_inset.get_or_insert(content_inset);
300 self
301 }
302}
303
304impl Default for MessageHeader {
305 fn default() -> Self {
306 Self::new()
307 }
308}
309
310impl ParentElement for MessageHeader {
311 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
312 self.children.extend(elements);
313 }
314}
315
316impl Styled for MessageHeader {
317 fn style(&mut self) -> &mut StyleRefinement {
318 &mut self.style
319 }
320}
321
322impl RenderOnce for MessageHeader {
323 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
324 let tokens = cx.theme().semantic_tokens();
325
326 h_flex()
327 .max_w_full()
328 .min_w_0()
329 .gap_1()
330 .text_xs()
331 .line_height(relative(1.25))
332 .font_medium()
333 .text_color(tokens.colors.muted_foreground)
334 .when(self.content_inset.unwrap_or(true), |this| this.px_3())
335 .refine_style(&self.style)
336 .children(self.children)
337 }
338}
339
340#[derive(IntoElement)]
342pub struct MessageContent {
343 style: StyleRefinement,
344 alignment: MessageAlignment,
345 has_ghost_bubble: bool,
346 children: Vec<AnyElement>,
347}
348
349impl MessageContent {
350 pub fn new() -> Self {
352 Self {
353 style: StyleRefinement::default(),
354 alignment: MessageAlignment::Start,
355 has_ghost_bubble: false,
356 children: Vec::new(),
357 }
358 }
359
360 pub fn bubble(mut self, bubble: Bubble) -> Self {
366 self.has_ghost_bubble |= bubble.is_ghost();
367 self.children.push(bubble.into_any_element());
368 self
369 }
370
371 fn aligned(mut self, alignment: MessageAlignment) -> Self {
372 self.alignment = alignment;
373 self
374 }
375}
376
377impl Default for MessageContent {
378 fn default() -> Self {
379 Self::new()
380 }
381}
382
383impl ParentElement for MessageContent {
384 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
385 self.children.extend(elements);
386 }
387}
388
389impl Styled for MessageContent {
390 fn style(&mut self) -> &mut StyleRefinement {
391 &mut self.style
392 }
393}
394
395impl RenderOnce for MessageContent {
396 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
397 v_flex()
398 .w_full()
399 .max_w_full()
400 .min_w_0()
401 .gap(rems(0.625))
402 .map(|this| match self.alignment {
403 MessageAlignment::Start => this.items_start(),
404 MessageAlignment::End => this.items_end(),
405 })
406 .refine_style(&self.style)
407 .children(self.children)
408 }
409}
410
411#[derive(IntoElement)]
413pub struct MessageFooter {
414 style: StyleRefinement,
415 content_inset: Option<bool>,
416 children: Vec<AnyElement>,
417}
418
419impl MessageFooter {
420 pub fn new() -> Self {
422 Self {
423 style: StyleRefinement::default(),
424 content_inset: None,
425 children: Vec::new(),
426 }
427 }
428
429 pub fn content_inset(mut self, content_inset: bool) -> Self {
431 self.content_inset = Some(content_inset);
432 self
433 }
434
435 fn with_inherited_content_inset(mut self, content_inset: bool) -> Self {
436 self.content_inset.get_or_insert(content_inset);
437 self
438 }
439}
440
441impl Default for MessageFooter {
442 fn default() -> Self {
443 Self::new()
444 }
445}
446
447impl ParentElement for MessageFooter {
448 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
449 self.children.extend(elements);
450 }
451}
452
453impl Styled for MessageFooter {
454 fn style(&mut self) -> &mut StyleRefinement {
455 &mut self.style
456 }
457}
458
459impl RenderOnce for MessageFooter {
460 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
461 let tokens = cx.theme().semantic_tokens();
462
463 h_flex()
464 .max_w_full()
465 .min_w_0()
466 .gap_1()
467 .text_xs()
468 .line_height(relative(1.25))
469 .font_medium()
470 .text_color(tokens.colors.muted_foreground)
471 .when(self.content_inset.unwrap_or(true), |this| this.px_3())
472 .refine_style(&self.style)
473 .children(self.children)
474 }
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480
481 #[test]
482 fn test_message_builder() {
483 let stack_style = StyleRefinement::default().gap_1();
484 let message = Message::new()
485 .alignment(MessageAlignment::End)
486 .with_stack_style(stack_style.clone())
487 .avatar_slot(MessageAvatar::new().child(gpui::div()))
488 .header(MessageHeader::new().content_inset(false).child("Alice"))
489 .content(MessageContent::new().child("Hello"))
490 .footer(MessageFooter::new().content_inset(false).child("Delivered"));
491
492 assert_eq!(message.alignment, MessageAlignment::End);
493 assert_eq!(message.stack_style, stack_style);
494 assert!(message.avatar.is_some());
495 assert!(message.header.is_some());
496 assert!(message.content.is_some());
497 assert!(message.footer.is_some());
498 assert_eq!(message.header.as_ref().unwrap().content_inset, Some(false));
499 assert_eq!(message.footer.as_ref().unwrap().content_inset, Some(false));
500
501 let group = MessageGroup::new().child("First").child("Second");
502 assert_eq!(group.children.len(), 2);
503
504 let content = MessageContent::new().aligned(MessageAlignment::End);
505 assert_eq!(content.alignment, MessageAlignment::End);
506
507 let avatar = MessageAvatar::new().child("ME");
508 assert_eq!(avatar.children.len(), 1);
509 }
510
511 #[test]
512 fn test_ghost_bubble_inherits_message_slot_insets() {
513 let content = MessageContent::new()
514 .bubble(Bubble::new())
515 .bubble(Bubble::new().with_variant(crate::bubble::BubbleVariant::Ghost));
516
517 assert!(content.has_ghost_bubble);
518 assert_eq!(content.children.len(), 2);
519 assert_eq!(
520 MessageHeader::new()
521 .with_inherited_content_inset(false)
522 .content_inset,
523 Some(false)
524 );
525 assert_eq!(
526 MessageFooter::new()
527 .with_inherited_content_inset(false)
528 .content_inset,
529 Some(false)
530 );
531 assert_eq!(
532 MessageHeader::new()
533 .content_inset(true)
534 .with_inherited_content_inset(false)
535 .content_inset,
536 Some(true)
537 );
538 assert_eq!(
539 MessageFooter::new()
540 .content_inset(true)
541 .with_inherited_content_inset(false)
542 .content_inset,
543 Some(true)
544 );
545 }
546}