1 /// Module handling low-level user preferences, like the double click interval.
2 module fluid.io.preference;
3 
4 import core.time;
5 
6 import fluid.types;
7 import fluid.future.context;
8 
9 @safe:
10 
11 /// I/O interface for loading low-level user preferences, such as the double click interval, from the system.
12 ///
13 /// Right now, this interface only includes a few basic options. Other user-specific preference options
14 /// may be added in the future if they need to be handled at Fluid's level. When this happens, they will first
15 /// be added through a separate interface, and become merged on a major release.
16 ///
17 /// Using values from the system, rather than guessing or supplying our own, has benefits for accessibility.
18 /// These preferences help people of varying age and reaction times, or with disabilities related to vision
19 /// and muscular function.
20 interface PreferenceIO : IO {
21 
22     /// Get the double click interval from the system
23     ///
24     /// This interval defines the maximum amount of time that can pass between two clicks for a double click event
25     /// to trigger, or, between each individual click in a triple click sequence. Detecting double clicks has to be
26     /// implemented at node level, and double clicks do not normally have a corresponding input action.
27     ///
28     /// If caching is necessary, it has to be done at I/O level. This way, the I/O system may support reloading
29     /// preferences at runtime.
30     ///
31     /// Returns:
32     ///     The double click interval.
33     Duration doubleClickInterval() const nothrow;
34 
35     /// Get the maximum distance allowed between two clicks for them to count as a double click.
36     ///
37     /// Many systems do not provide this value, so it may be necessary to make a guess.
38     /// This is typically a small value around 5 pixels.
39     ///
40     /// Returns:
41     ///     Maximum distance a pointer can travel before dismissing a double click.
42     float maximumDoubleClickDistance() const nothrow;
43 
44     /// Get the desired scroll speed (in pixels, or 1/96th of an inch) for every scroll unit. This value should
45     /// be used by mouse devices to translate scroll in ticks to screen space.
46     ///
47     /// The way scroll values are specified may vary across systems, but scroll speed is usually separate.
48     /// `PreferenceIO` should take care of normalizing this, ensuring that this behavior is consistent.
49     ///
50     /// Returns:
51     ///     Desired scroll speed in pixels per unit for both movement axes.
52     Vector2 scrollSpeed() const nothrow;
53 
54 }
55 
56 /// Helper struct to detect double clicks, triple clicks, or overall repeated clicks.
57 ///
58 /// To make use of the sensor, you need to call its two methods — `hold` and `activate` — whenever the relevant
59 /// event occurs. `hold` should be called for every instance, whereas `activate` should only be called if the
60 /// event is active. For example, to implement double clicks via mouse using input actions, you'd need to implement
61 /// two input handlers:
62 ///
63 /// ---
64 /// mixin enableInputActions;
65 /// TimeIO timeIO;
66 /// PreferenceIO preferenceIO;
67 /// DoubleClickSensor sensor;
68 ///
69 /// override void resizeImpl(Vector2) {
70 ///     require(timeIO);
71 ///     require(preferenceIO);
72 /// }
73 ///
74 /// @(FluidInputAction.press, WhileHeld)
75 /// override bool hold(HoverPointer pointer) {
76 ///     sensor.hold(timeIO, preferenceIO, pointer);
77 /// }
78 ///
79 /// @(FluidInputAction.press)
80 /// override bool press(HoverPointer) {
81 ///     sensor.activate();
82 /// }
83 /// ---
84 struct MultipleClickSensor {
85 
86     import fluid.io.time;
87     import fluid.io.action;
88     import fluid.io.hover;
89 
90     /// Number of registered clicks.
91     int clicks;
92 
93     /// Time the event has last triggered. Following an activated click event, this is only updated once.
94     private MonoTime _lastClickTime;
95     private Vector2 _lastPosition;
96     private bool _down;
97 
98     /// Clear the counter, resetting click count to 0.
99     void clear() {
100         clicks = 0;
101         _down = false;
102     }
103 
104     /// Call this function every time the desired click event is emitted.
105     ///
106     /// This overload accepts `TimeIO` and `ActionIO` systems and reads their properties to determine
107     /// the right values. If for some reason you cannot use these systems, use the other overload instead.
108     ///
109     /// Params:
110     ///     timeIO          = Time I/O system.
111     ///     preferenceIO    = User preferences I/O system.
112     ///     pointer         = Pointer emitting the event.
113     ///     pointerPosition = Alternatively to `pointer`, just the pointer's position.
114     void hold(TimeIO timeIO, PreferenceIO preferenceIO, HoverPointer pointer) {
115         return hold(
116             timeIO.now,
117             preferenceIO.doubleClickInterval,
118             preferenceIO.maximumDoubleClickDistance,
119             pointer.position
120         );
121     }
122 
123     /// ditto
124     void hold(TimeIO timeIO, PreferenceIO preferenceIO, Vector2 pointerPosition) {
125         return hold(
126             timeIO.now,
127             preferenceIO.doubleClickInterval,
128             preferenceIO.maximumDoubleClickDistance,
129             pointerPosition
130         );
131     }
132 
133     /// Call this function every time the desired click event is emitted.
134     ///
135     /// This overload accepts raw values for settings. You should use Fluid's I/O systems where possible,
136     /// so the other overload is preferable over this one.
137     ///
138     /// Params:
139     ///     currentTime         = Current time in the system.
140     ///     doubleClickInterval = Maximum time allowed between two clicks.
141     ///     maximumDistance     = Maximum distance the pointer can travel before.
142     ///     pointerPosition     = Position of the pointer emitting the event.
143     void hold(MonoTime currentTime, Duration doubleClickInterval, float maximumDistance, Vector2 pointerPosition) {
144 
145         import fluid.utils : distance2;
146 
147         if (_down) return;
148 
149         const shouldReset = currentTime - _lastClickTime > doubleClickInterval
150             || distance2(pointerPosition, _lastPosition) > maximumDistance^^2
151             || clicks == 0;
152 
153         // Reset clicks if enough time has passed, or if the cursor has gone too far
154         if (shouldReset) {
155             clicks = 1;
156         }
157         else {
158             clicks++;
159         }
160 
161         // Update values
162         _down = true;
163         _lastClickTime = currentTime;
164         _lastPosition = pointerPosition;
165 
166     }
167 
168     /// Call this function every time the desired click event is active.
169     void activate() {
170         _down = false;
171     }
172 
173 }
174 
175 @("MultipleClickSensor can detect double clicks")
176 unittest {
177 
178     MultipleClickSensor sensor;
179     MonoTime start;
180     const interval = 500.msecs;
181     const maxDistance = 5;
182     const position = Vector2();
183 
184     sensor.hold(start +  0.msecs, interval, maxDistance, position);
185     assert(sensor.clicks == 1);
186     sensor.activate();
187     assert(sensor.clicks == 1);
188 
189     sensor.hold(start + 140.msecs, interval, maxDistance, position);
190     assert(sensor.clicks == 2);
191     sensor.activate();
192     assert(sensor.clicks == 2);
193 
194 }
195 
196 @("MultipleClickSensor can detect triple clicks")
197 unittest {
198 
199     MultipleClickSensor sensor;
200     MonoTime start;
201     const interval = 500.msecs;
202     const maxDistance = 5;
203     const position = Vector2();
204 
205     sensor.hold(start +  0.msecs, interval, maxDistance, position);
206     assert(sensor.clicks == 1);
207     sensor.activate();
208     assert(sensor.clicks == 1);
209 
210     sensor.hold(start + 300.msecs, interval, maxDistance, position);
211     assert(sensor.clicks == 2);
212     sensor.activate();
213     assert(sensor.clicks == 2);
214 
215     sensor.hold(start + 600.msecs, interval, maxDistance, position);
216     assert(sensor.clicks == 3);
217     sensor.activate();
218     assert(sensor.clicks == 3);
219 
220 }
221 
222 @("MultipleClickSensor checks doubleClickInterval")
223 unittest {
224 
225     MultipleClickSensor sensor;
226     MonoTime start;
227     const interval = 500.msecs;
228     const maxDistance = 5;
229     const position = Vector2();
230 
231     sensor.hold(start +  0.msecs, interval, maxDistance, position);
232     assert(sensor.clicks == 1);
233     sensor.activate();
234     assert(sensor.clicks == 1);
235 
236     sensor.hold(start + 600.msecs, interval, maxDistance, position);
237     assert(sensor.clicks == 1);
238     sensor.activate();
239     assert(sensor.clicks == 1);
240 
241 }
242 
243 @("MultipleClickSensor checks maxDistance")
244 unittest {
245 
246     MultipleClickSensor sensor;
247     MonoTime start;
248     const interval = 500.msecs;
249     const maxDistance = 5;
250     const position1 = Vector2(0, 0);
251     const position2 = Vector2(5, 5);
252 
253     sensor.hold(start +  0.msecs, interval, maxDistance, position1);
254     assert(sensor.clicks == 1);
255     sensor.activate();
256     assert(sensor.clicks == 1);
257 
258     sensor.hold(start + 200.msecs, interval, maxDistance, position2);
259     assert(sensor.clicks == 1);
260     sensor.activate();
261     assert(sensor.clicks == 1);
262 
263 }
264 
265 @("MultipleClickSensor allows for dragging")
266 unittest {
267 
268     MultipleClickSensor sensor;
269     MonoTime start;
270     const interval = 500.msecs;
271     const maxDistance = 5;
272     const position1 = Vector2(0, 0);
273     const position2 = Vector2(3, 0);
274     const position3 = Vector2(5, 5);
275     const position4 = Vector2(10, 11);
276 
277     sensor.hold(start +  0.msecs, interval, maxDistance, position1);
278     assert(sensor.clicks == 1);
279     sensor.activate();
280     assert(sensor.clicks == 1);
281 
282     sensor.hold(start + 200.msecs, interval, maxDistance, position2);
283     assert(sensor.clicks == 2);
284     sensor.hold(start + 250.msecs, interval, maxDistance, position3);
285     assert(sensor.clicks == 2);
286     sensor.hold(start + 300.msecs, interval, maxDistance, position4);
287     assert(sensor.clicks == 2);
288     sensor.activate();
289 
290 }