mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-21 05:23:19 +00:00
Merge remote-tracking branch 'origin/main' into fix/glance-notice-section
This commit is contained in:
commit
7e74dcd426
6 changed files with 353 additions and 36 deletions
|
|
@ -15,7 +15,7 @@ final class CapacityDockController {
|
|||
private var localEventMonitor: Any?
|
||||
private var globalMouseMonitor: Any?
|
||||
private var hoverPollTimer: Timer?
|
||||
private var lastPolledPointer: CGPoint?
|
||||
private var pointerMonitoringSession = CapacityDockPointerMonitoringSession()
|
||||
|
||||
private var expansionWork: DispatchWorkItem?
|
||||
private var collapseWork: DispatchWorkItem?
|
||||
|
|
@ -122,9 +122,8 @@ final class CapacityDockController {
|
|||
pointerInsideDetail = false
|
||||
hoveredRowProvider = nil
|
||||
|
||||
detailPanel?.orderOut(nil)
|
||||
completeDetailDismissal()
|
||||
railPanel?.orderOut(nil)
|
||||
detailPanel = nil
|
||||
railPanel = nil
|
||||
lastKnownRailTop = nil
|
||||
}
|
||||
|
|
@ -246,6 +245,17 @@ final class CapacityDockController {
|
|||
railPanel = panel
|
||||
}
|
||||
|
||||
/// The bubble is cheap to rebuild and expensive to keep: a hidden, ordered-out
|
||||
/// bubble panel left the app re-laying it out at display cadence for as long
|
||||
/// as it lived (0.1 percent idle CPU before the first hover, 6 percent after).
|
||||
/// Every completed dismissal, animated or immediate, converges here.
|
||||
private func completeDetailDismissal() {
|
||||
model.hoveredProvider = nil
|
||||
detailIsDismissing = false
|
||||
detailPanel?.orderOut(nil)
|
||||
detailPanel = nil
|
||||
}
|
||||
|
||||
private func ensureDetailPanel() {
|
||||
guard detailPanel == nil else { return }
|
||||
|
||||
|
|
@ -283,19 +293,20 @@ final class CapacityDockController {
|
|||
|
||||
private func startEventMonitoring() {
|
||||
guard localEventMonitor == nil, globalMouseMonitor == nil else { return }
|
||||
let monitoringGeneration = pointerMonitoringSession.start()
|
||||
// Global .mouseMoved monitors only deliver while the frontmost app
|
||||
// itself requests mouse-moved events (Chrome does, Finder and Terminal
|
||||
// don't), so the monitors alone leave hover dead over most apps. Poll
|
||||
// the pointer as the activation-independent fallback; the diff guard
|
||||
// makes idle ticks free.
|
||||
// the pointer as the activation-independent fallback. All three paths
|
||||
// feed one coalescer so a monitor event and its following poll do not
|
||||
// publish the same point twice.
|
||||
let timer = Timer(timeInterval: 0.1, repeats: true) { [weak self] _ in
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
let point = NSEvent.mouseLocation
|
||||
if point == self.lastPolledPointer { return }
|
||||
self.lastPolledPointer = point
|
||||
self.updateMouseEventPassthrough(at: point)
|
||||
self.syncPointerHover(at: point)
|
||||
self.enqueuePointerUpdate(
|
||||
at: NSEvent.mouseLocation,
|
||||
monitoringGeneration: monitoringGeneration
|
||||
)
|
||||
}
|
||||
}
|
||||
timer.tolerance = 0.05
|
||||
|
|
@ -304,18 +315,25 @@ final class CapacityDockController {
|
|||
localEventMonitor = NSEvent.addLocalMonitorForEvents(
|
||||
matching: [.leftMouseDown, .rightMouseDown, .keyDown, .mouseMoved]
|
||||
) { [weak self] event in
|
||||
guard let self else { return event }
|
||||
guard let self,
|
||||
self.pointerMonitoringSession.isCurrent(monitoringGeneration) else { return event }
|
||||
if event.type == .mouseMoved {
|
||||
let point = NSEvent.mouseLocation
|
||||
self.updateMouseEventPassthrough(at: point)
|
||||
self.syncPointerHover(at: point)
|
||||
self.enqueuePointerUpdate(
|
||||
at: NSEvent.mouseLocation,
|
||||
monitoringGeneration: monitoringGeneration
|
||||
)
|
||||
} else if event.type == .keyDown, event.keyCode == 53 {
|
||||
if self.model.interaction.handleEscape() {
|
||||
self.hideDetail(animated: false)
|
||||
self.layoutRail(animate: false)
|
||||
}
|
||||
} else if event.type == .leftMouseDown || event.type == .rightMouseDown {
|
||||
self.dismissIfOutside(point: NSEvent.mouseLocation)
|
||||
let point = NSEvent.mouseLocation
|
||||
self.flushPendingPointerUpdate(
|
||||
at: point,
|
||||
monitoringGeneration: monitoringGeneration
|
||||
)
|
||||
self.dismissIfOutside(point: point)
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
|
@ -323,12 +341,20 @@ final class CapacityDockController {
|
|||
matching: [.leftMouseDown, .rightMouseDown, .mouseMoved]
|
||||
) { [weak self] event in
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
guard let self,
|
||||
self.pointerMonitoringSession.isCurrent(monitoringGeneration) else { return }
|
||||
let point = NSEvent.mouseLocation
|
||||
self.updateMouseEventPassthrough(at: point)
|
||||
if event.type == .mouseMoved {
|
||||
self.syncPointerHover(at: point)
|
||||
self.enqueuePointerUpdate(
|
||||
at: point,
|
||||
monitoringGeneration: monitoringGeneration
|
||||
)
|
||||
} else {
|
||||
self.flushPendingPointerUpdate(
|
||||
at: point,
|
||||
monitoringGeneration: monitoringGeneration
|
||||
)
|
||||
self.updateMouseEventPassthrough(at: point)
|
||||
self.dismissIfOutside(point: point)
|
||||
}
|
||||
}
|
||||
|
|
@ -336,11 +362,11 @@ final class CapacityDockController {
|
|||
}
|
||||
|
||||
private func stopEventMonitoring() {
|
||||
pointerMonitoringSession.stop()
|
||||
railPanel?.ignoresMouseEvents = false
|
||||
detailPanel?.ignoresMouseEvents = false
|
||||
hoverPollTimer?.invalidate()
|
||||
hoverPollTimer = nil
|
||||
lastPolledPointer = nil
|
||||
if let localEventMonitor {
|
||||
NSEvent.removeMonitor(localEventMonitor)
|
||||
self.localEventMonitor = nil
|
||||
|
|
@ -351,6 +377,41 @@ final class CapacityDockController {
|
|||
}
|
||||
}
|
||||
|
||||
private func enqueuePointerUpdate(
|
||||
at point: CGPoint,
|
||||
monitoringGeneration: CapacityDockPointerMonitoringSession.Generation
|
||||
) {
|
||||
guard pointerMonitoringSession.enqueue(
|
||||
point,
|
||||
generation: monitoringGeneration
|
||||
) == .scheduleDrain else { return }
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.drainPointerUpdate(monitoringGeneration: monitoringGeneration)
|
||||
}
|
||||
}
|
||||
|
||||
private func drainPointerUpdate(
|
||||
monitoringGeneration: CapacityDockPointerMonitoringSession.Generation
|
||||
) {
|
||||
guard let point = pointerMonitoringSession.drain(
|
||||
generation: monitoringGeneration
|
||||
) else { return }
|
||||
updateMouseEventPassthrough(at: point)
|
||||
syncPointerHover(at: point)
|
||||
}
|
||||
|
||||
/// Makes a mouse-down authoritative without publishing hover changes before
|
||||
/// AppKit delivers the local event to its target view.
|
||||
private func flushPendingPointerUpdate(
|
||||
at point: CGPoint,
|
||||
monitoringGeneration: CapacityDockPointerMonitoringSession.Generation
|
||||
) {
|
||||
_ = pointerMonitoringSession.flush(
|
||||
with: point,
|
||||
generation: monitoringGeneration
|
||||
)
|
||||
}
|
||||
|
||||
/// `NSHostingView.hitTest` can reject a transparent pixel, but AppKit still
|
||||
/// routes that click to the owning window. Make the whole panel ignore mouse
|
||||
/// events while the pointer is over one of those pixels; the global movement
|
||||
|
|
@ -570,19 +631,14 @@ final class CapacityDockController {
|
|||
let provider = model.hoveredProvider
|
||||
model.interaction.setDetailHovered(false)
|
||||
guard let provider, detailPanel?.isVisible == true else {
|
||||
model.hoveredProvider = nil
|
||||
detailIsDismissing = false
|
||||
detailPanel?.orderOut(nil)
|
||||
completeDetailDismissal()
|
||||
return
|
||||
}
|
||||
if animated {
|
||||
dismissDetail(for: provider)
|
||||
} else {
|
||||
stopDetailMotion()
|
||||
model.hoveredProvider = nil
|
||||
detailIsDismissing = false
|
||||
detailPanel?.orderOut(nil)
|
||||
detailPanel?.alphaValue = 1
|
||||
completeDetailDismissal()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1142,10 +1198,7 @@ final class CapacityDockController {
|
|||
guard let self, generation == self.detailMotionGeneration else { return }
|
||||
self.detailMotion = nil
|
||||
if fadeOut {
|
||||
self.detailPanel?.orderOut(nil)
|
||||
self.detailPanel?.alphaValue = 1
|
||||
self.model.hoveredProvider = nil
|
||||
self.detailIsDismissing = false
|
||||
self.completeDetailDismissal()
|
||||
} else {
|
||||
self.detailPanel?.setFrame(target, display: true)
|
||||
self.detailPanel?.alphaValue = 1
|
||||
|
|
@ -1174,10 +1227,7 @@ final class CapacityDockController {
|
|||
)
|
||||
} else {
|
||||
stopDetailMotion()
|
||||
model.hoveredProvider = nil
|
||||
detailIsDismissing = false
|
||||
detailPanel.orderOut(nil)
|
||||
detailPanel.alphaValue = 1
|
||||
completeDetailDismissal()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
import CoreGraphics
|
||||
|
||||
/// Coalesces pointer samples from the event monitors and fallback poller into
|
||||
/// one update per main-runloop turn. Repeated coordinates are ignored even
|
||||
/// after a drain, so the poller does not replay the point a monitor delivered.
|
||||
struct CapacityDockPointerUpdateCoalescer {
|
||||
enum EnqueueResult: Equatable {
|
||||
case scheduleDrain
|
||||
case coalesced
|
||||
case duplicate
|
||||
}
|
||||
|
||||
private var latestPoint: CGPoint?
|
||||
private var pendingPoint: CGPoint?
|
||||
private var hasScheduledDrain = false
|
||||
|
||||
mutating func enqueue(_ point: CGPoint) -> EnqueueResult {
|
||||
guard point != latestPoint else { return .duplicate }
|
||||
latestPoint = point
|
||||
pendingPoint = point
|
||||
|
||||
guard !hasScheduledDrain else { return .coalesced }
|
||||
hasScheduledDrain = true
|
||||
return .scheduleDrain
|
||||
}
|
||||
|
||||
mutating func drain() -> CGPoint? {
|
||||
guard hasScheduledDrain else { return nil }
|
||||
defer {
|
||||
pendingPoint = nil
|
||||
hasScheduledDrain = false
|
||||
}
|
||||
return pendingPoint
|
||||
}
|
||||
|
||||
/// Makes an immediate interaction point authoritative and cancels the
|
||||
/// queued drain that held an older sample.
|
||||
mutating func flush(with point: CGPoint) -> CGPoint {
|
||||
latestPoint = point
|
||||
pendingPoint = nil
|
||||
hasScheduledDrain = false
|
||||
return point
|
||||
}
|
||||
|
||||
mutating func reset() {
|
||||
latestPoint = nil
|
||||
pendingPoint = nil
|
||||
hasScheduledDrain = false
|
||||
}
|
||||
}
|
||||
|
||||
/// Owns one generation of pointer monitoring so callbacks queued by a stopped
|
||||
/// monitor cannot publish into a later session or drain that session's point.
|
||||
struct CapacityDockPointerMonitoringSession {
|
||||
typealias Generation = UInt
|
||||
|
||||
private var generation: Generation = 0
|
||||
private var coalescer = CapacityDockPointerUpdateCoalescer()
|
||||
|
||||
mutating func start() -> Generation {
|
||||
generation &+= 1
|
||||
coalescer.reset()
|
||||
return generation
|
||||
}
|
||||
|
||||
mutating func stop() {
|
||||
generation &+= 1
|
||||
coalescer.reset()
|
||||
}
|
||||
|
||||
func isCurrent(_ candidate: Generation) -> Bool {
|
||||
candidate == generation
|
||||
}
|
||||
|
||||
mutating func enqueue(
|
||||
_ point: CGPoint,
|
||||
generation candidate: Generation
|
||||
) -> CapacityDockPointerUpdateCoalescer.EnqueueResult? {
|
||||
guard isCurrent(candidate) else { return nil }
|
||||
return coalescer.enqueue(point)
|
||||
}
|
||||
|
||||
mutating func drain(generation candidate: Generation) -> CGPoint? {
|
||||
guard isCurrent(candidate) else { return nil }
|
||||
return coalescer.drain()
|
||||
}
|
||||
|
||||
mutating func flush(
|
||||
with point: CGPoint,
|
||||
generation candidate: Generation
|
||||
) -> CGPoint? {
|
||||
guard isCurrent(candidate) else { return nil }
|
||||
return coalescer.flush(with: point)
|
||||
}
|
||||
}
|
||||
|
|
@ -1398,12 +1398,28 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate, NSM
|
|||
popover.animates = true
|
||||
popover.delegate = self
|
||||
|
||||
popover.contentViewController = makePopoverContent()
|
||||
}
|
||||
|
||||
/// Built fresh for every show and dropped on close: a popover content view
|
||||
/// that outlives its window kept re-laying itself out at display cadence
|
||||
/// after being shown once during the first load (6 percent idle CPU until
|
||||
/// relaunch). Rebuilding a 360pt view on open is far cheaper than that.
|
||||
private func makePopoverContent() -> NSViewController {
|
||||
let content = MenuBarContent()
|
||||
.environment(store)
|
||||
.environment(updateChecker)
|
||||
.frame(width: popoverWidth)
|
||||
|
||||
popover.contentViewController = NSHostingController(rootView: content)
|
||||
let hosting = NSHostingController(rootView: content)
|
||||
// Let the popover size itself from preferredContentSize only. With the
|
||||
// default options the hosting view also pushes min/max content sizes at
|
||||
// its window on every pass, and once the popover has been shown while
|
||||
// loading (content 520pt tall, then the real height), the closed
|
||||
// popover keeps re-laying itself out at display cadence: 6 percent idle
|
||||
// CPU until relaunch.
|
||||
hosting.sizingOptions = [.preferredContentSize]
|
||||
return hosting
|
||||
}
|
||||
|
||||
@objc private func handleButtonClick(_ sender: AnyObject?) {
|
||||
|
|
@ -1427,6 +1443,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate, NSM
|
|||
// closes. The popover's window takes keyboard focus on its own
|
||||
// via makeKeyAndOrderFront, which is enough for keystrokes to
|
||||
// reach the SwiftUI content.
|
||||
if popover.contentViewController == nil {
|
||||
popover.contentViewController = makePopoverContent()
|
||||
}
|
||||
store.menuPopoverVisible = true
|
||||
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
|
||||
if let window = popover.contentViewController?.view.window {
|
||||
|
|
@ -1642,6 +1661,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate, NSM
|
|||
|
||||
func popoverDidClose(_ notification: Notification) {
|
||||
store.menuPopoverVisible = false
|
||||
popover.contentViewController = nil
|
||||
// Catch up on any menubar title updates that were skipped while the
|
||||
// popover was anchored.
|
||||
refreshStatusButton()
|
||||
|
|
|
|||
|
|
@ -327,7 +327,13 @@ final class CapacityDockViewModel {
|
|||
let eased = p * p * (3 - 2 * p)
|
||||
return CapacityDockMetrics.edgeShoulderDepth(scale: scale) * 0.6 * eased
|
||||
}
|
||||
var railAlongPad: CGFloat { CapacityDockMetrics.railAlongPad(scale: scale) + flareCompensation }
|
||||
// Rounded after the flare is added, not before: the panel's length is built from this
|
||||
// padding, and a fractional length re-runs the window's layout at display cadence for
|
||||
// as long as the dock is up. `points` rounds the base, the eased flare then reintroduced
|
||||
// the fraction for the whole attach animation.
|
||||
var railAlongPad: CGFloat {
|
||||
(CapacityDockMetrics.railAlongPad(scale: scale) + flareCompensation).rounded()
|
||||
}
|
||||
var railCrossPad: CGFloat { CapacityDockMetrics.railCrossPad(scale: scale) }
|
||||
var railTopPadding: CGFloat { railAlongPad }
|
||||
var railBottomPadding: CGFloat { railAlongPad }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
import CoreGraphics
|
||||
import Testing
|
||||
@testable import CodeBurnMenubar
|
||||
|
||||
@Suite("Capacity Dock pointer update coalescer")
|
||||
struct CapacityDockPointerUpdateCoalescerTests {
|
||||
@Test("A burst schedules once and drains its latest point")
|
||||
func burstCoalescesToLatestPoint() {
|
||||
var coalescer = CapacityDockPointerUpdateCoalescer()
|
||||
let first = CGPoint(x: 10, y: 20)
|
||||
let second = CGPoint(x: 11, y: 21)
|
||||
let latest = CGPoint(x: 12, y: 22)
|
||||
|
||||
#expect(coalescer.enqueue(first) == .scheduleDrain)
|
||||
#expect(coalescer.enqueue(second) == .coalesced)
|
||||
#expect(coalescer.enqueue(latest) == .coalesced)
|
||||
#expect(coalescer.drain() == latest)
|
||||
#expect(coalescer.drain() == nil)
|
||||
}
|
||||
|
||||
@Test("A poll duplicate of a monitor point is suppressed across a drain")
|
||||
func duplicatePointIsSuppressed() {
|
||||
var coalescer = CapacityDockPointerUpdateCoalescer()
|
||||
let point = CGPoint(x: 40, y: 80)
|
||||
|
||||
#expect(coalescer.enqueue(point) == .scheduleDrain)
|
||||
#expect(coalescer.enqueue(point) == .duplicate)
|
||||
#expect(coalescer.drain() == point)
|
||||
#expect(coalescer.enqueue(point) == .duplicate)
|
||||
#expect(coalescer.drain() == nil)
|
||||
}
|
||||
|
||||
@Test("Reset cancels pending work and forgets duplicate history")
|
||||
func resetStopsPendingUpdate() {
|
||||
var coalescer = CapacityDockPointerUpdateCoalescer()
|
||||
let point = CGPoint(x: 5, y: 9)
|
||||
|
||||
#expect(coalescer.enqueue(point) == .scheduleDrain)
|
||||
coalescer.reset()
|
||||
#expect(coalescer.drain() == nil)
|
||||
#expect(coalescer.enqueue(point) == .scheduleDrain)
|
||||
#expect(coalescer.drain() == point)
|
||||
}
|
||||
|
||||
@Test("A new point schedules another drain after the prior drain")
|
||||
func reschedulesAfterDrain() {
|
||||
var coalescer = CapacityDockPointerUpdateCoalescer()
|
||||
let first = CGPoint(x: 1, y: 2)
|
||||
let second = CGPoint(x: 2, y: 3)
|
||||
|
||||
#expect(coalescer.enqueue(first) == .scheduleDrain)
|
||||
#expect(coalescer.drain() == first)
|
||||
#expect(coalescer.enqueue(second) == .scheduleDrain)
|
||||
#expect(coalescer.drain() == second)
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Capacity Dock pointer monitoring session")
|
||||
struct CapacityDockPointerMonitoringSessionTests {
|
||||
@Test("A click flushes an older movement before its queued drain")
|
||||
func clickFlushInvalidatesQueuedDrain() {
|
||||
var session = CapacityDockPointerMonitoringSession()
|
||||
let insidePoint = CGPoint(x: 40, y: 80)
|
||||
let outsideClick = CGPoint(x: 400, y: 800)
|
||||
let generation = session.start()
|
||||
|
||||
#expect(session.enqueue(insidePoint, generation: generation) == .scheduleDrain)
|
||||
#expect(session.flush(with: outsideClick, generation: generation) == outsideClick)
|
||||
#expect(session.drain(generation: generation) == nil)
|
||||
#expect(session.enqueue(outsideClick, generation: generation) == .duplicate)
|
||||
}
|
||||
|
||||
@Test("Stop invalidates queued callbacks and drains across restart")
|
||||
func staleWorkCannotCrossRestart() {
|
||||
var session = CapacityDockPointerMonitoringSession()
|
||||
let point = CGPoint(x: 40, y: 80)
|
||||
let stalePoint = CGPoint(x: 41, y: 81)
|
||||
let firstGeneration = session.start()
|
||||
|
||||
#expect(session.enqueue(point, generation: firstGeneration) == .scheduleDrain)
|
||||
session.stop()
|
||||
#expect(session.enqueue(stalePoint, generation: firstGeneration) == nil)
|
||||
#expect(session.drain(generation: firstGeneration) == nil)
|
||||
|
||||
let restartedGeneration = session.start()
|
||||
#expect(restartedGeneration != firstGeneration)
|
||||
#expect(session.enqueue(point, generation: restartedGeneration) == .scheduleDrain)
|
||||
|
||||
// A drain queued by the stopped generation must not consume the
|
||||
// restarted session's current stationary point.
|
||||
#expect(session.drain(generation: firstGeneration) == nil)
|
||||
#expect(session.drain(generation: restartedGeneration) == point)
|
||||
}
|
||||
|
||||
@Test("Stopped generations reject click and key callback tokens")
|
||||
func lifecycleTokenInvalidation() {
|
||||
var session = CapacityDockPointerMonitoringSession()
|
||||
let generation = session.start()
|
||||
#expect(session.isCurrent(generation))
|
||||
|
||||
session.stop()
|
||||
#expect(!session.isCurrent(generation))
|
||||
}
|
||||
}
|
||||
|
|
@ -73,6 +73,48 @@ struct CapacityDockPresentationTests {
|
|||
#expect(horizontal.height == CapacityDockMetrics.horizontalRailWidth(scale: 1.2))
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test("Supported settled scales and edges compose integral panel dimensions")
|
||||
func supportedSettledGeometryIsIntegral() {
|
||||
let suite = "CodeBurnMenubarTests.CapacityDock.IntegralGeometry.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
defer { defaults.removePersistentDomain(forName: suite) }
|
||||
CapacityDockPreferences.setSelectedProviders([.codex, .claude, .gemini], defaults: defaults)
|
||||
|
||||
for step in 0...12 {
|
||||
let scale = 0.6 + Double(step) * 0.05
|
||||
CapacityDockPreferences.setScale(scale, defaults: defaults)
|
||||
|
||||
for edge in CapacityDockEdge.allCases {
|
||||
CapacityDockPreferences.setPlacement(
|
||||
dockedEdge: edge,
|
||||
attachmentEdge: edge,
|
||||
normalizedHorizontalOffset: 0.5,
|
||||
normalizedVerticalOffset: 0.5,
|
||||
defaults: defaults
|
||||
)
|
||||
let model = CapacityDockViewModel(
|
||||
preferences: CapacityDockPreferences.load(defaults: defaults)
|
||||
)
|
||||
|
||||
let resting = model.targetPanelSize(forAttachmentProgress: 1)
|
||||
#expect(resting.width.rounded() == resting.width)
|
||||
#expect(resting.height.rounded() == resting.height)
|
||||
#expect(model.railAlongPad.rounded() == model.railAlongPad)
|
||||
|
||||
model.interaction.setRailHovered(true)
|
||||
let expanded = model.targetPanelSize(forAttachmentProgress: 1)
|
||||
#expect(expanded.width.rounded() == expanded.width)
|
||||
#expect(expanded.height.rounded() == expanded.height)
|
||||
|
||||
#expect(model.detailWidth.rounded() == model.detailWidth)
|
||||
// The glance popover's own height is asserted whole across scales by
|
||||
// CapacityDockGlanceTests.heightIsAlwaysWhole, against the signature it
|
||||
// grew when the glance replaced the old row-counted popover.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Docked silhouette flares smoothly into one flush contact chord without horns")
|
||||
func dockedRailSilhouette() {
|
||||
let path = CapacityDockRailShape(bodyWidth: 88, bodyLength: 356, attachmentProgress: 1, edge: .right)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue