gled-ui 2.34.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! Moving a canvas's view: the middle button and the scrollbars along its
//! edges.

use crate::canvas::scene_modification::{SceneModded, pan_bounds};
use crate::canvas::view::SceneView;
use egui::{PointerButton, Rangef, Rect, Sense, Ui, Vec2, pos2};

/// Whether the middle button is panning the view. While it is, the canvas
/// hands no drag to the fixtures. A touchpad has no middle button and goes on
/// panning by scrolling.
pub(crate) fn panning(ui: &Ui) -> bool {
    ui.input(|i| i.pointer.button_down(PointerButton::Middle))
}

/// Moves the view by a drag of `by` scene units. The rig follows the pointer,
/// so the view travels against it.
pub(crate) fn pan_view(view: &mut SceneView, by: Vec2) {
    view.center -= by;
}

/// How wide the strip taken off the canvas for a scrollbar is.
pub(crate) const SCROLLBAR_WIDTH: f32 = 10.0;

/// The two bars along the right and bottom edges. They show where the view sits
/// in [`pan_bounds`] and drag it along.
pub(crate) fn scrollbars(ui: &mut Ui, canvas: Rect, scene: &SceneModded, view: &mut SceneView) {
    let bounds = pan_bounds(scene.scene_rect());
    let visible = scene.visible(canvas, view);

    let horizontal = Rect::from_min_max(
        pos2(canvas.left(), canvas.bottom()),
        pos2(canvas.right(), canvas.bottom() + SCROLLBAR_WIDTH),
    );
    let vertical = Rect::from_min_max(
        pos2(canvas.right(), canvas.top()),
        pos2(canvas.right() + SCROLLBAR_WIDTH, canvas.bottom()),
    );

    view.center.x += scrollbar(ui, horizontal, true, bounds.x_range(), visible.x_range());
    view.center.y += scrollbar(ui, vertical, false, bounds.y_range(), visible.y_range());
}

/// Where a bar's thumb sits in its track, as fractions of the track: the share
/// of everything the view may reach that it currently covers.
fn thumb_range(bounds: Rangef, visible: Rangef) -> (f32, f32) {
    let fraction = |value: f32| ((value - bounds.min) / bounds.span()).clamp(0.0, 1.0);
    (fraction(visible.min), fraction(visible.max))
}

/// One bar. Returns how far a drag asks the view to travel, in scene units.
fn scrollbar(ui: &mut Ui, rect: Rect, horizontal: bool, bounds: Rangef, visible: Rangef) -> f32 {
    if !ui.is_rect_visible(rect) || bounds.span() <= 0.0 {
        return 0.0;
    }
    let track = if horizontal {
        rect.width()
    } else {
        rect.height()
    };
    let along = |fraction: f32| {
        if horizontal {
            rect.left() + fraction * track
        } else {
            rect.top() + fraction * track
        }
    };
    let (start, end) = thumb_range(bounds, visible);
    let (from, to) = (along(start), along(end).max(along(start) + 8.0));
    let thumb = if horizontal {
        Rect::from_min_max(pos2(from, rect.top() + 2.0), pos2(to, rect.bottom() - 2.0))
    } else {
        Rect::from_min_max(pos2(rect.left() + 2.0, from), pos2(rect.right() - 2.0, to))
    };

    let response = ui.interact(
        rect,
        ui.id().with(("scrollbar", horizontal)),
        Sense::click_and_drag(),
    );
    let visuals = ui.visuals();
    ui.painter()
        .rect_filled(rect, 0.0, visuals.extreme_bg_color.gamma_multiply(0.5));
    ui.painter().rect_filled(
        thumb,
        2.0,
        if response.dragged() {
            visuals.widgets.active.bg_fill
        } else if response.hovered() {
            visuals.widgets.hovered.bg_fill
        } else {
            visuals.widgets.inactive.bg_fill
        },
    );

    let dragged = if horizontal {
        response.drag_delta().x
    } else {
        response.drag_delta().y
    };
    dragged / track * bounds.span()
}

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

    /// The thumb says what share of everything reachable is on screen, so a
    /// view that covers it all fills its track.
    #[test]
    fn the_thumb_measures_the_view_against_the_bounds() {
        let bounds = Rangef::new(-1000.0, 3000.0);

        assert_eq!(thumb_range(bounds, bounds), (0.0, 1.0));
        assert_eq!(
            thumb_range(bounds, Rangef::new(-1000.0, 1000.0)),
            (0.0, 0.5)
        );
        assert_eq!(thumb_range(bounds, Rangef::new(1000.0, 3000.0)), (0.5, 1.0));
        assert_eq!(
            thumb_range(bounds, Rangef::new(-9_000.0, 9_000.0)),
            (0.0, 1.0),
            "a view wider than the bounds fills the track rather than running off it"
        );
    }

    /// The middle button drags the rig along with the pointer, so the view
    /// travels against it.
    #[test]
    fn a_middle_drag_moves_the_view_against_the_pointer() {
        let mut view = SceneView {
            center: pos2(1000.0, 1000.0),
            zoom: Some(2.0),
        };

        pan_view(&mut view, Vec2::new(5.0, -10.0));

        assert_eq!(view.center, pos2(995.0, 1010.0));
    }
}