1
//! Built-in widgets for the Azul GUI system
2

            
3
/// Implements `Display, Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Hash`
4
/// for a Callback with a `.cb` field.
5
///
6
/// This is necessary to work around for <https://github.com/rust-lang/rust/issues/54508>
7
///
8
/// # Host-invoker plumbing for managed-FFI bindings
9
///
10
/// Widget callbacks have varying shapes — some are
11
/// `(RefAny, CallbackInfo) -> Update` (Button), others add a state
12
/// struct (CheckBox/Tab/etc.), a few have two extras (`ListView`). The
13
/// macro therefore does **not** auto-emit an `impl_managed_callback!`
14
/// invocation; per-widget files apply it themselves with the right
15
/// extras list. The base invocation still produces the standard
16
/// `Display`/`Debug`/`Clone`/`From<CallbackType>`/`From<Callback>` impls
17
/// that all widget callbacks share.
18
#[macro_export]
19
macro_rules! impl_widget_callback {
20
    (
21
        $callback_wrapper:ident,
22
        $option_callback_wrapper:ident,
23
        $callback_value:ident,
24
        $callback_ty:ident
25
    ) => {
26
        #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
27
        #[repr(C)]
28
        pub struct $callback_wrapper {
29
            pub refany: RefAny,
30
            pub callback: $callback_value,
31
        }
32

            
33
        #[repr(C)]
34
        pub struct $callback_value {
35
            pub cb: $callback_ty,
36
            /// For FFI: stores the foreign callable (e.g., `PyFunction`)
37
            /// Native Rust code sets this to None
38
            pub ctx: azul_core::refany::OptionRefAny,
39
        }
40

            
41
        azul_css::impl_option!(
42
            $callback_wrapper,
43
            $option_callback_wrapper,
44
            copy = false,
45
            [Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
46
        );
47

            
48
        impl $callback_value {
49
            /// Create a new callback with just a function pointer (for native Rust code)
50
            pub fn create<I: Into<$callback_value>>(cb: I) -> $callback_value {
51
                cb.into()
52
            }
53
        }
54

            
55
        impl ::core::fmt::Display for $callback_value {
56
            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
57
                write!(f, "{:?}", self)
58
            }
59
        }
60

            
61
        impl ::core::fmt::Debug for $callback_value {
62
            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
63
                let callback = stringify!($callback_value);
64
                write!(f, "{} @ 0x{:x}", callback, self.cb as *const () as usize)
65
            }
66
        }
67

            
68
        impl Clone for $callback_value {
69
5841
            fn clone(&self) -> Self {
70
5841
                $callback_value {
71
5841
                    cb: self.cb.clone(),
72
5841
                    ctx: self.ctx.clone(),
73
5841
                }
74
5841
            }
75
        }
76

            
77
        impl core::hash::Hash for $callback_value {
78
            fn hash<H>(&self, state: &mut H)
79
            where
80
                H: ::core::hash::Hasher,
81
            {
82
                state.write_usize(self.cb as *const () as usize);
83
            }
84
        }
85

            
86
        impl PartialEq for $callback_value {
87
44
            fn eq(&self, rhs: &Self) -> bool {
88
44
                self.cb as *const () as usize == rhs.cb as usize
89
44
            }
90
        }
91

            
92
        impl PartialOrd for $callback_value {
93
            fn partial_cmp(&self, other: &Self) -> Option<::core::cmp::Ordering> {
94
                Some((self.cb as *const () as usize).cmp(&(other.cb as usize)))
95
            }
96
        }
97

            
98
        impl Ord for $callback_value {
99
            fn cmp(&self, other: &Self) -> ::core::cmp::Ordering {
100
                (self.cb as *const () as usize).cmp(&(other.cb as usize))
101
            }
102
        }
103

            
104
        impl Eq for $callback_value {}
105

            
106
        /// Allow creating callback from a raw function pointer
107
        /// Sets callable to None (for native Rust/C usage)
108
        impl From<$callback_ty> for $callback_value {
109
722
            fn from(cb: $callback_ty) -> $callback_value {
110
722
                $callback_value {
111
722
                    cb,
112
722
                    ctx: azul_core::refany::OptionRefAny::None,
113
722
                }
114
722
            }
115
        }
116

            
117
        /// Allow creating widget callback from a generic Callback
118
        /// This enables Python/FFI code to pass generic callbacks to widget methods
119
        impl From<$crate::callbacks::Callback> for $callback_value {
120
            // transmute target ($callback_value's cb fn-ptr type) varies per macro
121
            // instantiation, so an explicit annotation can't be written generically here.
122
            #[allow(clippy::missing_transmute_annotations, clippy::useless_transmute)]
123
13
            fn from(cb: $crate::callbacks::Callback) -> $callback_value {
124
13
                $callback_value {
125
13
                    cb: unsafe { core::mem::transmute(cb.cb) },
126
13
                    ctx: cb.ctx,
127
13
                }
128
13
            }
129
        }
130
    };
