1#![deny(deprecated)]
2#[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
32pub(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#[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#[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#[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#[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#[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#[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#[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#[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#[doc(hidden)]
282pub use window_overlays::{
283 OverlaySynthesisEvent, OverlaySynthesisKind, OverlaySynthesisOutcome, OverlaySynthesisSource,
284 WindowOverlaySynthesisDiagnosticsStore,
285};
286
287pub 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
333pub 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}