gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
//! Selecting by dragging a rubber band.

use crate::canvas::shapes::Shapes;
use egui::emath::TSTransform;
use egui::{Color32, Modifiers, Pos2, Rect, Stroke, StrokeKind};
use std::collections::BTreeSet;

/// What a rubber band does to the selection it started from.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BoxIntent {
    Replace,
    Add,
    Subtract,
}

impl BoxIntent {
    /// Alt subtracts, shift adds; cmd also adds, matching cmd+click.
    pub(crate) fn from_modifiers(modifiers: Modifiers) -> Self {
        if modifiers.alt {
            Self::Subtract
        } else if modifiers.shift || modifiers.command {
            Self::Add
        } else {
            Self::Replace
        }
    }

    /// The selection a band covering `covered` makes out of `base`, the
    /// selection from before the band started. Deriving it from `base` every
    /// frame lets the band shrink again as well as grow.
    fn combine<K: Ord + Clone>(
        self,
        base: &BTreeSet<K>,
        covered: impl IntoIterator<Item = K>,
    ) -> BTreeSet<K> {
        let mut selection = match self {
            Self::Replace => BTreeSet::new(),
            Self::Add | Self::Subtract => base.clone(),
        };
        for key in covered {
            match self {
                Self::Subtract => selection.remove(&key),
                Self::Replace | Self::Add => selection.insert(key),
            };
        }
        selection
    }
}

/// A rubber band in flight: its corners in scene units, and the selection it
/// started from, so an additive or subtractive band can shrink again as well
/// as grow.
#[derive(Debug, Clone)]
pub(crate) struct Band<K> {
    pub(crate) start: Pos2,
    pub(crate) current: Pos2,
    base: BTreeSet<K>,
    intent: BoxIntent,
}

impl<K: Ord + Clone> Band<K> {
    /// A band pulled from `at` with `modifiers` held, over `selection`; a
    /// plain band starts by clearing it.
    pub(crate) fn start(at: Pos2, selection: &mut BTreeSet<K>, modifiers: Modifiers) -> Self {
        let intent = BoxIntent::from_modifiers(modifiers);
        let base = selection.clone();
        if intent == BoxIntent::Replace {
            selection.clear();
        }
        Self {
            start: at,
            current: at,
            base,
            intent,
        }
    }

    /// What the band spans, in scene units.
    pub(crate) fn rect(&self) -> Rect {
        Rect::from_two_pos(self.start, self.current)
    }

    /// The selection with the band covering `covered`.
    pub(crate) fn selection(&self, covered: impl IntoIterator<Item = K>) -> BTreeSet<K> {
        self.intent.combine(&self.base, covered)
    }
}

/// Draws a rubber band spanning `band`, given in scene units.
pub(crate) fn draw_band(overlay: &mut Shapes, scene_transform: &TSTransform, band: Rect) {
    overlay.rect(
        Rect::from_min_max(*scene_transform * band.min, *scene_transform * band.max),
        0.0,
        Color32::from_rgba_unmultiplied(100, 150, 255, 30),
        Stroke::new(1.5, Color32::from_rgba_unmultiplied(100, 150, 255, 200)),
        StrokeKind::Outside,
    );
}

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

    fn set(keys: &[usize]) -> BTreeSet<usize> {
        keys.iter().copied().collect()
    }

    #[test]
    fn a_band_replaces_adds_to_or_subtracts_from_its_base() {
        let base = set(&[1, 2]);
        assert_eq!(BoxIntent::Replace.combine(&base, [3]), set(&[3]));
        assert_eq!(BoxIntent::Add.combine(&base, [3]), set(&[1, 2, 3]));
        assert_eq!(BoxIntent::Subtract.combine(&base, [2]), set(&[1]));
    }
}