pix-engine 0.7.0

A cross-platform graphics/UI engine framework for simple games, visualizations, and graphics demos.
Documentation
//! UI widget rendering methods.
//!
//! Provided [`PixState`] methods:
//!
//! - [`PixState::button`]
//! - [`PixState::checkbox`]
//! - [`PixState::radio`]
//!
//! # Example
//!
//! ```
//! # use pix_engine::prelude::*;
//! # struct App { checkbox: bool, radio: usize };
//! # impl PixEngine for App {
//! fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
//!     if s.button("Button")? {
//!         // was clicked
//!     }
//!     s.checkbox("Checkbox", &mut self.checkbox)?;
//!     s.radio("Radio 1", &mut self.radio, 0)?;
//!     s.radio("Radio 2", &mut self.radio, 1)?;
//!     s.radio("Radio 3", &mut self.radio, 2)?;
//!     Ok(())
//! }
//! # }
//! ```

use crate::{gui::Direction, ops::clamp_size, prelude::*};

pub mod field;
pub mod select;
pub mod slider;
pub mod text;
pub mod tooltip;

impl PixState {
    /// Draw a button to the current canvas that returns `true` when clicked.
    ///
    /// # Errors
    ///
    /// If the renderer fails to draw to the current render target, then an error is returned.
    ///
    /// # Example
    ///
    /// ```
    /// # use pix_engine::prelude::*;
    /// # struct App;
    /// # impl PixEngine for App {
    /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
    ///     if s.button("Button")? {
    ///         // was clicked
    ///     }
    ///     Ok(())
    /// }
    /// # }
    /// ```
    pub fn button<L>(&mut self, label: L) -> PixResult<bool>
    where
        L: AsRef<str>,
    {
        let label = label.as_ref();

        let s = self;
        let id = s.ui.get_id(&label);
        let label = s.ui.get_label(label);
        let pos = s.cursor_pos();
        let fpad = s.theme.spacing.frame_pad;

        // Calculate button size
        let (label_width, label_height) = s.text_size(label)?;
        let width = s.ui.next_width.take().unwrap_or(label_width);
        let button = rect![pos, width, label_height].offset_size(2 * fpad);

        // Check hover/active/keyboard focus
        let hovered = s.focused() && s.ui.try_hover(id, &button);
        if s.focused() {
            s.ui.try_focus(id);
        }
        let disabled = s.ui.disabled;
        let active = s.ui.is_active(id);

        s.push();
        s.ui.push_cursor();

        // Render
        s.rect_mode(RectMode::Corner);
        if hovered {
            s.frame_cursor(&Cursor::hand())?;
        }
        let [stroke, bg, fg] = s.widget_colors(id, ColorType::Primary);
        s.stroke(stroke);
        s.fill(bg);
        if active {
            s.rect(button.offset([1, 1]))?;
        } else {
            s.rect(button)?;
        }

        // Button text
        s.rect_mode(RectMode::Center);
        s.clip(button)?;
        s.set_cursor_pos(button.center());
        s.stroke(None);
        s.fill(fg);
        s.text(label)?;
        s.clip(None)?;

        s.ui.pop_cursor();
        s.pop();

        // Process input
        s.ui.handle_focus(id);
        s.advance_cursor(button.size());
        Ok(!disabled && s.ui.was_clicked(id))
    }

    /// Draw a text link to the current canvas that returns `true` when clicked.
    ///
    /// # Errors
    ///
    /// If the renderer fails to draw to the current render target, then an error is returned.
    ///
    /// # Example
    ///
    /// ```
    /// # use pix_engine::prelude::*;
    /// # struct App;
    /// # impl PixEngine for App {
    /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
    ///     if s.link("Link")? {
    ///         // was clicked
    ///     }
    ///     Ok(())
    /// }
    /// # }
    /// ```
    pub fn link<S>(&mut self, text: S) -> PixResult<bool>
    where
        S: AsRef<str>,
    {
        let text = text.as_ref();

        let s = self;
        let id = s.ui.get_id(&text);
        let text = s.ui.get_label(text);
        let pos = s.cursor_pos();
        let pad = s.theme.spacing.item_pad;

        // Calculate button size
        let (width, height) = s.text_size(text)?;
        let bounding_box = rect![pos, width, height].grow(pad / 2);

        // Check hover/active/keyboard focus
        let hovered = s.focused() && s.ui.try_hover(id, &bounding_box);
        let focused = s.focused() && s.ui.try_focus(id);
        let disabled = s.ui.disabled;
        let active = s.ui.is_active(id);

        s.push();

        // Render
        if hovered {
            s.frame_cursor(&Cursor::hand())?;
        }
        let [stroke, bg, fg] = s.widget_colors(id, ColorType::Primary);
        if focused {
            s.stroke(stroke);
            s.fill(None);
            s.rect(bounding_box)?;
        }

        // Button text
        s.stroke(None);
        if active {
            s.fill(fg.blended(bg, 0.04));
        } else {
            s.fill(bg);
        }
        s.text(text)?;

        s.pop();

        // Process input
        s.ui.handle_focus(id);
        Ok(!disabled && s.ui.was_clicked(id))
    }

