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}