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
273
274
275
// Example of retained mode UI on top of RAUI.
// It's goals are very similar to Unreal's UMG on top of Slate.
// Evolution of this approach allows to use retained mode views
// within declarative mode widgets and vice versa - they
// interleave quite seamingly.
use std::any::Any;
use raui_app::app::retained::RetainedApp;
use raui_core::{
application::ChangeNotifier,
make_widget,
widget::{
component::{
containers::{
content_box::content_box,
horizontal_box::{HorizontalBoxProps, horizontal_box},
vertical_box::{VerticalBoxProps, vertical_box},
},
image_box::{ImageBoxProps, image_box},
interactive::{
button::{ButtonNotifyMessage, ButtonNotifyProps, button},
navigation::{NavItemActive, use_nav_container_active},
},
text_box::{TextBoxProps, text_box},
},
context::{WidgetContext, WidgetMountOrChangeContext},
node::WidgetNode,
unit::{flex::FlexBoxItemLayout, text::TextBoxFont},
utils::Color,
},
};
use raui_retained::{View, ViewState, ViewValue};
const FONT: &str = "./demos/hello-world/resources/verdana.ttf";
// root view of an application.
struct AppView {
pub counter: View<CounterView>,
pub increment_button: View<Button<LabelView>>,
pub decrement_button: View<Button<LabelView>>,
}
impl ViewState for AppView {
// `on_render` method constructs declarative nodes out of
// retained node. this is similar to how Unreal's UMG builds
// Slate widgets tree. you can do here whatever you would do
// normally in RAUI widget component functions.
fn on_render(&self, mut context: WidgetContext) -> WidgetNode {
// as usual, at least root view should produce navigable
// container to enable navigation on the UI, here navigation
// being button clicks.
context.use_hook(use_nav_container_active);
make_widget!(vertical_box)
.with_props(VerticalBoxProps {
override_slots_layout: Some(FlexBoxItemLayout {
basis: Some(48.0),
grow: 0.0,
shrink: 0.0,
..Default::default()
}),
..Default::default()
})
.listed_slot(self.counter.component().key("counter"))
.listed_slot(
make_widget!(horizontal_box)
.with_props(HorizontalBoxProps {
separation: 50.0,
..Default::default()
})
.listed_slot(self.increment_button.component().key("increment"))
.listed_slot(self.decrement_button.component().key("decrement")),
)
.into()
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
// counter view stores value that can notify RAUI about this
// widget being changed, so it will get re-renderred.
// if we don't wrap data that has to be observed in `ViewValue`,
// then we would need to find other way to notify RAUI app
// about the change in data whenever it happen, usually manually.
// alternatively we could use View-Model feature as we would
// normally do with RAUI, if we don't want to store host data in
// views (which is always good approach to take).
struct CounterView {
pub counter: ViewValue<usize>,
}
impl ViewState for CounterView {
fn on_render(&self, _: WidgetContext) -> WidgetNode {
make_widget!(text_box)
// to allow `ViewValue` notify RAUI app about changes,
// we need to pass its widget ref to RAUI component.
// `ViewValue` pass that widget id to change notifications.
.idref(self.counter.widget_ref())
.with_props(TextBoxProps {
text: self.counter.to_string(),
font: TextBoxFont {
name: FONT.to_owned(),
size: 32.0,
},
color: Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
},
..Default::default()
})
.into()
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
struct LabelView {
pub text: String,
}
impl LabelView {
fn new(text: impl ToString) -> Self {
Self {
text: text.to_string(),
}
}
}
impl ViewState for LabelView {
fn on_render(&self, _: WidgetContext) -> WidgetNode {
make_widget!(text_box)
.with_props(TextBoxProps {
text: self.text.to_owned(),
font: TextBoxFont {
name: FONT.to_owned(),
size: 32.0,
},
color: Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
},
..Default::default()
})
.into()
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
// button that can store `on click` callback that
// gets called whenever RAUI button detects click.
struct Button<T: ViewState> {
pub content: View<T>,
on_click: Option<Box<dyn Fn() + Send + Sync>>,
}
impl<T: ViewState> Button<T> {
fn new(content: View<T>) -> Self {
Self {
content,
on_click: None,
}
}
fn on_click(mut self, on_click: impl Fn() + Send + Sync + 'static) -> Self {
self.on_click = Some(Box::new(on_click));
self
}
}
impl<T: ViewState> ViewState for Button<T> {
fn on_change(&mut self, context: WidgetMountOrChangeContext) {
// as usual, we listen for button messages sent to this
// widget and call stored callback.
if let Some(on_click) = self.on_click.take() {
for message in context.messenger.messages {
if let Some(message) = message.as_any().downcast_ref::<ButtonNotifyMessage>()
&& message.trigger_start()
{
on_click();
}
}
self.on_click = Some(on_click);
}
}
fn on_render(&self, context: WidgetContext) -> WidgetNode {
make_widget!(button)
.with_props(NavItemActive)
// this enables RAUI interaction system to send button
// events to same widget
.with_props(ButtonNotifyProps(context.id.to_owned().into()))
.named_slot(
"content",
make_widget!(content_box)
.listed_slot(make_widget!(image_box).with_props(ImageBoxProps::colored(
Color {
r: 0.75,
g: 0.75,
b: 0.75,
a: 1.0,
},
)))
.listed_slot(self.content.component()),
)
.into()
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
// here we construct application view tree out of view objects.
fn create_app(notifier: ChangeNotifier) -> View<AppView> {
// create counter view and get lazy handles to it for buttons to use.
// nice thing about lazy views is that they can be shared across
// entire application - think of them just as handles/references to
// any view you create, that can be accessed from whatever place.
let counter = View::new(CounterView {
counter: ViewValue::new(0).with_notifier(notifier),
});
let lazy_counter_increment = counter.lazy();
let lazy_counter_decrement = counter.lazy();
let increment_button = View::new(Button::new(View::new(LabelView::new("Add"))).on_click(
move || {
// we can access other views using lazy views.
*lazy_counter_increment.write().unwrap().counter += 1;
},
));
let decrement_button = View::new(Button::new(View::new(LabelView::new("Subtract"))).on_click(
move || {
let mut access = lazy_counter_decrement.write().unwrap();
*access.counter = access.counter.saturating_sub(1);
},
));
View::new(AppView {
counter,
increment_button,
decrement_button,
})
}
fn main() {
RetainedApp::simple("Retained mode UI", create_app);
}