graphix-package-tui 0.9.0

A dataflow language for UIs and network programming, TUI package
Documentation
use super::{compile, DirectionV, FlexV, SizeV, TuiW, TuiWidget};
use anyhow::{bail, Context, Result};
use arcstr::ArcStr;
use async_trait::async_trait;
use crossterm::event::Event;
use futures::future;
use graphix_compiler::expr::ExprId;
use graphix_rt::{GXExt, GXHandle, Ref, TRef};
use netidx::publisher::{FromValue, Value};
use ratatui::{
    layout::{Constraint, Layout, Rect, Spacing},
    Frame,
};
use smallvec::SmallVec;
use tokio::try_join;

#[derive(Clone, Copy)]
pub(super) struct ConstraintV(pub Constraint);

impl FromValue for ConstraintV {
    fn from_value(v: Value) -> Result<Self> {
        let t = match &v.cast_to::<SmallVec<[Value; 3]>>()?[..] {
            [Value::String(s), Value::I64(p)] => match &**s {
                "Min" => Constraint::Min(*p as u16),
                "Max" => Constraint::Max(*p as u16),
                "Percentage" => Constraint::Percentage(*p as u16),
                "Fill" => Constraint::Fill(*p as u16),
                s => bail!("invalid constraint tag {s}"),
            },
            [Value::String(s), Value::I64(n), Value::I64(d)] if &**s == "Ratio" => {
                Constraint::Ratio(*n as u32, *d as u32)
            }
            v => bail!("invalid constraint {v:?}"),
        };
        Ok(Self(t))
    }
}

#[derive(Clone)]
struct SpacingV(Spacing);

impl FromValue for SpacingV {
    fn from_value(v: Value) -> Result<Self> {
        let t = match v.cast_to::<(ArcStr, u16)>()? {
            (s, p) if &*s == "Space" => Spacing::Space(p),
            (s, p) if &*s == "Overlap" => Spacing::Overlap(p),
            (s, _) => bail!("invalid spacing tag {s}"),
        };
        Ok(Self(t))
    }
}

struct ChildW<X: GXExt> {
    size_ref: Ref<X>,
    last_size: SizeV,
    constraint: Constraint,
    child: TuiW,
}

impl<X: GXExt> ChildW<X> {
    async fn compile(gx: GXHandle<X>, v: Value) -> Result<Self> {
        let ((_, child), (_, constraint), (_, size)) =
            v.cast_to::<((ArcStr, Value), (ArcStr, ConstraintV), (ArcStr, u64))>()?;
        let child = compile(gx.clone(), child).await.context("compiling child")?;
        let constraint = constraint.0;
        let size_ref = gx.compile_ref(size).await.context("compiling size ref")?;
        Ok(Self { size_ref, last_size: SizeV::default(), constraint, child })
    }
}

pub(super) struct LayoutW<X: GXExt> {
    gx: GXHandle<X>,
    children: Vec<ChildW<X>>,
    children_ref: Ref<X>,
    direction: TRef<X, Option<DirectionV>>,
    flex: TRef<X, Option<FlexV>>,
    horizontal_margin: TRef<X, Option<u16>>,
    margin: TRef<X, Option<u16>>,
    spacing: TRef<X, Option<SpacingV>>,
    vertical_margin: TRef<X, Option<u16>>,
    focused: TRef<X, Option<u32>>,
}

