Skip to content

Make the ready-made pickers configurable - #338

Merged
smelfungus merged 4 commits into
masterfrom
feat/picker-configurability
Sep 16, 2026
Merged

smelfungus merged 4 commits into
masterfrom
feat/picker-configurability

Conversation

@smelfungus

Copy link
Copy Markdown
Member

The six full pickers took state, modifier, showAlpha, coloringMode, colors and shapes, and nothing else. The English labels and screen-reader strings could not be changed without rebuilding a picker out of its sliders; the dialog forwarded its own title and buttons but nothing to the picker inside it; the string "enabled" appeared nowhere in the public API, so nothing could be greyed out; trackHeight existed only on the low-level ColorSlider; and the custom thumb the README demonstrates could not be handed to a picker at all, though the prose beside it says every slider takes one.

The androidx component guidelines rule out the obvious fix. A String parameter "restricts styling choice" β€” no AnnotatedString, no icon, no choice of component β€” and Material 3 takes DatePicker's title and headline as slots for the same reason. So each slider is a slot, defaulted to the channel slider it names, and localizing is replacing one.

That leaves what a replacement would otherwise have to re-declare. Rather than a receiver scope, which the same guidelines reserve for layout APIs, colors and shapes move into CompositionLocals read in parameter defaults β€” the one use those guidelines sanction, on the condition that the read happens in the default so an explicit argument still wins. A picker provides them, so a replaced slot inherits without the call site forwarding anything.

ColorPickerDimensions joins them, bundling the loose constants. That is what fixes trackHeight, and it fixes it for all twenty-one channel sliders without adding a parameter to any of them: a slider reads the track height from the theme, so setting it once reaches every one.

enabled stays an explicit parameter, because it is state rather than theme and the same guidelines warn against implicit inputs. A picker refuses pointer input at its own level as well as passing enabled down, so a slot replaced to change a label cannot be left live inside a disabled picker by a call site that forgot to forward it. Such a slider is inert but undimmed, which is the one seam in this; the alternative is the implicit input the guidelines warn about. Disabled drawing is a single disabledAlpha on ColorPickerColors rather than a parallel palette, since a picker's track is a gradient of the colours being chosen and there is no fixed colour to swap in.

PickerConfigurationTest covers a replaced slot being drawn, inheriting the picker's colours unforwarded, a themed track height reaching a slider, and a disabled picker refusing a drag on a slot that never saw enabled. The last one needs the drag to land on the track to mean anything, so the enabled case is asserted beside it: without that control the disabled assertions pass whether or not the gesture ever reached a slider.

The fourteen screenshot references are unchanged.

The six full pickers took state, modifier, showAlpha, coloringMode, colors and shapes, and nothing else. The English labels and screen-reader strings could not be changed without rebuilding a picker out of its sliders; the dialog forwarded its own title and buttons but nothing to the picker inside it; the string "enabled" appeared nowhere in the public API, so nothing could be greyed out; trackHeight existed only on the low-level ColorSlider; and the custom thumb the README demonstrates could not be handed to a picker at all, though the prose beside it says every slider takes one.

The androidx component guidelines rule out the obvious fix. A String parameter "restricts styling choice" β€” no AnnotatedString, no icon, no choice of component β€” and Material 3 takes DatePicker's title and headline as slots for the same reason. So each slider is a slot, defaulted to the channel slider it names, and localizing is replacing one.

That leaves what a replacement would otherwise have to re-declare. Rather than a receiver scope, which the same guidelines reserve for layout APIs, colors and shapes move into CompositionLocals read in parameter defaults β€” the one use those guidelines sanction, on the condition that the read happens in the default so an explicit argument still wins. A picker provides them, so a replaced slot inherits without the call site forwarding anything.

ColorPickerDimensions joins them, bundling the loose constants. That is what fixes trackHeight, and it fixes it for all twenty-one channel sliders without adding a parameter to any of them: a slider reads the track height from the theme, so setting it once reaches every one.

enabled stays an explicit parameter, because it is state rather than theme and the same guidelines warn against implicit inputs. A picker refuses pointer input at its own level as well as passing enabled down, so a slot replaced to change a label cannot be left live inside a disabled picker by a call site that forgot to forward it. Such a slider is inert but undimmed, which is the one seam in this; the alternative is the implicit input the guidelines warn about. Disabled drawing is a single disabledAlpha on ColorPickerColors rather than a parallel palette, since a picker's track is a gradient of the colours being chosen and there is no fixed colour to swap in.

PickerConfigurationTest covers a replaced slot being drawn, inheriting the picker's colours unforwarded, a themed track height reaching a slider, and a disabled picker refusing a drag on a slot that never saw enabled. The last one needs the drag to land on the track to mean anything, so the enabled case is asserted beside it: without that control the disabled assertions pass whether or not the gesture ever reached a slider.

The fourteen screenshot references are unchanged.
The pickers gained an enabled parameter with nothing demonstrating it. The sample now carries a switch beside the coloring mode buttons that every slider and plane below reads, so the whole screen greys out together and the difference is something you can look at rather than infer from a parameter list.

Two tests come with it, because refusing a gesture and looking refused are separate things and only the first was covered. Both measure how much colour is left in the track and the plane surface β€” how far apart the channels stay across a row β€” since dimming over the background flattens that rather than moving it in any one direction. The slider and the plane dim through different code, so they are checked separately.

Verified on a Pixel 6 Pro as well: at one colour, with the switch thrown, the plane's saturated corner goes pale while the swatch above it, which belongs to no picker, stays as it was.
The file is about to carry how a disabled control looks as well as whether it takes input, and the old name only covers the second.
Dimming to 38% is the Material convention and it is what a disabled picker did. It is a poor fit for this one control. A picker's track is made of the colours being chosen, so dimming shows paler versions of real colours β€” a wrong answer rather than an absent one β€” and alpha composites against a background the library does not own, so the same value pales the track on white and darkens it on black.

Draining the colour has neither problem: it is background independent, and grey claims nothing. It is not strictly better either, since a fully grey saturation track can read as a legitimate low-saturation choice rather than as switched off.

So both are knobs. disabledAlpha keeps the Material default of 0.38 and disabledSaturation arrives at 1, which leaves colour alone, so nothing changes until a caller asks. Either at its neutral value switches it off, giving dimming, draining, both or neither, and both are applied in one layer so asking for both costs no more than one.

disabledAppearance replaces the two alpha layers ColorSlider used, so the labels dim with the track they belong to rather than staying lit above a greyed-out control.
@smelfungus
smelfungus force-pushed the feat/picker-configurability branch from 16c450d to fb980a6 Compare September 16, 2026 00:49
@smelfungus smelfungus self-assigned this Sep 16, 2026
@smelfungus
smelfungus merged commit 2065d2a into master Sep 16, 2026
2 checks passed
@smelfungus
smelfungus deleted the feat/picker-configurability branch September 16, 2026 00:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant