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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions