1use std::time::Instant;
4
5use crate::tab::DOUBLE_CLICK_DURATION;
6use cosmic::iced::core::border::Border;
7use cosmic::iced::core::event::Event;
8use cosmic::iced::core::mouse::{self, click};
9use cosmic::iced::core::renderer::{self, Quad, Renderer as _};
10use cosmic::iced::core::widget::{Operation, Tree, tree};
11use cosmic::iced::core::{
12 Clipboard, Color, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, layout,
13 overlay, touch,
14};
15use cosmic::widget::Id;
16use cosmic::{Element, Renderer, Theme};
17
18#[allow(missing_debug_implementations)]
20pub struct MouseArea<'a, Message> {
21 id: Id,
22 content: Element<'a, Message>,
23 on_auto_scroll: Option<Box<dyn OnAutoScroll<'a, Message>>>,
24 on_drag: Option<Box<dyn OnDrag<'a, Message>>>,
25 on_double_click: Option<Box<dyn OnMouseButton<'a, Message>>>,
26 on_press: Option<Box<dyn OnMouseButton<'a, Message>>>,
27 on_drag_end: Option<Box<dyn OnMouseButton<'a, Message>>>,
28 on_release: Option<Box<dyn OnMouseButton<'a, Message>>>,
29 on_resize: Option<Box<dyn OnResize<'a, Message>>>,
30 on_right_press: Option<Box<dyn OnMouseButton<'a, Message>>>,
31 on_right_press_no_capture: bool,
32 on_right_press_window_position: bool,
33 on_right_release: Option<Box<dyn OnMouseButton<'a, Message>>>,
34 on_middle_press: Option<Box<dyn OnMouseButton<'a, Message>>>,
35 on_middle_release: Option<Box<dyn OnMouseButton<'a, Message>>>,
36 on_back_press: Option<Box<dyn OnMouseButton<'a, Message>>>,
37 on_back_release: Option<Box<dyn OnMouseButton<'a, Message>>>,
38 on_forward_press: Option<Box<dyn OnMouseButton<'a, Message>>>,
39 on_forward_release: Option<Box<dyn OnMouseButton<'a, Message>>>,
40 on_scroll: Option<Box<dyn OnScroll<'a, Message>>>,
41 on_enter: Option<Box<dyn OnEnterExit<'a, Message>>>,
42 on_exit: Option<Box<dyn OnEnterExit<'a, Message>>>,
43 show_drag_rect: bool,
44}
45
46impl<'a, Message> MouseArea<'a, Message> {
47 #[must_use]
49 pub fn on_auto_scroll(mut self, message: impl OnAutoScroll<'a, Message>) -> Self {
50 self.on_auto_scroll = Some(Box::new(message));
51 self
52 }
53
54 #[must_use]
56 pub fn on_drag(mut self, message: impl OnDrag<'a, Message>) -> Self {
57 self.on_drag = Some(Box::new(message));
58 self
59 }
60
61 #[must_use]
63 pub fn on_drag_end(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
64 self.on_drag_end = Some(Box::new(message));
65 self
66 }
67
68 #[must_use]
70 pub fn on_double_click(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
71 self.on_double_click = Some(Box::new(message));
72 self
73 }
74
75 #[must_use]
77 pub fn on_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
78 self.on_press = Some(Box::new(message));
79 self
80 }
81
82 #[must_use]
84 pub fn on_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
85 self.on_release = Some(Box::new(message));
86 self
87 }
88
89 #[must_use]
91 pub fn on_resize(mut self, message: impl OnResize<'a, Message>) -> Self {
92 self.on_resize = Some(Box::new(message));
93 self
94 }
95
96 #[must_use]
98 pub fn on_right_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
99 self.on_right_press = Some(Box::new(message));
100 self
101 }
102
103 #[must_use]
105 pub fn on_right_press_no_capture(mut self) -> Self {
106 self.on_right_press_no_capture = true;
107 self
108 }
109
110 #[must_use]
112 pub fn wayland_on_right_press_window_position(self) -> Self {
113 #[cfg(feature = "wayland")]
114 {
115 self.on_right_press_window_position = true;
116 }
117 self
118 }
119
120 #[must_use]
122 pub fn on_right_press_window_position(mut self) -> Self {
123 self.on_right_press_window_position = true;
124 self
125 }
126
127 #[must_use]
129 pub fn on_right_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
130 self.on_right_release = Some(Box::new(message));
131 self
132 }
133
134 #[must_use]
136 pub fn on_middle_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
137 self.on_middle_press = Some(Box::new(message));
138 self
139 }
140
141 #[must_use]
143 pub fn on_middle_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
144 self.on_middle_release = Some(Box::new(message));
145 self
146 }
147
148 #[must_use]
150 pub fn on_back_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
151 self.on_back_press = Some(Box::new(message));
152 self
153 }
154
155 #[must_use]
157 pub fn on_back_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
158 self.on_back_release = Some(Box::new(message));
159 self
160 }
161
162 #[must_use]
164 pub fn on_forward_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
165 self.on_forward_press = Some(Box::new(message));
166 self
167 }
168
169 #[must_use]
171 pub fn on_forward_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
172 self.on_forward_release = Some(Box::new(message));
173 self
174 }
175
176 #[must_use]
178 pub fn on_scroll(mut self, message: impl OnScroll<'a, Message>) -> Self {
179 self.on_scroll = Some(Box::new(message));
180 self
181 }
182
183 #[must_use]
185 pub fn on_enter(mut self, message: impl OnEnterExit<'a, Message>) -> Self {
186 self.on_enter = Some(Box::new(message));
187 self
188 }
189
190 #[must_use]
192 pub fn on_exit(mut self, message: impl OnEnterExit<'a, Message>) -> Self {
193 self.on_exit = Some(Box::new(message));
194 self
195 }
196
197 #[must_use]
198 pub const fn show_drag_rect(mut self, show_drag_rect: bool) -> Self {
199 self.show_drag_rect = show_drag_rect;
200 self
201 }
202
203 #[must_use]
205 pub fn with_id(mut self, id: Id) -> Self {
206 self.id = id;
207 self
208 }
209}
210
211pub trait OnAutoScroll<'a, Message>: Fn(Option<f32>) -> Message + 'a {}
212impl<'a, Message, F> OnAutoScroll<'a, Message> for F where F: Fn(Option<f32>) -> Message + 'a {}
213
214pub trait OnMouseButton<'a, Message>: Fn(Option<Point>) -> Message + 'a {}
215impl<'a, Message, F> OnMouseButton<'a, Message> for F where F: Fn(Option<Point>) -> Message + 'a {}
216
217pub trait OnDrag<'a, Message>: Fn(Option<Rectangle>) -> Message + 'a {}
218impl<'a, Message, F> OnDrag<'a, Message> for F where F: Fn(Option<Rectangle>) -> Message + 'a {}
219
220pub trait OnResize<'a, Message>: Fn(Rectangle) -> Message + 'a {}
221impl<'a, Message, F> OnResize<'a, Message> for F where F: Fn(Rectangle) -> Message + 'a {}
222
223pub trait OnScroll<'a, Message>: Fn(mouse::ScrollDelta) -> Option<Message> + 'a {}
224impl<'a, Message, F> OnScroll<'a, Message> for F where
225 F: Fn(mouse::ScrollDelta) -> Option<Message> + 'a
226{
227}
228
229pub trait OnEnterExit<'a, Message>: Fn() -> Message + 'a {}
230impl<'a, Message, F> OnEnterExit<'a, Message> for F where F: Fn() -> Message + 'a {}
231
232#[derive(Default)]
234struct State {
235 last_auto_scroll: Option<f32>,
236 last_position: Option<Point>,
237 last_virtual_position: Option<Point>,
238 drag_initiated: Option<Point>,
239 prev_click: Option<(mouse::Click, Instant)>,
240 viewport: Option<Rectangle>,
241}
242
243impl State {
244 fn drag_rect(&self, cursor: mouse::Cursor) -> Option<Rectangle> {
245 if let Some(drag_source) = self.drag_initiated
246 && let Some(position) = cursor.position().or(self.last_virtual_position)
247 && position.distance(drag_source) > 1.0
248 {
249 let min_x = drag_source.x.min(position.x);
250 let max_x = drag_source.x.max(position.x);
251 let min_y = drag_source.y.min(position.y);
252 let max_y = drag_source.y.max(position.y);
253 return Some(Rectangle::new(
254 Point::new(min_x, min_y),
255 Size::new(max_x - min_x, max_y - min_y),
256 ));
257 }
258 None
259 }
260
261 fn click(&mut self, pos: Point) -> mouse::Click {
262 let now = Instant::now();
263
264 let new = if let Some((prev_click, prev_time)) = self.prev_click.take() {
265 if now.duration_since(prev_time) < DOUBLE_CLICK_DURATION {
266 match prev_click.kind() {
267 mouse::click::Kind::Single => {
268 mouse::Click::new(pos, mouse::Button::Left, Some(prev_click))
269 }
270 mouse::click::Kind::Double => {
271 mouse::Click::new(pos, mouse::Button::Left, Some(prev_click))
272 }
273 mouse::click::Kind::Triple => {
274 mouse::Click::new(pos, mouse::Button::Left, Some(prev_click))
275 }
276 }
277 } else {
278 mouse::Click::new(pos, mouse::Button::Left, None)
279 }
280 } else {
281 mouse::Click::new(pos, mouse::Button::Left, None)
282 };
283 self.prev_click = Some((new, now));
284 new
285 }
286}
287
288impl<'a, Message> MouseArea<'a, Message> {
289 pub fn new(content: impl Into<Element<'a, Message>>) -> Self {
291 MouseArea {
292 id: Id::unique(),
293 content: content.into(),
294 on_auto_scroll: None,
295 on_drag: None,
296 on_drag_end: None,
297 on_double_click: None,
298 on_press: None,
299 on_release: None,
300 on_resize: None,
301 on_right_press: None,
302 on_right_press_no_capture: false,
303 on_right_press_window_position: false,
304 on_right_release: None,
305 on_middle_press: None,
306 on_middle_release: None,
307 on_back_press: None,
308 on_back_release: None,
309 on_forward_press: None,
310 on_forward_release: None,
311 on_enter: None,
312 on_exit: None,
313 on_scroll: None,
314 show_drag_rect: false,
315 }
316 }
317}
318
319impl<Message> Widget<Message, Theme, Renderer> for MouseArea<'_, Message>
320where
321 Message: Clone,
322{
323 fn tag(&self) -> tree::Tag {
324 tree::Tag::of::<State>()
325 }
326
327 fn state(&self) -> tree::State {
328 tree::State::new(State::default())
329 }
330
331 fn children(&self) -> Vec<Tree> {
332 vec![Tree::new(&self.content)]
333 }
334
335 fn diff(&mut self, tree: &mut Tree) {
336 tree.diff_children(std::slice::from_mut(&mut self.content));
337 }
338
339 fn size(&self) -> Size<Length> {
340 self.content.as_widget().size()
341 }
342
343 fn layout(
344 &mut self,
345 tree: &mut Tree,
346 renderer: &Renderer,
347 limits: &layout::Limits,
348 ) -> layout::Node {
349 self.content
350 .as_widget_mut()
351 .layout(&mut tree.children[0], renderer, limits)
352 }
353
354 fn operate(
355 &mut self,
356 tree: &mut Tree,
357 layout: Layout<'_>,
358 renderer: &Renderer,
359 operation: &mut dyn Operation,
360 ) {
361 self.content
362 .as_widget_mut()
363 .operate(&mut tree.children[0], layout, renderer, operation);
364 }
365
366 fn update(
367 &mut self,
368 tree: &mut Tree,
369 event: &Event,
370 layout: Layout<'_>,
371 cursor: mouse::Cursor,
372 renderer: &Renderer,
373 clipboard: &mut dyn Clipboard,
374 shell: &mut Shell<'_, Message>,
375 viewport: &Rectangle,
376 ) {
377 self.content.as_widget_mut().update(
378 &mut tree.children[0],
379 event,
380 layout,
381 cursor,
382 renderer,
383 clipboard,
384 shell,
385 viewport,
386 );
387
388 if shell.is_event_captured() {
389 return;
390 }
391
392 update(
393 self,
394 event,
395 layout,
396 cursor,
397 shell,
398 tree.state.downcast_mut::<State>(),
399 viewport,
400 );
401 }
402
403 fn mouse_interaction(
404 &self,
405 tree: &Tree,
406 layout: Layout<'_>,
407 cursor: mouse::Cursor,
408 viewport: &Rectangle,
409 renderer: &Renderer,
410 ) -> mouse::Interaction {
411 self.content.as_widget().mouse_interaction(
412 &tree.children[0],
413 layout,
414 cursor,
415 viewport,
416 renderer,
417 )
418 }
419
420 fn draw(
421 &self,
422 tree: &Tree,
423 renderer: &mut Renderer,
424 theme: &Theme,
425 renderer_style: &renderer::Style,
426 layout: Layout<'_>,
427 cursor: mouse::Cursor,
428 viewport: &Rectangle,
429 ) {
430 self.content.as_widget().draw(
431 &tree.children[0],
432 renderer,
433 theme,
434 renderer_style,
435 layout,
436 cursor,
437 viewport,
438 );
439
440 if self.show_drag_rect {
441 let state = tree.state.downcast_ref::<State>();
442 if let Some(bounds) = state.drag_rect(cursor) {
443 let cosmic = theme.cosmic();
444 let mut bg_color = cosmic.accent_color();
445 bg_color.alpha = 0.2;
447 renderer.start_layer(*viewport);
448 renderer.fill_quad(
449 Quad {
450 bounds,
451 border: Border {
452 color: cosmic.accent_color().into(),
453 width: 1.0,
454 radius: cosmic.radius_xs().into(),
455 },
456 ..Default::default()
457 },
458 Color::from(bg_color),
459 );
460 renderer.end_layer();
461 }
462 }
463 }
464
465 fn overlay<'b>(
466 &'b mut self,
467 tree: &'b mut Tree,
468 layout: Layout<'b>,
469 renderer: &Renderer,
470 viewport: &Rectangle,
471 translation: Vector,
472 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
473 self.content.as_widget_mut().overlay(
474 &mut tree.children[0],
475 layout,
476 renderer,
477 viewport,
478 translation,
479 )
480 }
481
482 fn drag_destinations(
483 &self,
484 state: &Tree,
485 layout: Layout<'_>,
486 renderer: &Renderer,
487 dnd_rectangles: &mut cosmic::iced::core::clipboard::DndDestinationRectangles,
488 ) {
489 self.content.as_widget().drag_destinations(
490 &state.children[0],
491 layout,
492 renderer,
493 dnd_rectangles,
494 );
495 }
496
497 fn id(&self) -> Option<Id> {
498 Some(self.id.clone())
499 }
500
501 fn set_id(&mut self, id: Id) {
502 self.id = id;
503 }
504}
505
506impl<'a, Message> From<MouseArea<'a, Message>> for Element<'a, Message>
507where
508 Message: 'a + Clone,
509 Renderer: 'a + renderer::Renderer,
510 Theme: 'a,
511{
512 fn from(area: MouseArea<'a, Message>) -> Self {
513 Element::new(area)
514 }
515}
516
517fn update<Message: Clone>(
520 widget: &mut MouseArea<'_, Message>,
521 event: &Event,
522 layout: Layout<'_>,
523 cursor: mouse::Cursor,
524 shell: &mut Shell<'_, Message>,
525 state: &mut State,
526 viewport: &Rectangle,
527) {
528 let offset = layout.virtual_offset();
529 let layout_bounds = layout.bounds();
530
531 let viewport_changed = state.viewport != Some(*viewport);
532
533 if let Some(message) = widget.on_resize.as_ref()
534 && viewport_changed
535 {
536 shell.publish(message(*viewport));
537 }
538
539 state.viewport = Some(*viewport);
540
541 let should_check_hover = viewport_changed
542 || matches!(
543 event,
544 Event::Mouse(mouse::Event::CursorMoved { .. })
545 | Event::Mouse(mouse::Event::WheelScrolled { .. })
546 );
547
548 if should_check_hover {
549 let position_in = cursor.position_in(layout_bounds);
550 match (position_in, state.last_position) {
551 (None, Some(_)) => {
552 if let Some(message) = widget.on_exit.as_ref() {
553 shell.publish(message());
554 }
555 }
556 (Some(_), None) => {
557 if let Some(message) = widget.on_enter.as_ref() {
558 shell.publish(message());
559 }
560 }
561 _ => {}
562 }
563 state.last_position = position_in;
564 }
565
566 if let Event::Mouse(mouse::Event::CursorMoved { position }) = event {
567 let virtual_position = Point::new(
568 viewport.x - layout_bounds.x + position.x,
569 viewport.y - layout_bounds.y + position.y,
570 );
571 state.last_virtual_position = Some(virtual_position);
572
573 if let Some(message) = widget.on_auto_scroll.as_ref() {
574 let auto_scroll = if state.drag_initiated.is_some() {
575 let bottom = viewport.y;
576 let top = viewport.y + viewport.height;
577 if virtual_position.y < bottom {
578 Some(virtual_position.y - bottom)
579 } else if virtual_position.y > top {
580 Some(virtual_position.y - top)
581 } else {
582 None
583 }
584 } else {
585 None
586 };
587 if state.last_auto_scroll != auto_scroll {
588 shell.publish(message(auto_scroll));
589 state.last_auto_scroll = auto_scroll;
590 }
591 }
592 }
593
594 if state.drag_initiated.is_none() && !cursor.is_over(layout_bounds) {
595 return;
596 }
597
598 if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
599 | Event::Touch(touch::Event::FingerPressed { .. }) = event
600 {
601 let click = state.click(cursor.position_in(layout_bounds).unwrap_or_default());
602 match click.kind() {
603 click::Kind::Single => {
604 if let Some(message) = widget.on_press.as_ref() {
605 shell.publish(message(cursor.position_in(layout_bounds)));
606 }
607 }
608 click::Kind::Double => {
609 if let Some(message) = widget.on_double_click.as_ref() {
610 shell.publish(message(cursor.position_in(layout_bounds)));
611 }
612 }
613 click::Kind::Triple => {
614 if let Some(message) = widget.on_press.as_ref() {
616 shell.publish(message(cursor.position_in(layout_bounds)));
617 }
618 }
619 }
620 if widget.on_drag.is_some() {
621 state.drag_initiated = cursor.position();
622 }
623
624 if widget.on_press.is_some() {
625 shell.capture_event();
626 return;
627 }
628 }
629
630 let distance_dragged = state
631 .drag_initiated
632 .map(|initiated| initiated.distance(cursor.position().unwrap_or_default()))
633 .unwrap_or_default();
634 if matches!(
635 event,
636 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
637 | Event::Touch(touch::Event::FingerLifted { .. })
638 ) && distance_dragged > 1.0
639 {
640 state.drag_initiated = None;
641 state.prev_click = None;
642 if let Some(message) = widget.on_drag_end.as_ref() {
643 shell.publish(message(cursor.position_in(layout_bounds)));
644 }
645 }
646
647 let recent_click = state
648 .prev_click
649 .as_ref()
650 .is_some_and(|(_, i)| Instant::now().duration_since(*i) <= DOUBLE_CLICK_DURATION);
651 if matches!(
652 event,
653 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
654 | Event::Touch(touch::Event::FingerLifted { .. })
655 ) && state.prev_click.is_some()
656 {
657 if !recent_click {
658 state.prev_click = None;
659 return;
660 }
661 state.drag_initiated = None;
662 if let Some(message) = widget.on_release.as_ref() {
663 shell.publish(message(cursor.position_in(layout_bounds)));
664
665 shell.capture_event();
666 return;
667 }
668 }
669
670 if let Some(message) = widget.on_right_press.as_ref()
671 && matches!(
672 event,
673 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right))
674 )
675 {
676 let point_opt = if widget.on_right_press_window_position {
677 cursor.position_over(layout_bounds).map(|mut p| {
678 p.x -= offset.x;
679 p.y -= offset.y;
680 p
681 })
682 } else {
683 cursor.position_in(layout_bounds)
684 };
685 shell.publish(message(point_opt));
686
687 if widget.on_right_press_no_capture {
688 return;
689 }
690 shell.capture_event();
691 return;
692 }
693
694 if let Some(message) = widget.on_right_release.as_ref()
695 && matches!(
696 event,
697 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right))
698 )
699 {
700 shell.publish(message(cursor.position_in(layout_bounds)));
701
702 shell.capture_event();
703 return;
704 }
705
706 if let Some(message) = widget.on_middle_press.as_ref()
707 && matches!(
708 event,
709 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle))
710 )
711 {
712 shell.publish(message(cursor.position_in(layout_bounds)));
713
714 shell.capture_event();
715 return;
716 }
717
718 if let Some(message) = widget.on_middle_release.as_ref()
719 && matches!(
720 event,
721 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle))
722 )
723 {
724 shell.publish(message(cursor.position_in(layout_bounds)));
725
726 shell.capture_event();
727 return;
728 }
729
730 if let Some(message) = widget.on_back_press.as_ref()
731 && matches!(
732 event,
733 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Back))
734 )
735 {
736 shell.publish(message(cursor.position_in(layout_bounds)));
737
738 shell.capture_event();
739 return;
740 }
741
742 if let Some(message) = widget.on_back_release.as_ref()
743 && matches!(
744 event,
745 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Back))
746 )
747 {
748 shell.publish(message(cursor.position_in(layout_bounds)));
749
750 shell.capture_event();
751 return;
752 }
753
754 if let Some(message) = widget.on_forward_press.as_ref()
755 && matches!(
756 event,
757 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Forward))
758 )
759 {
760 shell.publish(message(cursor.position_in(layout_bounds)));
761
762 shell.capture_event();
763 return;
764 }
765
766 if let Some(message) = widget.on_forward_release.as_ref()
767 && matches!(
768 event,
769 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Forward))
770 )
771 {
772 shell.publish(message(cursor.position_in(layout_bounds)));
773
774 shell.capture_event();
775 return;
776 }
777
778 if let Some(on_scroll) = widget.on_scroll.as_ref()
779 && let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event
780 && let Some(message) = on_scroll(*delta)
781 {
782 shell.publish(message);
783 shell.capture_event();
784 return;
785 }
786
787 if let Some((message, drag_rect)) = widget.on_drag.as_ref().zip(state.drag_rect(cursor)) {
788 shell.publish(message(drag_rect.intersection(&layout_bounds).map(
789 |mut rect| {
790 rect.x -= layout_bounds.x;
791 rect.y -= layout_bounds.y;
792 rect
793 },
794 )));
795 }
796}