winio-ui-winui 0.4.0

WinUI backend for winio.
Documentation
use std::{
    cell::RefCell,
    future::Future,
    os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle, RawHandle},
    ptr::null,
    sync::{
        Arc,
        atomic::{AtomicBool, Ordering},
    },
};

use compio::driver::AsRawFd;
use compio_log::*;
use windows::{
    Foundation::Uri,
    Win32::Graphics::Direct2D::ID2D1Factory2,
    core::{Array, HSTRING, IInspectable_Vtbl, Interface, Ref, h, imp::WeakRefCount, implement},
};
use windows_sys::Win32::{
    Foundation::{HWND, WAIT_FAILED, WAIT_OBJECT_0},
    System::Threading::{CreateEventW, INFINITE, SetEvent, WaitForMultipleObjects},
    UI::WindowsAndMessaging::MSG,
};
use winio_ui_windows_common::{PreferredAppMode, init_dark, set_preferred_app_mode};
use winui3::{
    ApartmentType, ChildClass, ChildClassImpl, Compose, CreateInstanceFn,
    Microsoft::UI::{
        Dispatching::{DispatcherQueue, DispatcherQueueHandler},
        Xaml::{
            Application, ApplicationInitializationCallback,
            ApplicationInitializationCallbackParams,
            Controls::XamlControlsResources,
            IApplicationFactory, IApplicationFactory_Vtbl, IApplicationOverrides,
            IApplicationOverrides_Impl, LaunchActivatedEventArgs,
            Markup::{
                IXamlMetadataProvider, IXamlMetadataProvider_Impl, IXamlType, XmlnsDefinition,
            },
            ResourceDictionary, UnhandledExceptionEventHandler,
            XamlTypeInfo::XamlControlsXamlMetaDataProvider,
        },
    },
    Windows::UI::Xaml::Interop::TypeName,
    bootstrap::{PackageDependency, WindowsAppSDKVersion},
    init_apartment,
};

use crate::{Error, RUNTIME, Result};

pub struct Runtime {
    runtime: winio_ui_windows_common::Runtime,
    #[allow(dead_code)]
    winui_dependency: PackageDependency,
    shutdown: Option<Arc<OwnedHandle>>,
}

fn init_appsdk_with(
    vers: impl IntoIterator<Item = WindowsAppSDKVersion>,
) -> Result<PackageDependency> {
    for ver in vers {
        if let Ok(p) = PackageDependency::initialize_version(ver) {
            return Ok(p);
        }
    }
    PackageDependency::initialize()
}

impl Runtime {
    pub fn new() -> Result<Self> {
        init_apartment(ApartmentType::SingleThreaded)?;

        let winui_dependency = init_appsdk_with({
            use WindowsAppSDKVersion::*;
            [
                V1_8,
                V1_7,
                V1_6,
                #[cfg(feature = "enable-cbs")]
                Cbs1_6,
                V1_5,
                #[cfg(feature = "enable-cbs")]
                Cbs,
                V1_4,
                V1_3,
                V1_2,
                #[cfg(not(feature = "media"))]
                V1_1,
                #[cfg(not(feature = "media"))]
                V1_0,
            ]
        })?;

        debug!("WinUI initialized: {winui_dependency:?}");

        init_dark();
        set_preferred_app_mode(PreferredAppMode::AllowDark);

        crate::hook::mrm::init_hook();
        let shutdown = if !crate::hook::mq::init_hook() {
            warn!("Message queue hooking failed, fallback to dedicated thread");
            Some(Arc::new(unsafe {
                OwnedHandle::from_raw_handle({
                    let handle = CreateEventW(null(), 0, 0, null());
                    if handle.is_null() {
                        return Err(Error::from_thread());
                    }
                    handle
                })
            }))
        } else {
            None
        };

        let runtime = winio_ui_windows_common::Runtime::new()?;

        Ok(Self {
            runtime,
            winui_dependency,
            shutdown,
        })
    }

    pub(crate) fn d2d1(&self) -> Result<&ID2D1Factory2> {
        self.runtime.d2d1()
    }

    pub(crate) fn run(&self) -> bool {
        self.runtime.run()
    }

    fn enter<T, F: FnOnce() -> T>(&self, f: F) -> T {
        self.runtime.enter(|| RUNTIME.set(self, f))
    }

    pub fn block_on<F: Future>(&self, future: F) -> F::Output {
        self.enter(|| {
            let mut result = None;
            unsafe {
                self.runtime.spawn_unchecked(async {
                    result = Some(future.await);
                    Application::Current()
                        .expect("Failed to get current application")
                        .Exit()
                        .expect("Failed to exit application");
                })
            }
            .detach();

            Application::Start(&ApplicationInitializationCallback::new(app_start))
                .expect("Failed to start application");

            if let Some(shutdown) = &self.shutdown {
                unsafe {
                    SetEvent(shutdown.as_raw_handle());
                }
            }

            result.expect("Application exits but no result")
        })
    }
}

