boing 0.7.0

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

//! An application window.

use std::ptr;

use crate::prelude::*;

impl Ui {
    /// Creates a new [`Window`].
    pub fn create_window(
        &self,
        title: impl Into<Vec<u8>>,
        width: impl Into<NonNegativeInt>,
        height: impl Into<NonNegativeInt>,
        has_menubar: bool,
        should_quit_on_close: bool,
    ) -> Result<&mut Window, crate::Error> {
        let title = self.make_cstring(title)?;
        let window = unsafe {
            call_libui_new_fn!(
                ui: self,
                fn: uiNewWindow(
                    title.as_ptr(),
                    width.into().to_libui(),
                    height.into().to_libui(),
                    has_menubar.into(),
                ) -> Window,
            )?
        };

        if should_quit_on_close {
            unsafe extern "C" fn on_closing(_: *mut uiWindow, _: *mut c_void) -> c_int {
                // When the window receives an event to close, call `uiQuit`.
                uiQuit();

                // Returning `true` tells *libui-ng* to destroy the window, which isn't necessary in
                // this case as `uiQuit` accomplishes that.
                false.into()
            }

            unsafe extern "C" fn on_should_quit(_: *mut c_void) -> c_int {
                true.into()
            }

            unsafe {
                let window = window.as_ptr();
                uiWindowOnClosing(window, Some(on_closing), ptr::null_mut());
                uiOnShouldQuit(Some(on_should_quit), ptr::null_mut());
            }
        } else {
            unsafe extern "C" fn on_closing(window: *mut uiWindow, _: *mut c_void) -> c_int {
                // SAFETY: we can't return `true` here, as that would cause the window to be
                // destroyed, invalidating the window's inner pointer and allowing use-after-frees
                // to occur. Instead, we will simply hide the window. This is kind of a silly
                // solution, but it works.
                //
                // FIXME: this appears visually equivalent to closing the window on Windows. We
                // should test macOS and Linux as well to confirm that the window doesn't instantly
                // disappear rather than display a closing animation on those platforms.

                uiControlHide(window.cast());

                false.into()
            }

            unsafe {
                uiWindowOnClosing(window.as_ptr(), Some(on_closing), ptr::null_mut());
            }
        }

        Ok(window)
    }
}

/// An application window.
#[subcontrol(handle = "uiWindow")]
pub struct Window;

impl<'ui> Window<'ui> {
    /// The title of this window.
    #[bind_text(fn = "uiWindowTitle")]
    pub fn title(&self) -> _;

    /// Sets the title of the window.
    ///
    /// This overrides the title supplied to [`Ui::create_window`].
    pub fn set_title(&self, title: impl Into<Vec<u8>>) -> Result<(), crate::Error> {
        let title = self.ui.make_cstring(title)?;
        unsafe { uiWindowSetTitle(self.as_ptr(), title.as_ptr()) };

        Ok(())
    }

    /// Sets a callback for when the content size of this window changes.
    ///
    /// This callback is unset by default.
    #[bind_callback(fn = "uiWindowOnContentSizeChanged")]
    pub fn on_content_size_changed(&self, f: fn()) {
        f();
    }

    /// Sets a callback for when this window is requested to close.
    ///
    /// This overrides the default behavior of the `should_quit_on_close` argument passed to
    /// [`Ui::create_window`]. As such, if you still want the window to quit or hide after your
    /// callback is executed, you must manually call [`uiQuit`] or [`uiControlHide`] in your
    /// callback.
    #[bind_callback(fn = "uiWindowOnClosing", return = "c_int")]
    pub fn on_closing(&self, f: fn()) {
        f();

        // SAFETY: here, we return a value to [`uiWindowOnClosing`] (but not the outer function)
        // that indicates if *libui-ng* should destroy the window when the callback exits. This is
        // never desirable, so we will return `false`.
        break c_int::from(false);
    }

    /// Sets a callback for when this window changes focus.
    ///
    /// This callback is unset by default.
    #[bind_callback(fn = "uiWindowOnFocusChanged")]
    pub fn on_focus_changed(&self, f: fn()) {
        f();
    }

    /// Determines if this window is fullscreen.
    ///
    /// Windows are not fullscreen by default.
    #[inline]
    pub fn is_fullscreen(&self) -> bool {
        bool_from_libui(unsafe { uiWindowFullscreen(self.as_ptr()) })
    }

    /// Sets whether or not this window is fullscreen.
    #[inline]
    pub fn set_fullscreen(&self, value: bool) {
        unsafe { uiWindowSetFullscreen(self.as_ptr(), value.into()) };
    }

    /// Determines if this window is borderless.
    #[inline]
    pub fn is_borderless(&self) -> bool {
        bool_from_libui(unsafe { uiWindowBorderless(self.as_ptr()) })
    }

    /// Sets whether or not this window is borderless.
    #[inline]
    pub fn set_borderless(&self, value: bool) {
        unsafe { uiWindowSetBorderless(self.as_ptr(), value.into()) };
    }

    /// Sets the child control of this window.
    ///
    /// This is unset by default.
    #[bind_set_child(fn = "uiWindowSetChild")]
    pub fn set_child(&self, ...) -> _;

    /// Determines if this window has margins.
    #[inline]
    pub fn is_margined(&self) -> bool {
        bool_from_libui(unsafe { uiWindowMargined(self.as_ptr()) })
    }

    /// Sets whether or not this window has margins.
    #[inline]
    pub fn set_margined(&self, value: bool) {
        unsafe { uiWindowSetMargined(self.as_ptr(), value.into()) };
    }

    /// Determines if this window is resizable.
    ///
    /// Windows are resizable by default.
    #[inline]
    pub fn is_resizeable(&self) -> bool {
        bool_from_libui(unsafe { uiWindowResizeable(self.as_ptr()) })
    }

    /// Sets whether or not this window is resizable.
    #[inline]
    pub fn set_resizeable(&self, value: bool) {
        unsafe { uiWindowSetResizeable(self.as_ptr(), value.into()) };
    }

    /// Sets the inner size of this window.
    pub fn set_content_size(
        &self,
        width: impl Into<NonNegativeInt>,
        height: impl Into<NonNegativeInt>,
    ) {
        unsafe {
            uiWindowSetContentSize(
                self.as_ptr(),
                width.into().to_libui(),
                height.into().to_libui(),
            )
        };
    }

    /// The inner size of this window.
    pub fn content_size(&self) -> (NonNegativeInt, NonNegativeInt) {
        let (mut width, mut height) = (0, 0);
        unsafe {
            uiWindowContentSize(
                self.as_ptr(),
                ptr::addr_of_mut!(width),
                ptr::addr_of_mut!(height),
            );
        }

        unsafe { (NonNegativeInt::from_libui(width), NonNegativeInt::from_libui(height)) }
    }
}

macro_rules! impl_present_fn {
    (
        $self_fn:ident,
        $libui_fn:ident $(,)?
    ) => {
        impl Window<'_> {
            pub fn $self_fn(
                &self,
                title: impl Into<Vec<u8>>,
                desc: impl Into<Vec<u8>>,
            ) -> Result<(), crate::Error> {
                let title = self.ui.make_cstring(title)?;
                let desc = self.ui.make_cstring(desc)?;
                unsafe { $libui_fn(self.as_ptr(), title.as_ptr(), desc.as_ptr()) };

                Ok(())
            }
        }
    };
}

impl_present_fn!(present_alert, uiMsgBox);
impl_present_fn!(present_error, uiMsgBoxError);