Class ScrollBar

java.lang.Object
limn.scene.Widget<ScrollBar>
limn.components.ScrollBar

public final class ScrollBar extends Widget<ScrollBar>
The one scrollbar shared by every scrollable component (ListView, TextArea, ScrollView, …), so they look and behave identically. It is an overlay widget the host lays out along an edge; it reads/writes scroll through a ScrollBar.Model and handles its own thumb drag and track paging.
  • Themed: the thumb is drawn in Theme.textMuted, so it is visible on both light and dark surfaces.
  • Draggable: drag the thumb, or click the track to page.
  • Policy (setPolicy(limn.components.ScrollBar.Policy)): ScrollBar.Policy.HIDDEN, ScrollBar.Policy.ALWAYS, ScrollBar.Policy.ON_SCROLL (fades in on scroll) or ScrollBar.Policy.AUTO (also while the pointer is active over the host).
  • Thin idle / wide on approach: a slim bar that widens when the pointer is over it or dragging, ready to grab.
  • Mirrored: a horizontal bar reads with its subtree, so right to left the thumb rests at the right end of the track and walks left as the offset grows. A vertical bar's travel is a y and never mirrors; only the side its thumb hugs does, following the side the host puts the strip on.
  • Scrolls under a drag: while something is dragged with the pointer, resting it near an edge of the host's box, or past that edge, scrolls the host along the bar's axis toward that edge, faster the nearer the edge and the farther past it. So a widget of your own that scrolls through a bar scrolls under a drag as the toolkit's do, with no code of its own.
The host calls onScrolled() when it scrolls and onHostActivity() on pointer movement (for AUTO).

