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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
//! Safe, owned browser DOM handles for Atlas applications.
mod canvas;
mod clipboard;
mod content_box;
mod document;
mod event;
mod file_drop;
mod gpu_canvas;
mod keyboard;
mod page;
mod text;
pub use self::canvas::{CanvasSize, RgbaFrame, WebCanvas};
pub use self::clipboard::WebClipboard;
pub use self::event::{
PointerMetadata, PointerModifiers, PointerType, WebEvent, WebEventListener, WheelDeltaMode,
WheelMetadata,
};
pub use self::file_drop::{BrowserFiles, DropFiles, DropMetadata, DroppedFile, DroppedFileAccess};
pub use self::gpu_canvas::WebGpuCanvas;
pub use self::keyboard::KeyboardMetadata;
pub use self::page::WebLocation;
pub use self::text::{
CompositionMetadata, TextInputMetadata, TextSelection, TextSelectionDirection,
};
use std::io;
pub use crate::content_box::ContentBoxPoint;
use wasm_bindgen::JsCast;
use web_sys::{
Document, Element, HtmlButtonElement, HtmlDialogElement, HtmlElement, HtmlInputElement,
HtmlSelectElement, HtmlTextAreaElement,
};
/// A browser document obtained from the current window.
#[derive(Clone)]
pub struct WebDocument {
document: Document,
}
/// An owned browser element handle.
#[derive(Clone)]
pub struct WebElement {
element: Element,
}
/// An element extent measured in CSS pixels.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ElementSize {
width: f64,
height: f64,
}
impl ElementSize {
/// Returns the rendered width in CSS pixels.
#[must_use]
pub const fn width(self) -> f64 {
self.width
}
/// Returns the rendered height in CSS pixels.
#[must_use]
pub const fn height(self) -> f64 {
self.height
}
}
impl WebElement {
/// Returns the element's identifier, or an empty string when it has none.
#[must_use]
pub fn id(&self) -> String {
self.element.id()
}
/// Replaces the element's HTML contents.
///
/// The caller owns the markup string and must only pass trusted
/// application-authored content. Untrusted values belong in
/// [`Self::set_text`].
pub fn set_inner_html(&self, markup: &str) {
self.element.set_inner_html(markup);
}
/// Replaces the element's text content without interpreting markup.
pub fn set_text(&self, text: &str) {
self.element.set_text_content(Some(text));
}
/// Sets one DOM attribute.
///
/// # Errors
/// Returns the browser's DOM validation error as [`io::ErrorKind::InvalidInput`].
pub fn set_attribute(&self, name: &str, value: &str) -> io::Result<()> {
self.element
.set_attribute(name, value)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Invalid DOM attribute"))
}
/// Sets one inline CSS property through the browser's CSS object model.
///
/// Property-by-property mutation avoids replacing the element's complete
/// `style` attribute and remains usable when a content security policy
/// rejects application-authored inline style declarations.
///
/// # Errors
/// Returns [`io::ErrorKind::InvalidInput`] when this is not an HTML
/// element or setting the property throws a browser exception.
pub fn set_style_property(&self, name: &str, value: &str) -> io::Result<()> {
let element = self.element.dyn_ref::<HtmlElement>().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "DOM element has no CSS style")
})?;
element.style().set_property(name, value).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Browser rejected CSS style property",
)
})
}
/// Returns the element's rendered border-box extent in fractional CSS pixels.
///
/// The dimensions include padding and borders and reflect CSS transforms,
/// matching the browser's `getBoundingClientRect` semantics.
#[must_use]
pub fn bounding_size(&self) -> ElementSize {
let bounds = self.element.get_bounding_client_rect();
ElementSize {
width: bounds.width(),
height: bounds.height(),
}
}
/// Maps a viewport client point into the untransformed local content box.
///
/// The returned point carries the content extent measured in the same
/// layout read as the coordinate. Borders and padding are excluded. The
/// mapping composes this element's and its ancestors' two-dimensional
/// affine CSS transforms. The bounding rectangle supplies only the
/// transformed box translation; its transformed extent is never treated as
/// a local content size.
///
/// # Errors
/// Returns a typed error for unavailable or invalid CSS geometry,
/// three-dimensional or singular transforms, motion paths, or CSS zoom.
/// Closed shadow roots cannot expose their composed transform ancestry;
/// callers embedding a canvas there must keep transforming wrappers inside
/// the inspectable root.
pub fn content_box_point(&self, client_x: f64, client_y: f64) -> io::Result<ContentBoxPoint> {
content_box::measure_point(&self.element, client_x, client_y)
}
/// Returns the disabled state of a button, input, or select control.
#[must_use]
pub fn disabled(&self) -> Option<bool> {
self.element
.dyn_ref::<HtmlButtonElement>()
.map(HtmlButtonElement::disabled)
.or_else(|| {
self.element
.dyn_ref::<HtmlInputElement>()
.map(HtmlInputElement::disabled)
})
.or_else(|| {
self.element
.dyn_ref::<HtmlSelectElement>()
.map(HtmlSelectElement::disabled)
})
}
/// Sets the disabled state of a button, input, or select control.
///
/// # Errors
/// Returns [`io::ErrorKind::InvalidInput`] when this element is not a
/// disableable form control.
pub fn set_disabled(&self, disabled: bool) -> io::Result<()> {
if let Some(button) = self.element.dyn_ref::<HtmlButtonElement>() {
button.set_disabled(disabled);
return Ok(());
}
if let Some(input) = self.element.dyn_ref::<HtmlInputElement>() {
input.set_disabled(disabled);
return Ok(());
}
if let Some(select) = self.element.dyn_ref::<HtmlSelectElement>() {
select.set_disabled(disabled);
return Ok(());
}
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"DOM element is not a disableable form control",
))
}
/// Returns whether this HTML dialog is open.
#[must_use]
pub fn dialog_open(&self) -> Option<bool> {
self.element
.dyn_ref::<HtmlDialogElement>()
.map(HtmlDialogElement::open)
}
/// Opens this HTML dialog as a modal dialog.
///
/// # Errors
/// Returns [`io::ErrorKind::InvalidInput`] when this element is not a
/// dialog or when the browser rejects the modal transition.
pub fn show_modal(&self) -> io::Result<()> {
let dialog = self.element.dyn_ref::<HtmlDialogElement>().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "DOM element is not a dialog")
})?;
dialog.show_modal().map(|_| ()).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "Browser rejected modal dialog")
})
}
/// Closes this HTML dialog.
///
/// # Errors
/// Returns [`io::ErrorKind::InvalidInput`] when this element is not a
/// dialog.
pub fn close_dialog(&self) -> io::Result<()> {
let dialog = self.element.dyn_ref::<HtmlDialogElement>().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "DOM element is not a dialog")
})?;
dialog.close();
Ok(())
}
/// Moves browser focus to this HTML element.
///
/// # Errors
/// Returns [`io::ErrorKind::InvalidInput`] when this element is not an
/// HTML element or when the browser rejects the focus request.
pub fn focus(&self) -> io::Result<()> {
let element = self.element.dyn_ref::<HtmlElement>().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"DOM element cannot receive focus",
)
})?;
element.focus().map(|_| ()).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Browser rejected focus request",
)
})
}
/// Captures subsequent pointer events for this element.
///
/// # Errors
/// Returns [`io::ErrorKind::InvalidInput`] when the browser rejects the
/// pointer identifier or capture request.
pub fn set_pointer_capture(&self, pointer_id: i32) -> io::Result<()> {
self.element.set_pointer_capture(pointer_id).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Browser rejected pointer capture",
)
})
}
/// Releases a pointer previously captured by this element.
///
/// # Errors
/// Returns [`io::ErrorKind::InvalidInput`] when the browser rejects the
/// pointer identifier or release request.
pub fn release_pointer_capture(&self, pointer_id: i32) -> io::Result<()> {
self.element
.release_pointer_capture(pointer_id)
.map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Browser rejected pointer release",
)
})
}
/// Returns whether this element currently captures a pointer identifier.
#[must_use]
pub fn has_pointer_capture(&self, pointer_id: i32) -> bool {
self.element.has_pointer_capture(pointer_id)
}
/// Appends a child and returns no detached handle.
///
/// # Errors
/// Returns [`io::ErrorKind::InvalidInput`] when the browser rejects the
/// parent/child relationship.
pub fn append_child(&self, child: &Self) -> io::Result<()> {
self.element
.append_child(&child.element)
.map(|_| ())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Invalid DOM child"))
}
/// Reads the value of a browser input or select element.
#[must_use]
pub fn value(&self) -> Option<String> {
self.element
.dyn_ref::<HtmlInputElement>()
.map(HtmlInputElement::value)
.or_else(|| {
self.element
.dyn_ref::<HtmlSelectElement>()
.map(HtmlSelectElement::value)
})
}
/// Reads the checked state of a browser input element.
///
/// Returns [`None`] when this element is not an input. Checkbox and radio
/// controls expose their current state through the same browser property;
/// other input kinds return their browser-defined unchecked state.
#[must_use]
pub fn checked(&self) -> Option<bool> {
self.element
.dyn_ref::<HtmlInputElement>()
.map(HtmlInputElement::checked)
}
/// Replaces the value of a browser input, textarea or select element.
///
/// A select element selects the option whose value matches. Browsers
/// clear the selection for a value no option carries; that value is
/// rejected instead and the previous selection is kept.
///
/// # Errors
/// Returns [`io::ErrorKind::InvalidInput`] when the element is not a
/// value-bearing control or a select has no option with `value`.
pub fn set_value(&self, value: &str) -> io::Result<()> {
if let Some(input) = self.element.dyn_ref::<HtmlInputElement>() {
input.set_value(value);
return Ok(());
}
if let Some(textarea) = self.element.dyn_ref::<HtmlTextAreaElement>() {
textarea.set_value(value);
return Ok(());
}
if let Some(select) = self.element.dyn_ref::<HtmlSelectElement>() {
let previous = select.value();
select.set_value(value);
if select.value() == value {
return Ok(());
}
select.set_value(&previous);
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Select element has no option with the requested value",
));
}
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"DOM element is not an input, textarea or select",
))
}
/// Registers a callback whose lifetime is tied to the returned listener.
///
/// Dropping the listener removes the callback from the element before the
/// closure is released, so cancellation cannot leave a JavaScript root.
///
/// # Errors
/// Returns [`io::ErrorKind::InvalidInput`] when the event name is invalid
/// or the browser refuses the listener.
pub fn add_event_listener<F>(
&self,
event_name: &str,
callback: F,
) -> io::Result<WebEventListener>
where
F: FnMut(WebEvent) + 'static,
{
event::listen(self.element.clone().unchecked_into(), event_name, callback)
}
}