gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
//! The canvas a definition is drawn on: its pixels at their real size, with
//! their DMX numbers, grouped by colour in the edited mode. Pixels are picked
//! and moved with the pointer, scaled and turned by the box around them.

mod paint;

use crate::canvas::box_scale::{BoxScale, HANDLE_HIT, box_handles, scaled_box};
use crate::canvas::box_select::Band;
use crate::canvas::frame::{CanvasSpec, show_canvas};
use crate::canvas::rotate_handle::{RotateDrag, rotation_handle};
use crate::canvas::scrollbars::panning;
use crate::canvas::selection::ClickIntent;
use crate::canvas::shapes::Shapes;
use crate::fixture_definition_editor::canvas_geometry::{
    covered_pixels, origin, pixel_under, selection_bbox, to_metres, to_scene,
};
use crate::fixture_definition_editor::draw_tools::{self, Pointer, Tool};
use crate::fixture_definition_editor::edit::{DefinitionEdit, Edit, context_actions};
use crate::fixture_definition_editor::generators::{self, GeneratorKind};
use crate::fixture_definition_editor::state::{Drag, EditorState};
use crate::fixture_definition_editor::toolbar;
use egui::emath::{Rot2, TSTransform};
use egui::{Key, Modifiers, PointerButton, Pos2, Response, Sense, Ui, Vec2, vec2};
use gled_model::patching::fixture::UNITS_PER_METRE;

/// How far in the canvas zooms: a 5 mm LED is then a good thumb wide.
const MAX_ZOOM: f32 = 64.0;
/// Smallest a pixel is on screen for pointing at it, in screen pixels.
const MIN_PIXEL_PX: f32 = 8.0;
/// How far an arrow key moves the selection, in metres; ten times that with
/// shift.
const NUDGE_M: f32 = 0.001;

/// Draws the canvas into the space left in `ui` and handles its input.
pub(crate) fn show(
    ui: &mut Ui,
    asset_id: uuid::Uuid,
    edit: &mut Edit<'_>,
    state: &mut EditorState,
) {
    let side = state.scene_side;
    let mut view = state.view;
    let spec = CanvasSpec {
        id_salt: "fixture definition canvas",
        scene_size: Vec2::splat(side),
        max_zoom: MAX_ZOOM,
        sense: Sense::click_and_drag(),
    };
    let shown = show_canvas(ui, spec, &mut view, |canvas| {
        toolbar::keys(canvas.ui.ctx(), edit, state);
        if let Some(action) = context_actions(&canvas.response) {
            action.apply(edit, state);
        }
        nudge(canvas.ui, edit, state);

        let mut frame = Frame {
            edit,
            state,
            scene_transform: canvas.scene_transform,
            overlay: Shapes::default(),
        };
        frame.interact(canvas.ui, &canvas.response);
        frame.paint(&canvas.painter, asset_id, &canvas.response);
    });

    // an edit that grew the scene moved its origin; the view goes along
    if state.scene_side != side {
        view.center += origin(state.scene_side) - origin(side);
    }
    state.view = view;
    state.zoom = shown.zoom;
    state.canvas_rect = shown.clip_rect;
}

/// The arrow keys move the selection a millimetre, ten with shift.
fn nudge(ui: &Ui, edit: &mut Edit<'_>, state: &mut EditorState) {
    if state.selection.is_empty() || ui.ctx().text_edit_focused() {
        return;
    }
    let mut step = Vec2::ZERO;
    ui.input_mut(|input| {
        for (key, direction) in [
            (Key::ArrowLeft, vec2(-1.0, 0.0)),
            (Key::ArrowRight, vec2(1.0, 0.0)),
            (Key::ArrowUp, vec2(0.0, -1.0)),
            (Key::ArrowDown, vec2(0.0, 1.0)),
        ] {
            if input.consume_key(Modifiers::SHIFT, key) {
                step += direction * 10.0;
            } else if input.consume_key(Modifiers::NONE, key) {
                step += direction;
            }
        }
    });
    if step == Vec2::ZERO {
        return;
    }
    let selection = state.selection.clone();
    edit.change(state, |definition| {
        for pixel in &selection {
            if let Some(pixel) = definition.pixels.get_mut(*pixel) {
                pixel.position_m[0] += step.x * NUDGE_M;
                pixel.position_m[1] += step.y * NUDGE_M;
            }
        }
    });
}

/// One frame of the canvas.
struct Frame<'a, 'b> {
    edit: &'a mut Edit<'b>,
    state: &'a mut EditorState,
    /// Scene units to screen pixels.
    scene_transform: TSTransform,
    /// Drawn over the definition: the box, its handles and the band.
    overlay: Shapes,
}

