From 4bdc33e7f249e98e49ec5d7fd85b3c6c3f18891a Mon Sep 17 00:00:00 2001 From: DaxxSec Date: Wed, 13 May 2026 09:51:13 -0600 Subject: [PATCH] =?UTF-8?q?fix(ui):=20detail=20card=20=E2=80=94=20tighter?= =?UTF-8?q?=20status=20pill=20+=20dynamic=20name=20label=20width?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs reported visually after PR #10 merged. 1. Status pill has dead space The pill was sized at 96pt to fit "◆ TEMPLATE" with breathing room, but most statuses ("○ STOPPED", "● RUNNING") are 9 chars at the monospaced caption font (~63pt). Center-aligned text inside a 96pt pill left a ~16pt empty band on each side that read as "padding bug" rather than "tactical pill". Pill is now 80pt — still fits "◆ TEMPLATE" (~70pt content) without clipping, but the shorter statuses look tight and intentional. 2. VM name overruns past the metric grid at default window width At the 1200pt default window width, the card is ~950pt wide. The metric grid + actions cluster on the right was 955pt (with the prior widths), so the right-anchored container's left edge sat at x = -5 — meaning the metrics container started BEFORE the card's own left edge and overlapped the name label. A long VM name on the left side rendered past the visible card area (it wasn't clipped by the card because NSView doesn't clip by default), then visually collided with the OS/Uptime cells once the user widened the window. Two-part fix: a) Trim metric widths to claw back ~65pt (osW 120→100, uptimeW 80→70, cpuW 110→95, netModeW 100→80). diskW / rateW / packetsW kept at prior values because their content can legitimately hit those widths. b) Name label width is now dynamic. New `resizeDetailNameLabelToFit()` clamps the label's right edge to `metricsContainer.frame.minX - 8` so the name truncates with `.byTruncatingTail` instead of overflowing. Called from the initial card-build path and again on every windowDidResize, so the layout is honest at every size. New ivar: `detailMetricsContainer` tracks the right-anchored metric grid view so the resize logic can read its actual minX rather than recomputing widths. No test changes — these are visual bugs in code with no test surface (the existing TacticalUITests cover row/header/hover components; the detail-card identity layout has no test today). A snapshot test for the card at three window widths would be the right follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) --- SecVF/VMLibraryWindowController.swift | 69 ++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/SecVF/VMLibraryWindowController.swift b/SecVF/VMLibraryWindowController.swift index 90c912a..a8474a0 100644 --- a/SecVF/VMLibraryWindowController.swift +++ b/SecVF/VMLibraryWindowController.swift @@ -192,6 +192,12 @@ class VMLibraryWindowController: NSWindowController, // selected VM isn't running. private var detailConsoleButton: NSButton? private var detailCaptureButton: NSButton? + // Right-anchored container that holds the metric grid (OS / Uptime / + // CPU·RAM / Disk / Network / Rate / Packets). Tracked so + // windowDidResize can use its actual minX to clamp the name label's + // right edge — without that clamp, long VM names overrun past the + // metrics on narrow windows. + private var detailMetricsContainer: NSView? // Per-VM run timestamps, keyed by UUID. Populated when a VM // transitions to .running, cleared when it transitions away. private var vmStartedAt: [UUID: Date] = [:] @@ -1246,8 +1252,13 @@ class VMLibraryWindowController: NSWindowController, let valueH: CGFloat = 20 let captionY: CGFloat = 41 let captionH: CGFloat = 12 - let pillW: CGFloat = 96 // bumped 84→96 so "◆ TEMPLATE" / "● RUNNING" have breathing room + let pillW: CGFloat = 80 // tight to longest content ("◆ TEMPLATE" ~70pt); centered text reads clean without dead space let pillH: CGFloat = 22 + // Initial name-label width — actual width is recomputed in + // windowDidResize to fill the gap between the pill and the + // right-anchored metrics container, so the name truncates + // gracefully on narrow windows instead of overrunning into + // the metric grid. let nameW: CGFloat = 200 let gapMetric: CGFloat = LayoutConstants.spacingLG // 16pt between metric cells (was 12) @@ -1267,6 +1278,10 @@ class VMLibraryWindowController: NSWindowController, y: valueY, width: nameW, height: valueH) nameLabel.lineBreakMode = .byTruncatingTail + // Width is recomputed in windowDidResize to fill the gap between + // the pill and the right-anchored metrics container. Pin the + // label's left edge so it doesn't drift with the card's resize. + nameLabel.autoresizingMask = [.width] nameLabel.setAccessibilityLabel("Selected VM name") card.addSubview(nameLabel) detailNameLabel = nameLabel @@ -1275,12 +1290,18 @@ class VMLibraryWindowController: NSWindowController, // Compute total width: 5 cells with their widths + 4 gaps + leading // divider region. The container's x = card.width - totalW - cellPadding, // and .minXMargin autoresizing keeps it glued to the right edge. - let osW: CGFloat = 120 // trimmed -10 to make room for action buttons - let uptimeW: CGFloat = 80 // "12h 47m" / "—" - let cpuW: CGFloat = 110 - let diskW: CGFloat = 110 - let netModeW: CGFloat = 100 - let rateW: CGFloat = 150 // trimmed -10; "host-routed" still fits + // Metric widths sized to fit the longest realistic value at the + // monospaced body font (~7.5pt per char). Tighter than the first + // pass — the original widths overflowed the card at the default + // 1200pt window width, which is what was pushing the VM-name + // label past its visible area before windowDidResize ran the + // dynamic-truncation logic. + let osW: CGFloat = 100 // "Kali 2024.1" / "macOS 15.2" / "Linux" + let uptimeW: CGFloat = 70 // "12h 47m" / "3d 4h" / "—" + let cpuW: CGFloat = 95 // "4 · 4.0 GB" + let diskW: CGFloat = 110 // "108 / 256 GB" — keep generous for big drives + let netModeW: CGFloat = 80 // "VIRTUAL" / "ROUTER" / "NAT" + let rateW: CGFloat = 150 // "100 kB/s ↓ 50 kB/s ↑" let packetsW: CGFloat = 90 // "1,234,567" upper bound for a long capture let dividerW: CGFloat = 1 let dividerGap: CGFloat = LayoutConstants.spacingLG // gap divider → first metric cell @@ -1335,6 +1356,7 @@ class VMLibraryWindowController: NSWindowController, width: metricsW, height: frame.height) metrics.autoresizingMask = [.minXMargin] card.addSubview(metrics) + detailMetricsContainer = metrics // Vertical divider between identity and metrics — sits at x=0 inside // the metrics container so it's flush with the metric grid's left @@ -1439,6 +1461,14 @@ class VMLibraryWindowController: NSWindowController, contentView.addSubview(card) selectedVMDetailCard = card + // Initial sizing pass for the name label. Without this the first + // paint uses the static 200pt width set above and the label can + // overrun into the metrics container at the default window + // width (the metrics+actions cluster needs more horizontal room + // than the card has at minWindowWidth). windowDidResize fires + // it again on every resize. + resizeDetailNameLabelToFit() + // Populate with whatever the table currently has selected (if anything) updateSelectedVMDetailCard() } @@ -1535,6 +1565,24 @@ class VMLibraryWindowController: NSWindowController, /// cornerRadius is half the pill height = perfect capsule. Caller sizes /// the pill to ~96 × 22pt so the centered "◆ TEMPLATE" / "● RUNNING" /// content has ~10pt of horizontal breathing room. + /// Clamp the detail-card name label's width so its right edge sits + /// just inside the metrics container's left edge. Without this, a + /// long VM name on a narrow window overruns the card's identity + /// area and visually collides with the OS / Uptime cells. The label + /// uses `.byTruncatingTail`, so the right behavior is "shrink the + /// frame and let AppKit ellipsize" — never let the frame extend + /// past the metrics container. + private func resizeDetailNameLabelToFit() { + guard let name = detailNameLabel, + let metrics = detailMetricsContainer else { return } + let gap: CGFloat = LayoutConstants.spacingSM + let rightLimit = metrics.frame.minX - gap + let available = max(40, rightLimit - name.frame.minX) + var f = name.frame + f.size.width = available + name.frame = f + } + private func makeStatusPill() -> NSTextField { let pill = NSTextField(labelWithString: "—") pill.alignment = .center @@ -3517,6 +3565,13 @@ class VMLibraryWindowController: NSWindowController, // Detail card stays at fixed Y (autoresize handles its width). selectedVMDetailCard?.frame.origin.y = detailCardY + // Detail card's name label is fixed-width by default but + // gets resized here to fill the gap between the pill and the + // right-anchored metrics container — without this, narrow + // windows let a long VM name overrun past the metrics into + // the void on the right of the card. + resizeDetailNameLabelToFit() + // (Active VMs / Legend panels were removed; no right-side re-anchor.) // Packet panel sits just above the bottom status bar.