Navigate the ⌘T and ⌘P task lists with arrow keys and Enter
This commit is contained in:
@@ -11,7 +11,6 @@ struct ProjectView: View {
|
||||
@Environment(Overlays.self) private var overlays
|
||||
@ViewState private var todos: [String: Result<Todo, ApiError>] = [:]
|
||||
@ViewState private var off: (() -> Void)?
|
||||
@FocusState private var focused: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
@@ -46,20 +45,10 @@ struct ProjectView: View {
|
||||
Muted(text: "No project selected.")
|
||||
}
|
||||
}
|
||||
.followCursor()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.focusable()
|
||||
.focusEffectDisabled()
|
||||
.focused($focused)
|
||||
.onChange(of: overlays.projectFocus, initial: true) {
|
||||
if overlays.projectFocus {
|
||||
overlays.projectFocus = false
|
||||
DispatchQueue.main.async {
|
||||
focused = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
off = store.events.add { ev in
|
||||
if refreshKinds.contains(ev.kind), let projectId = ev.projectId {
|
||||
@@ -238,6 +227,77 @@ private func rowIndent(_ task: ByreTask, _ depth: Int) -> CGFloat {
|
||||
task.parentTaskId != nil ? 20 * CGFloat(depth - 1) : 0
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func listedTasks(_ store: Store, _ overlays: Overlays) -> [ByreTask]? {
|
||||
let active = { (p: Project) in nested(store, (store.tasks[p.id] ?? []).filter { $0.status == .active }).map(\.task) }
|
||||
if overlays.taskList {
|
||||
return store.projects.flatMap(active)
|
||||
}
|
||||
if store.selectedTaskId == nil, overlays.projectTab == .tasks, let p = store.focusedProject() {
|
||||
return active(p)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func cursorTask(_ store: Store, _ overlays: Overlays) -> String? {
|
||||
guard overlays.listKeys, let tasks = listedTasks(store, overlays) else {
|
||||
return nil
|
||||
}
|
||||
return tasks.first { $0.id == overlays.listCursor }?.id ?? tasks.first?.id
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
@MainActor
|
||||
func listKey(_ code: UInt16, _ store: Store, _ overlays: Overlays) -> Bool {
|
||||
if NSApp.keyWindow?.firstResponder is NSTextView || terminalFocused() {
|
||||
overlays.listKeys = false
|
||||
return false
|
||||
}
|
||||
guard let tasks = listedTasks(store, overlays), !tasks.isEmpty else {
|
||||
return false
|
||||
}
|
||||
let i = tasks.firstIndex { $0.id == cursorTask(store, overlays) } ?? 0
|
||||
if code == 125 || code == 126 {
|
||||
overlays.listCursor = tasks[max(0, min(i + (code == 125 ? 1 : -1), tasks.count - 1))].id
|
||||
return true
|
||||
}
|
||||
overlays.listKeys = false
|
||||
store.focusTask(tasks[i].id)
|
||||
return true
|
||||
}
|
||||
#endif
|
||||
|
||||
extension EnvironmentValues {
|
||||
@Entry var taskCursor: String?
|
||||
}
|
||||
|
||||
private extension View {
|
||||
func followCursor() -> some View {
|
||||
modifier(FollowCursor())
|
||||
}
|
||||
}
|
||||
|
||||
private struct FollowCursor: ViewModifier {
|
||||
@Environment(Store.self) private var store
|
||||
@Environment(Overlays.self) private var overlays
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
let cursor = cursorTask(store, overlays)
|
||||
ScrollViewReader { proxy in
|
||||
content
|
||||
.environment(\.taskCursor, cursor)
|
||||
.onChange(of: cursor, initial: true) {
|
||||
if let cursor {
|
||||
DispatchQueue.main.async {
|
||||
proxy.scrollTo(cursor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TaskList: View {
|
||||
@Environment(Store.self) private var store
|
||||
@Environment(Overlays.self) private var overlays
|
||||
@@ -280,6 +340,7 @@ struct TaskList: View {
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
.followCursor()
|
||||
.safeAreaInset(edge: .top, spacing: 0) {
|
||||
HStack {
|
||||
Spacer(minLength: 0)
|
||||
@@ -328,6 +389,7 @@ struct TaskRows: View {
|
||||
|
||||
struct HoverRow<Content: View>: View {
|
||||
var selected = false
|
||||
var cursor = false
|
||||
var indent: CGFloat = 0
|
||||
let action: () -> Void
|
||||
@ViewBuilder let content: Content
|
||||
@@ -343,6 +405,7 @@ struct HoverRow<Content: View>: View {
|
||||
.padding(.vertical, 6)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(selected ? Palette.accentTint : hover ? Palette.surface : .clear, in: RoundedRectangle(cornerRadius: 6))
|
||||
.overlay(RoundedRectangle(cornerRadius: 6).stroke(cursor ? Palette.accent : .clear))
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -354,12 +417,13 @@ private struct TaskRow: View {
|
||||
let task: ByreTask
|
||||
let depth: Int
|
||||
@Environment(Store.self) private var store
|
||||
@Environment(\.taskCursor) private var cursor
|
||||
|
||||
var body: some View {
|
||||
let ts = store.taskState(task)
|
||||
let color = taskPalette[task.color]
|
||||
let count = store.inbox.filter { $0.taskId == task.id }.count
|
||||
HoverRow(selected: task.id == store.selectedTaskId, indent: rowIndent(task, depth)) {
|
||||
HoverRow(selected: task.id == store.selectedTaskId, cursor: task.id == cursor, indent: rowIndent(task, depth)) {
|
||||
store.focusTask(task.id)
|
||||
} content: {
|
||||
SubtaskMark(task: task)
|
||||
@@ -405,10 +469,11 @@ private struct BackgroundRow: View {
|
||||
let task: ByreTask
|
||||
let depth: Int
|
||||
@Environment(Store.self) private var store
|
||||
@Environment(\.taskCursor) private var cursor
|
||||
|
||||
var body: some View {
|
||||
let ts = store.taskState(task)
|
||||
HoverRow(selected: task.id == store.selectedTaskId, indent: rowIndent(task, depth)) {
|
||||
HoverRow(selected: task.id == store.selectedTaskId, cursor: task.id == cursor, indent: rowIndent(task, depth)) {
|
||||
store.focusTask(task.id)
|
||||
} content: {
|
||||
SubtaskMark(task: task)
|
||||
|
||||
Reference in New Issue
Block a user