62 Commits

Author SHA1 Message Date
Byson94
df910786a6 chore: minor rebranding 2026-01-10 11:13:36 +05:30
Byson94
d902343bb4 Merge pull request #18 from buzz/feat/graph
feat: add support for graph widget
2026-01-10 11:13:05 +05:30
Byson94
ecb85dd5bf Merge branch 'iidev' into feat/graph 2026-01-10 11:12:32 +05:30
Byson94
e4ab87bc2a feat: touch support to scale widget 2026-01-04 15:26:56 +05:30
Byson94
37b57aee60 feat: implement orientation for scale widget 2026-01-02 13:08:53 +05:30
Byson94
33ef1720e3 feat: return data early if mutations is empty 2026-01-01 12:55:40 +05:30
Byson94
c41a495ee0 localsignal: fix no compiled ast warning returning early 2026-01-01 12:49:39 +05:30
Byson94
47f93e9cab chore: run cargo fmt 2026-01-01 12:46:56 +05:30
Byson94
9e91ae61a5 Merge pull request #19 from BinaryHarbinger/main
chore: Change source of Binarydots example
2026-01-01 11:06:58 +05:30
BinaryHarbinger
87cc157055 chore: Change source of Binarydots example 2025-12-31 20:43:19 +03:00
Byson94
29983ab9da feat: add eval_ignore prop to all widgets 2025-12-30 09:25:00 +05:30
Byson94
ff9db50831 feat: add mutations property to localsignal 2025-12-28 19:41:01 +05:30
Byson94
4cb6ac05d3 docs: remove unnecessary feat in changelog 2025-12-28 14:25:55 +05:30
Byson94
73a64944b8 docs(changelog): fix removed section in unreleased 2025-12-25 10:54:16 +05:30
Byson94
d25c2db420 chore: cargo fmt 2025-12-24 11:19:09 +05:30
Byson94
216775f55a feat(img wdgt): custom rendering + new feats 2025-12-24 11:18:33 +05:30
Byson94
ad79e81c50 feat: new image widget features & fully remove icon 2025-12-23 14:57:52 +05:30
Byson94
df7226d06c feat: remove icon widget 2025-12-23 14:50:16 +05:30
Byson94
6e03473133 feat: replace image widget rendering 2025-12-22 19:17:27 +05:30
Byson94
36c58e211d feat: update props of icon widget 2025-12-22 18:18:26 +05:30
buzz
2335ca5fe5 feat: add support for graph widget 2025-12-20 11:37:00 +01:00
buzz
8865fc0f6f chore: add v4_18 to gtk4 crate
Enable more recent Gtk APIs like `gsk::PathBuilder` and `gtk::Widget::color()`.
2025-12-20 10:55:16 +01:00
buzz
f6eec1fe51 feat: add skip_unchanged property to poll 2025-12-13 11:24:30 +01:00
Byson94
70de347bcf fix: clockwise prop not working on circ progress 2025-12-10 14:52:56 +05:30
Byson94
2cbf64e250 feat: add text and show_text prop to progressbar widget 2025-12-09 18:34:44 +05:30
Byson94
a14e559c80 feat: rename WidgetNode::Slider to WidgetNode::Scale 2025-12-09 17:04:32 +05:30
Byson94
59fb1b85eb feat: remove ewwii_anims from cargo.toml 2025-12-08 21:32:06 +05:30
Byson94
4197a863e5 feat: change doccomment description of rhai_impl crate 2025-12-08 19:49:09 +05:30
Byson94
4293c6877d feat: migrate most legacycontroller to gestureclick
This provides touch support to widgets
2025-12-07 14:14:35 +05:30
Byson94
f393627932 feat: parse widget_action ucontainer actions like shell 2025-12-06 16:14:53 +05:30
Byson94
c54ce27505 feat: add features section in readme 2025-12-02 19:01:32 +05:30
Byson94
97518eb49c chore: run cargo fmt 2025-11-29 20:45:31 +05:30
Byson94
ddce15481f feat: remove remove action from widget_action utility 2025-11-29 19:49:52 +05:30
Byson94
43721426e8 feat: proptotype widgetaction utility widget 2025-11-28 20:25:18 +05:30
Byson94
6baa9c7858 feat: add transition_duration prop to stack widget 2025-11-26 20:18:28 +05:30
Byson94
8ec080a290 feat: add add/remove-class subcommand to wc command 2025-11-26 19:15:28 +05:30
Byson94
166c440978 chore: cargo fmt 2025-11-25 21:05:24 +05:30
Byson94
6e9dca9d42 feat: add placeholder property to input widget 2025-11-25 21:04:32 +05:30
Byson94
b50f41b1e0 feat: add propety-update argument to widget control command 2025-11-25 20:49:26 +05:30
Byson94
357dcaacbc fix: doc comment of epapi register_function 2025-11-24 19:33:49 +05:30
Byson94
61e681e6bd chore: run cargo fmt 2025-11-24 19:26:30 +05:30
Byson94
ebd4264621 fix: localbind not finding properties of range subclasses 2025-11-23 17:49:22 +05:30
Byson94
4167c64fde feat: fix register_function docs in epapi 2025-11-22 17:41:24 +05:30
Byson94
6456b2998d feat: add link to documentation in readme 2025-11-22 17:39:16 +05:30
Byson94
4de5ab3c59 chore: commit change in cargo.lock 2025-11-22 12:08:07 +05:30
Byson94
04ca79a5af feat: improve epapi; bump epapi to 0.7.0; 2025-11-22 12:05:30 +05:30
Byson94
0d803fd962 chore: run cargo fmt 2025-11-22 11:13:24 +05:30
Byson94
8406c86117 feat: bump ewwii_plugin_api version 2025-11-21 21:53:41 +05:30
Byson94
1aee3163e0 feat: make register_function on ewwii_plugin_api register directly 2025-11-21 21:53:19 +05:30
Byson94
9aec974e9e feat: add warning on ewwii_plugin_api until it is fixed 2025-11-21 18:06:52 +05:30
Byson94
4de148be58 feat: remove debug in action_with_engine 2025-11-20 21:02:23 +05:30
Byson94
3f48178333 feat: matching rhai features across all crates 2025-11-19 14:35:55 +05:30
Byson94
8af01e44f2 feat: improve plugin loading order 2025-11-17 20:05:26 +05:30
Byson94
a76440f242 feat: add trace log in action_with_engine 2025-11-16 20:29:14 +05:30
Byson94
49c0d14ace feat: update wc actions to take vec of strings 2025-11-13 19:27:46 +05:30
Byson94
6a6856192c feat: make wc add eval rhai code instead of .ui 2025-11-08 18:47:09 +05:30
Byson94
b821d8bb6a feat: add widget-control command 2025-11-08 17:09:51 +05:30
Byson94
e12d5f7fb9 feat: add gtk_ui function for loading .ui 2025-11-07 20:22:56 +05:30
Byson94
26ee4e5560 chore: cargo fmt 2025-11-01 19:38:27 +05:30
Byson94
622a9c1b06 feat: bump to ewwii 0.3.1 2025-11-01 19:37:36 +05:30
Byson94
6b0e94698b fix: localsignal not working for non-gchararray props 2025-11-01 18:43:41 +05:30
Byson94
08e1f2d5f3 fix: circularprogress not updating to dyn vars 2025-11-01 18:30:43 +05:30
29 changed files with 1595 additions and 723 deletions

View File

