gooey-rs 0.12.1

Tile-based UI library with audio support
Documentation
//! Remote Opengl backend: events are collected locally and rendering is done on
//! a remote thread.
//!
//! `Opengl::init` should be called first. It will wait until the
//! `Remote::make_interface` initialization process has placed the windowed
//! context and display receiver into a shared mutex so the `Render`
//! initialization can proceed. The `Remote::make_interface` initialization
//! process will then in turn wait for the `Opengl::init` process to finish.

use std::{env, thread, time, vec};
use std::sync::{atomic, Arc, LazyLock, Mutex};
use nsys::{self, gl, math};
use nsys::gl::glium::{self, glutin};
use nsys::gl::winit;
use key_vec::KeyVec;
use unbounded_spsc;

use crate::prelude::*;
use super::{render, Graphics, InputHandler, InputState, Presentation};

/// Used to transfer view display events to the rendering backend
#[expect(clippy::type_complexity)]
static DISPLAY_RECEIVER
  : LazyLock <Mutex <Option <unbounded_spsc::Receiver <Vec <(NodeId, Display)>>>>>
  = LazyLock::new (|| Mutex::new (None));
/// Used to share window with render thread
static GLUTIN_WINDOW
  : LazyLock <Mutex <Option <(winit::window::Window, glutin::config::Config)>>>
  = LazyLock::new (|| Mutex::new (None));
/// Frame counter
static FRAME : LazyLock <Arc <atomic::AtomicU64>> = LazyLock::new (||
  Arc::new (atomic::AtomicU64::new (0)));

/// Remote rendering handle and glutin event loop
pub struct Remote {
  pub event_loop : winit::event_loop::EventLoop <()>,
  display        : unbounded_spsc::Sender <Vec <(NodeId, Display)>>,
  input_state    : InputState,
  screen_tiles   : Option <NodeId>
}

/// Rendering backend that can receive and handle display updates
pub struct Opengl {
  pub draw_crosshair : bool,
  pub inner       : gl::Render,
  tileset_id      : Option <gl::render::resource::DefaultTilesetId>,
  updates         : unbounded_spsc::Receiver <Vec <(NodeId, Display)>>,
  id_map          : KeyVec <NodeId, NodeId>,
  view            : Tree <View>,
  glium_frame     : Option <glium::Frame>,
  /// Counts number of batches of display events have been processed
  display_counter : u64
}

/// Create a tile child frame of the root screen frame.
///
/// Must be called after `Opengl::init()`.
pub fn create_screen_tiles <A, P> (interface : &mut Interface <A, P>) where
  A : Application,
  P : Presentation + presentation::HasGraphics <Remote>
{
  let screen_id     = interface.root_id().clone();
  let screen_tiles  = frame::free::Builder::<A>::new (
    interface.elements(), &screen_id
  ) .clear_color (canvas::ClearColor::Fixed (None))
    .coord_kind_override (coordinates::Kind::Tile)
    .layout (layout::Free { size: Size::fill(), .. layout::Free::default_tile() })
    .name ("screen-tiles".to_string())
    .build_element();
  let (_, Event::Create (_, tiles_id, _)) = interface
    .action (&screen_id, Action::create_singleton (screen_tiles, CreateOrder::Append))
    .next().unwrap() else { unreachable!() };
  interface.presentation.graphics().screen_tiles = Some (tiles_id);
}

impl Default for Remote {
  fn default() -> Self {
    let event_loop = {
      let (event_loop, window, gl_config) =
        gl::init::glutin_window ("Gooey Opengl Remote Window");
      {
        let mut glutin_window_lock = GLUTIN_WINDOW.lock().unwrap();
        debug_assert!(glutin_window_lock.is_none());
        // hide & grab cursor
        // NOTE: as of winit 0.30.5 setting cursor visibility on other threads seems to
        // block indefinitely on windows, so we set it here before sending to remote
        // thread
        window.set_cursor_visible (false);
        // NOTE: grabbing cursor here on x11 results in error:
        // Misc("Cursor could not be confined: confine location not viewable")
        //window.set_cursor_grab (winit::window::CursorGrabMode::Confined).unwrap();
        *glutin_window_lock = Some ((window, gl_config));
      }
      event_loop
    };
    let display = {
      let (sender, receiver) = unbounded_spsc::channel::<Vec <(NodeId, Display)>>();
      {
        let mut display_receiver_lock = DISPLAY_RECEIVER.lock().unwrap();
        debug_assert!(display_receiver_lock.is_none());
        *display_receiver_lock = Some (receiver);
      }
      sender
    };
    Remote {
      event_loop,
      display,
      input_state:  InputState::new(),
      screen_tiles: None
    }
  }
}

