gled-core 2.32.1

The engine of gled without a user interface: inputs, timing, rendering, Art-Net/DMX output and storage
Documentation
//! What is selected: the scene, and the effect within it that controllers
//! and interfaces work on as "the selected effect".

use gled_model::asset::{project::Project, scene::grid::GridLocation};
use gled_protocol::state::GridLine;

use super::Engine;
use crate::action::UiAction;

/// The effect selected in a scene. It goes back to the first effect when
/// the selection is read for another scene than it was made in, so every
/// way of selecting a scene resets it.
#[derive(Debug, Default, Clone, Copy)]
pub(super) struct EffectSelection {
    scene: GridLocation,
    effect: usize,
}

impl EffectSelection {
    /// Back to the first effect, for a new project.
    pub(super) fn reset(&mut self) {
        *self = Self::default();
    }

    /// The effect selected in `scene`.
    pub(super) fn effect_in(&mut self, scene: GridLocation) -> usize {
        if self.scene != scene {
            self.scene = scene;
            self.effect = 0;
        }
        self.effect
    }

    pub(super) fn select(&mut self, scene: GridLocation, effect: usize) {
        self.scene = scene;
        self.effect = effect;
    }
}

impl Engine {
    /// Moves the scene or effect selection. Reads the project only, so it is
    /// not copied.
    pub(super) fn apply_selection_action(&mut self, action: UiAction) {
        if let UiAction::SetMidiMode {
            port,
            mode,
            highlight,
        } = action
        {
            self.controller_line =
                self.controllers
                    .switch(port, mode, highlight, self.controller_line);
            return;
        }
        if let UiAction::SetTouchedControl { touch, control } = action {
            self.controllers.touch(touch, control);
            return;
        }
        let Some(project) = self.project.as_deref() else {
            return;
        };
        let scene = self.selected_scene_instance;
        match action {
            UiAction::SelectSceneRelative(steps) => {
                if let Some(location) = scene_steps_away(project, scene, steps) {
                    self.selected_scene_instance = location;
                }
            }
            UiAction::SelectSceneRowRelative(steps) => {
                self.selected_scene_instance = cell_away(project, scene, steps, 0);
                self.controller_line = Some(GridLine::Row);
            }
            UiAction::SelectSceneColumnRelative(steps) => {
                self.selected_scene_instance = cell_away(project, scene, 0, steps);
                self.controller_line = Some(GridLine::Column);
            }
            UiAction::SelectSceneEffect(effect) => self.effect_selection.select(scene, effect),
            UiAction::SelectSceneEffectRelative(steps) => {
                let count = effect_count(project, scene);
                let current = self.effect_selection.effect_in(scene);
                self.effect_selection
                    .select(scene, index_steps_away(current, count, steps));
            }
            _ => {}
        }
    }
}

/// The number of effects of the scene at `location`; 0 when none is there.
fn effect_count(project: &Project, location: GridLocation) -> usize {
    project
        .scenes_instances_grid
        .get(&location)
        .map_or(0, |instance| instance.scene.effects().len())
}

/// The placed scene `steps` away from `from` in row-major order, stopping
/// at the ends. From an empty cell it starts at the scene before it.
fn scene_steps_away(project: &Project, from: GridLocation, steps: i32) -> Option<GridLocation> {
    let locations = project.scene_locations_row_major();
    let current = match locations.binary_search_by_key(&(from.row, from.col), |location| {
        (location.row, location.col)
    }) {
        Ok(index) => index,
        Err(index) => index.saturating_sub(1),
    };
    locations
        .get(index_steps_away(current, locations.len(), steps))
        .copied()
}

/// The cell `rows` and `cols` away from `from`, stopping at the grid's
/// edges. It may be empty.
fn cell_away(project: &Project, from: GridLocation, rows: i32, cols: i32) -> GridLocation {
    GridLocation {
        row: index_steps_away(from.row, project.grid_height(), rows),
        col: index_steps_away(from.col, project.grid_width(), cols),
    }
}

/// `current` moved by `steps` within `0..count`, stopping at the ends.
fn index_steps_away(current: usize, count: usize, steps: i32) -> usize {
    let last = count.saturating_sub(1);
    let current = current.min(last);
    let magnitude = steps.unsigned_abs() as usize;
    if steps < 0 {
        current.saturating_sub(magnitude)
    } else {
        current.saturating_add(magnitude).min(last)
    }
}

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

    #[test]
    fn steps_stop_at_the_first_and_last_index() {
        assert_eq!(index_steps_away(1, 4, -3), 0);
        assert_eq!(index_steps_away(1, 4, 5), 3);
        assert_eq!(index_steps_away(1, 4, 1), 2);
    }

    #[test]
    fn a_selection_past_the_end_steps_from_the_last_index() {
        assert_eq!(index_steps_away(9, 3, -1), 1);
    }

    #[test]
    fn steps_in_an_empty_list_stay_at_zero() {
        assert_eq!(index_steps_away(0, 0, 2), 0);
    }

    #[test]
    fn rows_and_columns_stop_at_the_edges_of_the_grid() {
        let project = Project::default();
        let last_row = project.grid_height() - 1;
        let last_col = project.grid_width() - 1;
        let from = GridLocation { row: 1, col: 3 };
        assert_eq!(
            cell_away(&project, from, 2, 0),
            GridLocation { row: 3, col: 3 }
        );
        assert_eq!(
            cell_away(&project, from, -5, 0),
            GridLocation { row: 0, col: 3 }
        );
        assert_eq!(
            cell_away(&project, from, 99, 0),
            GridLocation {
                row: last_row,
                col: 3
            }
        );
        assert_eq!(
            cell_away(&project, from, 0, -1),
            GridLocation { row: 1, col: 2 }
        );
        assert_eq!(
            cell_away(&project, from, 0, 99),
            GridLocation {
                row: 1,
                col: last_col
            }
        );
    }

    #[test]
    fn selecting_another_scene_selects_its_first_effect() {
        let mut selection = EffectSelection::default();
        selection.select(GridLocation::new(0, 0), 3);
        assert_eq!(selection.effect_in(GridLocation::new(0, 0)), 3);
        assert_eq!(selection.effect_in(GridLocation::new(1, 0)), 0);
    }
}