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},
};
use zsui::{
    button, column, native_window, row, styled_text, text, textbox, AppCx, DirectoryDialogSpec, Dp,
    NativeViewKey, NativeWindowSmokeRunOptions, SemanticTextStyle, Size, WidgetId,
    ZsTextEditCommand, ZsTextSelection, ZsuiResult,
};
const FIELD: WidgetId = WidgetId::new(951);
const ACTION: WidgetId = WidgetId::new(952);
const SELECT: WidgetId = WidgetId::new(953);
const PICK: WidgetId = WidgetId::new(954);
#[derive(Clone)]
enum Msg {
    Changed(String),
    Selection(ZsTextSelection),
    Action,
    Select,
    Pick,
    Picked(ZsuiResult<Option<PathBuf>>),
}
struct State {
    path: String,
    actions: usize,
    selections: usize,
    selected_all: bool,
    picked: Option<ZsuiResult<Option<PathBuf>>>,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = env::args().skip(1).collect::<Vec<_>>();
    let arg = |name: &str| {
        args.windows(2)
            .find(|pair| pair[0] == name)
            .map(|pair| pair[1].clone())
    };
    let directory = arg("--scene").as_deref() == Some("directory");
    let initial = format!("C:/{}document.txt", "工程资料/长路径🙂/".repeat(36));
    let state = Arc::new(Mutex::new(State {
        path: initial.clone(),
        actions: 0,
        selections: 0,
        selected_all: false,
        picked: None,
    }));
    let initial_folder = arg("--initial-directory").map(PathBuf::from);
    let builder = native_window("ZSUI Desktop Reliability")
        .size(960, 320)
        .stateful_view(
            state.clone(),
            move |state| {
                let state = state.lock().unwrap();
                if directory {
                    column([
                        text("Directory"),
                        button("Choose directory").id(PICK).on_click(Msg::Pick),
                        text(format!("{:?}", state.picked)),
                    ])
                    .padding(Dp::new(20.0))
                } else {
                    column([
                        row([
                            styled_text(
                                state.path.clone(),
                                SemanticTextStyle {
                                    ellipsis: true,
                                    ..SemanticTextStyle::body()
                                },
                            )
                            .flex(1.0),
                            button("Action")
                                .id(ACTION)
                                .width(Dp::new(96.0))
                                .on_click(Msg::Action),
                        ])
                        .gap(Dp::new(12.0)),
                        textbox(state.path.clone())
                            .id(FIELD)
                            .width(Dp::new(560.0))
                            .on_change(Msg::Changed)
                            .on_text_selection_change(Msg::Selection),
                        button("Select all").id(SELECT).on_click(Msg::Select),
                        text(format!("Actions: {}", state.actions)),
                    ])
                    .padding(Dp::new(20.0))
                    .gap(Dp::new(16.0))
                }
            },
            move |state, message, cx: &mut AppCx| {
                let mut state = state.lock().unwrap();
                match message {
                    Msg::Changed(value) => state.path = value,
                    Msg::Selection(selection) => {
                        state.selected_all |=
                            selection.ordered() == (0, state.path.chars().count());
                        state.selections += 1;
                    }
                    Msg::Action => state.actions += 1,
                    Msg::Select => cx.text_edit_command_for(FIELD, ZsTextEditCommand::SelectAll),
                    Msg::Pick => {
                        let mut spec = DirectoryDialogSpec::new("Choose directory");
                        if let Some(path) = &initial_folder {
                            spec = spec.current_path(path);
                        }
                        if let Err(error) = cx.try_pick_directory(spec, Msg::Picked) {
                            state.picked = Some(Err(zsui::ZsuiError::host(
                                "directory_queue",
                                error.to_string(),
                            )));
                        }
                    }
                    Msg::Picked(result) => state.picked = Some(result),
                }
            },
        );
    if !args.iter().any(|arg| arg == "--smoke") {
        builder.run()?;
        return Ok(());
    }
    let output = PathBuf::from(arg("--output").ok_or("--smoke requires --output")?);
    fs::create_dir_all(&output)?;
    let duration = arg("--duration-ms")
        .map(|value| value.parse())
        .transpose()?
        .unwrap_or(if directory { 15000 } else { 1800 });
    let mut options = NativeWindowSmokeRunOptions::new(duration)
        .screenshot_file(output.join("window.png").to_string_lossy())
        .require_screenshot(true);
    if directory {
        options = options.native_view_click_widget(PICK);
    } else {
        options = options
            .native_window_resize(Size {
                width: 640,
                height: 320,
            })
            .require_native_window_resize(true)
            .native_view_click_widget(ACTION)
            .native_view_click_widget(FIELD)
            .native_view_key_down(NativeViewKey::Home)
            .native_view_key_down(NativeViewKey::End)
            .native_view_ime_preedit("中文🙂", Some((3, 3)))
            .native_view_ime_commit("中文🙂")
            .native_view_click_widget(SELECT)
            .native_view_click_widget(FIELD)
            .native_view_key_down(NativeViewKey::End)
            .native_view_text_input("tail");
    }
    let report = builder.run_smoke(options)?;
    let state = state.lock().unwrap();
    fs::write(
        output.join("proof.json"),
        serde_json::to_vec_pretty(
            &serde_json::json!({"path":state.path,"actions":state.actions,"selection_changes":state.selections,"directory_result":state.picked,"runtime":report}),
        )?,
    )?;
    assert!(report.visible_window_was_created() && report.screenshot_captured);
    if directory {
        assert!(
            state.picked.as_ref().is_some_and(|result| result.is_ok()),
            "directory service did not complete successfully"
        );
    } else {
        assert_eq!(state.path, format!("{initial}中文🙂tail"));
        assert_eq!(state.actions, 1);
        assert!(state.selections > 0);
        assert!(state.selected_all);
        assert!(report.native_view_ime_commit_count > 0);
    }
    Ok(())
}