gled-ui 2.31.2

gled's egui interface, shared by the desktop app and the browser
Documentation
use crate::fixture_editor::{APPLY_FILL, Draft, FixtureWindow};
use egui::emath::TSTransform;
use egui::{
    Context, Event, Key, Modifiers, PointerButton, Pos2, RawInput, Rect, Shape, pos2, vec2,
};
use gled_model::asset::project::Project;
use gled_model::collection::Collections;
use gled_model::patching::Patch;
use gled_model::patching::definition::FixtureDefinition;
use gled_model::patching::fixture::{Fixture, FixtureId, LayoutPosition};
use gled_model::test_support::collections_with;

/// Whether `shape` is, or holds, the enabled Apply button.
fn shows_apply(shape: &Shape) -> bool {
    match shape {
        Shape::Rect(rect) => {
            // a hair transparent while something fades
            let [r, g, b, _] = rect.fill.to_srgba_unmultiplied();
            let [apply_r, apply_g, apply_b, _] = APPLY_FILL.to_array();
            r.abs_diff(apply_r) <= 2 && g.abs_diff(apply_g) <= 2 && b.abs_diff(apply_b) <= 2
        }
        Shape::Vec(shapes) => shapes.iter().any(shows_apply),
        _ => false,
    }
}

/// The window on a 1600 × 1000 screen, with two 4 m square fixtures side by
/// side in the middle of the scene.
pub(super) struct Session {
    ctx: Context,
    pub(super) window: FixtureWindow,
    project: Option<Project>,
    collections: Collections,
    time: f64,
    pub(super) left: FixtureId,
    pub(super) right: FixtureId,
}

impl Session {
    pub(super) fn new() -> Self {
        Self::with_fixtures(0)
    }

    /// The window with `extra` small fixtures in rows below the two.
    pub(super) fn with_fixtures(extra: usize) -> Self {
        let (collections, ids) = collections_with(vec![
            FixtureDefinition {
                size_m: [4.0, 4.0, 0.1],
                ..Default::default()
            },
            FixtureDefinition {
                size_m: [0.4, 0.4, 0.1],
                ..Default::default()
            },
        ]);
        let mut project = Project::default();
        let mut fixture = |name: &str, definition, position| {
            let mut fixture = Fixture::new(name);
            fixture.definition = Some(definition);
            project
                .patch
                .add_fixture(fixture, LayoutPosition::at(position))
        };
        let left = fixture("left", ids[0], pos2(700.0, 1000.0));
        let right = fixture("right", ids[0], pos2(1300.0, 1000.0));
        for index in 0..extra {
            let (col, row) = (index % 25, index / 25);
            let position = pos2(250.0 + col as f32 * 60.0, 1300.0 + row as f32 * 30.0);
            fixture(&format!("small {index}"), ids[1], position);
        }
        let ctx = Context::default();
        crate::style::apply(&ctx);
        let mut window = FixtureWindow::default();
        window.open();
        let mut session = Self {
            ctx,
            window,
            project: Some(project),
            collections,
            time: 0.0,
            left,
            right,
        };
        // the first frames lay the window out
        for _ in 0..3 {
            session.frame(vec![]);
        }
        session
    }

    /// One frame, 30 ms after the last one. Returns whether it shows Apply
    /// enabled.
    pub(super) fn frame(&mut self, events: Vec<Event>) -> bool {
        self.time += 0.03;
        let input = RawInput {
            screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(1600.0, 1000.0))),
            time: Some(self.time),
            events,
            ..Default::default()
        };
        let Self {
            window,
            project,
            collections,
            ..
        } = self;
        let mut output = self
            .ctx
            .run_ui(input, |ui| window.update(ui.ctx(), project, collections));
        output.textures_delta.clear();
        output
            .shapes
            .iter()
            .any(|clipped| shows_apply(&clipped.shape))
    }

    /// Scene units to screen pixels on the canvas.
    fn to_screen(&self) -> TSTransform {
        let layers: Vec<_> = self.ctx.memory(|memory| memory.layer_ids().collect());
        layers
            .into_iter()
            .find_map(|layer| self.ctx.layer_transform_to_global(layer))
            .expect("the canvas has a scene layer")
    }

    /// Where `id` is on screen.
    pub(super) fn on_screen(&self, id: FixtureId) -> Pos2 {
        self.to_screen() * self.patch().topology.default_layout.members[&id].pos
    }

    pub(super) fn patch(&self) -> &Patch {
        &self.draft().patch
    }

    /// The interface's project, which Apply hands the draft to.
    pub(super) fn project(&self) -> &Project {
        self.project.as_ref().expect("the session has a project")
    }

    /// The interface adopting the engine's project with `patch`, and a frame.
    pub(super) fn adopt(&mut self, patch: Patch) {
        if let Some(project) = self.project.as_mut() {
            project.patch = patch;
        }
        self.window.project_replaced();
        self.frame(vec![]);
    }

    /// What the Apply button does.
    pub(super) fn apply(&mut self) {
        let mut draft = self.window.draft.take().expect("the window has a draft");
        let project = self.project.as_mut().expect("the session has a project");
        self.window.apply(&mut draft, project);
        self.window.draft = Some(draft);
    }

    pub(super) fn draft(&self) -> &Draft {
        self.window.draft.as_ref().expect("the window has a draft")
    }

    pub(super) fn button(pos: Pos2, pressed: bool) -> Event {
        Event::PointerButton {
            pos,
            button: PointerButton::Primary,
            pressed,
            modifiers: Modifiers::NONE,
        }
    }

    /// A primary drag from `from` to `to` in 20 steps, one frame each, and a
    /// 1 px nudge after letting go — what the XTest run sent. With
    /// `move_and_press_together` the move to `from` and the press arrive in
    /// one frame.
    pub(super) fn drag(&mut self, from: Pos2, to: Pos2, move_and_press_together: bool) {
        if move_and_press_together {
            self.frame(vec![Event::PointerMoved(from), Self::button(from, true)]);
        } else {
            self.frame(vec![Event::PointerMoved(from)]);
            self.frame(vec![Self::button(from, true)]);
        }
        for step in 1..=20 {
            let at = from + (to - from) * step as f32 / 20.0;
            self.frame(vec![Event::PointerMoved(at)]);
        }
        self.frame(vec![Self::button(to, false)]);
        self.frame(vec![Event::PointerMoved(to + vec2(1.0, 0.0))]);
        self.frame(vec![]);
    }

    fn key(&mut self, modifiers: Modifiers, key: Key) {
        let event = |pressed| Event::Key {
            key,
            physical_key: None,
            pressed,
            repeat: false,
            modifiers,
        };
        self.frame(vec![Event::ModifiersChanged(modifiers), event(true)]);
        self.frame(vec![event(false), Event::ModifiersChanged(Modifiers::NONE)]);
        self.frame(vec![]);
    }

    pub(super) fn undo(&mut self) {
        self.key(Modifiers::COMMAND, Key::Z);
    }

    pub(super) fn redo(&mut self) {
        self.key(Modifiers::COMMAND | Modifiers::SHIFT, Key::Z);
    }

    pub(super) fn can_undo(&self) -> bool {
        self.draft().undo.can_undo()
    }

    pub(super) fn can_redo(&self) -> bool {
        self.draft().undo.can_redo()
    }
}