adui_dioxus/components/
app.rs

1use crate::components::message::{MessageApi, MessageHost, use_message_entries_provider};
2use crate::components::notification::{
3    NotificationApi, NotificationHost, use_notification_entries_provider,
4};
5use crate::components::overlay::{OverlayHandle, OverlayKind, use_overlay_provider};
6use dioxus::prelude::*;
7
8/// Minimal modal API exposed by `App`.
9#[derive(Clone)]
10pub struct ModalApi {
11    overlay: OverlayHandle,
12}
13
14impl ModalApi {
15    pub fn new(overlay: OverlayHandle) -> Self {
16        Self { overlay }
17    }
18
19    /// Placeholder for future `confirm`/`open` helpers.
20    pub fn open(&self) {
21        let _ = self.overlay.open(OverlayKind::Modal, true);
22    }
23}
24
25/// Context value shared by `App` and consumed by `use_app` / `use_message` /
26/// `use_notification` / `use_modal`.
27#[derive(Clone, Default)]
28pub struct AppContextValue {
29    pub message: Option<MessageApi>,
30    pub notification: Option<NotificationApi>,
31    pub modal: Option<ModalApi>,
32}
33
34/// Props for the top-level `App` container.
35#[derive(Props, Clone, PartialEq)]
36pub struct AppProps {
37    #[props(optional)]
38    pub class: Option<String>,
39    #[props(optional)]
40    pub style: Option<String>,
41    pub children: Element,
42}
43
44/// Top-level App component that wires OverlayManager and exposes global
45/// feedback APIs through context.
46#[component]
47pub fn App(props: AppProps) -> Element {
48    let AppProps {
49        class,
50        style,
51        children,
52    } = props;
53
54    // Install a single overlay manager for this App subtree.
55    let overlay = use_overlay_provider();
56
57    // Install per-App message & notification queues in context.
58    let message_entries = use_message_entries_provider();
59    let notification_entries = use_notification_entries_provider();
60
61    let ctx = AppContextValue {
62        message: Some(MessageApi::new(overlay.clone(), message_entries)),
63        notification: Some(NotificationApi::new(overlay.clone(), notification_entries)),
64        modal: Some(ModalApi::new(overlay)),
65    };
66
67    use_context_provider(|| ctx.clone());
68
69    let class_attr = class.unwrap_or_default();
70    let style_attr = style.unwrap_or_default();
71
72    rsx! {
73        div {
74            class: "{class_attr}",
75            style: "{style_attr}",
76            {children}
77        }
78        // Global feedback hosts
79        MessageHost {}
80        NotificationHost {}
81    }
82}
83
84/// Access the full App context. Returns a default value when used outside of
85/// an `App` subtree so that callers can opt into a graceful fallback.
86pub fn use_app() -> AppContextValue {
87    try_use_context::<AppContextValue>().unwrap_or_default()
88}
89
90/// Convenience hook to access the message API.
91pub fn use_message() -> Option<MessageApi> {
92    use_app().message
93}
94
95/// Convenience hook to access the notification API.
96pub fn use_notification() -> Option<NotificationApi> {
97    use_app().notification
98}
99
100/// Convenience hook to access the modal API.
101pub fn use_modal() -> Option<ModalApi> {
102    use_app().modal
103}
104
105#[cfg(test)]
106mod app_tests {
107    use super::*;
108
109    #[test]
110    fn app_context_value_default() {
111        let ctx = AppContextValue::default();
112        assert!(ctx.message.is_none());
113        assert!(ctx.notification.is_none());
114        assert!(ctx.modal.is_none());
115    }
116
117    #[test]
118    fn app_context_value_clone() {
119        let ctx1 = AppContextValue::default();
120        let ctx2 = ctx1.clone();
121        // Verify clone works - both should have None for all fields
122        assert!(ctx1.message.is_none());
123        assert!(ctx2.message.is_none());
124        assert!(ctx1.notification.is_none());
125        assert!(ctx2.notification.is_none());
126        assert!(ctx1.modal.is_none());
127        assert!(ctx2.modal.is_none());
128    }
129
130    #[test]
131    fn app_context_value_with_all_fields() {
132        // Test that AppContextValue can hold all optional fields
133        let ctx = AppContextValue {
134            message: None,
135            notification: None,
136            modal: None,
137        };
138        assert!(ctx.message.is_none());
139        assert!(ctx.notification.is_none());
140        assert!(ctx.modal.is_none());
141    }
142
143    #[test]
144    fn modal_api_structure() {
145        // Verify ModalApi structure and methods exist
146        // Note: Creating an actual instance requires runtime context with Signal
147        // But we can verify the type structure and method signatures
148        fn assert_modal_api_methods() {
149            // ModalApi::new takes OverlayHandle and returns ModalApi
150            // ModalApi::open takes &self and returns ()
151            // These are verified by compilation
152        }
153        assert_modal_api_methods();
154    }
155
156    #[test]
157    fn app_props_structure() {
158        // Verify AppProps can be created with optional fields
159        // Note: Creating actual Element requires runtime context
160        // But we can verify the structure
161        fn assert_app_props_structure() {
162            // AppProps has optional class and style, and required children
163            // This is verified by compilation
164        }
165        assert_app_props_structure();
166    }
167}