Class SegmentedControl

java.lang.Object
limn.scene.Widget
limn.components.SegmentedControl

public class SegmentedControl extends Widget
A row of connected segments with a single selection, a compact alternative to a RadioButton group or tabs. The selected segment is marked by an accent indicator that slides between segments (animated like the TabbedPane indicator). Click a segment, or move with Left/Right when focused. Colours come from Theme; the box, the type and the gutters come from the resolved ControlSize row (SizeTokens), while the track border stays one weight at every step (Strokes).

Width: the track is exactly as wide as its segments need and no wider, centred in whatever box the parent assigns. That matters because the common parent (a column with STRETCH) hands every child the full row, and a pill track drawn edge to edge with four short segments huddled at its left end reads as broken. The margins either side belong to what is behind this control: a click landing on one selects nothing.

Overflow: when the segments do not fit, the strip becomes a clipped, scrollable viewport with a chevron at each edge, the dead side drawn disabled rather than dropped, since dropping it would resize the viewport and make the strip jump at the ends. The mouse wheel scrolls it, a chevron click scrolls by most of a viewport, and selecting a segment by any route (click, Left/Right, or setSelectedIndex(int)) scrolls it into view. Nothing is ever painted outside the track. There is no all-segments popup, which is the one part of the TabbedPane overflow story this control does not carry: its labels are handed in by the caller and are few and short by construction, and a control that fits in a toolbar row has nowhere to put a third piece of chrome.

Reading direction: the strip is a row of items in reading order, so it mirrors. The segments are placed from the edge reading starts at, the back chevron sits in that same gutter and points back towards the first segment, and Left selects the segment that is visually on the left: a keyboard that walked the other way from the pointer would be arguing with the screen. Home and End do not mirror, because they name the first and last segment rather than a side. Only the placement moves; the track, the viewport and each label inside its own segment are symmetric and stay where they are, and there is no mirror transform anywhere here.

This or a TabbedPane? Ask who owns the content. A segmented control owns none: it takes labels and hands back an index, so the caller keeps the content and switches it, which is what makes it the one that belongs in a toolbar row, a form, or inside another widget that already owns what changes. A tabbed pane is the opposite: you hand it the pages and it owns them, laying out and painting only the selected one, and carrying the overflow story that owning an unknown number of pages requires. Looking alike is not the test; a row of labels is what both are, and what they do is where they split.

  • Constructor Details

    • SegmentedControl

      public SegmentedControl(List<String> segments)
      A control over the given segment labels; the first is selected. Needs at least one.
  • Method Details

    • onSelect

      public SegmentedControl onSelect(Consumer<Integer> listener)
      Called with the chosen index whenever the selection changes: a click, an arrow key and a setSelectedIndex(int) from code all arrive here.
    • selectedIndex

      public int selectedIndex()
      The selected segment's index; never negative, since the control refuses an empty segment list and so always has exactly one selection. There is nothing to clear to.
    • setSelectedIndex

      public SegmentedControl setSelectedIndex(int index)
      Selects a segment, scrolls it into view and fires onSelect; code and a click take the same path, so a listener sees every change either way. Re-selecting the current segment still reveals it (a caller asking for a segment is asking to be shown it) but changes nothing and fires nothing; that early return is what keeps two controls bound to each other from recursing, so do not remove it. UI thread only.
      Parameters:
      index - a segment in [0, segmentCount)
      Throws:
      IndexOutOfBoundsException - if index is not a segment. Arrowing past an end is not this: keyboard traversal stops at the end rather than raising.
    • 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
    • baselineOffset

      protected float baselineOffset()
      Description copied from class: Widget
      Distance from this widget's top edge to its first text baseline, in logical points: the alignment reference for Flex.CrossAlignment.BASELINE. Default height(): align on the bottom edge, the correct fallback for a widget with no text. Text-bearing components override with the expression they already paint with, (height() - metrics.height()) / 2 + metrics.ascent().

      Valid only once this widget has been given a box. Flex guarantees that: it lays every child of a BASELINE line out at cross position 0 first, reads the baselines, then repositions with Widget.moveChild(limn.scene.Widget, float, float), which moves without re-running layout.

      Overrides:
      baselineOffset in class Widget
    • 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
    • onPaint

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

      protected float paintOutset()
      The focus ring reaches Strokes.FOCUS_RING_OUTSET beyond the box, and the scene inflates a widget's damage by 1 + paintOutset(). Without this the ring's fade-out sheds stale pixels under partial rendering.

      Declares reach only; it moves no ink.

      Overrides:
      paintOutset in class Widget
    • onFocusGained

      protected void onFocusGained()
      Description copied from class: Widget
      Called when this widget takes keyboard focus. Default: nothing.
      Overrides:
      onFocusGained in class Widget
    • onFocusLost

      protected void onFocusLost()
      Description copied from class: Widget
      Called when this widget loses keyboard focus. Default: nothing.
      Overrides:
      onFocusLost in class Widget
    • onMouseEvent

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

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