Skip to main content

graphix_package_gui/
lib.rs

1#![doc(
2    html_logo_url = "https://graphix-lang.github.io/graphix/graphix-icon.svg",
3    html_favicon_url = "https://graphix-lang.github.io/graphix/graphix-icon.svg"
4)]
5use async_trait::async_trait;
6use graphix_compiler::{
7    env::Env,
8    expr::{ExprId, ModPath},
9    typ::{Type, TypeRef},
10};
11use graphix_package::CustomDisplay;
12use graphix_rt::{CompExp, GXExt, GXHandle};
13use log::error;
14use netidx::publisher::Value;
15use std::{marker::PhantomData, sync::LazyLock};
16use tokio::sync::oneshot;
17use triomphe::Arc;
18use winit::{event_loop::EventLoopProxy, window::WindowId};
19
20mod clipboard;
21pub mod convert;
22mod event_loop;
23pub mod render;
24pub mod theme;
25pub mod types;
26pub mod widgets;
27pub mod window;
28
29#[cfg(test)]
30mod test;
31
32pub(crate) enum ToGui {
33    Update(ExprId, Value),
34    /// Fires once per resize burst, 100ms after the first `Resized`
35    /// event in the burst. The payload carries only the `WindowId`;
36    /// the event loop reads the most recent size from the window's
37    /// `pending_resize` slot rather than passing it through here.
38    /// Used purely to schedule renders during a drag — never touches
39    /// the graphix size ref (see `ResizeEnd`).
40    ResizeTimer(WindowId),
41    /// Fires once after a resize *burst* ends: the window has not
42    /// received a `Resized` event for `RESIZE_END_DEBOUNCE`. Carries
43    /// the final logical size the runtime's size ref should be set
44    /// to. Separate from `ResizeTimer` so that writing back to the
45    /// size ref — which echoes through the runtime and may trigger
46    /// a feedback `request_inner_size` — happens exactly once per
47    /// drag, not 10x/sec.
48    ResizeEnd(WindowId, crate::types::SizeV),
49    /// Wake the winit event loop so it runs its `about_to_wait`
50    /// render pass, picking up any widget state that was mutated
51    /// outside the iced event cycle (e.g. a netidx subscription
52    /// update setting `cells.dirty = true` on a data_table).
53    Redraw,
54    Stop(oneshot::Sender<()>),
55}
56
57/// Thread-safe handle the event loop hands out to widgets that
58/// update their view state from background tasks (netidx
59/// subscriptions in data_table being the motivating case). Calling
60/// `.wake()` posts a `ToGui::Redraw` which the event loop processes
61/// by flagging every window for redraw. Cheap to clone.
62#[derive(Clone)]
63pub(crate) struct RedrawWaker {
64    proxy: EventLoopProxy<ToGui>,
65}
66
67impl RedrawWaker {
68    pub(crate) fn new(proxy: EventLoopProxy<ToGui>) -> Self {
69        Self { proxy }
70    }
71
72    pub(crate) fn wake(&self) {
73        let _ = self.proxy.send_event(ToGui::Redraw);
74    }
75}
76
77/// Set by the event loop after it creates its proxy, so widgets
78/// compiled before the proxy exists (compilation happens inside the
79/// same call that builds the window) can still pick it up.
80pub(crate) static REDRAW_WAKER: std::sync::OnceLock<RedrawWaker> =
81    std::sync::OnceLock::new();
82
83struct Gui<X: GXExt> {
84    proxy: EventLoopProxy<ToGui>,
85    ph: PhantomData<X>,
86}
87
88impl<X: GXExt> Gui<X> {
89    async fn start(
90        gx: &GXHandle<X>,
91        _env: Env,
92        root: CompExp<X>,
93        stop: oneshot::Sender<()>,
94        run_on_main: graphix_package::MainThreadHandle,
95    ) -> Self {
96        let gx = gx.clone();
97        let (proxy_tx, proxy_rx) = oneshot::channel();
98        let rt_handle = tokio::runtime::Handle::current();
99        run_on_main
100            .run(Box::new(move || {
101                event_loop::run(gx, root, proxy_tx, stop, rt_handle);
102            }))
103            .expect("main thread receiver dropped");
104        let proxy = proxy_rx.await.expect("event loop failed to send proxy");
105        Self { proxy, ph: PhantomData }
106    }
107
108    fn update(&self, id: ExprId, v: Value) {
109        if self.proxy.send_event(ToGui::Update(id, v)).is_err() {
110            error!("could not send update because gui event loop closed")
111        }
112    }
113}
114
115pub static GUITYP: LazyLock<Type> = LazyLock::new(|| {
116    Type::Array(Arc::new(Type::ByRef(Arc::new(Type::Ref (TypeRef {
117        scope: ModPath::root(),
118        name: ModPath::from(["gui", "Window"]),
119        params: Arc::from_iter([]),
120     ..Default::default()})))))
121});
122
123#[async_trait]
124impl<X: GXExt> CustomDisplay<X> for Gui<X> {
125    async fn clear(&mut self) {
126        let (tx, rx) = oneshot::channel::<()>();
127        let _ = self.proxy.send_event(ToGui::Stop(tx));
128        let _ = rx.await;
129    }
130
131    async fn process_update(&mut self, _env: &Env, id: ExprId, v: Value) {
132        self.update(id, v);
133    }
134}
135
136graphix_derive::defpackage! {
137    builtins => [
138        clipboard::ReadText,
139        clipboard::WriteText,
140        clipboard::ReadImage,
141        clipboard::WriteImage,
142        clipboard::ReadHtml,
143        clipboard::WriteHtml,
144        clipboard::ReadFiles,
145        clipboard::WriteFiles,
146        clipboard::Clear,
147    ],
148    is_custom => |gx, env, e| {
149        if let Some(typ) = e.typ.with_deref(|t| t.cloned())
150            && typ != Type::Bottom
151            && typ != Type::Any
152        {
153            GUITYP.contains(env, &typ).unwrap_or(false)
154        } else {
155            false
156        }
157    },
158    init_custom => |gx, env, stop, e, run_on_main| {
159        Ok(Box::new(Gui::<X>::start(gx, env.clone(), e, stop, run_on_main).await))
160    },
161}