impl Frame<'_, '_> {
    fn to_scene_point(&self, screen: Pos2) -> Pos2 {
        self.scene_transform.inverse() * screen
    }

    /// Half of the smallest pixel that can still be pointed at, in scene
    /// units.
    fn min_half(&self) -> f32 {
        MIN_PIXEL_PX / 2.0 / self.scene_transform.scaling
    }

    fn interact(&mut self, ui: &mut Ui, response: &Response) {
        let (pointer, press, modifiers, primary_down) = ui.input(|input| {
            (
                input.pointer.interact_pos(),
                input.pointer.press_origin(),
                button_modifiers(input),
                input.pointer.primary_down(),
            )
        });
        let pointer = pointer.map(|p| self.to_scene_point(p));
        let press = press.map(|p| self.to_scene_point(p));

        // carried to the pointer before it can end: a slow frame may take in
        // the last moves and the release at once
        if let Some(pointer) = pointer {
            self.continue_drag(pointer, modifiers);
        }
        if !primary_down && self.state.drag.take().is_some() {
            self.edit.end();
            self.state.clamp(self.edit.value);
        }
        // The press is forgotten on the frame the button comes up. A
        // response's positions are already in the canvas layer's own scene
        // units; only raw input is on screen.
        let clicked_at = response.interact_pointer_pos();
        if self.state.draw.tool != Tool::Select {
            let origin = self.state.origin();
            let metres = |scene: Pos2| to_metres(origin, scene);
            let drawing = Pointer {
                at: pointer.map(metres),
                press: press.map(metres),
                clicked_at: clicked_at.map(metres),
                pixels_per_metre: self.scene_transform.scaling * UNITS_PER_METRE,
                modifiers,
            };
            if !panning(ui) {
                draw_tools::interact(self.state, response, &drawing);
            }
            return;
        }
        self.selection_handles(ui, pointer);
        if !panning(ui) {
            self.start_drag(response, press, modifiers);
            if let Some(pointer) = pointer {
                self.continue_drag(pointer, modifiers);
            }
            self.click(response, clicked_at, modifiers);
        }
    }

    /// Carries an ongoing drag to where the pointer is now.
    fn continue_drag(&mut self, pointer: Pos2, modifiers: Modifiers) {
        let origin = self.state.origin();
        let Some(drag) = self.state.drag.as_mut() else {
            return;
        };
        match drag {
            Drag::Move {
                pointer_start,
                start,
            } => {
                let delta = (pointer - *pointer_start) / UNITS_PER_METRE;
                let start = start.clone();
                self.edit.continue_change(|definition| {
                    for (pixel, position) in &start {
                        if let Some(pixel) = definition.pixels.get_mut(*pixel) {
                            pixel.position_m =
                                [position[0] + delta.x, position[1] + delta.y, position[2]];
                        }
                    }
                });
            }
            Drag::Band(band) => {
                band.current = pointer;
                let covered = covered_pixels(self.edit.value, origin, band.rect());
                self.state.selection = band.selection(covered);
            }
            Drag::Scale(scale) => {
                let (about, scale_x, scale_y) =
                    scale.factors(pointer, modifiers.alt, modifiers.shift);
                let start = scale.start.clone();
                self.edit.continue_change(|definition| {
                    for (pixel, position) in &start {
                        let offset = to_scene(origin, *position) - about;
                        let scaled = about + vec2(offset.x * scale_x, offset.y * scale_y);
                        let [x, y] = to_metres(origin, scaled);
                        if let Some(pixel) = definition.pixels.get_mut(*pixel) {
                            pixel.position_m = [x, y, position[2]];
                        }
                    }
                });
            }
            Drag::Rotate(rotation) => {
                let angle = rotation.angle(pointer, modifiers.shift);
                let (centre, start) = (rotation.centre, rotation.start.clone());
                self.edit.continue_change(|definition| {
                    let rotation = Rot2::from_angle(angle);
                    for (pixel, position, turned) in &start {
                        let turned_to = centre + rotation * (to_scene(origin, *position) - centre);
                        let [x, y] = to_metres(origin, turned_to);
                        if let Some(pixel) = definition.pixels.get_mut(*pixel) {
                            pixel.position_m = [x, y, position[2]];
                            pixel.rotation = turned + angle;
                        }
                    }
                });
            }
        }
    }

