druid/widget/
aspect_ratio_box.rs

1// Copyright 2021 The Druid Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::debug_state::DebugState;
16
17use crate::widget::Axis;
18use druid::widget::prelude::*;
19use druid::Data;
20use tracing::{instrument, warn};
21
22/// A widget that preserves the aspect ratio given to it.
23///
24/// If given a child, this widget forces the child to have a width and height that preserves
25/// the aspect ratio.
26///
27/// If not given a child, The box will try to size itself  as large or small as possible
28/// to preserve the aspect ratio.
29pub struct AspectRatioBox<T> {
30    child: Box<dyn Widget<T>>,
31    ratio: f64,
32}
33
34impl<T> AspectRatioBox<T> {
35    /// Create container with a child and aspect ratio.
36    ///
37    /// The aspect ratio is defined as width / height.
38    ///
39    /// If aspect ratio <= 0.0, the ratio will be set to 1.0
40    pub fn new(child: impl Widget<T> + 'static, ratio: f64) -> Self {
41        Self {
42            child: Box::new(child),
43            ratio: clamp_ratio(ratio),
44        }
45    }
46
47    /// Set the ratio of the box.
48    ///
49    /// The ratio has to be a value between 0 and f64::MAX, excluding 0. It will be clamped
50    /// to those values if they exceed the bounds. If the ratio is 0, then the ratio
51    /// will become 1.
52    pub fn set_ratio(&mut self, ratio: f64) {
53        self.ratio = clamp_ratio(ratio);
54    }
55
56    /// Generate `BoxConstraints` that fit within the provided `BoxConstraints`.
57    ///
58    /// If the generated constraints do not fit then they are constrained to the
59    /// provided `BoxConstraints`.
60    fn generate_constraints(&self, bc: &BoxConstraints) -> BoxConstraints {
61        let (mut new_width, mut new_height) = (bc.max().width, bc.max().height);
62
63        if new_width == f64::INFINITY {
64            new_width = new_height * self.ratio;
65        } else {
66            new_height = new_width / self.ratio;
67        }
68
69        if new_width > bc.max().width {
70            new_width = bc.max().width;
71            new_height = new_width / self.ratio;
72        }
73
74        if new_height > bc.max().height {
75            new_height = bc.max().height;
76            new_width = new_height * self.ratio;
77        }
78
79        if new_width < bc.min().width {
80            new_width = bc.min().width;
81            new_height = new_width / self.ratio;
82        }
83
84        if new_height < bc.min().height {
85            new_height = bc.min().height;
86            new_width = new_height * self.ratio;
87        }
88
89        BoxConstraints::tight(bc.constrain(Size::new(new_width, new_height)))
90    }
91}
92
93/// Clamps the ratio between 0.0 and f64::MAX
94/// If ratio is 0.0 then it will return 1.0 to avoid creating NaN
95fn clamp_ratio(mut ratio: f64) -> f64 {
96    ratio = f64::clamp(ratio, 0.0, f64::MAX);
97
98    if ratio == 0.0 {
99        warn!("Provided ratio was <= 0.0.");
100        1.0
101    } else {
102        ratio
103    }
104}
105
106impl<T: Data> Widget<T> for AspectRatioBox<T> {
107    #[instrument(
108        name = "AspectRatioBox",
109        level = "trace",
110        skip(self, ctx, event, data, env)
111    )]
112    fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
113        self.child.event(ctx, event, data, env);
114    }
115
116    #[instrument(
117        name = "AspectRatioBox",
118        level = "trace",
119        skip(self, ctx, event, data, env)
120    )]
121    fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
122        self.child.lifecycle(ctx, event, data, env)
123    }
124
125    #[instrument(
126        name = "AspectRatioBox",
127        level = "trace",
128        skip(self, ctx, old_data, data, env)
129    )]
130    fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) {
131        self.child.update(ctx, old_data, data, env);
132    }
133
134    #[instrument(
135        name = "AspectRatioBox",
136        level = "trace",
137        skip(self, ctx, bc, data, env)
138    )]
139    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size {
140        bc.debug_check("AspectRatioBox");
141
142        if bc.max() == bc.min() {
143            warn!("Box constraints are tight. Aspect ratio box will not be able to preserve aspect ratio.");
144
145            return self.child.layout(ctx, bc, data, env);
146        }
147        if bc.max().width == f64::INFINITY && bc.max().height == f64::INFINITY {
148            warn!("Box constraints are INFINITE. Aspect ratio box won't be able to choose a size because the constraints given by the parent widget are INFINITE.");
149
150            return self.child.layout(ctx, bc, data, env);
151        }
152
153        let bc = self.generate_constraints(bc);
154
155        self.child.layout(ctx, &bc, data, env)
156    }
157
158    #[instrument(name = "AspectRatioBox", level = "trace", skip(self, ctx, data, env))]
159    fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
160        self.child.paint(ctx, data, env);
161    }
162
163    fn id(&self) -> Option<WidgetId> {
164        self.child.id()
165    }
166
167    fn debug_state(&self, data: &T) -> DebugState {
168        DebugState {
169            display_name: self.short_type_name().to_string(),
170            children: vec![self.child.debug_state(data)],
171            ..Default::default()
172        }
173    }
174
175    fn compute_max_intrinsic(
176        &mut self,
177        axis: Axis,
178        ctx: &mut LayoutCtx,
179        bc: &BoxConstraints,
180        data: &T,
181        env: &Env,
182    ) -> f64 {
183        match axis {
184            Axis::Horizontal => {
185                if bc.is_height_bounded() {
186                    bc.max().height * self.ratio
187                } else {
188                    self.child.compute_max_intrinsic(axis, ctx, bc, data, env)
189                }
190            }
191            Axis::Vertical => {
192                if bc.is_width_bounded() {
193                    bc.max().width / self.ratio
194                } else {
195                    self.child.compute_max_intrinsic(axis, ctx, bc, data, env)
196                }
197            }
198        }
199    }
200}