Skip to main content

windows_reactor/
element.rs

1use std::any::Any;
2use std::fmt;
3use std::rc::Rc;
4use std::sync::Arc;
5use std::time::Duration;
6
7use super::*;
8use crate::core::{ComponentToken, ComponentView, ContextProvision};
9
10pub(crate) fn validate_image_uri(value: &str) -> windows_core::Result<()> {
11    native::validate_native_image_uri(value)
12}
13
14pub(crate) fn validate_uri(value: &str) -> windows_core::Result<()> {
15    native::validate_native_uri(value)
16}
17
18/// Immutable encoded bitmap data for an [`Image`] or [`ImageIcon`] source.
19#[derive(Clone)]
20pub struct EncodedImage(EncodedImageBytes);
21
22#[derive(Clone)]
23enum EncodedImageBytes {
24    Static(&'static [u8]),
25    Shared(Arc<[u8]>),
26}
27
28impl EncodedImage {
29    /// Owns encoded bitmap data that may be shared across views.
30    ///
31    /// # Panics
32    ///
33    /// Panics if the data exceeds the WinRT buffer limit of 4 GiB.
34    pub fn new(bytes: impl Into<Arc<[u8]>>) -> Self {
35        let bytes = bytes.into();
36        assert!(
37            bytes.len() <= u32::MAX as usize,
38            "encoded image data cannot exceed 4 GiB"
39        );
40        Self(EncodedImageBytes::Shared(bytes))
41    }
42
43    /// Retains encoded bitmap data with static storage without copying it.
44    ///
45    /// # Panics
46    ///
47    /// Panics if the data exceeds the WinRT buffer limit of 4 GiB.
48    pub fn from_static(bytes: &'static [u8]) -> Self {
49        assert!(
50            bytes.len() <= u32::MAX as usize,
51            "encoded image data cannot exceed 4 GiB"
52        );
53        Self(EncodedImageBytes::Static(bytes))
54    }
55
56    /// Returns the encoded bitmap data.
57    pub fn as_bytes(&self) -> &[u8] {
58        match &self.0 {
59            EncodedImageBytes::Static(bytes) => bytes,
60            EncodedImageBytes::Shared(bytes) => bytes,
61        }
62    }
63}
64
65impl fmt::Debug for EncodedImage {
66    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
67        formatter
68            .debug_struct("EncodedImage")
69            .field("len", &self.as_bytes().len())
70            .finish()
71    }
72}
73
74impl PartialEq for EncodedImage {
75    fn eq(&self, other: &Self) -> bool {
76        match (&self.0, &other.0) {
77            (EncodedImageBytes::Static(left), EncodedImageBytes::Static(right))
78                if std::ptr::eq(*left, *right) =>
79            {
80                true
81            }
82            (EncodedImageBytes::Shared(left), EncodedImageBytes::Shared(right))
83                if Arc::ptr_eq(left, right) =>
84            {
85                true
86            }
87            _ => self.as_bytes() == other.as_bytes(),
88        }
89    }
90}
91
92#[derive(Clone, Debug, PartialEq)]
93pub(crate) enum ImageValue {
94    Uri(String),
95    Encoded(EncodedImage),
96}
97
98pub(crate) fn file_uri(path: &std::path::Path) -> windows_core::Result<String> {
99    if !path.is_absolute() {
100        return Err(windows_core::Error::new(
101            windows_core::HRESULT(0x80070057_u32 as _),
102            "Image::source_file requires an absolute path",
103        ));
104    }
105    let path = path.to_string_lossy();
106    let path = if let Some(path) = path.strip_prefix(r"\\?\UNC\") {
107        format!(r"\\{path}")
108    } else {
109        path.strip_prefix(r"\\?\").unwrap_or(&path).to_string()
110    };
111    let path = path.replace('\\', "/");
112    let mut encoded = String::with_capacity(path.len());
113    const HEX: &[u8; 16] = b"0123456789ABCDEF";
114    for byte in path.bytes() {
115        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~' | b'/' | b':') {
116            encoded.push(char::from(byte));
117        } else {
118            encoded.push('%');
119            encoded.push(char::from(HEX[usize::from(byte >> 4)]));
120            encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
121        }
122    }
123    if let Some(path) = encoded.strip_prefix("//") {
124        Ok(format!("file://{path}"))
125    } else if encoded.starts_with('/') {
126        Ok(format!("file://{encoded}"))
127    } else {
128        Ok(format!("file:///{encoded}"))
129    }
130}
131
132#[cfg(test)]
133mod file_uri_tests {
134    use super::*;
135
136    #[test]
137    fn encodes_drive_paths_and_reserved_characters() {
138        assert_eq!(
139            file_uri(std::path::Path::new(r"\\?\C:\work dir\a#b%20.png")).unwrap(),
140            "file:///C:/work%20dir/a%23b%2520.png"
141        );
142    }
143
144    #[test]
145    fn encodes_extended_unc_paths_with_an_authority() {
146        assert_eq!(
147            file_uri(std::path::Path::new(r"\\?\UNC\server\share dir\asset.png")).unwrap(),
148            "file://server/share%20dir/asset.png"
149        );
150    }
151
152    #[test]
153    fn rejects_relative_paths() {
154        assert!(file_uri(std::path::Path::new(r"images\asset.png")).is_err());
155    }
156}
157
158/// A brush resolved from the active WinUI theme resources.
159#[repr(u8)]
160#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
161pub enum ThemeBrush {
162    Accent,
163    AccentText,
164    PrimaryText,
165    SolidBackground,
166    CardBackground,
167    CardStroke,
168    SystemCritical,
169    SystemCriticalBackground,
170}
171
172/// An OpenType font weight in the inclusive range 1 through 999.
173#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
174pub struct FontWeight(u16);
175
176impl FontWeight {
177    pub const BLACK: Self = Self(900);
178    pub const BOLD: Self = Self(700);
179    pub const EXTRA_BLACK: Self = Self(950);
180    pub const EXTRA_BOLD: Self = Self(800);
181    pub const EXTRA_LIGHT: Self = Self(200);
182    pub const LIGHT: Self = Self(300);
183    pub const MEDIUM: Self = Self(500);
184    pub const NORMAL: Self = Self(400);
185    pub const SEMI_BOLD: Self = Self(600);
186    pub const SEMI_LIGHT: Self = Self(350);
187    pub const THIN: Self = Self(100);
188
189    /// Creates a font weight, returning `None` outside the OpenType range 1 through 999.
190    pub const fn new(weight: u16) -> Option<Self> {
191        if weight >= 1 && weight <= 999 {
192            Some(Self(weight))
193        } else {
194            None
195        }
196    }
197
198    /// Returns the numeric OpenType weight.
199    pub const fn get(self) -> u16 {
200        self.0
201    }
202}
203
204impl Default for FontWeight {
205    fn default() -> Self {
206        Self::NORMAL
207    }
208}
209
210#[cfg(test)]
211mod font_weight_tests {
212    use super::*;
213
214    #[test]
215    fn validates_open_type_weight_range() {
216        assert_eq!(FontWeight::new(0), None);
217        assert_eq!(FontWeight::new(1).unwrap().get(), 1);
218        assert_eq!(FontWeight::new(999).unwrap().get(), 999);
219        assert_eq!(FontWeight::new(1000), None);
220        assert_eq!(FontWeight::default(), FontWeight::NORMAL);
221    }
222}
223
224impl ThemeBrush {
225    pub(crate) fn resource_key(self) -> &'static str {
226        match self {
227            Self::Accent => "AccentFillColorDefaultBrush",
228            Self::AccentText => "AccentTextFillColorPrimaryBrush",
229            Self::PrimaryText => "TextFillColorPrimaryBrush",
230            Self::SolidBackground => "SolidBackgroundFillColorBaseBrush",
231            Self::CardBackground => "CardBackgroundFillColorDefaultBrush",
232            Self::CardStroke => "CardStrokeColorDefaultBrush",
233            Self::SystemCritical => "SystemFillColorCriticalBrush",
234            Self::SystemCriticalBackground => "SystemFillColorCriticalBackgroundBrush",
235        }
236    }
237}
238
239/// An 8-bit-per-channel ARGB color.
240#[repr(C)]
241#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
242pub struct Color {
243    pub a: u8,
244    pub r: u8,
245    pub g: u8,
246    pub b: u8,
247}
248
249impl Color {
250    /// Creates a color from alpha, red, green, and blue channels.
251    pub const fn argb(a: u8, r: u8, g: u8, b: u8) -> Self {
252        Self { a, r, g, b }
253    }
254
255    /// Creates an opaque color from red, green, and blue channels.
256    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
257        Self::argb(255, r, g, b)
258    }
259
260    /// Returns fully transparent black.
261    pub const fn transparent() -> Self {
262        Self::argb(0, 0, 0, 0)
263    }
264}
265
266/// A theme resource brush or a fixed solid color.
267#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
268pub enum Brush {
269    Theme(ThemeBrush),
270    Solid(Color),
271}
272
273/// Pointer state in element-local and window-relative device-independent pixels.
274#[derive(Clone, Copy, Debug, Default, PartialEq)]
275pub struct PointerEventInfo {
276    pub x: f64,
277    pub y: f64,
278    pub window_x: f64,
279    pub window_y: f64,
280    pub capture_succeeded: bool,
281    pub is_left_button_pressed: bool,
282    pub is_right_button_pressed: bool,
283    pub is_middle_button_pressed: bool,
284}
285
286#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
287pub enum NavigationViewDisplayMode {
288    Minimal,
289    Compact,
290    Expanded,
291}
292
293impl Brush {
294    pub(crate) const fn theme(self) -> Option<ThemeBrush> {
295        match self {
296            Self::Theme(value) => Some(value),
297            Self::Solid(_) => None,
298        }
299    }
300}
301
302impl From<ThemeBrush> for Brush {
303    fn from(value: ThemeBrush) -> Self {
304        Self::Theme(value)
305    }
306}
307
308impl From<Color> for Brush {
309    fn from(value: Color) -> Self {
310        Self::Solid(value)
311    }
312}
313
314pub(crate) mod sealed {
315    pub trait Sealed {}
316
317    pub(crate) trait NativeControl: Sealed + Sized {
318        fn into_element(self) -> super::Element;
319    }
320
321    pub(crate) trait LayoutControl: NativeControl {
322        fn element_state_mut(&mut self) -> &mut Option<std::rc::Rc<super::ElementState>>;
323    }
324
325    pub(crate) trait ContentControl: NativeControl {
326        fn into_content_view(self, content: super::View) -> super::View {
327            super::View(super::ViewKind::Content {
328                control: self.into_element(),
329                content: Box::new(content.into_kind()),
330            })
331        }
332    }
333
334    pub(crate) trait SlotIndex<S> {
335        fn slot_index(slot: S) -> u8;
336    }
337
338    pub trait StaticViews {
339        fn into_positioned(self) -> Vec<super::KeyedView>;
340    }
341}
342
343/// Stable identity for a keyed child, menu item, tree node, or command.
344#[derive(Clone, Debug, Eq, Hash, PartialEq)]
345pub struct Key(KeyKind);
346
347#[derive(Clone, Debug, Eq, Hash, PartialEq)]
348enum KeyKind {
349    Integer(u64),
350    String(Rc<str>),
351    Position(usize),
352}
353
354impl Key {
355    pub(crate) fn position(value: usize) -> Self {
356        Self(KeyKind::Position(value))
357    }
358}
359
360impl From<u64> for Key {
361    fn from(value: u64) -> Self {
362        Self(KeyKind::Integer(value))
363    }
364}
365
366impl From<u32> for Key {
367    fn from(value: u32) -> Self {
368        Self(KeyKind::Integer(value.into()))
369    }
370}
371
372impl From<usize> for Key {
373    fn from(value: usize) -> Self {
374        Self(KeyKind::Integer(u64::try_from(value).unwrap()))
375    }
376}
377
378impl From<String> for Key {
379    fn from(value: String) -> Self {
380        Self(KeyKind::String(value.into()))
381    }
382}
383
384impl From<&str> for Key {
385    fn from(value: &str) -> Self {
386        Self(KeyKind::String(value.into()))
387    }
388}
389
390#[derive(Clone, Debug, PartialEq)]
391pub(crate) struct KeyedElement {
392    key: Key,
393    element: Element,
394}
395
396impl KeyedElement {
397    #[cfg(test)]
398    pub(crate) fn new(key: impl Into<Key>, element: impl Into<Element>) -> Self {
399        Self {
400            key: key.into(),
401            element: element.into(),
402        }
403    }
404
405    pub(crate) fn key(&self) -> &Key {
406        &self.key
407    }
408
409    pub(crate) fn element(&self) -> &Element {
410        &self.element
411    }
412
413    pub(crate) fn into_parts(self) -> (Key, Element) {
414        (self.key, self.element)
415    }
416}
417
418#[cfg(test)]
419pub(crate) trait NativeContentTestExt: Sized {
420    fn native_content(self, content: impl Into<Element>) -> Self;
421}
422
423#[cfg(test)]
424pub(crate) trait NativeChildrenTestExt: Sized {
425    fn native_child(self, key: impl Into<Key>, child: impl Into<Element>) -> Self;
426    fn native_children(self, children: impl IntoIterator<Item = KeyedElement>) -> Self;
427}
428
429/// A declarative Reactor subtree.
430///
431/// Positional fragments preserve identity by position. Keyed fragments preserve child identity by
432/// [`Key`] as items are inserted, removed, or reordered.
433#[derive(Clone, Debug, PartialEq)]
434pub struct View(ViewKind);
435
436#[derive(Clone, Debug, PartialEq)]
437pub(crate) enum ViewKind {
438    Native(Element),
439    Component(ComponentView),
440    Fragment(Rc<Vec<KeyedView>>),
441    Provider {
442        provision: ContextProvision,
443        child: Box<Self>,
444    },
445    Content {
446        control: Element,
447        content: Box<Self>,
448    },
449    Children {
450        control: Element,
451        children: Rc<Vec<KeyedView>>,
452    },
453    Slots {
454        control: Element,
455        slots: Rc<Vec<SlottedView>>,
456    },
457    Tooltip {
458        target: Box<Self>,
459        tooltip: Tooltip,
460    },
461    Flyout {
462        target: Box<Self>,
463        flyout: Flyout,
464    },
465    Menu {
466        target: Box<Self>,
467        menu: Menu,
468    },
469    CommandBarFlyout {
470        target: Box<Self>,
471        flyout: CommandBarFlyout,
472    },
473    TreeNodes {
474        tree: Box<Self>,
475        nodes: Rc<Vec<TreeNode>>,
476    },
477    ContentDialog {
478        dialog: Box<Self>,
479        open: bool,
480    },
481}
482
483/// Converts a statically shaped expression into positional views.
484///
485/// This trait is sealed. `()` represents no views, fixed-size arrays represent homogeneous
486/// shapes, and tuples represent heterogeneous shapes. Dynamic collections require
487/// [`ChildrenControl::keyed_children`] or [`View::keyed_fragment`].
488///
489/// A `Vec` cannot supply positional children:
490///
491/// ```compile_fail
492/// use windows_reactor::*;
493///
494/// let dynamic: Vec<View> = vec![TextBlock::new().into()];
495/// let _ = StackPanel::new().children(dynamic);
496/// ```
497///
498/// Iterator adapters cannot supply positional children:
499///
500/// ```compile_fail
501/// use windows_reactor::*;
502///
503/// let dynamic = (0..3).map(|index| TextBlock::new().text(index.to_string()));
504/// let _ = StackPanel::new().children(dynamic);
505/// ```
506pub trait IntoViews: sealed::StaticViews {}
507
508/// Placement of a tooltip relative to its target.
509#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
510pub enum TooltipPlacement {
511    #[default]
512    Top,
513    Bottom,
514    Left,
515    Right,
516    Mouse,
517}
518
519/// Text or view content displayed as a tooltip.
520#[derive(Clone, Debug, PartialEq)]
521pub struct Tooltip {
522    pub(crate) content: Box<View>,
523    pub(crate) placement: TooltipPlacement,
524}
525
526impl Tooltip {
527    /// Creates a text tooltip with top placement.
528    pub fn text(value: impl Into<String>) -> Self {
529        Self::rich(TextBlock::new().text(value))
530    }
531
532    /// Creates a rich-content tooltip with top placement.
533    pub fn rich(content: impl Into<View>) -> Self {
534        Self {
535            content: Box::new(content.into()),
536            placement: TooltipPlacement::Top,
537        }
538    }
539
540    /// Sets the preferred placement.
541    pub fn placement(mut self, placement: TooltipPlacement) -> Self {
542        self.placement = placement;
543        self
544    }
545}
546
547/// Adds a tooltip to a view.
548pub trait TooltipExt: Into<View> + Sized {
549    fn tooltip(self, value: impl Into<String>) -> View {
550        self.tooltip_with(Tooltip::text(value))
551    }
552
553    fn tooltip_with(self, tooltip: Tooltip) -> View {
554        View(ViewKind::Tooltip {
555            target: Box::new(self.into().into_kind()),
556            tooltip,
557        })
558    }
559}
560
561impl<T> TooltipExt for T where T: Into<View> {}
562
563/// Placement of a flyout relative to its target.
564#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
565pub enum FlyoutPlacement {
566    #[default]
567    Top,
568    Bottom,
569    Left,
570    Right,
571    Full,
572    TopEdgeAlignedLeft,
573    TopEdgeAlignedRight,
574    BottomEdgeAlignedLeft,
575    BottomEdgeAlignedRight,
576    LeftEdgeAlignedTop,
577    LeftEdgeAlignedBottom,
578    RightEdgeAlignedTop,
579    RightEdgeAlignedBottom,
580    Auto,
581}
582
583/// Text or view content displayed in a flyout.
584#[derive(Clone, Debug, PartialEq)]
585pub struct Flyout {
586    pub(crate) content: Box<View>,
587    pub(crate) placement: FlyoutPlacement,
588}
589
590impl Flyout {
591    /// Creates a text flyout with top placement.
592    pub fn text(value: impl Into<String>) -> Self {
593        Self::rich(TextBlock::new().text(value))
594    }
595
596    /// Creates a rich-content flyout with top placement.
597    pub fn rich(content: impl Into<View>) -> Self {
598        Self {
599            content: Box::new(content.into()),
600            placement: FlyoutPlacement::Top,
601        }
602    }
603
604    /// Sets the preferred placement.
605    pub fn placement(mut self, placement: FlyoutPlacement) -> Self {
606        self.placement = placement;
607        self
608    }
609}
610
611/// Adds a flyout to a view.
612pub trait FlyoutExt: Into<View> + Sized {
613    fn flyout(self, value: impl Into<String>) -> View {
614        self.flyout_with(Flyout::text(value))
615    }
616
617    fn flyout_with(self, flyout: Flyout) -> View {
618        View(ViewKind::Flyout {
619            target: Box::new(self.into().into_kind()),
620            flyout,
621        })
622    }
623}
624
625impl<T> FlyoutExt for T where T: Into<View> {}
626
627/// A keyed item in a context menu.
628#[derive(Clone, Debug, PartialEq)]
629pub enum MenuItem {
630    Item {
631        key: Key,
632        label: String,
633        enabled: bool,
634    },
635    Separator {
636        key: Key,
637    },
638    Submenu {
639        key: Key,
640        label: String,
641        items: Vec<Self>,
642    },
643}
644
645impl MenuItem {
646    /// Creates an enabled command item.
647    pub fn item(key: impl Into<Key>, label: impl Into<String>) -> Self {
648        Self::Item {
649            key: key.into(),
650            label: label.into(),
651            enabled: true,
652        }
653    }
654
655    /// Creates a disabled command item.
656    pub fn disabled(key: impl Into<Key>, label: impl Into<String>) -> Self {
657        Self::Item {
658            key: key.into(),
659            label: label.into(),
660            enabled: false,
661        }
662    }
663
664    /// Creates a separator.
665    pub fn separator(key: impl Into<Key>) -> Self {
666        Self::Separator { key: key.into() }
667    }
668
669    /// Creates a nested submenu.
670    pub fn submenu(
671        key: impl Into<Key>,
672        label: impl Into<String>,
673        items: impl IntoIterator<Item = Self>,
674    ) -> Self {
675        Self::Submenu {
676            key: key.into(),
677            label: label.into(),
678            items: items.into_iter().collect(),
679        }
680    }
681
682    pub(crate) fn key(&self) -> &Key {
683        match self {
684            Self::Item { key, .. } | Self::Separator { key } | Self::Submenu { key, .. } => key,
685        }
686    }
687}
688
689/// A context menu whose click callback receives the selected item's label.
690#[derive(Clone, Debug, PartialEq)]
691pub struct Menu {
692    pub(crate) items: Vec<MenuItem>,
693    pub(crate) on_click: Callback<String>,
694}
695
696impl Menu {
697    /// Creates a menu and a callback that receives the selected item's label.
698    pub fn new(
699        items: impl IntoIterator<Item = MenuItem>,
700        on_click: impl IntoPayloadCallback<String>,
701    ) -> Self {
702        Self {
703            items: items.into_iter().collect(),
704            on_click: on_click.into_payload_callback(),
705        }
706    }
707}
708
709/// Adds a context menu to a view.
710pub trait MenuExt: Into<View> + Sized {
711    fn menu(self, menu: Menu) -> View {
712        View(ViewKind::Menu {
713            target: Box::new(self.into().into_kind()),
714            menu,
715        })
716    }
717}
718
719impl<T> MenuExt for T where T: Into<View> {}
720
721/// A keyed command owned by a command bar or command-bar flyout.
722#[derive(Clone, Debug, PartialEq)]
723pub enum CommandBarCommand {
724    Button {
725        key: Key,
726        label: String,
727        icon: Option<Symbol>,
728        enabled: bool,
729    },
730    Separator {
731        key: Key,
732    },
733}
734
735impl CommandBarCommand {
736    pub fn button(key: impl Into<Key>, label: impl Into<String>) -> Self {
737        Self::Button {
738            key: key.into(),
739            label: label.into(),
740            icon: None,
741            enabled: true,
742        }
743    }
744
745    pub fn button_with_icon(key: impl Into<Key>, label: impl Into<String>, icon: Symbol) -> Self {
746        Self::Button {
747            key: key.into(),
748            label: label.into(),
749            icon: Some(icon),
750            enabled: true,
751        }
752    }
753
754    pub fn separator(key: impl Into<Key>) -> Self {
755        Self::Separator { key: key.into() }
756    }
757
758    pub(crate) fn key(&self) -> &Key {
759        match self {
760            Self::Button { key, .. } | Self::Separator { key } => key,
761        }
762    }
763
764    fn into_keyed_view(self, on_click: &Callback<String>) -> KeyedView {
765        match self {
766            Self::Button {
767                key,
768                label,
769                icon,
770                enabled,
771            } => {
772                let callback = on_click.clone();
773                let clicked = label.clone();
774                let button = AppBarButton::new()
775                    .label(label)
776                    .is_enabled(enabled)
777                    .on_click(move || {
778                        let _ = callback.call(clicked.clone());
779                    });
780                let view = match icon {
781                    Some(icon) => button.slots([SlotView::new(
782                        AppBarButtonSlot::Icon,
783                        SymbolIcon::new().symbol(icon),
784                    )]),
785                    None => button.into(),
786                };
787                KeyedView { key, view }
788            }
789            Self::Separator { key } => KeyedView {
790                key,
791                view: AppBarSeparator::new().into(),
792            },
793        }
794    }
795}
796
797impl CommandBar {
798    pub fn owned_commands(
799        self,
800        primary: impl IntoIterator<Item = CommandBarCommand>,
801        secondary: impl IntoIterator<Item = CommandBarCommand>,
802        on_click: impl IntoPayloadCallback<String>,
803    ) -> View {
804        let on_click = on_click.into_payload_callback();
805        self.slots([
806            SlotView::collection(
807                CommandBarSlot::PrimaryCommands,
808                primary
809                    .into_iter()
810                    .map(|command| command.into_keyed_view(&on_click)),
811            ),
812            SlotView::collection(
813                CommandBarSlot::SecondaryCommands,
814                secondary
815                    .into_iter()
816                    .map(|command| command.into_keyed_view(&on_click)),
817            ),
818        ])
819    }
820}
821
822/// Primary and secondary commands displayed in a command-bar flyout.
823#[derive(Clone, Debug, PartialEq)]
824pub struct CommandBarFlyout {
825    pub(crate) primary: Vec<CommandBarCommand>,
826    pub(crate) secondary: Vec<CommandBarCommand>,
827    pub(crate) on_click: Callback<String>,
828}
829
830impl CommandBarFlyout {
831    pub fn new(
832        primary: impl IntoIterator<Item = CommandBarCommand>,
833        secondary: impl IntoIterator<Item = CommandBarCommand>,
834        on_click: impl IntoPayloadCallback<String>,
835    ) -> Self {
836        Self {
837            primary: primary.into_iter().collect(),
838            secondary: secondary.into_iter().collect(),
839            on_click: on_click.into_payload_callback(),
840        }
841    }
842}
843
844/// Adds a command-bar flyout to a view.
845pub trait CommandBarFlyoutExt: Into<View> + Sized {
846    fn command_bar_flyout(self, flyout: CommandBarFlyout) -> View {
847        View(ViewKind::CommandBarFlyout {
848            target: Box::new(self.into().into_kind()),
849            flyout,
850        })
851    }
852}
853
854impl<T> CommandBarFlyoutExt for T where T: Into<View> {}
855
856/// Paragraph content for a [`RichTextBlock`].
857#[derive(Clone, Debug, Default, Eq, PartialEq)]
858pub struct RichText {
859    pub(crate) paragraphs: Rc<Vec<RichTextParagraph>>,
860}
861
862impl RichText {
863    /// Creates rich text from paragraphs.
864    pub fn new(paragraphs: impl IntoIterator<Item = RichTextParagraph>) -> Self {
865        Self {
866            paragraphs: Rc::new(paragraphs.into_iter().collect()),
867        }
868    }
869
870    /// Creates rich text containing one paragraph.
871    pub fn single_paragraph(inlines: impl IntoIterator<Item = RichTextInline>) -> Self {
872        Self::new([RichTextParagraph::new(inlines)])
873    }
874}
875
876/// A paragraph of rich-text inline values.
877#[derive(Clone, Debug, Default, Eq, PartialEq)]
878pub struct RichTextParagraph {
879    pub(crate) inlines: Vec<RichTextInline>,
880}
881
882impl RichTextParagraph {
883    /// Creates a paragraph from inline values.
884    pub fn new(inlines: impl IntoIterator<Item = RichTextInline>) -> Self {
885        Self {
886            inlines: inlines.into_iter().collect(),
887        }
888    }
889}
890
891/// An inline run, hyperlink, or line break in rich text.
892#[derive(Clone, Debug, Eq, PartialEq)]
893pub enum RichTextInline {
894    Run(RichTextRun),
895    Hyperlink(RichTextHyperlink),
896    LineBreak,
897}
898
899/// A text run with optional bold and italic styling.
900#[derive(Clone, Debug, Default, Eq, PartialEq)]
901pub struct RichTextRun {
902    pub text: String,
903    pub is_bold: bool,
904    pub is_italic: bool,
905}
906
907impl RichTextRun {
908    /// Creates an unstyled text run.
909    pub fn plain(text: impl Into<String>) -> Self {
910        Self {
911            text: text.into(),
912            ..Self::default()
913        }
914    }
915}
916
917/// A hyperlink with display text and a target URI.
918#[derive(Clone, Debug, Eq, PartialEq)]
919pub struct RichTextHyperlink {
920    pub text: String,
921    pub uri: String,
922}
923
924/// A keyed node in a tree view.
925#[derive(Clone, Debug, Eq, PartialEq)]
926pub struct TreeNode {
927    pub(crate) key: Key,
928    pub(crate) text: String,
929    pub(crate) expanded: bool,
930    pub(crate) children: Vec<Self>,
931}
932
933impl TreeNode {
934    /// Creates a collapsed leaf node.
935    pub fn new(key: impl Into<Key>, text: impl Into<String>) -> Self {
936        Self {
937            key: key.into(),
938            text: text.into(),
939            expanded: false,
940            children: Vec::new(),
941        }
942    }
943
944    /// Sets whether the node is expanded.
945    pub fn expanded(mut self, value: bool) -> Self {
946        self.expanded = value;
947        self
948    }
949
950    /// Replaces the node's children.
951    pub fn children(mut self, children: impl IntoIterator<Item = Self>) -> Self {
952        self.children = children.into_iter().collect();
953        self
954    }
955
956    /// Appends one child node.
957    pub fn child(mut self, child: Self) -> Self {
958        self.children.push(child);
959        self
960    }
961}
962
963/// Supplies a keyed node hierarchy to a tree view.
964pub trait TreeViewExt: Into<View> + Sized {
965    fn nodes(self, nodes: impl IntoIterator<Item = TreeNode>) -> View {
966        View(ViewKind::TreeNodes {
967            tree: Box::new(self.into().into_kind()),
968            nodes: Rc::new(nodes.into_iter().collect()),
969        })
970    }
971}
972
973impl<T> TreeViewExt for T where T: Into<View> {}
974
975impl View {
976    /// Creates a fragment with no children.
977    pub fn empty() -> Self {
978        Self::fragment(())
979    }
980
981    pub(crate) fn native(control: impl Into<Element>) -> Self {
982        Self(ViewKind::Native(control.into()))
983    }
984
985    /// Creates a component view from its input.
986    pub fn component<C: Component>(input: C::Input) -> Self {
987        Self(ViewKind::Component(ComponentView::new::<C>(input)))
988    }
989
990    /// Creates a statically shaped fragment whose children are identified by position.
991    pub fn fragment(children: impl IntoViews) -> Self {
992        Self(ViewKind::Fragment(positioned(children)))
993    }
994
995    /// Creates a dynamic fragment whose children are reconciled by key.
996    pub fn keyed_fragment<T>(children: impl IntoIterator<Item = T>) -> Self
997    where
998        T: Into<KeyedView>,
999    {
1000        Self(ViewKind::Fragment(Rc::new(
1001            children.into_iter().map(Into::into).collect(),
1002        )))
1003    }
1004
1005    /// Provides a context value to `child` and its descendants.
1006    pub fn provide<T>(context: &Context<T>, value: T, child: impl Into<Self>) -> Self
1007    where
1008        T: Clone + PartialEq + 'static,
1009    {
1010        Self(ViewKind::Provider {
1011            provision: ContextProvision::new(context, value),
1012            child: Box::new(child.into().into_kind()),
1013        })
1014    }
1015
1016    pub(crate) fn from_kind(kind: ViewKind) -> Self {
1017        Self(kind)
1018    }
1019
1020    pub(crate) fn content_dialog(dialog: Element, content: Option<Self>, open: bool) -> Self {
1021        let dialog = match content {
1022            Some(content) => Self(ViewKind::Content {
1023                control: dialog,
1024                content: Box::new(content.into_kind()),
1025            }),
1026            None => Self::native(dialog),
1027        };
1028        Self(ViewKind::ContentDialog {
1029            dialog: Box::new(dialog.into_kind()),
1030            open,
1031        })
1032    }
1033
1034    pub(crate) fn as_kind(&self) -> &ViewKind {
1035        &self.0
1036    }
1037
1038    pub(crate) fn into_kind(self) -> ViewKind {
1039        self.0
1040    }
1041}
1042
1043/// Content assigned to one typed control slot.
1044#[derive(Clone, Debug, PartialEq)]
1045pub struct SlotView<S> {
1046    slot: S,
1047    content: SlotContent,
1048}
1049
1050impl<S> SlotView<S> {
1051    /// Creates a slot containing one view.
1052    pub fn new(slot: S, view: impl Into<View>) -> Self {
1053        Self {
1054            slot,
1055            content: SlotContent::Single(view.into()),
1056        }
1057    }
1058
1059    /// Creates a collection slot whose children are reconciled by key.
1060    pub fn collection<T>(slot: S, children: impl IntoIterator<Item = T>) -> Self
1061    where
1062        T: Into<KeyedView>,
1063    {
1064        Self {
1065            slot,
1066            content: SlotContent::Collection(Rc::new(
1067                children.into_iter().map(Into::into).collect(),
1068            )),
1069        }
1070    }
1071
1072    fn into_parts(self) -> (S, SlotContent) {
1073        (self.slot, self.content)
1074    }
1075}
1076
1077#[derive(Clone, Debug, PartialEq)]
1078pub(crate) enum SlotContent {
1079    Single(View),
1080    Collection(Rc<Vec<KeyedView>>),
1081}
1082
1083#[derive(Clone, Debug, PartialEq)]
1084pub(crate) struct SlottedView {
1085    pub(crate) slot: SlotId,
1086    pub(crate) content: SlotContent,
1087}
1088
1089impl From<Element> for View {
1090    fn from(value: Element) -> Self {
1091        Self(ViewKind::Native(value))
1092    }
1093}
1094
1095impl From<String> for View {
1096    fn from(value: String) -> Self {
1097        TextBlock::new().text(value).into()
1098    }
1099}
1100
1101impl From<&str> for View {
1102    fn from(value: &str) -> Self {
1103        value.to_string().into()
1104    }
1105}
1106
1107/// A view paired with stable reconciliation identity.
1108#[derive(Clone, Debug, PartialEq)]
1109pub struct KeyedView {
1110    key: Key,
1111    view: View,
1112}
1113
1114impl KeyedView {
1115    /// Associates `view` with `key`.
1116    pub fn new(key: impl Into<Key>, view: impl Into<View>) -> Self {
1117        Self {
1118            key: key.into(),
1119            view: view.into(),
1120        }
1121    }
1122
1123    pub fn key(&self) -> &Key {
1124        &self.key
1125    }
1126
1127    pub fn view(&self) -> &View {
1128        &self.view
1129    }
1130
1131    pub(crate) fn into_parts(self) -> (Key, View) {
1132        (self.key, self.view)
1133    }
1134
1135    fn position(position: usize, view: View) -> Self {
1136        Self {
1137            key: Key::position(position),
1138            view,
1139        }
1140    }
1141}
1142
1143impl<K, V> From<(K, V)> for KeyedView
1144where
1145    K: Into<Key>,
1146    V: Into<View>,
1147{
1148    fn from((key, view): (K, V)) -> Self {
1149        Self::new(key, view)
1150    }
1151}
1152
1153/// A lazily materialized, keyed source for a virtualizing control.
1154///
1155/// Item functions are called only for indices that the control needs to realize.
1156#[derive(Clone)]
1157pub struct VirtualSource {
1158    key_revision: u64,
1159    len: usize,
1160    key: Rc<dyn Fn(usize) -> Key>,
1161    view: Rc<dyn Fn(usize) -> View>,
1162}
1163
1164impl VirtualSource {
1165    /// Creates an indexed source whose item views are built only when needed.
1166    ///
1167    /// `key_revision` must change whenever the length, order, or value of any key changes. It may
1168    /// remain unchanged when only item view data changes. The key and view functions are called
1169    /// only with indices less than `len`.
1170    pub fn new<K, V, KI, VI>(key_revision: u64, len: usize, key: K, view: V) -> Self
1171    where
1172        K: Fn(usize) -> KI + 'static,
1173        V: Fn(usize) -> VI + 'static,
1174        KI: Into<Key>,
1175        VI: Into<View>,
1176    {
1177        Self {
1178            key_revision,
1179            len,
1180            key: Rc::new(move |index| key(index).into()),
1181            view: Rc::new(move |index| view(index).into()),
1182        }
1183    }
1184
1185    pub fn len(&self) -> usize {
1186        self.len
1187    }
1188
1189    pub fn is_empty(&self) -> bool {
1190        self.len == 0
1191    }
1192
1193    pub fn key_revision(&self) -> u64 {
1194        self.key_revision
1195    }
1196
1197    fn key(&self, index: usize) -> Key {
1198        (self.key)(index)
1199    }
1200
1201    fn view(&self, index: usize) -> View {
1202        (self.view)(index)
1203    }
1204}
1205
1206impl fmt::Debug for VirtualSource {
1207    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1208        formatter
1209            .debug_struct("VirtualSource")
1210            .field("key_revision", &self.key_revision)
1211            .field("len", &self.len)
1212            .finish_non_exhaustive()
1213    }
1214}
1215
1216impl PartialEq for VirtualSource {
1217    fn eq(&self, other: &Self) -> bool {
1218        self.key_revision == other.key_revision
1219            && self.len == other.len
1220            && Rc::ptr_eq(&self.key, &other.key)
1221            && Rc::ptr_eq(&self.view, &other.view)
1222    }
1223}
1224
1225#[derive(Clone, Debug, PartialEq)]
1226pub(crate) enum VirtualItems {
1227    Eager(Rc<Vec<KeyedView>>),
1228    Lazy(VirtualSource),
1229}
1230
1231impl Default for VirtualItems {
1232    fn default() -> Self {
1233        Self::Eager(Rc::default())
1234    }
1235}
1236
1237impl VirtualItems {
1238    pub(crate) fn len(&self) -> usize {
1239        match self {
1240            Self::Eager(items) => items.len(),
1241            Self::Lazy(source) => source.len(),
1242        }
1243    }
1244
1245    pub(crate) fn key(&self, index: usize) -> Option<Key> {
1246        match self {
1247            Self::Eager(items) => items.get(index).map(|item| item.key().clone()),
1248            Self::Lazy(source) => (index < source.len()).then(|| source.key(index)),
1249        }
1250    }
1251
1252    pub(crate) fn view(&self, index: usize) -> Option<View> {
1253        match self {
1254            Self::Eager(items) => items.get(index).map(|item| item.view().clone()),
1255            Self::Lazy(source) => (index < source.len()).then(|| source.view(index)),
1256        }
1257    }
1258
1259    pub(crate) fn changed_keys(&self, previous: &Self, keys: &[Key]) -> Option<Vec<Key>> {
1260        if let (Self::Lazy(current), Self::Lazy(previous)) = (self, previous)
1261            && current.key_revision() == previous.key_revision()
1262            && current.len() == previous.len()
1263            && current.len() == keys.len()
1264        {
1265            return None;
1266        }
1267        if let Self::Eager(items) = self
1268            && items.len() == keys.len()
1269            && keys
1270                .iter()
1271                .zip(items.iter())
1272                .all(|(key, item)| key == item.key())
1273        {
1274            return None;
1275        }
1276        let next = (0..self.len())
1277            .map(|index| self.key(index).unwrap())
1278            .collect::<Vec<_>>();
1279        (next != keys).then_some(next)
1280    }
1281}
1282
1283fn positioned(children: impl IntoViews) -> Rc<Vec<KeyedView>> {
1284    Rc::new(sealed::StaticViews::into_positioned(children))
1285}
1286
1287impl sealed::StaticViews for () {
1288    fn into_positioned(self) -> Vec<KeyedView> {
1289        Vec::new()
1290    }
1291}
1292
1293impl IntoViews for () {}
1294
1295impl<T, const N: usize> sealed::StaticViews for [T; N]
1296where
1297    T: Into<View>,
1298{
1299    fn into_positioned(self) -> Vec<KeyedView> {
1300        self.into_iter()
1301            .enumerate()
1302            .map(|(position, view)| KeyedView::position(position, view.into()))
1303            .collect()
1304    }
1305}
1306
1307impl<T, const N: usize> IntoViews for [T; N] where T: Into<View> {}
1308
1309macro_rules! impl_into_views_tuple {
1310    ($($type:ident $index:tt),+ $(,)?) => {
1311        impl<$($type),+> sealed::StaticViews for ($($type,)+)
1312        where
1313            $($type: Into<View>,)+
1314        {
1315            fn into_positioned(self) -> Vec<KeyedView> {
1316                vec![$(KeyedView::position($index, self.$index.into())),+]
1317            }
1318        }
1319
1320        impl<$($type),+> IntoViews for ($($type,)+)
1321        where
1322            $($type: Into<View>,)+
1323        {
1324        }
1325    };
1326}
1327
1328impl_into_views_tuple!(A 0);
1329impl_into_views_tuple!(A 0, B 1);
1330impl_into_views_tuple!(A 0, B 1, C 2);
1331impl_into_views_tuple!(A 0, B 1, C 2, D 3);
1332impl_into_views_tuple!(A 0, B 1, C 2, D 3, E 4);
1333impl_into_views_tuple!(A 0, B 1, C 2, D 3, E 4, F 5);
1334impl_into_views_tuple!(A 0, B 1, C 2, D 3, E 4, F 5, G 6);
1335impl_into_views_tuple!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7);
1336impl_into_views_tuple!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8);
1337impl_into_views_tuple!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9);
1338impl_into_views_tuple!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10);
1339impl_into_views_tuple!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11);
1340impl_into_views_tuple!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12);
1341impl_into_views_tuple!(
1342    A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13
1343);
1344impl_into_views_tuple!(
1345    A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14
1346);
1347impl_into_views_tuple!(
1348    A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15
1349);
1350
1351#[derive(Clone, Debug)]
1352enum FourValues {
1353    Uniform(f64),
1354    Values(Rc<[f64; 4]>),
1355}
1356
1357impl FourValues {
1358    fn new(values: [f64; 4]) -> Self {
1359        if values.iter().all(|value| *value == values[0]) {
1360            Self::Uniform(values[0])
1361        } else {
1362            Self::Values(Rc::new(values))
1363        }
1364    }
1365
1366    fn values(&self) -> [f64; 4] {
1367        match self {
1368            Self::Uniform(value) => [*value; 4],
1369            Self::Values(values) => **values,
1370        }
1371    }
1372}
1373
1374/// Four edge values measured in device-independent pixels (DIPs).
1375#[derive(Clone, Debug)]
1376pub struct Thickness(FourValues);
1377
1378impl Thickness {
1379    /// Uses the same DIP value for all four edges.
1380    pub fn uniform(value: f64) -> Self {
1381        Self(FourValues::Uniform(value))
1382    }
1383
1384    /// Uses one DIP value for horizontal edges and another for vertical edges.
1385    pub fn xy(horizontal: f64, vertical: f64) -> Self {
1386        Self::new(horizontal, vertical, horizontal, vertical)
1387    }
1388
1389    /// Creates edge values in left, top, right, bottom order, measured in DIPs.
1390    pub fn new(left: f64, top: f64, right: f64, bottom: f64) -> Self {
1391        Self(FourValues::new([left, top, right, bottom]))
1392    }
1393
1394    pub fn left(&self) -> f64 {
1395        self.values()[0]
1396    }
1397
1398    pub fn top(&self) -> f64 {
1399        self.values()[1]
1400    }
1401
1402    pub fn right(&self) -> f64 {
1403        self.values()[2]
1404    }
1405
1406    pub fn bottom(&self) -> f64 {
1407        self.values()[3]
1408    }
1409
1410    pub(crate) fn values(&self) -> [f64; 4] {
1411        self.0.values()
1412    }
1413
1414    pub(crate) fn is_finite_non_negative(&self) -> bool {
1415        self.values()
1416            .into_iter()
1417            .all(|value| value.is_finite() && value >= 0.0)
1418    }
1419
1420    pub(crate) fn is_finite(&self) -> bool {
1421        self.values().into_iter().all(f64::is_finite)
1422    }
1423}
1424
1425impl Default for Thickness {
1426    fn default() -> Self {
1427        Self::uniform(0.0)
1428    }
1429}
1430
1431impl From<f64> for Thickness {
1432    fn from(value: f64) -> Self {
1433        Self::uniform(value)
1434    }
1435}
1436
1437impl PartialEq for Thickness {
1438    fn eq(&self, other: &Self) -> bool {
1439        self.values() == other.values()
1440    }
1441}
1442
1443/// Four corner radii measured in device-independent pixels (DIPs).
1444#[derive(Clone, Debug)]
1445pub struct CornerRadius(FourValues);
1446
1447impl CornerRadius {
1448    /// Uses the same DIP radius for all four corners.
1449    pub fn uniform(value: f64) -> Self {
1450        Self(FourValues::Uniform(value))
1451    }
1452
1453    /// Creates radii in top-left, top-right, bottom-right, bottom-left order.
1454    pub fn new(top_left: f64, top_right: f64, bottom_right: f64, bottom_left: f64) -> Self {
1455        Self(FourValues::new([
1456            top_left,
1457            top_right,
1458            bottom_right,
1459            bottom_left,
1460        ]))
1461    }
1462
1463    pub fn top_left(&self) -> f64 {
1464        self.values()[0]
1465    }
1466
1467    pub fn top_right(&self) -> f64 {
1468        self.values()[1]
1469    }
1470
1471    pub fn bottom_right(&self) -> f64 {
1472        self.values()[2]
1473    }
1474
1475    pub fn bottom_left(&self) -> f64 {
1476        self.values()[3]
1477    }
1478
1479    pub(crate) fn values(&self) -> [f64; 4] {
1480        self.0.values()
1481    }
1482
1483    pub(crate) fn is_finite_non_negative(&self) -> bool {
1484        self.values()
1485            .into_iter()
1486            .all(|value| value.is_finite() && value >= 0.0)
1487    }
1488}
1489
1490impl Default for CornerRadius {
1491    fn default() -> Self {
1492        Self::uniform(0.0)
1493    }
1494}
1495
1496impl From<f64> for CornerRadius {
1497    fn from(value: f64) -> Self {
1498        Self::uniform(value)
1499    }
1500}
1501
1502impl PartialEq for CornerRadius {
1503    fn eq(&self, other: &Self) -> bool {
1504        self.values() == other.values()
1505    }
1506}
1507
1508/// A supported WinUI resource override value.
1509#[derive(Clone, Debug, PartialEq)]
1510pub enum ResourceValue {
1511    Color(Color),
1512    Thickness(Thickness),
1513    CornerRadius(CornerRadius),
1514}
1515
1516impl From<Color> for ResourceValue {
1517    fn from(value: Color) -> Self {
1518        Self::Color(value)
1519    }
1520}
1521
1522impl From<Thickness> for ResourceValue {
1523    fn from(value: Thickness) -> Self {
1524        Self::Thickness(value)
1525    }
1526}
1527
1528impl From<CornerRadius> for ResourceValue {
1529    fn from(value: CornerRadius) -> Self {
1530        Self::CornerRadius(value)
1531    }
1532}
1533
1534/// Theme resource values applied to a control subtree.
1535#[derive(Clone, Debug, Default, PartialEq)]
1536pub struct ResourceOverrides {
1537    values: std::collections::BTreeMap<String, ResourceValue>,
1538}
1539
1540impl ResourceOverrides {
1541    pub fn new() -> Self {
1542        Self::default()
1543    }
1544
1545    /// Adds or replaces a resource value.
1546    ///
1547    /// # Panics
1548    ///
1549    /// Panics if the key is empty, or if a thickness or radius is negative or non-finite.
1550    pub fn set(mut self, key: impl Into<String>, value: impl Into<ResourceValue>) -> Self {
1551        let key = key.into();
1552        assert!(!key.is_empty(), "resource override key must not be empty");
1553        let value = value.into();
1554        match &value {
1555            ResourceValue::Color(_) => {}
1556            ResourceValue::Thickness(value) => {
1557                assert!(
1558                    value.is_finite_non_negative(),
1559                    "resource override thickness must be finite and non-negative"
1560                );
1561            }
1562            ResourceValue::CornerRadius(value) => {
1563                assert!(
1564                    value.is_finite_non_negative(),
1565                    "resource override corner radius must be finite and non-negative"
1566                );
1567            }
1568        }
1569        self.values.insert(key, value);
1570        self
1571    }
1572
1573    pub(crate) fn values(&self) -> impl Iterator<Item = (&str, &ResourceValue)> {
1574        self.values.iter().map(|(key, value)| (key.as_str(), value))
1575    }
1576}
1577
1578/// The theme requested for a window.
1579#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1580pub enum WindowTheme {
1581    #[default]
1582    System,
1583    Light,
1584    Dark,
1585}
1586
1587/// Window client dimensions in device-independent pixels (DIPs).
1588#[derive(Clone, Copy, Debug, Default, PartialEq)]
1589pub struct WindowSize {
1590    pub width: f64,
1591    pub height: f64,
1592}
1593
1594/// Whether the active application color scheme is light or dark.
1595#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1596pub enum ColorScheme {
1597    #[default]
1598    Light,
1599    Dark,
1600}
1601
1602#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1603pub enum DragKind {
1604    StorageItems,
1605    Text,
1606    Unsupported,
1607}
1608
1609#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1610pub enum DragDropOperation {
1611    Copy,
1612    Move,
1613    Link,
1614}
1615
1616#[derive(Clone, Debug, Eq, PartialEq)]
1617pub struct DragDropAction {
1618    pub operation: DragDropOperation,
1619    pub caption: Option<String>,
1620}
1621
1622impl DragDropAction {
1623    pub fn new(operation: DragDropOperation) -> Self {
1624        Self {
1625            operation,
1626            caption: None,
1627        }
1628    }
1629
1630    pub fn caption(mut self, caption: impl Into<String>) -> Self {
1631        self.caption = Some(caption.into());
1632        self
1633    }
1634}
1635
1636#[derive(Clone, Debug, Default, Eq, PartialEq)]
1637pub struct DragDropPolicy {
1638    pub storage_items: Option<DragDropAction>,
1639    pub text: Option<DragDropAction>,
1640}
1641
1642impl DragDropPolicy {
1643    pub fn new() -> Self {
1644        Self::default()
1645    }
1646
1647    pub fn storage_items(mut self, action: impl Into<Option<DragDropAction>>) -> Self {
1648        self.storage_items = action.into();
1649        self
1650    }
1651
1652    pub fn text(mut self, action: impl Into<Option<DragDropAction>>) -> Self {
1653        self.text = action.into();
1654        self
1655    }
1656
1657    pub(crate) fn accepts(&self, kind: DragKind) -> bool {
1658        match kind {
1659            DragKind::StorageItems => self.storage_items.is_some(),
1660            DragKind::Text => self.text.is_some(),
1661            DragKind::Unsupported => false,
1662        }
1663    }
1664}
1665
1666#[derive(Clone, Debug, Eq, PartialEq)]
1667pub struct DroppedStorageItem {
1668    pub name: String,
1669    pub path: String,
1670}
1671
1672#[derive(Clone, Debug, Eq, PartialEq)]
1673pub enum DroppedData {
1674    StorageItems(Vec<DroppedStorageItem>),
1675    Text(String),
1676    Unsupported,
1677}
1678
1679/// Material used behind a window's content.
1680#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1681pub enum WindowBackdrop {
1682    #[default]
1683    None,
1684    Mica,
1685    MicaAlt,
1686    Acrylic,
1687}
1688
1689/// Height preset for an extended window title bar.
1690#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1691pub enum WindowTitleBarHeight {
1692    #[default]
1693    Standard,
1694    Tall,
1695}
1696
1697/// Optional window client-size limits in device-independent pixels (DIPs).
1698#[derive(Clone, Copy, Debug, Default, PartialEq)]
1699pub struct WindowConstraints {
1700    pub min_width: Option<f64>,
1701    pub min_height: Option<f64>,
1702    pub max_width: Option<f64>,
1703    pub max_height: Option<f64>,
1704}
1705
1706impl WindowConstraints {
1707    fn validate(self) {
1708        for value in [
1709            self.min_width,
1710            self.min_height,
1711            self.max_width,
1712            self.max_height,
1713        ]
1714        .into_iter()
1715        .flatten()
1716        {
1717            assert!(
1718                value.is_finite() && value > 0.0,
1719                "window constraints must be finite and positive"
1720            );
1721        }
1722        assert!(
1723            self.min_width
1724                .zip(self.max_width)
1725                .is_none_or(|(min, max)| min <= max)
1726                && self
1727                    .min_height
1728                    .zip(self.max_height)
1729                    .is_none_or(|(min, max)| min <= max),
1730            "window minimum constraints must not exceed maximum constraints"
1731        );
1732    }
1733}
1734
1735/// Window appearance and client sizing requested by a component publication.
1736#[derive(Clone, Copy, Debug, Default, PartialEq)]
1737pub struct WindowVisuals {
1738    pub(crate) backdrop: WindowBackdrop,
1739    pub(crate) client_size: Option<(f64, f64)>,
1740    pub(crate) constraints: Option<WindowConstraints>,
1741    pub(crate) icon: Option<&'static str>,
1742    pub(crate) theme: WindowTheme,
1743}
1744
1745impl WindowVisuals {
1746    pub fn new() -> Self {
1747        Self::default()
1748    }
1749
1750    pub fn backdrop(mut self, backdrop: WindowBackdrop) -> Self {
1751        self.backdrop = backdrop;
1752        self
1753    }
1754
1755    /// Sets the initial client size in DIPs.
1756    ///
1757    /// # Panics
1758    ///
1759    /// Panics unless both dimensions are finite and positive.
1760    pub fn client_size(mut self, width: f64, height: f64) -> Self {
1761        assert!(
1762            width.is_finite() && width > 0.0 && height.is_finite() && height > 0.0,
1763            "window client size must be finite and positive"
1764        );
1765        self.client_size = Some((width, height));
1766        self
1767    }
1768
1769    /// Sets the path to the window icon.
1770    ///
1771    /// # Panics
1772    ///
1773    /// Panics if `path` is empty.
1774    pub fn icon(mut self, path: &'static str) -> Self {
1775        assert!(!path.is_empty(), "window icon path must not be empty");
1776        self.icon = Some(path);
1777        self
1778    }
1779
1780    /// Sets client-size constraints in DIPs.
1781    ///
1782    /// # Panics
1783    ///
1784    /// Panics if a bound is non-positive or non-finite, or a minimum exceeds its maximum.
1785    pub fn constraints(mut self, constraints: WindowConstraints) -> Self {
1786        constraints.validate();
1787        self.constraints = Some(constraints);
1788        self
1789    }
1790
1791    pub fn theme(mut self, theme: WindowTheme) -> Self {
1792        self.theme = theme;
1793        self
1794    }
1795}
1796
1797#[cfg(test)]
1798mod visual_value_tests {
1799    use super::*;
1800    use crate::core::ThemeStyle;
1801    use std::mem::size_of;
1802
1803    #[test]
1804    fn four_value_types_keep_compact_layout_and_semantic_equality() {
1805        assert_eq!(size_of::<Thickness>(), 16);
1806        assert_eq!(size_of::<CornerRadius>(), 16);
1807        assert_eq!(size_of::<ThemeStyle>(), 4);
1808
1809        assert_eq!(Thickness::uniform(3.0), Thickness::new(3.0, 3.0, 3.0, 3.0));
1810        assert_eq!(
1811            CornerRadius::uniform(4.0),
1812            CornerRadius::new(4.0, 4.0, 4.0, 4.0)
1813        );
1814        assert_eq!(Thickness::xy(2.0, 5.0).values(), [2.0, 5.0, 2.0, 5.0]);
1815    }
1816
1817    #[test]
1818    fn window_client_size_rejects_invalid_values() {
1819        for (width, height) in [
1820            (0.0, 1.0),
1821            (1.0, -1.0),
1822            (f64::NAN, 1.0),
1823            (1.0, f64::INFINITY),
1824        ] {
1825            assert!(
1826                std::panic::catch_unwind(|| {
1827                    WindowVisuals::new().client_size(width, height);
1828                })
1829                .is_err()
1830            );
1831        }
1832    }
1833}
1834
1835/// A Grid row or column size.
1836#[derive(Clone, Copy, Debug)]
1837pub enum GridLength {
1838    /// Sizes to the content.
1839    Auto,
1840    /// Uses a fixed number of device-independent pixels (DIPs).
1841    Pixel(f64),
1842    /// Uses a weighted share of the remaining space.
1843    Star(f64),
1844}
1845
1846impl GridLength {
1847    /// One weighted share of the remaining space.
1848    pub const STAR: Self = Self::Star(1.0);
1849
1850    pub(crate) fn is_valid(self) -> bool {
1851        match self {
1852            Self::Auto => true,
1853            Self::Pixel(value) | Self::Star(value) => value.is_finite() && value >= 0.0,
1854        }
1855    }
1856}
1857
1858impl PartialEq for GridLength {
1859    fn eq(&self, other: &Self) -> bool {
1860        match (self, other) {
1861            (Self::Auto, Self::Auto) => true,
1862            (Self::Pixel(left), Self::Pixel(right)) | (Self::Star(left), Self::Star(right)) => {
1863                f64_eq(*left, *right)
1864            }
1865            _ => false,
1866        }
1867    }
1868}
1869
1870/// Horizontal placement within the space assigned by a parent.
1871#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1872pub enum HorizontalAlignment {
1873    Left,
1874    Center,
1875    Right,
1876    Stretch,
1877}
1878
1879/// Vertical placement within the space assigned by a parent.
1880#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1881pub enum VerticalAlignment {
1882    Top,
1883    Center,
1884    Bottom,
1885    Stretch,
1886}
1887
1888#[derive(Clone, Debug, Default, PartialEq)]
1889pub(crate) struct ElementState {
1890    width: Property<f64>,
1891    height: Property<f64>,
1892    min_width: Property<f64>,
1893    max_width: Property<f64>,
1894    min_height: Property<f64>,
1895    max_height: Property<f64>,
1896    opacity: Property<f64>,
1897    horizontal_alignment: Property<HorizontalAlignment>,
1898    vertical_alignment: Property<VerticalAlignment>,
1899    margin: Property<Thickness>,
1900    row: Option<i32>,
1901    column: Option<i32>,
1902    row_span: Option<i32>,
1903    column_span: Option<i32>,
1904    relative_align_left: bool,
1905    relative_align_top: bool,
1906    relative_align_right: bool,
1907    relative_align_bottom: bool,
1908    relative_align_horizontal_center: bool,
1909    relative_align_vertical_center: bool,
1910    canvas_left: Option<f64>,
1911    canvas_top: Option<f64>,
1912    automation_name: Option<String>,
1913    automation_id: Option<String>,
1914    automation_heading_level: Option<AutomationHeadingLevel>,
1915    exit_transition: Option<ExitTransition>,
1916}
1917
1918#[derive(Clone, Debug, Default, PartialEq)]
1919pub(crate) enum Property<T> {
1920    #[default]
1921    Inherited,
1922    Set(T),
1923}
1924
1925impl<T> Property<T> {
1926    pub(crate) fn as_set(&self) -> Option<&T> {
1927        match self {
1928            Self::Inherited => None,
1929            Self::Set(value) => Some(value),
1930        }
1931    }
1932}
1933
1934impl<T> From<Option<T>> for Property<T> {
1935    fn from(value: Option<T>) -> Self {
1936        match value {
1937            Some(value) => Self::Set(value),
1938            None => Self::Inherited,
1939        }
1940    }
1941}
1942
1943pub(crate) fn f64_eq(left: f64, right: f64) -> bool {
1944    left == right || left.is_nan() && right.is_nan()
1945}
1946
1947pub(crate) fn f64_property_eq(left: &Property<f64>, right: &Property<f64>) -> bool {
1948    match (left, right) {
1949        (Property::Inherited, Property::Inherited) => true,
1950        (Property::Set(left), Property::Set(right)) => f64_eq(*left, *right),
1951        _ => false,
1952    }
1953}
1954
1955#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1956pub(crate) struct CallbackSource {
1957    queue: usize,
1958    token: ComponentToken,
1959}
1960
1961impl CallbackSource {
1962    pub(crate) fn new(queue: usize, token: ComponentToken) -> Self {
1963        Self { queue, token }
1964    }
1965}
1966
1967trait ErasedCallbackIdentity {
1968    fn as_any(&self) -> &dyn Any;
1969    fn equals(&self, other: &dyn ErasedCallbackIdentity) -> bool;
1970}
1971
1972struct TypedCallbackIdentity<K> {
1973    key: K,
1974    source: CallbackSource,
1975}
1976
1977impl<K: PartialEq + 'static> ErasedCallbackIdentity for TypedCallbackIdentity<K> {
1978    fn as_any(&self) -> &dyn Any {
1979        self
1980    }
1981
1982    fn equals(&self, other: &dyn ErasedCallbackIdentity) -> bool {
1983        other
1984            .as_any()
1985            .downcast_ref::<Self>()
1986            .is_some_and(|other| self.source == other.source && self.key == other.key)
1987    }
1988}
1989
1990/// A clonable callback used by Reactor events.
1991///
1992/// Clones compare equal and retain the same function. Callbacks made by a [`LocalSender`] from a
1993/// captureless mapper can also compare equal across publications.
1994pub struct Callback<T> {
1995    callback: Rc<dyn Fn(T) -> bool>,
1996    identity: Option<Rc<dyn ErasedCallbackIdentity>>,
1997}
1998
1999impl<T> Callback<T> {
2000    /// Wraps a callback that always reports accepted delivery.
2001    pub fn new(callback: impl Fn(T) + 'static) -> Self {
2002        Self::new_with_acceptance(move |value| {
2003            callback(value);
2004            true
2005        })
2006    }
2007
2008    pub(crate) fn new_with_acceptance(callback: impl Fn(T) -> bool + 'static) -> Self {
2009        Self {
2010            callback: Rc::new(callback),
2011            identity: None,
2012        }
2013    }
2014
2015    pub(crate) fn new_identified<K>(
2016        source: CallbackSource,
2017        key: K,
2018        callback: impl Fn(T) -> bool + 'static,
2019    ) -> Self
2020    where
2021        K: PartialEq + 'static,
2022    {
2023        Self {
2024            callback: Rc::new(callback),
2025            identity: Some(Rc::new(TypedCallbackIdentity { key, source })),
2026        }
2027    }
2028
2029    #[must_use = "false means the adapted message was rejected"]
2030    /// Calls the handler and returns whether it accepted the value.
2031    ///
2032    /// Sender-backed callbacks return `false` when their component message cannot be queued.
2033    pub fn call(&self, value: T) -> bool {
2034        (self.callback)(value)
2035    }
2036}
2037
2038impl<T> Clone for Callback<T> {
2039    fn clone(&self) -> Self {
2040        Self {
2041            callback: Rc::clone(&self.callback),
2042            identity: self.identity.clone(),
2043        }
2044    }
2045}
2046
2047impl<T> fmt::Debug for Callback<T> {
2048    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2049        formatter
2050            .debug_tuple("Callback")
2051            .field(&Rc::as_ptr(&self.callback))
2052            .finish()
2053    }
2054}
2055
2056impl<T> PartialEq for Callback<T> {
2057    fn eq(&self, other: &Self) -> bool {
2058        Rc::ptr_eq(&self.callback, &other.callback)
2059            || self
2060                .identity
2061                .as_deref()
2062                .zip(other.identity.as_deref())
2063                .is_some_and(|(left, right)| left.equals(right))
2064    }
2065}
2066
2067/// Converts a payload handler or typed message callback into an event callback.
2068pub trait IntoPayloadCallback<T> {
2069    fn into_payload_callback(self) -> Callback<T>;
2070}
2071
2072impl<T, F> IntoPayloadCallback<T> for F
2073where
2074    F: Fn(T) + 'static,
2075{
2076    fn into_payload_callback(self) -> Callback<T> {
2077        Callback::new(self)
2078    }
2079}
2080
2081impl<T> IntoPayloadCallback<T> for Callback<T> {
2082    fn into_payload_callback(self) -> Self {
2083        self
2084    }
2085}
2086
2087/// Converts a zero-argument handler or typed message callback into an event callback.
2088pub trait IntoUnitCallback {
2089    fn into_unit_callback(self) -> Callback<()>;
2090}
2091
2092impl<F> IntoUnitCallback for F
2093where
2094    F: Fn() + 'static,
2095{
2096    fn into_unit_callback(self) -> Callback<()> {
2097        Callback::new(move |()| self())
2098    }
2099}
2100
2101impl IntoUnitCallback for Callback<()> {
2102    fn into_unit_callback(self) -> Self {
2103        self
2104    }
2105}
2106
2107/// A key supported by Reactor keyboard accelerators.
2108#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
2109pub enum AcceleratorKey {
2110    R,
2111    NumberPad0,
2112    NumberPad1,
2113    NumberPad2,
2114    NumberPad3,
2115    NumberPad4,
2116    NumberPad5,
2117    NumberPad6,
2118    NumberPad7,
2119    NumberPad8,
2120    NumberPad9,
2121    Divide,
2122    Multiply,
2123    Subtract,
2124    Add,
2125    Decimal,
2126    Enter,
2127}
2128
2129/// Modifier keys for a keyboard accelerator.
2130#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
2131pub enum AcceleratorModifiers {
2132    #[default]
2133    None,
2134    Control,
2135}
2136
2137#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
2138pub enum AutomationHeadingLevel {
2139    Level1,
2140    Level2,
2141    Level3,
2142    Level4,
2143    Level5,
2144    Level6,
2145    Level7,
2146    Level8,
2147    Level9,
2148}
2149
2150/// A fade applied while an element is removed from the native tree.
2151#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2152pub struct ExitTransition {
2153    duration: Duration,
2154}
2155
2156impl ExitTransition {
2157    /// Creates a fade-out transition.
2158    ///
2159    /// # Panics
2160    ///
2161    /// Panics if `duration` is zero.
2162    pub fn fade(duration: Duration) -> Self {
2163        assert!(
2164            !duration.is_zero(),
2165            "Exit transition duration must be positive"
2166        );
2167        Self { duration }
2168    }
2169
2170    pub fn duration(self) -> Duration {
2171        self.duration
2172    }
2173}
2174
2175/// A keyboard accelerator and its callback.
2176#[derive(Clone, Debug, PartialEq)]
2177pub struct KeyAccelerator {
2178    pub(crate) key: AcceleratorKey,
2179    pub(crate) modifiers: AcceleratorModifiers,
2180    pub(crate) callback: Callback<()>,
2181}
2182
2183impl KeyAccelerator {
2184    /// Creates an accelerator for `key` and `modifiers`.
2185    pub fn new(
2186        key: AcceleratorKey,
2187        modifiers: AcceleratorModifiers,
2188        callback: impl IntoUnitCallback,
2189    ) -> Self {
2190        Self {
2191            key,
2192            modifiers,
2193            callback: callback.into_unit_callback(),
2194        }
2195    }
2196}
2197
2198/// A set of keyboard accelerators assigned to a control.
2199#[derive(Clone, Debug, Default, PartialEq)]
2200pub struct KeyAccelerators {
2201    pub(crate) values: Vec<KeyAccelerator>,
2202}
2203
2204impl KeyAccelerators {
2205    pub fn new(values: impl IntoIterator<Item = KeyAccelerator>) -> Self {
2206        Self {
2207            values: values.into_iter().collect(),
2208        }
2209    }
2210}
2211
2212/// Applies layout, opacity, margin, and exit-transition properties to native controls.
2213///
2214/// Dimensions, margins, and Canvas positions use device-independent pixels (DIPs). Passing
2215/// `None` to an optional property leaves it inherited or unset.
2216#[allow(private_bounds)]
2217pub trait LayoutControl: sealed::LayoutControl {
2218    fn width(mut self, value: impl Into<Option<f64>>) -> Self
2219    where
2220        Self: Sized,
2221    {
2222        let value = value.into();
2223        assert!(
2224            value.is_none_or(|value| value.is_finite() && value >= 0.0),
2225            "Width must be finite and non-negative",
2226        );
2227        Rc::make_mut(
2228            sealed::LayoutControl::element_state_mut(&mut self)
2229                .get_or_insert_with(|| Rc::new(ElementState::default())),
2230        )
2231        .width = Property::from(value);
2232        self
2233    }
2234
2235    fn height(mut self, value: impl Into<Option<f64>>) -> Self
2236    where
2237        Self: Sized,
2238    {
2239        let value = value.into();
2240        assert!(
2241            value.is_none_or(|value| value.is_finite() && value >= 0.0),
2242            "Height must be finite and non-negative",
2243        );
2244        Rc::make_mut(
2245            sealed::LayoutControl::element_state_mut(&mut self)
2246                .get_or_insert_with(|| Rc::new(ElementState::default())),
2247        )
2248        .height = Property::from(value);
2249        self
2250    }
2251
2252    fn min_width(mut self, value: impl Into<Option<f64>>) -> Self
2253    where
2254        Self: Sized,
2255    {
2256        let value = value.into();
2257        assert!(
2258            value.is_none_or(|value| value.is_finite() && value >= 0.0),
2259            "Minimum width must be finite and non-negative",
2260        );
2261        Rc::make_mut(
2262            sealed::LayoutControl::element_state_mut(&mut self)
2263                .get_or_insert_with(|| Rc::new(ElementState::default())),
2264        )
2265        .min_width = Property::from(value);
2266        self
2267    }
2268
2269    fn max_width(mut self, value: impl Into<Option<f64>>) -> Self
2270    where
2271        Self: Sized,
2272    {
2273        let value = value.into();
2274        assert!(
2275            value.is_none_or(|value| value.is_finite() && value >= 0.0),
2276            "Maximum width must be finite and non-negative",
2277        );
2278        Rc::make_mut(
2279            sealed::LayoutControl::element_state_mut(&mut self)
2280                .get_or_insert_with(|| Rc::new(ElementState::default())),
2281        )
2282        .max_width = Property::from(value);
2283        self
2284    }
2285
2286    fn min_height(mut self, value: impl Into<Option<f64>>) -> Self
2287    where
2288        Self: Sized,
2289    {
2290        let value = value.into();
2291        assert!(
2292            value.is_none_or(|value| value.is_finite() && value >= 0.0),
2293            "Minimum height must be finite and non-negative",
2294        );
2295        Rc::make_mut(
2296            sealed::LayoutControl::element_state_mut(&mut self)
2297                .get_or_insert_with(|| Rc::new(ElementState::default())),
2298        )
2299        .min_height = Property::from(value);
2300        self
2301    }
2302
2303    fn max_height(mut self, value: impl Into<Option<f64>>) -> Self
2304    where
2305        Self: Sized,
2306    {
2307        let value = value.into();
2308        assert!(
2309            value.is_none_or(|value| value.is_finite() && value >= 0.0),
2310            "Maximum height must be finite and non-negative",
2311        );
2312        Rc::make_mut(
2313            sealed::LayoutControl::element_state_mut(&mut self)
2314                .get_or_insert_with(|| Rc::new(ElementState::default())),
2315        )
2316        .max_height = Property::from(value);
2317        self
2318    }
2319
2320    fn opacity(mut self, value: impl Into<Option<f64>>) -> Self
2321    where
2322        Self: Sized,
2323    {
2324        let value = value.into();
2325        assert!(
2326            value.is_none_or(|value| value.is_finite() && value >= 0.0),
2327            "Opacity must be finite and non-negative",
2328        );
2329        Rc::make_mut(
2330            sealed::LayoutControl::element_state_mut(&mut self)
2331                .get_or_insert_with(|| Rc::new(ElementState::default())),
2332        )
2333        .opacity = Property::from(value);
2334        self
2335    }
2336
2337    fn horizontal_alignment(mut self, value: impl Into<Option<HorizontalAlignment>>) -> Self
2338    where
2339        Self: Sized,
2340    {
2341        Rc::make_mut(
2342            sealed::LayoutControl::element_state_mut(&mut self)
2343                .get_or_insert_with(|| Rc::new(ElementState::default())),
2344        )
2345        .horizontal_alignment = Property::from(value.into());
2346        self
2347    }
2348
2349    fn vertical_alignment(mut self, value: impl Into<Option<VerticalAlignment>>) -> Self
2350    where
2351        Self: Sized,
2352    {
2353        Rc::make_mut(
2354            sealed::LayoutControl::element_state_mut(&mut self)
2355                .get_or_insert_with(|| Rc::new(ElementState::default())),
2356        )
2357        .vertical_alignment = Property::from(value.into());
2358        self
2359    }
2360
2361    fn margin(mut self, value: impl Into<Thickness>) -> Self
2362    where
2363        Self: Sized,
2364    {
2365        let value = value.into();
2366        assert!(value.is_finite(), "Margin must be finite");
2367        Rc::make_mut(
2368            sealed::LayoutControl::element_state_mut(&mut self)
2369                .get_or_insert_with(|| Rc::new(ElementState::default())),
2370        )
2371        .margin = Property::Set(value);
2372        self
2373    }
2374
2375    fn margin_optional<T>(mut self, value: Option<T>) -> Self
2376    where
2377        Self: Sized,
2378        T: Into<Thickness>,
2379    {
2380        let value = value.map(Into::into);
2381        assert!(
2382            value.as_ref().is_none_or(Thickness::is_finite),
2383            "Margin must be finite",
2384        );
2385        Rc::make_mut(
2386            sealed::LayoutControl::element_state_mut(&mut self)
2387                .get_or_insert_with(|| Rc::new(ElementState::default())),
2388        )
2389        .margin = Property::from(value);
2390        self
2391    }
2392
2393    fn exit_transition(mut self, transition: ExitTransition) -> Self
2394    where
2395        Self: Sized,
2396    {
2397        Rc::make_mut(
2398            sealed::LayoutControl::element_state_mut(&mut self)
2399                .get_or_insert_with(|| Rc::new(ElementState::default())),
2400        )
2401        .exit_transition = Some(transition);
2402        self
2403    }
2404}
2405
2406impl ElementState {
2407    pub(crate) fn exit_transition(&self) -> Option<ExitTransition> {
2408        self.exit_transition
2409    }
2410}
2411
2412/// Assigns one content view to a native content control.
2413#[allow(private_bounds)]
2414pub trait ContentControl: sealed::ContentControl + Sized {
2415    fn content(self, content: impl Into<View>) -> View {
2416        sealed::ContentControl::into_content_view(self, content.into())
2417    }
2418}
2419
2420/// Assigns children to a native container.
2421///
2422/// [`children`](Self::children) uses positional identity for static shapes.
2423/// [`keyed_children`](Self::keyed_children) preserves identity by key for dynamic collections.
2424#[allow(private_bounds)]
2425pub trait ChildrenControl: sealed::NativeControl + Sized {
2426    fn children(self, children: impl IntoViews) -> View {
2427        View(ViewKind::Children {
2428            control: sealed::NativeControl::into_element(self),
2429            children: positioned(children),
2430        })
2431    }
2432
2433    fn keyed_children<T>(self, children: impl IntoIterator<Item = T>) -> View
2434    where
2435        T: Into<KeyedView>,
2436    {
2437        View(ViewKind::Children {
2438            control: sealed::NativeControl::into_element(self),
2439            children: Rc::new(children.into_iter().map(Into::into).collect()),
2440        })
2441    }
2442}
2443
2444/// Assigns single views or keyed collections to a control's typed slots.
2445#[allow(private_bounds)]
2446pub trait SlotsControl: sealed::NativeControl + sealed::SlotIndex<Self::Slot> + Sized {
2447    type Slot: Copy;
2448
2449    fn slot(self, slot: Self::Slot, view: impl Into<View>) -> View {
2450        self.slots([SlotView::new(slot, view)])
2451    }
2452
2453    fn collection_slot<T>(self, slot: Self::Slot, children: impl IntoIterator<Item = T>) -> View
2454    where
2455        T: Into<KeyedView>,
2456    {
2457        self.slots([SlotView::collection(slot, children)])
2458    }
2459
2460    fn slots(self, slots: impl IntoIterator<Item = SlotView<Self::Slot>>) -> View {
2461        let control = sealed::NativeControl::into_element(self);
2462        let kind = control.kind();
2463        let slots = slots
2464            .into_iter()
2465            .map(|slot| {
2466                let (slot, content) = slot.into_parts();
2467                SlottedView {
2468                    slot: slot_id(
2469                        kind,
2470                        <Self as sealed::SlotIndex<Self::Slot>>::slot_index(slot),
2471                    )
2472                    .unwrap(),
2473                    content,
2474                }
2475            })
2476            .collect();
2477        View(ViewKind::Slots {
2478            control,
2479            slots: Rc::new(slots),
2480        })
2481    }
2482}
2483
2484/// Places a concrete native control in its parent Grid.
2485///
2486/// Components and fragments can produce more than one native root, so place a native wrapper when
2487/// a composed view needs Grid placement.
2488///
2489/// ```compile_fail
2490/// use windows_reactor::*;
2491///
2492/// struct Child;
2493/// # impl Component for Child {
2494/// #     type Message = ();
2495/// #     type Input = ();
2496/// #     fn create(_: &(), _: &ComponentContext<Self>) -> Self { Self }
2497/// #     fn update(&mut self, _: (), _: &ComponentContext<Self>) {}
2498/// #     fn view(&self, _: &(), _: &mut ViewContext<Self>) -> View { View::empty() }
2499/// # }
2500/// let _ = View::component::<Child>(()).grid_row(0);
2501/// ```
2502///
2503/// ```compile_fail
2504/// use windows_reactor::*;
2505///
2506/// let _ = View::fragment((TextBlock::new(), TextBlock::new())).grid_column(0);
2507/// ```
2508pub trait GridChildExt: LayoutControl + Sized {
2509    fn grid_row(mut self, row: i32) -> Self {
2510        assert!(row >= 0, "Grid row must be non-negative");
2511        Rc::make_mut(
2512            self.element_state_mut()
2513                .get_or_insert_with(|| Rc::new(ElementState::default())),
2514        )
2515        .row = Some(row);
2516        self
2517    }
2518
2519    fn grid_column(mut self, column: i32) -> Self {
2520        assert!(column >= 0, "Grid column must be non-negative");
2521        Rc::make_mut(
2522            self.element_state_mut()
2523                .get_or_insert_with(|| Rc::new(ElementState::default())),
2524        )
2525        .column = Some(column);
2526        self
2527    }
2528
2529    fn grid_row_span(mut self, span: i32) -> Self {
2530        assert!(span > 0, "Grid row span must be positive");
2531        Rc::make_mut(
2532            self.element_state_mut()
2533                .get_or_insert_with(|| Rc::new(ElementState::default())),
2534        )
2535        .row_span = Some(span);
2536        self
2537    }
2538
2539    fn grid_column_span(mut self, span: i32) -> Self {
2540        assert!(span > 0, "Grid column span must be positive");
2541        Rc::make_mut(
2542            self.element_state_mut()
2543                .get_or_insert_with(|| Rc::new(ElementState::default())),
2544        )
2545        .column_span = Some(span);
2546        self
2547    }
2548}
2549
2550impl<T: LayoutControl> GridChildExt for T {}
2551
2552/// Places a concrete native control in its parent RelativePanel.
2553pub trait RelativePanelChildExt: LayoutControl + Sized {
2554    fn relative_align_left(mut self) -> Self {
2555        Rc::make_mut(
2556            self.element_state_mut()
2557                .get_or_insert_with(|| Rc::new(ElementState::default())),
2558        )
2559        .relative_align_left = true;
2560        self
2561    }
2562
2563    fn relative_align_top(mut self) -> Self {
2564        Rc::make_mut(
2565            self.element_state_mut()
2566                .get_or_insert_with(|| Rc::new(ElementState::default())),
2567        )
2568        .relative_align_top = true;
2569        self
2570    }
2571
2572    fn relative_align_right(mut self) -> Self {
2573        Rc::make_mut(
2574            self.element_state_mut()
2575                .get_or_insert_with(|| Rc::new(ElementState::default())),
2576        )
2577        .relative_align_right = true;
2578        self
2579    }
2580
2581    fn relative_align_bottom(mut self) -> Self {
2582        Rc::make_mut(
2583            self.element_state_mut()
2584                .get_or_insert_with(|| Rc::new(ElementState::default())),
2585        )
2586        .relative_align_bottom = true;
2587        self
2588    }
2589
2590    fn relative_align_horizontal_center(mut self) -> Self {
2591        Rc::make_mut(
2592            self.element_state_mut()
2593                .get_or_insert_with(|| Rc::new(ElementState::default())),
2594        )
2595        .relative_align_horizontal_center = true;
2596        self
2597    }
2598
2599    fn relative_align_vertical_center(mut self) -> Self {
2600        Rc::make_mut(
2601            self.element_state_mut()
2602                .get_or_insert_with(|| Rc::new(ElementState::default())),
2603        )
2604        .relative_align_vertical_center = true;
2605        self
2606    }
2607}
2608
2609impl<T: LayoutControl> RelativePanelChildExt for T {}
2610
2611/// Places a concrete native control in its parent Canvas.
2612pub trait CanvasChildExt: LayoutControl + Sized {
2613    fn canvas_left(mut self, value: f64) -> Self {
2614        assert!(value.is_finite(), "Canvas left must be finite");
2615        Rc::make_mut(
2616            self.element_state_mut()
2617                .get_or_insert_with(|| Rc::new(ElementState::default())),
2618        )
2619        .canvas_left = Some(value);
2620        self
2621    }
2622
2623    fn canvas_top(mut self, value: f64) -> Self {
2624        assert!(value.is_finite(), "Canvas top must be finite");
2625        Rc::make_mut(
2626            self.element_state_mut()
2627                .get_or_insert_with(|| Rc::new(ElementState::default())),
2628        )
2629        .canvas_top = Some(value);
2630        self
2631    }
2632}
2633
2634impl<T: LayoutControl> CanvasChildExt for T {}
2635
2636/// Adds UI Automation metadata to a native control.
2637pub trait AutomationExt: LayoutControl + Sized {
2638    fn automation_name(mut self, value: impl Into<String>) -> Self {
2639        Rc::make_mut(
2640            self.element_state_mut()
2641                .get_or_insert_with(|| Rc::new(ElementState::default())),
2642        )
2643        .automation_name = Some(value.into());
2644        self
2645    }
2646
2647    fn automation_id(mut self, value: impl Into<String>) -> Self {
2648        Rc::make_mut(
2649            self.element_state_mut()
2650                .get_or_insert_with(|| Rc::new(ElementState::default())),
2651        )
2652        .automation_id = Some(value.into());
2653        self
2654    }
2655
2656    fn automation_heading_level(mut self, value: AutomationHeadingLevel) -> Self {
2657        Rc::make_mut(
2658            self.element_state_mut()
2659                .get_or_insert_with(|| Rc::new(ElementState::default())),
2660        )
2661        .automation_heading_level = Some(value);
2662        self
2663    }
2664}
2665
2666impl<T: LayoutControl> AutomationExt for T {}
2667
2668pub(crate) fn visit_element_state(
2669    placement: Option<&ElementState>,
2670    visit: &mut dyn FnMut(PropertyId, Option<PropertyValueRef<'_>>),
2671) {
2672    visit(
2673        PropertyId::Width,
2674        placement
2675            .and_then(|value| value.width.as_set())
2676            .copied()
2677            .map(PropertyValueRef::F64),
2678    );
2679    visit(
2680        PropertyId::Height,
2681        placement
2682            .and_then(|value| value.height.as_set())
2683            .copied()
2684            .map(PropertyValueRef::F64),
2685    );
2686    visit(
2687        PropertyId::MinWidth,
2688        placement
2689            .and_then(|value| value.min_width.as_set())
2690            .copied()
2691            .map(PropertyValueRef::F64),
2692    );
2693    visit(
2694        PropertyId::MaxWidth,
2695        placement
2696            .and_then(|value| value.max_width.as_set())
2697            .copied()
2698            .map(PropertyValueRef::F64),
2699    );
2700    visit(
2701        PropertyId::MinHeight,
2702        placement
2703            .and_then(|value| value.min_height.as_set())
2704            .copied()
2705            .map(PropertyValueRef::F64),
2706    );
2707    visit(
2708        PropertyId::MaxHeight,
2709        placement
2710            .and_then(|value| value.max_height.as_set())
2711            .copied()
2712            .map(PropertyValueRef::F64),
2713    );
2714    visit(
2715        PropertyId::Opacity,
2716        placement
2717            .and_then(|value| value.opacity.as_set())
2718            .copied()
2719            .map(PropertyValueRef::F64),
2720    );
2721    visit(
2722        PropertyId::HorizontalAlignment,
2723        placement
2724            .and_then(|value| value.horizontal_alignment.as_set())
2725            .copied()
2726            .map(PropertyValueRef::HorizontalAlignment),
2727    );
2728    visit(
2729        PropertyId::VerticalAlignment,
2730        placement
2731            .and_then(|value| value.vertical_alignment.as_set())
2732            .copied()
2733            .map(PropertyValueRef::VerticalAlignment),
2734    );
2735    visit(
2736        PropertyId::Margin,
2737        placement
2738            .and_then(|value| value.margin.as_set())
2739            .map(PropertyValueRef::Thickness),
2740    );
2741    let value = |value: Option<i32>| value.map(PropertyValueRef::I32);
2742    visit(
2743        PropertyId::GridRow,
2744        value(placement.and_then(|value| value.row)),
2745    );
2746    visit(
2747        PropertyId::GridColumn,
2748        value(placement.and_then(|value| value.column)),
2749    );
2750    visit(
2751        PropertyId::GridRowSpan,
2752        value(placement.and_then(|value| value.row_span)),
2753    );
2754    visit(
2755        PropertyId::GridColumnSpan,
2756        value(placement.and_then(|value| value.column_span)),
2757    );
2758    let relative = |value: bool| value.then_some(PropertyValueRef::Bool(true));
2759    visit(
2760        PropertyId::RelativeAlignLeft,
2761        relative(placement.is_some_and(|value| value.relative_align_left)),
2762    );
2763    visit(
2764        PropertyId::RelativeAlignTop,
2765        relative(placement.is_some_and(|value| value.relative_align_top)),
2766    );
2767    visit(
2768        PropertyId::RelativeAlignRight,
2769        relative(placement.is_some_and(|value| value.relative_align_right)),
2770    );
2771    visit(
2772        PropertyId::RelativeAlignBottom,
2773        relative(placement.is_some_and(|value| value.relative_align_bottom)),
2774    );
2775    visit(
2776        PropertyId::RelativeAlignHorizontalCenter,
2777        relative(placement.is_some_and(|value| value.relative_align_horizontal_center)),
2778    );
2779    visit(
2780        PropertyId::RelativeAlignVerticalCenter,
2781        relative(placement.is_some_and(|value| value.relative_align_vertical_center)),
2782    );
2783    visit(
2784        PropertyId::CanvasLeft,
2785        placement
2786            .and_then(|value| value.canvas_left)
2787            .map(PropertyValueRef::F64),
2788    );
2789    visit(
2790        PropertyId::CanvasTop,
2791        placement
2792            .and_then(|value| value.canvas_top)
2793            .map(PropertyValueRef::F64),
2794    );
2795    visit(
2796        PropertyId::AutomationName,
2797        placement
2798            .and_then(|value| value.automation_name.as_deref())
2799            .map(PropertyValueRef::Str),
2800    );
2801    visit(
2802        PropertyId::AutomationId,
2803        placement
2804            .and_then(|value| value.automation_id.as_deref())
2805            .map(PropertyValueRef::Str),
2806    );
2807    visit(
2808        PropertyId::AutomationHeadingLevel,
2809        placement
2810            .and_then(|value| value.automation_heading_level)
2811            .map(|value| PropertyValueRef::I32(value as i32 + 1)),
2812    );
2813}