feat(MAJOR): removed the need for dyn_id
This is a very very very big update for UX! Ewwii finally has support for automatically assigning `dyn_id`. This was actually not as hard as I thought! I just had to mutate the widget AST and inject a `dyn_id` in based on its parent. It works soooo well and the burden on the user just reduced sooo much!
This commit is contained in:
@@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
- Deprecated attribute warning which cluttered the logs.
|
||||
- `std::json` (Rhai has built in json support).
|
||||
- `std::math` (Rhai already convers everything that it has).
|
||||
- The need for `dyn_id` for dynamic system.
|
||||
|
||||
## [0.1.0-beta] - 2025-08-27
|
||||
|
||||
|
||||
@@ -27,10 +27,10 @@ use codespan_reporting::files::Files;
|
||||
use gdk::Monitor;
|
||||
use glib::ObjectExt;
|
||||
use gtk::{gdk, glib};
|
||||
use rhai_impl::ast::WidgetNode;
|
||||
use itertools::Itertools;
|
||||
use once_cell::sync::Lazy;
|
||||
use rhai::Dynamic;
|
||||
use rhai_impl::ast::WidgetNode;
|
||||
use shared_utils::Span;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use crate::{
|
||||
// ipc_server,
|
||||
// error_handling_ctx,
|
||||
paths::EwwPaths,
|
||||
window::backend_window_options::BackendWindowOptionsDef,
|
||||
};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use rhai_impl::{parser::ParseConfig, ast::WidgetNode};
|
||||
use rhai::{Map, AST};
|
||||
use rhai_impl::{ast::WidgetNode, parser::ParseConfig};
|
||||
|
||||
// use tokio::{net::UnixStream, runtime::Runtime, sync::mpsc};
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ use gtk::glib::translate::FromGlib;
|
||||
use gtk::prelude::LabelExt;
|
||||
use gtk::{self, prelude::*, DestDefaults, TargetEntry, TargetList};
|
||||
use gtk::{gdk, glib, pango};
|
||||
use rhai_impl::ast::{get_id_to_widget_info, hash_props_and_type, WidgetNode};
|
||||
use rhai::Map;
|
||||
use rhai_impl::ast::{get_id_to_widget_info, hash_props_and_type, WidgetNode};
|
||||
|
||||
use super::widget_definitions_helper::*;
|
||||
use shared_utils::extract_props::*;
|
||||
|
||||
@@ -23,7 +23,7 @@ fn children_to_vec(
|
||||
pub fn register_all_widgets(engine: &mut Engine) {
|
||||
engine.register_type::<WidgetNode>();
|
||||
|
||||
// --- Primitive widgets ---
|
||||
// == Primitive widgets ==
|
||||
macro_rules! register_primitive {
|
||||
($name:expr, $variant:ident) => {
|
||||
engine.register_fn($name, |props: Map| -> Result<WidgetNode, Box<EvalAltResult>> {
|
||||
@@ -47,7 +47,7 @@ pub fn register_all_widgets(engine: &mut Engine) {
|
||||
register_primitive!("color_button", ColorButton);
|
||||
register_primitive!("color_chooser", ColorChooser);
|
||||
|
||||
// --- Widgets with children ---
|
||||
// == Widgets with children ==
|
||||
macro_rules! register_with_children {
|
||||
($name:expr, $variant:ident) => {
|
||||
engine.register_fn(
|
||||
@@ -73,7 +73,7 @@ pub fn register_all_widgets(engine: &mut Engine) {
|
||||
register_with_children!("eventbox", EventBox);
|
||||
register_with_children!("tooltip", ToolTip);
|
||||
|
||||
// --- Top-level macros ---
|
||||
// == Top-level macros ==
|
||||
engine.register_fn(
|
||||
"defwindow",
|
||||
|name: &str, props: Map, node: WidgetNode| -> Result<WidgetNode, Box<EvalAltResult>> {
|
||||
|
||||
132
crates/rhai_impl/src/dyn_id.rs
Normal file
132
crates/rhai_impl/src/dyn_id.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use crate::ast::WidgetNode;
|
||||
use rhai::{Dynamic, Map};
|
||||
|
||||
impl WidgetNode {
|
||||
/// A very important implementation of [`WidgetNode`].
|
||||
/// This function implements dyn_id property to widgets.
|
||||
pub fn setup_for_rt(&self, parent_path: &str) -> Self {
|
||||
// fn to assign dyn_id to a node
|
||||
fn with_dyn_id(mut props: Map, dyn_id: &str) -> Map {
|
||||
props.insert("dyn_id".into(), Dynamic::from(dyn_id.to_string()));
|
||||
props
|
||||
}
|
||||
|
||||
// fn to process children of a container node
|
||||
fn process_children(
|
||||
children: &[WidgetNode],
|
||||
parent_path: &str,
|
||||
kind: &str,
|
||||
) -> Vec<WidgetNode> {
|
||||
children
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, child)| {
|
||||
let child_path = format!("{}_{}_{}", parent_path, kind, idx);
|
||||
child.setup_for_rt(&child_path)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
match self {
|
||||
WidgetNode::DefWindow { name, props, node } => WidgetNode::DefWindow {
|
||||
name: name.clone(),
|
||||
props: props.clone(),
|
||||
node: Box::new(node.setup_for_rt(name)),
|
||||
},
|
||||
|
||||
// == Containers with children ==
|
||||
WidgetNode::Box { props, children } => WidgetNode::Box {
|
||||
props: with_dyn_id(props.clone(), parent_path),
|
||||
children: process_children(children, parent_path, "box"),
|
||||
},
|
||||
WidgetNode::CenterBox { props, children } => WidgetNode::CenterBox {
|
||||
props: with_dyn_id(props.clone(), parent_path),
|
||||
children: process_children(children, parent_path, "centerbox"),
|
||||
},
|
||||
WidgetNode::Expander { props, children } => WidgetNode::Expander {
|
||||
props: with_dyn_id(props.clone(), parent_path),
|
||||
children: process_children(children, parent_path, "expander"),
|
||||
},
|
||||
WidgetNode::Revealer { props, children } => WidgetNode::Revealer {
|
||||
props: with_dyn_id(props.clone(), parent_path),
|
||||
children: process_children(children, parent_path, "revealer"),
|
||||
},
|
||||
WidgetNode::Scroll { props, children } => WidgetNode::Scroll {
|
||||
props: with_dyn_id(props.clone(), parent_path),
|
||||
children: process_children(children, parent_path, "scroll"),
|
||||
},
|
||||
WidgetNode::OverLay { props, children } => WidgetNode::OverLay {
|
||||
props: with_dyn_id(props.clone(), parent_path),
|
||||
children: process_children(children, parent_path, "overlay"),
|
||||
},
|
||||
WidgetNode::Stack { props, children } => WidgetNode::Stack {
|
||||
props: with_dyn_id(props.clone(), parent_path),
|
||||
children: process_children(children, parent_path, "stack"),
|
||||
},
|
||||
WidgetNode::EventBox { props, children } => WidgetNode::EventBox {
|
||||
props: with_dyn_id(props.clone(), parent_path),
|
||||
children: process_children(children, parent_path, "eventbox"),
|
||||
},
|
||||
WidgetNode::ToolTip { props, children } => WidgetNode::ToolTip {
|
||||
props: with_dyn_id(props.clone(), parent_path),
|
||||
children: process_children(children, parent_path, "tooltip"),
|
||||
},
|
||||
|
||||
// == Top-level container for multiple widgets ==
|
||||
WidgetNode::Enter(children) => {
|
||||
WidgetNode::Enter(process_children(children, parent_path, "enter"))
|
||||
}
|
||||
|
||||
// == Poll/Listen nodes ==
|
||||
WidgetNode::Poll { var, props } => WidgetNode::Poll {
|
||||
var: var.clone(),
|
||||
props: with_dyn_id(props.clone(), &format!("{}_poll_{}", parent_path, var)),
|
||||
},
|
||||
WidgetNode::Listen { var, props } => WidgetNode::Listen {
|
||||
var: var.clone(),
|
||||
props: with_dyn_id(props.clone(), &format!("{}_listen_{}", parent_path, var)),
|
||||
},
|
||||
|
||||
// == Leaf nodes ==
|
||||
node @ WidgetNode::Label { props }
|
||||
| node @ WidgetNode::Button { props }
|
||||
| node @ WidgetNode::Image { props }
|
||||
| node @ WidgetNode::Input { props }
|
||||
| node @ WidgetNode::Progress { props }
|
||||
| node @ WidgetNode::ComboBoxText { props }
|
||||
| node @ WidgetNode::Slider { props }
|
||||
| node @ WidgetNode::Checkbox { props }
|
||||
| node @ WidgetNode::Calendar { props }
|
||||
| node @ WidgetNode::ColorButton { props }
|
||||
| node @ WidgetNode::ColorChooser { props }
|
||||
| node @ WidgetNode::CircularProgress { props }
|
||||
| node @ WidgetNode::Graph { props }
|
||||
| node @ WidgetNode::Transform { props } => {
|
||||
let new_props = with_dyn_id(props.clone(), parent_path);
|
||||
match node {
|
||||
WidgetNode::Label { .. } => WidgetNode::Label { props: new_props },
|
||||
WidgetNode::Button { .. } => WidgetNode::Button { props: new_props },
|
||||
WidgetNode::Image { .. } => WidgetNode::Image { props: new_props },
|
||||
WidgetNode::Input { .. } => WidgetNode::Input { props: new_props },
|
||||
WidgetNode::Progress { .. } => WidgetNode::Progress { props: new_props },
|
||||
WidgetNode::ComboBoxText { .. } => {
|
||||
WidgetNode::ComboBoxText { props: new_props }
|
||||
}
|
||||
WidgetNode::Slider { .. } => WidgetNode::Slider { props: new_props },
|
||||
WidgetNode::Checkbox { .. } => WidgetNode::Checkbox { props: new_props },
|
||||
WidgetNode::Calendar { .. } => WidgetNode::Calendar { props: new_props },
|
||||
WidgetNode::ColorButton { .. } => WidgetNode::ColorButton { props: new_props },
|
||||
WidgetNode::ColorChooser { .. } => {
|
||||
WidgetNode::ColorChooser { props: new_props }
|
||||
}
|
||||
WidgetNode::CircularProgress { .. } => {
|
||||
WidgetNode::CircularProgress { props: new_props }
|
||||
}
|
||||
WidgetNode::Graph { .. } => WidgetNode::Graph { props: new_props },
|
||||
WidgetNode::Transform { .. } => WidgetNode::Transform { props: new_props },
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
pub mod ast;
|
||||
pub mod builtins;
|
||||
mod dyn_id;
|
||||
pub mod error;
|
||||
pub mod helper;
|
||||
pub mod module_resolver;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::{
|
||||
ast::WidgetNode,
|
||||
builtins::register_all_widgets,
|
||||
error::{format_eval_error, format_parse_error},
|
||||
helper::extract_poll_and_listen_vars,
|
||||
module_resolver::SimpleFileResolver,
|
||||
providers::register_all_providers,
|
||||
ast::WidgetNode,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use rhai::{Dynamic, Engine, Scope, AST};
|
||||
@@ -32,9 +32,12 @@ impl ParseConfig {
|
||||
|
||||
pub fn eval_code(&mut self, code: &str) -> Result<WidgetNode> {
|
||||
let mut scope = Scope::new();
|
||||
self.engine
|
||||
let node = self
|
||||
.engine
|
||||
.eval_with_scope::<WidgetNode>(&mut scope, code)
|
||||
.map_err(|e| anyhow!(format_eval_error(&e, code, &self.engine)))
|
||||
.map_err(|e| anyhow!(format_eval_error(&e, code, &self.engine)))?;
|
||||
|
||||
Ok(node.setup_for_rt("root"))
|
||||
}
|
||||
|
||||
pub fn compile_code(&mut self, code: &str) -> Result<AST> {
|
||||
@@ -52,16 +55,18 @@ impl ParseConfig {
|
||||
None => Scope::new(),
|
||||
};
|
||||
|
||||
match compiled_ast {
|
||||
let node = match compiled_ast {
|
||||
Some(ast) => self
|
||||
.engine
|
||||
.eval_ast_with_scope::<WidgetNode>(&mut scope, &ast)
|
||||
.map_err(|e| anyhow!(format_eval_error(&e, code, &self.engine))),
|
||||
.map_err(|e| anyhow!(format_eval_error(&e, code, &self.engine)))?,
|
||||
None => self
|
||||
.engine
|
||||
.eval_with_scope::<WidgetNode>(&mut scope, code)
|
||||
.map_err(|e| anyhow!(format_eval_error(&e, code, &self.engine))),
|
||||
}
|
||||
.map_err(|e| anyhow!(format_eval_error(&e, code, &self.engine)))?,
|
||||
};
|
||||
|
||||
Ok(node.setup_for_rt("root"))
|
||||
}
|
||||
|
||||
pub fn code_from_file<P: AsRef<Path>>(&mut self, file_path: P) -> Result<String> {
|
||||
|
||||
@@ -120,35 +120,3 @@ fn will_work(time, foo) { // time and foo is passed from `enter([])`
|
||||
return box(#{}, [ label(#{ text: time }), label(#{ text: foo }) ]);
|
||||
}
|
||||
```
|
||||
|
||||
## dyn_id
|
||||
|
||||
`dyn_id`'s are one of the most important properties of all. It is the property that decides if a widget should get updated dynamic or not.
|
||||
|
||||
`dyn_id` property is used to assign an id to a widget which the system will track to update if there is a change in property under the same id.
|
||||
|
||||
**Example usage:**
|
||||
|
||||
```js
|
||||
fn foo(foo_var) {
|
||||
return box(#{}, [
|
||||
label(#{ text: foo_var, dyn_id: "foo_var_user" }); // dyn_id used to make label dynamic
|
||||
]);
|
||||
}
|
||||
|
||||
enter([
|
||||
poll("foo", #{
|
||||
cmd: "echo baz",
|
||||
initial: "",
|
||||
interval: "1s"
|
||||
}),
|
||||
|
||||
defwindow("bar", #{
|
||||
// .. properties omitted
|
||||
}, bar(foo)),
|
||||
])
|
||||
```
|
||||
|
||||
Here, when the variable foo changes, the text of label changes as well. If there is no `dyn_id` defined, then ewwii will ingore that change. But if it is defined with a unique value, then it will find the widget that is defined with the id that matches dyn_id and then update its text property which will result in a change in the UI.
|
||||
|
||||
> **Tip:** Always add a `dyn_id` to every single widget that you create if you are working with a dynamic system, as this method will avoid many surprises.
|
||||
|
||||
@@ -27,9 +27,8 @@ fn animalButton(emoji, selected) {
|
||||
class: class,
|
||||
cursor: "pointer",
|
||||
onclick: "echo " + emoji + " >> /tmp/selected_emoji.txt",
|
||||
dyn_id: "dyn_eventbox_" + emoji, // unique per emoji
|
||||
}, [
|
||||
label(#{ text: emoji, dyn_id: "dyn_label_" + emoji }) // unique per emoji
|
||||
label(#{ text: emoji }) // unique per emoji
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -12,9 +12,8 @@ fn sidestuff(volume, time) {
|
||||
label: "🔊",
|
||||
value: volume,
|
||||
onchange: "pamixer --set-volume {}",
|
||||
dyn_id: "volume_metric"
|
||||
}),
|
||||
label(#{ text: time, dyn_id: "current_time" })
|
||||
label(#{ text: time })
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -52,7 +51,7 @@ fn music(music_var) {
|
||||
space_evenly: false,
|
||||
halign: "center"
|
||||
}, [
|
||||
label(#{ text: label_text, dyn_id: "music_bar" }),
|
||||
label(#{ text: label_text }),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -60,8 +59,6 @@ fn metric(props) {
|
||||
let label_prop = props.label;
|
||||
let value_prop = props.value;
|
||||
let onchange_prop = props.onchange;
|
||||
let dyn_id_prop = props.dyn_id;
|
||||
|
||||
|
||||
return box(#{
|
||||
orientation: "h",
|
||||
@@ -75,7 +72,6 @@ fn metric(props) {
|
||||
active: onchange_prop != "",
|
||||
value: value_prop,
|
||||
onchange: onchange_prop,
|
||||
dyn_id: dyn_id_prop
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use rhai_impl::providers;
|
||||
use rhai::{Engine, module_resolvers::StaticModuleResolver};
|
||||
use rhai_autodocs::{export::options, generate::mdbook};
|
||||
use rhai_impl::providers;
|
||||
use std::{env, fs, path::Path};
|
||||
|
||||
fn generate_docs(
|
||||
|
||||
Reference in New Issue
Block a user