Skip to main content

ferroui_base/input/
input_scope.rs

1//! Ordered, live WinUI input-scope objects using FerroUI's property and list APIs.
2//! Copyright (c) Microsoft Corporation. See NOTICE.md for source attribution.
3
4use super::{text_input::TextInputContentType, InputScopeNameValue};
5use crate::collections::FerroList;
6use crate::metadata::IAddChild;
7use crate::reactive::{Disposable, IDisposable};
8use crate::utilities::{FormatError, HandlerList};
9use crate::{
10    ferro_class, instantiate, FerroObject, FerroObjectImpl, FerroObjectImplExt, FerroProperty, Ref,
11    StyledProperty,
12};
13use std::cell::{Cell, RefCell};
14use std::collections::HashSet;
15use std::rc::Rc;
16
17/// One live input-scope hint. The name remains mutable after insertion into
18/// one or more input scopes; each scope publishes a fresh immutable snapshot.
19#[repr(C)]
20pub struct InputScopeName {
21    base: FerroObject,
22}
23ferro_class!(InputScopeName: FerroObject);
24crate::ferro_impl_classes!(InputScopeName: FerroObjectImpl);
25crate::ferro_class_info!(InputScopeName {
26    new: InputScopeName::new,
27    markup: {
28        content: NameValue,
29        constructors: [(InputScopeNameValue) => InputScopeName::with_name_value],
30    },
31});
32crate::ferro_properties! { impl InputScopeName {
33    pub fn name_value_property() -> StyledProperty<InputScopeNameValue> {
34        FerroProperty::register::<InputScopeName, _>("NameValue", InputScopeNameValue::Default)
35    }
36} }
37
38impl InputScopeName {
39    pub fn construct() -> Self {
40        Self {
41            base: FerroObject::construct(),
42        }
43    }
44    pub fn new() -> Ref<Self> {
45        instantiate(Self::construct())
46    }
47    pub fn with_name_value(name_value: InputScopeNameValue) -> Ref<Self> {
48        let result = Self::new();
49        result.set_name_value(name_value);
50        result
51    }
52    pub fn parse(value: &str) -> Result<Ref<Self>, FormatError> {
53        InputScopeNameValue::parse(value).map(Self::with_name_value)
54    }
55    pub fn name_value(&self) -> InputScopeNameValue {
56        self.get_value(Self::name_value_property())
57    }
58    pub fn set_name_value(&self, value: InputScopeNameValue) {
59        self.set_value(Self::name_value_property(), value);
60    }
61}
62
63/// An ordered, shared collection of input-method hints. A newly constructed
64/// scope has no names, as in WinUI; that falls back to the host's normal policy.
65#[repr(C)]
66pub struct InputScope {
67    base: FerroObject,
68    names: FerroList<Ref<InputScopeName>>,
69    collection_subscription: Cell<Option<u64>>,
70    item_subscriptions: RefCell<Vec<Rc<dyn IDisposable>>>,
71    changed: Rc<HandlerList<dyn Fn()>>,
72}
73ferro_class!(InputScope: FerroObject);
74crate::ferro_class_info!(InputScope {
75    new: InputScope::new,
76    interfaces: [Rc<dyn IAddChild<Ref<InputScopeName>>> => InputScope::as_add_child],
77    markup: {
78        content: Names,
79        parse: InputScope::parse,
80        properties: [Names: FerroList<Ref<InputScopeName>> { get: InputScope::names }],
81    },
82});
83
84impl FerroObjectImpl for InputScope {
85    fn constructed(this: &Self) {
86        Self::parent_constructed(this);
87        let weak = this.to_ref().downgrade();
88        let token = this.names.add_collection_changed(Rc::new(move |_| {
89            if let Some(this) = weak.upgrade() {
90                this.refresh_item_subscriptions();
91                this.invalidate();
92            }
93        }));
94        this.collection_subscription.set(Some(token));
95    }
96}
97
98impl InputScope {
99    pub fn construct() -> Self {
100        Self {
101            base: FerroObject::construct(),
102            names: FerroList::new(),
103            collection_subscription: Cell::new(None),
104            item_subscriptions: RefCell::new(Vec::new()),
105            changed: Rc::new(HandlerList::new()),
106        }
107    }
108    pub fn new() -> Ref<Self> {
109        instantiate(Self::construct())
110    }
111    pub fn with_name_value(value: InputScopeNameValue) -> Ref<Self> {
112        let result = Self::new();
113        result.names.add(InputScopeName::with_name_value(value));
114        result
115    }
116    /// Converts one name, matching WinUI CInputScope::Create. The legacy
117    /// FerroUI shortcut names remain accepted on this object converter.
118    pub fn parse(value: &str) -> Result<Ref<Self>, FormatError> {
119        let legacy = [
120            ("Normal", InputScopeNameValue::Default),
121            ("Alpha", InputScopeNameValue::AlphanumericHalfWidth),
122            ("Email", InputScopeNameValue::EmailSmtpAddress),
123            ("Name", InputScopeNameValue::PersonalFullName),
124            ("Pin", InputScopeNameValue::NumericPin),
125            ("Social", InputScopeNameValue::Chat),
126        ]
127        .into_iter()
128        .find_map(|(name, scope)| name.eq_ignore_ascii_case(value.trim()).then_some(scope));
129        legacy
130            .map(Ok)
131            .unwrap_or_else(|| InputScopeNameValue::parse(value))
132            .map(Self::with_name_value)
133    }
134    pub fn names(&self) -> FerroList<Ref<InputScopeName>> {
135        self.names.clone()
136    }
137    /// Captures values, never live objects, across the platform boundary.
138    pub fn snapshot(&self) -> Rc<[InputScopeNameValue]> {
139        self.names
140            .snapshot()
141            .iter()
142            .map(|name| name.name_value())
143            .collect()
144    }
145    pub fn changed(&self, handler: impl Fn() + 'static) -> Rc<dyn IDisposable> {
146        let token = self.changed.add(Rc::new(handler));
147        let weak = Rc::downgrade(&self.changed);
148        Disposable::create(move || {
149            if let Some(handlers) = weak.upgrade() {
150                handlers.remove(token);
151            }
152        })
153    }
154    pub fn as_add_child(this: Ref<Self>) -> Rc<dyn IAddChild<Ref<InputScopeName>>> {
155        Rc::new(InputScopeAddChild(this))
156    }
157    fn refresh_item_subscriptions(&self) {
158        let old = std::mem::take(&mut *self.item_subscriptions.borrow_mut());
159        for subscription in old {
160            subscription.dispose();
161        }
162        let mut subscriptions = Vec::new();
163        let mut seen = HashSet::new();
164        for name in self.names.snapshot().iter() {
165            if !seen.insert(name.clone()) {
166                continue;
167            }
168            let weak = self.to_ref().downgrade();
169            subscriptions.push(name.property_changed(move |change| {
170                if change.property() == InputScopeName::name_value_property().as_property() {
171                    if let Some(this) = weak.upgrade() {
172                        this.invalidate();
173                    }
174                }
175            }));
176        }
177        *self.item_subscriptions.borrow_mut() = subscriptions;
178    }
179    fn invalidate(&self) {
180        for (_, handler) in self.changed.snapshot().iter() {
181            handler();
182        }
183    }
184}
185
186impl Drop for InputScope {
187    fn drop(&mut self) {
188        if let Some(token) = self.collection_subscription.take() {
189            self.names.remove_collection_changed(token);
190        }
191        for subscription in self.item_subscriptions.get_mut().drain(..) {
192            subscription.dispose();
193        }
194    }
195}
196
197// Preserve the former Rust convenience setter while exposing the native object
198// property and full collection. Callers can also pass a Ref<InputScope> or None.
199impl From<TextInputContentType> for Option<Ref<InputScope>> {
200    fn from(value: TextInputContentType) -> Self {
201        Some(InputScope::with_name_value(value.into()))
202    }
203}
204impl From<InputScopeNameValue> for Option<Ref<InputScope>> {
205    fn from(value: InputScopeNameValue) -> Self {
206        Some(InputScope::with_name_value(value))
207    }
208}
209
210struct InputScopeAddChild(Ref<InputScope>);
211impl IAddChild<Ref<InputScopeName>> for InputScopeAddChild {
212    fn add_child(&self, value: Ref<InputScopeName>) {
213        self.0.names.add(value);
214    }
215    fn reference_id(&self) -> usize {
216        &*self.0 as *const InputScope as usize
217    }
218}