Skip to main content

azul_layout/widgets/
modal.rs

1//! Modal / dialog widget — an in-app overlay dialog (NOT the native OS file/
2//! message dialogs, which live in the `dialog` module; this is the custom in-app
3//! variant). A blend of [`crate::widgets::frame::Frame`] (the bordered, elevated
4//! content panel) and [`crate::widgets::popover::Popover`] (overlay show/hide via
5//! `set_css_property(display)` driven by a toggled state).
6//!
7//! Structure: a full-area *backdrop* (`position: absolute`, covering its parent,
8//! semi-transparent black) that centres a *panel* holding an optional title, an
9//! optional "x" close button (absolutely positioned in the panel's top-right
10//! corner), and the arbitrary `content: Dom`. The whole thing is hidden by
11//! default (`display: none`) and shown by building it with `with_open(true)` (or
12//! by the host flipping it). Clicking the close button flips `open` to `false`,
13//! invokes the optional user `on_close(state)`, and hides the backdrop via
14//! `set_css_property(display: none)` (mirroring popover's live restyle).
15//!
16//! TODO2 — several "real modal" behaviours are NOT reachable from a widget
17//! handler and are deliberately omitted (be honest rather than fake them):
18//!   * **Focus-trap** (confining keyboard focus to the dialog while open) depends
19//!     on the focus model and is not controllable from a widget handler.
20//!   * **Escape-to-close** depends on a global key handler the widget does not own
21//!     (the panel/backdrop are not keyboard-focused), so it is not wired.
22//!   * **Backdrop-click-to-close** is NOT wired: with `currentTarget` hit
23//!     semantics (see `popover`), a click handler on the backdrop reports the
24//!     backdrop as the hit node even when the *panel* (a descendant) was clicked,
25//!     so it cannot distinguish an outside click from an inside click — wiring it
26//!     would close the dialog when clicking its own content. Only the explicit "x"
27//!     closes it.
28//!   * **Covering sibling widgets**: the backdrop is `position: absolute` and
29//!     relies on paint order (being a later sibling) to overlay other content;
30//!     there is no real stacking-context / z-index. Place the modal as the LAST
31//!     child of a positioned, full-size container for a correct overlay.
32//!   * The `display:none/flex` relayout itself is not GUI-verified in this build.
33//!
34//! Key types: [`Modal`], [`ModalState`], [`ModalOnClose`].
35
36use azul_core::{
37    callbacks::{CoreCallback, CoreCallbackData, Update},
38    dom::{Dom, DomVec, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
39    refany::RefAny,
40};
41use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
42use azul_css::{
43    props::{
44        basic::{color::ColorU, font::{StyleFontFamily, StyleFontFamilyVec}, PixelValue, StyleFontSize},
45        layout::{LayoutDisplay, LayoutPosition, LayoutTop, LayoutLeft, LayoutWidth, LayoutHeight, LayoutFlexDirection, LayoutJustifyContent, LayoutAlignItems, LayoutFlexGrow, LayoutMinWidth, LayoutMaxWidth, LayoutPaddingTop, LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight, LayoutRight},
46        property::{CssProperty, *},
47        style::{StyleBackgroundContentVec, StyleBackgroundContent, LayoutBorderTopWidth, LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth, StyleBorderTopStyle, BorderStyle, StyleBorderBottomStyle, StyleBorderLeftStyle, StyleBorderRightStyle, StyleBorderTopColor, StyleBorderBottomColor, StyleBorderLeftColor, StyleBorderRightColor, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius, StyleTextColor, StyleTextAlign, StyleUserSelect, StyleCursor},
48    },
49    impl_option_inner, AzString,
50};
51
52use crate::callbacks::{Callback, CallbackInfo};
53
54static MODAL_BACKDROP_CLASS: &[IdOrClass] =
55    &[Class(AzString::from_const_str("__azul-native-modal"))];
56static MODAL_PANEL_CLASS: &[IdOrClass] =
57    &[Class(AzString::from_const_str("__azul-native-modal-panel"))];
58static MODAL_TITLE_CLASS: &[IdOrClass] =
59    &[Class(AzString::from_const_str("__azul-native-modal-title"))];
60static MODAL_CLOSE_CLASS: &[IdOrClass] =
61    &[Class(AzString::from_const_str("__azul-native-modal-close"))];
62static MODAL_CONTENT_CLASS: &[IdOrClass] =
63    &[Class(AzString::from_const_str("__azul-native-modal-content"))];
64
65const SYSTEM_UI_STR: AzString = AzString::from_const_str("system:ui");
66const SYSTEM_UI_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SYSTEM_UI_STR)];
67const SYSTEM_UI_FAMILY: StyleFontFamilyVec =
68    StyleFontFamilyVec::from_const_slice(SYSTEM_UI_FAMILIES);
69
70// ---- layout (logical px) ----
71const PANEL_MIN_WIDTH: isize = 280;
72const PANEL_MAX_WIDTH: isize = 520;
73const PANEL_RADIUS: isize = 8;
74
75// ---- colours ----
76/// Semi-transparent black backdrop (rgba(0,0,0,0.5)).
77const BACKDROP_COLOR: ColorU = ColorU { r: 0, g: 0, b: 0, a: 128 };
78const PANEL_BG_COLOR: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
79const PANEL_BORDER_COLOR: ColorU = ColorU { r: 204, g: 204, b: 204, a: 255 }; // #cccccc
80const TITLE_COLOR: ColorU = ColorU { r: 33, g: 37, b: 41, a: 255 }; // #212529
81const CLOSE_COLOR: ColorU = ColorU { r: 108, g: 117, b: 125, a: 255 }; // #6c757d
82
83/// Callback invoked when the modal's "x" close button is clicked. The
84/// [`ModalState`] carries the *new* (`false`) open value.
85pub type ModalOnCloseCallbackType = extern "C" fn(RefAny, CallbackInfo, ModalState) -> Update;
86impl_widget_callback!(
87    ModalOnClose,
88    OptionModalOnClose,
89    ModalOnCloseCallback,
90    ModalOnCloseCallbackType
91);
92
93azul_core::impl_managed_callback! {
94    wrapper:        ModalOnCloseCallback,
95    info_ty:        CallbackInfo,
96    return_ty:      Update,
97    default_ret:    Update::DoNothing,
98    invoker_static: MODAL_ON_CLOSE_INVOKER,
99    invoker_ty:     AzModalOnCloseCallbackInvoker,
100    thunk_fn:       az_modal_on_close_callback_thunk,
101    setter_fn:      AzApp_setModalOnCloseCallbackInvoker,
102    from_handle_fn: AzModalOnCloseCallback_createFromHostHandle,
103    extra_args:     [ state: ModalState ],
104}
105
106/// An in-app overlay dialog holding arbitrary content, with an optional title and
107/// close button.
108#[derive(Debug, Clone, PartialEq, Eq)]
109#[repr(C)]
110pub struct Modal {
111    /// Runtime state (`open`) plus the optional close callback.
112    pub modal_state: ModalStateWrapper,
113    /// The dialog title (empty = no title bar).
114    pub title: AzString,
115    /// The arbitrary content shown inside the panel.
116    pub content: Dom,
117    /// Whether to render the "x" close button.
118    pub show_close_button: bool,
119    /// Style of the full-area backdrop (includes its current `display`).
120    pub backdrop_style: CssPropertyWithConditionsVec,
121}
122
123#[derive(Debug, Default, Clone, PartialEq, Eq)]
124#[repr(C)]
125pub struct ModalStateWrapper {
126    /// Whether the dialog is currently open (shown).
127    pub inner: ModalState,
128    /// Optional: function to call when the dialog is closed.
129    pub on_close: OptionModalOnClose,
130}
131
132/// The open/closed state of a [`Modal`].
133#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
134#[repr(C)]
135pub struct ModalState {
136    /// `true` = dialog shown, `false` (default) = dialog hidden.
137    pub open: bool,
138}
139
140/// Builds the backdrop style. Only the `display` (open vs closed) differs; all
141/// other props are present in both so the runtime `set_css_property(display)`
142/// toggle has everything it needs (mirroring popover/accordion).
143fn build_backdrop_style(open: bool) -> CssPropertyWithConditionsVec {
144    let display = if open {
145        LayoutDisplay::Flex
146    } else {
147        LayoutDisplay::None
148    };
149    let bg_vec = StyleBackgroundContentVec::from_vec(alloc::vec![StyleBackgroundContent::Color(
150        BACKDROP_COLOR
151    )]);
152    CssPropertyWithConditionsVec::from_vec(alloc::vec![
153        CssPropertyWithConditions::simple(CssProperty::const_display(display)),
154        CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
155        CssPropertyWithConditions::simple(CssProperty::const_top(LayoutTop::const_px(0))),
156        CssPropertyWithConditions::simple(CssProperty::const_left(LayoutLeft::const_px(0))),
157        // Cover the full parent (see the z-order TODO2 — depends on a full-size,
158        // positioned parent).
159        CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::Px(
160            PixelValue::const_percent(100),
161        ))),
162        CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::Px(
163            PixelValue::const_percent(100),
164        ))),
165        // Centre the panel.
166        CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
167            LayoutFlexDirection::Row,
168        )),
169        CssPropertyWithConditions::simple(CssProperty::const_justify_content(
170            LayoutJustifyContent::Center,
171        )),
172        CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
173        CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
174        CssPropertyWithConditions::simple(CssProperty::const_background_content(bg_vec)),
175    ])
176}
177
178/// The centred dialog panel: a bordered, rounded white box (frame-like). Elevation
179/// is conveyed by the dimmed backdrop behind it + the border/radius; a drop
180/// `box-shadow` is intentionally omitted (it requires a runtime-heap shadow value
181/// — see `progressbar.rs` — and is not needed for a clear modal read).
182static MODAL_PANEL_STYLE: &[CssPropertyWithConditions] = &[
183    CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Relative)),
184    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
185    CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Column)),
186    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
187    CssPropertyWithConditions::simple(CssProperty::const_min_width(LayoutMinWidth::const_px(
188        PANEL_MIN_WIDTH,
189    ))),
190    CssPropertyWithConditions::simple(CssProperty::const_max_width(LayoutMaxWidth::const_px(
191        PANEL_MAX_WIDTH,
192    ))),
193    // padding: 20px
194    CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(20))),
195    CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
196        LayoutPaddingBottom::const_px(20),
197    )),
198    CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(
199        20,
200    ))),
201    CssPropertyWithConditions::simple(CssProperty::const_padding_right(
202        LayoutPaddingRight::const_px(20),
203    )),
204    // border: 1px solid #cccccc
205    CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
206        LayoutBorderTopWidth::const_px(1),
207    )),
208    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
209        LayoutBorderBottomWidth::const_px(1),
210    )),
211    CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
212        LayoutBorderLeftWidth::const_px(1),
213    )),
214    CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
215        LayoutBorderRightWidth::const_px(1),
216    )),
217    CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
218        inner: BorderStyle::Solid,
219    })),
220    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
221        StyleBorderBottomStyle {
222            inner: BorderStyle::Solid,
223        },
224    )),
225    CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
226        inner: BorderStyle::Solid,
227    })),
228    CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
229        StyleBorderRightStyle {
230            inner: BorderStyle::Solid,
231        },
232    )),
233    CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
234        inner: PANEL_BORDER_COLOR,
235    })),
236    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
237        StyleBorderBottomColor {
238            inner: PANEL_BORDER_COLOR,
239        },
240    )),
241    CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
242        inner: PANEL_BORDER_COLOR,
243    })),
244    CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
245        StyleBorderRightColor {
246            inner: PANEL_BORDER_COLOR,
247        },
248    )),
249    // border-radius: 8px
250    CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
251        StyleBorderTopLeftRadius::const_px(PANEL_RADIUS),
252    )),
253    CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
254        StyleBorderTopRightRadius::const_px(PANEL_RADIUS),
255    )),
256    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
257        StyleBorderBottomLeftRadius::const_px(PANEL_RADIUS),
258    )),
259    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
260        StyleBorderBottomRightRadius::const_px(PANEL_RADIUS),
261    )),
262    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(14))),
263    CssPropertyWithConditions::simple(CssProperty::const_font_family(SYSTEM_UI_FAMILY)),
264    CssPropertyWithConditions::simple(CssProperty::const_background_content(
265        StyleBackgroundContentVec::from_const_slice(&[StyleBackgroundContent::Color(
266            PANEL_BG_COLOR,
267        )]),
268    )),
269];
270
271/// Title style: larger, bold-ish dark text with a bottom gap; right padding keeps
272/// it clear of the absolutely-positioned "x".
273static MODAL_TITLE_STYLE: &[CssPropertyWithConditions] = &[
274    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
275    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(18))),
276    CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
277        inner: TITLE_COLOR,
278    })),
279    CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Left)),
280    CssPropertyWithConditions::simple(CssProperty::const_padding_right(
281        LayoutPaddingRight::const_px(24),
282    )),
283    CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
284        LayoutPaddingBottom::const_px(12),
285    )),
286    CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
287];
288
289/// "x" close-button style: an absolutely-positioned pointer-cursor glyph in the
290/// panel's top-right corner.
291static MODAL_CLOSE_STYLE: &[CssPropertyWithConditions] = &[
292    CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
293    CssPropertyWithConditions::simple(CssProperty::const_top(LayoutTop::const_px(8))),
294    CssPropertyWithConditions::simple(CssProperty::const_right(LayoutRight::const_px(12))),
295    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(22))),
296    CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
297        inner: CLOSE_COLOR,
298    })),
299    CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
300    CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
301];
302
303/// Content-wrapper style: takes the remaining vertical space.
304static MODAL_CONTENT_STYLE: &[CssPropertyWithConditions] = &[
305    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Block)),
306    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
307];
308
309impl Modal {
310    /// Creates a new (closed) modal holding `content`, with a "x" close button and
311    /// no title.
312    #[must_use] pub fn create(content: Dom) -> Self {
313        Self {
314            modal_state: ModalStateWrapper::default(),
315            title: AzString::from_const_str(""),
316            content,
317            show_close_button: true,
318            backdrop_style: build_backdrop_style(false),
319        }
320    }
321
322    /// Sets the dialog title (empty = no title).
323    #[inline]
324    pub fn set_title(&mut self, title: AzString) {
325        self.title = title;
326    }
327
328    /// Builder-style setter for the title.
329    #[inline]
330    #[must_use] pub fn with_title(mut self, title: AzString) -> Self {
331        self.set_title(title);
332        self
333    }
334
335    /// Replaces the content shown inside the panel.
336    #[inline]
337    pub fn set_content(&mut self, content: Dom) {
338        self.content = content;
339    }
340
341    /// Builder-style setter for the content.
342    #[inline]
343    #[must_use] pub fn with_content(mut self, content: Dom) -> Self {
344        self.set_content(content);
345        self
346    }
347
348    /// Sets whether the dialog is currently open, recomputing the backdrop style.
349    #[inline]
350    pub fn set_open(&mut self, open: bool) {
351        self.modal_state.inner.open = open;
352        self.backdrop_style = build_backdrop_style(open);
353    }
354
355    /// Builder-style setter for the initial open state.
356    #[inline]
357    #[must_use] pub fn with_open(mut self, open: bool) -> Self {
358        self.set_open(open);
359        self
360    }
361
362    /// Sets whether the "x" close button is shown.
363    #[inline]
364    pub const fn set_close_button(&mut self, show: bool) {
365        self.show_close_button = show;
366    }
367
368    /// Builder-style setter for the close-button flag.
369    #[inline]
370    #[must_use] pub const fn with_close_button(mut self, show: bool) -> Self {
371        self.set_close_button(show);
372        self
373    }
374
375    /// Sets the close callback (invoked with the new state when "x" is clicked).
376    #[inline]
377    pub fn set_on_close<C: Into<ModalOnCloseCallback>>(&mut self, data: RefAny, on_close: C) {
378        self.modal_state.on_close = Some(ModalOnClose {
379            callback: on_close.into(),
380            refany: data,
381        })
382        .into();
383    }
384
385    /// Builder-style setter for the close callback.
386    #[inline]
387    #[must_use] pub fn with_on_close<C: Into<ModalOnCloseCallback>>(
388        mut self,
389        data: RefAny,
390        on_close: C,
391    ) -> Self {
392        self.set_on_close(data, on_close);
393        self
394    }
395
396    /// Replaces `self` with a default (empty, closed) modal and returns the original.
397    #[inline]
398    #[must_use] pub fn swap_with_default(&mut self) -> Self {
399        let mut s = Self::create(Dom::default());
400        core::mem::swap(&mut s, self);
401        s
402    }
403
404    /// Renders the modal into a [`Dom`] subtree with the `__azul-native-modal`
405    /// class (the backdrop).
406    #[must_use] pub fn dom(self) -> Dom {
407        // Panel children: [close?, title?, content]. The close button is
408        // absolutely positioned (top-right), so its document order does not affect
409        // the title/content stacking.
410        let mut panel_children = Vec::new();
411
412        if self.show_close_button {
413            let close = Dom::create_text(AzString::from_const_str("\u{00D7}"))
414                .with_ids_and_classes(IdOrClassVec::from_const_slice(MODAL_CLOSE_CLASS))
415                .with_css_props(CssPropertyWithConditionsVec::from_const_slice(MODAL_CLOSE_STYLE))
416                .with_tab_index(TabIndex::Auto)
417                .with_callbacks(
418                    alloc::vec![CoreCallbackData {
419                        event: azul_core::dom::EventFilter::Hover(
420                            azul_core::dom::HoverEventFilter::MouseUp,
421                        ),
422                        callback: CoreCallback {
423                            cb: on_modal_close as usize,
424                            ctx: azul_core::refany::OptionRefAny::None,
425                        },
426                        refany: RefAny::new(self.modal_state),
427                    }]
428                    .into(),
429                );
430            panel_children.push(close);
431        }
432
433        if !self.title.as_str().is_empty() {
434            let title = Dom::create_text(self.title)
435                .with_ids_and_classes(IdOrClassVec::from_const_slice(MODAL_TITLE_CLASS))
436                .with_css_props(CssPropertyWithConditionsVec::from_const_slice(MODAL_TITLE_STYLE));
437            panel_children.push(title);
438        }
439
440        let content = Dom::create_div()
441            .with_ids_and_classes(IdOrClassVec::from_const_slice(MODAL_CONTENT_CLASS))
442            .with_css_props(CssPropertyWithConditionsVec::from_const_slice(MODAL_CONTENT_STYLE))
443            .with_children(DomVec::from_vec(alloc::vec![self.content]));
444        panel_children.push(content);
445
446        let panel = Dom::create_div()
447            .with_ids_and_classes(IdOrClassVec::from_const_slice(MODAL_PANEL_CLASS))
448            .with_css_props(CssPropertyWithConditionsVec::from_const_slice(MODAL_PANEL_STYLE))
449            .with_children(DomVec::from_vec(panel_children));
450
451        Dom::create_div()
452            .with_ids_and_classes(IdOrClassVec::from_const_slice(MODAL_BACKDROP_CLASS))
453            .with_css_props(self.backdrop_style)
454            .with_children(DomVec::from_vec(alloc::vec![panel]))
455    }
456}
457
458impl Default for Modal {
459    fn default() -> Self {
460        Self::create(Dom::default())
461    }
462}
463
464/// "x" close-button click handler. The hit node is the close button (the
465/// callback-bearing node, per `currentTarget` semantics — see `popover`); its
466/// parent is the panel and the panel's parent is the backdrop. Flips `open` to
467/// `false`, invokes the optional user callback, then hides the backdrop via
468/// `display: none`.
469extern "C" fn on_modal_close(mut data: RefAny, mut info: CallbackInfo) -> Update {
470    let close_node = info.get_hit_node();
471    let Some(panel) = info.get_parent(close_node) else {
472        return Update::DoNothing;
473    };
474    let Some(backdrop) = info.get_parent(panel) else {
475        return Update::DoNothing;
476    };
477
478    let result = {
479        let Some(mut modal) = data.downcast_mut::<ModalStateWrapper>() else {
480            return Update::DoNothing;
481        };
482        modal.inner.open = false;
483        let inner = modal.inner;
484        let modal = &mut *modal;
485        match modal.on_close.as_mut() {
486            Some(ModalOnClose { callback, refany }) => (callback.cb)(refany.clone(), info, inner),
487            None => Update::DoNothing,
488        }
489    };
490
491    // TODO2: hides the whole dialog by toggling `display: none` via
492    // set_css_property (the proven live-restyle pattern of popover/alert); the
493    // relayout itself is not GUI-verified in this build.
494    info.set_css_property(backdrop, CssProperty::const_display(LayoutDisplay::None));
495
496    result
497}
498
499impl From<Modal> for Dom {
500    fn from(m: Modal) -> Self {
501        m.dom()
502    }
503}