Skip to main content

fret_ui_kit/
viewport_tooling.rs

1//! Viewport tool arbitration helpers (Tier A embedding).
2//!
3//! This module provides a small, policy-heavy router for editor-style viewport tooling:
4//! - gizmos,
5//! - selection tools,
6//! - camera navigation tools.
7//!
8//! It is built on top of the policy-light protocol types in `fret-viewport-tooling` (ADR 0153).
9
10use std::cmp::Reverse;
11
12use fret_core::{MouseButton, ViewportInputEvent, ViewportInputKind};
13use fret_viewport_tooling::{
14    ViewportTool, ViewportToolCx, ViewportToolId, ViewportToolInput, ViewportToolPriority,
15    ViewportToolResult,
16};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum ViewportToolCoordinateSpace {
20    /// Use render-target pixels for cursor coordinates (recommended for 3D gizmos).
21    #[default]
22    TargetPx,
23    /// Use window logical pixels for cursor coordinates (useful for HUD-ish tools).
24    ScreenPx,
25}
26
27#[derive(Debug, Clone, Copy)]
28pub struct ViewportToolArbitratorConfig {
29    pub primary_button: MouseButton,
30    pub coordinate_space: ViewportToolCoordinateSpace,
31}
32
33impl Default for ViewportToolArbitratorConfig {
34    fn default() -> Self {
35        Self {
36            primary_button: MouseButton::Left,
37            coordinate_space: ViewportToolCoordinateSpace::TargetPx,
38        }
39    }
40}
41
42#[derive(Default)]
43pub struct ViewportToolArbitrator {
44    pub config: ViewportToolArbitratorConfig,
45    tools: Vec<Box<dyn ViewportTool>>,
46    hot: Option<ViewportToolId>,
47    active: Option<ViewportToolId>,
48    active_button: Option<MouseButton>,
49    active_pointer_id: Option<fret_core::PointerId>,
50}
51
52#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
53pub struct ViewportToolRouterState {
54    pub hot: Option<ViewportToolId>,
55    pub active: Option<ViewportToolId>,
56    pub active_button: Option<MouseButton>,
57    pub active_pointer_id: Option<fret_core::PointerId>,
58}
59
60pub struct ViewportToolEntry<T> {
61    pub id: ViewportToolId,
62    pub priority: ViewportToolPriority,
63    pub set_hot: Option<fn(&mut T, bool)>,
64    pub hit_test: fn(&mut T, ViewportToolCx<'_>) -> bool,
65    pub handle_event: fn(&mut T, ViewportToolCx<'_>, bool, bool) -> ViewportToolResult,
66    pub cancel: Option<fn(&mut T)>,
67}
68
69/// Cancels the active tool interaction (if any) and clears router hot/active state.
70///
71/// This is intended for keyboard-driven cancellation (e.g. Escape) or host-driven teardown
72/// (switching tool modes, closing a viewport, etc.).
73pub fn cancel_active_viewport_tools<T>(
74    state: &mut ViewportToolRouterState,
75    host: &mut T,
76    tools: &mut [ViewportToolEntry<T>],
77) -> bool {
78    let Some(active) = state.active else {
79        return false;
80    };
81
82    if let Some(entry) = tools.iter_mut().find(|t| t.id == active)
83        && let Some(cancel) = entry.cancel
84    {
85        cancel(host);
86    }
87
88    if let Some(hot) = state.hot
89        && let Some(entry) = tools.iter_mut().find(|t| t.id == hot)
90    {
91        call_set_hot(host, entry, false);
92    }
93
94    state.hot = None;
95    state.active = None;
96    state.active_button = None;
97    state.active_pointer_id = None;
98    true
99}
100
101pub fn route_viewport_tools<T>(
102    state: &mut ViewportToolRouterState,
103    config: ViewportToolArbitratorConfig,
104    host: &mut T,
105    event: &ViewportInputEvent,
106    tools: &mut [ViewportToolEntry<T>],
107) -> bool {
108    if tools.is_empty() {
109        state.hot = None;
110        state.active = None;
111        state.active_button = None;
112        state.active_pointer_id = None;
113        return false;
114    }
115
116    tools.sort_by_key(|t| Reverse(t.priority.0));
117
118    if let ViewportInputKind::PointerCancel { .. } = event.kind {
119        let active_pointer_matches = state
120            .active_pointer_id
121            .is_none_or(|p| p == event.pointer_id);
122        if state.active.is_some() && active_pointer_matches {
123            return cancel_active_viewport_tools(state, host, tools);
124        }
125        return false;
126    }
127
128    let input = derive_input(config, state.active_button, event);
129    let cx = ViewportToolCx { event, input };
130
131    if let Some(active) = state.active {
132        if let Some(active_pointer_id) = state.active_pointer_id
133            && active_pointer_id != event.pointer_id
134        {
135            return false;
136        }
137        if state.active_pointer_id.is_none() {
138            state.active_pointer_id = Some(event.pointer_id);
139        }
140
141        force_hot(state, host, tools, active);
142        let handled = if let Some(entry) = tools.iter_mut().find(|t| t.id == active) {
143            (entry.handle_event)(host, cx, true, true).handled
144        } else {
145            false
146        };
147
148        if !cx.input.dragging {
149            state.active = None;
150            state.active_button = None;
151            state.active_pointer_id = None;
152        }
153        return handled;
154    }
155
156    match event.kind {
157        ViewportInputKind::PointerMove { .. }
158        | ViewportInputKind::PointerDown { .. }
159        | ViewportInputKind::Wheel { .. } => update_hot(state, host, tools, cx),
160        ViewportInputKind::PointerUp { .. } => {}
161        ViewportInputKind::PointerCancel { .. } => {}
162    }
163
164    match event.kind {
165        ViewportInputKind::PointerDown { .. } => dispatch_pointer_down(state, host, tools, cx),
166        ViewportInputKind::PointerMove { .. } | ViewportInputKind::PointerUp { .. } => {
167            dispatch_hot_only(state, host, tools, cx)
168        }
169        ViewportInputKind::Wheel { .. } => dispatch_wheel(state, host, tools, cx),
170        ViewportInputKind::PointerCancel { .. } => false,
171    }
172}
173
174fn derive_input(
175    config: ViewportToolArbitratorConfig,
176    active_button: Option<MouseButton>,
177    event: &ViewportInputEvent,
178) -> ViewportToolInput {
179    let primary_button = active_button.unwrap_or(config.primary_button);
180    let mut input = match config.coordinate_space {
181        ViewportToolCoordinateSpace::TargetPx => {
182            ViewportToolInput::from_viewport_input_target_px(event, primary_button)
183        }
184        ViewportToolCoordinateSpace::ScreenPx => {
185            ViewportToolInput::from_viewport_input_screen_px(event, primary_button)
186        }
187    };
188
189    // Some platforms can produce inconsistent `buttons` state for move events. When a tool is
190    // active we want to keep it latched until an explicit `PointerUp` arrives.
191    if active_button.is_some() && matches!(event.kind, ViewportInputKind::PointerMove { .. }) {
192        input.dragging = true;
193    }
194
195    input
196}
197
198fn call_set_hot<T>(host: &mut T, entry: &mut ViewportToolEntry<T>, hot: bool) {
199    if let Some(f) = entry.set_hot {
200        f(host, hot);
201    }
202}
203
204fn force_hot<T>(
205    state: &mut ViewportToolRouterState,
206    host: &mut T,
207    tools: &mut [ViewportToolEntry<T>],
208    id: ViewportToolId,
209) {
210    if state.hot == Some(id) {
211        return;
212    }
213
214    if let Some(old) = state.hot
215        && let Some(entry) = tools.iter_mut().find(|t| t.id == old)
216    {
217        call_set_hot(host, entry, false);
218    }
219
220    if let Some(entry) = tools.iter_mut().find(|t| t.id == id) {
221        call_set_hot(host, entry, true);
222        state.hot = Some(id);
223    } else {
224        state.hot = None;
225    }
226}
227
228fn update_hot<T>(
229    state: &mut ViewportToolRouterState,
230    host: &mut T,
231    tools: &mut [ViewportToolEntry<T>],
232    cx: ViewportToolCx<'_>,
233) {
234    let mut next_hot = None;
235    for tool in tools.iter_mut() {
236        if (tool.hit_test)(host, cx) {
237            next_hot = Some(tool.id);
238            break;
239        }
240    }
241
242    if next_hot == state.hot {
243        return;
244    }
245
246    if let Some(old) = state.hot
247        && let Some(entry) = tools.iter_mut().find(|t| t.id == old)
248    {
249        call_set_hot(host, entry, false);
250    }
251    if let Some(next) = next_hot
252        && let Some(entry) = tools.iter_mut().find(|t| t.id == next)
253    {
254        call_set_hot(host, entry, true);
255        state.hot = Some(next);
256    } else {
257        state.hot = None;
258    }
259}
260
261fn dispatch_pointer_down<T>(
262    state: &mut ViewportToolRouterState,
263    host: &mut T,
264    tools: &mut [ViewportToolEntry<T>],
265    cx: ViewportToolCx<'_>,
266) -> bool {
267    let down_button = match cx.event.kind {
268        ViewportInputKind::PointerDown { button, .. } => Some(button),
269        _ => None,
270    };
271    for tool in tools.iter_mut() {
272        let id = tool.id;
273        let hot = state.hot == Some(id);
274        let res = (tool.handle_event)(host, cx, hot, false);
275        if !res.handled {
276            continue;
277        }
278
279        if res.capture {
280            state.active = Some(id);
281            state.active_button = down_button;
282            state.active_pointer_id = Some(cx.event.pointer_id);
283            force_hot(state, host, tools, id);
284        }
285        return true;
286    }
287    false
288}
289
290fn dispatch_hot_only<T>(
291    state: &mut ViewportToolRouterState,
292    host: &mut T,
293    tools: &mut [ViewportToolEntry<T>],
294    cx: ViewportToolCx<'_>,
295) -> bool {
296    let Some(hot) = state.hot else {
297        return false;
298    };
299    let Some(entry) = tools.iter_mut().find(|t| t.id == hot) else {
300        state.hot = None;
301        return false;
302    };
303    (entry.handle_event)(host, cx, true, false).handled
304}
305
306fn dispatch_wheel<T>(
307    state: &mut ViewportToolRouterState,
308    host: &mut T,
309    tools: &mut [ViewportToolEntry<T>],
310    cx: ViewportToolCx<'_>,
311) -> bool {
312    if let Some(hot) = state.hot
313        && let Some(entry) = tools.iter_mut().find(|t| t.id == hot)
314        && (entry.handle_event)(host, cx, true, false).handled
315    {
316        return true;
317    }
318
319    for tool in tools.iter_mut() {
320        let id = tool.id;
321        if Some(id) == state.hot {
322            continue;
323        }
324        let res = (tool.handle_event)(host, cx, false, false);
325        if res.handled {
326            return true;
327        }
328    }
329    false
330}
331
332impl ViewportToolArbitrator {
333    pub fn new(config: ViewportToolArbitratorConfig) -> Self {
334        Self {
335            config,
336            tools: Vec::new(),
337            hot: None,
338            active: None,
339            active_button: None,
340            active_pointer_id: None,
341        }
342    }
343
344    pub fn tools_mut(&mut self) -> &mut [Box<dyn ViewportTool>] {
345        &mut self.tools
346    }
347
348    pub fn hot_tool(&self) -> Option<ViewportToolId> {
349        self.hot
350    }
351
352    pub fn active_tool(&self) -> Option<ViewportToolId> {
353        self.active
354    }
355
356    pub fn set_tools(&mut self, tools: impl IntoIterator<Item = Box<dyn ViewportTool>>) {
357        let mut tools: Vec<Box<dyn ViewportTool>> = tools.into_iter().collect();
358        tools.sort_by_key(|t| Reverse(t.priority().0));
359        self.tools = tools;
360        self.hot = None;
361        self.active = None;
362        self.active_button = None;
363        self.active_pointer_id = None;
364    }
365
366    pub fn clear_tools(&mut self) {
367        self.tools.clear();
368        self.hot = None;
369        self.active = None;
370        self.active_button = None;
371        self.active_pointer_id = None;
372    }
373
374    pub fn cancel_active(&mut self) {
375        if let Some(active) = self.active
376            && let Some(idx) = self.index_of(active)
377        {
378            self.tools[idx].cancel();
379        }
380        self.active = None;
381        self.active_button = None;
382        self.active_pointer_id = None;
383    }
384
385    /// Cancels the active interaction (if any) and clears the hot tool.
386    pub fn cancel_active_and_clear_hot(&mut self) {
387        self.cancel_active();
388        if let Some(hot) = self.hot
389            && let Some(idx) = self.index_of(hot)
390        {
391            self.tools[idx].set_hot(false);
392        }
393        self.hot = None;
394    }
395
396    pub fn handle_event(&mut self, event: &ViewportInputEvent) -> bool {
397        if self.tools.is_empty() {
398            self.hot = None;
399            self.active = None;
400            self.active_button = None;
401            self.active_pointer_id = None;
402            return false;
403        }
404
405        if let ViewportInputKind::PointerCancel { .. } = event.kind {
406            if self
407                .active_pointer_id
408                .is_some_and(|p| p != event.pointer_id)
409            {
410                return false;
411            }
412            if self.active.is_some() {
413                self.cancel_active_and_clear_hot();
414                return true;
415            }
416            return false;
417        }
418
419        let input = self.derive_input(event);
420        let cx = ViewportToolCx { event, input };
421
422        if let Some(active) = self.active {
423            if let Some(active_pointer_id) = self.active_pointer_id
424                && active_pointer_id != event.pointer_id
425            {
426                return false;
427            }
428            if self.active_pointer_id.is_none() {
429                self.active_pointer_id = Some(event.pointer_id);
430            }
431
432            self.force_hot(active);
433            let handled = if let Some(idx) = self.index_of(active) {
434                self.tools[idx].handle_event(cx, true, true).handled
435            } else {
436                false
437            };
438
439            if !cx.input.dragging {
440                self.active = None;
441                self.active_button = None;
442                self.active_pointer_id = None;
443            }
444            return handled;
445        }
446
447        match event.kind {
448            ViewportInputKind::PointerMove { .. } | ViewportInputKind::PointerDown { .. } => {
449                self.update_hot(cx);
450            }
451            ViewportInputKind::PointerUp { .. } => {}
452            ViewportInputKind::Wheel { .. } => {
453                self.update_hot(cx);
454            }
455            ViewportInputKind::PointerCancel { .. } => {}
456        }
457
458        match event.kind {
459            ViewportInputKind::PointerDown { .. } => self.dispatch_pointer_down(cx),
460            ViewportInputKind::PointerMove { .. } => self.dispatch_hot_only(cx),
461            ViewportInputKind::PointerUp { .. } => self.dispatch_hot_only(cx),
462            ViewportInputKind::Wheel { .. } => self.dispatch_wheel(cx),
463            ViewportInputKind::PointerCancel { .. } => false,
464        }
465    }
466
467    fn derive_input(&self, event: &ViewportInputEvent) -> ViewportToolInput {
468        let primary_button = self.active_button.unwrap_or(self.config.primary_button);
469        let mut input = match self.config.coordinate_space {
470            ViewportToolCoordinateSpace::TargetPx => {
471                ViewportToolInput::from_viewport_input_target_px(event, primary_button)
472            }
473            ViewportToolCoordinateSpace::ScreenPx => {
474                ViewportToolInput::from_viewport_input_screen_px(event, primary_button)
475            }
476        };
477
478        // Some platforms can produce inconsistent `buttons` state for move events. When a tool is
479        // active we want to keep it latched until an explicit `PointerUp` arrives.
480        if self.active_button.is_some()
481            && matches!(event.kind, ViewportInputKind::PointerMove { .. })
482        {
483            input.dragging = true;
484        }
485
486        input
487    }
488
489    fn index_of(&self, id: ViewportToolId) -> Option<usize> {
490        self.tools.iter().position(|t| t.id() == id)
491    }
492
493    fn force_hot(&mut self, id: ViewportToolId) {
494        if self.hot == Some(id) {
495            return;
496        }
497        if let Some(old) = self.hot
498            && let Some(idx) = self.index_of(old)
499        {
500            self.tools[idx].set_hot(false);
501        }
502        if let Some(idx) = self.index_of(id) {
503            self.tools[idx].set_hot(true);
504            self.hot = Some(id);
505        } else {
506            self.hot = None;
507        }
508    }
509
510    fn update_hot(&mut self, cx: ViewportToolCx<'_>) {
511        let mut next_hot = None;
512        for tool in &mut self.tools {
513            if tool.hit_test(cx) {
514                next_hot = Some(tool.id());
515                break;
516            }
517        }
518
519        if next_hot == self.hot {
520            return;
521        }
522
523        if let Some(old) = self.hot
524            && let Some(idx) = self.index_of(old)
525        {
526            self.tools[idx].set_hot(false);
527        }
528        if let Some(next) = next_hot
529            && let Some(idx) = self.index_of(next)
530        {
531            self.tools[idx].set_hot(true);
532            self.hot = Some(next);
533        } else {
534            self.hot = None;
535        }
536    }
537
538    fn dispatch_pointer_down(&mut self, cx: ViewportToolCx<'_>) -> bool {
539        let down_button = match cx.event.kind {
540            ViewportInputKind::PointerDown { button, .. } => Some(button),
541            _ => None,
542        };
543        for tool in &mut self.tools {
544            let id = tool.id();
545            let hot = self.hot == Some(id);
546            let res = tool.handle_event(cx, hot, false);
547            if !res.handled {
548                continue;
549            }
550
551            if res.capture {
552                self.active = Some(id);
553                self.active_button = down_button;
554                self.active_pointer_id = Some(cx.event.pointer_id);
555                self.force_hot(id);
556            }
557            return true;
558        }
559        false
560    }
561
562    fn dispatch_hot_only(&mut self, cx: ViewportToolCx<'_>) -> bool {
563        let Some(hot) = self.hot else {
564            return false;
565        };
566        let Some(idx) = self.index_of(hot) else {
567            self.hot = None;
568            return false;
569        };
570        self.tools[idx].handle_event(cx, true, false).handled
571    }
572
573    fn dispatch_wheel(&mut self, cx: ViewportToolCx<'_>) -> bool {
574        if let Some(hot) = self.hot
575            && let Some(idx) = self.index_of(hot)
576            && self.tools[idx].handle_event(cx, true, false).handled
577        {
578            return true;
579        }
580
581        for tool in &mut self.tools {
582            let id = tool.id();
583            if Some(id) == self.hot {
584                continue;
585            }
586            let res = tool.handle_event(cx, false, false);
587            if res.handled {
588                return true;
589            }
590        }
591        false
592    }
593}
594
595#[cfg(test)]
596mod tests {
597    use super::*;
598    use fret_core::geometry::{Px, Rect, Size};
599    use fret_core::{AppWindowId, Modifiers, RenderTargetId, ViewportFit, ViewportInputGeometry};
600    use fret_viewport_tooling::{ViewportToolPriority, ViewportToolResult};
601
602    fn dummy_event(kind: ViewportInputKind) -> ViewportInputEvent {
603        ViewportInputEvent {
604            window: AppWindowId::default(),
605            target: RenderTargetId::default(),
606            pointer_id: fret_core::PointerId(0),
607            pointer_type: fret_core::PointerType::Mouse,
608            geometry: ViewportInputGeometry {
609                content_rect_px: Rect::new(
610                    fret_core::geometry::Point::new(Px(0.0), Px(0.0)),
611                    Size::new(Px(100.0), Px(50.0)),
612                ),
613                draw_rect_px: Rect::new(
614                    fret_core::geometry::Point::new(Px(0.0), Px(0.0)),
615                    Size::new(Px(100.0), Px(50.0)),
616                ),
617                target_px_size: (1000, 500),
618                fit: ViewportFit::Stretch,
619                pixels_per_point: 2.0,
620            },
621            cursor_px: fret_core::geometry::Point::new(Px(10.0), Px(10.0)),
622            uv: (0.0, 0.0),
623            target_px: (0, 0),
624            kind,
625        }
626    }
627
628    struct TestTool {
629        id: ViewportToolId,
630        prio: i32,
631        hit: bool,
632        down_capture: bool,
633        down_handled: bool,
634        hot: bool,
635        cancelled: bool,
636        calls: Vec<&'static str>,
637    }
638
639    impl TestTool {
640        fn new(id: u64, prio: i32) -> Self {
641            Self {
642                id: ViewportToolId(id),
643                prio,
644                hit: false,
645                down_capture: false,
646                down_handled: false,
647                hot: false,
648                cancelled: false,
649                calls: Vec::new(),
650            }
651        }
652    }
653
654    impl ViewportTool for TestTool {
655        fn id(&self) -> ViewportToolId {
656            self.id
657        }
658
659        fn priority(&self) -> ViewportToolPriority {
660            ViewportToolPriority(self.prio)
661        }
662
663        fn set_hot(&mut self, hot: bool) {
664            self.hot = hot;
665            self.calls.push(if hot { "hot_on" } else { "hot_off" });
666        }
667
668        fn hit_test(&mut self, _cx: ViewportToolCx<'_>) -> bool {
669            self.calls.push("hit_test");
670            self.hit
671        }
672
673        fn handle_event(
674            &mut self,
675            cx: ViewportToolCx<'_>,
676            hot: bool,
677            active: bool,
678        ) -> ViewportToolResult {
679            match cx.event.kind {
680                ViewportInputKind::PointerDown { .. } => {
681                    self.calls.push(if hot { "down_hot" } else { "down_cold" });
682                    if self.down_handled {
683                        if self.down_capture {
684                            return ViewportToolResult::handled_and_capture();
685                        }
686                        return ViewportToolResult::handled();
687                    }
688                }
689                ViewportInputKind::PointerMove { .. } => {
690                    self.calls.push(if hot { "move_hot" } else { "move_cold" });
691                }
692                ViewportInputKind::PointerUp { .. } => {
693                    self.calls
694                        .push(if active { "up_active" } else { "up_inactive" });
695                }
696                ViewportInputKind::Wheel { .. } => {
697                    self.calls
698                        .push(if hot { "wheel_hot" } else { "wheel_cold" });
699                }
700                ViewportInputKind::PointerCancel { .. } => {
701                    self.calls.push("pointer_cancel");
702                }
703            }
704            ViewportToolResult::unhandled()
705        }
706
707        fn cancel(&mut self) {
708            self.cancelled = true;
709            self.calls.push("cancel");
710        }
711    }
712
713    #[test]
714    fn picks_hot_by_priority_and_clears_previous() {
715        let mut a = TestTool::new(1, 10);
716        a.hit = false;
717        let mut b = TestTool::new(2, 0);
718        b.hit = true;
719
720        let mut arb = ViewportToolArbitrator::new(Default::default());
721        arb.set_tools(vec![
722            Box::new(a) as Box<dyn ViewportTool>,
723            Box::new(b) as Box<dyn ViewportTool>,
724        ]);
725
726        let handled = arb.handle_event(&dummy_event(ViewportInputKind::PointerMove {
727            buttons: Default::default(),
728            modifiers: Modifiers::default(),
729        }));
730        assert!(!handled);
731        assert_eq!(arb.hot_tool(), Some(ViewportToolId(2)));
732    }
733
734    #[test]
735    fn pointer_down_captures_and_routes_followup_to_active_only() {
736        let mut a = TestTool::new(1, 10);
737        a.hit = true;
738        a.down_handled = true;
739        a.down_capture = true;
740        let mut b = TestTool::new(2, 0);
741        b.hit = true;
742        b.down_handled = true;
743        b.down_capture = true;
744
745        let mut arb = ViewportToolArbitrator::new(Default::default());
746        arb.set_tools(vec![
747            Box::new(a) as Box<dyn ViewportTool>,
748            Box::new(b) as Box<dyn ViewportTool>,
749        ]);
750
751        assert!(
752            arb.handle_event(&dummy_event(ViewportInputKind::PointerDown {
753                button: MouseButton::Left,
754                modifiers: Modifiers::default(),
755                click_count: 1,
756            }))
757        );
758        assert_eq!(arb.active_tool(), Some(ViewportToolId(1)));
759
760        let _ = arb.handle_event(&dummy_event(ViewportInputKind::PointerUp {
761            button: MouseButton::Left,
762            modifiers: Modifiers::default(),
763            is_click: true,
764            click_count: 1,
765        }));
766        assert_eq!(arb.active_tool(), None);
767    }
768
769    #[test]
770    fn cancel_active_and_clear_hot_resets_state() {
771        let mut a = TestTool::new(1, 10);
772        a.hit = true;
773        a.down_handled = true;
774        a.down_capture = true;
775
776        let mut arb = ViewportToolArbitrator::new(Default::default());
777        arb.set_tools(vec![Box::new(a) as Box<dyn ViewportTool>]);
778
779        assert!(
780            arb.handle_event(&dummy_event(ViewportInputKind::PointerDown {
781                button: MouseButton::Left,
782                modifiers: Modifiers::default(),
783                click_count: 1,
784            }))
785        );
786        assert_eq!(arb.active_tool(), Some(ViewportToolId(1)));
787        assert_eq!(arb.hot_tool(), Some(ViewportToolId(1)));
788
789        arb.cancel_active_and_clear_hot();
790        assert_eq!(arb.active_tool(), None);
791        assert_eq!(arb.hot_tool(), None);
792    }
793
794    #[test]
795    fn callback_router_cancel_clears_active_and_hot() {
796        #[derive(Default)]
797        struct Host {
798            cancelled: bool,
799            hot: bool,
800        }
801
802        fn set_hot(host: &mut Host, hot: bool) {
803            host.hot = hot;
804        }
805
806        fn hit_test(_host: &mut Host, _cx: ViewportToolCx<'_>) -> bool {
807            false
808        }
809
810        fn handle_event(
811            _host: &mut Host,
812            _cx: ViewportToolCx<'_>,
813            _hot: bool,
814            _active: bool,
815        ) -> ViewportToolResult {
816            ViewportToolResult::unhandled()
817        }
818
819        fn cancel(host: &mut Host) {
820            host.cancelled = true;
821        }
822
823        let mut host = Host::default();
824        let mut state = ViewportToolRouterState {
825            hot: Some(ViewportToolId(1)),
826            active: Some(ViewportToolId(1)),
827            active_button: Some(MouseButton::Left),
828            active_pointer_id: Some(fret_core::PointerId(0)),
829        };
830        let mut tools = [ViewportToolEntry {
831            id: ViewportToolId(1),
832            priority: ViewportToolPriority(0),
833            set_hot: Some(set_hot),
834            hit_test,
835            handle_event,
836            cancel: Some(cancel),
837        }];
838
839        let cancelled = cancel_active_viewport_tools(&mut state, &mut host, &mut tools);
840        assert!(cancelled);
841        assert!(host.cancelled);
842        assert!(!host.hot);
843        assert_eq!(state.hot, None);
844        assert_eq!(state.active, None);
845        assert_eq!(state.active_button, None);
846        assert_eq!(state.active_pointer_id, None);
847    }
848
849    #[test]
850    fn callback_router_active_tool_is_pointer_local() {
851        #[derive(Default)]
852        struct Host {
853            moves_active: u32,
854        }
855
856        fn set_hot(_host: &mut Host, _hot: bool) {}
857        fn hit_test(_host: &mut Host, _cx: ViewportToolCx<'_>) -> bool {
858            true
859        }
860        fn handle_event(
861            host: &mut Host,
862            cx: ViewportToolCx<'_>,
863            _hot: bool,
864            active: bool,
865        ) -> ViewportToolResult {
866            match cx.event.kind {
867                ViewportInputKind::PointerDown { .. } => ViewportToolResult::handled_and_capture(),
868                ViewportInputKind::PointerMove { .. } if active => {
869                    host.moves_active += 1;
870                    ViewportToolResult::handled()
871                }
872                _ => ViewportToolResult::unhandled(),
873            }
874        }
875
876        let mut host = Host::default();
877        let mut state = ViewportToolRouterState::default();
878        let mut tools = [ViewportToolEntry {
879            id: ViewportToolId(1),
880            priority: ViewportToolPriority(0),
881            set_hot: Some(set_hot),
882            hit_test,
883            handle_event,
884            cancel: None,
885        }];
886
887        let mut down = dummy_event(ViewportInputKind::PointerDown {
888            button: MouseButton::Left,
889            modifiers: Modifiers::default(),
890            click_count: 1,
891        });
892        down.pointer_id = fret_core::PointerId(0);
893        assert!(route_viewport_tools(
894            &mut state,
895            Default::default(),
896            &mut host,
897            &down,
898            &mut tools
899        ));
900        assert_eq!(state.active, Some(ViewportToolId(1)));
901        assert_eq!(state.active_pointer_id, Some(fret_core::PointerId(0)));
902
903        let mut move_other = dummy_event(ViewportInputKind::PointerMove {
904            buttons: fret_core::MouseButtons::default(),
905            modifiers: Modifiers::default(),
906        });
907        move_other.pointer_id = fret_core::PointerId(1);
908        assert!(!route_viewport_tools(
909            &mut state,
910            Default::default(),
911            &mut host,
912            &move_other,
913            &mut tools
914        ));
915        assert_eq!(host.moves_active, 0);
916        assert_eq!(state.active, Some(ViewportToolId(1)));
917
918        let mut move_active = move_other;
919        move_active.pointer_id = fret_core::PointerId(0);
920        assert!(route_viewport_tools(
921            &mut state,
922            Default::default(),
923            &mut host,
924            &move_active,
925            &mut tools
926        ));
927        assert_eq!(host.moves_active, 1);
928    }
929
930    #[test]
931    fn arbitrator_active_tool_is_pointer_local() {
932        let mut a = TestTool::new(1, 0);
933        a.hit = true;
934        a.down_handled = true;
935        a.down_capture = true;
936
937        let mut arb = ViewportToolArbitrator::new(Default::default());
938        arb.set_tools(vec![Box::new(a) as Box<dyn ViewportTool>]);
939
940        let mut down = dummy_event(ViewportInputKind::PointerDown {
941            button: MouseButton::Left,
942            modifiers: Modifiers::default(),
943            click_count: 1,
944        });
945        down.pointer_id = fret_core::PointerId(0);
946        assert!(arb.handle_event(&down));
947        assert_eq!(arb.active_tool(), Some(ViewportToolId(1)));
948
949        let mut move_other = dummy_event(ViewportInputKind::PointerMove {
950            buttons: fret_core::MouseButtons::default(),
951            modifiers: Modifiers::default(),
952        });
953        move_other.pointer_id = fret_core::PointerId(1);
954        assert!(!arb.handle_event(&move_other));
955        assert_eq!(arb.active_tool(), Some(ViewportToolId(1)));
956    }
957}