    /// The box around a selection of several pixels, with handles to scale
    /// it, and a handle above it to turn the selection.
    fn selection_handles(&mut self, ui: &mut Ui, pointer: Option<Pos2>) {
        let origin = self.state.origin();
        // a margin as wide as a handle, so no handle covers a pixel
        let margin = HANDLE_HIT / self.scene_transform.scaling;
        let Some(live_box) = selection_bbox(
            self.edit.value,
            origin,
            self.state.selection.iter().copied(),
        )
        .map(|bbox| bbox.expand(margin)) else {
            return;
        };
        let modifiers = ui.input(|input| input.modifiers);
        let shown_box = match (&self.state.drag, pointer) {
            (Some(Drag::Scale(scale)), Some(pointer)) => {
                let (about, scale_x, scale_y) =
                    scale.factors(pointer, modifiers.alt, modifiers.shift);
                scaled_box(scale.start_box, about, scale_x, scale_y)
            }
            // while turning, the handle stays where the drag took hold of it
            (Some(Drag::Rotate(rotation)), _) => rotation.start_box,
            _ => live_box,
        };

        if self.state.selection.len() > 1
            && let Some(handle) =
                box_handles(ui, &mut self.overlay, &self.scene_transform, shown_box)
            && let Some(pointer) = pointer
        {
            self.edit.begin();
            self.state.drag = Some(Drag::Scale(BoxScale {
                handle,
                start_box: live_box,
                start_pointer: pointer,
                start: self
                    .selected_positions()
                    .map(|(pixel, position, _)| (pixel, position))
                    .collect(),
            }));
        }

        let rotate = rotation_handle(ui, &mut self.overlay, &self.scene_transform, shown_box);
        if rotate.drag_started_by(PointerButton::Primary)
            && let Some(pointer) = pointer
        {
            self.edit.begin();
            self.state.drag = Some(Drag::Rotate(RotateDrag::new(
                live_box,
                pointer,
                self.selected_positions().collect(),
            )));
        }
    }

    fn selected_positions(&self) -> impl Iterator<Item = (usize, [f32; 3], f32)> + '_ {
        self.state.selection.iter().filter_map(|pixel| {
            self.edit
                .value
                .pixels
                .get(*pixel)
                .map(|found| (*pixel, found.position_m, found.rotation))
        })
    }

    /// A drag on a pixel moves the selection; on the background it draws a
    /// band.
    fn start_drag(&mut self, response: &Response, press: Option<Pos2>, modifiers: Modifiers) {
        if !response.drag_started_by(PointerButton::Primary) || self.state.drag.is_some() {
            return;
        }
        let Some(press) = press else {
            return;
        };
        let origin = self.state.origin();
        match pixel_under(self.edit.value, origin, press, self.min_half()) {
            Some(pixel) => {
                if !self.state.selection.contains(&pixel) {
                    if !(modifiers.shift || modifiers.command) {
                        self.state.selection.clear();
                    }
                    self.state.selection.insert(pixel);
                }
                self.edit.begin();
                self.state.drag = Some(Drag::Move {
                    pointer_start: press,
                    start: self
                        .selected_positions()
                        .map(|(pixel, position, _)| (pixel, position))
                        .collect(),
                });
            }
            None => {
                let band = Band::start(press, &mut self.state.selection, modifiers);
                self.state.drag = Some(Drag::Band(band));
            }
        }
    }

    /// A click picks a pixel: alone, or toggled with cmd, added with shift,
    /// removed with alt. A click beside every pixel clears the selection, a
    /// double click there adds a pixel.
    fn click(&mut self, response: &Response, press: Option<Pos2>, modifiers: Modifiers) {
        let Some(press) = press else {
            return;
        };
        let origin = self.state.origin();
        let picked = pixel_under(self.edit.value, origin, press, self.min_half());
        if response.double_clicked() && picked.is_none() {
            generators::start(
                GeneratorKind::Pixel,
                self.edit,
                self.state,
                to_metres(origin, press),
            );
            return;
        }
        if !response.clicked_by(PointerButton::Primary) {
            return;
        }
        self.state.message = None;
        ClickIntent::from_modifiers(modifiers).apply(&mut self.state.selection, picked);
    }
}

/// The modifiers held when the last mouse button this frame went down or up.
/// A slow frame can take in a shift press, a click and the shift release at
/// once; the modifiers held at the end of it would lose the shift.
fn button_modifiers(input: &egui::InputState) -> Modifiers {
    input
        .events
        .iter()
        .rev()
        .find_map(|event| match event {
            egui::Event::PointerButton { modifiers, .. } => Some(*modifiers),
            _ => None,
        })
        .unwrap_or(input.modifiers)
}