zsui 0.3.0-alpha.2

Rust-first feature-gated native UI framework for Win32, AppKit, and Linux.
Documentation
#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")]
use std::{
    env, fs,
    path::PathBuf,
    sync::{Arc, Mutex},
    thread,
    time::Duration,
};
use zsui::{
    button, column, native_window, row, text, AppCx, Command, DirectoryDialogSpec, Dp,
    FileDialogService, FileDialogSpec, NativeFileDialogService, NativeWindowSmokeRunOptions,
    SaveFileDialogSpec, UiInvalidationHandle, ViewNode, WidgetId, ZsWindowCommand, ZsWindowState,
};

#[derive(Default)]
struct State {
    actions: Vec<String>,
    window_states: Vec<bool>,
    close_requests: usize,
    worker_result: Option<Result<String, String>>,
}
#[derive(Clone)]
enum Msg {
    Action(ZsWindowCommand),
    WindowState(ZsWindowState),
    CloseRequested,
}
fn view(state: &State) -> ViewNode<Msg> {
    column([
        text("Desktop window lifecycle").window_drag_region(),
        row([
            button("Minimize")
                .id(WidgetId::new(991))
                .on_click(Msg::Action(ZsWindowCommand::Minimize)),
            button("Maximize / restore")
                .id(WidgetId::new(992))
                .on_click(Msg::Action(ZsWindowCommand::ToggleMaximize)),
            button("Hide")
                .id(WidgetId::new(993))
                .on_click(Msg::Action(ZsWindowCommand::Hide)),
        ])
        .gap(Dp::new(8.0)),
        text(format!("Actions: {}", state.actions.len())),
        text(format!("Close requests: {}", state.close_requests)),
        text(format!("Panel response: {:?}", state.worker_result)),
    ])
    .padding(Dp::new(20.0))
    .gap(Dp::new(12.0))
    .on_window_state_change(Msg::WindowState)
}
fn update(state: &mut State, msg: Msg, cx: &mut AppCx) {
    match msg {
        Msg::Action(command) => {
            state.actions.push(format!("{command:?}"));
            cx.window_command(command);
        }
        Msg::WindowState(value) => state.window_states.push(value.maximized),
        Msg::CloseRequested => state.close_requests += 1,
    }
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args: Vec<_> = env::args().collect();
    let argument = |name: &str| {
        args.iter()
            .position(|s| s == name)
            .and_then(|i| args.get(i + 1))
            .cloned()
    };
    let scene = argument("--scene").unwrap_or_else(|| "commands".into());
    let output = PathBuf::from(argument("--output").ok_or("--output is required")?);
    if !output.is_absolute() {
        return Err("--output must be absolute".into());
    }
    fs::create_dir_all(&output)?;
    if scene == "provider-failure" || scene == "provider-unavailable" || scene == "worker-no-host" {
        let result = thread::spawn(|| {
            NativeFileDialogService::new()
                .pick_directory_dialog(&DirectoryDialogSpec::new("Provider response"))
        })
        .join()
        .unwrap();
        fs::write(
            output.join("proof.json"),
            serde_json::to_vec_pretty(&serde_json::json!({"scene":scene,"result":result}))?,
        )?;
        if scene == "provider-unavailable" {
            assert!(matches!(result, Err(zsui::ZsuiError::Unsupported { .. })));
        } else {
            assert!(matches!(result, Err(zsui::ZsuiError::Host { .. })));
        }
        return Ok(());
    }
    let state = Arc::new(Mutex::new(State::default()));
    let view_state = state.clone();
    let update_state = state.clone();
    let wake = UiInvalidationHandle::new();
    let worker_wake = wake.clone();
    let worker_state = state.clone();
    let worker_scene = scene.clone();
    let worker = thread::spawn(move || {
        thread::sleep(Duration::from_millis(400));
        if worker_scene == "commands" {
            for id in [
                "maximize", "maximize", "minimize", "show", "hide", "show", "close",
            ] {
                worker_wake
                    .request_app_command(Command::custom(id))
                    .unwrap();
                thread::sleep(Duration::from_millis(450));
                if id == "hide" {
                    worker_wake.request_rebuild();
                    thread::sleep(Duration::from_millis(200));
                }
            }
        } else {
            let mut service = NativeFileDialogService::new();
            let result = match worker_scene.as_str() {
                "worker-open" => service
                    .open_file_dialog(&FileDialogSpec::new("Open a file"))
                    .map(|v| v.is_none()),
                "worker-save" => service
                    .save_file_dialog(&SaveFileDialogSpec::new("Save a file"))
                    .map(|v| v.is_none()),
                "worker-directory" => service
                    .pick_directory_dialog(&DirectoryDialogSpec::new("Choose a directory"))
                    .map(|v| v.is_none()),
                _ => panic!("unknown lifecycle scene"),
            }
            .map(|cancelled| {
                if cancelled {
                    "cancelled".to_owned()
                } else {
                    "selected".to_owned()
                }
            })
            .map_err(|e| e.to_string());
            worker_state.lock().unwrap().worker_result = Some(result);
            worker_wake.request_rebuild();
        }
    });
    let builder = native_window("ZSUI Desktop Lifecycle")
        .size(640, 300)
        .custom_title_bar(true)
        .release_view_when_hidden()
        .invalidation_handle(wake)
        .on_close_requested(Command::custom("close-requested"))
        .stateful_view_with_app_commands(
            (),
            move |_| view(&view_state.lock().unwrap()),
            move |_, msg, cx| update(&mut update_state.lock().unwrap(), msg, cx),
            |command| {
                if let Command::Custom { id, .. } = command {
                    match id.as_str() {
                        "maximize" => Some(Msg::Action(ZsWindowCommand::ToggleMaximize)),
                        "minimize" => Some(Msg::Action(ZsWindowCommand::Minimize)),
                        "show" => Some(Msg::Action(ZsWindowCommand::Show)),
                        "hide" => Some(Msg::Action(ZsWindowCommand::Hide)),
                        "close" => Some(Msg::Action(ZsWindowCommand::Close)),
                        "close-requested" => Some(Msg::CloseRequested),
                        _ => None,
                    }
                } else {
                    None
                }
            },
        );
    let report = builder.run_smoke(
        NativeWindowSmokeRunOptions::new(5500)
            .screenshot_file(output.join("window.png").to_string_lossy())
            .require_screenshot(true),
    )?;
    worker.join().unwrap();
    let state = state.lock().unwrap();
    fs::write(
        output.join("proof.json"),
        serde_json::to_vec_pretty(
            &serde_json::json!({"scene":scene,"actions":state.actions,"native_maximized_states":state.window_states,"close_requests":state.close_requests,"worker_result":state.worker_result,"runtime":report}),
        )?,
    )?;
    if scene == "commands" {
        assert_eq!(state.actions.len(), 7);
        assert_eq!(state.close_requests, 1);
        assert!(state.window_states.contains(&true));
        assert_eq!(state.window_states.last(), Some(&false));
    } else {
        assert_eq!(state.worker_result, Some(Ok("cancelled".into())));
    }
    assert!(report.visible_window_was_created() && report.screenshot_captured);
    Ok(())
}