fn app_start(_: Ref<'_, ApplicationInitializationCallbackParams>) -> Result<()> {
    debug!("Application::Start");

    let app = App::compose()?;
    app.UnhandledException(Some(&UnhandledExceptionEventHandler::new(
        |_sender, args| {
            #[allow(clippy::single_match)]
            match args.as_ref() {
                #[allow(unused)]
                Some(args) => {
                    error!("Unhandled exception: {}", args.Exception()?);
                    error!("{}", args.Message()?);
                }
                None => {
                    error!("Unhandled exception occurred");
                }
            }
            Ok(())
        },
    )))?;

    RUNTIME.with(|runtime| {
        if let Some(shutdown) = runtime.shutdown.clone() {
            spawn_runtime_thread(runtime.runtime.as_raw_fd() as *const _ as usize, shutdown)?;
        }
        Result::Ok(())
    })?;

    Ok(())
}

pub(crate) unsafe fn run_runtime(msg: *mut MSG, hwnd: HWND, min: u32, max: u32) -> Option<i32> {
    if RUNTIME.is_set() {
        let res =
            RUNTIME.with(|runtime| unsafe { runtime.runtime.get_message(msg, hwnd, min, max) });
        Some(res)
    } else {
        None
    }
}

static THREAD_COUNTER: AtomicBool = AtomicBool::new(false);

struct ThreadGuard;

impl ThreadGuard {
    fn new() -> Option<Self> {
        if THREAD_COUNTER
            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
            .is_ok()
        {
            Some(Self)
        } else {
            None
        }
    }
}

impl Drop for ThreadGuard {
    fn drop(&mut self) {
        THREAD_COUNTER.store(false, Ordering::Release);
        info!("Runtime thread exited");
    }
}

fn spawn_runtime_thread(runtime: usize, shutdown: Arc<OwnedHandle>) -> Result<()> {
    if let Some(guard) = ThreadGuard::new() {
        let dispatcher = DispatcherQueue::GetForCurrentThread()?;
        compio::runtime::spawn_blocking(move || {
            let _guard = guard;
            loop {
                let timeout = resume_foreground(&dispatcher, {
                    move || RUNTIME.with(|runtime| runtime.runtime.poll_and_run())
                });
                let Some(timeout) = timeout else {
                    break;
                };
                debug!("Waiting in {timeout:?}");
                let timeout = match timeout {
                    Some(timeout) => timeout.as_millis() as u32,
                    None => INFINITE,
                };
                let handles = [shutdown.as_raw_handle(), runtime as RawHandle];
                let res = unsafe { WaitForMultipleObjects(2, handles.as_ptr(), 0, timeout) };
                if res == WAIT_OBJECT_0 {
                    break;
                } else if res == WAIT_FAILED {
                    error!("WaitForMultipleObjects: {:?}", Error::from_thread());
                    break;
                }
            }
        })
        .detach();
    }
    Ok(())
}

fn resume_foreground<T: Send + 'static>(
    dispatcher: &DispatcherQueue,
    f: impl (Fn() -> T) + Send + 'static,
) -> Option<T> {
    let (tx, rx) = oneshot::channel();
    let tx = RefCell::new(Some(tx));
    let queued = dispatcher
        .TryEnqueue(&DispatcherQueueHandler::new(move || {
            if let Some(tx) = tx.borrow_mut().take() {
                tx.send(f()).ok();
            }
            Ok(())
        }))
        .unwrap_or_default();
    if queued { rx.recv().ok() } else { None }
}

#[implement(IApplicationOverrides, IXamlMetadataProvider)]
struct App {
    provider: XamlControlsXamlMetaDataProvider,
}

impl App {
    pub(crate) fn compose() -> Result<Application> {
        Compose::compose(Self {
            provider: XamlControlsXamlMetaDataProvider::new()?,
        })
    }
}

impl ChildClassImpl for App_Impl {}

impl IApplicationOverrides_Impl for App_Impl {
    fn OnLaunched(&self, _: Ref<LaunchActivatedEventArgs>) -> Result<()> {
        debug!("App::OnLaunched");

        let resources = self.base()?.cast::<Application>()?.Resources()?;
        let merged_dictionaries = resources.MergedDictionaries()?;
        let xaml_controls_resources = XamlControlsResources::new()?;
        merged_dictionaries.Append(&xaml_controls_resources)?;

        let compact_resources = ResourceDictionary::new()?;
        compact_resources.SetSource(&Uri::CreateUri(h!(
            "ms-appx:///Microsoft.UI.Xaml/DensityStyles/Compact.xaml"
        ))?)?;
        merged_dictionaries.Append(&compact_resources)?;

        Ok(())
    }
}

impl IXamlMetadataProvider_Impl for App_Impl {
    fn GetXamlType(&self, ty: &TypeName) -> Result<IXamlType> {
        self.provider.GetXamlType(ty)
    }

    fn GetXamlTypeByFullName(&self, name: &HSTRING) -> Result<IXamlType> {
        self.provider.GetXamlTypeByFullName(name)
    }

    fn GetXmlnsDefinitions(&self) -> Result<Array<XmlnsDefinition>> {
        self.provider.GetXmlnsDefinitions()
    }
}

impl ChildClass for App {
    type BaseType = Application;
    type FactoryInterface = IApplicationFactory;

    fn create_interface_fn(vtable: &IApplicationFactory_Vtbl) -> CreateInstanceFn {
        vtable.CreateInstance
    }

    fn identity_vtable(vtable: &mut Self::Outer) -> &mut &'static IInspectable_Vtbl {
        &mut vtable.identity
    }

    fn ref_count(vtable: &Self::Outer) -> &WeakRefCount {
        &vtable.count
    }

    fn into_outer(self) -> Self::Outer {
        Self::into_outer(self)
    }
}