use bevy_ecs::message::Messages;
use bevy_ecs::prelude::*;
use std::time::Instant;
use wintf::ecs::drag::{
DragAccumulatorResource, DragConstraint, DragEndEvent, DragEvent, DragStartEvent,
DragTransition, DraggingState, WindowDragContextResource, WindowDragging, cleanup_drag_state,
dispatch_drag_events,
};
use wintf::ecs::layout::{Arrangement, Offset};
use wintf::ecs::window::{Window, WindowPos};
use wintf::ecs::{PhysicalPoint, Point, SizeI};
fn make_world() -> World {
let mut world = World::new();
world.insert_resource(DragAccumulatorResource::new());
world.insert_resource(WindowDragContextResource::new());
world.init_resource::<Messages<DragStartEvent>>();
world.init_resource::<Messages<DragEvent>>();
world.init_resource::<Messages<DragEndEvent>>();
world
}
fn drain_messages<E: bevy_ecs::message::Message + Clone>(world: &mut World) -> Vec<E> {
let mut reader = world.resource_mut::<Messages<E>>();
reader.drain().collect()
}
#[test]
fn dispatch_started_inserts_dragging_state_and_marker_and_event() {
let mut world = make_world();
let window = world
.spawn((
Window::default(),
WindowPos {
position: Some(Point { x: 100, y: 200 }),
size: Some(SizeI {
width: 800,
height: 600,
}),
..Default::default()
},
))
.id();
let target = world.spawn(ChildOf(window)).id();
world
.resource::<DragAccumulatorResource>()
.set_transition(DragTransition::Started {
entity: target,
start_pos: PhysicalPoint::new(110, 210),
timestamp: Instant::now(),
});
dispatch_drag_events(&mut world);
let ds = world
.get::<DraggingState>(target)
.expect("DraggingState が挿入されるべき");
assert_eq!((ds.drag_start_pos.x, ds.drag_start_pos.y), (110, 210));
assert_eq!(ds.initial_inset, (100.0, 200.0));
assert!(
world.get::<WindowDragging>(window).is_some(),
"Window 祖先に WindowDragging が付与されるべき"
);
let events = drain_messages::<DragStartEvent>(&mut world);
assert_eq!(events.len(), 1, "DragStartEvent は 1 件書き込まれるべき");
assert_eq!(events[0].target, target);
assert_eq!((events[0].position.x, events[0].position.y), (110, 210));
assert!(events[0].is_primary);
}
#[test]
fn dispatch_started_writes_drag_context_resource() {
use wintf::ecs::drag::DragConfig;
let mut world = make_world();
let window = world
.spawn((
Window::default(),
WindowPos {
position: Some(Point { x: 0, y: 0 }),
size: Some(SizeI {
width: 640,
height: 480,
}),
..Default::default()
},
))
.id();
let constraint = DragConstraint {
min_x: Some(-50),
max_x: Some(1000),
min_y: None,
max_y: None,
};
let target = world
.spawn((
ChildOf(window),
DragConfig {
move_window: true,
..Default::default()
},
constraint,
))
.id();
world
.resource::<DragAccumulatorResource>()
.set_transition(DragTransition::Started {
entity: target,
start_pos: PhysicalPoint::new(5, 5),
timestamp: Instant::now(),
});
dispatch_drag_events(&mut world);
let ctx = world
.resource::<WindowDragContextResource>()
.get()
.expect("context が読めるべき");
assert!(
ctx.move_window,
"DragConfig.move_window=true が転送されるべき"
);
let c = ctx.constraint.expect("DragConstraint が転送されるべき");
assert_eq!(c.min_x, Some(-50));
assert_eq!(c.max_x, Some(1000));
}
#[test]
fn dispatch_ended_removes_state_marker_and_syncs_offset() {
let mut world = make_world();
let window = world
.spawn((
Window::default(),
WindowPos {
position: Some(Point { x: 300, y: 400 }),
size: Some(SizeI {
width: 800,
height: 600,
}),
..Default::default()
},
Arrangement {
offset: Offset { x: 0.0, y: 0.0 },
..Default::default()
},
))
.id();
let target = world.spawn(ChildOf(window)).id();
world
.resource::<DragAccumulatorResource>()
.set_transition(DragTransition::Started {
entity: target,
start_pos: PhysicalPoint::new(310, 410),
timestamp: Instant::now(),
});
dispatch_drag_events(&mut world);
assert!(world.get::<DraggingState>(target).is_some());
assert!(world.get::<WindowDragging>(window).is_some());
world
.resource::<DragAccumulatorResource>()
.set_transition(DragTransition::Ended {
entity: target,
end_pos: PhysicalPoint::new(350, 450),
cancelled: false,
});
dispatch_drag_events(&mut world);
assert!(
world.get::<DraggingState>(target).is_none(),
"DraggingState は Ended で削除されるべき"
);
assert!(
world.get::<WindowDragging>(window).is_none(),
"WindowDragging は Ended で除去されるべき"
);
let arr = world.get::<Arrangement>(window).unwrap();
assert_eq!(
arr.offset,
Offset { x: 300.0, y: 400.0 },
"Arrangement.offset は WindowPos.position に同期されるべき"
);
let events = drain_messages::<DragEndEvent>(&mut world);
assert_eq!(events.len(), 1);
assert_eq!(events[0].target, target);
assert_eq!((events[0].position.x, events[0].position.y), (350, 450));
assert!(!events[0].cancelled);
}
#[test]
fn dispatch_ended_cancelled_propagates_flag() {
let mut world = make_world();
let window = world
.spawn((
Window::default(),
WindowPos {
position: Some(Point { x: 10, y: 20 }),
size: Some(SizeI {
width: 100,
height: 100,
}),
..Default::default()
},
Arrangement::default(),
))
.id();
let target = world.spawn(ChildOf(window)).id();
world
.resource::<DragAccumulatorResource>()
.set_transition(DragTransition::Started {
entity: target,
start_pos: PhysicalPoint::new(11, 21),
timestamp: Instant::now(),
});
dispatch_drag_events(&mut world);
world
.resource::<DragAccumulatorResource>()
.set_transition(DragTransition::Ended {
entity: target,
end_pos: PhysicalPoint::new(11, 22),
cancelled: true,
});
dispatch_drag_events(&mut world);
let events = drain_messages::<DragEndEvent>(&mut world);
assert_eq!(events.len(), 1);
assert!(
events[0].cancelled,
"cancelled=true が DragEndEvent に伝播するべき"
);
let ctx = world
.resource::<WindowDragContextResource>()
.get()
.expect("context が読めるべき");
assert!(ctx.hwnd.is_none());
assert!(!ctx.move_window);
}
#[derive(Resource, Default)]
struct ReceivedOrder(Vec<&'static str>);
fn record_start(
world: &mut World,
_sender: Entity,
_entity: Entity,
ev: &wintf::ecs::pointer::Phase<DragStartEvent>,
) -> bool {
if let wintf::ecs::pointer::Phase::Bubble(_) = ev {
world.resource_mut::<ReceivedOrder>().0.push("start");
}
false
}
fn record_end(
world: &mut World,
_sender: Entity,
_entity: Entity,
ev: &wintf::ecs::pointer::Phase<DragEndEvent>,
) -> bool {
if let wintf::ecs::pointer::Phase::Bubble(_) = ev {
world.resource_mut::<ReceivedOrder>().0.push("end");
}
false
}
#[test]
fn dispatch_started_and_ended_in_one_flush_delivers_both_in_order() {
use wintf::ecs::drag::{OnDragEnd, OnDragStart};
let mut world = make_world();
world.init_resource::<ReceivedOrder>();
let window = world
.spawn((
Window::default(),
WindowPos {
position: Some(Point { x: 100, y: 200 }),
size: Some(SizeI {
width: 800,
height: 600,
}),
..Default::default()
},
Arrangement::default(),
))
.id();
let target = world
.spawn((
ChildOf(window),
OnDragStart(record_start),
OnDragEnd(record_end),
))
.id();
let acc = world.resource::<DragAccumulatorResource>();
acc.set_transition(DragTransition::Started {
entity: target,
start_pos: PhysicalPoint::new(110, 210),
timestamp: Instant::now(),
});
acc.set_transition(DragTransition::Ended {
entity: target,
end_pos: PhysicalPoint::new(160, 260),
cancelled: false,
});
dispatch_drag_events(&mut world);
let starts = drain_messages::<DragStartEvent>(&mut world);
assert_eq!(starts.len(), 1, "DragStartEvent は 1 件届くべき");
assert_eq!(starts[0].target, target);
let ends = drain_messages::<DragEndEvent>(&mut world);
assert_eq!(ends.len(), 1, "DragEndEvent は 1 件届くべき");
assert_eq!(ends[0].target, target);
assert!(!ends[0].cancelled);
assert_eq!(
world.resource::<ReceivedOrder>().0,
vec!["start", "end"],
"受け手には開始 → 終了の順に届くべき"
);
assert!(
world.get::<DraggingState>(target).is_none(),
"配り終えた時点で DraggingState は外れているべき"
);
assert!(
world.get::<WindowDragging>(window).is_none(),
"配り終えた時点で WindowDragging は外れているべき"
);
}
#[test]
fn dispatch_emits_drag_event_when_delta_nonzero() {
let mut world = make_world();
let target = world
.spawn(DraggingState {
drag_start_pos: PhysicalPoint::new(100, 100),
initial_inset: (0.0, 0.0),
})
.id();
let acc = world.resource::<DragAccumulatorResource>();
acc.set_transition(DragTransition::Started {
entity: target,
start_pos: PhysicalPoint::new(100, 100),
timestamp: Instant::now(),
});
acc.update_position(PhysicalPoint::new(140, 160));
acc.accumulate_delta(PhysicalPoint::new(40, 60));
dispatch_drag_events(&mut world);
let events = drain_messages::<DragEvent>(&mut world);
assert_eq!(
events.len(),
1,
"デルタ非ゼロで DragEvent が 1 件発火するべき"
);
assert_eq!(events[0].target, target);
assert_eq!((events[0].position.x, events[0].position.y), (140, 160));
assert_eq!(
(events[0].start_position.x, events[0].start_position.y),
(100, 100),
"start_position は DraggingState.drag_start_pos から取得されるべき"
);
}
#[test]
fn dispatch_no_drag_event_when_delta_zero() {
let mut world = make_world();
let target = world
.spawn(DraggingState {
drag_start_pos: PhysicalPoint::new(0, 0),
initial_inset: (0.0, 0.0),
})
.id();
let acc = world.resource::<DragAccumulatorResource>();
acc.set_transition(DragTransition::Started {
entity: target,
start_pos: PhysicalPoint::new(0, 0),
timestamp: Instant::now(),
});
acc.update_position(PhysicalPoint::new(0, 0));
dispatch_drag_events(&mut world);
let events = drain_messages::<DragEvent>(&mut world);
assert!(
events.is_empty(),
"デルタゼロでは DragEvent を発火しないべき"
);
}
#[test]
fn dispatch_noop_when_no_transition_and_no_entity() {
let mut world = make_world();
dispatch_drag_events(&mut world);
assert!(drain_messages::<DragStartEvent>(&mut world).is_empty());
assert!(drain_messages::<DragEvent>(&mut world).is_empty());
assert!(drain_messages::<DragEndEvent>(&mut world).is_empty());
}
#[test]
fn cleanup_removes_dragging_state_on_end_event() {
let mut world = make_world();
let target = world
.spawn(DraggingState {
drag_start_pos: PhysicalPoint::new(0, 0),
initial_inset: (0.0, 0.0),
})
.id();
world
.resource_mut::<Messages<DragEndEvent>>()
.write(DragEndEvent {
target,
position: PhysicalPoint::new(1, 1),
cancelled: false,
is_primary: true,
timestamp: Instant::now(),
});
let mut schedule = Schedule::default();
schedule.add_systems(cleanup_drag_state);
schedule.run(&mut world);
assert!(
world.get::<DraggingState>(target).is_none(),
"DragEndEvent 受信で DraggingState が削除されるべき"
);
}
#[test]
fn cleanup_noop_without_end_event() {
let mut world = make_world();
let target = world
.spawn(DraggingState {
drag_start_pos: PhysicalPoint::new(0, 0),
initial_inset: (0.0, 0.0),
})
.id();
let mut schedule = Schedule::default();
schedule.add_systems(cleanup_drag_state);
schedule.run(&mut world);
assert!(
world.get::<DraggingState>(target).is_some(),
"イベントなしでは DraggingState は残るべき"
);
}