131
}
132

            
133
/// Button widget
134
pub mod button;
135
/// Checkbox widget
136
pub mod check_box;
137
/// Box displaying a color with a callback for value changes
138
pub mod color_input;
139
/// File input widget
140
pub mod file_input;
141
/// Label widget (centered text)
142
pub mod label;
143
/// Drop-down select widget
144
pub mod drop_down;
145
/// Frame container widget
146
pub mod frame;
147
/// List view widget
148
pub mod list_view;
149
/// Shared core for the video-ish widgets (camera/screencap/video): the
150
/// `VideoFrame` type + the GL-texture `present_frame` writeback.
151
///
152
/// See
153
/// `capture_common.rs`.
154
pub mod capture_common;
155
/// Camera-preview widget (P6) — a "dumb widget" owning a background capture
156
/// thread + a GL-texture ImageRef; no camera logic in core.
157
///
158
/// Same RefAny-
159
/// dataset + merge-callback design as the map widget. See `camera.rs`.
160
pub mod camera;
161
/// Screen-capture widget (P6) — identical "dumb widget" architecture to the
162
/// camera widget, capturing a display/window instead.
163
///
164
/// See `screencap.rs`.
165
pub mod screencap;
166
/// Video-playback widget (P6) — same "dumb widget" architecture, decoding a
167
/// video source (vk-video) into a GL texture.
168
///
169
/// See `video.rs`.
170
pub mod video;
171
/// Microphone-capture widget (P7) — same "dumb widget" architecture as the
172
/// capture widgets, audio instead of video (no GL): a background thread feeds
173
/// each `AudioFrame` to the user's `on_frame` hook.
174
///
175
/// See `microphone.rs`.
176
pub mod microphone;
177
/// Map widget — MVT tile + MapCSS → SVG → DOM (AzulMaps goal app, P3).
178
///
179
/// Cache lives in a dataset RefAny owned by a merge callback so it
180
/// survives relayout. See `layout/src/widgets/map.rs` for the design.
181
pub mod map;
182
/// Software menu-bar widget (Linux fallback when there is no native global menu).
183
///
184
/// Renders a window's `Menu` as a horizontal bar; items open dropdowns via the
185
/// unified `WindowPosition::RelativeToParentWindow` popup path.
186
pub mod menubar;
187
/// Node graph widget
188
pub mod node_graph;
189
/// Same as text input, but only allows numeric input
190
pub mod number_input;
191
/// Progress bar widget
192
pub mod progressbar;
193
/// Ribbon widget
194
pub mod ribbon;
195
/// Office-style backstage view (the full-window "FILE" screen): accent nav
196
/// column + back ring + app-provided pane content. the Office-2013-era look look by default;
197
/// pairs with the ribbon's `RibbonAppButton`. See `backstage.rs`.
198
pub mod backstage;
199
/// Office-style status bar: left text segments, view switcher, zoom cluster
200
/// (embeds the `slider` widget). the Office-2013-era look look by default. See `statusbar.rs`.
201
pub mod statusbar;
202
/// Office-style title band with a Quick Access Toolbar (save/undo/redo),
203
/// centered title and window buttons, drawn as DOM. the Office-2013-era look look by
204
/// default; use `titlebar` instead for native-caption windows. See
205
/// `quick_access.rs`.
206
pub mod quick_access;
207
/// Tab container widgets
208
pub mod tabs;
209
/// Single line text input widget
210
pub mod text_input;
211
/// Titlebar widget for custom window chrome
212
pub mod titlebar;
213
/// Tree view widget
214
pub mod tree_view;
215
/// Switch / toggle widget.
216
///
217
/// Boolean on/off with a sliding knob; see `switch.rs`.
218
pub mod switch;
219
/// Divider / separator rule widget (horizontal or vertical).
220
///
221
/// See `divider.rs`.
222
pub mod divider;
223
/// Card container widget.
224
///
225
/// Elevated/bordered content box (no title); see `card.rs`.
226
pub mod card;
227
/// Badge widget.
228
///
229
/// A small rounded count/status pill (stateless); see `badge.rs`.
230
pub mod badge;
231
/// Slider / range widget.
232
///
233
/// Draggable thumb on a track → numeric value; see `slider.rs`.
234
pub mod slider;
235
/// Segmented control widget.
236
///
237
/// Joined row of mutually-exclusive buttons; see `segmented.rs`.
238
pub mod segmented;
239
/// Radio-group widget.
240
///
241
/// Vertical/horizontal group of mutually-exclusive options (exactly one selected) with a circular indicator; see `radio_group.rs`.
242
pub mod radio_group;
243
/// Tooltip widget.
244
///
245
/// Shows a small text popup near an anchor on hover; see `tooltip.rs`.
246
pub mod tooltip;
247
/// Multi-line text input (text area) widget.
248
///
249
/// See `text_area.rs`.
250
pub mod text_area;
251
/// Alert / banner widget.
252
///
253
/// A coloured inline message box with an optional dismissible close button; see `alert.rs`.
254
pub mod alert;
255
/// Accordion / expander widget.
256
///
257
/// One or more collapsible titled sections; see `accordion.rs`.
258
pub mod accordion;
259
/// Avatar widget.
260
///
261
/// A circular image/initials badge (stateless); see `avatar.rs`.
262
pub mod avatar;
263
/// Chip / tag widget.
264
///
265
/// A compact rounded pill with a label + optional removable "x" (stateful when removable, mirrors alert's dismiss); see `chip.rs`.
266
pub mod chip;
267
/// Spinner / activity widget.
268
///
269
/// A static indeterminate busy ring (stateless; no animation — see the file's PARTIAL/TODO2 note); see `spinner.rs`.
270
pub mod spinner;
271
/// Popover widget.
272
///
273
/// A click-triggered floating panel holding arbitrary content, anchored to a `Dom` (the click-toggled sibling of tooltip); see `popover.rs`.
274
pub mod popover;
275
/// Combobox widget.
276
///
277
/// An editable text field with a click-toggled drop-down list of options (drop_down's select + text_input's editable field); see `combobox.rs`.
278
pub mod combobox;
279
/// Modal / dialog widget.
280
///
281
/// An in-app overlay dialog (backdrop + centred panel + arbitrary content), shown/hidden via state toggle; see `modal.rs`.
282
pub mod modal;
283
/// Toast / snackbar widget.
284
///
285
/// A transient floating notification banner pinned to a corner, manually dismissed via "x" (auto-timeout needs a host timer — see the file's TODO2); a near-clone of `alert.rs` positioned as an overlay; see `toast.rs`.
286
pub mod toast;
287
/// Breadcrumb widget.
288
///
289
/// A horizontal trail of clickable crumb links separated by "/", ending in the current (non-clickable) page; see `breadcrumb.rs`.
290
pub mod breadcrumb;
291
/// Pagination widget.
292
///
293
/// A `Prev` / page-numbers / `Next` page navigator with an active-page restyle (segmented-style); see `pagination.rs`.
294
pub mod pagination;
295
/// Stepper / wizard widget.
296
///
297
/// A horizontal numbered-step progress indicator with connector lines and an accent/muted restyle on step change (segmented-style + progressbar-style filled connector); see `stepper.rs`.
298
pub mod stepper;
299
/// Split-pane / splitter widget.
300
///
301
/// A two-pane (horizontal/vertical) container with a draggable divider that live-resizes the panes via `set_css_property` (the frame two-box layout + the map/slider pointer-drag state machine); see `split_pane.rs`.
302
pub mod split_pane;
303
/// Time picker widget.
304
///
305
/// Two clamped numeric up/down spinners (hour + minute) side by side with an optional AM/PM toggle for 12-hour mode (the number_input clamp/retext path + segmented's clickable-cell navigation); see `time_picker.rs`.
306
pub mod time_picker;
307
/// Calendar date picker widget.
308
///
309
/// A month header (‹ / `Month YYYY` / ›) above a weekday-labelled 7-column day grid computed from real calendar math; clicking a day selects + restyles it (segmented-style), and the per-cell day number is carried drop_down-style. Month nav fires on_change but cannot rebuild the grid in-widget (prominent module TODO2); see `date_picker.rs`.
310
pub mod date_picker;
311
// /// Spreadsheet (virtualized view) widget
312
// pub mod spreadsheet;
313

            
314
/// Every shipped widget's `dom()` with reasonable defaults, for lints that
315
/// must hold across the whole widget set (the label-convention test below and
316
/// `dom_lint`'s runtime-warning twin). Test-only.
317
#[cfg(test)]
318
1
pub(crate) fn all_widget_doms_for_lint() -> Vec<(&'static str, azul_core::dom::Dom)> {
319
1
    label_convention::every_widget_dom()
320
1
}
321

            
322
#[cfg(test)]
323
#[allow(clippy::too_many_lines)]
324
mod label_convention {
325
    //! Workspace-level enforcement of the widget label convention (USER ruling,
326
    //! 2026-08-12): a widget must never attach state to a raw text node.
327
    //!
328
    //! `NodeType::Text` is unconditionally inline-level
329
    //! (`solver3::layout_tree`): it is given no rect and no `UnifiedLayout` of
330
    //! its own — the wrapping block box carries those. Anything attached to a
331
    //! text node is therefore attached to a box-less node and is silently
332
    //! INERT: box-model properties never paint, callbacks and `tab_index` have
333
    //! no hit area, and a dataset has no node to be found on.
334
    //!
335
    //! The canonical shape is `Dom::create_p_with_text(label)` (or
336
    //! `create_p().with_children([create_text_do_not_use_without_block_level_wrapper(label)])`) with every property on
337
    //! the `<p>`, or — where a dedicated styled `<div>` already is the box — a
338
    //! bare `create_text` leaf with the properties on that `<div>`.
339
    //!
340
    //! This generalises `ribbon`'s per-widget invariant test to every widget in
341
    //! the crate. Widgets that emit no text at all are still instantiated, so
342
    //! the list doubles as a smoke test that every `dom()` builds.
343

            
344
    use azul_core::dom::{Dom, NodeType};
345
    use azul_css::{props::basic::color::ColorU, AzString, OptionString, StringVec};
346

            
347
    /// Everything a node can carry that only a real box can honour, in the
348
    /// order the failure message lists it.
349
125
    fn inert_state_on(node: &Dom) -> Vec<&'static str> {
350
125
        let mut found = Vec::new();
351
125
        if !node.root.style.rules.as_ref().is_empty() {
352
1
            found.push("css props");
353
124
        }
354
        // A subtree stylesheet on a childless text node can only target the text
355
        // node itself (`with_css("width: …")` parses to `* { … }`), so it is the
356
        // same violation wearing the other API.
357
125
        if !node.css.as_ref().is_empty() {
358
            found.push("subtree css");
359
125
        }
360
125
        if !node.root.get_callbacks().as_ref().is_empty() {
361
            found.push("callbacks");
362
125
        }
363
125
        if node.root.get_tab_index().is_some() {
364
1
            found.push("tab_index");
365
124
        }
366
125
        if node.root.get_dataset().is_some() {
367
            found.push("dataset");
368
125
        }
369
125
        if !node.children.as_ref().is_empty() {
370
            found.push("children");
371
125
        }
372
125
        found
373
125
    }
