use std::collections::BTreeMap;
use std::ptr::NonNull;
use std::sync::mpsc::{self, Receiver, Sender};
use crate::{Axis, Button, Event, EventType, GamepadId, Update};
use block2::RcBlock;
use objc2::{MainThreadMarker, rc::Retained, runtime::Bool};
use objc2_game_controller::{
GCController, GCControllerAxisInput, GCControllerButtonInput, GCDevice, GCExtendedGamepad,
};
struct Pad {
controller: Retained<GCController>,
buttons: Vec<(Button, Retained<GCControllerButtonInput>)>,
axes: Vec<(Axis, Retained<GCControllerAxisInput>)>,
}
impl Pad {
fn new(
controller: Retained<GCController>,
profile: &GCExtendedGamepad,
id: GamepadId,
sender: &Sender<Update>,
) -> Self {
let (buttons, axes) = unsafe {
let dpad = profile.dpad();
let mut buttons = vec![
(Button::South, profile.buttonA()),
(Button::East, profile.buttonB()),
(Button::West, profile.buttonX()),
(Button::North, profile.buttonY()),
(Button::DPadUp, dpad.up()),
(Button::DPadDown, dpad.down()),
(Button::DPadLeft, dpad.left()),
(Button::DPadRight, dpad.right()),
(Button::LeftTrigger, profile.leftShoulder()),
(Button::RightTrigger, profile.rightShoulder()),
(Button::LeftTrigger2, profile.leftTrigger()),
(Button::RightTrigger2, profile.rightTrigger()),
(Button::Start, profile.buttonMenu()),
];
for (button, input) in [
(Button::Select, profile.buttonOptions()),
(Button::Mode, profile.buttonHome()),
(Button::LeftThumb, profile.leftThumbstickButton()),
(Button::RightThumb, profile.rightThumbstickButton()),
] {
if let Some(input) = input {
buttons.push((button, input));
}
}
let left = profile.leftThumbstick();
let right = profile.rightThumbstick();
(
buttons,
vec![
(Axis::LeftStickX, left.xAxis()),
(Axis::LeftStickY, left.yAxis()),
(Axis::RightStickX, right.xAxis()),
(Axis::RightStickY, right.yAxis()),
],
)
};
for (button, input) in &buttons {
let button = *button;
let events = sender.clone();
let handler = RcBlock::new(
move |_: NonNull<GCControllerButtonInput>, _: f32, pressed: Bool| {
let event = if pressed.as_bool() {
EventType::ButtonPressed(button)
} else {
EventType::ButtonReleased(button)
};
let _ = events.send(Update::Input(Event { id, event }));
},
);
unsafe {
input.setPressedChangedHandler(RcBlock::as_ptr(&handler));
}
if unsafe { input.isPressed() } {
let _ = sender.send(Update::Input(Event {
id,
event: EventType::ButtonPressed(button),
}));
}
}
for (axis, input) in &axes {
let axis = *axis;
let events = sender.clone();
let handler = RcBlock::new(move |_: NonNull<GCControllerAxisInput>, value: f32| {
let _ = events.send(Update::Input(Event {
id,
event: EventType::AxisChanged(axis, value),
}));
});
unsafe {
input.setValueChangedHandler(RcBlock::as_ptr(&handler));
let _ = sender.send(Update::Input(Event {
id,
event: EventType::AxisChanged(axis, input.value()),
}));
}
}
Self {
controller,
buttons,
axes,
}
}
}
impl Drop for Pad {
fn drop(&mut self) {
unsafe {
for (_, input) in &self.buttons {
input.setPressedChangedHandler(std::ptr::null_mut());
}
for (_, input) in &self.axes {
input.setValueChangedHandler(std::ptr::null_mut());
}
}
}
}
pub(crate) struct Backend {
_main_thread: MainThreadMarker,
pads: BTreeMap<GamepadId, Pad>,
next_id: usize,
previous_background_events: bool,
sender: Sender<Update>,
receiver: Receiver<Update>,
}
impl Backend {
pub(crate) fn new() -> Result<Self, String> {
let main_thread =
MainThreadMarker::new().ok_or("Create gamepad input on the window's main thread")?;
let previous_background_events = unsafe {
let previous = GCController::shouldMonitorBackgroundEvents();
GCController::setShouldMonitorBackgroundEvents(true);
previous
};
let (sender, receiver) = mpsc::channel();
Ok(Self {
_main_thread: main_thread,
pads: BTreeMap::new(),
next_id: 0,
previous_background_events,
sender,
receiver,
})
}
pub(crate) fn poll(&mut self) -> Vec<Update> {
let array = unsafe { GCController::controllers() };
let controllers: Vec<_> = (0..array.count()).map(|i| array.objectAtIndex(i)).collect();
let mut updates = Vec::new();
self.pads.retain(|id, pad| {
if controllers
.iter()
.any(|c| std::ptr::eq(&**c, &*pad.controller))
{
true
} else {
updates.push(Update::Input(Event {
id: *id,
event: EventType::Disconnected,
}));
false
}
});
for controller in controllers {
if self
.pads
.values()
.any(|pad| std::ptr::eq(&*controller, &*pad.controller))
{
continue;
}
let Some(profile) = (unsafe { controller.extendedGamepad() }) else {
continue;
};
let name = unsafe { controller.vendorName() }
.map_or_else(|| "Controller".into(), |s| s.to_string());
let id = GamepadId(self.next_id);
self.next_id += 1;
updates.push(Update::Connected(id, name));
self.pads
.insert(id, Pad::new(controller, &profile, id, &self.sender));
}
updates.extend(self.receiver.try_iter());
updates
}
}
impl Drop for Backend {
fn drop(&mut self) {
self.pads.clear();
unsafe {
GCController::setShouldMonitorBackgroundEvents(self.previous_background_events);
}
}
}