gled-ui 2.33.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! The fixture definition editor: a window that lists the definitions like
//! every asset window and draws the selected one on a canvas, where its
//! pixels are laid out, numbered in DMX order and grouped into modes.

mod canvas;
mod canvas_geometry;
mod draw_tools;
mod edit;
mod generators;
mod inspector;
mod models;
mod modes;
mod picked_file;
mod properties;
mod shape;
mod state;
mod svg_path;
mod toolbar;
mod units;

use crate::assets::AssetEdits;
use crate::canvas::test_screen;
use crate::canvas::undo::UndoStack;
use crate::fixture_definition_editor::edit::Edit;
use crate::fixture_definition_editor::state::EditorState;
use crate::ui::asset_tree::{AssetTree, TREE_WIDTH, TreeSelection};
use crate::ui::file_pick::FilePick;
use crate::ui::window_common::{
    embedded_close_clicked, gled_window_frame, raise, tool_window_builder,
};
use egui::{
    CollapsingHeader, Context, Id, Key, KeyboardShortcut, Modifiers, ScrollArea, Ui, Vec2,
    ViewportId, vec2,
};
use gled_model::asset::Asset;
use gled_model::asset_id::AssetId;
use gled_model::collection::Collections;
use gled_model::patching::definition::FixtureDefinition;
use std::sync::Arc;

const TITLE: &str = "Fixture Definitions";

#[derive(Debug, Default)]
pub(crate) struct FixtureDefinitionsWindow {
    open: bool,
    /// Set while the window still has to be brought forward.
    raise: bool,
    dirty: bool,
    tree: AssetTree<FixtureDefinition>,
    state: EditorState,
    undo: UndoStack<FixtureDefinition>,
    /// A definition to show once the window draws next.
    open_request: Option<AssetId<FixtureDefinition>>,
}

impl FixtureDefinitionsWindow {
    #[cfg_attr(feature = "profiling", profiling::function)]
    pub(crate) fn update(&mut self, ctx: &Context, collections: &mut Collections) {
        if !self.open {
            self.dirty = false;
            return;
        }
        if let Some(id) = self.open_request.take()
            && let Some(asset) = Asset::get(id, collections)
        {
            self.tree.selection = TreeSelection::Asset(Arc::unwrap_or_clone(asset));
            self.dirty = false;
        }

        ctx.show_viewport_immediate(
            ViewportId(Id::new("fixture definitions window")),
            tool_window_builder(TITLE)
                .with_inner_size(Vec2::new(1280.0, 820.0))
                .with_min_inner_size(Vec2::new(900.0, 600.0)),
            |ctx, _viewport_class| {
                raise(ctx, &mut self.raise);
                // before any field below has handled this frame's keys
                self.state.typing = ctx.text_edit_focused();
                ctx.input(|input| {
                    if input.viewport().close_requested() || embedded_close_clicked(TITLE) {
                        self.open = false;
                        // a file still being chosen is for a window that is gone
                        self.state.file_pick = FilePick::default();
                    }
                });

                // Ctrl+S saves, as the Save button does
                if ctx.input_mut(|input| {
                    input.consume_shortcut(&KeyboardShortcut::new(Modifiers::COMMAND, Key::S))
                }) && let TreeSelection::Asset(asset) = &self.tree.selection
                {
                    asset.clone().save(collections);
                    self.dirty = false;
                }

                gled_window_frame(ctx, TITLE, |ui| {
                    egui::Panel::left("fixture definitions tree")
                        .exact_size(TREE_WIDTH)
                        .resizable(false)
                        .show(ui, |ui| {
                            if self.tree.show(
                                ui,
                                ui.make_persistent_id("fixture_definitions_tree"),
                                collections,
                            ) {
                                self.dirty = false;
                            }
                        });

                    egui::CentralPanel::default().show(ui, |ui| {
                        self.tree.common_settings(ui, &mut self.dirty, collections);
                        self.follow_selection();
                        if let TreeSelection::Asset(asset) = &mut self.tree.selection {
                            editor(ui, asset, &mut self.state, &mut self.undo, &mut self.dirty);
                        }
                    });
                });
            },
        );
    }

    pub(crate) fn open(&mut self) {
        self.open = true;
        self.raise = true;
    }

    /// Opens the window on `definition`, or on whatever it showed last.
    pub(crate) fn open_with(&mut self, definition: Option<AssetId<FixtureDefinition>>) {
        self.open();
        self.open_request = definition;
    }

    /// Starts afresh when the tree selects another definition. A definition
    /// reset from disk keeps its history, so the reset can be undone.
    fn follow_selection(&mut self) {
        match &self.tree.selection {
            TreeSelection::Asset(asset) if self.state.editing != Some(asset.id.id) => {
                self.state.reset_for(Some(asset.id.id), &asset.data);
                self.undo.clear();
            }
            TreeSelection::Asset(asset) => self.state.clamp(&asset.data),
            _ if self.state.editing.is_some() => {
                self.state.editing = None;
                self.undo.clear();
            }
            _ => {}
        }
    }
}

/// The selected definition: panels on the right, the canvas in the middle.
fn editor(
    ui: &mut Ui,
    asset: &mut Asset<FixtureDefinition>,
    state: &mut EditorState,
    undo: &mut UndoStack<FixtureDefinition>,
    dirty: &mut bool,
) {
    let asset_id = asset.id.id;
    let mut edit = Edit::new(&mut asset.data, undo);
    state.caches.begin_frame();
    if let Some(picked) = state.file_pick.take() {
        picked_file::apply_picked_file(&ui.ctx().clone(), &mut edit, state, asset_id, picked);
    }

    egui::Panel::right("fixture definition panels")
        .default_size(340.0)
        .size_range(280.0..=560.0)
        .resizable(true)
        .show(ui, |ui| {
            ScrollArea::vertical().show(ui, |ui| {
                CollapsingHeader::new("Selection")
                    .default_open(true)
                    .show(ui, |ui| inspector::show(ui, &mut edit, state));
                CollapsingHeader::new("Modes")
                    .default_open(true)
                    .show(ui, |ui| modes::show(ui, &mut edit, state));
                CollapsingHeader::new("Test screen").show(ui, |ui| {
                    let width = ui.available_width();
                    let (rect, _) =
                        ui.allocate_exact_size(vec2(width, width * 0.75), egui::Sense::hover());
                    test_screen::show_definition(ui, rect, edit.value, state.mode);
                });
                CollapsingHeader::new("Models")
                    .default_open(true)
                    .show(ui, |ui| models::show(ui, asset_id, &mut edit, state));
                CollapsingHeader::new("Fixture")
                    .default_open(true)
                    .show(ui, |ui| properties::show(ui, &mut edit, state));
            });
        });

    egui::Panel::top("fixture definition toolbar")
        .show(ui, |ui| toolbar::show(ui, &mut edit, state));
    egui::CentralPanel::default().show(ui, |ui| canvas::show(ui, asset_id, &mut edit, state));
    generators::show(ui.ctx(), &mut edit, state);
    *dirty |= edit.changed();
}