diff options
| author | Igor Soarez <igor@soarez.org> | 2026-11-05 19:45:08 +0200 |
|---|---|---|
| committer | Igor Soarez <igor@soarez.org> | 2026-11-05 19:45:08 +0200 |
| commit | 9575ead9c637a2dc482c00f417a1d784d46e076a (patch) | |
| tree | a47abc5722d4f978e7c94428e06634e88101152a /menubar | |
| parent | 5842b093cc502b46076423e4f8efdbbc568f86aa (diff) | |
name pi agents by their reported session name, not the terminal title; scroll a panel list taller than the screen
Diffstat (limited to 'menubar')
| -rw-r--r-- | menubar/panel.swift | 98 |
1 files changed, 84 insertions, 14 deletions
diff --git a/menubar/panel.swift b/menubar/panel.swift index 174d30e..35494e0 100644 --- a/menubar/panel.swift +++ b/menubar/panel.swift @@ -472,9 +472,23 @@ private final class ToggleView: ClickView { override func hoverChanged() { label.textColor = hovered ? mochaText : mochaSubtext } } +private extension Optional where Wrapped == CGFloat { + var orZero: CGFloat { self ?? 0 } +} + +/// A document view whose content starts at the top. +private final class FlippedView: NSView { + override var isFlipped: Bool { true } +} + final class PanelController { private let panel: NSPanel + /// The list (offer, rows, overflow), which scrolls once the panel would + /// reach below the screen; the summary line stays under it. private let stack = NSStackView() + private let scroll = NSScrollView() + private let summary = NSStackView() + private var scrollHeight: NSLayoutConstraint! private let onClick: (String) -> Void private let onMarkUnread: (String) -> Void private let onToggleFull: () -> Void @@ -506,15 +520,39 @@ final class PanelController { let effect = darkHUD() panel.contentView = effect - stack.orientation = .vertical - stack.alignment = .leading - stack.spacing = 0 - stack.translatesAutoresizingMaskIntoConstraints = false - effect.addSubview(stack) + for column in [stack, summary] { + column.orientation = .vertical + column.alignment = .leading + column.spacing = 0 + column.translatesAutoresizingMaskIntoConstraints = false + } + let document = FlippedView() + document.translatesAutoresizingMaskIntoConstraints = false + document.addSubview(stack) + scroll.documentView = document + scroll.drawsBackground = false + scroll.hasVerticalScroller = true + scroll.autohidesScrollers = true + scroll.scrollerStyle = .overlay + scroll.translatesAutoresizingMaskIntoConstraints = false + effect.addSubview(scroll) + effect.addSubview(summary) + scrollHeight = scroll.heightAnchor.constraint(equalToConstant: 0) NSLayoutConstraint.activate([ - stack.leadingAnchor.constraint(equalTo: effect.leadingAnchor, constant: inset), - stack.trailingAnchor.constraint(equalTo: effect.trailingAnchor, constant: -inset), - stack.topAnchor.constraint(equalTo: effect.topAnchor, constant: inset), + scroll.leadingAnchor.constraint(equalTo: effect.leadingAnchor, constant: inset), + scroll.trailingAnchor.constraint(equalTo: effect.trailingAnchor, constant: -inset), + scroll.topAnchor.constraint(equalTo: effect.topAnchor, constant: inset), + scrollHeight, + document.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor), + document.trailingAnchor.constraint(equalTo: scroll.contentView.trailingAnchor), + document.topAnchor.constraint(equalTo: scroll.contentView.topAnchor), + stack.leadingAnchor.constraint(equalTo: document.leadingAnchor), + stack.trailingAnchor.constraint(equalTo: document.trailingAnchor), + stack.topAnchor.constraint(equalTo: document.topAnchor), + stack.bottomAnchor.constraint(equalTo: document.bottomAnchor), + summary.leadingAnchor.constraint(equalTo: scroll.leadingAnchor), + summary.trailingAnchor.constraint(equalTo: scroll.trailingAnchor), + summary.topAnchor.constraint(equalTo: scroll.bottomAnchor), ]) // Remember where the user put it; first time, top right of the screen. @@ -535,9 +573,11 @@ final class PanelController { guard text != rendered else { return false } rendered = text - stack.arrangedSubviews.forEach { view in - stack.removeArrangedSubview(view) - view.removeFromSuperview() + for column in [stack, summary] { + column.arrangedSubviews.forEach { view in + column.removeArrangedSubview(view) + view.removeFromSuperview() + } } if let offer = content.offer, let title = content.offerTitle { add(offerLine(title: title, detail: offer.detail, restoring: content.restoring), fullWidth: true) @@ -552,17 +592,34 @@ final class PanelController { string: "+\(content.overflow) more waiting, see the menu bar", attributes: [.font: NSFont.systemFont(ofSize: 11), .foregroundColor: mochaSubtext]))) } - if content.showsSummary { add(summaryLine(content), fullWidth: true) } + if content.showsSummary { + let line = summaryLine(content) + summary.addArrangedSubview(line) + line.widthAnchor.constraint(equalTo: summary.widthAnchor).isActive = true + } - // Grow or shrink downward from the top edge. + // Grow or shrink downward from the top edge, down to the bottom of + // the screen; past that the list scrolls. stack.layoutSubtreeIfNeeded() - let height = stack.fittingSize.height + inset * 2 + summary.layoutSubtreeIfNeeded() + let list = stack.arrangedSubviews.isEmpty ? 0 : stack.fittingSize.height + let below = summary.arrangedSubviews.isEmpty ? 0 : summary.fittingSize.height let top = panel.frame.maxY + let room = top - (panel.screen ?? NSScreen.main).map { $0.visibleFrame.minY + 16 }.orZero + // At least a few rows, wherever the panel was dragged. + let shown = max(min(list, 150), min(list, room - below - inset * 2)) + scrollHeight.constant = shown + let height = shown + below + inset * 2 panel.setFrame(NSRect(x: panel.frame.minX, y: top - height, width: panelWidth, height: height), display: true) + listScrolls = shown < list return true } + /// Whether the list is taller than the panel shows (tests). + private(set) var listScrolls = false + var frame: NSRect { panel.frame } + private func add(_ view: NSView, fullWidth: Bool = false) { stack.addArrangedSubview(view) if fullWidth { view.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true } @@ -785,6 +842,19 @@ func panelSelftest() -> [String] { check(full.plainText.hasSuffix("[Show less ▴]") && !compact.plainText.contains("Show"), "toggle offered only when allowed, labelled by layout") + // A long list scrolls instead of running off the screen. + _ = NSApplication.shared + if let area = NSScreen.main?.visibleFrame { + let panel = PanelController(persistFrame: false, onClick: { _ in }, onToggleFull: {}) + panel.render(PanelContent(rows: (0..<80).map { row("long\($0)", .idle) }, full: true, canToggle: true)) + check(panel.frame.minY >= area.minY && panel.frame.maxY <= area.maxY && panel.listScrolls, + "panel: 80 rows stay on screen and scroll (\(panel.frame) in \(area))") + panel.render(PanelContent(rows: (0..<3).map { row("short\($0)", .idle) }, full: true, canToggle: true)) + check(!panel.listScrolls && panel.frame.height < 300, "panel: 3 rows shown whole (\(panel.frame))") + } else { + check(false, "panel: no screen to measure against") + } + // A failed run. var broken = row("b", .done) broken.error = "401: invalid x-api-key" |
