conrod 0.36.0

An easy-to-use, extensible 2D GUI library
Documentation
//! Scroll related types and logic.

use {
    Align,
    Backend,
    Point,
    Padding,
    Range,
    Rect,
    Scalar,
    Ui,
};
use std::marker::PhantomData;


/// Arguments given via a scrollable `Widget`'s builder methods for the scrolling along a single
/// axis.
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Scroll {
    maybe_initial_alignment: Option<Align>,
}

/// Scroll state calculated for a single axis.
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct State<A> {
    /// The distance that has been scrolled from the origin.
    ///
    /// A positive offset pushes the scrollable range that is under the kid_area upwards.
    ///
    /// A negative offset pushes the scrollable range that is under the kid_area downwards.
    pub offset: Scalar,
    /// The start and end bounds for the offset along the axis.
    pub offset_bounds: Range,
    /// The total range which may be "offset" from the "root" range (aka the `kid_area`).
    ///
    /// The `scrollable_range` is determined as the bounding range around both the `kid_area` and
    /// all **un-scrolled** **visible** children widgets.
    pub scrollable_range_len: Scalar,
    /// Whether or not the this axis is currently scrolling.
    pub is_scrolling: bool,
    /// The axis type used to instantiate this state.
    axis: PhantomData<A>,
}

/// Methods for distinguishing behaviour between both scroll axes at compile-time.
pub trait Axis {
    /// The range of the given `Rect` that is parallel with this `Axis`.
    fn parallel_range(Rect) -> Range;
    /// The range of the given `Rect` that is perpendicular with this `Axis`.
    fn perpendicular_range(Rect) -> Range;
    /// Given some rectangular `Padding`, return the `Range` that corresponds with this `Axis`.
    fn padding_range(Padding) -> Range;
    /// The coordinate of the given mouse position that corresponds with this `Axis`.
    fn mouse_scalar(mouse_xy: Point) -> Scalar;
    /// A `Scalar` multiplier representing the direction in which positive offset shifts the
    /// `scrollable_range` (either `-1.0` or `1.0).
    fn offset_direction() -> Scalar;
}

/// Behaviour for scrolling across the `X` axis.
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum X {}

/// Behaviour for scrolling across the `Y` axis.
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Y {}

/// State for scrolling along the **X** axis.
pub type StateX = State<X>;

/// State for scrolling along the **Y** axis.
pub type StateY = State<Y>;


impl Scroll {
    /// The default `Scroll` args.
    pub fn new() -> Self {
        Scroll {
            maybe_initial_alignment: None,
        }
    }
}


