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}