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