servo/components/webdriver_server/actions.rs
Martin Robinson a1cf0cbf86
libservo: Stop using script_traits in the embedding layer (#35185)
Many types used directly in the `libservo` API are in the
`script_traits` crate, which was created to break circular dependencies.
Move all API exposed types to `embedder_traits` which now contains types
exposed via the `libservo` embedding API. Also expose these at the root
of the `libservo` `servo` crate so that the API won't break when they
move around in the future.

The idea with `embedder_traits` in the future is that it contains types
that are available throughout servo because they are used in the
embedding API and thus should have minimal dependencies on other Servo
crates (a bit like `base`).

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
2025-01-28 11:15:36 +00:00

512 lines
18 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::collections::HashSet;
use std::time::{Duration, Instant};
use std::{cmp, thread};
use compositing_traits::ConstellationMsg;
use embedder_traits::{MouseButton, MouseEventType};
use ipc_channel::ipc;
use keyboard_types::webdriver::KeyInputState;
use script_traits::webdriver_msg::WebDriverScriptCommand;
use script_traits::WebDriverCommandMsg;
use webdriver::actions::{
ActionSequence, ActionsType, GeneralAction, KeyAction, KeyActionItem, KeyDownAction,
KeyUpAction, NullActionItem, PointerAction, PointerActionItem, PointerActionParameters,
PointerDownAction, PointerMoveAction, PointerOrigin, PointerType, PointerUpAction,
};
use webdriver::error::ErrorStatus;
use crate::Handler;
// Interval between pointerMove increments in ms, based on common vsync
static POINTERMOVE_INTERVAL: u64 = 17;
// https://w3c.github.io/webdriver/#dfn-input-source-state
pub(crate) enum InputSourceState {
Null,
Key(KeyInputState),
Pointer(PointerInputState),
}
// https://w3c.github.io/webdriver/#dfn-pointer-input-source
pub(crate) struct PointerInputState {
subtype: PointerType,
pressed: HashSet<u64>,
x: i64,
y: i64,
}
impl PointerInputState {
pub fn new(subtype: &PointerType) -> PointerInputState {
PointerInputState {
subtype: match subtype {
PointerType::Mouse => PointerType::Mouse,
PointerType::Pen => PointerType::Pen,
PointerType::Touch => PointerType::Touch,
},
pressed: HashSet::new(),
x: 0,
y: 0,
}
}
}
// https://w3c.github.io/webdriver/#dfn-computing-the-tick-duration
fn compute_tick_duration(tick_actions: &ActionSequence) -> u64 {
let mut duration = 0;
match &tick_actions.actions {
ActionsType::Null { actions } => {
for action in actions.iter() {
let NullActionItem::General(GeneralAction::Pause(pause_action)) = action;
duration = cmp::max(duration, pause_action.duration.unwrap_or(0));
}
},
ActionsType::Pointer {
parameters: _,
actions,
} => {
for action in actions.iter() {
let action_duration = match action {
PointerActionItem::General(GeneralAction::Pause(action)) => action.duration,
PointerActionItem::Pointer(PointerAction::Move(action)) => action.duration,
_ => None,
};
duration = cmp::max(duration, action_duration.unwrap_or(0));
}
},
ActionsType::Key { actions: _ } => (),
ActionsType::Wheel { .. } => todo!("Not implemented."),
}
duration
}
fn u64_to_mouse_button(button: u64) -> Option<MouseButton> {
if embedder_traits::MouseButton::Left as u64 == button {
Some(MouseButton::Left)
} else if MouseButton::Middle as u64 == button {
Some(MouseButton::Middle)
} else if MouseButton::Right as u64 == button {
Some(MouseButton::Right)
} else {
None
}
}
impl Handler {
// https://w3c.github.io/webdriver/#dfn-dispatch-actions
pub(crate) fn dispatch_actions(
&mut self,
actions_by_tick: &[ActionSequence],
) -> Result<(), ErrorStatus> {
for tick_actions in actions_by_tick.iter() {
let tick_duration = compute_tick_duration(tick_actions);
self.dispatch_tick_actions(tick_actions, tick_duration)?;
}
Ok(())
}
fn dispatch_general_action(&mut self, source_id: &str) {
self.session_mut()
.unwrap()
.input_state_table
.entry(source_id.to_string())
.or_insert(InputSourceState::Null);
// https://w3c.github.io/webdriver/#dfn-dispatch-a-pause-action
// Nothing to be done
}
// https://w3c.github.io/webdriver/#dfn-dispatch-tick-actions
fn dispatch_tick_actions(
&mut self,
tick_actions: &ActionSequence,
tick_duration: u64,
) -> Result<(), ErrorStatus> {
let source_id = &tick_actions.id;
match &tick_actions.actions {
ActionsType::Null { actions } => {
for _action in actions.iter() {
self.dispatch_general_action(source_id);
}
},
ActionsType::Key { actions } => {
for action in actions.iter() {
match action {
KeyActionItem::General(_action) => {
self.dispatch_general_action(source_id);
},
KeyActionItem::Key(action) => {
self.session_mut()
.unwrap()
.input_state_table
.entry(source_id.to_string())
.or_insert(InputSourceState::Key(KeyInputState::new()));
match action {
KeyAction::Down(action) => {
self.dispatch_keydown_action(source_id, action)
},
KeyAction::Up(action) => {
self.dispatch_keyup_action(source_id, action)
},
};
},
}
}
},
ActionsType::Pointer {
parameters,
actions,
} => {
for action in actions.iter() {
match action {
PointerActionItem::General(_action) => {
self.dispatch_general_action(source_id);
},
PointerActionItem::Pointer(action) => {
self.session_mut()
.unwrap()
.input_state_table
.entry(source_id.to_string())
.or_insert(InputSourceState::Pointer(PointerInputState::new(
&parameters.pointer_type,
)));
match action {
PointerAction::Cancel => (),
PointerAction::Down(action) => {
self.dispatch_pointerdown_action(source_id, action)
},
PointerAction::Move(action) => self.dispatch_pointermove_action(
source_id,
action,
tick_duration,
)?,
PointerAction::Up(action) => {
self.dispatch_pointerup_action(source_id, action)
},
}
},
}
}
},
ActionsType::Wheel { .. } => todo!("Not implemented."),
}
Ok(())
}
// https://w3c.github.io/webdriver/#dfn-dispatch-a-keydown-action
fn dispatch_keydown_action(&mut self, source_id: &str, action: &KeyDownAction) {
let session = self.session.as_mut().unwrap();
let raw_key = action.value.chars().next().unwrap();
let key_input_state = match session.input_state_table.get_mut(source_id).unwrap() {
InputSourceState::Null => unreachable!(),
InputSourceState::Key(key_input_state) => key_input_state,
InputSourceState::Pointer(_) => unreachable!(),
};
session.input_cancel_list.push(ActionSequence {
id: source_id.into(),
actions: ActionsType::Key {
actions: vec![KeyActionItem::Key(KeyAction::Up(KeyUpAction {
value: action.value.clone(),
}))],
},
});
let keyboard_event = key_input_state.dispatch_keydown(raw_key);
let cmd_msg =
WebDriverCommandMsg::KeyboardAction(session.browsing_context_id, keyboard_event);
self.constellation_chan
.send(ConstellationMsg::WebDriverCommand(cmd_msg))
.unwrap();
}
// https://w3c.github.io/webdriver/#dfn-dispatch-a-keyup-action
fn dispatch_keyup_action(&mut self, source_id: &str, action: &KeyUpAction) {
let session = self.session.as_mut().unwrap();
let raw_key = action.value.chars().next().unwrap();
let key_input_state = match session.input_state_table.get_mut(source_id).unwrap() {
InputSourceState::Null => unreachable!(),
InputSourceState::Key(key_input_state) => key_input_state,
InputSourceState::Pointer(_) => unreachable!(),
};
session.input_cancel_list.push(ActionSequence {
id: source_id.into(),
actions: ActionsType::Key {
actions: vec![KeyActionItem::Key(KeyAction::Up(KeyUpAction {
value: action.value.clone(),
}))],
},
});
if let Some(keyboard_event) = key_input_state.dispatch_keyup(raw_key) {
let cmd_msg =
WebDriverCommandMsg::KeyboardAction(session.browsing_context_id, keyboard_event);
self.constellation_chan
.send(ConstellationMsg::WebDriverCommand(cmd_msg))
.unwrap();
}
}
// https://w3c.github.io/webdriver/#dfn-dispatch-a-pointerdown-action
pub(crate) fn dispatch_pointerdown_action(
&mut self,
source_id: &str,
action: &PointerDownAction,
) {
let session = self.session.as_mut().unwrap();
let pointer_input_state = match session.input_state_table.get_mut(source_id).unwrap() {
InputSourceState::Null => unreachable!(),
InputSourceState::Key(_) => unreachable!(),
InputSourceState::Pointer(pointer_input_state) => pointer_input_state,
};
if pointer_input_state.pressed.contains(&action.button) {
return;
}
pointer_input_state.pressed.insert(action.button);
session.input_cancel_list.push(ActionSequence {
id: source_id.into(),
actions: ActionsType::Pointer {
parameters: PointerActionParameters {
pointer_type: match pointer_input_state.subtype {
PointerType::Mouse => PointerType::Mouse,
PointerType::Pen => PointerType::Pen,
PointerType::Touch => PointerType::Touch,
},
},
actions: vec![PointerActionItem::Pointer(PointerAction::Up(
PointerUpAction {
button: action.button,
..Default::default()
},
))],
},
});
if let Some(button) = u64_to_mouse_button(action.button) {
let cmd_msg = WebDriverCommandMsg::MouseButtonAction(
MouseEventType::MouseDown,
button,
pointer_input_state.x as f32,
pointer_input_state.y as f32,
);
self.constellation_chan
.send(ConstellationMsg::WebDriverCommand(cmd_msg))
.unwrap();
}
}
// https://w3c.github.io/webdriver/#dfn-dispatch-a-pointerup-action
pub(crate) fn dispatch_pointerup_action(&mut self, source_id: &str, action: &PointerUpAction) {
let session = self.session.as_mut().unwrap();
let pointer_input_state = match session.input_state_table.get_mut(source_id).unwrap() {
InputSourceState::Null => unreachable!(),
InputSourceState::Key(_) => unreachable!(),
InputSourceState::Pointer(pointer_input_state) => pointer_input_state,
};
if !pointer_input_state.pressed.contains(&action.button) {
return;
}
pointer_input_state.pressed.remove(&action.button);
session.input_cancel_list.push(ActionSequence {
id: source_id.into(),
actions: ActionsType::Pointer {
parameters: PointerActionParameters {
pointer_type: match pointer_input_state.subtype {
PointerType::Mouse => PointerType::Mouse,
PointerType::Pen => PointerType::Pen,
PointerType::Touch => PointerType::Touch,
},
},
actions: vec![PointerActionItem::Pointer(PointerAction::Down(
PointerDownAction {
button: action.button,
..Default::default()
},
))],
},
});
if let Some(button) = u64_to_mouse_button(action.button) {
let cmd_msg = WebDriverCommandMsg::MouseButtonAction(
MouseEventType::MouseUp,
button,
pointer_input_state.x as f32,
pointer_input_state.y as f32,
);
self.constellation_chan
.send(ConstellationMsg::WebDriverCommand(cmd_msg))
.unwrap();
}
}
// https://w3c.github.io/webdriver/#dfn-dispatch-a-pointermove-action
pub(crate) fn dispatch_pointermove_action(
&mut self,
source_id: &str,
action: &PointerMoveAction,
tick_duration: u64,
) -> Result<(), ErrorStatus> {
let tick_start = Instant::now();
// Steps 1 - 2
let x_offset = action.x;
let y_offset = action.y;
// Steps 3 - 4
let (start_x, start_y) = match self
.session
.as_ref()
.unwrap()
.input_state_table
.get(source_id)
.unwrap()
{
InputSourceState::Null => unreachable!(),
InputSourceState::Key(_) => unreachable!(),
InputSourceState::Pointer(pointer_input_state) => {
(pointer_input_state.x, pointer_input_state.y)
},
};
// Step 5 - 6
let (x, y) = match action.origin {
PointerOrigin::Viewport => (x_offset, y_offset),
PointerOrigin::Pointer => (start_x + x_offset, start_y + y_offset),
PointerOrigin::Element(ref x) => {
let (sender, receiver) = ipc::channel().unwrap();
self.top_level_script_command(WebDriverScriptCommand::GetElementInViewCenterPoint(
x.to_string(),
sender,
))
.unwrap();
match receiver.recv().unwrap() {
Ok(point) => match point {
Some(point) => point,
None => return Err(ErrorStatus::UnknownError),
},
Err(_) => return Err(ErrorStatus::UnknownError),
}
},
};
let (sender, receiver) = ipc::channel().unwrap();
let cmd_msg = WebDriverCommandMsg::GetWindowSize(
self.session.as_ref().unwrap().top_level_browsing_context_id,
sender,
);
self.constellation_chan
.send(ConstellationMsg::WebDriverCommand(cmd_msg))
.unwrap();
// Steps 7 - 8
let viewport = receiver.recv().unwrap().initial_viewport;
if x < 0 || x as f32 > viewport.width || y < 0 || y as f32 > viewport.height {
return Err(ErrorStatus::MoveTargetOutOfBounds);
}
// Step 9
let duration = match action.duration {
Some(duration) => duration,
None => tick_duration,
};
// Step 10
if duration > 0 {
thread::sleep(Duration::from_millis(POINTERMOVE_INTERVAL));
}
// Step 11
self.perform_pointer_move(source_id, duration, start_x, start_y, x, y, tick_start);
// Step 12
Ok(())
}
/// <https://w3c.github.io/webdriver/#dfn-perform-a-pointer-move>
#[allow(clippy::too_many_arguments)]
fn perform_pointer_move(
&mut self,
source_id: &str,
duration: u64,
start_x: i64,
start_y: i64,
target_x: i64,
target_y: i64,
tick_start: Instant,
) {
let pointer_input_state = match self
.session
.as_mut()
.unwrap()
.input_state_table
.get_mut(source_id)
.unwrap()
{
InputSourceState::Null => unreachable!(),
InputSourceState::Key(_) => unreachable!(),
InputSourceState::Pointer(pointer_input_state) => pointer_input_state,
};
loop {
// Step 1
let time_delta = tick_start.elapsed().as_millis();
// Step 2
let duration_ratio = if duration > 0 {
time_delta as f64 / duration as f64
} else {
1.0
};
// Step 3
let last = 1.0 - duration_ratio < 0.001;
// Step 4
let (x, y) = if last {
(target_x, target_y)
} else {
(
(duration_ratio * (target_x - start_x) as f64) as i64 + start_x,
(duration_ratio * (target_y - start_y) as f64) as i64 + start_y,
)
};
// Steps 5 - 6
let current_x = pointer_input_state.x;
let current_y = pointer_input_state.y;
// Step 7
if x != current_x || y != current_y {
// Step 7.2
let cmd_msg = WebDriverCommandMsg::MouseMoveAction(x as f32, y as f32);
self.constellation_chan
.send(ConstellationMsg::WebDriverCommand(cmd_msg))
.unwrap();
// Step 7.3
pointer_input_state.x = x;
pointer_input_state.y = y;
}
// Step 8
if last {
return;
}
// Step 9
thread::sleep(Duration::from_millis(POINTERMOVE_INTERVAL));
}
}
}