Tryst::SegmentedControl
Inherits Tryst::OwnerDrawnWidget < Reference < Object
An iOS/web-style segmented control for tryst: a rounded pill row of mutually exclusive text options, with a sliding accent highlight (~200ms eased) behind whichever one is selected - replacing a line of ttk radio buttons. Built on OwnerDrawnWidget and rendered through tryst-vector, following the same App-layer pattern as Switch and ValueSlider: no ui.<type>, no bind: (see CUSTOM_WIDGETS.md for why a stateful, animated widget doesn't fit the WidgetType/AppContract seam). Two-way sync with a Tryst::UI::Var is manual wiring, same as both siblings' own READMEs document.
control = Tryst::SegmentedControl.new(app, options: ["Day", "Week", "Month", "Year"], selected: "Week")
control.pack
control.on_action { |value| puts "now on #{value}" }
Segment width is each option's own natural text width plus fixed padding, not a forced equal split across every segment - the approved design/mock.html reference uses natural width throughout, so this is the shipped behavior, not a placeholder.
Options must be non-empty and non-duplicate (a duplicate label would make #selected=/#disable_segment's own String-keyed API ambiguous - there would be no way to say which one was meant).
Constants
How far the sliding highlight sits inset from the pill's own edge, on all four sides equally. Equal insets on every side make the highlight's own rounded end caps exactly CONCENTRIC with the pill's own rounded caps, just smaller by this amount - which contains the highlight within the pill by construction, at every segment position, not just the ones eyeballed. Confirmed against design/mock.html's own first pass: the mock's highlight shadow originally reached further than this inset and visibly spilled past the pill at the two end segments - shrunk before that mock was approved. Do not add a shadow/glow to the highlight wider than this inset without re-deriving the same containment math.
Shared between #draw_hover (the canvas-drawn tint) and #position_labels (the opaque label sitting on top of it) so both can never drift apart - see #position_labels's own comment on why they have to agree exactly, not just be "close".
Space reserved around the pill's own bounds for the focus ring, which - unlike the sliding highlight below - is SUPPOSED to draw past the pill's own edge (the normal, accessible thing for a focus ring to do, same as Switch's own MARGIN comment). Overhang past the pill edge is exactly FOCUS_RING; MARGIN sits a little above that for antialiasing slack, the same "AA-clipped by the buffer's own bounds" issue Spinner's EDGE_MARGIN and Switch's own MARGIN both already document.
Constructors
options must be non-empty and non-duplicate (a duplicate would make #selected=/#disable_segment's own String-keyed API ambiguous). accent/disabled_dim follow the same convention as Switch/ ValueSlider. height sizes the pill only - segment width always comes from each option's own measured text, never from height.
animate_set: whether a PROGRAMMATIC #selected= slides the highlight (default) or jumps straight to the new segment. A user selection always animates either way - this knob only covers the Crystal-driven side of the same split #selected= already draws, and matches Switch's own animate_set:. Pass false for a control whose selection gets pushed in from saved state: an app populating a settings panel during startup otherwise arms one 16ms tween per control, animating a window nobody is looking at yet.
Instance methods
Releases the underlying canvas (and its Photo, if any) now, rather than waiting for a collection - same contract as Photo#delete. Bind-callback cleanup for #canvas's own path happens for free (see App's setup_destroy_cleanup, installed unconditionally for every widget); only this class's own extra state (running tweens) needs explicit teardown here.
Disables one segment by its own option text - dimmed, unclickable, and skipped by Left/Right navigation, while the rest of the control stays interactive. Independent of #disabled= (the whole- control switch inherited from OwnerDrawnWidget); either or both can be set at once. Raises ArgumentError if option isn't one of #options.
Re-enables a segment previously disabled via #disable_segment. Raises ArgumentError if option isn't one of #options.
Fires on every user-driven selection change (click, Left/Right) - never for a programmatic #selected=.
Subclasses draw here - called after every resize and every state change (#hover?/#pressed?/#focused?/#disabled? all changing). Given no arguments deliberately: #canvas's own current width/height (via #canvas.width/#canvas.height, i.e. real winfo queries) are always the authority, not a value that could go stale between when a resize fired and when this actually runs.
Whether option is currently disabled via #disable_segment - always false unless that's been called for it. Raises ArgumentError if option isn't one of #options.
Sets the selection programmatically - animates the same as a user selection unless the control was built with animate_set: false, in which case the highlight jumps straight to the new segment. Never fires #on_action either way (same "user action vs Crystal-driven set" split every other stateful widget in this codebase draws). Raises ArgumentError if value isn't one of #options.