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:
Byson94
2025-09-03 16:49:46 +05:30
parent abe59bc374
commit 39912d3f46
12 changed files with 159 additions and 57 deletions

View File

@@ -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

View File

@@ -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},

View File

@@ -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};

View File

@@ -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::*;

View File

@@ -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>> {

View 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!(),
}
}
}
}
}

View File

@@ -4,6 +4,7 @@
pub mod ast;
pub mod builtins;
mod dyn_id;
pub mod error;
pub mod helper;
pub mod module_resolver;

View File

@@ -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> {

View File

@@ -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.

View File

@@ -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
])
]);
}

View File

@@ -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
}),
]);
}

View File

@@ -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(