use crate::canvas::box_scale::drag_handle;
use crate::canvas::caches::Caches;
use crate::canvas::definition_drawing::{
EDITOR_PIXEL, MIN_FIXTURE_SCREEN_PX, PixelTint, Placement, Style, draw_definition, draw_photo,
fixture_footprint,
};
use crate::canvas::rotated_rect::RotatedRect;
use crate::canvas::scrollbars::panning;
use crate::canvas::shapes::{Shapes, draw_edge};
use crate::fixture_editor::drag::FixtureEnd;
use crate::fixture_editor::node_response::NodeResponse;
use crate::fixture_editor::port_inspector::port_label;
use egui::emath::{Rot2, TSTransform};
use egui::epaint::RectShape;
use egui::epaint::{Color32, FontFamily, PathShape};
use egui::{Align2, FontId, Painter, PointerButton, Pos2, Rect, Response, Stroke, Ui, vec2};
use egui_phosphor_icons::icons;
use gled_model::patching::definition::FixtureDefinition;
use gled_model::patching::fixture::{FixtureId, LayoutPosition};
use gled_model::patching::topology::Port;
const PORT_DIAMETER: f32 = 20.0;
const NETWORK_ATTACH_DISTANCE: f32 = 100.0;
pub(crate) struct FixtureUI<'a> {
fixture: FixtureId,
position: &'a LayoutPosition,
definition: &'a FixtureDefinition,
definition_id: Option<uuid::Uuid>,
color: Color32,
footprint: RotatedRect,
has_focus: bool,
attach_network: bool,
port: Option<Port>,
port_selected: bool,
to_global: TSTransform,
}
impl<'a> FixtureUI<'a> {
pub(crate) fn new(
fixture: FixtureId,
position: &'a LayoutPosition,
definition: &'a FixtureDefinition,
definition_id: Option<uuid::Uuid>,
color: Color32,
has_focus: bool,
attach_network: bool,
to_global: TSTransform,
) -> Self {
let footprint = fixture_footprint(
definition,
position,
to_global.scaling,
MIN_FIXTURE_SCREEN_PX,
);
FixtureUI {
fixture,
position,
definition,
definition_id,
color,
footprint,
has_focus,
attach_network,
port: None,
port_selected: false,
to_global,
}
}
pub(crate) fn with_port(mut self, port: Option<Port>, selected: bool) -> Self {
self.port = port;
self.port_selected = selected;
self
}
pub(crate) fn input_terminal(&self) -> Pos2 {
self.to_global * self.footprint.along_axis(0.0)
}
fn screen_footprint(&self) -> RotatedRect {
self.footprint.transformed(self.to_global)
}
pub(crate) fn show(
mut self,
ui: &mut Ui,
node_painter: &Painter,
overlay_painter: &mut Shapes,
edge_painter: &mut Shapes,
canvas_response: Response,
single_selected: bool,
pointer_was_down: &mut bool,
show_handles: bool,
caches: &mut Caches,
) -> NodeResponse {
self.draw_body(node_painter, caches);
let to_scene = self.to_global.inverse();
let (pointer, press) = ui.input(|i| (i.pointer.interact_pos(), i.pointer.press_origin()));
let on_fixture =
|at: Option<Pos2>| at.is_some_and(|at| self.footprint.contains(to_scene * at));
let hovering = on_fixture(pointer);
let pressed = on_fixture(press);
let screen = self.screen_footprint();
let node_response =
if *pointer_was_down && (canvas_response.clicked() || canvas_response.dragged()) {
*pointer_was_down = false;
NodeResponse::from_response(&canvas_response, &screen, false, hovering, pressed)
}
else if hovering || pressed {
if pressed && canvas_response.is_pointer_button_down_on() {
*pointer_was_down = true;
}
NodeResponse::from_response(
&canvas_response,
&screen,
*pointer_was_down,
hovering,
pressed,
)
} else {
NodeResponse::default()
};
if self.has_focus {
overlay_painter.add(PathShape::convex_polygon(
screen.corners().to_vec(),
Color32::TRANSPARENT,
Stroke::new(3.0, Color32::WHITE),
));
}
let mut node_response = node_response;
if self.has_focus && single_selected && show_handles {
for end in [FixtureEnd::Near, FixtureEnd::Far] {
if self.end_handle(ui, end, screen.along_axis(end.along()), overlay_painter) {
node_response.end_drag = Some(end);
}
}
}
if self.attach_network {
let response = self.network_output(ui, node_painter, edge_painter);
node_response.network_clicked = response.clicked();
}
node_response
}
fn draw_body(&self, painter: &Painter, caches: &mut Caches) {
let placement = Placement {
position: self.position,
to_global: self.to_global,
min_screen_px: MIN_FIXTURE_SCREEN_PX,
};
let style = Style {
outline: self.color,
pixels: PixelTint::Uniform(EDITOR_PIXEL),
};
draw_photo(painter, self.definition, &placement, caches);
draw_definition(
painter,
self.definition,
self.definition_id,
&placement,
style,
caches,
);
}
fn end_handle(
&self,
ui: &mut Ui,
end: FixtureEnd,
centre: Pos2,
overlay_painter: &mut Shapes,
) -> bool {
let id = ui.id().with(("fixture scale handle", self.fixture, end));
drag_handle(ui, overlay_painter, centre, Color32::WHITE, id)
.drag_started_by(PointerButton::Primary)
&& !panning(ui)
}
pub(crate) fn node_port(
&mut self,
ui: &mut Ui,
node_painter: &Painter,
next_connected: bool,
) -> Response {
let to_local = ui
.ctx()
.layer_transform_from_global(ui.layer_id())
.unwrap_or_default();
let position = self.output_terminal();
let rect_size = PORT_DIAMETER * to_local.scaling;
let rect = Rect::from_center_size(to_local * position, (rect_size, rect_size).into());
node_painter.add(
RectShape::filled(
Rect::from_center_size(
self.to_global * rect.center(),
vec2(PORT_DIAMETER, PORT_DIAMETER),
),
20.0,
Color32::from_black_alpha(100),
)
.with_blur_width(15.0),
);
let response = ui.interact(
rect,
ui.id().with(("fixture output port", self.fixture)),
egui::Sense::drag(),
);
let (icon, family, color) = match (response.hovered(), response.dragged(), next_connected) {
(true, false, true) => (icons::X.as_str(), "phosphor-regular", Color32::RED),
(true, false, _) => (icons::PLUGS.as_str(), "phosphor-fill", Color32::LIGHT_GREEN),
(_, true, _) => (icons::PLUGS.as_str(), "phosphor-fill", Color32::LIGHT_GREEN),
(false, _, true) => (
icons::PLUGS_CONNECTED.as_str(),
"phosphor-fill",
Color32::from_gray(150),
),
(false, _, _) => (
icons::PLUG.as_str(),
"phosphor-fill",
Color32::from_gray(150),
),
};
node_painter.text(
self.to_global * rect.center(),
Align2::CENTER_CENTER,
icon,
FontId::new(16.0, FontFamily::Name(family.into())),
color,
);
response
}
pub(crate) fn output_terminal(&self) -> Pos2 {
self.to_global * self.footprint.along_axis(1.0)
+ Rot2::from_angle(self.position.angle) * vec2(PORT_DIAMETER * 0.6, 0.0)
}
fn network_output(
&mut self,
ui: &mut Ui,
node_painter: &Painter,
edge_painter: &mut Shapes,
) -> Response {
let input_size = vec2(38.0, 26.0);
let input = self.input_terminal();
let attach_network_position = input
+ self.to_global.scaling
* (Rot2::from_angle(self.position.angle) * vec2(-NETWORK_ATTACH_DISTANCE, 0.0));
let source_rect = Rect::from_center_size(attach_network_position, input_size);
node_painter.rect_filled(source_rect, 4.0, Color32::BLUE);
if self.port_selected {
node_painter.rect_stroke(
source_rect.expand(2.0),
6.0,
Stroke::new(2.0, Color32::WHITE),
egui::StrokeKind::Outside,
);
}
let label = self.port.map(port_label).unwrap_or_default();
node_painter.text(
source_rect.center(),
Align2::CENTER_CENTER,
label,
FontId::proportional(13.0),
Color32::WHITE,
);
let edge_begin_position =
attach_network_position + (Rot2::from_angle(self.position.angle) * vec2(15.0, 0.0));
let edge_end_position = input + (Rot2::from_angle(self.position.angle) * vec2(-10.0, 0.0));
draw_edge(edge_painter, &edge_begin_position, &edge_end_position);
let to_local = ui
.ctx()
.layer_transform_from_global(ui.layer_id())
.unwrap_or_default();
ui.interact(
to_local * source_rect,
ui.id().with(("network output", self.fixture)),
egui::Sense::click(),
)
}
}
#[cfg(test)]
mod tests;