sdl2_window/
lib.rs

1#![deny(missing_docs)]
2//! A SDL2 window back-end for the Piston game engine.
3
4extern crate gl;
5extern crate input;
6extern crate sdl2;
7extern crate shader_version;
8extern crate window;
9
10// External crates.
11use input::HatState as PistonHat;
12use input::{
13    keyboard, Button, ButtonArgs, ButtonState, CloseArgs, ControllerAxisArgs, ControllerButton,
14    ControllerHat, Event, Input, Motion, MouseButton, ResizeArgs, TimeStamp, Touch, TouchArgs,
15};
16use sdl2::joystick::HatState;
17use window::{
18    AdvancedWindow, Api, BuildFromWindowSettings, OpenGLWindow, Position, ProcAddress, Size,
19    UnsupportedGraphicsApiError, Window, WindowSettings,
20};
21
22use std::error::Error;
23use std::time::Duration;
24use std::vec::Vec;
25
26pub use shader_version::OpenGL;
27
28struct JoystickState {
29    joysticks: Vec<sdl2::joystick::Joystick>,
30    subsystem: sdl2::JoystickSubsystem,
31}
32
33impl JoystickState {
34    fn new(subsystem: sdl2::JoystickSubsystem) -> Self {
35        JoystickState {
36            joysticks: Vec::new(),
37            subsystem,
38        }
39    }
40}
41
42/// A window implemented by SDL2 back-end.
43pub struct Sdl2Window {
44    /// SDL window handle.
45    pub window: sdl2::video::Window,
46    /// Allow dead code because this keeps track of the OpenGL context.
47    /// Will be released on drop.
48    #[allow(dead_code)]
49    pub context: sdl2::video::GLContext,
50    /// SDL context.
51    pub sdl_context: sdl2::Sdl,
52    /// Video subsystem.
53    pub video_subsystem: sdl2::VideoSubsystem,
54    joystick_state: Option<JoystickState>,
55    should_close: bool,
56    automatic_close: bool,
57    // Stores relative coordinates to emit on next poll.
58    mouse_relative: Option<(f64, f64, TimeStamp)>,
59    // Whether the cursor is captured.
60    is_capturing_cursor: bool,
61    // Used to ignore relative events when warping mouse
62    // to center of window.
63    ignore_relative_event: Option<(i32, i32)>,
64    exit_on_esc: bool,
65    title: String,
66}
67
68impl Sdl2Window {
69    /// Creates a new game window for SDL2. This will initialize SDL and the video subsystem.
70    /// You can retrieve both via the public fields on the `Sdl2Window` struct.
71    pub fn new(settings: &WindowSettings) -> Result<Self, Box<dyn Error>> {
72        let sdl = sdl2::init()?;
73        let video_subsystem = sdl.video()?;
74        Self::with_subsystem(video_subsystem, settings)
75    }
76
77    /// Creates a window with the supplied SDL Video subsystem.
78    pub fn with_subsystem(
79        video_subsystem: sdl2::VideoSubsystem,
80        settings: &WindowSettings,
81    ) -> Result<Self, Box<dyn Error>> {
82        use sdl2::video::GLProfile;
83
84        let sdl_context = video_subsystem.sdl();
85        let api = settings
86            .get_maybe_graphics_api()
87            .unwrap_or(Api::opengl(3, 2));
88        if api.api != "OpenGL" {
89            return Err(UnsupportedGraphicsApiError {
90                found: api.api,
91                expected: vec!["OpenGL".into()],
92            }
93            .into());
94        }
95
96        {
97            let gl_attr = video_subsystem.gl_attr();
98
99            // Not all drivers default to 32bit color, so explicitly set it to 32bit color.
100            gl_attr.set_red_size(8);
101            gl_attr.set_green_size(8);
102            gl_attr.set_blue_size(8);
103            gl_attr.set_alpha_size(8);
104            gl_attr.set_stencil_size(8);
105            gl_attr.set_context_version(api.major as u8, api.minor as u8);
106            gl_attr.set_framebuffer_srgb_compatible(settings.get_srgb());
107        }
108
109        if api >= Api::opengl(3, 2) {
110            video_subsystem
111                .gl_attr()
112                .set_context_profile(GLProfile::Core);
113        }
114        if settings.get_samples() != 0 {
115            let gl_attr = video_subsystem.gl_attr();
116            gl_attr.set_multisample_buffers(1);
117            gl_attr.set_multisample_samples(settings.get_samples());
118        }
119
120        let mut window_builder = video_subsystem.window(
121            &settings.get_title(),
122            settings.get_size().width as u32,
123            settings.get_size().height as u32,
124        );
125
126        let window_builder = window_builder.position_centered().opengl();
127
128        let window_builder = if settings.get_resizable() {
129            window_builder.resizable()
130        } else {
131            window_builder
132        };
133
134        let window_builder = if settings.get_decorated() {
135            window_builder
136        } else {
137            window_builder.borderless()
138        };
139
140        let window_builder = if settings.get_fullscreen() {
141            window_builder.fullscreen()
142        } else {
143            window_builder
144        };
145
146        let window = window_builder.build();
147
148        let window = match window {
149            Ok(w) => w,
150            Err(_) => {
151                if settings.get_samples() != 0 {
152                    // Retry without requiring anti-aliasing.
153                    let gl_attr = video_subsystem.gl_attr();
154                    gl_attr.set_multisample_buffers(0);
155                    gl_attr.set_multisample_samples(0);
156                    window_builder.build().map_err(|e| format!("{}", e))?
157                } else {
158                    window.map_err(|e| format!("{}", e))?
159                }
160            }
161        };
162
163        // Send text input events.
164        video_subsystem.text_input().start();
165
166        let context = window.gl_create_context()?;
167
168        // Load the OpenGL function pointers.
169        gl::load_with(|name| video_subsystem.gl_get_proc_address(name) as *const _);
170
171        if settings.get_vsync() {
172            video_subsystem.gl_set_swap_interval(1)?;
173        } else {
174            video_subsystem.gl_set_swap_interval(0)?;
175        }
176
177        let mut window = Sdl2Window {
178            exit_on_esc: settings.get_exit_on_esc(),
179            should_close: false,
180            automatic_close: settings.get_automatic_close(),
181            is_capturing_cursor: false,
182            ignore_relative_event: None,
183            window,
184            context,
185            sdl_context,
186            video_subsystem,
187            joystick_state: None,
188            mouse_relative: None,
189            title: settings.get_title(),
190        };
191        if settings.get_controllers() {
192            window.init_joysticks()?;
193        }
194        if settings.get_transparent() {
195            let _ = window.window.set_opacity(0.0);
196        }
197        Ok(window)
198    }
199
200    /// Initialize the joystick subsystem. Required before joystick input
201    /// events will be returned. Returns the number available or error.
202    pub fn init_joysticks(&mut self) -> Result<u32, String> {
203        let subsystem = self.sdl_context.joystick()?;
204        let mut state = JoystickState::new(subsystem);
205        let available = state.subsystem.num_joysticks()?;
206
207        // Open all the joysticks
208        for id in 0..available {
209            match state.subsystem.open(id) {
210                Ok(c) => state.joysticks.push(c),
211                Err(e) => return Err(format!("{}", e)),
212            }
213        }
214
215        self.joystick_state = Some(state);
216
217        Ok(available)
218    }
219
220    fn wait_event(&mut self) -> Event {
221        loop {
222            if let Some(event) = self.check_pending_event() {
223                return event;
224            };
225            let sdl_event = self.sdl_context.event_pump().unwrap().wait_event();
226            let mut unknown = false;
227            if let Some(event) = self.handle_event(Some(sdl_event), &mut unknown) {
228                return event;
229            }
230        }
231    }
232
233    fn wait_event_timeout(&mut self, timeout: Duration) -> Option<Event> {
234        let event = self.check_pending_event();
235        if event.is_some() {
236            return event;
237        };
238
239        let timeout_ms = timeout.as_millis() as u32;
240        let sdl_event = self
241            .sdl_context
242            .event_pump()
243            .unwrap()
244            .wait_event_timeout(timeout_ms);
245
246        let mut unknown = false;
247        let event = self.handle_event(sdl_event, &mut unknown);
248        if unknown {
249            self.poll_event()
250        } else {
251            event
252        }
253    }
254
255    fn poll_event(&mut self) -> Option<Event> {
256        // Loop for ignoring unknown events.
257        loop {
258            let event = self.check_pending_event();
259            if event.is_some() {
260                return event;
261            };
262
263            // Even though we create a new EventPump each time we poll an event
264            // this should not be a problem since it only contains phantom data
265            // and therefore should actually not have any overhead.
266            let sdl_event = self.sdl_context.event_pump().unwrap().poll_event();
267            let mut unknown = false;
268            let event = self.handle_event(sdl_event, &mut unknown);
269            if unknown {
270                continue;
271            };
272            return event;
273        }
274    }
275
276    fn check_pending_event(&mut self) -> Option<Event> {
277        // First check for a pending relative mouse move event.
278        if let Some((x, y, timestamp)) = self.mouse_relative {
279            self.mouse_relative = None;
280            return Some(input::Event::Input(
281                Input::Move(Motion::MouseRelative([x, y])),
282                Some(timestamp),
283            ));
284        }
285        None
286    }
287
288    fn handle_event(
289        &mut self,
290        sdl_event: Option<sdl2::event::Event>,
291        unknown: &mut bool,
292    ) -> Option<Event> {
293        use sdl2::event::{Event, WindowEvent};
294        let event = match sdl_event {
295            Some(ev) => {
296                if let Event::MouseMotion { xrel, yrel, .. } = ev {
297                    // Ignore a specific mouse motion event caused by
298                    // change of coordinates when warping the cursor
299                    // to the center.
300                    if Some((xrel, yrel)) == self.ignore_relative_event {
301                        self.ignore_relative_event = None;
302                        return None;
303                    }
304                }
305                ev
306            }
307            None => {
308                // Wait until event queue is empty to reduce
309                // risk of error in order.
310                if self.is_capturing_cursor {
311                    self.fake_capture();
312                }
313                return None;
314            }
315        };
316        match event {
317            Event::Quit { timestamp, .. } => {
318                if self.automatic_close {
319                    self.should_close = true;
320                }
321                return Some(input::Event::Input(
322                    Input::Close(CloseArgs),
323                    Some(timestamp),
324                ));
325            }
326            Event::TextInput {
327                text, timestamp, ..
328            } => {
329                return Some(input::Event::Input(Input::Text(text), Some(timestamp)));
330            }
331            Event::KeyDown {
332                keycode: Some(key),
333                repeat,
334                scancode,
335                timestamp,
336                ..
337            } => {
338                // SDL2 repeats the key down event.
339                // If the event is the same as last one, ignore it.
340                if repeat {
341                    return self.poll_event();
342                }
343
344                if self.exit_on_esc && key == sdl2::keyboard::Keycode::Escape {
345                    self.should_close = true;
346                } else {
347                    return Some(input::Event::Input(
348                        Input::Button(ButtonArgs {
349                            state: ButtonState::Press,
350                            button: Button::Keyboard(sdl2_map_key(key)),
351                            scancode: scancode.map(|scode| scode as i32),
352                        }),
353                        Some(timestamp),
354                    ));
355                }
356            }
357            Event::KeyUp {
358                keycode: Some(key),
359                repeat,
360                scancode,
361                timestamp,
362                ..
363            } => {
364                if repeat {
365                    return self.poll_event();
366                }
367                return Some(input::Event::Input(
368                    Input::Button(ButtonArgs {
369                        state: ButtonState::Release,
370                        button: Button::Keyboard(sdl2_map_key(key)),
371                        scancode: scancode.map(|scode| scode as i32),
372                    }),
373                    Some(timestamp),
374                ));
375            }
376            Event::MouseButtonDown {
377                mouse_btn: button,
378                timestamp,
379                ..
380            } => {
381                return Some(input::Event::Input(
382                    Input::Button(ButtonArgs {
383                        state: ButtonState::Press,
384                        button: Button::Mouse(sdl2_map_mouse(button)),
385                        scancode: None,
386                    }),
387                    Some(timestamp),
388                ));
389            }
390            Event::MouseButtonUp {
391                mouse_btn: button,
392                timestamp,
393                ..
394            } => {
395                return Some(input::Event::Input(
396                    Input::Button(ButtonArgs {
397                        state: ButtonState::Release,
398                        button: Button::Mouse(sdl2_map_mouse(button)),
399                        scancode: None,
400                    }),
401                    Some(timestamp),
402                ));
403            }
404            Event::MouseMotion {
405                x,
406                y,
407                xrel: dx,
408                yrel: dy,
409                timestamp,
410                ..
411            } => {
412                if self.is_capturing_cursor {
413                    // Skip normal mouse movement and emit relative motion only.
414                    return Some(input::Event::Input(
415                        Input::Move(Motion::MouseRelative([dx as f64, dy as f64])),
416                        Some(timestamp),
417                    ));
418                }
419                // Send relative move movement next time.
420                self.mouse_relative = Some((dx as f64, dy as f64, timestamp));
421                return Some(input::Event::Input(
422                    Input::Move(Motion::MouseCursor([x as f64, y as f64])),
423                    Some(timestamp),
424                ));
425            }
426            Event::MouseWheel {
427                x, y, timestamp, ..
428            } => {
429                return Some(input::Event::Input(
430                    Input::Move(Motion::MouseScroll([x as f64, y as f64])),
431                    Some(timestamp),
432                ));
433            }
434            Event::JoyAxisMotion {
435                which,
436                axis_idx,
437                value: val,
438                timestamp,
439                ..
440            } => {
441                // Axis motion is an absolute value in the range
442                // [-32768, 32767]. Normalize it down to a float.
443                let normalized_value = val as f64 / (i16::MAX) as f64;
444                return Some(input::Event::Input(
445                    Input::Move(Motion::ControllerAxis(ControllerAxisArgs::new(
446                        which,
447                        axis_idx,
448                        normalized_value,
449                    ))),
450                    Some(timestamp),
451                ));
452            }
453            Event::JoyButtonDown {
454                which,
455                button_idx,
456                timestamp,
457                ..
458            } => {
459                return Some(input::Event::Input(
460                    Input::Button(ButtonArgs {
461                        state: ButtonState::Press,
462                        button: Button::Controller(ControllerButton::new(which, button_idx)),
463                        scancode: None,
464                    }),
465                    Some(timestamp),
466                ))
467            }
468            Event::JoyButtonUp {
469                which,
470                button_idx,
471                timestamp,
472                ..
473            } => {
474                return Some(input::Event::Input(
475                    Input::Button(ButtonArgs {
476                        state: ButtonState::Release,
477                        button: Button::Controller(ControllerButton::new(which, button_idx)),
478                        scancode: None,
479                    }),
480                    Some(timestamp),
481                ))
482            }
483            Event::JoyHatMotion {
484                which,
485                hat_idx,
486                state,
487                timestamp,
488                ..
489            } => {
490                let state = match state {
491                    HatState::Centered => PistonHat::Centered,
492                    HatState::Up => PistonHat::Up,
493                    HatState::Right => PistonHat::Right,
494                    HatState::Down => PistonHat::Down,
495                    HatState::Left => PistonHat::Left,
496                    HatState::RightUp => PistonHat::RightUp,
497                    HatState::RightDown => PistonHat::RightDown,
498                    HatState::LeftUp => PistonHat::LeftUp,
499                    HatState::LeftDown => PistonHat::LeftDown,
500                };
501                return Some(input::Event::Input(
502                    Input::Button(ButtonArgs {
503                        state: ButtonState::Release,
504                        button: Button::Hat(ControllerHat::new(which, hat_idx, state)),
505                        scancode: None,
506                    }),
507                    Some(timestamp),
508                ));
509            }
510            Event::FingerDown {
511                touch_id,
512                finger_id,
513                x,
514                y,
515                pressure,
516                timestamp,
517                ..
518            } => {
519                return Some(input::Event::Input(
520                    Input::Move(Motion::Touch(TouchArgs::new(
521                        touch_id,
522                        finger_id,
523                        [x as f64, y as f64],
524                        pressure as f64,
525                        Touch::Start,
526                    ))),
527                    Some(timestamp),
528                ))
529            }
530            Event::FingerMotion {
531                touch_id,
532                finger_id,
533                x,
534                y,
535                pressure,
536                timestamp,
537                ..
538            } => {
539                return Some(input::Event::Input(
540                    Input::Move(Motion::Touch(TouchArgs::new(
541                        touch_id,
542                        finger_id,
543                        [x as f64, y as f64],
544                        pressure as f64,
545                        Touch::Move,
546                    ))),
547                    Some(timestamp),
548                ))
549            }
550            Event::FingerUp {
551                touch_id,
552                finger_id,
553                x,
554                y,
555                pressure,
556                timestamp,
557                ..
558            } => {
559                return Some(input::Event::Input(
560                    Input::Move(Motion::Touch(TouchArgs::new(
561                        touch_id,
562                        finger_id,
563                        [x as f64, y as f64],
564                        pressure as f64,
565                        Touch::End,
566                    ))),
567                    Some(timestamp),
568                ))
569            }
570            Event::Window {
571                win_event: sdl2::event::WindowEvent::Resized(w, h),
572                timestamp,
573                ..
574            } => {
575                let draw_size = self.draw_size();
576                return Some(input::Event::Input(
577                    Input::Resize(ResizeArgs {
578                        window_size: [w as f64, h as f64],
579                        draw_size: draw_size.into(),
580                    }),
581                    Some(timestamp),
582                ));
583            }
584            Event::Window {
585                win_event: WindowEvent::FocusGained,
586                timestamp,
587                ..
588            } => {
589                return Some(input::Event::Input(Input::Focus(true), Some(timestamp)));
590            }
591            Event::Window {
592                win_event: WindowEvent::FocusLost,
593                timestamp,
594                ..
595            } => {
596                return Some(input::Event::Input(Input::Focus(false), Some(timestamp)));
597            }
598            Event::Window {
599                win_event: WindowEvent::Enter,
600                timestamp,
601                ..
602            } => {
603                return Some(input::Event::Input(Input::Cursor(true), Some(timestamp)));
604            }
605            Event::Window {
606                win_event: WindowEvent::Leave,
607                timestamp,
608                ..
609            } => {
610                return Some(input::Event::Input(Input::Cursor(false), Some(timestamp)));
611            }
612            _ => {
613                *unknown = true;
614                return None;
615            }
616        }
617        None
618    }
619
620    fn fake_capture(&mut self) {
621        // Fake capturing of cursor.
622        let (w, h) = self.window.size();
623        let cx = (w / 2) as i32;
624        let cy = (h / 2) as i32;
625        let s = self.sdl_context.event_pump().unwrap().mouse_state();
626        let dx = cx - s.x();
627        let dy = cy - s.y();
628        if dx != 0 || dy != 0 {
629            self.ignore_relative_event = Some((dx, dy));
630            self.sdl_context
631                .mouse()
632                .warp_mouse_in_window(&self.window, cx, cy);
633        }
634    }
635}
636
637impl BuildFromWindowSettings for Sdl2Window {
638    fn build_from_window_settings(settings: &WindowSettings) -> Result<Self, Box<dyn Error>> {
639        Sdl2Window::new(settings)
640    }
641}
642
643impl Drop for Sdl2Window {
644    fn drop(&mut self) {
645        self.set_capture_cursor(false);
646    }
647}
648
649impl Window for Sdl2Window {
650    fn should_close(&self) -> bool {
651        self.should_close
652    }
653    fn set_should_close(&mut self, value: bool) {
654        self.should_close = value;
655    }
656    fn swap_buffers(&mut self) {
657        self.window.gl_swap_window();
658    }
659    fn size(&self) -> Size {
660        let (w, h) = self.window.size();
661        Size {
662            width: w as f64,
663            height: h as f64,
664        }
665    }
666    fn wait_event(&mut self) -> Event {
667        self.wait_event()
668    }
669    fn wait_event_timeout(&mut self, timeout: Duration) -> Option<Event> {
670        self.wait_event_timeout(timeout)
671    }
672    fn poll_event(&mut self) -> Option<Event> {
673        self.poll_event()
674    }
675    fn draw_size(&self) -> Size {
676        let (w, h) = self.window.drawable_size();
677        Size {
678            width: w as f64,
679            height: h as f64,
680        }
681    }
682}
683
684impl AdvancedWindow for Sdl2Window {
685    fn get_title(&self) -> String {
686        self.title.clone()
687    }
688    fn set_title(&mut self, value: String) {
689        let _ = self.window.set_title(&value);
690        self.title = value
691    }
692    fn get_automatic_close(&self) -> bool {
693        self.automatic_close
694    }
695    fn set_automatic_close(&mut self, value: bool) {
696        self.automatic_close = value;
697    }
698    fn get_exit_on_esc(&self) -> bool {
699        self.exit_on_esc
700    }
701    fn set_exit_on_esc(&mut self, value: bool) {
702        self.exit_on_esc = value;
703    }
704    fn set_capture_cursor(&mut self, value: bool) {
705        // Normally it should call `.set_relative_mouse_mode(value)`,
706        // but since it does not emit relative mouse events,
707        // we have to fake it by hiding the cursor and warping it
708        // back to the center of the window.
709        self.is_capturing_cursor = value;
710        self.sdl_context.mouse().show_cursor(!value);
711        if value {
712            // Move cursor to center of window now,
713            // to get right relative mouse motion to ignore.
714            self.fake_capture();
715        }
716    }
717    fn show(&mut self) {
718        self.window.show();
719    }
720    fn hide(&mut self) {
721        self.window.hide();
722    }
723    fn get_position(&self) -> Option<Position> {
724        let (x, y) = self.window.position();
725        Some(Position { x, y })
726    }
727    fn set_position<P: Into<Position>>(&mut self, pos: P) {
728        use sdl2::video::WindowPos;
729
730        let pos: Position = pos.into();
731        self.window
732            .set_position(WindowPos::Positioned(pos.x), WindowPos::Positioned(pos.y));
733    }
734    fn set_size<S: Into<Size>>(&mut self, size: S) {
735        let size: Size = size.into();
736        let _ = self.window.set_size(size.width as u32, size.height as u32);
737    }
738}
739
740impl OpenGLWindow for Sdl2Window {
741    fn get_proc_address(&mut self, proc_name: &str) -> ProcAddress {
742        self.video_subsystem.gl_get_proc_address(proc_name) as *const _
743    }
744
745    fn is_current(&self) -> bool {
746        self.context.is_current()
747    }
748
749    fn make_current(&mut self) {
750        self.window.gl_make_current(&self.context).unwrap();
751    }
752}
753
754/// Maps a SDL2 key to piston-input key.
755pub fn sdl2_map_key(keycode: sdl2::keyboard::Keycode) -> keyboard::Key {
756    let keycode = keycode.into_i32();
757    use std::convert::TryInto;
758    let keycode: u32 = keycode
759        .try_into()
760        .expect("sdl keycode purely uses positive numbers");
761    keycode.into()
762}
763
764/// Maps a SDL2 mouse button to piston-input button.
765pub fn sdl2_map_mouse(button: sdl2::mouse::MouseButton) -> MouseButton {
766    use sdl2::mouse::MouseButton as MB;
767
768    match button {
769        MB::Left => MouseButton::Left,
770        MB::Right => MouseButton::Right,
771        MB::Middle => MouseButton::Middle,
772        MB::X1 => MouseButton::X1,
773        MB::X2 => MouseButton::X2,
774        MB::Unknown => MouseButton::Unknown,
775    }
776}