Shirakumo / alloy

A new user interface protocol and toolkit implementation
https://shirakumo.github.io/alloy
zlib License
181 stars 12 forks source link

Autorepeat #9

Closed Trashtalk217 closed 1 year ago

Trashtalk217 commented 4 years ago

I didn't put this in the pullrequest yesterday, because it's more opinionated, so I would like to hear your take on it.

In my opinion the UI we create should behave similair to the UI we daily interact with, unless there's a really good reason not to. I think this is helpfull because you can make use of pre-existing mental models of the user. So I've taken a look at some UI frameworks and looked at how the sliders behaved.

When looking at this and that, I found out a couple of things:

I personally find the :step slot to be a bit annoying, because in my opinion there's only one good value for it: the value you have for :grid. If it's smaller it can't bridge the gaps and if it's bigger you don't have the same amount of precise control that mouse users get. This doesn't mean that this design doesn't come up sometimes. The volumeslider of Linux Mint has 100 different values, but using the arrow keys you can only move in 5% steps. Something very similair is true for the volumeslider Youtube uses.

Of course the reason for this feature is to make sure that keyboard users don't have to wait forever to change the volume, but I'd argue that you can achieve the same speed using autorepeat and that it's not worth giving up the finer control.

TLDR: Implement autorepeat for sliders, maybe remove the :step slot.

Shinmera commented 4 years ago

Yeah, I agree, though it might be nice to be able to influence how the autorepeat works (constant, linear, exponential, multiplier, etc.)

Autorepeat is also sorely missing from the text input. The reason it's not in is because I have not been able to figure out how to represent auto-repeat cleanly. Should it be a slot on the event that designates whether it's a repeat or not? Should it be a separate event (so you'd have, say, down, repeat, up)? Should it be a subclass of the down event, or should down be the subclass? What about input methods like an analog stick on a gamepad? How would 'auto repeat' work there? Or what about mouse clicks?

Trashtalk217 commented 4 years ago

I think the easiest thing for a potential library user to deal with is this, have :autorepeat be a slot where you can supply the following arguments:

:autorepeat NIL ; no autorepeat
:autorepeat (make-instance autorepeat-profile some-args) ; this might be overkill we may just get away with :lineair, :constant, etc
:autorepeat T ; use the default autorepeat profile specified in the class

I don't think repeat should be a seperate event, since the idea behind repeat is that it just repeats the normal :activate event, and changing that might be confusing, but maybe there are some usecases, I just haven't thought of them.

It might also be interesting to think of some examples that use autorepeat:

I am struggeling to think of some others, so please add some if you know any.

Shinmera commented 4 years ago

There are definitely cases where it is vital to distinguish between the first event and repeats.

As for how to handle the repeat behaviour, I was thinking more along the lines of a generic function protocol and mixin classes.

Autorepeat is needed for pretty much anything that offers navigation of some kind. This includes sliders, but also lists and tables, cursor navigation in a text field, etc.

Trashtalk217 commented 4 years ago

What do you mean with 'generic functions' and 'mixing classes', could you give me an example?

Shinmera commented 4 years ago

Not 'mixing', 'mixin'.

You define a protocol that describes the interface for the desired behaviour. You then define multiple classes that implement this protocol in different ways. Users can then choose the behaviour they want by using one or more of these classes as superclasses of their component.

Shinmera commented 4 years ago

An example of using mixins is the component: https://github.com/Shirakumo/alloy/blob/master/component.lisp#L9 Here we combine ('mix in') the behaviours of multiple classes in one overall class.

The presentations protocol is another example: https://github.com/Shirakumo/alloy/blob/master/renderers/simple/presentations/default.lisp Here we only define a 'default look and feel' class. However, one could easily define many classes that define the look and feel for a subset of components, or define a specific part of the appearance. A user could then compose the final look they want by combining the effects they want through inheritance.

Trashtalk217 commented 4 years ago

So it might look something like this (I really like examples)?

(defclass some-event (another-event repeatable) ())

(defmethod repeat (some-event event)
  (code))

Or am I making it too simplistic?

Shinmera commented 4 years ago

That's closer to an implementation of it. Something like this, maybe:

;; Protocol
(defgeneric repeat (event times repeater))
(defgeneric repeat-count (event repeater))
(defclass auto-repeater () ())
(defclass repeat (event) ())

(defmethod handle ((event repeat) (repeater auto-repeater))
  (let ((count (repeat-count event repeater)))
    (if count (repeat event count repeater) (decline))))

;; Implementations
(defclass constant-repeater (auto-repeater) ())
(defmethod repeat-count ((event repeat) (repeater constant-repeater)) 1)

(defclass ramping-repeater (auto-repeater)
  (factor max-factor))
(defmethod repeat-count :around ((event repeat) (repeater ramping-repeater))
  (min (max-factor repeater) (call-next-method))

(defclass linear-repeater (ramping-repeater) (count))
(defmethod repeat-count ((event repeat) (repeater linear-repeater))
  (incf (count repeater)))

(defclass numeric-repeater (auto-repeater) ())
(defgeneric repeat-direction (event))
(defmethod repeat ((event repeat) times (repeater numeric-repeater))
  (incf (value repeater) (* times (repeat-direction event))))

Etc. This is just a brief draft and would not be exactly right for the real protocol.

Notice though how the behaviour is tightly encapsulated into individual classes. A user could then for instance create a class that inherits from slider, numeric-repeater, and linear-repeater to combine their behaviours.

Trashtalk217 commented 4 years ago

It looks like a solid start. The one thing I'd mention is that I barely see ramping repeaters in my user interfaces, so it's probably best to focus on the constant-repeater behaviour. I'd also like your opinion on the :step slot in sliders. It has kinda gotten buried under all the autorepeater talk. Do you think it's a necessary feature?

P.S. If you say something like and would not be exactly right for the real protocol, it's very hard for me to offer suggestions as I don't know what's wrong with the protocol in the first place. A bit more explanation on that point would be nice.

Shinmera commented 4 years ago

Ramping repeaters are definitely around in interfaces, especially when you cover large ranges with analog inputs. It's especially common in game UIs.

If we have auto-repeaters I don't think the step would be necessary anymore.

Me saying it would not be exactly right is simply because I have not thought very deeply about it. I don't have much insight either beyond that I know it needs more thought. One immediate problem I see with the above proposal is that it's no longer obvious how to decline an event. I'm not sure how to solve that -- I'd have to sit down and think about the problem.

Shinmera commented 1 year ago

key-event now has a repeat-p indicator. It is left up to the backend to issue repeat events, as only the backend can distinguish the two when receiving them from the OS.