use super::*;
use crate::renderer::render::EmbeddedGpuSurfaceRuntime;
use waterui_graphics::input::{Code, Key, ScrollUnit, SurfaceInputEvent, SurfacePointerButton};
pub(crate) struct KeyDelivery<'a> {
pub(crate) pressed: bool,
pub(crate) logical: &'a Key,
pub(crate) code: Code,
pub(crate) repeat: bool,
pub(crate) modifiers: Modifiers,
}
pub(crate) trait EmbeddedInputSink {
fn identity(&self) -> *const ();
fn set_focus(&self, focused: bool);
fn set_modifiers(&self, modifiers: Modifiers);
fn pointer_move(&self, position: vello::kurbo::Point);
fn pointer_button(&self, pressed: bool, button: PointerButton, position: vello::kurbo::Point);
fn scroll(
&self,
position: vello::kurbo::Point,
delta_x: f32,
delta_y: f32,
unit: ScrollUnit,
finished: bool,
);
fn key(&self, delivery: &KeyDelivery<'_>);
fn text_input(&self, text: &str);
fn composition_start(&self);
fn composition_update(&self, text: &str, caret: Option<usize>);
fn composition_commit(&self, text: &str);
fn composition_cancel(&self);
fn ime_caret(&self) -> Option<vello::kurbo::Rect>;
}
#[derive(Clone)]
pub(crate) struct EmbeddedInputTarget {
pub(crate) local_bounds: vello::kurbo::Rect,
pub(crate) inverse_transform: vello::kurbo::Affine,
pub(crate) depth: usize,
pub(crate) order: usize,
pub(crate) sink: Rc<dyn EmbeddedInputSink>,
}
impl EmbeddedInputTarget {
pub(crate) fn local_position(&self, point: vello::kurbo::Point) -> Option<vello::kurbo::Point> {
self.local_bounds
.contains(self.inverse_transform * point)
.then(|| self.local_position_unclamped(point))
}
pub(crate) fn local_position_unclamped(
&self,
point: vello::kurbo::Point,
) -> vello::kurbo::Point {
let local = self.inverse_transform * point;
vello::kurbo::Point::new(
local.x - self.local_bounds.x0,
local.y - self.local_bounds.y0,
)
}
pub(crate) fn to_window_rect(&self, local: vello::kurbo::Rect) -> vello::kurbo::Rect {
self.inverse_transform.inverse().transform_rect_bbox(
local + vello::kurbo::Vec2::new(self.local_bounds.x0, self.local_bounds.y0),
)
}
}
pub(crate) struct GpuSurfaceInputSink {
runtime: Rc<RefCell<EmbeddedGpuSurfaceRuntime>>,
}
impl GpuSurfaceInputSink {
pub(crate) const fn new(runtime: Rc<RefCell<EmbeddedGpuSurfaceRuntime>>) -> Self {
Self { runtime }
}
fn send(&self, event: &SurfaceInputEvent) {
self.runtime.borrow_mut().input(event);
}
}
fn surface_pointer_button(button: PointerButton) -> Option<SurfacePointerButton> {
match button {
PointerButton::Primary => Some(SurfacePointerButton::Primary),
PointerButton::Secondary => Some(SurfacePointerButton::Secondary),
PointerButton::Middle => Some(SurfacePointerButton::Middle),
PointerButton::Back => Some(SurfacePointerButton::Back),
PointerButton::Forward => Some(SurfacePointerButton::Forward),
PointerButton::Other(_) => None,
}
}
impl EmbeddedInputSink for GpuSurfaceInputSink {
fn identity(&self) -> *const () {
Rc::as_ptr(&self.runtime).cast()
}
fn set_focus(&self, focused: bool) {
self.send(&SurfaceInputEvent::Focus(focused));
}
fn set_modifiers(&self, modifiers: Modifiers) {
self.send(&SurfaceInputEvent::Modifiers(modifiers.into()));
}
fn pointer_move(&self, position: vello::kurbo::Point) {
self.send(&SurfaceInputEvent::PointerMove { position });
}
fn pointer_button(&self, pressed: bool, button: PointerButton, position: vello::kurbo::Point) {
let Some(button) = surface_pointer_button(button) else {
tracing::trace!(
target: "waterui::hydrolysis::input",
button = ?button,
"dropped a pointer button with no W3C meaning for an embedded surface"
);
return;
};
self.send(&SurfaceInputEvent::PointerButton {
pressed,
button,
position,
});
}
fn scroll(
&self,
position: vello::kurbo::Point,
delta_x: f32,
delta_y: f32,
unit: ScrollUnit,
finished: bool,
) {
self.send(&SurfaceInputEvent::Scroll {
position,
delta_x: f64::from(delta_x),
delta_y: f64::from(delta_y),
unit,
finished,
});
}
fn key(&self, delivery: &KeyDelivery<'_>) {
self.send(&SurfaceInputEvent::Key {
pressed: delivery.pressed,
key: delivery.logical.clone(),
code: delivery.code,
modifiers: delivery.modifiers.into(),
repeat: delivery.repeat,
});
}
fn text_input(&self, text: &str) {
self.send(&SurfaceInputEvent::TextInput(text.to_owned().into()));
}
fn composition_start(&self) {
self.send(&SurfaceInputEvent::CompositionStart);
}
fn composition_update(&self, text: &str, caret: Option<usize>) {
self.send(&SurfaceInputEvent::CompositionUpdate {
text: text.to_owned().into(),
caret,
});
}
fn composition_commit(&self, text: &str) {
self.send(&SurfaceInputEvent::CompositionCommit(
text.to_owned().into(),
));
}
fn composition_cancel(&self) {
self.send(&SurfaceInputEvent::CompositionCancel);
}
fn ime_caret(&self) -> Option<vello::kurbo::Rect> {
self.runtime.borrow().ime_caret()
}
}
impl HydrolysisRenderer {
pub(crate) fn register_embedded_input_target(
&mut self,
local_bounds: vello::kurbo::Rect,
transform: vello::kurbo::Affine,
sink: Rc<dyn EmbeddedInputSink>,
) {
if self.hit_test.hit_test_opacity <= HIT_TEST_ALPHA_THRESHOLD {
return;
}
let determinant = transform.determinant();
assert!(
determinant.is_finite() && determinant.abs() > f64::EPSILON,
"embedded surface input transform must be finite and invertible"
);
let order = self.hit_test.next_hit_test_order();
tracing::trace!(
target: "waterui::hydrolysis::input",
bounds = ?local_bounds,
window_bounds = ?transform.transform_rect_bbox(local_bounds),
order,
"registered an embedded surface input target"
);
self.hit_test
.embedded_input_targets
.push(EmbeddedInputTarget {
local_bounds,
inverse_transform: transform.inverse(),
depth: self.render_depth,
order,
sink,
});
}
pub(crate) fn register_gpu_surface_input_target(
&mut self,
local_bounds: vello::kurbo::Rect,
transform: vello::kurbo::Affine,
runtime: Rc<RefCell<EmbeddedGpuSurfaceRuntime>>,
) {
self.register_embedded_input_target(
local_bounds,
transform,
Rc::new(GpuSurfaceInputSink::new(runtime)),
);
}
fn topmost_embedded_target_at(
&self,
point: vello::kurbo::Point,
) -> Option<(usize, vello::kurbo::Point)> {
self.hit_test
.embedded_input_targets
.iter()
.enumerate()
.filter_map(|(index, target)| {
target
.local_position(point)
.map(|position| (index, position))
})
.max_by(|(left, _), (right, _)| {
let left_target = &self.hit_test.embedded_input_targets[*left];
let right_target = &self.hit_test.embedded_input_targets[*right];
Self::target_hit_priority(left_target.depth, left_target.order, *left).cmp(
&Self::target_hit_priority(right_target.depth, right_target.order, *right),
)
})
}
pub(super) fn embedded_target_wins_at(
&self,
point: vello::kurbo::Point,
pointer_priority: Option<(usize, usize, usize)>,
text_priority: Option<(usize, usize, usize)>,
) -> Option<(EmbeddedInputTarget, vello::kurbo::Point)> {
let (index, position) = self.topmost_embedded_target_at(point)?;
let target = &self.hit_test.embedded_input_targets[index];
let embedded_priority = Self::target_hit_priority(target.depth, target.order, index);
if pointer_priority.is_some_and(|priority| priority > embedded_priority)
|| text_priority.is_some_and(|priority| priority > embedded_priority)
{
return None;
}
Some((target.clone(), position))
}
pub(crate) fn handle_embedded_pointer_move(&mut self, point: vello::kurbo::Point) -> bool {
if let Some(target) = self.hit_test.active_embedded_target.as_ref() {
target
.sink
.pointer_move(target.local_position_unclamped(point));
return true;
}
let pointer_priority = self
.hit_test
.pointer_targets
.iter()
.enumerate()
.filter(|(_, target)| target.bounds.contains(point))
.map(|(index, target)| Self::target_hit_priority(target.depth, target.order, index))
.max();
let text_priority = self.topmost_text_input_index_at_point(point).map(|index| {
let target = &self.text_editing.text_input_targets[index];
Self::target_hit_priority(target.depth, target.order, index)
});
let Some((target, position)) =
self.embedded_target_wins_at(point, pointer_priority, text_priority)
else {
return false;
};
target.sink.pointer_move(position);
true
}
pub(crate) fn handle_embedded_scroll(
&mut self,
point: vello::kurbo::Point,
delta_x: f32,
delta_y: f32,
unit: ScrollUnit,
finished: bool,
) -> bool {
let Some((index, position)) = self.topmost_embedded_target_at(point) else {
return false;
};
self.hit_test.embedded_input_targets[index]
.sink
.scroll(position, delta_x, delta_y, unit, finished);
true
}
pub(crate) fn handle_embedded_key(&mut self, delivery: &KeyDelivery<'_>) -> bool {
let Some(sink) = self.hit_test.focused_embedded_sink.as_ref() else {
return false;
};
sink.key(delivery);
true
}
pub(crate) fn handle_embedded_text_input(&mut self, text: &str) -> bool {
let Some(sink) = self.hit_test.focused_embedded_sink.as_ref() else {
return false;
};
sink.text_input(text);
true
}
pub(crate) fn handle_embedded_ime_preedit(&mut self, text: &str, caret: Option<usize>) -> bool {
let Some(sink) = self.hit_test.focused_embedded_sink.clone() else {
return false;
};
if text.is_empty() {
if self.hit_test.embedded_composing {
self.hit_test.embedded_composing = false;
sink.composition_cancel();
}
return true;
}
if !self.hit_test.embedded_composing {
self.hit_test.embedded_composing = true;
sink.composition_start();
}
sink.composition_update(text, caret);
true
}
pub(crate) fn handle_embedded_ime_commit(&mut self, text: &str) -> bool {
let Some(sink) = self.hit_test.focused_embedded_sink.clone() else {
return false;
};
if !self.hit_test.embedded_composing {
sink.composition_start();
}
self.hit_test.embedded_composing = false;
sink.composition_commit(text);
true
}
pub(crate) fn handle_embedded_ime_disabled(&mut self) -> bool {
let Some(sink) = self.hit_test.focused_embedded_sink.clone() else {
return false;
};
if !self.hit_test.embedded_composing {
return false;
}
self.hit_test.embedded_composing = false;
sink.composition_cancel();
true
}
pub(crate) fn update_embedded_modifiers(&mut self, modifiers: Modifiers) {
if let Some(sink) = self.hit_test.focused_embedded_sink.as_ref() {
sink.set_modifiers(modifiers);
}
}
pub(crate) fn focused_embedded_ime_caret(&self) -> Option<vello::kurbo::Rect> {
let sink = self.hit_test.focused_embedded_sink.as_ref()?;
let caret = sink.ime_caret()?;
let target = self
.hit_test
.embedded_input_targets
.iter()
.find(|target| target.sink.identity() == sink.identity())?;
Some(target.to_window_rect(caret))
}
}