gled-ui 2.31.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! The fixtures on the patch canvas, chain by chain: drawn, wired, selected
//! and dragged.

use crate::canvas::frame::CanvasFrame;
use crate::canvas::scrollbars::panning;
use crate::canvas::selection::ClickIntent;
use crate::canvas::shapes::{Shapes, draw_edge};
use crate::fixture_editor::canvas::{Definitions, continue_drag, definition_of, draw_layered_grid};
use crate::fixture_editor::chains::{draw_order, fixture_colors};
use crate::fixture_editor::commands::EditorTransaction::{MakeLeaf, MakeSuccessor};
use crate::fixture_editor::commands::{EditorTransaction, apply_transaction};
use crate::fixture_editor::drag::{Drag, EndDrag};
use crate::fixture_editor::fixture_ui::FixtureUI;
use crate::fixture_editor::selection_handles::{
    box_selection, draw_bounding_box, draw_rotation_handle, selected_positions,
};
use crate::fixture_editor::state::{CanvasMode, EditorState, PatchEdit};
use egui::epaint::PathShape;
use egui::{Color32, DragAndDrop, PointerButton, Pos2, Shape, Stroke};
use gled_model::collection::Collections;
use gled_model::patching::Patch;
use gled_model::patching::definition::Scaling;
use gled_model::patching::fixture::FixtureId;
use gled_model::patching::layout::LayoutRef;
use std::collections::{BTreeMap, HashSet};

#[derive(Clone)]
pub(crate) struct FloatingConnection {
    fixture: FixtureId,
    end: Pos2,
}

