gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
//! Selecting by clicking, alike in both editors.

use egui::Modifiers;
use std::collections::BTreeSet;

/// What a click does to the selection.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ClickIntent {
    /// Selects what was hit, and only that.
    Replace,
    Toggle,
    Add,
    Remove,
}

impl ClickIntent {
    /// Cmd toggles, shift adds, alt takes away; a plain click replaces.
    pub(crate) fn from_modifiers(modifiers: Modifiers) -> Self {
        if modifiers.command {
            Self::Toggle
        } else if modifiers.shift {
            Self::Add
        } else if modifiers.alt {
            Self::Remove
        } else {
            Self::Replace
        }
    }

    /// Applies a click on `hit` to `selection`. A click on the background,
    /// with no `hit`, clears the selection, unless a modifier is held: a
    /// modified click that misses is a slip, and keeps what it would have
    /// added to.
    pub(crate) fn apply<K: Ord>(self, selection: &mut BTreeSet<K>, hit: Option<K>) {
        let Some(hit) = hit else {
            if self == Self::Replace {
                selection.clear();
            }
            return;
        };
        match self {
            Self::Replace => {
                selection.clear();
                selection.insert(hit);
            }
            Self::Toggle => {
                if !selection.remove(&hit) {
                    selection.insert(hit);
                }
            }
            Self::Add => {
                selection.insert(hit);
            }
            Self::Remove => {
                selection.remove(&hit);
            }
        }
    }
}

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

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

    fn clicked(intent: ClickIntent, hit: Option<usize>) -> BTreeSet<usize> {
        let mut selection = set(&[1, 2]);
        intent.apply(&mut selection, hit);
        selection
    }

    #[test]
    fn a_click_replaces_toggles_adds_or_removes() {
        assert_eq!(clicked(ClickIntent::Replace, Some(3)), set(&[3]));
        assert_eq!(clicked(ClickIntent::Toggle, Some(3)), set(&[1, 2, 3]));
        assert_eq!(clicked(ClickIntent::Toggle, Some(2)), set(&[1]));
        assert_eq!(clicked(ClickIntent::Add, Some(2)), set(&[1, 2]));
        assert_eq!(clicked(ClickIntent::Remove, Some(2)), set(&[1]));
    }

    #[test]
    fn only_a_plain_click_on_the_background_clears() {
        assert!(clicked(ClickIntent::Replace, None).is_empty());
        for intent in [ClickIntent::Toggle, ClickIntent::Add, ClickIntent::Remove] {
            assert_eq!(clicked(intent, None), set(&[1, 2]), "{intent:?}");
        }
    }

    #[test]
    fn cmd_wins_over_shift_and_shift_over_alt() {
        let held = |command, shift, alt| Modifiers {
            command,
            shift,
            alt,
            ..Default::default()
        };
        assert_eq!(
            ClickIntent::from_modifiers(held(true, true, true)),
            ClickIntent::Toggle
        );
        assert_eq!(
            ClickIntent::from_modifiers(held(false, true, true)),
            ClickIntent::Add
        );
        assert_eq!(
            ClickIntent::from_modifiers(held(false, false, true)),
            ClickIntent::Remove
        );
        assert_eq!(
            ClickIntent::from_modifiers(Modifiers::NONE),
            ClickIntent::Replace
        );
    }
}