Skip to main content

ferroui_controls/
layout_transform_control.rs

1// The idea and the algorithm come from the layout transformer of the
2// Silverlight toolkit.
3
4use crate::{ControlImpl, Control, Decorator};
5use ferroui_base::input::InputElementImpl;
6use ferroui_base::interactivity::InteractiveImpl;
7use ferroui_base::layout::{LayoutableImpl, LayoutableImplExt};
8use ferroui_base::media::{ITransform, MatrixTransform};
9use ferroui_base::reactive::IDisposable;
10use ferroui_base::utilities::math_utilities::min;
11use ferroui_base::{
12    ferro_class, ferro_impl_classes, ferro_property, instantiate, FerroObjectImpl, FerroProperty,
13    FerroPropertyChangedEventArgs, Matrix, Rect, Ref, RelativePoint, RelativeUnit, Size, StyledElementImpl,
14    StyledProperty, Visual, VisualImpl, VisualImplExt, VisualTreeAttachmentEventArgs,
15};
16use std::cell::{Cell, OnceCell, RefCell};
17use std::rc::Rc;
18
19// About ACCEPTABLE_DELTA and DECIMALS_AFTER_ROUND - the original comment from
20// the Silverlight code explains:
21// Note: AcceptableDelta and DecimalsAfterRound work around double arithmetic
22// rounding issues on Silverlight.
23//
24// DECIMALS_AFTER_ROUND was increased from the original 4 to 8 because of
25// issues when zooming in and out at the end of very large objects.
26
27const ACCEPTABLE_DELTA: f64 = 0.0001;
28
29const DECIMALS_AFTER_ROUND: i32 = 8;
30
31/// Control that implements support for transformations as if applied by a
32/// layout transform.
33#[repr(C)]
34pub struct LayoutTransformControl {
35    base: Decorator,
36    render_transform_changed_event: RefCell<Option<Rc<dyn IDisposable>>>,
37    /// Actual desired size of the child when arranged.
38    child_actual_size: Cell<Size>,
39    /// Render transform / matrix transform applied to the transform root.
40    matrix_transform: OnceCell<Ref<MatrixTransform>>,
41    /// Transformation matrix corresponding to the matrix transform.
42    transformation: Cell<Matrix>,
43    /// The subscription to the changed notification of the layout transform.
44    layout_transform_changed: RefCell<Option<Rc<dyn IDisposable>>>,
45}
46
47ferro_class!(LayoutTransformControl: Decorator);
48ferroui_base::ferro_class_info!(LayoutTransformControl { new: LayoutTransformControl::new });
49ferro_impl_classes!(LayoutTransformControl: StyledElementImpl, InteractiveImpl, InputElementImpl, ControlImpl);
50
51ferroui_base::ferro_impl_classes!(LayoutTransformControl: FerroObjectImpl);
52
53impl VisualImpl for LayoutTransformControl {
54    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
55        Self::parent_on_attached_to_visual_tree(this, e);
56        this.subscribe_layout_transform(this.layout_transform());
57        this.apply_layout_transform();
58    }
59
60    fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
61        Self::parent_on_detached_from_visual_tree(this, e);
62        this.unsubscribe_layout_transform();
63    }
64}
65
66impl LayoutableImpl for LayoutTransformControl {
67    /// Provides the behaviour for the "arrange" pass of layout.
68    fn arrange_override(this: &Self, final_size: Size) -> Size {
69        let (Some(transform_root), Some(_)) = (this.transform_root(), this.layout_transform()) else {
70            this.set_current_value(Self::layout_transform_property(), this.render_transform());
71            return Self::parent_arrange_override(this, final_size);
72        };
73
74        // Determine the largest available size after the transformation
75        let mut final_size_transformed = this.compute_largest_transformed_size(final_size);
76        if Self::is_size_smaller(final_size_transformed, transform_root.desired_size()) {
77            // Some elements do not like being given less space than they
78            // asked for (ex: text blocks). Bump the working size up to do the
79            // right thing by them.
80            final_size_transformed = transform_root.desired_size();
81        }
82
83        // Transform the working size to find its width/height
84        let transformed_rect = Rect::new(0.0, 0.0, final_size_transformed.width, final_size_transformed.height)
85            .transform_to_aabb(this.transformation.get());
86        // Create the arrange rect to center the transformed content
87        let final_rect = Rect::new(
88            -transformed_rect.x + ((final_size.width - transformed_rect.width) / 2.0),
89            -transformed_rect.y + ((final_size.height - transformed_rect.height) / 2.0),
90            final_size_transformed.width,
91            final_size_transformed.height,
92        );
93
94        // Perform an arrange on the transform root (containing the child)
95        transform_root.arrange(final_rect);
96        let arranged_size = transform_root.bounds().size();
97
98        // This is the first opportunity to find out the child's true desired
99        // size. The reference implementation leaves the "measure again" step
100        // disabled, so only the flag is maintained.
101        if !(Self::is_size_smaller(final_size_transformed, arranged_size)
102            && this.child_actual_size.get() == Size::default())
103        {
104            // Clear the "need to measure/arrange again" flag
105            this.child_actual_size.set(Size::default());
106        }
107
108        // Return result to perform the transformation
109        final_size
110    }
111
112    /// Provides the behaviour for the "measure" pass of layout.
113    fn measure_override(this: &Self, available_size: Size) -> Size {
114        let (Some(transform_root), Some(_)) = (this.transform_root(), this.layout_transform()) else {
115            return Self::parent_measure_override(this, available_size);
116        };
117
118        let measure_size = if this.child_actual_size.get() == Size::default() {
119            // Determine the largest size after the transformation
120            this.compute_largest_transformed_size(available_size)
121        } else {
122            // Previous measure/arrange pass determined that the child's
123            // desired size was larger than believed
124            this.child_actual_size.get()
125        };
126
127        // Perform a measure on the transform root (containing the child)
128        transform_root.measure(measure_size);
129
130        let desired_size = transform_root.desired_size();
131
132        // Transform the desired size to find its width/height
133        let transformed_desired_rect =
134            Rect::new(0.0, 0.0, desired_size.width, desired_size.height).transform_to_aabb(this.transformation.get());
135
136        // Return result to allocate enough space for the transformation
137        Size::new(transformed_desired_rect.width, transformed_desired_rect.height)
138    }
139}
140
141ferroui_base::ferro_properties! { impl LayoutTransformControl {
142    ferro_property!(
143        /// Defines the `LayoutTransform` property.
144        pub fn layout_transform_property() -> StyledProperty<Option<Rc<dyn ITransform>>> {
145            FerroProperty::register::<LayoutTransformControl, _>("LayoutTransform", None)
146        }
147    );
148
149    ferro_property!(
150        /// Defines the `UseRenderTransform` property.
151        pub fn use_render_transform_property() -> StyledProperty<bool> {
152            FerroProperty::register::<LayoutTransformControl, _>("UseRenderTransform", false)
153        }
154    );
155} }
156
157impl LayoutTransformControl {
158    fn static_constructor() {
159        Visual::clip_to_bounds_property().override_default_value::<LayoutTransformControl>(true);
160
161        Self::layout_transform_property()
162            .changed()
163            .add_class_handler::<LayoutTransformControl>(|x, e| x.on_layout_transform_changed(e));
164
165        Decorator::child_property().changed().add_class_handler::<LayoutTransformControl>(|x, _| x.on_child_changed());
166
167        Self::use_render_transform_property()
168            .changed()
169            .add_class_handler::<LayoutTransformControl>(|x, e| x.on_use_render_transform_property_changed(e));
170    }
171
172    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
173    pub fn construct() -> Self {
174        Self {
175            base: Decorator::construct(),
176            render_transform_changed_event: RefCell::new(None),
177            child_actual_size: Cell::new(Size::default()),
178            matrix_transform: OnceCell::new(),
179            transformation: Cell::new(Matrix::IDENTITY),
180            layout_transform_changed: RefCell::new(None),
181        }
182    }
183
184    pub fn new() -> Ref<Self> {
185        instantiate(Self::construct())
186    }
187
188    /// A graphics transformation that should apply to this element when
189    /// layout is performed.
190    pub fn layout_transform(&self) -> Option<Rc<dyn ITransform>> {
191        self.get_value(Self::layout_transform_property())
192    }
193
194    pub fn set_layout_transform(&self, value: Option<Rc<dyn ITransform>>) {
195        self.set_value(Self::layout_transform_property(), value)
196    }
197
198    /// Whether the layout transform follows the render transform of the
199    /// control.
200    pub fn use_render_transform(&self) -> bool {
201        self.get_value(Self::use_render_transform_property())
202    }
203
204    pub fn set_use_render_transform(&self, value: bool) {
205        self.set_value(Self::use_render_transform_property(), value)
206    }
207
208    /// The control the transformation is applied to: the child.
209    pub fn transform_root(&self) -> Option<Ref<Control>> {
210        self.child()
211    }
212
213    fn matrix_transform(&self) -> &Ref<MatrixTransform> {
214        self.matrix_transform.get_or_init(MatrixTransform::new)
215    }
216
217    fn on_use_render_transform_property_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
218        // HACK: In theory, this method and the `UseRenderTransform` property
219        //       shouldn't exist but it's hard to animate this particular
220        //       control with style animations without property paths.
221        //
222        //       So until we get that implemented, we'll stick on this
223        //       not-so-good workaround.
224
225        let should_use_render_transform = e.get_new_value::<bool>();
226        if should_use_render_transform {
227            let subscription = Visual::render_transform_property().changed().subscribe(|x| {
228                if let Some(target) = x.sender().downcast_ref::<LayoutTransformControl>() {
229                    target.set_layout_transform(target.render_transform());
230                }
231            });
232            *self.render_transform_changed_event.borrow_mut() = Some(subscription);
233        } else {
234            let subscription = self.render_transform_changed_event.borrow().clone();
235            if let Some(subscription) = subscription {
236                subscription.dispose();
237            }
238            self.clear_value(Self::layout_transform_property());
239        }
240    }
241
242    fn on_child_changed(&self) {
243        if let Some(transform_root) = self.transform_root() {
244            transform_root.set_render_transform(Some(self.matrix_transform().into()));
245            transform_root.set_render_transform_origin(RelativePoint::new(0.0, 0.0, RelativeUnit::Absolute));
246        }
247
248        self.apply_layout_transform();
249    }
250
251    /// Tests if the first size is significantly smaller than the second.
252    fn is_size_smaller(a: Size, b: Size) -> bool {
253        (a.width + ACCEPTABLE_DELTA < b.width) || (a.height + ACCEPTABLE_DELTA < b.height)
254    }
255
256    /// Rounds the non-offset elements of a matrix to avoid issues due to
257    /// floating point imprecision.
258    fn round_matrix(matrix: Matrix, decimals: i32) -> Matrix {
259        Matrix::new(
260            round(matrix.m11, decimals),
261            round(matrix.m12, decimals),
262            round(matrix.m21, decimals),
263            round(matrix.m22, decimals),
264            matrix.m31,
265            matrix.m32,
266        )
267    }
268
269    /// Applies the layout transform on the content.
270    fn apply_layout_transform(&self) {
271        // Get the transform matrix and apply it
272        let matrix = match self.layout_transform() {
273            None => Matrix::IDENTITY,
274            Some(layout_transform) => Self::round_matrix(layout_transform.value(), DECIMALS_AFTER_ROUND),
275        };
276
277        if self.transformation.get() == matrix {
278            return;
279        }
280
281        self.transformation.set(matrix);
282        self.matrix_transform().set_matrix(matrix);
283
284        // New transform means re-layout is necessary
285        self.invalidate_measure();
286    }
287
288    /// Computes the largest usable size after applying the transformation to
289    /// the specified bounds.
290    fn compute_largest_transformed_size(&self, mut arrange_bounds: Size) -> Size {
291        // Computed largest transformed size
292        let mut computed_size = Size::default();
293        let transformation = self.transformation.get();
294
295        // Detect infinite bounds and constrain the scenario
296        let infinite_width = arrange_bounds.width.is_infinite();
297        if infinite_width {
298            arrange_bounds = arrange_bounds.with_width(arrange_bounds.height);
299        }
300        let infinite_height = arrange_bounds.height.is_infinite();
301        if infinite_height {
302            arrange_bounds = arrange_bounds.with_height(arrange_bounds.width);
303        }
304
305        // Capture the matrix parameters
306        let a = transformation.m11;
307        let b = transformation.m12;
308        let c = transformation.m21;
309        let d = transformation.m22;
310
311        // Compute maximum possible transformed width/height based on starting
312        // width/height. These constraints define two lines in the positive
313        // x/y quadrant.
314        let max_width_from_width = (arrange_bounds.width / a).abs();
315        let max_height_from_width = (arrange_bounds.width / c).abs();
316        let max_width_from_height = (arrange_bounds.height / b).abs();
317        let max_height_from_height = (arrange_bounds.height / d).abs();
318
319        // The transformed width/height that maximize the area under each
320        // segment is its midpoint. At most one of the two midpoints will
321        // satisfy both constraints.
322        let ideal_width_from_width = max_width_from_width / 2.0;
323        let ideal_height_from_width = max_height_from_width / 2.0;
324        let ideal_width_from_height = max_width_from_height / 2.0;
325        let ideal_height_from_height = max_height_from_height / 2.0;
326
327        // Compute slope of both constraint lines
328        let slope_from_width = -(max_height_from_width / max_width_from_width);
329        let slope_from_height = -(max_height_from_height / max_width_from_height);
330
331        if (0.0 == arrange_bounds.width) || (0.0 == arrange_bounds.height) {
332            // Check for empty bounds
333            computed_size = Size::new(arrange_bounds.width, arrange_bounds.height);
334        } else if infinite_width && infinite_height {
335            // Check for completely unbound scenario
336            computed_size = Size::new(f64::INFINITY, f64::INFINITY);
337        } else if !transformation.has_inverse() {
338            // Check for singular matrix
339            computed_size = Size::new(0.0, 0.0);
340        } else if (0.0 == b) || (0.0 == c) {
341            // Check for 0/180 degree special cases
342            let max_height = if infinite_height { f64::INFINITY } else { max_height_from_height };
343            let max_width = if infinite_width { f64::INFINITY } else { max_width_from_width };
344            if (0.0 == b) && (0.0 == c) {
345                // No constraints
346                computed_size = Size::new(max_width, max_height);
347            } else if 0.0 == b {
348                // Constrained by width
349                let computed_height = min(ideal_height_from_width, max_height);
350                computed_size = Size::new(max_width - ((c * computed_height) / a).abs(), computed_height);
351            } else if 0.0 == c {
352                // Constrained by height
353                let computed_width = min(ideal_width_from_height, max_width);
354                computed_size = Size::new(computed_width, max_height - ((b * computed_width) / d).abs());
355            }
356        } else if (0.0 == a) || (0.0 == d) {
357            // Check for 90/270 degree special cases
358            let max_width = if infinite_height { f64::INFINITY } else { max_width_from_height };
359            let max_height = if infinite_width { f64::INFINITY } else { max_height_from_width };
360            if (0.0 == a) && (0.0 == d) {
361                // No constraints
362                computed_size = Size::new(max_width, max_height);
363            } else if 0.0 == a {
364                // Constrained by width
365                let computed_height = min(ideal_height_from_height, max_height);
366                computed_size = Size::new(max_width - ((d * computed_height) / b).abs(), computed_height);
367            } else if 0.0 == d {
368                // Constrained by height
369                let computed_width = min(ideal_width_from_width, max_width);
370                computed_size = Size::new(computed_width, max_height - ((a * computed_width) / c).abs());
371            }
372        } else if ideal_height_from_width <= ((slope_from_height * ideal_width_from_width) + max_height_from_height) {
373            // Check the width midpoint for viability (by being below the
374            // height constraint line)
375            computed_size = Size::new(ideal_width_from_width, ideal_height_from_width);
376        } else if ideal_height_from_height <= ((slope_from_width * ideal_width_from_height) + max_height_from_width) {
377            // Check the height midpoint for viability (by being below the
378            // width constraint line)
379            computed_size = Size::new(ideal_width_from_height, ideal_height_from_height);
380        } else {
381            // Neither midpoint is viable; use the intersection of the two
382            // constraint lines instead.
383            // Compute width by setting heights equal (m1*x+c1=m2*x+c2)
384            let computed_width =
385                (max_height_from_height - max_height_from_width) / (slope_from_width - slope_from_height);
386            // Compute height from width constraint line (y=m*x+c; using
387            // height would give same result)
388            computed_size = Size::new(computed_width, (slope_from_width * computed_width) + max_height_from_width);
389        }
390
391        // Return result
392        computed_size
393    }
394
395    fn on_layout_transform_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
396        if self.is_attached_to_visual_tree() {
397            self.unsubscribe_layout_transform();
398            self.subscribe_layout_transform(e.get_new_value::<Option<Rc<dyn ITransform>>>());
399        }
400
401        self.apply_layout_transform();
402    }
403
404    /// Subscribes to the changed notification of a mutable transform.
405    fn subscribe_layout_transform(&self, transform: Option<Rc<dyn ITransform>>) {
406        let Some(transform) = transform else { return };
407        let Some(transform) = transform.as_mutable_transform() else { return };
408
409        let weak = self.to_ref().downgrade();
410        let subscription = transform.changed(Rc::new(move || {
411            if let Some(this) = weak.upgrade() {
412                this.apply_layout_transform();
413            }
414        }));
415        *self.layout_transform_changed.borrow_mut() = Some(subscription);
416    }
417
418    /// Ends the subscription made by `subscribe_layout_transform`.
419    fn unsubscribe_layout_transform(&self) {
420        let subscription = self.layout_transform_changed.borrow_mut().take();
421        if let Some(subscription) = subscription {
422            subscription.dispose();
423        }
424    }
425}
426
427/// Rounds a value to the given number of fractional digits, midpoints to the
428/// nearest even digit.
429fn round(value: f64, decimals: i32) -> f64 {
430    if value.abs() < 1e16 {
431        let power10 = 10f64.powi(decimals);
432        (value * power10).round_ties_even() / power10
433    } else {
434        value
435    }
436}