/// Gets the current rendered frame count
pub fn frame() -> u64 {
  FRAME.load (atomic::Ordering::SeqCst)
}

impl Remote {
  #[inline]
  pub const fn screen_tiles_id (&self) -> &NodeId {
    self.screen_tiles.as_ref().unwrap()
  }
  #[inline]
  pub const fn dimensions (&self) -> dimensions::Pixel {
    self.input_state.dimensions
  }
  pub const fn pointer_sensitivity (&self) -> f32 {
    self.input_state.pointer_sensitivity
  }
  pub const fn set_pointer_sensitivity (&mut self, pointer_sensitivity : f32) {
    self.input_state.pointer_sensitivity = pointer_sensitivity
  }
}

impl Graphics     for Remote { }
impl Presentation for Remote {
  /// &#9888; Will block until `Opengl::init` is called.
  ///
  /// Creates a root screen canvas with default dimensions (1x1).
  fn make_interface <A : Application> () -> Interface <A, Self> {
    let screen = {
      // create a root screen frame and rely on the resize control function to
      // update the dimensions
      let mut screen = frame::screen::PixelBuilder::<A>::new()
        .anchor (Alignment::pixel())
        .name ("screen".to_string())
        .build_element();
      // NOTE: viewport width/height must be non-zero
      let canvas = Canvas::try_ref_mut (&mut screen.view.component).unwrap();
      canvas.coordinates.modify_dimensions_horizontal (1);
      canvas.coordinates.modify_dimensions_vertical (1);
      screen
    };
    Interface::<A, Remote>::with_root (screen)
  }

  /// &#9888; Will block until `Opengl::init` is called.
  ///
  /// Expects a root `Canvas` component.
  fn with_root (root : View, root_id : NodeId) -> Self {
    let remote = Self::default();
    log::debug!("opengl remote with root: {root:?}");
    remote.display.send (vec![
      (root_id.clone(), Display::Create (root, root_id, CreateOrder::Append))
    ]).unwrap();
    // wait for Opengl::init to finish: to do so we check if the DISPLAY_RECEIVER has
    // been taken
    loop {
      if (*DISPLAY_RECEIVER).lock().unwrap().is_none() {
        break
      }
      thread::sleep (time::Duration::from_millis (100));
    }
    remote
  }

  fn get_input (&mut self, input_buffer : &mut Vec <Input>) {
    use winit::platform::pump_events::EventLoopExtPumpEvents;
    log::trace!("get input...");
    // TODO: we may want to ignore some extra glutin events, e.g. window axis
    // motion events or keyboard device events on linux
    self.event_loop.pump_app_events (
      Some (time::Duration::ZERO),
      &mut InputHandler { input_buffer, input_state: &mut self.input_state });
    log::trace!("...get input");
  }

  fn display_view <V : AsRef <View>> (&mut self,
    _view_tree      : &Tree <V>,
    display_values : vec::Drain <(NodeId, Display)>
  ) {
    self.display.send (display_values.collect()).unwrap();
  }
}

impl Opengl {
  /// &#9888; `Remote::make_interface` will block until this method is called.
  ///
  /// Get the windowed context and display receiver and create the `Opengl` backend.
  /// This function will not return until the `Remote` struct has been created.
  pub fn init (tileset_id : Option <gl::render::resource::DefaultTilesetId>) -> Self {
    let inner = {
      use gl::render::resource::MAIN_VIEWPORT;
      // wait for glutin window to be set
      let (window, gl_config) = loop {
        let maybe_glutin_window = (*GLUTIN_WINDOW).lock().unwrap().take();
        if let Some (glutin_window) = maybe_glutin_window {
          break glutin_window
        }
        thread::sleep (time::Duration::from_millis (100));
      };
      // NOTE: as of winit 0.30.5 setting cursor visibility/grab on other
      // threads seems to block indefinitely on windows, so we set it on main
      // before receiving here
      //window.set_cursor_visible (false);
      //window.set_cursor_grab (winit::window::CursorGrabMode::Confined).unwrap();
      let display = gl::init::glium_display_gl33core (&window, &gl_config);
      let mut render =
        gl::Render::<gl::render::resource::Default>::new (display, window);
      // set the tile dimensions
      let [tile_width, tile_height] =
        render.resource.tile_dimensions (tileset_id.unwrap_or_default());
      unsafe {
        env::set_var ("GOOEY_TILE_WIDTH", tile_width.to_string());
        env::set_var ("GOOEY_TILE_HEIGHT", tile_height.to_string());
      }
      // set the main viewport
      render.resource.draw2d.viewport_resources_set (MAIN_VIEWPORT, Default::default());
      render
    };
    let updates = loop {
      let maybe_receiver = (*DISPLAY_RECEIVER).lock().unwrap().take();
      if let Some (receiver) = maybe_receiver {
        break receiver
      }
      thread::sleep (time::Duration::from_millis (100));
    };
    Opengl {
      inner,
      tileset_id,
      updates,
      id_map:          KeyVec::new(),
      view:            Tree::new(),
      glium_frame:     None,
      display_counter: 0,
      draw_crosshair:  false
    }
  }

