diff --git a/crates/libs/composition/readme.md b/crates/libs/composition/readme.md index a0d47f0ed4f..8f9569fc43b 100644 --- a/crates/libs/composition/readme.md +++ b/crates/libs/composition/readme.md @@ -22,9 +22,9 @@ fn build(compositor: &Compositor) -> SpriteVisual { } ``` -Core types include `Compositor`, the visual types, brushes, shapes, and key-frame animations. For -system composition, create a `DispatcherQueueController` and `Compositor`, then host the root -visual with `Compositor::create_desktop_window_target`, which takes a +Core types include `Compositor`, the visual types, brushes, shapes, key-frame animations, and +animation groups. For system composition, create a `DispatcherQueueController` and `Compositor`, +then host the root visual with `Compositor::create_desktop_window_target`, which takes a [`windows-window`][window-guide] `Window`. See the [composition guide](https://github.com/microsoft/windows-rs/blob/master/docs/crates/windows-composition.md) for the API and hosting options. diff --git a/crates/libs/composition/src/animation.rs b/crates/libs/composition/src/animation.rs index f9a817e4553..0f4c74e47d4 100644 --- a/crates/libs/composition/src/animation.rs +++ b/crates/libs/composition/src/animation.rs @@ -167,6 +167,23 @@ impl Animation for Vector3KeyFrameAnimation { } } +/// A set of animations that start together. +#[derive(Clone)] +pub struct CompositionAnimationGroup(pub(crate) bindings::CompositionAnimationGroup); + +impl CompositionAnimationGroup { + /// Adds an animation to the group. + pub fn add(&self, animation: &impl Animation) { + self.0.Add(&animation.as_animation().0).unwrap(); + } + + /// Returns the lifted group as an inspectable object for a WinUI host. + #[cfg(feature = "reactor")] + pub fn as_host(&self) -> windows_core::IInspectable { + self.0.cast().unwrap() + } +} + /// A map of property-name -> animation applied to a visual so that changes to /// those properties animate automatically. /// diff --git a/crates/libs/composition/src/bindings.rs b/crates/libs/composition/src/bindings.rs index 958cc43efc3..a9978744ad6 100644 --- a/crates/libs/composition/src/bindings.rs +++ b/crates/libs/composition/src/bindings.rs @@ -62,6 +62,38 @@ impl windows_core::RuntimeName for CompositionAnimation { unsafe impl Send for CompositionAnimation {} unsafe impl Sync for CompositionAnimation {} #[repr(transparent)] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CompositionAnimationGroup(windows_core::IUnknown); +windows_core::imp::interface_hierarchy!( + CompositionAnimationGroup, + windows_core::IUnknown, + windows_core::IInspectable +); +windows_core::imp::required_hierarchy!( + CompositionAnimationGroup, + ICompositionAnimationBase, + CompositionObject +); +impl windows_core::RuntimeType for CompositionAnimationGroup { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_class::(); +} +unsafe impl windows_core::Interface for CompositionAnimationGroup { + type Vtable = ::Vtable; + const IID: windows_core::GUID = ::IID; +} +impl core::ops::Deref for CompositionAnimationGroup { + type Target = ICompositionAnimationGroup; + fn deref(&self) -> &Self::Target { + unsafe { core::mem::transmute(self) } + } +} +impl windows_core::RuntimeName for CompositionAnimationGroup { + const NAME: &'static str = "Windows.UI.Composition.CompositionAnimationGroup"; +} +unsafe impl Send for CompositionAnimationGroup {} +unsafe impl Sync for CompositionAnimationGroup {} +#[repr(transparent)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct CompositionBatchTypes(pub u32); impl CompositionBatchTypes { @@ -1002,6 +1034,38 @@ impl ICompositionAnimationBase_Vtbl { pub struct ICompositionAnimationBase_Vtbl { pub base__: windows_core::IInspectable_Vtbl, } +windows_core::imp::define_interface!( + ICompositionAnimationGroup, + ICompositionAnimationGroup_Vtbl, + 0x5e7cc90c_cd14_4e07_8a55_c72527aabdac +); +impl windows_core::RuntimeType for ICompositionAnimationGroup { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +impl ICompositionAnimationGroup { + pub(crate) fn Add(&self, value: P0) -> windows_core::Result<()> + where + P0: windows_core::Param, + { + unsafe { + (windows_core::Interface::vtable(self).Add)( + windows_core::Interface::as_raw(self), + value.param().abi(), + ) + .ok() + } + } +} +#[repr(C)] +pub struct ICompositionAnimationGroup_Vtbl { + pub base__: windows_core::IInspectable_Vtbl, + Count: usize, + pub Add: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + ) -> windows_core::HRESULT, +} windows_core::imp::define_interface!( ICompositionBrush, ICompositionBrush_Vtbl, @@ -1792,6 +1856,16 @@ impl windows_core::RuntimeType for ICompositor2 { windows_core::imp::ConstBuffer::for_interface::(); } impl ICompositor2 { + pub(crate) fn CreateAnimationGroup(&self) -> windows_core::Result { + unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(self).CreateAnimationGroup)( + windows_core::Interface::as_raw(self), + &mut result__, + ) + .and_then(|| windows_core::Type::from_abi(result__)) + } + } pub(crate) fn CreateImplicitAnimationCollection( &self, ) -> windows_core::Result { @@ -1819,7 +1893,10 @@ impl ICompositor2 { pub struct ICompositor2_Vtbl { pub base__: windows_core::IInspectable_Vtbl, CreateAmbientLight: usize, - CreateAnimationGroup: usize, + pub CreateAnimationGroup: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, CreateBackdropBrush: usize, CreateDistantLight: usize, CreateDropShadow: usize, diff --git a/crates/libs/composition/src/bindings_lifted.rs b/crates/libs/composition/src/bindings_lifted.rs index ce222756290..a9860d84fd7 100644 --- a/crates/libs/composition/src/bindings_lifted.rs +++ b/crates/libs/composition/src/bindings_lifted.rs @@ -61,6 +61,38 @@ impl windows_core::RuntimeName for CompositionAnimation { unsafe impl Send for CompositionAnimation {} unsafe impl Sync for CompositionAnimation {} #[repr(transparent)] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CompositionAnimationGroup(windows_core::IUnknown); +windows_core::imp::interface_hierarchy!( + CompositionAnimationGroup, + windows_core::IUnknown, + windows_core::IInspectable +); +windows_core::imp::required_hierarchy!( + CompositionAnimationGroup, + ICompositionAnimationBase, + CompositionObject +); +impl windows_core::RuntimeType for CompositionAnimationGroup { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_class::(); +} +unsafe impl windows_core::Interface for CompositionAnimationGroup { + type Vtable = ::Vtable; + const IID: windows_core::GUID = ::IID; +} +impl core::ops::Deref for CompositionAnimationGroup { + type Target = ICompositionAnimationGroup; + fn deref(&self) -> &Self::Target { + unsafe { core::mem::transmute(self) } + } +} +impl windows_core::RuntimeName for CompositionAnimationGroup { + const NAME: &'static str = "Microsoft.UI.Composition.CompositionAnimationGroup"; +} +unsafe impl Send for CompositionAnimationGroup {} +unsafe impl Sync for CompositionAnimationGroup {} +#[repr(transparent)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct CompositionBatchTypes(pub u32); impl CompositionBatchTypes { @@ -645,6 +677,38 @@ impl ICompositionAnimationBase_Vtbl { pub struct ICompositionAnimationBase_Vtbl { pub base__: windows_core::IInspectable_Vtbl, } +windows_core::imp::define_interface!( + ICompositionAnimationGroup, + ICompositionAnimationGroup_Vtbl, + 0xa51cdcac_b972_5ae7_81d0_9d91c71ecb7a +); +impl windows_core::RuntimeType for ICompositionAnimationGroup { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +impl ICompositionAnimationGroup { + pub(crate) fn Add(&self, value: P0) -> windows_core::Result<()> + where + P0: windows_core::Param, + { + unsafe { + (windows_core::Interface::vtable(self).Add)( + windows_core::Interface::as_raw(self), + value.param().abi(), + ) + .ok() + } + } +} +#[repr(C)] +pub struct ICompositionAnimationGroup_Vtbl { + pub base__: windows_core::IInspectable_Vtbl, + Count: usize, + pub Add: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + ) -> windows_core::HRESULT, +} windows_core::imp::define_interface!( ICompositionBrush, ICompositionBrush_Vtbl, @@ -1211,6 +1275,16 @@ impl windows_core::RuntimeType for ICompositor2 { windows_core::imp::ConstBuffer::for_interface::(); } impl ICompositor2 { + pub(crate) fn CreateAnimationGroup(&self) -> windows_core::Result { + unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(self).CreateAnimationGroup)( + windows_core::Interface::as_raw(self), + &mut result__, + ) + .and_then(|| windows_core::Type::from_abi(result__)) + } + } pub(crate) fn CreateImplicitAnimationCollection( &self, ) -> windows_core::Result { @@ -1238,7 +1312,10 @@ impl ICompositor2 { pub struct ICompositor2_Vtbl { pub base__: windows_core::IInspectable_Vtbl, CreateAmbientLight: usize, - CreateAnimationGroup: usize, + pub CreateAnimationGroup: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, CreateBackdropBrush: usize, CreateDistantLight: usize, CreateDropShadow: usize, diff --git a/crates/libs/composition/src/compositor.rs b/crates/libs/composition/src/compositor.rs index 1e61c2925d4..2357f13069b 100644 --- a/crates/libs/composition/src/compositor.rs +++ b/crates/libs/composition/src/compositor.rs @@ -126,6 +126,12 @@ impl Compositor { Vector3KeyFrameAnimation(self.0.CreateVector3KeyFrameAnimation().unwrap()) } + /// Creates an empty group whose animations start together. + pub fn create_animation_group(&self) -> CompositionAnimationGroup { + let compositor: bindings::ICompositor2 = self.0.cast().unwrap(); + CompositionAnimationGroup(compositor.CreateAnimationGroup().unwrap()) + } + /// Creates a scalar (`f32`) key-frame animation. pub fn create_scalar_key_frame_animation(&self) -> ScalarKeyFrameAnimation { ScalarKeyFrameAnimation(self.0.CreateScalarKeyFrameAnimation().unwrap()) diff --git a/crates/libs/composition/src/lib.rs b/crates/libs/composition/src/lib.rs index 0500291b6d3..47aa3606d2e 100644 --- a/crates/libs/composition/src/lib.rs +++ b/crates/libs/composition/src/lib.rs @@ -59,8 +59,8 @@ pub(crate) use sealed::Sealed; pub(crate) use windows_core::Interface; pub use animation::{ - Animation, CompositionAnimation, CompositionEasingFunction, ImplicitAnimationCollection, - ScalarKeyFrameAnimation, Vector3KeyFrameAnimation, + Animation, CompositionAnimation, CompositionAnimationGroup, CompositionEasingFunction, + ImplicitAnimationCollection, ScalarKeyFrameAnimation, Vector3KeyFrameAnimation, }; pub use batch::{BatchKind, CompositionScopedBatch}; pub use brush::{Brush, CompositionBrush, CompositionColorBrush, CompositionNineGridBrush}; diff --git a/crates/libs/reactor/readme.md b/crates/libs/reactor/readme.md index ad86f933a8e..018e751de80 100644 --- a/crates/libs/reactor/readme.md +++ b/crates/libs/reactor/readme.md @@ -35,3 +35,60 @@ builders convert to `Element` with `.into()`. `cx.use_state` returns the current whose `call` schedules a rerender. `ReactorWindow` opens more top-level windows. See the [reactor guide](https://github.com/microsoft/windows-rs/blob/master/docs/crates/windows-reactor.md) for components, hooks, layout, styling, and widgets. + +WinUI lightweight styling resources use typed values: + +```rust,ignore +button("Delete").resource_overrides(|resources| { + resources + .set("ButtonBackground", Color::rgb(178, 34, 34)) + .set("ButtonBorderThemeThickness", Thickness::uniform(0.0)) + .set("ControlCornerRadius", CornerRadius::uniform(8.0)) +}) +``` + +Replacing or clearing the builder removes only the resource keys previously owned by Reactor. + +`PointerEventInfo` reports both element-local `x`/`y` coordinates and stable window-relative +`window_x`/`window_y` coordinates for drag calculations whose target moves during the gesture. +Use `.capture_pointer_on_press()` for drag handles that must keep receiving moves outside their +hit-test bounds, and clear drag state from capture-lost and canceled callbacks. + +`NavigationView` can keep pane state controlled and react to its actual responsive display mode: + +```rust,ignore +NavigationView::new(items, content) + .pane_open(pane_open) + .on_pane_open_changed(set_pane_open) + .pane_display_mode(NavigationViewPaneDisplayMode::Auto) + .on_display_mode_changed(set_display_mode) +``` + +The callbacks report settled WinUI dependency-property values rather than pane transition intent. + +Lifecycle transitions run when an element enters or leaves the WinUI visual tree: + +```rust,ignore +button("Animated").transition( + Some(AnimationConfig::fade_in(Duration::from_millis(200))), + Some(AnimationConfig::fade_out(Duration::from_millis(300))), +) +``` + +Reactor removes the logical element immediately while WinUI Composition keeps the departing visual +alive until its exit animation finishes. See the `exit_transition` sample. + +`TabItem::with_key` supplies the identity returned by `TabView::on_close_requested`. Key changes, +including removal, update the existing native item without leaving stale close-callback identity. +See the `tab_view_item_key` sample. + +Icon-taking controls share one `Icon` model: + +```rust,ignore +button("Confirm").icon(Icon::path("F1 M 0,8 L 6,14 L 16,2 L 14,0 L 6,10 L 2,6 Z")); +button("Mask").icon(Icon::bitmap_icon("ms-appx:///Assets/mask.png", true)); +button("Logo").icon(Icon::image("ms-appx:///Assets/logo.svg")); +``` + +`bitmap_icon` uses WinUI `BitmapIcon` and makes monochrome rendering explicit. `image` uses +full-color `ImageIcon` and accepts raster, SVG, or surface sources. diff --git a/crates/libs/reactor/src/backend/mod.rs b/crates/libs/reactor/src/backend/mod.rs index 389b852af38..6e3adf66efb 100644 --- a/crates/libs/reactor/src/backend/mod.rs +++ b/crates/libs/reactor/src/backend/mod.rs @@ -278,7 +278,7 @@ pub enum PropValue { secondary: Vec, }, SelectorBarItems(Vec), - Resources(HashMap), + Resources(HashMap), } #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] @@ -296,6 +296,8 @@ pub enum Event { Expanding, ItemClicked, ItemInvoked, + NavigationDisplayModeChanged, + NavigationPaneOpenChanged, PaneClosed, PaneToggleRequested, PasswordChanged, @@ -320,6 +322,7 @@ pub enum EventHandler { I32(Callback), Color(Callback<(u8, u8, u8, u8)>), DateTime(Callback), + NavigationDisplayMode(Callback), TimeSpan(Callback), } @@ -333,6 +336,9 @@ impl fmt::Debug for EventHandler { Self::I32(_) => f.write_str("EventHandler::I32(..)"), Self::Color(_) => f.write_str("EventHandler::Color(..)"), Self::DateTime(_) => f.write_str("EventHandler::DateTime(..)"), + Self::NavigationDisplayMode(_) => { + f.write_str("EventHandler::NavigationDisplayMode(..)") + } Self::TimeSpan(_) => f.write_str("EventHandler::TimeSpan(..)"), } } @@ -398,6 +404,15 @@ impl EventHandler { } } + pub fn invoke_navigation_display_mode(&self, mode: NavigationViewDisplayMode) { + match self { + Self::NavigationDisplayMode(cb) => cb.invoke(mode), + other => { + panic!("EventHandler::invoke_navigation_display_mode() called on {other:?}") + } + } + } + pub fn invoke_timespan(&self, ts: TimeSpan) { match self { Self::TimeSpan(cb) => cb.invoke(ts), @@ -483,6 +498,14 @@ pub trait Backend { fn run_property_animation(&mut self, _id: ControlId, _config: Option) {} + fn set_element_transitions( + &mut self, + _id: ControlId, + _enter: Option, + _exit: Option, + ) { + } + fn set_rich_text_paragraphs(&mut self, _id: ControlId, _paragraphs: &[RichTextParagraph]) {} fn attach_templated_realization( diff --git a/crates/libs/reactor/src/backend/winui/convert.rs b/crates/libs/reactor/src/backend/winui/convert.rs index 92ac6f3a775..556f1f57d49 100644 --- a/crates/libs/reactor/src/backend/winui/convert.rs +++ b/crates/libs/reactor/src/backend/winui/convert.rs @@ -68,8 +68,23 @@ pub(super) fn build_image_source(source: &ImageSource) -> Result String { + let mut escaped = String::with_capacity(value.len()); + for character in value.chars() { + match character { + '&' => escaped.push_str("&"), + '<' => escaped.push_str("<"), + '>' => escaped.push_str(">"), + '"' => escaped.push_str("""), + '\'' => escaped.push_str("'"), + _ => escaped.push(character), + } + } + escaped +} + /// Builds the WinUI `IconElement` for an [`Icon`], dispatching to the matching -/// concrete type: `SymbolIcon`, `ImageIcon`, or `FontIcon`. +/// concrete type. pub(super) fn build_icon_element(icon: &Icon) -> Result { match icon { Icon::Symbol(sym) => bindings::SymbolIcon::CreateInstanceWithSymbol(*sym)?.cast(), @@ -80,6 +95,15 @@ pub(super) fn build_icon_element(icon: &Icon) -> Result { } icon.cast() } + Icon::Bitmap { + uri, + show_as_monochrome, + } => { + let icon = bindings::BitmapIcon::new()?; + icon.SetUriSource(&bindings::Uri::CreateUri(uri)?)?; + icon.SetShowAsMonochrome(*show_as_monochrome)?; + icon.cast() + } Icon::Font { glyph, family } => { let font_icon = bindings::FontIcon::new()?; font_icon.SetGlyph(glyph)?; @@ -88,6 +112,14 @@ pub(super) fn build_icon_element(icon: &Icon) -> Result { } font_icon.cast() } + Icon::Path(data) => { + let data = escape_xml_attribute(data); + bindings::XamlReader::Load(&format!( + "" + ))? + .cast() + } } } @@ -243,7 +275,12 @@ pub(super) fn build_command_bar_element( #[cfg(test)] mod tests { - use super::is_svg_uri; + use super::{escape_xml_attribute, is_svg_uri}; + + #[test] + fn xml_attribute_escaping_covers_markup_delimiters() { + assert_eq!(escape_xml_attribute("&<>'\""), "&<>'""); + } #[test] fn classifies_svg_uri_by_final_extension() { diff --git a/crates/libs/reactor/src/backend/winui/mod.rs b/crates/libs/reactor/src/backend/winui/mod.rs index b7680626758..f42f700b3c2 100644 --- a/crates/libs/reactor/src/backend/winui/mod.rs +++ b/crates/libs/reactor/src/backend/winui/mod.rs @@ -1,6 +1,6 @@ use std::cell::RefCell; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use super::*; @@ -127,6 +127,7 @@ define_handles! { pub struct WinUIBackend { controls: RefCell>, event_revokers: RefCell>>, + property_observers: RefCell>, templated_selection_revokers: RefCell>, /// Per-list virtualization state for templated ListView/GridView/FlipView. templated: RefCell>, @@ -140,6 +141,7 @@ pub struct WinUIBackend { menu_click_handlers: RefCell>, command_bar_flyout_handlers: RefCell>, theme_brush_registry: RefCell>>, + resource_keys: RefCell>>, /// Per-host window state for window-level props. window_state: RefCell>>, next_id: RefCell, @@ -154,6 +156,24 @@ struct PointerRevokerSet { moved: Option, entered: Option, exited: Option, + capture_lost: Option, + canceled: Option, + capture_on_press: bool, +} + +struct PropertyObserver { + object: bindings::DependencyObject, + property: bindings::DependencyProperty, + token: i64, +} + +impl Drop for PropertyObserver { + fn drop(&mut self) { + diag::dropped( + self.object + .UnregisterPropertyChangedCallback(&self.property, self.token), + ); + } } #[derive(Default)] @@ -200,6 +220,7 @@ impl WinUIBackend { Self { controls: RefCell::new(FxHashMap::default()), event_revokers: RefCell::new(FxHashMap::default()), + property_observers: RefCell::new(FxHashMap::default()), templated_selection_revokers: RefCell::new(FxHashMap::default()), templated: RefCell::new(FxHashMap::default()), content_template: RefCell::new(None), @@ -209,6 +230,7 @@ impl WinUIBackend { menu_click_handlers: RefCell::new(FxHashMap::default()), command_bar_flyout_handlers: RefCell::new(FxHashMap::default()), theme_brush_registry: RefCell::new(FxHashMap::default()), + resource_keys: RefCell::new(FxHashMap::default()), window_state: RefCell::new(None), next_id: RefCell::new(0), } @@ -246,6 +268,110 @@ impl WinUIBackend { *counter += 1; ControlId::new(*counter) } + + fn observe_navigation_state( + &self, + id: ControlId, + event: Event, + navigation: &bindings::NavigationView, + handler: EventHandler, + ) -> Result<()> { + let property = match event { + Event::NavigationPaneOpenChanged => bindings::NavigationView::IsPaneOpenProperty()?, + Event::NavigationDisplayModeChanged => bindings::NavigationView::DisplayModeProperty()?, + _ => unreachable!(), + }; + let object = navigation.cast::()?; + let navigation = navigation.clone(); + let callback = bindings::DependencyPropertyChangedCallback::new( + move |_sender, _property| match event { + Event::NavigationPaneOpenChanged => match navigation.IsPaneOpen() { + Ok(open) => handler.invoke_bool(open), + Err(error) => diag::warn(format_args!( + "failed to read NavigationView.IsPaneOpen for {id}: {error:?}" + )), + }, + Event::NavigationDisplayModeChanged => match navigation.DisplayMode() { + Ok(mode) => handler.invoke_navigation_display_mode(mode), + Err(error) => diag::warn(format_args!( + "failed to read NavigationView.DisplayMode for {id}: {error:?}" + )), + }, + _ => unreachable!(), + }, + ); + let token = object.RegisterPropertyChangedCallback(&property, &callback)?; + self.property_observers.borrow_mut().insert( + (id, event), + PropertyObserver { + object, + property, + token, + }, + ); + Ok(()) + } + + fn set_resources( + &self, + id: ControlId, + handle: &Handle, + resources: &HashMap, + ) -> Result<()> { + let dictionary = handle.as_framework_element().Resources()?; + let map = dictionary.cast::>()?; + + let previous = self + .resource_keys + .borrow() + .get(&id) + .cloned() + .unwrap_or_default(); + for key in previous { + if resources.contains_key(&key) { + continue; + } + let key = windows_reference::IReference::from(key.as_str()); + if map.HasKey(&key)? { + map.Remove(&key)?; + } + } + + for (key, value) in resources { + let key = windows_reference::IReference::from(key.as_str()); + let value: windows_core::IInspectable = match value { + ResourceValue::String(value) => { + windows_reference::IReference::from(value.as_str()).cast()? + } + ResourceValue::SolidColorBrush(color) => solid_brush(*color)?.cast()?, + ResourceValue::F64(value) => windows_reference::IReference::from(*value).cast()?, + ResourceValue::Thickness(value) => { + windows_reference::IReference::from(*value).cast()? + } + ResourceValue::CornerRadius(value) => { + windows_reference::IReference::from(bindings::CornerRadius { + top_left: value.top_left, + top_right: value.top_right, + bottom_right: value.bottom_right, + bottom_left: value.bottom_left, + }) + .cast()? + } + }; + map.Insert(&key, &value)?; + } + + let mut resource_keys = self.resource_keys.borrow_mut(); + if resources.is_empty() { + resource_keys.remove(&id); + } else { + resource_keys.insert(id, resources.keys().cloned().collect::>()); + } + Ok(()) + } /// `ContentDialog` is tracked logically but not attached as a visual child. fn is_phantom_child(&self, id: ControlId) -> bool { matches!( @@ -687,6 +813,77 @@ fn run_property_animation_inner(ui: &bindings::UIElement, cfg: AnimationConfig) Ok(()) } +fn build_element_transition_animation( + ui: &bindings::UIElement, + cfg: AnimationConfig, + is_enter: bool, +) -> Result> { + if cfg.opacity.is_none() && cfg.scale.is_none() { + return Ok(None); + } + + let visual = element_visual(ui)?; + let compositor = visual.compositor(); + let easing = easing_for(&compositor, cfg.easing); + let group = compositor.create_animation_group(); + + if let Some(opacity) = cfg.opacity { + let animation = compositor.create_scalar_key_frame_animation(); + animation.set_duration(cfg.duration); + animation.set_target("Opacity"); + if is_enter { + animation.insert_key_frame_with_easing(0.0, 0.0, &easing); + } + animation.insert_key_frame_with_easing(1.0, opacity as f32, &easing); + group.add(&animation); + } + + if let Some(scale) = cfg.scale { + let z = visual.scale().z; + let animation = compositor.create_vector3_key_frame_animation(); + animation.set_duration(cfg.duration); + animation.set_target("Scale"); + if is_enter { + animation.insert_key_frame_with_easing( + 0.0, + windows_numerics::Vector3 { x: 0.0, y: 0.0, z }, + &easing, + ); + } + let scale = scale as f32; + animation.insert_key_frame_with_easing( + 1.0, + windows_numerics::Vector3 { + x: scale, + y: scale, + z, + }, + &easing, + ); + group.add(&animation); + } + + Ok(Some(group.as_host().cast()?)) +} + +fn apply_element_transitions( + ui: &bindings::UIElement, + enter: Option, + exit: Option, +) -> Result<()> { + let enter = enter + .map(|config| build_element_transition_animation(ui, config, true)) + .transpose()? + .flatten(); + let exit = exit + .map(|config| build_element_transition_animation(ui, config, false)) + .transpose()? + .flatten(); + + bindings::ElementCompositionPreview::SetImplicitShowAnimation(ui, enter.as_ref())?; + bindings::ElementCompositionPreview::SetImplicitHideAnimation(ui, exit.as_ref()) +} + /// Handles props shared by base-class interfaces. fn try_universal_prop(handle: &Handle, prop: Prop, value: &PropValue) -> Result { match (prop, value) { @@ -810,20 +1007,6 @@ fn try_universal_prop(handle: &Handle, prop: Prop, value: &PropValue) -> Result< .SetIsEnabled(true)?; Ok(true) } - (Prop::Resources, PropValue::Resources(map)) => { - let rd = handle.as_framework_element().Resources()?; - let imap = - rd.cast::>()?; - for (k, v) in map { - let key = windows_reference::IReference::from(k.as_str()); - let val = windows_reference::IReference::from(v.as_str()); - imap.Insert(&key, &val)?; - } - Ok(true) - } (Prop::AttachedGridRow, PropValue::I32(v)) => { bindings::Grid::SetRow(&handle.as_framework_element(), *v)?; Ok(true) @@ -1103,6 +1286,10 @@ impl Backend for WinUIBackend { if generated_set_prop::dispatch(handle, prop, value)? { return Ok(()); } + if let (Prop::Resources, PropValue::Resources(resources)) = (prop, value) { + self.set_resources(id, handle, resources)?; + return Ok(()); + } if try_universal_prop(handle, prop, value)? { return Ok(()); } @@ -1357,6 +1544,9 @@ impl Backend for WinUIBackend { let tag = windows_reference::IReference::from(s.as_str()); ti.cast::()?.SetTag(&tag) } + (Prop::ItemKey, PropValue::Unset, Handle::TabViewItem(ti)) => { + ti.cast::()?.SetTag(None) + } (Prop::MenuItems, PropValue::NavMenuItems(items), Handle::NavigationView(nv)) => { let menu = nv.MenuItems()?; menu.Clear()?; @@ -2198,12 +2388,22 @@ impl Backend for WinUIBackend { fn destroy(&mut self, id: ControlId) { self.templated_selection_revokers.borrow_mut().remove(&id); self.templated.borrow_mut().remove(&id); - self.pointer_revokers.borrow_mut().remove(&id); + let captured = self + .pointer_revokers + .borrow_mut() + .remove(&id) + .is_some_and(|tokens| tokens.capture_on_press); + if captured && let Some(handle) = self.controls.borrow().get(&id) { + diag::dropped(handle.as_ui_element().ReleasePointerCaptures()); + } self.drag_revokers.borrow_mut().remove(&id); self.controls.borrow_mut().remove(&id); self.event_revokers .borrow_mut() .retain(|(hid, _), _| *hid != id); + self.property_observers + .borrow_mut() + .retain(|(hid, _), _| *hid != id); let mut kids = self.parent_children.borrow_mut(); kids.remove(&id); for list in kids.values_mut() { @@ -2212,6 +2412,7 @@ impl Backend for WinUIBackend { self.menu_click_handlers.borrow_mut().remove(&id); self.command_bar_flyout_handlers.borrow_mut().remove(&id); self.theme_brush_registry.borrow_mut().remove(&id); + self.resource_keys.borrow_mut().remove(&id); } fn attach_event(&mut self, id: ControlId, event: Event, handler: EventHandler) { let map = self.controls.borrow(); @@ -2219,6 +2420,21 @@ impl Backend for WinUIBackend { .get(&id) .unwrap_or_else(|| panic!("WinUIBackend::attach_event: unknown control {id}")); + if matches!( + event, + Event::NavigationPaneOpenChanged | Event::NavigationDisplayModeChanged + ) && let Handle::NavigationView(navigation) = handle + { + self.observe_navigation_state(id, event, navigation, handler) + .unwrap_or_else(|error| { + panic!( + "WinUIBackend::attach_event: failed to observe {event:?} \ + for control {id}: {error}" + ) + }); + return; + } + if let Some(revs) = generated_attach_event::dispatch(handle, event, &handler) { if !revs.is_empty() { self.event_revokers.borrow_mut().insert((id, event), revs); @@ -2591,6 +2807,7 @@ impl Backend for WinUIBackend { } fn detach_event(&mut self, id: ControlId, event: Event) { self.event_revokers.borrow_mut().remove(&(id, event)); + self.property_observers.borrow_mut().remove(&(id, event)); } fn set_theme_bindings( &mut self, @@ -2734,6 +2951,21 @@ impl Backend for WinUIBackend { diag::warn(format_args!("run_property_animation failed: {e:?}")); } } + fn set_element_transitions( + &mut self, + id: ControlId, + enter: Option, + exit: Option, + ) { + let map = self.controls.borrow(); + let Some(handle) = map.get(&id) else { + return; + }; + let ui = handle.as_ui_element(); + if let Err(error) = apply_element_transitions(&ui, enter, exit) { + diag::warn(format_args!("set_element_transitions failed: {error:?}")); + } + } fn set_rich_text_paragraphs(&mut self, id: ControlId, paragraphs: &[RichTextParagraph]) { let map = self.controls.borrow(); let Some(handle) = map.get(&id) else { @@ -2844,19 +3076,29 @@ impl Backend for WinUIBackend { } fn set_pointer_handlers(&mut self, id: ControlId, handlers: Option<&PointerHandlers>) { - // Replacing handlers must drop old event tokens first. + // Remove the old token set from backend ownership before replacing it. let prev = self.pointer_revokers.borrow_mut().remove(&id); let map = self.controls.borrow(); let Some(handle) = map.get(&id) else { return; }; let ui = handle.as_ui_element(); + let previous_capture = prev.as_ref().is_some_and(|tokens| tokens.capture_on_press); + let next_capture = handlers.is_some_and(|handlers| handlers.capture_pointer_on_press); + if previous_capture && !next_capture { + // Keep the previous capture-lost callback attached while ending + // an active gesture. + diag::dropped(ui.ReleasePointerCaptures()); + } drop(prev); let Some(handlers) = handlers else { return; }; - let mut tokens = PointerRevokerSet::default(); + let mut tokens = PointerRevokerSet { + capture_on_press: handlers.capture_pointer_on_press, + ..PointerRevokerSet::default() + }; if let Some(cb) = handlers.on_tapped.clone() { tokens.tapped = ui @@ -2874,22 +3116,54 @@ impl Backend for WinUIBackend { .ok(); } - if let Some(cb) = handlers.on_pointer_pressed.clone() { + if handlers.on_pointer_pressed.is_some() || handlers.capture_pointer_on_press { let element = ui.clone(); + let cb = handlers.on_pointer_pressed.clone(); + let capture = handlers.capture_pointer_on_press; tokens.pressed = ui .PointerPressed(move |_sender, args| { - let info = pointer_event_info(&element, args); - cb.invoke(info); + let capture_succeeded = if capture { + args.as_ref() + .and_then(|args| args.Pointer().ok()) + .is_some_and(|pointer| match element.CapturePointer(&pointer) { + Ok(true) => true, + Ok(false) => { + diag::warn(format_args!("pointer capture was refused")); + false + } + Err(error) => { + diag::warn(format_args!("pointer capture failed: {error:?}")); + false + } + }) + } else { + false + }; + if let Some(cb) = &cb { + let mut info = pointer_event_info(&element, args); + info.capture_succeeded = capture_succeeded; + cb.invoke(info); + } }) .ok(); } - if let Some(cb) = handlers.on_pointer_released.clone() { + if handlers.on_pointer_released.is_some() || handlers.capture_pointer_on_press { let element = ui.clone(); + let cb = handlers.on_pointer_released.clone(); + let capture = handlers.capture_pointer_on_press; tokens.released = ui .PointerReleased(move |_sender, args| { + let pointer = capture + .then(|| args.as_ref().and_then(|args| args.Pointer().ok())) + .flatten(); let info = pointer_event_info(&element, args); - cb.invoke(info); + if let Some(pointer) = pointer { + diag::dropped(element.ReleasePointerCapture(&pointer)); + } + if let Some(cb) = &cb { + cb.invoke(info); + } }) .ok(); } @@ -2922,6 +3196,22 @@ impl Backend for WinUIBackend { .ok(); } + if let Some(cb) = handlers.on_pointer_capture_lost.clone() { + tokens.capture_lost = ui + .PointerCaptureLost(move |_sender, _args| { + cb.invoke(()); + }) + .ok(); + } + + if let Some(cb) = handlers.on_pointer_canceled.clone() { + tokens.canceled = ui + .PointerCanceled(move |_sender, _args| { + cb.invoke(()); + }) + .ok(); + } + self.pointer_revokers.borrow_mut().insert(id, tokens); } @@ -3320,13 +3610,11 @@ fn accept_or_reject(cb: &C, args: Option<&bindings::DragEventArgs } } -/// Extract the pointer position and button state for a `PointerPressed` / -/// `PointerReleased` callback; falls back to defaults on any null hop. +/// Extract local/window pointer positions and button state for a pointer callback. /// /// `element` is captured once at attach time (the handler's own element), so /// there is no per-event `QueryInterface`: the arg/point/properties classes -/// each `Deref` to their default interface, and the coordinates are read -/// relative to `element` directly. +/// each `Deref` to their default interface. fn pointer_event_info( element: &bindings::UIElement, args: windows_core::InRef<'_, bindings::PointerRoutedEventArgs>, @@ -3335,19 +3623,26 @@ fn pointer_event_info( let Some(args) = args.as_ref() else { return info; }; - let Ok(point) = args.GetCurrentPoint(element) else { - return info; - }; - if let Ok(pos) = point.Position() { - info.x = pos.x as f64; - info.y = pos.y as f64; + + if let Ok(point) = args.GetCurrentPoint(element) { + if let Ok(pos) = point.Position() { + info.x = pos.x as f64; + info.y = pos.y as f64; + } + if let Ok(props) = point.Properties() { + info.is_left_button_pressed = props.IsLeftButtonPressed().unwrap_or(false); + info.is_right_button_pressed = props.IsRightButtonPressed().unwrap_or(false); + info.is_middle_button_pressed = props.IsMiddleButtonPressed().unwrap_or(false); + } } - let Ok(props) = point.Properties() else { - return info; - }; - info.is_left_button_pressed = props.IsLeftButtonPressed().unwrap_or(false); - info.is_right_button_pressed = props.IsRightButtonPressed().unwrap_or(false); - info.is_middle_button_pressed = props.IsMiddleButtonPressed().unwrap_or(false); + + if let Ok(point) = args.GetCurrentPoint(None::<&bindings::UIElement>) + && let Ok(pos) = point.Position() + { + info.window_x = pos.x as f64; + info.window_y = pos.y as f64; + } + info } diff --git a/crates/libs/reactor/src/bindings.rs b/crates/libs/reactor/src/bindings.rs index 7ab153292d0..593931edd3f 100644 --- a/crates/libs/reactor/src/bindings.rs +++ b/crates/libs/reactor/src/bindings.rs @@ -802,6 +802,61 @@ unsafe impl Send for AutomationProperties {} unsafe impl Sync for AutomationProperties {} #[repr(transparent)] #[derive(Clone, Debug, Eq, PartialEq)] +pub struct BitmapIcon(windows_core::IUnknown); +windows_core::imp::interface_hierarchy!( + BitmapIcon, + windows_core::IUnknown, + windows_core::IInspectable +); +windows_core::imp::required_hierarchy!( + BitmapIcon, + IconElement, + FrameworkElement, + UIElement, + DependencyObject +); +impl BitmapIcon { + pub(crate) fn new() -> windows_core::Result { + Self::IBitmapIconFactory(|this| unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(this).CreateInstance)( + windows_core::Interface::as_raw(this), + core::ptr::null_mut(), + core::ptr::null_mut(), + &mut result__, + ) + .and_then(|| windows_core::Type::from_abi(result__)) + }) + } + fn IBitmapIconFactory windows_core::Result>( + callback: F, + ) -> windows_core::Result { + static SHARED: windows_core::imp::FactoryCache = + windows_core::imp::FactoryCache::new(); + SHARED.call(callback) + } +} +impl windows_core::RuntimeType for BitmapIcon { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_class::(); +} +unsafe impl windows_core::Interface for BitmapIcon { + type Vtable = ::Vtable; + const IID: windows_core::GUID = ::IID; +} +impl core::ops::Deref for BitmapIcon { + type Target = IBitmapIcon; + fn deref(&self) -> &Self::Target { + unsafe { core::mem::transmute(self) } + } +} +impl windows_core::RuntimeName for BitmapIcon { + const NAME: &'static str = "Microsoft.UI.Xaml.Controls.BitmapIcon"; +} +unsafe impl Send for BitmapIcon {} +unsafe impl Sync for BitmapIcon {} +#[repr(transparent)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct BitmapImage(windows_core::IUnknown); windows_core::imp::interface_hierarchy!( BitmapImage, @@ -2374,6 +2429,100 @@ unsafe impl Send for DependencyObject {} unsafe impl Sync for DependencyObject {} #[repr(transparent)] #[derive(Clone, Debug, Eq, PartialEq)] +pub struct DependencyProperty(windows_core::IUnknown); +windows_core::imp::interface_hierarchy!( + DependencyProperty, + windows_core::IUnknown, + windows_core::IInspectable +); +impl windows_core::RuntimeType for DependencyProperty { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_class::(); +} +unsafe impl windows_core::Interface for DependencyProperty { + type Vtable = ::Vtable; + const IID: windows_core::GUID = ::IID; +} +impl core::ops::Deref for DependencyProperty { + type Target = IDependencyProperty; + fn deref(&self) -> &Self::Target { + unsafe { core::mem::transmute(self) } + } +} +impl windows_core::RuntimeName for DependencyProperty { + const NAME: &'static str = "Microsoft.UI.Xaml.DependencyProperty"; +} +unsafe impl Send for DependencyProperty {} +unsafe impl Sync for DependencyProperty {} +windows_core::imp::define_interface!( + DependencyPropertyChangedCallback, + DependencyPropertyChangedCallback_Vtbl, + 0xf055bb21_219b_5b0c_805d_bcaedae15458 +); +impl windows_core::RuntimeType for DependencyPropertyChangedCallback { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +impl DependencyPropertyChangedCallback { + pub(crate) fn new< + F: Fn(windows_core::Ref, windows_core::Ref) + 'static, + >( + invoke: F, + ) -> Self { + let com = windows_core::imp::DelegateBox::::new( + &DependencyPropertyChangedCallbackBox::::VTABLE, + invoke, + ); + unsafe { core::mem::transmute(windows_core::imp::box_new(com)) } + } +} +#[repr(C)] +pub struct DependencyPropertyChangedCallback_Vtbl { + base__: windows_core::IUnknown_Vtbl, + Invoke: unsafe extern "system" fn( + this: *mut core::ffi::c_void, + sender: *mut core::ffi::c_void, + dp: *mut core::ffi::c_void, + ) -> windows_core::HRESULT, +} +struct DependencyPropertyChangedCallbackBox< + F: Fn(windows_core::Ref, windows_core::Ref) + 'static, +>(core::marker::PhantomData<(fn() -> F,)>); +impl, windows_core::Ref) + 'static> + DependencyPropertyChangedCallbackBox +{ + const VTABLE: DependencyPropertyChangedCallback_Vtbl = DependencyPropertyChangedCallback_Vtbl { + base__: + windows_core::IUnknown_Vtbl { + QueryInterface: windows_core::imp::DelegateBox::< + DependencyPropertyChangedCallback, + F, + >::QueryInterface, + AddRef: + windows_core::imp::DelegateBox::::AddRef, + Release: + windows_core::imp::DelegateBox::::Release, + }, + Invoke: Self::Invoke, + }; + unsafe extern "system" fn Invoke( + this: *mut core::ffi::c_void, + sender: *mut core::ffi::c_void, + dp: *mut core::ffi::c_void, + ) -> windows_core::HRESULT { + unsafe { + let this = &mut *(this as *mut *mut core::ffi::c_void + as *mut windows_core::imp::DelegateBox); + (this.invoke)( + core::mem::transmute_copy(&sender), + core::mem::transmute_copy(&dp), + ); + windows_core::HRESULT(0) + } + } +} +#[repr(transparent)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct DesktopAcrylicBackdrop(windows_core::IUnknown); windows_core::imp::interface_hierarchy!( DesktopAcrylicBackdrop, @@ -2815,6 +2964,40 @@ impl ElementCompositionPreview { .ok() }) } + pub(crate) fn SetImplicitShowAnimation( + element: P0, + animation: P1, + ) -> windows_core::Result<()> + where + P0: windows_core::Param, + P1: windows_core::Param, + { + Self::IElementCompositionPreviewStatics(|this| unsafe { + (windows_core::Interface::vtable(this).SetImplicitShowAnimation)( + windows_core::Interface::as_raw(this), + element.param().abi(), + animation.param().abi(), + ) + .ok() + }) + } + pub(crate) fn SetImplicitHideAnimation( + element: P0, + animation: P1, + ) -> windows_core::Result<()> + where + P0: windows_core::Param, + P1: windows_core::Param, + { + Self::IElementCompositionPreviewStatics(|this| unsafe { + (windows_core::Interface::vtable(this).SetImplicitHideAnimation)( + windows_core::Interface::as_raw(this), + element.param().abi(), + animation.param().abi(), + ) + .ok() + }) + } fn IElementCompositionPreviewStatics< R, F: FnOnce(&IElementCompositionPreviewStatics) -> windows_core::Result, @@ -4718,6 +4901,69 @@ pub struct IAutomationPropertiesStatics_Vtbl { AutomationHeadingLevel, ) -> windows_core::HRESULT, } +windows_core::imp::define_interface!( + IBitmapIcon, + IBitmapIcon_Vtbl, + 0xc370bc29_805b_5bad_b615_ec640e579dbb +); +impl windows_core::RuntimeType for IBitmapIcon { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +impl IBitmapIcon { + pub(crate) fn SetUriSource(&self, value: P0) -> windows_core::Result<()> + where + P0: windows_core::Param, + { + unsafe { + (windows_core::Interface::vtable(self).SetUriSource)( + windows_core::Interface::as_raw(self), + value.param().abi(), + ) + .ok() + } + } + pub(crate) fn SetShowAsMonochrome(&self, value: bool) -> windows_core::Result<()> { + unsafe { + (windows_core::Interface::vtable(self).SetShowAsMonochrome)( + windows_core::Interface::as_raw(self), + value, + ) + .ok() + } + } +} +#[repr(C)] +pub struct IBitmapIcon_Vtbl { + pub base__: windows_core::IInspectable_Vtbl, + UriSource: usize, + pub SetUriSource: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + ) -> windows_core::HRESULT, + ShowAsMonochrome: usize, + pub SetShowAsMonochrome: + unsafe extern "system" fn(*mut core::ffi::c_void, bool) -> windows_core::HRESULT, +} +windows_core::imp::define_interface!( + IBitmapIconFactory, + IBitmapIconFactory_Vtbl, + 0xb43b5ddc_cdb5_5ad6_8ac1_2fcca33be39e +); +impl windows_core::RuntimeType for IBitmapIconFactory { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +#[repr(C)] +pub struct IBitmapIconFactory_Vtbl { + pub base__: windows_core::IInspectable_Vtbl, + pub CreateInstance: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, +} windows_core::imp::define_interface!( IBitmapImage, IBitmapImage_Vtbl, @@ -6155,6 +6401,24 @@ pub struct ICommandBarFlyoutFactory_Vtbl { *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT, } +windows_core::imp::define_interface!( + ICompositionAnimationBase, + ICompositionAnimationBase_Vtbl, + 0xa77c0e5a_f059_4e85_bcef_c068694cec78 +); +impl windows_core::RuntimeType for ICompositionAnimationBase { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +windows_core::imp::interface_hierarchy!( + ICompositionAnimationBase, + windows_core::IUnknown, + windows_core::IInspectable +); +#[repr(C)] +pub struct ICompositionAnimationBase_Vtbl { + pub base__: windows_core::IInspectable_Vtbl, +} windows_core::imp::define_interface!( ICompositionObject, ICompositionObject_Vtbl, @@ -7015,9 +7279,77 @@ impl windows_core::RuntimeType for IDependencyObject { const SIGNATURE: windows_core::imp::ConstBuffer = windows_core::imp::ConstBuffer::for_interface::(); } +impl IDependencyObject { + pub(crate) fn RegisterPropertyChangedCallback( + &self, + dp: P0, + callback: P1, + ) -> windows_core::Result + where + P0: windows_core::Param, + P1: windows_core::Param, + { + unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(self).RegisterPropertyChangedCallback)( + windows_core::Interface::as_raw(self), + dp.param().abi(), + callback.param().abi(), + &mut result__, + ) + .map(|| result__) + } + } + pub(crate) fn UnregisterPropertyChangedCallback( + &self, + dp: P0, + token: i64, + ) -> windows_core::Result<()> + where + P0: windows_core::Param, + { + unsafe { + (windows_core::Interface::vtable(self).UnregisterPropertyChangedCallback)( + windows_core::Interface::as_raw(self), + dp.param().abi(), + token, + ) + .ok() + } + } +} #[repr(C)] pub struct IDependencyObject_Vtbl { pub base__: windows_core::IInspectable_Vtbl, + GetValue: usize, + SetValue: usize, + ClearValue: usize, + ReadLocalValue: usize, + GetAnimationBaseValue: usize, + pub RegisterPropertyChangedCallback: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + *mut core::ffi::c_void, + *mut i64, + ) -> windows_core::HRESULT, + pub UnregisterPropertyChangedCallback: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + i64, + ) -> windows_core::HRESULT, +} +windows_core::imp::define_interface!( + IDependencyProperty, + IDependencyProperty_Vtbl, + 0x960eab49_9672_58a0_995b_3a42e5ea6278 +); +impl windows_core::RuntimeType for IDependencyProperty { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +#[repr(C)] +pub struct IDependencyProperty_Vtbl { + pub base__: windows_core::IInspectable_Vtbl, } windows_core::imp::define_interface!( IDesktopAcrylicBackdrop, @@ -7448,6 +7780,17 @@ pub struct IElementCompositionPreviewStatics_Vtbl { *mut core::ffi::c_void, *mut core::ffi::c_void, ) -> windows_core::HRESULT, + GetScrollViewerManipulationPropertySet: usize, + pub SetImplicitShowAnimation: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + *mut core::ffi::c_void, + ) -> windows_core::HRESULT, + pub SetImplicitHideAnimation: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + *mut core::ffi::c_void, + ) -> windows_core::HRESULT, } windows_core::imp::define_interface!( IEllipse, @@ -9778,6 +10121,16 @@ impl windows_core::RuntimeType for INavigationView { windows_core::imp::ConstBuffer::for_interface::(); } impl INavigationView { + pub(crate) fn IsPaneOpen(&self) -> windows_core::Result { + unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(self).IsPaneOpen)( + windows_core::Interface::as_raw(self), + &mut result__, + ) + .map(|| result__) + } + } pub(crate) fn SetIsPaneOpen(&self, value: bool) -> windows_core::Result<()> { unsafe { (windows_core::Interface::vtable(self).SetIsPaneOpen)( @@ -9811,6 +10164,16 @@ impl INavigationView { .ok() } } + pub(crate) fn DisplayMode(&self) -> windows_core::Result { + unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(self).DisplayMode)( + windows_core::Interface::as_raw(self), + &mut result__, + ) + .map(|| result__) + } + } pub(crate) fn SetIsSettingsVisible(&self, value: bool) -> windows_core::Result<()> { unsafe { (windows_core::Interface::vtable(self).SetIsSettingsVisible)( @@ -9928,7 +10291,8 @@ impl INavigationView { #[repr(C)] pub struct INavigationView_Vtbl { pub base__: windows_core::IInspectable_Vtbl, - IsPaneOpen: usize, + pub IsPaneOpen: + unsafe extern "system" fn(*mut core::ffi::c_void, *mut bool) -> windows_core::HRESULT, pub SetIsPaneOpen: unsafe extern "system" fn(*mut core::ffi::c_void, bool) -> windows_core::HRESULT, CompactModeThresholdWidth: usize, @@ -9950,7 +10314,10 @@ pub struct INavigationView_Vtbl { ) -> windows_core::HRESULT, HeaderTemplate: usize, SetHeaderTemplate: usize, - DisplayMode: usize, + pub DisplayMode: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut NavigationViewDisplayMode, + ) -> windows_core::HRESULT, IsSettingsVisible: usize, pub SetIsSettingsVisible: unsafe extern "system" fn(*mut core::ffi::c_void, bool) -> windows_core::HRESULT, @@ -10319,6 +10686,34 @@ pub struct INavigationViewSelectionChangedEventArgs_Vtbl { *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT, } +windows_core::imp::define_interface!( + INavigationViewStatics, + INavigationViewStatics_Vtbl, + 0xdcd04caf_1904_564b_b0de_babaff9962f5 +); +impl windows_core::RuntimeType for INavigationViewStatics { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +#[repr(C)] +pub struct INavigationViewStatics_Vtbl { + pub base__: windows_core::IInspectable_Vtbl, + pub IsPaneOpenProperty: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, + CompactModeThresholdWidthProperty: usize, + ExpandedModeThresholdWidthProperty: usize, + FooterMenuItemsProperty: usize, + FooterMenuItemsSourceProperty: usize, + PaneFooterProperty: usize, + HeaderProperty: usize, + HeaderTemplateProperty: usize, + pub DisplayModeProperty: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, +} windows_core::imp::define_interface!( INumberBox, INumberBox_Vtbl, @@ -11051,6 +11446,19 @@ pub struct IPivotItemFactory_Vtbl { *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT, } +windows_core::imp::define_interface!( + IPointer, + IPointer_Vtbl, + 0x1f9afbf5_11a3_5e68_aa1b_72febfa0ab23 +); +impl windows_core::RuntimeType for IPointer { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +#[repr(C)] +pub struct IPointer_Vtbl { + pub base__: windows_core::IInspectable_Vtbl, +} windows_core::imp::define_interface!( IPointerPoint, IPointerPoint_Vtbl, @@ -11165,6 +11573,16 @@ impl windows_core::RuntimeType for IPointerRoutedEventArgs { windows_core::imp::ConstBuffer::for_interface::(); } impl IPointerRoutedEventArgs { + pub(crate) fn Pointer(&self) -> windows_core::Result { + unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(self).Pointer)( + windows_core::Interface::as_raw(self), + &mut result__, + ) + .and_then(|| windows_core::Type::from_abi(result__)) + } + } pub(crate) fn GetCurrentPoint(&self, relativeto: P0) -> windows_core::Result where P0: windows_core::Param, @@ -11183,7 +11601,10 @@ impl IPointerRoutedEventArgs { #[repr(C)] pub struct IPointerRoutedEventArgs_Vtbl { pub base__: windows_core::IInspectable_Vtbl, - Pointer: usize, + pub Pointer: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, KeyModifiers: usize, Handled: usize, SetHandled: usize, @@ -16202,6 +16623,70 @@ impl IUIElement { )) } } + pub(crate) fn PointerCaptureLost( + &self, + handler: F, + ) -> windows_core::Result + where + F: Fn( + windows_core::Ref, + windows_core::Ref, + ) + 'static, + { + let handler: PointerEventHandler = { + let com = windows_core::imp::DelegateBox::::new( + &PointerEventHandlerBox::::VTABLE, + handler, + ); + unsafe { core::mem::transmute(windows_core::imp::box_new(com)) } + }; + unsafe { + let mut result__ = core::mem::zeroed(); + let token__ = (windows_core::Interface::vtable(self).PointerCaptureLost)( + windows_core::Interface::as_raw(self), + windows_core::Interface::as_raw(&handler), + &mut result__, + ) + .map(|| result__)?; + Ok(windows_core::EventRevoker::new( + self.clone(), + token__, + windows_core::Interface::vtable(self).RemovePointerCaptureLost, + )) + } + } + pub(crate) fn PointerCanceled( + &self, + handler: F, + ) -> windows_core::Result + where + F: Fn( + windows_core::Ref, + windows_core::Ref, + ) + 'static, + { + let handler: PointerEventHandler = { + let com = windows_core::imp::DelegateBox::::new( + &PointerEventHandlerBox::::VTABLE, + handler, + ); + unsafe { core::mem::transmute(windows_core::imp::box_new(com)) } + }; + unsafe { + let mut result__ = core::mem::zeroed(); + let token__ = (windows_core::Interface::vtable(self).PointerCanceled)( + windows_core::Interface::as_raw(self), + windows_core::Interface::as_raw(&handler), + &mut result__, + ) + .map(|| result__)?; + Ok(windows_core::EventRevoker::new( + self.clone(), + token__, + windows_core::Interface::vtable(self).RemovePointerCanceled, + )) + } + } pub(crate) fn Tapped(&self, handler: F) -> windows_core::Result where F: Fn( @@ -16263,6 +16748,40 @@ impl IUIElement { )) } } + pub(crate) fn CapturePointer(&self, value: P0) -> windows_core::Result + where + P0: windows_core::Param, + { + unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(self).CapturePointer)( + windows_core::Interface::as_raw(self), + value.param().abi(), + &mut result__, + ) + .map(|| result__) + } + } + pub(crate) fn ReleasePointerCapture(&self, value: P0) -> windows_core::Result<()> + where + P0: windows_core::Param, + { + unsafe { + (windows_core::Interface::vtable(self).ReleasePointerCapture)( + windows_core::Interface::as_raw(self), + value.param().abi(), + ) + .ok() + } + } + pub(crate) fn ReleasePointerCaptures(&self) -> windows_core::Result<()> { + unsafe { + (windows_core::Interface::vtable(self).ReleasePointerCaptures)( + windows_core::Interface::as_raw(self), + ) + .ok() + } + } } #[repr(C)] pub struct IUIElement_Vtbl { @@ -16481,10 +17000,20 @@ pub struct IUIElement_Vtbl { ) -> windows_core::HRESULT, pub RemovePointerExited: unsafe extern "system" fn(*mut core::ffi::c_void, i64) -> windows_core::HRESULT, - PointerCaptureLost: usize, - RemovePointerCaptureLost: usize, - PointerCanceled: usize, - RemovePointerCanceled: usize, + pub PointerCaptureLost: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + *mut i64, + ) -> windows_core::HRESULT, + pub RemovePointerCaptureLost: + unsafe extern "system" fn(*mut core::ffi::c_void, i64) -> windows_core::HRESULT, + pub PointerCanceled: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + *mut i64, + ) -> windows_core::HRESULT, + pub RemovePointerCanceled: + unsafe extern "system" fn(*mut core::ffi::c_void, i64) -> windows_core::HRESULT, PointerWheelChanged: usize, RemovePointerWheelChanged: usize, pub Tapped: unsafe extern "system" fn( @@ -16509,6 +17038,49 @@ pub struct IUIElement_Vtbl { ) -> windows_core::HRESULT, pub RemoveRightTapped: unsafe extern "system" fn(*mut core::ffi::c_void, i64) -> windows_core::HRESULT, + ManipulationStarting: usize, + RemoveManipulationStarting: usize, + ManipulationInertiaStarting: usize, + RemoveManipulationInertiaStarting: usize, + ManipulationStarted: usize, + RemoveManipulationStarted: usize, + ManipulationDelta: usize, + RemoveManipulationDelta: usize, + ManipulationCompleted: usize, + RemoveManipulationCompleted: usize, + AccessKeyDisplayRequested: usize, + RemoveAccessKeyDisplayRequested: usize, + AccessKeyDisplayDismissed: usize, + RemoveAccessKeyDisplayDismissed: usize, + AccessKeyInvoked: usize, + RemoveAccessKeyInvoked: usize, + ProcessKeyboardAccelerators: usize, + RemoveProcessKeyboardAccelerators: usize, + GettingFocus: usize, + RemoveGettingFocus: usize, + LosingFocus: usize, + RemoveLosingFocus: usize, + NoFocusCandidateFound: usize, + RemoveNoFocusCandidateFound: usize, + PreviewKeyDown: usize, + RemovePreviewKeyDown: usize, + PreviewKeyUp: usize, + RemovePreviewKeyUp: usize, + BringIntoViewRequested: usize, + RemoveBringIntoViewRequested: usize, + Measure: usize, + Arrange: usize, + pub CapturePointer: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + *mut bool, + ) -> windows_core::HRESULT, + pub ReleasePointerCapture: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + ) -> windows_core::HRESULT, + pub ReleasePointerCaptures: + unsafe extern "system" fn(*mut core::ffi::c_void) -> windows_core::HRESULT, } windows_core::imp::define_interface!( IUriRuntimeClass, @@ -18483,6 +19055,26 @@ impl NavigationView { .and_then(|| windows_core::Type::from_abi(result__)) }) } + pub(crate) fn IsPaneOpenProperty() -> windows_core::Result { + Self::INavigationViewStatics(|this| unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(this).IsPaneOpenProperty)( + windows_core::Interface::as_raw(this), + &mut result__, + ) + .and_then(|| windows_core::Type::from_abi(result__)) + }) + } + pub(crate) fn DisplayModeProperty() -> windows_core::Result { + Self::INavigationViewStatics(|this| unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(this).DisplayModeProperty)( + windows_core::Interface::as_raw(this), + &mut result__, + ) + .and_then(|| windows_core::Type::from_abi(result__)) + }) + } fn INavigationViewFactory windows_core::Result>( callback: F, ) -> windows_core::Result { @@ -18490,6 +19082,13 @@ impl NavigationView { windows_core::imp::FactoryCache::new(); SHARED.call(callback) } + fn INavigationViewStatics windows_core::Result>( + callback: F, + ) -> windows_core::Result { + static SHARED: windows_core::imp::FactoryCache = + windows_core::imp::FactoryCache::new(); + SHARED.call(callback) + } } impl windows_core::RuntimeType for NavigationView { const SIGNATURE: windows_core::imp::ConstBuffer = @@ -18554,6 +19153,22 @@ impl windows_core::RuntimeName for NavigationViewBackRequestedEventArgs { unsafe impl Send for NavigationViewBackRequestedEventArgs {} unsafe impl Sync for NavigationViewBackRequestedEventArgs {} #[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct NavigationViewDisplayMode(pub i32); +impl NavigationViewDisplayMode { + pub const Minimal: Self = Self(0); + pub const Compact: Self = Self(1); + pub const Expanded: Self = Self(2); +} +impl windows_core::TypeKind for NavigationViewDisplayMode { + type TypeKind = windows_core::CopyType; +} +impl windows_core::RuntimeType for NavigationViewDisplayMode { + const SIGNATURE: windows_core::imp::ConstBuffer = windows_core::imp::ConstBuffer::from_slice( + b"enum(Microsoft.UI.Xaml.Controls.NavigationViewDisplayMode;i4)", + ); +} +#[repr(transparent)] #[derive(Clone, Debug, Eq, PartialEq)] pub struct NavigationViewItem(windows_core::IUnknown); windows_core::imp::interface_hierarchy!( @@ -19223,6 +19838,33 @@ impl windows_core::RuntimeType for Point { const SIGNATURE: windows_core::imp::ConstBuffer = windows_core::imp::ConstBuffer::from_slice(b"struct(Windows.Foundation.Point;f4;f4)"); } +#[repr(transparent)] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Pointer(windows_core::IUnknown); +windows_core::imp::interface_hierarchy!( + Pointer, + windows_core::IUnknown, + windows_core::IInspectable +); +impl windows_core::RuntimeType for Pointer { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_class::(); +} +unsafe impl windows_core::Interface for Pointer { + type Vtable = ::Vtable; + const IID: windows_core::GUID = ::IID; +} +impl core::ops::Deref for Pointer { + type Target = IPointer; + fn deref(&self) -> &Self::Target { + unsafe { core::mem::transmute(self) } + } +} +impl windows_core::RuntimeName for Pointer { + const NAME: &'static str = "Microsoft.UI.Xaml.Input.Pointer"; +} +unsafe impl Send for Pointer {} +unsafe impl Sync for Pointer {} windows_core::imp::define_interface!( PointerEventHandler, PointerEventHandler_Vtbl, diff --git a/crates/libs/reactor/src/element.rs b/crates/libs/reactor/src/element.rs index c680e189026..426382f7310 100644 --- a/crates/libs/reactor/src/element.rs +++ b/crates/libs/reactor/src/element.rs @@ -855,6 +855,31 @@ pub trait ElementExt: Sized { self } + /// Capture the pressed pointer until release so moves continue outside + /// the element's hit-test bounds. + fn capture_pointer_on_press(mut self) -> Self { + if let Some(m) = self.modifiers_mut() { + ensure_pointer_handlers(m).capture_pointer_on_press = true; + } + self + } + + /// Register a callback for an involuntarily lost pointer capture. + fn on_pointer_capture_lost(mut self, f: impl IntoUnitCallback) -> Self { + if let Some(m) = self.modifiers_mut() { + ensure_pointer_handlers(m).on_pointer_capture_lost = Some(f.into_unit_callback()); + } + self + } + + /// Register a callback for a canceled pointer interaction. + fn on_pointer_canceled(mut self, f: impl IntoUnitCallback) -> Self { + if let Some(m) = self.modifiers_mut() { + ensure_pointer_handlers(m).on_pointer_canceled = Some(f.into_unit_callback()); + } + self + } + // Accessibility modifiers fn automation_name(mut self, name: impl Into) -> Self { @@ -979,19 +1004,30 @@ pub trait ElementExt: Sized { self } - fn resources( - mut self, - entries: impl IntoIterator, impl Into)>, - ) -> Self { + fn resources(mut self, entries: impl IntoIterator) -> Self + where + K: Into, + V: Into, + { if let Some(m) = self.modifiers_mut() { m.resources = entries .into_iter() - .map(|(k, v)| (k.into(), v.into())) + .map(|(key, value)| (key.into(), value.into())) .collect(); } self } + fn resource_overrides( + mut self, + configure: impl FnOnce(ResourceBuilder) -> ResourceBuilder, + ) -> Self { + if let Some(m) = self.modifiers_mut() { + m.resources = configure(ResourceBuilder::default()).entries; + } + self + } + fn allow_drop(mut self, v: bool) -> Self { if let Some(m) = self.modifiers_mut() { m.allow_drop = Some(v); diff --git a/crates/libs/reactor/src/lib.rs b/crates/libs/reactor/src/lib.rs index 4addc9db1d5..6a25ec504c5 100644 --- a/crates/libs/reactor/src/lib.rs +++ b/crates/libs/reactor/src/lib.rs @@ -41,6 +41,7 @@ pub use bindings::DispatcherQueuePriority; pub use bindings::FlyoutPlacementMode; pub use bindings::HorizontalAlignment; pub use bindings::InfoBarSeverity; +pub use bindings::NavigationViewDisplayMode; pub use bindings::NavigationViewPaneDisplayMode; pub use bindings::Orientation; pub use bindings::PasswordRevealMode; diff --git a/crates/libs/reactor/src/reconciler.rs b/crates/libs/reactor/src/reconciler.rs index 18fba93667c..523f99c0526 100644 --- a/crates/libs/reactor/src/reconciler.rs +++ b/crates/libs/reactor/src/reconciler.rs @@ -571,10 +571,17 @@ impl Reconciler { self.backend.set_layout_animation(id, Some(la)); } + let enter = match anim.property_animation { + None => anim.enter_transition, + Some(_) => None, + }; + if enter.is_some() || anim.exit_transition.is_some() { + self.backend + .set_element_transitions(id, enter, anim.exit_transition); + } + if let Some(p) = anim.property_animation { self.backend.run_property_animation(id, Some(p)); - } else if let Some(enter) = anim.enter_transition { - self.backend.run_property_animation(id, Some(enter)); } } @@ -601,6 +608,21 @@ impl Reconciler { if old_pa != new_pa { self.backend.run_property_animation(id, new_pa); } + + let old_enter = old.and_then(|a| match a.property_animation { + None => a.enter_transition, + Some(_) => None, + }); + let new_enter = new.and_then(|a| match a.property_animation { + None => a.enter_transition, + Some(_) => None, + }); + let old_exit = old.and_then(|a| a.exit_transition); + let new_exit = new.and_then(|a| a.exit_transition); + if old_enter != new_enter || old_exit != new_exit { + self.backend + .set_element_transitions(id, new_enter, new_exit); + } } fn apply_theme_bindings_for(&mut self, id: ControlId, mods: &Modifiers) { @@ -750,7 +772,7 @@ impl Reconciler { self.apply_grid_placement_full(id, new.grid.unwrap_or_default()); } - if old.resources != new.resources && !new.resources.is_empty() { + if old.resources != new.resources { self.backend.set_prop( id, Prop::Resources, diff --git a/crates/libs/reactor/src/reconciler/templated.rs b/crates/libs/reactor/src/reconciler/templated.rs index 61525ffe9b5..46f0a898901 100644 --- a/crates/libs/reactor/src/reconciler/templated.rs +++ b/crates/libs/reactor/src/reconciler/templated.rs @@ -207,11 +207,163 @@ impl Reconciler { } } - if !old.same_items_as(new) { + if !old.same_items_as(new) && !self.remap_keyed_realized_rows(id, old, new) { self.refresh_realized_rows(id, new); } } + /// Preserves realized row controls across an equal-length keyed reorder. + /// + /// The backend's item source remains an identity vector of native slots. Moving a logical item + /// therefore means detaching its realized control from the old slot and attaching it to the + /// slot where the same key now appears. Count changes stay on the positional path because they + /// can synchronously change WinUI realization while the item source is being resized. + fn remap_keyed_realized_rows( + &mut self, + id: ControlId, + old: &TemplatedListElement, + new: &TemplatedListElement, + ) -> bool { + let count = old.item_count(); + if count != new.item_count() || matches!(new.kind, TemplatedKind::FlipView) { + return false; + } + + let realized_indices: Vec = { + let state = self.templated_lists.get(&id).unwrap(); + state + .rows + .iter() + .enumerate() + .filter_map(|(idx, row)| row.as_ref().map(|_| idx)) + .collect() + }; + if realized_indices.is_empty() { + return false; + } + + // Content-only updates are common and should remain proportional to the realized window. + // A full key map is only needed when a visible slot actually changed identity. + let visible_order_changed = realized_indices.iter().copied().any(|idx| { + let old_key = old.item_key(idx); + let new_key = new.item_key(idx); + old_key.is_none() || new_key.is_none() || old_key != new_key + }); + if !visible_order_changed { + return false; + } + + let mut realized_slots = vec![false; count]; + for idx in realized_indices { + realized_slots[idx] = true; + } + + let mut new_indices = FxHashMap::default(); + for new_idx in 0..count { + let Some(key) = new.item_key(new_idx) else { + return false; + }; + if new_indices.insert(key, new_idx).is_some() { + return false; + } + } + + let mut old_keys = rustc_hash::FxHashSet::default(); + let mut old_to_new = Vec::with_capacity(count); + let mut order_changed = false; + for old_idx in 0..count { + let Some(key) = old.item_key(old_idx) else { + return false; + }; + let Some(new_idx) = new_indices.get(&key).copied() else { + return false; + }; + if !old_keys.insert(key) { + return false; + } + order_changed |= old_idx != new_idx; + old_to_new.push(new_idx); + } + if !order_changed { + return false; + } + + let old_rows = { + let state = self.templated_lists.get_mut(&id).unwrap(); + std::mem::take(&mut state.rows) + }; + let mut new_rows: Vec> = Vec::with_capacity(count); + new_rows.resize_with(count, || None); + let mut moved_into = vec![false; count]; + let mut dropped = Vec::new(); + + for (old_idx, row) in old_rows.into_iter().enumerate() { + let Some(row) = row else { continue }; + let new_idx = old_to_new[old_idx]; + if realized_slots[new_idx] { + new_rows[new_idx] = Some(row); + moved_into[new_idx] = old_idx != new_idx; + } else { + dropped.push(row.content_id); + } + } + + for (row_idx, realized) in realized_slots.iter().copied().enumerate() { + if realized && old_to_new[row_idx] != row_idx { + self.backend.set_templated_row_content(id, row_idx, None); + } + } + for content_id in dropped { + self.dispatch_disappeared(content_id); + self.unmount(content_id); + } + + if let Some(state) = self.templated_lists.get_mut(&id) { + state.rows = new_rows; + } + + for (row_idx, realized) in realized_slots.into_iter().enumerate() { + if !realized { + continue; + } + let existing = self + .templated_lists + .get_mut(&id) + .and_then(|state| state.rows[row_idx].take()); + let new_el = new.build_item_view(row_idx); + + if let Some(row) = existing { + let new_id = self.update(&row.rendered, &new_el, row.content_id); + if let Some(content_id) = new_id { + if moved_into[row_idx] || content_id != row.content_id { + self.backend + .set_templated_row_content(id, row_idx, Some(content_id)); + } + if let Some(state) = self.templated_lists.get_mut(&id) { + state.rows[row_idx] = Some(RealizedRow { + rendered: new_el, + content_id, + }); + } + } else { + self.backend.set_templated_row_content(id, row_idx, None); + } + } else if let Some(content_id) = self.mount(&new_el) { + self.backend + .set_templated_row_content(id, row_idx, Some(content_id)); + if let Some(state) = self.templated_lists.get_mut(&id) { + state.rows[row_idx] = Some(RealizedRow { + rendered: new_el, + content_id, + }); + } + self.dispatch_appeared(content_id); + } + } + + true + } + fn refresh_realized_rows(&mut self, id: ControlId, new: &TemplatedListElement) { let realized_indices: Vec = { let state = self.templated_lists.get(&id).unwrap(); @@ -234,7 +386,7 @@ impl Reconciler { }; let new_el = new.build_item_view(row_idx); - if can_skip_update(&old_el, &new_el) { + if !self.force_component_rerender && can_skip_update(&old_el, &new_el) { self.debug_elements_skipped += 1; if let Some(state) = self.templated_lists.get_mut(&id) && let Some(Some(row)) = state.rows.get_mut(row_idx) diff --git a/crates/libs/reactor/src/reconciler/widget_dispatch.rs b/crates/libs/reactor/src/reconciler/widget_dispatch.rs index 76f573321ce..47c7f57464b 100644 --- a/crates/libs/reactor/src/reconciler/widget_dispatch.rs +++ b/crates/libs/reactor/src/reconciler/widget_dispatch.rs @@ -235,11 +235,16 @@ impl Reconciler { self.backend .set_prop(tab_id, Prop::Header, &PropValue::Str(n.header.clone())); } - if o.key != n.key - && let Some(key) = &n.key - { - self.backend - .set_prop(tab_id, Prop::ItemKey, &PropValue::Str(key.clone())); + if o.key != n.key { + match &n.key { + Some(key) => { + self.backend + .set_prop(tab_id, Prop::ItemKey, &PropValue::Str(key.clone())); + } + None => self + .backend + .set_prop(tab_id, Prop::ItemKey, &PropValue::Unset), + } } if o.is_closable != n.is_closable { // Either explicit value (set new), or transition to default diff --git a/crates/libs/reactor/src/style.rs b/crates/libs/reactor/src/style.rs index 5cfb2c6b18e..1ce1bc7bded 100644 --- a/crates/libs/reactor/src/style.rs +++ b/crates/libs/reactor/src/style.rs @@ -35,6 +35,112 @@ impl From for Thickness { } } +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct CornerRadius { + pub top_left: f64, + pub top_right: f64, + pub bottom_right: f64, + pub bottom_left: f64, +} + +impl CornerRadius { + pub const fn uniform(v: f64) -> Self { + Self { + top_left: v, + top_right: v, + bottom_right: v, + bottom_left: v, + } + } + + pub const fn new(top_left: f64, top_right: f64, bottom_right: f64, bottom_left: f64) -> Self { + Self { + top_left, + top_right, + bottom_right, + bottom_left, + } + } +} + +impl From for CornerRadius { + fn from(v: f64) -> Self { + Self::uniform(v) + } +} + +/// A typed value stored in an element's WinUI resource dictionary. +#[derive(Clone, Debug, PartialEq)] +pub enum ResourceValue { + String(String), + /// A [`Color`] converted to a WinUI `SolidColorBrush`. + SolidColorBrush(Color), + F64(f64), + Thickness(Thickness), + CornerRadius(CornerRadius), +} + +impl From<&str> for ResourceValue { + fn from(value: &str) -> Self { + Self::String(value.into()) + } +} + +impl From for ResourceValue { + fn from(value: String) -> Self { + Self::String(value) + } +} + +impl From for ResourceValue { + fn from(value: Color) -> Self { + Self::SolidColorBrush(value) + } +} + +impl From for ResourceValue { + fn from(value: f64) -> Self { + Self::F64(value) + } +} + +impl From for ResourceValue { + fn from(value: Thickness) -> Self { + Self::Thickness(value) + } +} + +impl From for ResourceValue { + fn from(value: CornerRadius) -> Self { + Self::CornerRadius(value) + } +} + +#[derive(Default)] +pub struct ResourceBuilder { + pub(crate) entries: HashMap, +} + +impl ResourceBuilder { + pub fn set(mut self, key: impl Into, value: impl Into) -> Self { + self.entries.insert(key.into(), value.into()); + self + } + + pub fn extend(mut self, entries: impl IntoIterator) -> Self + where + K: Into, + V: Into, + { + self.entries.extend( + entries + .into_iter() + .map(|(key, value)| (key.into(), value.into())), + ); + self + } +} + impl Color { pub const fn rgb(r: u8, g: u8, b: u8) -> Self { Self { a: 255, r, g, b } @@ -474,7 +580,7 @@ pub struct Modifiers { pub drag_handlers: Option>, /// Fast path for grid row/column placement. pub grid: Option, - pub resources: HashMap, + pub resources: HashMap, } impl Modifiers { @@ -609,6 +715,9 @@ pub struct PointerHandlers { pub on_pointer_moved: Option>, pub on_pointer_entered: Option>, pub on_pointer_exited: Option>, + pub on_pointer_capture_lost: Option>, + pub on_pointer_canceled: Option>, + pub capture_pointer_on_press: bool, } impl PointerHandlers { @@ -620,14 +729,27 @@ impl PointerHandlers { && self.on_pointer_moved.is_none() && self.on_pointer_entered.is_none() && self.on_pointer_exited.is_none() + && self.on_pointer_capture_lost.is_none() + && self.on_pointer_canceled.is_none() + && !self.capture_pointer_on_press } } -/// Pointer callback state, with `x`/`y` in DIPs relative to the element. +/// Pointer callback state in element-local and window-relative DIPs. #[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct PointerEventInfo { + /// Horizontal position relative to the element. pub x: f64, + /// Vertical position relative to the element. pub y: f64, + /// Horizontal position relative to the overall window. + pub window_x: f64, + /// Vertical position relative to the overall window. + pub window_y: f64, + /// Whether `.capture_pointer_on_press()` captured this pointer. + /// + /// This is only set for `on_pointer_pressed` callbacks. + pub capture_succeeded: bool, pub is_left_button_pressed: bool, pub is_right_button_pressed: bool, pub is_middle_button_pressed: bool, diff --git a/crates/libs/reactor/src/widgets/icon.rs b/crates/libs/reactor/src/widgets/icon.rs index 0bd28af2bc2..93b4792ae65 100644 --- a/crates/libs/reactor/src/widgets/icon.rs +++ b/crates/libs/reactor/src/widgets/icon.rs @@ -3,15 +3,23 @@ use super::*; /// An icon displayed by controls that accept a WinUI `IconElement` - buttons, /// [`NavViewItem`]s, command-bar buttons, and [`SelectorBarItemDef`]s. /// -/// Construct one from a built-in [`Symbol`], an [`ImageSource`], or a font glyph. -/// A bare [`Symbol`] converts into an `Icon` automatically (`impl Into`), -/// so `.icon(Symbol::Home)` keeps working alongside `.icon(Icon::image(...))`. +/// Construct one from a built-in [`Symbol`], an [`ImageSource`], a bitmap mask, +/// a font glyph, or vector path data. A bare [`Symbol`] converts into an `Icon` +/// automatically (`impl Into`), so `.icon(Symbol::Home)` keeps working +/// alongside `.icon(Icon::image(...))`. #[derive(Clone, Debug, PartialEq)] pub enum Icon { /// A built-in system glyph from the [`Symbol`] enum (WinUI `SymbolIcon`). Symbol(Symbol), /// An image rendered in full color using the source's native format. Image(ImageSource), + /// A URI image rendered by WinUI `BitmapIcon`. + Bitmap { + /// The image URI. + uri: String, + /// Whether WinUI replaces non-transparent pixels with the icon foreground. + show_as_monochrome: bool, + }, /// A glyph from a font (WinUI `FontIcon`). When `family` is `None`, the /// control's default icon font is used. Font { @@ -20,6 +28,8 @@ pub enum Icon { /// The font family to select the glyph from, e.g. `"Segoe Fluent Icons"`. family: Option, }, + /// XAML path mini-language data rendered by WinUI `PathIcon`. + Path(String), } impl Icon { @@ -33,11 +43,17 @@ impl Icon { Self::Image(source.into()) } - /// A raster image loaded from a URI. + /// A native WinUI `BitmapIcon` loaded from a URI. /// - /// This is a compatibility shorthand for [`Icon::image`]. - pub fn bitmap(uri: impl Into) -> Self { - Self::image(ImageSource::uri(uri)) + /// Set `show_as_monochrome` to `true` for a foreground-tinted mask or + /// `false` to preserve the bitmap's colors. Use [`Icon::image`] for SVG + /// sources, surfaces, and full-color images that do not need `BitmapIcon` + /// behavior. + pub fn bitmap_icon(uri: impl Into, show_as_monochrome: bool) -> Self { + Self::Bitmap { + uri: uri.into(), + show_as_monochrome, + } } /// A font glyph rendered with the control's default icon font. @@ -55,6 +71,13 @@ impl Icon { family: Some(family.into()), } } + + /// A vector icon described with the XAML path mini-language. + /// + /// WinUI parses the path data when the native icon is created. + pub fn path(data: impl Into) -> Self { + Self::Path(data.into()) + } } impl From for Icon { diff --git a/crates/libs/reactor/src/widgets/navigation_view.rs b/crates/libs/reactor/src/widgets/navigation_view.rs index 6bc60c105f5..3bab897c183 100644 --- a/crates/libs/reactor/src/widgets/navigation_view.rs +++ b/crates/libs/reactor/src/widgets/navigation_view.rs @@ -44,7 +44,9 @@ pub struct NavigationView { pub selected_tag: Option, pub on_selection_changed: Option>, pub is_pane_open: bool, + pub on_pane_open_changed: Option>, pub pane_display_mode: NavigationViewPaneDisplayMode, + pub on_display_mode_changed: Option>, pub is_back_enabled: bool, pub on_back_requested: Option>, pub is_settings_visible: bool, @@ -70,7 +72,9 @@ impl Default for NavigationView { selected_tag: None, on_selection_changed: None, is_pane_open: true, + on_pane_open_changed: None, pane_display_mode: NavigationViewPaneDisplayMode::Auto, + on_display_mode_changed: None, is_back_enabled: false, on_back_requested: None, is_settings_visible: true, @@ -111,10 +115,21 @@ impl NavigationView { self.is_pane_open = v; self } + pub fn on_pane_open_changed(mut self, f: impl IntoCallback) -> Self { + self.on_pane_open_changed = Some(f.into_callback()); + self + } pub fn pane_display_mode(mut self, mode: NavigationViewPaneDisplayMode) -> Self { self.pane_display_mode = mode; self } + pub fn on_display_mode_changed( + mut self, + f: impl IntoCallback, + ) -> Self { + self.on_display_mode_changed = Some(f.into_callback()); + self + } pub fn back_enabled(mut self, v: bool) -> Self { self.is_back_enabled = v; self @@ -204,6 +219,18 @@ impl Widget for NavigationView { Prop::AutoSuggestItems, PropValue::StrList(self.auto_suggest_items.clone()), )); + out.push(Binding::Event( + Event::NavigationPaneOpenChanged, + self.on_pane_open_changed + .as_ref() + .map(|callback| EventHandler::Bool(callback.clone())), + )); + out.push(Binding::Event( + Event::NavigationDisplayModeChanged, + self.on_display_mode_changed + .as_ref() + .map(|callback| EventHandler::NavigationDisplayMode(callback.clone())), + )); out } fn children(&self) -> Children<'_> { diff --git a/crates/libs/reactor/src/widgets/tab_view.rs b/crates/libs/reactor/src/widgets/tab_view.rs index 6a190e31514..83d8a1e827c 100644 --- a/crates/libs/reactor/src/widgets/tab_view.rs +++ b/crates/libs/reactor/src/widgets/tab_view.rs @@ -73,6 +73,7 @@ impl TabView { } impl TabItem { + /// Sets the stable identity reported by [`TabView::on_close_requested`]. pub fn with_key(mut self, key: impl Into) -> Self { self.key = Some(key.into()); self diff --git a/crates/samples/reactor/samples/examples/exit_transition.rs b/crates/samples/reactor/samples/examples/exit_transition.rs new file mode 100644 index 00000000000..961f40ef13d --- /dev/null +++ b/crates/samples/reactor/samples/examples/exit_transition.rs @@ -0,0 +1,38 @@ +#![windows_subsystem = "windows"] + +use std::time::Duration; + +use windows_reactor::*; + +fn app(cx: &mut RenderCx) -> Element { + let (visible, set_visible) = cx.use_state(true); + let card: Element = if visible { + border( + text_block("This visual remains visible while its exit animation completes.") + .font_size(18.0), + ) + .padding(Thickness::uniform(24.0)) + .background(Color::rgb(32, 96, 160)) + .corner_radius(12.0) + .transition( + Some(AnimationConfig::fade_in(Duration::from_millis(300))), + Some(AnimationConfig::fade_out(Duration::from_millis(600))), + ) + .into() + } else { + Element::Empty + }; + + vstack(( + button(if visible { "Remove" } else { "Restore" }) + .on_click(move || set_visible.call(!visible)), + card, + )) + .spacing(16.0) + .padding(Thickness::uniform(24.0)) + .into() +} + +fn main() -> Result<()> { + reactor_samples::run("Exit Transition", app) +} diff --git a/crates/samples/reactor/samples/examples/icon_elements.rs b/crates/samples/reactor/samples/examples/icon_elements.rs index d9058be9cd3..7fa423812bc 100644 --- a/crates/samples/reactor/samples/examples/icon_elements.rs +++ b/crates/samples/reactor/samples/examples/icon_elements.rs @@ -2,15 +2,23 @@ use windows_reactor::*; fn app(cx: &mut RenderCx) -> Element { let (page, set_page) = cx.use_state(String::from("home")); + // The sample runner is unpackaged, so absolute file URIs point at the example assets. + // Packaged apps should use ms-appx:/// URIs for files included in the package. let image = format!( "file:///{}/examples/image.svg", env!("CARGO_MANIFEST_DIR").replace('\\', "/"), ); + let bitmap = format!( + "file:///{}/examples/image.png", + env!("CARGO_MANIFEST_DIR").replace('\\', "/"), + ); let content = match page.as_str() { "home" => text_block("Symbol icon (SymbolIcon)."), "starred" => text_block("Font-glyph icon (FontIcon)."), "repo" => text_block("SVG image icon (ImageIcon)."), + "bitmap" => text_block("Foreground-tinted bitmap mask (BitmapIcon)."), + "path" => text_block("Vector path data (PathIcon)."), _ => text_block("Unknown page"), }; @@ -23,6 +31,12 @@ fn app(cx: &mut RenderCx) -> Element { NavViewItem::new("Repository") .tag("repo") .icon(Icon::image(image)), + NavViewItem::new("Bitmap mask") + .tag("bitmap") + .icon(Icon::bitmap_icon(bitmap, true)), + NavViewItem::new("Path") + .tag("path") + .icon(Icon::path("F1 M 0,8 L 6,14 L 16,2 L 14,0 L 6,10 L 2,6 Z")), ], content, ) diff --git a/crates/samples/reactor/samples/examples/keyed_list_reorder.rs b/crates/samples/reactor/samples/examples/keyed_list_reorder.rs new file mode 100644 index 00000000000..65675e7733d --- /dev/null +++ b/crates/samples/reactor/samples/examples/keyed_list_reorder.rs @@ -0,0 +1,51 @@ +#![windows_subsystem = "windows"] + +use windows_reactor::*; + +#[derive(Clone, PartialEq)] +struct RowProps { + name: String, +} + +fn row(props: &RowProps, cx: &mut RenderCx) -> Element { + let (clicks, set_clicks) = cx.use_state(0_u32); + + hstack(( + text_block(format!("{}: {clicks}", props.name)).width(120.0), + button(format!("Increment {}", props.name)).on_click(move || set_clicks.call(clicks + 1)), + )) + .spacing(8.0) + .padding(Thickness::uniform(6.0)) + .into() +} + +fn app(cx: &mut RenderCx) -> Element { + let (items, set_items) = cx.use_state(vec![ + "Alpha".to_string(), + "Beta".to_string(), + "Gamma".to_string(), + "Delta".to_string(), + ]); + let shuffled = { + let mut items = items.clone(); + items.rotate_left(1); + items + }; + + vstack(( + text_block("Increment a row, then rotate the list. The count stays with its name."), + button("Rotate").on_click(move || set_items.call(shuffled.clone())), + list_view(items, |name, _| { + component(row, RowProps { name: name.clone() }) + }) + .with_key_selector(|name| name.clone()) + .height(240.0), + )) + .spacing(12.0) + .padding(Thickness::uniform(16.0)) + .into() +} + +fn main() -> Result<()> { + reactor_samples::run("KeyedListReorder", app) +} diff --git a/crates/samples/reactor/samples/examples/lightweight_resources.rs b/crates/samples/reactor/samples/examples/lightweight_resources.rs new file mode 100644 index 00000000000..49fb144a731 --- /dev/null +++ b/crates/samples/reactor/samples/examples/lightweight_resources.rs @@ -0,0 +1,39 @@ +#![windows_subsystem = "windows"] + +use windows_reactor::*; + +fn app(cx: &mut RenderCx) -> Element { + let (styled, set_styled) = cx.use_state(true); + + let target: Element = if styled { + button("Delete") + .resource_overrides(|resources| { + resources + .set("ButtonBackground", Color::rgb(178, 34, 34)) + .set("ButtonForeground", Color::rgb(255, 255, 255)) + .set("ButtonBorderThemeThickness", Thickness::uniform(0.0)) + .set("ControlCornerRadius", CornerRadius::uniform(8.0)) + }) + .into() + } else { + button("Delete").into() + }; + + vstack(( + text_block("Element resources override WinUI lightweight styling values."), + target, + button(if styled { + "Clear resources" + } else { + "Apply resources" + }) + .on_click(move || set_styled.call(!styled)), + )) + .spacing(12.0) + .padding(Thickness::uniform(16.0)) + .into() +} + +fn main() -> Result<()> { + reactor_samples::run("LightweightResources", app) +} diff --git a/crates/samples/reactor/samples/examples/pointer_resize.rs b/crates/samples/reactor/samples/examples/pointer_resize.rs new file mode 100644 index 00000000000..3610ea20d1f --- /dev/null +++ b/crates/samples/reactor/samples/examples/pointer_resize.rs @@ -0,0 +1,64 @@ +#![windows_subsystem = "windows"] + +use windows_reactor::*; + +fn app(cx: &mut RenderCx) -> Element { + let (width, set_width) = cx.use_state(260.0_f64); + let width_ref = cx.use_ref(width); + width_ref.set(width); + let drag_start = cx.use_ref(None::<(f64, f64)>); + + let on_pressed = cx.use_callback((), { + let drag_start = drag_start.clone(); + move |info: PointerEventInfo| { + if info.is_left_button_pressed && info.capture_succeeded { + drag_start.set(Some((info.window_x, width_ref.get_cloned()))); + } + } + }); + let on_moved = cx.use_callback((), { + let drag_start = drag_start.clone(); + move |info: PointerEventInfo| { + if !info.is_left_button_pressed { + drag_start.set(None); + return; + } + if let Some((start_x, start_width)) = drag_start.get_cloned() { + set_width.call((start_width + info.window_x - start_x).clamp(140.0, 520.0)); + } + } + }); + let on_released = cx.use_callback((), { + let drag_start = drag_start.clone(); + move |_: PointerEventInfo| drag_start.set(None) + }); + let on_capture_ended = cx.use_callback((), move |()| drag_start.set(None)); + + vstack(( + TitleBar::new("windows_reactor - pointer resize"), + text_block(format!("Left pane width: {width:.0} DIPs")), + hstack(( + border(text_block("Resizable pane").padding(Thickness::uniform(16.0))) + .width(width) + .background(Color::rgb(35, 90, 150)), + border(text_block("Drag").foreground(Color::rgb(255, 255, 255))) + .width(44.0) + .background(Color::rgb(90, 90, 100)) + .on_pointer_pressed(on_pressed) + .on_pointer_moved(on_moved) + .on_pointer_released(on_released) + .on_pointer_capture_lost(on_capture_ended.clone()) + .on_pointer_canceled(on_capture_ended) + .capture_pointer_on_press(), + border(text_block("The handle moves, but window_x remains stable.")) + .padding(Thickness::uniform(16.0)), + )) + .height(240.0), + )) + .spacing(12.0) + .into() +} + +fn main() -> Result<()> { + reactor_samples::run("Pointer Resize", app) +} diff --git a/crates/samples/reactor/samples/examples/responsive_navigation.rs b/crates/samples/reactor/samples/examples/responsive_navigation.rs new file mode 100644 index 00000000000..cbe8f23988d --- /dev/null +++ b/crates/samples/reactor/samples/examples/responsive_navigation.rs @@ -0,0 +1,61 @@ +#![windows_subsystem = "windows"] + +use windows_reactor::*; + +fn display_mode_name(mode: NavigationViewDisplayMode) -> &'static str { + match mode { + NavigationViewDisplayMode::Minimal => "minimal", + NavigationViewDisplayMode::Compact => "compact", + NavigationViewDisplayMode::Expanded => "expanded", + _ => "unknown", + } +} + +fn app(cx: &mut RenderCx) -> Element { + let (pane_open, set_pane_open) = cx.use_state(true); + let (display_mode, set_display_mode) = cx.use_state(NavigationViewDisplayMode::Expanded); + let footer = if display_mode == NavigationViewDisplayMode::Expanded { + "Signed in: Ada" + } else { + "AD" + }; + + NavigationView::new( + [ + NavViewItem::new("Home").tag("home").icon(Symbol::Home), + NavViewItem::new("Documents") + .tag("documents") + .icon(Symbol::Document), + ], + vstack(( + text_block(format!( + "Actual display mode: {}", + display_mode_name(display_mode) + )), + text_block(if pane_open { + "Pane is open" + } else { + "Pane is closed" + }), + button("Toggle pane").on_click({ + let set_pane_open = set_pane_open.clone(); + move || set_pane_open.call(!pane_open) + }), + text_block("Resize the window to cross compact and minimal thresholds."), + )) + .spacing(12.0) + .padding(Thickness::uniform(16.0)), + ) + .pane_open(pane_open) + .on_pane_open_changed(set_pane_open) + .pane_display_mode(NavigationViewPaneDisplayMode::Auto) + .on_display_mode_changed(set_display_mode) + .pane_title("Responsive navigation") + .pane_footer(text_block(footer)) + .settings_visible(false) + .into() +} + +fn main() -> Result<()> { + reactor_samples::run("Responsive Navigation", app) +} diff --git a/crates/samples/reactor/samples/examples/tab_view_item_key.rs b/crates/samples/reactor/samples/examples/tab_view_item_key.rs new file mode 100644 index 00000000000..7da888d78a0 --- /dev/null +++ b/crates/samples/reactor/samples/examples/tab_view_item_key.rs @@ -0,0 +1,43 @@ +use windows_reactor::*; + +fn app(cx: &mut RenderCx) -> Element { + let (keyed, set_keyed) = cx.use_state(true); + let (last_close_key, set_last_close_key) = cx.use_state(String::new()); + + let mut item = TabItem::new("Document", text_block("Close the tab to inspect its key.")); + if keyed { + item = item.with_key("document"); + } + + vstack(( + button(if keyed { + "Remove item key" + } else { + "Restore item key" + }) + .on_click(move || set_keyed.call(!keyed)), + TabView::new([item]).on_close_requested(move |key: String| { + set_last_close_key.call(if key.is_empty() { + "".to_string() + } else { + key + }); + }), + text_block(format!( + "configured key: {}; last close request: {}", + if keyed { "document" } else { "" }, + if last_close_key.is_empty() { + "" + } else { + &last_close_key + } + )), + )) + .spacing(8.0) + .padding(Thickness::uniform(16.0)) + .into() +} + +fn main() -> Result<()> { + reactor_samples::run("TabView Item Key", app) +} diff --git a/crates/tests/libs/composition/src/live.rs b/crates/tests/libs/composition/src/live.rs index f78f2e64b9f..23cb6d7986f 100644 --- a/crates/tests/libs/composition/src/live.rs +++ b/crates/tests/libs/composition/src/live.rs @@ -129,6 +129,22 @@ fn key_frame_animation_starts_on_visual() { visual.start_animation("Scale", &animation); } +#[test] +fn animation_group_accepts_multiple_animations() { + let c = compositor(); + let opacity = c.create_scalar_key_frame_animation(); + opacity.set_target("Opacity"); + opacity.insert_key_frame_with_easing(1.0, 1.0, &c.create_linear_easing_function()); + + let scale = c.create_vector3_key_frame_animation(); + scale.set_target("Scale"); + scale.insert_key_frame(1.0, Vector3::new(1.0, 1.0, 1.0)); + + let group = c.create_animation_group(); + group.add(&opacity); + group.add(&scale); +} + #[test] fn scoped_batch_ends() { let c = compositor(); diff --git a/crates/tests/libs/reactor/src/lib.rs b/crates/tests/libs/reactor/src/lib.rs index 7fa7c9313a8..2cb1fde8e5a 100644 --- a/crates/tests/libs/reactor/src/lib.rs +++ b/crates/tests/libs/reactor/src/lib.rs @@ -122,6 +122,11 @@ pub enum Op { id: ControlId, config: Option, }, + SetElementTransitions { + id: ControlId, + enter: Option, + exit: Option, + }, SetRichTextParagraphs { id: ControlId, paragraphs: Vec, @@ -234,6 +239,19 @@ impl RecordingBackend { h.invoke_i32(v); } + pub fn fire_navigation_display_mode( + &self, + id: ControlId, + event: Event, + mode: NavigationViewDisplayMode, + ) { + let h = self + .handlers + .get(&(id, event)) + .unwrap_or_else(|| panic!("no handler for ({id}, {event:?})")); + h.invoke_navigation_display_mode(mode); + } + pub fn fire_datetime(&self, id: ControlId, event: Event, dt: DateTime) { let h = self .handlers @@ -536,6 +554,15 @@ impl Backend for RecordingBackend { self.ops.push(Op::RunPropertyAnimation { id, config }); } + fn set_element_transitions( + &mut self, + id: ControlId, + enter: Option, + exit: Option, + ) { + self.ops.push(Op::SetElementTransitions { id, enter, exit }); + } + fn set_rich_text_paragraphs(&mut self, id: ControlId, paragraphs: &[RichTextParagraph]) { self.ops.push(Op::SetRichTextParagraphs { id, diff --git a/crates/tests/libs/reactor/tests/animation.rs b/crates/tests/libs/reactor/tests/animation.rs index 42f55384aa3..5f8cde145b0 100644 --- a/crates/tests/libs/reactor/tests/animation.rs +++ b/crates/tests/libs/reactor/tests/animation.rs @@ -48,6 +48,15 @@ fn property_op(ops: &[Op]) -> Option<&AnimationConfig> { }) } +fn element_transition_op( + ops: &[Op], +) -> Option<(&Option, &Option)> { + ops.iter().rev().find_map(|op| match op { + Op::SetElementTransitions { enter, exit, .. } => Some((enter, exit)), + _ => None, + }) +} + #[test] fn with_opacity_transition_emits_set_implicit_transitions() { let mut r = fresh(); @@ -271,8 +280,67 @@ fn enter_transition_fires_at_mount_time() { .into(); let _ = r.reconcile(None, &el, None, no_rerender()); - let p = property_op(&r.backend.ops).expect("enter should produce a run-property-animation op"); - assert_eq!(p.opacity, Some(1.0)); + let (enter, exit) = + element_transition_op(&r.backend.ops).expect("expected element transitions"); + assert_eq!(enter.unwrap().opacity, Some(1.0)); + assert!(exit.is_none()); + assert!( + property_op(&r.backend.ops).is_none(), + "enter transitions use WinUI's implicit show lifecycle" + ); +} + +#[test] +fn exit_transition_is_registered_at_mount_time() { + let mut r = fresh(); + let exit = AnimationConfig::fade_out(Duration::from_millis(200)); + let el: Element = button("hi").transition(None, Some(exit)).into(); + let _ = r.reconcile(None, &el, None, no_rerender()); + + let (enter, registered_exit) = + element_transition_op(&r.backend.ops).expect("expected element transitions"); + assert!(enter.is_none()); + assert_eq!(*registered_exit, Some(exit)); +} + +#[test] +fn property_animation_suppresses_enter_but_keeps_exit() { + let mut r = fresh(); + let property = AnimationConfig::fade_in(Duration::from_millis(100)); + let enter = AnimationConfig::fade_in(Duration::from_millis(200)); + let exit = AnimationConfig::fade_out(Duration::from_millis(300)); + let el: Element = button("hi") + .animate(property) + .transition(Some(enter), Some(exit)) + .into(); + let _ = r.reconcile(None, &el, None, no_rerender()); + + assert_eq!(property_op(&r.backend.ops), Some(&property)); + let (registered_enter, registered_exit) = + element_transition_op(&r.backend.ops).expect("expected element transitions"); + assert!(registered_enter.is_none()); + assert_eq!(*registered_exit, Some(exit)); +} + +#[test] +fn dropping_element_transitions_clears_them() { + let mut r = fresh(); + let v1: Element = button("hi") + .transition( + Some(AnimationConfig::fade_in(Duration::from_millis(100))), + Some(AnimationConfig::fade_out(Duration::from_millis(100))), + ) + .into(); + let id = r.reconcile(None, &v1, None, no_rerender()).unwrap(); + r.backend.clear_ops(); + + let v2: Element = button("hi").into(); + let _ = r.reconcile(Some(&v1), &v2, Some(id), no_rerender()); + + let (enter, exit) = + element_transition_op(&r.backend.ops).expect("expected transition clearing"); + assert!(enter.is_none()); + assert!(exit.is_none()); } #[test] diff --git a/crates/tests/libs/reactor/tests/controls_shell.rs b/crates/tests/libs/reactor/tests/controls_shell.rs index 23460774e6f..0e4e10992fe 100644 --- a/crates/tests/libs/reactor/tests/controls_shell.rs +++ b/crates/tests/libs/reactor/tests/controls_shell.rs @@ -8,9 +8,9 @@ use windows_reactor::text_block; use windows_reactor::{ BreadcrumbBar, CommandBar, CommandBarCommandDef, CommandBarDefaultLabelPosition, ContentDialog, InfoBadge, InfoBar, InfoBarSeverity, MenuBar, MenuBarItemDef, MenuItemDef, NavViewItem, - NavigationView, NavigationViewPaneDisplayMode, Pivot, PivotItem, SelectorBar, - SelectorBarItemDef, TabItem, TabView, TeachingTip, TeachingTipPlacementMode, TitleBar, - TreeNodeDef, TreeView, TreeViewSelectionMode, + NavigationView, NavigationViewDisplayMode, NavigationViewPaneDisplayMode, Pivot, PivotItem, + SelectorBar, SelectorBarItemDef, TabItem, TabView, TeachingTip, TeachingTipPlacementMode, + TitleBar, TreeNodeDef, TreeView, TreeViewSelectionMode, }; use windows_reactor::{ControlKind, Event, Prop, PropValue}; @@ -105,6 +105,40 @@ fn tab_view_selected_index_update_emits_single_set() { assert_eq!(selected_sets.len(), 1); } +#[test] +fn tab_item_key_removal_clears_the_existing_item() { + let tabs_a: Element = + TabView::new([TabItem::new("A", text_block("a")).with_key("stable")]).into(); + let tabs_b: Element = TabView::new([TabItem::new("A", text_block("a"))]).into(); + + let mut r = Reconciler::new(RecordingBackend::new()); + let parent = r.reconcile(None, &tabs_a, None, Rc::new(|| {})).unwrap(); + let item = r.backend.children_of(parent)[0]; + r.backend.clear_ops(); + + r.reconcile(Some(&tabs_a), &tabs_b, Some(parent), Rc::new(|| {})); + + assert!(r.backend.ops.iter().any(|op| { + matches!( + op, + Op::SetProp { + id, + prop: Prop::ItemKey, + value: PropValue::Unset, + } if *id == item + ) + })); + assert!(!r.backend.ops.iter().any(|op| { + matches!( + op, + Op::Create { + kind: ControlKind::TabViewItem, + .. + } | Op::Destroy { .. } + ) + })); +} + #[test] fn tab_view_add_tab_button_visible_prop_emits_set() { // WinUI default is true; non_default = "true" means emit only when false @@ -384,6 +418,47 @@ fn navigation_view_back_requested_fires_zero_arg() { assert_eq!(count.get(), 1); } +#[test] +fn navigation_view_state_callbacks_report_control_state() { + let pane_open = Rc::new(Cell::new(None)); + let display_mode = Rc::new(Cell::new(None)); + let nv: Element = NavigationView::new([NavViewItem::new("Home")], text_block("page")) + .on_pane_open_changed({ + let pane_open = pane_open.clone(); + move |open| pane_open.set(Some(open)) + }) + .on_display_mode_changed({ + let display_mode = display_mode.clone(); + move |mode| display_mode.set(Some(mode)) + }) + .into(); + let r = mount(&nv); + + let nv_id = match r.backend.ops.iter().find(|op| { + matches!( + op, + Op::Create { + kind: ControlKind::NavigationView, + .. + } + ) + }) { + Some(Op::Create { id, .. }) => *id, + _ => panic!("no NavigationView Create op"), + }; + + r.backend + .fire_bool(nv_id, Event::NavigationPaneOpenChanged, false); + r.backend.fire_navigation_display_mode( + nv_id, + Event::NavigationDisplayModeChanged, + NavigationViewDisplayMode::Compact, + ); + + assert_eq!(pane_open.get(), Some(false)); + assert_eq!(display_mode.get(), Some(NavigationViewDisplayMode::Compact)); +} + #[test] fn navigation_view_update_emits_only_changed_props() { let a: Element = diff --git a/crates/tests/libs/reactor/tests/icon_bindings.rs b/crates/tests/libs/reactor/tests/icon_bindings.rs index 996252bab2f..b294a2928b4 100644 --- a/crates/tests/libs/reactor/tests/icon_bindings.rs +++ b/crates/tests/libs/reactor/tests/icon_bindings.rs @@ -57,15 +57,16 @@ fn image_source_converts_into_icon() { } #[test] -fn bitmap_shorthand_uses_generic_image_source() { +fn bitmap_icon_carries_native_rendering_mode() { let el: Element = Button::new("b") - .icon(Icon::bitmap("ms-appx:///logo.png")) + .icon(Icon::bitmap_icon("ms-appx:///logo.png", true)) .into(); assert_eq!( icon_value(&el), - Some(PropValue::Icon(Icon::Image(ImageSource::uri( - "ms-appx:///logo.png" - )))) + Some(PropValue::Icon(Icon::Bitmap { + uri: "ms-appx:///logo.png".into(), + show_as_monochrome: true, + })) ); } @@ -92,6 +93,19 @@ fn font_icon_carries_glyph_and_optional_family() { ); } +#[test] +fn path_icon_carries_geometry_data() { + let el: Element = Button::new("b") + .icon(Icon::path("F1 M 16,12 20,2L 20,16 1,16")) + .into(); + assert_eq!( + icon_value(&el), + Some(PropValue::Icon(Icon::Path( + "F1 M 16,12 20,2L 20,16 1,16".into() + ))) + ); +} + #[test] fn no_icon_emits_no_icon_prop() { let el: Element = Button::new("b").into(); diff --git a/crates/tests/libs/reactor/tests/pointer_handlers.rs b/crates/tests/libs/reactor/tests/pointer_handlers.rs index 65d0efe3bed..86c20a9932a 100644 --- a/crates/tests/libs/reactor/tests/pointer_handlers.rs +++ b/crates/tests/libs/reactor/tests/pointer_handlers.rs @@ -65,6 +65,9 @@ fn all_handlers_attach_as_a_single_bundle() { .on_pointer_moved(|_| {}) .on_pointer_entered(|_| {}) .on_pointer_exited(|| {}) + .on_pointer_capture_lost(|| {}) + .on_pointer_canceled(|| {}) + .capture_pointer_on_press() .into(); let (r, _) = mount(&el); @@ -76,6 +79,9 @@ fn all_handlers_attach_as_a_single_bundle() { assert!(h.on_pointer_moved.is_some()); assert!(h.on_pointer_entered.is_some()); assert!(h.on_pointer_exited.is_some()); + assert!(h.on_pointer_capture_lost.is_some()); + assert!(h.on_pointer_canceled.is_some()); + assert!(h.capture_pointer_on_press); } #[test] @@ -156,6 +162,9 @@ fn recorded_handler_invokes_with_pointer_info() { let info = PointerEventInfo { x: 12.0, y: 34.0, + window_x: 112.0, + window_y: 234.0, + capture_succeeded: true, is_left_button_pressed: true, is_right_button_pressed: false, is_middle_button_pressed: false, @@ -165,6 +174,9 @@ fn recorded_handler_invokes_with_pointer_info() { let got = seen.get().expect("callback fired"); assert_eq!(got.x, 12.0); assert_eq!(got.y, 34.0); + assert_eq!(got.window_x, 112.0); + assert_eq!(got.window_y, 234.0); + assert!(got.capture_succeeded); assert!(got.is_left_button_pressed); assert!(!got.is_right_button_pressed); } diff --git a/crates/tests/libs/reactor/tests/resources.rs b/crates/tests/libs/reactor/tests/resources.rs new file mode 100644 index 00000000000..72a13183651 --- /dev/null +++ b/crates/tests/libs/reactor/tests/resources.rs @@ -0,0 +1,102 @@ +use std::collections::HashMap; +use std::rc::Rc; + +use test_reactor::{Op, RecordingBackend}; +use windows_reactor::{ + Color, CornerRadius, Element, ElementExt, Prop, PropValue, Reconciler, ResourceValue, + Thickness, button, +}; + +fn rr() -> Rc { + Rc::new(|| {}) +} + +fn resource_updates(reconciler: &Reconciler) -> Vec { + reconciler + .backend + .ops + .iter() + .filter_map(|op| match op { + Op::SetProp { + prop: Prop::Resources, + value, + .. + } => Some(value.clone()), + _ => None, + }) + .collect() +} + +#[test] +fn resources_support_heterogeneous_typed_values() { + let element: Element = button("Styled") + .resource_overrides(|resources| { + resources + .set("ButtonBackground", Color::rgb(178, 34, 34)) + .set("ButtonBorderThemeThickness", Thickness::uniform(0.0)) + .set("ReactorScalar", 12.0) + .set("ControlCornerRadius", CornerRadius::uniform(6.0)) + .set("Label", "Destructive") + }) + .into(); + + let mut reconciler = Reconciler::new(RecordingBackend::new()); + reconciler.reconcile(None, &element, None, rr()); + + let expected = HashMap::from([ + ( + "ButtonBackground".into(), + ResourceValue::SolidColorBrush(Color::rgb(178, 34, 34)), + ), + ( + "ButtonBorderThemeThickness".into(), + ResourceValue::Thickness(Thickness::uniform(0.0)), + ), + ("ReactorScalar".into(), ResourceValue::F64(12.0)), + ( + "ControlCornerRadius".into(), + ResourceValue::CornerRadius(CornerRadius::uniform(6.0)), + ), + ("Label".into(), ResourceValue::String("Destructive".into())), + ]); + assert_eq!( + resource_updates(&reconciler), + vec![PropValue::Resources(expected)] + ); +} + +#[test] +fn resources_preserves_the_iterator_api() { + let element: Element = button("Styled") + .resources([("Label", "Destructive")]) + .into(); + + let mut reconciler = Reconciler::new(RecordingBackend::new()); + reconciler.reconcile(None, &element, None, rr()); + + assert_eq!( + resource_updates(&reconciler), + vec![PropValue::Resources(HashMap::from([( + "Label".into(), + ResourceValue::String("Destructive".into()), + )]))] + ); +} + +#[test] +fn clearing_resources_emits_an_empty_replacement() { + let old: Element = button("Styled") + .resource_overrides(|resources| resources.set("ButtonBackground", Color::rgb(178, 34, 34))) + .into(); + let new: Element = button("Styled").into(); + + let mut reconciler = Reconciler::new(RecordingBackend::new()); + let id = reconciler.reconcile(None, &old, None, rr()).unwrap(); + reconciler.backend.clear_ops(); + reconciler.reconcile(Some(&old), &new, Some(id), rr()); + + assert_eq!( + resource_updates(&reconciler), + vec![PropValue::Resources(HashMap::new())] + ); +} diff --git a/crates/tests/libs/reactor/tests/templated_list.rs b/crates/tests/libs/reactor/tests/templated_list.rs index ad1eb516c6e..e22761ee223 100644 --- a/crates/tests/libs/reactor/tests/templated_list.rs +++ b/crates/tests/libs/reactor/tests/templated_list.rs @@ -487,3 +487,234 @@ fn reorder_callback_refreshes_on_update() { assert!(!first.get(), "stale reorder closure must not fire"); assert_eq!(second.take(), Some(vec![1, 0])); } + +#[test] +fn keyed_reorder_moves_realized_controls_without_recreating_them() { + let make = |items: Vec<&'static str>| { + list_view(items, |item, idx| { + TextBlock::new(format!("{item} at {idx}")) + }) + .with_key_selector(|item| (*item).to_string()) + .build() + }; + let old_el = make(vec!["A", "B", "C"]); + let new_el = make(vec!["C", "A", "B"]); + + let mut r = Reconciler::new(RecordingBackend::new()); + let list_id = r + .reconcile(None, &old_el, None, noop_request_rerender()) + .unwrap(); + for row_idx in 0..3 { + r.backend.simulate_prepare_row(list_id, row_idx); + } + r.drain_realizations(); + let before = r.backend.row_contents_of(list_id); + + r.backend.clear_ops(); + let _ = r.reconcile( + Some(&old_el), + &new_el, + Some(list_id), + noop_request_rerender(), + ); + let after = r.backend.row_contents_of(list_id); + + assert_eq!(after[&0], before[&2]); + assert_eq!(after[&1], before[&0]); + assert_eq!(after[&2], before[&1]); + assert!( + !r.backend + .ops + .iter() + .any(|op| matches!(op, Op::Create { .. } | Op::Destroy { .. })), + "a keyed reorder should move realized controls: {:?}", + r.backend.ops + ); + assert!( + r.backend.ops.iter().any(|op| matches!( + op, + Op::SetProp { + prop: windows_reactor::Prop::Text, + value: windows_reactor::PropValue::Str(text), + .. + } if text == "C at 0" + )), + "the moved control must still receive index-dependent updates" + ); +} + +#[test] +fn keyed_reorder_does_not_detach_an_unchanged_realized_row() { + let make = |items: Vec<&'static str>| { + list_view(items, |item, _| TextBlock::new(*item)) + .with_key_selector(|item| (*item).to_string()) + .build() + }; + let old_el = make(vec!["A", "B", "C"]); + let new_el = make(vec!["A", "C", "B"]); + + let mut r = Reconciler::new(RecordingBackend::new()); + let list_id = r + .reconcile(None, &old_el, None, noop_request_rerender()) + .unwrap(); + for row_idx in 0..3 { + r.backend.simulate_prepare_row(list_id, row_idx); + } + r.drain_realizations(); + let before = r.backend.row_contents_of(list_id); + + r.backend.clear_ops(); + let _ = r.reconcile( + Some(&old_el), + &new_el, + Some(list_id), + noop_request_rerender(), + ); + let after = r.backend.row_contents_of(list_id); + + assert_eq!(after[&0], before[&0]); + assert!(!r.backend.ops.iter().any(|op| matches!( + op, + Op::ClearRowContent { + list_id: id, + row_idx: 0 + } if *id == list_id + ))); + assert!(!r.backend.ops.iter().any(|op| matches!( + op, + Op::MountRowContent { + list_id: id, + row_idx: 0, + .. + } if *id == list_id + ))); +} + +#[test] +fn keyed_reorder_clears_an_unchanged_slot_that_becomes_empty() { + let make = |items: Vec<(&'static str, bool)>| { + list_view(items, |(item, visible), _| { + if *visible { + TextBlock::new(*item).into() + } else { + Element::Empty + } + }) + .with_key_selector(|(item, _)| (*item).to_string()) + .build() + }; + let old_el = make(vec![("A", true), ("B", true), ("C", true)]); + let new_el = make(vec![("A", false), ("C", true), ("B", true)]); + + let mut r = Reconciler::new(RecordingBackend::new()); + let list_id = r + .reconcile(None, &old_el, None, noop_request_rerender()) + .unwrap(); + for row_idx in 0..3 { + r.backend.simulate_prepare_row(list_id, row_idx); + } + r.drain_realizations(); + + r.backend.clear_ops(); + let _ = r.reconcile( + Some(&old_el), + &new_el, + Some(list_id), + noop_request_rerender(), + ); + + assert!(!r.backend.row_contents_of(list_id).contains_key(&0)); + assert!(r.backend.ops.iter().any(|op| matches!( + op, + Op::ClearRowContent { + list_id: id, + row_idx: 0 + } if *id == list_id + ))); +} + +#[test] +fn duplicate_keys_keep_positional_realized_controls() { + let make = |items: Vec<&'static str>| { + list_view(items, |item, _| TextBlock::new(*item)) + .with_key_selector(|item| (*item).to_string()) + .build() + }; + let old_el = make(vec!["A", "A", "B"]); + let new_el = make(vec!["B", "A", "A"]); + + let mut r = Reconciler::new(RecordingBackend::new()); + let list_id = r + .reconcile(None, &old_el, None, noop_request_rerender()) + .unwrap(); + for row_idx in 0..3 { + r.backend.simulate_prepare_row(list_id, row_idx); + } + r.drain_realizations(); + let before = r.backend.row_contents_of(list_id); + + r.backend.clear_ops(); + let _ = r.reconcile( + Some(&old_el), + &new_el, + Some(list_id), + noop_request_rerender(), + ); + let after = r.backend.row_contents_of(list_id); + + assert_eq!(after, before, "duplicate keys must use positional fallback"); +} + +#[test] +fn keyed_reorder_replaces_only_rows_crossing_the_realized_boundary() { + let make = |items: Vec<&'static str>| { + list_view(items, |item, _| TextBlock::new(*item)) + .with_key_selector(|item| (*item).to_string()) + .build() + }; + let old_el = make(vec!["A", "B", "C", "D"]); + let new_el = make(vec!["B", "C", "D", "A"]); + + let mut r = Reconciler::new(RecordingBackend::new()); + let list_id = r + .reconcile(None, &old_el, None, noop_request_rerender()) + .unwrap(); + r.backend.simulate_prepare_row(list_id, 0); + r.backend.simulate_prepare_row(list_id, 1); + r.drain_realizations(); + let before = r.backend.row_contents_of(list_id); + + r.backend.clear_ops(); + let _ = r.reconcile( + Some(&old_el), + &new_el, + Some(list_id), + noop_request_rerender(), + ); + let after = r.backend.row_contents_of(list_id); + + assert_eq!( + after[&0], before[&1], + "B should retain its realized control" + ); + assert_ne!( + after[&1], before[&0], + "C entered the realized range and needs a new control" + ); + assert_eq!( + r.backend + .ops + .iter() + .filter(|op| matches!(op, Op::Create { .. })) + .count(), + 1 + ); + assert_eq!( + r.backend + .ops + .iter() + .filter(|op| matches!(op, Op::Destroy { .. })) + .count(), + 1 + ); +} diff --git a/crates/tests/libs/reactor/tests/templated_list_mutate.rs b/crates/tests/libs/reactor/tests/templated_list_mutate.rs index 0c0ed30c589..e0ac4498e2e 100644 --- a/crates/tests/libs/reactor/tests/templated_list_mutate.rs +++ b/crates/tests/libs/reactor/tests/templated_list_mutate.rs @@ -1,3 +1,4 @@ +use std::cell::Cell; use std::rc::Rc; use test_reactor::{Op, RecordingBackend}; @@ -134,6 +135,40 @@ fn identical_list_update_emits_zero_ops() { ); } +#[test] +fn content_update_checks_only_realized_keys_before_positional_refresh() { + let key_calls = Rc::new(Cell::new(0_usize)); + let make = |prefix: &'static str| { + let key_calls = Rc::clone(&key_calls); + list_view((0..10_000_u32).collect::>(), move |n, _| { + TextBlock::new(format!("{prefix}-{n}")) + }) + .with_key_selector(move |n| { + key_calls.set(key_calls.get() + 1); + format!("k{n}") + }) + .build() + }; + let old_el = make("old"); + let new_el = make("new"); + + let mut r = Reconciler::new(RecordingBackend::new()); + let list_id = r + .reconcile(None, &old_el, None, noop()) + .expect("mount produced an id"); + r.backend.simulate_prepare_row(list_id, 5_000); + r.drain_realizations(); + + key_calls.set(0); + let _ = r.reconcile(Some(&old_el), &new_el, Some(list_id), noop()); + + assert_eq!( + key_calls.get(), + 2, + "content-only updates should compare keys only at realized slots" + ); +} + #[test] fn removing_realized_row_frees_its_content() { let items: Vec = (0..5).collect(); diff --git a/crates/tests/libs/reactor_selftest/src/bindings.rs b/crates/tests/libs/reactor_selftest/src/bindings.rs index dc6e4a928eb..4781244f16a 100644 --- a/crates/tests/libs/reactor_selftest/src/bindings.rs +++ b/crates/tests/libs/reactor_selftest/src/bindings.rs @@ -830,6 +830,61 @@ unsafe impl Send for AutomationProperties {} unsafe impl Sync for AutomationProperties {} #[repr(transparent)] #[derive(Clone, Debug, Eq, PartialEq)] +pub struct BitmapIcon(windows_core::IUnknown); +windows_core::imp::interface_hierarchy!( + BitmapIcon, + windows_core::IUnknown, + windows_core::IInspectable +); +windows_core::imp::required_hierarchy!( + BitmapIcon, + IconElement, + FrameworkElement, + UIElement, + DependencyObject +); +impl BitmapIcon { + pub(crate) fn new() -> windows_core::Result { + Self::IBitmapIconFactory(|this| unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(this).CreateInstance)( + windows_core::Interface::as_raw(this), + core::ptr::null_mut(), + core::ptr::null_mut(), + &mut result__, + ) + .and_then(|| windows_core::Type::from_abi(result__)) + }) + } + fn IBitmapIconFactory windows_core::Result>( + callback: F, + ) -> windows_core::Result { + static SHARED: windows_core::imp::FactoryCache = + windows_core::imp::FactoryCache::new(); + SHARED.call(callback) + } +} +impl windows_core::RuntimeType for BitmapIcon { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_class::(); +} +unsafe impl windows_core::Interface for BitmapIcon { + type Vtable = ::Vtable; + const IID: windows_core::GUID = ::IID; +} +impl core::ops::Deref for BitmapIcon { + type Target = IBitmapIcon; + fn deref(&self) -> &Self::Target { + unsafe { core::mem::transmute(self) } + } +} +impl windows_core::RuntimeName for BitmapIcon { + const NAME: &'static str = "Microsoft.UI.Xaml.Controls.BitmapIcon"; +} +unsafe impl Send for BitmapIcon {} +unsafe impl Sync for BitmapIcon {} +#[repr(transparent)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct BitmapImage(windows_core::IUnknown); windows_core::imp::interface_hierarchy!( BitmapImage, @@ -2530,6 +2585,100 @@ unsafe impl Send for DependencyObject {} unsafe impl Sync for DependencyObject {} #[repr(transparent)] #[derive(Clone, Debug, Eq, PartialEq)] +pub struct DependencyProperty(windows_core::IUnknown); +windows_core::imp::interface_hierarchy!( + DependencyProperty, + windows_core::IUnknown, + windows_core::IInspectable +); +impl windows_core::RuntimeType for DependencyProperty { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_class::(); +} +unsafe impl windows_core::Interface for DependencyProperty { + type Vtable = ::Vtable; + const IID: windows_core::GUID = ::IID; +} +impl core::ops::Deref for DependencyProperty { + type Target = IDependencyProperty; + fn deref(&self) -> &Self::Target { + unsafe { core::mem::transmute(self) } + } +} +impl windows_core::RuntimeName for DependencyProperty { + const NAME: &'static str = "Microsoft.UI.Xaml.DependencyProperty"; +} +unsafe impl Send for DependencyProperty {} +unsafe impl Sync for DependencyProperty {} +windows_core::imp::define_interface!( + DependencyPropertyChangedCallback, + DependencyPropertyChangedCallback_Vtbl, + 0xf055bb21_219b_5b0c_805d_bcaedae15458 +); +impl windows_core::RuntimeType for DependencyPropertyChangedCallback { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +impl DependencyPropertyChangedCallback { + pub(crate) fn new< + F: Fn(windows_core::Ref, windows_core::Ref) + 'static, + >( + invoke: F, + ) -> Self { + let com = windows_core::imp::DelegateBox::::new( + &DependencyPropertyChangedCallbackBox::::VTABLE, + invoke, + ); + unsafe { core::mem::transmute(windows_core::imp::box_new(com)) } + } +} +#[repr(C)] +pub struct DependencyPropertyChangedCallback_Vtbl { + base__: windows_core::IUnknown_Vtbl, + Invoke: unsafe extern "system" fn( + this: *mut core::ffi::c_void, + sender: *mut core::ffi::c_void, + dp: *mut core::ffi::c_void, + ) -> windows_core::HRESULT, +} +struct DependencyPropertyChangedCallbackBox< + F: Fn(windows_core::Ref, windows_core::Ref) + 'static, +>(core::marker::PhantomData<(fn() -> F,)>); +impl, windows_core::Ref) + 'static> + DependencyPropertyChangedCallbackBox +{ + const VTABLE: DependencyPropertyChangedCallback_Vtbl = DependencyPropertyChangedCallback_Vtbl { + base__: + windows_core::IUnknown_Vtbl { + QueryInterface: windows_core::imp::DelegateBox::< + DependencyPropertyChangedCallback, + F, + >::QueryInterface, + AddRef: + windows_core::imp::DelegateBox::::AddRef, + Release: + windows_core::imp::DelegateBox::::Release, + }, + Invoke: Self::Invoke, + }; + unsafe extern "system" fn Invoke( + this: *mut core::ffi::c_void, + sender: *mut core::ffi::c_void, + dp: *mut core::ffi::c_void, + ) -> windows_core::HRESULT { + unsafe { + let this = &mut *(this as *mut *mut core::ffi::c_void + as *mut windows_core::imp::DelegateBox); + (this.invoke)( + core::mem::transmute_copy(&sender), + core::mem::transmute_copy(&dp), + ); + windows_core::HRESULT(0) + } + } +} +#[repr(transparent)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct DesktopAcrylicBackdrop(windows_core::IUnknown); windows_core::imp::interface_hierarchy!( DesktopAcrylicBackdrop, @@ -2971,6 +3120,40 @@ impl ElementCompositionPreview { .ok() }) } + pub(crate) fn SetImplicitShowAnimation( + element: P0, + animation: P1, + ) -> windows_core::Result<()> + where + P0: windows_core::Param, + P1: windows_core::Param, + { + Self::IElementCompositionPreviewStatics(|this| unsafe { + (windows_core::Interface::vtable(this).SetImplicitShowAnimation)( + windows_core::Interface::as_raw(this), + element.param().abi(), + animation.param().abi(), + ) + .ok() + }) + } + pub(crate) fn SetImplicitHideAnimation( + element: P0, + animation: P1, + ) -> windows_core::Result<()> + where + P0: windows_core::Param, + P1: windows_core::Param, + { + Self::IElementCompositionPreviewStatics(|this| unsafe { + (windows_core::Interface::vtable(this).SetImplicitHideAnimation)( + windows_core::Interface::as_raw(this), + element.param().abi(), + animation.param().abi(), + ) + .ok() + }) + } fn IElementCompositionPreviewStatics< R, F: FnOnce(&IElementCompositionPreviewStatics) -> windows_core::Result, @@ -3730,6 +3913,34 @@ unsafe impl Send for FrameworkTemplate {} unsafe impl Sync for FrameworkTemplate {} #[repr(transparent)] #[derive(Clone, Debug, Eq, PartialEq)] +pub struct Geometry(windows_core::IUnknown); +windows_core::imp::interface_hierarchy!( + Geometry, + windows_core::IUnknown, + windows_core::IInspectable +); +windows_core::imp::required_hierarchy!(Geometry, DependencyObject); +impl windows_core::RuntimeType for Geometry { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_class::(); +} +unsafe impl windows_core::Interface for Geometry { + type Vtable = ::Vtable; + const IID: windows_core::GUID = ::IID; +} +impl core::ops::Deref for Geometry { + type Target = IGeometry; + fn deref(&self) -> &Self::Target { + unsafe { core::mem::transmute(self) } + } +} +impl windows_core::RuntimeName for Geometry { + const NAME: &'static str = "Microsoft.UI.Xaml.Media.Geometry"; +} +unsafe impl Send for Geometry {} +unsafe impl Sync for Geometry {} +#[repr(transparent)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Grid(windows_core::IUnknown); windows_core::imp::interface_hierarchy!(Grid, windows_core::IUnknown, windows_core::IInspectable); windows_core::imp::required_hierarchy!(Grid, Panel, FrameworkElement, UIElement, DependencyObject); @@ -5004,6 +5215,93 @@ pub struct IAutomationPropertiesStatics_Vtbl { AutomationHeadingLevel, ) -> windows_core::HRESULT, } +windows_core::imp::define_interface!( + IBitmapIcon, + IBitmapIcon_Vtbl, + 0xc370bc29_805b_5bad_b615_ec640e579dbb +); +impl windows_core::RuntimeType for IBitmapIcon { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +impl IBitmapIcon { + pub(crate) fn UriSource(&self) -> windows_core::Result { + unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(self).UriSource)( + windows_core::Interface::as_raw(self), + &mut result__, + ) + .and_then(|| windows_core::Type::from_abi(result__)) + } + } + pub(crate) fn SetUriSource(&self, value: P0) -> windows_core::Result<()> + where + P0: windows_core::Param, + { + unsafe { + (windows_core::Interface::vtable(self).SetUriSource)( + windows_core::Interface::as_raw(self), + value.param().abi(), + ) + .ok() + } + } + pub(crate) fn ShowAsMonochrome(&self) -> windows_core::Result { + unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(self).ShowAsMonochrome)( + windows_core::Interface::as_raw(self), + &mut result__, + ) + .map(|| result__) + } + } + pub(crate) fn SetShowAsMonochrome(&self, value: bool) -> windows_core::Result<()> { + unsafe { + (windows_core::Interface::vtable(self).SetShowAsMonochrome)( + windows_core::Interface::as_raw(self), + value, + ) + .ok() + } + } +} +#[repr(C)] +pub struct IBitmapIcon_Vtbl { + pub base__: windows_core::IInspectable_Vtbl, + pub UriSource: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, + pub SetUriSource: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + ) -> windows_core::HRESULT, + pub ShowAsMonochrome: + unsafe extern "system" fn(*mut core::ffi::c_void, *mut bool) -> windows_core::HRESULT, + pub SetShowAsMonochrome: + unsafe extern "system" fn(*mut core::ffi::c_void, bool) -> windows_core::HRESULT, +} +windows_core::imp::define_interface!( + IBitmapIconFactory, + IBitmapIconFactory_Vtbl, + 0xb43b5ddc_cdb5_5ad6_8ac1_2fcca33be39e +); +impl windows_core::RuntimeType for IBitmapIconFactory { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +#[repr(C)] +pub struct IBitmapIconFactory_Vtbl { + pub base__: windows_core::IInspectable_Vtbl, + pub CreateInstance: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, +} windows_core::imp::define_interface!( IBitmapImage, IBitmapImage_Vtbl, @@ -6511,6 +6809,42 @@ pub struct ICommandBarFlyoutFactory_Vtbl { *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT, } +windows_core::imp::define_interface!( + ICompositionAnimationBase, + ICompositionAnimationBase_Vtbl, + 0xa77c0e5a_f059_4e85_bcef_c068694cec78 +); +impl windows_core::RuntimeType for ICompositionAnimationBase { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +windows_core::imp::interface_hierarchy!( + ICompositionAnimationBase, + windows_core::IUnknown, + windows_core::IInspectable +); +impl windows_core::RuntimeName for ICompositionAnimationBase { + const NAME: &'static str = "Microsoft.UI.Composition.ICompositionAnimationBase"; +} +pub trait ICompositionAnimationBase_Impl: windows_core::IUnknownImpl {} +impl ICompositionAnimationBase_Vtbl { + pub const fn new() -> Self { + Self { + base__: windows_core::IInspectable_Vtbl::new::< + Identity, + ICompositionAnimationBase, + OFFSET, + >(), + } + } + pub fn matches(iid: &windows_core::GUID) -> bool { + iid == &::IID + } +} +#[repr(C)] +pub struct ICompositionAnimationBase_Vtbl { + pub base__: windows_core::IInspectable_Vtbl, +} windows_core::imp::define_interface!( ICompositionObject, ICompositionObject_Vtbl, @@ -7382,9 +7716,77 @@ impl windows_core::RuntimeType for IDependencyObject { const SIGNATURE: windows_core::imp::ConstBuffer = windows_core::imp::ConstBuffer::for_interface::(); } +impl IDependencyObject { + pub(crate) fn RegisterPropertyChangedCallback( + &self, + dp: P0, + callback: P1, + ) -> windows_core::Result + where + P0: windows_core::Param, + P1: windows_core::Param, + { + unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(self).RegisterPropertyChangedCallback)( + windows_core::Interface::as_raw(self), + dp.param().abi(), + callback.param().abi(), + &mut result__, + ) + .map(|| result__) + } + } + pub(crate) fn UnregisterPropertyChangedCallback( + &self, + dp: P0, + token: i64, + ) -> windows_core::Result<()> + where + P0: windows_core::Param, + { + unsafe { + (windows_core::Interface::vtable(self).UnregisterPropertyChangedCallback)( + windows_core::Interface::as_raw(self), + dp.param().abi(), + token, + ) + .ok() + } + } +} #[repr(C)] pub struct IDependencyObject_Vtbl { pub base__: windows_core::IInspectable_Vtbl, + GetValue: usize, + SetValue: usize, + ClearValue: usize, + ReadLocalValue: usize, + GetAnimationBaseValue: usize, + pub RegisterPropertyChangedCallback: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + *mut core::ffi::c_void, + *mut i64, + ) -> windows_core::HRESULT, + pub UnregisterPropertyChangedCallback: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + i64, + ) -> windows_core::HRESULT, +} +windows_core::imp::define_interface!( + IDependencyProperty, + IDependencyProperty_Vtbl, + 0x960eab49_9672_58a0_995b_3a42e5ea6278 +); +impl windows_core::RuntimeType for IDependencyProperty { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +#[repr(C)] +pub struct IDependencyProperty_Vtbl { + pub base__: windows_core::IInspectable_Vtbl, } windows_core::imp::define_interface!( IDesktopAcrylicBackdrop, @@ -7833,6 +8235,17 @@ pub struct IElementCompositionPreviewStatics_Vtbl { *mut core::ffi::c_void, *mut core::ffi::c_void, ) -> windows_core::HRESULT, + GetScrollViewerManipulationPropertySet: usize, + pub SetImplicitShowAnimation: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + *mut core::ffi::c_void, + ) -> windows_core::HRESULT, + pub SetImplicitHideAnimation: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + *mut core::ffi::c_void, + ) -> windows_core::HRESULT, } windows_core::imp::define_interface!( IEllipse, @@ -8691,6 +9104,19 @@ impl windows_core::RuntimeType for IFrameworkTemplate { pub struct IFrameworkTemplate_Vtbl { pub base__: windows_core::IInspectable_Vtbl, } +windows_core::imp::define_interface!( + IGeometry, + IGeometry_Vtbl, + 0xdc102dcc_3be2_5414_8599_94b6e76ef39b +); +impl windows_core::RuntimeType for IGeometry { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +#[repr(C)] +pub struct IGeometry_Vtbl { + pub base__: windows_core::IInspectable_Vtbl, +} windows_core::imp::define_interface!(IGrid, IGrid_Vtbl, 0xc4496219_9014_58a1_b4ad_c5044913a5bb); impl windows_core::RuntimeType for IGrid { const SIGNATURE: windows_core::imp::ConstBuffer = @@ -10429,6 +10855,16 @@ impl windows_core::RuntimeType for INavigationView { windows_core::imp::ConstBuffer::for_interface::(); } impl INavigationView { + pub(crate) fn IsPaneOpen(&self) -> windows_core::Result { + unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(self).IsPaneOpen)( + windows_core::Interface::as_raw(self), + &mut result__, + ) + .map(|| result__) + } + } pub(crate) fn SetIsPaneOpen(&self, value: bool) -> windows_core::Result<()> { unsafe { (windows_core::Interface::vtable(self).SetIsPaneOpen)( @@ -10462,6 +10898,16 @@ impl INavigationView { .ok() } } + pub(crate) fn DisplayMode(&self) -> windows_core::Result { + unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(self).DisplayMode)( + windows_core::Interface::as_raw(self), + &mut result__, + ) + .map(|| result__) + } + } pub(crate) fn SetIsSettingsVisible(&self, value: bool) -> windows_core::Result<()> { unsafe { (windows_core::Interface::vtable(self).SetIsSettingsVisible)( @@ -10579,7 +11025,8 @@ impl INavigationView { #[repr(C)] pub struct INavigationView_Vtbl { pub base__: windows_core::IInspectable_Vtbl, - IsPaneOpen: usize, + pub IsPaneOpen: + unsafe extern "system" fn(*mut core::ffi::c_void, *mut bool) -> windows_core::HRESULT, pub SetIsPaneOpen: unsafe extern "system" fn(*mut core::ffi::c_void, bool) -> windows_core::HRESULT, CompactModeThresholdWidth: usize, @@ -10601,7 +11048,10 @@ pub struct INavigationView_Vtbl { ) -> windows_core::HRESULT, HeaderTemplate: usize, SetHeaderTemplate: usize, - DisplayMode: usize, + pub DisplayMode: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut NavigationViewDisplayMode, + ) -> windows_core::HRESULT, IsSettingsVisible: usize, pub SetIsSettingsVisible: unsafe extern "system" fn(*mut core::ffi::c_void, bool) -> windows_core::HRESULT, @@ -10816,6 +11266,16 @@ impl windows_core::RuntimeType for INavigationViewItem { windows_core::imp::ConstBuffer::for_interface::(); } impl INavigationViewItem { + pub(crate) fn Icon(&self) -> windows_core::Result { + unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(self).Icon)( + windows_core::Interface::as_raw(self), + &mut result__, + ) + .and_then(|| windows_core::Type::from_abi(result__)) + } + } pub(crate) fn SetIcon(&self, value: P0) -> windows_core::Result<()> where P0: windows_core::Param, @@ -10832,7 +11292,10 @@ impl INavigationViewItem { #[repr(C)] pub struct INavigationViewItem_Vtbl { pub base__: windows_core::IInspectable_Vtbl, - Icon: usize, + pub Icon: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, pub SetIcon: unsafe extern "system" fn( *mut core::ffi::c_void, *mut core::ffi::c_void, @@ -10970,6 +11433,34 @@ pub struct INavigationViewSelectionChangedEventArgs_Vtbl { *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT, } +windows_core::imp::define_interface!( + INavigationViewStatics, + INavigationViewStatics_Vtbl, + 0xdcd04caf_1904_564b_b0de_babaff9962f5 +); +impl windows_core::RuntimeType for INavigationViewStatics { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +#[repr(C)] +pub struct INavigationViewStatics_Vtbl { + pub base__: windows_core::IInspectable_Vtbl, + pub IsPaneOpenProperty: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, + CompactModeThresholdWidthProperty: usize, + ExpandedModeThresholdWidthProperty: usize, + FooterMenuItemsProperty: usize, + FooterMenuItemsSourceProperty: usize, + PaneFooterProperty: usize, + HeaderProperty: usize, + HeaderTemplateProperty: usize, + pub DisplayModeProperty: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, +} windows_core::imp::define_interface!( INumberBox, INumberBox_Vtbl, @@ -11459,6 +11950,54 @@ pub struct IPasswordBox_Vtbl { pub RemovePasswordChanged: unsafe extern "system" fn(*mut core::ffi::c_void, i64) -> windows_core::HRESULT, } +windows_core::imp::define_interface!( + IPathIcon, + IPathIcon_Vtbl, + 0x5c8229db_51cd_5a3b_88ef_1d9a8ac97683 +); +impl windows_core::RuntimeType for IPathIcon { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +impl IPathIcon { + pub(crate) fn Data(&self) -> windows_core::Result { + unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(self).Data)( + windows_core::Interface::as_raw(self), + &mut result__, + ) + .and_then(|| windows_core::Type::from_abi(result__)) + } + } +} +#[repr(C)] +pub struct IPathIcon_Vtbl { + pub base__: windows_core::IInspectable_Vtbl, + pub Data: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, +} +windows_core::imp::define_interface!( + IPathIconFactory, + IPathIconFactory_Vtbl, + 0x8e88f087_f2cd_581c_91ca_a99335ca9599 +); +impl windows_core::RuntimeType for IPathIconFactory { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +#[repr(C)] +pub struct IPathIconFactory_Vtbl { + pub base__: windows_core::IInspectable_Vtbl, + pub CreateInstance: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, +} windows_core::imp::define_interface!( IPersonPicture, IPersonPicture_Vtbl, @@ -11702,6 +12241,19 @@ pub struct IPivotItemFactory_Vtbl { *mut *mut core::ffi::c_void, ) -> windows_core::HRESULT, } +windows_core::imp::define_interface!( + IPointer, + IPointer_Vtbl, + 0x1f9afbf5_11a3_5e68_aa1b_72febfa0ab23 +); +impl windows_core::RuntimeType for IPointer { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_interface::(); +} +#[repr(C)] +pub struct IPointer_Vtbl { + pub base__: windows_core::IInspectable_Vtbl, +} windows_core::imp::define_interface!( IPointerPoint, IPointerPoint_Vtbl, @@ -11816,6 +12368,16 @@ impl windows_core::RuntimeType for IPointerRoutedEventArgs { windows_core::imp::ConstBuffer::for_interface::(); } impl IPointerRoutedEventArgs { + pub(crate) fn Pointer(&self) -> windows_core::Result { + unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(self).Pointer)( + windows_core::Interface::as_raw(self), + &mut result__, + ) + .and_then(|| windows_core::Type::from_abi(result__)) + } + } pub(crate) fn GetCurrentPoint(&self, relativeto: P0) -> windows_core::Result where P0: windows_core::Param, @@ -11834,7 +12396,10 @@ impl IPointerRoutedEventArgs { #[repr(C)] pub struct IPointerRoutedEventArgs_Vtbl { pub base__: windows_core::IInspectable_Vtbl, - Pointer: usize, + pub Pointer: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut *mut core::ffi::c_void, + ) -> windows_core::HRESULT, KeyModifiers: usize, Handled: usize, SetHandled: usize, @@ -17166,6 +17731,70 @@ impl IUIElement { )) } } + pub(crate) fn PointerCaptureLost( + &self, + handler: F, + ) -> windows_core::Result + where + F: Fn( + windows_core::Ref, + windows_core::Ref, + ) + 'static, + { + let handler: PointerEventHandler = { + let com = windows_core::imp::DelegateBox::::new( + &PointerEventHandlerBox::::VTABLE, + handler, + ); + unsafe { core::mem::transmute(windows_core::imp::box_new(com)) } + }; + unsafe { + let mut result__ = core::mem::zeroed(); + let token__ = (windows_core::Interface::vtable(self).PointerCaptureLost)( + windows_core::Interface::as_raw(self), + windows_core::Interface::as_raw(&handler), + &mut result__, + ) + .map(|| result__)?; + Ok(windows_core::EventRevoker::new( + self.clone(), + token__, + windows_core::Interface::vtable(self).RemovePointerCaptureLost, + )) + } + } + pub(crate) fn PointerCanceled( + &self, + handler: F, + ) -> windows_core::Result + where + F: Fn( + windows_core::Ref, + windows_core::Ref, + ) + 'static, + { + let handler: PointerEventHandler = { + let com = windows_core::imp::DelegateBox::::new( + &PointerEventHandlerBox::::VTABLE, + handler, + ); + unsafe { core::mem::transmute(windows_core::imp::box_new(com)) } + }; + unsafe { + let mut result__ = core::mem::zeroed(); + let token__ = (windows_core::Interface::vtable(self).PointerCanceled)( + windows_core::Interface::as_raw(self), + windows_core::Interface::as_raw(&handler), + &mut result__, + ) + .map(|| result__)?; + Ok(windows_core::EventRevoker::new( + self.clone(), + token__, + windows_core::Interface::vtable(self).RemovePointerCanceled, + )) + } + } pub(crate) fn Tapped(&self, handler: F) -> windows_core::Result where F: Fn( @@ -17227,6 +17856,40 @@ impl IUIElement { )) } } + pub(crate) fn CapturePointer(&self, value: P0) -> windows_core::Result + where + P0: windows_core::Param, + { + unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(self).CapturePointer)( + windows_core::Interface::as_raw(self), + value.param().abi(), + &mut result__, + ) + .map(|| result__) + } + } + pub(crate) fn ReleasePointerCapture(&self, value: P0) -> windows_core::Result<()> + where + P0: windows_core::Param, + { + unsafe { + (windows_core::Interface::vtable(self).ReleasePointerCapture)( + windows_core::Interface::as_raw(self), + value.param().abi(), + ) + .ok() + } + } + pub(crate) fn ReleasePointerCaptures(&self) -> windows_core::Result<()> { + unsafe { + (windows_core::Interface::vtable(self).ReleasePointerCaptures)( + windows_core::Interface::as_raw(self), + ) + .ok() + } + } pub(crate) fn UpdateLayout(&self) -> windows_core::Result<()> { unsafe { (windows_core::Interface::vtable(self).UpdateLayout)(windows_core::Interface::as_raw( @@ -17455,10 +18118,20 @@ pub struct IUIElement_Vtbl { ) -> windows_core::HRESULT, pub RemovePointerExited: unsafe extern "system" fn(*mut core::ffi::c_void, i64) -> windows_core::HRESULT, - PointerCaptureLost: usize, - RemovePointerCaptureLost: usize, - PointerCanceled: usize, - RemovePointerCanceled: usize, + pub PointerCaptureLost: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + *mut i64, + ) -> windows_core::HRESULT, + pub RemovePointerCaptureLost: + unsafe extern "system" fn(*mut core::ffi::c_void, i64) -> windows_core::HRESULT, + pub PointerCanceled: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + *mut i64, + ) -> windows_core::HRESULT, + pub RemovePointerCanceled: + unsafe extern "system" fn(*mut core::ffi::c_void, i64) -> windows_core::HRESULT, PointerWheelChanged: usize, RemovePointerWheelChanged: usize, pub Tapped: unsafe extern "system" fn( @@ -17515,9 +18188,17 @@ pub struct IUIElement_Vtbl { RemoveBringIntoViewRequested: usize, Measure: usize, Arrange: usize, - CapturePointer: usize, - ReleasePointerCapture: usize, - ReleasePointerCaptures: usize, + pub CapturePointer: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + *mut bool, + ) -> windows_core::HRESULT, + pub ReleasePointerCapture: unsafe extern "system" fn( + *mut core::ffi::c_void, + *mut core::ffi::c_void, + ) -> windows_core::HRESULT, + pub ReleasePointerCaptures: + unsafe extern "system" fn(*mut core::ffi::c_void) -> windows_core::HRESULT, AddHandler: usize, RemoveHandler: usize, TransformToVisual: usize, @@ -19727,6 +20408,26 @@ impl NavigationView { .and_then(|| windows_core::Type::from_abi(result__)) }) } + pub(crate) fn IsPaneOpenProperty() -> windows_core::Result { + Self::INavigationViewStatics(|this| unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(this).IsPaneOpenProperty)( + windows_core::Interface::as_raw(this), + &mut result__, + ) + .and_then(|| windows_core::Type::from_abi(result__)) + }) + } + pub(crate) fn DisplayModeProperty() -> windows_core::Result { + Self::INavigationViewStatics(|this| unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(this).DisplayModeProperty)( + windows_core::Interface::as_raw(this), + &mut result__, + ) + .and_then(|| windows_core::Type::from_abi(result__)) + }) + } fn INavigationViewFactory windows_core::Result>( callback: F, ) -> windows_core::Result { @@ -19734,6 +20435,13 @@ impl NavigationView { windows_core::imp::FactoryCache::new(); SHARED.call(callback) } + fn INavigationViewStatics windows_core::Result>( + callback: F, + ) -> windows_core::Result { + static SHARED: windows_core::imp::FactoryCache = + windows_core::imp::FactoryCache::new(); + SHARED.call(callback) + } } impl windows_core::RuntimeType for NavigationView { const SIGNATURE: windows_core::imp::ConstBuffer = @@ -19798,6 +20506,22 @@ impl windows_core::RuntimeName for NavigationViewBackRequestedEventArgs { unsafe impl Send for NavigationViewBackRequestedEventArgs {} unsafe impl Sync for NavigationViewBackRequestedEventArgs {} #[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct NavigationViewDisplayMode(pub i32); +impl NavigationViewDisplayMode { + pub const Minimal: Self = Self(0); + pub const Compact: Self = Self(1); + pub const Expanded: Self = Self(2); +} +impl windows_core::TypeKind for NavigationViewDisplayMode { + type TypeKind = windows_core::CopyType; +} +impl windows_core::RuntimeType for NavigationViewDisplayMode { + const SIGNATURE: windows_core::imp::ConstBuffer = windows_core::imp::ConstBuffer::from_slice( + b"enum(Microsoft.UI.Xaml.Controls.NavigationViewDisplayMode;i4)", + ); +} +#[repr(transparent)] #[derive(Clone, Debug, Eq, PartialEq)] pub struct NavigationViewItem(windows_core::IUnknown); windows_core::imp::interface_hierarchy!( @@ -20274,6 +20998,61 @@ impl windows_core::RuntimeType for PasswordRevealMode { ); } #[repr(transparent)] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PathIcon(windows_core::IUnknown); +windows_core::imp::interface_hierarchy!( + PathIcon, + windows_core::IUnknown, + windows_core::IInspectable +); +windows_core::imp::required_hierarchy!( + PathIcon, + IconElement, + FrameworkElement, + UIElement, + DependencyObject +); +impl PathIcon { + pub(crate) fn new() -> windows_core::Result { + Self::IPathIconFactory(|this| unsafe { + let mut result__ = core::mem::zeroed(); + (windows_core::Interface::vtable(this).CreateInstance)( + windows_core::Interface::as_raw(this), + core::ptr::null_mut(), + core::ptr::null_mut(), + &mut result__, + ) + .and_then(|| windows_core::Type::from_abi(result__)) + }) + } + fn IPathIconFactory windows_core::Result>( + callback: F, + ) -> windows_core::Result { + static SHARED: windows_core::imp::FactoryCache = + windows_core::imp::FactoryCache::new(); + SHARED.call(callback) + } +} +impl windows_core::RuntimeType for PathIcon { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_class::(); +} +unsafe impl windows_core::Interface for PathIcon { + type Vtable = ::Vtable; + const IID: windows_core::GUID = ::IID; +} +impl core::ops::Deref for PathIcon { + type Target = IPathIcon; + fn deref(&self) -> &Self::Target { + unsafe { core::mem::transmute(self) } + } +} +impl windows_core::RuntimeName for PathIcon { + const NAME: &'static str = "Microsoft.UI.Xaml.Controls.PathIcon"; +} +unsafe impl Send for PathIcon {} +unsafe impl Sync for PathIcon {} +#[repr(transparent)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct PatternInterface(pub i32); impl PatternInterface { @@ -20514,6 +21293,33 @@ impl windows_core::RuntimeType for Point { const SIGNATURE: windows_core::imp::ConstBuffer = windows_core::imp::ConstBuffer::from_slice(b"struct(Windows.Foundation.Point;f4;f4)"); } +#[repr(transparent)] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Pointer(windows_core::IUnknown); +windows_core::imp::interface_hierarchy!( + Pointer, + windows_core::IUnknown, + windows_core::IInspectable +); +impl windows_core::RuntimeType for Pointer { + const SIGNATURE: windows_core::imp::ConstBuffer = + windows_core::imp::ConstBuffer::for_class::(); +} +unsafe impl windows_core::Interface for Pointer { + type Vtable = ::Vtable; + const IID: windows_core::GUID = ::IID; +} +impl core::ops::Deref for Pointer { + type Target = IPointer; + fn deref(&self) -> &Self::Target { + unsafe { core::mem::transmute(self) } + } +} +impl windows_core::RuntimeName for Pointer { + const NAME: &'static str = "Microsoft.UI.Xaml.Input.Pointer"; +} +unsafe impl Send for Pointer {} +unsafe impl Sync for Pointer {} windows_core::imp::define_interface!( PointerEventHandler, PointerEventHandler_Vtbl, diff --git a/crates/tests/libs/reactor_selftest/src/fixtures/controls.rs b/crates/tests/libs/reactor_selftest/src/fixtures/controls.rs index 356195e083e..987d353f4ae 100644 --- a/crates/tests/libs/reactor_selftest/src/fixtures/controls.rs +++ b/crates/tests/libs/reactor_selftest/src/fixtures/controls.rs @@ -1,11 +1,11 @@ use windows_reactor::BreadcrumbBar; use windows_reactor::{ Canvas, ComboBox, Expander, HyperlinkButton, Image, InfoBadge, InfoBar, NavViewItem, - NavigationView, NumberBox, PasswordBox, PasswordRevealMode, PersonPicture, Pivot, PivotItem, - ProgressBar, ProgressRing, RadioButton, RadioButtons, Shape, Slider, TabItem, TabView, - TitleBar, ToggleSwitch, Viewbox, + NavigationView, NavigationViewDisplayMode, NavigationViewPaneDisplayMode, NumberBox, + PasswordBox, PasswordRevealMode, PersonPicture, Pivot, PivotItem, ProgressBar, ProgressRing, + RadioButton, RadioButtons, Shape, Slider, TabItem, TabView, TitleBar, ToggleSwitch, Viewbox, }; -use windows_reactor::{Color, GridLength}; +use windows_reactor::{Color, CornerRadius, GridLength, RenderCx, Thickness, component}; use windows_reactor::{ ElementExt, border, button, check_box, scroll_viewer, swap_chain_panel, text_block, text_box, }; @@ -19,6 +19,83 @@ use crate::fixtures::reconciler::{FixtureFuture, cc}; use crate::harness::Harness; use windows_reactor::{grid, vstack}; +fn resource_map( + button: &bindings::Button, +) -> windows_collections::IMap { + button + .cast::() + .unwrap() + .Resources() + .unwrap() + .cast() + .unwrap() +} + +fn resource_value( + map: &windows_collections::IMap, + key: &str, +) -> Option { + map.Lookup(&windows_reference::IReference::from(key)).ok() +} + +fn resource_string( + map: &windows_collections::IMap, + key: &str, +) -> Option { + resource_value(map, key)? + .cast::>() + .ok()? + .Value() + .ok() + .map(|value| value.to_string_lossy()) +} + +fn resource_thickness( + map: &windows_collections::IMap, + key: &str, +) -> Option { + resource_value(map, key)? + .cast::>() + .ok()? + .Value() + .ok() +} + +fn resource_corner_radius( + map: &windows_collections::IMap, + key: &str, +) -> Option { + resource_value(map, key)? + .cast::>() + .ok()? + .Value() + .ok() +} + +fn has_resource_key( + map: &windows_collections::IMap, + key: &str, +) -> bool { + map.HasKey(&windows_reference::IReference::from(key)) + .unwrap_or(false) +} + +fn first_tab_item_tag(h: &Harness) -> Option { + let tab_view = h + .find_all::(&|_| true) + .into_iter() + .next()?; + let item = tab_view.TabItems().ok()?.GetAt(0).ok()?; + let tab_item: bindings::TabViewItem = item.cast().ok()?; + let tag = tab_item + .cast::() + .ok()? + .Tag() + .ok()?; + let value: windows_reference::IReference = tag.cast().ok()?; + value.Value().ok().map(|value| value.to_string_lossy()) +} + macro_rules! assert_present { ($h:expr, $name:expr, $ty:ty) => {{ let n = $h.count_controls::<$ty>(); @@ -237,6 +314,33 @@ pub fn mount_tab_view(h: Harness) -> FixtureFuture { }) } +pub fn tab_item_key_clear(h: Harness) -> FixtureFuture { + Box::pin(async move { + h.mount(cc(|cx| { + let (keyed, set_keyed) = cx.use_state(true); + let mut item = TabItem::new("Document", text_block("document-content")); + if keyed { + item = item.with_key("document"); + } + + vstack(( + button("Toggle tab key").on_click(move || set_keyed.call(!keyed)), + TabView::new([item]), + )) + .into() + })); + h.render().await; + h.check( + "TabView_ItemKey_InitiallySet", + first_tab_item_tag(&h).as_deref() == Some("document"), + ); + + let _ = h.click_button("Toggle tab key"); + h.render().await; + h.check("TabView_ItemKey_Cleared", first_tab_item_tag(&h).is_none()); + }) +} + pub fn mount_tab_view_add_button(h: Harness) -> FixtureFuture { Box::pin(async move { h.mount(cc(|_| { @@ -274,6 +378,110 @@ pub fn mount_navigation_view(h: Harness) -> FixtureFuture { }) } +fn navigation_display_mode_name(mode: i32) -> &'static str { + match mode { + value if value == NavigationViewDisplayMode::Minimal.0 => "Minimal", + value if value == NavigationViewDisplayMode::Compact.0 => "Compact", + value if value == NavigationViewDisplayMode::Expanded.0 => "Expanded", + _ => "Unknown", + } +} + +pub fn navigation_view_state_callbacks(h: Harness) -> FixtureFuture { + Box::pin(async move { + h.mount(cc(|cx| { + let (pane_open, set_pane_open) = cx.use_state(true); + let (display_mode, set_display_mode) = + cx.use_state(NavigationViewDisplayMode::Expanded); + + vstack(( + button("Toggle navigation pane").on_click({ + let set_pane_open = set_pane_open.clone(); + move || set_pane_open.call(!pane_open) + }), + text_block(if pane_open { + "Pane state: open" + } else { + "Pane state: closed" + }), + text_block(format!( + "Display mode: {}", + navigation_display_mode_name(display_mode.0) + )), + NavigationView::new( + [NavViewItem::new("Home").tag("home")], + text_block("navigation body"), + ) + .pane_open(pane_open) + .pane_display_mode(NavigationViewPaneDisplayMode::Left) + .on_pane_open_changed(move |open| set_pane_open.call(open)) + .on_display_mode_changed(move |mode| set_display_mode.call(mode)) + .settings_visible(false), + )) + .into() + })); + h.render().await; + + let navigation = h + .find_all::(&|_| true) + .into_iter() + .next() + .unwrap(); + h.check( + "NavigationState_StartsOpen", + navigation.IsPaneOpen().unwrap_or(false), + ); + + navigation.SetIsPaneOpen(false).unwrap(); + h.render_until("navigation pane close callback", |h| { + h.find_text("Pane state: closed").is_some() + }) + .await; + h.check( + "NavigationState_ControlCloseReported", + !navigation.IsPaneOpen().unwrap_or(true), + ); + + let _ = h.click_button("Toggle navigation pane"); + h.render_until("navigation pane reopen", |_| { + navigation.IsPaneOpen().unwrap_or(false) + }) + .await; + h.check( + "NavigationState_OneToggleReopens", + navigation.IsPaneOpen().unwrap_or(false), + ); + + navigation + .cast::() + .unwrap() + .SetPaneDisplayMode(bindings::NavigationViewPaneDisplayMode::Auto) + .unwrap(); + for (width, expected) in [ + (1200.0, NavigationViewDisplayMode::Expanded), + (800.0, NavigationViewDisplayMode::Compact), + (500.0, NavigationViewDisplayMode::Minimal), + ] { + navigation + .cast::() + .unwrap() + .SetWidth(width) + .unwrap(); + let expected_name = navigation_display_mode_name(expected.0); + let reached = h + .render_until("navigation responsive display mode", |h| { + navigation + .DisplayMode() + .is_ok_and(|mode| mode.0 == expected.0) + && h.find_text(&format!("Display mode: {expected_name}")) + .is_some() + }) + .await; + h.check(&format!("NavigationState_AutoMode{expected_name}"), reached); + } + }) +} + pub fn mount_pivot(h: Harness) -> FixtureFuture { Box::pin(async move { h.mount(cc(|_| { @@ -521,6 +729,73 @@ pub fn mount_virtual_list_alias(h: Harness) -> FixtureFuture { }) } +#[derive(Clone, PartialEq)] +struct KeyedRowProps { + name: String, +} + +fn keyed_row(props: &KeyedRowProps, cx: &mut RenderCx) -> windows_reactor::Element { + let (clicks, set_clicks) = cx.use_state(0_u32); + vstack(( + text_block(format!("{}: {clicks}", props.name)), + button(format!("Increment {}", props.name)).on_click(move || set_clicks.call(clicks + 1)), + )) + .into() +} + +pub fn keyed_templated_list_state(h: Harness) -> FixtureFuture { + Box::pin(async move { + h.mount(cc(|cx| { + let (items, set_items) = cx.use_state(vec![ + "Alpha".to_string(), + "Beta".to_string(), + "Gamma".to_string(), + "Delta".to_string(), + ]); + let rotated = { + let mut items = items.clone(); + items.rotate_left(1); + items + }; + + vstack(( + button("Rotate keyed rows").on_click(move || set_items.call(rotated.clone())), + list_view(items, |name, _| { + component(keyed_row, KeyedRowProps { name: name.clone() }) + }) + .with_key_selector(|name| name.clone()) + .height(240.0), + )) + .into() + })); + + let realized = h + .render_until("keyed list rows to realize", |h| { + h.find_text("Alpha: 0").is_some() && h.find_text("Delta: 0").is_some() + }) + .await; + h.check("Reconciler_KeyedList_RowsRealized", realized); + + let _ = h.click_button("Increment Alpha"); + h.render().await; + h.check( + "Reconciler_KeyedList_RowStateUpdates", + h.find_text("Alpha: 1").is_some(), + ); + + let _ = h.click_button("Rotate keyed rows"); + let rotated = h + .render_until("keyed rows to rotate", |h| { + h.find_text("Alpha: 1").is_some() + && h.find_text("Beta: 0").is_some() + && h.find_text("Gamma: 0").is_some() + && h.find_text("Delta: 0").is_some() + }) + .await; + h.check("Reconciler_KeyedList_StateFollowsKey", rotated); + }) +} + pub fn mount_password_box(h: Harness) -> FixtureFuture { Box::pin(async move { h.mount(cc(|_| { @@ -646,6 +921,141 @@ pub fn mount_button_text_link(h: Harness) -> FixtureFuture { }) } +pub fn lightweight_resources(h: Harness) -> FixtureFuture { + Box::pin(async move { + h.mount(cc(|cx| { + let (mode, set_mode) = cx.use_state(0_u8); + let target = match mode { + 0 => button("Resource target").resource_overrides(|resources| { + resources + .set("ButtonBackground", Color::rgb(178, 34, 34)) + .set("ReactorScalar", 1.0) + .set("ReactorString", "Destructive") + .set("ReactorThickness", Thickness::uniform(3.0)) + .set("ReactorCornerRadius", CornerRadius::uniform(6.0)) + }), + 1 => button("Resource target").resource_overrides(|resources| { + resources + .set("ButtonBackground", Color::rgb(30, 90, 180)) + .set("ReactorScalar", 2.0) + .set("ReactorString", "Updated") + .set("ReactorThickness", Thickness::uniform(4.0)) + .set("ReactorCornerRadius", CornerRadius::uniform(8.0)) + }), + _ => button("Resource target"), + }; + vstack(( + target, + button(match mode { + 0 => "Update resources", + 1 => "Clear resources", + _ => "Resources cleared", + }) + .on_click(move || set_mode.call(mode.saturating_add(1))), + )) + .into() + })); + h.render().await; + + let target = h.find_button("Resource target").unwrap(); + let map = resource_map(&target); + let initial_brush = resource_value(&map, "ButtonBackground"); + h.check( + "Resources_TypedBrush", + initial_brush + .as_ref() + .is_some_and(|value| value.cast::().is_ok()), + ); + h.check( + "Resources_TypedNumber", + resource_value(&map, "ReactorScalar") + .and_then(|value| value.cast::>().ok()) + .and_then(|value| value.Value().ok()) + == Some(1.0), + ); + h.check( + "Resources_TypedString", + resource_string(&map, "ReactorString").as_deref() == Some("Destructive"), + ); + h.check( + "Resources_TypedThickness", + resource_thickness(&map, "ReactorThickness") + == Some(bindings::Thickness { + left: 3.0, + top: 3.0, + right: 3.0, + bottom: 3.0, + }), + ); + h.check( + "Resources_TypedCornerRadius", + resource_corner_radius(&map, "ReactorCornerRadius") + == Some(bindings::CornerRadius { + top_left: 6.0, + top_right: 6.0, + bottom_right: 6.0, + bottom_left: 6.0, + }), + ); + + let _ = h.click_button("Update resources"); + h.render().await; + let target = h.find_button("Resource target").unwrap(); + let map = resource_map(&target); + let updated_brush = resource_value(&map, "ButtonBackground"); + h.check( + "Resources_UpdateBrush", + initial_brush + .zip(updated_brush.clone()) + .is_some_and(|(old, new)| old != new), + ); + h.check( + "Resources_UpdateNumber", + resource_value(&map, "ReactorScalar") + .and_then(|value| value.cast::>().ok()) + .and_then(|value| value.Value().ok()) + == Some(2.0), + ); + h.check( + "Resources_UpdateTypedValues", + resource_string(&map, "ReactorString").as_deref() == Some("Updated") + && resource_thickness(&map, "ReactorThickness") + .is_some_and(|value| value.left == 4.0) + && resource_corner_radius(&map, "ReactorCornerRadius") + .is_some_and(|value| value.top_left == 8.0), + ); + + let unrelated_key = windows_reference::IReference::from("UnrelatedResource"); + let unrelated_value = windows_reference::IReference::from("keep"); + map.Insert(&unrelated_key, &unrelated_value).unwrap(); + + let _ = h.click_button("Clear resources"); + h.render().await; + let target = h.find_button("Resource target").unwrap(); + let map = resource_map(&target); + h.check( + "Resources_ClearOwnedBrush", + updated_brush + .zip(resource_value(&map, "ButtonBackground")) + .is_some_and(|(old, new)| old != new), + ); + h.check( + "Resources_ClearOwnedNumber", + !has_resource_key(&map, "ReactorScalar"), + ); + h.check( + "Resources_ClearOwnedTypedValues", + !has_resource_key(&map, "ReactorString") + && !has_resource_key(&map, "ReactorThickness") + && !has_resource_key(&map, "ReactorCornerRadius"), + ); + h.check( + "Resources_PreserveUnrelatedKeys", + has_resource_key(&map, "UnrelatedResource"), + ); + }) +} + pub fn mount_person_picture(h: Harness) -> FixtureFuture { Box::pin(async move { h.mount(cc(|_| PersonPicture::new().initials("AB").into())); diff --git a/crates/tests/libs/reactor_selftest/src/fixtures/interactions.rs b/crates/tests/libs/reactor_selftest/src/fixtures/interactions.rs index da84885847e..617fa70cadc 100644 --- a/crates/tests/libs/reactor_selftest/src/fixtures/interactions.rs +++ b/crates/tests/libs/reactor_selftest/src/fixtures/interactions.rs @@ -3,10 +3,15 @@ //! fires and the next render reflects the new state. These complement the //! purely-structural `mount_*` fixtures, which only assert initial render. +use std::time::Duration; + use windows_core::Interface as _; +use windows_reactor::AnimationConfig; use windows_reactor::Element; use windows_reactor::Icon; +use windows_reactor::NavViewItem; +use windows_reactor::NavigationView; use windows_reactor::Symbol; use windows_reactor::vstack; use windows_reactor::{ComboBox, PasswordBox, RadioButtons, Slider, ToggleSwitch}; @@ -488,14 +493,27 @@ pub fn button_icon_glyph_change_preserves_text(h: Harness) -> FixtureFuture { } /// Verify that the non-`Symbol` [`Icon`](windows_reactor::Icon) kinds construct -/// and attach real WinUI elements. -pub fn button_image_and_font_icons(h: Harness) -> FixtureFuture { +/// and attach the intended WinUI `IconElement` subclasses. +pub fn button_icon_subclasses(h: Harness) -> FixtureFuture { Box::pin(async move { h.mount(cc(|_cx| { vstack(( button("Starred").icon(Icon::font("\u{E734}")), button("Raster").icon(Icon::image("ms-appx:///Assets/logo.png")), button("Vector").icon(Icon::image("ms-appx:///Assets/logo.svg")), + button("Monochrome bitmap") + .icon(Icon::bitmap_icon("ms-appx:///Assets/logo.png", true)), + button("Color bitmap").icon(Icon::bitmap_icon("ms-appx:///Assets/logo.png", false)), + button("Path").icon(Icon::path("F1 M 0,8 L 6,14 L 16,2 L 14,0 L 6,10 L 2,6 Z")), + NavigationView::new( + [ + NavViewItem::new("Bitmap item") + .icon(Icon::bitmap_icon("ms-appx:///Assets/logo.png", false)), + NavViewItem::new("Path item").icon(Icon::path("F1 M 0,0 L 12,0 L 6,12 Z")), + ], + text_block("Navigation icon host"), + ) + .settings_visible(false), )) .into() })); @@ -530,6 +548,96 @@ pub fn button_image_and_font_icons(h: Harness) -> FixtureFuture { .filter(|source| source.cast::().is_ok()) .count(); h.check("Interaction_ButtonIcon_SvgSourceCreated", svg_sources == 1); + + let bitmap_icons = h.find_all::(&|_| true); + h.check( + "Interaction_ButtonIcon_BitmapIconsCreated", + bitmap_icons.len() >= 2, + ); + let monochrome_modes: Vec<_> = bitmap_icons + .iter() + .map(|icon| icon.ShowAsMonochrome().unwrap()) + .collect(); + h.check( + "Interaction_ButtonIcon_BitmapModesApplied", + monochrome_modes.contains(&false) && monochrome_modes.contains(&true), + ); + + let path_icons = h.find_all::(&|_| true); + h.check( + "Interaction_ButtonIcon_PathIconCreated", + !path_icons.is_empty(), + ); + h.check( + "Interaction_ButtonIcon_PathDataParsed", + path_icons.first().is_some_and(|icon| icon.Data().is_ok()), + ); + + let navigation = h + .find_all::(&|_| true) + .into_iter() + .next() + .unwrap(); + let items = navigation.MenuItems().unwrap(); + let bitmap_item: crate::bindings::NavigationViewItem = + items.GetAt(0).unwrap().cast().unwrap(); + let path_item: crate::bindings::NavigationViewItem = + items.GetAt(1).unwrap().cast().unwrap(); + h.check( + "Interaction_NavigationViewItem_CustomIconsCreated", + bitmap_item + .Icon() + .is_ok_and(|icon| icon.cast::().is_ok()) + && path_item + .Icon() + .is_ok_and(|icon| icon.cast::().is_ok()), + ); + }) +} + +pub fn element_exit_transition(h: Harness) -> FixtureFuture { + Box::pin(async move { + h.mount(cc(|cx| { + let (visible, set_visible) = cx.use_state(true); + let (transition_enabled, set_transition_enabled) = cx.use_state(true); + let child: Element = if visible { + let mut child = button("Animated child"); + if transition_enabled { + child = child.transition( + Some(AnimationConfig::fade_in(Duration::from_millis(100))), + Some(AnimationConfig::fade_out(Duration::from_millis(800))), + ); + } + child.into() + } else { + Element::Empty + }; + + vstack(( + button("Toggle child transition") + .on_click(move || set_transition_enabled.call(!transition_enabled)), + button("Remove animated child").on_click(move || set_visible.call(false)), + child, + )) + .into() + })); + h.render().await; + + let _ = h.click_button("Toggle child transition"); + h.render().await; + h.check( + "Interaction_ExitTransition_ClearedWithoutRemoval", + h.find_button("Animated child").is_some(), + ); + + let _ = h.click_button("Toggle child transition"); + h.render().await; + let _ = h.click_button("Remove animated child"); + h.render().await; + h.check( + "Interaction_ExitTransition_LogicalRemovalImmediate", + h.find_button("Animated child").is_none(), + ); }) } diff --git a/crates/tests/libs/reactor_selftest/src/fixtures/pointer_input.rs b/crates/tests/libs/reactor_selftest/src/fixtures/pointer_input.rs index 862111701fe..1c3f07511c2 100644 --- a/crates/tests/libs/reactor_selftest/src/fixtures/pointer_input.rs +++ b/crates/tests/libs/reactor_selftest/src/fixtures/pointer_input.rs @@ -23,7 +23,7 @@ use crate::bindings::{ SetForegroundWindow, }; -use windows_reactor::{Color, ElementExt, PointerEventInfo, text_block, vstack}; +use windows_reactor::{Color, ElementExt, PointerEventInfo, Thickness, text_block, vstack}; use crate::fixtures::reconciler::{FixtureFuture, cc}; use crate::harness::Harness; @@ -35,10 +35,15 @@ struct PointerLog { pressed: u32, released: u32, exited: u32, + capture_lost: u32, + canceled: u32, + capture_succeeded: bool, left_on_press: bool, right_on_press: bool, last_x: f64, last_y: f64, + last_window_x: f64, + last_window_y: f64, } /// Screen pixel at a fraction (`fx`, `fy`) of the window's client area. @@ -96,16 +101,18 @@ pub fn pointer_injection_gesture(h: Harness) -> FixtureFuture { let comp_log = log.clone(); h.mount(cc(move |_cx| { - let (le, lm, lp, lr, lx) = ( + let (le, lm, lp, lr, lx, lcl, lc) = ( + comp_log.clone(), + comp_log.clone(), comp_log.clone(), comp_log.clone(), comp_log.clone(), comp_log.clone(), comp_log.clone(), ); - vstack((text_block("pointer target"),)) + vstack((vstack((text_block("pointer target"),)) .width(6000.0) - .height(6000.0) + .height(180.0) .background(Color { a: 255, r: 32, @@ -120,10 +127,13 @@ pub fn pointer_injection_gesture(h: Harness) -> FixtureFuture { b.moved += 1; b.last_x = info.x; b.last_y = info.y; + b.last_window_x = info.window_x; + b.last_window_y = info.window_y; }) .on_pointer_pressed(move |info: PointerEventInfo| { let mut b = lp.borrow_mut(); b.pressed += 1; + b.capture_succeeded = info.capture_succeeded; if info.is_left_button_pressed { b.left_on_press = true; } @@ -137,7 +147,15 @@ pub fn pointer_injection_gesture(h: Harness) -> FixtureFuture { .on_pointer_exited(move || { lx.borrow_mut().exited += 1; }) - .into() + .on_pointer_capture_lost(move || { + lcl.borrow_mut().capture_lost += 1; + }) + .on_pointer_canceled(move || { + lc.borrow_mut().canceled += 1; + }) + .capture_pointer_on_press(),)) + .padding(Thickness::uniform(80.0)) + .into() })); h.render().await; @@ -149,7 +167,7 @@ pub fn pointer_injection_gesture(h: Harness) -> FixtureFuture { return; }; - let Some((cx, cy)) = client_screen_point(h.hwnd(), 0.5, 0.5) else { + let Some((cx, cy)) = client_screen_point(h.hwnd(), 0.5, 0.2) else { h.check_skip("Pointer_Injection_Gesture", "client rect unavailable"); return; }; @@ -195,17 +213,63 @@ pub fn pointer_injection_gesture(h: Harness) -> FixtureFuture { lx > 0.0 && ly > 0.0, move || format!("last reported pointer position = ({lx}, {ly})"), ); + let (wx, wy) = (b.last_window_x, b.last_window_y); + h.check_with( + "Pointer_Injection_PositionInWindow", + wx > lx + 60.0 && wy > ly + 60.0, + move || format!("element position = ({lx}, {ly}), window position = ({wx}, {wy})"), + ); } - // Left press + release: PointerPressed (left flag), PointerReleased. + // Capture on left press, move outside the target, then release there. let _ = inject_at(&injector, cx, cy, InjectedInputMouseOptions::LeftDown); h.render_until_quiet("left button press", |_| log.borrow().pressed > 0) .await; - let _ = inject_at(&injector, cx, cy, InjectedInputMouseOptions::LeftUp); - h.render_until_quiet("left button release", |_| log.borrow().released > 0) - .await; h.check("Pointer_Injection_PressedLeft", log.borrow().left_on_press); + h.check( + "Pointer_Injection_CaptureSucceeded", + log.borrow().capture_succeeded, + ); + + let Some((outside_x, outside_y)) = client_screen_point(h.hwnd(), 0.5, 0.8) else { + h.check_skip( + "Pointer_Injection_CaptureOutside", + "client rect unavailable", + ); + return; + }; + let moved_before_capture_test = log.borrow().moved; + let _ = inject_at( + &injector, + outside_x, + outside_y, + InjectedInputMouseOptions::Move, + ); + h.render_until_quiet("captured move outside target", |_| { + log.borrow().moved > moved_before_capture_test + }) + .await; + h.check_with( + "Pointer_Injection_CaptureOutside", + log.borrow().moved > moved_before_capture_test && log.borrow().last_y > 180.0, + || { + let b = log.borrow(); + format!( + "moved before = {moved_before_capture_test}, moved after = {}, local y = {}", + b.moved, b.last_y + ) + }, + ); + + let _ = inject_at( + &injector, + outside_x, + outside_y, + InjectedInputMouseOptions::LeftUp, + ); + h.render_until_quiet("left button release", |_| log.borrow().released > 0) + .await; h.check("Pointer_Injection_Released", log.borrow().released > 0); // Right press + release: PointerPressed reports the right-button flag. diff --git a/crates/tests/libs/reactor_selftest/src/registry.rs b/crates/tests/libs/reactor_selftest/src/registry.rs index fe5ff06f6fb..02bf1387450 100644 --- a/crates/tests/libs/reactor_selftest/src/registry.rs +++ b/crates/tests/libs/reactor_selftest/src/registry.rs @@ -62,6 +62,10 @@ pub static FIXTURES: &[(&str, FixtureFn)] = &[ "Reconciler_Mount_ButtonTextLink", controls::mount_button_text_link, ), + ( + "Resources_TypedUpdateAndClear", + controls::lightweight_resources, + ), ("Reconciler_Mount_CheckBox", controls::mount_check_box), ("Reconciler_Mount_TextField", controls::mount_text_field), ( @@ -89,6 +93,7 @@ pub static FIXTURES: &[(&str, FixtureFn)] = &[ controls::mount_person_picture, ), ("Reconciler_Mount_TabView", controls::mount_tab_view), + ("TabView_ItemKey_Clear", controls::tab_item_key_clear), ( "Reconciler_Mount_TabView_AddButton", controls::mount_tab_view_add_button, @@ -97,6 +102,10 @@ pub static FIXTURES: &[(&str, FixtureFn)] = &[ "Reconciler_Mount_NavigationView", controls::mount_navigation_view, ), + ( + "NavigationView_StateCallbacks", + controls::navigation_view_state_callbacks, + ), ("Reconciler_Mount_TitleBar", controls::mount_title_bar), ("Reconciler_Mount_Pivot", controls::mount_pivot), ( @@ -131,6 +140,10 @@ pub static FIXTURES: &[(&str, FixtureFn)] = &[ "Reconciler_Mount_VirtualList", controls::mount_virtual_list_alias, ), + ( + "Reconciler_KeyedTemplatedListState", + controls::keyed_templated_list_state, + ), ("Reconciler_Mount_PasswordBox", controls::mount_password_box), ( "Reconciler_Mount_RadioButtons", @@ -187,8 +200,12 @@ pub static FIXTURES: &[(&str, FixtureFn)] = &[ interactions::button_icon_glyph_change_preserves_text, ), ( - "Interaction_ButtonIcon_ImageAndFont", - interactions::button_image_and_font_icons, + "Interaction_ButtonIcon_Subclasses", + interactions::button_icon_subclasses, + ), + ( + "Interaction_ElementExitTransition", + interactions::element_exit_transition, ), ( "Interaction_ButtonIcon_Removal", diff --git a/crates/tools/composition/src/composition.txt b/crates/tools/composition/src/composition.txt index fbd0d6c8754..7d120a6109e 100644 --- a/crates/tools/composition/src/composition.txt +++ b/crates/tools/composition/src/composition.txt @@ -24,7 +24,7 @@ Windows.UI.Composition.Compositor Windows.UI.Composition.Compositor::CreateInstance // endregion Windows.UI.Composition.ICompositor::{CreateContainerVisual, CreateSpriteVisual, CreateColorBrushWithColor, CreateScopedBatch, CreateScalarKeyFrameAnimation, CreateVector3KeyFrameAnimation, CreateLinearEasingFunction, CreateCubicBezierEasingFunction} -Windows.UI.Composition.ICompositor2::{CreateNineGridBrush, CreateImplicitAnimationCollection} +Windows.UI.Composition.ICompositor2::{CreateAnimationGroup, CreateNineGridBrush, CreateImplicitAnimationCollection} Windows.UI.Composition.ICompositor5::{CreateShapeVisual, CreateSpriteShapeWithGeometry, CreateContainerShape, CreateEllipseGeometry} Windows.UI.Composition.CompositionObject @@ -84,6 +84,8 @@ Windows.UI.Composition.ICompositionEllipseGeometry::{put_Radius} Windows.UI.Composition.CompositionAnimation Windows.UI.Composition.ICompositionAnimation2::{put_Target} Windows.UI.Composition.ICompositionAnimationBase +Windows.UI.Composition.CompositionAnimationGroup +Windows.UI.Composition.ICompositionAnimationGroup::Add Windows.UI.Composition.KeyFrameAnimation Windows.UI.Composition.IKeyFrameAnimation::{put_Duration, put_DelayTime, put_IterationBehavior, put_IterationCount, InsertExpressionKeyFrameWithEasingFunction} Windows.UI.Composition.ScalarKeyFrameAnimation diff --git a/crates/tools/reactor/src/base.txt b/crates/tools/reactor/src/base.txt index 9bf4c59e7d9..83a319e001c 100644 --- a/crates/tools/reactor/src/base.txt +++ b/crates/tools/reactor/src/base.txt @@ -36,6 +36,7 @@ Microsoft::UI::Xaml::Controls::AutoSuggestBoxQuerySubmittedEventArgs Microsoft::UI::Xaml::Controls::AutoSuggestBoxSuggestionChosenEventArgs Microsoft::UI::Xaml::Controls::AutoSuggestBoxTextChangedEventArgs Microsoft::UI::Xaml::Controls::AutoSuggestionBoxTextChangeReason +Microsoft::UI::Xaml::Controls::BitmapIcon::CreateInstance Microsoft::UI::Xaml::Controls::BreadcrumbBarItemClickedEventArgs Microsoft::UI::Xaml::Controls::CalendarDatePickerDateChangedEventArgs Microsoft::UI::Xaml::Controls::CalendarViewSelectedDatesChangedEventArgs @@ -68,6 +69,7 @@ Microsoft::UI::Xaml::Controls::IBorder::{put_Child, put_Padding, put_Background, Microsoft::UI::Xaml::Controls::IBreadcrumbBar::{put_ItemsSource, ItemClicked} Microsoft::UI::Xaml::Controls::IBreadcrumbBarItemClickedEventArgs::get_Index Microsoft::UI::Xaml::Controls::IButton::{put_Flyout, get_Flyout} +Microsoft::UI::Xaml::Controls::IBitmapIcon::{put_UriSource, put_ShowAsMonochrome} Microsoft::UI::Xaml::Controls::ICalendarDatePicker::DateChanged Microsoft::UI::Xaml::Controls::ICalendarDatePickerDateChangedEventArgs::get_NewDate Microsoft::UI::Xaml::Controls::ICalendarView::SelectedDatesChanged @@ -102,7 +104,8 @@ Microsoft::UI::Xaml::Controls::IMenuFlyout::get_Items Microsoft::UI::Xaml::Controls::IMenuFlyoutItem::{put_Text, get_Text, Click} Microsoft::UI::Xaml::Controls::IMenuFlyoutSubItem::{get_Items, put_Text} Microsoft::UI::Xaml::Controls::INavigationView2::{put_IsBackButtonVisible, BackRequested} -Microsoft::UI::Xaml::Controls::INavigationView::{get_MenuItems, put_SelectedItem, put_AutoSuggestBox, get_AutoSuggestBox, put_PaneFooter, SelectionChanged} +Microsoft::UI::Xaml::Controls::INavigationView::{get_IsPaneOpen, get_DisplayMode, get_MenuItems, put_SelectedItem, put_AutoSuggestBox, get_AutoSuggestBox, put_PaneFooter, SelectionChanged} +Microsoft::UI::Xaml::Controls::INavigationViewStatics::{get_IsPaneOpenProperty, get_DisplayModeProperty} Microsoft::UI::Xaml::Controls::INavigationViewItem2::get_MenuItems Microsoft::UI::Xaml::Controls::INavigationViewItem::put_Icon Microsoft::UI::Xaml::Controls::INavigationViewSelectionChangedEventArgs::get_SelectedItem @@ -202,6 +205,8 @@ Microsoft::UI::Xaml::Controls::UIElementCollection Microsoft::UI::Xaml::Controls::XamlControlsResources::CreateInstance Microsoft::UI::Xaml::CornerRadius Microsoft::UI::Xaml::DependencyObject +Microsoft::UI::Xaml::DependencyPropertyChangedCallback +Microsoft::UI::Xaml::IDependencyObject::{RegisterPropertyChangedCallback, UnregisterPropertyChangedCallback} Microsoft::UI::Xaml::Documents::BlockCollection Microsoft::UI::Xaml::Documents::IParagraph::get_Inlines Microsoft::UI::Xaml::Documents::IRun::put_Text @@ -218,8 +223,7 @@ Microsoft::UI::Xaml::FrameworkElement Microsoft::UI::Xaml::GridLength Microsoft::UI::Xaml::GridUnitType Microsoft::UI::Xaml::HorizontalAlignment -Microsoft::UI::Xaml::Hosting::ElementCompositionPreview::GetElementVisual -Microsoft::UI::Xaml::Hosting::ElementCompositionPreview::SetElementChildVisual +Microsoft::UI::Xaml::Hosting::ElementCompositionPreview::{GetElementVisual, SetElementChildVisual, SetImplicitShowAnimation, SetImplicitHideAnimation} Microsoft::UI::Xaml::IApplication::get_Resources Microsoft::UI::Xaml::IApplicationOverrides Microsoft::UI::Xaml::IDragEventArgs::{put_AcceptedOperation, get_DataView, get_DragUIOverride, GetDeferral} @@ -228,14 +232,14 @@ Microsoft::UI::Xaml::IDragUIOverride::{put_Caption, put_IsGlyphVisible, put_IsCo Microsoft::UI::Xaml::IFrameworkElement::{put_VerticalAlignment, put_HorizontalAlignment, put_Margin, put_Height, put_Width, put_MinWidth, put_MaxWidth, put_MinHeight, put_MaxHeight, put_RequestedTheme, get_ActualTheme, ActualThemeChanged, put_Tag, get_Tag, get_ActualWidth, get_ActualHeight, put_Style, SizeChanged, Loaded, get_Resources} Microsoft::UI::Xaml::IResourceDictionary::get_MergedDictionaries Microsoft::UI::Xaml::ISizeChangedEventArgs::get_NewSize -Microsoft::UI::Xaml::IUIElement::{put_AllowDrop, put_Opacity, put_XamlRoot, get_XamlRoot, get_KeyboardAccelerators, put_KeyboardAcceleratorPlacementMode, PointerPressed, PointerReleased, PointerMoved, PointerEntered, PointerExited, Tapped, RightTapped, DragEnter, DragLeave, DragOver, Drop} +Microsoft::UI::Xaml::IUIElement::{put_AllowDrop, put_Opacity, put_XamlRoot, get_XamlRoot, get_KeyboardAccelerators, put_KeyboardAcceleratorPlacementMode, CapturePointer, ReleasePointerCapture, ReleasePointerCaptures, PointerPressed, PointerReleased, PointerMoved, PointerEntered, PointerExited, PointerCaptureLost, PointerCanceled, Tapped, RightTapped, DragEnter, DragLeave, DragOver, Drop} Microsoft::UI::Xaml::IWindow2::{get_AppWindow, put_SystemBackdrop} Microsoft::UI::Xaml::IWindow::{put_Title, put_Content, put_ExtendsContentIntoTitleBar, SetTitleBar, Activate, Close, Closed} Microsoft::UI::Xaml::IWindowEventArgs::{} Microsoft::UI::Xaml::IXamlRoot::{get_RasterizationScale, Changed} Microsoft::UI::Xaml::Input::IKeyboardAccelerator::{put_Key, put_Modifiers, Invoked} Microsoft::UI::Xaml::Input::IKeyboardAcceleratorInvokedEventArgs::put_Handled -Microsoft::UI::Xaml::Input::IPointerRoutedEventArgs::GetCurrentPoint +Microsoft::UI::Xaml::Input::IPointerRoutedEventArgs::{GetCurrentPoint, get_Pointer} Microsoft::UI::Xaml::Input::KeyboardAccelerator::CreateInstance Microsoft::UI::Xaml::Input::KeyboardAcceleratorInvokedEventArgs Microsoft::UI::Xaml::Input::KeyboardAcceleratorPlacementMode diff --git a/crates/tools/reactor/src/test.txt b/crates/tools/reactor/src/test.txt index e3bd2156666..8c059fcbc01 100644 --- a/crates/tools/reactor/src/test.txt +++ b/crates/tools/reactor/src/test.txt @@ -6,11 +6,15 @@ Microsoft::UI::Xaml::Automation::Provider::IInvokeProvider Microsoft::UI::Xaml::Controls::Canvas::{GetLeft, GetTop} Microsoft::UI::Xaml::Controls::Grid::GetRow + Microsoft::UI::Xaml::Controls::PathIcon::CreateInstance Microsoft::UI::Xaml::Controls::IBorder::get_Child + Microsoft::UI::Xaml::Controls::IBitmapIcon::{get_UriSource, get_ShowAsMonochrome} Microsoft::UI::Xaml::Controls::IControl::get_IsEnabled Microsoft::UI::Xaml::Controls::IImage::get_Source Microsoft::UI::Xaml::Controls::IImageIcon::get_Source Microsoft::UI::Xaml::Controls::IListViewBase::{get_CanDragItems, get_CanReorderItems} + Microsoft::UI::Xaml::Controls::INavigationViewItem::get_Icon + Microsoft::UI::Xaml::Controls::IPathIcon::get_Data Microsoft::UI::Xaml::Controls::ISymbolIcon::get_Symbol Microsoft::UI::Xaml::Controls::Primitives::IRangeBase::get_Value Microsoft::UI::Xaml::Controls::Primitives::IToggleButton::get_IsChecked diff --git a/docs/crates/windows-composition.md b/docs/crates/windows-composition.md index 2ad0572e078..0127522c7be 100644 --- a/docs/crates/windows-composition.md +++ b/docs/crates/windows-composition.md @@ -120,7 +120,8 @@ Two patterns keep private `bindings::` types out of the public API. expected. - `Brush`, `Shape`, and `Animation` are sealed marker traits. Each trait exposes an `as_brush`, `as_shape`, or `as_animation` method. A method such as `SpriteVisual::set_brush(&impl Brush)` - accepts any crate-defined brush type. + accepts any crate-defined brush type. `CompositionAnimationGroup::add` accepts any `Animation`, + so scalar and vector animations can start together as one implicit animation. ### Module layout @@ -134,7 +135,7 @@ Two patterns keep private `bindings::` types out of the public API. | `visual.rs` | Visual wrappers, child collections, and visual properties. Lifted builds expose `Visual::as_raw`. | | `shape.rs` | Shape visuals, shape traits, sprite and container shapes, ellipse geometry, and shape collections. | | `brush.rs` | Brush traits, color brushes, nine-grid brushes, and surface brushes. | -| `animation.rs` | Animation traits, key-frame animations, easing functions, and implicit animation collections. | +| `animation.rs` | Animation traits, key-frame animations, animation groups, easing functions, and implicit animation collections. | | `batch.rs` | `CompositionScopedBatch` and `BatchKind`. | | `color.rs` | `Color` newtype over `Windows.UI.Color`. | @@ -194,8 +195,9 @@ This crate provides the lifted binding set and seam helpers for that bridge. `Visual::{from_host, as_raw}` adopts or exposes a visual's interop `IInspectable`. Both crates use the same `Microsoft.UI.winmd` input for lifted bindings. The `IInspectable` values -have matching IIDs, so the casts in the seam helpers are ABI-safe. Reactor's animation engine also -uses this crate's key-frame, easing, and implicit-animation wrappers. +have matching IIDs, so the casts in the seam helpers are ABI-safe. Reactor's animation engine also uses this crate's key-frame, animation-group, easing, and +implicit-animation wrappers. Element lifecycle transitions use one group when opacity and scale +must animate together. ### Canvas bridge diff --git a/docs/crates/windows-reactor.md b/docs/crates/windows-reactor.md index c6b8c1a67ab..3388a171eb3 100644 --- a/docs/crates/windows-reactor.md +++ b/docs/crates/windows-reactor.md @@ -92,10 +92,11 @@ Build elements with plain builder functions. Each returns a widget that becomes - Buttons: `button(content)` with `.on_click(..)`, `.accent()`, `.subtle()`, `.enabled(..)`, `.icon(..)`, `.flyout(..)`, `.menu_flyout(..)`. - Icons: any control that takes an icon (`button`, `NavViewItem`, command-bar buttons, - `selector_bar_item`) accepts `impl Into`. `Icon` has three kinds: `Symbol(Symbol)` for a - built-in system glyph (a bare `Symbol` converts automatically, so `.icon(Symbol::Home)` keeps - working), `Icon::image(source)` for a raster or SVG image (`ImageIcon`), and `Icon::font(glyph)` - or `Icon::font_family(glyph, family)` for a font glyph (`FontIcon`). Sample: + `selector_bar_item`) accepts `impl Into`. A bare `Symbol` creates a `SymbolIcon`; + `Icon::image(source)` creates a full-color `ImageIcon` from raster, SVG, or surface data; + `Icon::bitmap_icon(uri, show_as_monochrome)` creates a native `BitmapIcon`; `Icon::font(glyph)` + and `Icon::font_family(glyph, family)` create a `FontIcon`; and `Icon::path(data)` creates a + `PathIcon` from XAML path mini-language data. Sample: `cargo run -p reactor_samples --example icon_elements`. - Images: `Image::new(source)` accepts a URI or `ImageSource`. URI paths ending in `.svg` (case-insensitive, before any query or fragment) use the platform SVG decoder; other URIs use @@ -109,12 +110,36 @@ About 60 WinUI controls are wrapped, including `check_box`, `combo_box`, `slider `calendar_view`, `content_dialog`, `info_bar`, `teaching_tip`, and `command_bar`. See the [full catalog](https://github.com/microsoft/windows-rs/tree/master/crates/libs/reactor/src/widgets). +`TabItem::with_key` supplies the stable identity returned by `TabView::on_close_requested`. Adding, +changing, or removing that key updates the existing native `TabViewItem`; removing it also clears +the WinUI `Tag` instead of retaining stale callback identity. See the `tab_view_item_key` sample. + Layout and appearance modifiers are available on any `Element` through the `ElementExt` trait: `.margin(..)`, `.padding(..)`, `.width(..)`, `.height(..)`, `.horizontal_alignment(..)`, `.vertical_alignment(..)` (with `HorizontalAlignment` and `VerticalAlignment`), `.background(..)`, `.foreground(..)`, `.opacity(..)`, and transition helpers such as `.with_opacity_transition(..)`. Spacing values use `Thickness` (with `Thickness::uniform(..)`). +`transition(enter, exit)` runs lifecycle animations when an element enters or leaves the WinUI +visual tree. The logical Reactor element is removed synchronously; WinUI Composition retains its +departing visual until the implicit hide animation finishes. This keeps keyed and positional child +indices exact during reconciliation instead of reinserting a temporary "ghost" element. Opacity +and scale are supported, including both in one animation group: + +```rust +# use std::time::Duration; +# use windows_reactor::*; +button("Animated") + .transition( + Some(AnimationConfig::fade_in(Duration::from_millis(200))), + Some(AnimationConfig::fade_out(Duration::from_millis(300))), + ); +``` + +An explicit `.animate(..)` on the same element takes precedence over its enter transition because +both target the initial property animation. The exit transition remains registered. See the +`exit_transition` sample. + ## Handling events Event handlers take closures. `button(..).on_click(move || ...)` is the most common. Pointer and @@ -123,6 +148,35 @@ keyboard handlers live on `ElementExt`: `.on_tapped(..)`, `.on_pointer_pressed(. `.on_pointer_exited(..)`, `.keyboard_accelerator(..)`. You can pass a `SetState` or `Dispatch` directly wherever a handler is expected (through `IntoCallback`). +`PointerEventInfo::x` and `y` are relative to the element receiving the event. `window_x` and +`window_y` are relative to the overall window, so drag deltas remain stable when the element moves +while handling the gesture. A moving drag handle can also lose hit testing when the cursor outruns +layout. Add `.capture_pointer_on_press()` to keep receiving events until release, begin the drag +only when `PointerEventInfo::capture_succeeded` is true, and clear drag state from +`.on_pointer_capture_lost(..)` and `.on_pointer_canceled(..)`. See the `pointer_resize` sample. + +`NavigationView::on_pane_open_changed` reports the settled `IsPaneOpen` property, including +light-dismiss and adaptive changes made by WinUI. `on_display_mode_changed` reports the actual +`NavigationViewDisplayMode` (`Minimal`, `Compact`, or `Expanded`), while `pane_display_mode` +continues to configure the layout policy (`Auto`, `Left`, `Top`, and so on): + +```rust +# use windows_reactor::*; +# let items = [NavViewItem::new("Home")]; +# let content = text_block("Content"); +# let pane_open = true; +NavigationView::new(items, content) + .pane_open(pane_open) + .on_pane_open_changed(|_| {}) + .pane_display_mode(NavigationViewPaneDisplayMode::Auto) + .on_display_mode_changed(|_| {}); +``` + +The callbacks observe dependency properties rather than guessing state from pane transition +events. Transition events can be canceled and do not cover every property change. Observers are +attached only for callbacks the element requests, so an unused callback adds no native observer or +reconciliation work. See the `responsive_navigation` sample. + ### Handler identity When a value-carrying event just forwards its argument to a setter, pass the setter directly instead @@ -218,7 +272,8 @@ The [`crates/samples/reactor`](../../crates/samples/reactor) tree is the best re - `samples`: the smallest app plus an `examples/` folder with about 90 focused per-control and per-hook examples (`counter`, `calculator`, `navigation_view`, `list_view`, `content_dialog`, - `color_picker`, `secondary_window`, and more). + `keyed_list_reorder`, `lightweight_resources`, `pointer_resize`, `color_picker`, + `secondary_window`, and more). - `apps`: complete applications (`notepad`, `solitaire`, `minesweeper`, `tictactoe`, `dotsweeper`). - `gallery`: a WinUI-gallery-style shell with navigation across many controls. - `direct2d` and `swap_chain_panel`: hosting Direct2D and Direct3D content. @@ -634,12 +689,13 @@ keyed `Memo(key, ...)` wrapper. A small `when(cond, || el)` helper and a keyed m virtualized rows would read better at near-zero cost. These are ergonomics, not performance, and do not need the benchmark gate above. -### Known bug (fix regardless of the comparison) +### Element lifecycle transitions -`ElementExt::transition(enter, exit)` sets an exit transition that the reconciler never consumes: -`enter_transition` is read during reconciliation but the exit configuration is dropped. This is -already tracked in the repo-wide open-investigations list. Fix it independently of any feature work -here. +Lifecycle transitions use WinUI Composition implicit show/hide animations. This is smaller than +the C# Reactor implementation, which retains and reinserts removed controls until their exit +animations complete. Rust can destroy the logical subtree immediately while WinUI keeps only the +departing composition visual alive. The reconciler therefore needs no asynchronous removal state, +and keyed and positional child indices remain correct while an exit animation runs. ### Larger feature areas (product decisions, likely out of scope) @@ -652,3 +708,74 @@ localization, Roslyn-style analyzers that enforce rules of hooks (this crate rel hook-order checks; a clippy lint could cover part of this), hot reload and live preview, and a scaffolding CLI. Each is a large investment, and most cut against this crate's minimal, WinUI-native design. They belong in a separate decision, not in the reconciler or hooks work above. + +## Open issue working plan + +This plan tracks the open `windows-reactor` issues reviewed in August 2026. The order favors +correctness and common WinUI authoring needs. C# Reactor is a reference for behavior and test cases, +not a surface-area target. Each change must fit the Rust design, include focused headless coverage +where possible, and add the smallest runnable sample that proves the user-facing behavior. + +Before expanding an issue beyond its reported case: + +1. Reproduce the failure with a test or sample. +2. Identify the invariant that is missing from the current design. +3. Prefer a local correction over a new subsystem. +4. Compare the proposed behavior with C# Reactor, React, and WinUI where relevant. +5. Measure hot-path changes and reject added machinery that does not buy correctness or speed. +6. Reevaluate the remaining plan after the change lands. + +| Priority | Issue | Assessment | Direction | +| --- | --- | --- | --- | +| Done | [#4778](https://github.com/microsoft/windows-rs/issues/4778) keyed templated lists | Correctness bug: realized rows followed slots instead of keys. | Equal-count keyed reorders now preserve realized controls and row-local state. Missing and duplicate keys retain positional behavior. | +| Done | [#4776](https://github.com/microsoft/windows-rs/issues/4776) resources | Valid request, plus stale keys were never removed. | Typed string, solid-color brush, number, thickness, and corner-radius values now replace Reactor-owned keys. Theme references remain deferred because resolving them to concrete values would break WinUI theme-resource behavior. | +| Done | [#4772](https://github.com/microsoft/windows-rs/issues/4772) pointer coordinates | Element-relative coordinates cannot anchor a moving drag target, and a moving handle can lose routed events. | `PointerEventInfo` now copies both element-local and window-relative positions. Opt-in pointer capture keeps fast drags routed and exposes capture-loss/cancellation without raw WinRT arguments. | +| Done | [#4771](https://github.com/microsoft/windows-rs/issues/4771) navigation pane events | Valid state gap, but transition events are an unreliable controlled-state foundation. | Settled `IsPaneOpen` and actual `DisplayMode` callbacks now cover light dismiss, adaptive layout, and programmatic changes. Transition events remain deferred until a separate use case requires them. | +| Done | [#4720](https://github.com/microsoft/windows-rs/issues/4720) icon subclasses | Image and font icons worked, but native `BitmapIcon` and `PathIcon` support was missing. | `bitmap_icon(uri, mode)` now exposes native monochrome/full-color `BitmapIcon` behavior, while `path(data)` adds vector paths. Generic images remain a separate `ImageSource` path. | +| Done | Exit transition correctness | `transition(enter, exit)` stored the exit configuration but never consumed it. | WinUI implicit show/hide animations now run opacity and scale lifecycle transitions without retaining logical ghost children or adding asynchronous reconciler state. | +| Done | `TabItem` key clearing | Removing a key updated the Rust model but left the old native `Tag`, so close callbacks reported stale identity. | Key removal now emits the existing `Unset` property path and clears `FrameworkElement.Tag` on the same native item without remounting it. | +| Close | [#4753](https://github.com/microsoft/windows-rs/issues/4753) SVG support | Fixed by PR #4764 and covered by `ImageSource` extension dispatch. | Close after confirming the existing SVG sample. File/memory loading and colorization are separate requests. | +| Deferred | [#4692](https://github.com/microsoft/windows-rs/issues/4692) bootstrap mismatch | Documentation is fixed; the loader failure remains a sharp edge. | Leave `windows-reactor-setup` unchanged for now. If resumed, evaluate dynamic loading that reports misuse without silently bootstrapping a self-contained deployment. | + +### Planned order + +1. Fix keyed templated-list identity and add reorder tests plus a visible shuffle sample. +2. Correct resource ownership/removal, then add typed resource values and a lightweight-styling + sample. +3. Add root-relative pointer coordinates and a resize-drag sample. +4. Add `NavigationView` pane-open and display-mode callbacks with a responsive navigation sample. +5. Finish the remaining icon forms and separate unrelated image-loading requests. +6. Fix exit transitions without adding asynchronous reconciler state or temporary ghost children. +7. Clear stale native `TabItem` identity when an optional item key is removed. +8. Leave the bootstrap sharp edge deferred until setup work resumes. + +After each item, rerun the Rust/C# stress comparison if the reconciler or allocation behavior +changes. Rust should retain its smaller runtime model and reconciliation advantage; copying C# +pooling, collection, or descriptor machinery requires evidence that the existing Rust path cannot +meet the same invariant more directly. + +### Resource ownership and typed values + +`ElementExt::resources` still accepts an iterator when every entry has the same Rust value type. +`resource_overrides` uses a consuming builder when one resource dictionary contains different +WinUI value types: + +```rust +# use windows_reactor::*; +button("Delete").resource_overrides(|resources| { + resources + .set("ButtonBackground", Color::rgb(178, 34, 34)) + .set("ButtonBorderThemeThickness", Thickness::uniform(0.0)) + .set("ControlCornerRadius", CornerRadius::uniform(8.0)) +}); +``` + +Each native element tracks only the keys that Reactor inserted. Updating the builder removes +missing Reactor-owned keys before inserting current values, including when the new builder is +empty. Native or application code can keep unrelated entries in the same resource dictionary. + +`Color` values intentionally create `SolidColorBrush` instances because lightweight control +resources such as `ButtonBackground` expect brushes. Use strings only for resources that actually +expect strings. `ThemeRef` is not accepted here: looking up a theme key and storing its current +value would lose WinUI's element-aware `{ThemeResource}` resolution. Theme-aware control +properties continue to use the existing theme-binding APIs.