#![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,
};
struct AppState {
renderer: ComposedRenderer,
context: Context,
root_id: EntityId,
webview_id: EntityId,
}
unsafe extern "system" fn wnd_proc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
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);
}
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 => {
return LRESULT(1);
}
WM_DPICHANGED => {
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;
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();
app.context
.tick_system_frame(&TickType::TransitionAndAnimation);
app.context
.sync_layout(app.root_id, app.renderer.layout_size);
app.renderer.update_composition_tree(&mut app.context);
app.renderer.draw(&mut app.context);
let _ = unsafe { EndPaint(hwnd, &ps) };
if app.context.has_active_frame() || app.context.has_dirty() {
let _ = unsafe { InvalidateRect(Some(hwnd), None, false) };
}
return LRESULT(0);
}
WM_NCDESTROY => {
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;
let mut tme = TRACKMOUSEEVENT {
cbSize: std::mem::size_of::<TRACKMOUSEEVENT>() as u32,
dwFlags: TME_LEAVE,
hwndTrack: hwnd,
dwHoverTime: 0,
};
let _ = unsafe { TrackMouseEvent(&mut tme) };
let logical_pos =
LayoutPoint::new(x / app.renderer.scale_factor, y / app.renderer.scale_factor);
app.context
.inject_user_action(UserAction::PointerMove(logical_pos));
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
};
if state == ElementState::Pressed {
unsafe { SetCapture(hwnd) };
} else {
let _ = unsafe { ReleaseCapture() };
}
let modifiers = Modifiers {
shift: (wparam.0 & 0x0004) != 0, ctrl: (wparam.0 & 0x0008) != 0, alt: false,
logo: false,
};
let x = (lparam.0 & 0xffff) as i16 as f32;
let y = ((lparam.0 >> 16) & 0xffff) as i16 as f32;
let logical_pos =
LayoutPoint::new(x / app.renderer.scale_factor, y / app.renderer.scale_factor);
let hit_element = app.context.hit_test(logical_pos);
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),
);
}
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 => {
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);
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>> {
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");
unsafe {
let wnd_class = WNDCLASSW {
lpfnWndProc: Some(wnd_proc),
hInstance: h_instance.into(),
lpszClassName: class_name,
hbrBackground: HBRUSH::default(), hCursor: LoadCursorW(None, IDC_ARROW)?,
..Default::default()
};
RegisterClassW(&wnd_class);
}
let mut context = Context::new();
let webview_id_cell = std::cell::Cell::new(None);
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);
let hwnd = unsafe {
CreateWindowExW(
WS_EX_NOREDIRECTIONBITMAP, class_name,
w!("Michiu - Simple Text Demo"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
800,
600,
None,
None,
Some(HINSTANCE(h_instance.0)),
None, )?
};
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,
});
unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, Box::into_raw(app_state) as isize) };
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 };
app.renderer.resize((width, height), scale_factor);
app.context
.sync_layout(app.root_id, app.renderer.layout_size);
app.renderer.prewarm_webview2();
unsafe {
let _ = ShowWindow(hwnd, SW_SHOW);
let _ = InvalidateRect(Some(hwnd), None, false);
}
let mut msg = MSG::default();
unsafe {
while GetMessageW(&mut msg, None, 0, 0).as_bool() {
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
Ok(())
}