Two widths, one height — the symbol picker squares to 5:4, colour holds 6:4, and None's slash follows the swatch out of the grid

The owner's follow-up review on the shipped rectangles (Pipeline card
5004c540): height was close, so it stays exactly 2.1em on both controls;
width now diverges by role via PickerRectMetrics.WidthRatio (color 1.5,
symbol 1.25) instead of one shared multiplier. The symbol glyph gets a
small em-derived inset back (SymbolGlyphControl.glyphInset) — a sliver of
breath, not the old padded ring — applied to the face rect before sizing
and fitting, still centred on the same midpoint. The collapsed colour
swatch reuses ColorSwatchNoneStrike's own geometry to draw the popover's
None slash whenever the stored value is nil or unresolvable, rather than
sitting empty. Both mounting anchors (the card sidebar's side-by-side
columns, the board popover beside the rename field) pick up .fixedSize()
so the controls render at their own intrinsic size instead of stretching
into whatever slack an HStack proposal leaves them.

KanbanTests/SymbolCatalogTests.swift and ColorSwatchPickerTests.swift
updated for the per-role widths, the shared-height claim, the glyph
inset rule, and the None-face predicate.
This commit is contained in:
2026-08-09 21:37:29 -04:00
parent ac4f8b9050
commit f191e5c3ce
7 changed files with 264 additions and 63 deletions
+38 -13
View File
@@ -22,17 +22,28 @@ import AppKit
/// ### Sizing (the same ruling): "50% taller... and about 4:6 ratio of height to width"
///
/// `height` is **2.1 em** 50% over the two-zone chrome's last-shipped 1.4 em (18pt 27pt at the
/// standard 13pt body). `width` is `height` **times 1.5** the 4:6 ratio stated as a multiplier on
/// `height` rather than as its own independent em figure, so the ratio holds exactly (to rounding) at
/// standard 13pt body). `width` is `height` **times a per-role ratio**, stated as a multiplier on
/// `height` rather than as its own independent em figure, so each ratio holds exactly (to rounding) at
/// every body size instead of two multiples that could drift apart the way `ComboFieldMetrics`'
/// `height` and `width` once did (that file's own retired header told that story). 10-accessibility.md's
/// full-relative-scaling rule, unchanged: every figure is a multiple of the body point size.
///
/// ### Two widths, one height (the owner's 2026-08-10 follow-up, verbatim)
///
/// "width to height ratio should be closer to 6:4 [for color] ... the symbol picker should be even
/// more square at ratio of 5:4 or so ... the height for the two pickers should be exactly the same."
/// `height` stays a single shared figure nothing here lets it diverge between the two controls
/// while `width` becomes a function of which picker is asking, via `WidthRatio`. The ratio lives as a
/// multiplier rather than two hand-rounded em widths for the reason above: two independent em figures
/// can drift apart a rounding step at a time as the body size changes, the exact failure this file's
/// own `height`/`width` split was already written to avoid for the first ratio.
struct PickerRectMetrics: Equatable {
/// The control's height 2.1 em, 27pt at the standard 13pt body.
/// The control's height 2.1 em, 27pt at the standard 13pt body. **Identical between the two
/// pickers by the owner's own ruling**; nothing in this type lets a caller diverge it by role.
var height: CGFloat
/// The control's width `height × 1.5`, so the 4:6 ratio is exact rather than approximated by
/// two independently rounded em figures.
/// The control's width `height × widthRatio`, so the requested ratio is exact rather than
/// approximated by two independently rounded em figures.
var width: CGFloat
/// The rectangle's own radius, and the field's a point apart so the two rounded rects run
/// concentric. Unchanged from the two-zone chrome's own figures; nothing about the trigger's
@@ -40,10 +51,20 @@ struct PickerRectMetrics: Equatable {
var cornerRadius: CGFloat
var fieldRadius: CGFloat
static func metrics(bodyPointSize: CGFloat) -> PickerRectMetrics {
/// The width:height multiplier a picker asks for a closed set of two named cases (not a bare
/// `CGFloat` parameter) so a call site reads "the colour picker's ratio" rather than a literal
/// `1.5` a reader has to trust is still current.
enum WidthRatio: CGFloat, Equatable {
/// The colour rectangle "closer to 6:4" (width:height), the owner's 2026-08-10 follow-up.
case color = 1.5
/// The symbol rectangle "even more square... ratio of 5:4 or so", the same follow-up.
case symbol = 1.25
}
static func metrics(bodyPointSize: CGFloat, widthRatio: WidthRatio) -> PickerRectMetrics {
func em(_ multiple: CGFloat) -> CGFloat { max(1, (bodyPointSize * multiple).rounded()) }
let height = em(2.1)
let width = max(1, (height * 1.5).rounded())
let width = max(1, (height * widthRatio.rawValue).rounded())
return PickerRectMetrics(
height: height,
width: width,
@@ -52,11 +73,13 @@ struct PickerRectMetrics: Equatable {
)
}
/// The live metrics read from the same place every other font-derived geometry in the app
/// reads it (`CardWindowMetrics.bodyPointSize`), so a text-size change moves both rectangles with
/// everything else.
/// The live metrics for one picker's role read from the same place every other font-derived
/// geometry in the app reads it (`CardWindowMetrics.bodyPointSize`), so a text-size change moves
/// both rectangles, and both stay the same height, with everything else.
@MainActor
static var current: PickerRectMetrics { metrics(bodyPointSize: CardWindowMetrics.bodyPointSize) }
static func current(_ widthRatio: WidthRatio) -> PickerRectMetrics {
metrics(bodyPointSize: CardWindowMetrics.bodyPointSize, widthRatio: widthRatio)
}
}
// MARK: - The control
@@ -69,8 +92,10 @@ struct PickerRectMetrics: Equatable {
class PickerRectControl: NSControl {
/// The geometry every pass draws from, refreshed by the representable on each SwiftUI update so
/// a text-size change lands without recreating the view.
var metrics: PickerRectMetrics = .metrics(bodyPointSize: 13) {
/// a text-size change lands without recreating the view. The placeholder ratio here is arbitrary
/// every real subclass instance gets its own role's metrics on the first `updateNSView` before
/// this default is ever drawn or measured.
var metrics: PickerRectMetrics = .metrics(bodyPointSize: 13, widthRatio: .color) {
didSet {
guard metrics != oldValue else { return }
invalidateIntrinsicContentSize()