druid/tests/
helpers.rs

1// Copyright 2020 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//! Helper types for test writing.
16//!
17//! This includes tools for making throwaway widgets more easily.
18//!
19//! Note: Some of these types are undocumented. They're meant to help maintainers of Druid and
20//! people trying to build a framework on top of Druid (like crochet), not to be user-facing.
21
22#![allow(missing_docs)]
23
24use std::cell::RefCell;
25use std::collections::VecDeque;
26use std::rc::Rc;
27
28use crate::*;
29
30pub type EventFn<S, T> = dyn FnMut(&mut S, &mut EventCtx, &Event, &mut T, &Env);
31pub type LifeCycleFn<S, T> = dyn FnMut(&mut S, &mut LifeCycleCtx, &LifeCycle, &T, &Env);
32pub type UpdateFn<S, T> = dyn FnMut(&mut S, &mut UpdateCtx, &T, &T, &Env);
33pub type LayoutFn<S, T> = dyn FnMut(&mut S, &mut LayoutCtx, &BoxConstraints, &T, &Env) -> Size;
34pub type PaintFn<S, T> = dyn FnMut(&mut S, &mut PaintCtx, &T, &Env);
35
36pub const REPLACE_CHILD: Selector = Selector::new("druid-test.replace-child");
37
38/// A widget that can be constructed from individual functions, builder-style.
39///
40/// This widget is generic over its state, which is passed in at construction time.
41pub struct ModularWidget<S, T> {
42    state: S,
43    event: Option<Box<EventFn<S, T>>>,
44    lifecycle: Option<Box<LifeCycleFn<S, T>>>,
45    update: Option<Box<UpdateFn<S, T>>>,
46    layout: Option<Box<LayoutFn<S, T>>>,
47    paint: Option<Box<PaintFn<S, T>>>,
48}
49
50/// A widget that can replace its child on command
51pub struct ReplaceChild<T> {
52    child: WidgetPod<T, Box<dyn Widget<T>>>,
53    replacer: Box<dyn Fn() -> Box<dyn Widget<T>>>,
54}
55
56/// A widget that records each time one of its methods is called.
57///
58/// Make one like this:
59///
60/// ```
61/// # use druid::widget::Label;
62/// # use druid::{WidgetExt, LifeCycle};
63/// use druid::tests::helpers::{Recording, Record, TestWidgetExt};
64/// use druid::tests::harness::Harness;
65/// let recording = Recording::default();
66/// let widget = Label::new("Hello").padding(4.0).record(&recording);
67///
68/// Harness::create_simple((), widget, |harness| {
69///     harness.send_initial_events();
70///     assert!(matches!(recording.next(), Record::L(LifeCycle::WidgetAdded)));
71/// })
72/// ```
73pub struct Recorder<W> {
74    recording: Recording,
75    child: W,
76}
77
78/// A recording of widget method calls.
79#[derive(Debug, Clone, Default)]
80pub struct Recording(Rc<RefCell<VecDeque<Record>>>);
81
82/// A recording of a method call on a widget.
83///
84/// Each member of the enum corresponds to one of the methods on `Widget`.
85#[derive(Debug, Clone)]
86pub enum Record {
87    /// An `Event`.
88    E(Event),
89    /// A `LifeCycle` event.
90    L(LifeCycle),
91    Layout(Size),
92    Update(Region),
93    Paint,
94    // instead of always returning an Option<Record>, we have a none variant;
95    // this would be code smell elsewhere but here I think it makes the tests
96    // easier to read.
97    None,
98}
99
100/// like WidgetExt but just for this one thing
101pub trait TestWidgetExt<T: Data>: Widget<T> + Sized + 'static {
102    fn record(self, recording: &Recording) -> Recorder<Self> {
103        Recorder {
104            child: self,
105            recording: recording.clone(),
106        }
107    }
108}
109
110impl<T: Data, W: Widget<T> + 'static> TestWidgetExt<T> for W {}
111
112#[allow(dead_code)]
113impl<S, T> ModularWidget<S, T> {
114    pub fn new(state: S) -> Self {
115        ModularWidget {
116            state,
117            event: None,
118            lifecycle: None,
119            update: None,
120            layout: None,
121            paint: None,
122        }
123    }
124
125    pub fn event_fn(
126        mut self,
127        f: impl FnMut(&mut S, &mut EventCtx, &Event, &mut T, &Env) + 'static,
128    ) -> Self {
129        self.event = Some(Box::new(f));
130        self
131    }
132
133    pub fn lifecycle_fn(
134        mut self,
135        f: impl FnMut(&mut S, &mut LifeCycleCtx, &LifeCycle, &T, &Env) + 'static,
136    ) -> Self {
137        self.lifecycle = Some(Box::new(f));
138        self
139    }
140
141    pub fn update_fn(
142        mut self,
143        f: impl FnMut(&mut S, &mut UpdateCtx, &T, &T, &Env) + 'static,
144    ) -> Self {
145        self.update = Some(Box::new(f));
146        self
147    }
148
149    pub fn layout_fn(
150        mut self,
151        f: impl FnMut(&mut S, &mut LayoutCtx, &BoxConstraints, &T, &Env) -> Size + 'static,
152    ) -> Self {
153        self.layout = Some(Box::new(f));
154        self
155    }
156
157    pub fn paint_fn(mut self, f: impl FnMut(&mut S, &mut PaintCtx, &T, &Env) + 'static) -> Self {
158        self.paint = Some(Box::new(f));
159        self
160    }
161}
162
163impl<S, T: Data> Widget<T> for ModularWidget<S, T> {
164    fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
165        if let Some(f) = self.event.as_mut() {
166            f(&mut self.state, ctx, event, data, env)
167        }
168    }
169
170    fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
171        if let Some(f) = self.lifecycle.as_mut() {
172            f(&mut self.state, ctx, event, data, env)
173        }
174    }
175
176    fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) {
177        if let Some(f) = self.update.as_mut() {
178            f(&mut self.state, ctx, old_data, data, env)
179        }
180    }
181
182    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size {
183        let ModularWidget {
184            ref mut state,
185            ref mut layout,
186            ..
187        } = self;
188        layout
189            .as_mut()
190            .map(|f| f(state, ctx, bc, data, env))
191            .unwrap_or_else(|| Size::new(100., 100.))
192    }
193
194    fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
195        if let Some(f) = self.paint.as_mut() {
196            f(&mut self.state, ctx, data, env)
197        }
198    }
199}
200
201impl<T: Data> ReplaceChild<T> {
202    pub fn new<W: Widget<T> + 'static>(
203        child: impl Widget<T> + 'static,
204        f: impl Fn() -> W + 'static,
205    ) -> Self {
206        let child = WidgetPod::new(child.boxed());
207        let replacer = Box::new(move || f().boxed());
208        ReplaceChild { child, replacer }
209    }
210}
211
212impl<T: Data> Widget<T> for ReplaceChild<T> {
213    fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
214        if let Event::Command(cmd) = event {
215            if cmd.is(REPLACE_CHILD) {
216                self.child = WidgetPod::new((self.replacer)());
217                ctx.children_changed();
218                return;
219            }
220        }
221        self.child.event(ctx, event, data, env)
222    }
223
224    fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
225        self.child.lifecycle(ctx, event, data, env)
226    }
227
228    fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) {
229        self.child.update(ctx, data, env)
230    }
231
232    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size {
233        self.child.layout(ctx, bc, data, env)
234    }
235
236    fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
237        self.child.paint_raw(ctx, data, env)
238    }
239}
240
241#[allow(dead_code)]
242impl Recording {
243    pub fn is_empty(&self) -> bool {
244        self.0.borrow().is_empty()
245    }
246
247    #[allow(dead_code)]
248    pub fn len(&self) -> usize {
249        self.0.borrow().len()
250    }
251
252    pub fn clear(&self) {
253        self.0.borrow_mut().clear()
254    }
255
256    /// Returns the next event in the recording, if one exists.
257    ///
258    /// This consumes the event.
259    pub fn next(&self) -> Record {
260        self.0.borrow_mut().pop_front().unwrap_or(Record::None)
261    }
262
263    /// Returns an iterator of events drained from the recording.
264    pub fn drain(&self) -> impl Iterator<Item = Record> {
265        self.0
266            .borrow_mut()
267            .drain(..)
268            .collect::<Vec<_>>()
269            .into_iter()
270    }
271
272    fn push(&self, event: Record) {
273        self.0.borrow_mut().push_back(event)
274    }
275}
276
277impl<T: Data, W: Widget<T>> Widget<T> for Recorder<W> {
278    fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
279        self.recording.push(Record::E(event.clone()));
280        self.child.event(ctx, event, data, env)
281    }
282
283    fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
284        let should_record = !matches!(
285            event,
286            LifeCycle::Internal(InternalLifeCycle::DebugRequestState { .. })
287                | LifeCycle::Internal(InternalLifeCycle::DebugInspectState(_))
288        );
289
290        if should_record {
291            self.recording.push(Record::L(event.clone()));
292        }
293
294        self.child.lifecycle(ctx, event, data, env)
295    }
296
297    fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) {
298        self.child.update(ctx, old_data, data, env);
299        self.recording
300            .push(Record::Update(ctx.widget_state.invalid.clone()));
301    }
302
303    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size {
304        let size = self.child.layout(ctx, bc, data, env);
305        self.recording.push(Record::Layout(size));
306        size
307    }
308
309    fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
310        self.child.paint(ctx, data, env);
311        self.recording.push(Record::Paint)
312    }
313}
314
315pub fn widget_ids<const N: usize>() -> [WidgetId; N] {
316    let mut ids = [WidgetId::reserved(0); N];
317
318    for id in &mut ids {
319        *id = WidgetId::next()
320    }
321
322    ids
323}