use super::*;
use crate::ecs::Monitor;
use crate::ecs::test_support::capture_under_filter;
use windows::Win32::Foundation::RECT;
fn synthetic_monitor() -> Monitor {
Monitor {
handle: 0x1234_5678,
bounds: RECT {
left: -1920,
top: 0,
right: 0,
bottom: 1200,
},
work_area: RECT {
left: -1920,
top: 0,
right: 0,
bottom: 1160,
},
dpi: 192,
is_primary: false,
}
}
#[test]
fn enumerated_monitor_line_carries_handle_and_work_area() {
let monitor = synthetic_monitor();
let out = capture_under_filter("info,wintf::ecs::layout=debug", || {
log_enumerated_monitor(&monitor)
});
assert!(
out.contains("[initialize_layout_root] Creating Monitor entity"),
"モニタ列挙行が出ていない: {out}"
);
assert!(
out.contains("handle="),
"モニタ識別子フィールド `handle` が無い(要件 1.1): {out}"
);
assert!(
out.contains("work_area="),
"work area フィールド `work_area` が無い(要件 1.1): {out}"
);
}
#[test]
fn enumerated_monitor_work_area_reconstructs_all_four_edges() {
let monitor = synthetic_monitor();
let out = capture_under_filter("info,wintf::ecs::layout=debug", || {
log_enumerated_monitor(&monitor)
});
assert!(
out.contains("work_area=-1920,0,0,1160"),
"work area の 4 成分(l,t,r,b)が復元できない: {out}"
);
assert!(
out.contains("handle=305419896"),
"handle の実値が復元できない: {out}"
);
assert!(
out.contains("bounds_bottom=1200"),
"既存の bounds フィールドが失われている: {out}"
);
}
use crate::ecs::types::{Point, SizeI};
use crate::ecs::{DPI, WindowPos};
#[derive(Resource, Clone)]
struct InjectedMonitors(Vec<Monitor>);
#[derive(Resource, Default)]
struct RedriveCount(usize);
fn apply_injected_monitors(
mut commands: Commands,
layout_root: Query<Entity, With<LayoutRoot>>,
mut existing_monitors: Query<(Entity, &mut Monitor), With<Monitor>>,
mut windows: Query<(Entity, &WindowPos, &mut DPI)>,
mut taffy_res: ResMut<TaffyLayoutResource>,
injected: Res<InjectedMonitors>,
mut redriven: ResMut<RedriveCount>,
) {
let root_entity = layout_root
.single()
.expect("檻の LayoutRoot が単一で存在する");
redriven.0 = apply_monitor_snapshot(
&mut commands,
root_entity,
&mut existing_monitors,
&mut windows,
&mut taffy_res,
injected.0.clone(),
crate::ecs::window::transition_diag::Stamp { frame: 0, t_us: 0 },
);
}
fn redrive_count(world: &World) -> usize {
world.resource::<RedriveCount>().0
}
fn probe_monitor_before() -> Monitor {
Monitor {
handle: 0x0000_ABCD,
bounds: RECT {
left: 0,
top: 0,
right: 3840,
bottom: 2160,
},
work_area: RECT {
left: 0,
top: 0,
right: 3840,
bottom: 2100,
},
dpi: 120,
is_primary: true,
}
}
fn probe_monitor_after() -> Monitor {
Monitor {
work_area: RECT {
left: 0,
top: 0,
right: 3840,
bottom: 2064,
},
dpi: 192,
..probe_monitor_before()
}
}
fn assert_probe_is_not_a_fixed_point(before: &Monitor, after: &Monitor) {
assert_eq!(
before.handle, after.handle,
"探針の前提が壊れている: 識別子は不変でなければならない"
);
assert_ne!(
before.work_area.bottom, after.work_area.bottom,
"探針が不動点: work area が実際に動いていない"
);
assert_ne!(
before.dpi, after.dpi,
"探針が不動点: dpi が実際に動いていない"
);
assert_eq!(
*before, *after,
"探針が S4 の穴を踏んでいない: PartialEq(同一性)では等価に見えなければならない"
);
}
fn probe_world(injected: Vec<Monitor>) -> (World, Entity) {
let mut world = World::new();
world.insert_resource(TaffyLayoutResource::default());
world.insert_resource(InjectedMonitors(injected));
world.insert_resource(RedriveCount::default());
world.spawn(LayoutRoot);
let monitor_entity = world.spawn(probe_monitor_before()).id();
(world, monitor_entity)
}
fn run_apply(world: &mut World) {
let mut schedule = Schedule::default();
schedule.set_executor(bevy_ecs::schedule::SingleThreadedExecutor::new());
schedule.add_systems(apply_injected_monitors);
schedule.run(world);
}
fn spawn_probe_window(world: &mut World, x: i32, y: i32, dpi: u32) -> Entity {
world
.spawn((
WindowPos {
position: Some(Point { x, y }),
size: Some(SizeI {
width: 400,
height: 600,
}),
..Default::default()
},
DPI::from_dpi(dpi as u16, dpi as u16),
))
.id()
}
#[test]
fn s4_red_monitor_table_updates_when_only_values_change() {
let before = probe_monitor_before();
let after = probe_monitor_after();
assert_probe_is_not_a_fixed_point(&before, &after);
let (mut world, monitor_entity) = probe_world(vec![after.clone()]);
run_apply(&mut world);
let stored = world
.get::<Monitor>(monitor_entity)
.expect("Monitor エンティティが生存している")
.clone();
assert_eq!(
stored.work_area.bottom, 2064,
"work area が起動時の値のまま凍結している(S4): {stored:?}"
);
assert_eq!(
stored.dpi, 192,
"dpi が起動時の値のまま凍結している(S4): {stored:?}"
);
assert_eq!(stored.handle, before.handle, "識別子は不変であること");
assert_eq!(
world.query::<&Monitor>().iter(&world).count(),
1,
"Monitor エンティティが増減している"
);
}
#[test]
fn s4_red_window_dpi_redriven_without_wm_dpichanged() {
assert_probe_is_not_a_fixed_point(&probe_monitor_before(), &probe_monitor_after());
let (mut world, _monitor_entity) = probe_world(vec![probe_monitor_after()]);
let window = spawn_probe_window(&mut world, 1000, 500, 120);
run_apply(&mut world);
let dpi = *world.get::<DPI>(window).expect("窓の DPI が生存している");
assert_eq!(
dpi,
DPI::from_dpi(192, 192),
"モニタ表が更新されても窓 DPI が再導出されない(S4・Req 7.3): {dpi:?}"
);
}
#[test]
fn value_only_change_updates_monitor_and_reports_old_and_new() {
assert_probe_is_not_a_fixed_point(&probe_monitor_before(), &probe_monitor_after());
let (mut world, monitor_entity) = probe_world(vec![probe_monitor_after()]);
let out = capture_under_filter("info,wintf::ecs::layout=debug", || run_apply(&mut world));
let stored = world
.get::<Monitor>(monitor_entity)
.expect("Monitor エンティティが生存している")
.clone();
assert_eq!(
stored.work_area.bottom, 2064,
"work area が更新されていない"
);
assert_eq!(stored.dpi, 192, "dpi が更新されていない");
assert!(
out.contains("[detect_display_change_system] Updating Monitor entity"),
"更新のログが出ていない: {out}"
);
assert!(
out.contains("old_dpi=120") && out.contains("new_dpi=192"),
"何が変わったか(新旧 dpi)がログから読めない: {out}"
);
assert!(
out.contains("old_work_area=0,0,3840,2100") && out.contains("new_work_area=0,0,3840,2064"),
"何が変わったか(新旧 work area)がログから読めない: {out}"
);
}
#[test]
fn updated_monitor_redrives_window_dpi_and_reports_it() {
let (mut world, _) = probe_world(vec![probe_monitor_after()]);
let window = spawn_probe_window(&mut world, 1000, 500, 120);
let out = capture_under_filter("info,wintf::ecs::layout=debug", || run_apply(&mut world));
assert_eq!(
*world.get::<DPI>(window).expect("窓の DPI"),
DPI::from_dpi(192, 192),
"窓 DPI が再導出されていない"
);
assert!(
out.contains("Redriving window DPI from updated Monitor"),
"再導出の観測点が出ていない: {out}"
);
assert!(
out.contains("old_dpi_x=120") && out.contains("new_dpi_x=192"),
"再導出の新旧 DPI がログから読めない: {out}"
);
assert_eq!(redrive_count(&world), 1, "駆動した窓の数が 1 でない");
}
#[test]
fn identical_snapshot_updates_nothing() {
let (mut world, _) = probe_world(vec![probe_monitor_before()]);
let window = spawn_probe_window(&mut world, 1000, 500, 120);
let out = capture_under_filter("info,wintf::ecs::layout=debug", || run_apply(&mut world));
assert!(
!out.contains("[detect_display_change_system] Updating Monitor entity"),
"値が同一なのに更新が走っている: {out}"
);
assert!(
!out.contains("Redriving window DPI"),
"値が同一なのに窓 DPI の再導出が走っている: {out}"
);
assert_eq!(
*world.get::<DPI>(window).expect("窓の DPI"),
DPI::from_dpi(120, 120),
"窓 DPI が不用意に書き換わっている"
);
assert_eq!(
redrive_count(&world),
0,
"値が同一なのに再導出が駆動されている"
);
}
#[test]
fn window_outside_updated_monitor_is_not_redriven() {
let (mut world, _) = probe_world(vec![probe_monitor_after()]);
let outside = spawn_probe_window(&mut world, -1880, 500, 96);
let inside = spawn_probe_window(&mut world, 1000, 500, 120);
run_apply(&mut world);
assert_eq!(
*world.get::<DPI>(outside).expect("窓の DPI"),
DPI::from_dpi(96, 96),
"更新モニタ外の窓が書き換わっている"
);
assert_eq!(
*world.get::<DPI>(inside).expect("窓の DPI"),
DPI::from_dpi(192, 192),
"更新モニタ内の窓が書き換わっていない(対照が効いていない証拠)"
);
}
#[test]
fn window_with_cw_usedefault_is_skipped_before_overflow() {
let (mut world, _) = probe_world(vec![probe_monitor_after()]);
let window = world.spawn((WindowPos::default(), DPI::default())).id();
let default_pos = WindowPos::default();
assert_eq!(
default_pos.position.expect("既定の position").x,
CW_USEDEFAULT,
"WindowPos::default() が CW_USEDEFAULT でない=本檻の前提が崩れている"
);
let out = capture_under_filter("info,wintf::ecs::layout=debug", || run_apply(&mut world));
assert_eq!(
*world.get::<DPI>(window).expect("窓の DPI"),
DPI::default(),
"座標未確定(CW_USEDEFAULT)の窓が書き換わっている"
);
assert_eq!(
redrive_count(&world),
0,
"座標未確定の窓を駆動対象に数えている"
);
assert!(
out.contains("Window position/size undetermined, DPI redrive skipped"),
"CW_USEDEFAULT が「未確定」として打ち切られていない: {out}"
);
}
#[test]
fn window_without_position_is_skipped_at_debug_level() {
let (mut world, _) = probe_world(vec![probe_monitor_after()]);
let window = world
.spawn((
WindowPos {
position: None,
size: None,
..Default::default()
},
DPI::from_dpi(120, 120),
))
.id();
let out = capture_under_filter("info,wintf::ecs::layout=debug", || run_apply(&mut world));
assert_eq!(
*world.get::<DPI>(window).expect("窓の DPI"),
DPI::from_dpi(120, 120),
"帰属不明の窓が書き換わっている"
);
assert!(
out.contains("Window position/size undetermined, DPI redrive skipped"),
"打ち切りが観測できない: {out}"
);
}
#[test]
fn window_center_requires_both_position_and_size() {
let full = WindowPos {
position: Some(Point { x: 100, y: 200 }),
size: Some(SizeI {
width: 400,
height: 600,
}),
..Default::default()
};
assert_eq!(window_center(&full), Some((300, 500)));
let no_size = WindowPos { size: None, ..full };
assert_eq!(window_center(&no_size), None);
let no_pos = WindowPos {
position: None,
..full
};
assert_eq!(window_center(&no_pos), None);
}
#[test]
fn window_center_treats_cw_usedefault_as_undetermined() {
assert_eq!(window_center(&WindowPos::default()), None);
let pos_sentinel = WindowPos {
position: Some(Point {
x: CW_USEDEFAULT,
y: CW_USEDEFAULT,
}),
size: Some(SizeI {
width: 400,
height: 600,
}),
..Default::default()
};
assert_eq!(window_center(&pos_sentinel), None);
let size_sentinel = WindowPos {
position: Some(Point { x: 100, y: 200 }),
size: Some(SizeI {
width: CW_USEDEFAULT,
height: CW_USEDEFAULT,
}),
..Default::default()
};
assert_eq!(window_center(&size_sentinel), None);
assert_eq!(CW_USEDEFAULT, i32::MIN, "センチネルの実値が変わっている");
}
#[test]
fn monitor_containing_uses_half_open_bounds() {
let m = probe_monitor_before();
let monitors = [m.clone()];
assert!(
monitor_containing(&monitors, (0, 0)).is_some(),
"左上端は含む"
);
assert!(
monitor_containing(&monitors, (3839, 2159)).is_some(),
"右下端の 1px 内側は含む"
);
assert!(
monitor_containing(&monitors, (3840, 1000)).is_none(),
"右端は含まない(半開区間)"
);
assert!(
monitor_containing(&monitors, (1000, 2160)).is_none(),
"下端は含まない(半開区間)"
);
assert!(
monitor_containing(&monitors, (-1, 1000)).is_none(),
"左外は含まない"
);
}