gooey-rs 0.12.1

Tile-based UI library with audio support
Documentation
//! Rectangular window consisting of titlebar and contents.
//!
//! ```text
//! +---------------------------+
//! |         TITLEBAR          |
//! +---------------------------+
//! |                           |
//! |                           |
//! |                           |
//! |                           |
//! |                           |
//! |                           |
//! |                           |
//! |                           |
//! +---------------------------+
//! ```

use crate::prelude::*;

pub use self::builder::WindowBuilder as Builder;

/// Create a new window in the middle of the parent element and focus the container.
///
/// Returns [container, titlebar, contents] node IDs
// TODO: builder pattern ?
pub fn popup <A, P> (
  interface        : &mut Interface <A, P>,
  parent_id        : NodeId,
  title            : String,
  size             : Size,
  // add the ActivatePointer binding to Mouse1
  activate_pointer : bool,
  appearances      : Option <Appearances>,
  border           : Option <Border>
) -> [NodeId; 3] where
  A : Application,
  P : Presentation
{
  let layout = layout::Free::middle_center (size);
  let mut builder = Builder::<A, P>::new (interface, &parent_id, title).layout (layout);
  set_option!(builder, title_appearances, appearances.clone());
  set_option!(builder, frame_appearances, appearances);
  set_option!(builder, border, border);
  let [container_id, titlebar_id, contents_id] = builder.build();
  let _ = interface.action (&container_id, Action::Focus);
  if activate_pointer {
    let bind_pointer = Box::new (|controller : &mut Controller|
      controller.add_bindings (&bindings::Builder::<A>::new()
        .buttons (vec![(
          controls::button::Builtin::ActivatePointer.into(),
          input::button::Mouse::Mouse1.into()
        ).into()])
        .build()
      )
    );
    let _ = interface.action (&contents_id, Action::ModifyController (bind_pointer));
  }
  [container_id, titlebar_id, contents_id]
}

/// Pop-up a window containing a menu of buttons
// TODO: builder pattern ?
#[expect(clippy::too_many_arguments)]
pub fn popup_dialog <A, P> (
  interface        : &mut Interface <A, P>,
  parent_id        : NodeId,
  title            : String,
  size             : Size,
  // (label, callback) pairs
  buttons          : Vec <(String, application::CallbackId)>,
  // contain bindings for buttons and menu
  bindings         : &Bindings <A>,
  activate_pointer : bool,
  appearances      : Option <Appearances>,
  border           : Option <Border>
) -> [NodeId; 3] where
  A : Application + 'static,
  P : Presentation
{
  debug_assert!(!buttons.is_empty());
  let [container_id, titlebar_id, contents_id] =
    popup (interface, parent_id, title, size, activate_pointer, appearances, border);
  let switch_off = {
    let trigger = switch::Trigger::new::<A> (
      controls::button::Builtin::FormSubmitCallback.into(), None);
    switch::Builder::default()
      .style_fg ((switch::State::Off, State::Enabled), Color::WHITE)
      .style_bg ((switch::State::Off, State::Enabled), Color::RED)
      .style_fg ((switch::State::On, State::Enabled), Color::RED)
      .style_bg ((switch::State::On, State::Enabled), Color::WHITE)
      .style_fg ((switch::State::Off, State::Focused), Color::RED)
      .style_bg ((switch::State::Off, State::Focused), Color::WHITE)
      .style_fg ((switch::State::On, State::Focused), Color::WHITE)
      .style_bg ((switch::State::On, State::Focused), Color::RED)
      .enter (switch::State::On, trigger)
  };
  for (label, callback_id) in buttons {
    let size = Size {
      width:  ((label.len() as u32) + 2).into(),
      height: 3.into()
    };
    let layout  = layout::Variant::from (layout::Free::tile (size));
    let actions = button::Builder::<A>::new (interface.elements(), &contents_id)
      .bindings (bindings)
      .callback_id (callback_id)
      .switch (switch_off.clone().label (label).build())
      .frame_border (Border::eascii_lines())
      .frame_layout (layout.into())
      .build_actions();
    let _ = interface.actions (actions);
  }
  { // arrange buttons
    let anchor = Alignment {
      horizontal: alignment::Horizontal::Center,
      vertical:   alignment::Vertical::Bottom
    };
    let actions = frame::free::rearrange_absolute (
      interface.elements(), &contents_id, anchor, (0, 1),
      Orientation::Horizontal, 1, false);
    let _ = interface.actions (actions);
  }
  { // menu widget
    let selection = Selection { loop_: true, .. Selection::default() };
    let actions   = menu::Builder::<A>::new (interface.elements(), &container_id)
      .bindings (bindings)
      .selection (selection)
      .build_actions();
    let _ = interface.actions (actions);
  }
  let _ = interface.action (&container_id, Action::Focus);
  [container_id, titlebar_id, contents_id]
}

