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
4 changes: 0 additions & 4 deletions Sources/Flock/Theme/ChromeMetrics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,6 @@ enum ChromeMetrics {
static let markEchoOpacity: Double = 0.4
/// A finished herd, which stays listed until its workspace closes.
static let finishedOpacity: Double = 0.45
/// The loader's own loop, slowed and shallowed: rail furniture that
/// moves is only ever a hint, never a spinner.
static let motionTempo: Double = 0.4
static let motionDepth: Double = 0.5
}

enum Strip {
Expand Down
22 changes: 4 additions & 18 deletions Sources/Flock/Views/HerdsSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct HerdsSection: View {
// The section's one mark. Rows carry none: one moving glyph
// says "something in here is still going" without a row of
// them competing for the eye.
HerdMark(theme: theme, size: ChromeMetrics.RailSection.herdsMark, isMoving: summary.isAnyRunning)
HerdMark(theme: theme, size: ChromeMetrics.RailSection.herdsMark)
.offset(y: -ChromeMetrics.RailSection.herdsMarkLift)
} trailing: {
// The summary is the only notice a folded section gives, so
Expand Down Expand Up @@ -78,28 +78,14 @@ private struct HerdRow: View {
}
}

/// The ram as a grey glyph, drifting while its herd works and still when it
/// does not. Only the echoes move, as in the loader, so the leader stays put
/// where the eye already found it.
/// The ram as a grey glyph, never animated: a per-frame timeline here
/// re-lays out the whole window on every display frame while a herd runs.
struct HerdMark: View {
let theme: Theme
let size: CGFloat
let isMoving: Bool

@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var start = Date()

var body: some View {
if isMoving && !reduceMotion {
TimelineView(.animation) { context in
let elapsed = context.date.timeIntervalSince(start) * ChromeMetrics.Herds.motionTempo
HerdrRamMark(size: size, progress: { delay in
PaneLoaderChoreography.mergeProgress(elapsed: elapsed, startDelay: delay) * ChromeMetrics.Herds.motionDepth
}, tint: theme.textLabel, echoOpacityScale: ChromeMetrics.Herds.markEchoOpacity)
}
} else {
HerdrRamMark(size: size, tint: theme.textLabel, echoOpacityScale: ChromeMetrics.Herds.markEchoOpacity)
}
HerdrRamMark(size: size, tint: theme.textLabel, echoOpacityScale: ChromeMetrics.Herds.markEchoOpacity)
}
}

Expand Down
12 changes: 9 additions & 3 deletions Sources/Flock/Views/PaneLoaderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ import SwiftUI
///
/// The mark's brand colours are `HerdrRamTrail`'s, shared with the app icon
/// so this can never repaint the ram in the active theme; the trail's dots
/// follow `theme`. Under reduced motion the mark rests, still, in the
/// bottom-right corner the run starts from.
/// follow `theme`. Under reduced motion, and once the run has gone on for
/// `PaneLoaderPolicy.motionLimit`, the mark rests, still, in the bottom-right
/// corner the run starts from.
struct PaneLoaderView: View {
let theme: Theme
/// Lets a render test force the static path deterministically. Always
Expand All @@ -31,6 +32,7 @@ struct PaneLoaderView: View {
@Environment(\.accessibilityReduceMotion) private var systemReduceMotion
@State private var appeared = false
@State private var start = Date()
@State private var hasRunOut = false

private var reduceMotion: Bool { reducedMotionOverride ?? systemReduceMotion }

Expand All @@ -45,7 +47,7 @@ struct PaneLoaderView: View {

var body: some View {
Group {
if reduceMotion {
if reduceMotion || hasRunOut {
HerdrRamMark(size: Self.markSize)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
.padding(ChromeMetrics.Loader.badgeInset)
Expand All @@ -61,6 +63,10 @@ struct PaneLoaderView: View {
.onAppear {
withAnimation(.easeOut(duration: PaneLoaderPolicy.dismissCrossFade)) { appeared = true }
}
.task {
guard frozenElapsed == nil, (try? await Task.sleep(for: PaneLoaderPolicy.motionLimit)) != nil else { return }
withAnimation(.easeOut(duration: PaneLoaderPolicy.dismissCrossFade)) { hasRunOut = true }
}
// Nothing here is aimable, and the pane underneath owns every click
// it would otherwise swallow.
.allowsHitTesting(false)
Expand Down
2 changes: 1 addition & 1 deletion Sources/Flock/Views/WorkspaceMark.swift
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ struct WorkspaceMark: View {
.frame(width: size, height: size)
.accessibilityHidden(true)
} else {
HerdMark(theme: theme, size: size, isMoving: false)
HerdMark(theme: theme, size: size)
.frame(width: size, height: size)
}
}
Expand Down
6 changes: 2 additions & 4 deletions Sources/FlockCore/Herdr/HerdRail.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,9 @@ public struct HerdRail: Equatable, Sendable {
public struct Summary: Equatable, Sendable {
public let running: Int
public let done: Int
public let isAnyRunning: Bool

/// Herds finished out of all herds, in the same N/M form each herd
/// row uses for its workers. Whether anything is still moving is the
/// glyph's to say, which is what lets this fit the default rail
/// row uses for its workers, short enough to fit the default rail
/// beside the heading and its chevron.
public var text: String { "\(done)/\(running + done) done" }
}
Expand All @@ -38,7 +36,7 @@ public struct HerdRail: Equatable, Sendable {
static func summary(of herds: [Herd]) -> Summary? {
guard !herds.isEmpty else { return nil }
let done = herds.filter(\.isFinished).count
return Summary(running: herds.count - done, done: done, isAnyRunning: herds.contains(where: \.isRunning))
return Summary(running: herds.count - done, done: done)
}

/// `progress` is rt's count for each herd it answered for, keyed by
Expand Down
4 changes: 4 additions & 0 deletions Sources/FlockCore/Marks/PaneLoaderPolicy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ public enum PaneLoaderPolicy {
/// `Duration` because SwiftUI's animation curves take a `Double`.
public static let dismissCrossFade: Double = 0.1

/// How long the badge runs before it rests. A running badge redraws every
/// display frame, so a pane that never paints must not hold that cost.
public static let motionLimit: Duration = .seconds(10)

public static func dismissAt(
shownAt: ContinuousClock.Instant,
firstFrameAt: ContinuousClock.Instant,
Expand Down
11 changes: 3 additions & 8 deletions Tests/FlockCoreTests/HerdRailTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -204,35 +204,30 @@ final class HerdRailTests: XCTestCase {
])).summary
XCTAssertEqual(summary?.running, 2)
XCTAssertEqual(summary?.done, 1)
XCTAssertEqual(summary?.isAnyRunning, true)
XCTAssertEqual(summary?.text, "1/3 done")
}

func testEveryHerdDoneReadsOnlyDoneAndStops() {
func testEveryHerdDoneReadsOnlyDone() {
let summary = HerdRail(model: model([
WorkspaceSpec(id: "w1", label: "herd: review-shapes", statuses: [.done]),
WorkspaceSpec(id: "w2", label: "herd: ci-sweep", statuses: [.idle, .done]),
])).summary
XCTAssertEqual(summary?.text, "2/2 done")
XCTAssertEqual(summary?.isAnyRunning, false)
}

func testNoHerdDoneYetReadsOnlyRunning() {
let summary = HerdRail(model: model([
WorkspaceSpec(id: "w1", label: "herd: review-shapes", statuses: [.working]),
])).summary
XCTAssertEqual(summary?.text, "0/1 done")
XCTAssertEqual(summary?.isAnyRunning, true)
}

/// A herd held open by a pane with no agent still counts as running,
/// while the mark stays still because nothing in it is working.
func testAnOpenHerdWithNothingWorkingIsRunningButStill() {
/// A herd held open by a pane with no agent still counts as running.
func testAnOpenHerdWithNothingWorkingIsRunning() {
let summary = HerdRail(model: model([
WorkspaceSpec(id: "w1", label: "herd: review-shapes", statuses: [.done, .unknown]),
])).summary
XCTAssertEqual(summary?.running, 1)
XCTAssertEqual(summary?.isAnyRunning, false)
}

// MARK: - Lookup by id
Expand Down
4 changes: 4 additions & 0 deletions docs/releases/0.9.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## Fixes

- Flock no longer burns most of a CPU core while a herd is running. The ram beside Herds now stays still.
- A pane that takes a long time to attach stops animating its loading badge after 10 seconds.
Loading