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
11macro_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
21macro_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
34macro_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
44macro_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
90pub type Renderer = iced_renderer::Renderer;
93
94pub type IcedElement<'a> =
96 iced_core::Element<'a, Message, crate::theme::GraphixTheme, Renderer>;
97
98#[derive(Debug, Clone)]
100pub enum Message {
101 Nop,
102 Call(CallableId, ValArray),
103 EditorAction(ExprId, iced_widget::text_editor::Action),
104}
105
106pub trait GuiWidget<X: GXExt>: Send + 'static {
110 fn handle_update(
114 &mut self,
115 rt: &tokio::runtime::Handle,
116 id: ExprId,
117 v: &Value,
118 ) -> Result<bool>;
119
120 fn view(&self) -> IcedElement<'_>;
122
123 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
138pub type CompileFut<X> =
140 Pin<Box<dyn Future<Output = Result<GuiW<X>>> + Send + 'static>>;
141
142pub 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
160macro_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
308pub 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
351pub 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}