use crate::canvas::definition_drawing::draw_definition_fitted;
use crate::canvas::view::fit_into;
use crate::fixture_editor::canvas::{self, ask_background};
use crate::fixture_editor::commands::EditorTransaction::{AddFixture, Redo, Undo};
use crate::fixture_editor::commands::process_transaction;
use crate::fixture_editor::draft::DraftStatus;
use crate::fixture_editor::state::{CanvasMode, PatchEdit};
use crate::fixture_editor::{FixtureWindow, NAME_CHAR_LIMIT, WARNING, apply_button};
use egui::emath::Align;
use egui::{Atoms, Color32, Frame, Id, IntoAtoms, Layout, Sense, TextEdit, Ui, UiBuilder, Vec2};
use egui_phosphor_icons::icons;
use gled_gpu::constants::UNIVERSE_SLOTS;
use gled_model::asset::Asset;
use gled_model::collection::Collections;
use gled_model::patching::definition::FixtureDefinition;
use gled_model::patching::fixture::LayoutPosition;
use gled_model::patching::layout::{Layout as PatchLayout, LayoutRef};
impl FixtureWindow {
pub(super) fn draw_editor(
&mut self,
ui: &mut Ui,
edit: &mut PatchEdit<'_>,
collections: &Collections,
status: DraftStatus,
) -> bool {
let mode = self.state.canvas_mode();
let mut apply = false;
ui.horizontal(|ui| {
if cfg!(not(target_arch = "wasm32"))
&& ui
.add_enabled(
!self.state.background_pick.is_open(),
egui::Button::new("Select Image"),
)
.clicked()
{
ask_background(ui.ctx(), edit.value, &mut self.state);
}
match self.state.selection {
LayoutRef::Topology => self.add_fixture_button(ui, edit, collections),
LayoutRef::Layout(_) => {
let choose: Atoms =
(icons::SELECTION_PLUS.fill(), "Choose Fixtures…").into_atoms();
if ui.button(choose).clicked() {
self.state.pick_members(true);
}
}
}
self.mode_picker(ui, edit);
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
apply = ui
.add_enabled(status.unapplied, apply_button(status.unapplied))
.on_hover_text("Hand the patch over to the running show")
.clicked();
self.undo_buttons(ui, edit, collections, status);
ui.label(match mode {
CanvasMode::Topology => PatchLayout::DEFAULT_NAME,
_ => "Layout",
});
let addressing = edit.value.addressing(collections);
let dark = dark_universes(addressing.universes());
if let (Some(first), Some(last)) = (dark.first(), dark.last()) {
ui.colored_label(WARNING, format!("âš {} universes dark", dark.len()))
.on_hover_text(format!(
"gled renders up to {UNIVERSE_SLOTS} universes at once and this \
patch uses {}: universes {first} to {last} get no output.",
addressing.universes().len()
));
}
});
});
canvas::show(ui, edit, &mut self.state, collections);
apply
}
fn undo_buttons(
&mut self,
ui: &mut Ui,
edit: &mut PatchEdit<'_>,
collections: &Collections,
status: DraftStatus,
) {
let shortcut = |modifiers, key| {
ui.ctx()
.format_shortcut(&egui::KeyboardShortcut::new(modifiers, key))
};
let redo_hint = format!(
"Redo ({})",
shortcut(
egui::Modifiers::COMMAND | egui::Modifiers::SHIFT,
egui::Key::Z
)
);
let undo_hint = format!(
"Undo ({})",
shortcut(egui::Modifiers::COMMAND, egui::Key::Z)
);
if ui
.add_enabled(
status.can_redo,
egui::Button::new(icons::ARROW_CLOCKWISE.regular()),
)
.on_hover_text(redo_hint)
.clicked()
{
process_transaction(edit, &mut self.state, collections, Redo);
}
if ui
.add_enabled(
status.can_undo,
egui::Button::new(icons::ARROW_COUNTER_CLOCKWISE.regular()),
)
.on_hover_text(undo_hint)
.clicked()
{
process_transaction(edit, &mut self.state, collections, Undo);
}
}
fn mode_picker(&mut self, ui: &mut Ui, edit: &mut PatchEdit<'_>) {
ui.scope_builder(UiBuilder::new().id(Id::new("editor mode bar")), |ui| {
ui.horizontal(|ui| self.mode_controls(ui, edit));
});
}
fn mode_controls(&mut self, ui: &mut Ui, edit: &mut PatchEdit<'_>) {
let layout_ref = self.state.selection;
let Some(layout) = edit.value.layout(layout_ref) else {
return;
};
let names: Vec<String> = layout.modes.iter().map(|mode| mode.name.clone()).collect();
let selected = self.state.selected_mode.min(names.len().saturating_sub(1));
let mut picked = None;
let mut add = false;
let mut remove = false;
let mut renamed = names.get(selected).cloned().unwrap_or_default();
let mut rename = None;
ui.label("Mode");
egui::ComboBox::from_id_salt("editor layout mode")
.selected_text(names.get(selected).cloned().unwrap_or_default())
.show_ui(ui, |ui| {
for (index, name) in names.iter().enumerate() {
if ui.selectable_label(index == selected, name).clicked() {
picked = Some(index);
}
}
});
if ui
.button(icons::PLUS.regular())
.on_hover_text("Add a mode to this layout")
.clicked()
{
add = true;
}
if selected > 0 {
if ui
.button(icons::TRASH.regular().color(Color32::RED))
.on_hover_text("Remove this mode")
.clicked()
{
remove = true;
}
rename = Some(
ui.add(
TextEdit::singleline(&mut renamed)
.char_limit(NAME_CHAR_LIMIT)
.desired_width(110.0)
.hint_text("Mode name"),
),
);
}
if let Some(index) = picked {
self.state.selected_mode = index;
}
if let Some(response) = rename {
edit.tracked(&response, |patch| {
if let Some(layout) = patch.layout_mut(layout_ref) {
layout.mode_mut(selected).name = renamed;
}
});
}
if add
&& let Some(added) =
edit.apply(|patch| patch.layout_mut(layout_ref).map(|layout| layout.add_mode()))
{
self.state.selected_mode = added;
}
if remove && selected > 0 {
edit.apply(|patch| {
if let Some(layout) = patch.layout_mut(layout_ref) {
layout.remove_mode(selected);
}
});
self.state.selected_mode = 0;
}
}
fn add_fixture_button(
&mut self,
ui: &mut Ui,
edit: &mut PatchEdit<'_>,
collections: &Collections,
) {
let mut definitions = Asset::<FixtureDefinition>::all(collections);
let add: Atoms = (icons::LIGHTBULB.fill(), "Add Fixtures").into_atoms();
if definitions.is_empty() {
if ui
.add(egui::Button::new(add))
.on_hover_text("No fixture definitions yet: draw the first one")
.clicked()
{
self.definitions_request = Some(None);
}
return;
}
definitions.sort_by(|a, b| a.path.cmp(&b.path));
let mut chosen = None;
egui::containers::menu::MenuButton::new(add).ui(ui, |ui| {
if ui.button("Edit fixture definitions…").clicked() {
self.definitions_request = Some(None);
ui.close();
}
ui.separator();
let height =
(ui.ctx().content_rect().bottom() - ui.next_widget_position().y - 16.0).max(120.0);
egui::ScrollArea::vertical()
.max_height(height)
.show(ui, |ui| {
for definition in definitions {
if self.definition_entry(ui, &definition).clicked() {
chosen = Some(definition.id);
ui.close();
}
}
});
});
if let Some(definition) = chosen {
let at = self.state.view.center;
process_transaction(
edit,
&mut self.state,
collections,
AddFixture(LayoutPosition::at(at), Some(definition)),
);
}
}
fn definition_entry(
&mut self,
ui: &mut Ui,
definition: &Asset<FixtureDefinition>,
) -> egui::Response {
const PREVIEW: Vec2 = Vec2::new(96.0, 54.0);
const ROW_WIDTH: f32 = 340.0;
let photo = definition
.data
.photo
.as_ref()
.and_then(|photo| self.state.caches.images.texture(ui.ctx(), &photo.file));
let caches = &mut self.state.caches;
ui.scope_builder(
UiBuilder::new()
.id(Id::new(("definition entry", definition.id.id)))
.sense(Sense::click()),
|ui| {
let response = ui.response();
Frame::new()
.fill(if response.hovered() {
Color32::from_gray(60)
} else {
Color32::TRANSPARENT
})
.inner_margin(4.0)
.show(ui, |ui| {
ui.set_width(ROW_WIDTH);
ui.horizontal(|ui| {
ui.set_height(PREVIEW.y);
let (outline, _) = ui.allocate_exact_size(PREVIEW, Sense::hover());
draw_definition_fitted(
&ui.painter_at(outline),
outline.shrink(3.0),
&definition.data,
Some(definition.id.id),
caches,
);
let (slot, _) = ui.allocate_exact_size(PREVIEW, Sense::hover());
if let Some(photo) = &photo {
egui::Image::new(photo)
.corner_radius(3.0)
.paint_at(ui, fit_into(photo.size_vec2(), slot));
}
ui.vertical(|ui| {
ui.add(
egui::Label::new(
egui::RichText::new(definition.name()).strong(),
)
.selectable(false),
);
ui.add(
egui::Label::new(
egui::RichText::new(format!(
"{} px · {} ch",
definition.data.pixels.len(),
definition.data.channel_count()
))
.small()
.color(Color32::from_gray(160)),
)
.selectable(false),
);
});
});
});
},
)
.response
}
}
fn dark_universes(universes: &[u16]) -> &[u16] {
universes.get(UNIVERSE_SLOTS as usize..).unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_universes_past_the_slots_stay_dark() {
let slots = UNIVERSE_SLOTS as usize;
let universes: Vec<u16> = (0..slots as u16 + 44).map(|i| i * 2).collect();
assert!(dark_universes(&universes[..slots]).is_empty());
assert_eq!(dark_universes(&universes), &universes[slots..]);
assert!(dark_universes(&[]).is_empty());
}
}