graphix-package-gui 0.9.0

A dataflow language for UIs and network programming, GUI package
Documentation
use super::{GuiW, IcedElement, Message};
use crate::types::LengthV;
use anyhow::{Context, Result};
use arcstr::ArcStr;
use graphix_compiler::expr::ExprId;
use graphix_rt::{Callable, GXExt, GXHandle, Ref, TRef};
use iced_widget as widget;
use netidx::{protocol::valarray::ValArray, publisher::Value};
use tokio::try_join;

// TODO: Known issue — links in markdown flicker their hover
// underline when the cursor crosses between adjacent link spans
// (e.g. the words of a multi-word link, which pulldown-cmark splits
// into separate Text events and iced renders as separate spans).
//
// The bug is in iced's `iced_widget::text::rich::update`: it only
// calls `shell.request_redraw()` when the *presence* of a hovered
// link flips (`was_hovered != hovered_link.is_some()`), not when the
// hovered *index* changes. Moving from link-span-A to link-span-B
// updates state silently; the next unrelated repaint catches up,
// producing the flicker.
//
// Waiting on an upstream fix rather than carrying a local patch or a
// `Viewer` workaround that trades correct behavior (hover underlines)
// for a cosmetic change (always underline).

pub(crate) struct MarkdownW<X: GXExt> {
    gx: GXHandle<X>,
    content: TRef<X, String>,
    on_link: Ref<X>,
    on_link_callable: Option<Callable<X>>,
    spacing: TRef<X, Option<f64>>,
    text_size: TRef<X, Option<f64>>,
    width: TRef<X, LengthV>,
    items: Vec<widget::markdown::Item>,
}

impl<X: GXExt> MarkdownW<X> {
    pub(crate) async fn compile(gx: GXHandle<X>, source: Value) -> Result<GuiW<X>> {
        let [(_, content), (_, on_link), (_, spacing), (_, text_size), (_, width)] =
            source.cast_to::<[(ArcStr, u64); 5]>().context("markdown flds")?;
        let (content_ref, on_link, spacing, text_size, width) = try_join! {
            gx.compile_ref(content),
            gx.compile_ref(on_link),
            gx.compile_ref(spacing),
            gx.compile_ref(text_size),
            gx.compile_ref(width),
        }?;
        let callable = compile_callable!(gx, on_link, "markdown on_link");
        let content = TRef::new(content_ref).context("markdown tref content")?;
        let items = match content.t.as_deref() {
            Some(s) => widget::markdown::parse(s).collect(),
            None => vec![],
        };
        Ok(Box::new(Self {
            gx: gx.clone(),
            content,
            on_link,
            on_link_callable: callable,
            spacing: TRef::new(spacing).context("markdown tref spacing")?,
            text_size: TRef::new(text_size).context("markdown tref text_size")?,
            width: TRef::new(width).context("markdown tref width")?,
            items,
        }))
    }
}

impl<X: GXExt> super::GuiWidget<X> for MarkdownW<X> {
    fn handle_update(
        &mut self,
        rt: &tokio::runtime::Handle,
        id: ExprId,
        v: &Value,
    ) -> Result<bool> {
        let mut changed = false;
        if let Some(_) = self.content.update(id, v).context("markdown update content")? {
            self.items = match self.content.t.as_deref() {
                Some(s) => widget::markdown::parse(s).collect(),
                None => vec![],
            };
            changed = true;
        }
        changed |=
            self.spacing.update(id, v).context("markdown update spacing")?.is_some();
        changed |=
            self.text_size.update(id, v).context("markdown update text_size")?.is_some();
        changed |= self.width.update(id, v).context("markdown update width")?.is_some();
        update_callable!(
            self,
            rt,
            id,
            v,
            on_link,
            on_link_callable,
            "markdown on_link recompile"
        );
        Ok(changed)
    }

    fn view(&self) -> IcedElement<'_> {
        let text_size = self.text_size.t.flatten().unwrap_or(16.0) as f32;
        let settings =
            widget::markdown::Settings::with_text_size(text_size, iced_core::Theme::Dark);
        let on_link_id = self.on_link_callable.as_ref().map(|c| c.id());
        let md: iced_core::Element<'_, widget::markdown::Uri, _, _> =
            widget::markdown::view(&self.items, settings);
        let element = md.map(move |uri| match on_link_id {
            Some(id) => {
                Message::Call(id, ValArray::from_iter([Value::String(uri.into())]))
            }
            None => Message::Nop,
        });
        let mut container = iced_widget::Container::new(element);
        if let Some(w) = self.width.t.as_ref() {
            container = container.width(w.0);
        }
        container.into()
    }
}