gooey-rs 0.12.1

Tile-based UI library with audio support
Documentation
use std::convert::TryInto;
use derive_builder::Builder;
use log;

use crate::prelude::*;
use super::{Frame, set_layout};

//
//  controls
//

/// Builtin system control `FrameScreenResize`.
///
/// Frame must have free layout.
pub fn resize (
  system        : &input::System,
  elements      : &Tree <Element>,
  node_id       : &NodeId,
  action_buffer : &mut Vec <(NodeId, Action)>
) {
  log::trace!("resize...");
  match system {
    &input::System::Resized { width, height } => {
      *coordinates::SCREEN_WH.write().unwrap() = [width, height];
      let Widget (layout, _, canvas) = Frame::try_get (elements, node_id).unwrap();
      let coordinate_kind = Some (canvas.coordinates.kind());
      let layout = {
        use controller::component::Layout;
        let (mut free, area) = layout.variant.clone().try_into().unwrap();
        free.size   = {
          let width  = width.into();
          let height = height.into();
          Size { width, height }
        };
        free.offset = if canvas.coordinates.kind() == coordinates::Kind::Tile {
          tile_offset (free.size, free.anchor)
        } else {
          pixel_offset (free.size, free.anchor)
        };
        Layout { variant: (free, area).into(), .. layout.clone() }
      };
      // use the new layout size to recursively refresh child canvas coordinates
      set_layout (elements, node_id, layout, coordinate_kind, action_buffer);
    }
    _ => {}
  }
  log::trace!("...resize");
}

//
//  builder
//

#[derive(Builder)]
#[builder(pattern="owned", build_fn(private), setter(strip_option))]
#[builder(public)]
struct Pixel <'a, A : Application> {
  #[builder(default)]
  anchor          : Option <Alignment>,
  #[builder(default)]
  appearances     : Option <Appearances>,
  #[builder(default)]
  bindings        : Option <&'a Bindings <A>>,
  #[builder(default)]
  border          : Option <Border>,
  #[builder(default)]
  clear_color     : canvas::ClearColor,
  #[builder(default)]
  dimensions      : Option <dimensions::Pixel>,
  #[builder(default)]
  name            : Option <String>,
  #[builder(default)]
  orientation     : (Orientation, Area),
  #[builder(default)]
  system_control  : Option <controls::System>
}

#[derive(Builder)]
#[builder(pattern="owned", build_fn(private), setter(strip_option))]
#[builder(public)]
struct Tile <'a, A : Application> {
  #[builder(default)]
  anchor          : Option <Alignment>,
  #[builder(default)]
  appearances     : Option <Appearances>,
  #[builder(default)]
  bindings        : Option <&'a Bindings <A>>,
  #[builder(default)]
  border          : Option <Border>,
  #[builder(default)]
  clear_color     : canvas::ClearColor,
  #[builder(default)]
  dimensions      : Option <dimensions::Tile>,
  #[builder(default)]
  name            : Option <String>,
  #[builder(default)]
  orientation     : (Orientation, Area),
  #[builder(default)]
  system_control  : Option <controls::System>
}

impl <A : Application> PixelBuilder <'_, A> {
  pub const fn new() -> Self {
    PixelBuilder {
      anchor:          None,
      appearances:     None,
      bindings:        None,
      border:          None,
      clear_color:     None,
      dimensions:      None,
      name:            None,
      orientation:     None,
      system_control:  None
    }
  }
}

impl <A : Application> BuildElement for PixelBuilder <'_, A> {
  /// Create a frame widget representing the root screen surface for a pixel-addressable
  /// GUI.
  ///
  /// By default if no bindings are provided, the left mouse button (`Mouse1`) will be
  /// bound to `ActivePointer`.
  fn build_element (self) -> Element {
    log::trace!("pixel build element...");
    let Pixel {
      anchor, appearances, bindings, border, clear_color, dimensions, name,
      orientation, system_control
    } = self.build()
      .map_err(|err| log::error!("frame screen tile builder error: {err:?}"))
      .unwrap();
    let controller = {
      let layout = {
        use controller::{Alignment, Area, Size};
        use controller::component::{layout, Layout};
        let size = dimensions.map_or (
          Size::default_absolute(),
          |dimensions| Size {
            width:  dimensions.width().into(),
            height: dimensions.height().into()
          }
        );
        let anchor = anchor.unwrap_or (Alignment::pixel());
        let offset = pixel_offset (size, anchor);
        let free   = layout::Free { size, offset, anchor };
        Layout { orientation, .. layout::Variant::from ((free, Area::default())).into() }
      };
      let bindings_empty = Bindings::empty();
      let bindings       = {
        let mut bindings =
          bindings.unwrap_or (&bindings_empty).get_bindings (&super::CONTROLS);
        bindings.system  = system_control.map (Into::into)
          .or (Some (controls::system::Builtin::FrameScreenResize.into()));
        bindings
      };
      let mut controller     = Controller::with_bindings (&bindings);
      controller.component   = layout.into();
      controller.appearances = appearances.unwrap_or_default();
      controller
    };
    let view = {
      let coordinates = Coordinates::Pixel (
        position::Pixel::origin(),
        dimensions.unwrap_or (dimensions::Pixel::default()));
      let width  = coordinates.dimensions_horizontal();
      let height = coordinates.dimensions_vertical();
      *coordinates::SCREEN_WH.write().unwrap() = [width, height];
      let canvas = Canvas { coordinates, clear_color, border };
      let appearance = controller.get_appearance().clone();
      View { appearance, .. view::Component::from (canvas).into() }
    };
    let name = name.unwrap_or ("Frame".to_string());
    log::trace!("...pixel build element");
    Element::new (name, controller, Model::default(), view)
  }
}

