druid/widget/
widget_ext.rs

1// Copyright 2019 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
15//! Convenience methods for widgets.
16
17use super::invalidation::DebugInvalidation;
18#[allow(deprecated)]
19use super::Parse;
20use super::{
21    Added, Align, BackgroundBrush, Click, Container, Controller, ControllerHost, EnvScope,
22    IdentityWrapper, LensWrap, Padding, SizedBox, WidgetId,
23};
24use crate::widget::{DisabledIf, Scroll};
25use crate::{
26    Color, Data, Env, EventCtx, Insets, KeyOrValue, Lens, LifeCycleCtx, UnitPoint, Widget,
27};
28
29/// A trait that provides extra methods for combining `Widget`s.
30pub trait WidgetExt<T: Data>: Widget<T> + Sized + 'static {
31    /// Wrap this widget in a [`Padding`] widget with the given [`Insets`].
32    ///
33    /// Like [`Padding::new`], this can accept a variety of arguments, including
34    /// a [`Key`] referring to [`Insets`] in the [`Env`].
35    ///
36    /// [`Key`]: crate::Key
37    fn padding(self, insets: impl Into<KeyOrValue<Insets>>) -> Padding<T, Self> {
38        Padding::new(insets, self)
39    }
40
41    /// Wrap this widget in an [`Align`] widget, configured to center it.
42    fn center(self) -> Align<T> {
43        Align::centered(self)
44    }
45
46    /// Wrap this widget in an [`Align`] widget, configured to align left.
47    fn align_left(self) -> Align<T> {
48        Align::left(self)
49    }
50
51    /// Wrap this widget in an [`Align`] widget, configured to align right.
52    fn align_right(self) -> Align<T> {
53        Align::right(self)
54    }
55
56    /// Wrap this widget in an [`Align`] widget, configured to align vertically.
57    fn align_vertical(self, align: UnitPoint) -> Align<T> {
58        Align::vertical(align, self)
59    }
60
61    /// Wrap this widget in an [`Align`] widget, configured to align horizontally.
62    fn align_horizontal(self, align: UnitPoint) -> Align<T> {
63        Align::horizontal(align, self)
64    }
65
66    /// Wrap this widget in a [`SizedBox`] with an explicit width.
67    fn fix_width(self, width: impl Into<KeyOrValue<f64>>) -> SizedBox<T> {
68        SizedBox::new(self).width(width)
69    }
70
71    /// Wrap this widget in a [`SizedBox`] with an explicit height.
72    fn fix_height(self, height: impl Into<KeyOrValue<f64>>) -> SizedBox<T> {
73        SizedBox::new(self).height(height)
74    }
75
76    /// Wrap this widget in an [`SizedBox`] with an explicit width and height
77    fn fix_size(
78        self,
79        width: impl Into<KeyOrValue<f64>>,
80        height: impl Into<KeyOrValue<f64>>,
81    ) -> SizedBox<T> {
82        SizedBox::new(self).width(width).height(height)
83    }
84
85    /// Wrap this widget in a [`SizedBox`] with an infinite width and height.
86    ///
87    /// Only call this method if you want your widget to occupy all available
88    /// space. If you only care about expanding in one of width or height, use
89    /// [`expand_width`] or [`expand_height`] instead.
90    ///
91    /// [`expand_height`]: WidgetExt::expand_height
92    /// [`expand_width`]: WidgetExt::expand_width
93    fn expand(self) -> SizedBox<T> {
94        SizedBox::new(self).expand()
95    }
96
97    /// Wrap this widget in a [`SizedBox`] with an infinite width.
98    ///
99    /// This will force the child to use all available space on the x-axis.
100    fn expand_width(self) -> SizedBox<T> {
101        SizedBox::new(self).expand_width()
102    }
103
104    /// Wrap this widget in a [`SizedBox`] with an infinite width.
105    ///
106    /// This will force the child to use all available space on the y-axis.
107    fn expand_height(self) -> SizedBox<T> {
108        SizedBox::new(self).expand_height()
109    }
110
111    /// Wrap this widget in a [`Container`] with the provided background `brush`.
112    ///
113    /// See [`Container::background`] for more information.
114    fn background(self, brush: impl Into<BackgroundBrush<T>>) -> Container<T> {
115        Container::new(self).background(brush)
116    }
117
118    /// Wrap this widget in a [`Container`] with the provided foreground `brush`.
119    ///
120    /// See [`Container::foreground`] for more information.
121    fn foreground(self, brush: impl Into<BackgroundBrush<T>>) -> Container<T> {
122        Container::new(self).foreground(brush)
123    }
124
125    /// Wrap this widget in a [`Container`] with the given border.
126    ///
127    /// Arguments can be either concrete values, or a [`Key`] of the respective
128    /// type.
129    ///
130    /// [`Key`]: crate::Key
131    fn border(
132        self,
133        color: impl Into<KeyOrValue<Color>>,
134        width: impl Into<KeyOrValue<f64>>,
135    ) -> Container<T> {
136        Container::new(self).border(color, width)
137    }
138
139    /// Wrap this widget in a [`EnvScope`] widget, modifying the parent
140    /// [`Env`] with the provided closure.
141    fn env_scope(self, f: impl Fn(&mut Env, &T) + 'static) -> EnvScope<T, Self> {
142        EnvScope::new(f, self)
143    }
144
145    /// Wrap this widget with the provided [`Controller`].
146    fn controller<C: Controller<T, Self>>(self, controller: C) -> ControllerHost<Self, C> {
147        ControllerHost::new(self, controller)
148    }
149
150    /// Provide a closure that will be called when this widget is added to the widget tree.
151    ///
152    /// You can use this to perform any initial setup.
153    ///
154    /// This is equivalent to handling the [`LifeCycle::WidgetAdded`] event in a
155    /// custom [`Controller`].
156    ///
157    /// [`LifeCycle::WidgetAdded`]: crate::LifeCycle::WidgetAdded
158    fn on_added(
159        self,
160        f: impl Fn(&mut Self, &mut LifeCycleCtx, &T, &Env) + 'static,
161    ) -> ControllerHost<Self, Added<T, Self>> {
162        ControllerHost::new(self, Added::new(f))
163    }
164
165    /// Control the events of this widget with a [`Click`] widget. The closure
166    /// provided will be called when the widget is clicked with the left mouse
167    /// button.
168    ///
169    /// The child widget will also be updated on [`LifeCycle::HotChanged`] and
170    /// mouse down, which can be useful for painting based on `ctx.is_active()`
171    /// and `ctx.is_hot()`.
172    ///
173    /// [`LifeCycle::HotChanged`]: crate::LifeCycle::HotChanged
174    fn on_click(
175        self,
176        f: impl Fn(&mut EventCtx, &mut T, &Env) + 'static,
177    ) -> ControllerHost<Self, Click<T>> {
178        ControllerHost::new(self, Click::new(f))
179    }
180
181    /// Draw the [`layout`] `Rect`s of  this widget and its children.
182    ///
183    /// [`layout`]: Widget::layout
184    fn debug_paint_layout(self) -> EnvScope<T, Self> {
185        EnvScope::new(|env, _| env.set(Env::DEBUG_PAINT, true), self)
186    }
187
188    /// Display the `WidgetId`s for this widget and its children, when hot.
189    ///
190    /// When this is `true`, widgets that are `hot` (are under the mouse cursor)
191    /// will display their ids in their bottom right corner.
192    ///
193    /// These ids may overlap; in this case the id of a child will obscure
194    /// the id of its parent.
195    fn debug_widget_id(self) -> EnvScope<T, Self> {
196        EnvScope::new(|env, _| env.set(Env::DEBUG_WIDGET_ID, true), self)
197    }
198
199    /// Draw a color-changing rectangle over this widget, allowing you to see the
200    /// invalidation regions.
201    fn debug_invalidation(self) -> DebugInvalidation<T, Self> {
202        DebugInvalidation::new(self)
203    }
204
205    /// Set the [`DEBUG_WIDGET`] env variable for this widget (and its descendants).
206    ///
207    /// This does nothing by default, but you can use this variable while
208    /// debugging to only print messages from particular instances of a widget.
209    ///
210    /// [`DEBUG_WIDGET`]: crate::Env::DEBUG_WIDGET
211    fn debug_widget(self) -> EnvScope<T, Self> {
212        EnvScope::new(|env, _| env.set(Env::DEBUG_WIDGET, true), self)
213    }
214
215    /// Wrap this widget in a [`LensWrap`] widget for the provided [`Lens`].
216    fn lens<S: Data, L: Lens<S, T>>(self, lens: L) -> LensWrap<S, T, L, Self> {
217        LensWrap::new(self, lens)
218    }
219
220    /// Parse a `Widget<String>`'s contents
221    #[doc(hidden)]
222    #[deprecated(since = "0.7.0", note = "Use TextBox::with_formatter instead")]
223    #[allow(deprecated)]
224    fn parse(self) -> Parse<Self>
225    where
226        Self: Widget<String>,
227    {
228        Parse::new(self)
229    }
230
231    /// Assign the widget a specific [`WidgetId`].
232    ///
233    /// You must ensure that a given [`WidgetId`] is only ever used for
234    /// a single widget at a time.
235    ///
236    /// An id _may_ be reused over time; for instance if you replace one
237    /// widget with another, you may reuse the first widget's id.
238    fn with_id(self, id: WidgetId) -> IdentityWrapper<Self> {
239        IdentityWrapper::wrap(self, id)
240    }
241
242    /// Wrap this widget in a `Box`.
243    fn boxed(self) -> Box<dyn Widget<T>> {
244        Box::new(self)
245    }
246
247    /// Wrap this widget in a [`Scroll`] widget.
248    fn scroll(self) -> Scroll<T, Self> {
249        Scroll::new(self)
250    }
251
252    /// Wrap this widget in a [`DisabledIf`] widget.
253    ///
254    /// The provided closure will determine if the widget is disabled.
255    /// See [`is_disabled`] or [`set_disabled`] for more info about disabled state.
256    ///
257    /// [`is_disabled`]: EventCtx::is_disabled
258    /// [`set_disabled`]: EventCtx::set_disabled
259    fn disabled_if(self, disabled_if: impl Fn(&T, &Env) -> bool + 'static) -> DisabledIf<T, Self> {
260        DisabledIf::new(self, disabled_if)
261    }
262}
263
264impl<T: Data, W: Widget<T> + 'static> WidgetExt<T> for W {}
265
266// these are 'soft overrides' of methods on WidgetExt; resolution
267// will choose an impl on a type over an impl in a trait for methods with the same
268// name.
269
270#[doc(hidden)]
271impl<T: Data> SizedBox<T> {
272    pub fn fix_width(self, width: impl Into<KeyOrValue<f64>>) -> SizedBox<T> {
273        self.width(width)
274    }
275
276    pub fn fix_height(self, height: impl Into<KeyOrValue<f64>>) -> SizedBox<T> {
277        self.height(height)
278    }
279}
280
281// if two things are modifying an env one after another, just combine the modifications
282#[doc(hidden)]
283impl<T: Data, W> EnvScope<T, W> {
284    pub fn env_scope(self, f2: impl Fn(&mut Env, &T) + 'static) -> EnvScope<T, W> {
285        let EnvScope { f, child } = self;
286        let new_f = move |env: &mut Env, data: &T| {
287            f(env, data);
288            f2(env, data);
289        };
290        EnvScope {
291            f: Box::new(new_f),
292            child,
293        }
294    }
295
296    pub fn debug_paint_layout(self) -> EnvScope<T, W> {
297        self.env_scope(|env, _| env.set(Env::DEBUG_PAINT, true))
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use crate::widget::Slider;
305    use crate::{Color, Key};
306    use test_log::test;
307
308    #[test]
309    fn container_reuse() {
310        // this should be Container<Align<Container<Slider>>>
311        let widget = Slider::new()
312            .background(Color::BLACK)
313            .foreground(Color::WHITE)
314            .align_left()
315            .border(Color::BLACK, 1.0);
316        assert!(widget.border_is_some());
317        assert!(!widget.background_is_some());
318        assert!(!widget.foreground_is_some());
319
320        // this should be Container<Slider>
321        let widget = Slider::new()
322            .background(Color::BLACK)
323            .border(Color::BLACK, 1.0)
324            .foreground(Color::WHITE);
325        assert!(widget.background_is_some());
326        assert!(widget.border_is_some());
327        assert!(widget.foreground_is_some());
328    }
329
330    #[test]
331    fn sized_box_reuse() {
332        let mut env = Env::empty();
333
334        // this should be SizedBox<Align<SizedBox<Slider>>>
335        let widget = Slider::new().fix_height(10.0).align_left().fix_width(1.0);
336        assert_eq!(widget.width_and_height(&env), (Some(1.0), None));
337
338        // this should be SizedBox<Slider>
339        let widget = Slider::new().fix_height(10.0).fix_width(1.0);
340        assert_eq!(widget.width_and_height(&env), (Some(1.0), Some(10.0)));
341
342        const HEIGHT_KEY: Key<f64> = Key::new("test-sized-box-reuse-height");
343        const WIDTH_KEY: Key<f64> = Key::new("test-sized-box-reuse-width");
344        env.set(HEIGHT_KEY, 10.0);
345        env.set(WIDTH_KEY, 1.0);
346
347        // this should be SizedBox<Align<SizedBox<Slider>>>
348        let widget = Slider::new()
349            .fix_height(HEIGHT_KEY)
350            .align_left()
351            .fix_width(WIDTH_KEY);
352        assert_eq!(widget.width_and_height(&env), (Some(1.0), None));
353
354        // this should be SizedBox<Slider>
355        let widget = Slider::new().fix_height(HEIGHT_KEY).fix_width(WIDTH_KEY);
356        assert_eq!(widget.width_and_height(&env), (Some(1.0), Some(10.0)));
357    }
358
359    /// we only care that this will compile; see
360    /// https://github.com/linebender/druid/pull/1414/
361    #[test]
362    fn lens_with_generic_param() {
363        use crate::widget::{Checkbox, Flex, Slider};
364
365        #[derive(Debug, Clone, Data, Lens)]
366        struct MyData<T> {
367            data: T,
368            floatl: f64,
369        }
370
371        #[allow(dead_code)]
372        fn make_widget() -> impl Widget<MyData<bool>> {
373            Flex::row()
374                .with_child(Slider::new().lens(MyData::<bool>::floatl))
375                .with_child(Checkbox::new("checkbox").lens(MyData::<bool>::data))
376        }
377    }
378}