kas_widgets/
grip.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! `GripPart` control
7
8use std::fmt::Debug;
9
10use kas::event::{CursorIcon, PressStart};
11use kas::prelude::*;
12
13/// A message from a [`GripPart`]
14#[derive(Clone, Debug)]
15pub enum GripMsg {
16    /// Widget received [`Event::PressStart`]
17    ///
18    /// Some parents will call [`EventState::set_nav_focus`] on this event.
19    PressStart,
20    /// Widget received [`Event::PressMove`]
21    ///
22    /// Parameter: the new position of the grip relative to the track.
23    ///
24    /// The grip position is not adjusted; the caller should also call
25    /// [`GripPart::set_offset`] to do so. This is separate to allow adjustment of
26    /// the posision; e.g. `Slider` pins the position to the nearest detent.
27    PressMove(Offset),
28    /// Widget received [`Event::PressEnd`]
29    ///
30    /// Parameter: `success` (see [`Event::PressEnd`]).
31    PressEnd(bool),
32}
33
34#[impl_self]
35mod GripPart {
36    /// A draggable grip part
37    ///
38    /// [`Slider`](crate::Slider), [`ScrollBar`](crate::ScrollBar) and
39    /// [`Splitter`](crate::Splitter) all require a component which supports
40    /// click+drag behaviour. The appearance differs but event handling is the
41    /// same: this widget is its implementation.
42    ///
43    /// # Layout
44    ///
45    /// This widget is unusual in several ways:
46    ///
47    /// [`Layout::size_rules`] does not request any size; the parent is expected
48    /// to determine the grip's size.
49    /// (Calling `size_rules` is still required to comply with widget model.)
50    ///
51    /// [`Layout::set_rect`] sets the grip's rect directly.
52    /// [`Self::set_track`] must be called first.
53    ///
54    /// Often it is preferable to use [`Self::set_size`] to set the grip's size
55    /// then [`Self::set_offset`] to set the position.
56    /// (Calling `set_rect` is still required to comply with widget model.)
57    ///
58    /// [`Layout::draw`] does nothing. The parent should handle all drawing.
59    ///
60    /// # Event handling
61    ///
62    /// This widget handles click/touch events on the widget, pushing a
63    /// [`GripMsg`] to allow the parent to implement further handling.
64    ///
65    /// Optionally, the parent may call [`GripPart::handle_press_on_track`]
66    /// when a [`Event::PressStart`] occurs on the track area (which identifies
67    /// as being the parent widget).
68    #[derive(Clone, Debug, Default)]
69    #[widget]
70    pub struct GripPart {
71        core: widget_core!(),
72        // The track is the area within which this GripPart may move
73        track: Rect,
74        // The position of the grip handle
75        rect: Rect,
76        press_coord: Coord,
77    }
78
79    /// This implementation is unusual (see [`GripPart`] documentation).
80    impl Layout for GripPart {
81        fn rect(&self) -> Rect {
82            self.rect
83        }
84
85        fn size_rules(&mut self, _: SizeCx, _axis: AxisInfo) -> SizeRules {
86            SizeRules::EMPTY
87        }
88
89        fn set_rect(&mut self, _: &mut ConfigCx, rect: Rect, _: AlignHints) {
90            self.rect = rect;
91        }
92
93        fn draw(&self, _: DrawCx) {}
94    }
95
96    impl Tile for Self {
97        fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
98            Role::Grip
99        }
100    }
101
102    impl Events for GripPart {
103        const REDRAW_ON_MOUSE_OVER: bool = true;
104
105        type Data = ();
106
107        #[inline]
108        fn mouse_over_icon(&self) -> Option<CursorIcon> {
109            Some(CursorIcon::Grab)
110        }
111
112        fn handle_event(&mut self, cx: &mut EventCx, _: &Self::Data, event: Event) -> IsUsed {
113            match event {
114                Event::PressStart(press) => {
115                    cx.push(GripMsg::PressStart);
116                    press
117                        .grab_move(self.id())
118                        .with_icon(CursorIcon::Grabbing)
119                        .complete(cx);
120
121                    // Event delivery implies coord is over the grip.
122                    self.press_coord = press.coord() - self.offset();
123                    Used
124                }
125                Event::PressMove { press, .. } => {
126                    let offset = press.coord - self.press_coord;
127                    let offset = offset.clamp(Offset::ZERO, self.max_offset());
128                    cx.push(GripMsg::PressMove(offset));
129                    Used
130                }
131                Event::PressEnd { success, .. } => {
132                    cx.push(GripMsg::PressEnd(success));
133                    Used
134                }
135                _ => Unused,
136            }
137        }
138    }
139
140    impl GripPart {
141        /// Construct
142        pub fn new() -> Self {
143            GripPart {
144                core: Default::default(),
145                track: Default::default(),
146                rect: Default::default(),
147                press_coord: Coord::ZERO,
148            }
149        }
150
151        /// Set the track
152        ///
153        /// The `track` is the region within which the grip may be moved.
154        ///
155        /// This method must be called to set the `track`, presumably from the
156        /// parent widget's [`Layout::set_rect`] method.
157        /// It is expected that [`GripPart::set_offset`] is called after this.
158        pub fn set_track(&mut self, track: Rect) {
159            self.track = track;
160        }
161
162        /// Get the current track `Rect`
163        #[inline]
164        pub fn track(&self) -> Rect {
165            self.track
166        }
167
168        /// Set the grip's size
169        ///
170        /// It is expected that for each axis the `size` is no larger than the size
171        /// of the `track` (see [`GripPart::set_track`]). If equal, then the grip
172        /// may not be moved on this axis.
173        ///
174        /// This method must be called at least once.
175        /// It is expected that [`GripPart::set_offset`] is called after this.
176        ///
177        /// This size may be read via `self.rect().size`.
178        pub fn set_size(&mut self, size: Size) {
179            self.rect.size = size;
180        }
181
182        /// Get the current grip position
183        ///
184        /// The position returned is relative to `self.track().pos` and is always
185        /// between [`Offset::ZERO`] and [`Self::max_offset`].
186        #[inline]
187        pub fn offset(&self) -> Offset {
188            self.rect.pos - self.track.pos
189        }
190
191        /// Get the maximum allowed offset
192        ///
193        /// This is the maximum allowed [`Self::offset`], equal to the size of the
194        /// track minus the size of the grip.
195        #[inline]
196        pub fn max_offset(&self) -> Offset {
197            Offset::conv(self.track.size) - Offset::conv(self.rect.size)
198        }
199
200        /// Set a new grip position
201        ///
202        /// The input `offset` is clamped between [`Offset::ZERO`] and
203        /// [`Self::max_offset`].
204        ///
205        /// The return value is a tuple of the new offest.
206        ///
207        /// It is expected that [`Self::set_track`] and [`Self::set_size`] are
208        /// called before this method.
209        pub fn set_offset(&mut self, cx: &mut EventState, offset: Offset) -> Offset {
210            let offset = offset.min(self.max_offset()).max(Offset::ZERO);
211            let grip_pos = self.track.pos + offset;
212            if grip_pos != self.rect.pos {
213                self.rect.pos = grip_pos;
214                cx.redraw(self);
215            }
216            offset
217        }
218
219        /// Handle an event on the track itself
220        ///
221        /// If it is desired to make the grip move when the track area is clicked,
222        /// then the parent widget should call this method when receiving
223        /// [`Event::PressStart`].
224        ///
225        /// Returns the new grip position relative to the track.
226        ///
227        /// The grip position is not adjusted; the caller should also call
228        /// [`Self::set_offset`] to do so. This is separate to allow adjustment of
229        /// the posision; e.g. `Slider` pins the position to the nearest detent.
230        pub fn handle_press_on_track(&mut self, cx: &mut EventCx, press: &PressStart) -> Offset {
231            press
232                .grab_move(self.id())
233                .with_icon(CursorIcon::Grabbing)
234                .complete(cx);
235
236            let coord = press.coord();
237            let offset = coord - self.track.pos - Offset::conv(self.rect.size / 2);
238            let offset = offset.clamp(Offset::ZERO, self.max_offset());
239            self.press_coord = coord - offset;
240            offset
241        }
242    }
243}