Skip to content
Merged
Show file tree
Hide file tree
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
9 changes: 3 additions & 6 deletions Sources/StorageDaddy/DiskMapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ struct DiskMapView: View {
let matches = m.matchesFilter(shape.node)
let tint = isStaged ? Tints.coral : Tints.forKind(m.storageKind(shape.node))
let opacity = matches ? (isChild ? 0.65 : 0.9) : 0.18
ctx.fill(shape.path, with: .color(tint.opacity(opacity)))
ctx.fill(shape.path, with: .color(MapTileFill.color(tint, intensity: opacity)))
if isStaged {
ctx.stroke(shape.path, with: .color(.white.opacity(0.85)), style: StrokeStyle(lineWidth: 2, dash: [5, 3]))
}
Expand All @@ -41,12 +41,10 @@ struct DiskMapView: View {
ctx.stroke(shape.path, with: .color(selected ? Color.white : Color.black), lineWidth: 3)
}
if shape.labelRect.width > 55 && shape.labelRect.height > 28 {
// An opaque dark label backing keeps small text readable over
// semantic, nested, filtered and staged fills alike.
// Direct white labels sit on contrast-adjusted category fills.
let ink = matches ? Color.white : Color(white: 0.78)
if m.mode == .treemap, shape.labelRect.height > 65, shape.labelRect.width > 95 {
let r = shape.labelRect.insetBy(dx: isChild ? 10 : 14, dy: isChild ? 9 : 12)
ctx.fill(Path(roundedRect: CGRect(x: r.minX - 4, y: r.minY - 3, width: r.width + 8, height: 52), cornerRadius: 5), with: .color(.black))
let name = shortenedTileName((isStaged ? "✓ Cleanup · " : "") + StorageLabels.name(shape.node), width: r.width)
let size = m.mapLabel(m.mapWeight(shape.node))
let detail: String
Expand All @@ -58,9 +56,8 @@ struct DiskMapView: View {
detail = size
}
ctx.draw(Text(name).font(.system(size: isChild ? 14 : 17, weight: .semibold, design: .rounded)).foregroundColor(ink), in: CGRect(x: r.minX, y: r.minY, width: r.width, height: 24))
ctx.draw(Text(detail).font(.system(size: isChild ? 12 : 14, weight: .medium)).foregroundColor(ink.opacity(0.85)), in: CGRect(x: r.minX, y: r.minY + 26, width: r.width, height: 20))
ctx.draw(Text(detail).font(.system(size: isChild ? 12 : 14, weight: .medium)).foregroundColor(ink), in: CGRect(x: r.minX, y: r.minY + 26, width: r.width, height: 20))
} else {
ctx.fill(Path(roundedRect: shape.labelRect.insetBy(dx: 3, dy: 3), cornerRadius: 5), with: .color(.black))
let text = Text((isStaged ? "✓ " : "") + StorageLabels.name(shape.node)).font(.system(size: 12, weight: .semibold)).foregroundColor(ink)
ctx.draw(text, in: shape.labelRect.insetBy(dx: 7, dy: 5))
}
Expand Down
35 changes: 35 additions & 0 deletions Sources/StorageDaddy/MapTileFill.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import AppKit
import SwiftUI

/// Keep the semantic hue while providing contrast for small direct white labels.
enum MapTileFill {
static func color(_ tint: Color, intensity: Double) -> Color {
Color(nsColor: resolved(tint, intensity: intensity))
}

static func resolved(_ tint: Color, intensity: Double) -> NSColor {
let source = NSColor(tint).usingColorSpace(.sRGB) ?? .black
let depthScale = min(1, max(0, intensity / 0.9))
let red = source.redComponent * 0.9
let green = source.greenComponent * 0.9
let blue = source.blueComponent * 0.9
func luminance(_ scale: Double) -> Double {
func linear(_ channel: Double) -> Double {
channel <= 0.04045 ? channel / 12.92 : pow((channel + 0.055) / 1.055, 2.4)
}
return 0.2126 * linear(red * scale) + 0.7152 * linear(green * scale) + 0.0722 * linear(blue * scale)
}
var scale = 1.0
if luminance(scale) > 0.14 {
var low = 0.0, high = 1.0
for _ in 0..<24 {
let midpoint = (low + high) / 2
if luminance(midpoint) <= 0.14 { low = midpoint } else { high = midpoint }
}
scale = low
}
// Opaque tiles prevent nested colors from accumulating over parent fills.
// Intensity still makes children darker and filtered tiles quieter.
return NSColor(srgbRed: red * scale * depthScale, green: green * scale * depthScale, blue: blue * scale * depthScale, alpha: 1)
}
}
28 changes: 28 additions & 0 deletions Tests/StorageDaddyTests/MapTileFillTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import AppKit
import Testing
import DiskCore
@testable import StorageDaddy

@MainActor struct MapTileFillTests {
@Test func directLabelsHaveContrastAcrossEverySemanticAndReviewState() {
func luminance(_ color: NSColor) -> Double {
let c = color.usingColorSpace(.sRGB)!
func linear(_ value: Double) -> Double {
value <= 0.04045 ? value / 12.92 : pow((value + 0.055) / 1.055, 2.4)
}
return 0.2126 * linear(c.redComponent) + 0.7152 * linear(c.greenComponent) + 0.0722 * linear(c.blueComponent)
}
for tint in StorageKind.allCases.map(Tints.forKind) + [Tints.coral] {
let parent = MapTileFill.resolved(tint, intensity: 0.9)
let child = MapTileFill.resolved(tint, intensity: 0.65)
let filtered = MapTileFill.resolved(tint, intensity: 0.18)
for fill in [parent, child] {
#expect(1.05 / (luminance(fill) + 0.05) >= 5.5)
#expect(fill.alphaComponent == 1)
}
#expect((luminance(NSColor(white: 0.78, alpha: 1)) + 0.05) / (luminance(filtered) + 0.05) >= 4.5)
#expect(luminance(child) <= luminance(parent))
#expect(luminance(filtered) < luminance(child))
}
}
}
5 changes: 3 additions & 2 deletions Tests/StorageDaddyTests/StorageMapReviewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,9 @@ struct StorageMapReviewTests {
m.volumeFree = 45_000_000_000; m.volumeCapacity = 500_000_000_000
m.progress = "Synthetic scan · Nothing moved"; m.refreshFocus()
let wasActive = NSApplication.shared.isActive
let directory = URL(fileURLWithPath: #filePath).deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
.appendingPathComponent("artifacts/compact-map")
let directory = ProcessInfo.processInfo.environment["STORAGEDADDY_MAP_EVIDENCE_DIR"].map { URL(fileURLWithPath: $0) }
?? URL(fileURLWithPath: #filePath).deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
.appendingPathComponent("artifacts/compact-map")
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
for (width, height) in [(880, 600), (1200, 850), (1440, 900)] {
let states = ["bytes", "files", "filter", "staged", "inspector-hidden"]
Expand Down
Loading