use crate::canvas::frame::{CanvasSpec, show_canvas};
use crate::canvas::shapes::{Shapes, draw_adaptive_grid};
use crate::canvas::{images::decode_picture, pictures};
use crate::fixture_editor::commands::context_actions;
use crate::fixture_editor::drag::{Drag, drag_positions};
use crate::fixture_editor::fixtures::draw_fixtures;
use crate::fixture_editor::selection_handles::covered_by;
use crate::fixture_editor::state::{EditorState, PatchEdit, PickedBackground};
use crate::ui::file_pick::IMAGES;
use egui::emath::TSTransform;
use egui::{Color32, Rect, Sense, Ui, pos2};
use egui::{Modifiers, Pos2, Stroke, StrokeKind};
use gled_model::asset::picture::new_picture_path;
use gled_model::collection::Collections;
use gled_model::patching::Patch;
use gled_model::patching::definition::FixtureDefinition;
use gled_model::patching::fixture::FixtureId;
use gled_model::patching::layout::LayoutRef;
use gled_model::patching::layout::scene_bounds;
use std::collections::BTreeMap;
use std::sync::LazyLock;
pub(super) static UNPATCHED: LazyLock<FixtureDefinition> =
LazyLock::new(FixtureDefinition::default);
pub(super) type Definitions =
BTreeMap<FixtureId, std::sync::Arc<gled_model::asset::Asset<FixtureDefinition>>>;
pub(super) fn definition_of(definitions: &Definitions, id: FixtureId) -> &FixtureDefinition {
definitions
.get(&id)
.map(|asset| &asset.data)
.unwrap_or(&UNPATCHED)
}
pub(crate) fn show(
ui: &mut Ui,
edit: &mut PatchEdit<'_>,
state: &mut EditorState,
collections: &Collections,
) {
let drawn = state.selection;
let mode = state.canvas_mode();
if let Some(picked) = state.background_pick.take() {
set_background(ui.ctx(), edit, state, picked);
}
let background = edit
.value
.layout(drawn)
.and_then(|layout| layout.background.clone());
let texture = background
.as_deref()
.and_then(|path| state.caches.images.texture(ui.ctx(), path));
let active = !state.picking_members;
let mut view = state.view;
let spec = CanvasSpec {
id_salt: "editor_ui",
scene_size: scene_bounds().size(),
max_zoom: 8.0,
sense: if active {
Sense::click_and_drag()
} else {
Sense::hover()
},
};
show_canvas(ui, spec, &mut view, |canvas| {
if active {
context_actions(edit, state, collections, mode, &canvas.response);
}
if let Some(texture) = &texture {
canvas.ui.painter().image(
texture.id(),
scene_bounds(),
Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
Color32::from_gray(200),
);
}
draw_fixtures(canvas, edit, state, collections, mode, drawn, active);
});
state.view = view;
}
pub(crate) fn ask_background(ctx: &egui::Context, patch: &Patch, state: &mut EditorState) {
let layout = state.selection;
let name = patch
.layout(layout)
.map(|layout| layout.name.clone())
.unwrap_or_default();
state.background_pick.ask(ctx, IMAGES, move |file| {
let path = new_picture_path(&file.extension());
let image = decode_picture(&path, &file.bytes);
PickedBackground {
layout,
name,
path,
bytes: file.bytes,
image,
}
});
}
fn set_background(
ctx: &egui::Context,
edit: &mut PatchEdit<'_>,
state: &mut EditorState,
picked: PickedBackground,
) {
let target = picked.target(edit.value);
let PickedBackground {
path, bytes, image, ..
} = picked;
let decoded = image.is_ok();
state.caches.images.insert(ctx, path.clone(), image);
let Some(layout) = target else {
tracing::warn!(
"Left out {}: the layout it was chosen for is gone",
path.display()
);
return;
};
if decoded {
pictures::store(path.clone(), bytes);
edit.apply(|patch| {
if let Some(layout) = patch.layout_mut(layout) {
layout.background = Some(path);
}
});
}
}
pub(super) fn continue_drag(
edit: &mut PatchEdit<'_>,
state: &mut EditorState,
drawn: LayoutRef,
definitions: &Definitions,
pointer: Pos2,
modifiers: Modifiers,
zoom: f32,
) {
if let Some(Drag::Band(band)) = &mut state.drag {
band.current = pointer;
let covered = covered_by(edit.value, drawn, band.rect(), definitions, zoom);
let selection = band.selection(covered);
state.select_fixtures(selection);
return;
}
if let Some(drag) = &state.drag {
edit.continue_change(|patch| {
if let Some(layout) = patch.layout_mut(drawn) {
drag_positions(drag, layout, pointer, modifiers, |id| {
definition_of(definitions, id).scaling
});
}
});
}
}
pub(super) fn draw_layered_grid(edge_painter: &mut Shapes, scene_transform: &TSTransform) {
let rect = scene_bounds();
draw_adaptive_grid(
edge_painter,
rect,
scene_transform,
&[
(10.0, Stroke::new(0.3, Color32::from_white_alpha(30))),
(100.0, Stroke::new(0.4, Color32::from_white_alpha(60))),
],
);
edge_painter.rect_stroke(
Rect::from_min_max(*scene_transform * rect.min, *scene_transform * rect.max),
0.0,
Stroke::new(1.0, Color32::from_white_alpha(110)),
StrokeKind::Outside,
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::canvas::undo::UndoStack;
use egui::{Event, PointerButton, RawInput, vec2};
use gled_model::asset::Asset;
use gled_model::asset_id::AssetId;
use gled_model::collection::Collection;
use gled_model::patching::fixture::{Fixture, LayoutPosition};
struct Session {
ctx: egui::Context,
patch: Patch,
undo: UndoStack<Patch>,
state: EditorState,
collections: Collections,
}
impl Session {
fn new() -> (Self, FixtureId) {
let asset = Asset {
id: AssetId::default(),
path: vec!["square".to_string()],
data: FixtureDefinition {
size_m: [4.0, 4.0, 0.1],
..Default::default()
},
};
let mut fixture = Fixture::new("square");
fixture.definition = Some(asset.id);
let mut collection = Collection::default();
collection.set_asset(asset);
let mut collections = Collections::default();
collections.insert::<FixtureDefinition>(collection);
let mut patch = Patch::default();
let id = patch.add_fixture(fixture, LayoutPosition::at(scene_bounds().center()));
let ctx = egui::Context::default();
crate::style::apply(&ctx);
let session = Self {
ctx,
patch,
undo: UndoStack::default(),
state: EditorState::default(),
collections,
};
(session, id)
}
fn frame(&mut self, events: Vec<Event>) {
let input = RawInput {
screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))),
events,
..Default::default()
};
let mut output = self.ctx.run_ui(input, |ui| {
let mut edit = PatchEdit::new(&mut self.patch, &mut self.undo);
show(ui, &mut edit, &mut self.state, &self.collections);
});
output.textures_delta.clear();
}
fn drag(&mut self, button: PointerButton, from: Pos2, to: Pos2) {
let press = |pos, pressed| Event::PointerButton {
pos,
button,
pressed,
modifiers: Modifiers::NONE,
};
self.frame(vec![Event::PointerMoved(from)]);
self.frame(vec![press(from, true)]);
for step in 1..=4 {
self.frame(vec![Event::PointerMoved(
from + (to - from) * step as f32 / 4.0,
)]);
}
self.frame(vec![press(to, false)]);
self.frame(vec![]);
}
fn position(&self, id: FixtureId) -> Pos2 {
self.patch.topology.default_layout.members[&id].pos
}
}
const MIDDLE: Pos2 = Pos2::new(395.0, 295.0);
#[test]
fn a_primary_drag_moves_the_fixture() {
let (mut session, id) = Session::new();
session.frame(vec![]);
session.drag(PointerButton::Primary, MIDDLE, MIDDLE + vec2(40.0, 0.0));
assert!(session.position(id).x > scene_bounds().center().x);
assert!(session.undo.can_undo());
}
#[test]
fn a_secondary_drag_moves_nothing() {
let (mut session, id) = Session::new();
session.frame(vec![]);
session.drag(PointerButton::Secondary, MIDDLE, MIDDLE + vec2(40.0, 0.0));
assert_eq!(session.position(id), scene_bounds().center());
assert!(!session.undo.can_undo());
}
#[test]
fn a_secondary_drag_on_the_background_keeps_the_selection() {
let (mut session, id) = Session::new();
session.state.select_single(id);
session.frame(vec![]);
session.drag(
PointerButton::Secondary,
pos2(150.0, 500.0),
pos2(200.0, 550.0),
);
assert!(session.state.selected_fixtures.contains(&id));
}
#[test]
fn a_band_over_fixtures_leaves_the_network_output() {
let (mut session, id) = Session::new();
session.state.select_port(id);
session.frame(vec![]);
session.drag(
PointerButton::Primary,
pos2(150.0, 500.0),
MIDDLE + vec2(20.0, -20.0),
);
assert!(session.state.selected_fixtures.contains(&id));
assert_eq!(session.state.selected_port, None);
}
}