Skip to main content

ferroui_browser/interop/
input_helper.rs

1use super::completion_helper::{await_promise, PromiseOutcome};
2use super::{non_null, JsObject};
3use crate::browser_top_level_impl::BrowserTopLevelImpl;
4use wasm_bindgen::prelude::*;
5
6#[wasm_bindgen(raw_module = "./ferroui.js")]
7extern "C" {
8    /// The hidden input element of a view, for the properties the text
9    /// input method reads from it.
10    pub type HtmlInputElement;
11
12    #[wasm_bindgen(method, getter, js_name = selectionStart)]
13    fn selection_start_raw(this: &HtmlInputElement) -> Option<i32>;
14
15    #[wasm_bindgen(method, getter, js_name = selectionEnd)]
16    fn selection_end_raw(this: &HtmlInputElement) -> Option<i32>;
17
18    /// Subscribes to the input events of the element of a top-level.
19    /// Returns the subscription, to be passed to
20    /// [`unsubscribe_input_events`].
21    #[wasm_bindgen(js_namespace = InputHelper, js_name = subscribeInputEvents)]
22    pub fn subscribe_input_events(html_element: &JsObject, top_level_id: i32) -> JsObject;
23
24    /// Ends a subscription made by [`subscribe_input_events`].
25    // Differs from the original, which drops the subscription and never ends it: the top-level
26    // unsubscribes when it is disposed.
27    #[wasm_bindgen(js_namespace = InputHelper, js_name = unsubscribeInputEvents)]
28    pub fn unsubscribe_input_events(subscription: &JsObject);
29
30    /// The points the browser coalesced into a pointer move, six numbers
31    /// per point: x, y, pressure, x tilt, y tilt, twist.
32    #[wasm_bindgen(js_namespace = InputHelper, js_name = getCoalescedEvents)]
33    pub fn get_coalesced_events(pointer_event: &JsObject) -> Vec<f64>;
34
35    /// Empties the hidden input element.
36    #[wasm_bindgen(js_namespace = InputHelper, js_name = clearInput)]
37    pub fn clear_input_element(html_element: &JsObject);
38
39    /// Gives the keyboard focus of the page to the element.
40    #[wasm_bindgen(js_namespace = InputHelper, js_name = focusElement)]
41    pub fn focus_element(html_element: &JsObject);
42
43    /// Sets the CSS cursor of the element; `"default"` removes it.
44    #[wasm_bindgen(js_namespace = InputHelper, js_name = setCursor)]
45    pub fn set_cursor(html_element: &JsObject, kind: &str);
46
47    #[wasm_bindgen(js_namespace = InputHelper, js_name = hide)]
48    pub fn hide_element(html_element: &JsObject);
49
50    #[wasm_bindgen(js_namespace = InputHelper, js_name = show)]
51    pub fn show_element(html_element: &JsObject);
52
53    /// Mirrors the text around the caret and the selection into the hidden
54    /// input element.
55    #[wasm_bindgen(js_namespace = InputHelper, js_name = setSurroundingText)]
56    pub fn set_surrounding_text(html_element: &JsObject, text: &str, start: i32, end: i32);
57
58    /// Moves the hidden input element so that its caret is where the caret
59    /// of the text is.
60    #[wasm_bindgen(js_namespace = InputHelper, js_name = setBounds)]
61    pub fn set_bounds(html_element: &JsObject, x: i32, y: i32, width: i32, height: i32, caret: i32);
62
63    /// Installs the handlers of the page that are not bound to a view.
64    #[wasm_bindgen(js_namespace = InputHelper, js_name = initializeBackgroundHandlers)]
65    pub fn initialize_background_handlers(global_this: &JsObject);
66
67    /// Whether the clipboard of the page can hold values of a format.
68    #[wasm_bindgen(js_namespace = InputHelper, js_name = isClipboardFormatSupported)]
69    pub fn is_clipboard_format_supported(format: &str) -> bool;
70
71    /// Creates the list of items written to the clipboard.
72    #[wasm_bindgen(js_namespace = InputHelper, js_name = createWriteableClipboardSource)]
73    pub fn create_writeable_clipboard_source() -> JsObject;
74
75    /// Adds an item to a list created by
76    /// [`create_writeable_clipboard_source`] and returns it.
77    #[wasm_bindgen(js_namespace = InputHelper, js_name = createWriteableClipboardItem)]
78    pub fn create_writeable_clipboard_item(source: &JsObject) -> JsObject;
79
80    #[wasm_bindgen(js_namespace = InputHelper, js_name = addStringToWriteableClipboardItem)]
81    pub fn add_string_to_writeable_clipboard_item(item: &JsObject, format: &str, value: &str);
82
83    #[wasm_bindgen(js_namespace = InputHelper, js_name = addBytesToWriteableClipboardItem)]
84    pub fn add_bytes_to_writeable_clipboard_item(item: &JsObject, format: &str, value: &[u8]);
85
86    /// Reads the clipboard. Answers with a promise of the result (see
87    /// [`ClipboardResult`]).
88    #[wasm_bindgen(js_namespace = InputHelper, js_name = readClipboard)]
89    fn read_clipboard_raw(window: &JsObject) -> JsObject;
90
91    /// Writes the items of a list to the clipboard, or empties it when
92    /// there is no list. Answers with a promise of `"denied"` when the page
93    /// is not allowed to write, and of an empty string otherwise.
94    #[wasm_bindgen(js_namespace = InputHelper, js_name = writeClipboard)]
95    fn write_clipboard_raw(global_this: &JsObject, source: Option<&JsObject>) -> JsObject;
96
97    /// The format strings of a readable data item.
98    #[wasm_bindgen(js_namespace = InputHelper, js_name = getReadableDataItemFormats)]
99    pub fn get_readable_data_item_formats(item: &JsObject) -> Vec<String>;
100
101    /// Reads the value of a readable data item in a format. Answers with a
102    /// promise of the value (see [`ReadableDataValue`]) or of `null`.
103    #[wasm_bindgen(js_namespace = InputHelper, js_name = tryGetReadableDataItemValueAsync)]
104    fn try_get_readable_data_item_value_async_raw(item: &JsObject, format: &str) -> JsObject;
105
106    /// Reads the value of an item of a drag operation in a format, while
107    /// the event of the operation is being dispatched: the value (see
108    /// [`ReadableDataValue`]) or `null`.
109    #[wasm_bindgen(js_namespace = InputHelper, js_name = tryGetReadableDataItemValue)]
110    fn try_get_readable_data_item_value_raw(item: &JsObject, format: &str) -> JsObject;
111
112    // The identifier of a pointer is a whole number the page holds as a number.
113    #[wasm_bindgen(js_namespace = InputHelper, js_name = setPointerCapture)]
114    fn set_pointer_capture_raw(container_element: &JsObject, pointer_id: f64);
115
116    #[wasm_bindgen(js_namespace = InputHelper, js_name = releasePointerCapture)]
117    fn release_pointer_capture_raw(container_element: &JsObject, pointer_id: f64);
118
119    /// What `readClipboard` answers with: an error (`"denied"`) or the items
120    /// of the clipboard.
121    pub type ClipboardResult;
122
123    #[wasm_bindgen(method, getter)]
124    fn error(this: &ClipboardResult) -> Option<String>;
125
126    #[wasm_bindgen(method, getter)]
127    fn result(this: &ClipboardResult) -> JsObject;
128
129    /// An array of the page.
130    pub type JsArray;
131
132    #[wasm_bindgen(method, getter)]
133    fn length(this: &JsArray) -> u32;
134
135    #[wasm_bindgen(method, indexing_getter)]
136    fn get(this: &JsArray, index: u32) -> JsObject;
137
138    /// A value read from a readable data item: `type` is `"string"`,
139    /// `"bytes"` or `"file"`, and `value` the string, the bytes or the file.
140    pub type ReadableDataValue;
141
142    #[wasm_bindgen(method, getter, js_name = type)]
143    fn value_type(this: &ReadableDataValue) -> Option<String>;
144
145    #[wasm_bindgen(method, getter, js_name = value)]
146    fn string_value(this: &ReadableDataValue) -> Option<String>;
147
148    #[wasm_bindgen(method, getter, js_name = value)]
149    fn bytes_value(this: &ReadableDataValue) -> Vec<u8>;
150
151    #[wasm_bindgen(method, getter, js_name = value)]
152    fn object_value(this: &ReadableDataValue) -> JsObject;
153
154    /// The data transfer of a drag event of the page.
155    pub type DataTransfer;
156
157    #[wasm_bindgen(method, getter, js_name = effectAllowed)]
158    fn effect_allowed_raw(this: &DataTransfer) -> Option<String>;
159
160    #[wasm_bindgen(method, setter, js_name = dropEffect)]
161    fn set_drop_effect_raw(this: &DataTransfer, value: &str);
162}
163
164/// The outcome of reading the clipboard of the page.
165pub struct ClipboardReadResult {
166    /// `"denied"` when the page is not allowed to read the clipboard.
167    pub error: Option<String>,
168    /// The array of readable data items, if there is one.
169    pub items: Option<JsObject>,
170}
171
172/// Reads the clipboard of the page.
173pub async fn read_clipboard_async(window: &JsObject) -> PromiseOutcome<ClipboardReadResult> {
174    let result = await_promise(&read_clipboard_raw(window)).await?;
175    let result = result.unchecked_ref::<ClipboardResult>();
176    Ok(ClipboardReadResult { error: result.error(), items: non_null(result.result()) })
177}
178
179/// Writes a list of items to the clipboard of the page, or empties it.
180/// Resolves to `"denied"` when the page is not allowed to write.
181pub async fn write_clipboard_async(global_this: &JsObject, source: Option<&JsObject>) -> PromiseOutcome<String> {
182    let result = await_promise(&write_clipboard_raw(global_this, source)).await?;
183    Ok(result.as_string().unwrap_or_default())
184}
185
186/// Reads the value of a readable data item in a format.
187pub async fn try_get_readable_data_item_value_async(item: &JsObject, format: &str) -> PromiseOutcome<Option<JsObject>> {
188    let value = await_promise(&try_get_readable_data_item_value_async_raw(item, format)).await?;
189    Ok(non_null(value))
190}
191
192/// Reads the value of an item of a drag operation in a format.
193pub fn try_get_readable_data_item_value(item: &JsObject, format: &str) -> Option<JsObject> {
194    non_null(try_get_readable_data_item_value_raw(item, format))
195}
196
197/// The length of an array of the page.
198pub fn get_array_length(array: &JsObject) -> u32 {
199    array.unchecked_ref::<JsArray>().length()
200}
201
202/// An item of an array of the page.
203pub fn get_array_item(array: &JsObject, index: u32) -> JsObject {
204    array.unchecked_ref::<JsArray>().get(index)
205}
206
207/// What a readable data value holds.
208pub enum ReadableDataValueContent {
209    String(String),
210    Bytes(Vec<u8>),
211    File(JsObject),
212}
213
214/// The content of a readable data value; `None` for a type it cannot have.
215pub fn get_readable_data_value(value: &JsObject) -> Option<ReadableDataValueContent> {
216    let value = value.unchecked_ref::<ReadableDataValue>();
217    match value.value_type().as_deref() {
218        Some("string") => value.string_value().map(ReadableDataValueContent::String),
219        Some("bytes") => Some(ReadableDataValueContent::Bytes(value.bytes_value())),
220        Some("file") => non_null(value.object_value()).map(ReadableDataValueContent::File),
221        _ => None,
222    }
223}
224
225/// The effects the source of a drag operation allows (`effectAllowed`).
226pub fn get_effect_allowed(data_transfer: &JsObject) -> Option<String> {
227    data_transfer.unchecked_ref::<DataTransfer>().effect_allowed_raw()
228}
229
230/// Sets the effect of a drag operation the page shows (`dropEffect`).
231pub fn set_drop_effect(data_transfer: &JsObject, value: &str) {
232    data_transfer.unchecked_ref::<DataTransfer>().set_drop_effect_raw(value);
233}
234
235/// The start of the selection of an input element; 0 when it has none.
236pub fn get_selection_start(input_element: &JsObject) -> i32 {
237    input_element.unchecked_ref::<HtmlInputElement>().selection_start_raw().unwrap_or(0)
238}
239
240/// The end of the selection of an input element; 0 when it has none.
241pub fn get_selection_end(input_element: &JsObject) -> i32 {
242    input_element.unchecked_ref::<HtmlInputElement>().selection_end_raw().unwrap_or(0)
243}
244
245/// Captures a pointer to the element.
246pub fn set_pointer_capture(container_element: &JsObject, pointer_id: i64) {
247    set_pointer_capture_raw(container_element, pointer_id as f64);
248}
249
250/// Releases the capture of a pointer, if the element has it.
251pub fn release_pointer_capture(container_element: &JsObject, pointer_id: i64) {
252    release_pointer_capture_raw(container_element, pointer_id as f64);
253}
254
255// The original answers the page with a task (`RedirectInputAsync`, `RedirectInputRetunAsync`)
256// that the page awaits. Here every callback runs synchronously on the UI thread and returns its
257// result directly, so that the page can decide about the default action of the event while it
258// is still being dispatched.
259
260/// A key went down. Returns whether the application handled it.
261#[wasm_bindgen(js_name = InputHelper_OnKeyDown)]
262pub fn on_key_down(top_level_id: i32, code: Option<String>, key: Option<String>, modifier: i32) -> bool {
263    match BrowserTopLevelImpl::try_get_top_level(top_level_id) {
264        Some(top_level) => top_level.input_handler().on_key_down(code.as_deref(), key.as_deref(), modifier),
265        None => false,
266    }
267}
268
269/// A key went up. Returns whether the application handled it.
270#[wasm_bindgen(js_name = InputHelper_OnKeyUp)]
271pub fn on_key_up(top_level_id: i32, code: Option<String>, key: Option<String>, modifier: i32) -> bool {
272    match BrowserTopLevelImpl::try_get_top_level(top_level_id) {
273        Some(top_level) => top_level.input_handler().on_key_up(code.as_deref(), key.as_deref(), modifier),
274        None => false,
275    }
276}
277
278#[wasm_bindgen(js_name = InputHelper_OnBeforeInput)]
279pub fn on_before_input(top_level_id: i32, input_type: &str, start: i32, end: i32) {
280    if let Some(top_level) = BrowserTopLevelImpl::try_get_top_level(top_level_id) {
281        top_level.input_handler().text_input_method().on_before_input(input_type, start, end);
282    }
283}
284
285#[wasm_bindgen(js_name = InputHelper_OnCompositionStart)]
286pub fn on_composition_start(top_level_id: i32) {
287    if let Some(top_level) = BrowserTopLevelImpl::try_get_top_level(top_level_id) {
288        top_level.input_handler().text_input_method().on_composition_start();
289    }
290}
291
292#[wasm_bindgen(js_name = InputHelper_OnCompositionUpdate)]
293pub fn on_composition_update(top_level_id: i32, data: Option<String>) {
294    if let Some(top_level) = BrowserTopLevelImpl::try_get_top_level(top_level_id) {
295        top_level.input_handler().text_input_method().on_composition_update(data.as_deref());
296    }
297}
298
299#[wasm_bindgen(js_name = InputHelper_OnCompositionEnd)]
300pub fn on_composition_end(top_level_id: i32, data: Option<String>) {
301    if let Some(top_level) = BrowserTopLevelImpl::try_get_top_level(top_level_id) {
302        top_level.input_handler().text_input_method().on_composition_end(data.as_deref());
303    }
304}
305
306/// A pointer moved. `args_obj` is the event of the page, kept for the
307/// coalesced points of the move. `event_age` (each pointer event) is the
308/// milliseconds since the page raised the event.
309#[wasm_bindgen(js_name = InputHelper_OnPointerMove)]
310#[allow(clippy::too_many_arguments)]
311pub fn on_pointer_move(
312    top_level_id: i32,
313    pointer_type: &str,
314    pointer_id: f64,
315    offset_x: f64,
316    offset_y: f64,
317    pressure: f64,
318    tilt_x: f64,
319    tilt_y: f64,
320    twist: f64,
321    modifier: i32,
322    event_age: f64,
323    args_obj: JsObject,
324) {
325    if let Some(top_level) = BrowserTopLevelImpl::try_get_top_level(top_level_id) {
326        top_level.input_handler().on_pointer_move(
327            pointer_type,
328            pointer_id as i64,
329            offset_x,
330            offset_y,
331            pressure,
332            tilt_x,
333            tilt_y,
334            twist,
335            modifier,
336            event_age,
337            args_obj,
338        );
339    }
340}
341
342/// A pointer went down. `buttons` is the button of the event that changed
343/// its state.
344#[wasm_bindgen(js_name = InputHelper_OnPointerDown)]
345#[allow(clippy::too_many_arguments)]
346pub fn on_pointer_down(
347    top_level_id: i32,
348    pointer_type: &str,
349    pointer_id: f64,
350    buttons: i32,
351    offset_x: f64,
352    offset_y: f64,
353    pressure: f64,
354    tilt_x: f64,
355    tilt_y: f64,
356    twist: f64,
357    modifier: i32,
358    event_age: f64,
359) {
360    if let Some(top_level) = BrowserTopLevelImpl::try_get_top_level(top_level_id) {
361        top_level.input_handler().on_pointer_down(
362            pointer_type,
363            pointer_id as i64,
364            buttons,
365            offset_x,
366            offset_y,
367            pressure,
368            tilt_x,
369            tilt_y,
370            twist,
371            modifier,
372            event_age,
373        );
374    }
375}
376
377/// A pointer went up. `buttons` is the button of the event that changed its
378/// state.
379#[wasm_bindgen(js_name = InputHelper_OnPointerUp)]
380#[allow(clippy::too_many_arguments)]
381pub fn on_pointer_up(
382    top_level_id: i32,
383    pointer_type: &str,
384    pointer_id: f64,
385    buttons: i32,
386    offset_x: f64,
387    offset_y: f64,
388    pressure: f64,
389    tilt_x: f64,
390    tilt_y: f64,
391    twist: f64,
392    modifier: i32,
393    event_age: f64,
394) {
395    if let Some(top_level) = BrowserTopLevelImpl::try_get_top_level(top_level_id) {
396        top_level.input_handler().on_pointer_up(
397            pointer_type,
398            pointer_id as i64,
399            buttons,
400            offset_x,
401            offset_y,
402            pressure,
403            tilt_x,
404            tilt_y,
405            twist,
406            modifier,
407            event_age,
408        );
409    }
410}
411
412#[wasm_bindgen(js_name = InputHelper_OnPointerCancel)]
413#[allow(clippy::too_many_arguments)]
414pub fn on_pointer_cancel(
415    top_level_id: i32,
416    pointer_type: &str,
417    pointer_id: f64,
418    offset_x: f64,
419    offset_y: f64,
420    pressure: f64,
421    tilt_x: f64,
422    tilt_y: f64,
423    twist: f64,
424    modifier: i32,
425    event_age: f64,
426) {
427    if let Some(top_level) = BrowserTopLevelImpl::try_get_top_level(top_level_id) {
428        top_level.input_handler().on_pointer_cancel(
429            pointer_type,
430            pointer_id as i64,
431            offset_x,
432            offset_y,
433            pressure,
434            tilt_x,
435            tilt_y,
436            twist,
437            modifier,
438            event_age,
439        );
440    }
441}
442
443/// The wheel turned. `delta_mode` is the unit of the deltas, as the event
444/// of the page reports it.
445// Differs from the original, which does not pass the unit and takes every delta for pixels.
446#[wasm_bindgen(js_name = InputHelper_OnWheel)]
447pub fn on_wheel(
448    top_level_id: i32,
449    offset_x: f64,
450    offset_y: f64,
451    delta_x: f64,
452    delta_y: f64,
453    delta_mode: i32,
454    modifier: i32,
455) {
456    if let Some(top_level) = BrowserTopLevelImpl::try_get_top_level(top_level_id) {
457        top_level.input_handler().on_wheel(offset_x, offset_y, delta_x, delta_y, delta_mode, modifier);
458    }
459}
460
461/// A drag operation of the page entered, moved over, left or dropped on the
462/// element of a top-level. `data_transfer` is the data transfer of the
463/// event and `items` its items as readable data items.
464#[wasm_bindgen(js_name = InputHelper_OnDragDrop)]
465pub fn on_drag_drop(
466    top_level_id: i32,
467    type_: &str,
468    offset_x: f64,
469    offset_y: f64,
470    modifiers: i32,
471    data_transfer: JsObject,
472    items: JsObject,
473) {
474    if let Some(top_level) = BrowserTopLevelImpl::try_get_top_level(top_level_id) {
475        top_level.input_handler().on_drag_event(type_, offset_x, offset_y, modifiers, &data_transfer, items);
476    }
477}
478
479/// The on-screen keyboard changed the part of the view it covers.
480#[wasm_bindgen(js_name = InputHelper_OnKeyboardGeometryChange)]
481pub fn on_keyboard_geometry_change(top_level_id: i32, x: f64, y: f64, width: f64, height: f64) {
482    if let Some(top_level) = BrowserTopLevelImpl::try_get_top_level(top_level_id) {
483        top_level.input_handler().input_pane().on_geometry_change(x, y, width, height);
484    }
485}
486
487/// The keyboard focus of the page left the element of a top-level.
488// Not in the original, where the lost-focus notification of the top-level is never raised.
489#[wasm_bindgen(js_name = InputHelper_OnLostFocus)]
490pub fn on_lost_focus(top_level_id: i32) {
491    if let Some(top_level) = BrowserTopLevelImpl::try_get_top_level(top_level_id) {
492        top_level.on_lost_focus();
493    }
494}