Class ScrollView

java.lang.Object
limn.scene.Widget<ScrollView>
limn.components.ScrollView
All Implemented Interfaces:
Scrollable

public final class ScrollView extends Widget<ScrollView> implements Scrollable
Generic scroll container: the child is measured unbounded on the scrolling axes, painted offset by the scroll position and clipped to the viewport on those axes; the wheel scrolls whenever there is overflow, and so does a pointer drag that rests near one of its edges, or goes past one (see ScrollBar). The child is physically positioned at (-offsetX, -offsetY), so hit-testing and event coordinates work unchanged.

It clips exactly on an axis it scrolls while the content overflows it there, and nowhere else. Where the content scrolls, what lies past the viewport's edge is out of sight and is cut there. Across an axis that does not scroll, or one the content fits, nothing can move out of sight, so the content may paint past the viewport by a band, the reach of the theme's focus ring unless paintOverflow says otherwise: a control flush with the side of a view that scrolls only vertically keeps its whole focus ring, and so does one flush with its bottom until the content grows past the viewport.

Scroll indicators are the shared ScrollBar (draggable, themed, policy-driven), the same component every scroller uses.

The bars either float over the content or take a gutter of their own (setBarLayout(limn.components.ScrollGutters.Layout)). Which one is right is a property of what is being scrolled, not a taste: an image, a video or a page of prose wants every point of width and loses nothing to a bar that fades over it, while a table, a form or a tree wants its last column readable at all times, and a bar sitting on it is a defect however briefly it shows.

  • Constructor Details

    • ScrollView

      public ScrollView(Widget<?> child)
      Wraps child, scrolling vertically as its content overflows: clipped at the top and bottom while it does, and let past the sides, and past the top and bottom while it fits, by paintOverflow's band.
    • ScrollView

      public ScrollView(Widget<?> child, boolean horizontal, boolean vertical)
      Wraps child, scrolling only on the axes enabled here.
  • Method Details

    • setScrollbarPolicy

      public ScrollView setScrollbarPolicy(ScrollBar.Policy policy)
      Sets the scrollbar visibility policy (default ScrollBar.Policy.AUTO).
    • setBarLayout

      public ScrollView setBarLayout(ScrollGutters.Layout layout)
      Sets whether the bars float over the content or reserve a strip of their own (default ScrollGutters.Layout.OVERLAY).
    • barLayout

      public ScrollGutters.Layout barLayout()
      Whether the scrollbars overlay the content or reserve a gutter.
    • setBarInsets

      public ScrollView setBarInsets(Insets insets)
      Sets the bars' tracks in from the ends of their sides, for content that scrolls under something laid over it: a bar of glass across the top that the content passes beneath keeps its own thumb out from under the bar with the bar's height as the top inset. The vertical bar's track starts top down and ends bottom up; the horizontal bar's starts left in and ends right in, the two swapping sides when the view reads right to left, as a Padding's do. The thumb keeps its proportion of the shorter track. The content, its viewport and the scrolling are unchanged.
      Parameters:
      insets - how far each track is set in; Insets.NONE (the default) for none
      Returns:
      this view
    • barInsets

      public Insets barInsets()
      Returns:
      how far each bar's track is set in from the ends of its side
    • paintOverflow

      public ScrollView paintOverflow(float points)
      Declares how far the content may paint past the viewport across an axis this view does not scroll, or one its content fits, in logical points, in place of the reach the theme gives a focus ring. UI thread only.

      What it is for. On an axis it scrolls and the content overflows, this view clips the content exactly at the viewport, because there the content moves under the edge and what lies past it is out of sight. On an axis it does not scroll, or one the content fits, nothing can move out of sight, and a clip there cut only what a control draws outside its own box at the viewport's edge: the focus ring of a checkbox flush with the side of a view that scrolls vertically, or with its bottom while nothing overflows, a cursor centred on the edge of a colour plane. So across such an axis the content may paint this far past the viewport, on both sides, and is clipped there. A view whose content overflows it both ways lets nothing past; one whose content fits it lets the band past on all four sides, whatever it scrolls. The layout that makes the content overflow an axis or fit it again repaints the wider of the two clips, so that what the content drew in a band that closed is wiped and what it draws in one that opened is drawn at once.

      By default the band is the reach of the focus ring in the theme in effect for this view, Theme.focusRingReach(), the number every layout declares for its children, so a focused control flush with the view's side keeps its whole ring wherever the view has that much room round it. Declare more when the content draws farther and there is room for it, the padding of a card round the view for instance, and 0 for a view that must clip at its viewport on every side. paintOverflowFromTheme() goes back to the theme's number.

      Three things the band never reaches. A strip reserved for a bar (ScrollGutters.Layout.RESERVED) is not painted into: on the side the strip holds, the content stops at the viewport whatever this says. The bars stay clipped to this view's box. And the band is this view's allowance, not its ancestors': a clip above the view still cuts whatever reaches past it.

      It is also what this view declares to partial rendering as painting outside its box, through paintOutset(), which is one number for both axes. So the view declares it only while an axis is left unclipped, and a view whose content overflows it both ways declares nothing; on an axis the view clips the number over-declares by the band, which costs that many points of repaint above and below the view and never a stale pixel. The clip every descendant's damage is clamped to is the clip the content is painted in, band included, so a ring hanging into the band is repainted there when it changes.

      A declaration already in force changes nothing and repaints nothing. A new one repaints this view and the wider of the band it leaves and the band it takes, so that what the content drew in a band that narrowed is wiped, and what it draws in one that widened is drawn at once.

      Parameters:
      points - how far the content may paint past each side of the viewport across an axis this view does not scroll or its content fits, in logical points
      Returns:
      this view
      Throws:
      IllegalArgumentException - when negative, NaN or infinite
    • paintOverflowFromTheme

      public ScrollView paintOverflowFromTheme()
      Goes back to letting the content paint past the viewport by the reach of the theme's focus ring across an axis this view does not scroll or its content fits, which is the default; see paintOverflow(float). Repaints nothing when that is already the declaration. UI thread only.
      Returns:
      this view
    • viewportWidth

      public float viewportWidth()
      The width content is actually shown in: the box, less any reserved gutter. Every offset, clamp and reveal is measured against this rather than against Widget.width(), or a reserved bar would hide the last column it exists to keep clear.
    • viewportHeight

      public float viewportHeight()
      Visible height in logical points, excluding anything the content overflows by.
    • offsetX

      public float offsetX()
      Horizontal scroll position in logical points, 0 at the leading edge: the left edge in a left-to-right subtree and the right edge in a right-to-left one. The range is [0, maxOffsetX()] in both, so "scrolled to the start" is 0 either way.
    • offsetY

      public float offsetY()
      Vertical scroll position in logical points, 0 at the top.
    • maxOffsetX

      public float maxOffsetX()
      Largest valid offsetX(); 0 when the content fits.
    • maxOffsetY

      public float maxOffsetY()
      Largest valid offsetY(); 0 when the content fits.
    • scrollBy

      public void scrollBy(float dx, float dy)
      Scrolls by a delta in logical points (clamped).
    • revealRect

      public void revealRect(float x, float y, float rectWidth, float rectHeight)
      Scrolls the minimum so the rect (in viewport coordinates) becomes visible.
      Specified by:
      revealRect in interface Scrollable
    • scrollTo

      public void scrollTo(float newOffsetX, float newOffsetY)
      Scrolls to an absolute offset (clamped to the content). A caller's write, announced as VALUE/CODE; the wheel, the keys and the bars announce USER through the same seam, and a layout that clamped the offset an ADJUSTMENT. UI thread only.

      Called before the first layout, as a view built already scrolled is, the offset is kept and the first layout clamps it to the content it measures: the first frame shows the view where it was asked to be, never at the top first.

    • 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<ScrollView>
    • onLayout

      protected void onLayout()
      Description copied from class: Widget
      Containers position children here (measure + Widget.layoutBox(float, float, float, float) per child).
      Overrides:
      onLayout in class Widget<ScrollView>
    • onAccessibility

      protected void onAccessibility(Accessibility a)
      One scroll pane, whether or not anything overflows today, carrying where the viewport sits in the content and how much of it shows on each axis.

      The role is unconditional. The pane is the viewport whose clip decides what inside it is on screen at all, and a node that appeared when the content grew and vanished when it shrank would raise a structure change and a destroyed node on every resize; "nothing to scroll" is carried by the facet's two booleans instead. Without the role the pane declares nothing, is never focusable and paints nothing of its own, so the walk deletes it and hoists its content into whatever sits above it, which takes the scroll pattern out of a scene that is mostly scrollable.

      The facet is six primitives read from this widget's own numbers, so describing it allocates nothing and needs no witness. The percent is offset / maxOffset with the offset as this widget publishes it: zero at the leading edge in both directions, the range a positive magnitude either way, which is the facet's own "zero at the start" and is published unflipped under a right-to-left layout by decision, not omission; the mirroring lives in where the content is placed and never in the offset. The view size divides the viewport by the child and not by the content's measured size, because the layout gives the child the larger of the two on a scrolling axis and exactly the viewport on a fixed one, so the fraction never exceeds one and is exactly one on an axis that does not scroll, with no case for it. The zero guards are not defensive: the difference between two trees compares these fields with !=, and a pane laid out at nothing, or described before its first layout, would otherwise publish a not-a-number that differs from itself on every damaged frame and copy the whole tree each time. Scrollable is maxOffset > 0, the predicate the wheel and the Page keys use, and not the bar's half-point slop: what a reader is told can scroll is what a wheel notch would move.

      No name, no state and no action are declared here. The pane holds no string; the axis bits name the one axis of a single value and the pane has two; and the vocabulary has no scroll verb with an axis in it, so nothing is advertised that the pane cannot perform. A scroll republishes the tree through the same damage the repaint rides, so nothing here marks the node dirty by hand.

      Overrides:
      onAccessibility in class Widget<ScrollView>
      Parameters:
      a - the builder for this widget's node
    • clipsChildren

      protected boolean clipsChildren()
      Description copied from class: Widget
      Whether Widget.paintChildren(limn.graphics.Canvas) clips its children to this widget's bounds (scroll views, list viewports, tab strips). Partial rendering uses it to clamp a descendant's Widget.invalidate() damage to the visible region; a widget scrolled out of view damages nothing. Any override that clips in paintChildren should also override this, and one whose clip is smaller than its box for some child should override the four clip accessors below too.
      Overrides:
      clipsChildren in class Widget<ScrollView>
    • paintOutset

      protected float paintOutset()
      The band across an axis this view does not scroll or its content fits, which its content may paint into past the box; nothing while the content overflows it both ways, when the viewport bounds everything. One number for both axes, so on an axis the view clips it over-declares by the band, which repaints a little more and never leaves a stale pixel. O(1), from two flags and the band in force.
      Overrides:
      paintOutset in class Widget<ScrollView>
    • clipX

      protected float clipX(Widget<?> forChild)
      Description copied from class: Widget
      The left edge, in this widget's own coordinates, of the rectangle Widget.clipsChildren() clips child to.

      The whole box by default. A widget that paints one child inside an inset -- a scroll pane whose reserved gutters hold its bars, clipping the content to the viewport and the bars to the box -- answers per child, so that Widget.isShowing() and the damage clamp agree with what it paints: until it did, a descendant lying wholly inside the gutter was published on screen in the very rectangle the bar occupies. Consulted only while clipsChildren() is true, and it must not allocate, because the showing test runs for every node of every accessible walk.

      Overrides:
      clipX in class Widget<ScrollView>
      Parameters:
      forChild - the direct child the clipped descendant is under
      Returns:
      the clip rectangle's left edge
    • clipY

      protected float clipY(Widget<?> forChild)
      Description copied from class: Widget
      The top edge of the rectangle this widget clips child to; see Widget.clipX(Widget).
      Overrides:
      clipY in class Widget<ScrollView>
      Parameters:
      forChild - the direct child the clipped descendant is under
      Returns:
      the clip rectangle's top edge
    • clipWidth

      protected float clipWidth(Widget<?> forChild)
      Description copied from class: Widget
      The width of the rectangle this widget clips child to; see Widget.clipX(Widget).
      Overrides:
      clipWidth in class Widget<ScrollView>
      Parameters:
      forChild - the direct child the clipped descendant is under
      Returns:
      the clip rectangle's width
    • clipHeight

      protected float clipHeight(Widget<?> forChild)
      Description copied from class: Widget
      The height of the rectangle this widget clips child to; see Widget.clipX(Widget).
      Overrides:
      clipHeight in class Widget<ScrollView>
      Parameters:
      forChild - the direct child the clipped descendant is under
      Returns:
      the clip rectangle's height
    • paintChildren

      protected void paintChildren(Canvas canvas)
      Description copied from class: Widget
      Children pass; override to clip (e.g. scroll views).

      Indexed rather than an enhanced for, for the reason Scene's own overlay loop already carries: an iterator here is an allocation per widget per frame, and an idle window is the one place in this toolkit that is not allowed to allocate at all. A subtree of two hundred widgets was paying two hundred short-lived objects a frame to walk a list it can index.

      Overrides:
      paintChildren in class Widget<ScrollView>
    • onMouseEvent

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

      protected void onKeyEvent(KeyEvent event)
      Description copied from class: Widget
      Key events (focused widget first, then ancestors).
      Overrides:
      onKeyEvent in class Widget<ScrollView>