use crate::canvas::context_menu::ContextMenu;
use crate::fixture_editor::canvas::ask_background;
use crate::fixture_editor::clipboard::{clipboard_text, copy_selected, fixtures_from_clipboard};
use crate::fixture_editor::commands::EditorTransaction::{
CopySelected, CutSelected, DuplicateSelected, Paste, Redo, RemoveSelected, SelectAll, Undo,
};
use crate::fixture_editor::state::{CanvasMode, EditorState, PatchEdit};
use egui::{Key, KeyboardShortcut, Modifiers, Response, Vec2, vec2};
use gled_model::asset_id::AssetId;
use gled_model::collection::Collections;
use gled_model::patching::Patch;
use gled_model::patching::definition::FixtureDefinition;
use gled_model::patching::fixture::{Fixture, FixtureId, LayoutPosition};
use gled_model::patching::layout::LayoutRef;
#[derive(Clone)]
enum CanvasAction {
Edit(EditorTransaction),
ChangeBackground,
}
pub(super) fn context_actions(
edit: &mut PatchEdit<'_>,
state: &mut EditorState,
collections: &Collections,
mode: CanvasMode,
canvas_response: &Response,
) {
let ctx = &canvas_response.ctx;
let command = |key| KeyboardShortcut::new(Modifiers::COMMAND, key);
let mut menu = ContextMenu::new(ctx).entry(
"Select All",
CanvasAction::Edit(SelectAll),
Some(command(Key::A)),
);
if mode == CanvasMode::Topology {
menu = menu
.entry(
"Duplicate Fixture",
CanvasAction::Edit(DuplicateSelected),
Some(command(Key::D)),
)
.entry_shown("Copy", CanvasAction::Edit(CopySelected), command(Key::C))
.entry_shown("Cut", CanvasAction::Edit(CutSelected), command(Key::X))
.entry_shown("Paste", CanvasAction::Edit(Paste), command(Key::V));
}
menu = menu.entry(
remove_label(mode),
CanvasAction::Edit(RemoveSelected),
Some(KeyboardShortcut::new(Modifiers::NONE, Key::Delete)),
);
menu = menu.entry("Change Background", CanvasAction::ChangeBackground, None);
let menu = menu
.entry(
"Redo",
CanvasAction::Edit(Redo),
Some(KeyboardShortcut::new(
Modifiers::COMMAND | Modifiers::SHIFT,
Key::Z,
)),
)
.entry("Undo", CanvasAction::Edit(Undo), Some(command(Key::Z)))
.key(
KeyboardShortcut::new(Modifiers::NONE, Key::Backspace),
CanvasAction::Edit(RemoveSelected),
)
.key(command(Key::Y), CanvasAction::Edit(Redo));
let mut transaction = match menu.show(canvas_response) {
Some(CanvasAction::Edit(edit)) => Some(edit),
Some(CanvasAction::ChangeBackground) => {
ask_background(ctx, edit.value, state);
None
}
None => None,
};
if let Some(clipboard) = clipboard_action(state, ctx, mode) {
transaction = Some(clipboard);
}
if let Some(transaction) = transaction {
copy_to_system_clipboard(ctx, edit.value, state, &transaction);
process_transaction(edit, state, collections, transaction);
}
}
fn copy_to_system_clipboard(
ctx: &egui::Context,
patch: &Patch,
state: &EditorState,
transaction: &EditorTransaction,
) {
if matches!(transaction, CopySelected | CutSelected)
&& let Some(text) = clipboard_text(patch, state)
{
ctx.copy_text(text);
}
}
fn clipboard_action(
state: &mut EditorState,
ctx: &egui::Context,
mode: CanvasMode,
) -> Option<EditorTransaction> {
if mode != CanvasMode::Topology || ctx.text_edit_focused() {
return None;
}
let mut action = None;
ctx.input_mut(|input| {
input.events.retain(|event| match event {
egui::Event::Copy => {
action = Some(CopySelected);
false
}
egui::Event::Cut => {
action = Some(CutSelected);
false
}
egui::Event::Paste(text) => match fixtures_from_clipboard(text) {
Some(clipboard) => {
state.clipboard = clipboard;
action = Some(Paste);
false
}
None => true,
},
_ => true,
});
});
action
}
#[derive(Clone)]
pub(crate) enum EditorTransaction {
AddFixture(LayoutPosition, Option<AssetId<FixtureDefinition>>),
MakeLeaf(FixtureId),
MakeSuccessor(FixtureId, FixtureId),
RemoveSelected,
DuplicateSelected,
SelectAll,
CopySelected,
CutSelected,
Paste,
Undo,
Redo,
}
const PASTE_OFFSET: Vec2 = vec2(20.0, 20.0);
fn remove_fixtures(
patch: &mut Patch,
ids: impl IntoIterator<Item = FixtureId>,
collections: &Collections,
) {
for id in ids {
let detached = patch.topology.successor(id);
patch.remove_fixture(id);
if let Some(detached) = detached {
rehome(patch, detached, collections);
}
}
}
fn copy_of(patch: &mut Patch, fixture: &Fixture, mut position: LayoutPosition) -> FixtureId {
position.pos += PASTE_OFFSET;
let mut copy = Fixture::new(fixture.name.clone());
copy.definition = fixture.definition;
patch.add_fixture(copy, position)
}
fn rehome(patch: &mut Patch, head: FixtureId, collections: &Collections) {
if !patch.topology.is_chain_head(head) {
return;
}
let universe = patch.lowest_free_universe(collections, Some(head));
if let Some(port) = patch.topology.ports.get_mut(&head) {
port.universe = universe;
}
}
pub(crate) fn process_transaction(
edit: &mut PatchEdit<'_>,
state: &mut EditorState,
collections: &Collections,
transaction: EditorTransaction,
) {
let done = match transaction {
EditorTransaction::Undo => edit.undo(),
EditorTransaction::Redo => edit.redo(),
transaction => {
edit.apply(|patch| apply_transaction(patch, state, collections, transaction));
return;
}
};
if done {
state.drag = None;
state.clamp(edit.value);
}
}
pub(crate) fn apply_transaction(
patch: &mut Patch,
state: &mut EditorState,
collections: &Collections,
transaction: EditorTransaction,
) {
match transaction {
EditorTransaction::AddFixture(position, definition) => {
let mut fixture = Fixture::new("Light");
fixture.definition = definition;
let id = patch.add_fixture(fixture, position);
rehome(patch, id, collections);
state.select_single(id);
}
EditorTransaction::MakeLeaf(id) => {
let detached = patch.topology.successor(id);
patch.topology.make_leaf(id);
if let Some(detached) = detached {
rehome(patch, detached, collections);
}
}
EditorTransaction::MakeSuccessor(parent, child) => {
let detached = patch.topology.successor(parent);
if patch.topology.make_successor(parent, child)
&& let Some(detached) = detached
{
rehome(patch, detached, collections);
}
}
EditorTransaction::RemoveSelected => {
let selected = std::mem::take(&mut state.selected_fixtures);
match state.selection {
LayoutRef::Topology => remove_fixtures(patch, selected, collections),
layout => {
if let Some(layout) = patch.layout_mut(layout) {
for id in selected {
layout.forget(id);
}
}
}
}
}
EditorTransaction::DuplicateSelected => {
if state.selection != LayoutRef::Topology {
return;
}
let selected: Vec<FixtureId> = state.selected_fixtures.iter().copied().collect();
let mut created = vec![];
for id in selected {
let Some(source) = patch.fixture(id).cloned() else {
continue;
};
let Some(position) = patch.topology.default_layout.members.get(&id).copied() else {
continue;
};
created.push(copy_of(patch, &source, position));
}
for id in &created {
rehome(patch, *id, collections);
}
if !created.is_empty() {
state.selected_fixtures = created.into_iter().collect();
}
}
EditorTransaction::SelectAll => {
state.selected_fixtures = patch
.layout(state.selection)
.map(|layout| layout.members.keys().copied().collect())
.unwrap_or_default();
state.selected_port = None;
}
EditorTransaction::CopySelected => copy_selected(patch, state),
EditorTransaction::CutSelected => {
copy_selected(patch, state);
let selected = std::mem::take(&mut state.selected_fixtures);
remove_fixtures(patch, selected, collections);
}
EditorTransaction::Paste => {
if state.selection != LayoutRef::Topology {
return;
}
let clipboard = state.clipboard.clone();
let created: Vec<FixtureId> = clipboard
.gled_fixtures
.iter()
.map(|(fixture, position)| copy_of(patch, fixture, *position))
.collect();
for (parent, child) in clipboard.edges {
if let (Some(parent), Some(child)) = (created.get(parent), created.get(child)) {
patch.topology.make_successor(*parent, *child);
}
}
for id in &created {
rehome(patch, *id, collections);
}
if !created.is_empty() {
state.selected_fixtures = created.into_iter().collect();
state.selected_port = None;
}
}
EditorTransaction::Undo | EditorTransaction::Redo => {}
}
}
pub(crate) fn remove_label(mode: CanvasMode) -> &'static str {
match mode {
CanvasMode::Topology => "Remove Fixture",
_ => "Remove from Layout",
}
}
#[cfg(test)]
mod tests;