Skip to main content

ferroui_controls/
input_pane_aware_decorator.rs

1use crate::platform::{IInputPane, InputPaneStateEventArgs};
2use crate::{ControlImpl, Decorator, InputPaneAwareBehavior, TopLevel};
3use ferroui_base::animation::easings::{Easing, LinearEasing};
4use ferroui_base::animation::{DoubleTransition, ITransition, TimeSpan, Transitions};
5use ferroui_base::input::InputElementImpl;
6use ferroui_base::interactivity::InteractiveImpl;
7use ferroui_base::layout::{Layoutable, LayoutableImpl, LayoutableImplExt};
8use ferroui_base::reactive::IDisposable;
9use ferroui_base::{
10    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, instantiate, DirectProperty, FerroObjectImpl,
11    FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Matrix, Point, Rect, Ref, Size,
12    StyledElementImpl, StyledProperty, Thickness, VisualImpl, VisualImplExt, VisualTreeAttachmentEventArgs,
13};
14use std::cell::{Cell, RefCell};
15use std::rc::Rc;
16
17/// A control that automatically adjusts the position or the height of its
18/// child based on the height of the input pane to ensure the content is
19/// visible.
20#[repr(C)]
21pub struct InputPaneAwareDecorator {
22    base: Decorator,
23    behavior: Cell<InputPaneAwareBehavior>,
24    input_pane: RefCell<Option<Rc<dyn IInputPane>>>,
25    /// The subscription to the state changed event of the input pane.
26    input_pane_state_changed: RefCell<Option<Rc<dyn IDisposable>>>,
27    first_layout_done: Cell<bool>,
28}
29
30ferro_class!(InputPaneAwareDecorator: Decorator);
31ferro_class_info!(InputPaneAwareDecorator { new: InputPaneAwareDecorator::new });
32ferro_impl_classes!(InputPaneAwareDecorator: StyledElementImpl, InteractiveImpl, InputElementImpl, ControlImpl);
33
34impl FerroObjectImpl for InputPaneAwareDecorator {
35    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
36        Self::parent_on_property_changed(this, change);
37
38        if change.property() == Self::behavior_property().as_property() {
39            this.ensure_input_pane_padding_applied();
40        }
41    }
42}
43
44impl LayoutableImpl for InputPaneAwareDecorator {
45    fn measure_override(this: &Self, available_size: Size) -> Size {
46        if this.input_pane.borrow().is_some()
47            && this.first_layout_done.get()
48            && this.visual_root().is_some()
49            && this.behavior() == InputPaneAwareBehavior::Resize
50        {
51            this.set_current_value(
52                Decorator::padding_property(),
53                Thickness::new(0.0, 0.0, 0.0, this.current_input_pane_padding()),
54            );
55        }
56        Self::parent_measure_override(this, available_size)
57    }
58
59    fn arrange_override(this: &Self, final_size: Size) -> Size {
60        this.first_layout_done.set(true);
61
62        if this.behavior() == InputPaneAwareBehavior::Pan {
63            if let Some(child) = this.child() {
64                child.arrange(Rect::from_position_size(
65                    Point::new(0.0, -this.current_input_pane_padding()),
66                    final_size,
67                ));
68            }
69
70            final_size
71        } else {
72            Self::parent_arrange_override(this, final_size)
73        }
74    }
75}
76
77impl VisualImpl for InputPaneAwareDecorator {
78    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
79        Self::parent_on_attached_to_visual_tree(this, e);
80
81        let input_pane = TopLevel::get_top_level(Some(this)).and_then(|top_level| top_level.input_pane());
82        if let Some(input_pane) = &input_pane {
83            // The handler refers to the decorator weakly: the input pane
84            // belongs to the platform and must not keep the decorator alive.
85            let weak = this.to_ref().downgrade();
86            let subscription = input_pane.state_changed(Rc::new(move |e| {
87                if let Some(this) = weak.upgrade() {
88                    this.input_pane_aware_view_state_changed(e);
89                }
90            }));
91            *this.input_pane_state_changed.borrow_mut() = Some(subscription);
92        }
93        *this.input_pane.borrow_mut() = input_pane;
94        this.ensure_input_pane_padding_applied();
95    }
96
97    fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
98        Self::parent_on_detached_from_visual_tree(this, e);
99        let subscription = this.input_pane_state_changed.borrow_mut().take();
100        if let Some(subscription) = subscription {
101            subscription.dispose();
102        }
103        *this.input_pane.borrow_mut() = None;
104        this.set_current_value(Decorator::padding_property(), Thickness::default());
105        this.set_current_input_pane_padding(0.0);
106    }
107}
108
109ferro_properties! {
110    impl InputPaneAwareDecorator {
111        /// Defines the `Behavior` property.
112        pub fn behavior_property() -> DirectProperty<InputPaneAwareDecorator, InputPaneAwareBehavior> {
113            FerroProperty::register_direct::<InputPaneAwareDecorator, _>(
114                "Behavior",
115                |o| o.behavior(),
116                Some(|o, x| o.set_behavior(x)),
117                InputPaneAwareBehavior::None,
118            )
119        }
120
121        fn current_input_pane_padding_property() -> StyledProperty<f64> {
122            FerroProperty::register::<InputPaneAwareDecorator, _>("CurrentInputPanePadding", 0.0)
123        }
124    }
125}
126
127impl InputPaneAwareDecorator {
128    fn static_constructor() {
129        Layoutable::affects_measure::<InputPaneAwareDecorator>(&[
130            Self::behavior_property().as_property(),
131            Self::current_input_pane_padding_property().as_property(),
132        ]);
133        Layoutable::affects_arrange::<InputPaneAwareDecorator>(&[
134            Self::current_input_pane_padding_property().as_property()
135        ]);
136    }
137
138    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
139    pub fn construct() -> Self {
140        Self {
141            base: Decorator::construct(),
142            behavior: Cell::new(InputPaneAwareBehavior::None),
143            input_pane: RefCell::new(None),
144            input_pane_state_changed: RefCell::new(None),
145            first_layout_done: Cell::new(false),
146        }
147    }
148
149    pub fn new() -> Ref<Self> {
150        instantiate(Self::construct())
151    }
152
153    /// How the view reacts to the input pane state.
154    pub fn behavior(&self) -> InputPaneAwareBehavior {
155        self.behavior.get()
156    }
157
158    pub fn set_behavior(&self, value: InputPaneAwareBehavior) {
159        self.set_and_raise_cell(Self::behavior_property(), &self.behavior, value);
160    }
161
162    fn current_input_pane_padding(&self) -> f64 {
163        self.get_value(Self::current_input_pane_padding_property())
164    }
165
166    fn set_current_input_pane_padding(&self, value: f64) {
167        self.set_value(Self::current_input_pane_padding_property(), value)
168    }
169
170    fn ensure_input_pane_padding_applied(&self) {
171        self.set_current_value(Decorator::padding_property(), Thickness::default());
172
173        let input_pane = self.input_pane.borrow().clone();
174        match (input_pane, self.visual_root()) {
175            (Some(input_pane), Some(root))
176                if self.first_layout_done.get() && self.behavior() != InputPaneAwareBehavior::None =>
177            {
178                let occluded_rect = input_pane.occluded_rect();
179
180                let transform_matrix = self.transform_to_visual(&root).unwrap_or(Matrix::IDENTITY);
181
182                let translated_rect =
183                    Rect::from_size(self.bounds().size()).transform_to_aabb(transform_matrix);
184
185                let intersect = occluded_rect.intersect(translated_rect);
186                self.set_current_input_pane_padding(intersect.height);
187            }
188            _ => self.set_current_input_pane_padding(0.0),
189        }
190    }
191
192    fn input_pane_aware_view_state_changed(&self, e: &InputPaneStateEventArgs) {
193        let transition = DoubleTransition::new();
194        transition.set_property(Some(Self::current_input_pane_padding_property().as_property()));
195        transition.set_duration(TimeSpan::from(e.animation_duration()));
196        // Only an easing class is used; anything else is replaced by the
197        // linear easing.
198        transition.set_easing(match e.easing() {
199            Some(easing) if easing.derives_from_easing() => Easing::from_rc(easing.clone()),
200            _ => Easing::new(LinearEasing::new()),
201        });
202
203        let transition: Rc<dyn ITransition> = transition.into();
204        self.set_transitions(Some(Transitions::from_items([transition])));
205
206        self.ensure_input_pane_padding_applied();
207
208        self.invalidate_measure();
209    }
210}