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
macro_rules! ui_elements {
{$($element:ident : $base_ty:ty => $transform_ty:ty),* $(,)?} => {
#[derive(Clone)]
struct Elements {
$(
$element: $transform_ty,
)*
}
impl Elements {
fn new(document: &Document) -> Result<Elements, JsValue> {
Ok(Elements {
$(
$element: Rc::new(document
.get_element_by_id(stringify!($element))
.ok_or(concat!("failed to find ",
stringify!($element),
" element"))?
.dyn_into::<$base_ty>()?)
.into(),
)*
})
}
}
}
}
macro_rules! waterfallminmax_onchange {
($minmax:ident) => {
paste::paste! {
fn [<$minmax _onchange>](&self) -> Closure<dyn Fn()> {
let ui = self.clone();
Closure::new(move || {
let element = &ui.elements.$minmax;
if !element.report_validity() {
return;
}
if let Some(value) = element.get() {
ui.waterfall.borrow_mut().[<set_ $minmax>](value);
// try_borrow_mut prevents trying to update the
// preferences as a consequence of the
// Preferences::apply_client calling this closure
if let Ok(mut p) = ui.preferences.try_borrow_mut() {
if let Err(e) = p.[<update_ $minmax>](&value) {
web_sys::console::error_1(&e);
}
}
} else {
ui.window
.alert_with_message(concat!("Invalid value for ",
stringify!($minmax)))
.unwrap();
}
})
}
}
};
}
macro_rules! set_on {
($event:ident, $self:expr, $($element:ident),*) => {
paste::paste! {
$(
$self.elements.$element.[<set_on $event>](Some(
$self.[<$element _on $event>]()
.into_js_value()
.unchecked_ref(),
));
)*
}
}
}
macro_rules! impl_patch {
($name:ident, $patch_json:ty, $get_json:ty, $url:expr) => {
paste::paste! {
async fn [<patch_ $name>](&self, json: &$patch_json) -> Result<$get_json, crate::ui::patch::PatchError> {
let request = json_patch($url, json)?;
let response = JsFuture::from(self.window.fetch_with_request(&request))
.await?
.dyn_into::<Response>()?;
if !response.ok() {
let status = response.status();
let text = crate::ui::patch::response_to_string(&response).await?;
web_sys::console::error_1(&format!("PATCH request failed with HTTP code {status}. \
Server returned: {text}").into());
return Err(crate::ui::patch::PatchError::RequestFailed(text));
}
Ok(crate::ui::patch::response_to_json(&response).await?)
}
}
};
}
macro_rules! set_values_if_inactive {
($self:expr, $source:expr, $section:ident, $($element:ident),*) => {
let mut preferences = $self.preferences.borrow_mut();
paste::paste!{
$(
// A checkbox HtmlInputElement is always considered inactive,
// because the user interaction with it is limited to clicking
// (rather than typing). Therefore, we update it regardless of
// whether it has focus.
if !$self.document.is_element_active(stringify!([<$section _ $element>]))
|| $self.elements.[<$section _ $element>].type_() == "checkbox" {
$self.elements.[<$section _ $element>].set(&$source.$element);
}
if let Err(e) = preferences.[<update_ $section _ $element>](&$source.$element) {
web_sys::console::error_1(&e);
}
)*
}
}
}
macro_rules! set_values {
($self:expr, $source:expr, $section:ident, $($element:ident),*) => {
let mut preferences = $self.preferences.borrow_mut();
paste::paste! {
$(
$self.elements.[<$section _ $element>].set(&$source.$element);
if let Err(e) = preferences.[<update_ $section _ $element>](&$source.$element) {
web_sys::console::error_1(&e);
}
)*
}
}
}
macro_rules! impl_update_elements {
($name:ident, $json:ty, $($element:ident),*) => {
paste::paste! {
fn [<update_ $name _inactive_elements>](&self, json: &$json) {
set_values_if_inactive!(
self,
json,
$name,
$(
$element
),*
);
}
fn [<update_ $name _all_elements>](&self, json: &$json) {
set_values!(
self,
json,
$name,
$(
$element
),*
);
}
}
}
}
macro_rules! impl_section {
($name:ident, $json:ty, $patch_json:ty, $url:expr, $($element:ident),*) => {
impl_patch!($name, $patch_json, $json, $url);
impl_update_elements!(
$name,
$json,
$(
$element
),*
);
impl_onchange!(
$name,
$patch_json,
$(
$element
),*
);
paste::paste! {
async fn [<patch_ $name _update_elements>](&self, json: &$patch_json) -> Result<(), JsValue> {
match self.[<patch_ $name>](json).await {
Ok(json_output) => self.[<update_ $name _all_elements>](&json_output),
Err(crate::ui::patch::PatchError::RequestFailed(_)) => {
// The error has already been logged by patch_$name, so we do nothing
// and return Ok(()) so that the promise doesn't fail.
}
Err(crate::ui::patch::PatchError::OtherError(err)) => {
// Unhandled error. Make the promise fail (eventually) with this error.
return Err(err);
}
}
Ok(())
}
}
}
}
macro_rules! impl_onchange {
($name:ident, $patch_json:ty, $($element:ident),*) => {
paste::paste! {
$(
fn [<$name _ $element _onchange>](&self) -> Closure<dyn Fn() -> JsValue> {
let ui = self.clone();
Closure::new(move || {
if !ui.elements.[<$name _ $element>].report_validity() {
return JsValue::NULL;
}
let Some(value) = ui.elements.[<$name _ $element>].get() else {
// TODO: decide what to do in this case, since it passes validity
ui.window
.alert_with_message(concat!(
"Invalid value for ", stringify!([<$name _ $element>])))
.unwrap();
return JsValue::NULL;
};
let patch = $patch_json { $element: Some(value), ..Default::default() };
let ui = ui.clone();
future_to_promise(async move {
let patch = patch;
ui.[<patch_ $name _update_elements>](&patch).await?;
Ok(JsValue::NULL)
}).into()
})
}
)*
}
}
}