impl<A> State<A>
    where A: Axis
{

    /// Calculate the new scroll state for the single axis of a `Widget`.
    ///
    /// ```txt
    ///
    ///           >     +---+
    ///           |     |   |
    ///           |   =========================
    ///           |   | | a | scroll root     |
    ///           |   | +---+ aka `kid_area`  |
    ///           |   |            +--------+ |
    ///           |   |            |        | |
    ///           |   =========================
    ///           |                |   b    |
    ///           |                +--------+
    /// scrollable|    +--------+
    ///    range y|    |        |
    ///           |    |        | +------+
    ///           |    |    c   | |      |
    ///           |    +--------+ |  d   |
    ///           |               |      |
    ///           >               +------+
    ///
    ///                ^--------------------^
    ///                     scrollable
    ///                      range x
    ///
    /// ```
    ///
    /// - `kid_area` is the cropped area of the container widget in which kid widgets may be
    /// viewed.
    /// - `a`, `b`, `c` and `d` are widgets that are kids of the "scroll root" widget in their
    /// original, un-scrolled positions.
    /// - `scrollable_range` is the total range occuppied by all children widgets in their
    /// original, un-scrolled positions.
    ///
    /// Everything above and below the set of ==== bars of the parent widget is hidden, i.e:
    ///
    /// ```txt
    ///
    /// =========================
    /// | | a | scroll root     |
    /// | +---+ aka `kid_area`  |
    /// |            +--------+ |
    /// |            |   b    | |
    /// =========================
    ///
    /// ```
    ///
    /// The `scrollable_range` on each axis only becomes scrollable if its length exceeds the
    /// length of the `kid_area` on the same axis. Thus, in the above example, only the *y*
    /// scrollable_range is scrollable.
    ///
    /// The `offset_bounds` are calculated as the amount which the original, un-scrolled,
    /// `scrollable_range` may be offset from its origin.
    ///
    /// ```txt
    ///
    ///   offset +              >
    ///   bounds v              |
    ///   .start >              |   =========================
    ///                         |   |                       |
    ///                         |   |        kid_area       |
    ///                         |   |                       |
    ///                         |   |                       |
    ///          >   scrollable |   =========================
    ///          ^      range y |
    ///          ^              |    
    ///          ^              |    
    ///   offset ^              |    
    ///   bounds ^              |    
    ///     .end ^              |    
    ///          ^              |    
    ///          ^              |    
    ///          +              >    
    ///
    /// ```
    pub fn update<B>(ui: &Ui<B>,
                     idx: super::Index,
                     kid_area: &super::KidArea,
                     maybe_prev_scroll_state: Option<Self>,
                     additional_offset: Scalar) -> Self
        where B: Backend,
    {

        // Retrieve the *current* scroll offset.
        let current_offset = maybe_prev_scroll_state.as_ref()
            .map(|state| state.offset)
            .unwrap_or(0.0);

        // Padding for the range.
        let padding = A::padding_range(kid_area.pad);

        // Get the range for the Axis that concerns this particular scroll `State`.
        let kid_area_range = A::parallel_range(kid_area.rect).pad_ends(padding.start, padding.end);

        // The `kid_area_range` but centred at zero.
        let kid_area_range_origin = Range::from_pos_and_len(0.0, kid_area_range.magnitude());

        // The un-scrolled, scrollable_range relative to the kid_area_range's position.
        let scrollable_range = {
            ui.kids_bounding_box(idx)
                .map(|kids| {
                    A::parallel_range(kids)
                        .shift(-current_offset)
                        .shift(-kid_area_range.middle())
                })
                .unwrap_or_else(|| Range::new(0.0, 0.0))
        };

        // Determine the min and max offst bounds. These bounds are the limits to which the
        // scrollable_range may be shifted in either direction across the range.
        let min_offset = Range::new(scrollable_range.start, kid_area_range_origin.start).magnitude();
        let max_offset = Range::new(scrollable_range.end, kid_area_range_origin.end).magnitude();
        let offset_bounds = Range::new(min_offset, max_offset);

        // The range is only scrollable if it is longer than the padded kid_area_range.
        let is_scrollable = scrollable_range.len() > kid_area_range.len();

        // If the range is scrollable, calculate the new offset by adding the `additional_offset`.
        //
        // The `additional_offset` is given via a `Scroll` event.
        let new_offset_unbounded =
            if is_scrollable { current_offset + additional_offset }
            else             { current_offset };

        // Clamp the new offset to ensure it does not exceed the `offset_bounds`.
        let new_offset = {
            // If there was some previous scroll state, we must also ensure that our new offset does
            // exceed its `offset_bounds` either. We do this in order to avoid causing jitter when
            // scrolling towards either end of the Range.
            let new_offset = maybe_prev_scroll_state.as_ref()
                .map(|prev| prev.offset_bounds.clamp_value(new_offset_unbounded))
                .unwrap_or(new_offset_unbounded);
            offset_bounds.clamp_value(new_offset)
        };

        State {
            offset: new_offset,
            offset_bounds: offset_bounds,
            scrollable_range_len: scrollable_range.len(),
            axis: PhantomData,
            is_scrolling: additional_offset != 0.0,
        }
    }

}


impl Axis for X {

    fn parallel_range(rect: Rect) -> Range {
        rect.x
    }

    fn perpendicular_range(rect: Rect) -> Range {
        rect.y
    }

    fn padding_range(padding: Padding) -> Range {
        padding.x
    }

    fn mouse_scalar(mouse_xy: Point) -> Scalar {
        mouse_xy[0]
    }

    fn offset_direction() -> Scalar {
        1.0
    }

}


impl Axis for Y {

    fn parallel_range(rect: Rect) -> Range {
        rect.y
    }

    fn perpendicular_range(rect: Rect) -> Range {
        rect.x
    }

    fn padding_range(padding: Padding) -> Range {
        padding.y
    }

    fn mouse_scalar(mouse_xy: Point) -> Scalar {
        mouse_xy[1]
    }

    fn offset_direction() -> Scalar {
        -1.0
    }

}