winio-ui-android 0.1.0

Android backend for winio.
use jni::{
    Env,
    objects::{JObject, JString},
    refs::{LoaderContext, Reference},
};
use jni_min_helper::{DynamicProxy, JInteger};
use winio_handle::AsWindow;
use winio_primitive::{MessageBoxButton, MessageBoxResponse, MessageBoxStyle};

use crate::{
    Error, Result,
    java::{
        android::{
            app::AlertDialogBuilder,
            content::{
                DialogInterfaceOnCancelListener, DialogInterfaceOnClickListener,
                DialogInterfaceOnDismissListener,
            },
        },
        custom::Activity,
    },
    vm_exec,
};

#[derive(Debug, Default, Clone)]
pub struct MessageBox {
    msg: String,
    title: String,
    instr: String,
    style: MessageBoxStyle,
    btns: MessageBoxButton,
    cbtns: Vec<CustomButton>,
}

impl MessageBox {
    pub fn new() -> Self {
        Self {
            msg: String::new(),
            title: String::new(),
            instr: String::new(),
            style: MessageBoxStyle::None,
            btns: MessageBoxButton::empty(),
            cbtns: vec![],
        }
    }

    pub fn message(&mut self, msg: &str) {
        self.msg = msg.to_string();
    }

    pub fn title(&mut self, title: &str) {
        self.title = title.to_string();
    }

    pub fn instruction(&mut self, instr: &str) {
        self.instr = instr.to_string();
    }

    pub fn style(&mut self, style: MessageBoxStyle) {
        self.style = style;
    }

    pub fn buttons(&mut self, btns: MessageBoxButton) {
        self.btns = btns;
    }

    pub fn custom_button(&mut self, btn: CustomButton) {
        self.cbtns.push(btn);
    }

    pub fn custom_buttons(&mut self, btn: impl IntoIterator<Item = CustomButton>) {
        self.cbtns.extend(btn);
    }

    fn texts_and_responses<'local>(
        env: &mut Env<'local>,
        btns: MessageBoxButton,
        cbtns: &[CustomButton],
    ) -> Result<([Option<MessageBoxResponse>; 3], [JString<'local>; 3])> {
        let mut responses = [None; 3];
        let mut texts = [JString::null(), JString::null(), JString::null()];

        const POSITIVE: usize = 0;
        const NEGATIVE: usize = 1;
        const NEUTRAL: usize = 2;

        let mut push = |response: MessageBoxResponse, text: &str, index: usize| {
            if responses[index].replace(response).is_some() {
                return Err(Error::NotSupported);
            }
            texts[index] = env.new_string(text)?;
            Ok(())
        };
        if btns.contains(MessageBoxButton::Ok) {
            push(MessageBoxResponse::Ok, "Ok", POSITIVE)?;
        }
        if btns.contains(MessageBoxButton::Cancel) {
            push(MessageBoxResponse::Cancel, "Cancel", NEGATIVE)?;
        }
        if btns.contains(MessageBoxButton::Yes) {
            push(MessageBoxResponse::Yes, "Yes", POSITIVE)?;
        }
        if btns.contains(MessageBoxButton::No) {
            push(MessageBoxResponse::No, "No", NEGATIVE)?;
        }
        if btns.contains(MessageBoxButton::Retry) {
            push(MessageBoxResponse::Retry, "Retry", POSITIVE)?;
        }
        if btns.contains(MessageBoxButton::Close) {
            push(MessageBoxResponse::Close, "Close", NEGATIVE)?;
        }
        for cbtn in cbtns {
            push(
                MessageBoxResponse::Custom(cbtn.result),
                &cbtn.label,
                NEUTRAL,
            )?;
        }
        Ok((responses, texts))
    }

    pub fn show(
        self,
        parent: Option<impl AsWindow>,
    ) -> Result<impl Future<Output = Result<MessageBoxResponse>> + 'static> {
        let (mut rx, _proxy) = vm_exec(|env| {
            let activity = if let Some(parent) = parent {
                let act = env.new_local_ref(parent.as_window().to_android())?;
                unsafe { Activity::from_raw(env, act.into_raw()) }
            } else {
                crate::current_activity(env)?
            };
            let builder = AlertDialogBuilder::new(env, &activity)?;
            let msg = if self.instr.is_empty() {
                env.new_string(self.msg)?
            } else {
                env.new_string(format!("{}\n\n{}", self.instr, self.msg))?
            };
            builder.set_message(env, &msg)?;
            if !self.title.is_empty() {
                let title = env.new_string(self.title)?;
                builder.set_title(env, &title)?;
            }
            let (responses, texts) = Self::texts_and_responses(env, self.btns, &self.cbtns)?;

            let (tx, rx) = futures_channel::mpsc::unbounded::<MessageBoxResponse>();
            let proxy = DynamicProxy::build(
                env,
                &LoaderContext::None,
                [
                    DialogInterfaceOnClickListener::class_name(),
                    DialogInterfaceOnCancelListener::class_name(),
                    DialogInterfaceOnDismissListener::class_name(),
                ],
                move |env, method, args| {
                    let name = method.get_name(env)?.try_to_string(env)?;
                    if name == "onCancel" {
                        tx.unbounded_send(MessageBoxResponse::Cancel).ok();
                    } else if name == "onDismiss" {
                        tx.unbounded_send(MessageBoxResponse::Close).ok();
                    } else if name == "onClick" {
                        let which = args.get_element(env, 1)?;
                        let which =
                            unsafe { JInteger::from_raw(env, which.into_raw()) }.value(env)?;
                        let response = match which {
                            -3 => responses[2].unwrap(),
                            -2 => responses[1].unwrap(),
                            -1 => responses[0].unwrap(),
                            _ => panic!("Unknown button index: {}", which),
                        };
                        tx.unbounded_send(response).ok();
                    }
                    Ok(JObject::null())
                },
            )?;

            if responses[0].is_some() {
                builder.set_positive_button(env, &texts[0], &proxy)?;
            }
            if responses[1].is_some() {
                builder.set_negative_button(env, &texts[1], &proxy)?;
            }
            if responses[2].is_some() {
                builder.set_neutral_button(env, &texts[2], &proxy)?;
            }

            let dialog = builder.create(env)?;
            dialog.set_on_cancel_listener(env, &proxy)?;
            dialog.set_on_dismiss_listener(env, &proxy)?;
            dialog.show(env)?;
            Result::Ok((rx, proxy))
        })?;
        Ok(async move {
            rx.recv()
                .await
                .map_err(|e| Error::Io(std::io::Error::other(e)))
        })
    }
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct CustomButton {
    result: u16,
    label: String,
}

impl CustomButton {
    pub fn new(result: u16, label: impl AsRef<str>) -> Self {
        Self {
            result,
            label: label.as_ref().to_string(),
        }
    }
}