diff --git a/Sources/LockIME/Assets.xcassets/TrayLocked.imageset/tray-locked.png b/Sources/LockIME/Assets.xcassets/TrayLocked.imageset/tray-locked.png index f20c877..6d7aecc 100644 Binary files a/Sources/LockIME/Assets.xcassets/TrayLocked.imageset/tray-locked.png and b/Sources/LockIME/Assets.xcassets/TrayLocked.imageset/tray-locked.png differ diff --git a/Sources/LockIME/Assets.xcassets/TrayLocked.imageset/tray-locked@2x.png b/Sources/LockIME/Assets.xcassets/TrayLocked.imageset/tray-locked@2x.png index b71eb6c..b4c8806 100644 Binary files a/Sources/LockIME/Assets.xcassets/TrayLocked.imageset/tray-locked@2x.png and b/Sources/LockIME/Assets.xcassets/TrayLocked.imageset/tray-locked@2x.png differ diff --git a/Sources/LockIME/Assets.xcassets/TrayUnlocked.imageset/tray-unlocked.png b/Sources/LockIME/Assets.xcassets/TrayUnlocked.imageset/tray-unlocked.png index 0338abf..1bd1cf2 100644 Binary files a/Sources/LockIME/Assets.xcassets/TrayUnlocked.imageset/tray-unlocked.png and b/Sources/LockIME/Assets.xcassets/TrayUnlocked.imageset/tray-unlocked.png differ diff --git a/Sources/LockIME/Assets.xcassets/TrayUnlocked.imageset/tray-unlocked@2x.png b/Sources/LockIME/Assets.xcassets/TrayUnlocked.imageset/tray-unlocked@2x.png index 8d1f800..e6302dc 100644 Binary files a/Sources/LockIME/Assets.xcassets/TrayUnlocked.imageset/tray-unlocked@2x.png and b/Sources/LockIME/Assets.xcassets/TrayUnlocked.imageset/tray-unlocked@2x.png differ diff --git a/Sources/LockIME/LockIMEApp.swift b/Sources/LockIME/LockIMEApp.swift index a09a7a1..e25a2b2 100644 --- a/Sources/LockIME/LockIMEApp.swift +++ b/Sources/LockIME/LockIMEApp.swift @@ -28,9 +28,8 @@ struct LockIMEApp: App { MenuBarView() .localized(with: appState) } label: { - // The mascot is the state: hugging the keyboard = locked, - // snacking on bamboo = unlocked. Monochrome template glyphs so the - // system supplies the menu-bar tint (light/dark/active). + // Centered monochrome template glyphs let the system supply the + // menu-bar tint for light, dark, and active states. Image(appState.isLocked ? "TrayLocked" : "TrayUnlocked") .background(SettingsActionBridge(appState: appState)) } diff --git a/scripts/icon-tools/MakeTrayIcon.swift b/scripts/icon-tools/MakeTrayIcon.swift index ebff617..652c02e 100644 --- a/scripts/icon-tools/MakeTrayIcon.swift +++ b/scripts/icon-tools/MakeTrayIcon.swift @@ -1,103 +1,147 @@ -// MakeTrayIcon.swift — renders LockIME's menu-bar (tray) template glyphs: -// a keyboard with a small padlock badge at the top-right corner. Locked shows -// a closed shackle; unlocked pops the shackle open. Drawn vector-crisp with -// CoreGraphics at 1x/2x — solid black + alpha, for template rendering. +// MakeTrayIcon.swift - renders LockIME's menu-bar (tray) template glyphs: +// a centered rounded input panel with a keyhole symbol. Both states share the +// same panel geometry; locked uses a filled keyhole, while unlocked uses an +// outlined keyhole. Drawn vector-crisp with CoreGraphics at 1x/2x as solid +// black + alpha for template rendering. // // swift scripts/icon-tools/MakeTrayIcon.swift -// → tray-locked.png/tray-locked@2x.png/tray-unlocked.png/tray-unlocked@2x.png +// -> tray-locked.png/tray-locked@2x.png/tray-unlocked.png/tray-unlocked@2x.png +// +// If is an asset catalog containing TrayLocked.imageset and +// TrayUnlocked.imageset, files are written directly into those imagesets. import AppKit import CoreGraphics -let outDir = CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : "." -let pt: CGFloat = 18 +let outputPath = CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : "." +let outRoot = URL(fileURLWithPath: outputPath, isDirectory: true) +let pointSize: CGFloat = 18 + +struct TrayState { + let locked: Bool + let baseName: String + let imagesetName: String +} + +let states = [ + TrayState(locked: true, baseName: "tray-locked", imagesetName: "TrayLocked.imageset"), + TrayState(locked: false, baseName: "tray-unlocked", imagesetName: "TrayUnlocked.imageset"), +] + +func outputURL(for state: TrayState, scale: CGFloat) -> URL { + let imageset = outRoot.appendingPathComponent(state.imagesetName, isDirectory: true) + let targetDir = FileManager.default.fileExists(atPath: imageset.path) ? imageset : outRoot + let suffix = scale == 2 ? "@2x" : "" + return targetDir.appendingPathComponent("\(state.baseName)\(suffix).png") +} + +func keyholePath(inset: CGFloat = 0) -> CGPath { + let path = CGMutablePath() + let centerX: CGFloat = 9 + let circleRadius = max(0.55, 1.7 - inset) + let circleCenterY: CGFloat = 10.55 + let stemTopY: CGFloat = 9.0 - inset * 0.3 + let stemBottomY: CGFloat = 6.85 + inset * 0.85 + let stemTopHalfWidth = max(0.35, 0.82 - inset * 0.28) + let stemBottomHalfWidth = max(0.5, 1.28 - inset * 0.6) + + path.addEllipse(in: CGRect( + x: centerX - circleRadius, + y: circleCenterY - circleRadius, + width: circleRadius * 2, + height: circleRadius * 2 + )) + + path.move(to: CGPoint(x: centerX - stemTopHalfWidth, y: stemTopY)) + path.addLine(to: CGPoint(x: centerX + stemTopHalfWidth, y: stemTopY)) + path.addLine(to: CGPoint(x: centerX + stemBottomHalfWidth, y: stemBottomY + 0.25)) + path.addQuadCurve( + to: CGPoint(x: centerX + stemBottomHalfWidth - 0.35, y: stemBottomY), + control: CGPoint(x: centerX + stemBottomHalfWidth, y: stemBottomY) + ) + path.addLine(to: CGPoint(x: centerX - stemBottomHalfWidth + 0.35, y: stemBottomY)) + path.addQuadCurve( + to: CGPoint(x: centerX - stemBottomHalfWidth, y: stemBottomY + 0.25), + control: CGPoint(x: centerX - stemBottomHalfWidth, y: stemBottomY) + ) + path.closeSubpath() + return path +} + +func drawPanel(in ctx: CGContext) { + ctx.setLineWidth(1.75) + ctx.setLineCap(.round) + ctx.setLineJoin(.round) + + let left: CGFloat = 2.2 + let right: CGFloat = 15.8 + let bottom: CGFloat = 2.95 + let top: CGFloat = 15.0 + let radius: CGFloat = 2.45 + + let panel = CGMutablePath() + panel.move(to: CGPoint(x: 5.45, y: bottom)) + panel.addLine(to: CGPoint(x: left + radius, y: bottom)) + panel.addQuadCurve(to: CGPoint(x: left, y: bottom + radius), control: CGPoint(x: left, y: bottom)) + panel.addLine(to: CGPoint(x: left, y: top - radius)) + panel.addQuadCurve(to: CGPoint(x: left + radius, y: top), control: CGPoint(x: left, y: top)) + panel.addLine(to: CGPoint(x: right - radius, y: top)) + panel.addQuadCurve(to: CGPoint(x: right, y: top - radius), control: CGPoint(x: right, y: top)) + panel.addLine(to: CGPoint(x: right, y: bottom + radius)) + panel.addQuadCurve(to: CGPoint(x: right - radius, y: bottom), control: CGPoint(x: right, y: bottom)) + panel.addLine(to: CGPoint(x: 12.55, y: bottom)) + ctx.addPath(panel) + ctx.strokePath() + + let keyCorner: CGFloat = 0.48 + for key in [ + CGRect(x: 4.45, y: 5.15, width: 2.05, height: 0.95), + CGRect(x: 11.5, y: 5.15, width: 2.05, height: 0.95), + CGRect(x: 8.35, y: 2.85, width: 1.3, height: 2.6), + ] { + ctx.addPath(CGPath(roundedRect: key, cornerWidth: keyCorner, cornerHeight: keyCorner, transform: nil)) + ctx.fillPath() + } +} func draw(locked: Bool, scale: CGFloat) -> CGImage? { - let px = Int(pt * scale) + let px = Int(pointSize * scale) guard let ctx = CGContext( data: nil, width: px, height: px, bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue ) else { return nil } ctx.scaleBy(x: scale, y: scale) - ctx.setFillColor(.black) ctx.setStrokeColor(.black) + ctx.setFillColor(.black) - // — Keyboard: a wide rounded rect in the lower ~2/3, outline + key dots. - let kb = CGRect(x: 0.75, y: 0.75, width: 16.5, height: 10.5) - ctx.setLineWidth(1.5) - ctx.addPath(CGPath(roundedRect: kb, cornerWidth: 2.1, cornerHeight: 2.1, transform: nil)) - ctx.strokePath() - - // Key dots: two rows of five. - let dotR: CGFloat = 0.8 - for (rowIdx, y) in [CGFloat(8.0), 5.55].enumerated() { - for i in 0..<5 { - let x = 3.3 + CGFloat(i) * 2.85 + (rowIdx == 1 ? 0.7 : 0) - ctx.fillEllipse(in: CGRect(x: x - dotR, y: y - dotR, width: dotR * 2, height: dotR * 2)) - } - } - // Space bar. - let space = CGRect(x: 5.4, y: 2.45, width: 7.2, height: 1.5) - ctx.addPath(CGPath(roundedRect: space, cornerWidth: 0.75, cornerHeight: 0.75, transform: nil)) + drawPanel(in: ctx) + ctx.addPath(keyholePath()) ctx.fillPath() + if locked { + return ctx.makeImage() + } - // — Badge knockout: clear a halo behind the padlock so it reads as a badge. - let bodyRect = CGRect(x: 11.4, y: 9.6, width: 5.4, height: 4.6) - let badgeBounds = CGRect(x: 10.2, y: 8.4, width: 7.8, height: 9.4) ctx.setBlendMode(.clear) - ctx.addPath(CGPath(roundedRect: badgeBounds, cornerWidth: 2.6, cornerHeight: 2.6, transform: nil)) + ctx.addPath(keyholePath(inset: 0.55)) ctx.fillPath() ctx.setBlendMode(.normal) - // — Padlock body. - ctx.addPath(CGPath(roundedRect: bodyRect, cornerWidth: 1.2, cornerHeight: 1.2, transform: nil)) - ctx.fillPath() - - // — Shackle: closed = ∩ arch seated on the body; open = right leg free, - // arch swung up-right leaving a clear gap. - ctx.setLineWidth(1.35) - ctx.setLineCap(.round) - let cx = bodyRect.midX - let r: CGFloat = 1.75 - let bodyTop = bodyRect.maxY - if locked { - let archY = bodyTop + 1.1 - ctx.beginPath() - ctx.move(to: CGPoint(x: cx - r, y: bodyTop - 0.4)) - ctx.addLine(to: CGPoint(x: cx - r, y: archY)) - ctx.addArc(center: CGPoint(x: cx, y: archY), radius: r, - startAngle: .pi, endAngle: 0, clockwise: true) - ctx.addLine(to: CGPoint(x: cx + r, y: bodyTop - 0.4)) - ctx.strokePath() - } else { - // Hinged at the left leg, swung open ~35° to the right. - let archY = bodyTop + 1.1 - ctx.saveGState() - ctx.translateBy(x: cx - r, y: bodyTop - 0.2) - ctx.rotate(by: 35 * .pi / 180) - ctx.translateBy(x: -(cx - r), y: -(bodyTop - 0.2)) - ctx.beginPath() - ctx.move(to: CGPoint(x: cx - r, y: bodyTop - 0.4)) - ctx.addLine(to: CGPoint(x: cx - r, y: archY)) - ctx.addArc(center: CGPoint(x: cx, y: archY), radius: r, - startAngle: .pi, endAngle: 0, clockwise: true) - ctx.addLine(to: CGPoint(x: cx + r, y: archY - 0.2)) - ctx.strokePath() - ctx.restoreGState() - } - return ctx.makeImage() } -for locked in [true, false] { +for state in states { for scale in [CGFloat(1), 2] { - guard let img = draw(locked: locked, scale: scale) else { continue } + guard let img = draw(locked: state.locked, scale: scale) else { continue } let rep = NSBitmapImageRep(cgImage: img) guard let png = rep.representation(using: .png, properties: [:]) else { continue } - let name = "tray-\(locked ? "locked" : "unlocked")\(scale == 2 ? "@2x" : "").png" - let path = "\(outDir)/\(name)" - try? png.write(to: URL(fileURLWithPath: path)) - print("wrote \(path)") + let url = outputURL(for: state, scale: scale) + do { + try png.write(to: url) + print("wrote \(url.path)") + } catch { + FileHandle.standardError.write(Data("failed to write \(url.path): \(error)\n".utf8)) + exit(1) + } } }