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:
@@ -88,3 +88,35 @@ struct ColorSwatchPopoverLayoutTests {
|
||||
#expect(layout.popoverWidth == layout.gridWidth + layout.contentPadding * 2)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The collapsed face's None treatment
|
||||
|
||||
/// **The owner's 2026-08-10 follow-up, verbatim**: "when 'none' selected the strike-through line
|
||||
/// should show on the selected color too." `ColorSwatchControl.showsNoneStrike(for:)` is the pure
|
||||
/// predicate behind that draw — the same lenient "nil or unresolvable both read as no colour" rule
|
||||
/// `StyleWellFace.Face.color`'s own doc already states for the popover's grid, restated here for the
|
||||
/// collapsed face so the two surfaces cannot drift apart about what counts as "None."
|
||||
@Suite("ColorSwatchControl ▸ the None face")
|
||||
struct ColorSwatchControlNoneFaceTests {
|
||||
|
||||
@Test("A nil value shows the None strike")
|
||||
func nilShowsTheStrike() {
|
||||
#expect(ColorSwatchControl.showsNoneStrike(for: nil))
|
||||
}
|
||||
|
||||
@Test("A resolvable palette name does not show the strike")
|
||||
func resolvableNameHidesTheStrike() {
|
||||
let name = Palette.backgrounds[0].name
|
||||
#expect(!ColorSwatchControl.showsNoneStrike(for: name))
|
||||
}
|
||||
|
||||
@Test("A resolvable hand-written hex does not show the strike")
|
||||
func resolvableHexHidesTheStrike() {
|
||||
#expect(!ColorSwatchControl.showsNoneStrike(for: "#336699"))
|
||||
}
|
||||
|
||||
@Test("An unresolvable stored value shows the strike, the same lenient rule the popover's grid uses")
|
||||
func unresolvableValueShowsTheStrike() {
|
||||
#expect(ColorSwatchControl.showsNoneStrike(for: "not-a-real-palette-name"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,57 +214,93 @@ struct SymbolCatalogSearchTests {
|
||||
|
||||
// MARK: - The shared rectangle chrome
|
||||
|
||||
/// **The rhyme, asserted.** The owner's 2026-08-10 ruling asked for both pickers to become "a
|
||||
/// rectangle (slightly oversized)... about 4:6 ratio of height to width" — and the way that is made
|
||||
/// true is structural — one metrics value, one base control — so the test is about the structure
|
||||
/// rather than about two numbers that happen to agree today.
|
||||
/// **The rhyme, asserted — now with two widths.** The owner's 2026-08-10 ruling first asked for both
|
||||
/// pickers to become "a rectangle (slightly oversized)... about 4:6 ratio of height to width", then a
|
||||
/// same-day follow-up refined it per role: "width to height ratio should be closer to 6:4 [colour] ...
|
||||
/// 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." The way both halves of that are made true is structural — one
|
||||
/// metrics value, one base control, one shared `height`, a `WidthRatio` that only ever changes
|
||||
/// `width` — so the tests are about the structure rather than about numbers that happen to agree today.
|
||||
@Suite("PickerRect ▸ the shared chrome")
|
||||
struct PickerRectMetricsTests {
|
||||
|
||||
@Test("Every figure scales with the body font, and reproduces the shipped numbers at 13pt")
|
||||
func metricsAtTheStandardBody() {
|
||||
let metrics = PickerRectMetrics.metrics(bodyPointSize: 13)
|
||||
let color = PickerRectMetrics.metrics(bodyPointSize: 13, widthRatio: .color)
|
||||
let symbol = PickerRectMetrics.metrics(bodyPointSize: 13, widthRatio: .symbol)
|
||||
// Owner's ruling, 2026-08-10: "50% taller" than the retired two-zone chrome's 18pt (→ 27pt),
|
||||
// and "about 4:6 ratio" — width is height × 1.5 (`PickerRectMetrics`'s own doc comment).
|
||||
#expect(metrics.height == 27)
|
||||
#expect(metrics.width == 41)
|
||||
#expect(metrics.cornerRadius == 3)
|
||||
#expect(metrics.fieldRadius == 4)
|
||||
// shared by both pickers — and each picker's own width ratio from the same day's follow-up:
|
||||
// 6:4 (× 1.5) for colour, 5:4 (× 1.25) for symbol.
|
||||
#expect(color.height == 27)
|
||||
#expect(symbol.height == 27)
|
||||
#expect(color.width == 41)
|
||||
#expect(symbol.width == 34)
|
||||
for metrics in [color, symbol] {
|
||||
#expect(metrics.cornerRadius == 3)
|
||||
#expect(metrics.fieldRadius == 4)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("A larger text size grows every figure, and none collapses to zero")
|
||||
func metricsScale() {
|
||||
let small = PickerRectMetrics.metrics(bodyPointSize: 11)
|
||||
let large = PickerRectMetrics.metrics(bodyPointSize: 24)
|
||||
@Test("A larger text size grows every figure, and none collapses to zero", arguments: [
|
||||
PickerRectMetrics.WidthRatio.color, .symbol,
|
||||
])
|
||||
func metricsScale(widthRatio: PickerRectMetrics.WidthRatio) {
|
||||
let small = PickerRectMetrics.metrics(bodyPointSize: 11, widthRatio: widthRatio)
|
||||
let large = PickerRectMetrics.metrics(bodyPointSize: 24, widthRatio: widthRatio)
|
||||
#expect(large.height > small.height)
|
||||
#expect(large.width > small.width)
|
||||
for metrics in [PickerRectMetrics.metrics(bodyPointSize: 8), small, large] {
|
||||
for metrics in [PickerRectMetrics.metrics(bodyPointSize: 8, widthRatio: widthRatio), small, large] {
|
||||
#expect(metrics.height >= 1)
|
||||
#expect(metrics.width >= 1)
|
||||
#expect(metrics.cornerRadius >= 1)
|
||||
}
|
||||
}
|
||||
|
||||
/// **The owner's 2026-08-10 figures, pinned.** Height is 2.1 em; width is derived from height
|
||||
/// (`height × 1.5`) rather than its own independent em multiple, which is what keeps the 4:6
|
||||
/// ratio exact — to rounding — at every body size instead of the two figures drifting apart.
|
||||
@Test("Height is 2.1 em, width is 1.5 × height, at every body size")
|
||||
/// **The owner's 2026-08-10 figures, pinned, per role.** Height is 2.1 em for both pickers alike;
|
||||
/// width is derived from height (`height × widthRatio`) rather than its own independent em
|
||||
/// multiple, which is what keeps each ratio exact — to rounding — at every body size instead of
|
||||
/// the two figures drifting apart.
|
||||
@Test("Height is 2.1 em; width is 1.5 × height for colour, 1.25 × height for symbol, at every body size")
|
||||
func heightAndWidthHoldTheRatio() {
|
||||
for size in [8.0, 11.0, 13.0, 17.0, 24.0, 36.0] as [CGFloat] {
|
||||
let metrics = PickerRectMetrics.metrics(bodyPointSize: size)
|
||||
#expect(metrics.height == max(1, (size * 2.1).rounded()), "height drifted from 2.1 em at \(size)pt")
|
||||
#expect(metrics.width == max(1, (metrics.height * 1.5).rounded()), "width drifted from 1.5 × height at \(size)pt")
|
||||
// 4:6 as a ratio, within the slack one rounding step introduces at the smallest sizes.
|
||||
#expect(abs(metrics.width / metrics.height - 1.5) < 0.06, "ratio drifted from 4:6 at \(size)pt")
|
||||
for widthRatio in [PickerRectMetrics.WidthRatio.color, .symbol] {
|
||||
let metrics = PickerRectMetrics.metrics(bodyPointSize: size, widthRatio: widthRatio)
|
||||
#expect(metrics.height == max(1, (size * 2.1).rounded()), "height drifted from 2.1 em at \(size)pt (\(widthRatio))")
|
||||
#expect(
|
||||
metrics.width == max(1, (metrics.height * widthRatio.rawValue).rounded()),
|
||||
"width drifted from \(widthRatio.rawValue) × height at \(size)pt"
|
||||
)
|
||||
// Within the slack one rounding step introduces at the smallest sizes.
|
||||
#expect(
|
||||
abs(metrics.width / metrics.height - widthRatio.rawValue) < 0.06,
|
||||
"ratio drifted from \(widthRatio.rawValue) at \(size)pt"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// **The owner's most literal ask, pinned on its own**: "the height for the two pickers should be
|
||||
/// exactly the same" — not merely close, and not merely equal at 13pt, but equal at every body
|
||||
/// size, since `height` never reads `widthRatio` at all.
|
||||
@Test("The two pickers share exactly the same height at every body size")
|
||||
func heightsMatchAcrossRolesAtEverySize() {
|
||||
for size in [8.0, 11.0, 13.0, 17.0, 24.0, 36.0] as [CGFloat] {
|
||||
let color = PickerRectMetrics.metrics(bodyPointSize: size, widthRatio: .color)
|
||||
let symbol = PickerRectMetrics.metrics(bodyPointSize: size, widthRatio: .symbol)
|
||||
#expect(color.height == symbol.height, "heights diverged at \(size)pt")
|
||||
// And the widths must genuinely differ — the symbol picker is "even more square" than
|
||||
// the colour rectangle, not merely renamed to a ratio that happens to compute the same.
|
||||
#expect(symbol.width < color.width, "the symbol picker is not narrower than the colour picker at \(size)pt")
|
||||
}
|
||||
}
|
||||
|
||||
/// The field radius runs a point outside the face's so the two rounded rects stay concentric —
|
||||
/// a small thing, and exactly the kind of thing that drifts when two files own it.
|
||||
/// a small thing, and exactly the kind of thing that drifts when two files own it. Ratio-
|
||||
/// independent (neither radius reads `widthRatio`), so one role stands in for both.
|
||||
@Test("The field's radius stays outside the face's")
|
||||
func radiiAreConcentric() {
|
||||
for size in [11.0, 13.0, 17.0, 24.0] as [CGFloat] {
|
||||
let metrics = PickerRectMetrics.metrics(bodyPointSize: size)
|
||||
let metrics = PickerRectMetrics.metrics(bodyPointSize: size, widthRatio: .color)
|
||||
#expect(metrics.fieldRadius >= metrics.cornerRadius)
|
||||
}
|
||||
}
|
||||
@@ -301,20 +337,49 @@ struct PickerRectMetricsTests {
|
||||
"the scale-down must preserve the glyph's own aspect ratio")
|
||||
}
|
||||
|
||||
/// **The two controls are the same control.** Both are `PickerRectControl`s and both take their
|
||||
/// geometry from the same value, so a change to one lands on the other — which is the whole of
|
||||
/// the parity claim, and cheaper to assert than any pair of measurements. There are no zones left
|
||||
/// to compare (`PickerRect.swift`'s own retirement of the trigger strip); the whole bounds is the
|
||||
/// one hit zone on both, so intrinsic size is the whole of what "same chrome" means now.
|
||||
/// **"A sliver of padding around the symbol"** (owner's 2026-08-10 follow-up) — a pure static
|
||||
/// figure, so the rule (roughly 0.15–0.2 em, never zero) is assertable without a draw.
|
||||
@Test("The glyph's inset scales with the body font, sits in the owner's named range, and never zeroes out")
|
||||
func glyphInsetHoldsTheOwnersRange() {
|
||||
var previous: CGFloat = 0
|
||||
for size in [8.0, 11.0, 13.0, 17.0, 24.0, 36.0] as [CGFloat] {
|
||||
let inset = SymbolGlyphControl.glyphInset(bodyPointSize: size)
|
||||
#expect(inset >= 1, "the inset collapsed to nothing at \(size)pt")
|
||||
#expect(inset >= previous, "the inset shrank as the body font grew, at \(size)pt")
|
||||
previous = inset
|
||||
// Within the slack one rounding step introduces at the smallest sizes.
|
||||
#expect((0.15 - 0.03...0.2 + 0.03).contains(inset / size), "inset drifted outside ~0.15–0.2 em at \(size)pt")
|
||||
}
|
||||
}
|
||||
|
||||
/// A glyph inset by `glyphInset(bodyPointSize:)` genuinely narrows the rect `glyphPointSize(
|
||||
/// forFace:)`/`fittedSize(for:in:)` size against — the sliver has to actually shrink the drawn
|
||||
/// glyph, not merely exist as an unused figure.
|
||||
@Test("An inset face rect yields a smaller point size than the same rect uninset")
|
||||
func insetFaceRectShrinksTheGlyph() {
|
||||
let rect = NSRect(x: 0, y: 0, width: 34, height: 27)
|
||||
let inset = SymbolGlyphControl.glyphInset(bodyPointSize: 13)
|
||||
let insetRect = rect.insetBy(dx: inset, dy: inset)
|
||||
#expect(SymbolGlyphControl.glyphPointSize(forFace: insetRect) < SymbolGlyphControl.glyphPointSize(forFace: rect))
|
||||
}
|
||||
|
||||
/// **The two controls are the same control, at two different widths.** Both are
|
||||
/// `PickerRectControl`s and both take their geometry from the same value, so a change to one lands
|
||||
/// on the other — which is the whole of the parity claim, and cheaper to assert than any pair of
|
||||
/// measurements. There are no zones left to compare (`PickerRect.swift`'s own retirement of the
|
||||
/// trigger strip); the whole bounds is the one hit zone on both. Height is the one figure that
|
||||
/// still has to match exactly (the owner's 2026-08-10 follow-up); width now diverges by role on
|
||||
/// purpose, so "same chrome" means equal height and each control's own ratio-derived width.
|
||||
@MainActor
|
||||
@Test("Both rectangles are the same chrome, at the same size")
|
||||
@Test("Both rectangles are the same chrome — equal height, each its own ratio's width")
|
||||
func bothRectanglesShareTheChrome() {
|
||||
let colour = ColorSwatchControl(frame: .zero)
|
||||
let symbol = SymbolGlyphControl(frame: .zero)
|
||||
for control in [colour as PickerRectControl, symbol] {
|
||||
control.metrics = .metrics(bodyPointSize: 13)
|
||||
}
|
||||
colour.metrics = .metrics(bodyPointSize: 13, widthRatio: .color)
|
||||
symbol.metrics = .metrics(bodyPointSize: 13, widthRatio: .symbol)
|
||||
#expect(colour.intrinsicContentSize.height == symbol.intrinsicContentSize.height)
|
||||
#expect(colour.intrinsicContentSize.width == symbol.intrinsicContentSize.width)
|
||||
#expect(colour.intrinsicContentSize.width == 41)
|
||||
#expect(symbol.intrinsicContentSize.width == 34)
|
||||
#expect(symbol.intrinsicContentSize.width < colour.intrinsicContentSize.width)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user