linebender / druid

A data-first Rust-native UI design toolkit.
https://linebender.org/druid/
Apache License 2.0
9.5k stars 566 forks source link

Scroll refinements #73

Open raphlinus opened 5 years ago

raphlinus commented 5 years ago

This issue is a continuation of the review discussion for #69, which was a very basic scroll container, and describes some refinements that would be nice.

The easiest is probably scroll bars. Right now, there's no visual indication of scroll position. It would be pretty straightforward to draw those on top of the content, and not all that hard to wire up the click-drag gesture. I'd consider this "help wanted."

Of course, the scroll bars need to be subject to styling. This is a much deeper question, which we might get back to in a while. In the meantime, I think it makes sense to start to develop a default theme (at first hardcoded), and focus on making that not-so-terrible.

There's a cluster of issues around dynamics, listed in the review as "momentum and bounce." These require animation frames, which is currently not wired. I'd like to do something similar the "request_anim_frame" mechanism in old druid, but in any case that needs to happen before we can add things to this widget.

I think momentum is handled by the system on macOS. It seems to deliver a series of events to the app. We can get more metadata, including the momentum phase (this would allow us to turn off momentum by suppressing events), but I'm not sure how important this is. I believe on Windows momentum is not considered idiomatic. (For reference, see Handling Trackpad Events in the Cocoa docs, as the full range of trackpad gestures is extremely rich)

Bounce is legit, though, and I think can just be configured at widget creation time. It requires animation frames.

I'd also like to do smoothing, but this is something of a deeper question. This breaks down into two sub-issues. First, sometimes you get a discrete scrolling bump (for example from the scroll wheel, but also potentially from the content) and want to animate that out. Second, even more or less continuous sources of scroll deltas (trackpads) are noisy, with quantization both in time and value. For the latter, I think doing smoothing can improve the overall feel of the app. I'd like to do something very similar to the midi smoothing in synthesizer.io, but no doubt this will require some fine-tuning of parameters to make sure it feels good. (As a sub-issue, I'd also like druid-shell to have a more precise, jitter suppressed timebase for animation events, right now it just measures wall clock at event processing time - this should probably also be tracked by an issue because I'm not sure when I'm going to get to it).

Related to the above, the content window should also be able to request scrolling, for example in a text editor. I think an appropriate mechanism for this is to have an Action that's propagated up in the event flow. Similarly, the scroll offset can be propagated down as part of the environment. (Digression - in the design I'm currently contemplating, this will cause a flurry of update calls for all the descendants of the scroll container whenever the content is scrolled. I'm thinking maybe we have a way for widgets to indicate their interest in particular data sources)

I need a little convincing that we need "principal axis scrolling." In my experimentation, many of the problems with this on xi-mac come from poor scroll dynamics, where (for example) on a momentum flick the scrolling continues without being clamped by the app. Setting principal axis scrolling fixes this, by effectively quantizing the angle of the scroll, but I'm not sure it's the absolute best solution. That said, I just checked VS Code and Sublime Text on Windows, and it is the default scroll behavior there. I think I'd be happiest with it as an option but not the default (like bounce).

chris-morgan commented 5 years ago

Miscellaneous thoughts, based on many years of exposure to scroll bars, scrolls of antiquity, cheese and vegemite scrolls, certain projects with unusual scrolling behaviour, and the Fastmail web app where we have to be too clever for my liking about some of these things, but in a very successful way (because we work with what we’ve got rather than trying to reinvent the world):

Not all scrollable containers want scrollbars; maps, for example, want to be scrollable, but not show scrollbars. Infinite-scrolly things may or may not want scrollbars. I don’t know whether this should be something like an enum { NotScrollable, HiddenScrollbar, VisibleScrollbar } per axis, or whether scrollbars should be a separate widget.

Native scrollbar appearance and behaviour is very strongly desirable, but may not be readily replicable. At the same time, you probably want it to be stylable with at least something like CSS scrollbar-color and scrollbar-width. The ideal colours can vary based on the content being scrolled, even if it’s as simple as light versus dark.

Native scrollbar positioning may entail overlay scrollbars (e.g. macOS by default, Windows 10 UWP apps by default), or may take space out of the layout (e.g. Windows 10 Win32 apps).

Native scrollbar behaviour may require drawing outside the window client area, e.g. Unity’s overlay scrollbars from 2011 (I have no idea if that’s changed since then, I haven’t used Ubuntu for years; now that Ubuntu has switched to GNOME 3 I have no idea of the situation either).

Nesting of scrollable areas is very fiddly and varies by platform; see also the CSS overscroll-behavior property.

It can sometimes be desirable to have the scroll behaviour operating outside the rectangle that scrollbars would seem to operate upon; for example a spreadsheet may draw the scrollbars on the cells, but expect scrolling on the window chrome to scroll the sheet.

Focus is a fascinatingly complex area, particularly as regards using the keyboard for scrolling. In general, you want a scrollable area to be sorta-focusable: for example, that clicking inside it activates it, for scrolling purposes, but that if a widget inside it is focused then clicking on the scrollbar shouldn’t focus it. Other times you want keyboard scrolling to affect a particular pane, e.g. in a three-pane email client (mailboxes, messages, active) you want the email pane to be the default (and perhaps only) focused one for keyboard scrolling purposes.

ForLoveOfCats commented 4 years ago

Continuing my thoughts from https://github.com/linebender/druid/issues/799 (see this comment)

My idea is to distil some common scrolling logic into a scrolling component which widgets can use to easily achieve customized yet consistent scrolling behaviour. I want to avoid trying to shoehorn some method of a widget detecting if it is in a Scroll and using commands or something similar to coordinate between the two, which would be even more cumbersome for downstream widgets for which attempting to make a required modification to Scroll to enable some different behaviour is a much more difficult option.

The usage case for customized scrolling are text edit widgets which need to be able to set their own scroll offset when the caret moves out of paint bounds, widgets which need to be able to grab the paintable size of the window where their contents will be painted with offset, and allowing builtin vertical and horizontal scroll list widgets to be independent widgets.

The current Scroll code isn't very long which is good but it is still the product of a fair bit of work as well as trial and error which it would be nice to not require downstream widgets go through or even flat out copy paste. Stuff like detecting scrollbar mouseover, dragging scrollbars, painting scrollbars, a helper function over paint_with_save for widget contents, calculating free space after inset scrollbars have consumed paintable area, ect are all things I want to expose as individual methods which a widget can then plumb into in only a handful of line allowing the widget to instead worry about layout and its own contents while having more control over its scrolling state while the component does the paperwork to track offset, drag state, ect.

I wanted to get any input on this before starting work on a PR as I'd like to avoid working on an approach which does not have support.