summaryrefslogtreecommitdiff
path: root/src/macos/Sources/AppDelegate.swift
diff options
context:
space:
mode:
Diffstat (limited to 'src/macos/Sources/AppDelegate.swift')
-rw-r--r--src/macos/Sources/AppDelegate.swift133
1 files changed, 122 insertions, 11 deletions
diff --git a/src/macos/Sources/AppDelegate.swift b/src/macos/Sources/AppDelegate.swift
index f5ca4080..cda35ff0 100644
--- a/src/macos/Sources/AppDelegate.swift
+++ b/src/macos/Sources/AppDelegate.swift
@@ -15,6 +15,9 @@ import AppKit
final class AppDelegate: NSObject, NSApplicationDelegate, PardesViewDelegate {
private var window: NSWindow!
private var view: PardesView!
+ /// The blur behind the grid, shown only while the theme declares no
+ /// background of its own. See applyTheme.
+ private var backdrop: NSVisualEffectView!
// One pump chain at a time. pump() re-arms itself while a theme transition
// is in flight and every input pumps as well, so without this a burst of
// keys during a fade would leave one 60 Hz chain per keystroke, all of them
@@ -61,7 +64,32 @@ final class AppDelegate: NSObject, NSApplicationDelegate, PardesViewDelegate {
// isReleasedWhenClosed on would have AppKit release it out from under
// that reference the moment the close button is pressed.
window.isReleasedWhenClosed = false
- window.contentView = view
+ // The grid and the blur are SIBLINGS in a plain container, not parent
+ // and child. A transparent theme has to show something through the
+ // grid, and AppKit will not blur what is behind a window unless an
+ // NSVisualEffectView asks it to — but the backdrop is hidden for every
+ // theme that brings its own background, and hiding a superview hides
+ // its subviews with it. Nested, an opaque theme drew a blank window.
+ let container = NSView(frame: NSRect(origin: .zero, size: want))
+ container.autoresizesSubviews = true
+ backdrop = NSVisualEffectView(frame: container.bounds)
+ // .underWindowBackground is the material meant for exactly this — the
+ // full-window wash behind content, rather than the sidebar/HUD
+ // materials that carry their own tint. .behindWindow is what samples
+ // the desktop instead of the window's own layers.
+ backdrop.material = .underWindowBackground
+ backdrop.blendingMode = .behindWindow
+ // .active, not .followsWindowActiveState: the grid stays readable when
+ // the window is not key, and a backdrop that flattens to grey on focus
+ // loss makes an unfocused pardes look switched off.
+ backdrop.state = .active
+ backdrop.autoresizingMask = [.width, .height]
+ view.frame = container.bounds
+ view.autoresizingMask = [.width, .height]
+ // Order matters: the blur is BEHIND the grid.
+ container.addSubview(backdrop)
+ container.addSubview(view, positioned: .above, relativeTo: backdrop)
+ window.contentView = container
// Trim the content box down to whole cells: a partial column or row is
// dead space the core can never draw into.
@@ -89,16 +117,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, PardesViewDelegate {
// no grid behind it. macOS offers tabs on any titled resizable window
// unless told otherwise.
window.tabbingMode = .disallowed
- // The same constant PardesView paints as defaultBG. Two places name it
- // because two different things draw: the view fills its own bounds, and
- // AppKit fills the titlebar and every pixel of a live resize the view
- // has not caught up with yet — without this that gap flashes white on
- // every drag. The day the core exposes its theme background over the
- // ABI, both should read it instead of agreeing by hand.
- window.backgroundColor = NSColor(srgbRed: 0x12 / 255, green: 0x12 / 255, blue: 0x12 / 255, alpha: 1)
- // A dark window with a light-mode titlebar reads as a bug. This also
- // gets the traffic-light glyphs and the resize cursor right.
- window.appearance = NSAppearance(named: .darkAqua)
+ // What the core is wearing, not a constant agreed by hand — two things
+ // draw and both have to say the same thing: the view fills its own
+ // bounds, and AppKit fills the titlebar and every pixel of a live
+ // resize the view has not caught up with yet. Without that agreement
+ // the gap flashes on every drag. It also owns window.appearance: a dark
+ // window with a light-mode titlebar reads as a bug, and so does the
+ // reverse the moment someone wears `acme`.
+ applyTheme()
// On screen before pardes_init, so that the backingScaleFactor read
// when seeding the cell metrics below is the one of the screen the
// window actually landed on. Drawing before the core exists costs an
@@ -396,6 +422,86 @@ final class AppDelegate: NSObject, NSApplicationDelegate, PardesViewDelegate {
return ((directory as NSString).appendingPathComponent(expanded) as NSString).standardizingPath
}
+ /// Dress the WINDOW in what the core is wearing: the background AppKit
+ /// paints where the view does not (the titlebar, and the strip a live
+ /// resize outruns), and the blur behind a theme that brings no background
+ /// of its own.
+ ///
+ /// Cheap enough to call every pump: the view compares before it dirties
+ /// itself, and the window properties are written only when the answer
+ /// moved. `dressed` is what makes the FIRST call unconditional — a boot
+ /// theme whose background happened to equal the view's starting guess
+ /// would otherwise leave the window in AppKit's default clothes forever.
+ private var dressed = false
+
+ private func applyTheme() {
+ let changed = view.adoptThemeBG(pardes_theme_bg())
+ guard changed || !dressed else { return }
+ dressed = true
+ if let rgb = view.themeBG {
+ backdrop.isHidden = true
+ window.isOpaque = true
+ window.backgroundColor = NSColor(
+ srgbRed: CGFloat((rgb >> 16) & 0xFF) / 255,
+ green: CGFloat((rgb >> 8) & 0xFF) / 255,
+ blue: CGFloat(rgb & 0xFF) / 255,
+ alpha: 1)
+ // ...and follow the theme into light mode, so the titlebar, the
+ // traffic lights and the resize cursor stop belonging to a
+ // different application than the grid under them. Luminance off
+ // the same sRGB channels the grid is drawn with.
+ let luma = (0.2126 * CGFloat((rgb >> 16) & 0xFF)
+ + 0.7152 * CGFloat((rgb >> 8) & 0xFF)
+ + 0.0722 * CGFloat(rgb & 0xFF)) / 255
+ window.appearance = NSAppearance(named: luma > 0.5 ? .aqua : .darkAqua)
+ } else {
+ // Transparent: the window stops painting anything of its own and
+ // the blur takes over. isOpaque false is what lets the desktop
+ // reach the backdrop at all — a titled window is opaque by default
+ // and would composite over it.
+ backdrop.isHidden = false
+ window.isOpaque = false
+ window.backgroundColor = .clear
+ window.appearance = NSAppearance(named: .darkAqua)
+ }
+ }
+
+ /// The focused pane, in the titlebar: the proxy icon macOS lets you drag
+ /// and Cmd-click for the path, and the dot in the close button that means
+ /// unsaved.
+ ///
+ /// A pure read-out — the window says what the core already decided, and
+ /// nothing here can change it. There is no document ARCHITECTURE behind it
+ /// and deliberately so: no NSDocument, no save panel, no "do you want to
+ /// save" on close. Save is a builtin, the pane's tag says so, and this is
+ /// the same two facts spelled where a Mac user looks for them.
+ ///
+ /// Cached, because setting representedURL makes AppKit hit the filesystem
+ /// for the icon and this runs on every pump.
+ private var shownPath: String?
+ private var shownDirty = false
+
+ private func applyDocument() {
+ let path = pardes_active_path().map { String(cString: $0) }
+ if path != shownPath {
+ shownPath = path
+ // A terminal or an output buffer is not a document: no path means
+ // no proxy icon, rather than a stale one from the last file pane.
+ window.representedURL = path.map { URL(fileURLWithPath: $0) }
+ // ...and the title goes with it. A proxy icon beside a title that
+ // names something else reads as a bug, and AppKit will not fill the
+ // title in for us while `title` has been set by hand. The window is
+ // a SESSION and not a document, so it falls back to the app's own
+ // name the moment focus lands somewhere with no file behind it.
+ window.title = path.map { ($0 as NSString).lastPathComponent } ?? "pardes"
+ }
+ let dirty = pardes_active_dirty()
+ if dirty != shownDirty {
+ shownDirty = dirty
+ window.isDocumentEdited = dirty
+ }
+ }
+
@objc private func inputArrived(_ notification: Notification) {
pump()
}
@@ -419,6 +525,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, PardesViewDelegate {
// read bytes.
_ = pardes_tick()
view.needsDisplay = true
+ // After the tick, because a `Theme` command runs inside it and the
+ // window has to follow the grid in the same frame rather than at the
+ // next launch.
+ applyTheme()
+ applyDocument()
// Two verbs, two patterns, because they are two different answers:
// Exec did something, so it gets .generic, the definite tap of a