1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
//! [`ThemeBuilder`]: builds its subtree **from the ambient theme**.
//!
//! Milestone 309 gave the framework `caller ?? theme ?? framework`, and milestone 310
//! carried it into layout. Both work by handing a widget the theme when it is asked how
//! it looks or how big it is. That covers everything a theme decides *about a widget*,
//! and nothing a theme decides *about a composition*.
//!
//! An application bar is the example that forced this. Whether it centres its title
//! changes **which children exist and in what order** — a centred title is a spring, a
//! title and another spring; a flush one is a title after the leading. By the time
//! `paint` is called the row has already been assembled, so no amount of theme-at-paint
//! can answer it. And an `AppBar` is put together by a builder that never sees a theme in
//! the first place.
//!
//! ```ignore
//! ThemeBuilder::new(|theme| {
//! AppBar::new()
//! .title(Text::new("Inbox"))
//! .center_title(theme.widgets.app_bar.center_title.unwrap_or(platform()))
//! .build()
//! })
//! ```
//!
//! ## Why this is not `LayoutBuilder`
//!
//! [`LayoutBuilder`](crate::LayoutBuilder) also builds late, and its documentation is
//! blunt about the price: the content is rebuilt every frame from a **box**, so it has no
//! retained state — no persistent focus, no deferred overlays. A field inside one would
//! lose the caret between frames.
//!
//! A theme is not a box. It is the same from frame to frame unless the application
//! changes it, and when it changes, the tree is rebuilt anyway. So this builds **once per
//! instance**, the subtree keeps its positional identity, and retained state survives —
//! which is the whole reason an application bar can live in one and keep its overflow
//! menu open.
use std::cell::{OnceCell, RefCell};
use frus_core::{Rect, Scene};
use frus_layout::Style;
use crate::interaction::Status;
use crate::theme::Theme;
use crate::widget::Widget;
/// What a [`ThemeBuilder`] holds until the theme arrives: a composition that has not been
/// made yet.
type Deferred<Msg> = Box<dyn FnOnce(&Theme) -> Box<dyn Widget<Msg>>>;
/// Builds its child from the ambient theme (`theme → widget`).
///
/// Transparent otherwise: it has no box, no paint and no identity of its own, so the
/// child sits exactly where the builder does.
pub struct ThemeBuilder<Msg = crate::callback::Callback> {
/// Taken and run the first time the layout pass reaches this node. `FnOnce`, not
/// `Fn`: what it captures is usually a builder holding boxed widgets, which cannot be
/// produced twice.
build: RefCell<Option<Deferred<Msg>>>,
/// The built child, as the one-element slice [`Widget::children`] has to hand back.
built: OnceCell<Vec<Box<dyn Widget<Msg>>>>,
}
impl<Msg: 'static> ThemeBuilder<Msg> {
/// Defers a subtree until the ambient theme is known.
pub fn new<W: Widget<Msg> + 'static>(build: impl FnOnce(&Theme) -> W + 'static) -> Self {
Self {
build: RefCell::new(Some(Box::new(move |theme| {
Box::new(build(theme)) as Box<dyn Widget<Msg>>
}))),
built: OnceCell::new(),
}
}
/// Defers an **already boxed** subtree, which is what a builder's `build()` returns.
pub fn boxed(build: impl FnOnce(&Theme) -> Box<dyn Widget<Msg>> + 'static) -> Self {
Self {
build: RefCell::new(Some(Box::new(build))),
built: OnceCell::new(),
}
}
/// The child, building it if the layout pass has not already.
///
/// Every traversal goes through here rather than reading the cell directly, so a
/// caller that reaches this node without the layout pass having been down it first —
/// a bare `natural_size`, a test — gets a built subtree rather than an empty one.
fn child(&self, theme: &Theme) -> &[Box<dyn Widget<Msg>>] {
if self.built.get().is_none() {
let built = match self.build.borrow_mut().take() {
Some(build) => vec![build(theme)],
// Already taken and stored by another call: the `get` below finds it.
None => Vec::new(),
};
let _ = self.built.set(built);
}
self.built.get().map(Vec::as_slice).unwrap_or(&[])
}
}
impl<Msg: Clone + 'static> Widget<Msg> for ThemeBuilder<Msg> {
fn build_themed(&self, theme: &Theme) {
self.child(theme);
}
fn style(&self) -> Style {
Style::default()
}
fn style_themed(&self, theme: &Theme) -> Style {
// The builder is **transparent**: it takes the child's box, so putting one around
// a widget does not change where that widget lands.
self.child(theme)
.first()
.map(|c| c.style_themed(theme))
.unwrap_or_default()
}
fn children(&self) -> &[Box<dyn Widget<Msg>>] {
// Unbuilt, this is empty — which is why `build_themed` runs on the way down in
// the layout pass, before anything reads it. A traversal that arrives first
// should call `crate::build_deferred`, not this.
//
// That rule was a comment for three milestones and the shell broke it: a tree
// rebuilt outside a frame, on a burst of keystrokes, was read straight away and
// every deferred subtree in it answered "no children" — so nothing inside one
// could be found, focused, or scrolled to. It is a tripwire now, because the
// failure it guards against is **silence**, and silence is what a reader of this
// code would have to notice on their own.
#[cfg(debug_assertions)]
if self.built.get().is_none() && self.build.borrow().is_some() {
panic!(
"a ThemeBuilder's children were read before it was built: a traversal \
reached this tree before any layout pass did. Call `build_deferred(tree, \
&theme)` on it first — milestone 415."
);
}
self.built.get().map(Vec::as_slice).unwrap_or(&[])
}
fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
fn on_click(&self) -> Option<Msg> {
None
}
fn debug_name(&self) -> &'static str {
"ThemeBuilder"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::container::Container;
use crate::ui::build_ui;
use crate::{Runtime, WidgetId};
use frus_core::Color;
#[derive(Clone, Debug, PartialEq)]
enum Msg {}
/// The point of the widget: a subtree that could not be decided until the theme was
/// known. The same builder under two themes produces two different trees.
#[test]
fn the_subtree_is_built_from_the_ambient_theme() {
let under = |theme: Theme| {
// A subtree whose *shape* comes from the theme, which is the case no
// paint-time or style-time hook can serve.
let builder: ThemeBuilder<Msg> =
ThemeBuilder::new(|t: &Theme| Container::new().width(t.radius).height(10.0));
Widget::<Msg>::build_themed(&builder, &theme);
let children = Widget::<Msg>::children(&builder);
assert_eq!(children.len(), 1, "exactly one child, always");
children[0].style().width
};
let sharp = Theme {
radius: 0.0,
..Theme::default()
};
let round = Theme {
radius: 24.0,
..Theme::default()
};
assert_eq!(under(sharp), frus_layout::Dimension::Length(0.0));
assert_eq!(under(round), frus_layout::Dimension::Length(24.0));
}
/// A traversal that arrives before the layout pass must not see an empty node. Asking
/// for the style builds the subtree, so a bare `natural_size` or a test is not a
/// different answer from a real frame.
#[test]
fn asking_for_the_style_builds_it() {
let builder: ThemeBuilder<Msg> =
ThemeBuilder::new(|_: &Theme| Container::new().width(120.0).height(40.0));
let style = Widget::<Msg>::style_themed(&builder, &Theme::default());
assert_eq!(style.width, frus_layout::Dimension::Length(120.0));
assert_eq!(
Widget::<Msg>::children(&builder).len(),
1,
"and the child is there afterwards"
);
}
/// Transparent: the builder has no box of its own, so the child lands exactly where
/// it would have without it.
#[test]
fn the_builder_takes_no_room() {
let bare: Container<Msg> = Container::new()
.width(200.0)
.height(50.0)
.color(Color::rgb(1.0, 0.0, 0.0));
let wrapped: ThemeBuilder<Msg> = ThemeBuilder::new(|_: &Theme| {
Container::new()
.width(200.0)
.height(50.0)
.color(Color::rgb(1.0, 0.0, 0.0))
});
let rect_of = |w: &dyn Widget<Msg>| {
let runtime = Runtime::default();
let ui = build_ui(
w,
frus_core::Size::new(400.0, 300.0),
&runtime,
&Theme::default(),
);
ui.scene()
.primitives()
.iter()
.find_map(|p| match p {
frus_core::Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
_ => None,
})
.expect("the child painted")
};
assert_eq!(rect_of(&bare), rect_of(&wrapped));
}
/// Built **once**, not once per traversal: the closure runs a single time however
/// many passes go over the node, which is what lets the subtree keep retained state.
#[test]
fn the_subtree_is_built_once() {
use std::rc::Rc;
let calls = Rc::new(std::cell::Cell::new(0));
let seen = calls.clone();
let builder: ThemeBuilder<Msg> = ThemeBuilder::new(move |_: &Theme| {
seen.set(seen.get() + 1);
Container::new().width(1.0)
});
let theme = Theme::default();
for _ in 0..3 {
Widget::<Msg>::build_themed(&builder, &theme);
let _ = Widget::<Msg>::style_themed(&builder, &theme);
let _ = Widget::<Msg>::children(&builder);
}
assert_eq!(calls.get(), 1);
}
/// The subtree keeps a **stable identity** across frames, which is what separates this
/// from `LayoutBuilder`: the child's id is the builder's child id, and it does not
/// move because the builder was rebuilt.
#[test]
fn the_child_keeps_its_identity() {
let id_of = || {
let builder: ThemeBuilder<Msg> =
ThemeBuilder::new(|_: &Theme| Container::new().width(1.0));
Widget::<Msg>::build_themed(&builder, &Theme::default());
let child = &Widget::<Msg>::children(&builder)[0];
crate::ui::child_id(WidgetId::from_u64(7), 0, child.as_ref())
};
assert_eq!(id_of(), id_of());
}
}