michiu_ui 0.0.1

The michiu library handles layout and rendering.
Documentation
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
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
#![allow(clippy::pedantic, clippy::restriction, unused_must_use)]

use std::time::Duration;

use michiu_ui::{
    ComposedRenderer, ElementState, EntityId, Modifiers, MouseButton, StateFlag, prelude::*,
};

use windows::{
    Win32::{
        Foundation::*,
        Graphics::Gdi::*,
        System::{
            LibraryLoader::GetModuleHandleW,
            WinRT::{RO_INIT_SINGLETHREADED, RoInitialize},
        },
        UI::{
            Controls::WM_MOUSELEAVE,
            HiDpi::{
                DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, GetDpiForWindow,
                SetProcessDpiAwarenessContext,
            },
            Input::KeyboardAndMouse::{
                ReleaseCapture, SetCapture, TME_LEAVE, TRACKMOUSEEVENT, TrackMouseEvent,
            },
            WindowsAndMessaging::*,
        },
    },
    core::w,
};

/// ウィンドウメッセージ処理時に Context と Renderer を一元管理するためのアプリケーション状態
struct AppState {
    renderer: ComposedRenderer,
    context: Context,
    root_id: EntityId,
    webview_id: EntityId,
}

// Win32 ウィンドウプロシージャ
unsafe extern "system" fn wnd_proc(
    hwnd: HWND,
    msg: u32,
    wparam: WPARAM,
    lparam: LPARAM,
) -> LRESULT {
    // 1. ウィンドウ作成時に AppState の生ポインタを GWLP_USERDATA に退避
    if msg == WM_CREATE {
        let create_struct = lparam.0 as *const CREATESTRUCTW;
        let app_state_ptr = (unsafe { *create_struct }).lpCreateParams as *mut AppState;
        unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, app_state_ptr as isize) };

        return LRESULT(0);
    }

    // GWLP_USERDATA から AppState の参照を取得
    let app_state_ptr = unsafe { GetWindowLongPtrW(hwnd, GWLP_USERDATA) } as *mut AppState;
    if !app_state_ptr.is_null() {
        let app = unsafe { &mut *app_state_ptr };

        match msg {
            WM_ERASEBKGND => {
                // DComp描画時はGDIによる背景自動消去を抑制し、描画競合を完全に回避します
                return LRESULT(1);
            }
            WM_DPICHANGED => {
                // wparam から新しい DPI の値を取得 (96 DPI = 1.0倍)
                let dpi = (wparam.0 & 0xffff) as u32;
                let scale = dpi as f32 / 96.0;

                let mut client_rect = RECT::default();
                let _ = unsafe { GetClientRect(hwnd, &mut client_rect) };
                let width = (client_rect.right - client_rect.left) as u32;
                let height = (client_rect.bottom - client_rect.top) as u32;

                // 新しいスケール因数で wgpu と Taffy レイアウトをリサイズ同期
                app.renderer.resize((width, height), scale);

                let _ = unsafe { InvalidateRect(Some(hwnd), None, false) };
                return LRESULT(0);
            }
            WM_SIZE => {
                let width = (lparam.0 & 0xffff) as u32;
                let height = ((lparam.0 >> 16) & 0xffff) as u32;

                // レンダラーのリサイズとレイアウト物理サイズの更新
                app.renderer
                    .resize((width, height), app.renderer.scale_factor);

                // 再描画要求
                let _ = unsafe { InvalidateRect(Some(hwnd), None, false) };
                return LRESULT(0);
            }
            WM_PAINT => {
                app.context.begin_frame();

                let mut ps = PAINTSTRUCT::default();
                let _hdc = unsafe { BeginPaint(hwnd, &mut ps) };

                // 非同期キャプチャの完了など、バックグラウンドからメインスレッドに
                // 届いたタスク(クロージャ)をこの描画フレーム開始時にすべて安全に実行
                app.context.process_main_thread_tasks();

                // トランジション、およびキーフレームアニメーションを 1 Tick 進める
                app.context
                    .tick_system_frame(&TickType::TransitionAndAnimation);

                // 描画(draw)を行う直前に、必ず Taffy のレイアウトツリーの同期・再計算を実行
                // これにより、クリックによって変化したテキスト要素の「最新の幅」が、
                // リアルタイムに wgpu 側の描画枠(rect)に追従し、文字の縮みを完全に防ぎます。

                app.context
                    .sync_layout(app.root_id, app.renderer.layout_size);
                // 描画直前にDCompツリーおよびWebView2の配置も最新状態に追従させます
                app.renderer.update_composition_tree(&mut app.context);

                // 描画実行
                app.renderer.draw(&mut app.context);

                let _ = unsafe { EndPaint(hwnd, &ps) };

                // アニメーション駆動中の場合は Invalidate を自給自足する
                if app.context.has_active_frame() || app.context.has_dirty() {
                    let _ = unsafe { InvalidateRect(Some(hwnd), None, false) };
                }
                return LRESULT(0);
            }
            WM_NCDESTROY => {
                // ウィンドウ破棄時に生ポインタを安全に Box に戻してメモリ解放
                let app_state_ptr =
                    unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0) } as *mut AppState;
                if !app_state_ptr.is_null() {
                    let _ = unsafe { Box::from_raw(app_state_ptr) };
                }
                unsafe { PostQuitMessage(0) };
                return LRESULT(0);
            }
            WM_MOUSEMOVE => {
                let x = (lparam.0 & 0xffff) as i16 as f32;
                let y = ((lparam.0 >> 16) & 0xffff) as i16 as f32;

                // TrackMouseEvent を使って、マウスが外に出たときに WM_MOUSELEAVE を発行させる
                let mut tme = TRACKMOUSEEVENT {
                    cbSize: std::mem::size_of::<TRACKMOUSEEVENT>() as u32,
                    dwFlags: TME_LEAVE,
                    hwndTrack: hwnd,
                    dwHoverTime: 0,
                };
                let _ = unsafe { TrackMouseEvent(&mut tme) };

                // DPIスケールを考慮して論理座標に直して注入
                let logical_pos =
                    LayoutPoint::new(x / app.renderer.scale_factor, y / app.renderer.scale_factor);

                app.context
                    .inject_user_action(UserAction::PointerMove(logical_pos));

                // マウス移動メッセージを WebView2 コントローラーへ透過的にフォワード
                // 最前面にヒットした要素が WebView2 自身である場合のみ、イベントをフォワードする
                // 修正: ドラッグ(プレス)中であれば pressed 要素を優先ロック、なければヒット要素を取得
                let hit_element = app.context.hit_test(logical_pos);
                let target_element = app
                    .context
                    .interaction_id(InteractionState::Pressed)
                    .or(hit_element);
                if target_element == Some(app.webview_id) {
                    app.renderer.forward_mouse_input(
                        &app.context,
                        app.webview_id,
                        msg,
                        wparam,
                        lparam,
                        LayoutPoint::new(x, y),
                    );
                }

                // インタラクションによる変化(ホバー状態)をリアルタイムに再描画
                if app.context.has_dirty() {
                    let _ = unsafe { InvalidateRect(Some(hwnd), None, false) };
                }
                return LRESULT(0);
            }
            WM_MOUSELEAVE => {
                // ウィンドウ外に去ったため、論理空間外へポインタを移動させてホバーを確実に解除
                app.context
                    .inject_user_action(UserAction::PointerMove(LayoutPoint::new(
                        -9999.0, -9999.0,
                    )));

                let _ = unsafe { InvalidateRect(Some(hwnd), None, false) };
                return LRESULT(0);
            }
            WM_LBUTTONDOWN | WM_LBUTTONUP | WM_RBUTTONDOWN | WM_RBUTTONUP => {
                let state = if msg == WM_LBUTTONDOWN {
                    ElementState::Pressed
                } else {
                    ElementState::Released
                };

                // マウスキャプチャの Win32 制御
                if state == ElementState::Pressed {
                    unsafe { SetCapture(hwnd) };
                } else {
                    let _ = unsafe { ReleaseCapture() };
                }

                // 修飾キーの状態をビットマスクから解決
                let modifiers = Modifiers {
                    shift: (wparam.0 & 0x0004) != 0, // MK_SHIFT
                    ctrl: (wparam.0 & 0x0008) != 0,  // MK_CONTROL
                    alt: false,
                    logo: false,
                };

                let x = (lparam.0 & 0xffff) as i16 as f32;
                let y = ((lparam.0 >> 16) & 0xffff) as i16 as f32;

                // マウスクリックを WebView2 コントローラーへフォワード
                // これにより、ブラウザ内のリンククリックや各種操作が完璧に動作します。
                // クリック座標の最前面が WebView2 自身である場合のみフォワード
                let logical_pos =
                    LayoutPoint::new(x / app.renderer.scale_factor, y / app.renderer.scale_factor);

                // プレス状態が context.inject_pointer_button 内でクリアされる前にターゲットを特定
                let hit_element = app.context.hit_test(logical_pos);

                // ボタンを離した際は、ドラッグを開始した要素(Pressed)へメッセージを流す
                let target_element = if state == ElementState::Pressed {
                    hit_element
                } else {
                    app.context
                        .interaction_id(InteractionState::Pressed)
                        .or(hit_element)
                };

                app.context.inject_user_action(UserAction::PointerButton {
                    button: MouseButton::Left,
                    state,
                    modifiers,
                });

                if target_element == Some(app.webview_id) {
                    app.renderer.forward_mouse_input(
                        &app.context,
                        app.webview_id,
                        msg,
                        wparam,
                        lparam,
                        LayoutPoint::new(x, y),
                    );
                }

                // クリックした要素が実際に WebView2 である場合のみ、キーボードフォーカスをブラウザにアタッチ
                if msg == WM_LBUTTONDOWN
                    && app.context.interaction_id(InteractionState::Focused) == Some(app.webview_id)
                {
                    app.renderer.focus_webview(app.webview_id);
                }

                // クリックによる再描画を反映
                let _ = unsafe { InvalidateRect(Some(hwnd), None, false) };
                return LRESULT(0);
            }

            WM_MOUSEWHEEL => {
                // WM_MOUSEWHEEL の座標はスクリーン座標であるため、ScreenToClient で
                // クライアント領域の物理ピクセル座標に正確に直す必要がある
                let mut pt = POINT {
                    x: (lparam.0 & 0xffff) as i16 as i32,
                    y: ((lparam.0 >> 16) & 0xffff) as i16 as i32,
                };
                let _ = unsafe { ScreenToClient(hwnd, &mut pt) };

                let physical_pos = LayoutPoint::new(pt.x as f32, pt.y as f32);

                // WebView2 へ縦スクロールイベントを転送
                // スクロール位置の最前面が WebView2 自身である場合のみフォワード
                let logical_pos = LayoutPoint::new(
                    physical_pos.x / app.renderer.scale_factor,
                    physical_pos.y / app.renderer.scale_factor,
                );
                let hit_element = app.context.hit_test(logical_pos);

                if hit_element == Some(app.webview_id) {
                    app.renderer.forward_mouse_input(
                        &app.context,
                        app.webview_id,
                        msg,
                        wparam,
                        lparam,
                        physical_pos,
                    );
                }

                let _ = unsafe { InvalidateRect(Some(hwnd), None, false) };
                return LRESULT(0);
            }
            WM_ACTIVATE => {
                let activate_state = (wparam.0 & 0xffff) as u32;
                if activate_state == WA_INACTIVE {
                    // 他ウィンドウにフォーカスが移った瞬間、アプリ内部のフォーカスを強制的に解除
                    if let Some(focused_id) = app.context.interaction_id(InteractionState::Focused)
                    {
                        app.context
                            .set_states(focused_id, &StateFlag::Focused, false);
                        app.context
                            .interaction_states(None, InteractionState::Focused);
                    }

                    // 非アクティブ移行時のキャプチャプロセスを即時トリガー
                    let _ = unsafe { InvalidateRect(Some(hwnd), None, false) };
                }
                return LRESULT(0);
            }
            WM_ENTERSIZEMOVE => {
                app.context.set_window_resized(true);
                let _ = unsafe { InvalidateRect(Some(hwnd), None, false) };
                return LRESULT(0);
            }
            // ウィンドウドラッグリサイズの完了をキャッチ
            WM_EXITSIZEMOVE => {
                app.context.set_window_resized(false);
                // リサイズ完了後の再描画を即座にキックして、新サイズでの静止画キャプチャを誘発
                let _ = unsafe { InvalidateRect(Some(hwnd), None, false) };
                return LRESULT(0);
            }
            _ => {}
        }
    }

    unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. メインスレッド(UIスレッド)の COM を STA(Single Threaded Apartment)モードで初期化
    unsafe {
        let _ = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
        let _ = RoInitialize(RO_INIT_SINGLETHREADED);
    }

    let h_instance = unsafe { GetModuleHandleW(None)? };
    let class_name = w!("MichiuSimpleTextDemoClass");

    // 2. ウィンドウクラスの登録
    unsafe {
        let wnd_class = WNDCLASSW {
            lpfnWndProc: Some(wnd_proc),
            hInstance: h_instance.into(),
            lpszClassName: class_name,
            hbrBackground: HBRUSH::default(), // 背景ブラシを完全にクリア(GDIによる描画競合を防止)
            hCursor: LoadCursorW(None, IDC_ARROW)?,
            ..Default::default()
        };
        RegisterClassW(&wnd_class);
    }

    // 3. UI コンテキストの構築と静的テキスト要素の定義
    let mut context = Context::new();
    let webview_id_cell = std::cell::Cell::new(None);

    // build_ui を使って要素ツリーを宣言的に組み立て
    let root = build_ui(&mut context, || {
        let (is_open, set_is_open) = create_signal(false);
        let (is_active, set_is_active) = create_signal(false);
        let (is_opacity, set_is_opacity) = create_signal(false);

        let webview_element = {
            let wv = webview2(
                WebView2Contents::from_url("https://www.google.com/maps")
                    .enable_context_menu(true)
                    .enable_dev_tools(true)
                    .allow_interaction(true)
                    .always_active(true),
            )
            .style({
                let base = ts()
                    .size_full()
                    .r(2.0)
                    .resizable_bottom(true)
                    .dnd_droppable(DndDropTarget::Child, DndDragPayload::Element)
                    .overflow_hidden()
                    .transform(Transform::new().scale(1.0, 1.0))
                    .trans_transform(Duration::from_millis(150), AnimationCurve::EaseInOutQuad)
                    .pressed(ts().transform(Transform::new().scale(1.01, 1.01)));

                is_opacity.get_else(base.clone().opacity_50(), base.opacity_100())
            })
            .on_focus(move || set_is_active.set(false));

            webview_id_cell.set(Some(wv.id()));
            wv
        };

        let google_map = webview_element.child(v_flex({
            let base = ts()
                .absolute()
                .size((300.0, 200.0))
                .r(3.0)
                .inset_x(100.0)
                .inset_y(50.0)
                .bg_color(Color::DARK_GRAY)
                .opacity(0.9)
                .resizable_all(true)
                .dnd_draggable_root(DndDragPayload::Element, true)
                .dnd_draggable_original(ts().opacity_0())
                .dnd_draggable_placeholder(
                    ts().size(100.0)
                        .r(3.0)
                        .bg_color(Color::DARK_GRAY)
                        .opacity(0.9),
                );

            is_active.get_else(base.clone().flex(), base.hidden())
        }));

        let btn_style = |color: Color| {
            ts().justify_center()
                .items_center()
                .r(3.0)
                .size((100.0, 40.0))
                .border_dashed(1.0)
                .border_color(color)
                .hovered(ts().border_solid(1.0))
                .actived(ts().border_solid(1.0).bg_color(color))
        };

        let overlay_btn = h_flex(move || btn_style(Color::BLUE))
            .label("Overlay", ts().font_size(14.0).text_color(Color::WHITE))
            .active(is_active)
            .on_click(move || set_is_active.set(!is_active.get()));

        let opacity_btn = h_flex(move || btn_style(Color::CYAN))
            .label("Opacity", ts().font_size(14.0).text_color(Color::WHITE))
            .active(is_opacity)
            .on_click(move || set_is_opacity.set(!is_opacity.get()));

        let close_btn = h_flex(
            ts().justify_center()
                .items_center()
                .r(3.0)
                .size((100.0, 40.0))
                .border_dashed(1.0)
                .border_color(Color::RED)
                .hovered(ts().border_solid(1.0)),
        )
        .label("Close", ts().font_size(14.0).text_color(Color::RED))
        .on_click(move || set_is_open.set(false));

        v_flex(
            ts().size_full()
                .p(20.0)
                .gap(20.0)
                .items_center()
                .bg_color(hsla(0.0, 0.0, 3.0, 0.4))
                .backdrop_acrylic(),
        )
        .children([
            v_flex(
                ts().r(3.0)
                    .size((150.0, 40.0))
                    .justify_center()
                    .items_center()
                    .p(10.0)
                    .bg_color(hsl(0.0, 0.0, 50.0)),
            )
            .label("Open", ts().font_size(16.0).text_color(Color::BLACK))
            .on_click(move || set_is_open.set(true)),
            v_flex(
                ts().r(3.0)
                    .size_full()
                    .justify_center()
                    .items_center()
                    .border_dashed(2.0)
                    .border_color(Color::GRAY),
            )
            .children([
                div(ts().r(3.0).size(pct(50.0)).bg_color(hsl(0.0, 0.0, 10.0))),
                v_flex({
                    let base = ts()
                        .size_full()
                        .p(10.0)
                        .gap(10.0)
                        .justify_center()
                        .items_center()
                        .absolute()
                        .top(0.0)
                        .left(0.0)
                        .overflow_hidden();

                    is_open.get_else(base.clone().flex(), base.hidden())
                })
                .children([
                    google_map,
                    h_flex(
                        ts().p(10.0)
                            .gap(10.0)
                            .w_full()
                            .h_auto()
                            .justify_center()
                            .items_center(),
                    )
                    .children([overlay_btn, opacity_btn, close_btn]),
                ]),
            ]),
        ])
    });

    let webview_id = webview_id_cell.get().expect("WebView2 ID not assigned");
    let initial_layout_size = LayoutSize::new(800.0, 600.0);
    // 4. アプリケーション状態をヒープ上に準備
    // ※ ウィンドウ生成非同期ハンドラ (ComposedRenderer) を作成するためにプレシーティング
    // (ComposedRenderer::new は内部で setup_direct_composition(hwnd) を行います)
    // ウィンドウを作成する(まだlpParamはNullの状態で枠のみを一旦作成、またはlpParamにapp_stateポインタをバインド)
    // ダミーの HWND であらかじめレンダラーを作るのを防ぐため、
    // まず HWND を生成し、WM_CREATE のタイミングではなく作成直後に ComposedRenderer を生成して格納します。
    let hwnd = unsafe {
        CreateWindowExW(
            WS_EX_NOREDIRECTIONBITMAP, // GDIリダイレクションを無効化し DComp を露出させる
            class_name,
            w!("Michiu - Simple Text Demo"),
            WS_OVERLAPPEDWINDOW,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            800,
            600,
            None,
            None,
            Some(HINSTANCE(h_instance.0)),
            None, // WM_CREATE 時には一旦 Null にしておく
        )?
    };

    // レンダラーを作成
    let dpi = unsafe { GetDpiForWindow(hwnd) };
    let scale_factor = dpi as f32 / 96.0;
    let renderer = pollster::block_on(ComposedRenderer::new(
        hwnd,
        initial_layout_size,
        scale_factor,
    ))?;

    let app_state = Box::new(AppState {
        renderer,
        context,
        root_id: root.id(),
        webview_id,
    });

    // 作成完了した AppState の生ポインタを HWND にアタッチ
    unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, Box::into_raw(app_state) as isize) };
    // 最初の WM_SIZE がアタッチ前に無視されてしまうため、
    // ここで正確なクライアント領域サイズを取得し、手動で初回の正確なリサイズとレイアウト同期を叩き込みます。
    let mut client_rect = RECT::default();
    let _ = unsafe { GetClientRect(hwnd, &mut client_rect) };
    let width = (client_rect.right - client_rect.left) as u32;
    let height = (client_rect.bottom - client_rect.top) as u32;

    let app_ptr = unsafe { GetWindowLongPtrW(hwnd, GWLP_USERDATA) } as *mut AppState;
    let app = unsafe { &mut *app_ptr };

    // 正確なクライアント領域ピクセルで wgpu ターゲットを設定し、初回のレイアウト計算を確定
    app.renderer.resize((width, height), scale_factor);

    app.context
        .sync_layout(app.root_id, app.renderer.layout_size);
    app.renderer.prewarm_webview2();

    // 5. ウィンドウを表示して描画
    unsafe {
        let _ = ShowWindow(hwnd, SW_SHOW);
        let _ = InvalidateRect(Some(hwnd), None, false);
    }

    // 6. Win32 メッセージループの開始
    let mut msg = MSG::default();
    unsafe {
        while GetMessageW(&mut msg, None, 0, 0).as_bool() {
            let _ = TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }
    }

    Ok(())
}