use std::cell::Cell;
use std::rc::Rc;
use iced::advanced::widget::{Tree, Widget};
use iced::advanced::{Layout, layout, mouse, renderer};
use iced::widget::{container, text};
use iced::{Element, Length, Point, Rectangle, Size, Theme, Vector, window};
use iced_wgpu::core::clipboard;
use iced_nodegraph::{
AnchorStatus, Indexed, NodeGraph, PinRef, anchor, default_anchor_style, edge, node, pin,
};
mod common;
use common::record::Recorder;
struct InteractionProbe {
cursor_seen: Rc<Cell<Option<Point>>>,
}
impl<Message> Widget<Message, Theme, Recorder> for InteractionProbe {
fn size(&self) -> Size<Length> {
Size::new(
Length::Fixed(PROBE_SIZE.width),
Length::Fixed(PROBE_SIZE.height),
)
}
fn layout(&mut self, _: &mut Tree, _: &Recorder, limits: &layout::Limits) -> layout::Node {
layout::Node::new(limits.resolve(
Length::Fixed(PROBE_SIZE.width),
Length::Fixed(PROBE_SIZE.height),
Size::ZERO,
))
}
fn draw(
&self,
_: &Tree,
_: &mut Recorder,
_: &Theme,
_: &renderer::Style,
_: Layout<'_>,
_: mouse::Cursor,
_: &Rectangle,
) {
}
fn mouse_interaction(
&self,
_: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_: &Rectangle,
_: &Recorder,
) -> mouse::Interaction {
self.cursor_seen.set(cursor.position());
if cursor.is_over(layout.bounds()) {
mouse::Interaction::Pointer
} else {
mouse::Interaction::None
}
}
}
impl<'a, Message: 'a> From<InteractionProbe> for Element<'a, Message, Theme, Recorder> {
fn from(w: InteractionProbe) -> Self {
Element::new(w)
}
}
const PROBE_SIZE: Size = Size::new(40.0, 20.0);
const VIEWPORT: Size = Size::new(1024.0, 768.0);
const GRAPH_SIZE: Size = Size::new(400.0, 400.0);
type Graph = NodeGraph<'static, Indexed, (), Theme, Recorder>;
fn graph_with_probe(
origin: Vector,
node_world: Point,
camera_pos: Point,
camera_zoom: f32,
cursor_seen: Rc<Cell<Option<Point>>>,
renderer: &Recorder,
) -> (Graph, Tree, layout::Node) {
let mut graph: Graph = NodeGraph::default()
.width(Length::Fixed(GRAPH_SIZE.width))
.height(Length::Fixed(GRAPH_SIZE.height))
.camera(camera_pos, camera_zoom);
graph = graph.push_node(node(
0usize,
node_world,
Element::from(InteractionProbe { cursor_seen }),
));
let mut tree = Tree::new(&graph as &dyn Widget<(), Theme, Recorder>);
let layout_node = graph.layout(
&mut tree,
renderer,
&layout::Limits::new(Size::ZERO, VIEWPORT),
);
let layout = Layout::with_offset(origin, &layout_node);
drive(
&mut graph,
&mut tree,
layout,
&iced::Event::Mouse(mouse::Event::CursorMoved {
position: Point::new(-1.0, -1.0),
}),
mouse::Cursor::Unavailable,
renderer,
);
(graph, tree, layout_node)
}
fn drive(
graph: &mut Graph,
tree: &mut Tree,
layout: Layout<'_>,
event: &iced::Event,
cursor: mouse::Cursor,
renderer: &Recorder,
) -> window::RedrawRequest {
let mut msgs: Vec<()> = Vec::new();
let mut shell = iced_wgpu::core::Shell::new(&mut msgs);
let mut clip = clipboard::Null;
graph.update(
tree,
event,
layout,
cursor,
renderer,
&mut clip,
&mut shell,
&Rectangle::new(Point::ORIGIN, VIEWPORT),
);
shell.redraw_request()
}
fn to_screen(origin: Vector, world: Point, camera_pos: Point, zoom: f32) -> Point {
Point::new(
origin.x + (world.x + camera_pos.x) * zoom,
origin.y + (world.y + camera_pos.y) * zoom,
)
}
#[test]
fn child_under_cursor_sets_the_cursor() {
let origin = Vector::new(40.0, 100.0);
let world = Point::new(10.0, 20.0);
let cam_pos = Point::new(15.0, -5.0);
let zoom = 2.0;
let cursor_seen = Rc::new(Cell::new(None));
let renderer = Recorder::detached();
let (graph, tree, layout_node) =
graph_with_probe(origin, world, cam_pos, zoom, cursor_seen.clone(), &renderer);
let layout = Layout::with_offset(origin, &layout_node);
let inside_world = Point::new(
world.x + PROBE_SIZE.width / 2.0,
world.y + PROBE_SIZE.height / 2.0,
);
let screen = to_screen(origin, inside_world, cam_pos, zoom);
let interaction = graph.mouse_interaction(
&tree,
layout,
mouse::Cursor::Available(screen),
&Rectangle::new(Point::ORIGIN, VIEWPORT),
&renderer,
);
let seen = cursor_seen
.get()
.expect("the graph must forward the query to the node under the cursor");
let expected = Point::new(origin.x + inside_world.x, origin.y + inside_world.y);
assert!(
(seen.x - expected.x).abs() < 0.5 && (seen.y - expected.y).abs() < 0.5,
"probe was handed {seen:?}, expected layout-absolute {expected:?}",
);
assert_eq!(
interaction,
mouse::Interaction::Pointer,
"the child's cursor must be what the graph reports",
);
}
#[test]
fn empty_canvas_claims_nothing() {
let origin = Vector::new(40.0, 100.0);
let world = Point::new(10.0, 20.0);
let cam_pos = Point::ORIGIN;
let zoom = 1.0;
let cursor_seen = Rc::new(Cell::new(None));
let renderer = Recorder::detached();
let (graph, tree, layout_node) =
graph_with_probe(origin, world, cam_pos, zoom, cursor_seen.clone(), &renderer);
let layout = Layout::with_offset(origin, &layout_node);
let screen = to_screen(origin, Point::new(300.0, 300.0), cam_pos, zoom);
let interaction = graph.mouse_interaction(
&tree,
layout,
mouse::Cursor::Available(screen),
&Rectangle::new(Point::ORIGIN, VIEWPORT),
&renderer,
);
assert_eq!(
interaction,
mouse::Interaction::None,
"bare canvas must leave the cursor to the host",
);
assert!(
cursor_seen.get().is_none(),
"a node the cursor is not over must not be queried",
);
}
#[test]
fn cursor_outside_the_graph_claims_nothing() {
let origin = Vector::new(40.0, 100.0);
let renderer = Recorder::detached();
let (graph, tree, layout_node) = graph_with_probe(
origin,
Point::new(10.0, 20.0),
Point::ORIGIN,
1.0,
Rc::new(Cell::new(None)),
&renderer,
);
let layout = Layout::with_offset(origin, &layout_node);
let screen = Point::new(origin.x - 5.0, origin.y + 10.0);
let interaction = graph.mouse_interaction(
&tree,
layout,
mouse::Cursor::Available(screen),
&Rectangle::new(Point::ORIGIN, VIEWPORT),
&renderer,
);
assert_eq!(
interaction,
mouse::Interaction::None,
"outside its own bounds the graph must not claim the cursor",
);
}
#[test]
fn pan_drag_claims_the_grabbing_cursor() {
let origin = Vector::new(40.0, 100.0);
let renderer = Recorder::detached();
let (mut graph, mut tree, layout_node) = graph_with_probe(
origin,
Point::new(10.0, 20.0),
Point::ORIGIN,
1.0,
Rc::new(Cell::new(None)),
&renderer,
);
let layout = Layout::with_offset(origin, &layout_node);
let press = Point::new(origin.x + 300.0, origin.y + 300.0);
drive(
&mut graph,
&mut tree,
layout,
&iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)),
mouse::Cursor::Available(press),
&renderer,
);
let dragged = Point::new(origin.x - 200.0, origin.y - 90.0);
let interaction = graph.mouse_interaction(
&tree,
layout,
mouse::Cursor::Available(dragged),
&Rectangle::new(Point::ORIGIN, VIEWPORT),
&renderer,
);
assert_eq!(
interaction,
mouse::Interaction::Grabbing,
"a pan in flight must show the grabbing cursor",
);
}
#[test]
fn a_text_input_in_a_node_asks_for_the_text_cursor() {
let origin = Vector::new(40.0, 100.0);
let world = Point::new(10.0, 20.0);
let zoom = 2.0;
let renderer = Recorder::detached();
let mut graph: Graph = NodeGraph::default()
.width(Length::Fixed(GRAPH_SIZE.width))
.height(Length::Fixed(GRAPH_SIZE.height))
.camera(Point::ORIGIN, zoom);
graph = graph.push_node(node(
0usize,
world,
Element::from(
iced_widget::text_input::<(), Theme, Recorder>("", "hello")
.on_input(|_| ())
.width(Length::Fixed(120.0)),
),
));
let mut tree = Tree::new(&graph as &dyn Widget<(), Theme, Recorder>);
let layout_node = graph.layout(
&mut tree,
&renderer,
&layout::Limits::new(Size::ZERO, VIEWPORT),
);
let layout = Layout::with_offset(origin, &layout_node);
drive(
&mut graph,
&mut tree,
layout,
&iced::Event::Mouse(mouse::Event::CursorMoved {
position: Point::new(-1.0, -1.0),
}),
mouse::Cursor::Unavailable,
&renderer,
);
let inside = Point::new(world.x + 5.0, world.y + 5.0);
let screen = to_screen(origin, inside, Point::ORIGIN, zoom);
let interaction = graph.mouse_interaction(
&tree,
layout,
mouse::Cursor::Available(screen),
&Rectangle::new(Point::ORIGIN, VIEWPORT),
&renderer,
);
assert_eq!(
interaction,
mouse::Interaction::Text,
"hovering a text input inside a node must report the text cursor",
);
}
const CABLE_NODE_A: Point = Point::new(20.0, 40.0);
const CABLE_NODE_B: Point = Point::new(300.0, 40.0);
const CABLE_ANCHOR: usize = 9;
const CABLE_ANCHOR_AT: Point = Point::new(160.0, 160.0);
fn orbit_0() -> f32 {
default_anchor_style(&Theme::Dark, AnchorStatus::Idle).orbit_offset
}
fn wrap_probe() -> Point {
Point::new(CABLE_ANCHOR_AT.x, CABLE_ANCHOR_AT.y + orbit_0())
}
fn run_probe() -> Point {
Point::new(126.0, 100.0)
}
fn empty_probe() -> Point {
Point::new(350.0, 350.0)
}
fn pin_body() -> iced::widget::Container<'static, (), Theme, Recorder> {
container(text("p"))
.width(Length::Fixed(40.0))
.height(Length::Fixed(20.0))
}
fn graph_with_cable(
renderer: &Recorder,
wired: bool,
camera_pos: Point,
camera_zoom: f32,
) -> (Graph, Tree, layout::Node) {
let mut graph: Graph = NodeGraph::default()
.width(Length::Fixed(GRAPH_SIZE.width))
.height(Length::Fixed(GRAPH_SIZE.height))
.camera(camera_pos, camera_zoom);
if wired {
graph = graph
.on_anchor_move(|_, _| ())
.on_anchor_create(|_, _| ())
.on_route_attach(|_, _| ())
.on_route_detach(|_, _| ());
}
graph = graph.push_node(node(
0usize,
CABLE_NODE_A,
pin!(Right, 0usize, pin_body(), Output),
));
graph = graph.push_node(node(
1usize,
CABLE_NODE_B,
pin!(Left, 0usize, pin_body(), Input),
));
graph = graph.push_anchor(anchor(CABLE_ANCHOR, CABLE_ANCHOR_AT));
graph = graph.push_edge(edge((), PinRef::new(0, 0), PinRef::new(1, 0)).route([CABLE_ANCHOR]));
let mut tree = Tree::new(&graph as &dyn Widget<(), Theme, Recorder>);
let layout_node = graph.layout(
&mut tree,
renderer,
&layout::Limits::new(Size::ZERO, VIEWPORT),
);
let layout = Layout::with_offset(Vector::ZERO, &layout_node);
drive(
&mut graph,
&mut tree,
layout,
&iced::Event::Mouse(mouse::Event::CursorMoved {
position: Point::new(-1.0, -1.0),
}),
mouse::Cursor::Unavailable,
renderer,
);
(graph, tree, layout_node)
}
#[test]
fn an_anchors_grabbable_parts_offer_a_grab() {
let renderer = Recorder::detached();
let ask = |wired: bool, screen: Point| {
let (graph, tree, layout_node) = graph_with_cable(&renderer, wired, Point::ORIGIN, 1.0);
graph.mouse_interaction(
&tree,
Layout::with_offset(Vector::ZERO, &layout_node),
mouse::Cursor::Available(screen),
&Rectangle::new(Point::ORIGIN, VIEWPORT),
&renderer,
)
};
for (what, at) in [
("the anchor core", CABLE_ANCHOR_AT),
("the wrap", wrap_probe()),
("the cable's run", run_probe()),
] {
assert_eq!(
ask(true, at),
mouse::Interaction::Grab,
"{what} must offer a grab",
);
assert_eq!(
ask(false, at),
mouse::Interaction::None,
"{what} must claim nothing while its gesture is unwired",
);
}
assert_eq!(
ask(true, empty_probe()),
mouse::Interaction::None,
"bare canvas claims nothing",
);
}
#[test]
fn the_grabbable_parts_offer_a_grab_at_any_zoom() {
let renderer = Recorder::detached();
for zoom in [0.5, 2.0] {
let ask = |world: Point| {
let (graph, tree, layout_node) = graph_with_cable(&renderer, true, Point::ORIGIN, zoom);
graph.mouse_interaction(
&tree,
Layout::with_offset(Vector::ZERO, &layout_node),
mouse::Cursor::Available(to_screen(Vector::ZERO, world, Point::ORIGIN, zoom)),
&Rectangle::new(Point::ORIGIN, VIEWPORT),
&renderer,
)
};
for (what, at) in [
("the anchor core", CABLE_ANCHOR_AT),
("the wrap", wrap_probe()),
("the cable's run", run_probe()),
] {
assert_eq!(
ask(at),
mouse::Interaction::Grab,
"{what} must offer a grab at zoom {zoom}",
);
}
}
}
#[test]
fn hovering_an_anchor_asks_for_a_frame() {
let renderer = Recorder::detached();
let (mut graph, mut tree, layout_node) = graph_with_cable(&renderer, true, Point::ORIGIN, 1.0);
let layout = Layout::with_offset(Vector::ZERO, &layout_node);
let move_to = |graph: &mut Graph, tree: &mut Tree, to: Point| {
drive(
graph,
tree,
layout,
&iced::Event::Mouse(mouse::Event::CursorMoved { position: to }),
mouse::Cursor::Available(to),
&renderer,
)
};
let onto = move_to(&mut graph, &mut tree, CABLE_ANCHOR_AT);
let leaving = move_to(&mut graph, &mut tree, empty_probe());
let clear = move_to(&mut graph, &mut tree, Point::new(360.0, 360.0));
assert_ne!(
onto,
window::RedrawRequest::Wait,
"moving onto the core must ask for the frame its feedback draws in",
);
assert_ne!(
leaving,
window::RedrawRequest::Wait,
"moving off the core must ask for the frame that clears it",
);
assert_eq!(
clear,
window::RedrawRequest::Wait,
"moving over bare canvas must leave the graph idle",
);
}