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:
@@ -170,7 +170,7 @@ private struct ColorSwatchView: NSViewRepresentable {
|
||||
}
|
||||
|
||||
func updateNSView(_ control: ColorSwatchControl, context: Context) {
|
||||
control.metrics = .current
|
||||
control.metrics = .current(.color)
|
||||
control.isEnabled = isEnabled
|
||||
control.swatchValue = value
|
||||
control.accessibilityValueText = value ?? "None"
|
||||
@@ -208,17 +208,22 @@ final class ColorSwatchControl: PickerRectControl {
|
||||
|
||||
/// The colour rect: a `textBackgroundColor` underlay so a translucent stored colour composites
|
||||
/// the same way in light and dark, the resolved colour on top, a `separatorColor` hairline
|
||||
/// stroke last. `nil`/unresolvable value → underlay + stroke only, the same "there is no colour,
|
||||
/// so show none" rule the retired combo's face drew — straight into the control's own graphics
|
||||
/// context, exactly as before, rather than through `PaletteSwatch.rectImage`'s intermediate
|
||||
/// `NSImage` (that function had exactly one caller, the retired dropdown's menu rows, and is
|
||||
/// gone with it).
|
||||
/// stroke last. `nil`/unresolvable value → underlay + stroke **plus the popover well's own
|
||||
/// corner-to-corner slash** — the owner's 2026-08-10 follow-up ("when 'none' selected the
|
||||
/// strike-through line should show on the selected color too"): the collapsed face is itself
|
||||
/// showing the current value, so a value that resolves to "no colour" gets the identical None
|
||||
/// treatment the grid's own leading well draws, rather than a plain empty rectangle — straight
|
||||
/// into the control's own graphics context, exactly as the fill was before, rather than through
|
||||
/// `PaletteSwatch.rectImage`'s intermediate `NSImage` (that function had exactly one caller, the
|
||||
/// retired dropdown's menu rows, and is gone with it).
|
||||
override func drawFace(in rect: NSRect) {
|
||||
guard rect.width > 0, rect.height > 0 else { return }
|
||||
let path = NSBezierPath(roundedRect: rect, xRadius: metrics.cornerRadius, yRadius: metrics.cornerRadius)
|
||||
NSColor.textBackgroundColor.setFill()
|
||||
path.fill()
|
||||
if let swatchValue, let color = Palette.nsColor(for: swatchValue) {
|
||||
if Self.showsNoneStrike(for: swatchValue) {
|
||||
drawNoneStrike(in: rect)
|
||||
} else if let color = swatchValue.flatMap(Palette.nsColor(for:)) {
|
||||
color.setFill()
|
||||
path.fill()
|
||||
}
|
||||
@@ -226,6 +231,36 @@ final class ColorSwatchControl: PickerRectControl {
|
||||
path.lineWidth = 1
|
||||
path.stroke()
|
||||
}
|
||||
|
||||
/// Whether the face has nothing to fill and should draw the None strike instead — `nil`, and any
|
||||
/// stored value this app cannot resolve to a colour, both read the same lenient way
|
||||
/// `StyleWellFace.Face.color`'s own doc already states it for the grid's wells ("An unresolvable
|
||||
/// value draws like `noValue`"). A pure static predicate, not inlined into `drawFace`, so the rule
|
||||
/// is assertable without an actual draw — the same pattern `SymbolGlyphControl`'s sizing statics
|
||||
/// already use.
|
||||
static func showsNoneStrike(for value: String?) -> Bool {
|
||||
value.flatMap(Palette.nsColor(for:)) == nil
|
||||
}
|
||||
|
||||
/// Strokes `ColorSwatchNoneStrike`'s own corner-to-corner slash (below) across `rect` — the
|
||||
/// popover well's exact geometry, reused rather than restated, so the grid's None well and this
|
||||
/// collapsed face agree pixel for pixel about what "no colour" looks like. `ColorSwatchNoneStrike`
|
||||
/// is a plain SwiftUI `Shape`; its `path(in:)` is pure geometry with no SwiftUI environment or
|
||||
/// rendering context of its own, so calling it here and stroking the result straight into this
|
||||
/// control's AppKit graphics context is exactly as valid as the popover calling it inside
|
||||
/// `.stroke(...)`. The inset reads off `min(rect.width, rect.height)` rather than `layout.
|
||||
/// wellSide` — the well grid's own wells are square, this face is not (6:4/5:4), so the shorter
|
||||
/// side stands in for it.
|
||||
private func drawNoneStrike(in rect: NSRect) {
|
||||
guard let context = NSGraphicsContext.current?.cgContext else { return }
|
||||
let inset = max(1, (min(rect.width, rect.height) * 0.15).rounded())
|
||||
context.saveGState()
|
||||
context.addPath(ColorSwatchNoneStrike(inset: inset).path(in: rect).cgPath)
|
||||
context.setStrokeColor(NSColor.secondaryLabelColor.cgColor)
|
||||
context.setLineWidth(1)
|
||||
context.strokePath()
|
||||
context.restoreGState()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The popover's content
|
||||
|
||||
Reference in New Issue
Block a user