macOS: fix the animation of showing&hiding command palette (#9698)
https://github.com/user-attachments/assets/0069d634-4156-486f-9b59-141fb4565a19 > [!NOTE] > AI proofread my comments.pull/9123/merge
commit
9511b237f3
|
|
@ -44,6 +44,7 @@ struct CommandPaletteView: View {
|
||||||
@State private var query = ""
|
@State private var query = ""
|
||||||
@State private var selectedIndex: UInt?
|
@State private var selectedIndex: UInt?
|
||||||
@State private var hoveredOptionID: UUID?
|
@State private var hoveredOptionID: UUID?
|
||||||
|
@FocusState private var isTextFieldFocused: Bool
|
||||||
|
|
||||||
// The options that we should show, taking into account any filtering from
|
// The options that we should show, taking into account any filtering from
|
||||||
// the query.
|
// the query.
|
||||||
|
|
@ -72,7 +73,7 @@ struct CommandPaletteView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
CommandPaletteQuery(query: $query) { event in
|
CommandPaletteQuery(query: $query, isTextFieldFocused: _isTextFieldFocused) { event in
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case .exit:
|
case .exit:
|
||||||
isPresented = false
|
isPresented = false
|
||||||
|
|
@ -144,6 +145,28 @@ struct CommandPaletteView: View {
|
||||||
.shadow(radius: 32, x: 0, y: 12)
|
.shadow(radius: 32, x: 0, y: 12)
|
||||||
.padding()
|
.padding()
|
||||||
.environment(\.colorScheme, scheme)
|
.environment(\.colorScheme, scheme)
|
||||||
|
.onChange(of: isPresented) { newValue in
|
||||||
|
// Reset focus when quickly showing and hiding.
|
||||||
|
// macOS will destroy this view after a while,
|
||||||
|
// so task/onAppear will not be called again.
|
||||||
|
// If you toggle it rather quickly, we reset
|
||||||
|
// it here when dismissing.
|
||||||
|
isTextFieldFocused = newValue
|
||||||
|
if !isPresented {
|
||||||
|
// This is optional, since most of the time
|
||||||
|
// there will be a delay before the next use.
|
||||||
|
// To keep behavior the same as before, we reset it.
|
||||||
|
query = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
// Grab focus on the first appearance.
|
||||||
|
// This happens right after onAppear,
|
||||||
|
// so we don’t need to dispatch it again.
|
||||||
|
// Fixes: https://github.com/ghostty-org/ghostty/issues/8497
|
||||||
|
// Also fixes initial focus while animating.
|
||||||
|
isTextFieldFocused = isPresented
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,6 +176,12 @@ fileprivate struct CommandPaletteQuery: View {
|
||||||
var onEvent: ((KeyboardEvent) -> Void)? = nil
|
var onEvent: ((KeyboardEvent) -> Void)? = nil
|
||||||
@FocusState private var isTextFieldFocused: Bool
|
@FocusState private var isTextFieldFocused: Bool
|
||||||
|
|
||||||
|
init(query: Binding<String>, isTextFieldFocused: FocusState<Bool>, onEvent: ((KeyboardEvent) -> Void)? = nil) {
|
||||||
|
_query = query
|
||||||
|
self.onEvent = onEvent
|
||||||
|
_isTextFieldFocused = isTextFieldFocused
|
||||||
|
}
|
||||||
|
|
||||||
enum KeyboardEvent {
|
enum KeyboardEvent {
|
||||||
case exit
|
case exit
|
||||||
case submit
|
case submit
|
||||||
|
|
@ -185,14 +214,6 @@ fileprivate struct CommandPaletteQuery: View {
|
||||||
.frame(height: 48)
|
.frame(height: 48)
|
||||||
.textFieldStyle(.plain)
|
.textFieldStyle(.plain)
|
||||||
.focused($isTextFieldFocused)
|
.focused($isTextFieldFocused)
|
||||||
.onAppear {
|
|
||||||
// We want to grab focus on appearance. We have to do this after a tick
|
|
||||||
// on macOS Tahoe otherwise this doesn't work. See:
|
|
||||||
// https://github.com/ghostty-org/ghostty/issues/8497
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
isTextFieldFocused = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: isTextFieldFocused) { focused in
|
.onChange(of: isTextFieldFocused) { focused in
|
||||||
if !focused {
|
if !focused {
|
||||||
onEvent?(.exit)
|
onEvent?(.exit)
|
||||||
|
|
|
||||||
|
|
@ -90,19 +90,19 @@ struct TerminalCommandPaletteView: View {
|
||||||
backgroundColor: ghosttyConfig.backgroundColor,
|
backgroundColor: ghosttyConfig.backgroundColor,
|
||||||
options: commandOptions
|
options: commandOptions
|
||||||
)
|
)
|
||||||
.transition(
|
|
||||||
.move(edge: .top)
|
|
||||||
.combined(with: .opacity)
|
|
||||||
.animation(.spring(response: 0.4, dampingFraction: 0.8))
|
|
||||||
) // Spring animation
|
|
||||||
.zIndex(1) // Ensure it's on top
|
.zIndex(1) // Ensure it's on top
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .top)
|
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .top)
|
||||||
}
|
}
|
||||||
|
.transition(
|
||||||
|
.move(edge: .top)
|
||||||
|
.combined(with: .opacity)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.animation(.spring(response: 0.4, dampingFraction: 0.8), value: isPresented)
|
||||||
.onChange(of: isPresented) { newValue in
|
.onChange(of: isPresented) { newValue in
|
||||||
// When the command palette disappears we need to send focus back to the
|
// When the command palette disappears we need to send focus back to the
|
||||||
// surface view we were overlaid on top of. There's probably a better way
|
// surface view we were overlaid on top of. There's probably a better way
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue