Skip to main content

fret_core/input/
viewport.rs

1use crate::{AppWindowId, PointerId, RenderTargetId, ViewportFit, ViewportMapping};
2
3use super::{Modifiers, MouseButton, MouseButtons, PointerCancelReason, PointerType};
4use crate::geometry::{Point, Rect};
5
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub struct ViewportInputGeometry {
8    /// The viewport widget bounds in window-local logical pixels (ADR 0017).
9    pub content_rect_px: Rect,
10    /// The mapped draw rect in window-local logical pixels after applying the viewport `fit`.
11    pub draw_rect_px: Rect,
12    /// The backing render target size in physical pixels.
13    pub target_px_size: (u32, u32),
14    pub fit: ViewportFit,
15    /// Pixels-per-point (a.k.a. window scale factor) used to convert logical px → physical px.
16    pub pixels_per_point: f32,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct ViewportInputEvent {
21    pub window: AppWindowId,
22    pub target: RenderTargetId,
23    pub pointer_id: PointerId,
24    pub pointer_type: PointerType,
25    pub geometry: ViewportInputGeometry,
26    /// Cursor position in window-local logical pixels (ADR 0017).
27    pub cursor_px: Point,
28    pub uv: (f32, f32),
29    pub target_px: (u32, u32),
30    pub kind: ViewportInputKind,
31}
32
33impl ViewportInputEvent {
34    /// Returns the scale from window-local logical pixels ("screen px") to render-target pixels.
35    ///
36    /// This is derived from `self.geometry.draw_rect_px` (logical pixels) and the backing render
37    /// target size `self.geometry.target_px_size` (physical pixels).
38    ///
39    /// For `ViewportFit::Contain`/`Cover` this is uniform; for `ViewportFit::Stretch` the mapping
40    /// is non-uniform, so this returns the smaller axis scale as a conservative approximation for
41    /// isotropic thresholds (hit radii, click distances).
42    pub fn target_px_per_screen_px(&self) -> Option<f32> {
43        let (tw, th) = self.geometry.target_px_size;
44        let tw = tw.max(1) as f32;
45        let th = th.max(1) as f32;
46
47        let rect = self.geometry.draw_rect_px;
48        let dw = rect.size.width.0.max(0.0);
49        let dh = rect.size.height.0.max(0.0);
50        if dw <= 0.0 || dh <= 0.0 || !dw.is_finite() || !dh.is_finite() {
51            return None;
52        }
53
54        let sx = tw / dw;
55        let sy = th / dh;
56        let s = sx.min(sy);
57        (s.is_finite() && s > 0.0).then_some(s)
58    }
59
60    /// Computes the cursor position in the viewport render target's pixel space (float).
61    ///
62    /// - Input `self.cursor_px` is in window-local logical pixels (ADR 0017).
63    /// - The mapping uses `self.geometry.draw_rect_px` (logical pixels) as the area that maps to
64    ///   the full render target.
65    /// - Output is expressed in physical target pixels (`self.geometry.target_px_size`).
66    ///
67    /// This is useful for editor tooling that operates directly on render-target pixel buffers.
68    /// Prefer this over reconstructing target coordinates from `uv * target_px_size` because `uv`
69    /// and `target_px` may be clamped when pointer capture is active.
70    pub fn cursor_target_px_f32(&self) -> Option<(f32, f32)> {
71        let (tw, th) = self.geometry.target_px_size;
72        let tw = tw.max(1) as f32;
73        let th = th.max(1) as f32;
74
75        let rect = self.geometry.draw_rect_px;
76        let dw = rect.size.width.0.max(0.0);
77        let dh = rect.size.height.0.max(0.0);
78        if dw <= 0.0 || dh <= 0.0 || !dw.is_finite() || !dh.is_finite() {
79            return None;
80        }
81
82        let uv_x = (self.cursor_px.x.0 - rect.origin.x.0) / dw;
83        let uv_y = (self.cursor_px.y.0 - rect.origin.y.0) / dh;
84        Some((uv_x * tw, uv_y * th))
85    }
86
87    /// Like [`Self::cursor_target_px_f32`], but clamps the resulting coordinates to the render
88    /// target bounds.
89    pub fn cursor_target_px_f32_clamped(&self) -> (f32, f32) {
90        let (tw, th) = self.geometry.target_px_size;
91        let tw = tw.max(1) as f32;
92        let th = th.max(1) as f32;
93
94        let Some((x, y)) = self.cursor_target_px_f32() else {
95            return (self.target_px.0 as f32, self.target_px.1 as f32);
96        };
97        (x.clamp(0.0, tw), y.clamp(0.0, th))
98    }
99
100    #[allow(clippy::too_many_arguments)]
101    pub fn from_mapping_window_point(
102        window: AppWindowId,
103        target: RenderTargetId,
104        mapping: &ViewportMapping,
105        pixels_per_point: f32,
106        pointer_id: PointerId,
107        pointer_type: PointerType,
108        position: Point,
109        kind: ViewportInputKind,
110    ) -> Option<Self> {
111        let mapped = mapping.map();
112        let uv = mapping.window_point_to_uv(position)?;
113        let target_px = mapping.window_point_to_target_px(position)?;
114        Some(Self {
115            window,
116            target,
117            pointer_id,
118            pointer_type,
119            geometry: ViewportInputGeometry {
120                content_rect_px: mapping.content_rect,
121                draw_rect_px: mapped.draw_rect,
122                target_px_size: mapping.target_px_size,
123                fit: mapping.fit,
124                pixels_per_point,
125            },
126            cursor_px: position,
127            uv,
128            target_px,
129            kind,
130        })
131    }
132
133    #[allow(clippy::too_many_arguments)]
134    pub fn from_mapping_window_point_clamped(
135        window: AppWindowId,
136        target: RenderTargetId,
137        mapping: &ViewportMapping,
138        pixels_per_point: f32,
139        pointer_id: PointerId,
140        pointer_type: PointerType,
141        position: Point,
142        kind: ViewportInputKind,
143    ) -> Self {
144        let mapped = mapping.map();
145        let uv = mapping.window_point_to_uv_clamped(position);
146        let target_px = mapping.window_point_to_target_px_clamped(position);
147        Self {
148            window,
149            target,
150            pointer_id,
151            pointer_type,
152            geometry: ViewportInputGeometry {
153                content_rect_px: mapping.content_rect,
154                draw_rect_px: mapped.draw_rect,
155                target_px_size: mapping.target_px_size,
156                fit: mapping.fit,
157                pixels_per_point,
158            },
159            cursor_px: position,
160            uv,
161            target_px,
162            kind,
163        }
164    }
165
166    #[allow(clippy::too_many_arguments)]
167    pub fn from_mapping_window_point_maybe_clamped(
168        window: AppWindowId,
169        target: RenderTargetId,
170        mapping: &ViewportMapping,
171        pixels_per_point: f32,
172        pointer_id: PointerId,
173        pointer_type: PointerType,
174        position: Point,
175        kind: ViewportInputKind,
176        clamped: bool,
177    ) -> Option<Self> {
178        if clamped {
179            Some(Self::from_mapping_window_point_clamped(
180                window,
181                target,
182                mapping,
183                pixels_per_point,
184                pointer_id,
185                pointer_type,
186                position,
187                kind,
188            ))
189        } else {
190            Self::from_mapping_window_point(
191                window,
192                target,
193                mapping,
194                pixels_per_point,
195                pointer_id,
196                pointer_type,
197                position,
198                kind,
199            )
200        }
201    }
202}
203
204#[derive(Debug, Clone, Copy, PartialEq)]
205pub enum ViewportInputKind {
206    PointerMove {
207        buttons: MouseButtons,
208        modifiers: Modifiers,
209    },
210    PointerDown {
211        button: MouseButton,
212        modifiers: Modifiers,
213        /// See `PointerEvent::{Down,Up}.click_count` for normalization rules.
214        click_count: u8,
215    },
216    PointerUp {
217        button: MouseButton,
218        modifiers: Modifiers,
219        /// Whether this pointer-up completes a "true click".
220        ///
221        /// See `PointerEvent::Up.is_click` for normalization rules.
222        is_click: bool,
223        /// See `PointerEvent::{Down,Up}.click_count` for normalization rules.
224        click_count: u8,
225    },
226    PointerCancel {
227        buttons: MouseButtons,
228        modifiers: Modifiers,
229        reason: PointerCancelReason,
230    },
231    Wheel {
232        delta: Point,
233        modifiers: Modifiers,
234    },
235}