The combo drops its padding and turns portrait — a 4:5 field puts the symbol dead center
Owner's first review of the combo rework (2026-08-09): remove the face padding, make the field taller and narrower at about a 4:5 width:height ratio, and center the symbol glyph in its face. All three land in ComboFieldMetrics, so ColorComboControl and SymbolComboControl stay the identical shape they were built to share. - ComboFieldMetrics grows a width figure (height * widthToHeightRatio, 0.8), replacing NSView.noIntrinsicMetric — every combo now carries its own taller, narrower intrinsic size instead of stretching to whatever a caller's frame proposed. - facePaddingH/facePaddingV/glyphPadding are gone; a face fills its zone edge to edge. glyphPointSize is now whichever of the face's own width/height is smaller, with nothing subtracted for padding that no longer exists. - SymbolComboControl.drawFace centers the glyph on both axes — it only ever centered vertically before, despite its own doc comment claiming otherwise. - ComboFieldControl.drawTrigger bounds its chevron square by the smaller of the trigger strip's own width/height, not height alone, since the strip is no longer close to square once the field is much taller than it is wide. - CardSidebarSections drops the sidebar's old '* 0.55' fixed-width frame on both combo rows; each control now sizes itself, and both anchors (card sidebar, board popover) compose the narrower field with no other changes needed. - ComboFieldMetricsTests updated for the new figures, plus a ratio-holds-at-every-size test and a rewritten glyph-fit test matching the no-padding rule. Verification: xcodebuild build succeeded; xcodebuild test -only-testing:KanbanTests — 3220 tests in 559 suites, 3 failures, all PointerLatencyTests (documented locked-screen environmental mode, confirmed unrelated by isolated rerun). Pixel verification unexercised — same locked-screen constraint the first pass hit. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
+67
-36
@@ -32,36 +32,53 @@ import AppKit
|
||||
/// replaces did not follow. Every figure is a multiple of the body point size, chosen to reproduce
|
||||
/// the numbers `ColorComboControl` shipped with at the standard 13pt body — with one deliberate
|
||||
/// exception, `height`.
|
||||
|
||||
// MARK: - Metrics
|
||||
|
||||
/// The combo's geometry, as a pure value — no `NSView`, so the parity claim ("the two controls are
|
||||
/// the same size") is a thing a test can assert rather than a thing a screenshot suggests.
|
||||
///
|
||||
/// ### The 2026-08-09 iteration: no padding, a taller narrower field, a centred glyph
|
||||
///
|
||||
/// The owner's first review of the shape above: "remove padding from the combo. make the field
|
||||
/// taller and narrower. almost square. about 4:5 ratio. for symbol picker center the symbol." Three
|
||||
/// changes, all in this file —
|
||||
///
|
||||
/// - **No padding.** `facePaddingH`/`facePaddingV`/`glyphPadding` are gone; a face draws to fill its
|
||||
/// whole zone, not a rect inset inside it. A swatch is now a solid patch flush with the field's own
|
||||
/// edges rather than a bar floating in a ring of `controlColor`.
|
||||
/// - **`width` joined `height` as a real figure**, replacing `NSView.noIntrinsicMetric`. The first
|
||||
/// pass let every caller stretch the control to whatever width it had lying around — 93pt in the
|
||||
/// card sidebar, 120pt's fallback in the board popover — which is the wide-bar shape this iteration
|
||||
/// undoes. `width` is `height` scaled by `widthToHeightRatio`, never its own independent figure, so
|
||||
/// the two cannot drift into some other proportion at a text size nobody checked.
|
||||
/// - **The glyph centres in both axes.** It always claimed to (`SymbolComboControl.drawFace`'s own
|
||||
/// doc comment said "centred") but the code only ever centred it vertically and offset it from the
|
||||
/// leading edge by the swatch's own horizontal padding — dead code once that padding left, and the
|
||||
/// wrong rect even before it did.
|
||||
struct ComboFieldMetrics: Equatable {
|
||||
|
||||
/// **4:5, width:height** — "almost square," the owner's own phrase. Applied to `height` rather
|
||||
/// than carried as its own figure, so `width` is a restatement of `height` and not a second
|
||||
/// number that could quietly stop agreeing with it.
|
||||
static let widthToHeightRatio: CGFloat = 0.8
|
||||
|
||||
/// The control's height.
|
||||
///
|
||||
/// **1.4 em is 18pt at the standard body, where this shape shipped at a flat 14** — the one
|
||||
/// figure here that is not a restatement of an old constant. 14 was tuned for a colour bar and
|
||||
/// is a fine height for one; a *glyph* in a 14pt field, once the face's own padding is taken
|
||||
/// out, is six points tall and unreadable. 18 still reads as the compact control the original
|
||||
/// rework was after (a regular `NSPopUpButton` is about 25) and leaves the glyph room to be a
|
||||
/// glyph.
|
||||
/// **2.75 em is 36pt at the standard body, where the first pass shipped 1.4 em / 18pt.** 18 was
|
||||
/// tuned for a *wide* bar; reaching the 4:5 ratio without starving the trigger of the room a
|
||||
/// legible chevron needs (`triggerWidth` below, unchanged since the first pass) takes a taller
|
||||
/// field than that, which is the same request the owner made in words.
|
||||
var height: CGFloat
|
||||
/// The trigger strip at the trailing edge, full height.
|
||||
/// The whole control's width — `height` scaled by `widthToHeightRatio` and rounded. Every caller
|
||||
/// now gets this same narrow field unless it hands SwiftUI an explicit finite proposal of its
|
||||
/// own, which `sizeThatFits` on each representable still honours exactly as before.
|
||||
var width: CGFloat
|
||||
/// The trigger strip at the trailing edge, full height. Unchanged from the first pass: this
|
||||
/// iteration narrows the *field*, not the door onto the quick list, and 1.25 em already gave the
|
||||
/// trigger square room for a legible chevron.
|
||||
var triggerWidth: CGFloat
|
||||
/// The trigger square's inset from the strip's height — kept off the *ring* width rather than
|
||||
/// the face's larger padding, so the indicator stays a legible square instead of shrinking with
|
||||
/// every padding tweak the face takes.
|
||||
/// a face padding that no longer exists, so the indicator stays a legible square rather than
|
||||
/// growing to fill a strip that is now much taller than it is wide (`ComboFieldControl.
|
||||
/// drawTrigger`'s `min(width, height)`, which is what keeps this inset meaningful once the strip
|
||||
/// stopped being roughly as wide as the control is tall).
|
||||
var triggerInset: CGFloat
|
||||
/// The face's inset inside its zone, asymmetric and user-tuned: a wider berth at the sides than
|
||||
/// above and below, so a swatch reads as a bar sitting in the field rather than filling it wall
|
||||
/// to wall. The space comes out of the face — the control's overall size is untouched.
|
||||
var facePaddingH: CGFloat
|
||||
var facePaddingV: CGFloat
|
||||
/// A glyph face's own inset, much tighter than a swatch's: a symbol *is* the face, where a
|
||||
/// swatch is a sample sitting in one.
|
||||
var glyphPadding: CGFloat
|
||||
/// The face's and the trigger square's radius.
|
||||
var cornerRadius: CGFloat
|
||||
/// The field's own radius — a point more than the face's, so the two rounded rects run
|
||||
@@ -70,13 +87,12 @@ struct ComboFieldMetrics: Equatable {
|
||||
|
||||
static func metrics(bodyPointSize: CGFloat) -> ComboFieldMetrics {
|
||||
func em(_ multiple: CGFloat) -> CGFloat { max(1, (bodyPointSize * multiple).rounded()) }
|
||||
let height = em(2.75)
|
||||
return ComboFieldMetrics(
|
||||
height: em(1.4),
|
||||
height: height,
|
||||
width: max(1, (height * widthToHeightRatio).rounded()),
|
||||
triggerWidth: em(1.25),
|
||||
triggerInset: em(0.15),
|
||||
facePaddingH: em(0.54),
|
||||
facePaddingV: em(0.38),
|
||||
glyphPadding: em(0.15),
|
||||
cornerRadius: em(0.23),
|
||||
fieldRadius: em(0.31)
|
||||
)
|
||||
@@ -88,9 +104,17 @@ struct ComboFieldMetrics: Equatable {
|
||||
@MainActor
|
||||
static var current: ComboFieldMetrics { metrics(bodyPointSize: CardWindowMetrics.bodyPointSize) }
|
||||
|
||||
/// The glyph's point size inside a face — the face zone's height less its padding, which is
|
||||
/// what makes a symbol fill the control instead of floating in it.
|
||||
var glyphPointSize: CGFloat { max(1, height - 2 * glyphPadding) }
|
||||
/// The face zone's own width — the whole field less the trigger strip. Computed here rather than
|
||||
/// read off a live `NSView`'s `faceZone` so it is a plain function of `bodyPointSize`, available
|
||||
/// to a pure test and to `glyphPointSize` below with no control on screen.
|
||||
var faceWidth: CGFloat { max(0, width - triggerWidth) }
|
||||
|
||||
/// The glyph's point size inside a face — no padding subtracted now, so this is simply whichever
|
||||
/// of the face's two dimensions is smaller. That is `faceWidth` at every body size this control
|
||||
/// ships at (the field reads taller than wide by construction); `height` only becomes the binding
|
||||
/// one if a future caller widens `triggerWidth` past `width`'s own share of it, which the `min`
|
||||
/// guards against turning into an oversized, clipped glyph.
|
||||
var glyphPointSize: CGFloat { max(1, min(faceWidth, height)) }
|
||||
}
|
||||
|
||||
// MARK: - The control
|
||||
@@ -135,10 +159,12 @@ class ComboFieldControl: NSControl {
|
||||
}
|
||||
}
|
||||
|
||||
/// Width is `noIntrinsicMetric`: the control obeys whatever SwiftUI proposes, so a sidebar row
|
||||
/// sizes it and an unconstrained pass falls back to the representable's own default.
|
||||
/// Both dimensions are the metrics' own now. Width stopped being `NSView.noIntrinsicMetric` in
|
||||
/// the 2026-08-09 iteration that gave the field a fixed, taller-than-wide shape instead of
|
||||
/// whatever a caller's frame happened to propose; a representable's `sizeThatFits` still honours
|
||||
/// an explicit finite proposal over this default, exactly as before.
|
||||
override var intrinsicContentSize: NSSize {
|
||||
NSSize(width: NSView.noIntrinsicMetric, height: metrics.height)
|
||||
NSSize(width: metrics.width, height: metrics.height)
|
||||
}
|
||||
|
||||
// MARK: Zones
|
||||
@@ -204,11 +230,16 @@ class ComboFieldControl: NSControl {
|
||||
path.stroke()
|
||||
}
|
||||
|
||||
/// The trigger: a small vertically-centred rounded square, `controlAccentColor`-filled, with a
|
||||
/// white `chevron.up.chevron.down` centred inside — the standard `NSPopUpButton` indicator's own
|
||||
/// look, redrawn here since this control has no bezel of its own to borrow one from.
|
||||
/// The trigger: a small centred rounded square, `controlAccentColor`-filled, with a white
|
||||
/// `chevron.up.chevron.down` centred inside — the standard `NSPopUpButton` indicator's own look,
|
||||
/// redrawn here since this control has no bezel of its own to borrow one from.
|
||||
///
|
||||
/// **The side is bounded by both of the strip's own dimensions**, not just its height: the first
|
||||
/// pass only took `triggerRect.height` because the strip was never far from square, but the
|
||||
/// 2026-08-09 taller-narrower field can make a strip much taller than it is wide, and a side taken
|
||||
/// from height alone would then ask for a square wider than the strip itself.
|
||||
private func drawTrigger() {
|
||||
let side = triggerRect.height - 2 * metrics.triggerInset
|
||||
let side = min(triggerRect.width, triggerRect.height) - 2 * metrics.triggerInset
|
||||
let square = NSRect(
|
||||
x: triggerRect.midX - side / 2,
|
||||
y: triggerRect.midY - side / 2,
|
||||
|
||||
Reference in New Issue
Block a user