use crate::fixture_definition_editor::edit::{DefinitionEdit, Edit};
use crate::fixture_definition_editor::picked_file::DefinitionFile;
use crate::fixture_definition_editor::state::EditorState;
use crate::fixture_definition_editor::units::millimetres;
use crate::ui::file_pick;
use egui::{Button, ComboBox, Grid, Image, Slider, Ui, vec2};
use gled_model::patching::definition::{DEFAULT_PHOTO_OPACITY, FixtureDefinition, Photo, Scaling};
use std::path::PathBuf;
use strum::{EnumMessage, IntoEnumIterator};
const PHOTO_LENGTHS: [(&str, f32, fn(&mut Photo) -> &mut f32); 4] = [
("Width", 0.0, |photo| &mut photo.size_m[0]),
("Height", 0.0, |photo| &mut photo.size_m[1]),
("X", -MAX_LENGTH_M, |photo| &mut photo.offset_m[0]),
("Y", -MAX_LENGTH_M, |photo| &mut photo.offset_m[1]),
];
const MAX_LENGTH_M: f32 = 100.0;
pub(crate) fn show(ui: &mut Ui, edit: &mut Edit<'_>, state: &mut EditorState) {
ui.label(format!(
"{} pixels · {} channels",
edit.value.pixels.len(),
edit.value.channel_count()
));
Grid::new("fixture properties")
.num_columns(2)
.show(ui, |ui| {
for (axis, label) in ["Width", "Height", "Depth"].into_iter().enumerate() {
ui.label(label);
let mut metres = edit.value.size_m[axis];
let response = ui.add(millimetres(&mut metres).range(0.0..=MAX_LENGTH_M));
edit.tracked(&response, |definition| definition.size_m[axis] = metres);
ui.end_row();
}
ui.label("Distortion");
let mut scaling = edit.value.scaling;
ComboBox::from_id_salt("fixture scaling")
.selected_text(scaling.to_string())
.show_ui(ui, |ui| {
for candidate in Scaling::iter() {
ui.selectable_value(&mut scaling, candidate, candidate.to_string())
.on_hover_text(candidate.get_message().unwrap_or_default());
}
});
if scaling != edit.value.scaling {
edit.change(state, |definition| definition.scaling = scaling);
}
ui.end_row();
});
ui.horizontal(|ui| {
if ui.button("Fit size to pixels").clicked() {
edit.change(state, |definition| definition.fit_size());
}
let has_photo = edit.value.photo.is_some();
if ui
.add_enabled(has_photo, Button::new("Fit size to photo"))
.on_hover_text(
"The fixture's frame, which the patch editor selects and outlines, covers the whole photo",
)
.clicked()
{
edit.change(state, |definition| definition.fit_size_to_photo());
}
if ui
.button("Centre pixels")
.on_hover_text("A layout places and turns the fixture about its origin; the photo moves along")
.clicked()
{
edit.change(state, |definition| definition.centre_pixels());
}
});
ui.separator();
photo(ui, edit, state);
}
fn photo(ui: &mut Ui, edit: &mut Edit<'_>, state: &mut EditorState) {
ui.label("Photo");
let photo = edit.value.photo.clone();
ui.horizontal(|ui| {
if let Some(texture) = photo
.as_ref()
.and_then(|photo| state.caches.images.texture(ui.ctx(), &photo.file))
{
ui.add(
Image::new(&texture)
.fit_to_exact_size(vec2(96.0, 54.0))
.corner_radius(3.0),
);
}
choose_photo(ui, state);
if photo.is_some() && ui.button("Clear").clicked() {
edit.change(state, |definition| definition.photo = None);
}
});
let Some(mut photo) = photo else {
return;
};
Grid::new("fixture photo").num_columns(2).show(ui, |ui| {
ui.label("Opacity");
let mut percent = photo.opacity * 100.0;
let response = ui
.add(
Slider::new(&mut percent, 0.0..=100.0)
.suffix(" %")
.max_decimals(0),
)
.on_hover_text("Faint, the photo shows the lamp without hiding its pixels");
edit.tracked(&response, |definition| {
with_photo(definition, |photo| photo.opacity = percent / 100.0)
});
ui.end_row();
for (label, min_m, length) in PHOTO_LENGTHS {
ui.label(label);
let mut metres = *length(&mut photo);
let response = ui.add(millimetres(&mut metres).range(min_m..=MAX_LENGTH_M));
edit.tracked(&response, |definition| {
with_photo(definition, |photo| *length(photo) = metres)
});
ui.end_row();
}
});
}
fn with_photo(definition: &mut FixtureDefinition, change: impl FnOnce(&mut Photo)) {
if let Some(photo) = &mut definition.photo {
change(photo);
}
}
pub(crate) fn new_photo(file: PathBuf, fixture_size_m: [f32; 3], aspect: Option<f32>) -> Photo {
let aspect = aspect
.filter(|aspect| aspect.is_finite() && *aspect > 0.0)
.unwrap_or(1.0);
let [width, height] = [fixture_size_m[0].abs(), fixture_size_m[1].abs()];
let fit_width = if width > 0.0 { width } else { 1.0 };
let fit_height = if height > 0.0 { height } else { f32::INFINITY };
let width = fit_width.min(fit_height * aspect);
Photo {
file,
size_m: [width, width / aspect],
offset_m: [0.0, 0.0],
opacity: DEFAULT_PHOTO_OPACITY,
}
}
fn choose_photo(ui: &mut Ui, state: &mut EditorState) {
if ui
.add_enabled(!state.file_pick.is_open(), Button::new("Choose…"))
.clicked()
{
state.ask_file(ui.ctx(), file_pick::PHOTOS, DefinitionFile::photo);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_new_photo_fits_the_fixture_undistorted() {
let photo = new_photo("lamp.png".into(), [0.6, 0.6, 0.05], Some(2.0));
assert_eq!(photo.size_m, [0.6, 0.3]);
assert_eq!(photo.opacity, DEFAULT_PHOTO_OPACITY);
}
#[test]
fn a_photo_for_a_flat_fixture_takes_its_width() {
let photo = new_photo("bar.png".into(), [1.2, 0.0, 0.0], Some(4.0));
assert_eq!(photo.size_m, [1.2, 0.3]);
}
#[test]
fn a_photo_for_a_fixture_without_size_is_a_metre_wide() {
let photo = new_photo("lamp.png".into(), [0.0; 3], None);
assert_eq!(photo.size_m, [1.0, 1.0]);
}
}