The hold is a timer, not an animation. A reveal lasts HOLD_SECONDS past the last scroll or pointer movement and then fades; the fade in and out are transitions, which tick only while a value moves. The hold itself used to be a real-time ticker that returned "still holding" every frame for a second after every pointer move, and a scene keeps its frame pump running while any ticker is alive: moving the pointer over a form inside a scroll view rendered at the display's rate with nothing on screen changing, and resting it over the bar rendered forever. The hold is now a timestamp compared on demand and one delayed task armed for the moment it expires, so a bar that is showing and still asks for no frame at all.

  • Constructor Details

  • Method Details

    • setPolicy

      public ScrollBar setPolicy(ScrollBar.Policy newPolicy)
      When the bar is shown: always, never, or only while the content overflows.
    • policy

      public ScrollBar.Policy policy()
      The current visibility policy.
    • thickness

      public static float thickness()
      The strip thickness the host reserves/lays the bar out with.
    • onScrolled

      public void onScrolled()
      The host scrolled: reveal the bar (ON_SCROLL / AUTO).
    • onHostActivity

      public void onHostActivity()
      The pointer is active over the host: reveal the bar (AUTO). Called for every pointer move over the host, so it must cost nothing while the bar is already showing: a stamp and two comparisons, no invalidation and no frame.
    • refresh

      public void refresh()
      Re-evaluate visibility after the content/overflow changed.

      Content that has just become scrollable reveals the bar for one hold-and-fade, the way an overlay scroller flashes when a pane first has something to scroll. Under ScrollBar.Policy.AUTO (the default, and what a Dialog body uses) the bar is otherwise invisible until the pointer moves over the host or the wheel turns, so a card that was capped opens with nothing at all saying that anything was cut off. The gutter inset says the card is capped; only the bar says it can be scrolled.

      It repaints the bar only when something the thumb is drawn from changed since the last refresh: the content's length, the viewport's, the offset, or the bar's own size. A host calls this from its layout, and a layout can run again with nothing in it changed; a refresh that repainted every time turned each such layout into damage, and a host laid out on every frame into a frame that asked for the next one, for ever. The fades and the widening repaint the bar themselves as they run.

    • onMeasure

      protected Size onMeasure(Constraints constraints)
      Description copied from class: Widget
      Reports the size this widget wants within constraints. Called once per layout pass, and the result is cached against the constraints and the resolved axes (size step, layout direction, locale), so it must be a pure function of them and of this widget's own state.

      Resolve the ControlSize and the LayoutDirection once each here and thread them down; never read either in a constructor. The locale needs no threading: it is in scope, and I18n.locale() answers it wherever text is resolved.

      Specified by:
      onMeasure in class Widget<ScrollBar>
    • onPaint

      protected void onPaint(Canvas canvas)
      Description copied from class: Widget
      Widget's own background/content, in local coordinates.
      Overrides:
      onPaint in class Widget<ScrollBar>
    • onHitTest

      protected @Nullable Widget<?> onHitTest(float localX, float localY)
      Transparent to events while effectively invisible, so it never blocks the content.
      Overrides:
      onHitTest in class Widget<ScrollBar>
      Parameters:
      localX - the point's x in this widget's space
      localY - the point's y in this widget's space
      Returns:
      the widget the point lands on, or null
    • onMouseEvent

      protected void onMouseEvent(MouseEvent event)
      Description copied from class: Widget
      Mouse events (bubbling); call event.consume() when handled.
      Overrides:
      onMouseEvent in class Widget<ScrollBar>
    • onAccessibility

      protected void onAccessibility(Accessibility a)
      Describes the bar as one SCROLL_BAR node whose value is the host's model in the model's own points, or as nothing at all when there is nothing to scroll.

      The node exists exactly when the bar is operable by anyone. Under ScrollBar.Policy.HIDDEN the application asked for no bar, and content that fits its viewport leaves a range with a maximum of zero; in both the bar refuses the pointer at every opacity and paints nothing, and both are structural facts that move only on a policy, content or size change, never on a scroll. The fade is neither: a bar that has faded under ScrollBar.Policy.ON_SCROLL or ScrollBar.Policy.AUTO is still laid out, still visible and still showing, and an assistive technology's action does not travel through the hit test the fade gates. Its set reaches onScrolled() and reveals the bar exactly as the wheel does, so the node stays through the fade rather than appearing and vanishing in a reader's tree a second after every scroll and every pointer move over the host.

      No name, deliberately: the widget holds no string, every platform speaks the role and the orientation for a scroll bar, and it is not a tab stop. No value text: the number is the whole of it. The step is one viewport, the only step the bar has, because a track press pages and there is no line step and no keyboard. INCREMENT and DECREMENT are the two paging directions defined on the value; SET_VALUE is advertised by the facet's presence and is never listed. Nothing here allocates, and nothing here reads the opacity or the widen level, so the frames of a fade damage the bar and publish nothing.

      Overrides:
      onAccessibility in class Widget<ScrollBar>
      Parameters:
      a - the node being described
    • onAccessibilityAction

      protected boolean onAccessibilityAction(Accessible.Action action, Accessible.Argument arg)
      Scrolls the host as an assistive technology asked, through the same private path a track press and a thumb drag take, so the host's model is told the same way, the same clamp runs and the bar reveals itself as it does for the wheel.

      INCREMENT and DECREMENT page by one viewport and grow or shrink the offset in both layout directions: the verbs are defined on the published value, which is a magnitude, and the screen-side flip a mirrored horizontal bar applies to a track press belongs to the pointer alone. SET_VALUE takes an Accessible.Argument.OfValue in the model's own points and refuses anything else, including a value that is not a finite number, which the clamp would pass through untouched. Every verb is refused while the bar's own enabled flag is off, and while the node would not be in the tree at all — the policy hidden, or the content fitting — because an identifier can arrive after the node left, and the scene's gate re-checks the ancestors and not the bar's own two absences.

      Overrides:
      onAccessibilityAction in class Widget<ScrollBar>
      Parameters:
      action - what was asked
      arg - the value for SET_VALUE; ignored by the two pages
      Returns:
      whether the request ran