Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39a67a4564 | ||
|
|
4b80630b7e | ||
|
|
dee7979cd5 | ||
|
|
8709371e4e | ||
|
|
e4ab87bc2a | ||
|
|
37b57aee60 | ||
|
|
33ef1720e3 | ||
|
|
c41a495ee0 | ||
|
|
47f93e9cab | ||
|
|
9e91ae61a5 | ||
|
|
87cc157055 | ||
|
|
29983ab9da | ||
|
|
ff9db50831 | ||
|
|
4cb6ac05d3 | ||
|
|
73a64944b8 | ||
|
|
d25c2db420 | ||
|
|
216775f55a | ||
|
|
ad79e81c50 | ||
|
|
df7226d06c | ||
|
|
6e03473133 | ||
|
|
36c58e211d | ||
|
|
70de347bcf | ||
|
|
2cbf64e250 | ||
|
|
a14e559c80 | ||
|
|
59fb1b85eb | ||
|
|
4197a863e5 | ||
|
|
4293c6877d | ||
|
|
f393627932 | ||
|
|
c54ce27505 | ||
|
|
97518eb49c | ||
|
|
ddce15481f | ||
|
|
43721426e8 | ||
|
|
6baa9c7858 | ||
|
|
8ec080a290 | ||
|
|
166c440978 | ||
|
|
6e9dca9d42 | ||
|
|
b50f41b1e0 | ||
|
|
357dcaacbc | ||
|
|
61e681e6bd | ||
|
|
ebd4264621 | ||
|
|
4167c64fde | ||
|
|
6456b2998d | ||
|
|
4de5ab3c59 | ||
|
|
04ca79a5af | ||
|
|
0d803fd962 | ||
|
|
8406c86117 | ||
|
|
1aee3163e0 | ||
|
|
9aec974e9e | ||
|
|
4de148be58 | ||
|
|
3f48178333 | ||
|
|
8af01e44f2 | ||
|
|
a76440f242 | ||
|
|
49c0d14ace | ||
|
|
6a6856192c | ||
|
|
b821d8bb6a | ||
|
|
e12d5f7fb9 |
27
CHANGELOG.md
27
CHANGELOG.md
@@ -5,16 +5,41 @@ 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.
|
||||
- `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.
|
||||
|
||||
15
Cargo.lock
generated
15
Cargo.lock
generated
@@ -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",
|
||||
@@ -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"
|
||||
|
||||
@@ -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.1", features = ["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"
|
||||
|
||||
12
README.md
12
README.md
@@ -1,10 +1,11 @@
|
||||
[](https://deps.rs/repo/github/byson94/ewwii)
|
||||
[](https://ewwii-sh.github.io/docs)
|
||||
|
||||
# Ewwii
|
||||
|
||||
<img src="./.github/EwwiiLogo.png" height="100" align="left"/>
|
||||
|
||||
Elkowars Wacky Widgets Imporved Interface is a fork of Elkowars Wacky Widgets which is a standalone widget system made in Rust that allows you to implement your own, custom widgets in any window manager.
|
||||
Elkowars Wacky Widgets Improved Interface is a fork of Elkowars Wacky Widgets which is a standalone widget system made in Rust that allows you to implement your own, custom widgets in any window manager.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -16,9 +17,16 @@ Examples of projects powered by ewwii.
|
||||
| **Data Structures**<br>[- View Example](./examples/data-structures) | [](./examples/data-structures) |
|
||||
| **Wi-Fi GUI Template**<br>[- View on GitHub](https://github.com/Ewwii-sh/ewifi_gui_template) |  |
|
||||
| **Obsidian Bar Template**<br>[- View on GitHub](https://github.com/Ewwii-sh/obsidian-bar) | [](https://github.com/Ewwii-sh/obsidian-bar) |
|
||||
| **Binary Dots by [@BinaryHarbinger](https://github.com/BinaryHarbinger)**<br>[- View on GitHub](https://github.com/BinaryHarbinger/binarydots/) | [](https://github.com/BinaryHarbinger/binarydots)
|
||||
| **Binary Dots by [@BinaryHarbinger](https://github.com/BinaryHarbinger)**<br>[- View on GitHub](https://github.com/BinaryHarbinger/binarydots/) | [](https://github.com/BinaryHarbinger/binarydots)
|
||||
| **Astatine Dots (Linux Rice with Ewwii)**<br>[- View on GitHub](https://github.com/Ewwii-sh/astatine-dots) | [](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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ewwii"
|
||||
version = "0.3.1"
|
||||
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"
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>),
|
||||
}
|
||||
|
||||
@@ -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::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(),
|
||||
|
||||
@@ -202,6 +202,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 +218,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 +495,7 @@ pub(super) fn build_localbind_util(
|
||||
|
||||
if !current_val.is_empty() {
|
||||
if let Some(child) = gtk_widget.first_child() {
|
||||
set_property_from_string(&child, &prop_name, ¤t_val);
|
||||
set_property_from_string_anywhere(&child, &prop_name, ¤t_val);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,7 +508,7 @@ pub(super) fn build_localbind_util(
|
||||
gtk_widget,
|
||||
move |obj, _| {
|
||||
if let Some(child) = gtk_widget.first_child() {
|
||||
set_property_from_string(
|
||||
set_property_from_string_anywhere(
|
||||
&child,
|
||||
&prop_name,
|
||||
&obj.property::<String>("value"),
|
||||
@@ -467,6 +539,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: >k4::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, >k_widget)?;
|
||||
|
||||
let gtk_widget_clone = gtk_widget.clone();
|
||||
let update_fn: UpdateFn = Box::new(move |props: &Map| {
|
||||
let _ = apply_props(&props, >k_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,
|
||||
@@ -517,8 +698,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();
|
||||
@@ -589,12 +770,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);
|
||||
}
|
||||
));
|
||||
|
||||
@@ -625,46 +834,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,
|
||||
@@ -747,7 +916,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);
|
||||
@@ -1009,6 +1177,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);
|
||||
|
||||
@@ -1127,7 +1299,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);
|
||||
}
|
||||
|
||||
@@ -1278,7 +1450,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(())
|
||||
@@ -1317,6 +1497,9 @@ pub(super) fn build_image(
|
||||
|
||||
let apply_props = |props: &Map, widget: >k4::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))?;
|
||||
@@ -1326,6 +1509,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))?;
|
||||
@@ -1411,105 +1597,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: >k4::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(>k4::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::<>k4::gio::Cancellable>,
|
||||
)?;
|
||||
stream.close(None::<>k4::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, >k_widget)?;
|
||||
|
||||
let gtk_widget_clone = gtk_widget.clone();
|
||||
let update_fn: UpdateFn = Box::new(move |props: &Map| {
|
||||
let _ = apply_props(props, >k_widget_clone);
|
||||
|
||||
// now re-apply generic widget attrs
|
||||
if let Err(err) =
|
||||
resolve_rhai_widget_attrs(>k_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(>k_widget.clone().upcast::<gtk4::Widget>(), &props)?;
|
||||
|
||||
Ok(gtk_widget)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct GtkButtonCtrlData {
|
||||
// button press
|
||||
@@ -1535,7 +1622,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]
|
||||
@@ -1545,35 +1634,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
|
||||
}
|
||||
));
|
||||
|
||||
@@ -1593,7 +1673,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: >k4::Button,
|
||||
@@ -1774,6 +1854,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) {
|
||||
@@ -1971,6 +2055,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>,
|
||||
@@ -2257,10 +2358,15 @@ pub(super) fn build_gtk_scale(
|
||||
props: &Map,
|
||||
widget_registry: &mut WidgetRegistry,
|
||||
) -> Result<gtk4::Scale> {
|
||||
let gtk_widget = gtk4::Scale::new(
|
||||
gtk4::Orientation::Horizontal,
|
||||
Some(>k4::Adjustment::new(0.0, 0.0, 100.0, 1.0, 1.0, 1.0)),
|
||||
);
|
||||
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(orientation, Some(>k4::Adjustment::new(0.0, 0.0, 100.0, 1.0, 1.0, 1.0)));
|
||||
|
||||
// only allow changing the value via the value property if the user isn't currently dragging
|
||||
let scale_dat = Rc::new(RefCell::new(RangeCtrlData {
|
||||
@@ -2278,10 +2384,10 @@ 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;
|
||||
|
||||
@@ -2344,7 +2450,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
|
||||
@@ -2455,17 +2561,18 @@ pub(super) fn resolve_rhai_widget_attrs(gtk_widget: >k4::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) {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
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::{ObjectExt, StaticType, ToValue};
|
||||
use gtk4::prelude::{Cast, ObjectExt, RangeExt, StaticType, ToValue};
|
||||
use rhai::Map;
|
||||
use std::process::Command;
|
||||
|
||||
@@ -230,11 +233,15 @@ pub(super) fn parse_stack_transition(t: &str) -> Result<gtk4::StackTransitionTyp
|
||||
}
|
||||
|
||||
// For localbind
|
||||
pub(super) fn set_property_from_string(widget: >k4::Widget, prop_name: &str, value_str: &str) {
|
||||
if let Some(pspec) = widget.find_property(prop_name) {
|
||||
pub(super) fn set_property_from_string_anywhere(
|
||||
widget: >k4::Widget,
|
||||
prop_name: &str,
|
||||
value_str: &str,
|
||||
) {
|
||||
fn convert(pspec: &glib::ParamSpec, value_str: &str) -> Option<Value> {
|
||||
let value_type = pspec.value_type();
|
||||
|
||||
let gvalue: Option<Value> = if value_type == f64::static_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())
|
||||
@@ -244,19 +251,85 @@ pub(super) fn set_property_from_string(widget: >k4::Widget, prop_name: &str, v
|
||||
Some(value_str.to_value())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(v) = gvalue {
|
||||
widget.set_property(prop_name, &v);
|
||||
} else {
|
||||
log::error!(
|
||||
"Cannot convert '{}' to type {:?} for property '{}'",
|
||||
value_str,
|
||||
value_type,
|
||||
prop_name
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log::error!("Property '{}' not found on widget {:?}", prop_name, widget);
|
||||
}
|
||||
|
||||
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)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 == //
|
||||
|
||||
14
crates/ewwii_plugin_api/src/rhai_backend.rs
Normal file
14
crates/ewwii_plugin_api/src/rhai_backend.rs
Normal 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::*;
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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!(),
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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))?)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(¤t);
|
||||
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(¤t);
|
||||
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(¤t);
|
||||
} else {
|
||||
log::warn!("No LocalSignal found for id {}", id);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod extract_props;
|
||||
pub mod slib_store;
|
||||
pub mod span;
|
||||
|
||||
pub use span::*;
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
12
flake.lock
generated
12
flake.lock
generated
@@ -18,11 +18,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1725534445,
|
||||
"narHash": "sha256-Yd0FK9SkWy+ZPuNqUgmVPXokxDgMJoGuNpMEtkfcf84=",
|
||||
"lastModified": 1768569498,
|
||||
"narHash": "sha256-bB6Nt99Cj8Nu5nIUq0GLmpiErIT5KFshMQJGMZwgqUo=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9bb1e7571aadf31ddb4af77fc64b2d59580f9a39",
|
||||
"rev": "be5afa0fcb31f0a96bf9ecba05a516c66fcd8114",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -46,11 +46,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1725675754,
|
||||
"narHash": "sha256-hXW3csqePOcF2e/PYnpXj72KEYyNj2HzTrVNmS/F7Ug=",
|
||||
"lastModified": 1768704795,
|
||||
"narHash": "sha256-Y33TAp2BHEcuspYvcmBXXD0qdvjftv73PwyKTDOjoSY=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "8cc45e678e914a16c8e224c3237fb07cf21e5e54",
|
||||
"rev": "4b7472a78857ac789fb26616040f55cfcbd36c6e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -56,13 +56,12 @@
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
pkg-config
|
||||
wrapGAppsHook
|
||||
wrapGAppsHook4
|
||||
];
|
||||
buildInputs = with pkgs; [
|
||||
gtk3
|
||||
gtk4
|
||||
librsvg
|
||||
gtk-layer-shell
|
||||
libdbusmenu-gtk3
|
||||
gtk4-layer-shell
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
rhai_impl.workspace = true
|
||||
rhai = "1.22.2"
|
||||
rhai.workspace = true
|
||||
rhai-autodocs = "0.9.0"
|
||||
|
||||
Reference in New Issue
Block a user