fidget_gui/
lib.rs

1//! Platform-independent GUI abstractions
2use fidget_core::render::{ImageSize, VoxelSize};
3use nalgebra::{
4    Const, DefaultAllocator, DimNameAdd, DimNameSum, Matrix3, Matrix4, OMatrix,
5    OPoint, OVector, Point2, Point3, U1, Vector2, Vector3,
6    allocator::Allocator,
7};
8use serde::{Deserialize, Serialize};
9
10/// Object providing a world-to-model transform in 2D
11///
12/// Rendering and meshing happen in the ±1 square or cube; these are referred to
13/// as _world_ coordinates.  A `View` generates a homogeneous transform matrix
14/// that maps from positions in world coordinates to _model_ coordinates, which
15/// can be whatever you want.
16///
17/// For example, the world-to-model transform could map the ±1 region onto the
18/// ±0.5 region, which would be a zoom transform.
19///
20/// Here's an example of using a `View2` to focus on the region `[4, 6]`:
21///
22/// ```
23/// # use nalgebra::{Vector2, Point2};
24/// # use fidget_gui::{View2};
25/// let view = View2::from_center_and_scale(Vector2::new(5.0, 5.0), 1.0);
26///
27/// //   -------d-------
28/// //   |             |
29/// //   |             |
30/// //   c      a      b
31/// //   |             |
32/// //   |             |
33/// //   -------e-------
34/// let a = view.transform_point(&Point2::new(0.0, 0.0));
35/// assert_eq!(a, Point2::new(5.0, 5.0));
36///
37/// let b = view.transform_point(&Point2::new(1.0, 0.0));
38/// assert_eq!(b, Point2::new(6.0, 5.0));
39///
40/// let c = view.transform_point(&Point2::new(-1.0, 0.0));
41/// assert_eq!(c, Point2::new(4.0, 5.0));
42///
43/// let d = view.transform_point(&Point2::new(0.0, 1.0));
44/// assert_eq!(d, Point2::new(5.0, 6.0));
45///
46/// let e = view.transform_point(&Point2::new(0.0, -1.0));
47/// assert_eq!(e, Point2::new(5.0, 4.0));
48/// ```
49///
50/// See also
51/// [`RegionSize::screen_to_world`](fidget_core::render::RegionSize::screen_to_world),
52/// which converts from screen to world coordinates.
53#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
54pub struct View2 {
55    center: Vector2<f32>,
56    scale: f32,
57}
58
59impl Default for View2 {
60    fn default() -> Self {
61        Self {
62            scale: 1.0,
63            center: Vector2::new(0.0, 0.0),
64        }
65    }
66}
67
68impl View2 {
69    /// Builds a camera from a center (in world coordinates) and a scale
70    ///
71    /// The resulting camera will point at the center, and the viewport will be
72    /// ± `scale` in size.
73    pub fn from_center_and_scale(center: Vector2<f32>, scale: f32) -> Self {
74        Self { center, scale }
75    }
76
77    /// Returns a `(center, scale)` tuple
78    pub fn components(&self) -> (Vector2<f32>, f32) {
79        (self.center, self.scale)
80    }
81
82    /// Builds a view from its components
83    ///
84    /// This function is identical to
85    /// [`from_center_and_scale`](Self::from_center_and_scale)
86    pub fn from_components(center: Vector2<f32>, scale: f32) -> Self {
87        Self::from_center_and_scale(center, scale)
88    }
89
90    /// Returns the scaling matrix for this view
91    fn scale_mat(&self) -> Matrix3<f32> {
92        Matrix3::new_scaling(self.scale)
93    }
94
95    /// Returns the translation matrix for this view
96    fn translation_mat(&self) -> Matrix3<f32> {
97        Matrix3::new_translation(&self.center)
98    }
99
100    /// Returns the world-to-model transform matrix
101    pub fn world_to_model(&self) -> Matrix3<f32> {
102        self.translation_mat() * self.scale_mat()
103    }
104
105    /// Transform a point from world to model space
106    pub fn transform_point(&self, p: &Point2<f32>) -> Point2<f32> {
107        self.world_to_model().transform_point(p)
108    }
109
110    /// Begins a translation operation, given a point in world space
111    pub fn begin_translate(&self, start: Point2<f32>) -> TranslateHandle<2> {
112        let initial_mat = self.world_to_model();
113        TranslateHandle {
114            start: initial_mat.transform_point(&start),
115            initial_mat,
116            initial_center: self.center,
117        }
118    }
119
120    /// Applies a translation (in world units) to the current camera position
121    pub fn translate(
122        &mut self,
123        h: &TranslateHandle<2>,
124        pos: Point2<f32>,
125    ) -> bool {
126        let next_center = h.center(pos);
127        let changed = next_center != self.center;
128        self.center = next_center;
129        changed
130    }
131
132    /// Zooms the camera about a particular position (in world space)
133    ///
134    /// Returns `true` if the view has changed, `false` otherwise
135    pub fn zoom(&mut self, amount: f32, pos: Option<Point2<f32>>) -> bool {
136        match pos {
137            Some(before) => {
138                let pos_before = self.transform_point(&before);
139                self.scale *= amount;
140                let pos_after = self.transform_point(&before);
141                self.center += pos_before - pos_after;
142            }
143            None => {
144                self.scale *= amount;
145            }
146        }
147        amount != 1.0
148    }
149}
150
151/// Object providing a view-to-model transform in 2D
152#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
153pub struct View3 {
154    center: Vector3<f32>,
155    scale: f32,
156    yaw: f32,
157    pitch: f32,
158}
159
160impl Default for View3 {
161    fn default() -> Self {
162        Self {
163            center: Vector3::new(0.0, 0.0, 0.0),
164            scale: 1.0,
165            yaw: 0.0,
166            pitch: 0.0,
167        }
168    }
169}
170
171/// Object providing a world-to-model transform in 3D
172///
173/// This is implemented as a uniform scaling operation, followed by rotation
174/// (pitch / yaw, i.e. turntable rotation), followed by translation.
175///
176/// See [`View2`] for a diagram of coordinate spaces
177impl View3 {
178    /// Builds a camera from a center (in world coordinates) and a scale
179    ///
180    /// The resulting camera will point at the center along the `-Z` axis, and
181    /// the viewport will be ± `scale` in size.
182    pub fn from_center_and_scale(center: Vector3<f32>, scale: f32) -> Self {
183        Self {
184            center,
185            scale,
186            yaw: 0.0,
187            pitch: 0.0,
188        }
189    }
190
191    /// Returns a `(center, scale, yaw, pitch)` tuple
192    pub fn components(&self) -> (Vector3<f32>, f32, f32, f32) {
193        (self.center, self.scale, self.yaw, self.pitch)
194    }
195
196    /// Builds the view from its components
197    pub fn from_components(
198        center: Vector3<f32>,
199        scale: f32,
200        yaw: f32,
201        pitch: f32,
202    ) -> Self {
203        Self {
204            center,
205            scale,
206            yaw,
207            pitch,
208        }
209    }
210
211    /// Returns the world-to-model transform matrix
212    pub fn world_to_model(&self) -> Matrix4<f32> {
213        self.translation_mat() * self.rot_mat() * self.scale_mat()
214    }
215
216    /// Transform a point from world to model space
217    pub fn transform_point(&self, p: &Point3<f32>) -> Point3<f32> {
218        self.world_to_model().transform_point(p)
219    }
220
221    /// Begins a translation operation, given a point in world space
222    pub fn begin_translate(&self, start: Point3<f32>) -> TranslateHandle<3> {
223        let initial_mat = self.world_to_model();
224        TranslateHandle {
225            start: initial_mat.transform_point(&start),
226            initial_mat,
227            initial_center: self.center,
228        }
229    }
230
231    /// Returns the scaling matrix for this view
232    fn scale_mat(&self) -> Matrix4<f32> {
233        Matrix4::new_scaling(self.scale)
234    }
235
236    /// Returns the rotation matrix for this view
237    fn rot_mat(&self) -> Matrix4<f32> {
238        Matrix4::from_axis_angle(
239            &nalgebra::Unit::new_normalize(Vector3::new(0.0, 0.0, 1.0)),
240            self.yaw,
241        ) * Matrix4::from_axis_angle(
242            &nalgebra::Unit::new_normalize(Vector3::new(1.0, 0.0, 0.0)),
243            self.pitch,
244        )
245    }
246
247    /// Returns the translation matrix for this view
248    fn translation_mat(&self) -> Matrix4<f32> {
249        Matrix4::new_translation(&self.center)
250    }
251
252    /// Applies a translation (in world units) to the current camera position
253    pub fn translate(
254        &mut self,
255        h: &TranslateHandle<3>,
256        pos: Point3<f32>,
257    ) -> bool {
258        let next_center = h.center(pos);
259        let changed = next_center != self.center;
260        self.center = next_center;
261        changed
262    }
263
264    /// Zooms the camera about a particular position (in world space)
265    ///
266    /// Returns `true` if the view has changed, `false` otherwise
267    pub fn zoom(&mut self, amount: f32, pos: Option<Point3<f32>>) -> bool {
268        match pos {
269            Some(before) => {
270                let pos_before = self.transform_point(&before);
271                self.scale *= amount;
272                let pos_after = self.transform_point(&before);
273                self.center += pos_before - pos_after;
274            }
275            None => {
276                self.scale *= amount;
277            }
278        }
279        amount != 1.0
280    }
281
282    /// Begins a rotation operation, given a point in world space
283    pub fn begin_rotate(&self, start: Point3<f32>) -> RotateHandle {
284        RotateHandle {
285            start,
286            initial_yaw: self.yaw,
287            initial_pitch: self.pitch,
288        }
289    }
290
291    /// Rotates the camera, given a cursor end position in world space
292    ///
293    /// Returns `true` if the view has changed, `false` otherwise
294    pub fn rotate(&mut self, h: &RotateHandle, pos: Point3<f32>) -> bool {
295        let next_yaw = h.yaw(pos.x);
296        let next_pitch = h.pitch(pos.y);
297        let changed = (next_yaw != self.yaw) || (next_pitch != self.pitch);
298        self.yaw = next_yaw;
299        self.pitch = next_pitch;
300        changed
301    }
302}
303
304/// Handle to perform rotations on a [`View3`]
305#[derive(Copy, Clone)]
306pub struct RotateHandle {
307    /// Position of the initial click in world space
308    start: Point3<f32>,
309    initial_yaw: f32,
310    initial_pitch: f32,
311}
312
313/// Eyeballed for pleasant UI
314const ROTATE_SPEED: f32 = 2.0;
315
316impl RotateHandle {
317    fn yaw(&self, x: f32) -> f32 {
318        (self.initial_yaw + (self.start.x - x) * ROTATE_SPEED)
319            % std::f32::consts::TAU
320    }
321    fn pitch(&self, y: f32) -> f32 {
322        (self.initial_pitch + (y - self.start.y) * ROTATE_SPEED)
323            .clamp(0.0, std::f32::consts::PI)
324    }
325}
326
327/// Handle to perform translation on a [`View2`] or [`View3`]
328#[derive(Copy, Clone)]
329pub struct TranslateHandle<const N: usize>
330where
331    Const<N>: DimNameAdd<U1>,
332    DefaultAllocator:
333        Allocator<DimNameSum<Const<N>, U1>, DimNameSum<Const<N>, U1>>,
334    OMatrix<
335        f32,
336        <Const<N> as DimNameAdd<Const<1>>>::Output,
337        <Const<N> as DimNameAdd<Const<1>>>::Output,
338    >: Copy,
339{
340    /// Position of the initial click, in model space
341    start: OPoint<f32, Const<N>>,
342    /// Initial world-to-model transform matrix
343    initial_mat: OMatrix<
344        f32,
345        <Const<N> as DimNameAdd<Const<1>>>::Output,
346        <Const<N> as DimNameAdd<Const<1>>>::Output,
347    >,
348    /// Initial value of [`View2::center`] or [`View3::center`]
349    initial_center: OVector<f32, Const<N>>,
350}
351
352impl TranslateHandle<2> {
353    /// Returns the new value for [`View2::center`]
354    fn center(&self, pos: Point2<f32>) -> Vector2<f32> {
355        let pos_model = self.initial_mat.transform_point(&pos);
356        self.initial_center - (pos_model - self.start)
357    }
358}
359
360impl TranslateHandle<3> {
361    /// Returns the new value for [`View3::center`]
362    fn center(&self, pos: Point3<f32>) -> Vector3<f32> {
363        let pos_model = self.initial_mat.transform_point(&pos);
364        self.initial_center - (pos_model - self.start)
365    }
366}
367
368/// Stateful abstraction for a 2D canvas supporting drag and zoom
369///
370/// The canvas may be used in either immediate mode or callback mode.
371///
372/// In **immediate mode**, the user sends the complete cursor state to
373/// [`Canvas2::interact`].
374///
375/// In **callback mode**, lower-level functions should be invoked as callbacks:
376/// - [`Canvas2::resize`]
377/// - [`Canvas2::begin_drag`]
378/// - [`Canvas2::drag`]
379/// - [`Canvas2::end_drag`]
380/// - [`Canvas2::zoom`]
381#[derive(Copy, Clone)]
382pub struct Canvas2 {
383    view: View2,
384    image_size: ImageSize,
385    drag_start: Option<TranslateHandle<2>>,
386}
387
388/// On-screen cursor state
389#[derive(Copy, Clone, Debug)]
390pub struct CursorState<D> {
391    /// Position within the canvas, in screen coordinates
392    pub screen_pos: Point2<i32>,
393
394    /// Current drag state
395    ///
396    /// This is generic, because it varies between 2D and 3D
397    pub drag: D,
398}
399
400impl Canvas2 {
401    /// Builds a new canvas with the given image size and default view
402    pub fn new(image_size: ImageSize) -> Self {
403        Self {
404            view: View2::default(),
405            image_size,
406            drag_start: None,
407        }
408    }
409
410    /// Destructures the canvas into its components
411    pub fn components(&self) -> (View2, ImageSize) {
412        (self.view, self.image_size)
413    }
414
415    /// Builds the canvas from components
416    pub fn from_components(view: View2, image_size: ImageSize) -> Self {
417        Self {
418            view,
419            image_size,
420            drag_start: None,
421        }
422    }
423
424    /// Stateful interaction with the canvas
425    ///
426    /// The `cursor_state` indicates whether cursor is on-screen, and if so,
427    /// whether the mouse button is down.
428    ///
429    /// Returns a boolean value indicating whether the view has changed
430    #[must_use]
431    pub fn interact(
432        &mut self,
433        image_size: ImageSize,
434        cursor_state: Option<CursorState<bool>>,
435        scroll: f32,
436    ) -> bool {
437        self.image_size = image_size;
438        let mut changed = false;
439        let pos_screen = match cursor_state {
440            Some(cs) => {
441                if cs.drag {
442                    self.begin_drag(cs.screen_pos); // idempotent
443                    changed |= self.drag(cs.screen_pos);
444                } else {
445                    self.end_drag();
446                }
447                Some(cs.screen_pos)
448            }
449            _ => {
450                self.end_drag();
451                None
452            }
453        };
454        changed |= self.zoom(scroll, pos_screen);
455        changed
456    }
457
458    /// Returns the current view
459    pub fn view(&self) -> View2 {
460        self.view
461    }
462
463    /// Returns the current image size
464    pub fn image_size(&self) -> ImageSize {
465        self.image_size
466    }
467
468    /// Callback when the canvas is resized
469    pub fn resize(&mut self, image_size: ImageSize) {
470        self.image_size = image_size;
471    }
472
473    /// Begins a new drag with the mouse at the given screen position
474    ///
475    /// If a drag is already in progress, this function does nothing.
476    pub fn begin_drag(&mut self, pos_screen: Point2<i32>) {
477        if self.drag_start.is_none() {
478            let pos_world = self.image_size.transform_point(pos_screen);
479            self.drag_start = Some(self.view.begin_translate(pos_world));
480        }
481    }
482
483    /// Callback when the cursor is moved
484    ///
485    /// If a drag is in progress, then the drag continues and this function
486    /// returns a boolean value indicating whether the view has changed;
487    /// otherwise, it returns `false`
488    #[must_use]
489    pub fn drag(&mut self, pos_screen: Point2<i32>) -> bool {
490        if let Some(prev) = &self.drag_start {
491            let pos_world = self.image_size.transform_point(pos_screen);
492            self.view.translate(prev, pos_world)
493        } else {
494            false
495        }
496    }
497
498    /// Callback when the mouse button is released
499    pub fn end_drag(&mut self) {
500        self.drag_start = None;
501    }
502
503    /// Callback when the user scrolls
504    ///
505    /// `amount` should be a linear amount (either positive or negative); this
506    /// function converts it into an scaled exponential.
507    ///
508    /// Returns a boolean value indicating whether the view has changed
509    #[must_use]
510    pub fn zoom(
511        &mut self,
512        amount: f32,
513        pos_screen: Option<Point2<i32>>,
514    ) -> bool {
515        let pos_world = pos_screen.map(|p| self.image_size.transform_point(p));
516        self.view.zoom((amount / 100.0).exp2(), pos_world)
517    }
518}
519
520////////////////////////////////////////////////////////////////////////////////
521
522/// Stateful abstraction for a 3D canvas supporting pan, zoom, and rotate
523#[derive(Copy, Clone)]
524pub struct Canvas3 {
525    view: View3,
526    image_size: VoxelSize,
527    drag_start: Option<Drag3>,
528}
529
530/// 3D drag mode
531#[derive(Copy, Clone)]
532pub enum DragMode {
533    /// Translate the view in local XY coordinates
534    Pan,
535    /// Rotate the view about the current center
536    Rotate,
537}
538
539#[derive(Copy, Clone)]
540enum Drag3 {
541    Pan(TranslateHandle<3>),
542    Rotate(RotateHandle),
543}
544
545/// Operations have the same semantics as [`Canvas2`]; see those docstrings for
546/// details.
547#[allow(missing_docs)]
548impl Canvas3 {
549    pub fn new(image_size: VoxelSize) -> Self {
550        Self {
551            view: View3::default(),
552            image_size,
553            drag_start: None,
554        }
555    }
556
557    pub fn components(&self) -> (View3, VoxelSize) {
558        (self.view, self.image_size)
559    }
560
561    pub fn from_components(view: View3, image_size: VoxelSize) -> Self {
562        Self {
563            view,
564            image_size,
565            drag_start: None,
566        }
567    }
568
569    pub fn image_size(&self) -> VoxelSize {
570        self.image_size
571    }
572
573    #[must_use]
574    pub fn interact(
575        &mut self,
576        image_size: VoxelSize,
577        cursor_state: Option<CursorState<Option<DragMode>>>,
578        scroll: f32,
579    ) -> bool {
580        let mut changed = false;
581        self.image_size = image_size;
582        let pos_screen = match cursor_state {
583            Some(cs) => {
584                if let Some(drag_mode) = cs.drag {
585                    self.begin_drag(cs.screen_pos, drag_mode); // idempotent
586                    changed |= self.drag(cs.screen_pos);
587                } else {
588                    self.end_drag();
589                }
590                Some(cs.screen_pos)
591            }
592            _ => {
593                self.end_drag();
594                None
595            }
596        };
597        changed |= self.zoom(scroll, pos_screen);
598        changed
599    }
600
601    pub fn view(&self) -> View3 {
602        self.view
603    }
604
605    pub fn begin_drag(&mut self, pos_screen: Point2<i32>, drag_mode: DragMode) {
606        if self.drag_start.is_none() {
607            let pos_world = self.screen_to_world(pos_screen);
608            self.drag_start = Some(match drag_mode {
609                DragMode::Pan => {
610                    Drag3::Pan(self.view.begin_translate(pos_world))
611                }
612                DragMode::Rotate => {
613                    Drag3::Rotate(self.view.begin_rotate(pos_world))
614                }
615            });
616        }
617    }
618
619    fn screen_to_world(&self, pos_screen: Point2<i32>) -> Point3<f32> {
620        self.image_size.transform_point(Point3::new(
621            pos_screen.x,
622            pos_screen.y,
623            0,
624        ))
625    }
626
627    #[must_use]
628    pub fn drag(&mut self, pos_screen: Point2<i32>) -> bool {
629        let pos_world = self.screen_to_world(pos_screen);
630        match &self.drag_start {
631            Some(Drag3::Pan(prev)) => self.view.translate(prev, pos_world),
632            Some(Drag3::Rotate(prev)) => self.view.rotate(prev, pos_world),
633            None => false,
634        }
635    }
636
637    pub fn end_drag(&mut self) {
638        self.drag_start = None;
639    }
640
641    #[must_use]
642    pub fn zoom(
643        &mut self,
644        amount: f32,
645        pos_screen: Option<Point2<i32>>,
646    ) -> bool {
647        let pos_world = pos_screen.map(|p| self.screen_to_world(p));
648        self.view.zoom((amount / 100.0).exp2(), pos_world)
649    }
650}