use crate::canvas::caches::Caches;
use crate::canvas::images::Decoded;
use crate::canvas::selection::ClickIntent;
use crate::canvas::view::SceneView;
use crate::fixture_editor::drag::Drag;
use crate::ui::file_pick::FilePick;
use gled_model::patching::Patch;
use gled_model::patching::fixture::FixtureId;
use gled_model::patching::layout::{LayoutRef, scene_bounds};
use std::collections::{BTreeSet, HashSet};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CanvasMode {
Topology,
Layout,
}
impl CanvasMode {
pub(crate) fn from_selection(layout: LayoutRef) -> Self {
match layout {
LayoutRef::Topology => Self::Topology,
LayoutRef::Layout(_) => Self::Layout,
}
}
pub(crate) fn edits_network(self) -> bool {
self == Self::Topology
}
}
pub(crate) type PatchEdit<'a> = crate::canvas::edit::Edit<'a, Patch>;
pub(crate) struct PickedBackground {
pub(crate) layout: LayoutRef,
pub(crate) name: String,
pub(crate) path: PathBuf,
pub(crate) image: Decoded,
}
impl PickedBackground {
pub(crate) fn target(&self, patch: &Patch) -> Option<LayoutRef> {
if self.layout == LayoutRef::Topology {
return Some(LayoutRef::Topology);
}
let mut named = patch
.layouts
.iter()
.enumerate()
.filter(|(_, layout)| layout.name == self.name)
.map(|(index, _)| LayoutRef::Layout(index));
let found = named.next()?;
named.next().is_none().then_some(found)
}
}
pub(crate) struct EditorState {
pub(crate) selection: LayoutRef,
pub(crate) selected_mode: usize,
pub(crate) selected_fixtures: BTreeSet<FixtureId>,
pub(crate) selected_port: Option<FixtureId>,
pub(crate) pointer_down_on: HashSet<FixtureId>,
pub(crate) view: SceneView,
pub(crate) member_band: crate::fixture_editor::member_preview::Band,
pub(crate) drag: Option<Drag>,
pub(crate) background_pick: FilePick<PickedBackground>,
pub(crate) picking_members: bool,
pub(crate) clipboard: crate::fixture_editor::clipboard::ClipboardFixtures,
pub(crate) caches: Caches,
}
impl Default for EditorState {
fn default() -> Self {
Self {
selection: LayoutRef::Topology,
selected_mode: 0,
selected_fixtures: BTreeSet::new(),
selected_port: None,
pointer_down_on: HashSet::new(),
view: SceneView::centred_on(scene_bounds().center()),
member_band: None,
drag: None,
background_pick: FilePick::default(),
picking_members: false,
clipboard: Default::default(),
caches: Caches::default(),
}
}
}
impl EditorState {
pub(crate) fn select_single(&mut self, id: FixtureId) {
self.selected_fixtures.clear();
self.selected_fixtures.insert(id);
self.selected_port = None;
}
pub(crate) fn click(&mut self, intent: ClickIntent, hit: Option<FixtureId>) {
if hit.is_some() || intent == ClickIntent::Replace {
self.selected_port = None;
}
intent.apply(&mut self.selected_fixtures, hit);
}
pub(crate) fn select_fixtures(&mut self, selection: BTreeSet<FixtureId>) {
if selection != self.selected_fixtures {
self.selected_port = None;
}
self.selected_fixtures = selection;
}
pub(crate) fn select_port(&mut self, id: FixtureId) {
self.selected_fixtures.clear();
self.selected_port = Some(id);
}
pub(crate) fn clear_selection(&mut self) {
self.selected_fixtures.clear();
self.selected_port = None;
}
pub(crate) fn pick_members(&mut self, picking: bool) {
self.picking_members = picking;
self.member_band = None;
self.drag = None;
self.pointer_down_on.clear();
}
pub(crate) fn select_layout(&mut self, layout: LayoutRef, mode: usize) {
if self.selection != layout {
self.clear_selection();
self.drag = None;
self.pointer_down_on.clear();
}
self.selection = layout;
self.selected_mode = mode;
self.picking_members = false;
}
pub(crate) fn clamp(&mut self, patch: &Patch) {
if patch.layout(self.selection).is_none() {
self.select_layout(LayoutRef::Topology, 0);
}
if let Some(layout) = patch.layout(self.selection) {
self.selected_fixtures
.retain(|id| layout.members.contains_key(id));
}
if let Some(id) = self.selected_port {
self.selected_port = patch
.topology
.fixtures
.contains_key(&id)
.then(|| patch.topology.chain_head_of(id));
}
let modes = patch
.layout(self.selection)
.map(|layout| layout.modes.len())
.unwrap_or(1);
if self.selected_mode >= modes {
self.selected_mode = 0;
}
}
pub(crate) fn canvas_mode(&self) -> CanvasMode {
CanvasMode::from_selection(self.selection)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_a_plain_click_on_the_background_deselects() {
let mut state = EditorState::default();
let id = FixtureId::new();
state.click(ClickIntent::Toggle, Some(id));
assert!(state.selected_fixtures.contains(&id));
state.click(ClickIntent::Add, None);
assert!(state.selected_fixtures.contains(&id));
state.click(ClickIntent::Replace, None);
assert!(state.selected_fixtures.is_empty());
}
#[test]
fn selecting_another_layout_drops_the_selection() {
let mut state = EditorState::default();
state.selected_fixtures.insert(FixtureId::new());
state.select_layout(LayoutRef::Layout(0), 0);
assert!(state.selected_fixtures.is_empty());
}
#[test]
fn reselecting_the_same_layout_keeps_the_selection() {
let mut state = EditorState::default();
let id = FixtureId::new();
state.selected_fixtures.insert(id);
state.select_layout(LayoutRef::Topology, 0);
assert!(state.selected_fixtures.contains(&id));
}
#[test]
fn clamping_drops_what_the_patch_no_longer_has() {
let mut patch = Patch::default();
let kept = patch.add_fixture(
gled_model::patching::fixture::Fixture::new("kept"),
Default::default(),
);
let mut state = EditorState::default();
state.selected_fixtures.extend([kept, FixtureId::new()]);
state.clamp(&patch);
assert_eq!(state.selected_fixtures, [kept].into_iter().collect());
state.select_layout(LayoutRef::Layout(3), 0);
state.clamp(&patch);
assert_eq!(state.selection, LayoutRef::Topology);
}
fn picked(layout: LayoutRef, name: &str) -> PickedBackground {
PickedBackground {
layout,
name: name.to_string(),
path: PathBuf::from("stage.png"),
image: Err("not decoded in a test".to_string()),
}
}
#[test]
fn a_background_finds_its_layout_after_another_is_removed() {
let mut patch = Patch::default();
let first = patch.add_layout();
let second = patch.add_layout();
let name = patch.layouts[second].name.clone();
let chosen = picked(LayoutRef::Layout(second), &name);
patch.remove_layout(first);
assert_eq!(chosen.target(&patch), Some(LayoutRef::Layout(first)));
}
#[test]
fn a_background_for_a_removed_layout_is_left_out() {
let mut patch = Patch::default();
let first = patch.add_layout();
patch.add_layout();
let name = patch.layouts[first].name.clone();
let chosen = picked(LayoutRef::Layout(first), &name);
patch.remove_layout(first);
assert_eq!(chosen.target(&patch), None);
}
#[test]
fn canvas_mode_follows_the_selection() {
let mut state = EditorState::default();
assert_eq!(state.canvas_mode(), CanvasMode::Topology);
state.select_layout(LayoutRef::Layout(0), 0);
assert_eq!(state.canvas_mode(), CanvasMode::Layout);
}
}