Skip to main content

gooey/presentation/opengl/
mod.rs

1//! OpenGL backend
2
3use std;
4use std::convert::TryFrom;
5use std::time;
6use log;
7
8use nsys::{self, gl, math::Vector2};
9use nsys::gl::glium::{self, glutin};
10use nsys::gl::winit;
11
12use crate::prelude::*;
13use super::{Graphics, Presentation};
14
15pub mod remote;
16pub mod render;
17
18pub use self::remote::Remote;
19
20pub const RESOURCE_TEXTURES_16X16   : u16 = 0;
21pub const RESOURCE_TEXTURES_64X64   : u16 = 1;
22pub const RESOURCE_TEXTURES_ANYSIZE : u16 = 2;
23
24pub const POINTER_TEXTURE_INDEX : u16 = 0;
25
26/// An OpenGL presentation backend
27pub struct Opengl {
28  pub draw_crosshair : bool,
29  pub inner     : (winit::event_loop::EventLoop <()>, gl::Render),
30  input_state   : InputState,
31  tileset_id    : Option <gl::render::resource::DefaultTilesetId>,
32  glium_frame   : Option <glium::Frame>,
33  frame         : u64,
34  screen_tiles  : Option <NodeId>
35}
36
37pub (crate) struct InputState {
38  pub dimensions         : dimensions::Pixel,
39  // winit doesn't include modifiers state in key or mouse events so we have to
40  // track it
41  pub modifiers          : winit::keyboard::ModifiersState,
42  // due to the implementation of winit, CursorMoved events are not generated on
43  // windows when the mouse is hidden and grabbed so we need to create a virtual
44  // pointer position and update it based on device MouseMotion events
45  pub pointer_hv         : (f32, f32),
46  pub pointer_sensitivity : f32
47}
48
49pub (crate) struct InputHandler <'a> {
50  pub input_buffer : &'a mut Vec <Input>,
51  pub input_state  : &'a mut InputState,
52}
53
54/// Log glium info
55pub fn log_glium_info (
56  glium_display : &glium::Display <glutin::surface::WindowSurface>
57) {
58  let glium_info = gl::info::Glium::new (glium_display);
59  log::info!("{glium_info:#?}");
60}
61
62impl Opengl {
63  #[inline]
64  pub const fn inner (&self) -> &(winit::event_loop::EventLoop <()>, gl::Render) {
65    &self.inner
66  }
67  #[inline]
68  pub const fn inner_mut (&mut self)
69    -> &mut (winit::event_loop::EventLoop <()>, gl::Render)
70  {
71    &mut self.inner
72  }
73  #[inline]
74  pub const fn frame (&self) -> u64 {
75    self.frame
76  }
77  #[inline]
78  pub const fn screen_tiles_id (&self) -> &NodeId {
79    self.screen_tiles.as_ref().unwrap()
80  }
81  #[inline]
82  pub fn dimensions (&self) -> dimensions::Pixel {
83    let inner_size : (u32, u32) =
84      self.inner.1.window.inner_size().into();
85    Vector2::from (inner_size).into()
86  }
87  pub const fn pointer_sentivity (&self) -> f32 {
88    self.input_state.pointer_sensitivity
89  }
90  pub const fn set_pointer_sensitivity (&mut self, pointer_sensitivity : f32) {
91    self.input_state.pointer_sensitivity = pointer_sensitivity
92  }
93  pub fn load_pointer (&mut self,
94    key    : gl::render::resource::PointerTextureIndexRepr,
95    bytes  : &[u8],
96    offset : Vector2 <i16>
97  ) {
98    let render  = &mut self.inner_mut().1;
99    render::load_pointer (render, key, bytes, offset);
100  }
101  pub fn load_textures (&mut self,
102    textures_16x16   : &[&'static str],
103    textures_64x64   : &[&'static str],
104    textures_anysize : &[&'static str]
105  ) {
106    render::load_textures (
107      &mut self.inner.1, textures_16x16, textures_64x64, textures_anysize)
108  }
109}
110
111impl std::fmt::Debug for Opengl {
112  fn fmt (&self, f : &mut std::fmt::Formatter) -> Result <(), std::fmt::Error> {
113    write!(f, "Opengl {{ inner: ({:?}, Render), frame: {} }}",
114      self.inner.0, self.frame)
115  }
116}
117
118impl Default for Opengl {
119  fn default() -> Self {
120    // TODO: tileset parameter ?
121    let tileset_id  = gl::render::resource::DefaultTilesetId::EasciiAcorn128;
122    let input_state = InputState::new();
123    let inner = {
124      let (event_loop, window, _config, display) =
125        gl::init::glium_init_gl33core ("Gooey Opengl Window");
126      // hide & grab cursor
127      window.set_cursor_visible (false);
128      window.set_cursor_grab (winit::window::CursorGrabMode::Confined).unwrap();
129      let render = gl::Render::<gl::render::resource::Default>::new (display, window);
130      // set the tile dimensions
131      let [tile_width, tile_height] = render.resource.tile_dimensions (tileset_id);
132      unsafe {
133        std::env::set_var ("GOOEY_TILE_WIDTH", tile_width.to_string());
134        std::env::set_var ("GOOEY_TILE_HEIGHT", tile_height.to_string());
135      }
136      (event_loop, render)
137    };
138    Opengl {
139      inner, input_state,
140      tileset_id:     Some (tileset_id),
141      glium_frame:    None,
142      frame:          0,
143      screen_tiles:   None,
144      draw_crosshair: false
145    }
146  }
147}
148
149impl Graphics     for Opengl { }
150impl Presentation for Opengl {
151  /// Expects a root Canvas component
152  fn with_root (root : View, _id : NodeId) -> Self {
153    use gl::render::resource::{draw2d, MAIN_VIEWPORT};
154    let mut opengl = Self::default();
155    {
156      let draw_crosshair = opengl.draw_crosshair;
157      let render = &mut opengl.inner_mut().1;
158      render::set_screen_view (render, &root);
159      let resources = draw2d::ViewportResources {
160        draw_crosshair, .. Default::default()
161      };
162      render.resource.draw2d.viewport_resources_set (MAIN_VIEWPORT, resources);
163    }
164    log::debug!("opengl with root: {opengl:?}");
165    opengl
166  }
167
168  /// Create a new interface with a root screen pixel frame element and child
169  /// screen tile frame element
170  fn make_interface <A : Application> () -> Interface <A, Self> {
171    let screen = {
172      // create a root screen frame and rely on the resize control function to
173      // update the dimensions
174      let mut screen = frame::screen::PixelBuilder::<A>::new()
175        .name ("screen".to_string())
176        .anchor (Alignment::pixel())
177        .build_element();
178      // NOTE: viewport width/height must be non-zero
179      let canvas = Canvas::try_ref_mut (&mut screen.view.component).unwrap();
180      canvas.coordinates.modify_dimensions_horizontal (1);
181      canvas.coordinates.modify_dimensions_vertical (1);
182      screen
183    };
184    // create interface with screen root
185    let mut interface = Interface::<A, Opengl>::with_root (screen);
186    // create child tile frame
187    let screen_id     = interface.root_id().clone();
188    let screen_tiles  = frame::free::Builder::<A>::new (interface.elements(), &screen_id)
189      .name ("screen-tiles".to_string())
190      .layout (layout::Free { size: Size::fill(), .. layout::Free::default_tile() })
191      .clear_color (canvas::ClearColor::Fixed (None))
192      .coord_kind_override (coordinates::Kind::Tile)
193      .build_element();
194    let (_, Event::Create (_, tiles_id, _)) = interface
195      .action (&screen_id, Action::create_singleton (screen_tiles, CreateOrder::Append))
196      .next().unwrap() else { unreachable!() };
197    interface.presentation.screen_tiles = Some (tiles_id);
198    interface
199  }
200
201  fn get_input (&mut self, input_buffer : &mut Vec <Input>) {
202    use winit::platform::pump_events::EventLoopExtPumpEvents;
203    log::trace!("get input...");
204    // TODO: we may want to ignore some extra winit events, e.g. window axis
205    // motion events or keyboard device events on linux
206    self.inner.0.pump_app_events (
207      Some (time::Duration::ZERO),
208      &mut InputHandler { input_buffer, input_state: &mut self.input_state });
209    log::trace!("...get input");
210  }
211
212  fn display_view <V : AsRef <View>> (&mut self,
213    view_tree      : &Tree <V>,
214    _display_values : std::vec::Drain <(NodeId, Display)>
215  ) {
216    // update the renderer with the current view
217    let tileset_id     = self.tileset_id;
218    let draw_crosshair = self.draw_crosshair;
219    render::update (&mut self.inner_mut().1, tileset_id, view_tree,
220      draw_crosshair);
221    // render the frame
222    self.inner.1.do_frame (self.glium_frame.as_mut());
223    self.frame += 1;
224    log::trace!("...display view");
225  }
226}
227
228impl InputHandler <'_> {
229  fn handle_event (&mut self, event : winit::event::Event <()>) {
230    match self.input_state.try_convert_winit_event (event) {
231      Ok (input) => {
232        log::debug!("input: {input:?}");
233        self.input_buffer.extend (input)
234      }
235      Err (event) => log::trace!("ignored winit event: {event:?}")
236    }
237  }
238}
239
240impl InputState {
241  fn new() -> Self {
242    InputState {
243      dimensions:          Default::default(),
244      modifiers:           Default::default(),
245      // pointer is initialized to (-1,-1) until the first resize event is received at
246      // which point it will be moved to the center of the screen
247      pointer_hv:          (-1.0, -1.0),
248      pointer_sensitivity: 1.0
249    }
250  }
251  // NOTE: we possibly return multiple input events because winit no longer emits
252  // separate "text" events, so a key event can generate both a button event and a text
253  // event
254  #[expect(clippy::result_large_err)]  // TODO: box event ?
255  fn try_convert_winit_event <T> (&mut self, event : winit::event::Event <T>)
256    -> Result <Vec <Input>, winit::event::Event <T>>
257    where T : std::fmt::Debug
258  {
259    use winit::event::{self, Event};
260    use view::input;
261    log::trace!("winit event: {event:?}");
262    let winit_event = event; // rename to prevent shadowing
263    #[expect(clippy::match_same_arms)]
264    let input = match &winit_event {
265      Event::NewEvents (_) => return Err (winit_event),
266      Event::WindowEvent { event, .. } => match event {
267        event::WindowEvent::ModifiersChanged (modifiers) => {
268          self.modifiers = modifiers.state();
269          return Err (winit_event)
270        }
271        event::WindowEvent::AxisMotion {..}    => return Err (winit_event),
272        event::WindowEvent::CloseRequested     => input::System::Close.into(),
273        event::WindowEvent::CursorEntered {..} => input::System::CursorEntered.into(),
274        event::WindowEvent::CursorLeft {..}    => input::System::CursorLeft.into(),
275        // NOTE: due to the implementation of winit, CursorMoved events are not
276        // generated on windows when the mouse is hidden and grabbed so we need
277        // to create a virtual pointer position and update it based on device
278        // MouseMotion events
279        event::WindowEvent::CursorMoved {..}   => return Err (winit_event),
280        event::WindowEvent::RedrawRequested    => input::System::Redraw.into(),
281        event::WindowEvent::Destroyed          => input::System::Destroyed.into(),
282        event::WindowEvent::Focused (focused)  =>
283          input::System::Focused (*focused).into(),
284        event::WindowEvent::KeyboardInput { event, is_synthetic: false, .. } => {
285          let input = (event, self.modifiers).into();
286          if event.state == event::ElementState::Pressed &&
287            let Some (mut text) = event.text.clone().map (|text| text.to_string())
288          {
289            let text_input = if text.len() > 1 {
290              input::Text::String (text)
291            } else {
292              debug_assert_eq!(text.len(), 1);
293              input::Text::Char (text.pop().unwrap())
294            }.into();
295            return Ok (vec![input, text_input])
296          }
297          input
298        }
299        event::WindowEvent::KeyboardInput { is_synthetic: true, .. } =>
300          return Err (winit_event),
301        event::WindowEvent::MouseInput { button, state, .. } =>
302          mouse_button (*button, self.modifiers, *state),
303        event::WindowEvent::MouseWheel { delta, .. } =>
304          mouse_wheel (*delta, self.modifiers),
305        event::WindowEvent::Resized (logical_size) => {
306          // TODO: convert logical coordinates to physical coordinates ?
307          let (width, height) = (*logical_size).into();
308          let mut out = vec![input::System::Resized { width, height }.into()];
309          self.dimensions = dimensions::Pixel::new_wh (width, height);
310          if self.pointer_hv == (-1.0, -1.0) {
311            self.pointer_hv.0 = width  as f32 / 2.0;
312            self.pointer_hv.1 = height as f32 / 2.0;
313            out.push (self.pointer().into());
314          } else {
315            self.clip_pointer().map (|pointer| out.push (pointer.into()));
316          }
317          return Ok (out)
318        }
319        // "input method event" see winit documentation
320        event::WindowEvent::Ime (_) => input::System::InputMethod (/*TODO*/).into(),
321        event::WindowEvent::Occluded (occluded) =>
322          input::System::Occluded (*occluded).into(),
323        event::WindowEvent::Moved (_) => return Err (winit_event),
324        _ => {
325          log::error!("TODO: unhandled glutin window event: {event:?}");
326          unimplemented!("TODO: unhandled glutin window event: {:?}", event)
327        }
328      }
329      Event::DeviceEvent { event, .. } => match event {
330        // TODO: joystick/gamepad buttons? note this also returns mouse buttons
331        event::DeviceEvent::Button {..} => return Err (winit_event),
332        event::DeviceEvent::Key (_)     => return Err (winit_event),
333        // TODO: always round away from zero ?
334        event::DeviceEvent::Motion { axis, value } =>
335          input::Axis { axis: *axis, value: value.round() as i32 }.into(),
336        // TODO: always round away from zero ?
337        event::DeviceEvent::MouseMotion { delta: (x, y) } => {
338          let motion = input::Motion (x.round() as i32, y.round() as i32).into();
339          let pointer = {
340            self.pointer_hv.0 += (x.abs().powf (self.pointer_sensitivity as f64)
341              * x.signum()) as f32;
342            self.pointer_hv.1 -= (y.abs().powf (self.pointer_sensitivity as f64)
343              * y.signum()) as f32;
344            self.clip_pointer().unwrap_or_else (|| self.pointer()).into()
345          };
346          return Ok (vec![motion, pointer])
347        }
348        event::DeviceEvent::MouseWheel {..} => return Err (winit_event),
349        event::DeviceEvent::Added           => return Err (winit_event),
350        event::DeviceEvent::Removed         => {
351          log::error!("TODO: unhandled glutin device event: {event:?}");
352          unimplemented!("TODO: unhandled glutin device event: {:?}", event)
353        }
354      }
355      // NOTE: resume is spammed so we are not handling it for now
356      Event::Resumed     => return Err (winit_event),
357      Event::AboutToWait => return Err (winit_event),
358      _ => {
359        log::error!("TODO: unhandled winit event: {winit_event:?}");
360        unimplemented!("TODO: unhandld winit event: {:?}", winit_event)
361      }
362    };
363    Ok (vec![input])
364  }
365
366  fn pointer (&self) -> input::Pointer {
367    input::Pointer {
368      position_horizontal: self.pointer_hv.0,
369      position_vertical:   self.pointer_hv.1,
370      modifiers:           self.modifiers.into()
371    }
372  }
373
374  /// Return a pointer input event if position was clipped
375  fn clip_pointer (&mut self) -> Option <input::Pointer> {
376    let hv_before = self.pointer_hv;
377    self.pointer_hv.0 = f32::min (self.pointer_hv.0, self.dimensions.width() as f32);
378    self.pointer_hv.0 = f32::max (self.pointer_hv.0, 0.0);
379    self.pointer_hv.1 = f32::min (self.pointer_hv.1, self.dimensions.height() as f32);
380    self.pointer_hv.1 = f32::max (self.pointer_hv.1, 0.0);
381    if hv_before != self.pointer_hv {
382      Some (self.pointer())
383    } else {
384      None
385    }
386  }
387}
388
389impl winit::application::ApplicationHandler for InputHandler <'_> {
390  // required
391  fn resumed (&mut self, _event_loop : &winit::event_loop::ActiveEventLoop) {
392    self.handle_event (winit::event::Event::Resumed)
393  }
394  fn window_event (&mut self,
395    _event_loop : &winit::event_loop::ActiveEventLoop,
396    window_id   : winit::window::WindowId,
397    event       : winit::event::WindowEvent
398  ) {
399    self.handle_event (winit::event::Event::WindowEvent { window_id, event })
400  }
401  // optional
402  fn new_events (&mut self,
403    _event_loop : &winit::event_loop::ActiveEventLoop,
404    cause       : winit::event::StartCause
405  ) {
406    self.handle_event (winit::event::Event::NewEvents (cause))
407  }
408  fn user_event (&mut self,
409    _event_loop : &winit::event_loop::ActiveEventLoop,
410    event       : ()
411  ) {
412    self.handle_event (winit::event::Event::UserEvent (event))
413  }
414  fn device_event (&mut self,
415    _event_loop : &winit::event_loop::ActiveEventLoop,
416    device_id   : winit::event::DeviceId,
417    event       : winit::event::DeviceEvent
418  ) {
419    self.handle_event (winit::event::Event::DeviceEvent { device_id, event })
420  }
421  fn about_to_wait (&mut self, _event_loop : &winit::event_loop::ActiveEventLoop) {
422    self.handle_event (winit::event::Event::AboutToWait)
423  }
424  fn suspended (&mut self, _event_loop : &winit::event_loop::ActiveEventLoop) {
425    self.handle_event (winit::event::Event::Suspended)
426  }
427  fn exiting (&mut self, _event_loop : &winit::event_loop::ActiveEventLoop) {
428    self.handle_event (winit::event::Event::LoopExiting)
429  }
430  fn memory_warning (&mut self, _event_loop : &winit::event_loop::ActiveEventLoop) {
431    self.handle_event (winit::event::Event::MemoryWarning)
432  }
433}
434
435impl From <winit::event::MouseButton> for input::button::Mouse {
436  fn from (button : winit::event::MouseButton) -> input::button::Mouse {
437    use winit::event::MouseButton;
438    use view::input::button::Mouse;
439    match button {
440      MouseButton::Left      => Mouse::Mouse1,
441      MouseButton::Right     => Mouse::Mouse2,
442      MouseButton::Middle    => Mouse::Mouse3,
443      MouseButton::Other (4) => Mouse::Mouse4,
444      MouseButton::Other (5) => Mouse::Mouse5,
445      button => {
446        log::error!("unhandled glutin mouse button: {button:?}");
447        unimplemented!()
448      }
449    }
450  }
451}
452
453impl From <(&winit::event::KeyEvent, winit::keyboard::ModifiersState)> for Input {
454  fn from (
455    (input, modifiers) :
456    (&winit::event::KeyEvent, winit::keyboard::ModifiersState)
457  ) -> Input {
458    use std::str::FromStr;
459    use view::{input, Input};
460    use winit::keyboard;
461    let state  = input.state.into();
462    let button = {
463      let variant = match &input.logical_key {
464        keyboard::Key::Named (named_key) =>
465          input::button::Keycode::from ((*named_key, input.location)),
466        keyboard::Key::Character (ch) => input::button::Keycode::try_from (
467          char::from_str (ch.as_str()).unwrap()
468        ).unwrap(),
469        //  input::button::Keycode::from_scancode (input.scancode),
470        keyboard::Key::Unidentified (k) =>
471          unimplemented!("TODO: unidentified key: {:?}", k),
472        keyboard::Key::Dead (k) =>
473          unimplemented!("TODO: dead key: {:?}", k)
474      }.into();
475      input::Button { variant, modifiers: modifiers.into() }
476    };
477    Input::Button (button, state)
478  }
479}
480
481impl From <winit::event::ElementState> for input::button::State {
482  fn from (state : winit::event::ElementState) -> Self {
483    use winit::event::ElementState;
484    use view::input::button::State;
485    match state {
486      ElementState::Pressed  => State::Pressed,
487      ElementState::Released => State::Released
488    }
489  }
490}
491
492impl From <winit::keyboard::ModifiersState> for input::Modifiers {
493  fn from (modifiers : winit::keyboard::ModifiersState) -> Self {
494    use view::input::Modifiers;
495    let mut out = Modifiers::empty();
496    out.set (Modifiers::SHIFT, modifiers.shift_key());
497    out.set (Modifiers::CTRL,  modifiers.control_key());
498    out.set (Modifiers::ALT,   modifiers.alt_key());
499    out.set (Modifiers::SUPER, modifiers.super_key());
500    out
501  }
502}
503
504/// Convert a back-end mouse+modifier+element state event into an interface button input
505fn mouse_button (
506  button    : winit::event::MouseButton,
507  modifiers : winit::keyboard::ModifiersState,
508  state     : winit::event::ElementState
509) -> Input {
510  let state  = state.into();
511  let button = {
512    let variant   = input::button::Mouse::from (button).into();
513    let modifiers = modifiers.into();
514    input::Button { variant, modifiers }
515  };
516  Input::Button (button, state)
517}
518
519/// Convert a back-end mouse wheel delta+modifier event into an interface wheel
520/// input
521fn mouse_wheel (
522  delta     : winit::event::MouseScrollDelta,
523  modifiers : winit::keyboard::ModifiersState
524) -> Input {
525  let delta = match delta {
526    winit::event::MouseScrollDelta::LineDelta (x, y)
527      => (x.round() as i32, y.round() as i32),
528    winit::event::MouseScrollDelta::PixelDelta (
529      winit::dpi::PhysicalPosition { x, y }
530    ) => (x.round() as i32, y.round() as i32)
531  };
532  let modifiers = modifiers.into();
533  input::Wheel { delta, modifiers }.into()
534}