gled-ui 2.31.2

gled's egui interface, shared by the desktop app and the browser
Documentation
//! The shapes pixels and the outline are drawn with. A model is an SVG drawn
//! at a physical size; pixels refer to it by index.

use crate::canvas::model_textures::{aspect_bucket, svg_hash};
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, Frame, Image, Ui, vec2};
use gled_model::patching::definition::Model;

const THUMBNAIL: f32 = 36.0;

pub(crate) fn show(
    ui: &mut Ui,
    asset_id: uuid::Uuid,
    edit: &mut Edit<'_>,
    state: &mut EditorState,
) {
    for index in 0..edit.value.models.len() {
        Frame::group(ui.style()).show(ui, |ui| {
            ui.set_width(ui.available_width());
            model_row(ui, asset_id, index, edit, state);
        });
    }
    if edit.value.models.is_empty() {
        ui.weak("No models yet. Pixels need one to be drawn.");
    }

    ui.horizontal(|ui| {
        ui.menu_button("Add model", |ui| {
            let size = edit.value.size_m;
            let presets = [
                ("LED, round 10 mm", Model::circle("led", 0.01)),
                (
                    "LED, square 5 mm",
                    Model::rounded_rect("led", [0.005, 0.005]),
                ),
                (
                    "Tube segment 14 × 90 mm",
                    Model::rounded_rect("segment", [0.014, 0.09]),
                ),
                (
                    "Outline of the fixture",
                    Model::frame("body", [size[0].max(0.05), size[1].max(0.05)]),
                ),
            ];
            for (label, model) in presets {
                if ui.button(label).clicked() {
                    edit.change(state, |definition| definition.models.push(model));
                    ui.close();
                }
            }
        });
        load_svg_button(ui, state);
    });
}

fn model_row(
    ui: &mut Ui,
    asset_id: uuid::Uuid,
    index: usize,
    edit: &mut Edit<'_>,
    state: &mut EditorState,
) {
    let Some(model) = edit.value.models.get(index).cloned() else {
        return;
    };
    ui.horizontal(|ui| {
        let aspect = model.size_m[0] / model.size_m[1].max(f32::EPSILON);
        let key = (
            Some(asset_id),
            index,
            aspect_bucket(aspect),
            svg_hash(&model),
        );
        match state.caches.models.texture(ui.ctx(), key, &model) {
            Some(texture) => {
                ui.add(Image::new(&texture).fit_to_exact_size(vec2(THUMBNAIL, THUMBNAIL)));
            }
            None => {
                ui.add_sized(vec2(THUMBNAIL, THUMBNAIL), egui::Label::new("?"))
                    .on_hover_text("This SVG cannot be drawn");
            }
        }
        ui.vertical(|ui| {
            let mut name = model.name.clone();
            let response = ui.text_edit_singleline(&mut name);
            edit.tracked(&response, |definition| {
                if let Some(model) = definition.models.get_mut(index) {
                    model.name = name;
                }
            });
            ui.horizontal(|ui| {
                for axis in 0..2 {
                    let mut metres = model.size_m[axis];
                    let response =
                        ui.add(millimetres(&mut metres).speed(0.0005).range(0.0001..=10.0));
                    edit.tracked(&response, |definition| {
                        if let Some(model) = definition.models.get_mut(index) {
                            model.size_m[axis] = metres;
                        }
                    });
                }
            });
        });
    });

    ui.horizontal_wrapped(|ui| {
        ui.radio_value(&mut state.current_model, index, "new pixels")
            .on_hover_text("Pixels added from now on are drawn with this model");
        let mut outline = edit.value.outline == Some(index);
        if ui.checkbox(&mut outline, "outline").changed() {
            edit.change(state, |definition| {
                definition.outline = outline.then_some(index);
            });
        }
        let selection = state.selection.clone();
        if ui
            .add_enabled(!selection.is_empty(), Button::new("Use for selection"))
            .clicked()
        {
            edit.try_change(state, |definition| {
                definition.set_model_of(&selection, index)
            });
        }
        if ui.button("Remove").clicked() {
            edit.try_change(state, |definition| definition.remove_model(index));
        }
    });
}

/// Loads an SVG file as a new model, drawn at the size the file gives it.
fn load_svg_button(ui: &mut Ui, state: &mut EditorState) {
    if ui
        .add_enabled(!state.file_pick.is_open(), Button::new("Load SVG…"))
        .clicked()
    {
        state.ask_file(ui.ctx(), file_pick::SVG, DefinitionFile::model);
    }
}