use crate::canvas::multi_value::{MultiValue, shown};
use crate::fixture_editor::FixtureWindow;
use crate::fixture_editor::state::PatchEdit;
use egui::{ComboBox, DragValue, Frame, Grid, Response, ScrollArea, Ui};
use gled_model::collection::Collections;
use gled_model::patching::Patch;
use gled_model::patching::fixture::FixtureId;
use gled_model::patching::layout::LayoutRef;
impl FixtureWindow {
pub(super) fn show_inspector(
&mut self,
ui: &mut Ui,
edit: &mut PatchEdit<'_>,
collections: &Collections,
) {
if let Some(head) = self.state.selected_port {
self.show_port_inspector(ui, edit, head);
return;
}
let selected: Vec<FixtureId> = self
.state
.selected_fixtures
.iter()
.copied()
.filter(|id| edit.value.topology.fixtures.contains_key(id))
.collect();
let (layout, layout_mode) = (self.state.selection, self.state.selected_mode);
ScrollArea::vertical()
.id_salt("fixture inspector")
.auto_shrink([false; 2])
.show(ui, |ui| {
if selected.is_empty() {
ui.label("No selection");
return;
}
let view =
InspectorView::of(edit.value, collections, layout, layout_mode, &selected);
let picked = Frame::new().show(ui, |ui| view.show(ui)).inner;
picked.apply(edit, collections, layout, layout_mode, &selected);
});
}
}
#[derive(Debug)]
struct Picked {
mode: Option<String>,
rotation: Option<(Response, f32)>,
}
impl Picked {
fn apply(
self,
edit: &mut PatchEdit<'_>,
collections: &Collections,
layout: LayoutRef,
layout_mode: usize,
selected: &[FixtureId],
) {
if let Some((response, degrees)) = self.rotation {
edit.tracked(&response, |patch| {
if let Some(layout) = patch.layout_mut(layout) {
for id in selected {
if let Some(position) = layout.members.get_mut(id) {
position.angle = degrees.to_radians();
}
}
}
});
}
if let Some(name) = self.mode {
edit.apply(|patch| {
for id in selected {
let index = patch.definition(*id, collections).and_then(|definition| {
definition
.data
.mode_names()
.iter()
.position(|mode| *mode == name)
});
if let Some(index) = index {
patch.set_fixture_mode(layout, layout_mode, *id, index);
}
}
});
}
}
}
struct InspectorView {
heading: String,
address: Option<MultiValue<String>>,
definition: Option<MultiValue<String>>,
modes: Vec<String>,
mode: Option<MultiValue<String>>,
angles: Vec<f32>,
}
impl InspectorView {
fn of(
patch: &Patch,
collections: &Collections,
layout: LayoutRef,
layout_mode: usize,
selected: &[FixtureId],
) -> Self {
let heading = match selected {
[id] => patch
.fixture(*id)
.map(|fixture| fixture.name.clone())
.unwrap_or_default(),
_ => format!("{} fixtures selected", selected.len()),
};
let addresses = patch.addresses(collections);
let address = MultiValue::collect(
selected
.iter()
.map(|id| addresses.get(id).map(|a| a.to_string()).unwrap_or_default()),
);
let definition = MultiValue::collect(selected.iter().map(|id| {
patch
.definition(*id, collections)
.map(|definition| definition.name().to_string())
.unwrap_or_else(|| "none".to_string())
}));
let mode = MultiValue::collect(selected.iter().map(|id| {
patch
.fixture_mode(layout, layout_mode, *id, collections)
.and_then(|index| {
let definition = patch.definition(*id, collections)?;
Some(definition.data.mode(index)?.name.clone())
})
.unwrap_or_default()
}));
let angles = patch
.layout(layout)
.map(|layout| {
selected
.iter()
.filter_map(|id| layout.members.get(id))
.map(|position| shown_degrees(position.angle))
.collect()
})
.unwrap_or_default();
Self {
heading,
address,
definition,
modes: common_modes(patch, collections, selected),
mode,
angles,
}
}
fn show(&self, ui: &mut Ui) -> Picked {
ui.vertical(|ui| {
ui.heading(&self.heading);
Grid::new("fixture inspector")
.num_columns(2)
.spacing([8.0, 4.0])
.show(ui, |ui| {
ui.label("Definition");
ui.label(shown(&self.definition));
ui.end_row();
ui.label("Address");
ui.label(shown(&self.address))
.on_hover_text("Universe.channel, derived from the topology");
ui.end_row();
ui.label("Mode");
let mode = self.mode_combo(ui);
ui.end_row();
ui.label("Rotation");
let rotation = self.rotation(ui);
ui.end_row();
Picked { mode, rotation }
})
.inner
})
.inner
}
fn rotation(&self, ui: &mut Ui) -> Option<(Response, f32)> {
let first = *self.angles.first()?;
let differs = self.angles.iter().any(|angle| (angle - first).abs() > 0.05);
let mut degrees = first;
let mut value = DragValue::new(&mut degrees)
.speed(1.0)
.range(-180.0..=180.0)
.max_decimals(1)
.suffix("°");
if differs {
value = value.custom_formatter(|_, _| "-".to_string());
}
let response = ui
.add(value)
.on_hover_text("Clockwise; the handle above the selection turns it too");
Some((response, degrees))
}
fn mode_combo(&self, ui: &mut Ui) -> Option<String> {
let mut picked: Option<String> = None;
ui.add_enabled_ui(!self.modes.is_empty(), |ui| {
ComboBox::from_id_salt("fixture mode")
.selected_text(shown(&self.mode))
.show_ui(ui, |ui| {
for name in &self.modes {
let selected = self.mode.as_ref().and_then(MultiValue::same) == Some(name);
if ui.selectable_label(selected, name).clicked() {
picked = Some(name.clone());
}
}
});
});
picked
}
}
fn common_modes(patch: &Patch, collections: &Collections, selected: &[FixtureId]) -> Vec<String> {
let mut common: Option<Vec<String>> = None;
for id in selected {
let names: Vec<String> = patch
.definition(*id, collections)
.map(|definition| {
definition
.data
.mode_names()
.into_iter()
.map(str::to_string)
.collect()
})
.unwrap_or_default();
common = Some(match common {
None => names,
Some(existing) => existing
.into_iter()
.filter(|name| names.contains(name))
.collect(),
});
}
common.unwrap_or_default()
}
fn shown_degrees(angle: f32) -> f32 {
let degrees = angle.to_degrees().rem_euclid(360.0);
if degrees > 180.0 {
degrees - 360.0
} else {
degrees
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rotations_are_shown_between_minus_and_plus_half_a_turn() {
assert!((shown_degrees(270f32.to_radians()) + 90.0).abs() < 1e-3);
assert!((shown_degrees(-450f32.to_radians()) + 90.0).abs() < 1e-3);
assert!((shown_degrees(30f32.to_radians()) - 30.0).abs() < 1e-3);
}
}