1use std::fmt;
36
37use iced::advanced::layout::{self, Layout};
38use iced::advanced::widget::{Operation, Tree, Widget, tree};
39use iced::advanced::{Clipboard, Shell, overlay, renderer};
40use iced::touch::{self, Finger};
41use iced::{Element, Event, Length, Point, Rectangle, Renderer, Size, Theme, Vector, mouse};
42
43pub const DEFAULT_SLOP: f32 = 8.0;
45pub const DEFAULT_LINE_HEIGHT: f32 = 21.0;
48const WHEEL_PIXELS_PER_LINE: f32 = 4.0;
50
51pub fn wheel_delta(finger_dy: f32, line_height: f32) -> mouse::ScrollDelta {
54 let line_height = line_height.max(1.0);
55 mouse::ScrollDelta::Pixels {
56 x: 0.0,
57 y: finger_dy * WHEEL_PIXELS_PER_LINE / line_height,
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq)]
63pub enum Move {
64 Ignored,
66 Held,
68 Dragged { dy: f32, inside: bool },
71}
72
73#[derive(Debug, Clone, Copy, PartialEq)]
74struct Track {
75 finger: Finger,
76 origin: Point,
77 last: Point,
78 target: mouse::Cursor,
79 inside: bool,
80 dragging: bool,
81 cancelled: bool,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Default)]
86pub struct Translator {
87 track: Option<Track>,
88}
89
90impl Translator {
91 pub fn press(&mut self, finger: Finger, position: Point, target: mouse::Cursor, inside: bool) {
96 if let Some(track) = self.track.as_mut() {
97 track.cancelled = true;
98 return;
99 }
100 self.track = Some(Track {
101 finger,
102 origin: position,
103 last: position,
104 target,
105 inside,
106 dragging: false,
107 cancelled: false,
108 });
109 }
110
111 pub fn moved(&mut self, finger: Finger, position: Point, slop: f32) -> Move {
112 let Some(track) = self.track.as_mut().filter(|track| track.finger == finger) else {
113 return Move::Ignored;
114 };
115 if !track.dragging && track.origin.distance(position) > slop {
116 track.dragging = true;
117 }
118 let dy = position.y - track.last.y;
119 track.last = position;
120 if track.dragging {
121 Move::Dragged {
122 dy,
123 inside: track.inside,
124 }
125 } else {
126 Move::Held
127 }
128 }
129
130 pub fn lift(&mut self, finger: Finger) -> Option<mouse::Cursor> {
133 let track = self.track.filter(|track| track.finger == finger)?;
134 self.track = None;
135 (!track.dragging && !track.cancelled).then_some(track.target)
136 }
137
138 pub fn lost(&mut self, finger: Finger) {
139 if self.track.is_some_and(|track| track.finger == finger) {
140 self.track = None;
141 }
142 }
143
144 pub fn is_inside(&self, finger: Finger) -> bool {
146 self.track
147 .is_some_and(|track| track.finger == finger && track.inside)
148 }
149}
150
151pub struct TouchClicks<'a, Message> {
153 content: Element<'a, Message>,
154 slop: f32,
155 scroll: bool,
156 line_height: f32,
157}
158
159pub fn touch_clicks<'a, Message>(
161 content: impl Into<Element<'a, Message>>,
162) -> TouchClicks<'a, Message> {
163 TouchClicks {
164 content: content.into(),
165 slop: DEFAULT_SLOP,
166 scroll: false,
167 line_height: DEFAULT_LINE_HEIGHT,
168 }
169}
170
171impl<Message> fmt::Debug for TouchClicks<'_, Message> {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 f.debug_struct("TouchClicks")
174 .field("slop", &self.slop)
175 .field("scroll", &self.scroll)
176 .field("line_height", &self.line_height)
177 .finish_non_exhaustive()
178 }
179}
180
181impl<Message> TouchClicks<'_, Message> {
182 pub fn slop(mut self, slop: f32) -> Self {
185 self.slop = slop.max(0.0);
186 self
187 }
188
189 pub fn scroll(mut self, scroll: bool) -> Self {
193 self.scroll = scroll;
194 self
195 }
196
197 pub fn line_height(mut self, line_height: f32) -> Self {
200 self.line_height = line_height.max(1.0);
201 self
202 }
203}
204
205impl<'a, Message: 'a> From<TouchClicks<'a, Message>> for Element<'a, Message> {
206 fn from(clicks: TouchClicks<'a, Message>) -> Self {
207 Element::new(Wrapper { clicks })
208 }
209}
210
211struct Wrapper<'a, Message> {
212 clicks: TouchClicks<'a, Message>,
213}
214
215impl<Message> Wrapper<'_, Message> {
216 #[allow(clippy::too_many_arguments)]
220 fn deliver(
221 &mut self,
222 tree: &mut Tree,
223 events: &[Event],
224 cursor: mouse::Cursor,
225 layout: Layout<'_>,
226 renderer: &Renderer,
227 clipboard: &mut dyn Clipboard,
228 shell: &mut Shell<'_, Message>,
229 viewport: &Rectangle,
230 ) {
231 let mut messages = Vec::new();
232 let mut inner = Shell::new(&mut messages);
233 for event in events {
234 self.clicks.content.as_widget_mut().update(
235 &mut tree.children[0],
236 event,
237 layout,
238 cursor,
239 renderer,
240 clipboard,
241 &mut inner,
242 viewport,
243 );
244 }
245 let layout_invalid = inner.is_layout_invalid();
246 let widgets_invalid = inner.are_widgets_invalid();
247 let redraw = inner.redraw_request();
248 for message in messages {
249 shell.publish(message);
250 }
251 if layout_invalid {
252 shell.invalidate_layout();
253 }
254 if widgets_invalid {
255 shell.invalidate_widgets();
256 }
257 shell.request_redraw_at(redraw);
258 }
259}
260
261impl<Message> Widget<Message, Theme, Renderer> for Wrapper<'_, Message> {
262 fn tag(&self) -> tree::Tag {
263 tree::Tag::of::<Translator>()
264 }
265
266 fn state(&self) -> tree::State {
267 tree::State::new(Translator::default())
268 }
269
270 fn children(&self) -> Vec<Tree> {
271 vec![Tree::new(&self.clicks.content)]
272 }
273
274 fn diff(&self, tree: &mut Tree) {
275 tree.diff_children(std::slice::from_ref(&self.clicks.content));
276 }
277
278 fn size(&self) -> Size<Length> {
279 self.clicks.content.as_widget().size()
280 }
281
282 fn size_hint(&self) -> Size<Length> {
283 self.clicks.content.as_widget().size_hint()
284 }
285
286 fn layout(
287 &mut self,
288 tree: &mut Tree,
289 renderer: &Renderer,
290 limits: &layout::Limits,
291 ) -> layout::Node {
292 self.clicks
293 .content
294 .as_widget_mut()
295 .layout(&mut tree.children[0], renderer, limits)
296 }
297
298 fn draw(
299 &self,
300 tree: &Tree,
301 renderer: &mut Renderer,
302 theme: &Theme,
303 style: &renderer::Style,
304 layout: Layout<'_>,
305 cursor: mouse::Cursor,
306 viewport: &Rectangle,
307 ) {
308 self.clicks.content.as_widget().draw(
309 &tree.children[0],
310 renderer,
311 theme,
312 style,
313 layout,
314 cursor,
315 viewport,
316 );
317 }
318
319 fn operate(
320 &mut self,
321 tree: &mut Tree,
322 layout: Layout<'_>,
323 renderer: &Renderer,
324 operation: &mut dyn Operation,
325 ) {
326 self.clicks.content.as_widget_mut().operate(
327 &mut tree.children[0],
328 layout,
329 renderer,
330 operation,
331 );
332 }
333
334 fn update(
335 &mut self,
336 tree: &mut Tree,
337 event: &Event,
338 layout: Layout<'_>,
339 cursor: mouse::Cursor,
340 renderer: &Renderer,
341 clipboard: &mut dyn Clipboard,
342 shell: &mut Shell<'_, Message>,
343 viewport: &Rectangle,
344 ) {
345 self.clicks.content.as_widget_mut().update(
346 &mut tree.children[0],
347 event,
348 layout,
349 cursor,
350 renderer,
351 clipboard,
352 shell,
353 viewport,
354 );
355
356 let Event::Touch(touch) = event else {
357 return;
358 };
359 let bounds = layout.bounds();
360 match *touch {
361 touch::Event::FingerPressed { id, position } => {
362 let inside = cursor
363 .position()
364 .is_some_and(|target| bounds.contains(target) && viewport.contains(target));
365 tree.state
366 .downcast_mut::<Translator>()
367 .press(id, position, cursor, inside);
368 }
369 touch::Event::FingerMoved { id, position } => {
370 let translator = tree.state.downcast_mut::<Translator>();
371 let inside = translator.is_inside(id);
372 let change = translator.moved(id, position, self.clicks.slop);
373 if !self.clicks.scroll || !inside || change == Move::Ignored {
374 return;
375 }
376 shell.capture_event();
377 let Move::Dragged { dy, .. } = change else {
378 return;
379 };
380 let at = cursor
381 .position()
382 .filter(|position| bounds.contains(*position))
383 .unwrap_or(bounds.center());
384 let wheel = Event::Mouse(mouse::Event::WheelScrolled {
385 delta: wheel_delta(dy, self.clicks.line_height),
386 });
387 self.deliver(
388 tree,
389 &[wheel],
390 mouse::Cursor::Available(at),
391 layout,
392 renderer,
393 clipboard,
394 shell,
395 viewport,
396 );
397 }
398 touch::Event::FingerLifted { id, .. } => {
399 let Some(target) = tree.state.downcast_mut::<Translator>().lift(id) else {
400 return;
401 };
402 let moved = target
403 .position()
404 .map(|position| Event::Mouse(mouse::Event::CursorMoved { position }));
405 let click: Vec<Event> = moved
406 .into_iter()
407 .chain([
408 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
409 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
410 ])
411 .collect();
412 self.deliver(
413 tree, &click, target, layout, renderer, clipboard, shell, viewport,
414 );
415 }
416 touch::Event::FingerLost { id, .. } => {
417 tree.state.downcast_mut::<Translator>().lost(id);
418 }
419 }
420 }
421
422 fn mouse_interaction(
423 &self,
424 tree: &Tree,
425 layout: Layout<'_>,
426 cursor: mouse::Cursor,
427 viewport: &Rectangle,
428 renderer: &Renderer,
429 ) -> mouse::Interaction {
430 self.clicks.content.as_widget().mouse_interaction(
431 &tree.children[0],
432 layout,
433 cursor,
434 viewport,
435 renderer,
436 )
437 }
438
439 fn overlay<'b>(
440 &'b mut self,
441 tree: &'b mut Tree,
442 layout: Layout<'b>,
443 renderer: &Renderer,
444 viewport: &Rectangle,
445 translation: Vector,
446 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
447 self.clicks.content.as_widget_mut().overlay(
448 &mut tree.children[0],
449 layout,
450 renderer,
451 viewport,
452 translation,
453 )
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460
461 const ONE: Finger = Finger(1);
462
463 fn at(y: f32) -> Point {
464 Point::new(10.0, y)
465 }
466
467 #[test]
468 fn builder_defaults_and_clamps() {
469 let clicks: TouchClicks<'_, ()> = touch_clicks("Text");
470 assert_eq!(clicks.slop, DEFAULT_SLOP);
471 assert!(!clicks.scroll);
472 assert_eq!(clicks.line_height, DEFAULT_LINE_HEIGHT);
473 let clamped: TouchClicks<'_, ()> = touch_clicks("Text").slop(-1.0).line_height(0.0);
474 assert_eq!(clamped.slop, 0.0);
475 assert_eq!(clamped.line_height, 1.0);
476 }
477
478 fn cursor(y: f32) -> mouse::Cursor {
479 mouse::Cursor::Available(at(y))
480 }
481
482 #[test]
483 fn a_tap_clicks_where_the_content_was_touched() {
484 let mut translator = Translator::default();
485 translator.press(ONE, at(0.0), cursor(40.0), true);
486 assert_eq!(
487 translator.moved(ONE, at(DEFAULT_SLOP), DEFAULT_SLOP),
488 Move::Held
489 );
490 assert_eq!(translator.lift(ONE), Some(cursor(40.0)));
491 assert_eq!(translator.lift(ONE), None);
492 }
493
494 #[test]
495 fn a_tap_on_hidden_content_clicks_without_a_position() {
496 let mut translator = Translator::default();
497 let hidden = mouse::Cursor::Levitating(at(40.0));
498 translator.press(ONE, at(40.0), hidden, false);
499 assert_eq!(translator.lift(ONE), Some(hidden));
500 }
501
502 #[test]
503 fn a_drag_is_not_a_click_and_reports_its_movement() {
504 let mut translator = Translator::default();
505 translator.press(ONE, at(0.0), cursor(0.0), true);
506 assert_eq!(
507 translator.moved(ONE, at(20.0), DEFAULT_SLOP),
508 Move::Dragged {
509 dy: 20.0,
510 inside: true
511 }
512 );
513 assert_eq!(
514 translator.moved(ONE, at(15.0), DEFAULT_SLOP),
515 Move::Dragged {
516 dy: -5.0,
517 inside: true
518 }
519 );
520 assert_eq!(translator.lift(ONE), None);
521 }
522
523 #[test]
524 fn a_second_finger_cancels_and_others_are_ignored() {
525 let mut translator = Translator::default();
526 translator.press(ONE, at(0.0), cursor(0.0), true);
527 assert_eq!(
528 translator.moved(Finger(2), at(90.0), DEFAULT_SLOP),
529 Move::Ignored
530 );
531 translator.press(Finger(2), at(0.0), cursor(0.0), true);
532 assert_eq!(translator.lift(Finger(2)), None);
533 assert_eq!(translator.lift(ONE), None);
534
535 translator.press(ONE, at(0.0), cursor(0.0), false);
536 assert!(!translator.is_inside(ONE));
537 translator.lost(ONE);
538 assert_eq!(translator.lift(ONE), None);
539 }
540
541 #[test]
542 fn wheel_deltas_follow_the_finger_in_lines() {
543 let mouse::ScrollDelta::Pixels { x, y } = wheel_delta(-21.0, 21.0) else {
544 panic!("expected pixels");
545 };
546 assert_eq!(x, 0.0);
547 assert_eq!(y, -WHEEL_PIXELS_PER_LINE);
548 let mouse::ScrollDelta::Pixels { y, .. } = wheel_delta(10.0, 0.0) else {
549 panic!("expected pixels");
550 };
551 assert_eq!(y, 40.0);
552 }
553}