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
7 changes: 6 additions & 1 deletion Talkify/CoreHUD/HUDStage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ final class HUDStage {
/// How long the retract animation needs before the panel can order out.
/// A perceptual duration rather than the spring's settling time, per
/// WWDC23 "Animate with springs": don't wait for settling.
private static let dismissDuration = Duration.milliseconds(350)
/// Internal so a test can wait out the retract rather than guess at it.
static let dismissDuration = Duration.milliseconds(350)
private static let messageDuration = Duration.seconds(2)

/// Who holds the shape. `message` is its own occupant rather than a mode of
Expand Down Expand Up @@ -148,6 +149,10 @@ final class HUDStage {
dictationContent.isDismissing = false
dictationContent.text = ""
dictationContent.volatileText = ""
// The shaping caption outlived the shape: it is what decides whether
// the label is mounted, and the label animates every frame while it is.
dictationContent.shapingName = nil
dictationContent.shapingChoiceLabel = nil
dropContent.mode = .none
dropContent.heldIcon = nil
dropContent.transcript = nil
Expand Down
1 change: 1 addition & 0 deletions Talkify/Dictation/HUD/DictationHUDShellView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ struct DictationHUDShellView: View {
palette: settings.glowPalette,
scale: metrics.scale,
activity: label.activity,
isRevealed: content.isRevealed,
reduceMotion: reduceMotion
)
.frame(height: metrics.shapingBandHeight)
Expand Down
7 changes: 6 additions & 1 deletion Talkify/Dictation/HUD/HUDShapingLabel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ struct HUDShapingLabel: View {
let palette: HUDGlowPalette
let scale: CGFloat
let activity: Activity
/// False once the shape has retracted. The timeline pauses with it, the
/// way every other visual here pauses when its session is not live: a
/// running timeline redraws the shader every frame whether or not the
/// panel is on screen.
let isRevealed: Bool
let reduceMotion: Bool

@State private var start = Date()
Expand Down Expand Up @@ -58,7 +63,7 @@ struct HUDShapingLabel: View {
.minimumScaleFactor(0.6)
.foregroundStyle(Color(palette.statusAccent).opacity(0.85))
} else {
TimelineView(.animation) { context in
TimelineView(.animation(paused: !isRevealed)) { context in
let elapsed = context.date.timeIntervalSince(start)
Text(text)
.font(font)
Expand Down
26 changes: 26 additions & 0 deletions TalkifyTests/HUDPlaceholderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,32 @@ struct HUDPlaceholderTests {
)
}

/// The shaping caption decides whether the HUD mounts a label that
/// animates every frame, so state left behind after the shape retracts
/// costs about 20% of a CPU for as long as the app stays open.
@Test func retractingClearsTheShapingCaption() async throws {
let store = AppSettings.previewStore()
store.voiceVisual = .waveform
let stage = HUDStage(settings: store)
let hud = DictationHUDController(stage: stage, settings: store)

hud.showListening(on: CGDirectDisplayID?.none, isLatched: false, settings: session(store))
hud.showLiveText("some words")
hud.showShaping(with: "Tighten grammar")
#expect(stage.dictationContent.shapingName != nil)

hud.hide()
// An event wait, not a time bound. Sleeping for the retract plus a margin
// passed alone and failed under a parallel run, which is the flake #82 and
// #116 were about; this budget only spends itself on a broken run.
for _ in 0..<400 where stage.dictationContent.shapingName != nil {
try await Task.sleep(for: .milliseconds(5))
}

#expect(stage.dictationContent.shapingName == nil, "the caption outlived the shape")
#expect(stage.dictationContent.shapingChoiceLabel == nil)
}

/// The words being rewritten are not a placeholder, so the shaping phase
/// leaves them where they are.
@Test func theShapingPhaseKeepsARealDraft() {
Expand Down