winio-ui-android 0.1.0

Android backend for winio.
use std::sync::{Arc, Mutex};

use inherit_methods_macro::inherit_methods;
use jni::{
    objects::{JObject, JString},
    refs::{Global, LoaderContext, Reference},
};
use jni_min_helper::{DynamicProxy, JInteger};
use winio_callback::SyncCallback;
use winio_handle::{AsContainer, AsWidget};
use winio_primitive::{Point, Size};

use crate::{
    BaseWidget, Result, View, WINDOW_RESIZE_CALLBACK, current_activity,
    java::{
        android::{
            view::{View as AView, ViewOnLayoutChangeListener},
            widget::{LinearLayout, LinearLayoutLayoutParams, linear_layout::VERTICAL},
        },
        androidx::viewpager2::ViewPager2,
        custom::WinioTabViewAdapter,
        material::{
            TabLayout, TabLayoutMediator, TabLayoutMediatorTabConfigurationStrategy,
            TabLayoutOnTabSelectedListener, TabLayoutTab,
        },
    },
    vm_exec,
};

#[derive(Debug)]
#[allow(dead_code)]
pub struct TabView {
    handle: BaseWidget<LinearLayout<'static>>,
    tab: Global<TabLayout<'static>>,
    pager: Global<ViewPager2<'static>>,
    mediator: Global<TabLayoutMediator<'static>>,
    tab_titles: Arc<Mutex<Vec<Global<JString<'static>>>>>,
    tab_proxy: DynamicProxy,
    on_select: Arc<SyncCallback>,
    select_proxy: DynamicProxy,
    adapter: Global<WinioTabViewAdapter<'static>>,
}

