1use crate::metadata::PseudoClassesAttribute;
2use crate::primitives::{HeaderedContentControl, TemplatedControlImpl};
3use crate::{ContentControlImpl, ControlImpl};
4use ferroui_base::animation::IPageTransition;
5use ferroui_base::data::{BindingMode, BindingPriority};
6use ferroui_base::input::InputElementImpl;
7use ferroui_base::interactivity::{
8 CancelRoutedEventArgs, Interactive, InteractiveImpl, RoutedEvent, RoutedEventArgs, RoutedEventHandlerToken,
9 RoutingStrategies,
10};
11use ferroui_base::layout::LayoutableImpl;
12use ferroui_base::threading::{
13 CancellationTokenSource, Dispatcher, DispatcherPriority, FerroSynchronizationContext,
14};
15use ferroui_base::{
16 ferro_class, ferro_impl_classes, ferro_property, ferro_routed_event, instantiate, FerroObject, FerroObjectImpl,
17 FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Ref, StyledElementImpl, StyledProperty,
18 StyledPropertyOptions, Visual, VisualImpl,
19};
20use std::cell::{Cell, RefCell};
21use std::rc::Rc;
22
23const PC_EXPANDED: &str = ":expanded";
24const PC_UP: &str = ":up";
25const PC_DOWN: &str = ":down";
26const PC_LEFT: &str = ":left";
27const PC_RIGHT: &str = ":right";
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31#[repr(i32)]
32pub enum ExpandDirection {
33 Down = 0,
35 Up = 1,
37 Left = 2,
39 Right = 3,
41}
42
43#[repr(C)]
45pub struct Expander {
46 base: HeaderedContentControl,
47 ignore_property_changed: Cell<bool>,
48 last_transition_cts: RefCell<Option<CancellationTokenSource>>,
49}
50
51ferro_class! {
52 Expander: HeaderedContentControl, virtuals ExpanderImpl: ContentControlImpl {
53 fn on_collapsed(this, event_args: &RoutedEventArgs);
55 fn on_collapsing(this, event_args: &CancelRoutedEventArgs);
57 fn on_expanded(this, event_args: &RoutedEventArgs);
59 fn on_expanding(this, event_args: &CancelRoutedEventArgs);
61 fn on_coerce_is_expanded(this, value: bool) -> bool;
63 }
64}
65ferroui_base::ferro_class_info!(Expander { new: Expander::new });
66
67ferro_impl_classes!(
68 Expander: StyledElementImpl,
69 VisualImpl,
70 LayoutableImpl,
71 InteractiveImpl,
72 InputElementImpl,
73 TemplatedControlImpl,
74 ContentControlImpl
75);
76
77impl ControlImpl for Expander {
78 fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
79 crate::automation::peers::ExpanderAutomationPeer::new(this).upcast()
80 }
81}
82
83impl FerroObjectImpl for Expander {
84 fn constructed(this: &Self) {
85 Self::parent_constructed(this);
86 this.update_pseudo_classes();
87 }
88
89 fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
90 Self::parent_on_property_changed(this, change);
91
92 if this.ignore_property_changed.get() {
93 return;
94 }
95
96 if change.property() == Self::expand_direction_property().as_property() {
97 this.update_pseudo_classes();
98 } else if change.property() == Self::is_expanded_property().as_property() {
99 this.start_content_transition();
101
102 this.update_pseudo_classes();
103 }
104 }
105}
106
107impl ExpanderImpl for Expander {
108 fn on_collapsed(this: &Self, event_args: &RoutedEventArgs) {
109 this.raise_event(event_args);
110 }
111
112 fn on_collapsing(this: &Self, event_args: &CancelRoutedEventArgs) {
113 this.raise_event(event_args);
114 }
115
116 fn on_expanded(this: &Self, event_args: &RoutedEventArgs) {
117 this.raise_event(event_args);
118 }
119
120 fn on_expanding(this: &Self, event_args: &CancelRoutedEventArgs) {
121 this.raise_event(event_args);
122 }
123
124 fn on_coerce_is_expanded(this: &Self, value: bool) -> bool {
125 let event_args = if value {
126 let event_args = CancelRoutedEventArgs::with_event_and_source(Self::expanding_event(), this.to_ref());
127 this.on_expanding(&event_args);
128 event_args
129 } else {
130 let event_args = CancelRoutedEventArgs::with_event_and_source(Self::collapsing_event(), this.to_ref());
131 this.on_collapsing(&event_args);
132 event_args
133 };
134
135 if event_args.cancel() {
136 this.ignore_property_changed.set(true);
145
146 this.raise_property_changed(
147 Self::is_expanded_property().as_property(),
148 Some(&value),
149 &!value,
150 BindingPriority::LocalValue,
151 true,
152 );
153
154 this.ignore_property_changed.set(false);
155
156 return !value;
157 }
158
159 value
160 }
161}
162
163impl Expander {
164 pub const PSEUDO_CLASSES: PseudoClassesAttribute =
166 PseudoClassesAttribute::new(&[PC_EXPANDED, PC_UP, PC_DOWN, PC_LEFT, PC_RIGHT]);
167}
168
169ferroui_base::ferro_properties! { impl Expander {
170 ferro_property!(
171 pub fn content_transition_property() -> StyledProperty<Option<Rc<dyn IPageTransition>>> {
173 FerroProperty::register::<Expander, _>("ContentTransition", None)
174 }
175 );
176
177 ferro_property!(
178 pub fn expand_direction_property() -> StyledProperty<ExpandDirection> {
180 FerroProperty::register::<Expander, _>("ExpandDirection", ExpandDirection::Down)
181 }
182 );
183
184 ferro_property!(
185 pub fn is_expanded_property() -> StyledProperty<bool> {
187 FerroProperty::register_with::<Expander, _>(
188 "IsExpanded",
189 StyledPropertyOptions::new(false)
190 .default_binding_mode(BindingMode::TwoWay)
191 .coerce(Expander::coerce_is_expanded),
192 )
193 }
194 );
195} }
196
197impl Expander {
198 ferro_routed_event!(
199 pub fn collapsed_event() -> RoutedEvent<RoutedEventArgs> {
201 RoutedEvent::register::<Expander, _>("Collapsed", RoutingStrategies::BUBBLE)
202 }
203 );
204
205 ferro_routed_event!(
206 pub fn collapsing_event() -> RoutedEvent<CancelRoutedEventArgs> {
208 RoutedEvent::register::<Expander, _>("Collapsing", RoutingStrategies::BUBBLE)
209 }
210 );
211
212 ferro_routed_event!(
213 pub fn expanded_event() -> RoutedEvent<RoutedEventArgs> {
215 RoutedEvent::register::<Expander, _>("Expanded", RoutingStrategies::BUBBLE)
216 }
217 );
218
219 ferro_routed_event!(
220 pub fn expanding_event() -> RoutedEvent<CancelRoutedEventArgs> {
222 RoutedEvent::register::<Expander, _>("Expanding", RoutingStrategies::BUBBLE)
223 }
224 );
225
226 pub fn construct() -> Self {
228 Self {
229 base: HeaderedContentControl::construct(),
230 ignore_property_changed: Cell::new(false),
231 last_transition_cts: RefCell::new(None),
232 }
233 }
234
235 pub fn new() -> Ref<Self> {
236 instantiate(Self::construct())
237 }
238
239 pub fn content_transition(&self) -> Option<Rc<dyn IPageTransition>> {
241 self.get_value(Self::content_transition_property())
242 }
243
244 pub fn set_content_transition(&self, value: Option<Rc<dyn IPageTransition>>) {
245 self.set_value(Self::content_transition_property(), value)
246 }
247
248 pub fn expand_direction(&self) -> ExpandDirection {
250 self.get_value(Self::expand_direction_property())
251 }
252
253 pub fn set_expand_direction(&self, value: ExpandDirection) {
254 self.set_value(Self::expand_direction_property(), value)
255 }
256
257 pub fn is_expanded(&self) -> bool {
259 self.get_value(Self::is_expanded_property())
260 }
261
262 pub fn set_is_expanded(&self, value: bool) {
263 self.set_value(Self::is_expanded_property(), value)
264 }
265
266 pub fn collapsed(&self, handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static) -> RoutedEventHandlerToken {
269 self.add_handler(Self::collapsed_event(), handler)
270 }
271
272 pub fn collapsing(
277 &self,
278 handler: impl Fn(&Interactive, &CancelRoutedEventArgs) + 'static,
279 ) -> RoutedEventHandlerToken {
280 self.add_handler(Self::collapsing_event(), handler)
281 }
282
283 pub fn expanded(&self, handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static) -> RoutedEventHandlerToken {
286 self.add_handler(Self::expanded_event(), handler)
287 }
288
289 pub fn expanding(
294 &self,
295 handler: impl Fn(&Interactive, &CancelRoutedEventArgs) + 'static,
296 ) -> RoutedEventHandlerToken {
297 self.add_handler(Self::expanding_event(), handler)
298 }
299
300 fn start_content_transition(&self) {
303 let this = self.to_ref();
304
305 let post_completed = move || {
308 Dispatcher::ui_thread().post_local(
309 move || {
310 if this.is_expanded() {
311 this.on_expanded(&RoutedEventArgs::with_event_and_source(Self::expanded_event(), this.clone()));
312 } else {
313 this.on_collapsed(&RoutedEventArgs::with_event_and_source(
314 Self::collapsed_event(),
315 this.clone(),
316 ));
317 }
318 },
319 DispatcherPriority::DEFAULT,
320 );
321 };
322
323 let content_transition = self.content_transition();
324 let visual_content: Option<Ref<Visual>> = self.presenter().map(Ref::upcast);
325
326 if let (Some(_), Some(content_transition), Some(visual_content)) =
327 (self.content(), content_transition, visual_content)
328 {
329 let expand_direction = self.expand_direction();
330 let forward = expand_direction == ExpandDirection::Left || expand_direction == ExpandDirection::Up;
331
332 let cts = CancellationTokenSource::new();
333 let token = cts.token();
334 let last = self.last_transition_cts.replace(Some(cts));
335 if let Some(last) = last {
336 last.cancel();
337 }
338
339 let task = if self.is_expanded() {
340 content_transition.start(None, Some(&visual_content), forward, token)
341 } else {
342 content_transition.start(Some(&visual_content), None, !forward, token)
343 };
344
345 let context = FerroSynchronizationContext::current().unwrap_or_else(|| {
346 FerroSynchronizationContext::with_dispatcher(
347 &Dispatcher::current_dispatcher(),
348 DispatcherPriority::NORMAL,
349 )
350 });
351 drop(context.to_task_scheduler().start_local(async move {
352 if task.await.is_ok() {
353 post_completed();
354 }
355 }));
356 } else {
357 post_completed();
358 }
359 }
360
361 fn update_pseudo_classes(&self) {
364 let expand_direction = self.expand_direction();
365
366 self.pseudo_classes().set(PC_UP, expand_direction == ExpandDirection::Up);
367 self.pseudo_classes().set(PC_DOWN, expand_direction == ExpandDirection::Down);
368 self.pseudo_classes().set(PC_LEFT, expand_direction == ExpandDirection::Left);
369 self.pseudo_classes().set(PC_RIGHT, expand_direction == ExpandDirection::Right);
370
371 self.pseudo_classes().set(PC_EXPANDED, self.is_expanded());
372 }
373
374 fn coerce_is_expanded(instance: &FerroObject, value: bool) -> bool {
376 if let Some(expander) = instance.downcast_ref::<Expander>() {
377 return expander.on_coerce_is_expanded(value);
378 }
379
380 value
381 }
382}