1use tessera_ui::{
9 BuildModifierNode, Callback, CallbackWith, FocusProperties, FocusRequester, Modifier,
10 ModifierBuildContext, PointerInput, PointerInputModifierNode, Px, PxPosition, PxSize, State,
11 accesskit, modifier::ModifierCapabilityExt as _,
12};
13
14use crate::gesture::{DragAxis, DragRecognizer, DragSettings, LongPressRecognizer, TapRecognizer};
15
16#[derive(Clone, PartialEq, Copy, Debug)]
18pub struct PointerEventContext {
19 pub normalized_pos: [f32; 2],
21 pub size: PxSize,
23}
24
25type PressCallback = CallbackWith<PointerEventContext, ()>;
26type DragCallback = CallbackWith<DragDelta, ()>;
27
28#[derive(Clone)]
30pub struct ClickableArgs {
31 pub on_click: Callback,
33 pub enabled: bool,
35 pub block_input: bool,
37 pub on_press: Option<PressCallback>,
39 pub on_release: Option<PressCallback>,
41 pub role: Option<accesskit::Role>,
43 pub label: Option<String>,
45 pub description: Option<String>,
47 pub interaction_state: Option<State<InteractionState>>,
49 pub focus_requester: Option<FocusRequester>,
51 pub focus_properties: Option<FocusProperties>,
53 pub tap_recognizer: Option<State<TapRecognizer>>,
55 pub long_press_recognizer: Option<State<LongPressRecognizer>>,
58}
59
60impl Default for ClickableArgs {
61 fn default() -> Self {
62 Self {
63 on_click: Callback::noop(),
64 enabled: true,
65 block_input: true,
66 on_press: None,
67 on_release: None,
68 role: None,
69 label: None,
70 description: None,
71 interaction_state: None,
72 focus_requester: None,
73 focus_properties: None,
74 tap_recognizer: None,
75 long_press_recognizer: None,
76 }
77 }
78}
79
80#[derive(Clone)]
82pub struct ToggleableArgs {
83 pub value: bool,
85 pub on_value_change: CallbackWith<bool, ()>,
87 pub enabled: bool,
89 pub role: Option<accesskit::Role>,
91 pub label: Option<String>,
93 pub description: Option<String>,
95 pub interaction_state: Option<State<InteractionState>>,
97 pub on_press: Option<PressCallback>,
99 pub on_release: Option<PressCallback>,
101 pub focus_requester: Option<FocusRequester>,
103 pub tap_recognizer: Option<State<TapRecognizer>>,
105}
106
107impl Default for ToggleableArgs {
108 fn default() -> Self {
109 Self {
110 value: false,
111 on_value_change: CallbackWith::default_value(),
112 enabled: true,
113 role: None,
114 label: None,
115 description: None,
116 interaction_state: None,
117 on_press: None,
118 on_release: None,
119 focus_requester: None,
120 tap_recognizer: None,
121 }
122 }
123}
124
125#[derive(Clone)]
127pub struct SelectableArgs {
128 pub selected: bool,
130 pub on_click: Callback,
132 pub enabled: bool,
134 pub role: Option<accesskit::Role>,
136 pub label: Option<String>,
138 pub description: Option<String>,
140 pub interaction_state: Option<State<InteractionState>>,
142 pub on_press: Option<PressCallback>,
144 pub on_release: Option<PressCallback>,
146 pub focus_requester: Option<FocusRequester>,
148 pub tap_recognizer: Option<State<TapRecognizer>>,
150}
151
152impl Default for SelectableArgs {
153 fn default() -> Self {
154 Self {
155 selected: false,
156 on_click: Callback::noop(),
157 enabled: true,
158 role: None,
159 label: None,
160 description: None,
161 interaction_state: None,
162 on_press: None,
163 on_release: None,
164 focus_requester: None,
165 tap_recognizer: None,
166 }
167 }
168}
169
170#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
172pub struct DragDelta {
173 pub x: Px,
175 pub y: Px,
177}
178
179#[derive(Clone)]
181pub struct DraggableArgs {
182 pub on_drag_delta: DragCallback,
184 pub enabled: bool,
186 pub axis: Option<DragAxis>,
188 pub slop_px: f32,
190 pub consume_when_dragging: bool,
192 pub on_drag_started: Option<Callback>,
194 pub on_drag_stopped: Option<Callback>,
196 pub interaction_state: Option<State<InteractionState>>,
198 pub drag_recognizer: Option<State<DragRecognizer>>,
200}
201
202impl Default for DraggableArgs {
203 fn default() -> Self {
204 Self {
205 on_drag_delta: CallbackWith::default_value(),
206 enabled: true,
207 axis: None,
208 slop_px: DragSettings::default().slop_px,
209 consume_when_dragging: DragSettings::default().consume_when_dragging,
210 on_drag_started: None,
211 on_drag_stopped: None,
212 interaction_state: None,
213 drag_recognizer: None,
214 }
215 }
216}
217
218#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
233pub enum InteractionVisualState {
234 #[default]
236 Rest,
237 Hover,
239 Focus,
241 Pressed,
243 Dragged,
245}
246
247#[derive(Clone, PartialEq, Copy, Debug, Default)]
249pub struct InteractionState {
250 is_hovered: bool,
251 is_focused: bool,
252 is_dragged: bool,
253 is_pressed: bool,
254}
255
256impl InteractionState {
257 pub fn with_presentation(
263 self,
264 presentation_state: Option<InteractionVisualState>,
265 enabled: bool,
266 ) -> Self {
267 if !enabled {
268 return Self::default();
269 }
270 if self.is_hovered || self.is_focused || self.is_dragged || self.is_pressed {
271 return self;
272 }
273 Self {
274 is_hovered: presentation_state == Some(InteractionVisualState::Hover),
275 is_focused: presentation_state == Some(InteractionVisualState::Focus),
276 is_dragged: presentation_state == Some(InteractionVisualState::Dragged),
277 is_pressed: presentation_state == Some(InteractionVisualState::Pressed),
278 }
279 }
280
281 pub fn new() -> Self {
283 Self::default()
284 }
285
286 pub fn release(&mut self) {
288 self.set_pressed(false);
289 }
290
291 pub fn set_hovered(&mut self, hovered: bool) {
293 self.is_hovered = hovered;
294 }
295
296 pub fn is_hovered(&self) -> bool {
298 self.is_hovered
299 }
300
301 pub fn set_focused(&mut self, focused: bool) {
303 self.is_focused = focused;
304 }
305
306 pub fn is_focused(&self) -> bool {
308 self.is_focused
309 }
310
311 pub fn set_dragged(&mut self, dragged: bool) {
313 self.is_dragged = dragged;
314 }
315
316 pub fn is_dragged(&self) -> bool {
318 self.is_dragged
319 }
320
321 pub fn set_pressed(&mut self, pressed: bool) {
323 self.is_pressed = pressed;
324 }
325
326 pub fn is_pressed(&self) -> bool {
328 self.is_pressed
329 }
330
331 pub fn state_layer_alpha(&self) -> f32 {
333 if self.is_dragged {
334 0.16
335 } else if self.is_pressed || self.is_focused {
336 0.10
337 } else if self.is_hovered {
338 0.08
339 } else {
340 0.0
341 }
342 }
343}
344
345#[derive(Clone)]
346enum DragRecognizerHandle {
347 State(State<DragRecognizer>),
348}
349
350impl DragRecognizerHandle {
351 fn with_mut<R>(&self, f: impl FnOnce(&mut DragRecognizer) -> R) -> R {
352 match self {
353 Self::State(state) => state.with_mut(f),
354 }
355 }
356}
357
358struct DraggablePointerModifierNode {
359 drag_recognizer: DragRecognizerHandle,
360 on_drag_delta: DragCallback,
361 enabled: bool,
362 on_drag_started: Option<Callback>,
363 on_drag_stopped: Option<Callback>,
364 interaction_state: Option<State<InteractionState>>,
365}
366
367impl PointerInputModifierNode for DraggablePointerModifierNode {
368 fn on_pointer_input(&self, input: PointerInput<'_>) {
369 if !self.enabled {
370 return;
371 }
372
373 let within_bounds = cursor_within_bounds(
374 input.cursor_position_rel,
375 PxSize::new(input.computed_data.width, input.computed_data.height),
376 );
377 let (was_dragging, drag_result) = self.drag_recognizer.with_mut(|recognizer| {
378 let was_dragging = recognizer.is_dragging();
379 let drag_result = recognizer.update(input.pass, input.pointer_changes, within_bounds);
380 (was_dragging, drag_result)
381 });
382
383 if drag_result.started {
384 if let Some(interaction_state) = self.interaction_state {
385 interaction_state.with_mut(|state| state.set_dragged(true));
386 }
387 if let Some(on_drag_started) = self.on_drag_started {
388 on_drag_started.call();
389 }
390 }
391
392 if drag_result.updated {
393 let (x, y) = self.drag_recognizer.with_mut(|recognizer| {
394 recognizer.local_integer_delta(
395 drag_result.delta_x,
396 drag_result.delta_y,
397 input.local_to_window,
398 )
399 });
400 self.on_drag_delta.call(DragDelta { x, y });
401 }
402
403 if drag_result.ended && was_dragging {
404 if let Some(interaction_state) = self.interaction_state {
405 interaction_state.with_mut(|state| state.set_dragged(false));
406 }
407 if let Some(on_drag_stopped) = self.on_drag_stopped {
408 on_drag_stopped.call();
409 }
410 }
411 }
412}
413
414fn cursor_within_bounds(position: Option<PxPosition>, size: PxSize) -> bool {
415 let Some(position) = position else {
416 return false;
417 };
418
419 position.x >= 0.0
420 && position.y >= 0.0
421 && position.x < size.width.0 as f32
422 && position.y < size.height.0 as f32
423}
424
425pub(crate) fn apply_draggable_modifier(base: Modifier, args: DraggableArgs) -> Modifier {
426 let DraggableArgs {
427 on_drag_delta,
428 enabled,
429 axis,
430 slop_px,
431 consume_when_dragging,
432 on_drag_started,
433 on_drag_stopped,
434 interaction_state,
435 drag_recognizer,
436 } = args;
437
438 let settings = DragSettings {
439 slop_px,
440 consume_when_dragging,
441 axis,
442 };
443 base.push_build(DraggableBuild {
444 settings,
445 drag_recognizer,
446 on_drag_delta,
447 enabled,
448 on_drag_started,
449 on_drag_stopped,
450 interaction_state,
451 })
452}
453
454struct DraggableBuild {
455 settings: DragSettings,
456 drag_recognizer: Option<State<DragRecognizer>>,
457 on_drag_delta: DragCallback,
458 enabled: bool,
459 on_drag_started: Option<Callback>,
460 on_drag_stopped: Option<Callback>,
461 interaction_state: Option<State<InteractionState>>,
462}
463
464impl BuildModifierNode for DraggableBuild {
465 fn materialize(&self, context: &mut ModifierBuildContext) -> Option<Modifier> {
466 if !self.enabled {
467 if let Some(state) = self.interaction_state {
468 state.with_mut(|state| state.set_dragged(false));
469 }
470 return Some(Modifier::new());
471 }
472 let recognizer = self
473 .drag_recognizer
474 .unwrap_or_else(|| context.state(|| DragRecognizer::new(self.settings)));
475 recognizer.with_mut(|recognizer| recognizer.set_settings(self.settings));
476 Some(
477 Modifier::new().push_pointer_input(DraggablePointerModifierNode {
478 drag_recognizer: DragRecognizerHandle::State(recognizer),
479 on_drag_delta: self.on_drag_delta,
480 enabled: self.enabled,
481 on_drag_started: self.on_drag_started,
482 on_drag_stopped: self.on_drag_stopped,
483 interaction_state: self.interaction_state,
484 }),
485 )
486 }
487}
488
489#[cfg(test)]
490mod presentation_tests {
491 use super::{InteractionState, InteractionVisualState};
492
493 #[test]
494 fn presentation_preserves_live_combinations_and_disabled_priority() {
495 for bits in 0..16 {
496 let mut live = InteractionState::new();
497 live.set_hovered(bits & 1 != 0);
498 live.set_focused(bits & 2 != 0);
499 live.set_pressed(bits & 4 != 0);
500 live.set_dragged(bits & 8 != 0);
501 assert_eq!(live.with_presentation(None, true), live);
502 for preview in [
503 InteractionVisualState::Rest,
504 InteractionVisualState::Hover,
505 InteractionVisualState::Focus,
506 InteractionVisualState::Pressed,
507 InteractionVisualState::Dragged,
508 ] {
509 assert_eq!(
510 live.with_presentation(Some(preview), false),
511 InteractionState::new(),
512 );
513 let visual = live.with_presentation(Some(preview), true);
514 if bits != 0 {
515 assert_eq!(visual, live);
516 } else {
517 assert_eq!(
518 visual.is_hovered(),
519 preview == InteractionVisualState::Hover
520 );
521 assert_eq!(
522 visual.is_focused(),
523 preview == InteractionVisualState::Focus
524 );
525 assert_eq!(
526 visual.is_pressed(),
527 preview == InteractionVisualState::Pressed
528 );
529 assert_eq!(
530 visual.is_dragged(),
531 preview == InteractionVisualState::Dragged
532 );
533 }
534 }
535 }
536 }
537}