impl<X: GXExt> LayoutW<X> {
    pub(super) async fn compile(gx: GXHandle<X>, v: Value) -> Result<TuiW> {
        let [(_, children), (_, direction), (_, flex), (_, focused), (_, horizontal_margin), (_, margin), (_, spacing), (_, vertical_margin)] =
            v.cast_to::<[(ArcStr, u64); 8]>().context("layout fields")?;
        let (
            children_ref,
            direction,
            flex,
            focused,
            horizontal_margin,
            margin,
            spacing,
            vertical_margin,
        ) = try_join! {
            gx.compile_ref(children),
            gx.compile_ref(direction),
            gx.compile_ref(flex),
            gx.compile_ref(focused),
            gx.compile_ref(horizontal_margin),
            gx.compile_ref(margin),
            gx.compile_ref(spacing),
            gx.compile_ref(vertical_margin)
        }?;
        let direction = TRef::<X, Option<DirectionV>>::new(direction)
            .context("layout tref direction")?;
        let flex = TRef::<X, Option<FlexV>>::new(flex).context("layout tref flex")?;
        let horizontal_margin = TRef::<X, Option<u16>>::new(horizontal_margin)
            .context("layout tref horizontal_margin")?;
        let margin = TRef::<X, Option<u16>>::new(margin).context("layout tref margin")?;
        let spacing =
            TRef::<X, Option<SpacingV>>::new(spacing).context("layout tref spacing")?;
        let vertical_margin = TRef::<X, Option<u16>>::new(vertical_margin)
            .context("layout tref vertical_margin")?;
        let focused =
            TRef::<X, Option<u32>>::new(focused).context("layout tref focused")?;
        let mut t = Self {
            gx,
            children: vec![],
            children_ref,
            direction,
            flex,
            horizontal_margin,
            margin,
            spacing,
            vertical_margin,
            focused,
        };
        if let Some(v) = t.children_ref.last.take() {
            t.set_children(v).await?;
        }
        Ok(Box::new(t))
    }

    async fn set_children(&mut self, v: Value) -> Result<()> {
        self.children =
            future::join_all(v.cast_to::<SmallVec<[Value; 8]>>()?.into_iter().map(|v| {
                let gx = self.gx.clone();
                async move {
                    let child = ChildW::compile(gx, v).await?;
                    Ok(child)
                }
            }))
            .await
            .into_iter()
            .collect::<Result<Vec<_>>>()?;
        Ok(())
    }
}

#[async_trait]
impl<X: GXExt> TuiWidget for LayoutW<X> {
    async fn handle_event(&mut self, e: Event, v: Value) -> Result<()> {
        let idx = self.focused.t.and_then(|o| o.map(|i| i as usize)).unwrap_or(0);
        if let Some(c) = self.children.get_mut(idx) {
            c.child.handle_event(e, v).await?;
        }
        Ok(())
    }

    async fn handle_update(&mut self, id: ExprId, v: Value) -> Result<()> {
        let Self {
            gx: _,
            children: _,
            children_ref,
            direction,
            flex,
            focused,
            horizontal_margin,
            margin,
            spacing,
            vertical_margin,
        } = self;
        direction.update(id, &v).context("layout direction update")?;
        flex.update(id, &v).context("layout flex update")?;
        focused.update(id, &v).context("layout focused update")?;
        horizontal_margin.update(id, &v).context("layout horizontal_margin update")?;
        margin.update(id, &v).context("layout margin update")?;
        spacing.update(id, &v).context("layout spacing update")?;
        vertical_margin.update(id, &v).context("layout vertical_margin update")?;
        if children_ref.id == id {
            self.set_children(v.clone()).await?;
        }
        for c in &mut self.children {
            c.child.handle_update(id, v.clone()).await?
        }
        Ok(())
    }

    fn draw(&mut self, frame: &mut Frame, rect: Rect) -> Result<()> {
        let Self {
            gx: _,
            children,
            children_ref: _,
            direction,
            flex,
            focused: _,
            horizontal_margin,
            margin,
            spacing,
            vertical_margin,
        } = self;
        let mut layout = Layout::default();
        if let Some(Some(d)) = direction.t {
            layout = layout.direction(d.0);
        }
        if let Some(Some(f)) = flex.t {
            layout = layout.flex(f.0);
        }
        if let Some(Some(m)) = horizontal_margin.t {
            layout = layout.horizontal_margin(m);
        }
        if let Some(Some(m)) = margin.t {
            layout = layout.margin(m);
        }
        if let Some(Some(s)) = &spacing.t {
            layout = layout.spacing(s.0.clone());
        }
        if let Some(Some(m)) = vertical_margin.t {
            layout = layout.vertical_margin(m);
        }
        layout = layout.constraints(children.iter().map(|c| c.constraint));
        let areas = layout.split(rect);
        for (rect, child) in areas.iter().zip(children.iter_mut()) {
            let size = SizeV::from(*rect);
            if child.last_size != size {
                child.last_size = size;
                child.size_ref.set_deref(size)?;
            }
            child.child.draw(frame, *rect)?
        }
        Ok(())
    }
}