Skip to main content

fidget_gui/
lib.rs

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