boing 0.7.0

A safe wrapper over libui-ng-sys
Documentation
// SPDX-License-Identifier: MPL-2.0

//! A graphical user interface provided by *libui-ng*.

use std::{ffi::{CStr, CString}, os::raw::c_char, ptr};

use rodeo::bumpalo::Rodeo;

use crate::prelude::*;

impl Ui {
    /// Creates a new `Ui`.
    ///
    /// # Errors
    ///
    /// This function may only be called once. Calling `new` a second time will return
    /// [`Error::AlreadyInitedLibui`](crate::Error::AlreadyInitedLibui).
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use boing::Ui;
    ///
    /// assert!(Ui::new().is_ok());
    /// assert_eq!(Ui::new(), Err(boing::Error::AlreadyInitedLibui));
    /// ```
    pub fn new() -> Result<Self, crate::Error> {
        use std::sync::Once;

        static INIT: Once = Once::new();

        let mut result = Err(crate::Error::AlreadyInitedLibui);
        INIT.call_once(|| unsafe {
            // TODO: Calling `Self::init_unchecked` inside `INIT.call_once` prevents the caller from
            // retrying `Ui::new` if it fails the first time.
            result = Self::init_unchecked();
        });

        result.map(|_| Self { arena: Rodeo::new() })
    }

    /// Initializes *libui-ng* without checking if *libui-ng* has already been initialized.
    ///
    /// # Safety
    ///
    /// *libui-ng* must not already be initialized.
    unsafe fn init_unchecked() -> Result<(), crate::Error> {
        // TODO: What is `uiInitOptions`? What does the `Size` field mean?
        let mut init_options = uiInitOptions { Size: 0 };

        let err_msg = uiInit(ptr::addr_of_mut!(init_options));

        Self::result_from_err_msg(err_msg).map_err(|cause| {
            uiFreeInitError(err_msg);

            crate::Error::LibuiFn {
                name: "uiInit",
                cause: Some(cause),
            }
        })
    }

    fn result_from_err_msg(err_msg: *const c_char) -> Result<(), String> {
        if err_msg.is_null() {
            Ok(())
        } else {
            let err_msg = unsafe { CStr::from_ptr(err_msg) }
                .to_string_lossy()
                .into_owned();

            Err(err_msg)
        }
    }
}

/// A graphical user interface provided by *libui-ng*.
///
/// Access to a `Ui` object is necessary for creating widgets.
///
/// # Examples
///
/// ```no_run
/// # fn main() -> Result<(), boing::Error> {
/// use boing::Ui;
///
/// let ui = Ui::new()?;
///
/// let window = ui.create_window(
///     // Title.
///     "Hello World!",
///     // Width, in px.
///     640u16,
///     // Height, in px.
///     480u16,
///     // Does this window have a menubar?
///     false,
///     // Should this window quit the application when closed?
///     true,
/// )?;
///
/// window.show();
/// ui.run();
/// #
/// # Ok(())
/// # }
/// ```
pub struct Ui {
    // Spooky! Nearly nothing's here! `Ui` serves no functional purpose besides instructing the
    // compiler as to when it is valid for widgets to be created, used, and destroyed. The `arena`
    // here ensures that widgets are destroyed simultaneously, which is necessary to prevent callers
    // from destroying a parent control and then continuing to use its children.
    arena: Rodeo,
}

impl Ui {
    /// Runs *libui-ng*.
    ///
    /// Due to the nature of this method entering the OS' main UI event loop, `run` does not return
    /// instantaneously; rather, it returns only once the user clicks a "Quit" menu item or closes a
    /// window created with `should_quit_on_close = true`.
    ///
    /// It is permissible to call this method multiple times.
    #[inline]
    pub fn run(&self) {
        unsafe { uiMain() };
    }

    /// This function returns whether or not the GUI was exited. In the case it was, it can be
    /// recreated by calling this function or [`main`](Self::main).
    #[inline]
    pub fn step(&self) -> bool {
        unsafe { uiMainStep(0) == 1 }
    }

    /// Allocates an object in the internal `Ui` arena.
    ///
    /// Wrap a value in this method when you need it to live for as long as `Ui`.
    #[inline]
    pub fn alloc_object<'ui, T: 'ui>(&'ui self, value: T) -> &'ui mut T {
        self.arena.alloc(value)
    }

    pub(crate) fn make_cstring<'ui>(
        &'ui self,
        s: impl Into<Vec<u8>>,
    ) -> Result<&'ui CStr, crate::Error> {
        CString::new(s)
            .map_err(crate::Error::ConvertRustString)
            .map(|s| self.alloc_object(s).as_c_str())
    }
}