Class SplitPane

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

public final class SplitPane extends Widget<SplitPane>
Two panes and a divider the user drags to share the space between them.

 SplitPane split = SplitPane.horizontal(sidebar, editor).setRatio(0.25f);
 split.setMinimums(180, 320);          // neither pane below this
 split.onRatioChange(app::rememberLayout);
 

The orientation names the axis the panes are arranged along, not the divider: horizontal(limn.scene.Widget<?>, limn.scene.Widget<?>) puts them side by side, so the divider is the vertical line between them.

The first pane takes the leading edge. Reading left to right that is the left, and reading right to left it is the right; the ratio, the minimums and the divider's position are all magnitudes along the divided axis and mean the same thing either way. A stacked split has no reading axis at all and is untouched by the layout direction.

The split is a ratio, not a width, so growing the window grows both panes. An application that wants a sidebar to keep its width while the other pane absorbs every new pixel re-sets the ratio from its own resize handling; there is no weight knob, because which pane absorbs a change is a per-layout decision rather than a per-splitter one.

setMinimums(float, float) keeps a pane from being dragged away entirely; the default floor is a hit target's worth on each side. Pass 0 for a pane that is allowed to collapse to nothing, such as a preview the user is meant to be able to dismiss.

The divider is thin to look at and thick to hit: it paints a hairline that thickens into the accent on hover, and catches a band a few points wider than the gutter, overlapping both panes. Focus, hover and drag are the same mark in three tints of the accent, each lighter than the one before.

The divider is not a tab stop unless asked; see setDividerFocusable(boolean), which is what turns on the keyboard: arrows by a visible step, by one point with Shift, Home and End to the minimum on either side.

Three panes are two splitters: nest one in the other.

  • Method Details

    • onHitTest

      protected @Nullable Widget<?> onHitTest(float localX, float localY)
      The divider first, so it wins where its grab band overlaps the panes, which is the whole point of the band; everywhere else the panes as ever.
      Overrides:
      onHitTest in class Widget<SplitPane>
      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
    • paintChildren

      protected void paintChildren(Canvas canvas)
      The panes and then the divider, so its line is drawn over a ring that reaches the gutter.
      Overrides:
      paintChildren in class Widget<SplitPane>
    • horizontal

      public static SplitPane horizontal(Widget<?> left, Widget<?> right)
      Two panes side by side, divided by a vertical line. The first takes the leading edge, so the parameter names describe the default reading direction and not the screen: in a right-to-left layout left is the pane on the right.
    • vertical

      public static SplitPane vertical(Widget<?> top, Widget<?> bottom)
      Two panes stacked, divided by a horizontal line.
    • setRatio

      public SplitPane setRatio(float newRatio)
      Sets the share of the space given to the first pane, 0..1. Clamped by setMinimums(float, float) at layout, not here: the minimums are in points and the space is not known until there is a box to divide. Programmatic, so it does not fire onRatioChange.
    • ratio

      public float ratio()
      Returns:
      the requested share of the first pane, before any minimum applies
    • setMinimums

      public SplitPane setMinimums(float first, float second)
      Floors, in points, under which neither pane may be dragged. 0 lets that pane collapse entirely. When the box is too small for both floors the first one wins and the second pane takes what is left: a split that cannot honour both still has to lay out.

      first is the leading pane's floor and second the trailing one's, whichever way the layout reads; both are extents along the divided axis, so neither swaps when the direction does.

    • onRatioChange

      public SplitPane onRatioChange(@Nullable FloatConsumer listener)
      The application's response to the user moving the divider: a drag, a key, an assistive technology's step. Never for setRatio(float); to hear every move whatever caused it, watch the split instead.
      Parameters:
      listener - the handler, or null to clear the slot
      Returns:
      this split
      Throws:
      IllegalStateException - if a handler is already registered
    • handleUserChange

      protected void handleUserChange(Change.Aspect aspect)
      Description copied from class: Widget
      Runs this widget's application handler for a change it has just announced. Called by Widget.runHandler(limn.scene.Change), and so by Widget.notifyChange(limn.scene.Change), only for a USER origin and after every watcher has run.

      A component overrides it once and switches on the aspect, reading its own accessors for the payload: a slider passes value(), a combo box selectedIndex(), a text field text(). A component with no handler does not override it.

      An override that does not recognise the aspect must call super, because a subclass of a component that has a handler inherits that component's dispatch through this method: a SearchField that overrode this for its own aspect and did not chain would silently delete TextField's. The ordering lives in the base class, this is the one link in it a subclass can break, and the contract test drives every subclass through its parent's gestures to catch one that does.

      Overrides:
      handleUserChange in class Widget<SplitPane>
      Parameters:
      aspect - what changed; the origin is USER or this is not called
    • setDividerFocusable

      public SplitPane setDividerFocusable(boolean focusable)
      Makes the divider a tab stop, off by default. A split is reached with the pointer, and a tab stop that only moves a boundary costs every keyboard user a stop on the way to the panes' own controls, so the keyboard support is here to be turned on by a layout that wants it, not to be paid for by one that does not.

      On, the divider takes focus by Tab and by a click on its band, shows the accent at its focus tint, and answers the arrows, Shift+arrow, Home and End. Off, nothing else changes: dragging, the cursor, the hover tint and the grab band are the pointer's, and none of them asks about focus.

      The flag is the divider's rather than the split's, because the divider is what focus would land on; the split itself is a container and never a tab stop. UI thread only.

    • isDividerFocusable

      public boolean isDividerFocusable()
      Returns:
      whether the divider takes keyboard focus (false by default)
    • orientation

      public SplitPane.Orientation orientation()
      Which axis the panes are arranged along.
    • 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<SplitPane>
    • 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<SplitPane>
    • onAccessibility

      protected void onAccessibility(Accessibility a)
      Description copied from class: Widget
      Says what this widget is, for an assistive technology.

      Called on the UI thread inside the publish step, with this widget's own locale in scope, and never from a paint. A widget that overrides nothing still gets a node with its bounds, its enabled, focusable, visible and showing states, its language, its children in tree order, the focus and scroll-into-view verbs when it is focusable, and its tooltip as a name when nothing else supplied one. What it does not get is a role: a widget that declares no role, no name, no description, no state, no verb and no facet is scaffolding, and the tree removes it and hoists its children in its place, so that a screen reader hears the controls rather than the boxes.

      Nothing here may allocate. The builder's setters take a facet's fields rather than a facet, and a name is handed over as the localizable string this widget holds rather than as a resolved one, so that a frame which damaged this widget and changed nothing about it costs a comparison and no memory at all. Accessibility states the two rules and what breaks when they are ignored.

      Overrides:
      onAccessibility in class Widget<SplitPane>
      Parameters:
      a - the node being described; write into it, and never keep it