#[inherit_methods(from = "self.handle")]
impl TabView {
    pub fn new(parent: impl AsContainer) -> Result<Self> {
        vm_exec(|env| {
            let act = current_activity(env)?;
            let layout = LinearLayout::new(env, &act)?;
            layout.set_orientation(env, VERTICAL)?;
            let layout = BaseWidget::new_with_env(env, parent.as_container(), layout)?;

            let tab = TabLayout::new(env, &act)?;
            let params = LinearLayoutLayoutParams::new(env, -1, -2)?;
            tab.as_view().set_layout_params(env, params)?;
            layout.as_view_group().add_view(env, &tab)?;
            let tab = env.new_global_ref(tab)?;

            let on_select = Arc::new(SyncCallback::new());
            let select_proxy = DynamicProxy::build(
                env,
                &LoaderContext::FromObject(&act),
                [TabLayoutOnTabSelectedListener::class_name()],
                {
                    let on_select = on_select.clone();
                    move |_env, _method, _args| {
                        on_select.signal(());
                        Ok(JObject::null())
                    }
                },
            )?;
            tab.add_on_tab_selected_listener(env, &select_proxy)?;

            let pager = ViewPager2::new(env, &act)?;
            let params = LinearLayoutLayoutParams::with_weight(env, -1, -1, 1.0)?;
            pager.as_view().set_layout_params(env, params)?;
            layout.as_view_group().add_view(env, &pager)?;
            let pager = env.new_global_ref(pager)?;

            let adapter = WinioTabViewAdapter::new(env)?;
            pager.set_adapter(env, &adapter)?;
            let adapter = env.new_global_ref(adapter)?;

            let tab_titles = Arc::new(Mutex::new(Vec::<Global<JString<'static>>>::new()));

            let tab_proxy = DynamicProxy::build(
                env,
                &LoaderContext::FromObject(&act),
                [TabLayoutMediatorTabConfigurationStrategy::class_name()],
                {
                    let titles = tab_titles.clone();
                    move |env, _method, args| {
                        let tab = args.get_element(env, 0)?;
                        let position = args.get_element(env, 1)?;
                        let position =
                            unsafe { JInteger::from_raw(env, position.into_raw()) }.value(env)?;
                        let titles = titles.lock().unwrap();
                        if let Some(title) = titles.get(position as usize) {
                            let tab = unsafe { TabLayoutTab::from_raw(env, tab.into_raw()) };
                            tab.set_text(env, title)?;
                        }
                        Ok(JObject::null())
                    }
                },
            )?;

            let mediator = TabLayoutMediator::new(env, &tab, &pager, &tab_proxy)?;
            mediator.attach(env)?;
            let mediator = env.new_global_ref(mediator)?;

            Ok(Self {
                handle: layout,
                tab,
                pager,
                mediator,
                tab_titles,
                tab_proxy,
                on_select,
                select_proxy,
                adapter,
            })
        })
    }

    pub fn is_visible(&self) -> Result<bool>;

    pub fn set_visible(&mut self, v: bool) -> Result<()>;

    pub fn is_enabled(&self) -> Result<bool>;

    pub fn set_enabled(&mut self, v: bool) -> Result<()>;

    pub fn loc(&self) -> Result<Point>;

    pub fn set_loc(&mut self, p: Point) -> Result<()>;

    pub fn size(&self) -> Result<Size>;

    pub fn set_size(&mut self, v: Size) -> Result<()>;

    pub fn selection(&self) -> Result<Option<usize>> {
        vm_exec(|env| {
            let pos = self.tab.get_selected_tab_position(env)?;
            if pos >= 0 {
                Ok(Some(pos as usize))
            } else {
                Ok(None)
            }
        })
    }

    pub fn set_selection(&mut self, i: usize) -> Result<()> {
        vm_exec(|env| {
            let tab = self.tab.get_tab_at(env, i as _)?;
            self.tab.select_tab(env, &tab)?;
            Ok(())
        })
    }

    pub fn insert(&mut self, i: usize, item: &TabViewItem) -> Result<()> {
        vm_exec(|env| {
            let pages = self.adapter.get_pages(env)?;
            pages.add(env, item.handle.as_widget().to_android())?;
            let title = env.new_string(item.text()?)?;
            self.tab_titles
                .lock()
                .unwrap()
                .insert(i, env.new_global_ref(title)?);
            self.adapter.notify_item_inserted(env, i as _)?;
            Ok(())
        })
    }

    pub fn remove(&mut self, i: usize) -> Result<()> {
        vm_exec(|env| {
            let pages = self.adapter.get_pages(env)?;
            pages.remove(env, i as _)?;
            self.adapter.notify_item_removed(env, i as _)?;
            Ok(())
        })
    }

    pub fn len(&self) -> Result<usize> {
        vm_exec(|env| {
            let pages = self.adapter.get_pages(env)?;
            Ok(pages.size(env)? as usize)
        })
    }

    pub fn is_empty(&self) -> Result<bool> {
        Ok(self.len()? == 0)
    }

    pub fn clear(&mut self) -> Result<()> {
        vm_exec(|env| {
            let pages = self.adapter.get_pages(env)?;
            let len = pages.size(env)?;
            pages.clear(env)?;
            self.adapter.notify_item_range_removed(env, 0, len)?;
            Ok(())
        })
    }

    pub async fn wait_select(&self) {
        self.on_select.wait().await;
    }
}

winio_handle::impl_as_widget!(TabView, handle);

#[derive(Debug)]
pub struct TabViewItem {
    handle: View,
    text: String,
    #[allow(unused)]
    on_resize_proxy: DynamicProxy,
}

impl TabViewItem {
    pub fn new() -> Result<Self> {
        let handle = View::new_standalone()?;
        vm_exec(|env| {
            let on_resize_proxy = DynamicProxy::build(
                env,
                &LoaderContext::None,
                [ViewOnLayoutChangeListener::class_name()],
                {
                    move |env, method, args| {
                        let name = method.get_name(env)?;
                        if name.try_to_string(env)? == "onLayoutChange" {
                            let mut get_element = |i: usize| -> jni::errors::Result<i32> {
                                let obj = args.get_element(env, i)?;
                                let int = unsafe { JInteger::from_raw(env, obj.into_raw()) };
                                int.value(env)
                            };

                            let left = get_element(1)?;
                            let top = get_element(2)?;
                            let right = get_element(3)?;
                            let bottom = get_element(4)?;
                            let old_left = get_element(5)?;
                            let old_top = get_element(6)?;
                            let old_right = get_element(7)?;
                            let old_bottom = get_element(8)?;

                            if left != old_left
                                || top != old_top
                                || right != old_right
                                || bottom != old_bottom
                            {
                                let callback = WINDOW_RESIZE_CALLBACK.lock().unwrap();
                                if let Some(callback) = callback.as_ref() {
                                    callback.signal(());
                                }
                            }
                        }
                        Ok(JObject::null())
                    }
                },
            )?;
            let view = handle.as_widget().to_android();
            let view = env.new_local_ref(view)?;
            let view = unsafe { AView::from_raw(env, view.into_raw()) };
            view.add_on_layout_change_listener(env, &on_resize_proxy)?;
            Ok(Self {
                handle,
                text: String::new(),
                on_resize_proxy,
            })
        })
    }

    pub fn text(&self) -> Result<String> {
        Ok(self.text.clone())
    }

    pub fn set_text(&mut self, s: impl AsRef<str>) -> Result<()> {
        self.text = s.as_ref().into();
        Ok(())
    }

    pub fn size(&self) -> Result<Size> {
        self.handle.size()
    }
}

winio_handle::impl_as_container!(TabViewItem, handle);