Skip to main content

graphix_package_gui/widgets/
mod.rs

1use anyhow::{bail, Context, Result};
2use arcstr::ArcStr;
3use graphix_compiler::expr::ExprId;
4use graphix_rt::{CallableId, GXExt, GXHandle};
5use netidx::{protocol::valarray::ValArray, publisher::Value};
6use smallvec::SmallVec;
7use std::{future::Future, pin::Pin};
8
9use crate::types::{HAlignV, LengthV, PaddingV, VAlignV};
10
11/// Compile an optional callable ref during widget construction.
12macro_rules! compile_callable {
13    ($gx:expr, $ref:ident, $label:expr) => {
14        match $ref.last.as_ref() {
15            Some(v) => Some($gx.compile_callable(v.clone()).await.context($label)?),
16            None => None,
17        }
18    };
19}
20
21/// Recompile a callable ref inside `handle_update`.
22macro_rules! update_callable {
23    ($self:ident, $rt:ident, $id:ident, $v:ident, $field:ident, $callable:ident, $label:expr) => {
24        if $id == $self.$field.id {
25            $self.$field.last = Some($v.clone());
26            $self.$callable = Some(
27                $rt.block_on($self.gx.compile_callable($v.clone()))
28                    .context($label)?,
29            );
30        }
31    };
32}
33
34/// Compile a child widget ref during widget construction.
35macro_rules! compile_child {
36    ($gx:expr, $ref:ident, $label:expr) => {
37        match $ref.last.as_ref() {
38            None => Box::new(super::EmptyW) as GuiW<X>,
39            Some(v) => compile($gx.clone(), v.clone()).await.context($label)?,
40        }
41    };
42}
43
44/// Recompile a child widget ref inside `handle_update`.
45/// Sets `$changed = true` when the child is recompiled or updated.
46macro_rules! update_child {
47    ($self:ident, $rt:ident, $id:ident, $v:ident, $changed:ident, $ref:ident, $child:ident, $label:expr) => {
48        if $id == $self.$ref.id {
49            $self.$ref.last = Some($v.clone());
50            $self.$child = $rt
51                .block_on(compile($self.gx.clone(), $v.clone()))
52                .context($label)?;
53            $changed = true;
54        }
55        $changed |= $self.$child.handle_update($rt, $id, $v)?;
56    };
57}
58
59pub mod button;
60pub mod canvas;
61pub mod chart;
62pub mod combo_box;
63pub mod container;
64pub mod context_menu;
65pub mod context_menu_widget;
66pub mod grid;
67pub mod iced_keyboard_area;
68pub mod image;
69pub mod keyboard_area;
70pub mod markdown;
71pub mod menu_bar;
72pub mod menu_bar_widget;
73pub mod mouse_area;
74pub mod pick_list;
75pub mod progress_bar;
76pub mod qr_code;
77pub mod radio;
78pub mod rule;
79pub mod scrollable;
80pub mod slider;
81pub mod space;
82pub mod stack;
83pub mod table;
84pub mod text;
85pub mod text_editor;
86pub mod text_input;
87pub mod toggle;
88pub mod tooltip;
89
90/// Concrete iced renderer type used throughout the GUI package.
91/// Must match iced_widget's default Renderer parameter.
92pub type Renderer = iced_renderer::Renderer;
93
94/// Concrete iced Element type with our Message/Theme/Renderer.
95pub type IcedElement<'a> =
96    iced_core::Element<'a, Message, crate::theme::GraphixTheme, Renderer>;
97
98/// Message type for iced widget interactions.
99#[derive(Debug, Clone)]
100pub enum Message {
101    Nop,
102    Call(CallableId, ValArray),
103    EditorAction(ExprId, iced_widget::text_editor::Action),
104}
105
106/// Trait for GUI widgets. Unlike TUI widgets, GUI widgets are not
107/// async — handle_update is synchronous, and the view method builds
108/// an iced Element tree.
109pub trait GuiWidget<X: GXExt>: Send + 'static {
110    /// Process a value update from graphix. Widgets that own child
111    /// refs use `rt` to `block_on` recompilation of their subtree.
112    /// Returns `true` if the widget changed and the window should redraw.
113    fn handle_update(
114        &mut self,
115        rt: &tokio::runtime::Handle,
116        id: ExprId,
117        v: &Value,
118    ) -> Result<bool>;
119
120    /// Build the iced Element tree for rendering.
121    fn view(&self) -> IcedElement<'_>;
122
123    /// Route a text editor action to the widget that owns the given
124    /// content ref. Returns `Some((callable_id, value))` if the action
125    /// was an edit and the result should be called back to graphix.
126    fn editor_action(
127        &mut self,
128        id: ExprId,
129        action: &iced_widget::text_editor::Action,
130    ) -> Option<(CallableId, Value)> {
131        let _ = (id, action);
132        None
133    }
134}
135
136pub type GuiW<X> = Box<dyn GuiWidget<X>>;
137
138/// Future type for widget compilation (avoids infinite-size async fn).
139pub type CompileFut<X> =
140    Pin<Box<dyn Future<Output = Result<GuiW<X>>> + Send + 'static>>;
141
142/// Empty widget placeholder.
143pub struct EmptyW;
144
145impl<X: GXExt> GuiWidget<X> for EmptyW {
146    fn handle_update(
147        &mut self,
148        _rt: &tokio::runtime::Handle,
149        _id: ExprId,
150        _v: &Value,
151    ) -> Result<bool> {
152        Ok(false)
153    }
154
155    fn view(&self) -> IcedElement<'_> {
156        iced_widget::Space::new().into()
157    }
158}
159
160/// Generate a flex layout widget (Row or Column). All parameters use
161/// call-site tokens to satisfy macro hygiene for local variable names.
162macro_rules! flex_widget {
163    ($name:ident, $label:literal,
164     $spacing:ident, $padding:ident, $width:ident, $height:ident,
165     $align_ty:ty, $align:ident, $Widget:ident, $align_set:ident,
166     [$($f:ident),+]) => {
167        pub(crate) struct $name<X: GXExt> {
168            gx: GXHandle<X>,
169            $spacing: graphix_rt::TRef<X, f64>,
170            $padding: graphix_rt::TRef<X, PaddingV>,
171            $width: graphix_rt::TRef<X, LengthV>,
172            $height: graphix_rt::TRef<X, LengthV>,
173            $align: graphix_rt::TRef<X, $align_ty>,
174            children_ref: graphix_rt::Ref<X>,
175            children: Vec<GuiW<X>>,
176        }
177
178        impl<X: GXExt> $name<X> {
179            pub(crate) async fn compile(gx: GXHandle<X>, source: Value) -> Result<GuiW<X>> {
180                let [(_, children), $((_, $f)),+] =
181                    source.cast_to::<[(ArcStr, u64); 6]>()
182                        .context(concat!($label, " flds"))?;
183                let (children_ref, $($f),+) = tokio::try_join!(
184                    gx.compile_ref(children),
185                    $(gx.compile_ref($f)),+
186                )?;
187                let compiled_children = match children_ref.last.as_ref() {
188                    None => vec![],
189                    Some(v) => compile_children(gx.clone(), v.clone()).await
190                        .context(concat!($label, " children"))?,
191                };
192                Ok(Box::new(Self {
193                    gx: gx.clone(),
194                    $spacing: graphix_rt::TRef::new($spacing)
195                        .context(concat!($label, " tref spacing"))?,
196                    $padding: graphix_rt::TRef::new($padding)
197                        .context(concat!($label, " tref padding"))?,
198                    $width: graphix_rt::TRef::new($width)
199                        .context(concat!($label, " tref width"))?,
200                    $height: graphix_rt::TRef::new($height)
201                        .context(concat!($label, " tref height"))?,
202                    $align: graphix_rt::TRef::new($align)
203                        .context(concat!($label, " tref ", stringify!($align)))?,
204                    children_ref,
205                    children: compiled_children,
206                }))
207            }
208        }
209
210        impl<X: GXExt> GuiWidget<X> for $name<X> {
211            fn handle_update(
212                &mut self,
213                rt: &tokio::runtime::Handle,
214                id: ExprId,
215                v: &Value,
216            ) -> Result<bool> {
217                let mut changed = false;
218                changed |= self.$spacing.update(id, v)
219                    .context(concat!($label, " update spacing"))?.is_some();
220                changed |= self.$padding.update(id, v)
221                    .context(concat!($label, " update padding"))?.is_some();
222                changed |= self.$width.update(id, v)
223                    .context(concat!($label, " update width"))?.is_some();
224                changed |= self.$height.update(id, v)
225                    .context(concat!($label, " update height"))?.is_some();
226                changed |= self.$align.update(id, v)
227                    .context(concat!($label, " update ", stringify!($align)))?.is_some();
228                if id == self.children_ref.id {
229                    self.children_ref.last = Some(v.clone());
230                    self.children = rt.block_on(
231                        compile_children(self.gx.clone(), v.clone())
232                    ).context(concat!($label, " children recompile"))?;
233                    changed = true;
234                }
235                for child in &mut self.children {
236                    changed |= child.handle_update(rt, id, v)?;
237                }
238                Ok(changed)
239            }
240
241            fn editor_action(
242                &mut self,
243                id: ExprId,
244                action: &iced_widget::text_editor::Action,
245            ) -> Option<(CallableId, Value)> {
246                for child in &mut self.children {
247                    if let some @ Some(_) = child.editor_action(id, action) {
248                        return some;
249                    }
250                }
251                None
252            }
253
254            fn view(&self) -> IcedElement<'_> {
255                let mut w = iced_widget::$Widget::new();
256                if let Some(sp) = self.$spacing.t {
257                    w = w.spacing(sp as f32);
258                }
259                if let Some(p) = self.$padding.t.as_ref() {
260                    w = w.padding(p.0);
261                }
262                if let Some(wi) = self.$width.t.as_ref() {
263                    w = w.width(wi.0);
264                }
265                if let Some(h) = self.$height.t.as_ref() {
266                    w = w.height(h.0);
267                }
268                if let Some(a) = self.$align.t.as_ref() {
269                    w = w.$align_set(a.0);
270                }
271                for child in &self.children {
272                    w = w.push(child.view());
273                }
274                w.into()
275            }
276        }
277    };
278}
279
280flex_widget!(
281    RowW,
282    "row",
283    spacing,
284    padding,
285    width,
286    height,
287    VAlignV,
288    valign,
289    Row,
290    align_y,
291    [height, padding, spacing, valign, width]
292);
293
294flex_widget!(
295    ColumnW,
296    "column",
297    spacing,
298    padding,
299    width,
300    height,
301    HAlignV,
302    halign,
303    Column,
304    align_x,
305    [halign, height, padding, spacing, width]
306);
307
308/// Compile a widget value into a GuiW. Returns a boxed future to
309/// avoid infinite-size futures from recursive async calls.
310pub fn compile<X: GXExt>(gx: GXHandle<X>, source: Value) -> CompileFut<X> {
311    Box::pin(async move {
312        let (s, v) = source.cast_to::<(ArcStr, Value)>()?;
313        match s.as_str() {
314            "Text" => text::TextW::compile(gx, v).await,
315            "Column" => ColumnW::compile(gx, v).await,
316            "Row" => RowW::compile(gx, v).await,
317            "Container" => container::ContainerW::compile(gx, v).await,
318            "Grid" => grid::GridW::compile(gx, v).await,
319            "Button" => button::ButtonW::compile(gx, v).await,
320            "Space" => space::SpaceW::compile(gx, v).await,
321            "TextInput" => text_input::TextInputW::compile(gx, v).await,
322            "Checkbox" => toggle::CheckboxW::compile(gx, v).await,
323            "Toggler" => toggle::TogglerW::compile(gx, v).await,
324            "Slider" => slider::SliderW::compile(gx, v).await,
325            "ProgressBar" => progress_bar::ProgressBarW::compile(gx, v).await,
326            "Scrollable" => scrollable::ScrollableW::compile(gx, v).await,
327            "HorizontalRule" => rule::HorizontalRuleW::compile(gx, v).await,
328            "VerticalRule" => rule::VerticalRuleW::compile(gx, v).await,
329            "Tooltip" => tooltip::TooltipW::compile(gx, v).await,
330            "PickList" => pick_list::PickListW::compile(gx, v).await,
331            "Stack" => stack::StackW::compile(gx, v).await,
332            "Radio" => radio::RadioW::compile(gx, v).await,
333            "VerticalSlider" => slider::VerticalSliderW::compile(gx, v).await,
334            "ComboBox" => combo_box::ComboBoxW::compile(gx, v).await,
335            "TextEditor" => text_editor::TextEditorW::compile(gx, v).await,
336            "KeyboardArea" => keyboard_area::KeyboardAreaW::compile(gx, v).await,
337            "MouseArea" => mouse_area::MouseAreaW::compile(gx, v).await,
338            "Image" => image::ImageW::compile(gx, v).await,
339            "Canvas" => canvas::CanvasW::compile(gx, v).await,
340            "ContextMenu" => context_menu::ContextMenuW::compile(gx, v).await,
341            "Chart" => chart::ChartW::compile(gx, v).await,
342            "Markdown" => markdown::MarkdownW::compile(gx, v).await,
343            "MenuBar" => menu_bar::MenuBarW::compile(gx, v).await,
344            "QrCode" => qr_code::QrCodeW::compile(gx, v).await,
345            "Table" => table::TableW::compile(gx, v).await,
346            _ => bail!("invalid gui widget type `{s}({v})"),
347        }
348    })
349}
350
351/// Compile an array of widget values into a Vec of GuiW.
352pub async fn compile_children<X: GXExt>(
353    gx: GXHandle<X>,
354    v: Value,
355) -> Result<Vec<GuiW<X>>> {
356    let items = v.cast_to::<SmallVec<[Value; 8]>>()?;
357    let futs: Vec<_> = items.into_iter().map(|item| compile(gx.clone(), item)).collect();
358    futures::future::try_join_all(futs).await
359}