374

            
375
437
    fn walk(node: &Dom, widget: &str, bad: &mut Vec<String>) {
376
437
        if let NodeType::Text(text) = node.root.get_node_type() {
377
125
            let found = inert_state_on(node);
378
125
            if !found.is_empty() {
379
1
                bad.push(format!(
380
1
                    "{widget}: text node {:?} carries {} — move it onto a wrapping <p> \
381
1
                     (or onto the styled <div> that already boxes it)",
382
1
                    text.as_ref().as_str(),
383
1
                    found.join(" + "),
384
1
                ));
385
124
            }
386
312
        }
387
437
        for child in node.children.as_ref() {
388
390
            walk(child, widget, bad);
389
390
        }
390
437
    }
391

            
392
16
    fn labels(items: &[&str]) -> StringVec {
393
38
        StringVec::from_vec(items.iter().map(|s| AzString::from(*s)).collect::<Vec<_>>())
394
16
    }
395

            
396
    /// A user-content placeholder for the widgets that embed an arbitrary
397
    /// caller-supplied `Dom`. Deliberately property-free: this test governs
398
    /// what *widgets* emit, not what an application passes in.
399
22
    fn user_content() -> Dom {
400
22
        Dom::create_div()
401
22
    }
402

            
403
2
    fn node_graph_fixture() -> super::node_graph::NodeGraph {
404
        use super::node_graph::{
405
            InputConnectionVec, InputOutputInfo, InputOutputTypeId, InputOutputTypeIdInfoMap,
406
            InputOutputTypeIdInfoMapVec, InputOutputTypeIdVec, Node, NodeGraph, NodeGraphNodeId,
407
            NodeGraphNodePosition, NodeIdNodeMap, NodeIdNodeMapVec, NodeTypeField,
408
            NodeTypeFieldValue, NodeTypeFieldVec, NodeTypeId, NodeTypeIdInfoMap,
409
            NodeTypeIdInfoMapVec, NodeTypeInfo, OutputConnectionVec,
410
        };
411

            
412
        const TYPE_A: NodeTypeId = NodeTypeId { inner: 1 };
413
        const IO_A: InputOutputTypeId = InputOutputTypeId { inner: 1 };
414

            
415
2
        NodeGraph {
416
2
            node_types: NodeTypeIdInfoMapVec::from_vec(vec![NodeTypeIdInfoMap {
417
2
                node_type_id: TYPE_A,
418
2
                node_type_info: NodeTypeInfo {
419
2
                    is_root: true,
420
2
                    node_type_name: AzString::from("Add"),
421
2
                    inputs: InputOutputTypeIdVec::from_vec(vec![IO_A]),
422
2
                    outputs: InputOutputTypeIdVec::from_vec(vec![IO_A]),
423
2
                },
424
2
            }]),
425
2
            input_output_types: InputOutputTypeIdInfoMapVec::from_vec(vec![
426
2
                InputOutputTypeIdInfoMap {
427
2
                    io_type_id: IO_A,
428
2
                    io_info: InputOutputInfo {
429
2
                        data_type: AzString::from("number"),
430
2
                        color: ColorU { r: 0, g: 0, b: 0, a: 255 },
431
2
                    },
432
2
                },
433
2
            ]),
434
2
            nodes: NodeIdNodeMapVec::from_vec(vec![NodeIdNodeMap {
435
2
                node_id: NodeGraphNodeId { inner: 1 },
436
2
                node: Node {
437
2
                    node_type: TYPE_A,
438
2
                    position: NodeGraphNodePosition { x: 0.0, y: 0.0 },
439
2
                    fields: NodeTypeFieldVec::from_vec(vec![NodeTypeField {
440
2
                        key: AzString::from("enabled"),
441
2
                        value: NodeTypeFieldValue::CheckBox(false),
442
2
                    }]),
443
2
                    connect_in: InputConnectionVec::from_const_slice(&[]),
444
2
                    connect_out: OutputConnectionVec::from_const_slice(&[]),
445
2
                },
446
2
            }]),
447
2
            add_node_str: AzString::from("Add node"),
448
2
            ..NodeGraph::default()
449
2
        }
450
2
    }
