xplm/
command.rs

1use std::ffi::CString;
2use std::ffi::NulError;
3use std::ops::DerefMut;
4use std::os::raw::{c_int, c_void};
5
6use xplm_sys::*;
7
8/// A command created by X-Plane or another plugin, that can be triggered
9#[derive(Debug)]
10pub struct Command {
11    /// The command reference
12    id: XPLMCommandRef,
13}
14
15impl Command {
16    /// Finds a command
17    ///
18    /// The command should have already been created by X-Plane or another plugin.
19    pub fn find(name: &str) -> Result<Self, CommandFindError> {
20        let name_c = CString::new(name)?;
21        let command_ref = unsafe { XPLMFindCommand(name_c.as_ptr()) };
22        if !command_ref.is_null() {
23            Ok(Command { id: command_ref })
24        } else {
25            Err(CommandFindError::NotFound)
26        }
27    }
28
29    /// Triggers a command once
30    ///
31    /// This is equivalent to pressing a button down and immediately releasing it.
32    pub fn trigger(&mut self) {
33        unsafe {
34            XPLMCommandOnce(self.id);
35        }
36    }
37
38    /// Starts holding down this command
39    ///
40    /// The command will be released when the returned hold object is dropped.
41    pub fn hold_down(&mut self) -> CommandHold {
42        unsafe {
43            XPLMCommandBegin(self.id);
44        }
45        CommandHold { command: self }
46    }
47
48    /// Releases this command
49    fn release(&mut self) {
50        unsafe {
51            XPLMCommandEnd(self.id);
52        }
53    }
54}
55
56/// An RAII lock that keeps a command held down
57///
58/// The command will be released when this object is dropped.
59#[derive(Debug)]
60pub struct CommandHold<'a> {
61    /// The command being held
62    command: &'a mut Command,
63}
64
65impl<'a> Drop for CommandHold<'a> {
66    fn drop(&mut self) {
67        self.command.release();
68    }
69}
70
71/// Errors that can occur when finding a command
72#[derive(thiserror::Error, Debug)]
73pub enum CommandFindError {
74    /// The provided command name contained a null byte
75    #[error("Null byte in command name")]
76    Null(#[from] NulError),
77
78    /// The Command could not be found
79    #[error("Command not found")]
80    NotFound,
81}
82
83/// Trait for things that can handle commands
84pub trait CommandHandler: 'static {
85    /// Called when the command begins (corresponds to a button being pressed down)
86    fn command_begin(&mut self);
87    /// Called frequently while the command button is held down
88    fn command_continue(&mut self);
89    /// Called when the command ends (corresponds to a button being released)
90    fn command_end(&mut self);
91}
92
93/// A command created by this plugin that can be triggered by other components
94pub struct OwnedCommand {
95    /// The heap-allocated data
96    data: Box<OwnedCommandData>,
97    /// The handler callback, used to unregister
98    callback: XPLMCommandCallback_f,
99}
100
101impl OwnedCommand {
102    /// Creates a new command with a provided name and description,
103    /// or finds an existing command
104    ///
105    /// In either case, the command will trigger the provided handler.
106    pub fn new<H: CommandHandler>(
107        name: &str,
108        description: &str,
109        handler: H,
110    ) -> Result<Self, CommandCreateError> {
111        let mut data = Box::new(OwnedCommandData::new(name, description, handler)?);
112        let data_ptr: *mut OwnedCommandData = data.deref_mut();
113        unsafe {
114            XPLMRegisterCommandHandler(
115                data.id,
116                Some(command_handler::<H>),
117                1,
118                data_ptr as *mut c_void,
119            );
120        }
121        Ok(OwnedCommand {
122            data,
123            callback: Some(command_handler::<H>),
124        })
125    }
126}
127
128impl Drop for OwnedCommand {
129    fn drop(&mut self) {
130        let data_ptr: *mut OwnedCommandData = self.data.deref_mut();
131        unsafe {
132            XPLMUnregisterCommandHandler(self.data.id, self.callback, 1, data_ptr as *mut c_void);
133        }
134    }
135}
136
137/// Data for an owned command, used as a refcon
138struct OwnedCommandData {
139    /// The command reference
140    id: XPLMCommandRef,
141    /// The handler
142    handler: Box<dyn CommandHandler>,
143}
144
145impl OwnedCommandData {
146    pub fn new<H: CommandHandler>(
147        name: &str,
148        description: &str,
149        handler: H,
150    ) -> Result<Self, CommandCreateError> {
151        let name_c = CString::new(name)?;
152        let description_c = CString::new(description)?;
153
154        Ok(OwnedCommandData {
155            id: unsafe { XPLMCreateCommand(name_c.as_ptr(), description_c.as_ptr()) },
156            handler: Box::new(handler),
157        })
158    }
159}
160
161/// Command handler callback
162unsafe extern "C" fn command_handler<H: CommandHandler>(
163    _: XPLMCommandRef,
164    phase: XPLMCommandPhase,
165    refcon: *mut c_void,
166) -> c_int {
167    let data = refcon as *mut OwnedCommandData;
168    let handler: *mut dyn CommandHandler = (*data).handler.deref_mut();
169    let handler = handler as *mut H;
170    if phase == xplm_CommandBegin as i32 {
171        (*handler).command_begin();
172    } else if phase == xplm_CommandContinue as i32 {
173        (*handler).command_continue();
174    } else if phase == xplm_CommandEnd as i32 {
175        (*handler).command_end();
176    }
177    // Prevent other components from handling this equivalent
178    0
179}
180
181/// Errors that can occur when creating a Command
182#[derive(thiserror::Error, Debug)]
183pub enum CommandCreateError {
184    /// The provided Command name contained a null byte
185    #[error("Null byte in Command name")]
186    Null(#[from] NulError),
187
188    /// The Command exists already
189    #[deprecated(note = "commands persist between plugin reload - not an error if already exists")]
190    #[error("Command exists already")]
191    Exists,
192}