euv_ui/component/message/view/fn.rs
1use super::*;
2
3/// A toast stack pinned to the top centre of the viewport.
4///
5/// The component is purely presentational: it renders the messages it is
6/// given and reports the clicks through the item callbacks. It owns no
7/// queue and starts no timers, so the auto-dismiss timing stays with the
8/// caller — `use_timer` or `App::use_interval` — and the rendered tree
9/// stays deterministic and testable. The stack container is always
10/// mounted, even when `items` is empty, so a caller can push messages in
11/// without remounting the component.
12///
13/// # Arguments
14///
15/// - `VirtualNode<EuvMessageProps>` - The props node containing the messages.
16///
17/// # Returns
18///
19/// - `VirtualNode` - The message stack virtual DOM tree.
20#[component]
21pub fn euv_message(node: VirtualNode<EuvMessageProps>) -> VirtualNode {
22 let EuvMessageProps { items }: EuvMessageProps = node.try_get_props().unwrap_or_default();
23 let messages: Vec<VirtualNode> = items
24 .into_iter()
25 .enumerate()
26 .map(
27 |(index, item): (usize, message::view::r#struct::EuvMessageItem)| {
28 euv_message_item(index, item)
29 },
30 )
31 .collect();
32 html! {
33 div {
34 class: c_euv_message_stack()
35 messages
36 }
37 }
38}
39
40/// Renders one message: its text, the optional action and the optional close button.
41///
42/// # Arguments
43///
44/// - `usize` - The index of the message in the stack, used as the diff key.
45/// - `EuvMessageItem` - The message to render, consumed.
46///
47/// # Returns
48///
49/// - `VirtualNode` - The message virtual DOM tree.
50fn euv_message_item(index: usize, item: EuvMessageItem) -> VirtualNode {
51 let EuvMessageItem {
52 text,
53 action_label,
54 on_action,
55 on_close,
56 dismissible,
57 }: EuvMessageItem = item;
58 let has_action: bool = !action_label.is_empty();
59 let is_dismissible: bool = dismissible || on_close.is_some();
60 let action_handler: Option<Rc<dyn Fn(Event)>> = message_callback_handler(on_action);
61 let close_handler: Option<Rc<dyn Fn(Event)>> = message_callback_handler(on_close);
62 html! {
63 div {
64 key: index.to_string()
65 class: c_euv_message()
66 div {
67 class: c_euv_message_text()
68 text
69 }
70 if has_action {
71 button {
72 class: c_euv_message_action()
73 onclick: action_handler
74 action_label
75 }
76 }
77 if is_dismissible {
78 button {
79 class: c_euv_message_close()
80 onclick: close_handler
81 "×"
82 }
83 }
84 }
85 }
86}
87
88/// Builds the click handler that fires a caller's message callback.
89///
90/// The event type is a parameter so the DOM-free test suite can drive the
91/// handler with a stand-in event; the component always instantiates it
92/// with [`Event`].
93///
94/// # Arguments
95///
96/// - `Option<Rc<dyn Fn()>>` - The callback fired by the handler.
97///
98/// # Returns
99///
100/// - `Option<Rc<dyn Fn(E)>>` - The click handler, or `None` when there is no callback.
101pub fn message_callback_handler<E: 'static>(
102 callback: Option<Rc<dyn Fn()>>,
103) -> Option<Rc<dyn Fn(E)>> {
104 callback.map(|handler: Rc<dyn Fn()>| -> Rc<dyn Fn(E)> {
105 Rc::new(move |_: E| {
106 handler();
107 })
108 })
109}