451

            
452
    /// Every widget in the crate, built with defaults that actually exercise
453
    /// its label paths (a widget with no labels proves nothing).
454
    ///
455
    /// NOT in this list, and why:
456
    /// * `camera` / `microphone` / `screencap` / `video` — each `dom()` emits a
457
    ///   single replaced `<img>` (or nothing) fed by a background worker and
458
    ///   needs a device/GL config to construct; they contain no text node at
459
    ///   all, so there is nothing for this convention to govern.
460
    /// * `menubar` — a free function over a window `Menu`, not a `dom()` widget;
461
    ///   its bar items are already `div > bare text`.
462
    /// * `map`'s tile labels — emitted from the `VirtualView` render callback,
463
    ///   not from `dom()`, so the walk cannot reach them; they were converted by
464
    ///   hand and are pinned by the map widget's own tests.
465
2
    pub(super) fn every_widget_dom() -> Vec<(&'static str, Dom)> {
466
        use super::{
467
            accordion::{Accordion, AccordionSection, AccordionSectionVec},
468
            alert::Alert,
469
            avatar::Avatar,
470
            backstage::{Backstage, BackstageNavItem, BackstageNavItemVec},
471
            badge::Badge,
472
            breadcrumb::Breadcrumb,
473
            button::Button,
474
            card::Card,
475
            check_box::CheckBox,
476
            chip::Chip,
477
            color_input::ColorInput,
478
            combobox::ComboBox,
479
            date_picker::DatePicker,
480
            divider::Divider,
481
            drop_down::DropDown,
482
            file_input::FileInput,
483
            frame::Frame,
484
            label::Label,
485
            list_view::ListView,
486
            map::{MapTileLayer, MapWidget},
487
            menubar::build_menubar_dom,
488
            modal::Modal,
489
            number_input::NumberInput,
490
            pagination::Pagination,
491
            popover::Popover,
492
            progressbar::ProgressBar,
493
            quick_access::QuickAccessBar,
494
            radio_group::RadioGroup,
495
            ribbon::{Ribbon, RibbonAppButton, RibbonButton, RibbonGroup, RibbonItem, RibbonTab, RibbonTabVec},
496
            segmented::Segmented,
497
            slider::Slider,
498
            spinner::Spinner,
499
            split_pane::{SplitDirection, SplitPane},
500
            statusbar::{StatusBar, StatusBarSegment, StatusBarSegmentVec},
501
            stepper::Stepper,
502
            switch::Switch,
503
            tabs::{TabContent, TabHeader},
504
            text_area::TextArea,
505
            text_input::TextInput,
506
            time_picker::TimePicker,
507
            titlebar::Titlebar,
508
            toast::Toast,
509
            tooltip::Tooltip,
510
            tree_view::{TreeView, TreeViewNode},
511
        };
512

            
513
2
        vec![
514
2
            (
515
2
                "accordion",
516
2
                Accordion::new(AccordionSectionVec::from_vec(vec![
517
2
                    AccordionSection::new("Open section", user_content()).with_open(true),
518
2
                    AccordionSection::new("Closed section", user_content()),
519
2
                ]))
520
2
                .dom(),
521
2
            ),
522
2
            (
523
2
                "alert",
524
2
                Alert::create(AzString::from("Something happened"))
525
2
                    .with_dismissible(true)
526
2
                    .dom(),
527
2
            ),
528
2
            ("avatar", Avatar::create(AzString::from("AB")).dom()),
529
2
            (
530
2
                "backstage",
531
2
                Backstage::new(BackstageNavItemVec::from_vec(vec![
532
2
                    BackstageNavItem::new(AzString::from("Info")),
533
2
                    BackstageNavItem::new(AzString::from("Save")),
534
2
                ]))
535
2
                .dom(),
536
2
            ),
537
2
            ("badge", Badge::create(AzString::from("99+")).dom()),
538
2
            (
539
2
                "breadcrumb",
540
2
                Breadcrumb::create(labels(&["Home", "Docs", "Page"])).dom(),
541
2
            ),
542
2
            ("button", Button::create(AzString::from("Click me")).dom()),
543
2
            ("card", Card::create(user_content()).dom()),
544
2
            ("check_box", CheckBox::create(true).dom()),
545
2
            (
546
2
                "chip",
547
2
                Chip::create(AzString::from("tag")).with_removable(true).dom(),
548
2
            ),
549
2
            (
550
2
                "color_input",
551
2
                ColorInput::create(ColorU { r: 1, g: 2, b: 3, a: 255 }).dom(),
552
2
            ),
553
2
            ("combobox", ComboBox::new(labels(&["one", "two"])).dom()),
554
2
            ("date_picker", DatePicker::create(2024, 2, 15).dom()),
555
2
            ("divider", Divider::create().dom()),
556
2
            ("drop_down", DropDown::new(labels(&["one", "two"])).dom()),
557
2
            ("file_input", FileInput::create(OptionString::None).dom()),
558
2
            (
559
2
                "frame",
560
2
                Frame::create(AzString::from("Frame title"), user_content()).dom(),
561
2
            ),
562
2
            ("label", Label::create(AzString::from("A label")).dom()),
563
2
            ("list_view", ListView::create(labels(&["Name", "Size"])).dom()),
564
2
            ("map", MapWidget::create(MapTileLayer::default()).dom()),
565
2
            (
566
2
                "menubar",
567
2
                build_menubar_dom(&azul_core::menu::Menu::create(
568
2
                    azul_core::menu::MenuItemVec::from_vec(vec![
569
2
                        azul_core::menu::MenuItem::String(
570
2
                            azul_core::menu::StringMenuItem::create("File".into()),
571
2
                        ),
572
2
                        azul_core::menu::MenuItem::String(
573
2
                            azul_core::menu::StringMenuItem::create("Edit".into()),
574
2
                        ),
575
2
                    ]),
576
2
                )),
577
2
            ),
578
2
            (
579
2
                "modal",
580
2
                Modal::create(user_content())
581
2
                    .with_title(AzString::from("Dialog"))
582
2
                    .with_open(true)
583
2
                    .dom(),
584
2
            ),
585
2
            ("node_graph", node_graph_fixture().dom()),
586
2
            ("number_input", NumberInput::create(4.0).dom()),
587
2
            ("pagination", Pagination::create(2, 5).dom()),
588
2
            (
589
2
                "popover",
590
2
                Popover::new(user_content(), user_content()).with_open(true).dom(),
591
2
            ),
592
2
            ("progressbar", ProgressBar::create(40.0).dom()),
593
2
            (
594
2
                "quick_access",
595
2
                QuickAccessBar::new(AzString::from("Document1")).dom(),
596
2
            ),
597
2
            (
598
2
                "radio_group",
599
2
                RadioGroup::create(labels(&["First", "Second"])).dom(),
600
2
            ),
601
2
            (
602
2
                "ribbon",
603
2
                Ribbon::new(RibbonTabVec::from_vec(vec![
604
2
                    RibbonTab::new(AzString::from("HOME")).with_group(
605
2
                        RibbonGroup::new(AzString::from("Clipboard")).with_item(
606
2
                            RibbonItem::LargeButton(RibbonButton::new(
607
2
                                AzString::from("content_paste"),
608
2
                                AzString::from("Paste"),
609
2
                            )),
610
2
                        ),
611
2
                    ),
612
2
                    RibbonTab::new(AzString::from("PAGE LAYOUT")),
613
2
                ]))
614
2
                .with_app_button(RibbonAppButton::new(AzString::from("FILE")))
615
2
                .dom(),
616
2
            ),
617
2
            (
618
2
                "segmented",
619
2
                Segmented::create(labels(&["Day", "Week", "Month"])).dom(),
620
2
            ),
621
2
            ("slider", Slider::create(0.5, 0.0, 1.0).dom()),
622
2
            ("spinner", Spinner::create().dom()),
623
2
            (
624
2
                "split_pane",
625
2
                SplitPane::create(SplitDirection::Horizontal, user_content(), user_content()).dom(),
626
2
            ),
627
2
            (
628
2
                "statusbar",
629
2
                StatusBar::new(StatusBarSegmentVec::from_vec(vec![
630
2
                    StatusBarSegment::new(AzString::from("Page 1 of 3")),
631
2
                ]))
632
2
                .dom(),
633
2
            ),
634
2
            (
635
2
                "stepper",
636
2
                Stepper::create(labels(&["Start", "Details", "Done"])).dom(),
637
2
            ),
638
2
            ("switch", Switch::create(true).dom()),
639
2
            ("tabs (header)", TabHeader::create(labels(&["One", "Two"])).dom()),
640
2
            ("tabs (content)", TabContent::new(user_content()).dom()),
641
2
            ("text_area", TextArea::create().dom()),
642
2
            ("text_input", TextInput::create().dom()),
643
2
            (
644
2
                "time_picker",
645
2
                TimePicker::create(9, 30).with_24h(false).dom(),
646
2
            ),
647
2
            ("titlebar", Titlebar::create(AzString::from("Window")).dom()),
648
2
            ("toast", Toast::create(AzString::from("Saved")).dom()),
649
2
            (
650
2
                "tooltip",
651
2
                Tooltip::new(user_content(), AzString::from("Explains it")).dom(),
652
2
            ),
653
2
            (
654
2
                "tree_view",
655
2
                TreeView::new(
656
2
                    TreeViewNode::new("root")
657
2
                        .with_expanded(true)
658
2
                        .with_child(TreeViewNode::new("child")),
659
2
                )
660
2
                .dom(),
661
2
            ),
662
        ]
663
2
    }