    /// Draw a checkbox to the current canvas.
    ///
    /// # Errors
    ///
    /// If the renderer fails to draw to the current render target, then an error is returned.
    ///
    /// # Example
    ///
    /// ```
    /// # use pix_engine::prelude::*;
    /// # struct App { checkbox: bool };
    /// # impl PixEngine for App {
    /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
    ///     s.checkbox("Checkbox", &mut self.checkbox)?;
    ///     Ok(())
    /// }
    /// # }
    /// ```
    pub fn checkbox<S>(&mut self, label: S, checked: &mut bool) -> PixResult<bool>
    where
        S: AsRef<str>,
    {
        let label = label.as_ref();

        let s = self;
        let id = s.ui.get_id(&label);
        let label = s.ui.get_label(label);
        let pos = s.cursor_pos();
        let (_, checkbox_size) = s.text_size(label)?;

        // Calculate checkbox rect
        let checkbox = square![pos, checkbox_size];

        // Check hover/active/keyboard focus
        let hovered = s.focused() && s.ui.try_hover(id, &checkbox);
        if s.focused() {
            s.ui.try_focus(id);
        }
        let disabled = s.ui.disabled;

        s.push();

        // Checkbox
        s.rect_mode(RectMode::Corner);
        if hovered {
            s.frame_cursor(&Cursor::hand())?;
        }
        let [stroke, bg, fg] = if *checked {
            s.widget_colors(id, ColorType::Primary)
        } else {
            s.widget_colors(id, ColorType::Background)
        };
        s.stroke(stroke);
        s.fill(bg);
        s.rect(checkbox)?;

        if *checked {
            s.stroke(fg);
            s.stroke_weight(2);
            let half = checkbox_size / 2;
            let third = checkbox_size / 3;
            let x = checkbox.left() + half - 1;
            let y = checkbox.bottom() - third + 1;
            let start = point![x - third + 2, y - third + 2];
            let mid = point![x, y];
            let end = point![x + third + 1, y - half + 2];
            s.line([start, mid])?;
            s.line([mid, end])?;
        }
        s.advance_cursor(checkbox.size());
        s.pop();

        // Label
        s.same_line(None);
        s.text(label)?;

        // Process input
        s.ui.handle_focus(id);
        if disabled {
            Ok(false)
        } else {
            let clicked = s.ui.was_clicked(id);
            if clicked {
                *checked = !(*checked);
            }
            Ok(clicked)
        }
    }

    /// Draw a set of radio buttons to the current canvas.
    ///
    /// # Errors
    ///
    /// If the renderer fails to draw to the current render target, then an error is returned.
    ///
    /// # Example
    ///
    /// ```
    /// # use pix_engine::prelude::*;
    /// # struct App { radio: usize };
    /// # impl PixEngine for App {
    /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
    ///     s.radio("Radio 1", &mut self.radio, 0)?;
    ///     s.radio("Radio 2", &mut self.radio, 1)?;
    ///     s.radio("Radio 3", &mut self.radio, 2)?;
    ///     Ok(())
    /// }
    /// # }
    /// ```
    pub fn radio<S>(&mut self, label: S, selected: &mut usize, index: usize) -> PixResult<bool>
    where
        S: AsRef<str>,
    {
        let label = label.as_ref();

        let s = self;
        let id = s.ui.get_id(&label);
        let label = s.ui.get_label(label);
        let pos = s.cursor_pos();
        let (_, label_height) = s.text_size(label)?;
        let radio_size = label_height / 2;

        // Calculate radio rect
        let radio = circle![pos + radio_size, radio_size];

        // Check hover/active/keyboard focus
        let hovered = s.focused() && s.ui.try_hover(id, &radio);
        if s.focused() {
            s.ui.try_focus(id);
        }
        let disabled = s.ui.disabled;

        s.push();

        // Radio
        s.rect_mode(RectMode::Corner);
        s.ellipse_mode(EllipseMode::Center);
        if hovered {
            s.frame_cursor(&Cursor::hand())?;
        }
        let is_selected = *selected == index;
        let [stroke, bg, _] = if is_selected {
            s.widget_colors(id, ColorType::Primary)
        } else {
            s.widget_colors(id, ColorType::Background)
        };
        if is_selected {
            s.stroke(bg);
            s.fill(None);
        } else {
            s.stroke(stroke);
            s.fill(bg);
        }
        s.circle(radio)?;

        if is_selected {
            s.stroke(bg);
            s.fill(bg);
            s.circle([radio.x(), radio.y(), radio.radius() - 3])?;
        }
        s.advance_cursor(radio.bounding_rect().size());
        s.pop();

        // Label
        s.same_line(None);
        s.text(label)?;

        // Process input
        s.ui.handle_focus(id);
        if disabled {
            Ok(false)
        } else {
            let clicked = s.ui.was_clicked(id);
            if clicked {
                *selected = index;
            }
            Ok(clicked)
        }
    }

    /// Render an arrow aligned with the current font size.
    ///
    /// # Errors
    ///
    /// If the renderer fails to draw to the current render target, then an error is returned.
    pub fn arrow<P, S>(&mut self, pos: P, direction: Direction, scale: S) -> PixResult<()>
    where
        P: Into<Point<i32>>,
        S: Into<f64>,
    {
        let pos: Point<f64> = pos.into().as_();
        let scale = scale.into();

        let s = self;
        let font_size = clamp_size(s.theme.font_size);

        let height = f64::from(font_size);
        let mut ratio = height * 0.4 * scale;
        let center = pos + point![height * 0.5, height * 0.5 * scale];

        if let Direction::Up | Direction::Left = direction {
            ratio = -ratio;
        }
        let (p1, p2, p3) = match direction {
            Direction::Up | Direction::Down => (
                point![0.0, 0.75],
                point![-0.866, -0.75],
                point![0.866, -0.75],
            ),
            Direction::Left | Direction::Right => (
                point![0.75, 0.0],
                point![-0.75, 0.866],
                point![-0.75, -0.866],
            ),
        };

        s.triangle([
            (center + p1 * ratio).round().as_(),
            (center + p2 * ratio).round().as_(),
            (center + p3 * ratio).round().as_(),
        ])?;

        Ok(())
    }
}