impl <A : Application> TileBuilder <'_, A> {
  pub const fn new() -> Self {
    TileBuilder {
      anchor:         None,
      appearances:    None,
      bindings:       None,
      border:         None,
      clear_color:    None,
      dimensions:     None,
      name:           None,
      orientation:    None,
      system_control: None
    }
  }
}

impl <A : Application> BuildElement for TileBuilder <'_, A> {
  /// Create a frame widget representing the root screen for a text-mode GUI.
  fn build_element (self) -> Element {
    log::trace!("tile build element...");
    let Tile {
      anchor, appearances, bindings, border, clear_color, dimensions, name, orientation,
      system_control
    } = self.build()
      .map_err(|err| log::error!("frame screen tile builder error: {err:?}"))
      .unwrap();
    let controller = {
      let layout = {
        use controller::{Alignment, Area, Size};
        use controller::component::{layout, Layout};
        let size = dimensions.map_or (Size::default_absolute(),
          |dimensions| Size {
            width:  dimensions.columns().into(),
            height: dimensions.rows().into()
          }
        );
        let anchor = anchor.unwrap_or (Alignment::tile());
        let offset = tile_offset (size, anchor);
        let free   = layout::Free { size, offset, anchor };
        let area   = Area::default();
        Layout { orientation, .. layout::Variant::from ((free, area)).into() }
      };
      let bindings_empty = Bindings::empty();
      let bindings = {
        let mut bindings = bindings.unwrap_or (&bindings_empty)
          .get_bindings (&super::CONTROLS);
        bindings.system = system_control.map (Into::into)
          .or (Some (controls::system::Builtin::FrameScreenResize.into()));
        bindings
      };
      let mut controller = Controller::with_bindings (&bindings);
      controller.component   = layout.into();
      controller.appearances = appearances.unwrap_or_default();
      controller
    };
    let view = {
      let coordinates = Coordinates::Tile (
        position::Tile::origin(),
        dimensions.unwrap_or (dimensions::Tile::default()));
      let width  = coordinates.dimensions_horizontal();
      let height = coordinates.dimensions_vertical();
      *coordinates::SCREEN_WH.write().unwrap() = [width, height];
      let canvas = Canvas { coordinates, clear_color, border };
      let appearance = controller.get_appearance().clone();
      View { appearance, .. view::Component::from (canvas).into() }
    };
    let name = name.unwrap_or ("Frame".to_string());
    log::trace!("...tile build element");
    Element::new (name, controller, Model::default(), view)
  }
}

//
//  private
//
/// Special handling of alignment for screen canvases: when size is set, the alignment
/// anchor determines where the offset will be. The default pixel alignment is (Bottom,
/// Left), in which case the offset will be (0,0). For a centered alignment (Middle,
/// Center), the offset will be half of the size width and height. For a (Top, Right)
/// alignment, the offset will be negative both width and height.
pub fn pixel_offset (size : Size, anchor : Alignment) -> Offset {
  use controller::{alignment, offset, Offset};
  let horizontal = match anchor.horizontal {
    alignment::Horizontal::Left   => 0.into(),
    alignment::Horizontal::Center => -offset::Signed::from (size.width).half(),
    alignment::Horizontal::Right  => -offset::Signed::from (size.width)
  };
  let vertical   = match anchor.vertical {
    alignment::Vertical::Bottom => 0.into(),
    alignment::Vertical::Middle => -offset::Signed::from (size.height).half(),
    alignment::Vertical::Top    => -offset::Signed::from (size.height)
  };
  Offset { horizontal, vertical }
}

/// Special handling of alignment for screen canvases: when size is set, the alignment
/// anchor determines where the offset will be. The default tile alignment is (Top,
/// Left), in which case the offset will be (0,0). For a centered alignment (Middle,
/// Center), the offset will be half of the size width and height. For a (Bottom, Right)
/// alignment, the offset will be negative both width and height.
pub fn tile_offset (size : Size, anchor : Alignment) -> Offset {
  use controller::{alignment, offset, Offset};
  let horizontal = match anchor.horizontal {
    alignment::Horizontal::Left   => 0.into(),
    alignment::Horizontal::Center => -offset::Signed::from (size.width).half(),
    alignment::Horizontal::Right  => -offset::Signed::from (size.width)
  };
  let vertical   = match anchor.vertical {
    alignment::Vertical::Top    => 0.into(),
    alignment::Vertical::Middle => -offset::Signed::from (size.height).half(),
    alignment::Vertical::Bottom => -offset::Signed::from (size.height)
  };
  Offset { horizontal, vertical }
}