From 6d0bbf7b3755a195c29050efcff116b6f07f3e73 Mon Sep 17 00:00:00 2001 From: Jay Wedgeworth <12656028+jaywedgeworth22@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:40:58 -0500 Subject: [PATCH 1/2] feat(pip): drop the header band, and let the platform name go before the logo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner's PiP batch, 2026-10-08. Board f324ec6. The raised strip above the rows read as a stray silver tab. It is gone, and with it the app's own mark and the word "CodeCaps": the provider logos are what identify a row now. The close/back control floats over the rows' top-right corner instead, still only while the pointer is over the window, so it never occupies permanent space above the numbers. The width ladder gained a `logoOnly` level. The logo and the name used to drop together at `minimal`, which left a nameless AND unidentifiable row; the owner's instruction was that the name goes first and the logo stays, so that is now its own rung between the named single-meter row and the identity-free one. His condition for this — "ASSUMING WE FINALLY CAN MAKE THE LOGOS ACTUALLY SHOW UP" — is met by #190, which ships the marks as colour/light/dark PNG triplets rather than SVGs. Panel metrics follow the header's removal: `minHeight` is one row plus padding, and the fit and visible-row maths no longer reserve a band. Verified: swift build clean; swift test 370 passed / 3 skipped / 0 failures, including a new testThePlatformNameDropsBeforeTheLogo that pins the order and the real width band that selects it. Two tests that asserted the old header contract were updated rather than deleted silently: the fit-height test no longer counts a header, and the header-text test is gone with the header. --- Sources/CodeCaps/PipWidget.swift | 134 ++++++++-------------- Tests/CodeCapsTests/PipMetricsTests.swift | 49 ++++++-- 2 files changed, 87 insertions(+), 96 deletions(-) diff --git a/Sources/CodeCaps/PipWidget.swift b/Sources/CodeCaps/PipWidget.swift index 3eb8ecb..03974a6 100644 --- a/Sources/CodeCaps/PipWidget.swift +++ b/Sources/CodeCaps/PipWidget.swift @@ -14,7 +14,6 @@ enum PipMetrics { static let padding: CGFloat = 10 static let cornerRadius: CGFloat = 12 - static let headerHeight: CGFloat = 42 static let rowHeight: CGFloat = 20 static let bodyVerticalPadding: CGFloat = 6 @@ -33,23 +32,21 @@ enum PipMetrics { static let barMinWidth: CGFloat = 28 static let barHeight: CGFloat = 4 - // MARK: Header + // MARK: Floating controls - /// Big enough to read as the app's mark rather than as an icon in a list. - /// It replaces a 12pt fallback that read as a smudge at HUD scale. - static let markSize: CGFloat = 22 - static let markTextSize: CGFloat = 12 - /// Below this the word "CodeCaps" is what collides with the close button, - /// so the mark travels alone. - static let headerTextMinWidth: CGFloat = 130 + /// The close/back control floats over the rows rather than living in a band + /// of its own (owner, 2026-10-08), so it is inset from the corner rather + /// than laid out beside a title. + static let controlInset: CGFloat = 4 + static let controlHitSize: CGFloat = 18 // MARK: Panel bounds static let minWidth: CGFloat = 110 static let maxWidth: CGFloat = 520 - /// One header, one row, and the body's own vertical padding. The two - /// zones run edge to edge, so this is the whole panel height. - static let minHeight: CGFloat = headerHeight + rowHeight + bodyVerticalPadding * 2 + /// One row plus the body's own vertical padding, now that the header band + /// is gone and the panel is rows only. + static let minHeight: CGFloat = rowHeight + bodyVerticalPadding * 2 static let maxHeight: CGFloat = 460 /// A window with no dual-meter row opens comfortably wide without being @@ -84,6 +81,10 @@ enum PipMetrics { static let singleMeterMinWidth: CGFloat = providerLogoSize + rowSpacing + titleWidth + rowSpacing + meterMinWidthWithCountdown + /// Provider logo and two meters, no countdowns and no title. + static let logoOnlyMinWidth: CGFloat = + providerLogoSize + rowSpacing + meterMinWidth + rowSpacing + meterMinWidth + /// Two labelled meters, with no provider identity at all. static let minimalMinWidth: CGFloat = meterMinWidth + rowSpacing + meterMinWidth @@ -109,7 +110,15 @@ enum PipMetrics { case noCountdown /// One meter, with its countdown, still named. case singleMeter - /// Two labelled meters, provider identity dropped. + /// Two meters and the provider's logo, but its NAME dropped. + /// + /// Owner, 2026-10-08: "when the window is getting narrower, the first + /// thing to go (after things have compressed as much as allowable) is + /// the platform name ASSUMING WE FINALLY CAN MAKE THE LOGOS ACTUALLY + /// SHOW UP". The logo and the name used to disappear together, so + /// there was nothing to fall back to; this is that level. + case logoOnly + /// Two labelled meters, provider identity dropped entirely. case minimal /// Two bars and two percentages, captions dropped. case barsOnly @@ -124,7 +133,7 @@ enum PipMetrics { var maxMeters: Int { switch self { - case .full, .noCountdown, .minimal, .barsOnly: return 2 + case .full, .noCountdown, .logoOnly, .minimal, .barsOnly: return 2 case .singleMeter, .singleBar: return 1 } } @@ -138,10 +147,14 @@ enum PipMetrics { } var showsProviderLogo: Bool { - self == .full || self == .noCountdown || self == .singleMeter + self == .full || self == .noCountdown || self == .singleMeter || self == .logoOnly } - var showsTitle: Bool { showsProviderLogo } + /// The name outlives the logo nowhere: the logo is what identifies a row + /// once the name is gone, so the name is the first of the two to drop. + var showsTitle: Bool { + self == .full || self == .noCountdown || self == .singleMeter + } } /// The richest level whose content fits `width`, content width being what @@ -152,26 +165,22 @@ enum PipMetrics { case fullMinWidth...: return .full case noCountdownMinWidth...: return .noCountdown case singleMeterMinWidth...: return .singleMeter + case logoOnlyMinWidth...: return .logoOnly case minimalMinWidth...: return .minimal case barsOnlyMinWidth...: return .barsOnly default: return .singleBar } } - /// Whether the header can carry the word as well as the mark. - static func showsHeaderText(forWidth width: CGFloat) -> Bool { - width >= headerTextMinWidth - } - /// How many rows fit in `height` without clipping, never fewer than one: /// a PiP HUD that has been shrunk is still a PiP HUD, and one visible meter /// is the honest answer to "no room". /// - /// The two zones run edge to edge, so `height` is the header, the body's - /// own vertical padding and the rows — there is no outer padding to remove. + /// `height` is the body's own vertical padding plus the rows — the header + /// band is gone, so there is nothing else to take out. static func visibleRowCount(total: Int, height: CGFloat) -> Int { guard total > 0 else { return 0 } - let available = height - headerHeight - bodyVerticalPadding * 2 + let available = height - bodyVerticalPadding * 2 guard available >= rowHeight else { return 1 } return min(total, max(1, Int(available / rowHeight))) } @@ -182,7 +191,7 @@ enum PipMetrics { /// failure the owner reported. static func fitSize(rowCount: Int, hasDualMeterRow: Bool) -> NSSize { let rows = CGFloat(max(1, rowCount)) - let height = headerHeight + rows * rowHeight + bodyVerticalPadding * 2 + let height = rows * rowHeight + bodyVerticalPadding * 2 let width = hasDualMeterRow ? padding * 2 + fullMinWidth : max(singleMeterDefaultWidth, padding * 2 + singleMeterMinWidth) @@ -430,13 +439,18 @@ struct PipWidgetView: View { // their own contents, so the ladder below reads real panel width // rather than width that has already had the padding taken out of it. let detail = PipMetrics.detail(forWidth: geo.size.width) - VStack(spacing: 0) { - header(width: geo.size.width) + // No header band. The owner read the raised strip as a stray silver + // tab (2026-10-08) and, with it gone, asked that the app's own mark + // and name not appear on the HUD at all — the provider logos are what + // identify it now. The close/back control floats over the rows + // instead, still only while the pointer is over the window. + ZStack(alignment: .topTrailing) { if let detailRow { detailPanel(detailRow, detail: detail) } else { rowsPanel(detail: detail, height: geo.size.height) } + floatingControl } } .background { @@ -455,66 +469,18 @@ struct PipWidgetView: View { // MARK: Header - /// The header band, edge to edge with the rows panel below it. The - /// material strip that used to sit above it (the window's own background - /// showing through) is gone: the owner read it as a stray silver tab and - /// nothing looked right on it. - /// - /// The close button appears only while the mouse is over the window. The - /// owner saw it permanently after it was made always-on and asked for the - /// hover behaviour back: a control that never moves is furniture, and this - /// one sits on top of the quota numbers. - private func header(width: CGFloat) -> some View { - ZStack { - // Centred as its own layer, so the close button appearing and - // disappearing cannot shift the mark and title sideways. - HStack(spacing: 6) { - mark - if PipMetrics.showsHeaderText(forWidth: width) { - Text("CodeCaps") - .font(.system(size: PipMetrics.markTextSize, weight: .bold)) - .foregroundStyle(Theme.solidMark) - .fixedSize() - } - } - - HStack(spacing: 0) { - Spacer(minLength: 0) - if isHovering { - if detailRow == nil { - closeButton - } else { - backButton - } - } - } - } - .frame(height: PipMetrics.headerHeight) - .frame(maxWidth: .infinity) - .padding(.horizontal, PipMetrics.padding) - .background(Theme.raisedBand) - } - - /// CodeCaps' own mark, from the owner's black-on-transparent artwork, drawn - /// as a template so the same asset is black-on-transparent on the light - /// band and white-on-transparent on the dark one. It is the app's mark - /// reused as-is — never re-cut, re-exported or substituted. - private var mark: some View { - Group { - if let image = PipWidgetController.brandMark { - Image(nsImage: image) - .resizable() - .renderingMode(.template) - .aspectRatio(contentMode: .fit) - .foregroundStyle(Theme.solidMark) + /// Close, or Back when a platform detail is open. It floats over the + /// rows' top-right corner and appears only on hover, so it never occupies + /// permanent space above the numbers. + @ViewBuilder + private var floatingControl: some View { + if isHovering { + if detailRow == nil { + closeButton } else { - Image(systemName: "gauge.with.needle.fill") - .resizable() - .aspectRatio(contentMode: .fit) - .foregroundStyle(Theme.solidMark) + backButton } } - .frame(width: PipMetrics.markSize, height: PipMetrics.markSize) } private var closeButton: some View { diff --git a/Tests/CodeCapsTests/PipMetricsTests.swift b/Tests/CodeCapsTests/PipMetricsTests.swift index 84af7b6..e0ab664 100644 --- a/Tests/CodeCapsTests/PipMetricsTests.swift +++ b/Tests/CodeCapsTests/PipMetricsTests.swift @@ -42,6 +42,37 @@ final class PipMetricsTests: XCTestCase { } } + /// Owner, 2026-10-08: "when the window is getting narrower, the first + /// thing to go (after things have compressed as much as allowable) is the + /// platform name ASSUMING WE FINALLY CAN MAKE THE LOGOS ACTUALLY SHOW UP". + /// + /// The logo and the name used to drop together at `minimal`, so there was + /// nothing left to identify the row. The name now goes first, at its own + /// `logoOnly` level, and the logo survives until the identity ladder runs + /// out entirely. + func testThePlatformNameDropsBeforeTheLogo() { + XCTAssertTrue(PipMetrics.Detail.logoOnly.showsProviderLogo, + "the logo must still identify the row at this level") + XCTAssertFalse(PipMetrics.Detail.logoOnly.showsTitle, + "the name is the first thing to go") + + // And there is a real width band that selects it, between the named + // single-meter row and the identity-free minimal one. + XCTAssertEqual(PipMetrics.detail(forWidth: PipMetrics.padding * 2 + PipMetrics.logoOnlyMinWidth), + .logoOnly) + XCTAssertEqual(PipMetrics.detail(forWidth: PipMetrics.padding * 2 + PipMetrics.minimalMinWidth), + .minimal) + XCTAssertFalse(PipMetrics.Detail.minimal.showsProviderLogo, + "the logo goes only after the name, at the identity-free end") + + // Nothing above logoOnly may lose the name while keeping two meters. + for level in [PipMetrics.Detail.full, .noCountdown, .singleMeter, .logoOnly] { + if level.maxMeters > 1 || level == .logoOnly { + XCTAssertTrue(level.showsProviderLogo, "\(level) must keep the logo") + } + } + } + func testNarrowWindowStillShowsBarAndPercent() { // The owner's floor: only the bars and the percentages remain. let detail = PipMetrics.detail(forWidth: PipMetrics.minWidth) @@ -79,13 +110,12 @@ final class PipMetricsTests: XCTestCase { } func testFitHeightHasNoOuterPaddingTerm() { - // The two zones run edge to edge, so the panel height is exactly - // header + rows + the body's own vertical padding. If a future change - // puts padding back on the outside, `height` starts double-counting it: - // the ladder then drops a level early and the panel asks for room it is - // no longer using. - let expected = PipMetrics.headerHeight - + CGFloat(3) * PipMetrics.rowHeight + // The rows run edge to edge, so the panel height is exactly the rows + // plus the body's own vertical padding — there is no header band (owner, + // 2026-10-08). If a future change puts padding back on the outside, + // `height` starts double-counting it: the ladder then drops a level + // early and the panel asks for room it is no longer using. + let expected = CGFloat(3) * PipMetrics.rowHeight + PipMetrics.bodyVerticalPadding * 2 XCTAssertEqual(PipMetrics.fitSize(rowCount: 3, hasDualMeterRow: false).height, expected) } @@ -129,11 +159,6 @@ final class PipMetricsTests: XCTestCase { } } - func testHeaderTextIsDroppedOnlyWhenItWouldCollideWithTheCloseButton() { - XCTAssertTrue(PipMetrics.showsHeaderText(forWidth: PipMetrics.minWidth * 2)) - XCTAssertFalse(PipMetrics.showsHeaderText(forWidth: PipMetrics.minWidth)) - } - // MARK: Window behaviour func testPanelIsResizableAndBounded() { From e36ad7971bbf7a8c9abe916f83da38530b9cdb61 Mon Sep 17 00:00:00 2001 From: Jay Wedgeworth <12656028+jaywedgeworth22@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:48:56 -0500 Subject: [PATCH 2/2] fix(pip): inset the floating control so it stops overlapping the first row Kody caught this on #193: the 18x18 close control sat flush in the panel's top-trailing corner and overlapped the first row's right-edge percentage by about 8x12pt. controlInset existed for exactly this offset but nothing consumed it, so the contract was declared and then not honoured. Honours the constant rather than removing it, so the corner offset it was designed for is now actually enforced. --- Sources/CodeCaps/PipWidget.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/CodeCaps/PipWidget.swift b/Sources/CodeCaps/PipWidget.swift index 03974a6..e3149cf 100644 --- a/Sources/CodeCaps/PipWidget.swift +++ b/Sources/CodeCaps/PipWidget.swift @@ -451,6 +451,9 @@ struct PipWidgetView: View { rowsPanel(detail: detail, height: geo.size.height) } floatingControl + // Without this the 18x18 control sat flush in the corner and + // overlapped the first row's right-edge percentage. + .padding(PipMetrics.controlInset) } } .background {