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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 2 additions & 3 deletions Sources/LockIME/LockIMEApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
190 changes: 117 additions & 73 deletions scripts/icon-tools/MakeTrayIcon.swift
Original file line number Diff line number Diff line change
@@ -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 <outDir>
// → 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 <outDir> 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)
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading