1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#[macro_use]
extern crate lazy_static;
extern crate sdl2;
pub use sdl2::*;
use event::Event;

use std::collections::{HashMap, LinkedList};
use std::sync::{Arc, Mutex, mpsc};
use std::thread;
use std::thread::sleep;
use std::time::Duration;

type SdlLambda = FnMut(&mut Sdl, &mut HashMap<u32, render::WindowCanvas>) + Send;
type SdlCreateWindow = FnMut(&mut Sdl, &mut VideoSubsystem) -> Option<render::WindowCanvas> + Send;
type SdlHandleEvent = FnMut(&mut Sdl, &mut HashMap<u32, render::WindowCanvas>, &Event) -> bool + Send;

pub enum Sdl2Message {
    Lambda(Box<SdlLambda>),
    CreateWindow(Box<SdlCreateWindow>, mpsc::Sender<Option<u32>>),
    HandleEvent(Box<SdlHandleEvent>, mpsc::Sender<()>),
    Exit
}

use Sdl2Message::*;

fn sdl_handler(rx: mpsc::Receiver<Sdl2Message>) {

    // initialization of the library should be the only possible time we panic.
    let mut sdl_context = sdl2::init().unwrap();
    let mut video = sdl_context.video().unwrap();
    let mut events = sdl_context.event_pump().unwrap();

    let mut windows = HashMap::new();
    let mut unhandled_events = LinkedList::new(); // really, we need to drop old events at some point
    for message in rx {
        match message {
            // Lambda is used for simple, asynchronous blocks of code that need to be run on
            // the UI thread. This does not block the calling thread, so no tx sync is used.
            Lambda(mut lambda) => lambda(&mut sdl_context, &mut windows),

            CreateWindow(mut create_window, tx) => {
                let window_id;
                if let Some(canvas) = create_window(&mut sdl_context, &mut video) {
                    let id = canvas.window().id();
                    windows.insert(id, canvas);
                    window_id = Some(id);
                } else {
                    window_id = None;
                }

                // Send the Window ID back to the requesting thread
                // -----------------------------------------------------------------
                // if send fails, sdl2_mt can panic or print an error or do nothing.
                // panicking in a library is a bad plan.
                // printing errors from a library needs to be configurable.
                //   if printing is configurable, might as well make panicking an option too.
                // for now, sdl2_mt will do nothing.
                let _ = tx.send(window_id);
            },

            HandleEvent(mut handle_event, tx) => {
                // len() should be O(1) according to docs, unlike most linked lists
                let len = unhandled_events.len() as isize;
                let num_events_to_drop = len - 2000;
                for event_num in 0..len {
                    // we're within the length of the list, this unwrap is safe.
                    let event = unhandled_events.pop_front().unwrap();
                    if !handle_event(&mut sdl_context, &mut windows, &event) {
                        // place unhandled events back on the end of the list,
                        // dropping enough of the oldest events to keep at most
                        // 2000 unhandled events, which is enough for several
                        // seconds of collection even during fast user input.
                        //
                        // if no event handler takes responsibility for an event
                        // over the course of several entire seconds, it is then
                        // unlikely to ever be handled by any event handler.
                        if event_num >= num_events_to_drop {
                            unhandled_events.push_back(event);
                        }
                    }
                }

                for event in events.poll_iter() {
                    if !handle_event(&mut sdl_context, &mut windows, &event) {
                        // if the event was unhandled, add it to the list
                        unhandled_events.push_back(event);
                    }
                }
                
                // Synchronize with calling thread to prevent unbounded HandleEvents messages queueing up
                // Same logic as above regarding errors
                let _ = tx.send(()); 
            },

            Exit => break
        }
    }
}

#[derive(Clone)]
pub struct Sdl2Mt(mpsc::Sender<Sdl2Message>);

#[derive(Copy, Clone, Debug)]
pub struct UiThreadExited;

/// map_ute is a 'nop' function that simply converts any type into the UiThreadExited error
#[inline]
fn map_ute<T>(_: T) -> UiThreadExited {
    UiThreadExited
}

