winio-ui-android 0.2.0-beta.2

Android backend for winio.
use inherit_methods_macro::inherit_methods;
use jni::{Env, refs::Global};
use winio_handle::{AsContainer, BorrowedContainer};
use winio_primitive::{Point, Size};

use crate::{
    BaseWidget, JCharSequenceExt, Result, current_activity,
    java::{
        android::{
            view::{View as AView, gravity},
            widget::{FrameLayout, FrameLayoutLayoutParams, TextView as ATextView},
        },
        material::MaterialCardView,
    },
    vm_exec,
};

const TITLE_TEXT_SIZE: f32 = 18.0;
const PADDING_DP: f32 = 16.0;
const TITLE_GAP_DP: f32 = 8.0;
const TITLE_PADDING_DP: f32 = 4.0;

#[derive(Debug)]
pub struct GroupBox {
    handle: BaseWidget<MaterialCardView<'static>>,
    title: Global<ATextView<'static>>,
    content: Global<FrameLayout<'static>>,
    density: f32,
    padding: i32,
    title_gap: i32,
    title_height: i32,
}

#[inherit_methods(from = "self.handle")]
impl GroupBox {
    pub fn new(parent: impl AsContainer) -> Result<Self> {
        vm_exec(|env| {
            let act = current_activity(env)?;
            let resources = act.as_context().get_resources(env)?;
            let metrics = resources.get_display_metrics(env)?;
            let density = metrics.density(env)?;
            let padding = (PADDING_DP * density) as i32;
            let title_gap = (TITLE_GAP_DP * density) as i32;

            let card = MaterialCardView::new(env, &act)?;
            card.set_content_padding(env, 0, 0, 0, 0)?;
            card.set_card_elevation(env, 0.0)?;
            let handle = BaseWidget::new_with_env(env, parent.as_container(), card)?;

            let title = ATextView::new(env, &act)?;
            title.set_text_size(env, TITLE_TEXT_SIZE)?;
            title.set_single_line(env, true)?;
            title.set_include_font_padding(env, false)?;
            title.set_gravity(env, gravity::CENTER_VERTICAL | gravity::LEFT)?;
            let title_height = title.get_line_height(env)? + (TITLE_PADDING_DP * density) as i32;

            let content = FrameLayout::new(env, &act)?;
            content.as_view_group().set_clip_children(env, true)?;

            handle.as_view_group().add_view(env, content.as_view())?;
            handle.as_view_group().add_view(env, title.as_view())?;

            let title = env.new_global_ref(title)?;
            let content = env.new_global_ref(content)?;

            let this = Self {
                handle,
                title,
                content,
                density,
                padding,
                title_gap,
                title_height,
            };
            this.update_layout(env, 0, 0)?;
            Ok(this)
        })
    }

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

    pub fn set_visible(&mut self, visible: 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, size: Size) -> Result<()> {
        self.handle.set_size(size)?;
        let width = (size.width * self.density as f64) as i32;
        let height = (size.height * self.density as f64) as i32;
        vm_exec(move |env| self.update_layout(env, width, height))
    }

    pub fn preferred_size(&self) -> Result<Size> {
        let width = vm_exec(|env| -> Result<i32> {
            self.title.as_view().measure(env, 0, 0)?;
            Ok(self.title.as_view().get_measured_width(env)?)
        })?;
        Ok(Size::new(
            (width + self.padding * 2) as f64 / self.density as f64,
            (self.title_height + self.padding * 2 + self.title_gap) as f64 / self.density as f64,
        ))
    }

    pub fn client_size(&self) -> Result<Size> {
        let (width, height) = vm_exec(|env| -> Result<(i32, i32)> {
            let params = self.content.as_view().get_layout_params(env)?;
            let params = unsafe { FrameLayoutLayoutParams::from_raw(env, params.into_raw()) };
            let width = params.as_base().width(env)?;
            let height = params.as_base().height(env)?;
            Ok((width, height))
        })?;
        Ok(Size::new(
            width as f64 / self.density as f64,
            height as f64 / self.density as f64,
        ))
    }

    pub fn text(&self) -> Result<String> {
        vm_exec(|env| Ok(self.title.get_text(env)?.try_to_string(env)?))
    }

    pub fn set_text(&mut self, text: impl AsRef<str>) -> Result<()> {
        vm_exec(move |env| {
            let text = env.new_string(&text)?;
            self.title.set_text(env, text)?;
            Ok(())
        })
    }

    fn update_layout(&self, env: &mut Env<'_>, width: i32, height: i32) -> Result<()> {
        let inner_width = (width - self.padding * 2).max(0);
        set_child_layout(
            env,
            &self.title.as_view(),
            self.padding,
            self.padding,
            inner_width,
            self.title_height,
        )?;
        let content_top = self.padding + self.title_height + self.title_gap;
        let content_height = (height - content_top - self.padding).max(0);
        set_child_layout(
            env,
            &self.content.as_view(),
            self.padding,
            content_top,
            inner_width,
            content_height,
        )
    }
}

fn set_child_layout(
    env: &mut Env<'_>,
    view: &AView<'_>,
    left: i32,
    top: i32,
    width: i32,
    height: i32,
) -> Result<()> {
    let params = FrameLayoutLayoutParams::new(env, width, height)?;
    params.as_margin().set_left_margin(env, left)?;
    params.as_margin().set_top_margin(env, top)?;
    params.set_gravity(env, gravity::LEFT | gravity::TOP)?;
    view.set_layout_params(env, params)?;
    Ok(())
}

winio_handle::impl_as_widget!(GroupBox, handle);

impl AsContainer for GroupBox {
    fn as_container(&self) -> BorrowedContainer<'_> {
        unsafe { BorrowedContainer::android(self.content.as_obj()) }
    }
}