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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
//! # Monitor - マルチモニター管理
//!
//! このモジュールは、物理モニターのECSコンポーネントとモニター列挙機能を提供します。
//!
//! ## 主要コンポーネント
//!
//! - **`Monitor`**: 物理モニター情報(handle, bounds, DPI, プライマリフラグ)
//!
//! ## 主要関数
//!
//! - **`enumerate_monitors()`**: システムの全モニターを列挙
//!
//! ## 使用例
//!
//! ```rust,ignore
//! use wintf::ecs::monitor::{Monitor, enumerate_monitors};
//!
//! // 全モニターを列挙
//! let monitors = enumerate_monitors();
//! for monitor in monitors {
//! println!("Monitor: {:?}, DPI: {}, Primary: {}",
//! monitor.bounds, monitor.dpi, monitor.is_primary);
//! }
//! ```
use bevy_ecs::lifecycle::HookContext;
use bevy_ecs::prelude::*;
use bevy_ecs::world::DeferredWorld;
use std::fmt;
use tracing::warn;
use windows::Win32::Foundation::{LPARAM, RECT};
use windows::Win32::Graphics::Gdi::{
EnumDisplayMonitors, GetMonitorInfoW, HMONITOR, MONITORINFOEXW,
};
use windows::Win32::UI::HiDpi::{GetDpiForMonitor, MDT_EFFECTIVE_DPI};
use windows_core::BOOL;
/// Monitorコンポーネントが追加されたときに呼ばれるフック
/// Arrangementを自動挿入する(既に存在する場合はスキップ)
fn on_monitor_add(mut world: DeferredWorld, context: HookContext) {
let entity = context.entity;
// Arrangementがまだ存在しない場合のみ挿入
if world
.get::<crate::ecs::layout::Arrangement>(entity)
.is_none()
{
world
.commands()
.entity(entity)
.insert(crate::ecs::layout::Arrangement::default());
}
}
/// モニターコンポーネント
///
/// 物理モニターの情報を保持します。
/// - `handle`: モニターハンドル
/// - `bounds`: モニターの画面座標系での矩形(ピクセル単位)
/// - `work_area`: タスクバーなどを除いた作業領域
/// - `dpi`: モニターのDPI値
/// - `is_primary`: プライマリモニターかどうか
///
/// # ライフタイムイベント
/// - `on_add`: `Arrangement::default()`を自動挿入
/// - これにより`Arrangement`の`on_add`が連鎖的に`GlobalArrangement`と`ArrangementTreeChanged`を挿入
#[derive(Component, Clone)]
#[component(on_add = on_monitor_add)]
pub struct Monitor {
pub handle: isize,
pub bounds: RECT,
pub work_area: RECT,
pub dpi: u32,
pub is_primary: bool,
}
impl fmt::Debug for Monitor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Monitor")
.field("handle", &self.handle)
.field(
"bounds",
&format_args!(
"({},{},{},{})",
self.bounds.left, self.bounds.top, self.bounds.right, self.bounds.bottom
),
)
.field(
"work_area",
&format_args!(
"({},{},{},{})",
self.work_area.left,
self.work_area.top,
self.work_area.right,
self.work_area.bottom
),
)
.field("dpi", &self.dpi)
.field("is_primary", &self.is_primary)
.finish()
}
}
impl PartialEq for Monitor {
fn eq(&self, other: &Self) -> bool {
self.handle == other.handle
}
}
impl Monitor {
/// HMONITORからMonitorを構築
pub fn from_hmonitor(hmonitor: HMONITOR) -> Result<Self, MonitorError> {
unsafe {
// GetMonitorInfoWでモニター情報を取得
let mut monitor_info: MONITORINFOEXW = std::mem::zeroed();
monitor_info.monitorInfo.cbSize = std::mem::size_of::<MONITORINFOEXW>() as u32;
let success =
GetMonitorInfoW(hmonitor, &mut monitor_info.monitorInfo as *mut _ as *mut _);
if !success.as_bool() {
return Err(MonitorError::GetMonitorInfoFailed);
}
// GetDpiForMonitorでDPI取得
let mut dpi_x: u32 = 0;
let mut dpi_y: u32 = 0;
let hr = GetDpiForMonitor(hmonitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y);
if hr.is_err() {
return Err(MonitorError::GetDpiFailed);
}
Ok(Monitor {
handle: hmonitor.0 as isize,
bounds: monitor_info.monitorInfo.rcMonitor,
work_area: monitor_info.monitorInfo.rcWork,
dpi: dpi_x,
is_primary: (monitor_info.monitorInfo.dwFlags & 1) != 0,
})
}
}
/// モニターの物理サイズ(幅、高さ)を取得
pub fn physical_size(&self) -> (f32, f32) {
let width = (self.bounds.right - self.bounds.left) as f32;
let height = (self.bounds.bottom - self.bounds.top) as f32;
(width, height)
}
/// モニターの左上座標を取得
pub fn top_left(&self) -> (f32, f32) {
(self.bounds.left as f32, self.bounds.top as f32)
}
/// **追従対象フィールドの値**が異なるかを判定する(Req 7.2・設計決定 D14)。
///
/// # [`PartialEq`] とは別物である
///
/// [`PartialEq`](`monitor.rs` 本ファイル)は `handle` のみを比較する**同一性**の
/// 意味論で、「同じモニタか」を答える。本メソッドは「値が変わったか」を答える
/// **変化検出**の意味論で、`handle` は**意図的に見ない**(同一モニタ同士を比べる
/// 前提のため)。この 2 つを取り違えると、拡大率変更のように識別子が不変のまま
/// 値だけが変わる表示構成変更で更新分岐が構造的に恒偽となる——実際にそれが
/// 起きていた(診断レポート §2.7 の欠陥 S4)。呼出点はどちらの意味論を要求して
/// いるのかを名前で明示すること。
///
/// # フィールド追加時の漏れ防止
///
/// 本体は構造体分解パターンで書いてある。`Monitor` にフィールドを追加すると
/// パターンが網羅でなくなりコンパイルエラーになる——追随漏れが静かに起きない
/// ことが D14 帰結⑵ の要求である。`handle` を無視することも `handle: _` として
/// 明示的に記述する(省略ではなく宣言)。
///
/// # 例
/// ```
/// # use wintf::ecs::Monitor;
/// # use windows::Win32::Foundation::RECT;
/// let a = Monitor {
/// handle: 1,
/// 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,
/// };
/// let mut b = a.clone();
/// b.dpi = 192;
/// // 同一性では等価(handle が同じ)だが、値は異なる。
/// assert_eq!(a, b);
/// assert!(a.differs_in_value(&b));
/// ```
pub fn differs_in_value(&self, other: &Self) -> bool {
let Monitor {
handle: _,
bounds,
work_area,
dpi,
is_primary,
} = self;
let Monitor {
handle: _,
bounds: other_bounds,
work_area: other_work_area,
dpi: other_dpi,
is_primary: other_is_primary,
} = other;
bounds != other_bounds
|| work_area != other_work_area
|| dpi != other_dpi
|| is_primary != other_is_primary
}
}
/// モニター関連エラー
#[derive(Debug, Clone, Copy)]
pub enum MonitorError {
GetMonitorInfoFailed,
GetDpiFailed,
}
impl fmt::Display for MonitorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MonitorError::GetMonitorInfoFailed => write!(f, "GetMonitorInfoW failed"),
MonitorError::GetDpiFailed => write!(f, "GetDpiForMonitor failed"),
}
}
}
impl std::error::Error for MonitorError {}
/// システムの全モニターを列挙
pub fn enumerate_monitors() -> Vec<Monitor> {
let mut monitors = Vec::new();
// EnumDisplayMonitorsのコールバック関数
extern "system" fn enum_proc(
hmonitor: HMONITOR,
_hdc: windows::Win32::Graphics::Gdi::HDC,
_rect: *mut RECT,
lparam: LPARAM,
) -> BOOL {
let monitors = unsafe { &mut *(lparam.0 as *mut Vec<Monitor>) };
if let Ok(monitor) = Monitor::from_hmonitor(hmonitor) {
monitors.push(monitor);
} else {
warn!(hmonitor = ?hmonitor, "Failed to get monitor info");
}
BOOL(1)
}
let monitors_ptr = &mut monitors as *mut Vec<Monitor> as isize;
let result = unsafe { EnumDisplayMonitors(None, None, Some(enum_proc), LPARAM(monitors_ptr)) };
if !result.as_bool() {
warn!("EnumDisplayMonitors failed");
return Vec::new();
}
monitors
}
#[cfg(test)]
mod tests {
use super::*;
/// テスト用合成 Monitor(実 HMONITOR 不要)。bounds から physical_size/top_left が導出される。
fn make_monitor(handle: isize, left: i32, top: i32, right: i32, bottom: i32) -> Monitor {
Monitor {
handle,
bounds: RECT {
left,
top,
right,
bottom,
},
work_area: RECT {
left,
top,
right: right - 40,
bottom: bottom - 40,
},
dpi: 96,
is_primary: true,
}
}
#[test]
fn test_physical_size_from_bounds() {
// 幅 = right - left, 高さ = bottom - top
let m = make_monitor(1, 100, 200, 1380, 1000);
assert_eq!(m.physical_size(), (1280.0, 800.0));
}
#[test]
fn test_physical_size_with_negative_origin() {
// 左上が負座標(セカンダリモニタが主モニタの左にある等)でも幅/高さは正
let m = make_monitor(2, -1920, 0, 0, 1080);
assert_eq!(m.physical_size(), (1920.0, 1080.0));
}
#[test]
fn test_top_left_from_bounds() {
let m = make_monitor(1, 100, 200, 1380, 1000);
assert_eq!(m.top_left(), (100.0, 200.0));
}
#[test]
fn test_top_left_with_negative_origin() {
let m = make_monitor(2, -1920, -100, 0, 980);
assert_eq!(m.top_left(), (-1920.0, -100.0));
}
#[test]
fn test_partial_eq_compares_handle_only() {
// PartialEq は handle のみで等価判定する(bounds/dpi/is_primary は無視)。
let a = make_monitor(42, 0, 0, 800, 600);
let mut b = make_monitor(42, 100, 100, 1920, 1080); // 異なる bounds
b.dpi = 144; // 異なる dpi
b.is_primary = false; // 異なる primary フラグ
assert_eq!(a, b, "handle が同一なら他フィールドが異なっても等価");
let c = make_monitor(99, 0, 0, 800, 600); // 同一 bounds, 異なる handle
assert_ne!(a, c, "handle が異なれば非等価");
}
/// Req 7.2: 値差分の述語は同一性(`handle`)を**見ない**。
///
/// `handle` だけが違う同値のモニタは「値の変化なし」。これが `PartialEq` との
/// 意味論の分離そのものであり、本メソッドを `handle` 比較へ変異させれば赤になる。
#[test]
fn test_differs_in_value_ignores_handle() {
let a = make_monitor(42, 0, 0, 800, 600);
let b = make_monitor(99, 0, 0, 800, 600); // handle だけ違う
assert!(
!a.differs_in_value(&b),
"handle は追従対象フィールドではない"
);
// 一方 PartialEq(同一性)は別モニタとみなす——2 つの意味論が独立していること。
assert_ne!(a, b);
}
/// Req 7.2: 追従対象フィールド(bounds/work_area/dpi/is_primary)を**網羅**する。
///
/// どれか 1 つでも比較から落とせば、対応する探針が赤になる。
#[test]
fn test_differs_in_value_covers_every_tracked_field() {
let base = make_monitor(42, 0, 0, 800, 600);
// 同一値なら差分なし(探針が不動点でないことの土台)。
assert!(
!base.differs_in_value(&base.clone()),
"同一値で差分ありは誤り"
);
let mut bounds_changed = base.clone();
bounds_changed.bounds.right = 1920;
assert!(
base.differs_in_value(&bounds_changed),
"bounds の変化を検出できない"
);
let mut work_area_changed = base.clone();
work_area_changed.work_area.bottom -= 20;
assert!(
base.differs_in_value(&work_area_changed),
"work_area の変化を検出できない"
);
let mut dpi_changed = base.clone();
dpi_changed.dpi = 192;
assert!(
base.differs_in_value(&dpi_changed),
"dpi の変化を検出できない"
);
let mut primary_changed = base.clone();
primary_changed.is_primary = !base.is_primary;
assert!(
base.differs_in_value(&primary_changed),
"is_primary の変化を検出できない"
);
}
/// S4 の核心: 実機セッション②と同型(識別子・bounds 不変/work_area・dpi のみ変化)の
/// 構成で、同一性は「等価」・値差分は「変化あり」と**逆の答え**を返す。
#[test]
fn test_scale_change_is_identical_but_differs_in_value() {
let before = make_monitor(0xABCD, 0, 0, 3840, 2160);
let mut after = before.clone();
after.work_area.bottom -= 36; // タスクバーが物理的に太る
after.dpi = 192; // 125% → 200%
assert_eq!(before, after, "同一性: 同じモニタである");
assert!(
before.differs_in_value(&after),
"値の変化: 追従対象フィールドが動いている"
);
}
#[test]
fn test_debug_format_contains_fields() {
let m = make_monitor(7, 0, 0, 800, 600);
let s = format!("{:?}", m);
assert!(s.contains("Monitor"));
assert!(s.contains("handle"));
// bounds はカスタム整形 "(left,top,right,bottom)"
assert!(
s.contains("(0,0,800,600)"),
"bounds は (l,t,r,b) 形式で整形される: {s}"
);
assert!(s.contains("is_primary"));
}
#[test]
fn test_monitor_error_display() {
assert_eq!(
MonitorError::GetMonitorInfoFailed.to_string(),
"GetMonitorInfoW failed"
);
assert_eq!(
MonitorError::GetDpiFailed.to_string(),
"GetDpiForMonitor failed"
);
}
}