Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 62 additions & 7 deletions SecVF/VMLibraryWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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] = [:]
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Loading