1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
/// Layout-to-Graphics同期システムのテスト
///
/// このテストは、レイアウト計算結果が正しくグラフィックスコンポーネントに
/// 伝播することを検証します。
use bevy_ecs::prelude::*;
use bevy_ecs::system::IntoSystem;
use wintf::ecs::*;
use wintf::ecs::{Point, SizeI};
// sync_visual_from_layout_rootは廃止され、Arrangementから直接Surfaceサイズを取得するようになりました。
// Visual.sizeは削除され、Arrangement.sizeがSingle Source of Truthです。
#[test]
fn test_sync_window_pos() {
use bevy_ecs::schedule::Schedule;
// WorldとScheduleを準備
let mut world = World::new();
world.insert_resource(wintf::ecs::world::FrameCount(1)); // window_pos_sync_systemに必要
let mut schedule = Schedule::default();
schedule.add_systems(wintf::ecs::window_pos_sync_system);
// Window、Visual、WindowPosを持つエンティティを作成
// Note: Visual::on_addがArrangement::default()を自動挿入し、
// Arrangement::on_addがGlobalArrangement::default()を自動挿入する
let entity = world
.spawn((Window::default(), Visual::default(), WindowPos::default()))
.id();
// Commands適用のためにflush
world.flush();
// 自動挿入されたArrangementとGlobalArrangementを上書き
world.entity_mut(entity).insert((
layout::Arrangement {
offset: layout::Offset { x: 100.0, y: 50.0 },
scale: layout::LayoutScale::default(),
size: layout::Size {
width: 800.0,
height: 600.0,
},
},
layout::GlobalArrangement {
transform: windows_numerics::Matrix3x2::identity(),
bounds: layout::D2DRect {
left: 100.0,
top: 50.0,
right: 900.0,
bottom: 650.0,
},
},
));
// スケジュールを実行(GlobalArrangementがChangedとして検出される)
schedule.run(&mut world);
// WindowPosが更新されていることを確認
let window_pos = world.entity(entity).get::<WindowPos>().unwrap();
assert_eq!(window_pos.position, Some(Point { x: 100, y: 50 }));
assert_eq!(
window_pos.size,
Some(SizeI {
width: 800,
height: 600
})
);
}
#[test]
fn test_is_self_initiated_default_false() {
// TLS フラグの初期値は false であること
assert!(
!wintf::ecs::is_self_initiated(),
"is_self_initiated() の初期値は false であるべき"
);
}
#[test]
fn test_is_self_initiated_flag_lifecycle() {
// guarded_set_window_pos 呼び出し外では false
assert!(!wintf::ecs::is_self_initiated());
// 注: 実際の guarded_set_window_pos は有効な HWND が必要なため、
// ここでは TLS フラグの直接操作はテストできない。
// ラッパー関数は E2E テスト(taffy_flex_demo)で検証する。
// ここでは初期状態の確認のみ行う。
assert!(!wintf::ecs::is_self_initiated());
}
#[test]
fn test_skip_invalid_bounds() {
// WorldとScheduleを準備
let mut world = World::new();
// 無効なbounds (0,0,0,0)を持つエンティティを作成
let entity = world
.spawn((
Window::default(),
layout::LayoutRoot,
layout::GlobalArrangement {
transform: windows_numerics::Matrix3x2::identity(),
bounds: layout::D2DRect {
left: 0.0,
top: 0.0,
right: 0.0,
bottom: 0.0,
},
},
Visual::default(),
WindowPos {
position: Some(Point { x: 100, y: 100 }),
size: Some(SizeI {
width: 800,
height: 600,
}),
..Default::default()
},
))
.id();
// window_pos_sync_systemシステムを実行
let mut system = IntoSystem::into_system(wintf::ecs::window_pos_sync_system);
system.initialize(&mut world);
let _ = system.run((), &mut world);
system.apply_deferred(&mut world);
// 無効なboundsの場合、WindowPosは更新されないべき
let window_pos = world.entity(entity).get::<WindowPos>().unwrap();
assert_eq!(window_pos.position, Some(Point { x: 100, y: 100 }));
assert_eq!(
window_pos.size,
Some(SizeI {
width: 800,
height: 600
})
);
}
#[test]
fn test_echo_back_flow() {
use bevy_ecs::schedule::Schedule;
// WorldとScheduleを準備
let mut world = World::new();
world.insert_resource(wintf::ecs::world::FrameCount(1)); // window_pos_sync_systemに必要
let mut schedule = Schedule::default();
schedule.add_systems(wintf::ecs::window_pos_sync_system);
// Window、Visual、WindowPosを持つエンティティを作成
// Note: Visual::on_addがArrangement::default()を自動挿入し、
// Arrangement::on_addがGlobalArrangement::default()を自動挿入する
let entity = world
.spawn((Window::default(), Visual::default(), WindowPos::default()))
.id();
// Commands適用のためにflush
world.flush();
// 自動挿入されたArrangementとGlobalArrangementを上書き
world.entity_mut(entity).insert((
layout::Arrangement {
offset: layout::Offset { x: 100.0, y: 50.0 },
scale: layout::LayoutScale::default(),
size: layout::Size {
width: 800.0,
height: 600.0,
},
},
layout::GlobalArrangement {
transform: windows_numerics::Matrix3x2::identity(),
bounds: layout::D2DRect {
left: 100.0,
top: 50.0,
right: 900.0,
bottom: 650.0,
},
},
));
// スケジュールを実行してWindowPosを更新
schedule.run(&mut world);
// WindowPosが更新されたことを確認
let window_pos = world.entity(entity).get::<WindowPos>().unwrap();
assert_eq!(window_pos.position, Some(Point { x: 100, y: 50 }));
assert_eq!(
window_pos.size,
Some(SizeI {
width: 800,
height: 600
})
);
// TLS フラグ方式: echo 時は bypass_change_detection で値を更新する(Changed 非発火)
// これは WM_WINDOWPOSCHANGED ハンドラが is_self_initiated() == true の場合に行う処理
{
let mut entity_mut = world.entity_mut(entity);
let mut window_pos = entity_mut.get_mut::<WindowPos>().unwrap();
// echo シミュレーション: bypass で同一値を書き込み(Changed 非発火)
let bypass = window_pos.bypass_change_detection();
bypass.position = Some(Point { x: 100, y: 50 });
bypass.size = Some(SizeI {
width: 800,
height: 600,
});
}
// WindowPos が変わっていないことを確認
let window_pos = world.entity(entity).get::<WindowPos>().unwrap();
assert_eq!(window_pos.position, Some(Point { x: 100, y: 50 }));
assert_eq!(
window_pos.size,
Some(SizeI {
width: 800,
height: 600
})
);
}
#[test]
fn test_reverse_flow_simulation() {
// WorldとScheduleを準備
let mut world = World::new();
// Window、WindowPosを持つエンティティを作成
let entity = world
.spawn((
Window::default(),
WindowPos {
position: Some(Point { x: 100, y: 50 }),
size: Some(SizeI {
width: 800,
height: 600,
}),
..Default::default()
},
))
.id();
// ユーザーがウィンドウをリサイズ(WM_WINDOWPOSCHANGEDシミュレーション)
// 新しい値(外部変更)を受信
// TLS フラグ方式: is_self_initiated() == false の場合、DerefMut で更新
let new_position = Point { x: 150, y: 100 };
let new_size = SizeI {
width: 1024,
height: 768,
};
{
// 外部変更時の処理:WindowPosを通常代入で更新(Changed 発火)
let mut entity_mut = world.entity_mut(entity);
let mut window_pos = entity_mut.get_mut::<WindowPos>().unwrap();
window_pos.position = Some(new_position);
window_pos.size = Some(new_size);
}
// WindowPosが更新されたことを確認
let window_pos = world.entity(entity).get::<WindowPos>().unwrap();
assert_eq!(window_pos.position, Some(new_position));
assert_eq!(window_pos.size, Some(new_size));
}
#[test]
fn test_visual_partial_eq_optimization() {
// PartialEqが正しく実装されているかを確認
let visual1 = Visual {
opacity: 0.5,
..Default::default()
};
let visual2 = Visual {
opacity: 0.5,
..Default::default()
};
let visual3 = Visual {
opacity: 0.8,
..Default::default()
};
// 同じ値を持つVisualは等しいべき
assert_eq!(
visual1, visual2,
"同じ値を持つVisualはPartialEqでtrueを返すべき"
);
// 異なる値を持つVisualは等しくないべき
assert_ne!(
visual1, visual3,
"異なる値を持つVisualはPartialEqでfalseを返すべき"
);
// PartialEqは変更検知最適化の基礎となる
// Bevy ECSはMut<T>のDrop時にPartialEqで比較し、
// 値が変わっていなければChangedフラグを立てない仕組みを提供する
}