Skip to main content

graphix_package_gui/widgets/
mod.rs

1use anyhow::{bail, Context, Result};
2use arcstr::ArcStr;
3use compact_str::CompactString;
4use graphix_compiler::expr::ExprId;
5use graphix_rt::{CallableId, GXExt, GXHandle};
6use netidx::{protocol::valarray::ValArray, publisher::Value};
7use poolshark::local::LPooled;
8use smallvec::SmallVec;
9use std::{future::Future, pin::Pin};
10
11use crate::types::{HAlignV, LengthV, PaddingV, VAlignV};
12
13/// Compile an optional callable ref during widget construction.
14macro_rules! compile_callable {
15    ($gx:expr, $ref:ident, $label:expr) => {
16        match $ref.last.as_ref() {
17            Some(v) => Some($gx.compile_callable(v.clone()).await.context($label)?),
18            None => None,
19        }
20    };
21}
22
23/// Recompile a callable ref inside `handle_update`.
24macro_rules! update_callable {
25    ($self:ident, $rt:ident, $id:ident, $v:ident, $field:ident, $callable:ident, $label:expr) => {
26        if $id == $self.$field.id {
27            $self.$field.last = Some($v.clone());
28            $self.$callable = Some(
29                $rt.block_on($self.gx.compile_callable($v.clone())).context($label)?,
30            );
31        }
32    };
33}
34
35/// Compile a child widget ref during widget construction.
36macro_rules! compile_child {
37    ($gx:expr, $ref:ident, $label:expr) => {
38        match $ref.last.as_ref() {
39            None => Box::new(super::EmptyW) as GuiW<X>,
40            Some(v) => compile($gx.clone(), v.clone()).await.context($label)?,
41        }
42    };
43}
44
45/// Recompile a child widget ref inside `handle_update`.
46/// Sets `$changed = true` when the child is recompiled or updated.
47macro_rules! update_child {
48    ($self:ident, $rt:ident, $id:ident, $v:ident, $changed:ident, $ref:ident, $child:ident, $label:expr) => {
49        if $id == $self.$ref.id {
50            $self.$ref.last = Some($v.clone());
51            $self.$child =
52                $rt.block_on(compile($self.gx.clone(), $v.clone())).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 data_table;
67pub mod grid;
68pub mod iced_keyboard_area;
69pub mod image;
70pub mod keyboard_area;
71pub mod markdown;
72pub mod menu_bar;
73pub mod menu_bar_widget;
74pub mod mouse_area;
75pub mod pick_list;
76pub mod progress_bar;
77pub mod qr_code;
78pub mod radio;
79pub mod rule;
80pub mod scrollable;
81pub mod slider;
82pub mod space;
83pub mod stack;
84pub mod table;
85pub mod text;
86pub mod text_editor;
87pub mod text_input;
88pub mod toggle;
89pub mod tooltip;
90
91/// Concrete iced renderer type used throughout the GUI package.
92/// Must match iced_widget's default Renderer parameter.
93pub type Renderer = iced_renderer::Renderer;
94
95/// Concrete iced Element type with our Message/Theme/Renderer.
96pub type IcedElement<'a> =
97    iced_core::Element<'a, Message, crate::theme::GraphixTheme, Renderer>;
98
99/// Message type for iced widget interactions.
100#[derive(Debug, Clone)]
101pub enum Message {
102    Nop,
103    Call(CallableId, ValArray),
104    EditorAction(ExprId, iced_widget::text_editor::Action),
105    /// Virtual scroll position changed: (offset_x, offset_y, viewport_w, viewport_h)
106    /// All values in logical pixels.
107    Scroll(f32, f32, f32, f32),
108    /// A cell was clicked in a data table (row index, column name).
109    /// Column name is the data_table widget's cached `ColumnSpec.name`
110    /// or one of the synthesized sentinels (`ROW_NAME_KEY`/value-mode);
111    /// either way it's a refcount-bump clone, never a fresh allocation.
112    CellClick(usize, ArcStr),
113    /// A cell was clicked to begin editing (row index, column name).
114    CellEdit(usize, ArcStr),
115    /// Cell edit text changed (new text). `CompactString` keeps small
116    /// edits inline (≤ 24 bytes) without heap traffic on each
117    /// keystroke.
118    CellEditInput(CompactString),
119    /// Cell edit submitted (Enter pressed).
120    CellEditSubmit,
121    /// Cell edit cancelled (Escape or click elsewhere).
122    CellEditCancel,
123    /// Column resize drag started (col_meta index).
124    ColumnResizeStart(usize),
125    /// Cursor moved while a column resize drag might be active
126    /// (cursor x in widget-local coordinates). The event loop filters
127    /// this against the widget's `is_column_resizing` state — only
128    /// widgets currently dragging consume it.
129    ColumnResizeMove(f32),
130    /// Column resize drag ended.
131    ColumnResizeEnd,
132    /// Keyboard navigation in a data table.
133    TableKey(TableKeyAction),
134}
135
136/// Keyboard actions for data table navigation.
137#[derive(Debug, Clone)]
138pub enum TableKeyAction {
139    Up,
140    Down,
141    Left,
142    Right,
143    /// Enter: drill down (fire on_activate)
144    Enter,
145    /// Space: start editing selected cell
146    Space,
147    /// Escape: cancel editing
148    Escape,
149}
150
151/// Context passed to `GuiWidget::on_message` so handlers can read
152/// per-window state (e.g. the cursor position a column-resize needs)
153/// and publish follow-up messages (e.g. a `Call` fired from a drag
154/// update) without the event loop having to know widget specifics.
155pub struct MessageShell {
156    pub cursor_position: iced_core::Point,
157    pub out: LPooled<Vec<Message>>,
158}
159
160impl MessageShell {
161    pub fn new(cursor_position: iced_core::Point) -> Self {
162        Self { cursor_position, out: LPooled::take() }
163    }
164
165    pub fn publish(&mut self, msg: Message) {
166        self.out.push(msg);
167    }
168}
169
170/// Trait for GUI widgets. Unlike TUI widgets, GUI widgets are not
171/// async — handle_update is synchronous, and the view method builds
172/// an iced Element tree.
173pub trait GuiWidget<X: GXExt>: Send + 'static {
174    /// Process a value update from graphix. Widgets that own child
175    /// refs use `rt` to `block_on` recompilation of their subtree.
176    /// Returns `true` if the widget changed and the window should redraw.
177    fn handle_update(
178        &mut self,
179        rt: &tokio::runtime::Handle,
180        id: ExprId,
181        v: &Value,
182    ) -> Result<bool>;
183
184    /// Build the iced Element tree for rendering.
185    fn view(&self) -> IcedElement<'_>;
186
187    /// Child widgets that `on_message` and `before_view` should
188    /// forward to. Leaf widgets return `&mut []` (the default).
189    /// Containers (row, column, container, scrollable, stack, …)
190    /// override this so that messages flow down to nested widgets
191    /// like `data_table` — without it the event loop delivers
192    /// messages to the window's top-level widget only.
193    fn children_mut(&mut self) -> &mut [GuiW<X>] {
194        &mut []
195    }
196
197    fn children(&self) -> &[GuiW<X>] {
198        &[]
199    }
200
201    /// Dispatch a message to the widget. Returns `true` if the
202    /// widget changed and a redraw is needed. Widgets that emit
203    /// follow-up messages (e.g. a `Call` fired from a column-resize
204    /// drag) publish through `shell`. The default implementation
205    /// forwards to children — containers don't need to override.
206    fn on_message(&mut self, msg: &Message, shell: &mut MessageShell) -> bool {
207        let mut changed = false;
208        for child in self.children_mut() {
209            changed |= child.on_message(msg, shell);
210        }
211        changed
212    }
213
214    /// True if this widget (or any descendant) is currently tracking
215    /// a column-resize drag. The event loop polls this to decide
216    /// whether a cursor-moved event should be routed as a drag update.
217    fn is_column_resizing(&self) -> bool {
218        self.children().iter().any(|c| c.is_column_resizing())
219    }
220
221    /// Return a DataTableSnapshot if this widget is a data table.
222    /// Default returns None. Overridden by DataTableW.
223    #[cfg(test)]
224    fn data_table_snapshot(&self) -> Option<DataTableSnapshot> {
225        None
226    }
227
228    /// Downcast escape hatch for tests that need access to a concrete
229    /// widget type. Default panics — only widgets that need test-only
230    /// state inspection (currently just `DataTableW`) override this.
231    #[cfg(test)]
232    fn as_any(&self) -> &dyn std::any::Any {
233        unimplemented!("as_any not implemented for this widget")
234    }
235
236    #[cfg(test)]
237    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
238        unimplemented!("as_any_mut not implemented for this widget")
239    }
240
241    /// Called immediately before `view()` so widgets can flush deferred
242    /// state that arrived asynchronously from background tasks (e.g.
243    /// `data_table` re-sorting after sort-column subscription data
244    /// arrives outside of the graphix update cycle). Returns `true` if
245    /// state changed and the window should redraw. The default forwards
246    /// to children so containers don't have to.
247    fn before_view(&mut self) -> bool {
248        let mut changed = false;
249        for child in self.children_mut() {
250            changed |= child.before_view();
251        }
252        changed
253    }
254}
255
256pub type GuiW<X> = Box<dyn GuiWidget<X>>;
257
258/// Snapshot of data table state for test assertions.
259#[cfg(test)]
260#[derive(Debug, Clone, PartialEq)]
261pub struct DataTableSnapshot {
262    pub col_names: Vec<String>,
263    pub row_basenames: Vec<String>,
264    pub grid: Vec<Vec<String>>,
265    pub is_value_mode: bool,
266    pub selection: Vec<String>,
267}
268
269/// Future type for widget compilation (avoids infinite-size async fn).
270pub type CompileFut<X> = Pin<Box<dyn Future<Output = Result<GuiW<X>>> + Send + 'static>>;
271
272/// Empty widget placeholder.
273pub struct EmptyW;
274
275impl<X: GXExt> GuiWidget<X> for EmptyW {
276    fn handle_update(
277        &mut self,
278        _rt: &tokio::runtime::Handle,
279        _id: ExprId,
280        _v: &Value,
281    ) -> Result<bool> {
282        Ok(false)
283    }
284
285    fn view(&self) -> IcedElement<'_> {
286        iced_widget::Space::new().into()
287    }
288}
289
290/// Generate a flex layout widget (Row or Column). All parameters use
291/// call-site tokens to satisfy macro hygiene for local variable names.
292macro_rules! flex_widget {
293    ($name:ident, $label:literal,
294     $spacing:ident, $padding:ident, $width:ident, $height:ident,
295     $align_ty:ty, $align:ident, $Widget:ident, $align_set:ident,
296     [$($f:ident),+]) => {
297        pub(crate) struct $name<X: GXExt> {
298            gx: GXHandle<X>,
299            $spacing: graphix_rt::TRef<X, f64>,
300            $padding: graphix_rt::TRef<X, PaddingV>,
301            $width: graphix_rt::TRef<X, LengthV>,
302            $height: graphix_rt::TRef<X, LengthV>,
303            $align: graphix_rt::TRef<X, $align_ty>,
304            children_ref: graphix_rt::Ref<X>,
305            children: Vec<GuiW<X>>,
306        }
307
308        impl<X: GXExt> $name<X> {
309            pub(crate) async fn compile(gx: GXHandle<X>, source: Value) -> Result<GuiW<X>> {
310                let [(_, children), $((_, $f)),+] =
311                    source.cast_to::<[(ArcStr, u64); 6]>()
312                        .context(concat!($label, " flds"))?;
313                let (children_ref, $($f),+) = tokio::try_join!(
314                    gx.compile_ref(children),
315                    $(gx.compile_ref($f)),+
316                )?;
317                let compiled_children = match children_ref.last.as_ref() {
318                    None => vec![],
319                    Some(v) => compile_children(gx.clone(), v.clone()).await
320                        .context(concat!($label, " children"))?,
321                };
322                Ok(Box::new(Self {
323                    gx: gx.clone(),
324                    $spacing: graphix_rt::TRef::new($spacing)
325                        .context(concat!($label, " tref spacing"))?,
326                    $padding: graphix_rt::TRef::new($padding)
327                        .context(concat!($label, " tref padding"))?,
328                    $width: graphix_rt::TRef::new($width)
329                        .context(concat!($label, " tref width"))?,
330                    $height: graphix_rt::TRef::new($height)
331                        .context(concat!($label, " tref height"))?,
332                    $align: graphix_rt::TRef::new($align)
333                        .context(concat!($label, " tref ", stringify!($align)))?,
334                    children_ref,
335                    children: compiled_children,
336                }))
337            }
338        }
339
340        impl<X: GXExt> GuiWidget<X> for $name<X> {
341            fn children_mut(&mut self) -> &mut [GuiW<X>] {
342                &mut self.children
343            }
344
345            fn children(&self) -> &[GuiW<X>] {
346                &self.children
347            }
348
349            fn handle_update(
350                &mut self,
351                rt: &tokio::runtime::Handle,
352                id: ExprId,
353                v: &Value,
354            ) -> Result<bool> {
355                let mut changed = false;
356                changed |= self.$spacing.update(id, v)
357                    .context(concat!($label, " update spacing"))?.is_some();
358                changed |= self.$padding.update(id, v)
359                    .context(concat!($label, " update padding"))?.is_some();
360                changed |= self.$width.update(id, v)
361                    .context(concat!($label, " update width"))?.is_some();
362                changed |= self.$height.update(id, v)
363                    .context(concat!($label, " update height"))?.is_some();
364                changed |= self.$align.update(id, v)
365                    .context(concat!($label, " update ", stringify!($align)))?.is_some();
366                if id == self.children_ref.id {
367                    self.children_ref.last = Some(v.clone());
368                    self.children = rt.block_on(
369                        compile_children(self.gx.clone(), v.clone())
370                    ).context(concat!($label, " children recompile"))?;
371                    changed = true;
372                }
373                for child in &mut self.children {
374                    changed |= child.handle_update(rt, id, v)?;
375                }
376                Ok(changed)
377            }
378
379            fn view(&self) -> IcedElement<'_> {
380                let mut w = iced_widget::$Widget::new();
381                if let Some(sp) = self.$spacing.t {
382                    w = w.spacing(sp as f32);
383                }
384                if let Some(p) = self.$padding.t.as_ref() {
385                    w = w.padding(p.0);
386                }
387                if let Some(wi) = self.$width.t.as_ref() {
388                    w = w.width(wi.0);
389                }
390                if let Some(h) = self.$height.t.as_ref() {
391                    w = w.height(h.0);
392                }
393                if let Some(a) = self.$align.t.as_ref() {
394                    w = w.$align_set(a.0);
395                }
396                for child in &self.children {
397                    w = w.push(child.view());
398                }
399                w.into()
400            }
401        }
402    };
403}
404
405flex_widget!(
406    RowW,
407    "row",
408    spacing,
409    padding,
410    width,
411    height,
412    VAlignV,
413    valign,
414    Row,
415    align_y,
416    [height, padding, spacing, valign, width]
417);
418
419flex_widget!(
420    ColumnW,
421    "column",
422    spacing,
423    padding,
424    width,
425    height,
426    HAlignV,
427    halign,
428    Column,
429    align_x,
430    [halign, height, padding, spacing, width]
431);
432
433/// Compile a widget value into a GuiW. Returns a boxed future to
434/// avoid infinite-size futures from recursive async calls.
435pub fn compile<X: GXExt>(gx: GXHandle<X>, source: Value) -> CompileFut<X> {
436    Box::pin(async move {
437        let (s, v) = source.cast_to::<(ArcStr, Value)>()?;
438        match s.as_str() {
439            "Text" => text::TextW::compile(gx, v).await,
440            "Column" => ColumnW::compile(gx, v).await,
441            "Row" => RowW::compile(gx, v).await,
442            "Container" => container::ContainerW::compile(gx, v).await,
443            "Grid" => grid::GridW::compile(gx, v).await,
444            "Button" => button::ButtonW::compile(gx, v).await,
445            "Space" => space::SpaceW::compile(gx, v).await,
446            "TextInput" => text_input::TextInputW::compile(gx, v).await,
447            "Checkbox" => toggle::CheckboxW::compile(gx, v).await,
448            "Toggler" => toggle::TogglerW::compile(gx, v).await,
449            "Slider" => slider::SliderW::compile(gx, v).await,
450            "ProgressBar" => progress_bar::ProgressBarW::compile(gx, v).await,
451            "Scrollable" => scrollable::ScrollableW::compile(gx, v).await,
452            "HorizontalRule" => rule::HorizontalRuleW::compile(gx, v).await,
453            "VerticalRule" => rule::VerticalRuleW::compile(gx, v).await,
454            "Tooltip" => tooltip::TooltipW::compile(gx, v).await,
455            "PickList" => pick_list::PickListW::compile(gx, v).await,
456            "Stack" => stack::StackW::compile(gx, v).await,
457            "Radio" => radio::RadioW::compile(gx, v).await,
458            "VerticalSlider" => slider::VerticalSliderW::compile(gx, v).await,
459            "ComboBox" => combo_box::ComboBoxW::compile(gx, v).await,
460            "TextEditor" => text_editor::TextEditorW::compile(gx, v).await,
461            "KeyboardArea" => keyboard_area::KeyboardAreaW::compile(gx, v).await,
462            "MouseArea" => mouse_area::MouseAreaW::compile(gx, v).await,
463            "Image" => image::ImageW::compile(gx, v).await,
464            "Canvas" => canvas::CanvasW::compile(gx, v).await,
465            "ContextMenu" => context_menu::ContextMenuW::compile(gx, v).await,
466            "Chart" => chart::ChartW::compile(gx, v).await,
467            "Markdown" => markdown::MarkdownW::compile(gx, v).await,
468            "MenuBar" => menu_bar::MenuBarW::compile(gx, v).await,
469            "QrCode" => qr_code::QrCodeW::compile(gx, v).await,
470            "Table" => table::TableW::compile(gx, v).await,
471            "DataTable" => data_table::DataTableW::compile(gx, v).await,
472            _ => bail!("invalid gui widget type `{s}({v})"),
473        }
474    })
475}
476
477/// Compile an array of widget values into a Vec of GuiW.
478pub async fn compile_children<X: GXExt>(
479    gx: GXHandle<X>,
480    v: Value,
481) -> Result<Vec<GuiW<X>>> {
482    let items = v.cast_to::<SmallVec<[Value; 8]>>()?;
483    let futs: Vec<_> = items.into_iter().map(|item| compile(gx.clone(), item)).collect();
484    futures::future::try_join_all(futs).await
485}