retroglyph-ui 0.5.1

Immediate-mode terminal UI toolkit for retroglyph: widgets, layout, input/focus, theming, and animation
Documentation
//! [`Sense`]: what a widget wants [`Interaction::interact`](crate::interact::Interaction::interact)
//! to compute on its behalf.

use core::ops::{BitOr, BitOrAssign};

/// Which of a [`Response`](crate::interact::Response)'s fields
/// [`Interaction::interact`](crate::interact::Interaction::interact) should actually
/// populate for a given widget call.
///
/// A manual bitflag over `u8`: mirrors
/// [`KeyModifiers`](retroglyph_core::event::KeyModifiers)'s shape rather than
/// pulling in the `bitflags` crate for a handful of bits. Combine raw flags
/// with `|` (`Sense::HOVER | Sense::FOCUSABLE`), or reach for one of the
/// named constructors ([`click`](Self::click), [`drag`](Self::drag),
/// [`hover`](Self::hover), [`scroll`](Self::scroll)) for the common cases.
/// [`DISABLED`](Self::DISABLED) is the exception: it's a modifier over an
/// existing sense, not a capability of its own.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Sense(u8);

impl Sense {
    /// Register the widget's rect for hit-testing and report
    /// [`Response::hovered`](crate::interact::Response::hovered).
    pub const HOVER: Self = Self(1 << 0);
    /// Report [`Response::pressed`](crate::interact::Response::pressed),
    /// [`Response::released`](crate::interact::Response::released), and
    /// [`Response::clicked`](crate::interact::Response::clicked).
    pub const CLICK: Self = Self(1 << 1);
    /// Report [`Response::dragging`](crate::interact::Response::dragging) once the
    /// pointer moves past the drag threshold while pressed on this widget.
    pub const DRAG: Self = Self(1 << 2);
    /// Register the widget in the [`FocusRing`](crate::interact::FocusRing)'s
    /// Tab/Shift+Tab order and report
    /// [`Response::focused`](crate::interact::Response::focused). Combined with
    /// [`CLICK`](Self::CLICK), Enter/Space also activate the widget while
    /// it's focused: terminals are frequently mouse-less.
    pub const FOCUSABLE: Self = Self(1 << 3);
    /// Report [`Response::scroll_delta`](crate::interact::Response::scroll_delta)
    /// whenever the pointer is within this widget's rect. Unlike the other
    /// pointer senses, this is *not* limited to the single
    /// topmost widget under the pointer: see [`Interaction::interact`](crate::interact::Interaction::interact)'s
    /// doc comment on `scroll_delta` for why.
    pub const SCROLL: Self = Self(1 << 4);
    /// Report [`Response::secondary_clicked`](crate::interact::Response::secondary_clicked):
    /// the secondary (right) button pressed and released on this widget
    /// while still hovered. Independent of [`CLICK`](Self::CLICK): combine
    /// them (`Sense::click() | Sense::SECONDARY_CLICK`) for a widget that
    /// wants both a primary action and a secondary one (e.g. a context
    /// menu). Unlike [`CLICK`](Self::CLICK)/[`DRAG`](Self::DRAG), there's no
    /// drag-threshold suppression for the secondary button: a
    /// press-and-release on the same widget always counts, since
    /// secondary-button drags aren't a gesture this module resolves.
    pub const SECONDARY_CLICK: Self = Self(1 << 5);
    /// Keeps this call's hit-testing (so [`Response::hovered`](crate::interact::Response::hovered)
    /// still works, most of the value of showing a disabled control at
    /// all) but suppresses everything else this sense would otherwise
    /// register or report: no [`FocusRing`](crate::interact::FocusRing) registration,
    /// and [`Response::pressed`](crate::interact::Response::pressed),
    /// [`released`](crate::interact::Response::released), [`clicked`](crate::interact::Response::clicked),
    /// [`held`](crate::interact::Response::held), [`dragging`](crate::interact::Response::dragging),
    /// [`focused`](crate::interact::Response::focused), and
    /// [`secondary_clicked`](crate::interact::Response::secondary_clicked) all report
    /// `false`, even if the gesture would otherwise satisfy them.
    /// [`SCROLL`](Self::SCROLL) is suppressed too: a disabled row's own
    /// [`Sense::SCROLL`] never fires, though this doesn't stop an enclosing
    /// scrollable list (a separate widget) from scrolling past it.
    ///
    /// A modifier, not a capability of its own: combine it with an existing
    /// sense rather than using it alone, e.g. `Sense::click() |
    /// Sense::DISABLED`. See [`disabled_if`](Self::disabled_if) for the
    /// common call-site shape.
    pub const DISABLED: Self = Self(1 << 6);
    /// Senses nothing: [`interact`](crate::interact::Interaction::interact) still
    /// registers the id nowhere and every [`Response`](crate::interact::Response) field reports as if
    /// nothing happened, matching [`Response::default`](crate::interact::Response), with one exception:
    /// [`rect`](crate::interact::Response::rect) always echoes back the area passed to `interact`, useful
    /// for purely decorative widgets that still want layout echo.
    pub const NONE: Self = Self(0);

