Skip to content

Commit

Permalink
Get absolute mouse position in InputView (coordinate systems are anno…
Browse files Browse the repository at this point in the history
…ying) and make it available to DeltaCore via InputState
  • Loading branch information
stackotter committed Dec 20, 2023
1 parent 9c5a837 commit ec79c1d
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 16 deletions.
15 changes: 15 additions & 0 deletions Sources/Client/Extensions/View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,19 @@ extension View {
action(argument1, argument2, argument3)
}
}

/// Appends an action to an action stored property. Useful for implementing custom
/// view modifiers such as `onClick` etc. Allows the modifier to be called multiple
/// times without overwriting previous actions. If the stored action is nil, the
/// given action becomes the stored action, otherwise the new action is appended to
/// the existing action.
func appendingAction<T, U, V, W>(
to keyPath: WritableKeyPath<Self, ((T, U, V, W) -> Void)?>,
_ action: @escaping (T, U, V, W) -> Void
) -> Self {
with(keyPath) { argument1, argument2, argument3, argument4 in
self[keyPath: keyPath]?(argument1, argument2, argument3, argument4)
action(argument1, argument2, argument3, argument4)
}
}
}
4 changes: 2 additions & 2 deletions Sources/Client/Views/Play/GameView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ struct GameView: View {
.onKeyRelease { [weak client] key in
client?.release(key)
}
.onMouseMove { [weak client] deltaX, deltaY in
.onMouseMove { [weak client] x, y, deltaX, deltaY in
// TODO: Formalise this adjustment factor somewhere
let sensitivityAdjustmentFactor: Float = 0.004
let sensitivity = sensitivityAdjustmentFactor * managedConfig.mouseSensitivity
client?.moveMouse(sensitivity * deltaX, sensitivity * deltaY)
client?.moveMouse(x: x, y: y, deltaX: sensitivity * deltaX, deltaY: sensitivity * deltaY)
}
.passthroughClicks(!cursorCaptured)
}
Expand Down
57 changes: 53 additions & 4 deletions Sources/Client/Views/Play/InputView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import SwiftUI
import DeltaCore

struct InputView<Content: View>: View {
@EnvironmentObject var modal: Modal
@EnvironmentObject var appState: StateWrapper<AppState>

@State var monitorsAdded = false
@State var scrollWheelDeltaY: Float = 0

Expand All @@ -15,7 +18,12 @@ struct InputView<Content: View>: View {

private var handleKeyRelease: ((Key) -> Void)?
private var handleKeyPress: ((Key, [Character]) -> Void)?
private var handleMouseMove: ((_ deltaX: Float, _ deltaY: Float) -> Void)?
private var handleMouseMove: ((
_ x: Float,
_ y: Float,
_ deltaX: Float,
_ deltaY: Float
) -> Void)?
private var handleScroll: ((_ deltaY: Float) -> Void)?
private var shouldPassthroughClicks = false

Expand Down Expand Up @@ -67,7 +75,7 @@ struct InputView<Content: View>: View {
}

/// Adds an action to run when the mouse is moved.
func onMouseMove(_ action: @escaping (_ deltaX: Float, _ deltaY: Float) -> Void) -> Self {
func onMouseMove(_ action: @escaping (_ x: Float, _ y: Float, _ deltaX: Float, _ deltaY: Float) -> Void) -> Self {
appendingAction(to: \.handleMouseMove, action)
}

Expand All @@ -76,8 +84,39 @@ struct InputView<Content: View>: View {
appendingAction(to: \.handleScroll, action)
}

func mousePositionInView(with geometry: GeometryProxy) -> (x: Float, y: Float)? {
// This assumes that there's only one window and that this is only called once
// the view's body has been evaluated at least once.
guard let window = NSApplication.shared.orderedWindows.first else {
return nil
}

let viewFrame = geometry.frame(in: .global)
let x = (NSEvent.mouseLocation.x - window.frame.minX) - viewFrame.minX
let y = window.frame.maxY - NSEvent.mouseLocation.y - viewFrame.minY
return (Float(x), Float(y))
}

var body: some View {
content()
GeometryReader { geometry in
contentWithEventListeners(geometry)
}
}

func contentWithEventListeners(_ geometry: GeometryProxy) -> some View {
// Make sure that the latest position is known to any observers (e.g. if
// listening was disabled and now isn't, observers won't have been told
// about any changes that occured during the period in which listening
// was disabled).
if let mousePosition = mousePositionInView(with: geometry) {
handleMouseMove?(mousePosition.x, mousePosition.y, 0, 0)
} else {
modal.error("Failed to get mouse position (on demand)") {
appState.update(to: .serverList)
}
}

return content()
.frame(maxWidth: .infinity, maxHeight: .infinity)
#if os(iOS)
.gesture(TapGesture(count: 2).onEnded { _ in
Expand All @@ -104,10 +143,20 @@ struct InputView<Content: View>: View {
return event
}

guard let mousePosition = mousePositionInView(with: geometry) else {
modal.error("Failed to get mouse position") {
appState.update(to: .serverList)
}
return event
}

let x = mousePosition.x
let y = mousePosition.y

let deltaX = Float(event.deltaX)
let deltaY = Float(event.deltaY)

handleMouseMove?(deltaX, deltaY)
handleMouseMove?(x, y, deltaX, deltaY)

return event
})
Expand Down
16 changes: 14 additions & 2 deletions Sources/Core/Sources/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,23 @@ public final class Client: @unchecked Sendable {
}

/// Moves the mouse.
///
/// `deltaX` and `deltaY` aren't just the difference between the current and
/// previous values of `x` and `y` because there are ways for the mouse to
/// appear at a new position without causing in-game movement (e.g. if the
/// user opens the in-game menu, moves the mouse, and then closes the in-game
/// menu).
/// - Parameters:
/// - x: The absolute mouse x (relative to the play area's top left corner).
/// - y: The absolute mouse y (relative to the play area's top left corner).
/// - deltaX: The change in mouse x.
/// - deltaY: The change in mouse y.
public func moveMouse(_ deltaX: Float, _ deltaY: Float) {
game.moveMouse(deltaX, deltaY)
public func moveMouse(x: Float, y: Float, deltaX: Float, deltaY: Float) {
// TODO: Update this API (and everything else reliant on it) so that DeltaCore
// is the one that decides which input events to ignore (instead of InputView
// (and similar) deciding whether an event should be given to Client or not).
// This will allow the deltaX and deltaY parameters to be removed.
game.moveMouse(x: x, y: y, deltaX: deltaX, deltaY: deltaY)
}

/// Moves the left thumbstick.
Expand Down
22 changes: 16 additions & 6 deletions Sources/Core/Sources/ECS/Singles/InputState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@ import FirebladeMath

/// The game's input state.
public final class InputState: SingleComponent {
/// The maximum number of ticks between consecutive inputs to count as a double tap.
/// The maximum number of ticks between consecutive inputs to count as a
/// double tap.
public static let maximumDoubleTapDelay = 6

/// The newly pressed keys in the order that they were pressed. Only includes presses since last
/// call to ``flushInputs()``.
/// The newly pressed keys in the order that they were pressed. Only includes
/// presses since last call to ``flushInputs()``.
public private(set) var newlyPressed: [KeyPressEvent] = []
/// The newly released keys in the order that they were released. Only includes releases since
/// last call to ``flushInputs()``.
/// The newly released keys in the order that they were released. Only includes
/// releases since last call to ``flushInputs()``.
public private(set) var newlyReleased: [KeyReleaseEvent] = []

/// The currently pressed keys.
public private(set) var keys: Set<Key> = []
/// The currently pressed inputs.
public private(set) var inputs: Set<Input> = []

/// The current absolute mouse position relative to the play area's top left corner.
public private(set) var mousePosition: Vec2f = Vec2f(0, 0)
/// The mouse delta since the last call to ``resetMouseDelta()``.
public private(set) var mouseDelta: Vec2f = Vec2f(0, 0)
/// The position of the left thumbstick.
Expand Down Expand Up @@ -78,11 +81,18 @@ public final class InputState: SingleComponent {
}

/// Updates the current mouse delta by adding the given delta.
///
/// See ``Client/moveMouse(x:y:deltaX:deltaY:)`` for the reasoning behind
/// having both absolute and relative parameters (it's currently necessary
/// but could be fixed by cleaning up the input handling architecture).
/// - Parameters:
/// - x: The absolute mouse x (relative to the play area's top left corner).
/// - y: The absolute mouse y (relative to the play area's top left corner).
/// - deltaX: The change in mouse x.
/// - deltaY: The change in mouse y.
public func moveMouse(_ deltaX: Float, _ deltaY: Float) {
public func moveMouse(x: Float, y: Float, deltaX: Float, deltaY: Float) {
mouseDelta += Vec2f(deltaX, deltaY)
mousePosition = Vec2f(x, y)
}

/// Updates the current position of the left thumbstick.
Expand Down
10 changes: 8 additions & 2 deletions Sources/Core/Sources/Game.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,19 @@ public final class Game: @unchecked Sendable {
}

/// Moves the mouse.
///
/// See ``Client/moveMouse(x:y:deltaX:deltaY:)`` for the reasoning behind
/// having both absolute and relative parameters (it's currently necessary
/// but could be fixed by cleaning up the input handling architecture).
/// - Parameters:
/// - x: The absolute mouse x (relative to the play area's top left corner).
/// - y: The absolute mouse y (relative to the play area's top left corner).
/// - deltaX: The change in mouse x.
/// - deltaY: The change in mouse y.
public func moveMouse(_ deltaX: Float, _ deltaY: Float) {
public func moveMouse(x: Float, y: Float, deltaX: Float, deltaY: Float) {
nexusLock.acquireWriteLock()
defer { nexusLock.unlock() }
inputState.moveMouse(deltaX, deltaY)
inputState.moveMouse(x: x, y: y, deltaX: deltaX, deltaY: deltaY)
}

/// Moves the left thumbstick.
Expand Down

0 comments on commit ec79c1d

Please sign in to comment.