Skip to main content

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}