use waterui_core::layout::{Point as LayoutPoint, Rect as LayoutRect, Size as LayoutSize};
use waterui_graphics::input::{
Code, Key, Modifiers, ScrollUnit, SurfaceInputEvent, SurfacePointerButton,
};
use super::gpu_surface::{WuiGpuSurfaceState, with_semantic_input};
use crate::components::layouting::layout::WuiRect;
use crate::{IntoFFI, IntoRust, WuiStr};
pub const WUI_SURFACE_MODIFIER_SHIFT: u32 = 0x200;
pub const WUI_SURFACE_MODIFIER_CONTROL: u32 = 0x8;
pub const WUI_SURFACE_MODIFIER_ALT: u32 = 0x1;
pub const WUI_SURFACE_MODIFIER_META: u32 = 0x40;
pub const WUI_SURFACE_MODIFIER_CAPS_LOCK: u32 = 0x4;
pub const WUI_SURFACE_MODIFIER_NUM_LOCK: u32 = 0x80;
const SUPPORTED_MODIFIERS: u32 = WUI_SURFACE_MODIFIER_SHIFT
| WUI_SURFACE_MODIFIER_CONTROL
| WUI_SURFACE_MODIFIER_ALT
| WUI_SURFACE_MODIFIER_META
| WUI_SURFACE_MODIFIER_CAPS_LOCK
| WUI_SURFACE_MODIFIER_NUM_LOCK;
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WuiSurfaceInputEventKind {
Focus,
Modifiers,
PointerMove,
PointerButton,
Scroll,
Key,
TextInput,
CompositionStart,
CompositionUpdate,
CompositionCommit,
CompositionCancel,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WuiScrollUnit {
Line,
Pixel,
}
impl WuiScrollUnit {
const fn into_rust(self) -> ScrollUnit {
match self {
Self::Line => ScrollUnit::Line,
Self::Pixel => ScrollUnit::Pixel,
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WuiSurfacePointerButton {
Primary,
Secondary,
Middle,
Back,
Forward,
}
impl WuiSurfacePointerButton {
const fn into_rust(self) -> SurfacePointerButton {
match self {
Self::Primary => SurfacePointerButton::Primary,
Self::Secondary => SurfacePointerButton::Secondary,
Self::Middle => SurfacePointerButton::Middle,
Self::Back => SurfacePointerButton::Back,
Self::Forward => SurfacePointerButton::Forward,
}
}
}
#[repr(C)]
#[derive(Debug)]
pub struct WuiSurfaceInputEvent {
pub kind: WuiSurfaceInputEventKind,
pub focused: bool,
pub modifiers: u32,
pub x: f64,
pub y: f64,
pub pressed: bool,
pub button: WuiSurfacePointerButton,
pub delta_x: f64,
pub delta_y: f64,
pub scroll_unit: WuiScrollUnit,
pub finished: bool,
pub key: WuiStr,
pub code: WuiStr,
pub text: WuiStr,
pub repeat: bool,
pub caret: i64,
}
fn modifiers_from_ffi(bits: u32) -> Modifiers {
assert_eq!(
bits & !SUPPORTED_MODIFIERS,
0,
"waterui_gpu_surface_send_input_event: unsupported modifier bits {:#x}",
bits & !SUPPORTED_MODIFIERS
);
Modifiers::from_bits(bits).expect("supported modifier bits are a subset of `Modifiers`")
}
fn caret_from_ffi(caret: i64, text: &str) -> Option<usize> {
if caret < 0 {
assert_eq!(
caret, -1,
"waterui_gpu_surface_send_input_event: composition caret must be a byte offset or -1, got {caret}"
);
return None;
}
let caret = usize::try_from(caret)
.expect("a non-negative i64 caret fits usize on every platform WaterUI targets");
assert!(
text.is_char_boundary(caret),
"waterui_gpu_surface_send_input_event: composition caret {caret} is not a UTF-8 boundary of {text:?}"
);
Some(caret)
}
impl IntoRust for WuiSurfaceInputEvent {
type Rust = SurfaceInputEvent;
unsafe fn into_rust(self) -> Self::Rust {
let Self {
kind,
focused,
modifiers,
x,
y,
pressed,
button,
delta_x,
delta_y,
scroll_unit,
finished,
key,
code,
text,
repeat,
caret,
} = self;
let (key, code, text) = unsafe { (key.into_rust(), code.into_rust(), text.into_rust()) };
let position = kurbo::Point::new(x, y);
match kind {
WuiSurfaceInputEventKind::Focus => SurfaceInputEvent::Focus(focused),
WuiSurfaceInputEventKind::Modifiers => {
SurfaceInputEvent::Modifiers(modifiers_from_ffi(modifiers))
}
WuiSurfaceInputEventKind::PointerMove => SurfaceInputEvent::PointerMove { position },
WuiSurfaceInputEventKind::PointerButton => SurfaceInputEvent::PointerButton {
pressed,
button: button.into_rust(),
position,
},
WuiSurfaceInputEventKind::Scroll => SurfaceInputEvent::Scroll {
position,
delta_x,
delta_y,
unit: scroll_unit.into_rust(),
finished,
},
WuiSurfaceInputEventKind::Key => SurfaceInputEvent::Key {
pressed,
key: key.parse::<Key>().unwrap_or_else(|_| {
panic!(
"waterui_gpu_surface_send_input_event: {key:?} is not a W3C KeyboardEvent.key name"
)
}),
code: code.parse::<Code>().unwrap_or_else(|_| {
panic!(
"waterui_gpu_surface_send_input_event: {code:?} is not a W3C KeyboardEvent.code name"
)
}),
modifiers: modifiers_from_ffi(modifiers),
repeat,
},
WuiSurfaceInputEventKind::TextInput => SurfaceInputEvent::TextInput(text),
WuiSurfaceInputEventKind::CompositionStart => SurfaceInputEvent::CompositionStart,
WuiSurfaceInputEventKind::CompositionUpdate => SurfaceInputEvent::CompositionUpdate {
caret: caret_from_ffi(caret, &text),
text,
},
WuiSurfaceInputEventKind::CompositionCommit => {
SurfaceInputEvent::CompositionCommit(text)
}
WuiSurfaceInputEventKind::CompositionCancel => SurfaceInputEvent::CompositionCancel,
}
}
}
#[unsafe(no_mangle)]
pub const unsafe extern "C" fn waterui_gpu_surface_wants_input_events(
state: *const WuiGpuSurfaceState,
) -> bool {
let state = unsafe { crate::borrow_ffi(state) };
state.wants_input_events()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_gpu_surface_send_input_event(
state: *mut WuiGpuSurfaceState,
event: WuiSurfaceInputEvent,
) -> bool {
let state = unsafe { crate::borrow_ffi_mut(state) };
let event = unsafe { event.into_rust() };
if !state.wants_input_events() {
return false;
}
with_semantic_input(state, |gpu_surface| gpu_surface.input(&event)).is_some()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_gpu_surface_ime_caret(
state: *const WuiGpuSurfaceState,
out: *mut WuiRect,
) -> bool {
assert!(
!out.is_null(),
"waterui_gpu_surface_ime_caret: `out` must point to writable WuiRect storage"
);
let state = unsafe { crate::borrow_ffi(state) };
let Some(caret) = state.ime_caret() else {
return false;
};
assert!(
caret.x0.is_finite()
&& caret.y0.is_finite()
&& caret.x1.is_finite()
&& caret.y1.is_finite()
&& caret
.x0
.abs()
.max(caret.y0.abs())
.max(caret.width())
.max(caret.height())
<= f64::from(f32::MAX),
"waterui_gpu_surface_ime_caret: the view reported a caret the f32 layout ABI cannot carry: {caret:?}"
);
#[expect(
clippy::cast_possible_truncation,
reason = "narrowing to the f32 layout ABI, range-checked directly above"
)]
let rect = LayoutRect::new(
LayoutPoint::new(caret.x0 as f32, caret.y0 as f32),
LayoutSize::new(caret.width() as f32, caret.height() as f32),
);
unsafe { out.write(rect.into_ffi()) };
true
}
#[cfg(test)]
mod tests {
use super::{
WUI_SURFACE_MODIFIER_ALT, WUI_SURFACE_MODIFIER_CAPS_LOCK, WUI_SURFACE_MODIFIER_CONTROL,
WUI_SURFACE_MODIFIER_META, WUI_SURFACE_MODIFIER_NUM_LOCK, WUI_SURFACE_MODIFIER_SHIFT,
WuiScrollUnit, WuiSurfaceInputEvent, WuiSurfaceInputEventKind, WuiSurfacePointerButton,
};
use crate::{IntoFFI, IntoRust};
use waterui_core::Str;
use waterui_graphics::input::{
Code, Key, Modifiers, NamedKey, ScrollUnit, SurfaceInputEvent, SurfacePointerButton,
};
fn event(kind: WuiSurfaceInputEventKind) -> WuiSurfaceInputEvent {
WuiSurfaceInputEvent {
kind,
focused: false,
modifiers: 0,
x: 0.0,
y: 0.0,
pressed: false,
button: WuiSurfacePointerButton::Primary,
delta_x: 0.0,
delta_y: 0.0,
scroll_unit: WuiScrollUnit::Pixel,
finished: false,
key: Str::default().into_ffi(),
code: Str::default().into_ffi(),
text: Str::default().into_ffi(),
repeat: false,
caret: -1,
}
}
fn into_rust(event: WuiSurfaceInputEvent) -> SurfaceInputEvent {
unsafe { event.into_rust() }
}
#[test]
fn focus_round_trips() {
let mut ffi = event(WuiSurfaceInputEventKind::Focus);
ffi.focused = true;
assert_eq!(into_rust(ffi), SurfaceInputEvent::Focus(true));
let ffi = event(WuiSurfaceInputEventKind::Focus);
assert_eq!(into_rust(ffi), SurfaceInputEvent::Focus(false));
}
#[test]
fn every_modifier_bit_maps_to_its_w3c_flag() {
for (bits, expected) in [
(WUI_SURFACE_MODIFIER_SHIFT, Modifiers::SHIFT),
(WUI_SURFACE_MODIFIER_CONTROL, Modifiers::CONTROL),
(WUI_SURFACE_MODIFIER_ALT, Modifiers::ALT),
(WUI_SURFACE_MODIFIER_META, Modifiers::META),
(WUI_SURFACE_MODIFIER_CAPS_LOCK, Modifiers::CAPS_LOCK),
(WUI_SURFACE_MODIFIER_NUM_LOCK, Modifiers::NUM_LOCK),
] {
let mut ffi = event(WuiSurfaceInputEventKind::Modifiers);
ffi.modifiers = bits;
assert_eq!(into_rust(ffi), SurfaceInputEvent::Modifiers(expected));
}
}
#[test]
fn a_modifier_chord_round_trips_as_one_value() {
let mut ffi = event(WuiSurfaceInputEventKind::Modifiers);
ffi.modifiers = WUI_SURFACE_MODIFIER_SHIFT | WUI_SURFACE_MODIFIER_META;
assert_eq!(
into_rust(ffi),
SurfaceInputEvent::Modifiers(Modifiers::SHIFT | Modifiers::META)
);
}
#[test]
#[should_panic(expected = "unsupported modifier bits")]
fn a_modifier_bit_this_abi_does_not_carry_fails_fast() {
let mut ffi = event(WuiSurfaceInputEventKind::Modifiers);
ffi.modifiers = Modifiers::SCROLL_LOCK.bits();
let _ = into_rust(ffi);
}
#[test]
fn pointer_move_round_trips() {
let mut ffi = event(WuiSurfaceInputEventKind::PointerMove);
ffi.x = 12.5;
ffi.y = -3.25;
assert_eq!(
into_rust(ffi),
SurfaceInputEvent::PointerMove {
position: kurbo::Point::new(12.5, -3.25),
}
);
}
#[test]
fn every_pointer_button_round_trips() {
for (ffi_button, expected) in [
(
WuiSurfacePointerButton::Primary,
SurfacePointerButton::Primary,
),
(
WuiSurfacePointerButton::Secondary,
SurfacePointerButton::Secondary,
),
(
WuiSurfacePointerButton::Middle,
SurfacePointerButton::Middle,
),
(WuiSurfacePointerButton::Back, SurfacePointerButton::Back),
(
WuiSurfacePointerButton::Forward,
SurfacePointerButton::Forward,
),
] {
let mut ffi = event(WuiSurfaceInputEventKind::PointerButton);
ffi.pressed = true;
ffi.button = ffi_button;
ffi.x = 4.0;
ffi.y = 8.0;
assert_eq!(
into_rust(ffi),
SurfaceInputEvent::PointerButton {
pressed: true,
button: expected,
position: kurbo::Point::new(4.0, 8.0),
}
);
}
}
#[test]
fn scroll_round_trips_with_its_unit_and_finished_flag() {
for (ffi_unit, expected) in [
(WuiScrollUnit::Line, ScrollUnit::Line),
(WuiScrollUnit::Pixel, ScrollUnit::Pixel),
] {
let mut ffi = event(WuiSurfaceInputEventKind::Scroll);
ffi.x = 1.0;
ffi.y = 2.0;
ffi.delta_x = -10.0;
ffi.delta_y = 40.0;
ffi.scroll_unit = ffi_unit;
ffi.finished = true;
assert_eq!(
into_rust(ffi),
SurfaceInputEvent::Scroll {
position: kurbo::Point::new(1.0, 2.0),
delta_x: -10.0,
delta_y: 40.0,
unit: expected,
finished: true,
}
);
}
}
#[test]
fn a_character_key_round_trips_with_its_chord_and_repeat() {
let mut ffi = event(WuiSurfaceInputEventKind::Key);
ffi.pressed = true;
ffi.repeat = true;
ffi.key = Str::from_static("a").into_ffi();
ffi.code = Str::from_static("KeyA").into_ffi();
ffi.modifiers = WUI_SURFACE_MODIFIER_CONTROL;
assert_eq!(
into_rust(ffi),
SurfaceInputEvent::Key {
pressed: true,
key: Key::Character("a".to_owned()),
code: Code::KeyA,
modifiers: Modifiers::CONTROL,
repeat: true,
}
);
}
#[test]
fn a_named_key_round_trips() {
let mut ffi = event(WuiSurfaceInputEventKind::Key);
ffi.key = Str::from_static("ArrowLeft").into_ffi();
ffi.code = Str::from_static("ArrowLeft").into_ffi();
assert_eq!(
into_rust(ffi),
SurfaceInputEvent::Key {
pressed: false,
key: Key::Named(NamedKey::ArrowLeft),
code: Code::ArrowLeft,
modifiers: Modifiers::empty(),
repeat: false,
}
);
}
#[test]
#[should_panic(expected = "is not a W3C KeyboardEvent.key name")]
fn a_platform_key_name_fails_fast() {
let mut ffi = event(WuiSurfaceInputEventKind::Key);
ffi.key = Str::from_static("LeftArrow").into_ffi();
ffi.code = Str::from_static("ArrowLeft").into_ffi();
let _ = into_rust(ffi);
}
#[test]
#[should_panic(expected = "is not a W3C KeyboardEvent.code name")]
fn a_platform_code_name_fails_fast() {
let mut ffi = event(WuiSurfaceInputEventKind::Key);
ffi.key = Str::from_static("a").into_ffi();
ffi.code = Str::from_static("0").into_ffi();
let _ = into_rust(ffi);
}
#[test]
fn text_input_round_trips() {
let mut ffi = event(WuiSurfaceInputEventKind::TextInput);
ffi.text = Str::from_static("日本語").into_ffi();
assert_eq!(
into_rust(ffi),
SurfaceInputEvent::TextInput(Str::from_static("日本語"))
);
}
#[test]
fn composition_start_and_cancel_round_trip() {
assert_eq!(
into_rust(event(WuiSurfaceInputEventKind::CompositionStart)),
SurfaceInputEvent::CompositionStart
);
assert_eq!(
into_rust(event(WuiSurfaceInputEventKind::CompositionCancel)),
SurfaceInputEvent::CompositionCancel
);
}
#[test]
fn a_composition_update_carries_its_caret_or_none() {
let mut ffi = event(WuiSurfaceInputEventKind::CompositionUpdate);
ffi.text = Str::from_static("にほん").into_ffi();
ffi.caret = 6;
assert_eq!(
into_rust(ffi),
SurfaceInputEvent::CompositionUpdate {
text: Str::from_static("にほん"),
caret: Some(6),
}
);
let mut ffi = event(WuiSurfaceInputEventKind::CompositionUpdate);
ffi.text = Str::from_static("にほん").into_ffi();
assert_eq!(
into_rust(ffi),
SurfaceInputEvent::CompositionUpdate {
text: Str::from_static("にほん"),
caret: None,
}
);
}
#[test]
#[should_panic(expected = "is not a UTF-8 boundary")]
fn a_caret_inside_a_character_fails_fast() {
let mut ffi = event(WuiSurfaceInputEventKind::CompositionUpdate);
ffi.text = Str::from_static("にほん").into_ffi();
ffi.caret = 1;
let _ = into_rust(ffi);
}
#[test]
#[should_panic(expected = "must be a byte offset or -1")]
fn a_negative_caret_that_is_not_the_absent_sentinel_fails_fast() {
let mut ffi = event(WuiSurfaceInputEventKind::CompositionUpdate);
ffi.caret = -2;
let _ = into_rust(ffi);
}
#[test]
fn composition_commit_round_trips() {
let mut ffi = event(WuiSurfaceInputEventKind::CompositionCommit);
ffi.text = Str::from_static("日本").into_ffi();
assert_eq!(
into_rust(ffi),
SurfaceInputEvent::CompositionCommit(Str::from_static("日本"))
);
}
}