664

            
665
    /// THE convention. A widget that trips this has attached box-model CSS, a
666
    /// callback, a `tab_index`, a dataset or children to a node that owns no
667
    /// rect — all of which the layout engine silently discards.
668
    #[test]
669
1
    fn no_widget_attaches_state_to_a_rect_less_text_node() {
670
1
        let mut bad = Vec::new();
671
46
        for (name, dom) in every_widget_dom() {
672
46
            walk(&dom, name, &mut bad);
673
46
        }
674
1
        assert!(
675
1
            bad.is_empty(),
676
            "widget label convention violated ({} site(s)):\n{}",
677
            bad.len(),
678
            bad.join("\n"),
679
        );
680
1
    }
681

            
682
    /// A guard on the guard: the walk must be able to SEE a violation, or the
683
    /// test above would pass vacuously the day someone breaks `inert_state_on`.
684
    #[test]
685
1
    fn the_walk_reports_a_deliberately_broken_text_node() {
686
        use azul_core::dom::TabIndex;
687

            
688
1
        let mut leaf = Dom::create_text_do_not_use_without_block_level_wrapper(AzString::from("bare"));
689
1
        leaf.root.set_css("width: 10px;");
690
1
        let broken = Dom::create_div().with_child(leaf.with_tab_index(TabIndex::Auto));
691

            
692
1
        let mut bad = Vec::new();
693
1
        walk(&broken, "fixture", &mut bad);
694

            
695
1
        assert_eq!(bad.len(), 1, "the walk missed a hand-broken text node");
696
1
        assert!(bad[0].contains("css props"), "{}", bad[0]);
697
1
        assert!(bad[0].contains("tab_index"), "{}", bad[0]);
698
1
    }
699
}