Skip to content

MacosPopupButton: expose trigger height + caret inset (or ControlSize) for parity with MacosTextField #592

Description

@BruhGreg

Problem

MacosPopupButton renders its collapsed trigger at a hard-coded height with a hard-coded internal padding, which makes it impossible to visually align the control with a sibling MacosTextField or to give the up/down caret breathing room from the trigger's right edge.

Specifically, in lib/src/buttons/popup_button.dart:

const double _kPopupButtonHeight = 20.0;  // line 17

// Inside the collapsed trigger's build:
Container(
  padding: const EdgeInsets.only(left: 8.0, right: 2.0),
  height: _kPopupButtonHeight,
  child: Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    mainAxisSize: MainAxisSize.min,
    children: <Widget>[
      innerItemsWidget,
      Padding(
        padding: const EdgeInsets.only(left: 8.0),
        child: SizedBox(
          height: _kPopupButtonHeight - 4.0,
          width: _kPopupButtonHeight - 4.0,
          child: CustomPaint(painter: _UpDownCaretsPainter(...)),
        ),
      ),
    ],
  ),
)

Three constants are fixed by the library and cannot be overridden from user code:

  1. _kPopupButtonHeight (trigger container height, 20px).
  2. The Container's EdgeInsets.only(left: 8, right: 2) — particularly the 2px on the right, which pins the caret against the trigger's right border.
  3. The caret's own Padding(left: 8) between label and caret.

Why external wrappers don't help

Wrapping the widget in SizedBox(height: N) makes the outer box taller but leaves the native trigger at 20px centered inside. Wrapping in Padding(right: X) or Row(children: [Expanded(child: MacosPopupButton(...)), SizedBox(width: X)]) doesn't visibly push the caret inward: the internal Row uses MainAxisSize.min, so it shrink-wraps; the Container still paints its decoration at the stretched parent width, but the caret's position is set by spaceBetween against the Row's intrinsic content — the right: 2 of the inner Container is applied inside that paint region, so the caret ends up ~2px from the painted right edge regardless of the outer constraints.

Which means: short of forking the widget, users cannot visually match MacosPopupButton to a sibling MacosTextField (common in settings-style forms) or to Apple's own System Settings popups, which have ~28–32px trigger height.

Real-world use case — same-width controls, mismatched heights

A settings-style form where two popups and a text input all span the same content width should render at the same height. Right now:

Field A    ┌──────────────────────────┐
           │  Selected option       ▾ │   ← 20px tall (MacosPopupButton)
           └──────────────────────────┘

Field B    ┌──────────────────────────┐
           │  Selected option       ▾ │   ← 20px tall
           └──────────────────────────┘

Field C    ┌──────────────────────────┐
           │                          │
           │  Typed value             │   ← 32px tall (MacosTextField)
           │                          │
           └──────────────────────────┘

Width matches across all three. Height does not — the two popups are noticeably shorter than the text field, and stacked together the eye reads it as visual inconsistency rather than a style choice.

Caret-inset subproblem, same diagram

Zooming in on the right edge of a popup's collapsed trigger:

┌──────────────────────────┐
│  Selected option       ▾ │   ← caret ~2px from border; visually stuck
└──────────────────────────┘

┌──────────────────────────┐
│  Selected option     ▾   │   ← desired: caret ~6–8px from border
└──────────────────────────┘

The AppKit convention (and what macOS System Settings ships) is closer to the second drawing. The EdgeInsets.only(right: 2.0) in the hardcoded container padding makes the first drawing unavoidable.

Proposed API

Two non-exclusive options, either works:

Option A — Add ControlSize support (matches #408)

MacosPopupButton(
  controlSize: ControlSize.regular, // or .small / .large
  ...
)

ControlSize.regular bumps the trigger height to ~28px, .large to ~32px, matching the AppKit convention, with the inner padding and caret inset scaled proportionally.

Option B — Explicit size/padding overrides

MacosPopupButton(
  buttonHeight: 32,
  triggerPadding: EdgeInsets.only(left: 10, right: 8),
  caretLeftPadding: 10,
  ...
)

Option A is more idiomatic and composable with the existing enum that #408 is tracking for other widgets. Option B is a larger override API but ships faster.

Versions

  • macos_ui: 2.2.2 (reproducible on dev at HEAD as of this filing)
  • flutter: 3.41.x stable
  • macOS 15.7.4 (Intel)

References

  • Umbrella issue for ControlSize adoption: #408
  • ControlSize enum introduction: #386
  • Original popup button PR: #177

Happy to contribute a PR if the maintainers agree on the API direction (either option works — likely Option A to align with #408).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions