gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
//! The selected pixels' attributes. With several selected, an attribute they
//! share shows its value and one they differ in shows "-"; setting it sets it
//! for all of them.

use crate::canvas::multi_value::{MultiValue, shown};
use crate::fixture_definition_editor::edit::{DefinitionEdit, Edit};
use crate::fixture_definition_editor::generators::DEFAULT_PIXEL_PREFIX;
use crate::fixture_definition_editor::state::EditorState;
use crate::fixture_definition_editor::units::millimetres;
use egui::{ComboBox, DragValue, Grid, Ui};
use gled_model::patching::definition::{ColorOrder, FixtureDefinition, Pixel};
use strum::IntoEnumIterator;

/// Shows the inspector for the current selection.
pub(crate) fn show(ui: &mut Ui, edit: &mut Edit<'_>, state: &mut EditorState) {
    let selection: Vec<usize> = state.selection.iter().copied().collect();
    let pixels: Vec<&Pixel> = selection
        .iter()
        .filter_map(|pixel| edit.value.pixels.get(*pixel))
        .collect();
    match pixels.as_slice() {
        [] => {
            ui.weak("Click a pixel, or drag a band around several.");
            return;
        }
        [pixel] => ui.strong(&pixel.name),
        _ => ui.strong(format!("{} pixels", pixels.len())),
    };

    let collect =
        |value: fn(&Pixel) -> String| MultiValue::collect(pixels.iter().map(|pixel| value(pixel)));
    let model = MultiValue::collect(pixels.iter().map(|pixel| pixel.model));
    let order = collect(|pixel| {
        pixel
            .color_order()
            .map_or("custom".to_string(), |order| order.to_string())
    });
    let differs = |values: Option<MultiValue<u32>>| matches!(values, Some(MultiValue::Differing));
    let position_differs = [0, 1, 2].map(|axis| {
        differs(MultiValue::collect(
            pixels.iter().map(|pixel| pixel.position_m[axis].to_bits()),
        ))
    });
    let rotation_differs = differs(MultiValue::collect(
        pixels.iter().map(|pixel| pixel.rotation.to_bits()),
    ));
    let first = pixels[0].clone();
    let dmx = dmx_span(edit.value, &selection);

    Grid::new("pixel inspector").num_columns(2).show(ui, |ui| {
        if selection.len() == 1 {
            ui.label("Name");
            let mut name = first.name.clone();
            let response = ui.text_edit_singleline(&mut name);
            edit.tracked(&response, |definition| {
                if let Some(pixel) = definition.pixels.get_mut(selection[0]) {
                    pixel.name = name;
                }
            });
            ui.end_row();
        }

        for (axis, label) in ["X", "Y", "Z"].into_iter().enumerate() {
            ui.label(label);
            let mut metres = first.position_m[axis];
            let mut value = millimetres(&mut metres);
            if position_differs[axis] {
                value = value.custom_formatter(|_, _| "-".to_string());
            }
            let response = ui.add(value);
            edit.tracked(&response, |definition| {
                for pixel in &selection {
                    if let Some(pixel) = definition.pixels.get_mut(*pixel) {
                        pixel.position_m[axis] = metres;
                    }
                }
            });
            ui.end_row();
        }

        ui.label("Rotation");
        let mut degrees = first.rotation.to_degrees();
        let mut value = DragValue::new(&mut degrees).speed(1.0).suffix("°");
        if rotation_differs {
            value = value.custom_formatter(|_, _| "-".to_string());
        }
        let response = ui.add(value);
        edit.tracked(&response, |definition| {
            for pixel in &selection {
                if let Some(pixel) = definition.pixels.get_mut(*pixel) {
                    pixel.rotation = degrees.to_radians();
                }
            }
        });
        ui.end_row();

        ui.label("Model");
        let model_name = |index: usize| {
            edit.value
                .model(index)
                .map_or(format!("missing {index}"), |model| model.name.clone())
        };
        let mut chosen = None;
        ComboBox::from_id_salt("pixel model")
            .selected_text(match &model {
                Some(MultiValue::Same(index)) => model_name(*index),
                _ => "-".to_string(),
            })
            .show_ui(ui, |ui| {
                for index in 0..edit.value.models.len() {
                    if ui
                        .selectable_label(
                            model.as_ref().and_then(MultiValue::same) == Some(&index),
                            model_name(index),
                        )
                        .clicked()
                    {
                        chosen = Some(index);
                    }
                }
            });
        if let Some(index) = chosen {
            let selected = state.selection.clone();
            edit.try_change(state, |definition| {
                definition.set_model_of(&selected, index)
            });
        }
        ui.end_row();

        ui.label("Channel order");
        let mut picked = None;
        ComboBox::from_id_salt("pixel order")
            .selected_text(shown(&order))
            .show_ui(ui, |ui| {
                for candidate in ColorOrder::iter() {
                    let current =
                        order.as_ref().and_then(MultiValue::same) == Some(&candidate.to_string());
                    if ui
                        .selectable_label(current, candidate.to_string())
                        .clicked()
                    {
                        picked = Some(candidate);
                    }
                }
            });
        if let Some(candidate) = picked {
            edit.change(state, |definition| {
                for pixel in &selection {
                    if let Some(pixel) = definition.pixels.get_mut(*pixel) {
                        pixel.set_color_order(candidate);
                    }
                }
            });
        }
        ui.end_row();

        ui.label("DMX");
        ui.label(dmx);
        ui.end_row();
    });

    if selection.len() > 1 {
        rename_all(ui, edit, state, &selection);
    }
}

/// The channels the selection occupies, counted from one.
fn dmx_span(definition: &FixtureDefinition, selection: &[usize]) -> String {
    let spans: Vec<(u16, u16)> = selection
        .iter()
        .filter_map(|pixel| {
            let found = definition.pixels.get(*pixel)?;
            // saturating: a malformed definition can hold more channels than u16
            let first = definition.channel_offset(*pixel).saturating_add(1);
            let width = u16::try_from(found.channels.len().max(1)).unwrap_or(u16::MAX);
            Some((first, first.saturating_add(width - 1)))
        })
        .collect();
    match (
        spans.iter().map(|span| span.0).min(),
        spans.iter().map(|span| span.1).max(),
    ) {
        (Some(first), Some(last)) => format!("ch {first}–{last}"),
        _ => String::new(),
    }
}

/// Names the selected pixels with a prefix and a running number, in DMX
/// order.
fn rename_all(ui: &mut Ui, edit: &mut Edit<'_>, state: &mut EditorState, selection: &[usize]) {
    let id = ui.id().with("rename prefix");
    let mut prefix = ui
        .data(|data| data.get_temp::<String>(id))
        .unwrap_or_else(|| DEFAULT_PIXEL_PREFIX.to_string());
    ui.horizontal(|ui| {
        ui.label("Rename as");
        ui.add(egui::TextEdit::singleline(&mut prefix).desired_width(80.0));
        if ui.button("1, 2, 3…").clicked() {
            let prefix = prefix.clone();
            edit.change(state, |definition| {
                for (number, pixel) in selection.iter().enumerate() {
                    if let Some(pixel) = definition.pixels.get_mut(*pixel) {
                        pixel.name = format!("{prefix}{}", number + 1);
                    }
                }
            });
        }
    });
    ui.data_mut(|data| data.insert_temp(id, prefix));
}