gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
//! The strip above the canvas: the drawing tools and their options, adding
//! pixels, undo and redo, framing the fixture, and what went wrong last; and
//! the keys the drawing tools answer to.

use crate::fixture_definition_editor::canvas_geometry::to_metres;
use crate::fixture_definition_editor::draw_tools::Tool;
use crate::fixture_definition_editor::edit::{Edit, EditorAction};
use crate::fixture_definition_editor::generators::{self, GeneratorKind, MAX_LEDS};
use crate::fixture_definition_editor::picked_file::DefinitionFile;
use crate::fixture_definition_editor::shape::Shape;
use crate::fixture_definition_editor::state::EditorState;
use crate::fixture_definition_editor::units::millimetres;
use crate::ui::file_pick;
use egui::{Button, Color32, DragValue, Key, KeyboardShortcut, Modifiers, Ui};
use egui_phosphor_icons::icons;
use strum::IntoEnumIterator;

pub(crate) fn show(ui: &mut Ui, edit: &mut Edit<'_>, state: &mut EditorState) {
    // wrapped, so a narrow canvas does not push the buttons under the panels
    ui.horizontal_wrapped(|ui| {
        tool_buttons(ui, state);
        ui.separator();
        ui.menu_button((icons::PLUS.regular(), "Add"), |ui| {
            for kind in GeneratorKind::iter() {
                if ui.button(kind.to_string()).clicked() {
                    // a single pixel lands in the middle of what the canvas shows
                    let at = to_metres(state.origin(), state.view.center);
                    generators::start(kind, edit, state, at);
                    ui.close();
                }
            }
            ui.separator();
            if ui
                .add_enabled(
                    !state.file_pick.is_open(),
                    Button::new("Along an SVG path…"),
                )
                .on_hover_text("Lay LEDs along a path drawn in a vector editor, at its size")
                .clicked()
            {
                ui.close();
                state.ask_file(ui.ctx(), file_pick::SVG, DefinitionFile::path);
            }
        })
        .response
        .on_hover_text("Double-click the canvas to add a pixel there");

        let ctx = ui.ctx().clone();
        let shortcut = |modifiers, key| ctx.format_shortcut(&KeyboardShortcut::new(modifiers, key));
        let undo = Button::new((icons::ARROW_COUNTER_CLOCKWISE.regular(), "Undo"))
            .shortcut_text(shortcut(Modifiers::COMMAND, Key::Z));
        if ui.add_enabled(edit.can_undo(), undo).clicked() {
            EditorAction::Undo.apply(edit, state);
        }
        let redo = Button::new((icons::ARROW_CLOCKWISE.regular(), "Redo"))
            .shortcut_text(shortcut(Modifiers::COMMAND | Modifiers::SHIFT, Key::Z));
        if ui.add_enabled(edit.can_redo(), redo).clicked() {
            EditorAction::Redo.apply(edit, state);
        }
        if ui
            .button((icons::FRAME_CORNERS.regular(), "Fit view"))
            .clicked()
        {
            state.fit_view(edit.value);
        }

        if let Some(message) = &state.message {
            ui.colored_label(Color32::from_rgb(230, 120, 110), message);
        }
    });
    ui.horizontal_wrapped(|ui| options(ui, edit, state));
}

/// Adds the drawn shape's LEDs, named as the generator form names pixels, and
/// selects them. The tool stays, for the next strip.
fn commit(edit: &mut Edit<'_>, state: &mut EditorState) {
    if !state.draw.complete() {
        return;
    }
    let draw = state.draw.clone();
    generators::add_pixels(edit, state, |template| draw.pixels(template));
    state.draw.shape = None;
}

/// One button per tool.
fn tool_buttons(ui: &mut Ui, state: &mut EditorState) {
    for tool in Tool::iter() {
        let response = ui
            .selectable_label(state.draw.tool == tool, tool.label())
            .on_hover_text(format!("Key {}", tool.key().name()));
        if response.clicked() && state.draw.tool != tool {
            state.draw.tool = tool;
            state.draw.shape = None;
        }
    }
}

