boing 0.7.0

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

//! Fonts and font pickers.

use std::ptr;

use crate::prelude::*;

#[allow(non_camel_case_types)]
#[cfg(target_os = "windows")]
type c_enum = c_int;
#[allow(non_camel_case_types)]
#[cfg(not(target_os = "windows"))]
type c_enum = c_uint;

impl Ui {
    /// Creates a new [font picker button](Picker).
    pub fn create_font_picker<'ui>(&'ui self) -> Result<&'ui mut Picker, crate::Error> {
        unsafe { call_libui_new_fn!(ui: self, fn: uiNewFontButton() -> Picker) }
    }
}

/// A button that opens a font picker dialog.
#[subcontrol(handle = "uiFontButton")]
pub struct Picker;

/// A system font face.
pub struct Font {
    /// The font family (e.g., serif, sans-serif).
    pub family: String,
    /// The font size, in device units.
    pub size: f64,
    /// The font weight.
    pub weight: Weight,
    /// The font style.
    pub style: Style,
    /// The font "stretch".
    pub stretch: StretchKind,
}

/// A font weight.
pub enum Weight {
    Minimum,
    Thin,
    UltraLight,
    Light,
    Book,
    Normal,
    Medium,
    SemiBold,
    Bold,
    UltraBold,
    Heavy,
    UltraHeavy,
    Maximum,
}

impl Weight {
    /// Attempts to decode a `Weight` from a [`uiTextWeight`].
    fn try_from_libui(value: c_enum) -> Result<Self, ()> {
        match value {
            uiTextWeightMinimum => Ok(Self::Minimum),
            uiTextWeightThin => Ok(Self::Thin),
            uiTextWeightUltraLight => Ok(Self::UltraLight),
            uiTextWeightLight => Ok(Self::Light),
            uiTextWeightBook => Ok(Self::Book),
            uiTextWeightNormal => Ok(Self::Normal),
            uiTextWeightMedium => Ok(Self::Medium),
            uiTextWeightSemiBold => Ok(Self::SemiBold),
            uiTextWeightBold => Ok(Self::Bold),
            uiTextWeightUltraBold => Ok(Self::UltraBold),
            uiTextWeightHeavy => Ok(Self::Heavy),
            uiTextWeightUltraHeavy => Ok(Self::UltraHeavy),
            uiTextWeightMaximum => Ok(Self::Maximum),
            _ => Err(()),
        }
    }
}

/// A font style.
pub enum Style {
    /// The font is upright.
    ///
    /// This corresponds to [`uiTextItalicNormal`].
    Normal,
    /// The font is slanted in a "roman" style.
    ///
    /// This corresponds to [`uiTextItalicOblique`].
    Oblique,
    /// The font is slanted in an italic style.
    ///
    /// This corresponds to [`uiTextItalicItalic`].
    Italic,
}

impl Style {
    /// Attempts to decode a `Style` from a [`uiTextItalic`].
    fn try_from_libui(value: c_enum) -> Result<Self, ()> {
        match value {
            uiTextItalicNormal => Ok(Self::Normal),
            uiTextItalicOblique => Ok(Self::Oblique),
            uiTextItalicItalic => Ok(Self::Italic),
            _ => Err(()),
        }
    }
}

/// The "stretch", or [width class], of a [font](Font).
///
/// Some fonts feature multiple faces of varying width. Condensed faces are narrower than normal,
/// and expanded faces are wider than normal.
///
/// [width class]: https://learn.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass
pub enum StretchKind {
    /// 50% of normal by convention.
    UltraCondensed,
    /// 62.5% of normal by convention.
    ExtraCondensed,
    /// 75% of normal by convention.
    Condensed,
    /// 87.5% of normal by convention.
    SemiCondensed,
    /// The normal font width.
    ///
    /// This is also known as "medium" stretch.
    Normal,
    /// 112.5% of normal by convention.
    SemiExpanded,
    /// 125% of normal by convention.
    Expanded,
    /// 150% of normal by convention.
    ExtraExpanded,
    /// 200% of normal by convention.
    UltraExpanded,
}

impl StretchKind {
    /// Attempts to decode a `StretchKind` from a [`uiTextStretch`].
    fn try_from_libui(value: c_enum) -> Result<Self, ()> {
        match value {
            uiTextStretchUltraCondensed => Ok(Self::UltraCondensed),
            uiTextStretchExtraCondensed => Ok(Self::ExtraCondensed),
            uiTextStretchCondensed => Ok(Self::Condensed),
            uiTextStretchSemiCondensed => Ok(Self::SemiCondensed),
            uiTextStretchNormal => Ok(Self::Normal),
            uiTextStretchSemiExpanded => Ok(Self::SemiExpanded),
            uiTextStretchExtraExpanded => Ok(Self::ExtraExpanded),
            uiTextStretchUltraExpanded => Ok(Self::UltraExpanded),
            _ => Err(()),
        }
    }
}

impl<'ui> Picker<'ui> {
    /// The font currently selected by this picker.
    pub fn selected_font(&self) -> Font {
        // SAFETY: the *libui-ng* documentation does not describe the behavior when this function is
        // called and a font has not yet been selected, and I would rather not guess and invoke UB,
        // so we will initialize `desc` first.
        let mut desc = uiFontDescriptor {
            Family: ptr::null_mut(),
            Size: 0.,
            Weight: uiTextWeightNormal as _,
            Italic: uiTextItalicNormal as _,
            Stretch: uiTextStretchNormal as _,
        };
        unsafe { uiFontButtonFont(self.as_ptr(), ptr::addr_of_mut!(desc)) };
        let call_succeeded = !desc.Family.is_null();

        let family = if call_succeeded {
            unsafe { std::ffi::CStr::from_ptr(desc.Family) }.to_string_lossy().into()
        } else {
            String::new()
        };
        let size = desc.Size;
        let weight = Weight::try_from_libui(desc.Weight as _).unwrap();
        let style = Style::try_from_libui(desc.Italic as _).unwrap();
        let stretch = StretchKind::try_from_libui(desc.Stretch as _).unwrap();

        if call_succeeded {
            // Now that we've copied all the data from `desc`, we can free it.
            unsafe { uiFreeFontButtonFont(ptr::addr_of_mut!(desc)) };
        }

        Font {
            family,
            size,
            weight,
            style,
            stretch,
        }
    }

    ///  Sets a callback for when the user selects a new font.
    ///
    /// This callback is unset by default.
    #[bind_callback(fn = "uiFontButtonOnChanged")]
    pub fn on_changed(&self, f: fn()) {
        f();
    }
}