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) {
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() {
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));
}
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;
}
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;
}
}
}
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");
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());
}
pub(crate) fn keys(ctx: &egui::Context, edit: &mut Edit<'_>, state: &mut EditorState) {
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),
}
}
}