iced_chat_widget/
lib.rs

1pub mod action;
2mod consts;
3pub mod message;
4pub mod state;
5pub mod style;
6
7use crate::{
8    action::{ChatEvent, MessageAction},
9    consts::*,
10    message::ChatMessage,
11    state::ChatState,
12    style::ChatTheme,
13};
14
15use iced::{
16    Alignment::{self},
17    Element,
18    Length::{self, Fill},
19    Task, Theme,
20    alignment::Horizontal::Right,
21    widget::{button, column, container, row, scrollable, text},
22};
23
24pub struct ChatWidget<'a, M>
25where
26    M: ChatMessage,
27{
28    state: &'a ChatState<M>,
29    actions: Vec<MessageAction>,
30    theme: ChatTheme,
31}
32
33impl<'a, M> ChatWidget<'a, M>
34where
35    M: ChatMessage + Clone + 'static,
36{
37    pub fn new(theme: &Theme, state: &'a ChatState<M>) -> Self {
38        Self {
39            state: state,
40            actions: Vec::new(),
41            theme: ChatTheme::get_default(theme),
42        }
43    }
44
45    pub fn with_custom_theme(mut self, theme: ChatTheme) -> Self {
46        self.theme = theme;
47        self
48    }
49
50    pub fn with_actions(mut self, actions: Vec<MessageAction>) -> Self {
51        self.actions = actions;
52        self
53    }
54
55    pub fn scroll_to_bottom() -> Task<ChatEvent> {
56        scrollable::snap_to(
57            scrollable::Id::new(SCROLLABLE_ID),
58            scrollable::RelativeOffset::END,
59        )
60    }
61
62    pub fn view(self) -> Element<'static, ChatEvent> {
63        let theme = self.theme;
64        let actions = self.actions;
65
66        let ids = self.state.get_messages_ids();
67
68        let mut messages_ordered = ids
69            .iter()
70            .filter_map(|id| self.state.get_message(id))
71            .collect::<Vec<&M>>();
72        messages_ordered.sort_by_key(|msg| msg.timestamp());
73
74        let messages_ordered = messages_ordered
75            .iter()
76            .fold(column![].spacing(theme.spacing()), |col, msg| {
77                col.push(Self::render_message_owned(msg, &theme, actions.clone()))
78            });
79
80        let scrollable_messages = scrollable(
81            container(messages_ordered)
82                .padding(theme.padding())
83                .width(Length::Fill),
84        )
85        .height(Length::Fill);
86
87        let scrollable_messages = scrollable_messages.id(scrollable::Id::new(SCROLLABLE_ID));
88
89        let bg_color = theme.background_color();
90        let widget = container(scrollable_messages)
91            .width(Length::Fill)
92            .height(Length::Fill)
93            .style(move |_theme| container::Style {
94                background: Some(bg_color.into()),
95                ..Default::default()
96            })
97            .into();
98
99        widget
100    }
101
102    fn render_message_owned(
103        msg: &M,
104        theme: &ChatTheme,
105        actions: Vec<MessageAction>, // Takes owned Vec, not a slice
106    ) -> Element<'static, ChatEvent> {
107        let style = if msg.is_own_message() {
108            theme.own_message_style()
109        } else {
110            theme.other_message_style()
111        };
112
113        let is_own = msg.is_own_message();
114        let msg_id = msg.id().to_string();
115        let author = msg.author_id().to_string();
116        let content = msg.content().to_string();
117
118        // Extract all values from style upfront (copy them)
119        let author_text_size = style.author_text_size();
120        let content_text_size = style.content_text_size();
121        let text_color = style.text_color();
122        let padding = style.padding();
123        let background = style.background();
124        let border_radius = style.border_radius();
125
126        let mut message_col = column![]
127            .spacing(4.0)
128            .push(text(author).size(author_text_size).color(text_color))
129            .push(text(content).size(content_text_size).color(text_color));
130
131        if !actions.is_empty() {
132            let actions_row = actions
133                .into_iter() // Consume the owned Vec
134                .fold(row![].spacing(5.0), |row_acc, action| {
135                    let action_id = action.id; // Move, not clone
136                    let label = action.label; // Move, not clone
137                    let msg_id_clone = msg_id.clone();
138                    row_acc.push(
139                        button(text(label).size(12))
140                            .on_press(ChatEvent::ActionClicked {
141                                message_id: msg_id_clone,
142                                action_id,
143                            })
144                            .padding(5),
145                    )
146                });
147            message_col = message_col.push(actions_row);
148        }
149
150        let timestamp_text_size = style.timestamp_text_size();
151        let timestamp = msg.timestamp();
152        let timestamp_text = format!(
153            "{}",
154            timestamp.format(style.time_stamp_format()).to_string()
155        );
156        let timestamp_container = container(
157            text(timestamp_text)
158                .size(timestamp_text_size)
159                .color(style.time_stamp_text_color()),
160        )
161        .align_x(Right);
162
163        let timestamp_row = row![timestamp_container];
164        message_col = message_col.push(timestamp_row);
165
166        let message_container = container(message_col)
167            .padding(padding)
168            .style(move |_theme| container::Style {
169                background: Some(iced::Background::Color(background)),
170                border: iced::Border {
171                    radius: border_radius,
172                    ..Default::default()
173                },
174                ..Default::default()
175            });
176
177        let message_wrapper = container(message_container).width(Fill).align_x(if is_own {
178            Right
179        } else {
180            iced::alignment::Horizontal::Left
181        });
182
183        let row_widget: iced::widget::Row<'static, ChatEvent> = row![message_wrapper].width(Fill);
184
185        if is_own {
186            row_widget.align_y(Alignment::End).into()
187        } else {
188            row_widget.align_y(Alignment::Start).into()
189        }
190    }
191}