@@ -5,9 +5,46 @@ All notable changes to `ewwii` are documented here.
This changelog follows the [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format,
and this project adheres to [Semantic Versioning](https://semver.org/).
## [UNRELEASED]
### Added
- `gtk_ui` function for loading .ui files.
- `widget-control` (`wc` in short) command for controlling widgets.
- `placeholder` property to input widget.
- `transition_duration` property to stack widget.
- `widget_control` utility function for dynamic widget handling.
- `text` and `show_text` property to progressbar widget.
- `graph` widget back.
### Changed
- `v4_18` feature flag for to the `gtk4` crate to enable newer APIs.
- `content_fit` property to image widget.
- `can_shrink` property to image widget.
- `mutations` property to localsignal.
- `eval_ignore` property to all widgets.
- Touch support to scale widget.
### Fixed
- `clockwise` property not working on circular_progress.
### Removed
- icon widget.
## [0.3.1] - 2025-11-01
## Fixed
- Circular progress bar not updating dynamically.
- LocalSignal values not getting transformed to suite property type.
- LocalBind not finding properties of range subclasses.
## [0.3.0] - 2025-11-01
## Added
### Added
- `localsignal` signal for fast and cheap property update.
- `localbind` utility for binding `localsignal` to a widget property.

19
Cargo.lock generated
View File

@@ -486,7 +486,7 @@ dependencies = [
[[package]]
name = "ewwii"
version = "0.3.0"
version = "0.4.0"
dependencies = [
"anyhow",
"bincode",
@@ -515,6 +515,7 @@ dependencies = [
"serde",
"serde_json",
"shared_utils",
"shell-words",
"simple-signal",
"smart-default",
"static_assertions",
@@ -527,7 +528,7 @@ dependencies = [
[[package]]
name = "ewwii_plugin_api"
version = "0.6.1"
version = "0.7.0"
dependencies = [
"gtk4",
"rhai",
@@ -999,9 +1000,9 @@ dependencies = [
[[package]]
name = "gtk4"
version = "0.10.1"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f7887ee0ceeffedb25a418810a2c61497dacad51767fc13f9d60859b4023b8a"
checksum = "acb21d53cfc6f7bfaf43549731c43b67ca47d87348d81c8cfc4dcdd44828e1a4"
dependencies = [
"cairo-rs",
"field-offset",
@@ -1719,9 +1720,9 @@ checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]]
name = "rhai"
version = "1.23.4"
version = "1.23.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527390cc333a8d2cd8237890e15c36518c26f8b54c903d86fc59f42f08d25594"
checksum = "f4e35aaaa439a5bda2f8d15251bc375e4edfac75f9865734644782c9701b5709"
dependencies = [
"ahash",
"bitflags 2.9.4",
@@ -1925,6 +1926,12 @@ dependencies = [
"serde",
]
[[package]]
name = "shell-words"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]]
name = "signal-hook-registry"
version = "1.4.2"

View File

@@ -7,7 +7,7 @@ resolver = "2"
shared_utils = { version = "0.1.0", path = "crates/shared_utils" }
rhai_impl = { version = "0.1.0", path = "crates/rhai_impl" }
scan_prop_proc = { version = "0.1.0", path = "proc_macros/scan_prop_proc" }
ewwii_plugin_api = { version = "0.6.1", path = "crates/ewwii_plugin_api" }
ewwii_plugin_api = { version = "0.7.0", path = "crates/ewwii_plugin_api" }
anyhow = "1.0.86"
ahash = "0.8.12"
@@ -26,7 +26,7 @@ derive_more = { version = "1", features = [
extend = "1.2"
futures = "0.3.30"
grass = "0.13.4"
gtk4 = "0.10.1"
gtk4 = { version = "0.10.3", features = ["v4_18", "v4_8"] }
itertools = "0.13.0"
libc = "0.2"
log = "0.4"
@@ -36,7 +36,7 @@ once_cell = "1.19"
pretty_assertions = "1.4.0"
pretty_env_logger = "0.5.0"
regex = "1.10.5"
rhai = { version = "1.22.2" }
rhai = "1.23.6"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
simple-signal = "1.1"
@@ -49,6 +49,7 @@ wait-timeout = "0.2"
syn = "2.0.107"
quote = "1.0.41"
proc-macro2 = "1.0.101"
shell-words = "1.1.0"
[profile.dev]
split-debuginfo = "unpacked"

View File

@@ -1,4 +1,5 @@
[![dependency status](https://deps.rs/repo/github/byson94/ewwii/status.svg)](https://deps.rs/repo/github/byson94/ewwii)
[![docs link](https://img.shields.io/badge/documentation-link-blue)](https://ewwii-sh.github.io/docs)
# Ewwii
@@ -16,9 +17,16 @@ Examples of projects powered by ewwii.
| **Data Structures**<br>[- View Example](./examples/data-structures) | [![Data Structures](./examples/data-structures/data-structures-preview.png)](./examples/data-structures) |
| **Wi-Fi GUI Template**<br>[- View on GitHub](https://github.com/Ewwii-sh/ewifi_gui_template) | ![Wi-Fi GUI Template](https://raw.githubusercontent.com/Ewwii-sh/ewifi_gui_template/main/.github/wifi_manager_template.png) |
| **Obsidian Bar Template**<br>[- View on GitHub](https://github.com/Ewwii-sh/obsidian-bar) | [![Obsidian Bar](https://raw.githubusercontent.com/Ewwii-sh/obsidian-bar/main/.github/screenshot.png)](https://github.com/Ewwii-sh/obsidian-bar) |
| **Binary Dots by [@BinaryHarbinger](https://github.com/BinaryHarbinger)**<br>[- View on GitHub](https://github.com/BinaryHarbinger/binarydots/) | [![Binary Dots](https://raw.githubusercontent.com/BinaryHarbinger/binarydots/main/preview/desktop.png)](https://github.com/BinaryHarbinger/binarydots)
| **Binary Dots by [@BinaryHarbinger](https://github.com/BinaryHarbinger)**<br>[- View on GitHub](https://github.com/BinaryHarbinger/binarydots/) | [![Binary Dots](https://raw.githubusercontent.com/BinaryHarbinger/binarydots/main/preview/Desktop.png)](https://github.com/BinaryHarbinger/binarydots)
| **Astatine Dots (Linux Rice with Ewwii)**<br>[- View on GitHub](https://github.com/Ewwii-sh/astatine-dots) | [![Astatine Dots](https://github.com/user-attachments/assets/f028ca1f-e403-476d-a7d9-cadce47691b7)](https://github.com/Ewwii-sh/astatine-dots) |
## Features
- Powered by Gtk4
- Supports Hot reload
- Extensibility via plugins and rhai modules
- X11 + Wayland support
## Contribewwtiing
If you want to contribute anything, like adding new widgets, features, or subcommands (including sample configs), you should definitely do so.

View File

@@ -1,6 +1,6 @@
[package]
name = "ewwii"
version = "0.3.0"
version = "0.4.0"
authors = ["byson94 <byson94wastaken@gmail.com>"]
description = "Widgets for everyone made better!"
license = "GPL-3.0-or-later"
@@ -17,7 +17,7 @@ wayland = ["gtk4-layer-shell"]
[dependencies]
shared_utils.workspace = true
rhai_impl.workspace = true
ewwii_plugin_api = { workspace = true }
ewwii_plugin_api.workspace = true
gtk4-layer-shell = { version = "0.6.3", optional = true }
gdk4-x11 = { version = "0.10.1", optional = true }
@@ -49,7 +49,8 @@ simple-signal.workspace = true
tokio = { workspace = true, features = ["full"] }
unescape.workspace = true
wait-timeout.workspace = true
rhai.workspace = true
rhai = { workspace = true, features = ["internals"] }
shell-words.workspace = true
# Plugin loading
libloading = "0.8.9"

View File

@@ -23,6 +23,7 @@ use crate::{
*,
};
use anyhow::{anyhow, bail};
use ewwii_plugin_api as epapi;
use gdk::Monitor;
use gtk4::Window;
use gtk4::{gdk, glib};
@@ -84,6 +85,10 @@ pub enum DaemonCommand {
ShowState(DaemonResponseSender),
ListWindows(DaemonResponseSender),
ListActiveWindows(DaemonResponseSender),
WidgetControl {
action: crate::opts::WidgetControlAction,
sender: DaemonResponseSender,
},
TriggerUpdateUI {
inject_vars: Option<HashMap<String, String>>,
should_preserve_state: bool,
@@ -362,6 +367,12 @@ impl<B: DisplayBackend> App<B> {
Err(e) => sender.send_failure(e.to_string())?,
};
}
DaemonCommand::WidgetControl { action, sender } => {
match self.perform_widget_control(action) {
Ok(_) => sender.send_success(String::new())?,
Err(e) => sender.send_failure(e.to_string())?,
};
}
DaemonCommand::CallRhaiFns { calls, sender } => {
match self.call_rhai_fns(calls) {
Ok(_) => sender.send_success(String::new())?,
@@ -530,7 +541,10 @@ impl<B: DisplayBackend> App<B> {
let b_interval = self.rt_engine_config.batching_interval;
// kick start the localsignal
rhai_impl::updates::handle_localsignal_changes();
rhai_impl::updates::handle_localsignal_changes(
stored_parser_clone.clone(),
compiled_ast.clone(),
);
glib::MainContext::default().spawn_local(async move {
let mut pending_updates = HashSet::new();
@@ -748,6 +762,94 @@ impl<B: DisplayBackend> App<B> {
Ok(())
}
/// Perform widget control based on the action
pub fn perform_widget_control(
&mut self,
action: crate::opts::WidgetControlAction,
) -> Result<()> {
match action {
crate::opts::WidgetControlAction::Remove { names } => {
if let Ok(mut maybe_registry) = self.widget_reg_store.lock() {
if let Some(widget_registry) = maybe_registry.as_mut() {
for name in names {
widget_registry.remove_widget_by_name(&name);
}
} else {
log::error!("Widget registry is empty");
}
} else {
log::error!("Failed to acquire lock on widget registry");
}
}
crate::opts::WidgetControlAction::Create { rhai_codes, parent_name } => {
let mut parser = self.config_parser.borrow_mut();
for rhai_code in rhai_codes {
let widget_node = parser.eval_code_snippet(&rhai_code)?;
let wid = rhai_impl::ast::hash_props(widget_node.props().ok_or_else(|| {
anyhow::anyhow!("Failed to retreive the properties of this widget.")
})?);
if let Ok(mut maybe_registry) = self.widget_reg_store.lock() {
if let Some(widget_registry) = maybe_registry.as_mut() {
let pid =
widget_registry.get_widget_id_by_name(&parent_name).ok_or_else(
|| anyhow::anyhow!("Widget '{}' not found", parent_name),
)?;
widget_registry.create_widget(&widget_node, wid, pid)?;
} else {
log::error!("Widget registry is empty");
}
} else {
log::error!("Failed to acquire lock on widget registry");
}
}
}
crate::opts::WidgetControlAction::PropertyUpdate {
property_and_value,
widget_name,
} => {
if let Ok(mut maybe_registry) = self.widget_reg_store.lock() {
if let Some(widget_registry) = maybe_registry.as_mut() {
for (key, value) in &property_and_value {
widget_registry.update_property_by_name(
&widget_name,
(key.clone(), value.clone()),
);
}
} else {
log::error!("Widget registry is empty");
}
} else {
log::error!("Failed to acquire lock on widget registry");
}
}
crate::opts::WidgetControlAction::AddClass { class, widget_name } => {
if let Ok(mut maybe_registry) = self.widget_reg_store.lock() {
if let Some(widget_registry) = maybe_registry.as_mut() {
widget_registry.update_class_of_widget_by_name(&widget_name, &class, false);
} else {
log::error!("Widget registry is empty");
}
} else {
log::error!("Failed to acquire lock on widget registry");
}
}
crate::opts::WidgetControlAction::RemoveClass { class, widget_name } => {
if let Ok(mut maybe_registry) = self.widget_reg_store.lock() {
if let Some(widget_registry) = maybe_registry.as_mut() {
widget_registry.update_class_of_widget_by_name(&widget_name, &class, true);
} else {
log::error!("Widget registry is empty");
}
} else {
log::error!("Failed to acquire lock on widget registry");
}
}
}
Ok(())
}
/// Trigger a UI update with the given flags.
/// Even if there are no flags, the UI will still be updated.
pub fn trigger_ui_update_with(
@@ -858,17 +960,16 @@ impl<B: DisplayBackend> App<B> {
unsafe {
// Each plugin exposes: extern "C" fn create_plugin() -> Box<dyn Plugin>
let constructor: libloading::Symbol<
unsafe extern "C" fn() -> Box<dyn ewwii_plugin_api::Plugin>,
> = lib
.get(b"create_plugin")
.map_err(|e| anyhow!("Failed to find create_plugin: {}", e))?;
let constructor: libloading::Symbol<unsafe extern "C" fn() -> Box<dyn epapi::Plugin>> =
lib.get(b"create_plugin")
.map_err(|e| anyhow!("Failed to find create_plugin: {}", e))?;
let plugin = constructor(); // instantiate plugin
let host = crate::plugin::EwwiiImpl { requestor: tx.clone() };
plugin.init(&host); // call init immediately
set_active_plugin(lib)?; // keep library alive
let host = crate::plugin::EwwiiImpl { requestor: tx.clone() };
plugin.init(&host); // call init immediately
}
let cp = self.config_parser.clone();
@@ -876,13 +977,18 @@ impl<B: DisplayBackend> App<B> {
let handle_request = move |req: PluginRequest| match req {
PluginRequest::RhaiEngineAct(func) => {
cp.borrow_mut().action_with_engine(func);
func(&mut cp.borrow_mut().engine);
}
PluginRequest::RegisterFunc((name, func)) => {
if let Err(e) = shared_utils::slib_store::register_functions(name, func) {
log::error!("Error registering function: {}", e);
PluginRequest::RegisterFunc((name, namespace, func)) => match namespace {
epapi::rhai_backend::RhaiFnNamespace::Custom(ns) => {
let mut module = rhai::Module::new();
module.set_native_fn(name, func);
cp.borrow_mut().engine.register_static_module(&ns, module.into());
}
}
epapi::rhai_backend::RhaiFnNamespace::Global => {
cp.borrow_mut().engine.register_fn(name, func);
}
},
PluginRequest::ListWidgetIds(res_tx) => {
let wgs_guard = wgs.lock().unwrap();
if let Some(wgs_brw) = wgs_guard.as_ref() {

View File

@@ -1,4 +1,4 @@
//! Module concerned with handling the global application lifecycle of eww.
//! Module concerned with handling the global application lifecycle of ewwii.
//! Currently, this only means handling application exit by providing a global
//! `recv_exit()` function which can be awaited to receive an event in case of application termination.

View File

@@ -187,6 +187,13 @@ pub enum ActionWithServer {
// /// Print out the scope graph structure in graphviz dot format.
// #[command(name = "graph")]
// ShowGraph,
/// Control widgets through CLI.
#[command(name = "widget-control", alias = "wc")]
WidgetControl {
#[command(subcommand)]
action: WidgetControlAction,
},
/// Update the widgets of a particular window. Poll/Listen variables will be cleared
#[command(name = "update", alias = "u")]
TriggerUpdateUI {
@@ -234,6 +241,59 @@ pub enum ActionWithServer {
},
}
/// Subcommands for widget control
#[derive(Subcommand, Debug, Serialize, Deserialize, PartialEq)]
pub enum WidgetControlAction {
/// Remove widget by name
Remove {
/// Names of the widgets to remove
names: Vec<String>,
},
/// Create widgets
Create {
/// Rhai code to create widgets from
rhai_codes: Vec<String>,
/// Name of the widget to add these widgets as a child to
#[arg(long = "parent", short = 'p')]
parent_name: String,
},
/// Update properties of a widget by name
PropertyUpdate {
/// Properties and its value
///
/// Format: value="val1" widget_name="val2"
#[arg(value_parser = parse_inject_var_map)]
property_and_value: HashMap<String, String>,
/// Name of the widget to update the property of
#[arg(long = "widget", short = 'w')]
widget_name: String,
},
/// Add a class to a widget with given name
AddClass {
/// The class to add to the widget
class: String,
/// Name of the widget to add class to
#[arg(long = "widget", short = 'w')]
widget_name: String,
},
/// Remove a class to a widget with given name
RemoveClass {
/// The class to remove from the widget
class: String,
/// Name of the widget to remove class from
#[arg(long = "widget", short = 'w')]
widget_name: String,
},
}
impl Opt {
pub fn from_env() -> Self {
let raw: RawOpt = RawOpt::parse();
@@ -293,6 +353,12 @@ impl ActionWithServer {
self,
) -> (app::DaemonCommand, Option<daemon_response::DaemonResponseReceiver>) {
let command = match self {
ActionWithServer::WidgetControl { action } => {
return with_response_channel(|sender| app::DaemonCommand::WidgetControl {
action,
sender,
})
}
ActionWithServer::TriggerUpdateUI { inject_vars, should_preserve_state, lifetime } => {
return with_response_channel(|sender| app::DaemonCommand::TriggerUpdateUI {
inject_vars,

View File

@@ -1,5 +1,5 @@
use ewwii_plugin_api::{widget_backend, EwwiiAPI};
use rhai::{Array, Dynamic, Engine};
use ewwii_plugin_api::{rhai_backend, widget_backend, EwwiiAPI};
use rhai::{Array, Dynamic, Engine, EvalAltResult};
use std::sync::mpsc::{channel as mpsc_channel, Receiver, Sender};
pub(crate) struct EwwiiImpl {
@@ -36,9 +36,10 @@ impl EwwiiAPI for EwwiiImpl {
fn register_function(
&self,
name: String,
f: Box<dyn Fn(Array) -> Dynamic + Send + Sync>,
namespace: rhai_backend::RhaiFnNamespace,
f: Box<dyn Fn(Array) -> Result<Dynamic, Box<EvalAltResult>> + Send + Sync>,
) -> Result<(), String> {
let func_info = (name, f);
let func_info = (name, namespace, f);
self.requestor
.send(PluginRequest::RegisterFunc(func_info))
@@ -73,7 +74,13 @@ impl EwwiiAPI for EwwiiImpl {
pub(crate) enum PluginRequest {
RhaiEngineAct(Box<dyn FnOnce(&mut Engine) + Send>),
RegisterFunc((String, Box<dyn Fn(Array) -> Dynamic + Send + Sync>)),
RegisterFunc(
(
String,
rhai_backend::RhaiFnNamespace,
Box<dyn Fn(Array) -> Result<Dynamic, Box<EvalAltResult>> + Send + Sync>,
),
),
ListWidgetIds(Sender<Vec<u64>>),
WidgetRegistryAct(Box<dyn FnOnce(&mut widget_backend::WidgetRegistryRepr) + Send>),
}

View File

@@ -53,15 +53,18 @@ fn build_gtk_widget_from_node(
WidgetNode::LocalBind { props, children } => {
build_localbind_util(props, children, widget_reg)?.upcast()
}
WidgetNode::WidgetAction { props, children } => {
build_widgetaction_util(props, children, widget_reg)?.upcast()
}
WidgetNode::CircularProgress { props } => {
build_circular_progress_bar(props, widget_reg)?.upcast()
}
// WidgetNode::Graph { props } => build_graph(props, widget_reg)?.upcast(),
WidgetNode::GtkUI { props } => build_gtk_ui_file(props)?.upcast(),
WidgetNode::Graph { props } => build_graph(props, widget_reg)?.upcast(),
// WidgetNode::Transform { props } => build_transform(props, widget_reg)?.upcast(),
WidgetNode::Slider { props } => build_gtk_scale(props, widget_reg)?.upcast(),
WidgetNode::Scale { props } => build_gtk_scale(props, widget_reg)?.upcast(),
WidgetNode::Progress { props } => build_gtk_progress(props, widget_reg)?.upcast(),
WidgetNode::Image { props } => build_image(props, widget_reg)?.upcast(),
WidgetNode::Icon { props } => build_icon(props, widget_reg)?.upcast(),
WidgetNode::Button { props } => build_gtk_button(props, widget_reg)?.upcast(),
WidgetNode::Label { props } => build_gtk_label(props, widget_reg)?.upcast(),
// WIDGET_NAME_LITERAL => build_gtk_literal(node)?.upcast(),

View File

@@ -1,349 +1,654 @@
// use std::{cell::RefCell, collections::VecDeque};
// // https://www.figuiere.net/technotes/notes/tn002/
// // https://github.com/gtk-rs/examples/blob/master/src/bin/listbox_model.rs
// use anyhow::{anyhow, Result};
// use gtk4::glib::{self, object_subclass, wrapper, Properties};
// use gtk4::{cairo, gdk, prelude::*, subclass::prelude::*};
//! Graph widget with time-series visualization
//!
//! The graph renders data points in a 2D canvas with time on the horizontal axis
//! and values on the vertical axis by default (non-vertical mode).
//!
//! Canvas Layout (default horizontal orientation, no flipping):
//!
//! value ↑
//! │
//! max ┼──────────────┐ (past, max)
//! │ │
//! │ │
//! │ Graph │
//! │ Area │
//! │ │
//! min ┼──────────────┘ (past, min)
//! └──────────────┴───→ time
//! (past) (now)
//!
//! Key coordinates in widget space (after margins):
//! - (0, 0) : Top-left corner (past time, max value)
//! - (width, 0) : Top-right corner (current time, max value)
//! - (0, height): Bottom-left corner (past time, min value)
//! - (width, height): Bottom-right corner (current time, min value)
//!
//! Time flows from left (past) to right (present).
//! Most recent data points appear at the right edge.
//! Older points scroll leftward as time progresses.
//!
//! The `time-range` property controls how many milliseconds of history are visible.
//! Points older than `now - time-range` are automatically pruned.
//!
//! The graph supports multiple render types (Line, Fill, Step variants)
//! and can be flipped/rotated via properties.
// use crate::error_handling_ctx;
use gtk4::glib::property::PropertySet;
use gtk4::prelude::*;
use gtk4::subclass::prelude::*;
use gtk4::{gdk, glib, graphene, gsk};
use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::time::{Duration, Instant};
// // This widget shouldn't be a Bin/Container but I've not been
// // able to subclass just a gtk4::Widget
// wrapper! {
// pub struct Graph(ObjectSubclass<GraphPriv>)
// @extends gtk4::Bin, gtk4::Container, gtk4::Widget;
// }
mod imp {
use super::*;
// #[derive(Properties)]
// #[properties(wrapper_type = Graph)]
// pub struct GraphPriv {
// #[property(get, set, nick = "Value", blurb = "The value", minimum = 0f64, maximum = f64::MAX, default = 0f64)]
// value: RefCell<f64>,
const DEFAULT_VALUE: f64 = 0.0;
const DEFAULT_THICKNESS: f64 = 1.0;
const DEFAULT_MIN: f64 = 0.0;
const DEFAULT_MAX: f64 = 1.0;
const DEFAULT_DYNAMIC: bool = true;
const DEFAULT_TIME_RANGE: u32 = 10_000; // ms
const DEFAULT_FLIP_X: bool = false;
const DEFAULT_FLIP_Y: bool = false;
const DEFAULT_VERTICAL: bool = false;
const DEFAULT_ANIMATE: bool = true;
// #[property(get, set, nick = "Thickness", blurb = "The Thickness", minimum = 0f64, maximum = f64::MAX, default = 1f64)]
// thickness: RefCell<f64>,
pub struct Graph {
pub value: Cell<f64>,
pub thickness: Cell<f64>,
pub line_style: Cell<LineStyle>,
pub min: Cell<f64>,
pub max: Cell<f64>,
pub dynamic: Cell<bool>,
pub time_range: Cell<u32>,
pub flip_x: Cell<bool>,
pub flip_y: Cell<bool>,
pub vertical: Cell<bool>,
pub render_type: Cell<RenderType>,
// #[property(get, set, nick = "Line Style", blurb = "The Line Style", default = "miter")]
// line_style: RefCell<String>,
// Runtime state
history: RefCell<VecDeque<(Instant, f64)>>,
last_updated_at: RefCell<Instant>,
tick_id: RefCell<Option<gtk4::TickCallbackId>>,
min_value_cached: Cell<Option<f64>>,
max_value_cached: Cell<Option<f64>>,
has_received_value: Cell<bool>,
// #[property(get, set, nick = "Maximum Value", blurb = "The Maximum Value", minimum = 0f64, maximum = f64::MAX, default = 100f64)]
// min: RefCell<f64>,
// Cached path (Geometry)
cached_path: RefCell<Option<gsk::Path>>,
// The "anchor" time used to build the path (fixed time reference)
path_anchor_time: Cell<Instant>,
// Size when the path was built (for invalidation)
cached_path_size: Cell<(f32, f32)>,
}
// #[property(get, set, nick = "Minumum Value", blurb = "The Minimum Value", minimum = 0f64, maximum = f64::MAX, default = 0f64)]
// max: RefCell<f64>,
impl Default for Graph {
fn default() -> Self {
Self {
value: Cell::new(DEFAULT_VALUE),
thickness: Cell::new(DEFAULT_THICKNESS),
line_style: Cell::new(LineStyle::default()),
min: Cell::new(DEFAULT_MIN),
max: Cell::new(DEFAULT_MAX),
dynamic: Cell::new(DEFAULT_DYNAMIC),
time_range: Cell::new(DEFAULT_TIME_RANGE),
flip_x: Cell::new(DEFAULT_FLIP_X),
flip_y: Cell::new(DEFAULT_FLIP_Y),
vertical: Cell::new(DEFAULT_VERTICAL),
render_type: Cell::new(RenderType::default()),
// #[property(get, set, nick = "Dynamic", blurb = "If it is dynamic", default = true)]
// dynamic: RefCell<bool>,
history: RefCell::new(VecDeque::new()),
last_updated_at: RefCell::new(Instant::now()),
tick_id: RefCell::new(None),
min_value_cached: Cell::new(None),
max_value_cached: Cell::new(None),
has_received_value: Cell::new(false),
// #[property(get, set, nick = "Time Range", blurb = "The Time Range", minimum = 0u64, maximum = u64::MAX, default = 10u64)]
// time_range: RefCell<u64>,
cached_path: RefCell::new(None),
path_anchor_time: Cell::new(Instant::now()),
cached_path_size: Cell::new((0.0, 0.0)),
}
}
}
// #[property(get, set, nick = "Flip X", blurb = "Flip the x axis", default = true)]
// flip_x: RefCell<bool>,
// #[property(get, set, nick = "Flip Y", blurb = "Flip the y axis", default = true)]
// flip_y: RefCell<bool>,
// #[property(get, set, nick = "Vertical", blurb = "Exchange the x and y axes", default = false)]
// vertical: RefCell<bool>,
#[glib::object_subclass]
impl ObjectSubclass for Graph {
const NAME: &'static str = "EwwiiGraph";
type Type = super::Graph;
type ParentType = gtk4::Widget;
}
// history: RefCell<VecDeque<(std::time::Instant, f64)>>,
// extra_point: RefCell<Option<(std::time::Instant, f64)>>,
// last_updated_at: RefCell<std::time::Instant>,
// }
impl ObjectImpl for Graph {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
obj.add_css_class("graph");
// impl Default for GraphPriv {
// fn default() -> Self {
// Self {
// value: RefCell::new(0.0),
// thickness: RefCell::new(1.0),
// line_style: RefCell::new("miter".to_string()),
// min: RefCell::new(0.0),
// max: RefCell::new(100.0),
// dynamic: RefCell::new(true),
// time_range: RefCell::new(10),
// flip_x: RefCell::new(true),
// flip_y: RefCell::new(true),
// vertical: RefCell::new(false),
// history: RefCell::new(VecDeque::new()),
// extra_point: RefCell::new(None),
// last_updated_at: RefCell::new(std::time::Instant::now()),
// }
// }
// }
if DEFAULT_ANIMATE {
self.set_animate(&obj, true);
}
}
// impl GraphPriv {
// // Updates the history, removing points ouside the range
// fn update_history(&self, v: (std::time::Instant, f64)) {
// let mut history = self.history.borrow_mut();
// let mut last_value = self.extra_point.borrow_mut();
// let mut last_updated_at = self.last_updated_at.borrow_mut();
// *last_updated_at = std::time::Instant::now();
fn properties() -> &'static [glib::ParamSpec] {
use once_cell::sync::Lazy;
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecDouble::builder("value")
.minimum(0.0)
.maximum(f64::MAX)
.default_value(DEFAULT_VALUE)
.build(),
glib::ParamSpecDouble::builder("thickness")
.minimum(0.0)
.maximum(f64::MAX)
.default_value(DEFAULT_THICKNESS)
.build(),
glib::ParamSpecEnum::builder::<LineStyle>("line-style")
.default_value(LineStyle::default())
.build(),
glib::ParamSpecDouble::builder("min")
.minimum(f64::MIN)
.maximum(f64::MAX)
.default_value(DEFAULT_MIN)
.build(),
glib::ParamSpecDouble::builder("max")
.minimum(f64::MIN)
.maximum(f64::MAX)
.default_value(DEFAULT_MAX)
.build(),
glib::ParamSpecBoolean::builder("dynamic")
.default_value(DEFAULT_DYNAMIC)
.build(),
glib::ParamSpecUInt::builder("time-range")
.minimum(0)
.maximum(u32::MAX)
.default_value(DEFAULT_TIME_RANGE)
.build(),
glib::ParamSpecBoolean::builder("flip-x").default_value(DEFAULT_FLIP_X).build(),
glib::ParamSpecBoolean::builder("flip-y").default_value(DEFAULT_FLIP_Y).build(),
glib::ParamSpecBoolean::builder("vertical")
.default_value(DEFAULT_VERTICAL)
.build(),
glib::ParamSpecEnum::builder::<RenderType>("type")
.default_value(RenderType::default())
.build(),
glib::ParamSpecBoolean::builder("animate")
.default_value(DEFAULT_ANIMATE)
.build(),
]
});
PROPERTIES.as_ref()
}
// while let Some(entry) = history.front() {
// if last_updated_at.duration_since(entry.0).as_millis() as u64
// > *self.time_range.borrow()
// {
// *last_value = history.pop_front();
// } else {
// break;
// }
// }
// history.push_back(v);
// }
// /**
// * Receives normalized (0-1) coordinates `x` and `y` and convert them to the
// * point on the widget.
// */
// fn value_to_point(&self, width: f64, height: f64, x: f64, y: f64) -> (f64, f64) {
// let x = if *self.flip_x.borrow() { 1.0 - x } else { x };
// let y = if *self.flip_y.borrow() { 1.0 - y } else { y };
// let (x, y) = if *self.vertical.borrow() { (y, x) } else { (x, y) };
// (width * x, height * y)
// }
// }
fn set_property(&self, _: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
let needs_path_rebuild = match pspec.name() {
"value" => {
let v: f64 = value.get().unwrap();
self.value.set(v);
// impl ObjectImpl for GraphPriv {
// fn properties() -> &'static [glib::ParamSpec] {
// Self::derived_properties()
// }
// Don't record first value (default value)
if self.has_received_value.get() {
self.update_history((Instant::now(), v));
} else {
self.has_received_value.replace(true);
}
// fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
// match pspec.name() {
// "value" => {
// let value = value.get().unwrap();
// self.value.replace(value);
// self.update_history((std::time::Instant::now(), value));
// self.obj().queue_draw();
// }
// "thickness" => {
// self.thickness.replace(value.get().unwrap());
// }
// "max" => {
// self.max.replace(value.get().unwrap());
// }
// "min" => {
// self.min.replace(value.get().unwrap());
// }
// "dynamic" => {
// self.dynamic.replace(value.get().unwrap());
// }
// "time-range" => {
// self.time_range.replace(value.get().unwrap());
// }
// "line-style" => {
// self.line_style.replace(value.get().unwrap());
// }
// "flip-x" => {
// self.flip_x.replace(value.get().unwrap());
// }
// "flip-y" => {
// self.flip_y.replace(value.get().unwrap());
// }
// "vertical" => {
// self.vertical.replace(value.get().unwrap());
// }
// x => panic!("Tried to set inexistant property of Graph: {}", x,),
// }
// }
true
}
"thickness" => {
self.thickness.set(value.get().unwrap());
false // Thickness doesn't affect path geometry
}
"line-style" => {
self.line_style.set(value.get::<LineStyle>().unwrap());
false // Line style doesn't affect path geometry
}
"min" => {
self.min.set(value.get().unwrap());
true
}
"max" => {
self.max.set(value.get().unwrap());
true
}
"dynamic" => {
self.dynamic.set(value.get().unwrap());
true
}
"time-range" => {
self.time_range.set(value.get().unwrap());
true
}
"flip-x" => {
self.flip_x.set(value.get().unwrap());
true
}
"flip-y" => {
self.flip_y.set(value.get().unwrap());
true
}
"vertical" => {
self.vertical.set(value.get().unwrap());
true
}
"type" => {
self.render_type.set(value.get::<RenderType>().unwrap());
true
}
"animate" => {
let animate = value.get().unwrap();
self.set_animate(&self.obj(), animate);
false
}
x => panic!("Tried to set inexistent property of Graph: {}", x),
};
// fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value {
// self.derived_property(id, pspec)
// }
// }
if needs_path_rebuild {
self.cached_path.replace(None);
}
// #[object_subclass]
// impl ObjectSubclass for GraphPriv {
// type ParentType = gtk4::Bin;
// type Type = Graph;
// Queue redraw for any property change
self.obj().queue_draw();
}
// const NAME: &'static str = "Graph";
fn property(&self, _: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"value" => self.value.get().to_value(),
"thickness" => self.thickness.get().to_value(),
"line-style" => self.line_style.get().to_value(),
"min" => self.min.get().to_value(),
"max" => self.max.get().to_value(),
"dynamic" => self.dynamic.get().to_value(),
"time-range" => (self.time_range.get()).to_value(),
"flip-x" => self.flip_x.get().to_value(),
"flip-y" => self.flip_y.get().to_value(),
"vertical" => self.vertical.get().to_value(),
"type" => self.render_type.get().to_value(),
"animate" => self.is_animate().to_value(),
x => panic!("Tried to get inexistent property of Graph: {}", x),
}
}
}
// fn class_init(klass: &mut Self::Class) {
// klass.set_css_name("graph");
// }
// }
struct GraphGeometry {
width: f32,
height: f32,
flip_x: bool,
flip_y: bool,
vertical: bool,
}
// impl Default for Graph {
// fn default() -> Self {
// Self::new()
// }
// }
struct GraphRange {
min: f64,
max: f64,
time_range: f32,
anchor_time: Instant,
}
// impl Graph {
// pub fn new() -> Self {
// glib::Object::new::<Self>()
// }
// }
struct GraphStyle {
render_type: RenderType,
thickness: f32,
line_style: LineStyle,
color: gdk::RGBA,
animate: bool,
}
// impl ContainerImpl for GraphPriv {
// fn add(&self, _widget: &gtk4::Widget) {
// error_handling_ctx::print_error(anyhow!("Error, Graph widget shoudln't have any children"));
// }
// }
impl WidgetImpl for Graph {
fn measure(&self, _orientation: gtk4::Orientation, _for_size: i32) -> (i32, i32, i32, i32) {
let t = self.thickness.get().max(1.0) as i32;
// min, natural, -, -
(t, t * 4, -1, -1)
}
// impl BinImpl for GraphPriv {}
// impl WidgetImpl for GraphPriv {
// fn preferred_width(&self) -> (i32, i32) {
// let thickness = *self.thickness.borrow() as i32;
// (thickness, thickness)
// }
/// Snapshot render nodes.
///
/// Since this is potentially a hot code path, we want to keep it O(N) for maximum performance.
/// - min/max value calculation cached
/// - graph curve path cached
fn snapshot(&self, snapshot: &gtk4::Snapshot) {
let obj = self.obj();
let history = self.history.borrow();
// fn preferred_width_for_height(&self, height: i32) -> (i32, i32) {
// (height, height)
// }
if history.is_empty() {
return;
}
// fn preferred_height(&self) -> (i32, i32) {
// let thickness = *self.thickness.borrow() as i32;
// (thickness, thickness)
// }
// Margins
let margin_start = obj.margin_start() as f32;
let margin_end = obj.margin_end() as f32;
let margin_top = obj.margin_top() as f32;
let margin_bottom = obj.margin_bottom() as f32;
// fn preferred_height_for_width(&self, width: i32) -> (i32, i32) {
// (width, width)
// }
// Allocated size
let total_width = obj.width() as f32;
let total_height = obj.height() as f32;
let width = (total_width - margin_start - margin_end).max(0.0);
let height = (total_height - margin_top - margin_bottom).max(0.0);
// fn draw(&self, cr: &cairo::Context) -> glib::Propagation {
// let res: Result<()> = (|| {
// let history = &*self.history.borrow();
// let extra_point = *self.extra_point.borrow();
let geom = GraphGeometry {
width,
height,
flip_x: self.flip_x.get(),
flip_y: self.flip_y.get(),
vertical: self.vertical.get(),
};
// // Calculate the max value
// let (min, max) = {
// let mut max = *self.max.borrow();
// let min = *self.min.borrow();
// let dynamic = *self.dynamic.borrow();
// if dynamic {
// // Check for points higher than max
// for (_, value) in history {
// if *value > max {
// max = *value;
// }
// }
// if let Some((_, value)) = extra_point {
// if value > max {
// max = value;
// }
// }
// }
// (min, max)
// };
let style = GraphStyle {
render_type: self.render_type.get(),
thickness: self.thickness.get() as f32,
line_style: self.line_style.get(),
color: obj.color(),
animate: self.is_animate(),
};
// let styles = self.obj().style_context();
// let (margin_top, margin_right, margin_bottom, margin_left) = {
// let margin = styles.margin(gtk4::StateFlags::NORMAL);
// (margin.top as f64, margin.right as f64, margin.bottom as f64, margin.left as f64)
// };
// let width = self.obj().allocated_width() as f64 - margin_left - margin_right;
// let height = self.obj().allocated_height() as f64 - margin_top - margin_bottom;
let time_range_millis = self.time_range.get();
if time_range_millis == 0 {
return;
}
// // Calculate graph points once
// // Separating this into another function would require pasing a
// // GraphPriv that would hide interior mutability
// let points = {
// let value_range = max - min;
// let time_range = *self.time_range.borrow() as f64;
// let last_updated_at = self.last_updated_at.borrow();
// let mut points = history
// .iter()
// .map(|(instant, value)| {
// let t = last_updated_at.duration_since(*instant).as_millis() as f64;
// self.value_to_point(
// width,
// height,
// t / time_range,
// (value - min) / value_range,
// )
// })
// .collect::<VecDeque<(f64, f64)>>();
let render_time = Instant::now();
let time_range = time_range_millis as f32;
// // Aad an extra point outside of the graph to extend the line to the left
// if let Some((instant, value)) = extra_point {
// let t = last_updated_at.duration_since(instant).as_millis() as f64;
// let (x, y) = self.value_to_point(
// width,
// height,
// (t - time_range) / time_range,
// (value - min) / value_range,
// );
// points.push_front(if *self.vertical.borrow() { (x, -y) } else { (-x, y) });
// }
// points
// };
// Rebuild path?
let current_size = (width, height);
let mut cached_path = self.cached_path.borrow_mut();
if cached_path.is_none() || self.cached_path_size.get() != current_size {
self.cached_path_size.set(current_size);
self.path_anchor_time.set(render_time);
// // Actually draw the graph
// cr.save()?;
// cr.translate(margin_left, margin_top);
// cr.rectangle(0.0, 0.0, width, height);
// cr.clip();
let range = GraphRange {
min: self.min_value(),
max: self.max_value(),
time_range,
anchor_time: render_time,
};
// // Draw Background
// let bg_color: gdk::RGBA = styles
// .style_property_for_state("background-color", gtk4::StateFlags::NORMAL)
// .get()?;
// if bg_color.alpha() > 0.0 {
// if let Some(first_point) = points.front() {
// cr.line_to(first_point.0, height + margin_bottom);
// }
// for (x, y) in points.iter() {
// cr.line_to(*x, *y);
// }
// cr.line_to(width, height);
let new_path = build_path(&history, &geom, &range, &style);
*cached_path = Some(new_path);
}
// cr.set_source_rgba(
// bg_color.red(),
// bg_color.green(),
// bg_color.blue(),
// bg_color.alpha(),
// );
// cr.fill()?;
// }
// Draw the cached path with translation
snapshot.save();
snapshot.translate(&graphene::Point::new(margin_start, margin_top));
snapshot.push_clip(&graphene::Rect::new(0.0, 0.0, width, height));
// // Draw Line
// let line_color: gdk::RGBA = styles.color(gtk4::StateFlags::NORMAL);
// let thickness = *self.thickness.borrow();
// if line_color.alpha() > 0.0 && thickness > 0.0 {
// for (x, y) in points.iter() {
// cr.line_to(*x, *y);
// }
// Scroll animation
if style.animate && history.len() >= 2 {
// Shift by point interval to hide the gap between latest point and right edge
let interval = {
let t_old = history[history.len() - 2].0;
let t_new = history[history.len() - 1].0;
t_new.checked_duration_since(t_old).unwrap_or_default()
}
.as_millis() as f32;
// let line_style = &*self.line_style.borrow();
// apply_line_style(line_style.as_str(), cr)?;
// cr.set_line_width(thickness);
// cr.set_source_rgba(
// line_color.red(),
// line_color.green(),
// line_color.blue(),
// line_color.alpha(),
// );
// cr.stroke()?;
// }
// Calculate pixel shift based on time
let anchor_time = self.path_anchor_time.get();
let time_shift = render_time.duration_since(anchor_time).as_millis() as f32;
let pixel_shift = (time_shift - interval) / time_range
* if geom.vertical { geom.height } else { geom.width }
* if geom.flip_x { 1.0 } else { -1.0 };
// cr.reset_clip();
// cr.restore()?;
// Ok(())
// })();
// Apply the camera translation
if geom.vertical {
snapshot.translate(&graphene::Point::new(0.0, pixel_shift));
} else {
snapshot.translate(&graphene::Point::new(pixel_shift, 0.0));
}
};
// if let Err(error) = res {
// error_handling_ctx::print_error(error)
// };
// Draw path
if let Some(path) = cached_path.as_ref() {
if matches!(style.render_type, RenderType::Line | RenderType::StepLine) {
// Render as stroked path
let stroke = gsk::Stroke::new(style.thickness);
// glib::Propagation::Proceed
// }
// }
// Configure line style
match style.line_style {
LineStyle::Miter => {
stroke.set_line_cap(gsk::LineCap::Butt);
stroke.set_line_join(gsk::LineJoin::Miter);
}
LineStyle::Bevel => {
stroke.set_line_cap(gsk::LineCap::Square);
stroke.set_line_join(gsk::LineJoin::Bevel);
}
LineStyle::Round => {
stroke.set_line_cap(gsk::LineCap::Round);
stroke.set_line_join(gsk::LineJoin::Round);
}
}
// fn apply_line_style(style: &str, cr: &cairo::Context) -> Result<()> {
// match style {
// "miter" => {
// cr.set_line_cap(cairo::LineCap::Butt);
// cr.set_line_join(cairo::LineJoin::Miter);
// }
// "bevel" => {
// cr.set_line_cap(cairo::LineCap::Square);
// cr.set_line_join(cairo::LineJoin::Bevel);
// }
// "round" => {
// cr.set_line_cap(cairo::LineCap::Round);
// cr.set_line_join(cairo::LineJoin::Round);
// }
// _ => Err(anyhow!("Error, the value: {} for atribute join is not valid", style))?,
// };
// Ok(())
// }
snapshot.append_stroke(path, &stroke, &style.color);
} else {
// Render as filled path
snapshot.append_fill(path, gsk::FillRule::Winding, &style.color);
}
}
snapshot.pop(); // pop clip
snapshot.restore(); // restore translate
}
}
impl Graph {
// Updates the history, removing points outside the range
fn update_history(&self, v: (Instant, f64)) {
let mut history = self.history.borrow_mut();
let now = Instant::now();
self.last_updated_at.set(now);
let time_range_dur = Duration::from_millis(self.time_range.get().into());
let visible_start = now
.checked_sub(time_range_dur)
.unwrap_or_else(|| history.front().map(|(t, _)| *t).unwrap_or(now));
// Animate: Need one extra point more left of the visible area to avoid showing a gap
let points_to_keep_off_canvas = if self.is_animate() { 2 } else { 1 };
// Prune history from the front (oldest).
while history.len() > points_to_keep_off_canvas {
// We check the timestamp of the point at index `points_to_keep_off_canvas`.
// If that point is outside the canvas, we can safely remove the oldest point (index 0).
if history[points_to_keep_off_canvas].0 < visible_start {
if let Some(val) = history.pop_front() {
// Value dropped: min/max values need recalc?
if self.min_value_cached.get().map_or(false, |min| min == val.1) {
self.min_value_cached.set(None);
}
if self.max_value_cached.get().map_or(false, |max| max == val.1) {
self.max_value_cached.set(None);
}
}
} else {
break;
}
}
history.push_back(v);
// New value: Update cached min/max value?
if let Some(min) = self.min_value_cached.get() {
self.min_value_cached.set(Some(min.min(v.1)));
}
if let Some(max) = self.max_value_cached.get() {
self.max_value_cached.set(Some(max.max(v.1)));
}
}
fn set_animate(&self, obj: &super::Graph, animate: bool) {
let mut tick_id_storage = self.tick_id.borrow_mut();
if animate {
// If we want it ON, and it's currently None (OFF)
if tick_id_storage.is_none() {
let id = obj.add_tick_callback(|obj, _clock| {
obj.queue_draw();
glib::ControlFlow::Continue
});
*tick_id_storage = Some(id);
}
} else {
// We want it OFF
if let Some(id) = tick_id_storage.take() {
id.remove();
}
}
}
fn is_animate(&self) -> bool {
self.tick_id.borrow().is_some()
}
fn min_value(&self) -> f64 {
if self.dynamic.get() {
self.min_value_cached.get().unwrap_or_else(|| {
// Calculate from history values
let history = self.history.borrow();
let val = history.iter().fold(f64::INFINITY, |acc, &(_, v)| acc.min(v));
self.min_value_cached.set(Some(val));
val
})
} else {
self.min.get()
}
}
fn max_value(&self) -> f64 {
if self.dynamic.get() {
self.max_value_cached.get().unwrap_or_else(|| {
// Calculate from history values
let history = self.history.borrow();
let val = history.iter().fold(f64::NEG_INFINITY, |acc, &(_, v)| acc.max(v));
self.max_value_cached.set(Some(val));
val
})
} else {
self.max.get()
}
}
}
/// Builds a path relative to a fixed anchor time
fn build_path(
points: &VecDeque<(Instant, f64)>,
geom: &GraphGeometry,
range: &GraphRange,
style: &GraphStyle,
) -> gsk::Path {
if points.is_empty() {
return gsk::PathBuilder::new().to_path();
}
if range.time_range <= 0.0 {
return gsk::PathBuilder::new().to_path();
}
let builder = gsk::PathBuilder::new();
let is_line = matches!(style.render_type, RenderType::Line | RenderType::StepLine);
let is_step = matches!(style.render_type, RenderType::StepLine | RenderType::StepFill);
// Transform point relative to fixed anchor
let transform_point = |t_point: Instant, value: f64| -> (f32, f32) {
let t = range.anchor_time.duration_since(t_point).as_millis() as f32;
let nx = t / range.time_range;
let ny = ((value - range.min) / (range.max - range.min).max(f64::EPSILON)) as f32;
let x = if geom.flip_x { nx } else { 1.0 - nx };
let y = if geom.flip_y { ny } else { 1.0 - ny };
if geom.vertical {
(y * geom.width, x * geom.height)
} else {
(x * geom.width, y * geom.height)
}
};
// Calculate first point coordinates
let &(t0, v0) = &points[0];
let (mut last_x, mut last_y) = transform_point(t0, v0);
let (base_x, base_y) = transform_point(range.anchor_time, range.min);
if is_line {
// For lines: start at first point
builder.move_to(last_x, last_y);
} else {
// For fills: start at baseline then go to first point
if geom.vertical {
builder.move_to(base_x, last_y);
builder.line_to(last_x, last_y);
} else {
builder.move_to(last_x, base_y);
builder.line_to(last_x, last_y);
}
}
// Draw the main graph line
for i in 1..points.len() {
let &(_, v_prev) = &points[i - 1];
let &(t_curr, v_curr) = &points[i];
let (x_curr, y_curr) = transform_point(t_curr, v_curr);
if is_step {
// Create horizontal step segment
let (x_step, y_step) = transform_point(t_curr, v_prev);
builder.line_to(x_step, y_step);
}
builder.line_to(x_curr, y_curr);
last_x = x_curr;
last_y = y_curr;
}
if is_line {
// For lines, we're done
builder.to_path()
} else {
// For fills, close the path to baseline
if geom.vertical {
builder.line_to(base_x, last_y);
} else {
builder.line_to(last_x, base_y);
}
builder.close();
builder.to_path()
}
}
}
#[derive(glib::Enum, Debug, Clone, Copy, PartialEq, Eq, Default)]
#[enum_type(name = "EwwiiGraphLineStyle")]
pub enum LineStyle {
#[default]
Miter,
Bevel,
Round,
}
#[derive(glib::Enum, Debug, Clone, Copy, PartialEq, Eq, Default)]
#[enum_type(name = "EwwiiGraphRenderType")]
pub enum RenderType {
#[default]
Line,
StepLine,
Fill,
StepFill,
}
// public wrapper
glib::wrapper! {
pub struct Graph(ObjectSubclass<imp::Graph>)
@extends gtk4::Widget,
@implements gtk4::Accessible, gtk4::Actionable, gtk4::Buildable, gtk4::ConstraintTarget;
}
impl Graph {
pub fn new() -> Self {
glib::Object::builder().build()
}
}

View File

@@ -28,6 +28,7 @@ use std::{
// custom widgets
// use crate::widgets::{circular_progressbar::CircProg, transform::Transform};
use crate::widgets::circular_progressbar::CircProg;
use crate::widgets::graph::{Graph, RenderType};
/// Connect a gtk signal handler inside of this macro to ensure that when the same code gets run multiple times,
/// the previously connected singal handler first gets disconnected.
@@ -202,6 +203,11 @@ impl WidgetRegistry {
}
pub fn update_props(&self, widget_id: u64, new_props: Map) {
let ei = get_bool_prop(&new_props, "eval_ignore", Some(false)).unwrap_or(false);
if ei {
return;
}
if let Some(entry) = self.widgets.get(&widget_id) {
(entry.update_fn)(&new_props);
}
@@ -213,6 +219,73 @@ impl WidgetRegistry {
entry.widget.unparent();
}
}
pub fn remove_widget_by_name(&mut self, name: &str) -> bool {
if let Some((&id, _)) =
self.widgets.iter().find(|(_, entry)| entry.widget.widget_name().as_str() == name)
{
if let Some(entry) = self.widgets.remove(&id) {
entry.widget.unparent();
log::info!("Deleted widget '{}' on command.", name);
return true;
}
}
log::warn!("Widget '{}' not found", name);
false
}
pub fn get_widget_id_by_name(&self, name: &str) -> Option<u64> {
self.widgets
.iter()
.find(|(_, entry)| entry.widget.widget_name().as_str() == name)
.map(|(&id, _)| id)
}
pub fn update_property_by_name(
&mut self,
widget_name: &str,
property_and_value: (String, String),
) -> bool {
if let Some((&id, _)) = self
.widgets
.iter()
.find(|(_, entry)| entry.widget.widget_name().as_str() == widget_name)
{
if let Some(entry) = self.widgets.get(&id) {
set_property_from_string_anywhere(
&entry.widget,
&property_and_value.0,
&property_and_value.1,
);
}
}
false
}
pub fn update_class_of_widget_by_name(
&mut self,
widget_name: &str,
class: &str,
remove: bool,
) -> bool {
if let Some((&id, _)) = self
.widgets
.iter()
.find(|(_, entry)| entry.widget.widget_name().as_str() == widget_name)
{
if let Some(entry) = self.widgets.get(&id) {
if !remove {
entry.widget.style_context().add_class(class);
} else {
entry.widget.style_context().remove_class(class);
}
}
}
false
}
}
pub(super) fn build_gtk_box(
@@ -423,7 +496,7 @@ pub(super) fn build_localbind_util(
if !current_val.is_empty() {
if let Some(child) = gtk_widget.first_child() {
child.set_property(&prop_name, &current_val);
set_property_from_string_anywhere(&child, &prop_name, &current_val);
}
}
@@ -436,7 +509,11 @@ pub(super) fn build_localbind_util(
gtk_widget,
move |obj, _| {
if let Some(child) = gtk_widget.first_child() {
child.set_property(&prop_name, &obj.property::<String>("value"));
set_property_from_string_anywhere(
&child,
&prop_name,
&obj.property::<String>("value"),
);
}
}
)
@@ -463,6 +540,115 @@ pub(super) fn build_localbind_util(
Ok(gtk_widget)
}
pub(super) fn build_widgetaction_util(
props: &Map,
children: &Vec<WidgetNode>,
widget_registry: &mut WidgetRegistry,
) -> Result<gtk4::Box> {
let gtk_widget = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
let count = children.len();
if count < 1 {
bail!("widget action must contain exactly 1 child");
} else if count > 1 {
bail!("widget action must contain exactly 1 child, but got more");
}
let child_node = children.get(0).cloned().ok_or_else(|| anyhow!("missing child"))?;
let child_widget = build_gtk_widget(&WidgetInput::Node(child_node), widget_registry)?;
gtk_widget.append(&child_widget);
let apply_props = |props: &Map, gtk_widget: &gtk4::Box| -> Result<()> {
let trigger = props
.get("trigger")
.ok_or_else(|| anyhow!("Expected property `trigger`"))?
.clone()
.try_cast::<LocalSignal>()
.ok_or_else(|| anyhow!("Invalid widget action trigger: expected LocalSignal"))?;
let actions = match get_vec_string_prop(&props, "actions", None) {
Ok(a) => a,
Err(e) => bail!("Invalid widget action actions: {}", e),
};
let signal_widget = trigger.data;
connect_signal_handler!(
signal_widget,
signal_widget.connect_notify_local(
Some("value"),
glib::clone!(
#[weak]
gtk_widget,
#[strong]
actions,
move |_, _| {
if let Some(child) = gtk_widget.first_child() {
for action in &actions {
let parts = match shell_words::split(action) {
Ok(v) => v,
Err(err) => {
log::error!("Failed to parse action `{action}`: {err}");
continue;
}
};
let mut parts = parts.into_iter();
let cmd = parts.next();
match cmd.as_deref() {
Some("add-class") => {
if let Some(class) = parts.next() {
child.add_css_class(&class);
}
}
Some("remove-class") => {
if let Some(class) = parts.next() {
child.remove_css_class(&class);
}
}
Some("set-property") => {
if let (Some(prop), Some(value)) =
(parts.next(), parts.next())
{
set_property_from_string_anywhere(
&child, &prop, &value,
);
}
}
_ => {
eprintln!("Unknown action: {action}");
}
}
}
}
}
)
)
);
Ok(())
};
apply_props(&props, &gtk_widget)?;
let gtk_widget_clone = gtk_widget.clone();
let update_fn: UpdateFn = Box::new(move |props: &Map| {
let _ = apply_props(&props, &gtk_widget_clone);
});
let id = hash_props_and_type(&props, "WidgetAction");
widget_registry
.widgets
.insert(id, WidgetEntry { widget: gtk_widget.clone().upcast(), update_fn });
Ok(gtk_widget)
}
struct EventBoxCtrlData {
// hover controller data
onhover_cmd: String,
@@ -513,8 +699,8 @@ pub(super) fn build_event_box(
// controllers
let hover_controller = EventControllerMotion::new();
let gesture_controller = GestureClick::new();
gesture_controller.set_button(0);
let scroll_controller = EventControllerScroll::new(gtk4::EventControllerScrollFlags::BOTH_AXES);
let legacy_controller = EventControllerLegacy::new();
let drop_text_target = DropTarget::new(String::static_type(), gdk::DragAction::COPY);
let drop_uri_target = DropTarget::new(String::static_type(), gdk::DragAction::COPY);
let key_controller = EventControllerKey::new();
@@ -585,12 +771,40 @@ pub(super) fn build_event_box(
}
));
// Support :active selector and run command
// Support :active selector and onclick variant commands
gesture_controller.connect_pressed(glib::clone!(
#[weak]
gtk_widget,
move |_, _, _, _| {
#[strong]
controller_data,
move |gesture, _, _, _| {
gtk_widget.set_state_flags(gtk4::StateFlags::ACTIVE, false);
let controller = controller_data.borrow();
let button = gesture.current_button();
match button {
1 => run_command(controller.cmd_timeout, &controller.onclick_cmd, &[] as &[&str]),
2 => run_command(
controller.cmd_timeout,
&controller.onmiddleclick_cmd,
&[] as &[&str],
),
3 => run_command(
controller.cmd_timeout,
&controller.onrightclick_cmd,
&[] as &[&str],
),
_ => {}
}
}
));
gesture_controller.connect_released(glib::clone!(
#[weak]
gtk_widget,
move |_, _, _, _| {
gtk_widget.unset_state_flags(gtk4::StateFlags::ACTIVE);
}
));
@@ -621,46 +835,6 @@ pub(super) fn build_event_box(
}
));
gesture_controller.connect_released(glib::clone!(
#[weak]
gtk_widget,
move |_, _, _, _| {
gtk_widget.unset_state_flags(gtk4::StateFlags::ACTIVE);
}
));
legacy_controller.connect_event(glib::clone!(
#[strong]
controller_data,
move |_, event| {
if event.event_type() == gtk4::gdk::EventType::ButtonPress {
if let Some(button_event) = event.downcast_ref::<gtk4::gdk::ButtonEvent>() {
let button = button_event.button();
let controller = controller_data.borrow();
match button {
1 => run_command(
controller.cmd_timeout,
&controller.onclick_cmd,
&[] as &[&str],
),
2 => run_command(
controller.cmd_timeout,
&controller.onmiddleclick_cmd,
&[] as &[&str],
),
3 => run_command(
controller.cmd_timeout,
&controller.onrightclick_cmd,
&[] as &[&str],
),
_ => {}
}
}
}
glib::Propagation::Proceed
}
));
drop_uri_target.connect_drop(glib::clone!(
#[strong]
controller_data,
@@ -743,7 +917,6 @@ pub(super) fn build_event_box(
gtk_widget.add_controller(gesture_controller);
gtk_widget.add_controller(hover_controller);
gtk_widget.add_controller(scroll_controller);
gtk_widget.add_controller(legacy_controller);
gtk_widget.add_controller(drop_text_target);
gtk_widget.add_controller(drop_uri_target);
gtk_widget.add_controller(drag_source);
@@ -1005,6 +1178,10 @@ pub(super) fn build_gtk_stack(
let transition = get_string_prop(&props, "transition", Some("crossfade"))?;
widget.set_transition_type(parse_stack_transition(&transition)?);
if let Ok(transition_dur) = get_i32_prop(&props, "transition_duration", None) {
widget.set_transition_duration(transition_dur as u32);
}
// let same_size = get_bool_prop(&props, "same_size", Some(false))?;
// widget.set_homogeneous(same_size);
@@ -1123,7 +1300,7 @@ pub(super) fn build_circular_progress_bar(
widget.set_property("thickness", thickness);
}
if let Ok(clockwise) = get_f64_prop(&props, "clockwise", None) {
if let Ok(clockwise) = get_bool_prop(&props, "clockwise", None) {
widget.set_property("clockwise", clockwise);
}
@@ -1156,7 +1333,7 @@ pub(super) fn build_circular_progress_bar(
}
});
let id = hash_props_and_type(&props, "CircularProgressBar");
let id = hash_props_and_type(&props, "CircularProgress");
widget_registry.widgets.insert(id, WidgetEntry { update_fn, widget: widget.clone().upcast() });
@@ -1165,93 +1342,121 @@ pub(super) fn build_circular_progress_bar(
Ok(widget)
}
// pub(super) fn build_graph(
// props: &Map,
// widget_registry: &mut WidgetRegistry,
// ) -> Result<super::graph::Graph> {
// let widget = super::graph::Graph::new();
pub(super) fn build_graph(props: &Map, widget_registry: &mut WidgetRegistry) -> Result<Graph> {
let widget = Graph::new();
// let apply_props = |props: &Map, widget: &super::graph::Graph| -> Result<()> {
// if let Ok(value) = get_f64_prop(&props, "value", None) {
// if value.is_nan() || value.is_infinite() {
// return Err(anyhow!("Graph's value should never be NaN or infinite"));
// }
// widget.set_property("value", value);
// }
let apply_props = |props: &Map, widget: &Graph| -> Result<()> {
if let Ok(value) = get_f64_prop(&props, "value", None) {
if value.is_nan() || value.is_infinite() {
return Err(anyhow!("Graph's value should never be NaN or infinite"));
}
widget.set_property("value", value);
}
// if let Ok(thickness) = get_f64_prop(&props, "thickness", None) {
// widget.set_property("thickness", thickness);
// }
if let Ok(time_range) = get_duration_prop(&props, "time_range", None) {
let millis = time_range.as_millis();
let millis_u32 = u32::try_from(millis).map_err(|_| {
anyhow!(
"Graph's time_range ({}ms) exceeds maximum representable ({}ms)",
millis,
u32::MAX
)
})?;
// if let Ok(time_range) = get_duration_prop(&props, "time_range", None) {
// widget.set_property("time-range", time_range.as_millis() as u64);
// }
widget.set_property("time-range", millis_u32);
}
// let min = get_f64_prop(&props, "min", Some(0.0)).ok();
// let max = get_f64_prop(&props, "max", Some(100.0)).ok();
let min = get_f64_prop(&props, "min", Some(0.0)).ok();
let max = get_f64_prop(&props, "max", Some(100.0)).ok();
// if let (Some(mi), Some(ma)) = (min, max) {
// if mi > ma {
// return Err(anyhow!("Graph's min ({mi}) should never be higher than max ({ma})"));
// }
// }
if let (Some(min), Some(max)) = (min, max) {
if min > max {
return Err(anyhow!("Graph's min ({min}) should never be higher than max ({max})"));
}
}
// if let Some(mi) = min {
// widget.set_property("min", mi);
// }
if let Ok(dynamic) = get_bool_prop(&props, "dynamic", None) {
widget.set_property("dynamic", dynamic);
}
// if let Some(ma) = max {
// widget.set_property("max", ma);
// }
if let Some(min) = min {
widget.set_property("min", min);
}
// if let Ok(dynamic) = get_bool_prop(&props, "dynamic", None) {
// widget.set_property("dynamic", dynamic);
// }
if let Some(max) = max {
widget.set_property("max", max);
}
// if let Ok(line_style) = get_string_prop(&props, "line_style", None) {
// widget.set_property("line-style", line_style);
// }
if let Ok(render_type) = get_string_prop(&props, "type", None) {
match parse_graph_render_type(render_type.as_str()) {
Ok(t) => widget.set_property("type", t),
Err(e) => return Err(anyhow!("Failed to parse graph type property: {}", e)),
};
}
// // flip-x - whether the x axis should go from high to low
// if let Ok(flip_x) = get_bool_prop(&props, "flip_x", None) {
// widget.set_property("flip-x", flip_x);
// }
if let Ok(thickness) = get_f64_prop(&props, "thickness", None) {
if !matches!(widget.property("type"), RenderType::Line | RenderType::StepLine) {
return Err(anyhow!("Property thickness can only be used with line graphs"));
}
// // flip-y - whether the y axis should go from high to low
// if let Ok(flip_y) = get_bool_prop(&props, "flip_y", None) {
// widget.set_property("flip-y", flip_y);
// }
widget.set_property("thickness", thickness);
}
// // vertical - if set to true, the x and y axes will be exchanged
// if let Ok(vertical) = get_bool_prop(&props, "vertical", None) {
// widget.set_property("vertical", vertical);
// }
if let Ok(line_style) = get_string_prop(&props, "line_style", None) {
if !matches!(widget.property("type"), RenderType::Line | RenderType::StepLine) {
return Err(anyhow!("Property line-style can only be used with line graphs"));
}
// Ok(())
// };
match parse_graph_line_style(line_style.as_str()) {
Ok(ls) => widget.set_property("line-style", ls),
Err(e) => return Err(anyhow!("Failed to parse graph line-style property: {}", e)),
};
}
// apply_props(&props, &widget)?;
// flip-x - whether the x axis should go from high to low
if let Ok(flip_x) = get_bool_prop(&props, "flip_x", None) {
widget.set_property("flip-x", flip_x);
}
// let widget_clone = widget.clone();
// let update_fn: UpdateFn = Box::new(move |props: &Map| {
// let _ = apply_props(props, &widget_clone);
// flip-y - whether the y axis should go from high to low
if let Ok(flip_y) = get_bool_prop(&props, "flip_y", None) {
widget.set_property("flip-y", flip_y);
}
// // now re-apply generic widget attrs
// if let Err(err) =
// resolve_rhai_widget_attrs(&widget_clone.clone().upcast::<gtk4::Widget>(), &props)
// {
// eprintln!("Failed to update widget attrs: {:?}", err);
// }
// });
// vertical - if set to true, the x and y axes will be exchanged
if let Ok(vertical) = get_bool_prop(&props, "vertical", None) {
widget.set_property("vertical", vertical);
}
// let id = hash_props_and_type(&props, "Graph");
if let Ok(animate) = get_bool_prop(&props, "animate", None) {
widget.set_property("animate", animate);
}
// widget_registry.widgets.insert(id, WidgetEntry { update_fn, widget: widget.clone().upcast() });
Ok(())
};
// resolve_rhai_widget_attrs(&widget.clone().upcast::<gtk4::Widget>(), &props)?;
apply_props(&props, &widget)?;
// Ok(widget)
// }
let widget_clone = widget.clone();
let update_fn: UpdateFn = Box::new(move |props: &Map| {
let _ = apply_props(props, &widget_clone);
// now re-apply generic widget attrs
if let Err(err) =
resolve_rhai_widget_attrs(&widget_clone.clone().upcast::<gtk4::Widget>(), &props)
{
eprintln!("Failed to update widget attrs: {:?}", err);
}
});
let id = hash_props_and_type(&props, "Graph");
widget_registry.widgets.insert(id, WidgetEntry { update_fn, widget: widget.clone().upcast() });
resolve_rhai_widget_attrs(&widget.clone().upcast::<gtk4::Widget>(), &props)?;
Ok(widget)
}
pub(super) fn build_gtk_progress(
props: &Map,
@@ -1274,7 +1479,15 @@ pub(super) fn build_gtk_progress(
}
if let Ok(bar_value) = get_f64_prop(&props, "value", None) {
widget.set_fraction(bar_value / 100f64)
widget.set_fraction(bar_value / 100f64);
}
if let Ok(bar_text) = get_string_prop(&props, "text", None) {
widget.set_text(Some(&bar_text));
}
if let Ok(show_text) = get_bool_prop(&props, "show_text", None) {
widget.set_show_text(show_text);
}
Ok(())
@@ -1313,6 +1526,9 @@ pub(super) fn build_image(
let apply_props = |props: &Map, widget: &gtk4::Picture| -> Result<()> {
let path = get_string_prop(&props, "path", None)?;
let can_shrink = get_bool_prop(&props, "can_shrink", Some(true))?;
let content_fit_str = get_string_prop(&props, "content_fit", Some("contain"))?;
let content_fit = parse_content_fit(&content_fit_str)?;
let image_width = get_i32_prop(&props, "image_width", Some(-1))?;
let image_height = get_i32_prop(&props, "image_height", Some(-1))?;
let preserve_aspect_ratio = get_bool_prop(&props, "preserve_aspect_ratio", Some(true))?;
@@ -1322,6 +1538,9 @@ pub(super) fn build_image(
log::warn!("Fill attribute ignored, file is not an svg image");
}
widget.set_content_fit(content_fit);
widget.set_can_shrink(can_shrink);
if path.ends_with(".gif") {
let pixbuf_animation =
gtk4::gdk_pixbuf::PixbufAnimation::from_file(std::path::PathBuf::from(path))?;
@@ -1407,105 +1626,6 @@ pub(super) fn build_image(
Ok(gtk_widget)
}
pub(super) fn build_icon(props: &Map, widget_registry: &mut WidgetRegistry) -> Result<gtk4::Image> {
let gtk_widget = gtk4::Image::new();
let apply_props = |props: &Map, widget: &gtk4::Image| -> Result<()> {
let path = get_string_prop(&props, "path", None)?;
let image_width = get_i32_prop(&props, "image_width", Some(-1))?;
let image_height = get_i32_prop(&props, "image_height", Some(-1))?;
let preserve_aspect_ratio = get_bool_prop(&props, "preserve_aspect_ratio", Some(true))?;
let fill_svg = get_string_prop(&props, "fill_svg", Some(""))?;
if !path.ends_with(".svg") && !fill_svg.is_empty() {
log::warn!("Fill attribute ignored, file is not an svg image");
}
if path.ends_with(".gif") {
let pixbuf_animation =
gtk4::gdk_pixbuf::PixbufAnimation::from_file(std::path::PathBuf::from(path))?;
let iter = pixbuf_animation.iter(None);
let frame_pixbuf = iter.pixbuf();
widget.set_from_pixbuf(Some(&frame_pixbuf));
let widget_clone = widget.clone();
if let Some(delay) = iter.delay_time() {
glib::timeout_add_local(delay, move || {
let frame_pixbuf = iter.pixbuf();
widget_clone.set_from_pixbuf(Some(&frame_pixbuf));
glib::ControlFlow::Continue
});
}
} else {
let pixbuf;
// populate the pixel buffer
if path.ends_with(".svg") && !fill_svg.is_empty() {
let svg_data = std::fs::read_to_string(std::path::PathBuf::from(path.clone()))?;
// The fastest way to add/change fill color
let svg_data = if svg_data.contains("fill=") {
let reg = regex::Regex::new(r#"fill="[^"]*""#)?;
reg.replace(&svg_data, &format!("fill=\"{}\"", fill_svg))
} else {
let reg = regex::Regex::new(r"<svg")?;
reg.replace(&svg_data, &format!("<svg fill=\"{}\"", fill_svg))
};
let stream = gtk4::gio::MemoryInputStream::from_bytes(&gtk4::glib::Bytes::from(
svg_data.as_bytes(),
));
pixbuf = gtk4::gdk_pixbuf::Pixbuf::from_stream_at_scale(
&stream,
image_width,
image_height,
preserve_aspect_ratio,
None::<&gtk4::gio::Cancellable>,
)?;
stream.close(None::<&gtk4::gio::Cancellable>)?;
} else {
pixbuf = gtk4::gdk_pixbuf::Pixbuf::from_file_at_scale(
std::path::PathBuf::from(path),
image_width,
image_height,
preserve_aspect_ratio,
)?;
}
widget.set_from_pixbuf(Some(&pixbuf));
}
if let Ok(icon_name) = get_string_prop(&props, "icon", None) {
widget.set_icon_name(Some(&icon_name));
}
Ok(())
};
apply_props(&props, &gtk_widget)?;
let gtk_widget_clone = gtk_widget.clone();
let update_fn: UpdateFn = Box::new(move |props: &Map| {
let _ = apply_props(props, &gtk_widget_clone);
// now re-apply generic widget attrs
if let Err(err) =
resolve_rhai_widget_attrs(&gtk_widget_clone.clone().upcast::<gtk4::Widget>(), &props)
{
eprintln!("Failed to update widget attrs: {:?}", err);
}
});
let id = hash_props_and_type(&props, "Image");
widget_registry
.widgets
.insert(id, WidgetEntry { update_fn, widget: gtk_widget.clone().upcast() });
resolve_rhai_widget_attrs(&gtk_widget.clone().upcast::<gtk4::Widget>(), &props)?;
Ok(gtk_widget)
}
#[derive(Clone)]
struct GtkButtonCtrlData {
// button press
@@ -1531,7 +1651,9 @@ pub(super) fn build_gtk_button(
}));
let key_controller = EventControllerKey::new();
let legacy_controller = EventControllerLegacy::new();
let gesture_controller = GestureClick::new();
gesture_controller.set_propagation_phase(gtk4::PropagationPhase::Capture);
gesture_controller.set_button(0);
gtk_widget.connect_clicked(glib::clone!(
#[weak]
@@ -1541,35 +1663,26 @@ pub(super) fn build_gtk_button(
}
));
legacy_controller.connect_event(glib::clone!(
gesture_controller.connect_pressed(glib::clone!(
#[strong]
controller_data,
move |_, event| {
if event.event_type() == gtk4::gdk::EventType::ButtonPress {
if let Some(button_event) = event.downcast_ref::<gtk4::gdk::ButtonEvent>() {
let button = button_event.button();
let controller = controller_data.borrow();
match button {
1 => run_command(
controller.cmd_timeout,
&controller.onclick_cmd,
&[] as &[&str],
),
2 => run_command(
controller.cmd_timeout,
&controller.onmiddleclick_cmd,
&[] as &[&str],
),
3 => run_command(
controller.cmd_timeout,
&controller.onrightclick_cmd,
&[] as &[&str],
),
_ => {}
}
}
move |gesture, _, _, _| {
let button = gesture.current_button();
let controller = controller_data.borrow();
match button {
1 => run_command(controller.cmd_timeout, &controller.onclick_cmd, &[] as &[&str]),
2 => run_command(
controller.cmd_timeout,
&controller.onmiddleclick_cmd,
&[] as &[&str],
),
3 => run_command(
controller.cmd_timeout,
&controller.onrightclick_cmd,
&[] as &[&str],
),
_ => {}
}
gtk4::glib::Propagation::Proceed
}
));
@@ -1589,7 +1702,7 @@ pub(super) fn build_gtk_button(
));
gtk_widget.add_controller(key_controller);
gtk_widget.add_controller(legacy_controller);
gtk_widget.add_controller(gesture_controller);
let apply_props = |props: &Map,
widget: &gtk4::Button,
@@ -1770,6 +1883,10 @@ pub(super) fn build_gtk_input(
widget.set_text(&value);
}
if let Ok(value) = get_string_prop(&props, "placeholder", None) {
widget.set_placeholder_text(Some(&value));
}
let timeout = get_duration_prop(&props, "timeout", Some(Duration::from_millis(200)))?;
if let Ok(onchange) = get_string_prop(&props, "onchange", None) {
@@ -1967,6 +2084,23 @@ pub(super) fn build_gtk_combo_box_text(
Ok(gtk_widget)
}
pub(super) fn build_gtk_ui_file(props: &Map) -> Result<gtk4::Widget> {
let path = get_string_prop(&props, "file", None)?;
let main_id = get_string_prop(&props, "id", None)?;
if !std::path::Path::new(&path).exists() {
return Err(anyhow::anyhow!("UI file not found: {}", path));
}
let builder = gtk4::Builder::from_file(&path);
let gtk_widget = builder
.object(&main_id)
.ok_or_else(|| anyhow::anyhow!("No widget with id '{}' in {}", main_id, path))?;
Ok(gtk_widget)
}
pub(super) fn build_gtk_expander(
props: &Map,
children: &Vec<WidgetNode>,
@@ -2253,8 +2387,14 @@ pub(super) fn build_gtk_scale(
props: &Map,
widget_registry: &mut WidgetRegistry,
) -> Result<gtk4::Scale> {
let orientation = props .get("orientation")
.and_then(|v| v.clone().try_cast::<String>())
.map(|s| parse_orientation(&s))
.transpose()?
.unwrap_or(gtk4::Orientation::Horizontal);
let gtk_widget = gtk4::Scale::new(
gtk4::Orientation::Horizontal,
orientation,
Some(&gtk4::Adjustment::new(0.0, 0.0, 100.0, 1.0, 1.0, 1.0)),
);
@@ -2274,10 +2414,12 @@ pub(super) fn build_gtk_scale(
scale_dat,
move |ctrl, event| {
match event.event_type() {
gtk4::gdk::EventType::ButtonPress => {
gtk4::gdk::EventType::ButtonPress
| gtk4::gdk::EventType::TouchBegin => {
scale_dat.borrow_mut().is_being_dragged = true;
}
gtk4::gdk::EventType::ButtonRelease => {
gtk4::gdk::EventType::ButtonRelease
| gtk4::gdk::EventType::TouchEnd => {
let mut scale_dat_mut = scale_dat.borrow_mut();
scale_dat_mut.is_being_dragged = false;
@@ -2340,7 +2482,7 @@ pub(super) fn build_gtk_scale(
}
});
let id = hash_props_and_type(&props, "Slider");
let id = hash_props_and_type(&props, "Scale");
widget_registry
.widgets
@@ -2451,17 +2593,18 @@ pub(super) fn resolve_rhai_widget_attrs(gtk_widget: &gtk4::Widget, props: &Map)
}
}
let css_provider = gtk4::CssProvider::new();
let css_provider2 = css_provider.clone();
if let Ok(style_str) = get_string_prop(&props, "style", None) {
let css_provider = gtk4::CssProvider::new();
let scss = format!("* {{ {} }}", style_str);
css_provider.load_from_data(&grass::from_string(scss, &grass::Options::default())?);
gtk_widget.style_context().add_provider(&css_provider, 950);
}
if let Ok(css_str) = get_string_prop(&props, "css", None) {
let css_provider = gtk4::CssProvider::new();
css_provider.load_from_data(&grass::from_string(css_str, &grass::Options::default())?);
gtk_widget.style_context().add_provider(&css_provider, 950);
css_provider2.load_from_data(&grass::from_string(css_str, &grass::Options::default())?);
gtk_widget.style_context().add_provider(&css_provider2, 950);
}
if let Ok(valign) = get_string_prop(&props, "valign", None) {

View File

@@ -1,8 +1,15 @@
use anyhow::{anyhow, Result};
use gtk4::glib;
use gtk4::glib::gobject_ffi;
use gtk4::glib::translate::{FromGlibPtrNone, IntoGlib};
use gtk4::glib::Value;
use gtk4::pango;
use gtk4::prelude::{Cast, ObjectExt, RangeExt, StaticType, ToValue};
use rhai::Map;
use std::process::Command;
use crate::widgets::graph::{LineStyle, RenderType};
// Run a command and get the output
pub(super) fn run_command<T>(timeout: std::time::Duration, cmd: &str, args: &[T])
where
@@ -226,3 +233,126 @@ pub(super) fn parse_stack_transition(t: &str) -> Result<gtk4::StackTransitionTyp
_ => Err(anyhow!("Invalid stack transition: '{}'", t)),
}
}
/// Graph line style
pub(super) fn parse_graph_line_style(t: &str) -> Result<LineStyle> {
match t.to_ascii_lowercase().as_str() {
"miter" => Ok(LineStyle::Miter),
"bevel" => Ok(LineStyle::Bevel),
"round" => Ok(LineStyle::Round),
_ => Err(anyhow!("Invalid graph line style: '{}'", t)),
}
}
/// Graph render type
pub(super) fn parse_graph_render_type(t: &str) -> Result<RenderType> {
match t.to_ascii_lowercase().as_str() {
"line" => Ok(RenderType::Line),
"step-line" => Ok(RenderType::StepLine),
"fill" => Ok(RenderType::Fill),
"step-fill" => Ok(RenderType::StepFill),
_ => Err(anyhow!("Invalid graph render type: '{}'", t)),
}
}
// For localbind
pub(super) fn set_property_from_string_anywhere(
widget: &gtk4::Widget,
prop_name: &str,
value_str: &str,
) {
fn convert(pspec: &glib::ParamSpec, value_str: &str) -> Option<Value> {
let value_type = pspec.value_type();
if value_type == f64::static_type() {
value_str.parse::<f64>().ok().map(|v| v.to_value())
} else if value_type == i32::static_type() {
value_str.parse::<i32>().ok().map(|v| v.to_value())
} else if value_type == bool::static_type() {
value_str.parse::<bool>().ok().map(|v| v.to_value())
} else if value_type == String::static_type() {
Some(value_str.to_value())
} else {
None
}
}
let obj: &glib::Object = widget.upcast_ref();
if let Some(pspec) = obj.find_property(prop_name) {
if let Some(gv) = convert(&pspec, value_str) {
obj.set_property(prop_name, &gv);
}
return;
}
unsafe {
for iface_type in obj.type_().interfaces() {
for pspec in list_interface_properties(iface_type) {
if pspec.name() == prop_name {
if let Some(v) = convert(&pspec, value_str) {
obj.set_property(prop_name, &v);
}
return;
}
}
}
}
if let Some(range) = widget.downcast_ref::<gtk4::Range>() {
let range_obj: &glib::Object = range.upcast_ref();
if let Some(pspec) = range_obj.find_property(prop_name) {
if let Some(gv) = convert(&pspec, value_str) {
range_obj.set_property(prop_name, &gv);
}
return;
}
let adj = range.adjustment();
let adj_obj: &glib::Object = adj.upcast_ref();
if let Some(pspec) = adj_obj.find_property(prop_name) {
if let Some(gv) = convert(&pspec, value_str) {
adj_obj.set_property(prop_name, &gv);
}
return;
}
}
log::error!("Property '{}' not found on widget {}", prop_name, obj.type_().name());
}
unsafe fn list_interface_properties(iface_type: glib::Type) -> Vec<glib::ParamSpec> {
let mut n_props = 0;
let iface_ptr = gobject_ffi::g_type_default_interface_ref(iface_type.into_glib());
if iface_ptr.is_null() {
return vec![];
}
let props_ptr =
gobject_ffi::g_object_interface_list_properties(iface_ptr as *mut _, &mut n_props);
let props = (0..n_props)
.map(|i| {
let p = *props_ptr.add(i as usize);
glib::ParamSpec::from_glib_none(p)
})
.collect::<Vec<_>>();
gobject_ffi::g_type_default_interface_unref(iface_ptr);
props
}
/// Picture widget
pub(super) fn parse_content_fit(cf: &str) -> Result<gtk4::ContentFit> {
match cf.to_ascii_lowercase().as_str() {
"fill" => Ok(gtk4::ContentFit::Fill),
"contain" => Ok(gtk4::ContentFit::Contain),
"cover" => Ok(gtk4::ContentFit::Cover),
"scaledown" => Ok(gtk4::ContentFit::ScaleDown),
_ => Err(anyhow!("Invalid content fit: '{}'", cf)),
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ewwii_plugin_api"
version = "0.6.1"
version = "0.7.0"
authors = ["byson94 <byson94wastaken@gmail.com>"]
edition = "2021"
license = "GPL-3.0-or-later"

View File

@@ -29,6 +29,7 @@
mod export_macros;
pub mod example;
pub mod rhai_backend;
pub mod widget_backend;
#[cfg(feature = "include-rhai")]
@@ -76,31 +77,52 @@ pub trait EwwiiAPI: Send + Sync {
/// _(include-rhai)_ Expose a function that rhai configuration can call.
///
/// **NOTE:***
///
/// Due to TypeID mismatches, methods like `register_type`, `register_fn`,
/// etc. won't work on the engine and may cause a crash. It is recommended
/// to use the `register_function` API to register a funtion which `api::slib`
/// can call to in rhai.
///
/// # Example
///
/// ```rust
/// use ewwii_plugin_api::{EwwiiAPI, Plugin};
/// use ewwii_plugin_api::{EwwiiAPI, Plugin, rhai_backend::RhaiFnNamespace};
/// use rhai::Dynamic;
///
/// pub struct DummyStructure;
///
/// impl Plugin for DummyStructure {
/// fn init(&self, host: &dyn EwwiiAPI) {
/// host.register_function("my_func".to_string(), Box::new(|args| {
/// host.register_function(
/// "my_func".to_string(),
/// RhaiFnNamespace::Global,
/// Box::new(|args| {
/// // Do stuff
/// // - Perform things on the args (if needed)
/// // - And return a value
///
/// Dynamic::default() // return empty
/// Ok(Dynamic::default()) // return empty
/// }));
/// }
/// }
/// ```
///
/// This example will register a function with signature "my_func(Array)" in rhai.
///
/// ## Example use in rhai
///
/// ```js
/// print(my_func(["param1", "param2"]));
/// ```
#[cfg(feature = "include-rhai")]
fn register_function(
&self,
name: String,
f: Box<dyn Fn(rhai::Array) -> rhai::Dynamic + Send + Sync>,
namespace: rhai_backend::RhaiFnNamespace,
f: Box<
dyn Fn(rhai::Array) -> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> + Send + Sync,
>,
) -> Result<(), String>;
// == Widget Rendering & Logic == //

View File

@@ -0,0 +1,14 @@
//! Module exposing extra utilities for rhai.
#[cfg(feature = "include-rhai")]
mod rhai_included {
/// _(include-rhai)_ An enumrate providing options for
/// function registaration namespaces.
pub enum RhaiFnNamespace {
Custom(String),
Global,
}
}
#[cfg(feature = "include-rhai")]
pub use rhai_included::*;

View File

@@ -13,11 +13,10 @@ pub enum WidgetNode {
FlowBox { props: Map, children: Vec<WidgetNode> },
Button { props: Map },
Image { props: Map },
Icon { props: Map },
Input { props: Map },
Progress { props: Map },
ComboBoxText { props: Map },
Slider { props: Map },
Scale { props: Map },
Checkbox { props: Map },
Expander { props: Map, children: Vec<WidgetNode> },
Revealer { props: Map, children: Vec<WidgetNode> },
@@ -32,7 +31,11 @@ pub enum WidgetNode {
Transform { props: Map },
EventBox { props: Map, children: Vec<WidgetNode> },
ToolTip { props: Map, children: Vec<WidgetNode> },
// Special
LocalBind { props: Map, children: Vec<WidgetNode> },
WidgetAction { props: Map, children: Vec<WidgetNode> },
GtkUI { props: Map },
// Top-level macros
DefWindow { name: String, props: Map, node: Box<WidgetNode> },
@@ -102,9 +105,9 @@ pub fn get_id_to_widget_info<'a>(
// let id = hash_props_and_type(props, "Transform");
insert_wdgt_info(node, props, "Transform", &[], parent_id, id_to_props)?;
}
WidgetNode::Slider { props } => {
// let id = hash_props_and_type(props, "Slider");
insert_wdgt_info(node, props, "Slider", &[], parent_id, id_to_props)?;
WidgetNode::Scale { props } => {
// let id = hash_props_and_type(props, "Scale");
insert_wdgt_info(node, props, "Scale", &[], parent_id, id_to_props)?;
}
WidgetNode::Progress { props } => {
// let id = hash_props_and_type(props, "Progress");
@@ -114,10 +117,6 @@ pub fn get_id_to_widget_info<'a>(
// let id = hash_props_and_type(props, "Image");
insert_wdgt_info(node, props, "Image", &[], parent_id, id_to_props)?;
}
WidgetNode::Icon { props } => {
// let id = hash_props_and_type(props, "Icon");
insert_wdgt_info(node, props, "Icon", &[], parent_id, id_to_props)?;
}
WidgetNode::Button { props } => {
// let id = hash_props_and_type(props, "Button");
insert_wdgt_info(node, props, "Button", &[], parent_id, id_to_props)?;
@@ -166,6 +165,23 @@ pub fn get_id_to_widget_info<'a>(
get_id_to_widget_info(child, id_to_props, Some(id))?;
}
}
WidgetNode::WidgetAction { props, children } => {
let id = hash_props_and_type(props, "WidgetAction");
insert_wdgt_info(
node,
props,
"WidgetAction",
children.as_slice(),
parent_id,
id_to_props,
)?;
for child in children {
get_id_to_widget_info(child, id_to_props, Some(id))?;
}
}
WidgetNode::GtkUI { props } => {
insert_wdgt_info(node, props, "GtkUI", &[], parent_id, id_to_props)?;
}
WidgetNode::ColorChooser { props } => {
// let id = hash_props_and_type(props, "ColorChooser");
insert_wdgt_info(node, props, "ColorChooser", &[], parent_id, id_to_props)?;

View File

@@ -44,11 +44,10 @@ pub fn register_all_widgets(
register_primitive!("label", Label);
register_primitive!("button", Button);
register_primitive!("image", Image);
register_primitive!("icon", Icon);
register_primitive!("input", Input);
register_primitive!("progress", Progress);
register_primitive!("combo_box_text", ComboBoxText);
register_primitive!("scale", Slider);
register_primitive!("scale", Scale);
register_primitive!("checkbox", Checkbox);
register_primitive!("calendar", Calendar);
register_primitive!("graph", Graph);
@@ -83,6 +82,18 @@ pub fn register_all_widgets(
register_with_children!("eventbox", EventBox);
register_with_children!("tooltip", ToolTip);
register_with_children!("localbind", LocalBind);
register_with_children!("widget_action", WidgetAction);
// == Special widget
engine.register_fn(
"gtk_ui",
|path: &str, load: &str| -> Result<WidgetNode, Box<EvalAltResult>> {
let mut props = Map::new();
props.insert("file".into(), path.into());
props.insert("id".into(), load.into());
Ok(WidgetNode::GtkUI { props })
},
);
// == Special signal
let keep_signal_clone = keep_signal.clone();

View File

@@ -75,6 +75,10 @@ impl WidgetNode {
props: with_dyn_id(props.clone(), parent_path),
children: process_children(children, parent_path, "localbind"),
},
WidgetNode::WidgetAction { props, children } => WidgetNode::WidgetAction {
props: with_dyn_id(props.clone(), parent_path),
children: process_children(children, parent_path, "widget_action"),
},
// == Top-level container for multiple widgets ==
WidgetNode::Enter(children) => {
@@ -95,30 +99,29 @@ impl WidgetNode {
node @ WidgetNode::Label { props }
| node @ WidgetNode::Button { props }
| node @ WidgetNode::Image { props }
| node @ WidgetNode::Icon { props }
| node @ WidgetNode::Input { props }
| node @ WidgetNode::Progress { props }
| node @ WidgetNode::ComboBoxText { props }
| node @ WidgetNode::Slider { props }
| node @ WidgetNode::Scale { 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::GtkUI { 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::Icon { .. } => WidgetNode::Icon { 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::Scale { .. } => WidgetNode::Scale { props: new_props },
WidgetNode::Checkbox { .. } => WidgetNode::Checkbox { props: new_props },
WidgetNode::Calendar { .. } => WidgetNode::Calendar { props: new_props },
WidgetNode::ColorButton { .. } => WidgetNode::ColorButton { props: new_props },
@@ -129,6 +132,7 @@ impl WidgetNode {
WidgetNode::CircularProgress { props: new_props }
}
WidgetNode::Graph { .. } => WidgetNode::Graph { props: new_props },
WidgetNode::GtkUI { .. } => WidgetNode::GtkUI { props: new_props },
WidgetNode::Transform { .. } => WidgetNode::Transform { props: new_props },
_ => unreachable!(),
}

View File

@@ -1,4 +1,4 @@
//! IIRhai is a simple crate which configures rhai for the `ewwii` widget system.
//! rhai_impl is a simple crate which configures rhai for the `ewwii` widget system.
//!
//! This crate supports parsing, error handling, and has a custom module_resolver.

View File

@@ -15,7 +15,7 @@ use std::path::Path;
use std::rc::Rc;
pub struct ParseConfig {
engine: Engine,
pub engine: Engine,
all_nodes: Rc<RefCell<Vec<WidgetNode>>>,
keep_signal: Rc<RefCell<Vec<u64>>>,
}
@@ -95,6 +95,24 @@ impl ParseConfig {
Ok(merged_node.setup_dyn_ids("root"))
}
pub fn eval_code_snippet(&mut self, code: &str) -> Result<WidgetNode> {
let mut scope = Scope::new();
// Just eval as node will be in `all_nodes`
let node = self
.engine
.eval_with_scope::<WidgetNode>(&mut scope, code)
.map_err(|e| anyhow!(format_eval_error(&e, code, &self.engine, Some("<dyn eval>"))))?;
// Retain signals
crate::updates::retain_signals(&self.keep_signal.borrow());
// Clear all nodes
self.all_nodes.borrow_mut().clear();
Ok(node)
}
pub fn code_from_file<P: AsRef<Path>>(&mut self, file_path: P) -> Result<String> {
Ok(fs::read_to_string(&file_path)
.map_err(|e| anyhow!("Failed to read {:?}: {}", file_path.as_ref(), e))?)

View File

@@ -1,20 +1,17 @@
pub mod linux;
pub mod slib;
pub mod wifi;
use rhai::exported_module;
use rhai::module_resolvers::StaticModuleResolver;
pub fn register_apilib(resolver: &mut StaticModuleResolver) {
use crate::providers::apilib::{linux::linux, slib::slib, wifi::wifi};
use crate::providers::apilib::{linux::linux, wifi::wifi};
// adding modules
let wifi_mod = exported_module!(wifi);
let linux_mod = exported_module!(linux);
let slib_mod = exported_module!(slib);
// inserting modules
resolver.insert("api::wifi", wifi_mod);
resolver.insert("api::linux", linux_mod);
resolver.insert("api::slib", slib_mod);
}

View File

@@ -1,65 +0,0 @@
//! Slib, A rhai library for interacting with loaded shared libraries.
use rhai::{plugin::*, Array, Dynamic};
#[export_module]
pub mod slib {
/// Call a function registered by the currently loaded shared library
///
/// # Arguments
///
/// * `fn_name`: The name of the function to call
/// * `args`: The arguments to pass to the function (in an array)
///
/// # Returns
///
/// The result from the shared library
///
/// # Example
///
/// ```javascript
/// import "api::slib" as slib;
///
/// let eg_output = slib::call_fn("my_func", ["foo", 80, true]);
/// ```
pub fn call_fn(fn_name: String, args: Array) -> Dynamic {
match shared_utils::slib_store::call_registered(&fn_name, args) {
Ok(Some(d)) => d,
Ok(None) => Dynamic::default(),
Err(e) => {
log::error!("Error calling function: {}", e);
Dynamic::default()
}
}
}
/// List all the registered functions
///
/// # Arguments
///
/// None
///
/// # Returns
///
/// An array of strings containing the names
///
/// # Example
///
/// ```javascript
/// import "api::slib" as slib;
///
/// let eg_output = slib::list_fns();
/// print(eg_output);
/// ```
pub fn list_fns() -> Array {
match shared_utils::slib_store::list_registered() {
Ok(a) => a.into_iter().map(Dynamic::from).collect(),
Err(e) => {
log::error!("Error calling function: {}", e);
Array::new()
}
}
}
}

View File

@@ -1,4 +1,5 @@
use super::{get_prefered_shell, handle_listen, handle_poll};
use crate::parser::ParseConfig;
use gtk4::glib;
use gtk4::prelude::*;
use gtk4::subclass::prelude::*;
@@ -117,7 +118,10 @@ pub fn notify_all_localsignals() {
});
}
pub fn handle_localsignal_changes() {
pub fn handle_localsignal_changes(
parser: Rc<RefCell<ParseConfig>>,
ast: Option<Rc<RefCell<rhai::AST>>>,
) {
let shell = get_prefered_shell();
let get_string_fn = shared_utils::extract_props::get_string_prop;
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<String>();
@@ -174,7 +178,62 @@ pub fn handle_localsignal_changes() {
let mut registry_ref = registry.borrow_mut();
if let Some(signal) = registry_ref.get_mut(&id) {
signal.data.set_value(&value);
let original = value.to_string();
let mut current = original.clone();
let mutations: Vec<rhai::FnPtr> = match signal.props.get("mutations") {
Some(v) => {
if let Ok(arr) = v.as_array_ref() {
arr.iter()
.filter_map(|item| {
item.clone().try_cast::<rhai::FnPtr>().or_else(|| {
log::warn!("Non-function found in signal.props.mutations");
None
})
})
.collect()
} else {
log::warn!("Localsignal mutations property is not an array");
Vec::new()
}
}
None => Vec::new(),
};
if mutations.is_empty() {
signal.data.set_value(&current);
return;
}
let parser_rc = parser.borrow_mut();
let compiled_ast = match ast.as_ref() {
Some(rc) => rc.borrow(),
None => {
log::warn!("No compiled AST available");
signal.data.set_value(&current);
return;
}
};
for mutation in mutations {
match mutation.call::<String>(&parser_rc.engine, &compiled_ast, (current.clone(),)) {
Ok(v) => {
current = v;
}
Err(e) => {
log::warn!(
"Signal {} mutation failed ({}), reverting to original value",
id,
e
);
current = original.clone();
break;
}
}
}
signal.data.set_value(&current);
} else {
log::warn!("No LocalSignal found for id {}", id);
}

View File

@@ -42,6 +42,14 @@ pub fn handle_poll(
}
};
// Skip unchanged values?
const DEFAULT_SKIP: bool = true;
let skip_unchanged =
get_bool_prop(props, "skip_unchanged", Some(DEFAULT_SKIP)).unwrap_or_else(|e| {
log::warn!("Failed to parse skip_unchanged property of poll {}: {}", var_name, e);
DEFAULT_SKIP
});
// No need to do this as we apply the initial value before parsing
// Handle initial value
// if let Ok(initial) = get_string_prop(&props, "initial", None) {
@@ -96,12 +104,18 @@ pub fn handle_poll(
if let Ok(Some(stdout_line)) = output_line {
let stdout_trimmed = stdout_line.trim().to_string();
if Some(&stdout_trimmed) != last_value.as_ref() {
// changed → always send
last_value = Some(stdout_trimmed.clone());
log::debug!("[{}] polled value: {}", var_name, stdout_trimmed);
store.write().unwrap().insert(var_name.clone(), stdout_trimmed);
let _ = tx.send(var_name.clone());
} else {
} else if skip_unchanged {
// unchanged + skipping enabled
log::trace!("[{}] value unchanged, skipping tx", var_name);
} else {
// unchanged + skipping disabled → still send
log::trace!("[{}] value unchanged, skipping disabled", var_name);
let _ = tx.send(var_name.clone());
}
} else {
log::warn!("[{}] shell output ended or failed: {:?}", var_name, output_line);

View File

@@ -10,6 +10,6 @@ homepage = "https://github.com/ewwii-sh/ewwii"
[dependencies]
serde.workspace = true
rhai.workspace = true
rhai = { workspace = true, features = ["internals"] }
anyhow.workspace = true
once_cell.workspace = true

View File

@@ -1,5 +1,4 @@
pub mod extract_props;
pub mod slib_store;
pub mod span;
pub use span::*;

View File

@@ -1,31 +0,0 @@
use once_cell::sync::Lazy;
use rhai::{Array, Dynamic};
use std::collections::HashMap;
use std::sync::Mutex;
static FUNC_REGISTRY: Lazy<Mutex<HashMap<String, Box<dyn Fn(Array) -> Dynamic + Send + Sync>>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
pub fn register_functions(
name: String,
func: Box<dyn Fn(Array) -> Dynamic + Send + Sync>,
) -> Result<(), String> {
let mut registry = FUNC_REGISTRY.lock().map_err(|e| e.to_string())?;
registry.insert(name, func);
Ok(())
}
pub fn call_registered(name: &str, args: Array) -> Result<Option<Dynamic>, String> {
let registry = FUNC_REGISTRY.lock().map_err(|e| e.to_string())?;
if let Some(func) = registry.get(name) {
Ok(Some(func(args)))
} else {
Ok(None)
}
}
pub fn list_registered() -> Result<Vec<String>, String> {
let registry = FUNC_REGISTRY.lock().map_err(|e| e.to_string())?;
Ok(registry.keys().cloned().collect())
}

View File

@@ -5,5 +5,5 @@ edition = "2024"
[dependencies]
rhai_impl.workspace = true
rhai = "1.22.2"
rhai.workspace = true
rhai-autodocs = "0.9.0"