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 }