use std::cell::RefCell;
use std::rc::Rc;
use iced::advanced::renderer::Renderer as _;
use iced::advanced::widget::{Tree, Widget};
use iced::advanced::{Layout, layout, mouse, renderer};
use iced::keyboard;
use iced::touch;
use iced::{
Background, Color, Element, Length, Pixels, Point, Rectangle, Size, Theme, Transformation,
Vector,
};
use iced_wgpu::core::clipboard;
use iced_wgpu::core::image;
use iced_wgpu::core::text;
use crate::{NodeGraph, node};
#[derive(Debug, Default, Clone)]
struct Recorded {
quads: Vec<Rectangle>,
primitives: Vec<Rectangle>,
events: Vec<DrawEvent>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum DrawEvent {
Content(Rectangle),
Sdf(Rectangle),
}
struct Rec {
stack: Vec<Transformation>,
out: Rc<RefCell<Recorded>>,
}
impl Rec {
fn new(out: Rc<RefCell<Recorded>>) -> Self {
Self {
stack: vec![Transformation::IDENTITY],
out,
}
}
fn cur(&self) -> Transformation {
*self.stack.last().unwrap()
}
}
impl renderer::Renderer for Rec {
fn start_layer(&mut self, _bounds: Rectangle) {}
fn end_layer(&mut self) {}
fn start_transformation(&mut self, t: Transformation) {
self.stack.push(self.cur() * t);
}
fn end_transformation(&mut self) {
self.stack.pop();
if self.stack.is_empty() {
self.stack.push(Transformation::IDENTITY);
}
}
fn reset(&mut self, _new_bounds: Rectangle) {}
fn fill_quad(&mut self, quad: renderer::Quad, _background: impl Into<Background>) {
let abs = quad.bounds * self.cur();
let mut out = self.out.borrow_mut();
out.quads.push(abs);
out.events.push(DrawEvent::Content(abs));
}
fn allocate_image(
&mut self,
_handle: &image::Handle,
_callback: impl FnOnce(Result<image::Allocation, image::Error>) + Send + 'static,
) {
}
}
impl text::Renderer for Rec {
type Font = iced::Font;
type Paragraph = iced_wgpu::graphics::text::Paragraph;
type Editor = iced_wgpu::graphics::text::Editor;
const ICON_FONT: iced::Font = iced::Font::DEFAULT;
const CHECKMARK_ICON: char = '0';
const ARROW_DOWN_ICON: char = '0';
const SCROLL_UP_ICON: char = '0';
const SCROLL_DOWN_ICON: char = '0';
const SCROLL_LEFT_ICON: char = '0';
const SCROLL_RIGHT_ICON: char = '0';
const ICED_LOGO: char = '0';
fn default_font(&self) -> Self::Font {
iced::Font::default()
}
fn default_size(&self) -> Pixels {
Pixels(16.0)
}
fn fill_paragraph(&mut self, _: &Self::Paragraph, _: Point, _: Color, _: Rectangle) {}
fn fill_editor(&mut self, _: &Self::Editor, _: Point, _: Color, _: Rectangle) {}
fn fill_text(&mut self, _: text::Text, _: Point, _: Color, _: Rectangle) {}
}
impl iced_wgpu::primitive::Renderer for Rec {
fn draw_primitive(&mut self, bounds: Rectangle, _primitive: impl iced_wgpu::Primitive) {
let abs = bounds * self.cur();
let mut out = self.out.borrow_mut();
out.primitives.push(abs);
out.events.push(DrawEvent::Sdf(abs));
}
}
struct ContentProbe;
impl<Message> Widget<Message, Theme, Rec> for ContentProbe {
fn size(&self) -> Size<Length> {
Size::new(Length::Fixed(40.0), Length::Fixed(20.0))
}
fn layout(&mut self, _: &mut Tree, _: &Rec, limits: &layout::Limits) -> layout::Node {
layout::Node::new(limits.resolve(Length::Fixed(40.0), Length::Fixed(20.0), Size::ZERO))
}
fn draw(
&self,
_: &Tree,
renderer: &mut Rec,
_: &Theme,
_: &renderer::Style,
layout: Layout<'_>,
_: mouse::Cursor,
_: &Rectangle,
) {
renderer.fill_quad(
renderer::Quad {
bounds: layout.bounds(),
border: iced::Border::default(),
shadow: iced::Shadow::default(),
snap: false,
},
Background::Color(Color::BLACK),
);
}
}
impl<'a, Message: 'a> From<ContentProbe> for Element<'a, Message, Theme, Rec> {
fn from(w: ContentProbe) -> Self {
Element::new(w)
}
}
fn draw_at_origin(
widget_origin: Vector,
node_world: Point,
camera_pos: Point,
camera_zoom: f32,
) -> Recorded {
let mut graph: NodeGraph<'static, usize, usize, (), (), Theme, Rec> = NodeGraph::default()
.width(Length::Fixed(400.0))
.height(Length::Fixed(400.0))
.view(camera_pos, camera_zoom);
graph.push_node(node(0_usize, node_world, Element::from(ContentProbe)));
let mut tree = Tree::new(&graph as &dyn Widget<(), Theme, Rec>);
let out = Rc::new(RefCell::new(Recorded::default()));
let mut renderer = Rec::new(out.clone());
let layout_node = graph.layout(
&mut tree,
&renderer,
&layout::Limits::new(Size::ZERO, Size::new(1024.0, 768.0)),
);
let layout = Layout::with_offset(widget_origin, &layout_node);
let viewport = Rectangle::new(Point::ORIGIN, Size::new(1024.0, 768.0));
let mut msgs: Vec<()> = Vec::new();
let mut shell = iced_wgpu::core::Shell::new(&mut msgs);
let mut clipboard = clipboard::Null;
graph.update(
&mut tree,
&iced::Event::Mouse(mouse::Event::CursorMoved {
position: Point::new(-1.0, -1.0),
}),
layout,
mouse::Cursor::Unavailable,
&renderer,
&mut clipboard,
&mut shell,
&viewport,
);
graph.draw(
&tree,
&mut renderer,
&Theme::Dark,
&renderer::Style {
text_color: Color::WHITE,
},
layout,
mouse::Cursor::Unavailable,
&viewport,
);
out.borrow().clone()
}
fn expected_screen(origin: Vector, world: Point, pos: Point, zoom: f32) -> Point {
Point::new(
origin.x + (world.x + pos.x) * zoom,
origin.y + (world.y + pos.y) * zoom,
)
}
fn node_content_quad(rec: &Recorded) -> Rectangle {
rec.quads
.iter()
.copied()
.find(|q| q.width <= 200.0 && q.height <= 200.0)
.expect("node content quad was not recorded")
}
fn node_fill_primitive(rec: &Recorded) -> Rectangle {
rec.primitives
.iter()
.copied()
.find(|p| p.width <= 120.0 && p.height <= 120.0)
.expect("node fill primitive was not recorded")
}
fn click_select(
widget_origin: Vector,
node_world: Point,
camera_pos: Point,
camera_zoom: f32,
screen: Point,
) -> Option<Vec<usize>> {
let selected: Rc<RefCell<Option<Vec<usize>>>> = Rc::new(RefCell::new(None));
let sel = selected.clone();
let mut graph: NodeGraph<'static, usize, usize, (), (), Theme, Rec> = NodeGraph::default()
.width(Length::Fixed(400.0))
.height(Length::Fixed(400.0))
.view(camera_pos, camera_zoom)
.on_select(move |ids| {
*sel.borrow_mut() = Some(ids);
});
graph.push_node(node(0_usize, node_world, Element::from(ContentProbe)));
let mut tree = Tree::new(&graph as &dyn Widget<(), Theme, Rec>);
let out = Rc::new(RefCell::new(Recorded::default()));
let renderer = Rec::new(out);
let layout_node = graph.layout(
&mut tree,
&renderer,
&layout::Limits::new(Size::ZERO, Size::new(1024.0, 768.0)),
);
let layout = Layout::with_offset(widget_origin, &layout_node);
let viewport = Rectangle::new(Point::ORIGIN, Size::new(1024.0, 768.0));
let mut msgs: Vec<()> = Vec::new();
let mut shell = iced_wgpu::core::Shell::new(&mut msgs);
let mut clipboard = clipboard::Null;
let cursor = mouse::Cursor::Available(screen);
for event in [
iced::Event::Mouse(mouse::Event::CursorMoved { position: screen }),
iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
] {
graph.update(
&mut tree,
&event,
layout,
cursor,
&renderer,
&mut clipboard,
&mut shell,
&viewport,
);
}
selected.borrow().clone()
}
#[test]
fn click_hits_node_at_nonzero_origin_zoom1() {
let origin = Vector::new(0.0, 100.0);
let selected = click_select(
origin,
Point::new(30.0, 40.0),
Point::ORIGIN,
1.0,
Point::new(50.0, 150.0),
);
assert_eq!(
selected,
Some(vec![0]),
"click at the node's screen center must select it (origin {origin:?}, zoom 1)",
);
}
#[test]
fn click_hits_node_at_nonzero_origin_zoom2() {
let origin = Vector::new(0.0, 100.0);
let selected = click_select(
origin,
Point::new(30.0, 40.0),
Point::ORIGIN,
2.0,
Point::new(100.0, 200.0),
);
assert_eq!(
selected,
Some(vec![0]),
"click at the node's screen center must select it (origin {origin:?}, zoom 2)",
);
}
const FILL_PAD: f32 = 6.0;
#[test]
fn content_and_fill_correct_at_origin_zoom1() {
let origin = Vector::new(0.0, 100.0);
let world = Point::new(30.0, 40.0);
let rec = draw_at_origin(origin, world, Point::ORIGIN, 1.0);
let expected = expected_screen(origin, world, Point::ORIGIN, 1.0);
let content = node_content_quad(&rec);
let fill = node_fill_primitive(&rec);
assert!(
(content.x - expected.x).abs() < 1.0 && (content.y - expected.y).abs() < 1.0,
"content {content:?} should sit at {expected:?}",
);
assert!(
(fill.x - expected.x).abs() < FILL_PAD && (fill.y - expected.y).abs() < FILL_PAD,
"fill {fill:?} should sit at {expected:?}",
);
}
#[test]
fn content_correct_at_origin_zoom2() {
let origin = Vector::new(0.0, 100.0);
let world = Point::new(30.0, 40.0);
let zoom = 2.0;
let rec = draw_at_origin(origin, world, Point::ORIGIN, zoom);
let expected = expected_screen(origin, world, Point::ORIGIN, zoom);
let content = node_content_quad(&rec);
assert!(
(content.x - expected.x).abs() < 1.0 && (content.y - expected.y).abs() < 1.0,
"content {content:?} should sit at {expected:?} at zoom {zoom} \
with widget origin {origin:?}",
);
}
#[test]
fn fill_correct_at_origin_zoom2() {
let origin = Vector::new(0.0, 100.0);
let world = Point::new(30.0, 40.0);
let zoom = 2.0;
let rec = draw_at_origin(origin, world, Point::ORIGIN, zoom);
let expected = expected_screen(origin, world, Point::ORIGIN, zoom);
let fill = node_fill_primitive(&rec);
assert!(
(fill.x - expected.x).abs() < FILL_PAD && (fill.y - expected.y).abs() < FILL_PAD,
"fill {fill:?} should sit at {expected:?} at zoom {zoom} \
with widget origin {origin:?}",
);
}
#[test]
fn content_and_fill_coincide_at_origin_zoom2() {
let origin = Vector::new(0.0, 100.0);
let world = Point::new(30.0, 40.0);
let rec = draw_at_origin(origin, world, Point::ORIGIN, 2.0);
let content = node_content_quad(&rec);
let fill = node_fill_primitive(&rec);
let dx = (content.x - fill.x).abs();
let dy = (content.y - fill.y).abs();
assert!(
dx < FILL_PAD && dy < FILL_PAD,
"content {content:?} and fill {fill:?} diverge (dx={dx}, dy={dy})",
);
}
fn box_select_primitives(
widget_origin: Vector,
camera_zoom: f32,
p1: Point,
p2: Point,
) -> Vec<Rectangle> {
let mut graph: NodeGraph<'static, usize, usize, (), (), Theme, Rec> = NodeGraph::default()
.width(Length::Fixed(400.0))
.height(Length::Fixed(400.0))
.view(Point::ORIGIN, camera_zoom)
.on_select(|_ids| {});
graph.push_node(node(
0_usize,
Point::new(900.0, 900.0),
Element::from(ContentProbe),
));
let mut tree = Tree::new(&graph as &dyn Widget<(), Theme, Rec>);
let out = Rc::new(RefCell::new(Recorded::default()));
let mut renderer = Rec::new(out.clone());
let layout_node = graph.layout(
&mut tree,
&renderer,
&layout::Limits::new(Size::ZERO, Size::new(1024.0, 768.0)),
);
let layout = Layout::with_offset(widget_origin, &layout_node);
let viewport = Rectangle::new(Point::ORIGIN, Size::new(1024.0, 768.0));
let mut msgs: Vec<()> = Vec::new();
let mut shell = iced_wgpu::core::Shell::new(&mut msgs);
let mut clipboard = clipboard::Null;
let send = |graph: &mut NodeGraph<'static, usize, usize, (), (), Theme, Rec>,
tree: &mut Tree,
shell: &mut iced_wgpu::core::Shell<'_, ()>,
clipboard: &mut clipboard::Null,
renderer: &Rec,
event: iced::Event,
at: Point| {
graph.update(
tree,
&event,
layout,
mouse::Cursor::Available(at),
renderer,
clipboard,
shell,
&viewport,
);
};
send(
&mut graph,
&mut tree,
&mut shell,
&mut clipboard,
&renderer,
iced::Event::Mouse(mouse::Event::CursorMoved { position: p1 }),
p1,
);
send(
&mut graph,
&mut tree,
&mut shell,
&mut clipboard,
&renderer,
iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
p1,
);
send(
&mut graph,
&mut tree,
&mut shell,
&mut clipboard,
&renderer,
iced::Event::Mouse(mouse::Event::CursorMoved { position: p2 }),
p2,
);
graph.draw(
&tree,
&mut renderer,
&Theme::Dark,
&renderer::Style {
text_color: Color::WHITE,
},
layout,
mouse::Cursor::Available(p2),
&viewport,
);
out.borrow().primitives.clone()
}
#[test]
fn box_select_renders_where_dragged_at_nonzero_origin() {
let origin = Vector::new(0.0, 100.0);
let p1 = Point::new(40.0, 160.0);
let p2 = Point::new(120.0, 240.0);
let prims = box_select_primitives(origin, 2.0, p1, p2);
let expect = Rectangle::new(p1, Size::new(p2.x - p1.x, p2.y - p1.y));
let found = prims.iter().any(|r| {
(r.x - expect.x).abs() < 8.0
&& (r.y - expect.y).abs() < 8.0
&& (r.width - expect.width).abs() < 12.0
&& (r.height - expect.height).abs() < 12.0
});
assert!(
found,
"no selection-box primitive near dragged rect {expect:?}; got {prims:?}",
);
}
fn find_node_fill(rec: &Recorded) -> Option<Rectangle> {
rec.primitives
.iter()
.copied()
.find(|p| p.width <= 120.0 && p.height <= 120.0)
}
#[test]
fn node_far_offscreen_culls_sdf() {
let rec = draw_at_origin(Vector::ZERO, Point::new(900.0, 900.0), Point::ORIGIN, 1.0);
assert!(
find_node_fill(&rec).is_none(),
"a node entirely off-screen must not emit a fill primitive: {:?}",
rec.primitives,
);
}
#[test]
fn node_offscreen_negative_culls_sdf() {
let rec = draw_at_origin(Vector::ZERO, Point::new(-200.0, -200.0), Point::ORIGIN, 1.0);
assert!(
find_node_fill(&rec).is_none(),
"a node off the top-left must be culled: {:?}",
rec.primitives,
);
}
#[test]
fn node_onscreen_emits_sdf() {
let rec = draw_at_origin(Vector::ZERO, Point::new(100.0, 100.0), Point::ORIGIN, 1.0);
assert!(
find_node_fill(&rec).is_some(),
"an on-screen node must emit a fill primitive: {:?}",
rec.primitives,
);
}
#[test]
fn node_straddling_right_edge_clips_to_bounds() {
let rec = draw_at_origin(Vector::ZERO, Point::new(380.0, 100.0), Point::ORIGIN, 1.0);
let fill = find_node_fill(&rec).expect("a straddling node still emits a clipped fill");
assert!(
fill.x + fill.width <= 400.5,
"fill clip {fill:?} must not extend past the graph right edge (400)",
);
}
#[test]
fn node_barely_onscreen_not_culled() {
let rec = draw_at_origin(Vector::ZERO, Point::new(399.0, 100.0), Point::ORIGIN, 1.0);
assert!(
find_node_fill(&rec).is_some(),
"a 1px overlap must keep the node's fill: {:?}",
rec.primitives,
);
}
#[test]
fn pan_culls_then_restores_sdf() {
let off = draw_at_origin(
Vector::ZERO,
Point::new(100.0, 100.0),
Point::new(400.0, 0.0),
1.0,
);
assert!(
find_node_fill(&off).is_none(),
"panning the node off-screen must cull its fill: {:?}",
off.primitives,
);
let on = draw_at_origin(Vector::ZERO, Point::new(100.0, 100.0), Point::ORIGIN, 1.0);
assert!(
find_node_fill(&on).is_some(),
"panning the node back on-screen must emit its fill again",
);
}
#[test]
fn culling_holds_under_zoom() {
let on = draw_at_origin(Vector::ZERO, Point::new(250.0, 250.0), Point::ORIGIN, 1.0);
assert!(
find_node_fill(&on).is_some(),
"node at (250,250) must be visible at zoom 1: {:?}",
on.primitives,
);
let off = draw_at_origin(Vector::ZERO, Point::new(250.0, 250.0), Point::ORIGIN, 2.0);
assert!(
find_node_fill(&off).is_none(),
"the same node must cull once zoom pushes it off-screen: {:?}",
off.primitives,
);
}
fn geometry_fingerprint(rec: &Recorded) -> Vec<u32> {
let mut bits = Vec::new();
let mut push = |r: &Rectangle| {
bits.extend_from_slice(&[
r.x.to_bits(),
r.y.to_bits(),
r.width.to_bits(),
r.height.to_bits(),
]);
};
for r in &rec.primitives {
push(r);
}
for r in &rec.quads {
push(r);
}
bits
}
#[test]
fn recipe_hash_is_stable_across_120_frames() {
let mut graph: NodeGraph<'static, usize, usize, (), (), Theme, Rec> = NodeGraph::default()
.width(Length::Fixed(400.0))
.height(Length::Fixed(400.0))
.view(Point::ORIGIN, 1.0);
for (i, p) in [(30.0, 40.0), (140.0, 90.0), (60.0, 220.0)]
.into_iter()
.enumerate()
{
graph.push_node(node(i, Point::new(p.0, p.1), Element::from(ContentProbe)));
}
let mut tree = Tree::new(&graph as &dyn Widget<(), Theme, Rec>);
let layout_node = graph.layout(
&mut tree,
&Rec::new(Rc::new(RefCell::new(Recorded::default()))),
&layout::Limits::new(Size::ZERO, Size::new(1024.0, 768.0)),
);
let layout = Layout::with_offset(Vector::ZERO, &layout_node);
let viewport = Rectangle::new(Point::ORIGIN, Size::new(1024.0, 768.0));
let mut reference: Option<Vec<u32>> = None;
for frame in 0..120 {
let out = Rc::new(RefCell::new(Recorded::default()));
let mut renderer = Rec::new(out.clone());
let mut msgs: Vec<()> = Vec::new();
let mut shell = iced_wgpu::core::Shell::new(&mut msgs);
let mut clipboard = clipboard::Null;
graph.update(
&mut tree,
&iced::Event::Mouse(mouse::Event::CursorMoved {
position: Point::new(-1.0, -1.0),
}),
layout,
mouse::Cursor::Unavailable,
&renderer,
&mut clipboard,
&mut shell,
&viewport,
);
graph.draw(
&tree,
&mut renderer,
&Theme::Dark,
&renderer::Style {
text_color: Color::WHITE,
},
layout,
mouse::Cursor::Unavailable,
&viewport,
);
let fp = geometry_fingerprint(&out.borrow());
assert!(
!fp.is_empty(),
"frame {frame} recorded no geometry; the harness drew nothing",
);
match &reference {
None => reference = Some(fp),
Some(r) => assert!(
*r == fp,
"node geometry recipe changed on frame {frame}: the recipe is \
not hash-stable, so dedup/arena/instancing cannot be trusted",
),
}
}
}
fn content_event_indices(events: &[DrawEvent]) -> Vec<usize> {
events
.iter()
.enumerate()
.filter_map(|(i, e)| match e {
DrawEvent::Content(r) if r.width <= 200.0 && r.height <= 200.0 => Some(i),
_ => None,
})
.collect()
}
#[test]
fn hosted_content_sandwiched_between_sdf_layers() {
let mut graph: NodeGraph<'static, usize, usize, (), (), Theme, Rec> = NodeGraph::default()
.width(Length::Fixed(400.0))
.height(Length::Fixed(400.0))
.view(Point::ORIGIN, 1.0);
graph.push_node(node(
0_usize,
Point::new(40.0, 40.0),
Element::from(ContentProbe),
));
graph.push_node(node(
1_usize,
Point::new(180.0, 180.0),
Element::from(ContentProbe),
));
let mut tree = Tree::new(&graph as &dyn Widget<(), Theme, Rec>);
let out = Rc::new(RefCell::new(Recorded::default()));
let mut renderer = Rec::new(out.clone());
let layout_node = graph.layout(
&mut tree,
&renderer,
&layout::Limits::new(Size::ZERO, Size::new(1024.0, 768.0)),
);
let layout = Layout::with_offset(Vector::ZERO, &layout_node);
let viewport = Rectangle::new(Point::ORIGIN, Size::new(1024.0, 768.0));
let mut msgs: Vec<()> = Vec::new();
let mut shell = iced_wgpu::core::Shell::new(&mut msgs);
let mut clipboard = clipboard::Null;
graph.update(
&mut tree,
&iced::Event::Mouse(mouse::Event::CursorMoved {
position: Point::new(-1.0, -1.0),
}),
layout,
mouse::Cursor::Unavailable,
&renderer,
&mut clipboard,
&mut shell,
&viewport,
);
graph.draw(
&tree,
&mut renderer,
&Theme::Dark,
&renderer::Style {
text_color: Color::WHITE,
},
layout,
mouse::Cursor::Unavailable,
&viewport,
);
let rec = out.borrow();
let content = content_event_indices(&rec.events);
assert_eq!(
content.len(),
2,
"expected one content quad per node, got {}: {:?}",
content.len(),
rec.events,
);
for &i in &content {
assert!(
i > 0 && matches!(rec.events[i - 1], DrawEvent::Sdf(_)),
"node content at event {i} is not preceded by its SDF fill: {:?}",
rec.events,
);
assert!(
i + 1 < rec.events.len() && matches!(rec.events[i + 1], DrawEvent::Sdf(_)),
"node content at event {i} is not followed by its SDF foreground: {:?}",
rec.events,
);
}
assert!(
content[0] + 1 < content[1],
"an SDF layer must separate node 0's content from node 1's: {:?}",
rec.events,
);
}
fn key_press(c: char, code: keyboard::key::Code, modifiers: keyboard::Modifiers) -> iced::Event {
let key = keyboard::Key::Character(c.to_string().into());
iced::Event::Keyboard(keyboard::Event::KeyPressed {
key: key.clone(),
modified_key: key,
physical_key: keyboard::key::Physical::Code(code),
location: keyboard::Location::Standard,
modifiers,
text: None,
repeat: false,
})
}
fn run_events<Msg: 'static>(
mut graph: NodeGraph<'static, usize, usize, (), Msg, Theme, Rec>,
events: &[(iced::Event, mouse::Cursor)],
) -> Vec<Msg> {
graph.push_node(node(
0_usize,
Point::new(10.0, 10.0),
Element::from(ContentProbe),
));
graph.push_node(node(
1_usize,
Point::new(120.0, 10.0),
Element::from(ContentProbe),
));
let mut tree = Tree::new(&graph as &dyn Widget<Msg, Theme, Rec>);
let renderer = Rec::new(Rc::new(RefCell::new(Recorded::default())));
let layout_node = graph.layout(
&mut tree,
&renderer,
&layout::Limits::new(Size::ZERO, Size::new(1024.0, 768.0)),
);
let layout = Layout::new(&layout_node);
let viewport = Rectangle::new(Point::ORIGIN, Size::new(1024.0, 768.0));
let mut msgs: Vec<Msg> = Vec::new();
let mut clipboard = clipboard::Null;
for (event, cursor) in events {
let mut shell = iced_wgpu::core::Shell::new(&mut msgs);
graph.update(
&mut tree,
event,
layout,
*cursor,
&renderer,
&mut clipboard,
&mut shell,
&viewport,
);
}
msgs
}
#[test]
fn default_keymap_select_all_publishes_selection() {
let graph: NodeGraph<'static, usize, usize, (), Vec<usize>, Theme, Rec> = NodeGraph::default()
.width(Length::Fixed(400.0))
.height(Length::Fixed(400.0))
.on_select(|ids| ids);
let msgs = run_events(
graph,
&[(
key_press('a', keyboard::key::Code::KeyA, keyboard::Modifiers::COMMAND),
mouse::Cursor::Unavailable,
)],
);
let mut selected = msgs
.into_iter()
.next()
.expect("select-all published no selection");
selected.sort_unstable();
assert_eq!(selected, vec![0, 1]);
}
#[test]
fn rebound_select_all_moves_to_the_new_combo() {
let keymap = crate::Keymap {
select_all: Some(crate::KeyCombo::command('l')),
..crate::Keymap::default()
};
let graph: NodeGraph<'static, usize, usize, (), Vec<usize>, Theme, Rec> = NodeGraph::default()
.width(Length::Fixed(400.0))
.height(Length::Fixed(400.0))
.keymap(keymap)
.on_select(|ids| ids);
let msgs = run_events(
graph,
&[
(
key_press('a', keyboard::key::Code::KeyA, keyboard::Modifiers::COMMAND),
mouse::Cursor::Unavailable,
),
(
key_press('l', keyboard::key::Code::KeyL, keyboard::Modifiers::COMMAND),
mouse::Cursor::Unavailable,
),
],
);
assert_eq!(msgs.len(), 1, "only the rebound combo may select: {msgs:?}");
let mut selected = msgs.into_iter().next().unwrap();
selected.sort_unstable();
assert_eq!(selected, vec![0, 1]);
}
#[test]
fn keymap_none_disables_all_shortcuts() {
let graph: NodeGraph<'static, usize, usize, (), Vec<usize>, Theme, Rec> = NodeGraph::default()
.width(Length::Fixed(400.0))
.height(Length::Fixed(400.0))
.keymap(crate::Keymap::none())
.on_select(|ids| ids);
let msgs = run_events(
graph,
&[(
key_press('a', keyboard::key::Code::KeyA, keyboard::Modifiers::COMMAND),
mouse::Cursor::Unavailable,
)],
);
assert!(msgs.is_empty(), "disabled keymap still published: {msgs:?}");
}
#[test]
fn rebound_pan_button_commits_a_pan() {
let over = mouse::Cursor::Available(Point::new(200.0, 200.0));
let events = |button: mouse::Button| {
vec![
(
iced::Event::Mouse(mouse::Event::ButtonPressed(button)),
over,
),
(
iced::Event::Mouse(mouse::Event::ButtonReleased(button)),
over,
),
]
};
let default_graph: NodeGraph<'static, usize, usize, (), (Point, f32), Theme, Rec> =
NodeGraph::default()
.width(Length::Fixed(400.0))
.height(Length::Fixed(400.0))
.on_pan(|position, zoom| (position, zoom));
let msgs = run_events(default_graph, &events(mouse::Button::Middle));
assert!(
msgs.is_empty(),
"unbound middle button committed a pan: {msgs:?}"
);
let keymap = crate::Keymap {
pan_button: mouse::Button::Middle,
..crate::Keymap::default()
};
let rebound_graph: NodeGraph<'static, usize, usize, (), (Point, f32), Theme, Rec> =
NodeGraph::default()
.width(Length::Fixed(400.0))
.height(Length::Fixed(400.0))
.keymap(keymap)
.on_pan(|position, zoom| (position, zoom));
let msgs = run_events(rebound_graph, &events(mouse::Button::Middle));
assert_eq!(
msgs.len(),
1,
"rebound pan button must commit exactly one pan: {msgs:?}"
);
}
fn finger_press(id: u64, position: Point) -> (iced::Event, mouse::Cursor) {
(
iced::Event::Touch(touch::Event::FingerPressed {
id: touch::Finger(id),
position,
}),
mouse::Cursor::Unavailable,
)
}
fn finger_move(id: u64, position: Point) -> (iced::Event, mouse::Cursor) {
(
iced::Event::Touch(touch::Event::FingerMoved {
id: touch::Finger(id),
position,
}),
mouse::Cursor::Unavailable,
)
}
fn finger_lift(id: u64, position: Point) -> (iced::Event, mouse::Cursor) {
(
iced::Event::Touch(touch::Event::FingerLifted {
id: touch::Finger(id),
position,
}),
mouse::Cursor::Unavailable,
)
}
#[test]
fn touch_drag_on_empty_space_pans_the_graph() {
let graph: NodeGraph<'static, usize, usize, (), (Point, f32), Theme, Rec> =
NodeGraph::default()
.width(Length::Fixed(400.0))
.height(Length::Fixed(400.0))
.on_pan(|position, zoom| (position, zoom));
let msgs = run_events(
graph,
&[
finger_press(1, Point::new(300.0, 300.0)),
finger_move(1, Point::new(250.0, 300.0)),
finger_lift(1, Point::new(250.0, 300.0)),
],
);
assert_eq!(
msgs.len(),
1,
"touch pan must commit exactly once: {msgs:?}"
);
let (position, zoom) = msgs[0];
assert!(
(position.x + 50.0).abs() < 1e-3 && position.y.abs() < 1e-3,
"touch pan committed the wrong offset: {position:?}",
);
assert!((zoom - 1.0).abs() < 1e-6);
}
#[test]
fn touch_tap_selects_a_node_and_empty_tap_clears() {
let graph: NodeGraph<'static, usize, usize, (), Vec<usize>, Theme, Rec> = NodeGraph::default()
.width(Length::Fixed(400.0))
.height(Length::Fixed(400.0))
.on_select(|ids| ids);
let msgs = run_events(
graph,
&[
finger_press(1, Point::new(30.0, 20.0)),
finger_lift(1, Point::new(30.0, 20.0)),
finger_press(1, Point::new(300.0, 300.0)),
finger_lift(1, Point::new(300.0, 300.0)),
],
);
assert_eq!(
msgs,
vec![vec![0], vec![]],
"tap must select on press and empty tap must clear on lift",
);
}
#[test]
fn two_finger_pinch_zooms_the_camera() {
let graph: NodeGraph<'static, usize, usize, (), (Point, f32), Theme, Rec> =
NodeGraph::default()
.width(Length::Fixed(400.0))
.height(Length::Fixed(400.0))
.on_pan(|position, zoom| (position, zoom));
let msgs = run_events(
graph,
&[
finger_press(1, Point::new(100.0, 200.0)),
finger_press(2, Point::new(300.0, 200.0)),
finger_move(2, Point::new(400.0, 200.0)),
],
);
let (_, zoom) = *msgs.last().expect("pinch published no camera commit");
assert!(
(zoom - 1.5).abs() < 1e-4,
"pinch must scale zoom by the distance ratio, got {zoom}",
);
}
#[test]
fn second_finger_cancels_a_touch_node_drag() {
let graph: NodeGraph<'static, usize, usize, (), &'static str, Theme, Rec> =
NodeGraph::default()
.width(Length::Fixed(400.0))
.height(Length::Fixed(400.0))
.on_move(|_, _| "move")
.on_drag_end(|| "end");
let msgs = run_events(
graph,
&[
finger_press(1, Point::new(30.0, 20.0)),
finger_press(2, Point::new(300.0, 200.0)),
],
);
assert_eq!(
msgs,
vec!["end"],
"second finger must cancel the drag via on_drag_end",
);
}