impl Sdl2Mt {
    /// A quick, simple way to create a window. Just give it a name, width, and height.
    ///
    /// This function executes synchronously. It will block until the
    /// window_creator function has completed.
    ///
    /// # Panics
    ///
    /// This function will panic if the Window or the Canvas `build()` functions
    /// do not succeed.
    pub fn create_simple_window<IntoString: Into<String>>(&self, name: IntoString, width: u32, height: u32) -> Result<u32, UiThreadExited> {
        let name = name.into();
        self.create_window(Box::new(move |_sdl, video_subsystem| {
            let canvas = video_subsystem.window(&name, width, height)
                .position_centered()
                .resizable()
                .build()
                .unwrap()
                .into_canvas()
                .software()
                .build()
                .unwrap();

            // avoids some potential graphical glitches
            sleep(Duration::from_millis(20));
            
            Some(canvas)
        })).map(|id| id.unwrap())
    }

    /// Executes a window_creator function that accepts &mut VideoSubsystem
    /// and returns an Option<Window>. If Some(window), it will be
    /// added to a HashMap, hashing on the window's ID, which will
    /// then be returned here. If None, None will be returned here.
    ///
    /// This function executes synchronously. It will block until the
    /// window_creator function has completed.
    pub fn create_window(&self, window_creator: Box<SdlCreateWindow>) -> Result<Option<u32>, UiThreadExited> {
        let (tx, rx) = mpsc::channel();
        self.0.send(CreateWindow(window_creator, tx)).map_err(map_ute)?;
        rx.recv().map_err(map_ute)
    }

    //// Executes a lambda function on the UI thread
    //// Either succeeds or the channel is closed and it returns a `SendError`
    ///
    /// This function executes asynchronously. It will *not* block the calling thread.
    pub fn run_on_ui_thread(&self, lambda: Box<SdlLambda>) -> Result<(), UiThreadExited> {
        self.0.send(Lambda(lambda)).map_err(map_ute)
    }

    /// Executes an event_handler function.
    ///
    /// This function executes synchronously. It will block until the
    /// event_handler function has completed.
    pub fn handle_ui_events(&self, event_handler: Box<SdlHandleEvent>) -> Result<(), UiThreadExited> {
        let (tx, rx) = mpsc::channel();
        self.0.send(HandleEvent(event_handler, tx)).map_err(map_ute)?;
        rx.recv().map_err(map_ute)
    }

    /// Terminates the UI thread. Not strictly necessary if the program will exit anyways,
    /// such as when the main program thread returns from main.
    pub fn exit(self) -> Result<(), UiThreadExited> {
        self.0.send(Exit).map_err(map_ute)
    }
}

lazy_static! {
    static ref MT_HANDLE: Arc<Mutex<Sdl2Mt>> = {
        let (tx, rx) = mpsc::channel();
        thread::spawn(move || sdl_handler(rx));
        let handle = Sdl2Mt(tx);
        Arc::new(Mutex::new(handle))
    };
}

/// Initializes an `Sdl2Mt` instance, which also initializes the `Sdl2` library.
///
/// # Panics
///
/// `init()` will panic if `Sdl2` initialization fails. If this is unacceptable, you should
/// `catch_panic()` around your `init()` call. Initialization should never fail under
/// anything approaching reasonable circumstances.
pub fn init() -> Sdl2Mt {
    let handle = (*MT_HANDLE).clone();
    let locked = handle.lock().unwrap();
    let new_handle = locked.clone();
    new_handle
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::thread::sleep;
    use std::time::Duration;

    #[test]
    fn double_init() {
        let a = init();
        let b = init();
        sleep(Duration::from_millis(250));
        a.run_on_ui_thread(Box::new(|_, _| {})).unwrap();
        sleep(Duration::from_millis(250));
        b.run_on_ui_thread(Box::new(|_, _| {})).unwrap();
        sleep(Duration::from_millis(250));
    }
}