/// For a drawing tool: how many LEDs, and the button that adds them.
fn options(ui: &mut Ui, edit: &mut Edit<'_>, state: &mut EditorState) {
    let tool = state.draw.tool;
    if tool == Tool::Select {
        return;
    }
    ui.label("LEDs");
    ui.add(DragValue::new(&mut state.draw.count).range(1..=MAX_LEDS))
        .on_hover_text("+ and − change it, with shift by ten");
    // or as the strip has them: so many millimetres apart
    if let Some(mut pitch) = state.draw.pitch() {
        let response = ui
            .add(millimetres(&mut pitch).speed(0.0001).range(0.0005..=10.0).max_decimals(4).suffix(" mm apart"))
            .on_hover_text("Type the strip's LED pitch and the count follows; 60 LEDs per metre are 16.7 mm apart");
        if response.changed() {
            state.draw.count = state.draw.count_for_pitch(pitch);
        }
    }
    let open = !matches!(
        state.draw.shape,
        Some(Shape::Circle { .. }) | Some(Shape::Path { closed: true, .. })
    ) && tool != Tool::Circle;
    if open {
        if tool == Tool::Path {
            ui.checkbox(&mut state.draw.smooth, "smooth")
                .on_hover_text("Run a smooth curve through the corners instead of straight lines");
        }
        ui.checkbox(&mut state.draw.inset, "free ends")
            .on_hover_text(
                "Keep half a pitch free at either end instead of an LED on each end point",
            );
    }
    let add = Button::new(format!("Add {} LEDs", state.draw.count));
    if ui
        .add_enabled(state.draw.complete(), add)
        .on_hover_text("Enter")
        .clicked()
    {
        commit(edit, state);
    }
    ui.weak(tool.hint());
}

/// Enter adds the LEDs (or ends a path), Escape drops the shape or the tool,
/// Backspace takes back a path's last corner. Runs before the canvas's own
/// shortcuts, which would delete pixels on Backspace.
pub(crate) fn keys(ctx: &egui::Context, edit: &mut Edit<'_>, state: &mut EditorState) {
    // a text field or an open form has the keyboard
    if state.typing || ctx.text_edit_focused() || state.generator.is_some() {
        return;
    }
    let picked = ctx.input_mut(|input| {
        Tool::iter().find(|tool| input.consume_key(Modifiers::NONE, tool.key()))
    });
    if let Some(tool) = picked
        && tool != state.draw.tool
    {
        state.draw.tool = tool;
        state.draw.shape = None;
    }
    if state.draw.tool == Tool::Select {
        if ctx.input_mut(|input| input.consume_key(Modifiers::NONE, Key::Escape)) {
            state.selection.clear();
        }
        return;
    }
    let (enter, escape, backspace, more, fewer, step) = ctx.input_mut(|input| {
        (
            input.consume_key(Modifiers::NONE, Key::Enter),
            input.consume_key(Modifiers::NONE, Key::Escape),
            input.consume_key(Modifiers::NONE, Key::Backspace),
            input.consume_key(Modifiers::NONE, Key::Plus)
                || input.consume_key(Modifiers::NONE, Key::Equals),
            input.consume_key(Modifiers::NONE, Key::Minus),
            if input.modifiers.shift { 10 } else { 1 },
        )
    });
    if more {
        state.draw.count = (state.draw.count + step).min(MAX_LEDS);
    }
    if fewer {
        state.draw.count = state.draw.count.saturating_sub(step).max(1);
    }
    if escape && state.draw.shape.take().is_none() {
        state.draw.tool = Tool::Select;
    }
    if backspace
        && let Some(Shape::Path {
            points,
            finished: false,
            ..
        }) = &mut state.draw.shape
    {
        points.pop();
        if points.is_empty() {
            state.draw.shape = None;
        }
    }
    if enter {
        match &mut state.draw.shape {
            Some(Shape::Path { finished, .. }) if !*finished => *finished = true,
            _ => commit(edit, state),
        }
    }
}