    /// A clickable, hoverable, focusable widget: buttons, tabs, list
    /// rows. Equivalent to `HOVER | CLICK | FOCUSABLE`.
    #[must_use]
    pub const fn click() -> Self {
        Self(Self::HOVER.0 | Self::CLICK.0 | Self::FOCUSABLE.0)
    }

    /// A draggable widget, e.g. a slider or scrollbar thumb. Equivalent to
    /// <code>[click](Self::click) | DRAG</code>.
    #[must_use]
    pub const fn drag() -> Self {
        Self(Self::click().0 | Self::DRAG.0)
    }

    /// A hover-only widget with no click or focus behavior, e.g. a tooltip
    /// trigger. Equivalent to `HOVER`.
    #[must_use]
    pub const fn hover() -> Self {
        Self::HOVER
    }

    /// A scrollable region, e.g. a list or log panel. Equivalent to
    /// `HOVER | SCROLL`.
    #[must_use]
    pub const fn scroll() -> Self {
        Self(Self::HOVER.0 | Self::SCROLL.0)
    }

    /// A widget with a secondary (right-click) action but no primary click,
    /// e.g. a context-menu-only trigger. Equivalent to `HOVER | SECONDARY_CLICK`.
    /// Combine with [`click`](Self::click) (`Sense::click() | Sense::SECONDARY_CLICK`)
    /// for a widget with both a primary and a secondary action.
    #[must_use]
    pub const fn secondary_click() -> Self {
        Self(Self::HOVER.0 | Self::SECONDARY_CLICK.0)
    }

    /// `self` with [`DISABLED`](Self::DISABLED) set if `disabled` is
    /// `true`, unchanged otherwise: the common call-site shape,
    /// `Sense::click().disabled_if(!save_available)`, replacing what would
    /// otherwise be a branch between two `Sense` literals.
    #[must_use]
    pub const fn disabled_if(self, disabled: bool) -> Self {
        if disabled {
            Self(self.0 | Self::DISABLED.0)
        } else {
            self
        }
    }

    /// `true` if every bit set in `other` is also set in `self`.
    #[must_use]
    pub const fn contains(self, other: Self) -> bool {
        (self.0 & other.0) == other.0
    }

    /// `true` if [`DISABLED`](Self::DISABLED) is set.
    #[must_use]
    pub const fn is_disabled(self) -> bool {
        self.contains(Self::DISABLED)
    }

    /// `true` if this sense wants pointer hit-testing at all ([`HOVER`](Self::HOVER),
    /// [`CLICK`](Self::CLICK), [`DRAG`](Self::DRAG), [`SCROLL`](Self::SCROLL),
    /// or [`SECONDARY_CLICK`](Self::SECONDARY_CLICK)).
    #[must_use]
    pub const fn wants_pointer(self) -> bool {
        self.0
            & (Self::HOVER.0
                | Self::CLICK.0
                | Self::DRAG.0
                | Self::SCROLL.0
                | Self::SECONDARY_CLICK.0)
            != 0
    }
}

impl BitOr for Sense {
    type Output = Self;

    fn bitor(self, rhs: Self) -> Self {
        Self(self.0 | rhs.0)
    }
}

impl BitOrAssign for Sense {
    fn bitor_assign(&mut self, rhs: Self) {
        self.0 |= rhs.0;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn contains_checks_all_bits() {
        let s = Sense::HOVER | Sense::FOCUSABLE;
        assert!(s.contains(Sense::HOVER));
        assert!(s.contains(Sense::FOCUSABLE));
        assert!(!s.contains(Sense::CLICK));
        assert!(s.contains(Sense::NONE)); // vacuously true
    }

    #[test]
    fn constructors_match_their_documented_bit_combinations() {
        assert_eq!(
            Sense::click(),
            Sense::HOVER | Sense::CLICK | Sense::FOCUSABLE
        );
        assert_eq!(Sense::drag(), Sense::click() | Sense::DRAG);
        assert_eq!(Sense::hover(), Sense::HOVER);
        assert_eq!(Sense::scroll(), Sense::HOVER | Sense::SCROLL);
    }

    #[test]
    fn wants_pointer_ignores_focusable() {
        assert!(!Sense::FOCUSABLE.wants_pointer());
        assert!(Sense::HOVER.wants_pointer());
        assert!(Sense::CLICK.wants_pointer());
        assert!(Sense::DRAG.wants_pointer());
        assert!(Sense::SCROLL.wants_pointer());
        assert!(!Sense::NONE.wants_pointer());
    }

    #[test]
    fn default_is_none() {
        assert_eq!(Sense::default(), Sense::NONE);
    }

    #[test]
    fn disabled_if_sets_the_bit_only_when_true() {
        assert_eq!(
            Sense::click().disabled_if(true),
            Sense::click() | Sense::DISABLED
        );
        assert_eq!(Sense::click().disabled_if(false), Sense::click());
    }

    #[test]
    fn is_disabled_reads_the_bit() {
        assert!(!Sense::click().is_disabled());
        assert!((Sense::click() | Sense::DISABLED).is_disabled());
    }
}