/// Utility function to set the textbox contents to the given string
pub fn set_title (
  elements      : &Tree <Element>,
  window_id     : &NodeId,
  title         : String,
  action_buffer : &mut Vec <(NodeId, Action)>
) {
   let titlebar_id = elements.children_ids (window_id).unwrap().next().unwrap();
   let textbox_id  = elements.children_ids (titlebar_id).unwrap().next().unwrap();
   textbox::set_contents (elements, textbox_id, title, action_buffer);
}

mod builder {
  use derive_builder::Builder;
  use crate::prelude::*;

  #[derive(Builder)]
  #[builder(public, pattern="owned", build_fn(private, name="build_"),
    setter(strip_option))]
  struct Window <'a, A : Application, P : Presentation> {
    interface    : &'a mut Interface <A, P>,
    parent_id    : &'a NodeId,
    title        : String,
    #[builder(default)]
    title_appearances : Appearances,
    #[builder(default)]
    bindings          : Option <&'a Bindings <A>>,
    #[builder(default)]
    border            : Option <Border>,
    #[builder(default)]
    frame_appearances : Appearances,
    #[builder(default)]
    layout            : layout::Free,
    #[builder(default)]
    name              : Option <String>,
    #[builder(default)]
    orientation       : (Orientation, Area)
  }

  impl <'a, A : Application, P : Presentation> WindowBuilder <'a, A, P> {
    pub const fn new (
      interface : &'a mut Interface <A, P>,
      parent_id : &'a NodeId,
      title     : String
    ) -> Self {
      WindowBuilder {
        interface:         Some (interface),
        parent_id:         Some (parent_id),
        title:             Some (title),
        title_appearances: None,
        bindings:          None,
        border:            None,
        frame_appearances: None,
        layout:            None,
        name:              None,
        orientation:       None
      }
    }

    /// Returns [container, titlebar, contents] node IDs
    pub fn build (self) -> [NodeId; 3] {
      let Window {
        interface, parent_id, title, title_appearances, bindings, border,
        frame_appearances, layout, name, orientation
      } = self.build_()
        .map_err(|err| log::error!("window builder error: {err:?}"))
        .unwrap();
      let container_id = {
        let mut builder = frame::free::Builder::<A>::new (
          interface.elements(), parent_id
        ) .appearances (AppearancesBuilder::transparent().build())
          .layout (layout)
          .orientation ((Orientation::Vertical, Area::Interior));
        set_option!(builder, name, name);
        let frame = builder.build_element();
        interface.create_singleton (parent_id, frame, CreateOrder::Append)
      };
      let titlebar_id = {
        let scroll = Scroll {
          alignment: Alignment::middle_center(),
          .. Scroll::default_tile_absolute()
        };
        let layout = layout::Variant::from (
          layout::Tiled::Absolute (std::num::NonZeroU32::new (3).unwrap()));
        let mut builder = textbox::Builder::<A>::new (
          interface.elements(), &container_id
        ) .appearances (title_appearances)
          .frame_appearances (frame_appearances.clone())
          .frame_layout (layout.into())
          .scroll (scroll)
          .text (title);
        set_option!(builder, frame_border, border.clone());
        build_and_return_node_id!(interface, builder)
      };
      let _ = interface.action (&titlebar_id, Action::Disable);
      let contents_id = {
        let mut builder = frame::tiled::Builder::<A>::new (
          interface.elements(), &container_id
        ) .appearances (frame_appearances)
          .orientation (orientation);
        set_option!(builder, bindings, bindings);
        set_option!(builder, border, border);
        build_and_return_node_id!(interface, builder)
      };
      [container_id, titlebar_id, contents_id]
    }
  }
}