1use crate::components::overlay::{OverlayHandle, OverlayKey, OverlayKind, OverlayMeta};
2use dioxus::prelude::*;
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum MessageType {
7 Info,
8 Success,
9 Warning,
10 Error,
11 Loading,
12}
13
14#[derive(Clone, Debug, PartialEq)]
16pub struct MessageConfig {
17 pub content: String,
18 pub r#type: MessageType,
19 pub duration: f32,
21 pub icon: Option<Element>,
23 pub class: Option<String>,
25 pub style: Option<String>,
27 pub key: Option<String>,
29 pub on_click: Option<EventHandler<()>>,
31}
32
33impl Default for MessageConfig {
34 fn default() -> Self {
35 Self {
36 content: String::new(),
37 r#type: MessageType::Info,
38 duration: 3.0,
39 icon: None,
40 class: None,
41 style: None,
42 key: None,
43 on_click: None,
44 }
45 }
46}
47
48#[derive(Clone, Debug, PartialEq)]
50pub struct MessageEntry {
51 pub key: OverlayKey,
52 pub meta: OverlayMeta,
53 pub config: MessageConfig,
54}
55
56pub type MessageEntriesSignal = Signal<Vec<MessageEntry>>;
58
59pub fn use_message_entries_provider() -> MessageEntriesSignal {
63 let signal: MessageEntriesSignal = use_context_provider(|| Signal::new(Vec::new()));
64 signal
65}
66
67pub fn use_message_entries() -> MessageEntriesSignal {
69 use_context::<MessageEntriesSignal>()
70}
71
72#[derive(Clone)]
74pub struct MessageApi {
75 overlay: OverlayHandle,
76 entries: MessageEntriesSignal,
77}
78
79impl MessageApi {
80 pub fn new(overlay: OverlayHandle, entries: MessageEntriesSignal) -> Self {
81 Self { overlay, entries }
82 }
83
84 pub fn open(&self, config: MessageConfig) -> OverlayKey {
86 let (key, meta) = self.overlay.open(OverlayKind::Message, false);
87 let mut entries = self.entries;
88 entries.write().push(MessageEntry {
89 key,
90 meta,
91 config: config.clone(),
92 });
93
94 schedule_message_dismiss(key, self.entries, self.overlay.clone(), config.duration);
95 key
96 }
97
98 fn open_with_type(&self, content: impl Into<String>, kind: MessageType) -> OverlayKey {
99 let cfg = MessageConfig {
100 content: content.into(),
101 r#type: kind,
102 duration: 3.0,
103 icon: None,
104 class: None,
105 style: None,
106 key: None,
107 on_click: None,
108 };
109 self.open(cfg)
110 }
111
112 pub fn info(&self, content: impl Into<String>) -> OverlayKey {
113 self.open_with_type(content, MessageType::Info)
114 }
115
116 pub fn success(&self, content: impl Into<String>) -> OverlayKey {
117 self.open_with_type(content, MessageType::Success)
118 }
119
120 pub fn warning(&self, content: impl Into<String>) -> OverlayKey {
121 self.open_with_type(content, MessageType::Warning)
122 }
123
124 pub fn error(&self, content: impl Into<String>) -> OverlayKey {
125 self.open_with_type(content, MessageType::Error)
126 }
127
128 pub fn loading(&self, content: impl Into<String>) -> OverlayKey {
129 let cfg = MessageConfig {
131 content: content.into(),
132 r#type: MessageType::Loading,
133 duration: 0.0,
134 icon: None,
135 class: None,
136 style: None,
137 key: None,
138 on_click: None,
139 };
140 self.open(cfg)
141 }
142
143 pub fn destroy(&self, key: Option<OverlayKey>) {
145 let mut entries = self.entries;
146 match key {
147 Some(k) => {
148 entries.write().retain(|e| e.key != k);
149 self.overlay.close(k);
150 }
151 None => {
152 let current: Vec<_> = entries.read().iter().map(|e| e.key).collect();
153 entries.write().clear();
154 for k in current {
155 self.overlay.close(k);
156 }
157 }
158 }
159 }
160}
161
162#[component]
164pub fn MessageHost() -> Element {
165 let entries_signal = use_message_entries();
166 let entries = entries_signal.read().clone();
167
168 if entries.is_empty() {
169 return rsx! {};
170 }
171
172 rsx! {
173 div {
174 class: "adui-message-root",
175 style: "position: fixed; top: 24px; inset-inline-end: 0; z-index: 1000; display: flex; flex-direction: column; gap: 8px; padding-inline-end: 24px; pointer-events: none;",
176 {entries.iter().map(|entry| {
177 let key = entry.key;
178 let z = entry.meta.z_index;
179 let text = entry.config.content.clone();
180 let kind_class = match entry.config.r#type {
181 MessageType::Info => "adui-message-info",
182 MessageType::Success => "adui-message-success",
183 MessageType::Warning => "adui-message-warning",
184 MessageType::Error => "adui-message-error",
185 MessageType::Loading => "adui-message-loading",
186 };
187 rsx! {
188 div {
189 key: "message-{key:?}",
190 class: "adui-message {kind_class}",
191 style: "pointer-events: auto; z-index: {z}; min-width: 200px; max-width: 480px; padding: 8px 16px; border-radius: 4px; background: var(--adui-color-bg-container); box-shadow: var(--adui-shadow); color: var(--adui-color-text); border: 1px solid var(--adui-color-border);",
192 span { "{text}" }
193 button {
194 style: "margin-left: 8px; background: none; border: none; cursor: pointer; color: var(--adui-color-text-secondary);",
195 onclick: move |_| {
196 let mut entries = entries_signal;
197 entries.write().retain(|e| e.key != key);
198 },
199 "×"
200 }
201 }
202 }
203 })}
204 }
205 }
206}
207
208#[cfg(target_arch = "wasm32")]
209fn schedule_message_dismiss(
210 key: OverlayKey,
211 entries: MessageEntriesSignal,
212 overlay: OverlayHandle,
213 duration_secs: f32,
214) {
215 use wasm_bindgen::{JsCast, closure::Closure};
216
217 if duration_secs <= 0.0 {
218 return;
219 }
220
221 if let Some(window) = web_sys::window() {
222 let delay_ms = (duration_secs * 1000.0) as i32;
223 let mut entries_signal = entries;
224 let overlay_clone = overlay.clone();
225 let callback = Closure::once(move || {
226 entries_signal.write().retain(|e| e.key != key);
227 overlay_clone.close(key);
228 });
229 let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
230 callback.as_ref().unchecked_ref(),
231 delay_ms,
232 );
233 callback.forget();
234 }
235}
236
237#[cfg(not(target_arch = "wasm32"))]
238fn schedule_message_dismiss(
239 _key: OverlayKey,
240 _entries: MessageEntriesSignal,
241 _overlay: OverlayHandle,
242 _duration_secs: f32,
243) {
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn message_type_all_variants() {
252 assert_eq!(MessageType::Info, MessageType::Info);
253 assert_eq!(MessageType::Success, MessageType::Success);
254 assert_eq!(MessageType::Warning, MessageType::Warning);
255 assert_eq!(MessageType::Error, MessageType::Error);
256 assert_eq!(MessageType::Loading, MessageType::Loading);
257 assert_ne!(MessageType::Info, MessageType::Success);
258 assert_ne!(MessageType::Success, MessageType::Warning);
259 assert_ne!(MessageType::Warning, MessageType::Error);
260 assert_ne!(MessageType::Error, MessageType::Loading);
261 }
262
263 #[test]
264 fn message_type_clone() {
265 let original = MessageType::Success;
266 let cloned = original;
267 assert_eq!(original, cloned);
268 }
269
270 #[test]
271 fn message_config_default() {
272 let config = MessageConfig::default();
273 assert_eq!(config.content, "");
274 assert_eq!(config.r#type, MessageType::Info);
275 assert_eq!(config.duration, 3.0);
276 assert_eq!(config.icon, None);
277 assert_eq!(config.class, None);
278 assert_eq!(config.style, None);
279 assert_eq!(config.key, None);
280 assert_eq!(config.on_click, None);
281 }
282
283 #[test]
284 fn message_config_clone() {
285 let config1 = MessageConfig {
286 content: "Test message".to_string(),
287 r#type: MessageType::Success,
288 duration: 5.0,
289 icon: None,
290 class: Some("custom-class".to_string()),
291 style: Some("color: red;".to_string()),
292 key: Some("msg-1".to_string()),
293 on_click: None,
294 };
295 let config2 = config1.clone();
296 assert_eq!(config1, config2);
297 }
298
299 #[test]
300 fn message_config_partial_eq() {
301 let config1 = MessageConfig {
302 content: "Test".to_string(),
303 r#type: MessageType::Info,
304 duration: 3.0,
305 icon: None,
306 class: None,
307 style: None,
308 key: None,
309 on_click: None,
310 };
311 let config2 = MessageConfig {
312 content: "Test".to_string(),
313 r#type: MessageType::Info,
314 duration: 3.0,
315 icon: None,
316 class: None,
317 style: None,
318 key: None,
319 on_click: None,
320 };
321 let config3 = MessageConfig {
322 content: "Different".to_string(),
323 r#type: MessageType::Error,
324 duration: 5.0,
325 icon: None,
326 class: None,
327 style: None,
328 key: None,
329 on_click: None,
330 };
331 assert_eq!(config1, config2);
332 assert_ne!(config1, config3);
333 }
334}