diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..cb7c8c6 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,40 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- NodeLayout enum + To control layout of nodes in the graph + Can be set globally in SnarlStyle and overridden per node with SnarlViewer::node_layout + Defaults to NodeLayout::Basic which is the previous layout + NodeLayout::Sandwich is a new layout that places inputs, body and outputs vertically with inputs on top and outputs on bottom + NodeLayout::FlippedSandwich is the same as Sandwich but with outputs on top and inputs on bottom + +- SnarlViewer::draw_input_pin/draw_output_pin can be used to override how pins are drawn. + Default implementation matches old behavior. + This mechanism is meant to replace PinShape::Custom that was removed. + +- SnarlViewer::draw_node_background can be used to override how node background is drawn. + Default implementation matches old behavior. + This mechanism is meant to replace BackgroundPattern::Custom that was removed. + +### Removed + +- BackgroundPattern::Custom is removed. + It contained opaque function to draw custom background pattern + and permitted !Send and !Sync captures which made SnarlStyle !Send and !Sync as well + +- PinShape::Custom is removed. + It is replaced by SnarlViewer::draw_input_pin/draw_output_pin which is more flexible. + +- BasicPinShape is removed. SnarlStyle::pin_shape has PinShape type now. + +### Fixed + +- Crash after centering graph when no nodes are present and adding a node afterwards \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 374da43..429be89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ workspace = { members = ["demo"] } [package] name = "egui-snarl" -version = "0.5.0" +version = "0.6.0" edition = "2021" description = "Node-graphs for egui" license = "MIT OR Apache-2.0" @@ -18,7 +18,6 @@ serde = ["dep:serde", "egui/serde", "slab/serde"] egui = { version = "0.29" } slab = { version = "0.4" } serde = { version = "1.0", features = ["derive"], optional = true } -tiny-fn = { version = "0.1.7" } egui-probe = { version = "0.6.0", features = ["derive"], optional = true } diff --git a/src/ui.rs b/src/ui.rs index 3985e29..702103e 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -4,8 +4,8 @@ use std::{collections::HashMap, hash::Hash}; use egui::{ collapsing_header::paint_default_icon, epaint::Shadow, pos2, vec2, Align, Color32, Frame, Id, - Layout, Margin, Modifiers, PointerButton, Pos2, Rect, Rounding, Sense, Shape, Stroke, Ui, - UiBuilder, Vec2, + Layout, Margin, Modifiers, PointerButton, Pos2, Rect, Rounding, Sense, Shape, Stroke, Style, + Ui, UiBuilder, Vec2, }; use crate::{InPin, InPinId, Node, NodeId, OutPin, OutPinId, Snarl}; @@ -18,19 +18,71 @@ mod wire; mod zoom; use self::{ - pin::{draw_pin, AnyPin}, + pin::AnyPin, state::{NewWires, NodeState, SnarlState}, wire::{draw_wire, hit_wire, pick_wire_style}, zoom::Zoom, }; pub use self::{ - background_pattern::{BackgroundPattern, CustomBackground, Grid, Viewport}, - pin::{AnyPins, BasicPinShape, CustomPinShape, PinInfo, PinShape}, + background_pattern::{BackgroundPattern, Grid, Viewport}, + pin::{AnyPins, PinInfo, PinShape}, viewer::SnarlViewer, wire::{WireLayer, WireStyle}, }; +/// Controls how header, pins, body and footer are laid out in the node. +#[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 enum NodeLayout { + /// Input pins, body and output pins are placed horizontally. + /// With header on top and footer on bottom. + /// + /// +---------------------+ + /// | Header | + /// +----+-----------+----+ + /// | In | Body | Out| + /// +----+-----------+----+ + /// | Footer | + /// +---------------------+ + /// + #[default] + Basic, + + /// All elements are placed in vertical stack. + /// Header is on top, then input pins, body, output pins and footer. + /// + /// +---------------------+ + /// | Header | + /// +---------------------+ + /// | In | + /// +---------------------+ + /// | Body | + /// +---------------------+ + /// | Out | + /// +---------------------+ + /// | Footer | + /// +---------------------+ + Sandwich, + + /// All elements are placed in vertical stack. + /// Header is on top, then output pins, body, input pins and footer. + /// + /// +---------------------+ + /// | Header | + /// +---------------------+ + /// | Out | + /// +---------------------+ + /// | Body | + /// +---------------------+ + /// | In | + /// +---------------------+ + /// | Footer | + /// +---------------------+ + FlippedSandwich, +} + /// Controls style of node selection rect. #[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -54,6 +106,58 @@ pub struct SelectionStyle { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "egui-probe", derive(egui_probe::EguiProbe))] pub struct SnarlStyle { + /// Controls how nodes are laid out. + /// Defaults to [`NodeLayout::Basic`]. + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub node_layout: Option, + + /// Frame used to draw nodes. + /// Defaults to [`Frame::window`] constructed from current ui's style. + #[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( + skip_serializing_if = "Option::is_none", + default, + with = "serde_frame_option" + ) + )] + pub header_frame: Option, + + /// Blank space for dragging node by its header. + /// Elements in the header are placed after this space. + #[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. + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "Option::is_none", default) + )] + pub collapsible: Option, + /// Size of pins. #[cfg_attr(feature = "egui-probe", egui_probe(range = 0.0..))] #[cfg_attr( @@ -81,7 +185,7 @@ pub struct SnarlStyle { feature = "serde", serde(skip_serializing_if = "Option::is_none", default) )] - pub pin_shape: Option, + pub pin_shape: Option, /// Width of wires. #[cfg_attr(feature = "egui-probe", egui_probe(range = 0.0..))] @@ -127,22 +231,6 @@ pub struct SnarlStyle { )] pub wire_layer: Option, - /// Additional blank space for dragging node by header. - #[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. - #[cfg_attr( - feature = "serde", - serde(skip_serializing_if = "Option::is_none", default) - )] - pub collapsible: Option, - /// Frame used to draw background #[cfg_attr( feature = "serde", @@ -170,7 +258,7 @@ pub struct SnarlStyle { )] pub bg_pattern_stroke: Option, - /// Minimum scale that can be set. + /// Minimum viewport scale that can be set. #[cfg_attr(feature = "egui-probe", egui_probe(range = 0.0..=1.0))] #[cfg_attr( feature = "serde", @@ -178,7 +266,7 @@ pub struct SnarlStyle { )] pub min_scale: Option, - /// Maximum scale that can be set. + /// Maximum viewport scale that can be set. #[cfg_attr(feature = "egui-probe", egui_probe(range = 1.0..))] #[cfg_attr( feature = "serde", @@ -186,7 +274,7 @@ pub struct SnarlStyle { )] pub max_scale: Option, - /// Scale velocity when scaling with mouse wheel. + /// Velocity of viewport scale when scaling with mouse wheel. #[cfg_attr(feature = "egui-probe", egui_probe(range = 0.0..))] #[cfg_attr( feature = "serde", @@ -194,33 +282,6 @@ pub struct SnarlStyle { )] pub scale_velocity: Option, - /// Frame used to draw nodes. - /// Defaults to [`Frame::window`] constructed from current ui's style. - #[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( - 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", @@ -262,37 +323,42 @@ pub struct SnarlStyle { } impl SnarlStyle { - fn get_pin_size(&self, scale: f32, ui: &Ui) -> f32 { + fn get_node_layout(&self) -> NodeLayout { + self.node_layout.unwrap_or(NodeLayout::Basic) + } + + fn get_pin_size(&self, scale: f32, style: &Style) -> f32 { self.pin_size .zoomed(scale) - .unwrap_or_else(|| ui.spacing().interact_size.y * 0.5) + .unwrap_or_else(|| style.spacing.interact_size.y * 0.5) } - fn get_pin_fill(&self, ui: &Ui) -> Color32 { - self.pin_fill.unwrap_or(ui.visuals().widgets.active.bg_fill) + fn get_pin_fill(&self, style: &Style) -> Color32 { + self.pin_fill + .unwrap_or(style.visuals.widgets.active.bg_fill) } - fn get_pin_stoke(&self, scale: f32, ui: &Ui) -> Stroke { + fn get_pin_stroke(&self, scale: f32, style: &Style) -> Stroke { self.pin_stroke.zoomed(scale).unwrap_or(Stroke::new( - ui.visuals().widgets.active.bg_stroke.width, - ui.visuals().widgets.active.bg_stroke.color, + style.visuals.widgets.active.bg_stroke.width, + style.visuals.widgets.active.bg_stroke.color, )) } fn get_pin_shape(&self) -> PinShape { - self.pin_shape.unwrap_or(BasicPinShape::Circle).into() + self.pin_shape.unwrap_or(PinShape::Circle).into() } - fn get_wire_width(&self, scale: f32, ui: &Ui) -> f32 { + fn get_wire_width(&self, scale: f32, style: &Style) -> f32 { self.wire_width .zoomed(scale) - .unwrap_or(self.get_pin_size(scale, ui) * 0.2) + .unwrap_or(self.get_pin_size(scale, style) * 0.2) } - fn get_wire_frame_size(&self, scale: f32, ui: &Ui) -> f32 { + fn get_wire_frame_size(&self, scale: f32, style: &Style) -> f32 { self.wire_frame_size .zoomed(scale) - .unwrap_or(self.get_pin_size(scale, ui) * 5.0) + .unwrap_or(self.get_pin_size(scale, style) * 5.0) } fn get_downscale_wire_frame(&self) -> bool { @@ -311,31 +377,24 @@ impl SnarlStyle { self.wire_layer.unwrap_or(WireLayer::BehindNodes) } - fn get_header_drag_space(&self, scale: f32, ui: &Ui) -> Vec2 { + fn get_header_drag_space(&self, scale: f32, style: &Style) -> Vec2 { self.header_drag_space .zoomed(scale) - .unwrap_or(vec2(ui.spacing().icon_width, ui.spacing().icon_width)) + .unwrap_or(vec2(style.spacing.icon_width, style.spacing.icon_width)) } fn get_collapsible(&self) -> bool { self.collapsible.unwrap_or(true) } - fn get_bg_frame(&self, ui: &Ui) -> Frame { - self.bg_frame.unwrap_or(Frame::canvas(ui.style())) + fn get_bg_frame(&self, style: &Style) -> Frame { + self.bg_frame.unwrap_or(Frame::canvas(style)) } - fn get_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 get_bg_pattern_stroke(&self, scale: f32, ui: &Ui) -> Stroke { + fn get_bg_pattern_stroke(&self, scale: f32, style: &Style) -> Stroke { self.bg_pattern_stroke .zoomed(scale) - .unwrap_or(ui.visuals().widgets.noninteractive.bg_stroke) + .unwrap_or(style.visuals.widgets.noninteractive.bg_stroke) } fn get_min_scale(&self) -> f32 { @@ -350,44 +409,44 @@ impl SnarlStyle { self.scale_velocity.unwrap_or(0.005) } - fn get_node_frame(&self, scale: f32, ui: &Ui) -> Frame { + fn get_node_frame(&self, scale: f32, style: &Style) -> Frame { self.node_frame .zoomed(scale) - .unwrap_or_else(|| Frame::window(ui.style())) + .unwrap_or_else(|| Frame::window(style)) } - fn get_header_frame(&self, scale: f32, ui: &Ui) -> Frame { + fn get_header_frame(&self, scale: f32, style: &Style) -> Frame { self.header_frame .zoomed(scale) - .unwrap_or_else(|| self.get_node_frame(scale, ui).shadow(Shadow::NONE)) + .unwrap_or_else(|| self.get_node_frame(scale, style).shadow(Shadow::NONE)) } fn get_centering(&self) -> bool { self.centering.unwrap_or(true) } - fn get_select_stroke(&self, scale: f32, ui: &Ui) -> Stroke { + fn get_select_stroke(&self, scale: f32, style: &Style) -> Stroke { self.select_stoke.zoomed(scale).unwrap_or(Stroke::new( - ui.visuals().selection.stroke.width, - ui.visuals().selection.stroke.color.gamma_multiply(0.5), + style.visuals.selection.stroke.width, + style.visuals.selection.stroke.color.gamma_multiply(0.5), )) } - fn get_select_fill(&self, ui: &Ui) -> Color32 { + fn get_select_fill(&self, style: &Style) -> Color32 { self.select_fill - .unwrap_or(ui.visuals().selection.bg_fill.gamma_multiply(0.3)) + .unwrap_or(style.visuals.selection.bg_fill.gamma_multiply(0.3)) } fn get_select_rect_contained(&self) -> bool { self.select_rect_contained.unwrap_or(false) } - fn get_select_style(&self, scale: f32, ui: &Ui) -> SelectionStyle { + fn get_select_style(&self, scale: f32, style: &Style) -> SelectionStyle { self.select_style.zoomed(scale).unwrap_or(SelectionStyle { - margin: ui.spacing().window_margin, - rounding: ui.visuals().window_rounding, - fill: self.get_select_fill(ui), - stroke: self.get_select_stroke(scale, ui), + margin: style.spacing.window_margin, + rounding: style.visuals.window_rounding, + fill: self.get_select_fill(style), + stroke: self.get_select_stroke(scale, style), }) } } @@ -445,6 +504,7 @@ impl SnarlStyle { #[must_use] pub const fn new() -> Self { SnarlStyle { + node_layout: None, pin_size: None, pin_fill: None, pin_stroke: None, @@ -499,24 +559,50 @@ struct DrawNodeResponse { node_to_top: Option, drag_released: bool, pin_hovered: Option, - rect: Rect, + final_rect: Rect, +} + +struct DrawPinsResponse { + drag_released: bool, + pin_hovered: Option, + final_rect: Rect, +} + +struct DrawBodyResponse { + final_rect: Rect, } struct PinResponse { pos: Pos2, - pin_fill: Color32, + pin_color: Color32, wire_style: Option, } impl Snarl { - fn draw_background(style: &SnarlStyle, snarl_state: &SnarlState, viewport: &Rect, ui: &mut Ui) { + fn draw_background( + &self, + viewer: &mut V, + style: &SnarlStyle, + snarl_state: &SnarlState, + viewport: &Rect, + ui: &mut Ui, + ) where + V: SnarlViewer, + { let viewport = Viewport { rect: *viewport, scale: snarl_state.scale(), offset: snarl_state.offset(), }; - style.get_draw_bg_pattern(style, &viewport, ui); + viewer.draw_background( + style.bg_pattern.as_ref(), + &viewport, + style, + ui.style(), + ui.painter(), + self, + ); } /// Render [`Snarl`] using given viewer and style into the [`Ui`]. @@ -530,7 +616,7 @@ impl Snarl { let snarl_id = ui.make_persistent_id(id_salt); // Draw background pattern. - let bg_frame = style.get_bg_frame(ui); + let bg_frame = style.get_bg_frame(ui.style()); let input = ui.ctx().input(|i| Input { scroll_delta: i.raw_scroll_delta.y, @@ -560,12 +646,12 @@ impl Snarl { // node_style.zoom(snarl_state.scale()); //Draw background - Self::draw_background(style, &snarl_state, &viewport, ui); + self.draw_background(viewer, style, &snarl_state, &viewport, ui); - let wire_frame_size = style.get_wire_frame_size(snarl_state.scale(), ui); - let wire_width = style.get_wire_width(snarl_state.scale(), ui); - let node_frame = style.get_node_frame(snarl_state.scale(), ui); - let header_frame = style.get_header_frame(snarl_state.scale(), ui); + let wire_frame_size = style.get_wire_frame_size(snarl_state.scale(), ui.style()); + let wire_width = style.get_wire_width(snarl_state.scale(), ui.style()); + let node_frame = style.get_node_frame(snarl_state.scale(), ui.style()); + let header_frame = style.get_header_frame(snarl_state.scale(), ui.style()); let wire_shape_idx = match style.get_wire_layer() { WireLayer::BehindNodes => Some(ui.painter().add(Shape::Noop)), @@ -632,11 +718,11 @@ impl Snarl { } drag_released |= response.drag_released; - centers_sum += response.rect.center().to_vec2(); + centers_sum += response.final_rect.center().to_vec2(); centers_weight += 1; if snarl_state.is_rect_selection() { - node_rects.push((node_idx, response.rect)); + node_rects.push((node_idx, response.final_rect)); } } } @@ -685,7 +771,7 @@ impl Snarl { } } - let color = mix_colors(from_r.pin_fill, to_r.pin_fill); + let color = mix_colors(from_r.pin_color, to_r.pin_color); let mut draw_width = wire_width; if hovered_wire == Some(wire) { @@ -763,8 +849,8 @@ impl Snarl { ui.painter().rect( snarl_state.graph_rect_to_screen(select_rect, viewport), 0.0, - style.get_select_fill(ui), - style.get_select_stroke(snarl_state.scale(), ui), + style.get_select_fill(ui.style()), + style.get_select_stroke(snarl_state.scale(), ui.style()), ); } @@ -781,8 +867,8 @@ impl Snarl { bg_r.clicked = false; } - //Do centering - if style.get_centering() && bg_r.double_clicked() { + // Do centering unless no nodes are present. + if style.get_centering() && bg_r.double_clicked() && centers_weight > 0 { centers_sum /= centers_weight as f32; snarl_state.set_offset(centers_sum * snarl_state.scale()); } @@ -905,7 +991,7 @@ impl Snarl { style.get_downscale_wire_frame(), from_pos, to_r.pos, - Stroke::new(wire_width, to_r.pin_fill), + Stroke::new(wire_width, to_r.pin_color), to_r.wire_style .zoomed(snarl_state.scale()) .unwrap_or(style.get_wire_style(snarl_state.scale())), @@ -925,7 +1011,7 @@ impl Snarl { style.get_downscale_wire_frame(), from_r.pos, to_pos, - Stroke::new(wire_width, from_r.pin_fill), + Stroke::new(wire_width, from_r.pin_color), from_r .wire_style .zoomed(snarl_state.scale()) @@ -972,6 +1058,326 @@ impl Snarl { }); } + fn draw_inputs( + &mut self, + viewer: &mut V, + node: NodeId, + inputs: &[InPin], + pin_size: f32, + style: &SnarlStyle, + ui: &mut Ui, + inputs_rect: Rect, + clip_rect: Rect, + viewport: Rect, + input_x: f32, + min_pin_y: f32, + snarl_state: &mut SnarlState, + input: &Input, + input_positions: &mut HashMap, + ) -> DrawPinsResponse + where + V: SnarlViewer, + { + let mut drag_released = false; + let mut pin_hovered = None; + + // Input pins on the left. + let inputs_ui = &mut ui.new_child( + UiBuilder::new() + .max_rect(inputs_rect) + .layout(Layout::top_down(Align::Min)) + .id_salt("inputs"), + ); + + inputs_ui.set_clip_rect(clip_rect.intersect(viewport)); + + for in_pin in inputs { + // Show input pin. + inputs_ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + // Allocate space for pin shape. + let (pin_id, _) = ui.allocate_space(vec2(pin_size * 1.5, pin_size * 1.5)); + + let y0 = ui.cursor().min.y; + + // Show input content + let pin_info = viewer.show_input(in_pin, ui, snarl_state.scale(), self); + if !self.nodes.contains(node.0) { + // If removed + return; + } + + let y1 = ui.min_rect().max.y; + + // ui.end_row(); + + // Centered vertically. + let y = min_pin_y.max((y0 + y1) * 0.5); + + let pin_pos = pos2(input_x, y); + + // Interact with pin shape. + let r = ui.interact( + Rect::from_center_size(pin_pos, vec2(pin_size, pin_size)), + pin_id, + Sense::click_and_drag(), + ); + + if r.clicked_by(PointerButton::Secondary) { + if snarl_state.has_new_wires() { + snarl_state.remove_new_wire_in(in_pin.id); + } else { + viewer.drop_inputs(in_pin, self); + if !self.nodes.contains(node.0) { + // If removed + return; + } + } + } + if r.drag_started_by(PointerButton::Primary) { + if input.modifiers.command { + snarl_state.start_new_wires_out(&in_pin.remotes); + if !input.modifiers.shift { + self.drop_inputs(in_pin.id); + if !self.nodes.contains(node.0) { + // If removed + return; + } + } + } else { + snarl_state.start_new_wire_in(in_pin.id); + } + } + if r.drag_stopped() { + drag_released = true; + } + + let mut pin_size = pin_size; + + match input.hover_pos { + Some(hover_pos) if r.rect.contains(hover_pos) => { + if input.modifiers.shift { + snarl_state.add_new_wire_in(in_pin.id); + } else if input.secondary_pressed { + snarl_state.remove_new_wire_in(in_pin.id); + } + pin_hovered = Some(AnyPin::In(in_pin.id)); + pin_size *= 1.2; + } + _ => {} + } + + let pin_color = viewer.draw_input_pin( + in_pin, + &pin_info, + pin_pos, + pin_size, + style, + ui.style(), + ui.painter(), + snarl_state.scale(), + self, + ); + + input_positions.insert( + in_pin.id, + PinResponse { + pos: pin_pos, + pin_color, + wire_style: pin_info.wire_style, + }, + ); + }); + } + + let final_rect = inputs_ui.min_rect(); + ui.expand_to_include_rect(final_rect.intersect(clip_rect)); + + DrawPinsResponse { + drag_released, + pin_hovered, + final_rect, + } + } + + fn draw_outputs( + &mut self, + viewer: &mut V, + node: NodeId, + outputs: &[OutPin], + pin_size: f32, + style: &SnarlStyle, + ui: &mut Ui, + outputs_rect: Rect, + clip_rect: Rect, + viewport: Rect, + output_x: f32, + min_pin_y: f32, + snarl_state: &mut SnarlState, + input: &Input, + output_positions: &mut HashMap, + ) -> DrawPinsResponse + where + V: SnarlViewer, + { + let mut drag_released = false; + let mut pin_hovered = None; + + let outputs_ui = &mut ui.new_child( + UiBuilder::new() + .max_rect(outputs_rect) + .layout(Layout::top_down(Align::Max)) + .id_salt("outputs"), + ); + + outputs_ui.set_clip_rect(clip_rect.intersect(viewport)); + + // Output pins on the right. + for out_pin in outputs { + // Show output pin. + outputs_ui.with_layout(Layout::right_to_left(Align::Min), |ui| { + // Allocate space for pin shape. + + let (pin_id, _) = ui.allocate_space(vec2(pin_size * 1.5, pin_size * 1.5)); + + let y0 = ui.cursor().min.y; + + // Show output content + let pin_info = viewer.show_output(out_pin, ui, snarl_state.scale(), self); + if !self.nodes.contains(node.0) { + // If removed + return; + } + + let y1 = ui.min_rect().max.y; + + // ui.end_row(); + + // Centered vertically. + let y = min_pin_y.max((y0 + y1) * 0.5); + + let pin_pos = pos2(output_x, y); + + let r = ui.interact( + Rect::from_center_size(pin_pos, vec2(pin_size, pin_size)), + pin_id, + Sense::click_and_drag(), + ); + + if r.clicked_by(PointerButton::Secondary) { + if snarl_state.has_new_wires() { + snarl_state.remove_new_wire_out(out_pin.id); + } else { + viewer.drop_outputs(out_pin, self); + if !self.nodes.contains(node.0) { + // If removed + return; + } + } + } + if r.drag_started_by(PointerButton::Primary) { + if input.modifiers.command { + snarl_state.start_new_wires_in(&out_pin.remotes); + + if !input.modifiers.shift { + self.drop_outputs(out_pin.id); + if !self.nodes.contains(node.0) { + // If removed + return; + } + } + } else { + snarl_state.start_new_wire_out(out_pin.id); + } + } + if r.drag_stopped() { + drag_released = true; + } + + let mut pin_size = pin_size; + match input.hover_pos { + Some(hover_pos) if r.rect.contains(hover_pos) => { + if input.modifiers.shift { + snarl_state.add_new_wire_out(out_pin.id); + } else if input.secondary_pressed { + snarl_state.remove_new_wire_out(out_pin.id); + } + pin_hovered = Some(AnyPin::Out(out_pin.id)); + pin_size *= 1.2; + } + _ => {} + } + + let pin_color = viewer.draw_output_pin( + out_pin, + &pin_info, + pin_pos, + pin_size, + style, + ui.style(), + ui.painter(), + snarl_state.scale(), + self, + ); + + output_positions.insert( + out_pin.id, + PinResponse { + pos: pin_pos, + pin_color, + wire_style: pin_info.wire_style, + }, + ); + }); + } + let final_rect = outputs_ui.min_rect(); + ui.expand_to_include_rect(final_rect.intersect(clip_rect)); + + DrawPinsResponse { + drag_released, + pin_hovered, + final_rect, + } + } + + fn draw_body( + &mut self, + viewer: &mut V, + node: NodeId, + inputs: &[InPin], + outputs: &[OutPin], + ui: &mut Ui, + body_rect: Rect, + clip_rect: Rect, + viewport: Rect, + snarl_state: &mut SnarlState, + ) -> DrawBodyResponse + where + V: SnarlViewer, + { + let mut body_ui = ui.new_child( + UiBuilder::new() + .max_rect(body_rect) + .layout(Layout::left_to_right(Align::Min)) + .id_salt("body"), + ); + body_ui.set_clip_rect(clip_rect.intersect(viewport)); + + viewer.show_body( + node, + &inputs, + &outputs, + &mut body_ui, + snarl_state.scale(), + self, + ); + + let final_rect = body_ui.min_rect(); + ui.expand_to_include_rect(final_rect.intersect(clip_rect)); + // node_state.set_body_width(body_size.x); + + DrawBodyResponse { final_rect } + } + //First step for split big function to parts /// Draw one node. Return Pins info #[inline] @@ -1026,7 +1432,7 @@ impl Snarl { let node_frame_rect = node_rect + node_frame.total_margin(); if snarl_state.selected_nodes().contains(&node) { - let select_style = style.get_select_style(snarl_state.scale(), ui); + let select_style = style.get_select_style(snarl_state.scale(), ui.style()); let select_rect = node_frame_rect + select_style.margin; @@ -1038,10 +1444,10 @@ impl Snarl { ); } - let pin_size = style.get_pin_size(snarl_state.scale(), ui).max(0.0); + let pin_size = style.get_pin_size(snarl_state.scale(), ui.style()).max(0.0); let header_drag_space = style - .get_header_drag_space(snarl_state.scale(), ui) + .get_header_drag_space(snarl_state.scale(), ui.style()) .max(Vec2::ZERO); let inputs = (0..inputs_count) @@ -1137,315 +1543,363 @@ impl Snarl { node_rect.max, ); + let node_layout = viewer + .node_layout(node, &inputs, &outputs, self) + .unwrap_or(style.get_node_layout()); + let payload_clip_rect = Rect::from_min_max(node_rect.min, pos2(node_rect.max.x, f32::INFINITY)); - // Show input pins. - - // Input pins on the left. - let inputs_ui = &mut ui.new_child( - UiBuilder::new() - .max_rect(payload_rect) - .layout(Layout::top_down(Align::Min)) - .id_salt("inputs"), - ); + let pins_rect = match node_layout { + NodeLayout::Basic => { + // Show input pins. + let r = self.draw_inputs( + viewer, + node, + &inputs, + pin_size, + style, + ui, + payload_rect, + payload_clip_rect, + viewport, + input_x, + min_pin_y, + snarl_state, + input, + input_positions, + ); - inputs_ui.set_clip_rect(payload_clip_rect.intersect(viewport)); + drag_released |= r.drag_released; - for in_pin in &inputs { - // Show input pin. - inputs_ui.with_layout(Layout::left_to_right(Align::Min), |ui| { - // Allocate space for pin shape. - let (pin_id, _) = ui.allocate_space(vec2(pin_size * 1.5, pin_size * 1.5)); + if r.pin_hovered.is_some() { + pin_hovered = r.pin_hovered; + } - let y0 = ui.cursor().min.y; + let inputs_rect = r.final_rect; + let inputs_size = inputs_rect.size(); - // Show input content - let pin_info = viewer.show_input(in_pin, ui, snarl_state.scale(), self); if !self.nodes.contains(node.0) { // If removed return; } - let y1 = ui.min_rect().max.y; + // Show output pins. - // ui.end_row(); + let r = self.draw_outputs( + viewer, + node, + &outputs, + pin_size, + style, + ui, + payload_rect, + payload_clip_rect, + viewport, + output_x, + min_pin_y, + snarl_state, + input, + output_positions, + ); - // Centered vertically. - let y = min_pin_y.max((y0 + y1) * 0.5); + drag_released |= r.drag_released; - let pin_pos = pos2(input_x, y); + if r.pin_hovered.is_some() { + pin_hovered = r.pin_hovered; + } + + let outputs_rect = r.final_rect; + let outputs_size = outputs_rect.size(); - // Interact with pin shape. - let r = ui.interact( - Rect::from_center_size(pin_pos, vec2(pin_size, pin_size)), - pin_id, - Sense::click_and_drag(), + if !self.nodes.contains(node.0) { + // If removed + return; + } + + new_pins_size = vec2( + inputs_size.x + outputs_size.x + ui.spacing().item_spacing.x, + f32::max(inputs_size.y, outputs_size.y), ); - if r.clicked_by(PointerButton::Secondary) { - if snarl_state.has_new_wires() { - snarl_state.remove_new_wire_in(in_pin.id); - } else { - viewer.drop_inputs(in_pin, self); - if !self.nodes.contains(node.0) { - // If removed - return; - } + let mut pins_rect = inputs_rect.union(outputs_rect); + + // Show body if there's one. + if viewer.has_body(&self.nodes.get(node.0).unwrap().value) { + 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 body_bottom = payload_rect.bottom(); + + let body_rect = Rect::from_min_max( + pos2(body_left, body_top), + pos2(body_right, body_bottom), + ); + + let r = self.draw_body( + viewer, + node, + &inputs, + &outputs, + ui, + body_rect, + payload_clip_rect, + viewport, + snarl_state, + ); + + new_pins_size.x += r.final_rect.width() + ui.spacing().item_spacing.x; + new_pins_size.y = f32::max(new_pins_size.y, r.final_rect.height()); + + pins_rect = pins_rect.union(body_rect); + + if !self.nodes.contains(node.0) { + // If removed + return; } } - if r.drag_started_by(PointerButton::Primary) { - if input.modifiers.command { - snarl_state.start_new_wires_out(&in_pin.remotes); - if !input.modifiers.shift { - self.drop_inputs(in_pin.id); - if !self.nodes.contains(node.0) { - // If removed - return; - } - } - } else { - snarl_state.start_new_wire_in(in_pin.id); - } + + pins_rect + } + NodeLayout::Sandwich => { + // Show input pins. + + let inputs_rect = payload_rect; + let r = self.draw_inputs( + viewer, + node, + &inputs, + pin_size, + style, + ui, + inputs_rect, + payload_clip_rect, + viewport, + input_x, + min_pin_y, + snarl_state, + input, + input_positions, + ); + + drag_released |= r.drag_released; + + if r.pin_hovered.is_some() { + pin_hovered = r.pin_hovered; } - if r.drag_stopped() { - drag_released = true; + + let inputs_rect = r.final_rect; + let inputs_size = inputs_rect.size(); + + let mut next_y = inputs_rect.bottom() + ui.spacing().item_spacing.y; + + if !self.nodes.contains(node.0) { + // If removed + return; } - let mut pin_size = pin_size; + let mut pins_rect = inputs_rect; - match input.hover_pos { - Some(hover_pos) if r.rect.contains(hover_pos) => { - if input.modifiers.shift { - snarl_state.add_new_wire_in(in_pin.id); - } else if input.secondary_pressed { - snarl_state.remove_new_wire_in(in_pin.id); - } - pin_hovered = Some(AnyPin::In(in_pin.id)); - pin_size *= 1.2; + // Show body if there's one. + if viewer.has_body(&self.nodes.get(node.0).unwrap().value) { + let body_rect = payload_rect.intersect(Rect::everything_below(next_y)); + + let r = self.draw_body( + viewer, + node, + &inputs, + &outputs, + ui, + body_rect, + payload_clip_rect, + viewport, + snarl_state, + ); + + let body_rect = r.final_rect; + + new_pins_size.x += body_rect.width() + ui.spacing().item_spacing.x; + new_pins_size.y = f32::max(new_pins_size.y, body_rect.height()); + + if !self.nodes.contains(node.0) { + // If removed + return; } - _ => {} + + pins_rect = pins_rect.union(body_rect); + next_y = body_rect.bottom() + ui.spacing().item_spacing.y; } - let pin_fill = pin_info.fill.unwrap_or(style.get_pin_fill(ui)); - - draw_pin( - ui.painter(), - pin_info.shape.as_ref().unwrap_or(&style.get_pin_shape()), - pin_fill, - pin_info - .stroke - .zoomed(snarl_state.scale()) - .unwrap_or(style.get_pin_stoke(snarl_state.scale(), ui)), - pin_pos, + // Show output pins. + + let outputs_rect = payload_rect.intersect(Rect::everything_below(next_y)); + + let r = self.draw_outputs( + viewer, + node, + &outputs, pin_size, + style, + ui, + outputs_rect, + payload_clip_rect, + viewport, + output_x, + min_pin_y, + snarl_state, + input, + output_positions, ); - input_positions.insert( - in_pin.id, - PinResponse { - pos: pin_pos, - pin_fill, - wire_style: pin_info.wire_style, - }, - ); - }); - } - let inputs_rect = inputs_ui.min_rect(); - ui.expand_to_include_rect(inputs_rect.intersect(payload_clip_rect)); - let inputs_size = inputs_rect.size(); + drag_released |= r.drag_released; - if !self.nodes.contains(node.0) { - // If removed - return; - } + if r.pin_hovered.is_some() { + pin_hovered = r.pin_hovered; + } - // Show output pins. + let outputs_rect = r.final_rect; + let outputs_size = outputs_rect.size(); - // Outputs are placed under the header and must not go outside of the header frame. + if !self.nodes.contains(node.0) { + // If removed + return; + } - let outputs_ui = &mut ui.new_child( - UiBuilder::new() - .max_rect(payload_rect) - .layout(Layout::top_down(Align::Max)) - .id_salt("outputs"), - ); + new_pins_size = vec2( + inputs_size.x + outputs_size.x + ui.spacing().item_spacing.x, + f32::max(inputs_size.y, outputs_size.y), + ); + + pins_rect = pins_rect.union(outputs_rect); + + pins_rect + } + NodeLayout::FlippedSandwich => { + // Show input pins. + + let outputs_rect = payload_rect; + let r = self.draw_outputs( + viewer, + node, + &outputs, + pin_size, + style, + ui, + outputs_rect, + payload_clip_rect, + viewport, + output_x, + min_pin_y, + snarl_state, + input, + output_positions, + ); - outputs_ui.set_clip_rect(payload_clip_rect.intersect(viewport)); + drag_released |= r.drag_released; - // Output pins on the right. - for out_pin in &outputs { - // Show output pin. - outputs_ui.with_layout(Layout::right_to_left(Align::Min), |ui| { - // Allocate space for pin shape. + if r.pin_hovered.is_some() { + pin_hovered = r.pin_hovered; + } - let (pin_id, _) = ui.allocate_space(vec2(pin_size * 1.5, pin_size * 1.5)); + let outputs_rect = r.final_rect; + let outputs_size = outputs_rect.size(); - let y0 = ui.cursor().min.y; + let mut next_y = outputs_rect.bottom() + ui.spacing().item_spacing.y; - // Show output content - let pin_info = viewer.show_output(out_pin, ui, snarl_state.scale(), self); if !self.nodes.contains(node.0) { // If removed return; } - let y1 = ui.min_rect().max.y; + let mut pins_rect = outputs_rect; - // ui.end_row(); + // Show body if there's one. + if viewer.has_body(&self.nodes.get(node.0).unwrap().value) { + let body_rect = payload_rect.intersect(Rect::everything_below(next_y)); - // Centered vertically. - let y = min_pin_y.max((y0 + y1) * 0.5); + let r = self.draw_body( + viewer, + node, + &inputs, + &outputs, + ui, + body_rect, + payload_clip_rect, + viewport, + snarl_state, + ); - let pin_pos = pos2(output_x, y); + let body_rect = r.final_rect; - let r = ui.interact( - Rect::from_center_size(pin_pos, vec2(pin_size, pin_size)), - pin_id, - Sense::click_and_drag(), - ); + new_pins_size.x += body_rect.width() + ui.spacing().item_spacing.x; + new_pins_size.y = f32::max(new_pins_size.y, body_rect.height()); - if r.clicked_by(PointerButton::Secondary) { - if snarl_state.has_new_wires() { - snarl_state.remove_new_wire_out(out_pin.id); - } else { - viewer.drop_outputs(out_pin, self); - if !self.nodes.contains(node.0) { - // If removed - return; - } + if !self.nodes.contains(node.0) { + // If removed + return; } - } - if r.drag_started_by(PointerButton::Primary) { - if input.modifiers.command { - snarl_state.start_new_wires_in(&out_pin.remotes); - - if !input.modifiers.shift { - self.drop_outputs(out_pin.id); - if !self.nodes.contains(node.0) { - // If removed - return; - } - } - } else { - snarl_state.start_new_wire_out(out_pin.id); - } - } - if r.drag_stopped() { - drag_released = true; - } - let mut pin_size = pin_size; - match input.hover_pos { - Some(hover_pos) if r.rect.contains(hover_pos) => { - if input.modifiers.shift { - snarl_state.add_new_wire_out(out_pin.id); - } else if input.secondary_pressed { - snarl_state.remove_new_wire_out(out_pin.id); - } - pin_hovered = Some(AnyPin::Out(out_pin.id)); - pin_size *= 1.2; - } - _ => {} + pins_rect = pins_rect.union(body_rect); + next_y = body_rect.bottom() + ui.spacing().item_spacing.y; } - let pin_fill = pin_info.fill.unwrap_or(style.get_pin_fill(ui)); - - draw_pin( - ui.painter(), - pin_info.shape.as_ref().unwrap_or(&style.get_pin_shape()), - pin_fill, - pin_info - .stroke - .zoomed(snarl_state.scale()) - .unwrap_or(style.get_pin_stoke(snarl_state.scale(), ui)), - pin_pos, - pin_size, - ); - - output_positions.insert( - out_pin.id, - PinResponse { - pos: pin_pos, - pin_fill, - wire_style: pin_info.wire_style, - }, - ); - }); - } - let outputs_rect = outputs_ui.min_rect(); - ui.expand_to_include_rect(outputs_rect.intersect(payload_clip_rect)); - let outputs_size = outputs_rect.size(); - - if !self.nodes.contains(node.0) { - // If removed - return; - } - - new_pins_size = vec2( - inputs_size.x + outputs_size.x + ui.spacing().item_spacing.x, - f32::max(inputs_size.y, outputs_size.y), - ); + // Show output pins. - let mut pins_bottom = f32::max(inputs_rect.bottom(), outputs_rect.bottom()); + let inputs_rect = payload_rect.intersect(Rect::everything_below(next_y)); - // Show body if there's one. - if viewer.has_body(&self.nodes.get(node.0).unwrap().value) { - 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 body_bottom = payload_rect.bottom(); + let r = self.draw_inputs( + viewer, + node, + &inputs, + pin_size, + style, + ui, + inputs_rect, + payload_clip_rect, + viewport, + input_x, + min_pin_y, + snarl_state, + input, + input_positions, + ); - let mut body_rect = - Rect::from_min_max(pos2(body_left, body_top), pos2(body_right, body_bottom)); - body_rect = node_state.align_body(body_rect); + drag_released |= r.drag_released; - let mut body_ui = ui.new_child( - UiBuilder::new() - .max_rect(body_rect) - .layout(Layout::left_to_right(Align::Min)) - .id_salt("body"), - ); - body_ui.set_clip_rect(payload_clip_rect.intersect(viewport)); + if r.pin_hovered.is_some() { + pin_hovered = r.pin_hovered; + } - viewer.show_body( - node, - &inputs, - &outputs, - &mut body_ui, - snarl_state.scale(), - self, - ); + let inputs_rect = r.final_rect; + let inputs_size = inputs_rect.size(); - body_rect = body_ui.min_rect(); - ui.expand_to_include_rect(body_rect.intersect(payload_clip_rect)); - let body_size = body_rect.size(); - node_state.set_body_width(body_size.x); + if !self.nodes.contains(node.0) { + // If removed + return; + } - 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); + new_pins_size = vec2( + inputs_size.x + outputs_size.x + ui.spacing().item_spacing.x, + f32::max(inputs_size.y, outputs_size.y), + ); - pins_bottom = f32::max(pins_bottom, body_rect.bottom()); + pins_rect = pins_rect.union(inputs_rect); - if !self.nodes.contains(node.0) { - // If removed - return; + pins_rect } - } + }; 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 + ui.spacing().item_spacing.y; + let footer_top = pins_rect.bottom() + ui.spacing().item_spacing.y; let footer_bottom = node_rect.bottom(); - let mut footer_rect = Rect::from_min_max( + let footer_rect = Rect::from_min_max( pos2(footer_left, footer_top), pos2(footer_right, footer_bottom), ); - footer_rect = node_state.align_footer(footer_rect); - let mut footer_ui = ui.new_child( UiBuilder::new() .max_rect(footer_rect) @@ -1463,10 +1917,9 @@ impl Snarl { self, ); - footer_rect = footer_ui.min_rect(); - ui.expand_to_include_rect(footer_rect.intersect(payload_clip_rect)); - let footer_size = footer_rect.size(); - node_state.set_footer_width(footer_size.x); + let final_rect = footer_ui.min_rect(); + ui.expand_to_include_rect(final_rect.intersect(payload_clip_rect)); + let footer_size = final_rect.size(); new_pins_size.x = f32::max(new_pins_size.x, footer_size.x); new_pins_size.y += footer_size.y + ui.spacing().item_spacing.y; @@ -1560,7 +2013,7 @@ impl Snarl { node_to_top, drag_released, pin_hovered, - rect: final_rect, + final_rect, }) } } @@ -1641,3 +2094,9 @@ fn mix_colors(a: Color32, b: Color32) -> Color32 { // ), // }) // } + +#[test] +fn snarl_style_is_send_sync() { + fn is_send_sync() {} + is_send_sync::(); +} diff --git a/src/ui/background_pattern.rs b/src/ui/background_pattern.rs index 5f2a02d..1172333 100644 --- a/src/ui/background_pattern.rs +++ b/src/ui/background_pattern.rs @@ -1,6 +1,4 @@ -use std::fmt; - -use egui::{emath::Rot2, vec2, Pos2, Rect, Ui, Vec2}; +use egui::{emath::Rot2, vec2, Painter, Pos2, Rect, Style, Vec2}; use super::SnarlStyle; @@ -97,8 +95,14 @@ impl Grid { Self { spacing, angle } } - fn draw(&self, style: &SnarlStyle, viewport: &Viewport, ui: &mut Ui) { - let bg_stroke = style.get_bg_pattern_stroke(viewport.scale, ui); + fn draw( + &self, + viewport: &Viewport, + snarl_style: &SnarlStyle, + style: &Style, + painter: &Painter, + ) { + let bg_stroke = snarl_style.get_bg_pattern_stroke(viewport.scale, style); let spacing = vec2(self.spacing.x.max(1.0), self.spacing.y.max(1.0)); @@ -125,7 +129,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], bg_stroke); + painter.line_segment([top, bottom], bg_stroke); } let min_y = (pattern_bounds.min.y / spacing.y).ceil(); @@ -141,35 +145,13 @@ impl Grid { let top = viewport.graph_pos_to_screen(top); let bottom = viewport.graph_pos_to_screen(bottom); - ui.painter().line_segment([top, bottom], bg_stroke); + painter.line_segment([top, bottom], bg_stroke); } } } -tiny_fn::tiny_fn! { - /// Custom background pattern function with signature - /// `Fn(style: &SnarlStyle, viewport: &Viewport, ui: &mut Ui)` - pub struct CustomBackground = Fn(style: &SnarlStyle, viewport: &Viewport, ui: &mut Ui); -} - -impl Default for CustomBackground<'_, INLINE_SIZE> { - fn default() -> Self { - Self::new(|_, _, _| {}) - } -} - -#[cfg(feature = "egui-probe")] -impl egui_probe::EguiProbe for CustomBackground<'_, INLINE_SIZE> { - fn probe( - &mut self, - ui: &mut egui_probe::egui::Ui, - _style: &egui_probe::Style, - ) -> egui_probe::egui::Response { - ui.weak("Custom") - } -} - /// Background pattern show beneath nodes and wires. +#[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "egui-probe", derive(egui_probe::EguiProbe))] pub enum BackgroundPattern { @@ -179,34 +161,6 @@ pub enum BackgroundPattern { /// Linear grid. #[cfg_attr(feature = "egui-probe", egui_probe(transparent))] Grid(Grid), - - /// Custom pattern. - /// Contains function with signature - /// `Fn(style: &SnarlStyle, viewport: &Viewport, ui: &mut Ui)` - #[cfg_attr(feature = "egui-probe", egui_probe(transparent))] - Custom(#[cfg_attr(feature = "serde", serde(skip))] CustomBackground<'static>), -} - -impl PartialEq for BackgroundPattern { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (BackgroundPattern::Grid(l), BackgroundPattern::Grid(r)) => *l == *r, - _ => false, - } - } -} - -impl fmt::Debug for BackgroundPattern { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - BackgroundPattern::Grid(grid) => f - .debug_tuple("BackgroundPattern::Grid") - .field(grid) - .finish(), - BackgroundPattern::Custom(_) => f.write_str("BackgroundPattern::Custom"), - BackgroundPattern::NoPattern => f.write_str("BackgroundPattern::NoPattern"), - } - } } impl Default for BackgroundPattern { @@ -232,19 +186,16 @@ impl BackgroundPattern { Self::Grid(Grid::new(spacing, angle)) } - /// Create new custom background pattern. - pub fn custom(f: F) -> Self - where - F: Fn(&SnarlStyle, &Viewport, &mut Ui) + 'static, - { - Self::Custom(CustomBackground::new(f)) - } - /// Draws background pattern. - pub(super) fn draw(&self, style: &SnarlStyle, viewport: &Viewport, ui: &mut Ui) { + pub fn draw( + &self, + viewport: &Viewport, + snarl_style: &SnarlStyle, + style: &Style, + painter: &Painter, + ) { match self { - BackgroundPattern::Grid(g) => g.draw(style, viewport, ui), - BackgroundPattern::Custom(c) => c.call(style, viewport, ui), + BackgroundPattern::Grid(g) => g.draw(viewport, snarl_style, style, painter), BackgroundPattern::NoPattern => {} } } diff --git a/src/ui/pin.rs b/src/ui/pin.rs index d71dd45..71f4606 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}; +use egui::{epaint::PathShape, vec2, Color32, Painter, Pos2, Shape, Stroke, Style, Vec2}; use crate::{InPinId, OutPinId}; -use super::WireStyle; +use super::{zoom::Zoom, SnarlStyle, WireStyle}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum AnyPin { @@ -20,36 +20,13 @@ pub enum AnyPins<'a> { In(&'a [InPinId]), } -tiny_fn::tiny_fn! { - /// Custom pin shape drawing function with signature - /// `Fn(painter: &Painter, rect: Rect, fill: Color32, stroke: Stroke)` - pub struct CustomPinShape = Fn(painter: &Painter, rect: Rect, fill: Color32, stroke: Stroke); -} - /// Shape of a pin. -pub enum PinShape { - /// Circle shape. - Circle, - - /// Triangle shape. - Triangle, - - /// Square shape. - Square, - - /// Star shape. - Star, - - /// Custom shape. - Custom(CustomPinShape<'static>), -} - -/// Default shape of a pin. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "egui-probe", derive(egui_probe::EguiProbe))] -pub enum BasicPinShape { +pub enum PinShape { /// Circle shape. + #[default] Circle, /// Triangle shape. @@ -62,25 +39,6 @@ pub enum BasicPinShape { Star, } -impl Default for BasicPinShape { - #[inline(always)] - fn default() -> Self { - BasicPinShape::Circle - } -} - -impl From for PinShape { - #[inline(always)] - fn from(shape: BasicPinShape) -> Self { - match shape { - BasicPinShape::Circle => PinShape::Circle, - BasicPinShape::Triangle => PinShape::Triangle, - BasicPinShape::Square => PinShape::Square, - BasicPinShape::Star => PinShape::Star, - } - } -} - /// Information about a pin returned by `SnarlViewer::show_input` and `SnarlViewer::show_output`. /// /// All fields are optional. @@ -166,21 +124,35 @@ impl PinInfo { } } - /// Creates a square pin. - pub fn custom(f: F) -> Self - where - F: Fn(&Painter, Rect, Color32, Stroke) + 'static, - { - PinInfo { - shape: Some(PinShape::Custom(CustomPinShape::new(f))), - ..Default::default() - } + /// Draws the pin and returns color. + /// + /// Wires are drawn with returned color by default. + pub fn draw( + &self, + pos: Pos2, + size: f32, + snarl_style: &SnarlStyle, + style: &Style, + painter: &Painter, + scale: f32, + ) -> Color32 { + let shape = self.shape.unwrap_or(snarl_style.get_pin_shape()); + let fill = self.fill.unwrap_or(snarl_style.get_pin_fill(style)); + let stroke = self + .stroke + .zoomed(scale) + .unwrap_or(snarl_style.get_pin_stroke(scale, style)); + let size = self.size.map_or(size, |s| s * size); + + draw_pin(painter, shape, fill, stroke, pos, size); + + fill } } pub fn draw_pin( painter: &Painter, - shape: &PinShape, + shape: PinShape, fill: Color32, stroke: Stroke, pos: Pos2, @@ -241,12 +213,5 @@ pub fn draw_pin( stroke: stroke.into(), })); } - - PinShape::Custom(f) => f.call( - painter, - Rect::from_center_size(pos, vec2(size, size)), - fill, - stroke, - ), } } diff --git a/src/ui/state.rs b/src/ui/state.rs index ec6f3b7..c4bda56 100644 --- a/src/ui/state.rs +++ b/src/ui/state.rs @@ -1,6 +1,6 @@ use std::hash::Hash; -use egui::{ahash::HashSet, style::Spacing, Align, Context, Id, Pos2, Rect, Ui, Vec2}; +use egui::{ahash::HashSet, style::Spacing, Context, Id, Pos2, Rect, Ui, Vec2}; use crate::{InPinId, NodeId, OutPinId, Snarl}; @@ -13,8 +13,6 @@ pub struct NodeState { /// It is updated to fit content. size: Vec2, header_height: f32, - body_width: f32, - footer_width: f32, id: Id, scale: f32, @@ -25,8 +23,6 @@ pub struct NodeState { struct NodeData { unscaled_size: Vec2, unscaled_header_height: f32, - unscaled_body_width: f32, - unsacled_footer_width: f32, } impl NodeState { @@ -35,8 +31,6 @@ impl NodeState { Some(data) => NodeState { size: data.unscaled_size * scale, header_height: data.unscaled_header_height * scale, - body_width: data.unscaled_body_width * scale, - footer_width: data.unsacled_footer_width * scale, id, scale, dirty: false, @@ -57,8 +51,6 @@ impl NodeState { NodeData { unscaled_size: self.size / self.scale, unscaled_header_height: self.header_height / self.scale, - unscaled_body_width: self.body_width / self.scale, - unsacled_footer_width: self.footer_width / self.scale, }, ) }); @@ -80,16 +72,6 @@ impl NodeState { (self.size.y) * (1.0 - openness) } - pub fn align_body(&mut self, rect: Rect) -> Rect { - let x_range = Align::Center.align_size_within_range(self.body_width, rect.x_range()); - Rect::from_x_y_ranges(x_range, rect.y_range()) - } - - pub fn align_footer(&mut self, rect: Rect) -> Rect { - let x_range = Align::Center.align_size_within_range(self.footer_width, rect.x_range()); - Rect::from_x_y_ranges(x_range, rect.y_range()) - } - pub fn set_size(&mut self, size: Vec2) { if self.size != size { self.size = size; @@ -108,26 +90,10 @@ impl NodeState { } } - pub fn set_body_width(&mut self, width: f32) { - if self.body_width != width { - self.body_width = width; - self.dirty = true; - } - } - - pub fn set_footer_width(&mut self, width: f32) { - if self.footer_width != width { - self.footer_width = width; - self.dirty = true; - } - } - fn initial(id: Id, spacing: &Spacing, scale: f32) -> Self { NodeState { size: spacing.interact_size, header_height: spacing.interact_size.y, - body_width: 0.0, - footer_width: 0.0, id, dirty: true, scale, diff --git a/src/ui/viewer.rs b/src/ui/viewer.rs index 2170f17..1d5c731 100644 --- a/src/ui/viewer.rs +++ b/src/ui/viewer.rs @@ -1,8 +1,8 @@ -use egui::{Pos2, Rect, Ui}; +use egui::{Color32, Painter, Pos2, Rect, Style, Ui}; use crate::{InPin, InPinId, NodeId, OutPin, OutPinId, Snarl}; -use super::pin::{AnyPins, PinInfo}; +use super::{pin::AnyPins, BackgroundPattern, NodeLayout, PinInfo, SnarlStyle, Viewport}; /// SnarlViewer is a trait for viewing a Snarl. /// @@ -12,6 +12,22 @@ pub trait SnarlViewer { /// Returns title of the node. fn title(&mut self, node: &T) -> String; + /// Returns layout override for the node. + /// + /// This method can be used to override the default layout of the node. + /// By default it returns `None` and layout from the style is used. + #[inline] + fn node_layout( + &mut self, + node: NodeId, + inputs: &[InPin], + outputs: &[OutPin], + snarl: &Snarl, + ) -> Option { + let _ = (node, inputs, outputs, snarl); + None + } + /// Renders the node's header. #[inline] fn show_header( @@ -27,17 +43,21 @@ pub trait SnarlViewer { ui.label(self.title(&snarl[node])); } - /// Returns number of output pins of the node. - fn outputs(&mut self, node: &T) -> usize; - /// Returns number of input pins of the node. + /// + /// [`SnarlViewer::show_input`] and [`SnarlViewer::draw_input_pin`] will be called for each input in range `0..inputs()`. fn inputs(&mut self, node: &T) -> usize; - /// Renders the node's input pin. + /// Renders the node's input. fn show_input(&mut self, pin: &InPin, ui: &mut Ui, scale: f32, snarl: &mut Snarl) -> PinInfo; - /// Renders the node's output pin. + /// Returns number of output pins of the node. + /// + /// [`SnarlViewer::show_output`] and [`SnarlViewer::show_output_ping`] will be called for each output in range `0..outputs()`. + fn outputs(&mut self, node: &T) -> usize; + + /// Renders the node's output. fn show_output( &mut self, pin: &OutPin, @@ -91,7 +111,7 @@ pub trait SnarlViewer { /// Reports the final node's rect after rendering. /// /// 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. + /// Node's position can be modified directly in this method. #[inline] fn final_node_rect( &mut self, @@ -240,4 +260,72 @@ pub trait SnarlViewer { fn drop_inputs(&mut self, pin: &InPin, snarl: &mut Snarl) { snarl.drop_inputs(pin.id); } + + /// Draws the node's input pin. + /// + /// This method is called after [`SnarlViewer::show_input`] and can be used to draw the pin shape. + /// By default it draws a pin with the shape and style returned by [`SnarlViewer::show_input`]. + /// + /// If you want to draw the pin yourself, you can override this method. + fn draw_input_pin( + &mut self, + pin: &InPin, + pin_info: &PinInfo, + pos: Pos2, + size: f32, + snarl_style: &SnarlStyle, + style: &Style, + painter: &Painter, + scale: f32, + snarl: &Snarl, + ) -> Color32 { + let _ = (pin, snarl); + + pin_info.draw(pos, size, snarl_style, style, painter, scale) + } + + /// Draws the node's output pin. + /// + /// This method is called after [`SnarlViewer::show_output`] and can be used to draw the pin shape. + /// By default it draws a pin with the shape and style returned by [`SnarlViewer::show_output`]. + /// + /// If you want to draw the pin yourself, you can override this method. + fn draw_output_pin( + &mut self, + pin: &OutPin, + pin_info: &PinInfo, + pos: Pos2, + size: f32, + snarl_style: &SnarlStyle, + style: &Style, + painter: &Painter, + scale: f32, + snarl: &Snarl, + ) -> Color32 { + let _ = (pin, snarl); + + pin_info.draw(pos, size, snarl_style, style, painter, scale) + } + + /// Draws background of the snarl view. + /// + /// By default it draws the background pattern using [`BackgroundPattern::draw`]. + /// + /// If you want to draw the background yourself, you can override this method. + fn draw_background( + &mut self, + background: Option<&BackgroundPattern>, + viewport: &Viewport, + snarl_style: &SnarlStyle, + style: &Style, + painter: &Painter, + snarl: &Snarl, + ) { + let _ = snarl; + + match background { + Some(background) => background.draw(viewport, snarl_style, style, painter), + None => {} + } + } }