gled-ui 2.31.2

gled's egui interface, shared by the desktop app and the browser
Documentation
//! The picture of the rig in the membership picker.
//!
//! Choosing what a layout holds is not editing, so this is not an editor:
//! there is nothing here to drag, zoom or wire. The rig is drawn once at a
//! fixed fit, as plain outlines at the positions the topology gives them —
//! enough to tell which fixture is which, which is all the choice needs.

use crate::canvas::box_select::draw_band;
use crate::canvas::caches::Caches;
use crate::canvas::definition_drawing::{Placement, draw_outline, fixture_footprint};
use crate::canvas::shapes::Shapes;
use crate::canvas::view::fit_scene_square;
use egui::emath::TSTransform;
use egui::{Color32, PointerButton, Pos2, Rect, Response, Sense, Ui};
use gled_model::collection::Collections;
use gled_model::patching::Patch;
use gled_model::patching::fixture::FixtureId;
use gled_model::patching::layout::LayoutRef;

/// How small a fixture may be drawn before it is blown up. A fixture nobody
/// can see is a fixture nobody can pick.
const MIN_FIXTURE_PX: f32 = 12.0;

const IN_LAYOUT: Color32 = Color32::from_rgb(110, 200, 110);
const IN_LAYOUT_HOVERED: Color32 = Color32::from_rgb(175, 255, 175);
const AVAILABLE: Color32 = Color32::from_gray(85);
const AVAILABLE_HOVERED: Color32 = Color32::from_gray(150);

/// A rubber band, in screen coordinates. It lives only as long as the drag.
pub(crate) type Band = Option<(Pos2, Pos2)>;

/// Draws the rig and returns the membership the user changed.
pub(crate) fn member_preview(
    ui: &mut Ui,
    patch: &Patch,
    layout: LayoutRef,
    collections: &Collections,
    caches: &mut Caches,
    band: &mut Band,
) -> Vec<(FixtureId, bool)> {
    let (rect, response) =
        ui.allocate_exact_size(ui.available_size_before_wrap(), Sense::click_and_drag());
    let to_global = fit_scene_square(rect);
    let painter = ui.painter_at(rect);

    let layout = patch.layout(layout);
    let member = |id: &FixtureId| layout.is_some_and(|layout| layout.contains(*id));
    let pointer = ui
        .ctx()
        .pointer_hover_pos()
        .filter(|position| rect.contains(*position));
    let scene_pointer = pointer.map(|position| to_global.inverse() * position);

    let mut clicked = None;
    let mut covered = vec![];
    let band_rect = band.map(|(start, end)| Rect::from_two_pos(start, end));

    for (id, position) in &patch.topology.default_layout.members {
        let Some(definition) = patch.definition(*id, collections) else {
            continue;
        };
        let footprint = fixture_footprint(
            &definition.data,
            position,
            to_global.scaling,
            MIN_FIXTURE_PX,
        );
        let hovered = scene_pointer.is_some_and(|at| footprint.contains(at));
        if hovered {
            clicked = Some(*id);
        }
        if let Some(band_rect) = band_rect
            && band_rect.intersects(footprint.transformed(to_global).bounds())
        {
            covered.push(*id);
        }

        let member = member(id);
        let placement = Placement {
            position,
            to_global,
            min_screen_px: MIN_FIXTURE_PX,
        };
        draw_outline(
            &painter,
            &definition.data,
            Some(definition.id.id),
            &placement,
            match (member, hovered) {
                (true, false) => IN_LAYOUT,
                (true, true) => IN_LAYOUT_HOVERED,
                (false, false) => AVAILABLE,
                (false, true) => AVAILABLE_HOVERED,
            },
            caches,
        );
    }

    if let Some(band_rect) = band_rect {
        // the band is in screen coordinates already
        let mut shapes = Shapes::default();
        draw_band(&mut shapes, &TSTransform::IDENTITY, band_rect);
        painter.add(shapes.into_shape());
    }

    let mut moves = vec![];
    if response.clicked()
        && let Some(id) = clicked
    {
        moves.push((id, !member(&id)));
    }
    // alt takes the covered fixtures back out again, as the hint says
    let including = !ui.input(|input| input.modifiers.alt);
    moves.extend(drag(&response, band, covered).map(|id| (id, including)));
    moves
}

/// Runs the rubber band, and on release hands back what it covered. A band
/// with no area is a click egui happened to call a drag, and sweeps nothing.
fn drag(
    response: &Response,
    band: &mut Band,
    covered: Vec<FixtureId>,
) -> impl Iterator<Item = FixtureId> {
    if response.drag_started_by(PointerButton::Primary)
        && let Some(at) = response.interact_pointer_pos()
    {
        *band = Some((at, at));
    }
    if response.dragged()
        && let Some((start, _)) = *band
        && let Some(at) = response.interact_pointer_pos()
    {
        *band = Some((start, at));
    }
    let swept = response.drag_stopped().then(|| band.take()).flatten();
    swept
        .filter(|(start, end)| (*end - *start).length() > 1.0)
        .map(|_| covered)
        .unwrap_or_default()
        .into_iter()
}