diff --git a/examples/demo.rs b/examples/demo.rs index 802c4dc..6a83ef8 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -175,7 +175,7 @@ impl SnarlViewer for DemoViewer { match &*pin.remotes { [] => { ui.label("None"); - PinInfo::circle().with_fill(UNTYPED_COLOR) + PinInfo::star().with_fill(UNTYPED_COLOR) } [remote] => match snarl[remote.node] { DemoNode::Sink => unreachable!("Sink node has no outputs"), diff --git a/src/ui.rs b/src/ui.rs index 4328cff..7386296 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -4,7 +4,7 @@ use std::{collections::HashMap, hash::Hash}; use egui::{ collapsing_header::paint_default_icon, epaint::Shadow, pos2, vec2, Align, Color32, Frame, Id, - Layout, Modifiers, PointerButton, Pos2, Rect, Sense, Shape, Stroke, Style, Ui, Vec2, + Layout, Margin, Modifiers, PointerButton, Pos2, Rect, Rounding, Sense, Shape, Stroke, Ui, Vec2, }; use crate::{InPin, InPinId, Node, NodeId, OutPin, OutPinId, Snarl}; @@ -17,7 +17,7 @@ mod wire; mod zoom; use self::{ - pin::{default_pin_fill, default_pin_stroke, draw_pin, AnyPin}, + pin::{draw_pin, AnyPin}, state::{NewWires, NodeState, SnarlState}, wire::{draw_wire, hit_wire, pick_wire_style}, zoom::Zoom, @@ -25,11 +25,29 @@ use self::{ pub use self::{ background_pattern::{BackgroundPattern, CustomBackground, Grid, Viewport}, - pin::{AnyPins, CustomPinShape, DefaultPinShape, PinInfo, PinShape}, + pin::{AnyPins, BasicPinShape, CustomPinShape, PinInfo, PinShape}, viewer::SnarlViewer, wire::{WireLayer, WireStyle}, }; +/// Controls style of node selection rect. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "egui-probe", derive(egui_probe::EguiProbe))] +pub struct SelectionStyle { + /// Margin between selection rect and node frame. + pub margin: Margin, + + /// Rounding of selection rect. + pub rounding: Rounding, + + /// Fill color of selection rect. + pub fill: Color32, + + /// Stroke of selection rect. + pub stroke: Stroke, +} + /// Style for rendering Snarl. #[derive(Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -37,88 +55,342 @@ pub use self::{ pub struct SnarlStyle { /// Size of pins. #[cfg_attr(feature = "egui-probe", egui_probe(range = 0.0..))] - pub pin_size: Option, + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _pin_size: Option, /// Default fill color for pins. - pub pin_fill: Option, + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _pin_fill: Option, /// Default stroke for pins. - pub pin_stroke: Option, + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _pin_stroke: Option, /// Shape of pins. - pub pin_shape: DefaultPinShape, + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _pin_shape: Option, /// Width of wires. #[cfg_attr(feature = "egui-probe", egui_probe(range = 0.0..))] - pub wire_width: Option, + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _wire_width: Option, /// Size of wire frame which controls curvature of wires. - pub wire_frame_size: Option, + #[cfg_attr(feature = "egui-probe", egui_probe(range = 0.0..))] + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _wire_frame_size: Option, /// Whether to downscale wire frame when nodes are close. - pub downscale_wire_frame: bool, + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _downscale_wire_frame: Option, /// Weather to upscale wire frame when nodes are far. - pub upscale_wire_frame: bool, + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _upscale_wire_frame: Option, /// Controls default style of wires. - pub wire_style: WireStyle, + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _wire_style: Option, /// Layer where wires are rendered. - pub wire_layer: WireLayer, + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _wire_layer: Option, /// Additional blank space for dragging node by header. - pub header_drag_space: Option, + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _header_drag_space: Option, /// Whether nodes can be collapsed. /// If true, headers will have collapsing button. /// When collapsed, node will not show its pins, body and footer. - pub collapsible: bool, - - /// Background fill color. - /// Defaults to `ui.visuals().widgets.noninteractive.bg_fill`. - pub bg_fill: Option, + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _collapsible: Option, + + /// Frame used to draw background + #[cfg_attr( + feature = "serde", + serde( + skip_serializing_if = "Option::is_none", + default, + with = "serde_frame_option" + ) + )] + pub _bg_frame: Option, /// Background pattern. /// Defaults to [`BackgroundPattern::Grid`]. - pub bg_pattern: BackgroundPattern, + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _bg_pattern: Option, /// Stroke for background pattern. /// Defaults to `ui.visuals().widgets.noninteractive.bg_stroke`. - pub background_pattern_stroke: Option, + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _bg_pattern_stroke: Option, /// Minimum scale that can be set. #[cfg_attr(feature = "egui-probe", egui_probe(range = 0.0..=1.0))] - pub min_scale: f32, + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _min_scale: Option, /// Maximum scale that can be set. #[cfg_attr(feature = "egui-probe", egui_probe(range = 1.0..))] - pub max_scale: f32, + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _max_scale: Option, /// Scale velocity when scaling with mouse wheel. #[cfg_attr(feature = "egui-probe", egui_probe(range = 0.0..))] - pub scale_velocity: f32, + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _scale_velocity: Option, /// Frame used to draw nodes. /// Defaults to [`Frame::window`] constructed from current ui's style. - #[cfg_attr(feature = "serde", serde(with = "serde_frame_option"))] - pub node_frame: Option, + #[cfg_attr( + feature = "serde", + serde( + skip_serializing_if = "Option::is_none", + default, + with = "serde_frame_option" + ) + )] + pub _node_frame: Option, /// Frame used to draw node headers. /// Defaults to [`node_frame`] without shadow and transparent fill. /// /// If set, it should not have shadow and fill should be either opaque of fully transparent /// unless layering of header fill color with node fill color is desired. - #[cfg_attr(feature = "serde", serde(with = "serde_frame_option"))] - pub header_frame: Option, + #[cfg_attr( + feature = "serde", + serde( + skip_serializing_if = "Option::is_none", + default, + with = "serde_frame_option" + ) + )] + pub _header_frame: Option, + + /// Enable centering by double click on background + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _centering: Option, + + /// Stroke for selection. + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _select_stoke: Option, + + /// Fill for selection. + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _select_fill: Option, + + /// Flag to control how rect selection works. + /// If set to true, only nodes fully contained in selection rect will be selected. + /// If set to false, nodes intersecting with selection rect will be selected. + pub _select_rect_contained: Option, + + /// Style for node selection. + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub _select_style: Option, #[doc(hidden)] #[cfg_attr(feature = "egui-probe", egui_probe(skip))] /// Do not access other than with .., here to emulate `#[non_exhaustive(pub)]` pub _non_exhaustive: (), +} - /// Enable centering by double click on background - pub centering: bool, +impl SnarlStyle { + fn pin_size(&self, scale: f32, ui: &Ui) -> f32 { + self._pin_size + .zoomed(scale) + .unwrap_or_else(|| ui.spacing().interact_size.y * 0.5) + } + + fn pin_fill(&self, ui: &Ui) -> Color32 { + self._pin_fill + .unwrap_or(ui.visuals().widgets.active.bg_fill) + } + + fn pin_stoke(&self, scale: f32, ui: &Ui) -> Stroke { + self._pin_stroke.zoomed(scale).unwrap_or(Stroke::new( + ui.visuals().widgets.active.bg_stroke.width, + ui.visuals().widgets.active.bg_stroke.color, + )) + } + + fn pin_shape(&self) -> PinShape { + self._pin_shape.unwrap_or(BasicPinShape::Circle).into() + } + + fn wire_width(&self, scale: f32, ui: &Ui) -> f32 { + self._wire_width + .zoomed(scale) + .unwrap_or(self.pin_size(scale, ui) * 0.2) + } + + fn wire_frame_size(&self, scale: f32, ui: &Ui) -> f32 { + self._wire_frame_size + .zoomed(scale) + .unwrap_or(self.pin_size(scale, ui) * 5.0) + } + + fn downscale_wire_frame(&self) -> bool { + self._downscale_wire_frame.unwrap_or(true) + } + + fn upscale_wire_frame(&self) -> bool { + self._upscale_wire_frame.unwrap_or(false) + } + + fn wire_style(&self, scale: f32) -> WireStyle { + self._wire_style.zoomed(scale).unwrap_or(WireStyle::Bezier5) + } + + fn wire_layer(&self) -> WireLayer { + self._wire_layer.unwrap_or(WireLayer::BehindNodes) + } + + fn header_drag_space(&self, scale: f32, ui: &Ui) -> Vec2 { + self._header_drag_space + .zoomed(scale) + .unwrap_or(vec2(ui.spacing().icon_width, ui.spacing().icon_width)) + } + + fn collapsible(&self) -> bool { + self._collapsible.unwrap_or(true) + } + + fn bg_frame(&self, ui: &Ui) -> Frame { + self._bg_frame.unwrap_or(Frame::canvas(ui.style())) + } + + fn draw_bg_pattern(&self, style: &SnarlStyle, viewport: &Viewport, ui: &mut Ui) { + match &self._bg_pattern { + None => BackgroundPattern::new().draw(style, viewport, ui), + Some(pattern) => pattern.draw(style, viewport, ui), + } + } + + fn bg_pattern_stroke(&self, scale: f32, ui: &Ui) -> Stroke { + self._bg_pattern_stroke + .zoomed(scale) + .unwrap_or(ui.visuals().widgets.noninteractive.bg_stroke) + } + + fn min_scale(&self) -> f32 { + self._min_scale.unwrap_or(0.2) + } + + fn max_scale(&self) -> f32 { + self._max_scale.unwrap_or(5.0) + } + + fn scale_velocity(&self) -> f32 { + self._scale_velocity.unwrap_or(0.005) + } + + fn node_frame(&self, scale: f32, ui: &Ui) -> Frame { + self._node_frame + .zoomed(scale) + .unwrap_or_else(|| Frame::window(ui.style())) + } + + fn header_frame(&self, scale: f32, ui: &Ui) -> Frame { + self._header_frame.zoomed(scale).unwrap_or_else(|| { + self.node_frame(scale, ui) + .shadow(Shadow::NONE) + .fill(Color32::TRANSPARENT) + }) + } + + fn centering(&self) -> bool { + self._centering.unwrap_or(true) + } + + fn select_stroke(&self, scale: f32, ui: &Ui) -> Stroke { + self._select_stoke.zoomed(scale).unwrap_or(Stroke::new( + ui.visuals().selection.stroke.width, + ui.visuals().selection.stroke.color.gamma_multiply(0.5), + )) + } + + fn select_fill(&self, ui: &Ui) -> Color32 { + self._select_fill + .unwrap_or(ui.visuals().selection.bg_fill.gamma_multiply(0.3)) + } + + fn select_rect_contained(&self) -> bool { + self._select_rect_contained.unwrap_or(false) + } + + fn select_style(&self, scale: f32, ui: &Ui) -> SelectionStyle { + self._select_style.zoomed(scale).unwrap_or(SelectionStyle { + margin: ui.spacing().window_margin, + rounding: ui.visuals().window_rounding, + fill: self.select_fill(ui), + stroke: self.select_stroke(scale, ui), + }) + } } #[cfg(feature = "serde")] @@ -174,31 +446,35 @@ impl SnarlStyle { #[must_use] pub const fn new() -> Self { SnarlStyle { - pin_size: None, - pin_fill: None, - pin_stroke: None, - pin_shape: DefaultPinShape::Circle, - wire_width: None, - wire_frame_size: None, - downscale_wire_frame: false, - upscale_wire_frame: true, - wire_style: WireStyle::Bezier5, - wire_layer: WireLayer::BehindNodes, - header_drag_space: None, - collapsible: true, - - bg_fill: None, - bg_pattern: background_pattern::BackgroundPattern::new(), - background_pattern_stroke: None, - - min_scale: 0.1, - max_scale: 2.0, - scale_velocity: 0.005, - node_frame: None, - header_frame: None, + _pin_size: None, + _pin_fill: None, + _pin_stroke: None, + _pin_shape: None, + _wire_width: None, + _wire_frame_size: None, + _downscale_wire_frame: None, + _upscale_wire_frame: None, + _wire_style: None, + _wire_layer: None, + _header_drag_space: None, + _collapsible: None, + + _bg_frame: None, + _bg_pattern: None, + _bg_pattern_stroke: None, + + _min_scale: None, + _max_scale: None, + _scale_velocity: None, + _node_frame: None, + _header_frame: None, + _centering: None, + _select_stoke: None, + _select_fill: None, + _select_rect_contained: None, + _select_style: None, _non_exhaustive: (), - centering: true, } } } @@ -212,6 +488,7 @@ impl Default for SnarlStyle { struct Input { hover_pos: Option, + interact_pos: Option, scroll_delta: f32, // primary_pressed: bool, secondary_pressed: bool, @@ -223,7 +500,7 @@ struct DrawNodeResponse { node_to_top: Option, drag_released: bool, pin_hovered: Option, - center: Pos2, + rect: Rect, } struct PinResponse { @@ -240,7 +517,7 @@ impl Snarl { offset: snarl_state.offset(), }; - style.bg_pattern.draw(style, &viewport, ui); + style.draw_bg_pattern(style, &viewport, ui); } /// Render [`Snarl`] using given viewer and style into the [`Ui`]. @@ -254,114 +531,92 @@ impl Snarl { let snarl_id = ui.make_persistent_id(id_source); // Draw background pattern. - let bg_fill = style - .bg_fill - .unwrap_or_else(|| ui.visuals().widgets.noninteractive.bg_fill); - - let bg_stroke = style - .background_pattern_stroke - .unwrap_or_else(|| ui.visuals().widgets.noninteractive.bg_stroke); + let bg_frame = style.bg_frame(ui); let input = ui.ctx().input(|i| Input { scroll_delta: i.raw_scroll_delta.y, hover_pos: i.pointer.hover_pos(), + interact_pos: i.pointer.interact_pos(), modifiers: i.modifiers, // primary_pressed: i.pointer.primary_pressed(), secondary_pressed: i.pointer.secondary_pressed(), }); - Frame::none() - .fill(bg_fill) - .stroke(bg_stroke) - .show(ui, |ui| { - let mut node_moved = None; - let mut node_to_top = None; - - let mut bg_r = ui.allocate_rect(ui.max_rect(), Sense::click_and_drag()); - let viewport = bg_r.rect; - ui.set_clip_rect(viewport); - - let pivot = input.hover_pos.unwrap_or_else(|| viewport.center()); - - let mut snarl_state = - SnarlState::load(ui.ctx(), snarl_id, pivot, viewport, self, style); - - let mut node_style: Style = (**ui.style()).clone(); - node_style.zoom(snarl_state.scale()); - - //Draw background - Self::draw_background(style, &snarl_state, &viewport, ui); - - let pin_size = style - .pin_size - .zoomed(snarl_state.scale()) - .unwrap_or(node_style.spacing.interact_size.y * 0.5); - - let wire_frame_size = style - .wire_frame_size - .zoomed(snarl_state.scale()) - .unwrap_or(pin_size * 5.0); - let wire_width = style - .wire_width - .zoomed(snarl_state.scale()) - .unwrap_or(pin_size * 0.2); - - let node_frame = style - .node_frame - .zoomed(snarl_state.scale()) - .unwrap_or_else(|| Frame::window(&node_style)); - - let header_frame = style - .header_frame - .zoomed(snarl_state.scale()) - .unwrap_or_else(|| node_frame.shadow(Shadow::NONE).fill(Color32::TRANSPARENT)); - - let wire_shape_idx = match style.wire_layer { - WireLayer::BehindNodes => Some(ui.painter().add(Shape::Noop)), - WireLayer::AboveNodes => None, - }; - - // Zooming - match input.hover_pos { - Some(hover_pos) if viewport.contains(hover_pos) => { - if input.scroll_delta != 0.0 { - let new_scale = (snarl_state.scale() - * (1.0 + input.scroll_delta * style.scale_velocity)) - .clamp(style.min_scale, style.max_scale); - - snarl_state.set_scale(new_scale); - } + bg_frame.show(ui, |ui| { + let mut node_moved = None; + let mut node_to_top = None; + + let mut bg_r = ui.allocate_rect(ui.max_rect(), Sense::click_and_drag()); + let viewport = bg_r.rect; + ui.set_clip_rect(viewport); + + let pivot = input.hover_pos.unwrap_or_else(|| viewport.center()); + + let mut snarl_state = + SnarlState::load(ui.ctx(), snarl_id, pivot, viewport, self, style); + + ui.style_mut().zoom(snarl_state.scale()); + + // let mut node_style: Style = (**ui.style()).clone(); + // node_style.zoom(snarl_state.scale()); + + //Draw background + Self::draw_background(style, &snarl_state, &viewport, ui); + + let wire_frame_size = style.wire_frame_size(snarl_state.scale(), ui); + let wire_width = style.wire_width(snarl_state.scale(), ui); + let node_frame = style.node_frame(snarl_state.scale(), ui); + let header_frame = style.header_frame(snarl_state.scale(), ui); + + let wire_shape_idx = match style.wire_layer() { + WireLayer::BehindNodes => Some(ui.painter().add(Shape::Noop)), + WireLayer::AboveNodes => None, + }; + + // Zooming + match input.hover_pos { + Some(hover_pos) if viewport.contains(hover_pos) => { + if input.scroll_delta != 0.0 { + let new_scale = (snarl_state.scale() + * (1.0 + input.scroll_delta * style.scale_velocity())) + .clamp(style.min_scale(), style.max_scale()); + + snarl_state.set_scale(new_scale); } - _ => {} } + _ => {} + } - let mut input_info = HashMap::new(); - let mut output_info = HashMap::new(); - - let mut pin_hovered = None; - - let draw_order = snarl_state.update_draw_order(self); - let mut drag_released = false; - - let mut centers_sum = vec2(0.0, 0.0); - let centers_weight = draw_order.len(); - - for node_idx in draw_order { - // show_node(node_idx); - let response = self.draw_node( - ui, - node_idx, - viewer, - &mut snarl_state, - style, - snarl_id, - &node_style, - &node_frame, - &header_frame, - &mut input_info, - &input, - &mut output_info, - ); + let mut input_info = HashMap::new(); + let mut output_info = HashMap::new(); + + let mut pin_hovered = None; + + let draw_order = snarl_state.update_draw_order(self); + let mut drag_released = false; + + let mut centers_sum = vec2(0.0, 0.0); + let mut centers_weight = 0; + + let mut node_rects = Vec::new(); + + for node_idx in draw_order { + // show_node(node_idx); + let response = self.draw_node( + ui, + node_idx, + viewer, + &mut snarl_state, + style, + snarl_id, + &node_frame, + &header_frame, + &mut input_info, + &input, + &mut output_info, + ); + + if let Some(response) = response { if let Some(v) = response.node_to_top { node_to_top = Some(v); } @@ -372,283 +627,342 @@ impl Snarl { pin_hovered = Some(v); } drag_released |= response.drag_released; - centers_sum += response.center.to_vec2(); - } - let mut hovered_wire = None; - let mut hovered_wire_disconnect = false; - let mut wire_shapes = Vec::new(); - let mut wire_hit = false; + centers_sum += response.rect.center().to_vec2(); + centers_weight += 1; - for wire in self.wires.iter() { - let from_r = &output_info[&wire.out_pin]; - let to_r = &input_info[&wire.in_pin]; + if snarl_state.is_rect_selection() { + node_rects.push((node_idx, response.rect)); + } + } + } - if !wire_hit - && !snarl_state.has_new_wires() - && bg_r.hovered() - && !bg_r.dragged() - { - // Try to find hovered wire - // If not draggin new wire - // And not hovering over item above. - - if let Some(hover_pos) = input.hover_pos { - wire_hit = hit_wire( - hover_pos, - wire_frame_size, - style.upscale_wire_frame, - style.downscale_wire_frame, - from_r.pos, - to_r.pos, - wire_width.max(1.5), - pick_wire_style( - style.wire_style, - from_r.wire_style, - to_r.wire_style, - ) - .zoomed(snarl_state.scale()), - ); + let mut hovered_wire = None; + let mut hovered_wire_disconnect = false; + let mut wire_shapes = Vec::new(); + let mut wire_hit = false; + + for wire in self.wires.iter() { + let from_r = &output_info[&wire.out_pin]; + let to_r = &input_info[&wire.in_pin]; + + if !wire_hit && !snarl_state.has_new_wires() && bg_r.hovered() && !bg_r.dragged() { + // Try to find hovered wire + // If not draggin new wire + // And not hovering over item above. + + if let Some(interact_pos) = input.interact_pos { + wire_hit = hit_wire( + interact_pos, + wire_frame_size, + style.upscale_wire_frame(), + style.downscale_wire_frame(), + from_r.pos, + to_r.pos, + wire_width.max(1.5), + pick_wire_style( + style.wire_style(snarl_state.scale()), + from_r.wire_style, + to_r.wire_style, + ) + .zoomed(snarl_state.scale()), + ); - if wire_hit { - hovered_wire = Some(wire); + if wire_hit { + hovered_wire = Some(wire); - //Remove hovered wire by second click - hovered_wire_disconnect |= - bg_r.clicked_by(PointerButton::Secondary); + //Remove hovered wire by second click + hovered_wire_disconnect |= bg_r.clicked_by(PointerButton::Secondary); - // Background is not hovered then. - bg_r.hovered = false; - bg_r.clicked = false; - } + // Background is not hovered then. + bg_r.hovered = false; + bg_r.clicked = false; } } + } - let color = mix_colors(from_r.pin_fill, to_r.pin_fill); - - let mut draw_width = wire_width; - if hovered_wire == Some(wire) { - draw_width *= 1.5; - } + let color = mix_colors(from_r.pin_fill, to_r.pin_fill); - draw_wire( - ui, - &mut wire_shapes, - wire_frame_size, - style.upscale_wire_frame, - style.downscale_wire_frame, - from_r.pos, - to_r.pos, - Stroke::new(draw_width, color), - pick_wire_style( - style.wire_style.zoomed(snarl_state.scale()), - from_r.wire_style, - to_r.wire_style, - ), - ); + let mut draw_width = wire_width; + if hovered_wire == Some(wire) { + draw_width *= 1.5; } - //Remove hovered wire by second click - if hovered_wire_disconnect { - if let Some(wire) = hovered_wire { - let out_pin = OutPin::new(self, wire.out_pin); - let in_pin = InPin::new(self, wire.in_pin); - viewer.disconnect(&out_pin, &in_pin, self); - } - } + draw_wire( + ui, + &mut wire_shapes, + wire_frame_size, + style.upscale_wire_frame(), + style.downscale_wire_frame(), + from_r.pos, + to_r.pos, + Stroke::new(draw_width, color), + pick_wire_style( + style.wire_style(snarl_state.scale()), + from_r.wire_style.zoomed(snarl_state.scale()), + to_r.wire_style.zoomed(snarl_state.scale()), + ), + ); + } - if bg_r.dragged_by(PointerButton::Primary) { - snarl_state.pan(-bg_r.drag_delta()); + //Remove hovered wire by second click + if hovered_wire_disconnect { + if let Some(wire) = hovered_wire { + let out_pin = OutPin::new(self, wire.out_pin); + let in_pin = InPin::new(self, wire.in_pin); + viewer.disconnect(&out_pin, &in_pin, self); } + } - // If right button is clicked while new wire is being dragged, cancel it. - // This is to provide way to 'not open' the link graph node menu, but just - // releasing the new wire to empty space. - // - // This uses `button_down` directly, instead of `clicked_by` to improve - // responsiveness of the cancel action. - if snarl_state.has_new_wires() - && ui.input(|x| x.pointer.button_down(PointerButton::Secondary)) - { - let _ = snarl_state.take_wires(); - bg_r.clicked = false; - } + if bg_r.drag_started_by(PointerButton::Primary) && input.modifiers.shift { + let screen_pos = input.interact_pos.unwrap_or(viewport.center()); + let graph_pos = snarl_state.screen_pos_to_graph(screen_pos, viewport); + snarl_state.start_rect_selection(graph_pos); + } - //Do centering - if style.centering && bg_r.double_clicked() { - centers_sum /= centers_weight as f32; - snarl_state.set_offset(centers_sum * snarl_state.scale()); + if bg_r.dragged_by(PointerButton::Primary) { + if snarl_state.is_rect_selection() && input.hover_pos.is_some() { + let screen_pos = input.hover_pos.unwrap(); + let graph_pos = snarl_state.screen_pos_to_graph(screen_pos, viewport); + snarl_state.update_rect_selection(graph_pos); + } else { + snarl_state.pan(-bg_r.drag_delta()); } + } - // Wire end position will be overrided when link graph menu is opened. - let mut wire_end_pos = input.hover_pos.unwrap_or_default(); + if bg_r.drag_stopped_by(PointerButton::Primary) { + if let Some(select_rect) = snarl_state.rect_selection() { + let select_nodes = node_rects.into_iter().filter_map(|(id, rect)| { + let select = match style.select_rect_contained() { + true => select_rect.contains_rect(rect), + false => select_rect.intersects(rect), + }; - if drag_released { - let new_wires = snarl_state.take_wires(); - if new_wires.is_some() { - ui.ctx().request_repaint(); - } - match (new_wires, pin_hovered) { - (Some(NewWires::In(in_pins)), Some(AnyPin::Out(out_pin))) => { - for in_pin in in_pins { - viewer.connect( - &OutPin::new(self, out_pin), - &InPin::new(self, in_pin), - self, - ); - } - } - (Some(NewWires::Out(out_pins)), Some(AnyPin::In(in_pin))) => { - for out_pin in out_pins { - viewer.connect( - &OutPin::new(self, out_pin), - &InPin::new(self, in_pin), - self, - ); - } + if select { + Some(id) + } else { + None } - (Some(new_wires), None) if bg_r.hovered() => { - // A new pin is dropped without connecting it anywhere. This - // will open a pop-up window for creating a new node. - snarl_state.revert_take_wires(new_wires); + }); - // Force open context menu. - bg_r.long_touched = true; - } - _ => {} + if input.modifiers.command { + snarl_state.deselect_many_nodes(select_nodes); + } else { + snarl_state.select_many_nodes(!input.modifiers.shift, select_nodes); } + + snarl_state.stop_rect_selection(); } + } - // Open graph menu when right-clicking on empty space. - let mut is_menu_visible = false; + if let Some(select_rect) = snarl_state.rect_selection() { + ui.painter().rect( + snarl_state.graph_rect_to_screen(select_rect, viewport), + 0.0, + style.select_fill(ui), + style.select_stroke(snarl_state.scale(), ui), + ); + } - if let Some(interact_pos) = ui.ctx().input(|i| i.pointer.interact_pos()) { - if snarl_state.has_new_wires() { - let pins = match snarl_state.new_wires().unwrap() { - NewWires::In(x) => AnyPins::In(x), - NewWires::Out(x) => AnyPins::Out(x), - }; + // If right button is clicked while new wire is being dragged, cancel it. + // This is to provide way to 'not open' the link graph node menu, but just + // releasing the new wire to empty space. + // + // This uses `button_down` directly, instead of `clicked_by` to improve + // responsiveness of the cancel action. + if snarl_state.has_new_wires() + && ui.input(|x| x.pointer.button_down(PointerButton::Secondary)) + { + let _ = snarl_state.take_wires(); + bg_r.clicked = false; + } - if viewer.has_dropped_wire_menu(pins, self) { - bg_r.context_menu(|ui| { - is_menu_visible = true; - if !snarl_state.is_link_menu_open() { - // Mark link menu is now visible. - snarl_state.open_link_menu(); - } + //Do centering + if style.centering() && bg_r.double_clicked() { + centers_sum /= centers_weight as f32; + snarl_state.set_offset(centers_sum * snarl_state.scale()); + } - let pins = match snarl_state.new_wires().unwrap() { - NewWires::In(x) => AnyPins::In(x), - NewWires::Out(x) => AnyPins::Out(x), - }; - - wire_end_pos = ui.cursor().min; - - // The context menu is opened as *link* graph menu. - viewer.show_dropped_wire_menu( - snarl_state.screen_pos_to_graph(ui.cursor().min, viewport), - ui, - snarl_state.scale(), - pins, - self, - ); - }); - } - } else { - if snarl_state.is_link_menu_open() - || viewer.has_graph_menu(interact_pos, self) - { - bg_r.context_menu(|ui| { - is_menu_visible = true; - if !snarl_state.is_link_menu_open() { - // Mark link menu is now visible. - snarl_state.open_link_menu(); - } + if input.modifiers.command && bg_r.clicked_by(PointerButton::Primary) { + snarl_state.deselect_all_nodes(); + } + + // Wire end position will be overrided when link graph menu is opened. + let mut wire_end_pos = input.hover_pos.unwrap_or_default(); - viewer.show_graph_menu( - snarl_state.screen_pos_to_graph(ui.cursor().min, viewport), - ui, - snarl_state.scale(), - self, - ); - }); + if drag_released { + let new_wires = snarl_state.take_wires(); + if new_wires.is_some() { + ui.ctx().request_repaint(); + } + match (new_wires, pin_hovered) { + (Some(NewWires::In(in_pins)), Some(AnyPin::Out(out_pin))) => { + for in_pin in in_pins { + viewer.connect( + &OutPin::new(self, out_pin), + &InPin::new(self, in_pin), + self, + ); } } - } + (Some(NewWires::Out(out_pins)), Some(AnyPin::In(in_pin))) => { + for out_pin in out_pins { + viewer.connect( + &OutPin::new(self, out_pin), + &InPin::new(self, in_pin), + self, + ); + } + } + (Some(new_wires), None) if bg_r.hovered() => { + // A new pin is dropped without connecting it anywhere. This + // will open a pop-up window for creating a new node. + snarl_state.revert_take_wires(new_wires); - if !is_menu_visible && snarl_state.is_link_menu_open() { - // It seems that the context menu was closed. Remove new wires. - snarl_state.close_link_menu(); + // Force open context menu. + bg_r.long_touched = true; + } + _ => {} } + } + + // Open graph menu when right-clicking on empty space. + let mut is_menu_visible = false; + + if let Some(interact_pos) = ui.ctx().input(|i| i.pointer.interact_pos()) { + if snarl_state.has_new_wires() { + let pins = match snarl_state.new_wires().unwrap() { + NewWires::In(x) => AnyPins::In(x), + NewWires::Out(x) => AnyPins::Out(x), + }; + + if viewer.has_dropped_wire_menu(pins, self) { + bg_r.context_menu(|ui| { + is_menu_visible = true; + if !snarl_state.is_link_menu_open() { + // Mark link menu is now visible. + snarl_state.open_link_menu(); + } - match snarl_state.new_wires() { - None => {} - Some(NewWires::In(pins)) => { - for pin in pins { - let from_pos = wire_end_pos; - let to_r = &input_info[pin]; + let pins = match snarl_state.new_wires().unwrap() { + NewWires::In(x) => AnyPins::In(x), + NewWires::Out(x) => AnyPins::Out(x), + }; - draw_wire( + wire_end_pos = ui.cursor().min; + + // The context menu is opened as *link* graph menu. + viewer.show_dropped_wire_menu( + snarl_state.screen_pos_to_graph(ui.cursor().min, viewport), ui, - &mut wire_shapes, - wire_frame_size, - style.upscale_wire_frame, - style.downscale_wire_frame, - from_pos, - to_r.pos, - Stroke::new(wire_width, to_r.pin_fill), - to_r.wire_style - .unwrap_or(style.wire_style) - .zoomed(snarl_state.scale()), + snarl_state.scale(), + pins, + self, ); - } + }); } - Some(NewWires::Out(pins)) => { - for pin in pins { - let from_r = &output_info[pin]; - let to_pos = wire_end_pos; + } else { + if snarl_state.is_link_menu_open() || viewer.has_graph_menu(interact_pos, self) + { + bg_r.context_menu(|ui| { + is_menu_visible = true; + if !snarl_state.is_link_menu_open() { + // Mark link menu is now visible. + snarl_state.open_link_menu(); + } - draw_wire( + viewer.show_graph_menu( + snarl_state.screen_pos_to_graph(ui.cursor().min, viewport), ui, - &mut wire_shapes, - wire_frame_size, - style.upscale_wire_frame, - style.downscale_wire_frame, - from_r.pos, - to_pos, - Stroke::new(wire_width, from_r.pin_fill), - from_r - .wire_style - .unwrap_or(style.wire_style) - .zoomed(snarl_state.scale()), + snarl_state.scale(), + self, ); - } + }); } } + } - match wire_shape_idx { - None => { - ui.painter().add(Shape::Vec(wire_shapes)); + if !is_menu_visible && snarl_state.is_link_menu_open() { + // It seems that the context menu was closed. Remove new wires. + snarl_state.close_link_menu(); + } + + match snarl_state.new_wires() { + None => {} + Some(NewWires::In(pins)) => { + for pin in pins { + let from_pos = wire_end_pos; + let to_r = &input_info[pin]; + + draw_wire( + ui, + &mut wire_shapes, + wire_frame_size, + style.upscale_wire_frame(), + style.downscale_wire_frame(), + from_pos, + to_r.pos, + Stroke::new(wire_width, to_r.pin_fill), + to_r.wire_style + .zoomed(snarl_state.scale()) + .unwrap_or(style.wire_style(snarl_state.scale())), + ); } - Some(idx) => { - ui.painter().set(idx, Shape::Vec(wire_shapes)); + } + Some(NewWires::Out(pins)) => { + for pin in pins { + let from_r = &output_info[pin]; + let to_pos = wire_end_pos; + + draw_wire( + ui, + &mut wire_shapes, + wire_frame_size, + style.upscale_wire_frame(), + style.downscale_wire_frame(), + from_r.pos, + to_pos, + Stroke::new(wire_width, from_r.pin_fill), + from_r + .wire_style + .zoomed(snarl_state.scale()) + .unwrap_or(style.wire_style(snarl_state.scale())), + ); } } + } - ui.advance_cursor_after_rect(Rect::from_min_size(viewport.min, Vec2::ZERO)); - - if let Some(node) = node_to_top { - ui.ctx().request_repaint(); - snarl_state.node_to_top(node); + match wire_shape_idx { + None => { + ui.painter().add(Shape::Vec(wire_shapes)); + } + Some(idx) => { + ui.painter().set(idx, Shape::Vec(wire_shapes)); } - snarl_state.store(ui.ctx()); + } - if let Some((node, delta)) = node_moved { - ui.ctx().request_repaint(); + ui.advance_cursor_after_rect(Rect::from_min_size(viewport.min, Vec2::ZERO)); + + if let Some(node) = node_to_top { + ui.ctx().request_repaint(); + snarl_state.node_to_top(node); + } + + if let Some((node, delta)) = node_moved { + ui.ctx().request_repaint(); + if snarl_state.selected_nodes().contains(&node) { + for node in snarl_state.selected_nodes() { + let node = &mut self.nodes[node.0]; + node.pos += delta; + } + } else { let node = &mut self.nodes[node.0]; node.pos += delta; } - }); + } + + snarl_state.store(ui.ctx()); + }); } //First step for split big function to parts @@ -664,13 +978,12 @@ impl Snarl { snarl_state: &mut SnarlState, style: &SnarlStyle, snarl_id: Id, - node_style: &Style, node_frame: &Frame, header_frame: &Frame, input_positions: &mut HashMap, input: &Input, output_positions: &mut HashMap, - ) -> DrawNodeResponse + ) -> Option where V: SnarlViewer, { @@ -680,14 +993,6 @@ impl Snarl { ref value, } = self.nodes[node.0]; - let mut response = DrawNodeResponse { - node_to_top: None, - node_moved: None, - drag_released: false, - pin_hovered: None, - center: pos, - }; - let viewport = ui.max_rect(); // Collect pins @@ -701,23 +1006,34 @@ impl Snarl { let openness = ui.ctx().animate_bool(node_id, open); - let mut node_state = - NodeState::load(ui.ctx(), node_id, &node_style.spacing, snarl_state.scale()); + let mut node_state = NodeState::load(ui.ctx(), node_id, ui.spacing(), snarl_state.scale()); let node_rect = node_state.node_rect(node_pos, openness); + let mut node_to_top = None; + let mut node_moved = None; + let mut drag_released = false; + let mut pin_hovered = None; + // Rect for node + frame margin. let node_frame_rect = node_frame.total_margin().expand_rect(node_rect); - let pin_size = style - .pin_size - .zoomed(snarl_state.scale()) - .unwrap_or(node_style.spacing.interact_size.y * 0.5); + if snarl_state.selected_nodes().contains(&node) { + let select_style = style.select_style(snarl_state.scale(), ui); + + let select_rect = select_style.margin.expand_rect(node_frame_rect); - let header_drag_space = style - .header_drag_space - .zoomed(snarl_state.scale()) - .unwrap_or_else(|| vec2(node_style.spacing.icon_width, node_style.spacing.icon_width)); + ui.painter().rect( + select_rect, + select_style.rounding, + select_style.fill, + select_style.stroke, + ); + } + + let pin_size = style.pin_size(snarl_state.scale(), ui); + + let header_drag_space = style.header_drag_space(snarl_state.scale(), ui); let inputs = (0..inputs_count) .map(|idx| InPin::new(self, InPinId { node, input: idx })) @@ -730,11 +1046,23 @@ impl Snarl { // Interact with node frame. let r = ui.interact(node_frame_rect, node_id, Sense::click_and_drag()); - if r.dragged_by(PointerButton::Primary) { - response.node_moved = Some((node, snarl_state.screen_vec_to_graph(r.drag_delta()))); + if !input.modifiers.shift + && !input.modifiers.command + && r.dragged_by(PointerButton::Primary) + { + node_moved = Some((node, snarl_state.screen_vec_to_graph(r.drag_delta()))); } + + if r.clicked_by(PointerButton::Primary) || r.dragged_by(PointerButton::Primary) { + if input.modifiers.shift { + snarl_state.select_one_node(input.modifiers.command, node); + } else if input.modifiers.command { + snarl_state.deselect_one_node(node); + } + } + if r.clicked() || r.dragged() { - response.node_to_top = Some(node); + node_to_top = Some(node); } if viewer.has_node_menu(&self.nodes[node.0].value) { @@ -746,7 +1074,7 @@ impl Snarl { if !self.nodes.contains(node.0) { node_state.clear(ui.ctx()); // If removed - return response; + return None; } if viewer.has_on_hover_popup(&self.nodes[node.0].value) { @@ -758,7 +1086,7 @@ impl Snarl { if !self.nodes.contains(node.0) { node_state.clear(ui.ctx()); // If removed - return response; + return None; } let node_ui = &mut ui.child_ui_with_id_source( @@ -766,7 +1094,6 @@ impl Snarl { Layout::top_down(Align::Center), ("node", node_id), ); - node_ui.set_style(node_style.clone()); let r = node_frame.show(node_ui, |ui| { // Render header frame. @@ -783,9 +1110,9 @@ impl Snarl { header_frame.show(header_ui, |ui: &mut Ui| { ui.with_layout(Layout::left_to_right(Align::Min), |ui| { - if style.collapsible { + if style.collapsible() { let (_, r) = ui.allocate_exact_size( - vec2(node_style.spacing.icon_width, node_style.spacing.icon_width), + vec2(ui.spacing().icon_width, ui.spacing().icon_width), Sense::click(), ); paint_default_icon(ui, openness, &r); @@ -839,7 +1166,7 @@ impl Snarl { let payload_rect = Rect::from_min_max( pos2( header_rect.min.x, - header_frame_rect.max.y + node_style.spacing.item_spacing.y + header_frame_rect.max.y + ui.spacing().item_spacing.y - node_state.payload_offset(openness), ), pos2(f32::max(node_rect.max.x, header_rect.max.x), f32::INFINITY), @@ -918,7 +1245,7 @@ impl Snarl { } } if r.drag_stopped() { - response.drag_released = true; + drag_released = true; } let mut pin_size = pin_size; @@ -930,23 +1257,22 @@ impl Snarl { } else if input.secondary_pressed { snarl_state.remove_new_wire_in(in_pin.id); } - response.pin_hovered = Some(AnyPin::In(in_pin.id)); + pin_hovered = Some(AnyPin::In(in_pin.id)); pin_size *= 1.2; } _ => {} } - let pin_fill = pin_info - .fill - .unwrap_or(default_pin_fill(style, &node_style.visuals)); + let pin_fill = pin_info.fill.unwrap_or(style.pin_fill(ui)); draw_pin( ui.painter(), - pin_info.shape.as_ref().unwrap_or(&style.pin_shape.into()), + pin_info.shape.as_ref().unwrap_or(&style.pin_shape()), pin_fill, pin_info .stroke - .unwrap_or(default_pin_stroke(style, &node_style.visuals)), + .zoomed(snarl_state.scale()) + .unwrap_or(style.pin_stoke(snarl_state.scale(), ui)), pin_pos, pin_size, ); @@ -1041,7 +1367,7 @@ impl Snarl { } } if r.drag_stopped() { - response.drag_released = true; + drag_released = true; } let mut pin_size = pin_size; @@ -1052,23 +1378,22 @@ impl Snarl { } else if input.secondary_pressed { snarl_state.remove_new_wire_out(out_pin.id); } - response.pin_hovered = Some(AnyPin::Out(out_pin.id)); + pin_hovered = Some(AnyPin::Out(out_pin.id)); pin_size *= 1.2; } _ => {} } - let pin_fill = pin_info - .fill - .unwrap_or(default_pin_fill(style, &node_style.visuals)); + let pin_fill = pin_info.fill.unwrap_or(style.pin_fill(ui)); draw_pin( ui.painter(), - pin_info.shape.as_ref().unwrap_or(&style.pin_shape.into()), + pin_info.shape.as_ref().unwrap_or(&style.pin_shape()), pin_fill, pin_info .stroke - .unwrap_or(default_pin_stroke(style, &node_style.visuals)), + .zoomed(snarl_state.scale()) + .unwrap_or(style.pin_stoke(snarl_state.scale(), ui)), pin_pos, pin_size, ); @@ -1093,7 +1418,7 @@ impl Snarl { } let mut new_pins_size = vec2( - inputs_size.x + outputs_size.x + node_style.spacing.item_spacing.x, + inputs_size.x + outputs_size.x + ui.spacing().item_spacing.x, f32::max(inputs_size.y, outputs_size.y), ); @@ -1101,8 +1426,8 @@ impl Snarl { // Show body if there's one. if viewer.has_body(&self.nodes.get(node.0).unwrap().value) { - let body_left = inputs_rect.right() + node_style.spacing.item_spacing.x; - let body_right = outputs_rect.left() - node_style.spacing.item_spacing.x; + let body_left = inputs_rect.right() + ui.spacing().item_spacing.x; + let body_right = outputs_rect.left() - ui.spacing().item_spacing.x; let body_top = payload_rect.top(); let mut body_rect = @@ -1130,7 +1455,7 @@ impl Snarl { let body_size = body_rect.size(); node_state.set_body_width(body_size.x); - new_pins_size.x += body_size.x + node_style.spacing.item_spacing.x; + new_pins_size.x += body_size.x + ui.spacing().item_spacing.x; new_pins_size.y = f32::max(new_pins_size.y, body_size.y); pins_bottom = f32::max(pins_bottom, body_rect.bottom()); @@ -1144,7 +1469,7 @@ impl Snarl { if viewer.has_footer(&self.nodes[node.0].value) { let footer_left = node_rect.left(); let footer_right = node_rect.right(); - let footer_top = pins_bottom + node_style.spacing.item_spacing.y; + let footer_top = pins_bottom + ui.spacing().item_spacing.y; let mut footer_rect = Rect::from_min_max( pos2(footer_left, footer_top), @@ -1175,7 +1500,7 @@ impl Snarl { node_state.set_footer_width(footer_size.x); new_pins_size.x = f32::max(new_pins_size.x, footer_size.x); - new_pins_size.y += footer_size.y + node_style.spacing.item_spacing.y; + new_pins_size.y += footer_size.y + ui.spacing().item_spacing.y; if !self.nodes.contains(node.0) { // If removed @@ -1187,25 +1512,37 @@ impl Snarl { f32::max(header_size.x, new_pins_size.x), header_size.y + header_frame.total_margin().bottom - + node_style.spacing.item_spacing.y + + ui.spacing().item_spacing.y + new_pins_size.y, )); }); - let node_graph_rect = snarl_state.screen_rect_to_graph(r.response.rect, viewport); - viewer.final_node_rect(node, node_graph_rect, self); - response.center = node_graph_rect.center(); - if !self.nodes.contains(node.0) { ui.ctx().request_repaint(); node_state.clear(ui.ctx()); // If removed - return response; + return None; } + let final_rect = snarl_state.screen_rect_to_graph(r.response.rect, viewport); + viewer.final_node_rect( + node, + r.response.rect, + final_rect, + ui, + snarl_state.scale(), + self, + ); + node_state.store(ui.ctx()); ui.ctx().request_repaint(); - response + Some(DrawNodeResponse { + node_moved, + node_to_top, + drag_released, + pin_hovered, + rect: final_rect, + }) } } diff --git a/src/ui/background_pattern.rs b/src/ui/background_pattern.rs index 44e448b..b888835 100644 --- a/src/ui/background_pattern.rs +++ b/src/ui/background_pattern.rs @@ -1,6 +1,6 @@ use std::fmt; -use egui::{emath::Rot2, vec2, Pos2, Rect, Stroke, Ui, Vec2}; +use egui::{emath::Rot2, vec2, Pos2, Rect, Ui, Vec2}; use super::SnarlStyle; @@ -68,10 +68,10 @@ pub struct Grid { pub angle: f32, } -const DEFAULT_GRID_SPACING: Vec2 = vec2(5.0, 5.0); +const DEFAULT_GRID_SPACING: Vec2 = vec2(50.0, 50.0); macro_rules! default_grid_spacing { () => { - stringify!(vec2(5.0, 5.0)) + stringify!(vec2(50.0, 50.0)) }; } @@ -98,17 +98,9 @@ impl Grid { } fn draw(&self, style: &SnarlStyle, viewport: &Viewport, ui: &mut Ui) { - let bg_stroke = style - .background_pattern_stroke - .unwrap_or_else(|| ui.visuals().widgets.noninteractive.bg_stroke); + let bg_stroke = style.bg_pattern_stroke(viewport.scale, ui); - let stroke = Stroke::new( - bg_stroke.width * viewport.scale.max(1.0), - bg_stroke.color.gamma_multiply(viewport.scale.min(1.0)), - ); - - let spacing = - ui.spacing().icon_width * vec2(self.spacing.x.max(1.0), self.spacing.y.max(1.0)); + let spacing = vec2(self.spacing.x.max(1.0), self.spacing.y.max(1.0)); let rot = Rot2::from_angle(self.angle); let rot_inv = rot.inverse(); @@ -133,7 +125,7 @@ impl Grid { let top = viewport.graph_pos_to_screen(top); let bottom = viewport.graph_pos_to_screen(bottom); - ui.painter().line_segment([top, bottom], stroke); + ui.painter().line_segment([top, bottom], bg_stroke); } let min_y = (pattern_bounds.min.y / spacing.y).ceil(); @@ -149,7 +141,7 @@ impl Grid { let top = viewport.graph_pos_to_screen(top); let bottom = viewport.graph_pos_to_screen(bottom); - ui.painter().line_segment([top, bottom], stroke); + ui.painter().line_segment([top, bottom], bg_stroke); } } } @@ -219,7 +211,7 @@ impl fmt::Debug for BackgroundPattern { impl Default for BackgroundPattern { fn default() -> Self { - Self::Grid(Default::default()) + BackgroundPattern::new() } } diff --git a/src/ui/pin.rs b/src/ui/pin.rs index ce4e5ac..9db58fe 100644 --- a/src/ui/pin.rs +++ b/src/ui/pin.rs @@ -1,8 +1,8 @@ -use egui::{epaint::PathShape, vec2, Color32, Painter, Pos2, Rect, Shape, Stroke, Vec2, Visuals}; +use egui::{epaint::PathShape, vec2, Color32, Painter, Pos2, Rect, Shape, Stroke, Vec2}; use crate::{InPinId, OutPinId}; -use super::{SnarlStyle, WireStyle}; +use super::WireStyle; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum AnyPin { @@ -37,7 +37,7 @@ pub enum PinShape { /// Square shape. Square, - /// Star + /// Star shape. Star, /// Custom shape. @@ -48,7 +48,7 @@ pub enum PinShape { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "egui-probe", derive(egui_probe::EguiProbe))] -pub enum DefaultPinShape { +pub enum BasicPinShape { /// Circle shape. Circle, @@ -57,22 +57,26 @@ pub enum DefaultPinShape { /// Square shape. Square, + + /// Star shape. + Star, } -impl Default for DefaultPinShape { +impl Default for BasicPinShape { #[inline(always)] fn default() -> Self { - DefaultPinShape::Circle + BasicPinShape::Circle } } -impl From for PinShape { +impl From for PinShape { #[inline(always)] - fn from(shape: DefaultPinShape) -> Self { + fn from(shape: BasicPinShape) -> Self { match shape { - DefaultPinShape::Circle => PinShape::Circle, - DefaultPinShape::Triangle => PinShape::Triangle, - DefaultPinShape::Square => PinShape::Square, + BasicPinShape::Circle => PinShape::Circle, + BasicPinShape::Triangle => PinShape::Triangle, + BasicPinShape::Square => PinShape::Square, + BasicPinShape::Star => PinShape::Star, } } } @@ -165,6 +169,14 @@ impl PinInfo { } } + /// Creates a star pin. + pub fn star() -> Self { + PinInfo { + shape: Some(PinShape::Star), + ..Default::default() + } + } + /// Creates a square pin. pub fn custom(f: F) -> Self where @@ -221,17 +233,16 @@ pub fn draw_pin( PinShape::Star => { let points = vec![ - pos + (vec2(12.0, 2.0) - vec2(12.0, 12.0)) / 22.0 * size, - pos + (vec2(15.09, 8.26) - vec2(12.0, 12.0)) / 22.0 * size, - pos + (vec2(22.0, 9.27) - vec2(12.0, 12.0)) / 22.0 * size, - pos + (vec2(17.0, 14.14) - vec2(12.0, 12.0)) / 22.0 * size, - pos + (vec2(18.18, 21.02) - vec2(12.0, 12.0)) / 22.0 * size, - pos + (vec2(12.0, 17.77) - vec2(12.0, 12.0)) / 22.0 * size, - pos + (vec2(5.82, 21.02) - vec2(12.0, 12.0)) / 22.0 * size, - pos + (vec2(7.0, 14.14) - vec2(12.0, 12.0)) / 22.0 * size, - pos + (vec2(2.0, 9.27) - vec2(12.0, 12.0)) / 22.0 * size, - pos + (vec2(8.91, 8.26) - vec2(12.0, 12.0)) / 22.0 * size, - pos + (vec2(12.0, 2.0) - vec2(12.0, 12.0)) / 22.0 * size, + pos + size * 0.700000 * vec2(0.0, -1.0), + pos + size * 0.267376 * vec2(-0.587785, -0.809017), + pos + size * 0.700000 * vec2(-0.951057, -0.309017), + pos + size * 0.267376 * vec2(-0.951057, 0.309017), + pos + size * 0.700000 * vec2(-0.587785, 0.809017), + pos + size * 0.267376 * vec2(0.0, 1.0), + pos + size * 0.700000 * vec2(0.587785, 0.809017), + pos + size * 0.267376 * vec2(0.951057, 0.309017), + pos + size * 0.700000 * vec2(0.951057, -0.309017), + pos + size * 0.267376 * vec2(0.587785, -0.809017), ]; painter.add(Shape::Path(PathShape { @@ -250,14 +261,3 @@ pub fn draw_pin( ), } } - -pub fn default_pin_fill(style: &SnarlStyle, visuals: &Visuals) -> Color32 { - style.pin_fill.unwrap_or(visuals.widgets.active.bg_fill) -} - -pub fn default_pin_stroke(style: &SnarlStyle, visuals: &Visuals) -> Stroke { - style.pin_stroke.unwrap_or(Stroke::new( - visuals.widgets.active.bg_stroke.width, - visuals.widgets.active.bg_stroke.color, - )) -} diff --git a/src/ui/state.rs b/src/ui/state.rs index 363690a..6b9a60e 100644 --- a/src/ui/state.rs +++ b/src/ui/state.rs @@ -135,6 +135,12 @@ pub enum NewWires { Out(Vec), } +#[derive(Clone, Copy)] +struct RectSelect { + origin: Pos2, + current: Pos2, +} + pub struct SnarlState { /// Where viewport's center in graph's space. offset: Vec2, @@ -156,6 +162,12 @@ pub struct SnarlState { /// Order of nodes to draw. draw_order: Vec, + + /// Active rect selection. + rect_selection: Option, + + /// Set of currently selected nodes. + selected_nodes: HashSet, } #[derive(Clone)] @@ -166,6 +178,8 @@ struct SnarlStateData { new_wires: Option, is_link_menu_open: bool, draw_order: Vec, + rect_selection: Option, + selected_nodes: HashSet, } impl SnarlState { @@ -201,6 +215,8 @@ impl SnarlState { id, dirty, draw_order: data.draw_order, + rect_selection: data.rect_selection, + selected_nodes: data.selected_nodes, } } @@ -211,33 +227,23 @@ impl SnarlState { bb.extend_with(node.pos); } - if !bb.is_positive() { - let scale = 1.0f32.clamp(style.min_scale, style.max_scale); + let mut offset = Vec2::ZERO; + let mut scale = 1.0f32.clamp(style.min_scale(), style.max_scale()); - return SnarlState { - offset: Vec2::ZERO, - scale, - target_scale: scale, - new_wires: None, - is_link_menu_open: false, - id, - dirty: true, - draw_order: Vec::new(), - }; - } - - bb = bb.expand(100.0); + if bb.is_positive() { + bb = bb.expand(100.0); - let bb_size = bb.size(); - let viewport_size = viewport.size(); + let bb_size = bb.size(); + let viewport_size = viewport.size(); - let scale = (viewport_size.x / bb_size.x) - .min(1.0) - .min(viewport_size.y / bb_size.y) - .min(style.max_scale) - .max(style.min_scale); + scale = (viewport_size.x / bb_size.x) + .min(1.0) + .min(viewport_size.y / bb_size.y) + .min(style.max_scale()) + .max(style.min_scale()); - let offset = bb.center().to_vec2() * scale; + offset = bb.center().to_vec2() * scale; + } SnarlState { offset, @@ -248,6 +254,8 @@ impl SnarlState { id, dirty: true, draw_order: Vec::new(), + rect_selection: None, + selected_nodes: HashSet::default(), } } @@ -264,6 +272,8 @@ impl SnarlState { new_wires: self.new_wires, is_link_menu_open: self.is_link_menu_open, draw_order: self.draw_order, + rect_selection: self.rect_selection, + selected_nodes: self.selected_nodes, }, ) }); @@ -310,6 +320,14 @@ impl SnarlState { ) } + #[inline(always)] + pub fn graph_rect_to_screen(&self, rect: Rect, viewport: Rect) -> Rect { + Rect::from_min_max( + self.graph_pos_to_screen(rect.min, viewport), + self.graph_pos_to_screen(rect.max, viewport), + ) + } + // #[inline(always)] // pub fn graph_vec_to_screen(&self, size: Vec2) -> Vec2 { // size * self.scale @@ -452,4 +470,69 @@ impl SnarlState { self.offset = offset; self.dirty = true; } + + pub fn selected_nodes(&self) -> &HashSet { + &self.selected_nodes + } + + pub fn select_one_node(&mut self, reset: bool, node: NodeId) { + if reset { + self.deselect_all_nodes(); + } + + self.dirty |= self.selected_nodes.insert(node); + } + + pub fn select_many_nodes(&mut self, reset: bool, nodes: impl Iterator) { + if reset { + self.deselect_all_nodes(); + } + + self.selected_nodes.extend(nodes); + self.dirty |= !self.selected_nodes.is_empty(); + } + + pub fn deselect_one_node(&mut self, node: NodeId) { + self.dirty |= self.selected_nodes.remove(&node); + } + + pub fn deselect_many_nodes(&mut self, nodes: impl Iterator) { + for node in nodes { + self.dirty |= self.selected_nodes.remove(&node); + } + } + + pub fn deselect_all_nodes(&mut self) { + self.dirty |= !self.selected_nodes.is_empty(); + self.selected_nodes.clear(); + } + + pub fn start_rect_selection(&mut self, pos: Pos2) { + self.dirty |= self.rect_selection.is_none(); + self.rect_selection = Some(RectSelect { + origin: pos, + current: pos, + }); + } + + pub fn stop_rect_selection(&mut self) { + self.dirty |= self.rect_selection.is_some(); + self.rect_selection = None; + } + + pub fn is_rect_selection(&self) -> bool { + self.rect_selection.is_some() + } + + pub fn update_rect_selection(&mut self, pos: Pos2) { + if let Some(rect_selection) = &mut self.rect_selection { + rect_selection.current = pos; + self.dirty = true; + } + } + + pub fn rect_selection(&self) -> Option { + let rect = self.rect_selection?; + Some(Rect::from_two_pos(rect.origin, rect.current)) + } } diff --git a/src/ui/viewer.rs b/src/ui/viewer.rs index 278928e..2170f17 100644 --- a/src/ui/viewer.rs +++ b/src/ui/viewer.rs @@ -93,8 +93,16 @@ pub trait SnarlViewer { /// It aimed to be used for custom positioning of nodes that requires node dimensions for calculations. /// Node's position can be modfied directly in this method. #[inline] - fn final_node_rect(&mut self, node: NodeId, rect: Rect, snarl: &mut Snarl) { - let _ = (node, rect, snarl); + fn final_node_rect( + &mut self, + node: NodeId, + ui_rect: Rect, + graph_rect: Rect, + ui: &mut Ui, + scale: f32, + snarl: &mut Snarl, + ) { + let _ = (node, ui_rect, graph_rect, ui, scale, snarl); } /// Checks if node has something to show in on-hover popup. diff --git a/src/ui/wire.rs b/src/ui/wire.rs index c9fff3a..6533a4f 100644 --- a/src/ui/wire.rs +++ b/src/ui/wire.rs @@ -15,6 +15,12 @@ pub enum WireLayer { AboveNodes, } +impl Default for WireLayer { + fn default() -> Self { + WireLayer::BehindNodes + } +} + /// Controls style in which wire is rendered. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -34,6 +40,12 @@ pub enum WireStyle { }, } +impl Default for WireStyle { + fn default() -> Self { + WireStyle::Bezier5 + } +} + pub(crate) fn pick_wire_style( default: WireStyle, left: Option, diff --git a/src/ui/zoom.rs b/src/ui/zoom.rs index 6910479..d6e5797 100644 --- a/src/ui/zoom.rs +++ b/src/ui/zoom.rs @@ -4,9 +4,7 @@ use egui::{ FontId, Frame, Margin, Rounding, Stroke, Style, Vec2, Visuals, }; -use crate::Wire; - -use super::WireStyle; +use super::{SelectionStyle, WireStyle}; pub trait Zoom { #[inline(always)] @@ -198,6 +196,7 @@ impl Zoom for Frame { } impl Zoom for WireStyle { + #[inline(always)] fn zoom(&mut self, zoom: f32) { match self { WireStyle::Bezier3 | WireStyle::Bezier5 => {} @@ -207,3 +206,12 @@ impl Zoom for WireStyle { } } } + +impl Zoom for SelectionStyle { + #[inline(always)] + fn zoom(&mut self, zoom: f32) { + self.margin.zoom(zoom); + self.rounding.zoom(zoom); + self.stroke.zoom(zoom); + } +}