  #[inline]
  pub const fn inner (&self) -> &gl::Render {
    &self.inner
  }

  #[inline]
  pub const fn inner_mut (&mut self) -> &mut gl::Render {
    &mut self.inner
  }

  #[inline]
  #[deprecated = "use remote::frame() function instead"]
  pub fn frame (&self) -> u64 {
    frame()
  }

  #[inline]
  pub const fn display_counter (&self) -> u64 {
    self.display_counter
  }

  #[inline]
  pub const fn reset_display_counter (&mut self) {
    self.display_counter = 0;
  }

  pub fn load_pointer (&mut self,
    key    : gl::render::resource::PointerTextureIndexRepr,
    bytes  : &[u8],
    offset : math::Vector2 <i16>
  ) {
    let render = &mut self.inner;
    render::load_pointer (render, key, bytes, offset);
  }

  pub fn load_textures (&mut self,
    textures_16x16   : &[&'static str],
    textures_64x64   : &[&'static str],
    textures_anysize : &[&'static str]
  ) {
    render::load_textures (
      &mut self.inner, textures_16x16, textures_64x64, textures_anysize)
  }

  #[inline]
  pub fn process_display_events (&mut self) {
    while self.process_display_events_single() { }
  }

  /// Returns `true` if a batch of display events was processed, `false` otherwise
  pub fn process_display_events_single (&mut self) -> bool {
    if let Ok (updates) = self.updates.try_recv() {
      for (node_id, display) in updates {
        match display {
          Display::Create (view, child_id, order) => {
            let new_id = if self.id_map.is_empty() {
              // special case for initial root node: parent_id == child_id
              self.view.insert (Node::new (view),
                tree::InsertBehavior::AsRoot).unwrap()
            } else {
              let parent_id = self.id_map.get (&node_id).unwrap();
              let new_id    = self.view.insert (
                Node::new (view),
                tree::InsertBehavior::UnderNode (parent_id)).unwrap();
              match order {
                CreateOrder::Prepend => {
                  let _ = self.view.make_first_sibling (&new_id).unwrap();
                }
                CreateOrder::NthSibling (n) =>
                  self.view.make_nth_sibling (&new_id, n as usize).unwrap(),
                CreateOrder::Append => {}
              }
              new_id
            };
            assert!(self.id_map.insert (child_id, new_id).is_none());
          }
          Display::Update (update) => {
            let id = self.id_map.get (&node_id).unwrap();
            match update {
              Update::View (view) =>
                *self.view.get_mut (id).unwrap().data_mut() = view,
              Update::FocusTop => {
                self.view.make_last_sibling (id).unwrap();
              }
            }
          }
          Display::Destroy => {
            let id = self.id_map.remove (&node_id).unwrap();
            let _ = self.view.remove_node (id, tree::RemoveBehavior::DropChildren)
              .unwrap();
          }
        }
      }
      self.display_counter += 1;
      true
    } else {
      false
    }
  }
  pub fn update_and_do_frame (&mut self) {
    // update the renderer with the current view
    render::update (&mut self.inner, self.tileset_id, &self.view, self.draw_crosshair);
    // render the frame
    self.inner.do_frame (self.glium_frame.as_mut());
    FRAME.fetch_add (1, atomic::Ordering::SeqCst);
  }
  /// Pump display events and render a frame
  pub fn display (&mut self) {
    self.process_display_events();
    self.update_and_do_frame();
  }
}

/// Hack to allow passing a tree of views directly to the `render::update` function
impl AsRef <View> for View {
  fn as_ref (&self) -> &View {
    self
  }
}