chat_message.rs

  1use std::sync::Arc;
  2
  3use client::User;
  4use gpui::{AnyElement, ClickEvent};
  5use ui::{prelude::*, Avatar};
  6
  7use crate::MessageId;
  8
  9pub enum UserOrAssistant {
 10    User(Option<Arc<User>>),
 11    Assistant,
 12}
 13
 14#[derive(IntoElement)]
 15pub struct ChatMessage {
 16    id: MessageId,
 17    player: UserOrAssistant,
 18    message: Option<AnyElement>,
 19    collapsed: bool,
 20    on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
 21}
 22
 23impl ChatMessage {
 24    pub fn new(
 25        id: MessageId,
 26        player: UserOrAssistant,
 27        message: Option<AnyElement>,
 28        collapsed: bool,
 29        on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
 30    ) -> Self {
 31        Self {
 32            id,
 33            player,
 34            message,
 35            collapsed,
 36            on_collapse_handle_click,
 37        }
 38    }
 39}
 40
 41impl RenderOnce for ChatMessage {
 42    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
 43        let collapse_handle_id = SharedString::from(format!("{}_collapse_handle", self.id.0));
 44        let collapse_handle = h_flex()
 45            .id(collapse_handle_id.clone())
 46            .group(collapse_handle_id.clone())
 47            .flex_none()
 48            .justify_center()
 49            .w_1()
 50            .mx_2()
 51            .h_full()
 52            .on_click(self.on_collapse_handle_click)
 53            .child(
 54                div()
 55                    .w_px()
 56                    .h_full()
 57                    .rounded_lg()
 58                    .overflow_hidden()
 59                    .bg(cx.theme().colors().element_background)
 60                    .group_hover(collapse_handle_id, |this| {
 61                        this.bg(cx.theme().colors().element_hover)
 62                    }),
 63            );
 64
 65        let content_padding = rems(1.);
 66        // Clamp the message height to exactly 1.5 lines when collapsed.
 67        let collapsed_height = content_padding.to_pixels(cx.rem_size()) + cx.line_height() * 1.5;
 68
 69        let content = self.message.map(|message| {
 70            div()
 71                .overflow_hidden()
 72                .w_full()
 73                .p(content_padding)
 74                .rounded_lg()
 75                .when(self.collapsed, |this| this.h(collapsed_height))
 76                .bg(cx.theme().colors().surface_background)
 77                .child(message)
 78        });
 79
 80        v_flex()
 81            .gap_1()
 82            .child(ChatMessageHeader::new(self.player))
 83            .child(h_flex().gap_3().child(collapse_handle).children(content))
 84    }
 85}
 86
 87#[derive(IntoElement)]
 88struct ChatMessageHeader {
 89    player: UserOrAssistant,
 90    contexts: Vec<()>,
 91}
 92
 93impl ChatMessageHeader {
 94    fn new(player: UserOrAssistant) -> Self {
 95        Self {
 96            player,
 97            contexts: Vec::new(),
 98        }
 99    }
100}
101
102impl RenderOnce for ChatMessageHeader {
103    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
104        let (username, avatar_uri) = match self.player {
105            UserOrAssistant::Assistant => (
106                "Assistant".into(),
107                Some("https://zed.dev/assistant_avatar.png".into()),
108            ),
109            UserOrAssistant::User(Some(user)) => {
110                (user.github_login.clone(), Some(user.avatar_uri.clone()))
111            }
112            UserOrAssistant::User(None) => ("You".into(), None),
113        };
114
115        h_flex()
116            .justify_between()
117            .child(
118                h_flex()
119                    .gap_3()
120                    .map(|this| {
121                        let avatar_size = rems_from_px(20.);
122                        if let Some(avatar_uri) = avatar_uri {
123                            this.child(Avatar::new(avatar_uri).size(avatar_size))
124                        } else {
125                            this.child(div().size(avatar_size))
126                        }
127                    })
128                    .child(Label::new(username).color(Color::Default)),
129            )
130            .child(div().when(!self.contexts.is_empty(), |this| {
131                this.child(Label::new(self.contexts.len().to_string()).color(Color::Muted))
132            }))
133    }
134}