use boa_engine::object::ObjectInitializer;
use boa_engine::property::Attribute;
use boa_engine::{Context, JsArgs, JsError, JsResult, JsValue};
use super::bind::{NOT_SUPPORTED, define_accessor, host, qualified, string_of};
const BAD_OBJECT: &str = "Object no longer exists.";
const INVALID_SET: &str = "Set not possible, invalid or unknown.";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum EventKind {
#[default]
Unknown,
MouseEnter,
MouseExit,
MouseDown,
MouseUp,
Focus,
Blur,
Keystroke,
Validate,
Calculate,
Format,
}
impl EventKind {
pub(crate) fn name(self) -> &'static str {
match self {
EventKind::Unknown => "",
EventKind::MouseEnter => "Mouse Enter",
EventKind::MouseExit => "Mouse Exit",
EventKind::MouseDown => "Mouse Down",
EventKind::MouseUp => "Mouse Up",
EventKind::Focus => "Focus",
EventKind::Blur => "Blur",
EventKind::Keystroke => "Keystroke",
EventKind::Validate => "Validate",
EventKind::Calculate => "Calculate",
EventKind::Format => "Format",
}
}
pub(crate) fn kind_type(self) -> &'static str {
match self {
EventKind::Unknown => "",
_ => "Field",
}
}
pub(crate) fn is_user_gesture(self) -> bool {
matches!(
self,
EventKind::MouseDown | EventKind::MouseUp | EventKind::Keystroke
)
}
}
#[allow(
clippy::struct_excessive_bools,
reason = "each flag is one JavaScript property a script reads — \
`event.keyDown`, `event.modifier`, `event.shift`, \
`event.willCommit`, `event.fieldFull`, `event.rc`, and the \
liveness of `event.value`. They are the `event` object's own \
surface, not a state machine, and folding any pair into an \
enum would put a name between the accessor and the value it \
answers."
)]
#[derive(Debug, Clone, Default)]
pub(crate) struct EventState {
pub(crate) kind: EventKind,
pub(crate) target_name: String,
pub(crate) source_name: String,
pub(crate) value: String,
pub(crate) has_value: bool,
pub(crate) change: String,
pub(crate) change_ex: String,
pub(crate) commit_key: i32,
pub(crate) key_down: bool,
pub(crate) modifier: bool,
pub(crate) shift: bool,
pub(crate) sel_start: i32,
pub(crate) sel_end: i32,
pub(crate) will_commit: bool,
pub(crate) field_full: bool,
pub(crate) rc: bool,
pub(crate) target_index: Option<u32>,
pub(crate) source_index: Option<u32>,
}
impl EventState {
pub(crate) fn initialize(kind: EventKind) -> EventState {
EventState {
kind,
target_name: String::new(),
source_name: String::new(),
value: String::new(),
has_value: false,
change: String::new(),
change_ex: String::new(),
commit_key: -1,
key_down: false,
modifier: false,
shift: false,
sel_start: 0,
sel_end: 0,
will_commit: false,
field_full: false,
rc: matches!(
kind,
EventKind::Keystroke | EventKind::Validate | EventKind::Calculate
),
target_index: None,
source_index: None,
}
}
}
fn read<T>(context: &Context, f: impl FnOnce(&EventState) -> T) -> Option<T> {
let host = host(context)?;
let state = host.borrow();
Some(f(&state.event))
}
fn write(context: &Context, f: impl FnOnce(&mut EventState)) {
if let Some(host) = host(context) {
f(&mut host.borrow_mut().event);
}
}
fn read_only(member: &str) -> JsError {
qualified(&format!("event.{member}"), NOT_SUPPORTED)
}
macro_rules! declined {
($fn_name:ident, $member:literal) => {
fn $fn_name(_t: &JsValue, _a: &[JsValue], _c: &mut Context) -> JsResult<JsValue> {
Err(read_only($member))
}
};
}
declined!(no_change_ex, "changeEx");
declined!(no_commit_key, "commitKey");
declined!(no_field_full, "fieldFull");
declined!(no_key_down, "keyDown");
declined!(no_modifier, "modifier");
declined!(no_name, "name");
declined!(no_shift, "shift");
declined!(no_source, "source");
declined!(no_target, "target");
declined!(no_target_name, "targetName");
declined!(no_type, "type");
declined!(no_will_commit, "willCommit");
macro_rules! plain {
($fn_name:ident, $slot:ident, $wrap:expr) => {
fn $fn_name(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let value = read(context, |event| event.$slot.clone()).unwrap_or_default();
let wrap = $wrap;
Ok(wrap(value))
}
};
}
plain!(get_change_ex, change_ex, |v: String| JsValue::from(
boa_engine::js_string!(v)
));
plain!(get_commit_key, commit_key, JsValue::from);
plain!(get_key_down, key_down, JsValue::from);
plain!(get_modifier, modifier, JsValue::from);
plain!(get_shift, shift, JsValue::from);
plain!(get_will_commit, will_commit, JsValue::from);
plain!(get_target_name, target_name, |v: String| JsValue::from(
boa_engine::js_string!(v)
));
#[allow(clippy::unnecessary_wraps)]
fn get_name(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let name = read(context, |event| event.kind.name()).unwrap_or("");
Ok(JsValue::from(boa_engine::js_string!(name)))
}
#[allow(clippy::unnecessary_wraps)]
fn get_type(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let kind = read(context, |event| event.kind.kind_type()).unwrap_or("");
Ok(JsValue::from(boa_engine::js_string!(kind)))
}
#[allow(clippy::unnecessary_wraps)]
fn get_change(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let change = read(context, |event| event.change.clone()).unwrap_or_default();
Ok(JsValue::from(boa_engine::js_string!(change)))
}
fn set_change(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let value = args.get_or_undefined(0).clone();
if value.is_string() {
let text = string_of(&value, context)?;
write(context, |event| event.change = text);
}
Ok(JsValue::undefined())
}
#[allow(clippy::unnecessary_wraps)]
fn get_rc(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::from(
read(context, |event| event.rc).unwrap_or(false),
))
}
#[allow(clippy::unnecessary_wraps)]
fn set_rc(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let value = args.get_or_undefined(0).to_boolean();
write(context, |event| event.rc = value);
Ok(JsValue::undefined())
}
#[allow(clippy::unnecessary_wraps)]
fn rich_noop(_t: &JsValue, _a: &[JsValue], _c: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::undefined())
}
fn get_field_full(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some((kind, full)) = read(context, |event| (event.kind, event.field_full)) else {
return Err(qualified("event.fieldFull", "unrecognized event"));
};
if kind != EventKind::Keystroke {
return Err(qualified("event.fieldFull", "unrecognized event"));
}
Ok(JsValue::from(full))
}
macro_rules! selection {
($get:ident, $set:ident, $slot:ident) => {
#[allow(clippy::unnecessary_wraps)]
fn $get(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some((kind, value)) = read(context, |event| (event.kind, event.$slot)) else {
return Ok(JsValue::undefined());
};
if kind != EventKind::Keystroke {
return Ok(JsValue::undefined());
}
Ok(JsValue::from(value))
}
fn $set(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let kind = read(context, |event| event.kind).unwrap_or_default();
if kind == EventKind::Keystroke {
let value = args.get_or_undefined(0).to_i32(context)?;
write(context, |event| event.$slot = value);
}
Ok(JsValue::undefined())
}
};
}
selection!(get_sel_start, set_sel_start, sel_start);
selection!(get_sel_end, set_sel_end, sel_end);
fn get_value(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some((kind, has_value, value)) = read(context, |event| {
(event.kind, event.has_value, event.value.clone())
}) else {
return Err(qualified("event.value", BAD_OBJECT));
};
if kind.kind_type() != "Field" {
return Err(qualified("event.value", "Bad event type."));
}
if !has_value {
return Err(qualified("event.value", BAD_OBJECT));
}
Ok(JsValue::from(boa_engine::js_string!(value)))
}
fn set_value(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some((kind, has_value)) = read(context, |event| (event.kind, event.has_value)) else {
return Err(qualified("event.value", BAD_OBJECT));
};
if kind.kind_type() != "Field" {
return Err(qualified("event.value", "Bad event type."));
}
if !has_value {
return Err(qualified("event.value", BAD_OBJECT));
}
let incoming = args.get_or_undefined(0).clone();
if incoming.is_null_or_undefined() || incoming.is_boolean() {
return Err(qualified("event.value", INVALID_SET));
}
let text = string_of(&incoming, context)?;
write(context, |event| event.value = text);
Ok(JsValue::undefined())
}
macro_rules! field_of {
($fn_name:ident, $name_slot:ident, $index_slot:ident, $member:literal) => {
fn $fn_name(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some((name, index)) = read(context, |event| {
(event.$name_slot.clone(), event.$index_slot)
}) else {
return Err(qualified(concat!("event.", $member), BAD_OBJECT));
};
let index = index.and_then(|i| usize::try_from(i).ok()).unwrap_or(0);
super::field::build(index, &name, context).map(JsValue::from)
}
};
}
field_of!(get_target, target_name, target_index, "target");
field_of!(get_source, source_name, source_index, "source");
pub(crate) fn install(context: &mut Context) -> JsResult<()> {
let event = ObjectInitializer::new(context).build();
let properties: [(&str, super::af::Bound, super::af::Bound); 20] = [
("change", get_change, set_change),
("changeEx", get_change_ex, no_change_ex),
("commitKey", get_commit_key, no_commit_key),
("fieldFull", get_field_full, no_field_full),
("keyDown", get_key_down, no_key_down),
("modifier", get_modifier, no_modifier),
("name", get_name, no_name),
("rc", get_rc, set_rc),
("richChange", rich_noop, rich_noop),
("richChangeEx", rich_noop, rich_noop),
("richValue", rich_noop, rich_noop),
("selEnd", get_sel_end, set_sel_end),
("selStart", get_sel_start, set_sel_start),
("shift", get_shift, no_shift),
("source", get_source, no_source),
("target", get_target, no_target),
("targetName", get_target_name, no_target_name),
("type", get_type, no_type),
("value", get_value, set_value),
("willCommit", get_will_commit, no_will_commit),
];
for (name, get, set) in properties {
define_accessor(&event, context, name, get, set)?;
}
context.register_global_property(
boa_engine::js_string!("event"),
JsValue::from(event),
Attribute::all(),
)?;
Ok(())
}