pub(super) fn draw_fixtures(
    canvas: &mut CanvasFrame<'_>,
    edit: &mut PatchEdit<'_>,
    state: &mut EditorState,
    collections: &Collections,
    mode: CanvasMode,
    drawn: LayoutRef,
    active: bool,
) {
    let scene_transform = canvas.scene_transform;
    let node_painter = &canvas.painter;
    let canvas_response = &canvas.response;
    let child_ui = &mut *canvas.ui;
    // The grid and the edges belong under every fixture, so they go into a
    // slot taken before any of them is drawn.
    let under_everything = node_painter.add(Shape::Noop);
    let mut edge_painter = Shapes::default();
    let mut overlay_painter = Shapes::default();
    draw_layered_grid(&mut edge_painter, &scene_transform);

    let modifiers = child_ui.input(|i| i.modifiers);
    let cmd_down = modifiers.command;
    let single_selected = state.selected_fixtures.len() == 1;
    let chains = draw_order(edit.value, mode, drawn);

    let definitions: Definitions = chains
        .iter()
        .flatten()
        .filter_map(|id| {
            edit.value
                .definition(*id, collections)
                .map(|asset| (*id, asset))
        })
        .collect();

    let to_scene = scene_transform.inverse();
    let (pointer, press, primary_down) = child_ui.input(|i| {
        (
            i.pointer.interact_pos().map(|at| to_scene * at),
            i.pointer.press_origin().map(|at| to_scene * at),
            i.pointer.primary_down(),
        )
    });
    // Carried to the pointer before anything is drawn, so the fixtures and
    // their handles show where the drag puts them this frame.
    if active
        && !panning(child_ui)
        && let Some(pointer) = pointer
    {
        continue_drag(
            edit,
            state,
            drawn,
            &definitions,
            pointer,
            modifiers,
            scene_transform.scaling,
        );
    }
    if !primary_down
        && let Some(drag) = state.drag.take()
        && drag.edits_patch()
    {
        edit.end();
    }
    let dragging = state.drag.is_some();
    // read only from here on, until what this frame asked for is applied
    let patch: &Patch = edit.value;

    let colors = fixture_colors(patch, state, &chains, mode, collections);
    let successors: BTreeMap<FixtureId, bool> = chains
        .iter()
        .flatten()
        .map(|id| (*id, patch.topology.successor(*id).is_some()))
        .collect();
    let heads: HashSet<FixtureId> = patch.topology.chain_heads().into_iter().collect();
    let ports = &patch.topology.ports;

    let mut deferred_operations: Vec<EditorTransaction> = vec![];
    let mut deferred_selection: Option<FixtureId> = None;
    let mut deferred_port: Option<FixtureId> = None;
    let mut fixture_took_focus = false;
    let mut dnd_released_onto_fixture = false;

    for chain in &chains {
        let mut prev_terminal: Option<Pos2> = None;

        for id in chain {
            let id = *id;
            let Some(position) = patch
                .layout(drawn)
                .and_then(|layout| layout.members.get(&id).copied())
            else {
                continue;
            };

            let is_selected = state.selected_fixtures.contains(&id);
            let attach_network = heads.contains(&id);
            let color = colors.get(&id).copied().unwrap_or(Color32::LIGHT_RED);

            // A bar is defined by its two ends, so it gets a handle at each;
            // anything a layout may distort per axis gets the bounding box
            // instead, even on its own.
            let end_handles = definition_of(&definitions, id).scaling == Scaling::AlongAxis;

            let mut pointer_was_down = state.pointer_down_on.contains(&id);
            let mut fixture_ui = FixtureUI::new(
                id,
                &position,
                definition_of(&definitions, id),
                definitions.get(&id).map(|asset| asset.id.id),
                color,
                is_selected,
                attach_network,
                scene_transform,
            )
            .with_port(ports.get(&id).copied(), state.selected_port == Some(id));

            {
                if let Some(prev_terminal) = &prev_terminal {
                    draw_edge(
                        &mut edge_painter,
                        prev_terminal,
                        &fixture_ui.input_terminal(),
                    );
                }
                prev_terminal = Some(fixture_ui.output_terminal());
            }

            if mode.edits_network() && active {
                let next_connected = successors.get(&id).copied().unwrap_or(false);
                let output_port_response =
                    fixture_ui.node_port(child_ui, node_painter, next_connected);

                if output_port_response.dragged() && !panning(child_ui) {
                    DragAndDrop::set_payload(
                        child_ui.ctx(),
                        FloatingConnection {
                            fixture: id,
                            end: fixture_ui.output_terminal(),
                        },
                    );
                }
            }

            let node_response = fixture_ui.show(
                child_ui,
                node_painter,
                &mut overlay_painter,
                &mut edge_painter,
                canvas_response.clone(),
                single_selected,
                &mut pointer_was_down,
                end_handles && active,
                &mut state.caches,
            );

            if pointer_was_down {
                state.pointer_down_on.insert(id);
            } else {
                state.pointer_down_on.remove(&id);
            }

            if let Some(end) = node_response.end_drag
                && let Some(press) = press
            {
                state.drag = Some(Drag::End(EndDrag::new(
                    id,
                    definition_of(&definitions, id),
                    position,
                    end,
                    press,
                )));
            }

            // handle release of connection payloads
            if let Some(dnd_response) = &node_response.released_payload {
                // Reject a connection that would form a cycle: that happens when
                // the drop target is already an ancestor of the drag source.
                if !patch.topology.is_reachable(id, dnd_response.fixture) {
                    deferred_operations.push(MakeLeaf(dnd_response.fixture));
                    deferred_operations.push(MakeSuccessor(dnd_response.fixture, id));
                    dnd_released_onto_fixture = true;
                }
            }

            // select with click or cmd+click
            // cmd + click only works once egui has decided click vs. drag;
            // a plain click must select instantly so an unselected fixture can
            // be dragged straight away
            if node_response.clicked && !(cmd_down && node_response.dragged) {
                fixture_took_focus = true;
                deferred_selection = Some(id);
            }

            if node_response.network_clicked {
                fixture_took_focus = true;
                deferred_port = Some(id);
            }

            // a fixture that was pressed still awaits egui's verdict, so the
            // background must not react
            if node_response.has_unhandled_pointer_down {
                fixture_took_focus = true;
            }

            // hover outline (unselected fixtures)
            if !node_response.dragged
                && let Some(hover_rect) = node_response.hovered
                && !is_selected
            {
                overlay_painter.add(PathShape::convex_polygon(
                    hover_rect.corners().to_vec(),
                    Color32::TRANSPARENT,
                    Stroke::new(2.0, Color32::from_gray(180)),
                ));
            }

            // Drag-and-drop hover outline. The fixture the connection is
            // dragged from is not a drop target, so it gets no outline at all.
            if let Some((hover_rect, other)) = node_response.hover_payload.clone()
                && other.fixture != id
            {
                let color = if patch.topology.is_reachable(id, other.fixture) {
                    Color32::RED
                } else {
                    Color32::CYAN
                };
                overlay_painter.add(PathShape::convex_polygon(
                    hover_rect.corners().to_vec(),
                    Color32::TRANSPARENT,
                    Stroke::new(5.0, color),
                ));
            }

            // Dragging a selected fixture drags the whole selection along;
            // dragging an unselected one selects only it.
            if !panning(child_ui)
                && node_response.dragged
                && canvas_response.drag_started_by(PointerButton::Primary)
            {
                if !is_selected {
                    state.select_single(id);
                }
                if let Some(press) = press {
                    state.drag = Some(Drag::Move {
                        start_pointer: press,
                        start: selected_positions(patch, state, drawn),
                    });
                }
                fixture_took_focus = true;
            }
        }
    }

    // The canvas under the open picker still has the band state; running it
    // there too would draw the band twice.
    if active && !panning(child_ui) {
        box_selection(
            state,
            canvas_response,
            &mut overlay_painter,
            &scene_transform,
            press.or(pointer),
            modifiers,
            fixture_took_focus,
        );
    }

    // Bounding-box handles: for any multi-selection, and for a single fixture
    // that has no end handles of its own.
    let boxed_selection = state.selected_fixtures.len() > 1
        || state
            .selected_fixtures
            .iter()
            .any(|id| definition_of(&definitions, *id).scaling == Scaling::Free);
    if active && !state.selected_fixtures.is_empty() && boxed_selection {
        draw_bounding_box(
            child_ui,
            patch,
            state,
            drawn,
            &mut overlay_painter,
            &scene_transform,
            &definitions,
            pointer,
            modifiers,
        );
    }
    if active && !state.selected_fixtures.is_empty() {
        draw_rotation_handle(
            child_ui,
            patch,
            state,
            drawn,
            &mut overlay_painter,
            &scene_transform,
            &definitions,
            pointer,
        );
    }

    // a connection dropped onto empty canvas just removes the existing one
    if let Some(released) = canvas_response.dnd_release_payload::<FloatingConnection>()
        && !dnd_released_onto_fixture
    {
        deferred_operations.push(MakeLeaf(released.fixture));
    }
    // A drag that changes the patch and started this frame: the patch as it
    // is now is what undoing it goes back to. egui recognises a drag only
    // once the pointer has moved, so the drag is carried there right away.
    if !dragging && state.drag.as_ref().is_some_and(Drag::edits_patch) {
        edit.begin();
        if let Some(pointer) = pointer {
            continue_drag(
                edit,
                state,
                drawn,
                &definitions,
                pointer,
                modifiers,
                scene_transform.scaling,
            );
        }
    }
    // unhooking a connection and making the new one are one change
    if !deferred_operations.is_empty() {
        edit.apply(|patch| {
            for operation in deferred_operations {
                apply_transaction(patch, state, collections, operation);
            }
        });
    }

    let intent = ClickIntent::from_modifiers(modifiers);
    if let Some(id) = deferred_selection {
        state.click(intent, Some(id));
    }
    if let Some(id) = deferred_port {
        state.select_port(id);
    }

    if !fixture_took_focus && canvas_response.clicked() {
        state.click(intent, None);
    }

    // floating connection line while dragging a port
    let floating = DragAndDrop::payload::<FloatingConnection>(child_ui.ctx());
    if let (Some(fc), Some(hoverpos)) = (floating, child_ui.ctx().pointer_hover_pos()) {
        draw_edge(&mut edge_painter, &fc.end, &hoverpos);
    }

    node_painter.set(under_everything, edge_painter.into_shape());
    node_painter.add(overlay_painter.into_shape());
}