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        .anchor (Alignment::pixel())
176        .build_element();
177      // NOTE: viewport width/height must be non-zero
178      let canvas = Canvas::try_ref_mut (&mut screen.view.component).unwrap();
179      canvas.coordinates.modify_dimensions_horizontal (1);
180      canvas.coordinates.modify_dimensions_vertical (1);
181      screen
182    };
183    // create interface with screen root
184    let mut interface = Interface::<A, Opengl>::with_root (screen);
185    // create child tile frame
186    let screen_id     = interface.root_id().clone();
187    let screen_tiles  = frame::free::Builder::<A>::new (interface.elements(), &screen_id)
188      .layout (layout::Free { size: Size::fill(), .. layout::Free::default_tile() })
189      .clear_color (canvas::ClearColor::Fixed (None))
190      .coord_kind_override (coordinates::Kind::Tile)
191      .build_element();
192    let (_, Event::Create (_, tiles_id, _)) = interface
193      .action (&screen_id, Action::create_singleton (screen_tiles, CreateOrder::Append))
194      .next().unwrap() else { unreachable!() };
195    interface.presentation.screen_tiles = Some (tiles_id);
196    interface
197  }
198
199  fn get_input (&mut self, input_buffer : &mut Vec <Input>) {
200    use winit::platform::pump_events::EventLoopExtPumpEvents;
201    log::trace!("get input...");
202    // TODO: we may want to ignore some extra winit events, e.g. window axis
203    // motion events or keyboard device events on linux
204    self.inner.0.pump_app_events (
205      Some (time::Duration::ZERO),
206      &mut InputHandler { input_buffer, input_state: &mut self.input_state });
207    log::trace!("...get input");
208  }
209
210  fn display_view <V : AsRef <View>> (&mut self,
211    view_tree      : &Tree <V>,
212    _display_values : std::vec::Drain <(NodeId, Display)>
213  ) {
214    // update the renderer with the current view
215    let tileset_id     = self.tileset_id;
216    let draw_crosshair = self.draw_crosshair;
217    render::update (&mut self.inner_mut().1, tileset_id, view_tree,
218      draw_crosshair);
219    // render the frame
220    self.inner.1.do_frame (self.glium_frame.as_mut());
221    self.frame += 1;
222    log::trace!("...display view");
223  }
224}
225
226impl InputHandler <'_> {
227  fn handle_event (&mut self, event : winit::event::Event <()>) {
228    match self.input_state.try_convert_winit_event (event) {
229      Ok (input) => {
230        log::debug!("input: {input:?}");
231        self.input_buffer.extend (input)
232      }
233      Err (event) => log::trace!("ignored winit event: {event:?}")
234    }
235  }
236}
237
238impl InputState {
239  fn new() -> Self {
240    InputState {
241      dimensions:          Default::default(),
242      modifiers:           Default::default(),
243      // pointer is initialized to (-1,-1) until the first resize event is received at
244      // which point it will be moved to the center of the screen
245      pointer_hv:          (-1.0, -1.0),
246      pointer_sensitivity: 1.0
247    }
248  }
249  // NOTE: we possibly return multiple input events because winit no longer emits
250  // separate "text" events, so a key event can generate both a button event and a text
251  // event
252  #[expect(clippy::result_large_err)]  // TODO: box event ?
253  fn try_convert_winit_event <T> (&mut self, event : winit::event::Event <T>)
254    -> Result <Vec <Input>, winit::event::Event <T>>
255    where T : std::fmt::Debug
256  {
257    use winit::event::{self, Event};
258    use view::input;
259    log::trace!("winit event: {event:?}");
260    let winit_event = event; // rename to prevent shadowing
261    #[expect(clippy::match_same_arms)]
262    let input = match &winit_event {
263      Event::NewEvents (_) => return Err (winit_event),
264      Event::WindowEvent { event, .. } => match event {
265        event::WindowEvent::ModifiersChanged (modifiers) => {
266          self.modifiers = modifiers.state();
267          return Err (winit_event)
268        }
269        event::WindowEvent::AxisMotion {..}    => return Err (winit_event),
270        event::WindowEvent::CloseRequested     => input::System::Close.into(),
271        event::WindowEvent::CursorEntered {..} => input::System::CursorEntered.into(),
272        event::WindowEvent::CursorLeft {..}    => input::System::CursorLeft.into(),
273        // NOTE: due to the implementation of winit, CursorMoved events are not
274        // generated on windows when the mouse is hidden and grabbed so we need
275        // to create a virtual pointer position and update it based on device
276        // MouseMotion events
277        event::WindowEvent::CursorMoved {..}   => return Err (winit_event),
278        event::WindowEvent::RedrawRequested    => input::System::Redraw.into(),
279        event::WindowEvent::Destroyed          => input::System::Destroyed.into(),
280        event::WindowEvent::Focused (focused)  =>
281          input::System::Focused (*focused).into(),
282        event::WindowEvent::KeyboardInput { event, is_synthetic: false, .. } => {
283          let input = (event, self.modifiers).into();
284          if event.state == event::ElementState::Pressed &&
285            let Some (mut text) = event.text.clone().map (|text| text.to_string())
286          {
287            let text_input = if text.len() > 1 {
288              input::Text::String (text)
289            } else {
290              debug_assert_eq!(text.len(), 1);
291              input::Text::Char (text.pop().unwrap())
292            }.into();
293            return Ok (vec![input, text_input])
294          }
295          input
296        }
297        event::WindowEvent::KeyboardInput { is_synthetic: true, .. } =>
298          return Err (winit_event),
299        event::WindowEvent::MouseInput { button, state, .. } =>
300          mouse_button (*button, self.modifiers, *state),
301        event::WindowEvent::MouseWheel { delta, .. } =>
302          mouse_wheel (*delta, self.modifiers),
303        event::WindowEvent::Resized (logical_size) => {
304          // TODO: convert logical coordinates to physical coordinates ?
305          let (width, height) = (*logical_size).into();
306          let mut out = vec![input::System::Resized { width, height }.into()];
307          self.dimensions = dimensions::Pixel::new_wh (width, height);
308          if self.pointer_hv == (-1.0, -1.0) {
309            self.pointer_hv.0 = width  as f32 / 2.0;
310            self.pointer_hv.1 = height as f32 / 2.0;
311            out.push (self.pointer().into());
312          } else {
313            self.clip_pointer().map (|pointer| out.push (pointer.into()));
314          }
315          return Ok (out)
316        }
317        // "input method event" see winit documentation
318        event::WindowEvent::Ime (_) => input::System::InputMethod (/*TODO*/).into(),
319        event::WindowEvent::Occluded (occluded) =>
320          input::System::Occluded (*occluded).into(),
321        event::WindowEvent::Moved (_) => return Err (winit_event),
322        _ => {
323          log::error!("TODO: unhandled glutin window event: {event:?}");
324          unimplemented!("TODO: unhandled glutin window event: {:?}", event)
325        }
326      }
327      Event::DeviceEvent { event, .. } => match event {
328        // TODO: joystick/gamepad buttons? note this also returns mouse buttons
329        event::DeviceEvent::Button {..} => return Err (winit_event),
330        event::DeviceEvent::Key (_)     => return Err (winit_event),
331        // TODO: always round away from zero ?
332        event::DeviceEvent::Motion { axis, value } =>
333          input::Axis { axis: *axis, value: value.round() as i32 }.into(),
334        // TODO: always round away from zero ?
335        event::DeviceEvent::MouseMotion { delta: (x, y) } => {
336          let motion = input::Motion (x.round() as i32, y.round() as i32).into();
337          let pointer = {
338            self.pointer_hv.0 += (x.abs().powf (self.pointer_sensitivity as f64)
339              * x.signum()) as f32;
340            self.pointer_hv.1 -= (y.abs().powf (self.pointer_sensitivity as f64)
341              * y.signum()) as f32;
342            self.clip_pointer().unwrap_or_else (|| self.pointer()).into()
343          };
344          return Ok (vec![motion, pointer])
345        }
346        event::DeviceEvent::MouseWheel {..} => return Err (winit_event),
347        event::DeviceEvent::Added           => return Err (winit_event),
348        event::DeviceEvent::Removed         => {
349          log::error!("TODO: unhandled glutin device event: {event:?}");
350          unimplemented!("TODO: unhandled glutin device event: {:?}", event)
351        }
352      }
353      // NOTE: resume is spammed so we are not handling it for now
354      Event::Resumed     => return Err (winit_event),
355      Event::AboutToWait => return Err (winit_event),
356      _ => {
357        log::error!("TODO: unhandled winit event: {winit_event:?}");
358        unimplemented!("TODO: unhandld winit event: {:?}", winit_event)
359      }
360    };
361    Ok (vec![input])
362  }
363
364  fn pointer (&self) -> input::Pointer {
365    input::Pointer {
366      position_horizontal: self.pointer_hv.0,
367      position_vertical:   self.pointer_hv.1,
368      modifiers:           self.modifiers.into()
369    }
370  }
371
372  /// Return a pointer input event if position was clipped
373  fn clip_pointer (&mut self) -> Option <input::Pointer> {
374    let hv_before = self.pointer_hv;
375    self.pointer_hv.0 = f32::min (self.pointer_hv.0, self.dimensions.width() as f32);
376    self.pointer_hv.0 = f32::max (self.pointer_hv.0, 0.0);
377    self.pointer_hv.1 = f32::min (self.pointer_hv.1, self.dimensions.height() as f32);
378    self.pointer_hv.1 = f32::max (self.pointer_hv.1, 0.0);
379    if hv_before != self.pointer_hv {
380      Some (self.pointer())
381    } else {
382      None
383    }
384  }
385}
386
387impl winit::application::ApplicationHandler for InputHandler <'_> {
388  // required
389  fn resumed (&mut self, _event_loop : &winit::event_loop::ActiveEventLoop) {
390    self.handle_event (winit::event::Event::Resumed)
391  }
392  fn window_event (&mut self,
393    _event_loop : &winit::event_loop::ActiveEventLoop,
394    window_id   : winit::window::WindowId,
395    event       : winit::event::WindowEvent
396  ) {
397    self.handle_event (winit::event::Event::WindowEvent { window_id, event })
398  }
399  // optional
400  fn new_events (&mut self,
401    _event_loop : &winit::event_loop::ActiveEventLoop,
402    cause       : winit::event::StartCause
403  ) {
404    self.handle_event (winit::event::Event::NewEvents (cause))
405  }
406  fn user_event (&mut self,
407    _event_loop : &winit::event_loop::ActiveEventLoop,
408    event       : ()
409  ) {
410    self.handle_event (winit::event::Event::UserEvent (event))
411  }
412  fn device_event (&mut self,
413    _event_loop : &winit::event_loop::ActiveEventLoop,
414    device_id   : winit::event::DeviceId,
415    event       : winit::event::DeviceEvent
416  ) {
417    self.handle_event (winit::event::Event::DeviceEvent { device_id, event })
418  }
419  fn about_to_wait (&mut self, _event_loop : &winit::event_loop::ActiveEventLoop) {
420    self.handle_event (winit::event::Event::AboutToWait)
421  }
422  fn suspended (&mut self, _event_loop : &winit::event_loop::ActiveEventLoop) {
423    self.handle_event (winit::event::Event::Suspended)
424  }
425  fn exiting (&mut self, _event_loop : &winit::event_loop::ActiveEventLoop) {
426    self.handle_event (winit::event::Event::LoopExiting)
427  }
428  fn memory_warning (&mut self, _event_loop : &winit::event_loop::ActiveEventLoop) {
429    self.handle_event (winit::event::Event::MemoryWarning)
430  }
431}
432
433impl From <winit::event::MouseButton> for input::button::Mouse {
434  fn from (button : winit::event::MouseButton) -> input::button::Mouse {
435    use winit::event::MouseButton;
436    use view::input::button::Mouse;
437    match button {
438      MouseButton::Left      => Mouse::Mouse1,
439      MouseButton::Right     => Mouse::Mouse2,
440      MouseButton::Middle    => Mouse::Mouse3,
441      MouseButton::Other (4) => Mouse::Mouse4,
442      MouseButton::Other (5) => Mouse::Mouse5,
443      button => {
444        log::error!("unhandled glutin mouse button: {button:?}");
445        unimplemented!()
446      }
447    }
448  }
449}
450
451impl From <(&winit::event::KeyEvent, winit::keyboard::ModifiersState)> for Input {
452  fn from (
453    (input, modifiers) :
454    (&winit::event::KeyEvent, winit::keyboard::ModifiersState)
455  ) -> Input {
456    use std::str::FromStr;
457    use view::{input, Input};
458    use winit::keyboard;
459    let state  = input.state.into();
460    let button = {
461      let variant = match &input.logical_key {
462        keyboard::Key::Named (named_key) =>
463          input::button::Keycode::from ((*named_key, input.location)),
464        keyboard::Key::Character (ch) => input::button::Keycode::try_from (
465          char::from_str (ch.as_str()).unwrap()
466        ).unwrap(),
467        //  input::button::Keycode::from_scancode (input.scancode),
468        keyboard::Key::Unidentified (k) =>
469          unimplemented!("TODO: unidentified key: {:?}", k),
470        keyboard::Key::Dead (k) =>
471          unimplemented!("TODO: dead key: {:?}", k)
472      }.into();
473      input::Button { variant, modifiers: modifiers.into() }
474    };
475    Input::Button (button, state)
476  }
477}
478
479impl From <winit::event::ElementState> for input::button::State {
480  fn from (state : winit::event::ElementState) -> Self {
481    use winit::event::ElementState;
482    use view::input::button::State;
483    match state {
484      ElementState::Pressed  => State::Pressed,
485      ElementState::Released => State::Released
486    }
487  }
488}
489
490impl From <winit::keyboard::ModifiersState> for input::Modifiers {
491  fn from (modifiers : winit::keyboard::ModifiersState) -> Self {
492    use view::input::Modifiers;
493    let mut out = Modifiers::empty();
494    out.set (Modifiers::SHIFT, modifiers.shift_key());
495    out.set (Modifiers::CTRL,  modifiers.control_key());
496    out.set (Modifiers::ALT,   modifiers.alt_key());
497    out.set (Modifiers::SUPER, modifiers.super_key());
498    out
499  }
500}
501
502/// Convert a back-end mouse+modifier+element state event into an interface button input
503fn mouse_button (
504  button    : winit::event::MouseButton,
505  modifiers : winit::keyboard::ModifiersState,
506  state     : winit::event::ElementState
507) -> Input {
508  let state  = state.into();
509  let button = {
510    let variant   = input::button::Mouse::from (button).into();
511    let modifiers = modifiers.into();
512    input::Button { variant, modifiers }
513  };
514  Input::Button (button, state)
515}
516
517/// Convert a back-end mouse wheel delta+modifier event into an interface wheel
518/// input
519fn mouse_wheel (
520  delta     : winit::event::MouseScrollDelta,
521  modifiers : winit::keyboard::ModifiersState
522) -> Input {
523  let delta = match delta {
524    winit::event::MouseScrollDelta::LineDelta (x, y)
525      => (x.round() as i32, y.round() as i32),
526    winit::event::MouseScrollDelta::PixelDelta (
527      winit::dpi::PhysicalPosition { x, y }
528    ) => (x.round() as i32, y.round() as i32)
529  };
530  let modifiers = modifiers.into();
531  input::Wheel { delta, modifiers }.into()
532}