use std::cell::Cell;
use std::rc::Rc;
use iced::advanced::widget::{Tree, Widget};
use iced::advanced::{Layout, layout, mouse, renderer};
use iced::{Color, Element, Length, Point, Rectangle, Size, Theme, Vector};
use iced_wgpu::core::clipboard;
use iced_nodegraph::{Indexed, NodeGraph, node};
mod common;
use common::record::Recorder;
#[derive(Clone)]
struct Capture(Rc<Cell<Option<Rectangle>>>);
struct ViewportRecorder {
on_draw: Capture,
on_update: Capture,
on_clip: Capture,
}
impl<Message> Widget<Message, Theme, Recorder> for ViewportRecorder {
fn size(&self) -> Size<Length> {
Size::new(Length::Fixed(40.0), Length::Fixed(20.0))
}
fn layout(
&mut self,
_tree: &mut Tree,
_renderer: &Recorder,
limits: &layout::Limits,
) -> layout::Node {
layout::Node::new(limits.resolve(Length::Fixed(40.0), Length::Fixed(20.0), Size::ZERO))
}
fn draw(
&self,
_tree: &Tree,
renderer: &mut Recorder,
_theme: &Theme,
_style: &renderer::Style,
_layout: Layout<'_>,
_cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.on_draw.0.set(Some(*viewport));
self.on_clip.0.set(renderer.clip());
}
fn update(
&mut self,
_tree: &mut Tree,
_event: &iced::Event,
_layout: Layout<'_>,
_cursor: mouse::Cursor,
_renderer: &Recorder,
_clipboard: &mut dyn clipboard::Clipboard,
_shell: &mut iced_wgpu::core::Shell<'_, Message>,
viewport: &Rectangle,
) {
self.on_update.0.set(Some(*viewport));
}
}
impl<'a, Message: 'a> From<ViewportRecorder> for Element<'a, Message, Theme, Recorder> {
fn from(value: ViewportRecorder) -> Self {
Element::new(value)
}
}
type Graph = NodeGraph<'static, Indexed, (), Theme, Recorder>;
fn build_graph_with_recorder(
graph_w: f32,
graph_h: f32,
node_world_pos: Point,
) -> (
Graph,
Capture, // on_draw
Capture, // on_update
Capture, // innermost clip at the moment the child was drawn
) {
let on_draw = Capture(Rc::new(Cell::new(None)));
let on_update = Capture(Rc::new(Cell::new(None)));
let on_clip = Capture(Rc::new(Cell::new(None)));
let recorder = ViewportRecorder {
on_draw: on_draw.clone(),
on_update: on_update.clone(),
on_clip: on_clip.clone(),
};
let mut graph: Graph = NodeGraph::default()
.width(Length::Fixed(graph_w))
.height(Length::Fixed(graph_h));
graph = graph.push_node(node(0_usize, node_world_pos, Element::from(recorder)));
(graph, on_draw, on_update, on_clip)
}
#[test]
fn draw_clips_child_viewport_to_graph_bounds() {
let (mut graph, on_draw, _on_update, _on_clip) =
build_graph_with_recorder(200.0, 200.0, Point::new(500.0, 500.0));
let mut tree = Tree::new(&graph as &dyn Widget<(), Theme, Recorder>);
let mut renderer = Recorder::detached();
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 outer_viewport = Rectangle::new(Point::ORIGIN, Size::new(1024.0, 768.0));
graph.draw(
&tree,
&mut renderer,
&Theme::Light,
&renderer::Style {
text_color: Color::BLACK,
},
layout,
mouse::Cursor::Unavailable,
&outer_viewport,
);
let recorded = on_draw
.0
.get()
.expect("recorder was never drawn — NodeGraph::draw did not reach child");
assert!(
recorded.width <= 200.0 && recorded.height <= 200.0,
"child draw viewport {recorded:?} should be clipped to NodeGraph bounds (200x200)",
);
}
#[test]
fn draw_clips_node_content_layer_to_graph_bounds() {
let (mut graph, _on_draw, _on_update, on_clip) =
build_graph_with_recorder(200.0, 200.0, Point::new(180.0, 50.0));
let mut tree = Tree::new(&graph as &dyn Widget<(), Theme, Recorder>);
let mut renderer = Recorder::detached();
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 outer_viewport = Rectangle::new(Point::ORIGIN, Size::new(1024.0, 768.0));
graph.draw(
&tree,
&mut renderer,
&Theme::Light,
&renderer::Style {
text_color: Color::BLACK,
},
layout,
mouse::Cursor::Unavailable,
&outer_viewport,
);
let clip = on_clip
.0
.get()
.expect("node content was never drawn under a clip layer");
assert!(
clip.x + clip.width <= 200.5,
"node content clip {clip:?} must not extend past graph right edge (200)",
);
}
fn wheel_event() -> iced::Event {
iced::Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Lines { x: 0.0, y: 5.0 },
})
}
fn run_update_with_cursor(graph_w: f32, graph_h: f32, cursor: mouse::Cursor) -> Rc<Cell<bool>> {
let mut base_graph: Graph = NodeGraph::default()
.width(Length::Fixed(graph_w))
.height(Length::Fixed(graph_h));
base_graph = base_graph.push_node(node(
0_usize,
Point::new(0.0, 0.0),
Element::<(), _, _>::from(EmptyLeaf),
));
let camera_changed = Rc::new(Cell::new(false));
let cc = camera_changed.clone();
let mut graph = base_graph.on_camera(move |_pos, _zoom| cc.set(true));
let mut tree = Tree::new(&graph as &dyn Widget<(), Theme, Recorder>);
let renderer = Recorder::detached();
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 mut messages: Vec<()> = Vec::new();
let mut shell = iced_wgpu::core::Shell::new(&mut messages);
let mut clipboard = clipboard::Null;
let viewport = Rectangle::new(Point::ORIGIN, Size::new(1024.0, 768.0));
graph.update(
&mut tree,
&wheel_event(),
layout,
cursor,
&renderer,
&mut clipboard,
&mut shell,
&viewport,
);
camera_changed
}
struct EmptyLeaf;
impl<Message> Widget<Message, Theme, Recorder> for EmptyLeaf {
fn size(&self) -> Size<Length> {
Size::new(Length::Fixed(10.0), Length::Fixed(10.0))
}
fn layout(&mut self, _: &mut Tree, _: &Recorder, limits: &layout::Limits) -> layout::Node {
layout::Node::new(limits.resolve(Length::Fixed(10.0), Length::Fixed(10.0), Size::ZERO))
}
fn draw(
&self,
_: &Tree,
_: &mut Recorder,
_: &Theme,
_: &renderer::Style,
_: Layout<'_>,
_: mouse::Cursor,
_: &Rectangle,
) {
}
}
impl<'a, Message: 'a> From<EmptyLeaf> for Element<'a, Message, Theme, Recorder> {
fn from(w: EmptyLeaf) -> Self {
Element::new(w)
}
}
#[test]
fn wheel_scroll_with_levitating_cursor_does_not_zoom() {
let changed = run_update_with_cursor(
200.0,
200.0,
mouse::Cursor::Levitating(Point::new(100.0, 100.0)),
);
assert!(
!changed.get(),
"wheel scroll under a levitating cursor must not change the camera",
);
}
#[test]
fn wheel_scroll_outside_graph_bounds_does_not_zoom() {
let changed = run_update_with_cursor(
200.0,
200.0,
mouse::Cursor::Available(Point::new(500.0, 500.0)),
);
assert!(
!changed.get(),
"wheel scroll outside the graph bounds must not change the camera",
);
}
#[test]
fn wheel_scroll_inside_graph_bounds_zooms() {
let changed = run_update_with_cursor(
200.0,
200.0,
mouse::Cursor::Available(Point::new(100.0, 100.0)),
);
assert!(
changed.get(),
"wheel scroll over the graph must still change the camera",
);
}
#[test]
fn update_clips_child_viewport_to_graph_bounds() {
let (mut graph, _on_draw, on_update, _on_clip) =
build_graph_with_recorder(200.0, 200.0, Point::new(500.0, 500.0));
let mut tree = Tree::new(&graph as &dyn Widget<(), Theme, Recorder>);
let renderer = Recorder::detached();
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 outer_viewport = Rectangle::new(Point::ORIGIN, Size::new(1024.0, 768.0));
let mut shell_messages: Vec<()> = Vec::new();
let mut shell = iced_wgpu::core::Shell::new(&mut shell_messages);
let mut clipboard = clipboard::Null;
let event = iced::Event::Mouse(mouse::Event::CursorMoved {
position: Point::new(50.0, 50.0),
});
graph.update(
&mut tree,
&event,
layout,
mouse::Cursor::Available(Point::new(50.0, 50.0)),
&renderer,
&mut clipboard,
&mut shell,
&outer_viewport,
);
let recorded = on_update
.0
.get()
.expect("recorder did not see an update event from NodeGraph");
assert!(
recorded.width <= 200.0 && recorded.height <= 200.0,
"child update viewport {recorded:?} should be clipped to NodeGraph bounds (200x200)",
);
}
fn content_clip_at(widget_origin: Vector, node_world: Point) -> Option<Rectangle> {
let (mut graph, _on_draw, _on_update, on_clip) =
build_graph_with_recorder(200.0, 200.0, node_world);
let mut tree = Tree::new(&graph as &dyn Widget<(), Theme, Recorder>);
let mut renderer = Recorder::detached();
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 outer = Rectangle::new(Point::ORIGIN, Size::new(1024.0, 768.0));
graph.draw(
&tree,
&mut renderer,
&Theme::Light,
&renderer::Style {
text_color: Color::BLACK,
},
layout,
mouse::Cursor::Unavailable,
&outer,
);
on_clip.0.get()
}
#[test]
fn content_clip_at_left_edge_does_not_go_negative() {
let clip = content_clip_at(Vector::ZERO, Point::new(-20.0, 50.0))
.expect("node content was never drawn under a clip layer");
assert!(
clip.x >= -0.5,
"left-straddling content clip {clip:?} must not extend past the graph left edge (0)",
);
}
#[test]
fn content_clip_at_top_edge_does_not_go_negative() {
let clip = content_clip_at(Vector::ZERO, Point::new(50.0, -10.0))
.expect("node content was never drawn under a clip layer");
assert!(
clip.y >= -0.5,
"top-straddling content clip {clip:?} must not extend past the graph top edge (0)",
);
}
#[test]
fn content_clip_fully_inside_is_not_overclipped() {
let clip = content_clip_at(Vector::ZERO, Point::new(50.0, 50.0))
.expect("node content was never drawn under a clip layer");
assert!(
(clip.width - 40.0).abs() < 1.0 && (clip.height - 20.0).abs() < 1.0,
"fully-inside content clip {clip:?} should match the node bounds (40x20)",
);
}
#[test]
fn content_clip_fully_outside_is_empty() {
let clip = content_clip_at(Vector::ZERO, Point::new(500.0, 500.0))
.expect("node content was never drawn under a clip layer");
assert!(
clip.width == 0.0 && clip.height == 0.0,
"fully-outside content clip {clip:?} must have zero area",
);
}
#[test]
fn content_clip_respects_nonzero_widget_origin() {
let clip = content_clip_at(Vector::new(0.0, 100.0), Point::new(50.0, 50.0))
.expect("node content was never drawn under a clip layer");
assert!(
clip.y >= 99.5 && clip.y + clip.height <= 300.5,
"content clip {clip:?} must stay within the origin-offset graph bounds (y 100..300)",
);
}