Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
17 kB · 392 lines
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393// WeekView.swift — custom-drawn NSView showing one week (Sun→Sat) with// hour rows (6:00–24:00). Today's column is highlighted and a "now" line// is drawn. Each event renders as a rounded box positioned by start/end.//// AESTHETICAL events : solid fill, editable (click → edit sheet).// GOOGLE/feed events : outline + a source-color dot, read-only// (click shows read-only details, NO edit).//// A small legend of source colors sits along the bottom.import AppKit
// A unified, laid-out event the view can draw regardless of source.struct LaidEvent { enum Kind { case aesthetical, feed } var kind: Kind var uid: String var title: String var start: Date var end: Date var allDay: Bool var note: String var visibility: String // aesthetical only var sourceLabel: String? // feed only var color: NSColor? // feed source color}
protocol WeekViewDelegate: AnyObject { // Clicking an editable (aesthetical) event opens the editor. func weekView(_ view: WeekView, didSelectEditable event: LaidEvent) // Clicking a read-only (feed) event shows details. func weekView(_ view: WeekView, didSelectReadOnly event: LaidEvent) // Clicking an empty slot proposes a new event at that day/hour. func weekView(_ view: WeekView, didRequestNewEventAt day: Date, hour: Int)}
final class WeekView: NSView { weak var delegate: WeekViewDelegate?
// The local-midnight date that begins the displayed range (a Sunday in // week mode; any day in day mode). var weekStart: Date = WeekView.startOfWeek(for: Date()) { didSet { needsDisplay = true } } // How many day columns to draw: 7 = week, 1 = single day. var dayCount: Int = 7 { didSet { needsDisplay = true } } var events: [LaidEvent] = [] { didSet { needsDisplay = true } }
private let startHour = 6 private let endHour = 24 private let headerH: CGFloat = 38 private let legendH: CGFloat = 26 private let gutterW: CGFloat = 44
// KidLisp Friday — @jeffrey's standing weekly ritual (keep/drop new pieces + // push KidLisp infra/story/platform). Marked on every Friday column in the // pals pink (#cd5c9b), regardless of sign-in state. private static let kidLispPink = NSColor(calibratedRed: 0.804, green: 0.361, blue: 0.608, alpha: 1) private func isKidLispFriday(_ date: Date) -> Bool { Calendar.current.component(.weekday, from: date) == 6 // 1=Sun … 6=Fri }
// Hit-test boxes recomputed each draw. private struct HitBox { var rect: NSRect; var event: LaidEvent } private var hitBoxes: [HitBox] = []
override var isFlipped: Bool { true } // y grows downward — natural for a day grid.
// ── geometry helpers ─────────────────────────────────────────────
static func startOfWeek(for date: Date) -> Date { var cal = Calendar.current cal.firstWeekday = 1 // Sunday let comps = cal.dateComponents([.yearForWeekOfYear, .weekOfYear], from: date) return cal.date(from: comps) ?? date }
private var hourCount: Int { endHour - startHour }
private var gridRect: NSRect { NSRect(x: gutterW, y: headerH, width: bounds.width - gutterW, height: bounds.height - headerH - legendH) }
private func columnWidth() -> CGFloat { gridRect.width / CGFloat(max(1, dayCount)) } private func rowHeight() -> CGFloat { gridRect.height / CGFloat(hourCount) }
// Day index 0..<dayCount for a date relative to weekStart (-1 if outside). // Compare by start-of-day so a partial-day offset (e.g. tonight vs. tomorrow // midnight) doesn't round to 0 and leak into the wrong column. private func dayIndex(for date: Date) -> Int { let cal = Calendar.current let days = cal.dateComponents([.day], from: cal.startOfDay(for: weekStart), to: cal.startOfDay(for: date)).day ?? -1 return (days >= 0 && days < dayCount) ? days : -1 }
// Fractional hours-from-startHour for a date (clamped to the visible band). private func fractionalHour(for date: Date) -> CGFloat { let cal = Calendar.current let h = cal.component(.hour, from: date) let m = cal.component(.minute, from: date) let v = CGFloat(h) + CGFloat(m) / 60.0 - CGFloat(startHour) return max(0, min(CGFloat(hourCount), v)) }
private func dayDate(_ idx: Int) -> Date { Calendar.current.date(byAdding: .day, value: idx, to: weekStart) ?? weekStart }
// ── drawing ──────────────────────────────────────────────────────
// Scrim opacity over the living backdrop. 0 = fully transparent (raw // video), 1 = solid. Low enough to see the DateWizard loop behind the // cells, high enough to keep small calendar text legible. private let scrimAlpha: CGFloat = 0.24
override func draw(_ dirtyRect: NSRect) { // Transparent background: the living DateWizard backdrop (sibling // BackdropView, drawn behind this view) shows through. Only a faint // scrim is laid down for legibility. NSColor.windowBackgroundColor.withAlphaComponent(scrimAlpha).setFill() bounds.fill()
drawHeaderAndGrid() drawEvents() drawNowLine() drawLegend() }
private func drawHeaderAndGrid() { let grid = gridRect let colW = columnWidth() let rowH = rowHeight() let cal = Calendar.current let today = dayIndex(for: Date())
// Short weekday names, indexed by calendar weekday (1 = Sunday). In day // mode the single column can be any weekday, so derive the name from the // column's actual date rather than assuming column 0 is Sunday. let dayNames = ["", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
// KidLisp Friday — a soft pink wash down every Friday column so the // ritual is always visible on the calendar (drawn under the grid lines). for d in 0..<dayCount where isKidLispFriday(dayDate(d)) { WeekView.kidLispPink.withAlphaComponent(0.09).setFill() NSRect(x: grid.minX + CGFloat(d) * colW, y: 0, width: colW, height: bounds.height - legendH).fill() }
// Today's column highlight — washed in today's own day color. if today >= 0 { DayPalette.color(for: dayDate(today)).withAlphaComponent(0.10).setFill() NSRect(x: grid.minX + CGFloat(today) * colW, y: 0, width: colW, height: bounds.height - legendH).fill() }
// Hour grid lines + labels. NSColor.separatorColor.withAlphaComponent(0.5).setStroke() for r in 0...hourCount { let y = grid.minY + CGFloat(r) * rowH let line = NSBezierPath() line.move(to: NSPoint(x: grid.minX, y: y)) line.line(to: NSPoint(x: grid.maxX, y: y)) line.lineWidth = 0.5 line.stroke() if r < hourCount { let hour = startHour + r let label = String(format: "%02d:00", hour) let attrs: [NSAttributedString.Key: Any] = [ .font: NSFont.monospacedSystemFont(ofSize: 9, weight: .regular), .foregroundColor: NSColor.secondaryLabelColor, ] (label as NSString).draw(at: NSPoint(x: 4, y: y + 2), withAttributes: attrs) } }
// Day columns + header labels. for d in 0...dayCount { let x = grid.minX + CGFloat(d) * colW let line = NSBezierPath() line.move(to: NSPoint(x: x, y: headerH)) line.line(to: NSPoint(x: x, y: grid.maxY)) line.lineWidth = 0.5 line.stroke() } for d in 0..<dayCount { let date = dayDate(d) let dayNum = cal.component(.day, from: date) let weekday = cal.component(.weekday, from: date) // 1...7 let isToday = (d == today) // Day mode: a fuller "Tue, Jun 16" label since there's room. let header: String if dayCount == 1 { let mfmt = DateFormatter(); mfmt.dateFormat = "MMM" header = "\(dayNames[weekday]), \(mfmt.string(from: date)) \(dayNum)" } else { header = "\(dayNames[weekday]) \(dayNum)" } // Headers stay black (label color), larger type, today bolder. A // soft shadow keeps them legible over the living backdrop. let style = NSMutableParagraphStyle(); style.alignment = .center let shadow = NSShadow() shadow.shadowColor = NSColor.white.withAlphaComponent(0.6) shadow.shadowBlurRadius = 1.5 shadow.shadowOffset = NSSize(width: 0, height: -0.5) let isFriday = isKidLispFriday(date) let attrs: [NSAttributedString.Key: Any] = [ .font: NSFont.systemFont(ofSize: dayCount == 1 ? 15 : 13, weight: isToday ? .heavy : .bold), .foregroundColor: isFriday ? WeekView.kidLispPink : NSColor.labelColor, .paragraphStyle: style, .shadow: shadow, ] (header as NSString).draw( in: NSRect(x: grid.minX + CGFloat(d) * colW, y: 9, width: colW, height: 20), withAttributes: attrs)
// KidLisp Friday sublabel, tucked under the "Fri N" header. if isFriday { let subAttrs: [NSAttributedString.Key: Any] = [ .font: NSFont.systemFont(ofSize: 8, weight: .heavy), .foregroundColor: WeekView.kidLispPink, .paragraphStyle: style, .shadow: shadow, ] ("🔮 kidlisp" as NSString).draw( in: NSRect(x: grid.minX + CGFloat(d) * colW, y: 25, width: colW, height: 12), withAttributes: subAttrs) } } }
private func drawEvents() { hitBoxes.removeAll() let grid = gridRect let colW = columnWidth() let rowH = rowHeight() let inset: CGFloat = 2
// Stable ordering: aesthetical drawn last so they sit on top. let ordered = events.sorted { a, b in if a.kind == b.kind { return a.start < b.start } return a.kind == .feed // feed first → aesthetical on top }
for ev in ordered { let d = dayIndex(for: ev.start) guard d >= 0 else { continue }
let yTop: CGFloat let yBot: CGFloat if ev.allDay { // All-day events render as a slim band at the very top of the column. yTop = grid.minY yBot = grid.minY + 14 } else { yTop = grid.minY + fractionalHour(for: ev.start) * rowH let endFrac = max(fractionalHour(for: ev.end), fractionalHour(for: ev.start) + 0.4) // min visible height yBot = grid.minY + endFrac * rowH } let rect = NSRect(x: grid.minX + CGFloat(d) * colW + inset, y: yTop + inset, width: colW - inset * 2, height: max(16, yBot - yTop - inset * 2)) let path = NSBezierPath(roundedRect: rect, xRadius: 4, yRadius: 4)
if ev.kind == .aesthetical { // Solid fill — editable. let fill = (ev.visibility == "public") ? NSColor.systemGreen : NSColor.controlAccentColor fill.withAlphaComponent(0.85).setFill() path.fill() fill.setStroke() path.lineWidth = 1 path.stroke() drawEventLabel(ev.title, in: rect, color: .white) } else { // Outline + hatch + source-color dot — read-only. let c = ev.color ?? NSColor.systemGray c.withAlphaComponent(0.10).setFill() path.fill() c.setStroke() path.lineWidth = 1 path.setLineDash([3, 2], count: 2, phase: 0) path.stroke() // Source dot, top-left inside the box. let dot = NSBezierPath(ovalIn: NSRect(x: rect.minX + 4, y: rect.minY + 4, width: 6, height: 6)) c.setFill(); dot.fill() drawEventLabel(ev.title, in: rect.insetBy(dx: 12, dy: 0).offsetBy(dx: 0, dy: 0), color: .labelColor) } hitBoxes.append(HitBox(rect: rect, event: ev)) } }
private func drawEventLabel(_ title: String, in rect: NSRect, color: NSColor) { let style = NSMutableParagraphStyle() style.lineBreakMode = .byTruncatingTail let attrs: [NSAttributedString.Key: Any] = [ .font: NSFont.systemFont(ofSize: 10, weight: .medium), .foregroundColor: color, .paragraphStyle: style, ] (title as NSString).draw( in: NSRect(x: rect.minX + 4, y: rect.minY + 2, width: rect.width - 6, height: min(rect.height - 2, 28)), withAttributes: attrs) }
private func drawNowLine() { let now = Date() let d = dayIndex(for: now) guard d >= 0 else { return } let grid = gridRect let colW = columnWidth() let rowH = rowHeight() let y = grid.minY + fractionalHour(for: now) * rowH let x0 = grid.minX + CGFloat(d) * colW NSColor.systemRed.setStroke() let line = NSBezierPath() line.move(to: NSPoint(x: x0, y: y)) line.line(to: NSPoint(x: x0 + colW, y: y)) line.lineWidth = 1.5 line.stroke() let knob = NSBezierPath(ovalIn: NSRect(x: x0 - 2, y: y - 2, width: 5, height: 5)) NSColor.systemRed.setFill(); knob.fill() }
private func drawLegend() { let y = bounds.height - legendH + 6 var x: CGFloat = gutterW func chip(_ label: String, _ color: NSColor, dashed: Bool) { let box = NSRect(x: x, y: y, width: 14, height: 12) let path = NSBezierPath(roundedRect: box, xRadius: 2, yRadius: 2) if dashed { color.withAlphaComponent(0.15).setFill(); path.fill() color.setStroke(); path.lineWidth = 1 path.setLineDash([3, 2], count: 2, phase: 0); path.stroke() } else { color.withAlphaComponent(0.85).setFill(); path.fill() } let attrs: [NSAttributedString.Key: Any] = [ .font: NSFont.systemFont(ofSize: 9), .foregroundColor: NSColor.secondaryLabelColor, ] (label as NSString).draw(at: NSPoint(x: x + 18, y: y), withAttributes: attrs) x += 18 + (label as NSString).size(withAttributes: attrs).width + 14 } chip("aesthetical", NSColor.controlAccentColor, dashed: false) chip("public", NSColor.systemGreen, dashed: false) // One chip per distinct feed source color present this week. var seen = Set<String>() for ev in events where ev.kind == .feed { let label = ev.sourceLabel ?? "calendar" if seen.contains(label) { continue } seen.insert(label) chip(label, ev.color ?? NSColor.systemGray, dashed: true) } }
// ── interaction ──────────────────────────────────────────────────
override func mouseDown(with event: NSEvent) { let p = convert(event.locationInWindow, from: nil) // Topmost box wins — hitBoxes are in draw order (feed first), so // iterate in reverse to prefer aesthetical events on top. for box in hitBoxes.reversed() where box.rect.contains(p) { if box.event.kind == .aesthetical { delegate?.weekView(self, didSelectEditable: box.event) } else { delegate?.weekView(self, didSelectReadOnly: box.event) } return } // Empty slot → propose a new event at that day/hour. let grid = gridRect guard grid.contains(p) else { return } let colW = columnWidth() let rowH = rowHeight() let d = Int((p.x - grid.minX) / colW) let hourOffset = Int((p.y - grid.minY) / rowH) guard d >= 0 && d < dayCount else { return } let day = dayDate(d) delegate?.weekView(self, didRequestNewEventAt: day, hour: startHour + hourOffset) }}