Skip to main content

fret_ui_kit/
lib.rs

1#![deny(deprecated)]
2//! General-purpose UI components built on top of `fret-ui`.
3//!
4//! This crate is intentionally domain-agnostic (no engine/editor-specific concepts).
5//! Styling is token-driven and supports namespaced extension tokens (see ADR 0050).
6//!
7//! Note: This crate is declarative-only. Retained-widget authoring is intentionally not part of
8//! the public component surface.
9
10/// Build a heterogeneous `Vec<AnyElement>` without repetitive child landing boilerplate.
11///
12/// Intended for ergonomic authoring inside layout builders, including host-bound late builders,
13/// e.g.: `ui::h_flex(|cx| ui::children![cx; Button::new("OK").ui(), cx.text("...")])`.
14#[macro_export]
15macro_rules! children {
16    ($cx:ident;) => {
17        ::std::vec::Vec::new()
18    };
19    ($cx:ident; $($child:expr),+ $(,)?) => {{
20        let mut children = ::std::vec::Vec::new();
21        $(
22            {
23                let child = $child;
24                let element = $crate::land_child(&mut *$cx, child);
25                children.push(element);
26            }
27        )+
28        children
29    }};
30}
31
32/// Land typed child values at the last possible moment inside wrapper-style helpers.
33pub(crate) fn collect_children<'a, H, Cx, I>(
34    cx: &mut Cx,
35    children: I,
36) -> Vec<fret_ui::element::AnyElement>
37where
38    H: fret_ui::UiHost + 'a,
39    Cx: fret_ui::ElementContextAccess<'a, H>,
40    I: IntoIterator,
41    I::Item: crate::ui_builder::IntoUiElement<H>,
42{
43    let mut out = Vec::new();
44    for child in children {
45        out.push(crate::land_child(cx, child));
46    }
47    out
48}
49
50/// Land one typed child through any explicit element-context access surface.
51#[doc(hidden)]
52#[track_caller]
53pub fn land_child<'a, H, Cx, T>(cx: &mut Cx, child: T) -> fret_ui::element::AnyElement
54where
55    H: fret_ui::UiHost + 'a,
56    Cx: fret_ui::ElementContextAccess<'a, H>,
57    T: crate::ui_builder::IntoUiElement<H>,
58{
59    crate::ui_builder::IntoUiElement::into_element(child, cx.elements())
60}
61
62/// Implement the `UiBuilder` patch + render glue for a component that supports both chrome and
63/// layout refinements.
64///
65/// The type must provide:
66/// - `fn refine_style(self, ChromeRefinement) -> Self`
67/// - `fn refine_layout(self, LayoutRefinement) -> Self`
68/// - `fn into_element(self, &mut ElementContext<'_, H>) -> AnyElement`
69#[macro_export]
70macro_rules! ui_component_chrome_layout {
71    ($ty:ty) => {
72        impl $crate::UiPatchTarget for $ty {
73            fn apply_ui_patch(self, patch: $crate::UiPatch) -> Self {
74                self.refine_style(patch.chrome).refine_layout(patch.layout)
75            }
76        }
77
78        impl $crate::UiSupportsChrome for $ty {}
79        impl $crate::UiSupportsLayout for $ty {}
80
81        impl<H: ::fret_ui::UiHost> $crate::IntoUiElement<H> for $ty {
82            #[track_caller]
83            fn into_element(
84                self,
85                cx: &mut ::fret_ui::ElementContext<'_, H>,
86            ) -> ::fret_ui::element::AnyElement {
87                <$ty>::into_element(self, cx)
88            }
89        }
90    };
91}
92
93/// Implement the `UiBuilder` patch + render glue for a component that supports layout refinements
94/// only.
95///
96/// The type must provide:
97/// - `fn refine_layout(self, LayoutRefinement) -> Self`
98/// - `fn into_element(self, &mut ElementContext<'_, H>) -> AnyElement`
99#[macro_export]
100macro_rules! ui_component_layout_only {
101    ($ty:ty) => {
102        impl $crate::UiPatchTarget for $ty {
103            fn apply_ui_patch(self, patch: $crate::UiPatch) -> Self {
104                self.refine_layout(patch.layout)
105            }
106        }
107
108        impl $crate::UiSupportsLayout for $ty {}
109
110        impl<H: ::fret_ui::UiHost> $crate::IntoUiElement<H> for $ty {
111            #[track_caller]
112            fn into_element(
113                self,
114                cx: &mut ::fret_ui::ElementContext<'_, H>,
115            ) -> ::fret_ui::element::AnyElement {
116                <$ty>::into_element(self, cx)
117            }
118        }
119    };
120}
121
122/// Implement `UiPatchTarget` + `UiSupports*` for a component that supports both chrome and layout
123/// refinements, without implementing rendering glue.
124#[macro_export]
125macro_rules! ui_component_chrome_layout_patch_only {
126    ($ty:ty) => {
127        impl $crate::UiPatchTarget for $ty {
128            fn apply_ui_patch(self, patch: $crate::UiPatch) -> Self {
129                self.refine_style(patch.chrome).refine_layout(patch.layout)
130            }
131        }
132
133        impl $crate::UiSupportsChrome for $ty {}
134        impl $crate::UiSupportsLayout for $ty {}
135    };
136}
137
138/// Implement `UiPatchTarget` + `UiSupportsLayout` for a component that supports layout refinements
139/// only, without implementing rendering glue.
140#[macro_export]
141macro_rules! ui_component_layout_only_patch_only {
142    ($ty:ty) => {
143        impl $crate::UiPatchTarget for $ty {
144            fn apply_ui_patch(self, patch: $crate::UiPatch) -> Self {
145                self.refine_layout(patch.layout)
146            }
147        }
148
149        impl $crate::UiSupportsLayout for $ty {}
150    };
151}
152
153/// Implement patch + render glue for a component that does not accept any `UiPatch`, but still
154/// wants to opt into the `.ui()` surface (e.g. purely cosmetic elements).
155#[macro_export]
156macro_rules! ui_component_passthrough {
157    ($ty:ty) => {
158        impl $crate::UiPatchTarget for $ty {
159            fn apply_ui_patch(self, _patch: $crate::UiPatch) -> Self {
160                self
161            }
162        }
163
164        impl<H: ::fret_ui::UiHost> $crate::IntoUiElement<H> for $ty {
165            #[track_caller]
166            fn into_element(
167                self,
168                cx: &mut ::fret_ui::ElementContext<'_, H>,
169            ) -> ::fret_ui::element::AnyElement {
170                <$ty>::into_element(self, cx)
171            }
172        }
173    };
174}
175
176/// Implement `UiPatchTarget` for a component that does not accept any `UiPatch`, without
177/// implementing rendering glue.
178#[macro_export]
179macro_rules! ui_component_passthrough_patch_only {
180    ($ty:ty) => {
181        impl $crate::UiPatchTarget for $ty {
182            fn apply_ui_patch(self, _patch: $crate::UiPatch) -> Self {
183                self
184            }
185        }
186    };
187}
188
189/// Implement internal landing glue for a stateless component authored as `RenderOnce` (ADR 0039).
190///
191/// Note: we intentionally avoid a blanket impl due to coherence restrictions on upstream types.
192#[macro_export]
193macro_rules! ui_component_render_once {
194    ($ty:ty) => {
195        impl<H: ::fret_ui::UiHost> $crate::IntoUiElement<H> for $ty {
196            #[track_caller]
197            fn into_element(
198                self,
199                cx: &mut ::fret_ui::ElementContext<'_, H>,
200            ) -> ::fret_ui::element::AnyElement {
201                ::fret_ui::element::RenderOnce::render_once(self, cx)
202            }
203        }
204    };
205}
206
207pub mod activate;
208pub mod colors;
209pub mod command;
210mod corners4;
211pub mod custom_effects;
212pub mod declarative;
213#[cfg(feature = "dnd")]
214pub mod dnd;
215mod edges4;
216pub mod headless;
217pub mod image_metadata;
218pub mod image_sampling;
219#[cfg(feature = "imui")]
220pub mod imui;
221pub mod node_graph;
222pub mod overlay;
223pub mod overlay_controller;
224pub mod primitives;
225pub mod recipes;
226pub mod theme_tokens;
227pub mod tooltip_provider;
228pub mod tree;
229pub mod typography;
230pub mod ui;
231pub mod ui_builder;
232pub mod viewport_tooling;
233#[cfg(feature = "unstable-internals")]
234pub mod window_overlays;
235#[cfg(not(feature = "unstable-internals"))]
236mod window_overlays;
237
238mod ui_builder_impls;
239
240mod sizing;
241mod style;
242mod styled;
243
244pub use activate::{
245    on_activate, on_activate_notify, on_activate_request_redraw, on_activate_request_redraw_notify,
246};
247pub use corners4::Corners4;
248pub use edges4::{Edges4, MarginEdge};
249pub use image_metadata::{ImageMetadata, ImageMetadataStore, with_image_metadata_store_mut};
250pub use image_sampling::ImageSamplingExt;
251pub use sizing::{Sizable, Size};
252pub use style::{
253    ChromeRefinement, ColorFallback, ColorRef, Items, Justify, LayoutRefinement, LengthRefinement,
254    MetricRef, OverflowRefinement, OverrideSlot, PaddingRefinement, Radius, ShadowPreset,
255    SignedMetricRef, Space, WidgetState, WidgetStateProperty, WidgetStates, merge_override_slot,
256    merge_slot, resolve_override_slot, resolve_override_slot_opt, resolve_override_slot_opt_with,
257    resolve_override_slot_with, resolve_slot,
258};
259pub use styled::{RefineStyle, Stylable, Styled, StyledExt};
260pub use ui_builder::{
261    IntoUiElement, IntoUiElementInExt, UiBuilder, UiExt, UiPatch, UiPatchTarget, UiSupportsChrome,
262    UiSupportsLayout,
263};
264
265pub use overlay_controller::{
266    OverlayArbitrationSnapshot, OverlayController, OverlayKind, OverlayPresence, OverlayRequest,
267    OverlayStackEntryKind, ToastLayerSpec, WindowOverlayStackEntry, WindowOverlayStackSnapshot,
268};
269pub use window_overlays::{
270    DEFAULT_MAX_TOASTS, DEFAULT_TOAST_DURATION, DEFAULT_VISIBLE_TOASTS, ToastAction, ToastAsyncMsg,
271    ToastAsyncQueueHandle, ToastButtonStyle, ToastDescription, ToastDuration, ToastIconButtonStyle,
272    ToastIconOverride, ToastIconOverrides, ToastId, ToastLayerStyle, ToastOffset, ToastPosition,
273    ToastRequest, ToastStore, ToastSwipeConfig, ToastSwipeDirection, ToastSwipeDirections,
274    ToastTextStyle, ToastVariant, ToastVariantColors, ToastVariantPalette, toast_async_queue,
275};
276
277pub use window_overlays::TOAST_VIEWPORT_FOCUS_COMMAND;
278pub use window_overlays::TOAST_VIEWPORT_RESTORE_COMMAND;
279
280// Diagnostics-only exports: used by `fret-bootstrap` to export bundle.json fields.
281#[doc(hidden)]
282pub use window_overlays::{
283    OverlaySynthesisEvent, OverlaySynthesisKind, OverlaySynthesisOutcome, OverlaySynthesisSource,
284    WindowOverlaySynthesisDiagnosticsStore,
285};
286
287/// Common imports for component/app code using `fret-ui-kit`.
288///
289/// Recommended: `use fret_ui_kit::prelude::*;`
290pub mod prelude {
291    pub use crate::IntoUiElement as _;
292    pub use crate::command::ElementCommandGatingExt as _;
293    pub use crate::declarative::prelude::*;
294    pub use crate::declarative::style;
295    pub use crate::declarative::{CachedSubtreeExt, CachedSubtreeProps};
296    pub use crate::ui;
297    pub use crate::ui::UiElementSinkExt as _;
298    pub use crate::{
299        on_activate, on_activate_notify, on_activate_request_redraw,
300        on_activate_request_redraw_notify,
301    };
302
303    #[cfg(feature = "imui")]
304    pub use crate::imui::UiWriterUiKitExt as _;
305
306    #[cfg(feature = "imui")]
307    pub use crate::imui::UiWriterImUiFacadeExt as _;
308
309    #[cfg(feature = "icons")]
310    pub use crate::declarative::icon;
311    #[cfg(feature = "icons")]
312    pub use fret_icons::IconId;
313
314    pub use crate::{
315        ChromeRefinement, ColorFallback, ColorRef, Corners4, Edges4, ImageMetadata,
316        ImageMetadataStore, ImageSamplingExt, IntoUiElement, LayoutRefinement, MarginEdge,
317        MetricRef, OverrideSlot, Radius, ShadowPreset, SignedMetricRef, Size, Space, StyledExt,
318        UiExt, WidgetState, WidgetStateProperty, WidgetStates, merge_override_slot, merge_slot,
319        resolve_override_slot, resolve_override_slot_opt, resolve_override_slot_opt_with,
320        resolve_override_slot_with, resolve_slot,
321    };
322    pub use crate::{OverlayArbitrationSnapshot, OverlayController, OverlayKind, OverlayPresence};
323    pub use crate::{OverlayRequest, OverlayStackEntryKind};
324    pub use crate::{WindowOverlayStackEntry, WindowOverlayStackSnapshot};
325
326    pub use fret_core::scene::ImageSamplingHint;
327    pub use fret_core::{AppWindowId, Px, TextOverflow, TextWrap, UiServices};
328    pub use fret_runtime::{ActionId, CommandId, Model, TypedAction};
329    pub use fret_ui::element::{AnyElement, AnyElementIterExt as _, TextProps};
330    pub use fret_ui::{ElementContext, Invalidation, Theme, UiHost, UiTree};
331}
332
333/// Attempts to handle a window-scoped command that targets `fret-ui-kit` overlay substrates.
334///
335/// This is intended to be called by app drivers after `UiTree::dispatch_command` returns `false`.
336pub fn try_handle_window_overlays_command<H: fret_ui::UiHost>(
337    ui: &mut fret_ui::UiTree<H>,
338    app: &mut H,
339    window: fret_core::AppWindowId,
340    command: &fret_runtime::CommandId,
341) -> bool {
342    window_overlays::try_handle_window_command(ui, app, window, command)
343}
344
345pub use tree::{
346    TreeEntry, TreeItem, TreeItemId, TreeRowRenderer, TreeRowState, TreeState, flatten_tree,
347};
348
349#[cfg(test)]
350mod ui_component_macro_tests {
351    use super::*;
352    use fret_ui::element::TextProps;
353
354    #[derive(Debug, Clone, Copy)]
355    struct DummyComponent;
356
357    impl DummyComponent {
358        fn refine_style(self, _chrome: ChromeRefinement) -> Self {
359            self
360        }
361
362        fn refine_layout(self, _layout: LayoutRefinement) -> Self {
363            self
364        }
365
366        fn into_element<H: fret_ui::UiHost>(
367            self,
368            cx: &mut fret_ui::ElementContext<'_, H>,
369        ) -> fret_ui::element::AnyElement {
370            cx.text_props(TextProps::new("dummy"))
371        }
372    }
373
374    ui_component_chrome_layout!(DummyComponent);
375
376    #[test]
377    fn ui_component_chrome_layout_macro_compiles() {
378        fn assert_traits<
379            T: UiPatchTarget + UiSupportsChrome + UiSupportsLayout + IntoUiElement<fret_app::App>,
380        >() {
381        }
382        assert_traits::<DummyComponent>();
383    }
384
385    #[derive(Debug, Clone, Copy)]
386    struct DummyRenderOnceComponent;
387
388    impl fret_ui::element::RenderOnce for DummyRenderOnceComponent {
389        fn render_once<H: fret_ui::UiHost>(
390            self,
391            cx: &mut fret_ui::ElementContext<'_, H>,
392        ) -> fret_ui::element::AnyElement {
393            cx.text_props(TextProps::new("dummy-render-once"))
394        }
395    }
396
397    ui_component_render_once!(DummyRenderOnceComponent);
398
399    #[test]
400    fn ui_component_render_once_macro_compiles() {
401        fn assert_into_element<T: IntoUiElement<fret_app::App>>() {}
402        assert_into_element::<DummyRenderOnceComponent>();
403    }
404}
405
406#[cfg(test)]
407mod default_semantics_tests {
408    #[test]
409    fn text_box_presets_have_expected_wrap_defaults() {
410        let sm = crate::ui::TextBox::new("sm", crate::ui::TextPreset::Sm);
411        assert_eq!(sm.wrap, fret_core::TextWrap::Word);
412        assert_eq!(sm.overflow, fret_core::TextOverflow::Clip);
413
414        let label = crate::ui::TextBox::new("label", crate::ui::TextPreset::Label);
415        assert_eq!(label.wrap, fret_core::TextWrap::None);
416        assert_eq!(label.overflow, fret_core::TextOverflow::Clip);
417    }
418}
419
420#[cfg(test)]
421mod source_policy_tests {
422    const LIB_RS: &str = include_str!("lib.rs");
423    const README: &str = include_str!("../README.md");
424    const DECLARATIVE_BLOOM_RS: &str = include_str!("declarative/bloom.rs");
425    const DECLARATIVE_CACHED_SUBTREE_RS: &str = include_str!("declarative/cached_subtree.rs");
426    const DECLARATIVE_CHROME_RS: &str = include_str!("declarative/chrome.rs");
427    const DECLARATIVE_CONTAINER_QUERIES_RS: &str = include_str!("declarative/container_queries.rs");
428    const DECLARATIVE_DISMISSIBLE_RS: &str = include_str!("declarative/dismissible.rs");
429    const DECLARATIVE_GLASS_RS: &str = include_str!("declarative/glass.rs");
430    const DECLARATIVE_LIST_RS: &str = include_str!("declarative/list.rs");
431    const DECLARATIVE_MOD_RS: &str = include_str!("declarative/mod.rs");
432    const DECLARATIVE_MODEL_WATCH_RS: &str = include_str!("declarative/model_watch.rs");
433    const DECLARATIVE_PRELUDE_RS: &str = include_str!("declarative/prelude.rs");
434    const DECLARATIVE_SCROLL_RS: &str = include_str!("declarative/scroll.rs");
435    const DECLARATIVE_SEMANTICS_RS: &str = include_str!("declarative/semantics.rs");
436    const DECLARATIVE_TABLE_RS: &str = include_str!("declarative/table.rs");
437    const DECLARATIVE_VISUALLY_HIDDEN_RS: &str = include_str!("declarative/visually_hidden.rs");
438    const DECLARATIVE_PIXELATE_RS: &str = include_str!("declarative/pixelate.rs");
439    const IMUI_RS: &str = include_str!("imui.rs");
440    const PRIMITIVES_DISMISSABLE_LAYER_RS: &str = include_str!("primitives/dismissable_layer.rs");
441    const PRIMITIVES_ALERT_DIALOG_RS: &str = include_str!("primitives/alert_dialog.rs");
442    const PRIMITIVES_DIALOG_RS: &str = include_str!("primitives/dialog.rs");
443    const PRIMITIVES_FOCUS_SCOPE_RS: &str = include_str!("primitives/focus_scope.rs");
444    const PRIMITIVES_ACCORDION_RS: &str = include_str!("primitives/accordion.rs");
445    const PRIMITIVES_MENU_CONTENT_PANEL_RS: &str = include_str!("primitives/menu/content_panel.rs");
446    const PRIMITIVES_MENU_CONTENT_RS: &str = include_str!("primitives/menu/content.rs");
447    const PRIMITIVES_MENU_SUB_CONTENT_RS: &str = include_str!("primitives/menu/sub_content.rs");
448    const PRIMITIVES_POPPER_CONTENT_RS: &str = include_str!("primitives/popper_content.rs");
449    const PRIMITIVES_POPOVER_RS: &str = include_str!("primitives/popover.rs");
450    const PRIMITIVES_ROVING_FOCUS_GROUP_RS: &str = include_str!("primitives/roving_focus_group.rs");
451    const PRIMITIVES_SELECT_RS: &str = include_str!("primitives/select.rs");
452    const PRIMITIVES_TABS_RS: &str = include_str!("primitives/tabs.rs");
453    const PRIMITIVES_TOGGLE_RS: &str = include_str!("primitives/toggle.rs");
454    const PRIMITIVES_TOOLBAR_RS: &str = include_str!("primitives/toolbar.rs");
455    const PRIMITIVES_TOOLTIP_RS: &str = include_str!("primitives/tooltip.rs");
456    const RECIPES_SORTABLE_DND_RS: &str = include_str!("recipes/sortable_dnd.rs");
457    const UI_RS: &str = include_str!("ui.rs");
458    const UI_BUILDER_RS: &str = include_str!("ui_builder.rs");
459
460    fn visit_rust_files(dir: &std::path::Path, f: &mut impl FnMut(&std::path::Path, &str)) {
461        for entry in std::fs::read_dir(dir).unwrap_or_else(|err| {
462            panic!(
463                "failed to read source-policy directory {}: {err}",
464                dir.display()
465            )
466        }) {
467            let entry = entry.unwrap_or_else(|err| {
468                panic!(
469                    "failed to read source-policy entry in {}: {err}",
470                    dir.display()
471                )
472            });
473            let path = entry.path();
474            if path.is_dir() {
475                visit_rust_files(&path, f);
476                continue;
477            }
478            if path.extension().and_then(std::ffi::OsStr::to_str) != Some("rs") {
479                continue;
480            }
481            let source = std::fs::read_to_string(&path).unwrap_or_else(|err| {
482                panic!(
483                    "failed to read source-policy file {}: {err}",
484                    path.display()
485                )
486            });
487            f(&path, &source);
488        }
489    }
490
491    #[test]
492    fn root_surface_omits_host_bound_conversion_alias() {
493        let tests_start = LIB_RS.find("#[cfg(test)]").unwrap_or(LIB_RS.len());
494        let public_surface = &LIB_RS[..tests_start];
495        assert!(!public_surface.contains("UiHostBoundIntoElement"));
496    }
497
498    #[test]
499    fn root_surface_omits_legacy_conversion_exports() {
500        let tests_start = LIB_RS.find("#[cfg(test)]").unwrap_or(LIB_RS.len());
501        let public_surface = &LIB_RS[..tests_start];
502        assert!(!public_surface.contains("pub use ui::UiChildIntoElement;"));
503        assert!(!public_surface.contains("pub use ui_builder::UiIntoElement;"));
504        assert!(!public_surface.contains("pub(crate) use ui_builder::UiIntoElement;"));
505
506        let export_start = public_surface
507            .find("pub use ui_builder::{")
508            .expect("ui_builder export block should exist");
509        let export_tail = &public_surface[export_start..];
510        let export_end = export_tail
511            .find("};")
512            .expect("ui_builder export block should terminate");
513        let export_block = &export_tail[..export_end];
514        assert!(!export_block.contains("UiIntoElement"));
515        assert!(export_block.contains("IntoUiElement"));
516    }
517
518    #[test]
519    fn legacy_ui_into_element_bridge_name_is_deleted_from_ui_builder() {
520        assert!(!UI_BUILDER_RS.contains("trait UiIntoElement"));
521        assert!(!UI_BUILDER_RS.contains("T: UiIntoElement"));
522        assert!(!UI_BUILDER_RS.contains("UiIntoElement::into_element"));
523        assert!(UI_BUILDER_RS.contains("impl<H: UiHost> IntoUiElement<H> for AnyElement"));
524    }
525
526    #[test]
527    fn exported_component_macros_attach_public_conversion_trait_directly() {
528        let tests_start = LIB_RS.find("#[cfg(test)]").unwrap_or(LIB_RS.len());
529        let public_surface = &LIB_RS[..tests_start];
530        assert!(!public_surface.contains("impl $crate::ui_builder::UiIntoElement for $ty"));
531        assert!(
532            public_surface.contains("impl<H: ::fret_ui::UiHost> $crate::IntoUiElement<H> for $ty")
533        );
534        assert!(public_surface.contains("macro_rules! ui_component_render_once"));
535        assert!(!public_surface.contains("macro_rules! ui_into_element_render_once"));
536    }
537
538    #[test]
539    fn child_pipeline_stays_on_unified_component_conversion_trait() {
540        assert!(!UI_RS.contains("trait UiChildIntoElement"));
541        assert!(!IMUI_RS.contains("UiChildIntoElement"));
542        assert!(UI_RS.contains("I::Item: IntoUiElement<H>"));
543        assert!(UI_RS.contains("crate::land_child(cx, child)"));
544        assert!(
545            LIB_RS.contains("crate::ui_builder::IntoUiElement::into_element(child, cx.elements())")
546        );
547        assert!(IMUI_RS.contains("B: IntoUiElement<H>"));
548    }
549
550    #[test]
551    fn ui_builtin_text_primitives_land_through_public_conversion_trait() {
552        let tests_start = UI_RS.find("#[cfg(test)]").unwrap_or(UI_RS.len());
553        let public_surface = &UI_RS[..tests_start];
554
555        assert!(
556            !public_surface.contains("UiIntoElement"),
557            "ui.rs production surface should not depend on UiIntoElement"
558        );
559        assert!(public_surface.contains("IntoUiElement<H> for TextBox"));
560        assert!(public_surface.contains("IntoUiElement<H> for RawTextBox"));
561    }
562
563    #[test]
564    fn declarative_semantics_ext_names_drop_legacy_conversion_prefix() {
565        for (label, source) in [
566            ("declarative/mod.rs", DECLARATIVE_MOD_RS),
567            ("declarative/prelude.rs", DECLARATIVE_PRELUDE_RS),
568        ] {
569            assert!(
570                source.contains("UiElementA11yExt"),
571                "{label} should export UiElementA11yExt"
572            );
573            assert!(
574                source.contains("UiElementKeyContextExt"),
575                "{label} should export UiElementKeyContextExt"
576            );
577            assert!(
578                source.contains("UiElementTestIdExt"),
579                "{label} should export UiElementTestIdExt"
580            );
581            assert!(
582                !source.contains("UiIntoElementA11yExt"),
583                "{label} reintroduced UiIntoElementA11yExt"
584            );
585            assert!(
586                !source.contains("UiIntoElementKeyContextExt"),
587                "{label} reintroduced UiIntoElementKeyContextExt"
588            );
589            assert!(
590                !source.contains("UiIntoElementTestIdExt"),
591                "{label} reintroduced UiIntoElementTestIdExt"
592            );
593        }
594    }
595
596    #[test]
597    fn declarative_semantics_wrappers_land_through_public_conversion_trait() {
598        let tests_start = DECLARATIVE_SEMANTICS_RS
599            .find("#[cfg(test)]")
600            .unwrap_or(DECLARATIVE_SEMANTICS_RS.len());
601        let public_surface = &DECLARATIVE_SEMANTICS_RS[..tests_start];
602
603        assert!(
604            !public_surface.contains("UiIntoElement"),
605            "declarative/semantics.rs production surface should not depend on UiIntoElement"
606        );
607        assert!(public_surface.contains("IntoUiElement<H> for UiElementWithTestId<T>"));
608        assert!(public_surface.contains("IntoUiElement<H> for UiElementWithA11y<T>"));
609        assert!(public_surface.contains("IntoUiElement<H> for UiElementWithKeyContext<T>"));
610        assert!(public_surface.contains("pub trait UiElementTestIdExt: Sized"));
611        assert!(public_surface.contains("pub trait UiElementA11yExt: Sized"));
612        assert!(public_surface.contains("pub trait UiElementKeyContextExt: Sized"));
613    }
614
615    #[test]
616    fn wrapper_helpers_prefer_typed_child_inputs() {
617        for (label, source, landing_snippet) in [
618            (
619                "declarative/bloom.rs",
620                DECLARATIVE_BLOOM_RS,
621                "collect_children(cx,",
622            ),
623            (
624                "declarative/cached_subtree.rs",
625                DECLARATIVE_CACHED_SUBTREE_RS,
626                "collect_children(cx,",
627            ),
628            (
629                "declarative/chrome.rs",
630                DECLARATIVE_CHROME_RS,
631                "collect_children(cx,",
632            ),
633            (
634                "declarative/container_queries.rs",
635                DECLARATIVE_CONTAINER_QUERIES_RS,
636                "collect_children(cx,",
637            ),
638            (
639                "declarative/dismissible.rs",
640                DECLARATIVE_DISMISSIBLE_RS,
641                "collect_children(cx,",
642            ),
643            (
644                "declarative/glass.rs",
645                DECLARATIVE_GLASS_RS,
646                "collect_children(cx,",
647            ),
648            (
649                "declarative/list.rs",
650                DECLARATIVE_LIST_RS,
651                "collect_children(cx,",
652            ),
653            (
654                "declarative/pixelate.rs",
655                DECLARATIVE_PIXELATE_RS,
656                "collect_children(cx,",
657            ),
658            (
659                "declarative/scroll.rs",
660                DECLARATIVE_SCROLL_RS,
661                "collect_children(cx,",
662            ),
663            (
664                "declarative/table.rs",
665                DECLARATIVE_TABLE_RS,
666                "collect_children(",
667            ),
668            (
669                "declarative/visually_hidden.rs",
670                DECLARATIVE_VISUALLY_HIDDEN_RS,
671                "collect_children(cx,",
672            ),
673            (
674                "primitives/accordion.rs",
675                PRIMITIVES_ACCORDION_RS,
676                "collect_children(cx, items)",
677            ),
678            (
679                "primitives/dismissable_layer.rs",
680                PRIMITIVES_DISMISSABLE_LAYER_RS,
681                "render_dismissible_root_with_hooks(",
682            ),
683            (
684                "primitives/focus_scope.rs",
685                PRIMITIVES_FOCUS_SCOPE_RS,
686                "collect_children(cx,",
687            ),
688            (
689                "primitives/menu/content.rs",
690                PRIMITIVES_MENU_CONTENT_RS,
691                "roving_focus_group::roving_focus_group_apg_entry_fallback(",
692            ),
693            (
694                "primitives/menu/content_panel.rs",
695                PRIMITIVES_MENU_CONTENT_PANEL_RS,
696                "collect_children(cx,",
697            ),
698            (
699                "primitives/menu/sub_content.rs",
700                PRIMITIVES_MENU_SUB_CONTENT_RS,
701                "collect_children(cx,",
702            ),
703            (
704                "primitives/popper_content.rs",
705                PRIMITIVES_POPPER_CONTENT_RS,
706                "collect_children(cx,",
707            ),
708            (
709                "primitives/roving_focus_group.rs",
710                PRIMITIVES_ROVING_FOCUS_GROUP_RS,
711                "collect_children(cx,",
712            ),
713            (
714                "primitives/tabs.rs",
715                PRIMITIVES_TABS_RS,
716                "collect_children(cx, items)",
717            ),
718            (
719                "primitives/toggle.rs",
720                PRIMITIVES_TOGGLE_RS,
721                "collect_children(cx, items)",
722            ),
723            (
724                "primitives/toolbar.rs",
725                PRIMITIVES_TOOLBAR_RS,
726                "roving_focus_group::roving_focus_group_apg(",
727            ),
728            (
729                "recipes/sortable_dnd.rs",
730                RECIPES_SORTABLE_DND_RS,
731                "collect_children(cx, items)",
732            ),
733        ] {
734            assert!(
735                source.contains("IntoUiElement<"),
736                "{label} should accept typed child values on the public wrapper surface"
737            );
738            assert!(
739                !source.contains("IntoIterator<Item = AnyElement>"),
740                "{label} reintroduced raw AnyElement child items on the public surface"
741            );
742            assert!(
743                source.contains(landing_snippet),
744                "{label} should only land typed child values behind a typed wrapper seam"
745            );
746        }
747    }
748
749    #[test]
750    fn overlay_wrapper_helpers_land_typed_children_before_request_seams() {
751        for (label, source, typed_signature, landing_snippet, raw_request_snippet) in [
752            (
753                "primitives/alert_dialog.rs",
754                PRIMITIVES_ALERT_DIALOG_RS,
755                "pub fn alert_dialog_modal_barrier<H: UiHost, I, T>(",
756                "collect_children(cx, children)",
757                "children: impl IntoIterator<Item = AnyElement>",
758            ),
759            (
760                "primitives/dialog.rs",
761                PRIMITIVES_DIALOG_RS,
762                "pub fn modal_barrier<H: UiHost, I, T>(",
763                "let children = collect_children(cx, children);",
764                "children: impl IntoIterator<Item = AnyElement>",
765            ),
766            (
767                "primitives/popover.rs",
768                PRIMITIVES_POPOVER_RS,
769                "pub fn popover_dialog_wrapper<H: UiHost, I, T>(",
770                "collect_children(cx, items)",
771                "children: impl IntoIterator<Item = AnyElement>",
772            ),
773            (
774                "primitives/select.rs",
775                PRIMITIVES_SELECT_RS,
776                "pub fn select_modal_barrier<H: UiHost, I, T>(",
777                "collect_children(cx, barrier_children)",
778                "children: impl IntoIterator<Item = AnyElement>",
779            ),
780            (
781                "primitives/tooltip.rs",
782                PRIMITIVES_TOOLTIP_RS,
783                "pub fn request<H: UiHost, I, T>(",
784                "collect_children(cx, children)",
785                "children: impl IntoIterator<Item = AnyElement>",
786            ),
787        ] {
788            assert!(
789                source.contains(typed_signature),
790                "{label} should expose typed child wrappers where an ElementContext is available"
791            );
792            assert!(
793                source.contains("IntoUiElement<H>"),
794                "{label} should accept typed child values on wrapper helpers"
795            );
796            assert!(
797                source.contains(landing_snippet),
798                "{label} should land typed child values behind collect_children(...)"
799            );
800            assert!(
801                source.contains(raw_request_snippet),
802                "{label} should still document the raw AnyElement overlay-request landing seam"
803            );
804        }
805    }
806
807    #[test]
808    fn primitives_and_base_recipes_stay_state_stack_agnostic() {
809        let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
810        let markers = [
811            "use fret_query",
812            "fret_query::",
813            "use fret_selector",
814            "fret_selector::",
815        ];
816
817        for root in [
818            manifest_dir.join("src/primitives"),
819            manifest_dir.join("src/recipes"),
820            manifest_dir.join("../fret-ui-headless/src"),
821        ] {
822            visit_rust_files(&root, &mut |path, source| {
823                let label = path.strip_prefix(manifest_dir).unwrap_or(path);
824                for marker in markers {
825                    assert!(
826                        !source.contains(marker),
827                        "{} reintroduced state-stack marker `{marker}` into a base primitive/recipe seam",
828                        label.display()
829                    );
830                }
831            });
832        }
833    }
834
835    #[test]
836    fn query_watch_helpers_stay_opt_in_and_out_of_default_declarative_prelude() {
837        assert!(
838            DECLARATIVE_MODEL_WATCH_RS.contains("#[cfg(feature = \"state-query\")]"),
839            "declarative/model_watch.rs should keep query-watch helpers behind the `state-query` feature"
840        );
841        assert!(
842            DECLARATIVE_MODEL_WATCH_RS.contains("pub trait QueryHandleWatchExt<T: 'static>"),
843            "declarative/model_watch.rs should keep the opt-in query-watch helper explicit"
844        );
845        assert!(
846            DECLARATIVE_MOD_RS.contains("pub use model_watch::QueryHandleWatchExt;"),
847            "declarative/mod.rs should keep query-watch helpers on the explicit declarative root"
848        );
849        assert!(
850            !DECLARATIVE_PRELUDE_RS.contains("QueryHandleWatchExt"),
851            "declarative/prelude.rs should not make query-watch helpers part of the default prelude"
852        );
853        assert!(
854            !DECLARATIVE_PRELUDE_RS.contains("fret_query"),
855            "declarative/prelude.rs should remain free of direct query-crate imports"
856        );
857    }
858
859    #[test]
860    fn tracked_model_handle_helpers_are_part_of_default_declarative_surface() {
861        assert!(
862            DECLARATIVE_MODEL_WATCH_RS.contains("pub trait TrackedModelExt<T: Any>"),
863            "declarative/model_watch.rs should expose the handle-first tracked-model helper"
864        );
865        assert!(
866            DECLARATIVE_MOD_RS.contains("pub use model_watch::TrackedModelExt;"),
867            "declarative/mod.rs should re-export the tracked-model helper"
868        );
869        assert!(
870            DECLARATIVE_PRELUDE_RS.contains("pub use super::model_watch::TrackedModelExt;"),
871            "declarative/prelude.rs should include the tracked-model helper in the default declarative prelude"
872        );
873    }
874
875    #[test]
876    fn readme_keeps_icon_provider_installation_explicit_for_ui_kit() {
877        assert!(README.contains("it does not install a default icon pack"));
878        assert!(README.contains("`fret_icons_lucide::app::install`"));
879        assert!(README.contains("`fret_icons_radix::app::install`"));
880        assert!(README.contains("semantic `IconId`"));
881        assert!(README.contains("semantic `IconId` / `ui.*`"));
882        assert!(README.contains("one named installer/bundle surface"));
883        assert!(README.contains("use fret_icons::ids;"));
884        assert!(README.contains("use fret_ui_kit::prelude::*;"));
885        assert!(README.contains("fret_icons_lucide::app::install(app);"));
886        assert!(README.contains("let _icon = icon(ids::ui::SEARCH);"));
887    }
888}