style: cargo fmt with the workspace tab policy (hard_tabs)

Whitespace only; the fork has no own rustfmt.toml so the Oak root
policy applies.
This commit is contained in:
2026-08-11 23:06:13 +08:00
parent 507d7e6e8d
commit af5482d774
318 changed files with 132564 additions and 132170 deletions
+117 -117
View File
@@ -34,12 +34,12 @@ pub struct EdgeId(pub u64);
/// Whether a port accepts incoming connections or produces outgoing ones.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PortKind {
/// A port that consumes data; conventionally drawn on the left side of a
/// node and accepts connections *from* an [`PortKind::Output`] port.
Input,
/// A port that produces data; conventionally drawn on the right side of a
/// node and connects *to* an [`PortKind::Input`] port.
Output,
/// A port that consumes data; conventionally drawn on the left side of a
/// node and accepts connections *from* an [`PortKind::Output`] port.
Input,
/// A port that produces data; conventionally drawn on the right side of a
/// node and connects *to* an [`PortKind::Input`] port.
Output,
}
/// A lightweight, app-defined descriptor of the data flowing through a port.
@@ -59,28 +59,28 @@ pub enum PortKind {
/// distinct types sharing a name should disambiguate the name.
#[derive(Clone, Debug)]
pub struct PortDataType {
/// Human-readable type name, e.g. `"video"`, `"audio"`, `"matte"`.
/// Also used as the identity of the type (see type-level docs).
pub name: SharedString,
/// Color used to tint port dots and wires carrying this type.
pub color: Hsla,
/// Human-readable type name, e.g. `"video"`, `"audio"`, `"matte"`.
/// Also used as the identity of the type (see type-level docs).
pub name: SharedString,
/// Color used to tint port dots and wires carrying this type.
pub color: Hsla,
}
impl PortDataType {
/// Creates a new data-type descriptor with the given display name and
/// tint color.
pub fn new(name: impl Into<SharedString>, color: Hsla) -> Self {
Self {
name: name.into(),
color,
}
}
/// Creates a new data-type descriptor with the given display name and
/// tint color.
pub fn new(name: impl Into<SharedString>, color: Hsla) -> Self {
Self {
name: name.into(),
color,
}
}
}
impl PartialEq for PortDataType {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
impl Eq for PortDataType {}
@@ -91,27 +91,27 @@ impl Eq for PortDataType {}
/// [`PortId`], a direction ([`PortKind`]) and a [`PortDataType`] used for
/// tinting.
pub trait PortData {
/// Returns the globally unique identifier of this port.
fn id(&self) -> PortId;
/// Returns the globally unique identifier of this port.
fn id(&self) -> PortId;
/// Returns whether this is an input or an output port.
fn kind(&self) -> PortKind;
/// Returns whether this is an input or an output port.
fn kind(&self) -> PortKind;
/// Returns the short label drawn next to the port dot (e.g. `"in"`,
/// `"mask"`). May be empty, in which case only the dot is drawn.
fn label(&self) -> SharedString;
/// Returns the short label drawn next to the port dot (e.g. `"in"`,
/// `"mask"`). May be empty, in which case only the dot is drawn.
fn label(&self) -> SharedString;
/// Returns the data type of this port, used to tint the port dot and any
/// wires connected to it.
fn data_type(&self) -> PortDataType;
/// Returns the data type of this port, used to tint the port dot and any
/// wires connected to it.
fn data_type(&self) -> PortDataType;
/// Returns whether this port currently has at least one edge attached.
///
/// Used only for rendering (connected dots are filled, unconnected dots
/// are hollow) and for styling during wire drags; the widget does not
/// enforce any cardinality rules from it — that is the job of
/// [`NodeGraphDataSource::can_connect`].
fn is_connected(&self) -> bool;
/// Returns whether this port currently has at least one edge attached.
///
/// Used only for rendering (connected dots are filled, unconnected dots
/// are hollow) and for styling during wire drags; the widget does not
/// enforce any cardinality rules from it — that is the job of
/// [`NodeGraphDataSource::can_connect`].
fn is_connected(&self) -> bool;
}
/// A single node in the graph.
@@ -120,67 +120,67 @@ pub trait PortData {
/// left and a column of output ports on the right (see
/// [`NodeElement`](crate::node_graph::NodeElement)).
pub trait NodeData {
/// The port type used by this node's inputs and outputs.
type Port: PortData;
/// The port type used by this node's inputs and outputs.
type Port: PortData;
/// Returns the unique identifier of this node.
fn id(&self) -> NodeId;
/// Returns the unique identifier of this node.
fn id(&self) -> NodeId;
/// Returns the title drawn in the node's header.
fn title(&self) -> SharedString;
/// Returns the title drawn in the node's header.
fn title(&self) -> SharedString;
/// Returns the position of the node's top-left corner in *graph space*
/// (the document coordinate system).
///
/// Graph space is an unbounded, zoom-independent coordinate system: a
/// node at `point(px(100.), px(40.))` stays attached to that document
/// location regardless of pan and zoom. The view converts to screen
/// coordinates with
/// [`GraphViewState::graph_to_screen`](crate::node_graph::GraphViewState::graph_to_screen).
fn position(&self) -> Point<Pixels>;
/// Returns the position of the node's top-left corner in *graph space*
/// (the document coordinate system).
///
/// Graph space is an unbounded, zoom-independent coordinate system: a
/// node at `point(px(100.), px(40.))` stays attached to that document
/// location regardless of pan and zoom. The view converts to screen
/// coordinates with
/// [`GraphViewState::graph_to_screen`](crate::node_graph::GraphViewState::graph_to_screen).
fn position(&self) -> Point<Pixels>;
/// Returns the input ports of this node, in top-to-bottom draw order.
fn inputs(&self) -> Vec<Self::Port>;
/// Returns the input ports of this node, in top-to-bottom draw order.
fn inputs(&self) -> Vec<Self::Port>;
/// Returns the output ports of this node, in top-to-bottom draw order.
fn outputs(&self) -> Vec<Self::Port>;
/// Returns the output ports of this node, in top-to-bottom draw order.
fn outputs(&self) -> Vec<Self::Port>;
/// Returns an optional accent color for the node header, or `None` to use
/// the theme default. Apps typically use this to group nodes by category
/// (inputs, transforms, color management, outputs, …).
fn header_color(&self) -> Option<Hsla>;
/// Returns an optional accent color for the node header, or `None` to use
/// the theme default. Apps typically use this to group nodes by category
/// (inputs, transforms, color management, outputs, …).
fn header_color(&self) -> Option<Hsla>;
/// Returns whether the node is collapsed to just its header.
///
/// Collapsed nodes draw no ports and cannot be connection targets. The
/// collapsed state itself belongs to the app's model (or view state); the
/// widget only reflects it.
fn is_collapsed(&self) -> bool;
/// Returns whether the node is collapsed to just its header.
///
/// Collapsed nodes draw no ports and cannot be connection targets. The
/// collapsed state itself belongs to the app's model (or view state); the
/// widget only reflects it.
fn is_collapsed(&self) -> bool;
/// Returns whether the node is enabled.
///
/// Disabled nodes (e.g. a bypassed effect) are drawn dimmed. This is a
/// purely visual hint; the widget does not change interaction behavior
/// for disabled nodes.
fn is_enabled(&self) -> bool;
/// Returns whether the node is enabled.
///
/// Disabled nodes (e.g. a bypassed effect) are drawn dimmed. This is a
/// purely visual hint; the widget does not change interaction behavior
/// for disabled nodes.
fn is_enabled(&self) -> bool;
}
/// A single directed connection from an output port to an input port.
pub trait EdgeData {
/// Returns the unique identifier of this edge.
fn id(&self) -> EdgeId;
/// Returns the unique identifier of this edge.
fn id(&self) -> EdgeId;
/// Returns the id of the node the connection starts at.
fn from_node(&self) -> NodeId;
/// Returns the id of the node the connection starts at.
fn from_node(&self) -> NodeId;
/// Returns the id of the output port the connection starts at.
fn from_port(&self) -> PortId;
/// Returns the id of the output port the connection starts at.
fn from_port(&self) -> PortId;
/// Returns the id of the node the connection ends at.
fn to_node(&self) -> NodeId;
/// Returns the id of the node the connection ends at.
fn to_node(&self) -> NodeId;
/// Returns the id of the input port the connection ends at.
fn to_port(&self) -> PortId;
/// Returns the id of the input port the connection ends at.
fn to_port(&self) -> PortId;
}
/// The data source backing a [`NodeGraphView`](crate::node_graph::NodeGraphView).
@@ -195,40 +195,40 @@ pub trait EdgeData {
///
/// [`NodeGraphEvent`]: crate::node_graph::NodeGraphEvent
pub trait NodeGraphDataSource {
/// The node type returned by [`nodes()`](Self::nodes).
type Node: NodeData;
/// The edge type returned by [`edges()`](Self::edges).
type Edge: EdgeData;
/// The node type returned by [`nodes()`](Self::nodes).
type Node: NodeData;
/// The edge type returned by [`edges()`](Self::edges).
type Edge: EdgeData;
/// Returns all nodes to display, in no required order (the view sorts for
/// painting; selection order is unaffected).
fn nodes(&self) -> Vec<Self::Node>;
/// Returns all nodes to display, in no required order (the view sorts for
/// painting; selection order is unaffected).
fn nodes(&self) -> Vec<Self::Node>;
/// Returns all edges to display. Edges referencing ports or nodes that
/// are not part of [`nodes()`](Self::nodes) are ignored by the view.
fn edges(&self) -> Vec<Self::Edge>;
/// Returns all edges to display. Edges referencing ports or nodes that
/// are not part of [`nodes()`](Self::nodes) are ignored by the view.
fn edges(&self) -> Vec<Self::Edge>;
/// Returns whether connecting output port `from` to input port `to`
/// would be valid.
///
/// This is the single place where the app enforces its connection rules:
/// data-type compatibility (including implicit conversions), cycle
/// prevention, port cardinality, node enablement, and so on. The view
/// calls this:
///
/// - *continuously during a wire drag* to highlight compatible target
/// ports and to mark the ghost wire as valid/invalid, and
/// - *once on drop* before emitting
/// [`NodeGraphEvent::ConnectionRequested`](crate::node_graph::NodeGraphEvent::ConnectionRequested)
/// — a drop on a port for which this returns `false` cancels the drag
/// silently.
///
/// It must be cheap, pure, and consistent: the same arguments must yield
/// the same answer within a frame. The view passes output port first,
/// input port second, regardless of which end the user started the drag
/// from. Returning `true` here does not commit the app to accepting the
/// connection; the engine may still reject it when the event arrives
/// (e.g. it raced with another edit), in which case the app simply does
/// not apply it.
fn can_connect(&self, from: PortId, to: PortId) -> bool;
/// Returns whether connecting output port `from` to input port `to`
/// would be valid.
///
/// This is the single place where the app enforces its connection rules:
/// data-type compatibility (including implicit conversions), cycle
/// prevention, port cardinality, node enablement, and so on. The view
/// calls this:
///
/// - *continuously during a wire drag* to highlight compatible target
/// ports and to mark the ghost wire as valid/invalid, and
/// - *once on drop* before emitting
/// [`NodeGraphEvent::ConnectionRequested`](crate::node_graph::NodeGraphEvent::ConnectionRequested)
/// — a drop on a port for which this returns `false` cancels the drag
/// silently.
///
/// It must be cheap, pure, and consistent: the same arguments must yield
/// the same answer within a frame. The view passes output port first,
/// input port second, regardless of which end the user started the drag
/// from. Returning `true` here does not commit the app to accepting the
/// connection; the engine may still reject it when the event arrives
/// (e.g. it raced with another edit), in which case the app simply does
/// not apply it.
fn can_connect(&self, from: PortId, to: PortId) -> bool;
}
File diff suppressed because it is too large Load Diff
+103 -100
View File
@@ -19,10 +19,13 @@
// lands.
#![allow(clippy::extra_unused_type_parameters)]
use crate::{Bounds, Empty, IntoElement, Pixels, Point, Window, canvas, deferred, fill, hsla, point, px, size};
use crate::{
Bounds, Empty, IntoElement, Pixels, Point, Window, canvas, deferred, fill, hsla, point, px,
size,
};
use crate::node_graph::{
DEFAULT_NODE_WIDTH, GraphViewState, NodeElement, NodeGraphDataSource, NodeData, NodeVisualState,
DEFAULT_NODE_WIDTH, GraphViewState, NodeData, NodeElement, NodeGraphDataSource, NodeVisualState,
};
/// Scale factor from graph-space coordinates to minimap coordinates.
@@ -35,118 +38,118 @@ pub const MINIMAP_CONTENT_SCALE: f32 = 0.15;
/// minimap draws no node rectangles and does not react to clicks; it is a
/// passive viewport indicator.
pub struct GraphMinimap {
/// Whether the minimap is shown. Toggled by the app's view menu; the
/// minimap renders nothing and ignores input when `false`.
visible: bool,
/// Whether the minimap is shown. Toggled by the app's view menu; the
/// minimap renders nothing and ignores input when `false`.
visible: bool,
}
impl Default for GraphMinimap {
fn default() -> Self {
Self { visible: true }
}
fn default() -> Self {
Self { visible: true }
}
}
impl GraphMinimap {
/// Creates a visible minimap overlay.
pub fn new() -> Self {
Self::default()
}
/// Creates a visible minimap overlay.
pub fn new() -> Self {
Self::default()
}
/// Shows or hides the minimap.
pub fn set_visible(&mut self, visible: bool) {
self.visible = visible;
}
/// Shows or hides the minimap.
pub fn set_visible(&mut self, visible: bool) {
self.visible = visible;
}
/// Returns whether the minimap is currently shown.
pub fn is_visible(&self) -> bool {
self.visible
}
/// Returns whether the minimap is currently shown.
pub fn is_visible(&self) -> bool {
self.visible
}
/// Computes the axis-aligned bounding box of all nodes in graph space,
/// used as the minimap's content rect. Returns `None` for an empty
/// graph (the minimap then renders only its backdrop).
///
/// Node extents are derived from each node's position plus its rendered
/// card size ([`DEFAULT_NODE_WIDTH`] × [`NodeElement::height`]).
fn graph_bounds<D: NodeGraphDataSource>(data: &D) -> Option<(Point<Pixels>, Point<Pixels>)> {
let mut nodes = data.nodes().into_iter();
let first = nodes.next()?;
let extent = |node: &D::Node| {
let pos = node.position();
(
pos,
pos + point(
DEFAULT_NODE_WIDTH,
NodeElement::from_node(node, NodeVisualState::default()).height(),
),
)
};
let (mut min, mut max) = extent(&first);
for node in nodes {
let (node_min, node_max) = extent(&node);
min = min.min(&node_min);
max = max.max(&node_max);
}
Some((min, max))
}
/// Computes the axis-aligned bounding box of all nodes in graph space,
/// used as the minimap's content rect. Returns `None` for an empty
/// graph (the minimap then renders only its backdrop).
///
/// Node extents are derived from each node's position plus its rendered
/// card size ([`DEFAULT_NODE_WIDTH`] × [`NodeElement::height`]).
fn graph_bounds<D: NodeGraphDataSource>(data: &D) -> Option<(Point<Pixels>, Point<Pixels>)> {
let mut nodes = data.nodes().into_iter();
let first = nodes.next()?;
let extent = |node: &D::Node| {
let pos = node.position();
(
pos,
pos + point(
DEFAULT_NODE_WIDTH,
NodeElement::from_node(node, NodeVisualState::default()).height(),
),
)
};
let (mut min, mut max) = extent(&first);
for node in nodes {
let (node_min, node_max) = extent(&node);
min = min.min(&node_min);
max = max.max(&node_max);
}
Some((min, max))
}
/// Renders the minimap: a translucent backdrop with a viewport indicator
/// rectangle, laid out over `viewport_bounds` (the main view's
/// screen-space bounds).
///
/// The viewport rectangle is derived from `state` (offset + zoom): the
/// screen-space viewport is mapped back into graph space (`-offset / zoom`
/// plus `viewport size / zoom`) and then down to minimap scale.
pub fn render<D: NodeGraphDataSource>(
&mut self,
state: &GraphViewState,
viewport_bounds: Bounds<Pixels>,
_window: &mut Window,
) -> impl IntoElement {
if !self.visible {
return deferred(Empty);
}
let offset = state.offset();
let zoom = state.zoom();
deferred(canvas(
move |_bounds, _window, _cx| MinimapDraw {
offset,
zoom,
viewport_bounds,
},
move |bounds, draw, window, _cx| {
// Backdrop.
window.paint_quad(fill(bounds, hsla(0.0, 0.0, 0.0, 0.6)));
/// Renders the minimap: a translucent backdrop with a viewport indicator
/// rectangle, laid out over `viewport_bounds` (the main view's
/// screen-space bounds).
///
/// The viewport rectangle is derived from `state` (offset + zoom): the
/// screen-space viewport is mapped back into graph space (`-offset / zoom`
/// plus `viewport size / zoom`) and then down to minimap scale.
pub fn render<D: NodeGraphDataSource>(
&mut self,
state: &GraphViewState,
viewport_bounds: Bounds<Pixels>,
_window: &mut Window,
) -> impl IntoElement {
if !self.visible {
return deferred(Empty);
}
let offset = state.offset();
let zoom = state.zoom();
deferred(canvas(
move |_bounds, _window, _cx| MinimapDraw {
offset,
zoom,
viewport_bounds,
},
move |bounds, draw, window, _cx| {
// Backdrop.
window.paint_quad(fill(bounds, hsla(0.0, 0.0, 0.0, 0.6)));
// Viewport indicator: the graph-space viewport rect (screen
// size scaled back through `zoom`) mapped down to minimap
// scale, positioned at `-offset / zoom`.
let scale = MINIMAP_CONTENT_SCALE;
let origin = bounds.origin
+ point(
px(-(draw.offset.x.0 / draw.zoom) * scale),
px(-(draw.offset.y.0 / draw.zoom) * scale),
);
let vp_size = size(
px(draw.viewport_bounds.size.width.0 / draw.zoom * scale),
px(draw.viewport_bounds.size.height.0 / draw.zoom * scale),
);
window.paint_quad(fill(
Bounds::new(origin, vp_size),
hsla(0.63, 0.55, 0.55, 0.5),
));
},
))
}
// Viewport indicator: the graph-space viewport rect (screen
// size scaled back through `zoom`) mapped down to minimap
// scale, positioned at `-offset / zoom`.
let scale = MINIMAP_CONTENT_SCALE;
let origin = bounds.origin
+ point(
px(-(draw.offset.x.0 / draw.zoom) * scale),
px(-(draw.offset.y.0 / draw.zoom) * scale),
);
let vp_size = size(
px(draw.viewport_bounds.size.width.0 / draw.zoom * scale),
px(draw.viewport_bounds.size.height.0 / draw.zoom * scale),
);
window.paint_quad(fill(
Bounds::new(origin, vp_size),
hsla(0.63, 0.55, 0.55, 0.5),
));
},
))
}
}
/// Per-frame snapshot passed from the canvas prepaint to its paint closure.
#[derive(Clone, Copy)]
struct MinimapDraw {
/// Pan offset (screen-space position of the graph origin).
offset: Point<Pixels>,
/// Zoom factor.
zoom: f32,
/// The main view's screen-space bounds.
viewport_bounds: Bounds<Pixels>,
/// Pan offset (screen-space position of the graph origin).
offset: Point<Pixels>,
/// Zoom factor.
zoom: f32,
/// The main view's screen-space bounds.
viewport_bounds: Bounds<Pixels>,
}
+322 -311
View File
@@ -36,11 +36,11 @@
//! same function, so anchors and dots can never drift apart.
use crate::{
colors::DefaultColors, App, BorderStyle, Bounds, Corners, Edges, Font, Hsla, PaintQuad, Pixels,
Point, SharedString, TextAlign, TextRun, Window, fill, hsla, point, px, size,
App, BorderStyle, Bounds, Corners, Edges, Font, Hsla, PaintQuad, Pixels, Point, SharedString,
TextAlign, TextRun, Window, colors::DefaultColors, fill, hsla, point, px, size,
};
use crate::node_graph::{data::PortData, NodeData, NodeId, PortId};
use crate::node_graph::{NodeData, NodeId, PortId, data::PortData};
/// The default width of a node card. Node width is fixed; only the height
/// grows with the port count.
@@ -61,15 +61,15 @@ pub const PORT_INSET: Pixels = Pixels(8.0);
/// Visual state of a node card, supplied by the view at render time.
#[derive(Clone, Copy, Debug, Default)]
pub struct NodeVisualState {
/// Whether the node is part of the current selection (drawn with a
/// selection outline).
pub selected: bool,
/// Whether a wire drag is in progress and this node contains at least
/// one port that [`NodeGraphDataSource::can_connect`] approved as a drop
/// target (drawn with a subtle glow).
///
/// [`NodeGraphDataSource::can_connect`]: crate::node_graph::NodeGraphDataSource::can_connect
pub has_compatible_port: bool,
/// Whether the node is part of the current selection (drawn with a
/// selection outline).
pub selected: bool,
/// Whether a wire drag is in progress and this node contains at least
/// one port that [`NodeGraphDataSource::can_connect`] approved as a drop
/// target (drawn with a subtle glow).
///
/// [`NodeGraphDataSource::can_connect`]: crate::node_graph::NodeGraphDataSource::can_connect
pub has_compatible_port: bool,
}
/// A single rendered node card.
@@ -80,335 +80,346 @@ pub struct NodeVisualState {
/// [`NodeGraphView`](crate::node_graph::NodeGraphView), which owns the
/// gesture state machine.
pub struct NodeElement {
node: NodeId,
title: SharedString,
header_color: Option<Hsla>,
collapsed: bool,
enabled: bool,
visual: NodeVisualState,
inputs: Vec<PortRow>,
outputs: Vec<PortRow>,
node: NodeId,
title: SharedString,
header_color: Option<Hsla>,
collapsed: bool,
enabled: bool,
visual: NodeVisualState,
inputs: Vec<PortRow>,
outputs: Vec<PortRow>,
}
/// One rendered port row: everything needed to draw a port dot and label
/// without re-querying the data source.
#[derive(Clone, Debug)]
struct PortRow {
id: PortId,
label: SharedString,
color: Hsla,
connected: bool,
id: PortId,
label: SharedString,
color: Hsla,
connected: bool,
}
impl NodeElement {
/// Builds the element from a node snapshot and its visual state.
///
/// Reads title, header color, collapse/enable flags and both port columns
/// off `node`. Port rows are taken in the order returned by
/// [`NodeData::inputs`] / [`NodeData::outputs`], which defines their
/// top-to-bottom draw order.
pub fn from_node<N: NodeData>(node: &N, visual: NodeVisualState) -> Self {
let inputs = node
.inputs()
.into_iter()
.map(|port| PortRow {
id: port.id(),
label: port.label(),
color: port.data_type().color,
connected: port.is_connected(),
})
.collect();
let outputs = node
.outputs()
.into_iter()
.map(|port| PortRow {
id: port.id(),
label: port.label(),
color: port.data_type().color,
connected: port.is_connected(),
})
.collect();
Self {
node: node.id(),
title: node.title(),
header_color: node.header_color(),
collapsed: node.is_collapsed(),
enabled: node.is_enabled(),
visual,
inputs,
outputs,
}
}
/// Builds the element from a node snapshot and its visual state.
///
/// Reads title, header color, collapse/enable flags and both port columns
/// off `node`. Port rows are taken in the order returned by
/// [`NodeData::inputs`] / [`NodeData::outputs`], which defines their
/// top-to-bottom draw order.
pub fn from_node<N: NodeData>(node: &N, visual: NodeVisualState) -> Self {
let inputs = node
.inputs()
.into_iter()
.map(|port| PortRow {
id: port.id(),
label: port.label(),
color: port.data_type().color,
connected: port.is_connected(),
})
.collect();
let outputs = node
.outputs()
.into_iter()
.map(|port| PortRow {
id: port.id(),
label: port.label(),
color: port.data_type().color,
connected: port.is_connected(),
})
.collect();
Self {
node: node.id(),
title: node.title(),
header_color: node.header_color(),
collapsed: node.is_collapsed(),
enabled: node.is_enabled(),
visual,
inputs,
outputs,
}
}
/// Returns the id of the node this element renders.
pub fn node_id(&self) -> NodeId {
self.node
}
/// Returns the id of the node this element renders.
pub fn node_id(&self) -> NodeId {
self.node
}
/// Returns the total height of the node card: the header plus
/// `max(inputs, outputs)` port rows (zero rows when collapsed).
pub fn height(&self) -> Pixels {
if self.collapsed {
HEADER_HEIGHT
} else {
HEADER_HEIGHT + PORT_ROW_HEIGHT * self.inputs.len().max(self.outputs.len()) as f32
}
}
/// Returns the total height of the node card: the header plus
/// `max(inputs, outputs)` port rows (zero rows when collapsed).
pub fn height(&self) -> Pixels {
if self.collapsed {
HEADER_HEIGHT
} else {
HEADER_HEIGHT + PORT_ROW_HEIGHT * self.inputs.len().max(self.outputs.len()) as f32
}
}
/// Computes the node-local anchor point (port dot center) of the given
/// port, per the formula in the [module docs](crate::node_graph::node_element).
/// Wires attach here.
///
/// Returns `None` when the port is not part of this node or the node is
/// collapsed (collapsed nodes expose no anchors and cannot be
/// connection targets).
///
/// # Panics
///
/// Never panics; unknown ports yield `None`.
pub fn port_anchor(&self, port: PortId) -> Option<Point<Pixels>> {
if self.collapsed {
return None;
}
let row_y = |row: usize| HEADER_HEIGHT + PORT_ROW_HEIGHT * row as f32 + PORT_ROW_HEIGHT * 0.5;
if let Some(row) = self.inputs.iter().position(|p| p.id == port) {
return Some(point(PORT_DOT_RADIUS + PORT_INSET, row_y(row)));
}
if let Some(row) = self.outputs.iter().position(|p| p.id == port) {
return Some(point(
DEFAULT_NODE_WIDTH - PORT_DOT_RADIUS - PORT_INSET,
row_y(row),
));
}
None
}
/// Computes the node-local anchor point (port dot center) of the given
/// port, per the formula in the [module docs](crate::node_graph::node_element).
/// Wires attach here.
///
/// Returns `None` when the port is not part of this node or the node is
/// collapsed (collapsed nodes expose no anchors and cannot be
/// connection targets).
///
/// # Panics
///
/// Never panics; unknown ports yield `None`.
pub fn port_anchor(&self, port: PortId) -> Option<Point<Pixels>> {
if self.collapsed {
return None;
}
let row_y =
|row: usize| HEADER_HEIGHT + PORT_ROW_HEIGHT * row as f32 + PORT_ROW_HEIGHT * 0.5;
if let Some(row) = self.inputs.iter().position(|p| p.id == port) {
return Some(point(PORT_DOT_RADIUS + PORT_INSET, row_y(row)));
}
if let Some(row) = self.outputs.iter().position(|p| p.id == port) {
return Some(point(
DEFAULT_NODE_WIDTH - PORT_DOT_RADIUS - PORT_INSET,
row_y(row),
));
}
None
}
/// Hit-tests a node-local point against port dots and returns the id of
/// the port whose dot (inflated by a small grab margin) contains it.
/// Used to start wire drags. Header and body hits return `None`.
pub fn port_at(&self, position: Point<Pixels>) -> Option<PortId> {
let hit_radius = PORT_DOT_RADIUS + px(4.0);
for port in self.inputs.iter().chain(self.outputs.iter()) {
if let Some(anchor) = self.port_anchor(port.id) {
let dx = (position.x - anchor.x).0;
let dy = (position.y - anchor.y).0;
if dx * dx + dy * dy <= hit_radius.0 * hit_radius.0 {
return Some(port.id);
}
}
}
None
}
/// Hit-tests a node-local point against port dots and returns the id of
/// the port whose dot (inflated by a small grab margin) contains it.
/// Used to start wire drags. Header and body hits return `None`.
pub fn port_at(&self, position: Point<Pixels>) -> Option<PortId> {
let hit_radius = PORT_DOT_RADIUS + px(4.0);
for port in self.inputs.iter().chain(self.outputs.iter()) {
if let Some(anchor) = self.port_anchor(port.id) {
let dx = (position.x - anchor.x).0;
let dy = (position.y - anchor.y).0;
if dx * dx + dy * dy <= hit_radius.0 * hit_radius.0 {
return Some(port.id);
}
}
}
None
}
/// Returns whether a node-local point lands on the collapse toggle in the
/// header. The view uses this to distinguish "toggle collapse" clicks
/// from drag starts.
pub fn collapse_toggle_hit(&self, position: Point<Pixels>) -> bool {
position.x.0 >= 0.0
&& position.x.0 <= HEADER_HEIGHT.0
&& position.y.0 >= 0.0
&& position.y.0 <= HEADER_HEIGHT.0
}
/// Returns whether a node-local point lands on the collapse toggle in the
/// header. The view uses this to distinguish "toggle collapse" clicks
/// from drag starts.
pub fn collapse_toggle_hit(&self, position: Point<Pixels>) -> bool {
position.x.0 >= 0.0
&& position.x.0 <= HEADER_HEIGHT.0
&& position.y.0 >= 0.0
&& position.y.0 <= HEADER_HEIGHT.0
}
/// Returns whether a node-local point lands on the enable/bypass toggle
/// in the header. Toggling emits no dedicated event — it is handled like
/// any other edit: the view emits a request and the app flips the flag in
/// its model.
pub fn enable_toggle_hit(&self, position: Point<Pixels>) -> bool {
position.x.0 >= DEFAULT_NODE_WIDTH.0 - HEADER_HEIGHT.0
&& position.x.0 <= DEFAULT_NODE_WIDTH.0
&& position.y.0 >= 0.0
&& position.y.0 <= HEADER_HEIGHT.0
}
/// Returns whether a node-local point lands on the enable/bypass toggle
/// in the header. Toggling emits no dedicated event — it is handled like
/// any other edit: the view emits a request and the app flips the flag in
/// its model.
pub fn enable_toggle_hit(&self, position: Point<Pixels>) -> bool {
position.x.0 >= DEFAULT_NODE_WIDTH.0 - HEADER_HEIGHT.0
&& position.x.0 <= DEFAULT_NODE_WIDTH.0
&& position.y.0 >= 0.0
&& position.y.0 <= HEADER_HEIGHT.0
}
/// Paints the node card into the current window layer: header with title
/// and toggles, port dots tinted by data type (filled when connected,
/// hollow otherwise) with labels, selection outline, disabled dimming and
/// the compatible-port glow. `origin` is the card's screen-space top-left
/// corner; all geometry within the card is node-local.
pub(crate) fn paint(&self, origin: Point<Pixels>, window: &mut Window, cx: &mut App) {
let colors = cx.default_colors().clone();
let bounds = Bounds::new(origin, size(DEFAULT_NODE_WIDTH, self.height()));
/// Paints the node card into the current window layer: header with title
/// and toggles, port dots tinted by data type (filled when connected,
/// hollow otherwise) with labels, selection outline, disabled dimming and
/// the compatible-port glow. `origin` is the card's screen-space top-left
/// corner; all geometry within the card is node-local.
pub(crate) fn paint(&self, origin: Point<Pixels>, window: &mut Window, cx: &mut App) {
let colors = cx.default_colors().clone();
let bounds = Bounds::new(origin, size(DEFAULT_NODE_WIDTH, self.height()));
// Compatible-port glow: a slightly inflated rect behind the card while
// a wire drag offers at least one valid drop target on this node.
if self.visual.has_compatible_port {
let glow = Bounds::new(
point(origin.x - px(2.0), origin.y - px(2.0)),
size(DEFAULT_NODE_WIDTH + px(4.0), self.height() + px(4.0)),
);
window.paint_quad(fill(glow, Hsla::from(colors.selected).opacity(0.2)));
}
// Compatible-port glow: a slightly inflated rect behind the card while
// a wire drag offers at least one valid drop target on this node.
if self.visual.has_compatible_port {
let glow = Bounds::new(
point(origin.x - px(2.0), origin.y - px(2.0)),
size(DEFAULT_NODE_WIDTH + px(4.0), self.height() + px(4.0)),
);
window.paint_quad(fill(glow, Hsla::from(colors.selected).opacity(0.2)));
}
// Card body.
window.paint_quad(fill(bounds, colors.background));
// Card body.
window.paint_quad(fill(bounds, colors.background));
// Border quad: transparent fill, themed border (accent when selected).
window.paint_quad(PaintQuad {
bounds,
corner_radii: Corners::all(px(4.0)),
background: hsla(0.0, 0.0, 0.0, 0.0).into(),
border_widths: Edges::all(if self.visual.selected { px(1.5) } else { px(1.0) }),
border_color: if self.visual.selected {
Hsla::from(colors.selected)
} else {
Hsla::from(colors.border)
},
border_style: BorderStyle::Solid,
});
// Border quad: transparent fill, themed border (accent when selected).
window.paint_quad(PaintQuad {
bounds,
corner_radii: Corners::all(px(4.0)),
background: hsla(0.0, 0.0, 0.0, 0.0).into(),
border_widths: Edges::all(if self.visual.selected {
px(1.5)
} else {
px(1.0)
}),
border_color: if self.visual.selected {
Hsla::from(colors.selected)
} else {
Hsla::from(colors.border)
},
border_style: BorderStyle::Solid,
});
// Header bar with the node's accent color (or the theme container
// color), containing the title and the collapse/enable toggles.
let header_bounds = Bounds::new(origin, size(DEFAULT_NODE_WIDTH, HEADER_HEIGHT));
window.paint_quad(fill(
header_bounds,
self.header_color.unwrap_or(Hsla::from(colors.container)),
));
// Header bar with the node's accent color (or the theme container
// color), containing the title and the collapse/enable toggles.
let header_bounds = Bounds::new(origin, size(DEFAULT_NODE_WIDTH, HEADER_HEIGHT));
window.paint_quad(fill(
header_bounds,
self.header_color.unwrap_or(Hsla::from(colors.container)),
));
let text_y = bounds.top() + px((HEADER_HEIGHT.0 - 12.0) / 2.0);
paint_text(
window,
cx,
&self.title,
px(12.0),
point(bounds.left() + px(28.0), text_y),
px(12.0),
Hsla::from(colors.text),
TextAlign::Left,
None,
);
let text_y = bounds.top() + px((HEADER_HEIGHT.0 - 12.0) / 2.0);
paint_text(
window,
cx,
&self.title,
px(12.0),
point(bounds.left() + px(28.0), text_y),
px(12.0),
Hsla::from(colors.text),
TextAlign::Left,
None,
);
// Collapse toggle: "▶" when collapsed (click to expand), "▼" when
// expanded (click to collapse).
paint_text(
window,
cx,
if self.collapsed { "▶" } else { "▼" },
px(10.0),
point(bounds.left() + px(10.0), text_y),
px(12.0),
Hsla::from(colors.text),
TextAlign::Left,
None,
);
// Collapse toggle: "▶" when collapsed (click to expand), "▼" when
// expanded (click to collapse).
paint_text(
window,
cx,
if self.collapsed { "▶" } else { "▼" },
px(10.0),
point(bounds.left() + px(10.0), text_y),
px(12.0),
Hsla::from(colors.text),
TextAlign::Left,
None,
);
// Enable toggle glyph (power symbol) on the right edge of the header.
paint_text(
window,
cx,
"⏻",
px(12.0),
point(bounds.right() - px(20.0), text_y),
px(12.0),
Hsla::from(colors.text),
TextAlign::Left,
None,
);
// Enable toggle glyph (power symbol) on the right edge of the header.
paint_text(
window,
cx,
"⏻",
px(12.0),
point(bounds.right() - px(20.0), text_y),
px(12.0),
Hsla::from(colors.text),
TextAlign::Left,
None,
);
// Port dots and labels, only when the node is expanded.
if !self.collapsed {
let label_font_size = px(11.0);
let label_height = px(12.0);
for port in self.inputs.iter().chain(self.outputs.iter()) {
let Some(anchor) = self.port_anchor(port.id) else {
continue;
};
let dot_bounds = Bounds::new(
point(anchor.x - PORT_DOT_RADIUS, anchor.y - PORT_DOT_RADIUS),
size(PORT_DOT_RADIUS * 2.0, PORT_DOT_RADIUS * 2.0),
);
if port.connected {
// Connected dots are solid tinted circles.
window.paint_quad(PaintQuad {
bounds: dot_bounds,
corner_radii: Corners::all(PORT_DOT_RADIUS),
background: port.color.into(),
border_widths: Edges::all(px(0.0)),
border_color: hsla(0.0, 0.0, 0.0, 0.0),
border_style: BorderStyle::Solid,
});
} else {
// Unconnected dots are hollow: a tinted ring around the
// card's background color.
window.paint_quad(fill(dot_bounds, port.color));
let inner = Bounds::new(
point(anchor.x - PORT_DOT_RADIUS + px(2.0), anchor.y - PORT_DOT_RADIUS + px(2.0)),
size(PORT_DOT_RADIUS * 2.0 - px(4.0), PORT_DOT_RADIUS * 2.0 - px(4.0)),
);
window.paint_quad(fill(inner, colors.background));
}
// Port dots and labels, only when the node is expanded.
if !self.collapsed {
let label_font_size = px(11.0);
let label_height = px(12.0);
for port in self.inputs.iter().chain(self.outputs.iter()) {
let Some(anchor) = self.port_anchor(port.id) else {
continue;
};
let dot_bounds = Bounds::new(
point(anchor.x - PORT_DOT_RADIUS, anchor.y - PORT_DOT_RADIUS),
size(PORT_DOT_RADIUS * 2.0, PORT_DOT_RADIUS * 2.0),
);
if port.connected {
// Connected dots are solid tinted circles.
window.paint_quad(PaintQuad {
bounds: dot_bounds,
corner_radii: Corners::all(PORT_DOT_RADIUS),
background: port.color.into(),
border_widths: Edges::all(px(0.0)),
border_color: hsla(0.0, 0.0, 0.0, 0.0),
border_style: BorderStyle::Solid,
});
} else {
// Unconnected dots are hollow: a tinted ring around the
// card's background color.
window.paint_quad(fill(dot_bounds, port.color));
let inner = Bounds::new(
point(
anchor.x - PORT_DOT_RADIUS + px(2.0),
anchor.y - PORT_DOT_RADIUS + px(2.0),
),
size(
PORT_DOT_RADIUS * 2.0 - px(4.0),
PORT_DOT_RADIUS * 2.0 - px(4.0),
),
);
window.paint_quad(fill(inner, colors.background));
}
if !port.label.is_empty() {
if self.inputs.iter().any(|p| p.id == port.id) {
// Input labels: left-aligned, starting right of the dot.
paint_text(
window,
cx,
&port.label,
label_font_size,
point(anchor.x + PORT_DOT_RADIUS + px(6.0), anchor.y - px(6.0)),
label_height,
Hsla::from(colors.text),
TextAlign::Left,
None,
);
} else {
// Output labels: right-aligned so they end just left of
// the dot. The box origin sits `align_width` left of the
// dot; the label's right edge lands at the box right.
let align_width = px(100.0);
paint_text(
window,
cx,
&port.label,
label_font_size,
point(
anchor.x - PORT_DOT_RADIUS - px(6.0) - align_width,
anchor.y - px(6.0),
),
label_height,
Hsla::from(colors.text),
TextAlign::Right,
Some(align_width),
);
}
}
}
}
if !port.label.is_empty() {
if self.inputs.iter().any(|p| p.id == port.id) {
// Input labels: left-aligned, starting right of the dot.
paint_text(
window,
cx,
&port.label,
label_font_size,
point(anchor.x + PORT_DOT_RADIUS + px(6.0), anchor.y - px(6.0)),
label_height,
Hsla::from(colors.text),
TextAlign::Left,
None,
);
} else {
// Output labels: right-aligned so they end just left of
// the dot. The box origin sits `align_width` left of the
// dot; the label's right edge lands at the box right.
let align_width = px(100.0);
paint_text(
window,
cx,
&port.label,
label_font_size,
point(
anchor.x - PORT_DOT_RADIUS - px(6.0) - align_width,
anchor.y - px(6.0),
),
label_height,
Hsla::from(colors.text),
TextAlign::Right,
Some(align_width),
);
}
}
}
}
// Disabled nodes are dimmed with a dark overlay.
if !self.enabled {
window.paint_quad(fill(bounds, hsla(0.0, 0.0, 0.0, 0.5)));
}
}
// Disabled nodes are dimmed with a dark overlay.
if !self.enabled {
window.paint_quad(fill(bounds, hsla(0.0, 0.0, 0.0, 0.5)));
}
}
}
/// Shapes and paints a single text line at `origin` (the top-left of the
/// line box) with the given font size, line height, alignment and color.
fn paint_text(
window: &mut Window,
cx: &mut App,
text: &str,
font_size: Pixels,
origin: Point<Pixels>,
line_height: Pixels,
color: Hsla,
align: TextAlign,
align_width: Option<Pixels>,
window: &mut Window,
cx: &mut App,
text: &str,
font_size: Pixels,
origin: Point<Pixels>,
line_height: Pixels,
color: Hsla,
align: TextAlign,
align_width: Option<Pixels>,
) {
let line = window.text_system().shape_line(
SharedString::from(text),
font_size,
&[TextRun {
len: text.len(),
font: Font::default(),
color,
background_color: None,
underline: None,
strikethrough: None,
letter_spacing: None,
}],
None,
);
let _ = line.paint(origin, line_height, align, align_width, window, cx);
let line = window.text_system().shape_line(
SharedString::from(text),
font_size,
&[TextRun {
len: text.len(),
font: Font::default(),
color,
background_color: None,
underline: None,
strikethrough: None,
letter_spacing: None,
}],
None,
);
let _ = line.paint(origin, line_height, align, align_width, window, cx);
}
+256 -242
View File
@@ -39,213 +39,213 @@ pub const MAX_ZOOM: f32 = 4.0;
/// ```
#[derive(Clone, Debug)]
pub struct GraphViewState {
/// Pan offset in screen space: the screen-space position of the graph
/// origin. Positive values move the graph content down-right.
offset: Point<Pixels>,
/// Zoom factor, always within [`MIN_ZOOM`]..=[`MAX_ZOOM`]. `1.0` is 100%.
zoom: f32,
/// The currently selected nodes. Kept sorted (B-Tree) so that
/// `SelectionChanged` events are deterministic and cheap to diff.
selection: BTreeSet<NodeId>,
/// An in-progress marquee (rubber-band) selection rectangle, in screen
/// space, if the user is currently dragging one.
marquee: Option<SelectionRect>,
/// Pan offset in screen space: the screen-space position of the graph
/// origin. Positive values move the graph content down-right.
offset: Point<Pixels>,
/// Zoom factor, always within [`MIN_ZOOM`]..=[`MAX_ZOOM`]. `1.0` is 100%.
zoom: f32,
/// The currently selected nodes. Kept sorted (B-Tree) so that
/// `SelectionChanged` events are deterministic and cheap to diff.
selection: BTreeSet<NodeId>,
/// An in-progress marquee (rubber-band) selection rectangle, in screen
/// space, if the user is currently dragging one.
marquee: Option<SelectionRect>,
}
impl Default for GraphViewState {
fn default() -> Self {
Self {
offset: point(Pixels::ZERO, Pixels::ZERO),
zoom: 1.0,
selection: BTreeSet::new(),
marquee: None,
}
}
fn default() -> Self {
Self {
offset: point(Pixels::ZERO, Pixels::ZERO),
zoom: 1.0,
selection: BTreeSet::new(),
marquee: None,
}
}
}
impl GraphViewState {
/// Creates a fresh view state: no pan, 100% zoom, empty selection.
pub fn new() -> Self {
Self::default()
}
/// Creates a fresh view state: no pan, 100% zoom, empty selection.
pub fn new() -> Self {
Self::default()
}
/// Returns the current pan offset (the screen-space position of the
/// graph origin).
pub fn offset(&self) -> Point<Pixels> {
self.offset
}
/// Returns the current pan offset (the screen-space position of the
/// graph origin).
pub fn offset(&self) -> Point<Pixels> {
self.offset
}
/// Sets the pan offset directly. No clamping is applied — the graph is
/// unbounded.
pub fn set_offset(&mut self, offset: Point<Pixels>) {
self.offset = offset;
}
/// Sets the pan offset directly. No clamping is applied — the graph is
/// unbounded.
pub fn set_offset(&mut self, offset: Point<Pixels>) {
self.offset = offset;
}
/// Pans the view by a screen-space delta (typically a drag delta).
pub fn pan_by(&mut self, delta: Point<Pixels>) {
self.offset = self.offset + delta;
}
/// Pans the view by a screen-space delta (typically a drag delta).
pub fn pan_by(&mut self, delta: Point<Pixels>) {
self.offset = self.offset + delta;
}
/// Returns the current zoom factor, guaranteed within
/// [`MIN_ZOOM`]..=[`MAX_ZOOM`].
pub fn zoom(&self) -> f32 {
self.zoom
}
/// Returns the current zoom factor, guaranteed within
/// [`MIN_ZOOM`]..=[`MAX_ZOOM`].
pub fn zoom(&self) -> f32 {
self.zoom
}
/// Sets the zoom factor, clamped to [`MIN_ZOOM`]..=[`MAX_ZOOM`].
///
/// Unlike [`zoom_at`](Self::zoom_at) this does not preserve any anchor
/// point; the graph origin stays put and content scales around it.
pub fn set_zoom(&mut self, zoom: f32) {
self.zoom = zoom.clamp(MIN_ZOOM, MAX_ZOOM);
}
/// Sets the zoom factor, clamped to [`MIN_ZOOM`]..=[`MAX_ZOOM`].
///
/// Unlike [`zoom_at`](Self::zoom_at) this does not preserve any anchor
/// point; the graph origin stays put and content scales around it.
pub fn set_zoom(&mut self, zoom: f32) {
self.zoom = zoom.clamp(MIN_ZOOM, MAX_ZOOM);
}
/// Zooms by `factor` (e.g. `1.1` per scroll step) while keeping the
/// graph point under `anchor` (a screen-space position, usually the
/// cursor) stationary on screen.
///
/// # Math contract
///
/// Let `z` be the old zoom and `z' = clamp(z * factor, MIN_ZOOM,
/// MAX_ZOOM)` the new one. The offset is adjusted so that
/// `graph_to_screen(g)` is identical before and after for the graph point
/// `g = screen_to_graph(anchor)`:
///
/// ```text
/// offset' = anchor - (anchor - offset) * (z' / z)
/// ```
///
/// When the zoom is clamped (already at the min/max), `z' == z` and the
/// offset is left untouched — the call is then a no-op.
pub fn zoom_at(&mut self, anchor: Point<Pixels>, factor: f32) {
let new_zoom = (self.zoom * factor).clamp(MIN_ZOOM, MAX_ZOOM);
if new_zoom == self.zoom {
return;
}
let scale = new_zoom / self.zoom;
self.offset = point(
anchor.x - (anchor.x - self.offset.x) * scale,
anchor.y - (anchor.y - self.offset.y) * scale,
);
self.zoom = new_zoom;
}
/// Zooms by `factor` (e.g. `1.1` per scroll step) while keeping the
/// graph point under `anchor` (a screen-space position, usually the
/// cursor) stationary on screen.
///
/// # Math contract
///
/// Let `z` be the old zoom and `z' = clamp(z * factor, MIN_ZOOM,
/// MAX_ZOOM)` the new one. The offset is adjusted so that
/// `graph_to_screen(g)` is identical before and after for the graph point
/// `g = screen_to_graph(anchor)`:
///
/// ```text
/// offset' = anchor - (anchor - offset) * (z' / z)
/// ```
///
/// When the zoom is clamped (already at the min/max), `z' == z` and the
/// offset is left untouched — the call is then a no-op.
pub fn zoom_at(&mut self, anchor: Point<Pixels>, factor: f32) {
let new_zoom = (self.zoom * factor).clamp(MIN_ZOOM, MAX_ZOOM);
if new_zoom == self.zoom {
return;
}
let scale = new_zoom / self.zoom;
self.offset = point(
anchor.x - (anchor.x - self.offset.x) * scale,
anchor.y - (anchor.y - self.offset.y) * scale,
);
self.zoom = new_zoom;
}
/// Fits the graph-space rectangle `rect` (typically the union of every
/// node's bounds) into the `viewport` screen-space size: zooms so the
/// rect occupies at most 95% of the viewport (clamped to
/// [`MIN_ZOOM`]..=[`MAX_ZOOM`]) and pans so the rect is centered.
///
/// No-op when either size is non-positive. Used by hosts for a "fit
/// window" command and as the initial viewport after the first layout.
pub fn fit_to_rect(&mut self, rect: Bounds<Pixels>, viewport: Size<Pixels>) {
const PADDING: f32 = 40.0;
let (rw, rh) = (rect.size.width.0, rect.size.height.0);
let (vw, vh) = (viewport.width.0, viewport.height.0);
if rw <= 0.0 || rh <= 0.0 || vw <= 0.0 || vh <= 0.0 {
return;
}
// Fit the larger axis; the padding keeps a breathing margin.
let zoom = (vw / (rw + PADDING * 2.0))
.min(vh / (rh + PADDING * 2.0))
.clamp(MIN_ZOOM, MAX_ZOOM);
// Center the rect: offset = (viewport - rect_size * zoom) / 2
// - rect_origin * zoom.
self.zoom = zoom;
self.offset = point(
Pixels((vw - rw * zoom) * 0.5 - rect.origin.x.0 * zoom),
Pixels((vh - rh * zoom) * 0.5 - rect.origin.y.0 * zoom),
);
}
/// Fits the graph-space rectangle `rect` (typically the union of every
/// node's bounds) into the `viewport` screen-space size: zooms so the
/// rect occupies at most 95% of the viewport (clamped to
/// [`MIN_ZOOM`]..=[`MAX_ZOOM`]) and pans so the rect is centered.
///
/// No-op when either size is non-positive. Used by hosts for a "fit
/// window" command and as the initial viewport after the first layout.
pub fn fit_to_rect(&mut self, rect: Bounds<Pixels>, viewport: Size<Pixels>) {
const PADDING: f32 = 40.0;
let (rw, rh) = (rect.size.width.0, rect.size.height.0);
let (vw, vh) = (viewport.width.0, viewport.height.0);
if rw <= 0.0 || rh <= 0.0 || vw <= 0.0 || vh <= 0.0 {
return;
}
// Fit the larger axis; the padding keeps a breathing margin.
let zoom = (vw / (rw + PADDING * 2.0))
.min(vh / (rh + PADDING * 2.0))
.clamp(MIN_ZOOM, MAX_ZOOM);
// Center the rect: offset = (viewport - rect_size * zoom) / 2
// - rect_origin * zoom.
self.zoom = zoom;
self.offset = point(
Pixels((vw - rw * zoom) * 0.5 - rect.origin.x.0 * zoom),
Pixels((vh - rh * zoom) * 0.5 - rect.origin.y.0 * zoom),
);
}
/// Maps a graph-space (document) point to screen space:
/// `screen = graph * zoom + offset`.
pub fn graph_to_screen(&self, graph: Point<Pixels>) -> Point<Pixels> {
point(
graph.x * self.zoom + self.offset.x,
graph.y * self.zoom + self.offset.y,
)
}
/// Maps a graph-space (document) point to screen space:
/// `screen = graph * zoom + offset`.
pub fn graph_to_screen(&self, graph: Point<Pixels>) -> Point<Pixels> {
point(
graph.x * self.zoom + self.offset.x,
graph.y * self.zoom + self.offset.y,
)
}
/// Maps a screen-space point to graph space:
/// `graph = (screen - offset) / zoom`. This is the exact inverse of
/// [`graph_to_screen`](Self::graph_to_screen).
pub fn screen_to_graph(&self, screen: Point<Pixels>) -> Point<Pixels> {
point(
(screen.x - self.offset.x) / self.zoom,
(screen.y - self.offset.y) / self.zoom,
)
}
/// Maps a screen-space point to graph space:
/// `graph = (screen - offset) / zoom`. This is the exact inverse of
/// [`graph_to_screen`](Self::graph_to_screen).
pub fn screen_to_graph(&self, screen: Point<Pixels>) -> Point<Pixels> {
point(
(screen.x - self.offset.x) / self.zoom,
(screen.y - self.offset.y) / self.zoom,
)
}
/// Returns the set of currently selected nodes.
pub fn selection(&self) -> &BTreeSet<NodeId> {
&self.selection
}
/// Returns the set of currently selected nodes.
pub fn selection(&self) -> &BTreeSet<NodeId> {
&self.selection
}
/// Returns whether the given node is currently selected.
pub fn is_selected(&self, node: NodeId) -> bool {
self.selection.contains(&node)
}
/// Returns whether the given node is currently selected.
pub fn is_selected(&self, node: NodeId) -> bool {
self.selection.contains(&node)
}
/// Replaces the selection with exactly the given nodes.
///
/// The view compares before/after and emits
/// [`NodeGraphEvent::SelectionChanged`](crate::node_graph::NodeGraphEvent::SelectionChanged)
/// when the set actually changed; calling this directly does not emit
/// events on its own.
pub fn set_selection(&mut self, nodes: impl IntoIterator<Item = NodeId>) {
self.selection = nodes.into_iter().collect();
}
/// Replaces the selection with exactly the given nodes.
///
/// The view compares before/after and emits
/// [`NodeGraphEvent::SelectionChanged`](crate::node_graph::NodeGraphEvent::SelectionChanged)
/// when the set actually changed; calling this directly does not emit
/// events on its own.
pub fn set_selection(&mut self, nodes: impl IntoIterator<Item = NodeId>) {
self.selection = nodes.into_iter().collect();
}
/// Adds `node` to the selection (shift-click semantics).
pub fn select(&mut self, node: NodeId) {
self.selection.insert(node);
}
/// Adds `node` to the selection (shift-click semantics).
pub fn select(&mut self, node: NodeId) {
self.selection.insert(node);
}
/// Removes `node` from the selection; returns whether it was selected.
pub fn deselect(&mut self, node: NodeId) -> bool {
self.selection.remove(&node)
}
/// Removes `node` from the selection; returns whether it was selected.
pub fn deselect(&mut self, node: NodeId) -> bool {
self.selection.remove(&node)
}
/// Toggles `node` in the selection (shift-click toggle semantics).
pub fn toggle_selection(&mut self, node: NodeId) {
if !self.deselect(node) {
self.select(node);
}
}
/// Toggles `node` in the selection (shift-click toggle semantics).
pub fn toggle_selection(&mut self, node: NodeId) {
if !self.deselect(node) {
self.select(node);
}
}
/// Clears the selection.
pub fn clear_selection(&mut self) {
self.selection.clear();
}
/// Clears the selection.
pub fn clear_selection(&mut self) {
self.selection.clear();
}
/// Returns the in-progress marquee selection rectangle, if any.
pub fn marquee(&self) -> Option<&SelectionRect> {
self.marquee.as_ref()
}
/// Returns the in-progress marquee selection rectangle, if any.
pub fn marquee(&self) -> Option<&SelectionRect> {
self.marquee.as_ref()
}
/// Begins a marquee selection anchored at the given screen-space point.
pub fn begin_marquee(&mut self, anchor: Point<Pixels>) {
self.marquee = Some(SelectionRect {
anchor,
current: anchor,
});
}
/// Begins a marquee selection anchored at the given screen-space point.
pub fn begin_marquee(&mut self, anchor: Point<Pixels>) {
self.marquee = Some(SelectionRect {
anchor,
current: anchor,
});
}
/// Updates the current corner of the in-progress marquee. Does nothing if
/// no marquee is in progress.
pub fn update_marquee(&mut self, current: Point<Pixels>) {
if let Some(marquee) = &mut self.marquee {
marquee.current = current;
}
}
/// Updates the current corner of the in-progress marquee. Does nothing if
/// no marquee is in progress.
pub fn update_marquee(&mut self, current: Point<Pixels>) {
if let Some(marquee) = &mut self.marquee {
marquee.current = current;
}
}
/// Ends the marquee and returns it, or `None` if none was in progress.
///
/// The caller (the view) converts the rect to graph space and selects all
/// nodes intersecting it.
pub fn end_marquee(&mut self) -> Option<SelectionRect> {
self.marquee.take()
}
/// Ends the marquee and returns it, or `None` if none was in progress.
///
/// The caller (the view) converts the rect to graph space and selects all
/// nodes intersecting it.
pub fn end_marquee(&mut self) -> Option<SelectionRect> {
self.marquee.take()
}
}
/// A marquee (rubber-band) selection rectangle in screen space.
@@ -255,78 +255,92 @@ impl GraphViewState {
/// well-ordered rect regardless of drag direction.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SelectionRect {
/// The screen-space point where the marquee drag started.
pub anchor: Point<Pixels>,
/// The current screen-space corner (usually the cursor position).
pub current: Point<Pixels>,
/// The screen-space point where the marquee drag started.
pub anchor: Point<Pixels>,
/// The current screen-space corner (usually the cursor position).
pub current: Point<Pixels>,
}
impl SelectionRect {
/// Returns the axis-aligned rectangle with `min` as the top-left and
/// `max` as the bottom-right corner, independent of drag direction.
pub fn normalized(&self) -> (Point<Pixels>, Point<Pixels>) {
let min = point(self.anchor.x.min(self.current.x), self.anchor.y.min(self.current.y));
let max = point(self.anchor.x.max(self.current.x), self.anchor.y.max(self.current.y));
(min, max)
}
/// Returns the axis-aligned rectangle with `min` as the top-left and
/// `max` as the bottom-right corner, independent of drag direction.
pub fn normalized(&self) -> (Point<Pixels>, Point<Pixels>) {
let min = point(
self.anchor.x.min(self.current.x),
self.anchor.y.min(self.current.y),
);
let max = point(
self.anchor.x.max(self.current.x),
self.anchor.y.max(self.current.y),
);
(min, max)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{px, size};
use super::*;
use crate::{px, size};
/// Fitting a graph rect into a viewport centers it and picks a zoom that
/// fits the larger axis; the mapping must stay consistent afterwards.
#[test]
fn fit_centers_and_fits_the_rect() {
let mut state = GraphViewState::new();
let rect = Bounds::new(point(px(40.0), px(60.0)), size(px(1040.0), px(230.0)));
state.fit_to_rect(rect, size(px(640.0), px(500.0)));
/// Fitting a graph rect into a viewport centers it and picks a zoom that
/// fits the larger axis; the mapping must stay consistent afterwards.
#[test]
fn fit_centers_and_fits_the_rect() {
let mut state = GraphViewState::new();
let rect = Bounds::new(point(px(40.0), px(60.0)), size(px(1040.0), px(230.0)));
state.fit_to_rect(rect, size(px(640.0), px(500.0)));
// The rect's center must map to the viewport's center.
let graph_center = rect.center();
let screen_center = state.graph_to_screen(graph_center);
assert!((screen_center.x.0 - 320.0).abs() < 0.5, "x center: {}", screen_center.x.0);
assert!((screen_center.y.0 - 250.0).abs() < 0.5, "y center: {}", screen_center.y.0);
// The rect's center must map to the viewport's center.
let graph_center = rect.center();
let screen_center = state.graph_to_screen(graph_center);
assert!(
(screen_center.x.0 - 320.0).abs() < 0.5,
"x center: {}",
screen_center.x.0
);
assert!(
(screen_center.y.0 - 250.0).abs() < 0.5,
"y center: {}",
screen_center.y.0
);
// The fitted rect must fit within the viewport (with the 40px padding).
let top_left = state.graph_to_screen(rect.origin);
let bottom_right = state.graph_to_screen(rect.bottom_right());
assert!(top_left.x.0 >= 0.0 && bottom_right.x.0 <= 640.0);
assert!(top_left.y.0 >= 0.0 && bottom_right.y.0 <= 500.0);
}
// The fitted rect must fit within the viewport (with the 40px padding).
let top_left = state.graph_to_screen(rect.origin);
let bottom_right = state.graph_to_screen(rect.bottom_right());
assert!(top_left.x.0 >= 0.0 && bottom_right.x.0 <= 640.0);
assert!(top_left.y.0 >= 0.0 && bottom_right.y.0 <= 500.0);
}
/// The width and height both shrink when the rect is tall and wide
/// (whichever axis is more constraining drives the zoom).
#[test]
fn fit_respects_both_axes() {
let mut state = GraphViewState::new();
// A wide rect in a narrow viewport: width drives the zoom.
let rect = Bounds::new(point(px(0.0), px(0.0)), size(px(2000.0), px(100.0)));
state.fit_to_rect(rect, size(px(400.0), px(400.0)));
let fitted = state.graph_to_screen(rect.bottom_right());
assert!(fitted.x.0 <= 400.0 && fitted.y.0 <= 400.0);
assert!(state.zoom() < 1.0);
}
/// The width and height both shrink when the rect is tall and wide
/// (whichever axis is more constraining drives the zoom).
#[test]
fn fit_respects_both_axes() {
let mut state = GraphViewState::new();
// A wide rect in a narrow viewport: width drives the zoom.
let rect = Bounds::new(point(px(0.0), px(0.0)), size(px(2000.0), px(100.0)));
state.fit_to_rect(rect, size(px(400.0), px(400.0)));
let fitted = state.graph_to_screen(rect.bottom_right());
assert!(fitted.x.0 <= 400.0 && fitted.y.0 <= 400.0);
assert!(state.zoom() < 1.0);
}
/// A rect smaller than the viewport zooms in (clamped to [`MAX_ZOOM`]).
#[test]
fn fit_zooms_in_for_small_graphs() {
let mut state = GraphViewState::new();
let rect = Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(60.0)));
state.fit_to_rect(rect, size(px(1000.0), px(800.0)));
assert_eq!(state.zoom(), MAX_ZOOM);
}
/// A rect smaller than the viewport zooms in (clamped to [`MAX_ZOOM`]).
#[test]
fn fit_zooms_in_for_small_graphs() {
let mut state = GraphViewState::new();
let rect = Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(60.0)));
state.fit_to_rect(rect, size(px(1000.0), px(800.0)));
assert_eq!(state.zoom(), MAX_ZOOM);
}
/// Non-positive viewport or rect sizes are ignored.
#[test]
fn fit_ignores_non_positive_sizes() {
let mut state = GraphViewState::new();
let before = state.clone();
let rect = Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(60.0)));
state.fit_to_rect(rect, size(px(0.0), px(800.0)));
assert_eq!(state.zoom(), before.zoom());
assert_eq!(state.offset(), before.offset());
}
/// Non-positive viewport or rect sizes are ignored.
#[test]
fn fit_ignores_non_positive_sizes() {
let mut state = GraphViewState::new();
let before = state.clone();
let rect = Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(60.0)));
state.fit_to_rect(rect, size(px(0.0), px(800.0)));
assert_eq!(state.zoom(), before.zoom());
assert_eq!(state.offset(), before.offset());
}
}
+198 -206
View File
@@ -18,19 +18,19 @@ pub const WIRE_CURVATURE: Pixels = px(60.0);
/// The visual state of a wire, chosen by the view per frame.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum WireVisualState {
/// A regular, idle wire.
#[default]
Normal,
/// The wire is hovered (slightly brightened; click targets become
/// discoverable).
Hovered,
/// The wire is part of the selection (accent color, thicker stroke).
Selected,
/// The ghost wire of an in-progress drag whose current hover target (if
/// any) was rejected by
/// [`NodeGraphDataSource::can_connect`](crate::node_graph::NodeGraphDataSource::can_connect).
/// Drawn dashed/red to signal "dropping here will not connect".
InvalidDrag,
/// A regular, idle wire.
#[default]
Normal,
/// The wire is hovered (slightly brightened; click targets become
/// discoverable).
Hovered,
/// The wire is part of the selection (accent color, thicker stroke).
Selected,
/// The ghost wire of an in-progress drag whose current hover target (if
/// any) was rejected by
/// [`NodeGraphDataSource::can_connect`](crate::node_graph::NodeGraphDataSource::can_connect).
/// Drawn dashed/red to signal "dropping here will not connect".
InvalidDrag,
}
/// A fully-resolved wire ready to paint: both endpoints are already computed
@@ -40,76 +40,72 @@ pub enum WireVisualState {
/// from an [`EdgeData`](crate::node_graph::EdgeData) plus the port anchors of
/// the two endpoint nodes.
pub struct Wire {
edge: EdgeId,
from: Point<Pixels>,
to: Point<Pixels>,
color: Hsla,
state: WireVisualState,
edge: EdgeId,
from: Point<Pixels>,
to: Point<Pixels>,
color: Hsla,
state: WireVisualState,
}
impl Wire {
/// Creates a wire between two screen-space anchor points, tinted with the
/// connection's data-type color.
pub fn new(
edge: EdgeId,
from: Point<Pixels>,
to: Point<Pixels>,
data_type: &PortDataType,
state: WireVisualState,
) -> Self {
Self {
edge,
from,
to,
color: data_type.color,
state,
}
}
/// Creates a wire between two screen-space anchor points, tinted with the
/// connection's data-type color.
pub fn new(
edge: EdgeId,
from: Point<Pixels>,
to: Point<Pixels>,
data_type: &PortDataType,
state: WireVisualState,
) -> Self {
Self {
edge,
from,
to,
color: data_type.color,
state,
}
}
/// Returns the edge this wire represents.
pub fn edge(&self) -> EdgeId {
self.edge
}
/// Returns the edge this wire represents.
pub fn edge(&self) -> EdgeId {
self.edge
}
/// Builds the cubic bezier [`crate::Path`] for a wire from `from` to
/// `to`, leaving both endpoints horizontally: the control points are
/// placed `WIRE_CURVATURE * zoom` to the right of `from` and to the left
/// of `to`. Shared by regular wires and the ghost wire so both have
/// identical curvature behavior.
///
/// Returns `None` when the path cannot be built (degenerate input); the
/// caller simply skips painting that frame.
pub fn build_path(
from: Point<Pixels>,
to: Point<Pixels>,
zoom: f32,
) -> Option<Path<Pixels>> {
wire_path(from, to, zoom, px(2.0), None)
}
/// Builds the cubic bezier [`crate::Path`] for a wire from `from` to
/// `to`, leaving both endpoints horizontally: the control points are
/// placed `WIRE_CURVATURE * zoom` to the right of `from` and to the left
/// of `to`. Shared by regular wires and the ghost wire so both have
/// identical curvature behavior.
///
/// Returns `None` when the path cannot be built (degenerate input); the
/// caller simply skips painting that frame.
pub fn build_path(from: Point<Pixels>, to: Point<Pixels>, zoom: f32) -> Option<Path<Pixels>> {
wire_path(from, to, zoom, px(2.0), None)
}
/// Paints the wire with [`Window::paint_path`], applying the stroke width
/// and color adjustments implied by its [`WireVisualState`].
pub fn paint(&self, window: &mut Window, zoom: f32) {
let (color, width, dash) = match self.state {
WireVisualState::Normal => (self.color.opacity(0.6), px(2.0), None),
WireVisualState::Hovered => (self.color, px(2.5), None),
WireVisualState::Selected => (self.color, px(3.0), None),
WireVisualState::InvalidDrag => (
hsla(0.0, 0.85, 0.55, 1.0),
px(2.0),
Some([px(6.0), px(4.0)]),
),
};
if let Some(path) = wire_path(
self.from,
self.to,
zoom,
width,
dash.as_ref().map(|dash| &dash[..]),
) {
window.paint_path(path, color);
}
}
/// Paints the wire with [`Window::paint_path`], applying the stroke width
/// and color adjustments implied by its [`WireVisualState`].
pub fn paint(&self, window: &mut Window, zoom: f32) {
let (color, width, dash) = match self.state {
WireVisualState::Normal => (self.color.opacity(0.6), px(2.0), None),
WireVisualState::Hovered => (self.color, px(2.5), None),
WireVisualState::Selected => (self.color, px(3.0), None),
WireVisualState::InvalidDrag => (
hsla(0.0, 0.85, 0.55, 1.0),
px(2.0),
Some([px(6.0), px(4.0)]),
),
};
if let Some(path) = wire_path(
self.from,
self.to,
zoom,
width,
dash.as_ref().map(|dash| &dash[..]),
) {
window.paint_path(path, color);
}
}
}
/// Builds the cubic bezier path for a wire stroke with the given width and
@@ -117,24 +113,24 @@ impl Wire {
/// [`Wire::build_path`] and the ghost wire both delegate to it so every wire
/// shares the same curvature behavior.
pub(crate) fn wire_path(
from: Point<Pixels>,
to: Point<Pixels>,
zoom: f32,
width: Pixels,
dash: Option<&[Pixels]>,
from: Point<Pixels>,
to: Point<Pixels>,
zoom: f32,
width: Pixels,
dash: Option<&[Pixels]>,
) -> Option<Path<Pixels>> {
let mut builder = PathBuilder::stroke(width);
if let Some(dash) = dash {
builder = builder.dash_array(dash);
}
let curvature = WIRE_CURVATURE * zoom;
builder.move_to(from);
builder.cubic_bezier_to(
to,
point(from.x + curvature, from.y),
point(to.x - curvature, to.y),
);
builder.build().ok()
let mut builder = PathBuilder::stroke(width);
if let Some(dash) = dash {
builder = builder.dash_array(dash);
}
let curvature = WIRE_CURVATURE * zoom;
builder.move_to(from);
builder.cubic_bezier_to(
to,
point(from.x + curvature, from.y),
point(to.x - curvature, to.y),
);
builder.build().ok()
}
/// The transient "ghost" wire shown while the user drags a connection from a
@@ -146,89 +142,85 @@ pub(crate) fn wire_path(
/// [`WireVisualState::InvalidDrag`] depending on
/// [`NodeGraphDataSource::can_connect`](crate::node_graph::NodeGraphDataSource::can_connect).
pub struct GhostWire {
/// The screen-space anchor of the port the drag started from.
source: Point<Pixels>,
/// The current screen-space position of the free end (cursor, or a
/// snapped hover-target anchor).
free_end: Point<Pixels>,
/// Data type of the source port; tints the ghost.
color: Hsla,
/// Whether the current hover target is a valid drop (drives the
/// [`WireVisualState::InvalidDrag`] styling).
target_valid: bool,
/// Whether the drag started from an output port. When `false` (drag
/// started from an input), `source`/`free_end` are swapped when building
/// the path so the bezier tangents still point the right way.
from_output: bool,
/// The screen-space anchor of the port the drag started from.
source: Point<Pixels>,
/// The current screen-space position of the free end (cursor, or a
/// snapped hover-target anchor).
free_end: Point<Pixels>,
/// Data type of the source port; tints the ghost.
color: Hsla,
/// Whether the current hover target is a valid drop (drives the
/// [`WireVisualState::InvalidDrag`] styling).
target_valid: bool,
/// Whether the drag started from an output port. When `false` (drag
/// started from an input), `source`/`free_end` are swapped when building
/// the path so the bezier tangents still point the right way.
from_output: bool,
}
impl GhostWire {
/// Creates a ghost wire anchored at `source` (screen space), tinted with
/// the source port's data type. `from_output` records the drag direction;
/// see the field docs.
pub fn new(
source: Point<Pixels>,
data_type: &PortDataType,
from_output: bool,
) -> Self {
Self {
source,
free_end: source,
color: data_type.color,
target_valid: false,
from_output,
}
}
/// Creates a ghost wire anchored at `source` (screen space), tinted with
/// the source port's data type. `from_output` records the drag direction;
/// see the field docs.
pub fn new(source: Point<Pixels>, data_type: &PortDataType, from_output: bool) -> Self {
Self {
source,
free_end: source,
color: data_type.color,
target_valid: false,
from_output,
}
}
/// Returns the screen-space anchor of the port the drag started from.
pub(crate) fn source(&self) -> Point<Pixels> {
self.source
}
/// Returns the screen-space anchor of the port the drag started from.
pub(crate) fn source(&self) -> Point<Pixels> {
self.source
}
/// Returns the current screen-space position of the free end.
pub(crate) fn free_end(&self) -> Point<Pixels> {
self.free_end
}
/// Returns the current screen-space position of the free end.
pub(crate) fn free_end(&self) -> Point<Pixels> {
self.free_end
}
/// Returns the data-type color tinting the ghost.
pub(crate) fn color(&self) -> Hsla {
self.color
}
/// Returns the data-type color tinting the ghost.
pub(crate) fn color(&self) -> Hsla {
self.color
}
/// Returns whether the currently hovered port is a valid drop target.
pub(crate) fn is_target_valid(&self) -> bool {
self.target_valid
}
/// Returns whether the currently hovered port is a valid drop target.
pub(crate) fn is_target_valid(&self) -> bool {
self.target_valid
}
/// Returns whether the drag started from an output port.
pub(crate) fn is_from_output(&self) -> bool {
self.from_output
}
/// Returns whether the drag started from an output port.
pub(crate) fn is_from_output(&self) -> bool {
self.from_output
}
/// Moves the free end to `cursor` (screen space) and records whether the
/// currently hovered port — if any — is a valid drop target. Pass
/// `snapped = Some(anchor)` instead of the raw cursor when the cursor is
/// inside a port's grab radius, so the ghost visually snaps onto it.
pub fn update(
&mut self,
cursor: Point<Pixels>,
snapped: Option<Point<Pixels>>,
target_valid: bool,
) {
self.free_end = snapped.unwrap_or(cursor);
self.target_valid = target_valid;
}
/// Moves the free end to `cursor` (screen space) and records whether the
/// currently hovered port — if any — is a valid drop target. Pass
/// `snapped = Some(anchor)` instead of the raw cursor when the cursor is
/// inside a port's grab radius, so the ghost visually snaps onto it.
pub fn update(
&mut self,
cursor: Point<Pixels>,
snapped: Option<Point<Pixels>>,
target_valid: bool,
) {
self.free_end = snapped.unwrap_or(cursor);
self.target_valid = target_valid;
}
/// Paints the ghost wire using the same bezier shape as [`Wire`], with
/// its state styling.
pub fn paint(&self, window: &mut Window, zoom: f32) {
let (from, to) = if self.from_output {
(self.source, self.free_end)
} else {
(self.free_end, self.source)
};
paint_ghost(window, from, to, self.color, self.target_valid, zoom);
}
/// Paints the ghost wire using the same bezier shape as [`Wire`], with
/// its state styling.
pub fn paint(&self, window: &mut Window, zoom: f32) {
let (from, to) = if self.from_output {
(self.source, self.free_end)
} else {
(self.free_end, self.source)
};
paint_ghost(window, from, to, self.color, self.target_valid, zoom);
}
}
/// Paints the ghost wire between two screen-space anchors. Valid drops are
@@ -237,20 +229,20 @@ impl GhostWire {
/// connect. Used both by [`GhostWire::paint`] and by the view's frame
/// snapshot.
pub(crate) fn paint_ghost(
window: &mut Window,
from: Point<Pixels>,
to: Point<Pixels>,
color: Hsla,
target_valid: bool,
zoom: f32,
window: &mut Window,
from: Point<Pixels>,
to: Point<Pixels>,
color: Hsla,
target_valid: bool,
zoom: f32,
) {
if target_valid {
if let Some(path) = wire_path(from, to, zoom, px(2.5), None) {
window.paint_path(path, color);
}
} else if let Some(path) = wire_path(from, to, zoom, px(2.0), Some(&[px(6.0), px(4.0)])) {
window.paint_path(path, hsla(0.0, 0.85, 0.55, 1.0));
}
if target_valid {
if let Some(path) = wire_path(from, to, zoom, px(2.5), None) {
window.paint_path(path, color);
}
} else if let Some(path) = wire_path(from, to, zoom, px(2.0), Some(&[px(6.0), px(4.0)])) {
window.paint_path(path, hsla(0.0, 0.85, 0.55, 1.0));
}
}
/// Pixels per second the [`FlowAnimation`] dash phase advances while active.
@@ -267,36 +259,36 @@ pub const FLOW_SPEED: f32 = 60.0;
/// their static style while inactive.
#[derive(Clone, Debug, Default)]
pub struct FlowAnimation {
/// Current dash phase in pixels, monotonically increasing while active.
phase: Pixels,
/// Whether the animation is currently running (e.g. during playback).
active: bool,
/// Current dash phase in pixels, monotonically increasing while active.
phase: Pixels,
/// Whether the animation is currently running (e.g. during playback).
active: bool,
}
impl FlowAnimation {
/// Starts the flow animation (e.g. when playback begins), resetting the
/// phase to zero.
pub fn start(&mut self) {
self.active = true;
self.phase = px(0.0);
}
/// Starts the flow animation (e.g. when playback begins), resetting the
/// phase to zero.
pub fn start(&mut self) {
self.active = true;
self.phase = px(0.0);
}
/// Stops the flow animation; wires fall back to their static style.
pub fn stop(&mut self) {
self.active = false;
}
/// Stops the flow animation; wires fall back to their static style.
pub fn stop(&mut self) {
self.active = false;
}
/// Advances the phase by one frame. `dt` is the elapsed frame time in
/// seconds; flow speed is a fixed px/s constant. No-op while inactive.
pub fn advance(&mut self, dt: f32) {
if self.active {
self.phase += px(FLOW_SPEED * dt);
}
}
/// Advances the phase by one frame. `dt` is the elapsed frame time in
/// seconds; flow speed is a fixed px/s constant. No-op while inactive.
pub fn advance(&mut self, dt: f32) {
if self.active {
self.phase += px(FLOW_SPEED * dt);
}
}
/// Returns the current dash phase to apply to wire strokes, or `None`
/// while inactive.
pub fn phase(&self) -> Option<Pixels> {
self.active.then_some(self.phase)
}
/// Returns the current dash phase to apply to wire strokes, or `None`
/// while inactive.
pub fn phase(&self) -> Option<Pixels> {
self.active.then_some(self.phase)
}
}