From 89a472010551bb79c5fc19c0ee817e9ea526d0cf Mon Sep 17 00:00:00 2001 From: druskus20 Date: Tue, 26 Apr 2022 08:39:33 +0200 Subject: [PATCH 01/10] AttrValue / Update --- crates/eww/src/widgets/def_widget_macro.rs | 37 +++++++++++++++------- crates/yuck/src/config/action.rs | 22 +++++++++++++ crates/yuck/src/config/mod.rs | 1 + crates/yuck/src/error.rs | 3 ++ crates/yuck/src/format_diagnostic.rs | 5 +++ crates/yuck/src/parser/ast_iterator.rs | 6 ++-- crates/yuck/src/parser/from_ast.rs | 28 +++++++++++++++- crates/yuck/src/parser/mod.rs | 2 +- 8 files changed, 88 insertions(+), 16 deletions(-) create mode 100644 crates/yuck/src/config/action.rs diff --git a/crates/eww/src/widgets/def_widget_macro.rs b/crates/eww/src/widgets/def_widget_macro.rs index 14beee374..72ae7ea69 100644 --- a/crates/eww/src/widgets/def_widget_macro.rs +++ b/crates/eww/src/widgets/def_widget_macro.rs @@ -16,7 +16,7 @@ macro_rules! def_widget { // If an attribute is explicitly marked as optional (? appended to type) // the attribute will still show up here, as a `None` value. Otherwise, all values in this map // will be `Some`. - let attr_map: Result>> = try { + let attr_map: Result>> = try { ::maplit::hashmap! { $( eww_shared_util::AttrName(::std::stringify!($attr_name).to_owned()) => @@ -32,7 +32,7 @@ macro_rules! def_widget { // Get all the variables that are referred to in any of the attributes expressions let required_vars: Vec = attr_map .values() - .flat_map(|expr| expr.as_ref().map(|x| x.collect_var_refs()).unwrap_or_default()) + .flat_map(|expr| expr.as_ref().and_then(|x| x.try_into_simplexpr()).map(|x| x.collect_var_refs()).unwrap_or_default()) .collect(); $args.scope_graph.register_listener( @@ -53,12 +53,13 @@ macro_rules! def_widget { let $attr_name = attr_map.get(::std::stringify!($attr_name)) .context("Missing attribute, this should never happen")?; - // if the value is Some, evaluate and typecast it as expected - let $attr_name = if let Some(x) = $attr_name { - Some(x.eval(&values)?.$typecast_func()?) - } else { - None - }; + + + // If the value is some, evaluate and typecast it. + // This now uses a new macro, to match on the type cast function: + // if we're casting into an action, we wanna do a different thing than if we where casting into an expr + let $attr_name = def_widget!(@value_depending_on_type values, $attr_name : $typecast_func $(? $(@ $optional @)?)? $(= $default)?); + // If the attribute is optional, keep it as Option, otherwise unwrap // because we _know_ the value in the attr_map is Some if the attribute is not optional. def_widget!(@unwrap_if_required $attr_name $(? $($optional)?)?); @@ -76,6 +77,20 @@ macro_rules! def_widget { })+ }; + (@value_depending_on_type $values:expr, $attr_name:ident : as_action $(? $(@ $optional:tt @)?)? $(= $default:expr)?) => { + match $attr_name { + Some(yuck::config::attr_value::AttrValue::Action(action)) => Some(action.eval_exprs(&$values)?), + _ => None, + } + }; + + (@value_depending_on_type $values:expr, $attr_name:ident : $typecast_func:ident $(? $(@ $optional:tt @)?)? $(= $default:expr)?) => { + match $attr_name { + Some(yuck::config::attr_value::AttrValue::SimplExpr(expr)) => Some(expr.eval(&$values)?.$typecast_func()?), + _ => None, + } + }; + (@unwrap_if_required $value:ident ?) => { }; (@unwrap_if_required $value:ident) => { let $value = $value.unwrap(); @@ -83,16 +98,16 @@ macro_rules! def_widget { // The attribute is explicitly marked as optional - the value should be provided to the prop function body as Option (@get_value $args:ident, $name:expr, ?) => { - $args.widget_use.attrs.ast_optional::($name)?.clone() + $args.widget_use.attrs.ast_optional::($name)?.clone() }; // The attribute has a default value (@get_value $args:ident, $name:expr, = $default:expr) => { - Some($args.widget_use.attrs.ast_optional::($name)?.clone().unwrap_or_else(|| simplexpr::SimplExpr::synth_literal($default))) + Some($args.widget_use.attrs.ast_optional::($name)?.clone().unwrap_or_else(|| simplexpr::SimplExpr::synth_literal($default))) }; // The attribute is required - the prop will only be ran if this attribute is actually provided. (@get_value $args:ident, $name:expr,) => { - Some($args.widget_use.attrs.ast_required::($name)?.clone()) + Some($args.widget_use.attrs.ast_required::($name)?.clone()) } } diff --git a/crates/yuck/src/config/action.rs b/crates/yuck/src/config/action.rs new file mode 100644 index 000000000..d9c19152d --- /dev/null +++ b/crates/yuck/src/config/action.rs @@ -0,0 +1,22 @@ +use eww_shared_util::VarName; +use simplexpr::SimplExpr; + +pub static ACTION_NAMES: &[&str] = &["update"]; + +// TODO: Maybe separate that into another file +#[derive(Debug, Clone)] +pub enum AttrValue { + Action(Action), + SimplExpr(SimplExpr), +} + +#[derive(Debug, Clone)] +pub enum Action { + Update(Update), +} + +#[derive(Debug, Clone)] +pub struct Update { + pub varname: VarName, + pub value: SimplExpr, +} diff --git a/crates/yuck/src/config/mod.rs b/crates/yuck/src/config/mod.rs index e532f623b..7e4638727 100644 --- a/crates/yuck/src/config/mod.rs +++ b/crates/yuck/src/config/mod.rs @@ -1,3 +1,4 @@ +pub mod action; pub mod attributes; pub mod backend_window_options; pub mod config; diff --git a/crates/yuck/src/error.rs b/crates/yuck/src/error.rs index 104cab9c8..1047fe475 100644 --- a/crates/yuck/src/error.rs +++ b/crates/yuck/src/error.rs @@ -33,6 +33,8 @@ pub enum AstError { NotAValue(Span, AstType), #[error("Expected element {1}, but read {2}")] MismatchedElementName(Span, String, String), + #[error("Unknown action {1}")] + UnknownAction(Span, String), #[error("Keyword `{1}` is missing a value")] DanglingKeyword(Span, String), @@ -116,6 +118,7 @@ impl Spanned for AstError { AstError::NoMoreElementsExpected(span) => *span, AstError::SimplExpr(err) => err.span(), AstError::FormFormatError(err) => err.span(), + AstError::UnknownAction(span, ..) => *span, } } } diff --git a/crates/yuck/src/format_diagnostic.rs b/crates/yuck/src/format_diagnostic.rs index 7da4ed7d3..3730e14d1 100644 --- a/crates/yuck/src/format_diagnostic.rs +++ b/crates/yuck/src/format_diagnostic.rs @@ -96,6 +96,11 @@ impl ToDiagnostic for AstError { label = span => "Expected some value here", note = format!("Got: {}", actual), }, + AstError::UnknownAction(span, actual) => gen_diagnostic! { + msg = format!("Unknown action `{}`", actual), + label = span, + note = format!("Must be one of: {}", crate::config::action::ACTION_NAMES.iter().join(", ")), + }, AstError::ParseError { file_id, source } => lalrpop_error_to_diagnostic(source, *file_id), AstError::MismatchedElementName(span, expected, got) => gen_diagnostic! { diff --git a/crates/yuck/src/parser/ast_iterator.rs b/crates/yuck/src/parser/ast_iterator.rs index 12577ac1a..fe1981fd3 100644 --- a/crates/yuck/src/parser/ast_iterator.rs +++ b/crates/yuck/src/parser/ast_iterator.rs @@ -40,9 +40,9 @@ macro_rules! return_or_put_back { impl> AstIterator { return_or_put_back! { - fn expect_symbol -> AstType::Symbol, (Span, String) = Ast::Symbol(span, x) => (span, x) - fn expect_list -> AstType::List, (Span, Vec) = Ast::List(span, x) => (span, x) - fn expect_array -> AstType::Array, (Span, Vec) = Ast::Array(span, x) => (span, x) + fn expect_symbol -> AstType::Symbol, (Span, String) = Ast::Symbol(span, x) => (span, x) + fn expect_list -> AstType::List, (Span, Vec) = Ast::List(span, x) => (span, x) + fn expect_array -> AstType::Array, (Span, Vec) = Ast::Array(span, x) => (span, x) } pub fn expect_literal(&mut self) -> AstResult<(Span, DynVal)> { diff --git a/crates/yuck/src/parser/from_ast.rs b/crates/yuck/src/parser/from_ast.rs index 5cc53979a..1c2544b40 100644 --- a/crates/yuck/src/parser/from_ast.rs +++ b/crates/yuck/src/parser/from_ast.rs @@ -2,7 +2,11 @@ use super::{ ast::{Ast, AstType}, ast_iterator::AstIterator, }; -use crate::{error::*, parser}; +use crate::{ + config::action::{AttrValue, Update}, + error::*, + parser, +}; use eww_shared_util::{AttrName, Span, VarName}; use itertools::Itertools; use simplexpr::{ast::SimplExpr, dynval::DynVal}; @@ -56,3 +60,25 @@ impl FromAst for SimplExpr { } } } + +use crate::config::action::Action; +impl FromAst for Action { + fn from_ast(e: Ast) -> AstResult { + let mut iter = e.try_ast_iter()?; + let (span, action) = iter.expect_symbol()?; + match action.as_str() { + "update" => { + let (varname_span, varname) = iter.expect_symbol()?; + let (value_span, value) = iter.expect_simplexpr()?; + iter.expect_done()?; + Ok(Action::Update(Update { varname: VarName(varname), value })) + } + _ => Err(AstError::UnknownAction(span, action)), + } + } +} +impl FromAst for AttrValue { + fn from_ast(e: Ast) -> AstResult { + todo!() + } +} diff --git a/crates/yuck/src/parser/mod.rs b/crates/yuck/src/parser/mod.rs index 62c729141..e5ee14ea5 100644 --- a/crates/yuck/src/parser/mod.rs +++ b/crates/yuck/src/parser/mod.rs @@ -33,7 +33,7 @@ pub fn parse_toplevel(file_id: usize, s: String) -> AstResult<(Span, Vec)> parser.parse(file_id, lexer).map_err(|e| AstError::from_parse_error(file_id, e)) } -/// get a single ast node from a list of asts, returning an Err if the length is not exactly 1. +/// get a single ast node from a list of asts, returning an Err if the length is not exactly 1. (This is used for parsing literals) pub fn require_single_toplevel(span: Span, mut asts: Vec) -> AstResult { match asts.len() { 0 => Err(AstError::MissingNode(span)), From 3fb5b9de031cd836f5947c70e1391c152ad76630 Mon Sep 17 00:00:00 2001 From: elkowar <5300871+elkowar@users.noreply.github.com> Date: Tue, 26 Apr 2022 10:41:06 +0200 Subject: [PATCH 02/10] Support actions in def_widget macro --- crates/eww/src/widgets/def_widget_macro.rs | 10 +++--- crates/eww/src/widgets/widget_definitions.rs | 2 +- crates/yuck/src/config/action.rs | 35 ++++++++++++++++---- crates/yuck/src/parser/from_ast.rs | 13 ++++---- 4 files changed, 41 insertions(+), 19 deletions(-) diff --git a/crates/eww/src/widgets/def_widget_macro.rs b/crates/eww/src/widgets/def_widget_macro.rs index 72ae7ea69..785cc2869 100644 --- a/crates/eww/src/widgets/def_widget_macro.rs +++ b/crates/eww/src/widgets/def_widget_macro.rs @@ -16,7 +16,7 @@ macro_rules! def_widget { // If an attribute is explicitly marked as optional (? appended to type) // the attribute will still show up here, as a `None` value. Otherwise, all values in this map // will be `Some`. - let attr_map: Result>> = try { + let attr_map: Result>> = try { ::maplit::hashmap! { $( eww_shared_util::AttrName(::std::stringify!($attr_name).to_owned()) => @@ -79,14 +79,14 @@ macro_rules! def_widget { (@value_depending_on_type $values:expr, $attr_name:ident : as_action $(? $(@ $optional:tt @)?)? $(= $default:expr)?) => { match $attr_name { - Some(yuck::config::attr_value::AttrValue::Action(action)) => Some(action.eval_exprs(&$values)?), + Some(yuck::config::action::AttrValue::Action(action)) => Some(action.eval_exprs(&$values)?), _ => None, } }; (@value_depending_on_type $values:expr, $attr_name:ident : $typecast_func:ident $(? $(@ $optional:tt @)?)? $(= $default:expr)?) => { match $attr_name { - Some(yuck::config::attr_value::AttrValue::SimplExpr(expr)) => Some(expr.eval(&$values)?.$typecast_func()?), + Some(yuck::config::action::AttrValue::SimplExpr(expr)) => Some(expr.eval(&$values)?.$typecast_func()?), _ => None, } }; @@ -103,7 +103,9 @@ macro_rules! def_widget { // The attribute has a default value (@get_value $args:ident, $name:expr, = $default:expr) => { - Some($args.widget_use.attrs.ast_optional::($name)?.clone().unwrap_or_else(|| simplexpr::SimplExpr::synth_literal($default))) + Some($args.widget_use.attrs.ast_optional::($name)? + .clone() + .unwrap_or_else(|| yuck::config::action::AttrValue::SimplExpr(simplexpr::SimplExpr::synth_literal($default)))) }; // The attribute is required - the prop will only be ran if this attribute is actually provided. diff --git a/crates/eww/src/widgets/widget_definitions.rs b/crates/eww/src/widgets/widget_definitions.rs index 0eff5cfe9..27154b2d2 100644 --- a/crates/eww/src/widgets/widget_definitions.rs +++ b/crates/eww/src/widgets/widget_definitions.rs @@ -1,5 +1,5 @@ #![allow(clippy::option_map_unit_fn)] -use super::{build_widget::BuilderArgs, circular_progressbar::*, transform::*, run_command}; +use super::{build_widget::BuilderArgs, circular_progressbar::*, run_command, transform::*}; use crate::{ def_widget, enum_parse, error::DiagError, diff --git a/crates/yuck/src/config/action.rs b/crates/yuck/src/config/action.rs index d9c19152d..48427e78d 100644 --- a/crates/yuck/src/config/action.rs +++ b/crates/yuck/src/config/action.rs @@ -1,5 +1,7 @@ +use std::collections::HashMap; + use eww_shared_util::VarName; -use simplexpr::SimplExpr; +use simplexpr::{dynval::DynVal, eval::EvalError, SimplExpr}; pub static ACTION_NAMES: &[&str] = &["update"]; @@ -10,13 +12,32 @@ pub enum AttrValue { SimplExpr(SimplExpr), } -#[derive(Debug, Clone)] +impl AttrValue { + pub fn try_into_simplexpr(&self) -> Option<&SimplExpr> { + match self { + Self::SimplExpr(x) => Some(x), + _ => None, + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] pub enum Action { - Update(Update), + Update(VarName, SimplExpr), + Noop, } -#[derive(Debug, Clone)] -pub struct Update { - pub varname: VarName, - pub value: SimplExpr, +impl Action { + pub fn eval_exprs(&self, values: &HashMap) -> Result { + Ok(match self { + Self::Update(varname, expr) => ResolvedAction::Update(varname.clone(), expr.eval(values)?), + Self::Noop => ResolvedAction::Noop, + }) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ResolvedAction { + Update(VarName, DynVal), + Noop, } diff --git a/crates/yuck/src/parser/from_ast.rs b/crates/yuck/src/parser/from_ast.rs index 1c2544b40..2138d3cec 100644 --- a/crates/yuck/src/parser/from_ast.rs +++ b/crates/yuck/src/parser/from_ast.rs @@ -2,11 +2,7 @@ use super::{ ast::{Ast, AstType}, ast_iterator::AstIterator, }; -use crate::{ - config::action::{AttrValue, Update}, - error::*, - parser, -}; +use crate::{config::action::AttrValue, error::*, parser}; use eww_shared_util::{AttrName, Span, VarName}; use itertools::Itertools; use simplexpr::{ast::SimplExpr, dynval::DynVal}; @@ -71,7 +67,7 @@ impl FromAst for Action { let (varname_span, varname) = iter.expect_symbol()?; let (value_span, value) = iter.expect_simplexpr()?; iter.expect_done()?; - Ok(Action::Update(Update { varname: VarName(varname), value })) + Ok(Action::Update(VarName(varname), value)) } _ => Err(AstError::UnknownAction(span, action)), } @@ -79,6 +75,9 @@ impl FromAst for Action { } impl FromAst for AttrValue { fn from_ast(e: Ast) -> AstResult { - todo!() + match &e { + Ast::List(..) => Ok(AttrValue::Action(Action::from_ast(e)?)), + _ => Ok(AttrValue::SimplExpr(SimplExpr::from_ast(e)?)), + } } } From c84e067169ffa1ea7e13c9eec0ed177f4d5c267c Mon Sep 17 00:00:00 2001 From: elkowar <5300871+elkowar@users.noreply.github.com> Date: Tue, 26 Apr 2022 10:31:39 +0200 Subject: [PATCH 03/10] Rename module from action to attr_value --- crates/eww/src/widgets/def_widget_macro.rs | 14 +++++++------- .../yuck/src/config/{action.rs => attr_value.rs} | 0 crates/yuck/src/config/mod.rs | 2 +- crates/yuck/src/format_diagnostic.rs | 2 +- crates/yuck/src/parser/from_ast.rs | 7 +++++-- 5 files changed, 14 insertions(+), 11 deletions(-) rename crates/yuck/src/config/{action.rs => attr_value.rs} (100%) diff --git a/crates/eww/src/widgets/def_widget_macro.rs b/crates/eww/src/widgets/def_widget_macro.rs index 785cc2869..813f641c8 100644 --- a/crates/eww/src/widgets/def_widget_macro.rs +++ b/crates/eww/src/widgets/def_widget_macro.rs @@ -16,7 +16,7 @@ macro_rules! def_widget { // If an attribute is explicitly marked as optional (? appended to type) // the attribute will still show up here, as a `None` value. Otherwise, all values in this map // will be `Some`. - let attr_map: Result>> = try { + let attr_map: Result>> = try { ::maplit::hashmap! { $( eww_shared_util::AttrName(::std::stringify!($attr_name).to_owned()) => @@ -79,14 +79,14 @@ macro_rules! def_widget { (@value_depending_on_type $values:expr, $attr_name:ident : as_action $(? $(@ $optional:tt @)?)? $(= $default:expr)?) => { match $attr_name { - Some(yuck::config::action::AttrValue::Action(action)) => Some(action.eval_exprs(&$values)?), + Some(yuck::config::attr_value::AttrValue::Action(action)) => Some(action.eval_exprs(&$values)?), _ => None, } }; (@value_depending_on_type $values:expr, $attr_name:ident : $typecast_func:ident $(? $(@ $optional:tt @)?)? $(= $default:expr)?) => { match $attr_name { - Some(yuck::config::action::AttrValue::SimplExpr(expr)) => Some(expr.eval(&$values)?.$typecast_func()?), + Some(yuck::config::attr_value::AttrValue::SimplExpr(expr)) => Some(expr.eval(&$values)?.$typecast_func()?), _ => None, } }; @@ -98,18 +98,18 @@ macro_rules! def_widget { // The attribute is explicitly marked as optional - the value should be provided to the prop function body as Option (@get_value $args:ident, $name:expr, ?) => { - $args.widget_use.attrs.ast_optional::($name)?.clone() + $args.widget_use.attrs.ast_optional::($name)?.clone() }; // The attribute has a default value (@get_value $args:ident, $name:expr, = $default:expr) => { - Some($args.widget_use.attrs.ast_optional::($name)? + Some($args.widget_use.attrs.ast_optional::($name)? .clone() - .unwrap_or_else(|| yuck::config::action::AttrValue::SimplExpr(simplexpr::SimplExpr::synth_literal($default)))) + .unwrap_or_else(|| yuck::config::attr_value::AttrValue::SimplExpr(simplexpr::SimplExpr::synth_literal($default)))) }; // The attribute is required - the prop will only be ran if this attribute is actually provided. (@get_value $args:ident, $name:expr,) => { - Some($args.widget_use.attrs.ast_required::($name)?.clone()) + Some($args.widget_use.attrs.ast_required::($name)?.clone()) } } diff --git a/crates/yuck/src/config/action.rs b/crates/yuck/src/config/attr_value.rs similarity index 100% rename from crates/yuck/src/config/action.rs rename to crates/yuck/src/config/attr_value.rs diff --git a/crates/yuck/src/config/mod.rs b/crates/yuck/src/config/mod.rs index 7e4638727..e91ca3b06 100644 --- a/crates/yuck/src/config/mod.rs +++ b/crates/yuck/src/config/mod.rs @@ -1,4 +1,4 @@ -pub mod action; +pub mod attr_value; pub mod attributes; pub mod backend_window_options; pub mod config; diff --git a/crates/yuck/src/format_diagnostic.rs b/crates/yuck/src/format_diagnostic.rs index 3730e14d1..cfd076a6f 100644 --- a/crates/yuck/src/format_diagnostic.rs +++ b/crates/yuck/src/format_diagnostic.rs @@ -99,7 +99,7 @@ impl ToDiagnostic for AstError { AstError::UnknownAction(span, actual) => gen_diagnostic! { msg = format!("Unknown action `{}`", actual), label = span, - note = format!("Must be one of: {}", crate::config::action::ACTION_NAMES.iter().join(", ")), + note = format!("Must be one of: {}", crate::config::attr_value::ACTION_NAMES.iter().join(", ")), }, AstError::ParseError { file_id, source } => lalrpop_error_to_diagnostic(source, *file_id), diff --git a/crates/yuck/src/parser/from_ast.rs b/crates/yuck/src/parser/from_ast.rs index 2138d3cec..65c999ce4 100644 --- a/crates/yuck/src/parser/from_ast.rs +++ b/crates/yuck/src/parser/from_ast.rs @@ -2,7 +2,11 @@ use super::{ ast::{Ast, AstType}, ast_iterator::AstIterator, }; -use crate::{config::action::AttrValue, error::*, parser}; +use crate::{ + config::attr_value::{Action, AttrValue}, + error::*, + parser, +}; use eww_shared_util::{AttrName, Span, VarName}; use itertools::Itertools; use simplexpr::{ast::SimplExpr, dynval::DynVal}; @@ -57,7 +61,6 @@ impl FromAst for SimplExpr { } } -use crate::config::action::Action; impl FromAst for Action { fn from_ast(e: Ast) -> AstResult { let mut iter = e.try_ast_iter()?; From f89b317138987b0af22b62164f4f57bf4d3e4313 Mon Sep 17 00:00:00 2001 From: elkowar <5300871+elkowar@users.noreply.github.com> Date: Wed, 27 Apr 2022 17:46:27 +0200 Subject: [PATCH 04/10] Start implement the state part of this --- crates/eww/src/widgets/build_widget.rs | 26 +++++++++++++- crates/eww/src/widgets/def_widget_macro.rs | 6 ++-- crates/eww/src/widgets/mod.rs | 16 +++++++++ crates/eww/src/widgets/widget_definitions.rs | 15 +++++--- crates/yuck/src/config/attr_value.rs | 15 -------- crates/yuck/src/config/widget_use.rs | 37 ++++++++++++++++++-- crates/yuck/src/error.rs | 3 ++ crates/yuck/src/format_diagnostic.rs | 5 +++ 8 files changed, 97 insertions(+), 26 deletions(-) diff --git a/crates/eww/src/widgets/build_widget.rs b/crates/eww/src/widgets/build_widget.rs index 897a912bf..da2172c9a 100644 --- a/crates/eww/src/widgets/build_widget.rs +++ b/crates/eww/src/widgets/build_widget.rs @@ -13,7 +13,7 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc}; use yuck::{ config::{ widget_definition::WidgetDefinition, - widget_use::{BasicWidgetUse, ChildrenWidgetUse, LoopWidgetUse, WidgetUse}, + widget_use::{BasicWidgetUse, ChildrenWidgetUse, LetWidgetUse, LoopWidgetUse, WidgetUse}, }, gen_diagnostic, }; @@ -56,6 +56,7 @@ pub fn build_gtk_widget( WidgetUse::Basic(widget_use) => { build_basic_gtk_widget(graph, widget_defs, calling_scope, widget_use, custom_widget_invocation) } + WidgetUse::Let(let_use) => build_let_special_widget(graph, widget_defs, calling_scope, let_use, custom_widget_invocation), WidgetUse::Loop(_) | WidgetUse::Children(_) => Err(anyhow::anyhow!(DiagError::new(gen_diagnostic! { msg = "This widget can only be used as a child of some container widget such as box", label = widget_use.span(), @@ -111,6 +112,29 @@ fn build_basic_gtk_widget( } } +fn build_let_special_widget( + graph: &mut ScopeGraph, + widget_defs: Rc>, + calling_scope: ScopeIndex, + widget_use: LetWidgetUse, + custom_widget_invocation: Option>, +) -> Result { + let child = widget_use.body.first().expect("no child in let"); + let let_scope = graph.register_new_scope( + "let-widget".to_string(), + Some(calling_scope), + calling_scope, + widget_use.defined_vars.into_iter().map(|(k, v)| (AttrName(k.to_string()), v)).collect(), + )?; + let gtk_widget = build_gtk_widget(graph, widget_defs, let_scope, child.clone(), custom_widget_invocation)?; + let scope_graph_sender = graph.event_sender.clone(); + + gtk_widget.connect_destroy(move |_| { + let _ = scope_graph_sender.send(ScopeGraphEvent::RemoveScope(let_scope)); + }); + Ok(gtk_widget) +} + /// build a [`gtk::Widget`] out of a [`WidgetUse`] that uses a /// **builtin widget**. User defined widgets are handled by [`widget_definitions::widget_use_to_gtk_widget`]. /// diff --git a/crates/eww/src/widgets/def_widget_macro.rs b/crates/eww/src/widgets/def_widget_macro.rs index 813f641c8..73c1938ab 100644 --- a/crates/eww/src/widgets/def_widget_macro.rs +++ b/crates/eww/src/widgets/def_widget_macro.rs @@ -42,7 +42,7 @@ macro_rules! def_widget { f: Box::new({ let $gtk_widget = gdk::glib::clone::Downgrade::downgrade(&$gtk_widget); move |$scope_graph, values| { - let $gtk_widget = gdk::glib::clone::Upgrade::upgrade(&$gtk_widget).unwrap(); + let $gtk_widget = gdk::glib::clone::Upgrade::upgrade(&$gtk_widget).expect("Failed to upgrade widget ref"); // values is a map of all the variables that are required to evaluate the // attributes expression. @@ -79,7 +79,7 @@ macro_rules! def_widget { (@value_depending_on_type $values:expr, $attr_name:ident : as_action $(? $(@ $optional:tt @)?)? $(= $default:expr)?) => { match $attr_name { - Some(yuck::config::attr_value::AttrValue::Action(action)) => Some(action.eval_exprs(&$values)?), + Some(yuck::config::attr_value::AttrValue::Action(action)) => Some(action), _ => None, } }; @@ -93,7 +93,7 @@ macro_rules! def_widget { (@unwrap_if_required $value:ident ?) => { }; (@unwrap_if_required $value:ident) => { - let $value = $value.unwrap(); + let $value = $value.expect("No value was provided, eventhough value was required"); }; // The attribute is explicitly marked as optional - the value should be provided to the prop function body as Option diff --git a/crates/eww/src/widgets/mod.rs b/crates/eww/src/widgets/mod.rs index 56008854a..a96050a9c 100644 --- a/crates/eww/src/widgets/mod.rs +++ b/crates/eww/src/widgets/mod.rs @@ -1,5 +1,10 @@ use std::process::Command; +use anyhow::Result; +use yuck::config::attr_value::Action; + +use crate::state::scope_graph::{ScopeGraph, ScopeIndex}; + pub mod build_widget; pub mod circular_progressbar; pub mod def_widget_macro; @@ -60,3 +65,14 @@ mod test { assert_eq!("baz foo bar", replace_placeholders("{1} foo {0}", &["bar", "baz"]),); } } + +pub(self) fn run_action(graph: &mut ScopeGraph, scope: ScopeIndex, action: &Action) -> Result<()> { + match action { + Action::Update(varname, expr) => { + let value = graph.evaluate_simplexpr_in_scope(scope, expr)?; + graph.update_value(scope, varname, value)?; + } + Action::Noop => {} + } + Ok(()) +} diff --git a/crates/eww/src/widgets/widget_definitions.rs b/crates/eww/src/widgets/widget_definitions.rs index 27154b2d2..e82e37d37 100644 --- a/crates/eww/src/widgets/widget_definitions.rs +++ b/crates/eww/src/widgets/widget_definitions.rs @@ -5,7 +5,7 @@ use crate::{ error::DiagError, error_handling_ctx, util::{list_difference, unindent}, - widgets::build_widget::build_gtk_widget, + widgets::{build_widget::build_gtk_widget, run_action}, }; use anyhow::{anyhow, Context, Result}; use codespan_reporting::diagnostic::Severity; @@ -25,7 +25,7 @@ use std::{ time::Duration, }; use yuck::{ - config::validate::ValidationError, + config::{validate::ValidationError, attr_value::Action}, error::{AstError, AstResult}, gen_diagnostic, parser::from_ast::FromAst, @@ -416,21 +416,26 @@ fn build_gtk_input(bargs: &mut BuilderArgs) -> Result { fn build_gtk_button(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::Button::new(); - def_widget!(bargs, _g, gtk_widget, { + def_widget!(bargs, scope_graph, gtk_widget, { // @prop onclick - a command that get's run when the button is clicked // @prop onmiddleclick - a command that get's run when the button is middleclicked // @prop onrightclick - a command that get's run when the button is rightclicked // @prop timeout - timeout of the command prop( timeout: as_duration = Duration::from_millis(200), - onclick: as_string = "", + onclick: as_action?, onmiddleclick: as_string = "", onrightclick: as_string = "" ) { gtk_widget.add_events(gdk::EventMask::BUTTON_PRESS_MASK); + let onclick = onclick.cloned().unwrap_or(Action::Noop); connect_signal_handler!(gtk_widget, gtk_widget.connect_button_press_event(move |_, evt| { match evt.button() { - 1 => run_command(timeout, &onclick, &[""]), + 1 => { + if let Err(e) = run_action(scope_graph, bargs.calling_scope, &onclick) { + log::error!("{}", e); + } + } 2 => run_command(timeout, &onmiddleclick, &[""]), 3 => run_command(timeout, &onrightclick, &[""]), _ => {}, diff --git a/crates/yuck/src/config/attr_value.rs b/crates/yuck/src/config/attr_value.rs index 48427e78d..920c6417e 100644 --- a/crates/yuck/src/config/attr_value.rs +++ b/crates/yuck/src/config/attr_value.rs @@ -26,18 +26,3 @@ pub enum Action { Update(VarName, SimplExpr), Noop, } - -impl Action { - pub fn eval_exprs(&self, values: &HashMap) -> Result { - Ok(match self { - Self::Update(varname, expr) => ResolvedAction::Update(varname.clone(), expr.eval(values)?), - Self::Noop => ResolvedAction::Noop, - }) - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum ResolvedAction { - Update(VarName, DynVal), - Noop, -} diff --git a/crates/yuck/src/config/widget_use.rs b/crates/yuck/src/config/widget_use.rs index a8a91933a..0bb54c5c1 100644 --- a/crates/yuck/src/config/widget_use.rs +++ b/crates/yuck/src/config/widget_use.rs @@ -20,6 +20,15 @@ pub enum WidgetUse { Basic(BasicWidgetUse), Loop(LoopWidgetUse), Children(ChildrenWidgetUse), + Let(LetWidgetUse), +} + +#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize)] +pub struct LetWidgetUse { + pub defined_vars: HashMap, + pub body: Vec, + pub vars_span: Span, + pub span: Span, } #[derive(Debug, PartialEq, Eq, Clone, serde::Serialize)] @@ -100,6 +109,24 @@ impl FromAstElementContent for ChildrenWidgetUse { } } +impl FromAstElementContent for LetWidgetUse { + const ELEMENT_NAME: &'static str = "let"; + + fn from_tail>(span: Span, mut iter: AstIterator) -> AstResult { + let (vars_span, vars) = iter.expect_array()?; + let mut vars_iter = vars.into_iter(); + let mut vars = HashMap::::new(); + while let Some(var_name) = vars_iter.next() { + let var_name = var_name.as_symbol()?; + let value = + vars_iter.next().ok_or_else(|| AstError::LetVarWithoutValue(vars_span, var_name.clone()))?.as_simplexpr()?; + vars.insert(var_name.into(), value); + } + let children = iter.map(WidgetUse::from_ast).collect::>>()?; + Ok(Self { defined_vars: vars, body: children, vars_span, span }) + } +} + impl FromAst for WidgetUse { fn from_ast(e: Ast) -> AstResult { let span = e.span(); @@ -109,6 +136,7 @@ impl FromAst for WidgetUse { let mut iter = e.try_ast_iter()?; let (name_span, name) = iter.expect_symbol()?; match name.as_ref() { + LetWidgetUse::ELEMENT_NAME => Ok(WidgetUse::Let(LetWidgetUse::from_tail(span, iter)?)), LoopWidgetUse::ELEMENT_NAME => Ok(WidgetUse::Loop(LoopWidgetUse::from_tail(span, iter)?)), ChildrenWidgetUse::ELEMENT_NAME => Ok(WidgetUse::Children(ChildrenWidgetUse::from_tail(span, iter)?)), _ => Ok(WidgetUse::Basic(BasicWidgetUse::from_iter(span, name, name_span, iter)?)), @@ -136,7 +164,7 @@ fn label_from_simplexpr(value: SimplExpr, span: Span) -> BasicWidgetUse { } macro_rules! impl_spanned { - ($($super:ident => $name:ident),*) => { + ($($super:ident => $name:ident),* $(,)?) => { $(impl Spanned for $name { fn span(&self) -> Span { self.span } })* impl Spanned for WidgetUse { fn span(&self) -> Span { @@ -145,4 +173,9 @@ macro_rules! impl_spanned { } } } -impl_spanned!(Basic => BasicWidgetUse, Loop => LoopWidgetUse, Children => ChildrenWidgetUse); +impl_spanned!( + Basic => BasicWidgetUse, + Loop => LoopWidgetUse, + Children => ChildrenWidgetUse, + Let => LetWidgetUse, +); diff --git a/crates/yuck/src/error.rs b/crates/yuck/src/error.rs index 1047fe475..e6cefb90e 100644 --- a/crates/yuck/src/error.rs +++ b/crates/yuck/src/error.rs @@ -35,6 +35,8 @@ pub enum AstError { MismatchedElementName(Span, String, String), #[error("Unknown action {1}")] UnknownAction(Span, String), + #[error("Variable {1} in let-block is missing a value")] + LetVarWithoutValue(Span, String), #[error("Keyword `{1}` is missing a value")] DanglingKeyword(Span, String), @@ -119,6 +121,7 @@ impl Spanned for AstError { AstError::SimplExpr(err) => err.span(), AstError::FormFormatError(err) => err.span(), AstError::UnknownAction(span, ..) => *span, + AstError::LetVarWithoutValue(span, _) => *span, } } } diff --git a/crates/yuck/src/format_diagnostic.rs b/crates/yuck/src/format_diagnostic.rs index cfd076a6f..a32e7fb5e 100644 --- a/crates/yuck/src/format_diagnostic.rs +++ b/crates/yuck/src/format_diagnostic.rs @@ -129,6 +129,11 @@ impl ToDiagnostic for AstError { msg = self, label = span => "No value provided for this", }, + AstError::LetVarWithoutValue(span, var_name) => gen_diagnostic! { + msg = self, + label = span => "In this let block", + note = "Let-blocks need to have a value specified for each variable." + }, AstError::ErrorNote(note, source) => source.to_diagnostic().with_notes(vec![note.to_string()]), AstError::ValidationError(source) => source.to_diagnostic(), AstError::NoMoreElementsExpected(span) => gen_diagnostic!(self, span), From 344ac327ec9d1c212ecca661c5e441348b172c07 Mon Sep 17 00:00:00 2001 From: elkowar <5300871+elkowar@users.noreply.github.com> Date: Wed, 27 Apr 2022 17:49:11 +0200 Subject: [PATCH 05/10] Implement run_action --- .vimspector.json | 12 -- crates/eww/src/state/scope_graph.rs | 7 ++ crates/eww/src/widgets/build_widget.rs | 14 ++- crates/eww/src/widgets/def_widget_macro.rs | 9 +- crates/eww/src/widgets/mod.rs | 29 +++-- crates/eww/src/widgets/widget_definitions.rs | 115 ++++++++++++------- crates/simplexpr/src/eval.rs | 1 + crates/yuck/src/config/attr_value.rs | 34 +++++- 8 files changed, 151 insertions(+), 70 deletions(-) delete mode 100644 .vimspector.json diff --git a/.vimspector.json b/.vimspector.json deleted file mode 100644 index 0907b1906..000000000 --- a/.vimspector.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "configurations": { - "launch": { - "adapter": "CodeLLDB", - "configuration": { - "request": "launch", - "program": "${workspaceRoot}/target/debug/eww", - "args": ["open", "main_window"] - } - } - } -} diff --git a/crates/eww/src/state/scope_graph.rs b/crates/eww/src/state/scope_graph.rs index 052a1c412..ab102c2fc 100644 --- a/crates/eww/src/state/scope_graph.rs +++ b/crates/eww/src/state/scope_graph.rs @@ -26,8 +26,10 @@ impl ScopeIndex { } } +#[derive(Debug)] pub enum ScopeGraphEvent { RemoveScope(ScopeIndex), + UpdateValue(ScopeIndex, VarName, DynVal), } /// A graph structure of scopes where each scope may inherit from another scope, @@ -87,6 +89,11 @@ impl ScopeGraph { ScopeGraphEvent::RemoveScope(scope_index) => { self.remove_scope(scope_index); } + ScopeGraphEvent::UpdateValue(scope_index, name, value) => { + if let Err(e) = self.update_value(scope_index, &name, value) { + log::error!("{}", e); + } + } } } diff --git a/crates/eww/src/widgets/build_widget.rs b/crates/eww/src/widgets/build_widget.rs index da2172c9a..6b5e2a955 100644 --- a/crates/eww/src/widgets/build_widget.rs +++ b/crates/eww/src/widgets/build_widget.rs @@ -120,11 +120,23 @@ fn build_let_special_widget( custom_widget_invocation: Option>, ) -> Result { let child = widget_use.body.first().expect("no child in let"); + + // Evaluate explicitly here, so we don't keep linking the state changes here. + // If that was desired, it'd suffice to just pass the simplexprs as attributes to register_new_scope, + // rather than converting them into literals explicitly. + let mut defined_vars = HashMap::new(); + for (name, expr) in widget_use.defined_vars.into_iter() { + let mut needed_vars = graph.lookup_variables_in_scope(calling_scope, &expr.collect_var_refs())?; + needed_vars.extend(defined_vars.clone().into_iter()); + let value = expr.eval(&needed_vars)?; + defined_vars.insert(name, value); + } + let let_scope = graph.register_new_scope( "let-widget".to_string(), Some(calling_scope), calling_scope, - widget_use.defined_vars.into_iter().map(|(k, v)| (AttrName(k.to_string()), v)).collect(), + defined_vars.into_iter().map(|(k, v)| (AttrName(k.to_string()), SimplExpr::Literal(v))).collect(), )?; let gtk_widget = build_gtk_widget(graph, widget_defs, let_scope, child.clone(), custom_widget_invocation)?; let scope_graph_sender = graph.event_sender.clone(); diff --git a/crates/eww/src/widgets/def_widget_macro.rs b/crates/eww/src/widgets/def_widget_macro.rs index 73c1938ab..47d4f91ad 100644 --- a/crates/eww/src/widgets/def_widget_macro.rs +++ b/crates/eww/src/widgets/def_widget_macro.rs @@ -32,16 +32,16 @@ macro_rules! def_widget { // Get all the variables that are referred to in any of the attributes expressions let required_vars: Vec = attr_map .values() - .flat_map(|expr| expr.as_ref().and_then(|x| x.try_into_simplexpr()).map(|x| x.collect_var_refs()).unwrap_or_default()) + .flat_map(|expr| expr.as_ref().map(|x| x.collect_var_refs()).unwrap_or_default()) .collect(); $args.scope_graph.register_listener( $args.calling_scope, - crate::state::scope::Listener { + crate::state::scope::Listener { needed_variables: required_vars, f: Box::new({ let $gtk_widget = gdk::glib::clone::Downgrade::downgrade(&$gtk_widget); - move |$scope_graph, values| { + move |#[allow(unused)] $scope_graph, values| { let $gtk_widget = gdk::glib::clone::Upgrade::upgrade(&$gtk_widget).expect("Failed to upgrade widget ref"); // values is a map of all the variables that are required to evaluate the // attributes expression. @@ -79,7 +79,8 @@ macro_rules! def_widget { (@value_depending_on_type $values:expr, $attr_name:ident : as_action $(? $(@ $optional:tt @)?)? $(= $default:expr)?) => { match $attr_name { - Some(yuck::config::attr_value::AttrValue::Action(action)) => Some(action), + Some(yuck::config::attr_value::AttrValue::Action(action)) => Some(action.eval_exprs(&$values)?), + Some(yuck::config::attr_value::AttrValue::SimplExpr(expr)) => Some(ExecutableAction::Shell(expr.eval(&$values)?.as_string()?)), _ => None, } }; diff --git a/crates/eww/src/widgets/mod.rs b/crates/eww/src/widgets/mod.rs index a96050a9c..32961c89d 100644 --- a/crates/eww/src/widgets/mod.rs +++ b/crates/eww/src/widgets/mod.rs @@ -1,9 +1,8 @@ use std::process::Command; -use anyhow::Result; -use yuck::config::attr_value::Action; +use yuck::config::attr_value::ExecutableAction; -use crate::state::scope_graph::{ScopeGraph, ScopeIndex}; +use crate::state::scope_graph::{ScopeGraphEvent, ScopeIndex}; pub mod build_widget; pub mod circular_progressbar; @@ -66,13 +65,25 @@ mod test { } } -pub(self) fn run_action(graph: &mut ScopeGraph, scope: ScopeIndex, action: &Action) -> Result<()> { +pub(self) fn run_action( + sender: tokio::sync::mpsc::UnboundedSender, + scope: ScopeIndex, + timeout: std::time::Duration, + action: &ExecutableAction, + args: &[T], +) where + T: 'static + std::fmt::Display + Send + Sync + Clone, +{ match action { - Action::Update(varname, expr) => { - let value = graph.evaluate_simplexpr_in_scope(scope, expr)?; - graph.update_value(scope, varname, value)?; + ExecutableAction::Update(varname, value) => { + let res = sender.send(ScopeGraphEvent::UpdateValue(scope, varname.clone(), value.clone())); + if let Err(e) = res { + log::error!("{}", e); + } + } + ExecutableAction::Shell(command) => { + run_command(timeout, command, args); } - Action::Noop => {} + ExecutableAction::Noop => {} } - Ok(()) } diff --git a/crates/eww/src/widgets/widget_definitions.rs b/crates/eww/src/widgets/widget_definitions.rs index e82e37d37..e8251cd14 100644 --- a/crates/eww/src/widgets/widget_definitions.rs +++ b/crates/eww/src/widgets/widget_definitions.rs @@ -1,5 +1,5 @@ #![allow(clippy::option_map_unit_fn)] -use super::{build_widget::BuilderArgs, circular_progressbar::*, run_command, transform::*}; +use super::{build_widget::BuilderArgs, circular_progressbar::*, transform::*}; use crate::{ def_widget, enum_parse, error::DiagError, @@ -25,7 +25,7 @@ use std::{ time::Duration, }; use yuck::{ - config::{validate::ValidationError, attr_value::Action}, + config::{attr_value::ExecutableAction, validate::ValidationError}, error::{AstError, AstResult}, gen_diagnostic, parser::from_ast::FromAst, @@ -195,6 +195,7 @@ pub(super) fn resolve_widget_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Wi /// @widget !range pub(super) fn resolve_range_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Range) -> Result<()> { gtk_widget.set_sensitive(false); + let calling_scope = bargs.calling_scope.clone(); // only allow changing the value via the value property if the user isn't currently dragging let is_being_dragged = Rc::new(RefCell::new(false)); @@ -207,7 +208,7 @@ pub(super) fn resolve_range_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Ran gtk::Inhibit(false) })); - def_widget!(bargs, _g, gtk_widget, { + def_widget!(bargs, graph, gtk_widget, { // @prop value - the value prop(value: as_f64) { if !*is_being_dragged.borrow() { @@ -220,11 +221,12 @@ pub(super) fn resolve_range_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Ran prop(max: as_f64) { gtk_widget.adjustment().set_upper(max)}, // @prop timeout - timeout of the command // @prop onchange - command executed once the value is changes. The placeholder `{}`, used in the command will be replaced by the new value. - prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) { + prop(timeout: as_duration = Duration::from_millis(200), onchange: as_action) { + let scope_sender = graph.event_sender.clone(); gtk_widget.set_sensitive(true); gtk_widget.add_events(gdk::EventMask::PROPERTY_CHANGE_MASK); connect_signal_handler!(gtk_widget, gtk_widget.connect_value_changed(move |gtk_widget| { - run_command(timeout, &onchange, &[gtk_widget.value()]); + run_action(scope_sender.clone(), calling_scope, timeout, &onchange, &[gtk_widget.value()]); })); } }); @@ -246,7 +248,8 @@ pub(super) fn resolve_orientable_attrs(bargs: &mut BuilderArgs, gtk_widget: >k /// @desc A combo box allowing the user to choose between several items. fn build_gtk_combo_box_text(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::ComboBoxText::new(); - def_widget!(bargs, _g, gtk_widget, { + let calling_scope = bargs.calling_scope.clone(); + def_widget!(bargs, graph, gtk_widget, { // @prop items - Items that should be displayed in the combo box prop(items: as_vec) { gtk_widget.remove_all(); @@ -256,9 +259,10 @@ fn build_gtk_combo_box_text(bargs: &mut BuilderArgs) -> Result Result { /// @desc A checkbox that can trigger events on checked / unchecked. fn build_gtk_checkbox(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::CheckButton::new(); - def_widget!(bargs, _g, gtk_widget, { + let calling_scope = bargs.calling_scope.clone(); + def_widget!(bargs, graph, gtk_widget, { // @prop timeout - timeout of the command // @prop onchecked - action (command) to be executed when checked by the user // @prop onunchecked - similar to onchecked but when the widget is unchecked - prop(timeout: as_duration = Duration::from_millis(200), onchecked: as_string = "", onunchecked: as_string = "") { + prop(timeout: as_duration = Duration::from_millis(200), onchecked: as_action?, onunchecked: as_action?) { + let scope_sender = graph.event_sender.clone(); + let onchecked = onchecked.unwrap_or(ExecutableAction::Noop); + let onunchecked = onunchecked.unwrap_or(ExecutableAction::Noop); connect_signal_handler!(gtk_widget, gtk_widget.connect_toggled(move |gtk_widget| { - run_command(timeout, if gtk_widget.is_active() { &onchecked } else { &onunchecked }, &[""]); + run_action(scope_sender.clone(), calling_scope, timeout, if gtk_widget.is_active() { &onchecked } else { &onunchecked }, &[""]); })); } }); @@ -314,15 +322,17 @@ fn build_gtk_checkbox(bargs: &mut BuilderArgs) -> Result { /// @desc A button opening a color chooser window fn build_gtk_color_button(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::ColorButtonBuilder::new().build(); - def_widget!(bargs, _g, gtk_widget, { + let calling_scope = bargs.calling_scope.clone(); + def_widget!(bargs, graph, gtk_widget, { // @prop use-alpha - bool to whether or not use alpha prop(use_alpha: as_bool) {gtk_widget.set_use_alpha(use_alpha);}, // @prop onchange - runs the code when the color was selected // @prop timeout - timeout of the command - prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) { + prop(timeout: as_duration = Duration::from_millis(200), onchange: as_action) { + let scope_sender = graph.event_sender.clone(); connect_signal_handler!(gtk_widget, gtk_widget.connect_color_set(move |gtk_widget| { - run_command(timeout, &onchange, &[gtk_widget.rgba()]); + run_action(scope_sender.clone(), calling_scope, timeout, &onchange, &[gtk_widget.rgba()]); })); } }); @@ -334,15 +344,17 @@ fn build_gtk_color_button(bargs: &mut BuilderArgs) -> Result { /// @desc A color chooser widget fn build_gtk_color_chooser(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::ColorChooserWidget::new(); - def_widget!(bargs, _g, gtk_widget, { + let calling_scope = bargs.calling_scope.clone(); + def_widget!(bargs, graph, gtk_widget, { // @prop use-alpha - bool to wether or not use alpha prop(use_alpha: as_bool) {gtk_widget.set_use_alpha(use_alpha);}, // @prop onchange - runs the code when the color was selected // @prop timeout - timeout of the command - prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) { + prop(timeout: as_duration = Duration::from_millis(200), onchange: as_action) { + let scope_sender = graph.event_sender.clone(); connect_signal_handler!(gtk_widget, gtk_widget.connect_color_activated(move |_a, color| { - run_command(timeout, &onchange, &[*color]); + run_action(scope_sender.clone(), calling_scope, timeout, &onchange, &[*color]); })); } }); @@ -394,7 +406,8 @@ fn build_gtk_progress(bargs: &mut BuilderArgs) -> Result { /// @desc An input field. For this to be useful, set `focusable="true"` on the window. fn build_gtk_input(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::Entry::new(); - def_widget!(bargs, _g, gtk_widget, { + let calling_scope = bargs.calling_scope.clone(); + def_widget!(bargs, graph, gtk_widget, { // @prop value - the content of the text field prop(value: as_string) { gtk_widget.set_text(&value); @@ -402,9 +415,10 @@ fn build_gtk_input(bargs: &mut BuilderArgs) -> Result { // @prop onchange - Command to run when the text changes. The placeholder `{}` will be replaced by the value // @prop timeout - timeout of the command - prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) { + prop(timeout: as_duration = Duration::from_millis(200), onchange: as_action) { + let scope_sender = graph.event_sender.clone(); connect_signal_handler!(gtk_widget, gtk_widget.connect_changed(move |gtk_widget| { - run_command(timeout, &onchange, &[gtk_widget.text().to_string()]); + run_action(scope_sender.clone(), calling_scope, timeout, &onchange, &[gtk_widget.text().to_string()]); })); } }); @@ -415,8 +429,9 @@ fn build_gtk_input(bargs: &mut BuilderArgs) -> Result { /// @desc A button fn build_gtk_button(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::Button::new(); + let calling_scope = bargs.calling_scope.clone(); - def_widget!(bargs, scope_graph, gtk_widget, { + def_widget!(bargs, graph, gtk_widget, { // @prop onclick - a command that get's run when the button is clicked // @prop onmiddleclick - a command that get's run when the button is middleclicked // @prop onrightclick - a command that get's run when the button is rightclicked @@ -424,20 +439,25 @@ fn build_gtk_button(bargs: &mut BuilderArgs) -> Result { prop( timeout: as_duration = Duration::from_millis(200), onclick: as_action?, - onmiddleclick: as_string = "", - onrightclick: as_string = "" + onmiddleclick: as_action?, + onrightclick: as_action? ) { + let scope_sender = graph.event_sender.clone(); gtk_widget.add_events(gdk::EventMask::BUTTON_PRESS_MASK); - let onclick = onclick.cloned().unwrap_or(Action::Noop); + let onclick = onclick.unwrap_or(ExecutableAction::Noop); + let onmiddleclick = onmiddleclick.unwrap_or(ExecutableAction::Noop); + let onrightclick = onrightclick.unwrap_or(ExecutableAction::Noop); connect_signal_handler!(gtk_widget, gtk_widget.connect_button_press_event(move |_, evt| { match evt.button() { 1 => { - if let Err(e) = run_action(scope_graph, bargs.calling_scope, &onclick) { - log::error!("{}", e); - } - } - 2 => run_command(timeout, &onmiddleclick, &[""]), - 3 => run_command(timeout, &onrightclick, &[""]), + run_action(scope_sender.clone(), calling_scope, timeout, &onclick, &[""]); + }, + 2 => { + run_action(scope_sender.clone(), calling_scope, timeout, &onmiddleclick, &[""]); + }, + 3 => { + run_action(scope_sender.clone(), calling_scope, timeout, &onrightclick, &[""]); + }, _ => {}, } gtk::Inhibit(false) @@ -556,6 +576,7 @@ fn build_gtk_scrolledwindow(bargs: &mut BuilderArgs) -> Result Result { let gtk_widget = gtk::EventBox::new(); + let calling_scope = bargs.calling_scope.clone(); // Support :hover selector gtk_widget.connect_enter_notify_event(|gtk_widget, evt| { @@ -572,38 +593,41 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { gtk::Inhibit(false) }); - def_widget!(bargs, _g, gtk_widget, { + def_widget!(bargs, graph, gtk_widget, { // @prop timeout - timeout of the command // @prop onscroll - event to execute when the user scrolls with the mouse over the widget. The placeholder `{}` used in the command will be replaced with either `up` or `down`. - prop(timeout: as_duration = Duration::from_millis(200), onscroll: as_string) { + prop(timeout: as_duration = Duration::from_millis(200), onscroll: as_action) { + let scope_sender = graph.event_sender.clone(); gtk_widget.add_events(gdk::EventMask::SCROLL_MASK); gtk_widget.add_events(gdk::EventMask::SMOOTH_SCROLL_MASK); connect_signal_handler!(gtk_widget, gtk_widget.connect_scroll_event(move |_, evt| { let delta = evt.delta().1; if delta != 0f64 { // Ignore the first event https://bugzilla.gnome.org/show_bug.cgi?id=675959 - run_command(timeout, &onscroll, &[if delta < 0f64 { "up" } else { "down" }]); + run_action(scope_sender.clone(), calling_scope, timeout, &onscroll, &[if delta < 0f64 { "up" } else { "down" }]); } gtk::Inhibit(false) })); }, // @prop timeout - timeout of the command // @prop onhover - event to execute when the user hovers over the widget - prop(timeout: as_duration = Duration::from_millis(200), onhover: as_string) { + prop(timeout: as_duration = Duration::from_millis(200), onhover: as_action) { + let scope_sender = graph.event_sender.clone(); gtk_widget.add_events(gdk::EventMask::ENTER_NOTIFY_MASK); connect_signal_handler!(gtk_widget, gtk_widget.connect_enter_notify_event(move |_, evt| { if evt.detail() != NotifyType::Inferior { - run_command(timeout, &onhover, &[evt.position().0, evt.position().1]); + run_action(scope_sender.clone(), calling_scope, timeout, &onhover, &[evt.position().0, evt.position().1]); } gtk::Inhibit(false) })); }, // @prop timeout - timeout of the command // @prop onhoverlost - event to execute when the user losts hovers over the widget - prop(timeout: as_duration = Duration::from_millis(200), onhoverlost: as_string) { + prop(timeout: as_duration = Duration::from_millis(200), onhoverlost: as_action) { + let scope_sender = graph.event_sender.clone(); gtk_widget.add_events(gdk::EventMask::LEAVE_NOTIFY_MASK); connect_signal_handler!(gtk_widget, gtk_widget.connect_leave_notify_event(move |_, evt| { if evt.detail() != NotifyType::Inferior { - run_command(timeout, &onhoverlost, &[evt.position().0, evt.position().1]); + run_action(scope_sender.clone(), calling_scope, timeout, &onhoverlost, &[evt.position().0, evt.position().1]); } gtk::Inhibit(false) })); @@ -635,7 +659,8 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { }, // @prop timeout - timeout of the command // @prop on_dropped - Command to execute when something is dropped on top of this element. The placeholder `{}` used in the command will be replaced with the uri to the dropped thing. - prop(timeout: as_duration = Duration::from_millis(200), ondropped: as_string) { + prop(timeout: as_duration = Duration::from_millis(200), ondropped: as_action) { + let scope_sender = graph.event_sender.clone(); gtk_widget.drag_dest_set( DestDefaults::ALL, &[ @@ -646,9 +671,9 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { ); connect_signal_handler!(gtk_widget, gtk_widget.connect_drag_data_received(move |_, _, _x, _y, selection_data, _target_type, _timestamp| { if let Some(data) = selection_data.uris().first(){ - run_command(timeout, &ondropped, &[data.to_string(), "file".to_string()]); + run_action(scope_sender.clone(), calling_scope, timeout, &ondropped, &[data.to_string(), "file".to_string()]); } else if let Some(data) = selection_data.text(){ - run_command(timeout, &ondropped, &[data.to_string(), "text".to_string()]); + run_action(scope_sender.clone(), calling_scope, timeout, &ondropped, &[data.to_string(), "text".to_string()]); } })); }, @@ -768,7 +793,8 @@ fn build_gtk_literal(bargs: &mut BuilderArgs) -> Result { /// @desc A widget that displays a calendar fn build_gtk_calendar(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::Calendar::new(); - def_widget!(bargs, _g, gtk_widget, { + let calling_scope = bargs.calling_scope.clone(); + def_widget!(bargs, graph, gtk_widget, { // @prop day - the selected day prop(day: as_f64) { gtk_widget.set_day(day as i32) }, // @prop month - the selected month @@ -785,10 +811,13 @@ fn build_gtk_calendar(bargs: &mut BuilderArgs) -> Result { prop(show_week_numbers: as_bool) { gtk_widget.set_show_week_numbers(show_week_numbers) }, // @prop onclick - command to run when the user selects a date. The `{0}` placeholder will be replaced by the selected day, `{1}` will be replaced by the month, and `{2}` by the year. // @prop timeout - timeout of the command - prop(timeout: as_duration = Duration::from_millis(200), onclick: as_string) { + prop(timeout: as_duration = Duration::from_millis(200), onclick: as_action) { + let scope_sender = graph.event_sender.clone(); connect_signal_handler!(gtk_widget, gtk_widget.connect_day_selected(move |w| { log::warn!("BREAKING CHANGE: The date is now provided via three values, set by the placeholders {{0}}, {{1}} and {{2}}. If you're currently using the onclick date, you will need to change this."); - run_command( + run_action( + scope_sender.clone(), + calling_scope, timeout, &onclick, &[w.day(), w.month(), w.year()] diff --git a/crates/simplexpr/src/eval.rs b/crates/simplexpr/src/eval.rs index 21c950b81..7f74ed113 100644 --- a/crates/simplexpr/src/eval.rs +++ b/crates/simplexpr/src/eval.rs @@ -154,6 +154,7 @@ impl SimplExpr { } } + pub fn eval(&self, values: &HashMap) -> Result { let span = self.span(); let value = match self { diff --git a/crates/yuck/src/config/attr_value.rs b/crates/yuck/src/config/attr_value.rs index 920c6417e..ccbcd98e1 100644 --- a/crates/yuck/src/config/attr_value.rs +++ b/crates/yuck/src/config/attr_value.rs @@ -5,7 +5,6 @@ use simplexpr::{dynval::DynVal, eval::EvalError, SimplExpr}; pub static ACTION_NAMES: &[&str] = &["update"]; -// TODO: Maybe separate that into another file #[derive(Debug, Clone)] pub enum AttrValue { Action(Action), @@ -19,10 +18,43 @@ impl AttrValue { _ => None, } } + + pub fn collect_var_refs(&self) -> Vec { + match self { + Self::SimplExpr(expr) => expr.collect_var_refs(), + Self::Action(action) => action.collect_var_refs(), + } + } } #[derive(Debug, Clone, Eq, PartialEq)] pub enum Action { Update(VarName, SimplExpr), + Shell(SimplExpr), + Noop, +} + +impl Action { + pub fn eval_exprs(&self, values: &HashMap) -> Result { + Ok(match self { + Self::Update(varname, expr) => ExecutableAction::Update(varname.clone(), expr.eval(values)?), + Self::Shell(expr) => ExecutableAction::Shell(expr.eval(values)?.as_string()?), + Self::Noop => ExecutableAction::Noop, + }) + } + + pub fn collect_var_refs(&self) -> Vec { + match self { + Self::Update(_, expr) => expr.collect_var_refs(), + Self::Shell(expr) => expr.collect_var_refs(), + Self::Noop => vec![], + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ExecutableAction { + Update(VarName, DynVal), + Shell(String), Noop, } From 3b6081eda5cadb241d8c0adbeb61012136ccd6c7 Mon Sep 17 00:00:00 2001 From: elkowar <5300871+elkowar@users.noreply.github.com> Date: Wed, 27 Apr 2022 17:40:09 +0200 Subject: [PATCH 06/10] Handle multiple children in let --- crates/eww/src/widgets/build_widget.rs | 102 ++++++++++++++++--------- 1 file changed, 68 insertions(+), 34 deletions(-) diff --git a/crates/eww/src/widgets/build_widget.rs b/crates/eww/src/widgets/build_widget.rs index 6b5e2a955..e5044a565 100644 --- a/crates/eww/src/widgets/build_widget.rs +++ b/crates/eww/src/widgets/build_widget.rs @@ -56,7 +56,9 @@ pub fn build_gtk_widget( WidgetUse::Basic(widget_use) => { build_basic_gtk_widget(graph, widget_defs, calling_scope, widget_use, custom_widget_invocation) } - WidgetUse::Let(let_use) => build_let_special_widget(graph, widget_defs, calling_scope, let_use, custom_widget_invocation), + WidgetUse::Let(let_use) => { + build_let_special_widget_single_child(graph, widget_defs, calling_scope, let_use, custom_widget_invocation) + } WidgetUse::Loop(_) | WidgetUse::Children(_) => Err(anyhow::anyhow!(DiagError::new(gen_diagnostic! { msg = "This widget can only be used as a child of some container widget such as box", label = widget_use.span(), @@ -112,20 +114,12 @@ fn build_basic_gtk_widget( } } -fn build_let_special_widget( - graph: &mut ScopeGraph, - widget_defs: Rc>, - calling_scope: ScopeIndex, - widget_use: LetWidgetUse, - custom_widget_invocation: Option>, -) -> Result { - let child = widget_use.body.first().expect("no child in let"); - +fn init_let_widget_scope(graph: &mut ScopeGraph, calling_scope: ScopeIndex, widget_use: &LetWidgetUse) -> Result { // Evaluate explicitly here, so we don't keep linking the state changes here. // If that was desired, it'd suffice to just pass the simplexprs as attributes to register_new_scope, // rather than converting them into literals explicitly. let mut defined_vars = HashMap::new(); - for (name, expr) in widget_use.defined_vars.into_iter() { + for (name, expr) in widget_use.defined_vars.clone().into_iter() { let mut needed_vars = graph.lookup_variables_in_scope(calling_scope, &expr.collect_var_refs())?; needed_vars.extend(defined_vars.clone().into_iter()); let value = expr.eval(&needed_vars)?; @@ -138,13 +132,48 @@ fn build_let_special_widget( calling_scope, defined_vars.into_iter().map(|(k, v)| (AttrName(k.to_string()), SimplExpr::Literal(v))).collect(), )?; - let gtk_widget = build_gtk_widget(graph, widget_defs, let_scope, child.clone(), custom_widget_invocation)?; + Ok(let_scope) +} + +fn build_let_special_widget_single_child( + graph: &mut ScopeGraph, + widget_defs: Rc>, + calling_scope: ScopeIndex, + widget_use: LetWidgetUse, + custom_widget_invocation: Option>, +) -> Result { + assert!(widget_use.body.len() == 1, "build_let_special_widget_single_child called with let with more than one child"); + let child = widget_use.body.first().cloned().expect("no child in let"); + let let_scope = init_let_widget_scope(graph, calling_scope, &widget_use)?; + let child_widget = build_gtk_widget(graph, widget_defs, let_scope, child.clone(), custom_widget_invocation)?; let scope_graph_sender = graph.event_sender.clone(); + child_widget.connect_destroy(move |_| { + let _ = scope_graph_sender.send(ScopeGraphEvent::RemoveScope(let_scope)); + }); + Ok(child_widget) +} - gtk_widget.connect_destroy(move |_| { +fn build_let_special_widget_multiple_children( + graph: &mut ScopeGraph, + widget_defs: Rc>, + calling_scope: ScopeIndex, + widget_use: LetWidgetUse, + gtk_container: >k::Container, + custom_widget_invocation: Option>, +) -> Result<()> { + let let_scope = init_let_widget_scope(graph, calling_scope, &widget_use)?; + let scope_graph_sender = graph.event_sender.clone(); + + for child in &widget_use.body { + let child_widget = + build_gtk_widget(graph, widget_defs.clone(), let_scope, child.clone(), custom_widget_invocation.clone())?; + gtk_container.add(&child_widget); + } + + gtk_container.connect_destroy(move |_| { let _ = scope_graph_sender.send(ScopeGraphEvent::RemoveScope(let_scope)); }); - Ok(gtk_widget) + Ok(()) } /// build a [`gtk::Widget`] out of a [`WidgetUse`] that uses a @@ -220,26 +249,31 @@ fn populate_widget_children( ) -> Result<()> { for child in widget_use_children { match child { - WidgetUse::Children(child) => { - build_children_special_widget( - tree, - widget_defs.clone(), - calling_scope, - child, - gtk_container, - custom_widget_invocation.clone().context("Not in a custom widget invocation")?, - )?; - } - WidgetUse::Loop(child) => { - build_loop_special_widget( - tree, - widget_defs.clone(), - calling_scope, - child, - gtk_container, - custom_widget_invocation.clone(), - )?; - } + WidgetUse::Children(child) => build_children_special_widget( + tree, + widget_defs.clone(), + calling_scope, + child, + gtk_container, + custom_widget_invocation.clone().context("Not in a custom widget invocation")?, + )?, + + WidgetUse::Let(child) => build_let_special_widget_multiple_children( + tree, + widget_defs.clone(), + calling_scope, + child, + gtk_container, + custom_widget_invocation.clone(), + )?, + WidgetUse::Loop(child) => build_loop_special_widget( + tree, + widget_defs.clone(), + calling_scope, + child, + gtk_container, + custom_widget_invocation.clone(), + )?, _ => { let child_widget = build_gtk_widget(tree, widget_defs.clone(), calling_scope, child, custom_widget_invocation.clone())?; From ed09edbbddf42f7edb5c90337079cf901c9f4f3d Mon Sep 17 00:00:00 2001 From: elkowar <5300871+elkowar@users.noreply.github.com> Date: Wed, 27 Apr 2022 18:05:54 +0200 Subject: [PATCH 07/10] Properly handle optional actions in def_widget --- crates/eww/src/widgets/def_widget_macro.rs | 33 +++++++++++++------- crates/eww/src/widgets/widget_definitions.rs | 17 ++-------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/crates/eww/src/widgets/def_widget_macro.rs b/crates/eww/src/widgets/def_widget_macro.rs index 47d4f91ad..2a9c57a0f 100644 --- a/crates/eww/src/widgets/def_widget_macro.rs +++ b/crates/eww/src/widgets/def_widget_macro.rs @@ -20,7 +20,7 @@ macro_rules! def_widget { ::maplit::hashmap! { $( eww_shared_util::AttrName(::std::stringify!($attr_name).to_owned()) => - def_widget!(@get_value $args, &::std::stringify!($attr_name).replace('_', "-"), $(? $($optional)?)? $(= $default)?) + def_widget!(@get_value $args, &::std::stringify!($attr_name).replace('_', "-"), $typecast_func, $(? $($optional)?)? $(= $default)?) ),* } }; @@ -58,11 +58,11 @@ macro_rules! def_widget { // If the value is some, evaluate and typecast it. // This now uses a new macro, to match on the type cast function: // if we're casting into an action, we wanna do a different thing than if we where casting into an expr - let $attr_name = def_widget!(@value_depending_on_type values, $attr_name : $typecast_func $(? $(@ $optional @)?)? $(= $default)?); + let $attr_name = def_widget!(@value_depending_on_type values, $attr_name : $typecast_func); // If the attribute is optional, keep it as Option, otherwise unwrap // because we _know_ the value in the attr_map is Some if the attribute is not optional. - def_widget!(@unwrap_if_required $attr_name $(? $($optional)?)?); + def_widget!(@unwrap_if_required $attr_name : $typecast_func $(? $($optional)?)?); )* // And then run the provided code with those attributes in scope. @@ -77,7 +77,7 @@ macro_rules! def_widget { })+ }; - (@value_depending_on_type $values:expr, $attr_name:ident : as_action $(? $(@ $optional:tt @)?)? $(= $default:expr)?) => { + (@value_depending_on_type $values:expr, $attr_name:ident : as_action) => { match $attr_name { Some(yuck::config::attr_value::AttrValue::Action(action)) => Some(action.eval_exprs(&$values)?), Some(yuck::config::attr_value::AttrValue::SimplExpr(expr)) => Some(ExecutableAction::Shell(expr.eval(&$values)?.as_string()?)), @@ -85,32 +85,43 @@ macro_rules! def_widget { } }; - (@value_depending_on_type $values:expr, $attr_name:ident : $typecast_func:ident $(? $(@ $optional:tt @)?)? $(= $default:expr)?) => { + (@value_depending_on_type $values:expr, $attr_name:ident : $typecast_func:ident) => { match $attr_name { Some(yuck::config::attr_value::AttrValue::SimplExpr(expr)) => Some(expr.eval(&$values)?.$typecast_func()?), _ => None, } }; - (@unwrap_if_required $value:ident ?) => { }; - (@unwrap_if_required $value:ident) => { + // optional actions are a special case, as those should default to Noop rather than being represented as options. + (@unwrap_if_required $value:ident : as_action ?) => { let $value = $value.expect("No value was provided, eventhough value was required"); }; - + // Optional values don't need unwrapping, they're supposed to be optional + (@unwrap_if_required $value:ident : $typecast_func:ident ?) => { }; + // required values will still be stored as option at this point (because typechecking) -- but they are known to exist, and thus we can unwrap- + (@unwrap_if_required $value:ident : $typecast_func:ident) => { + let $value = $value.expect("No value was provided, eventhough value was required"); + }; + // The attribute is explicitly marked as optional and is an action. Optional actions should just default to Noop + (@get_value $args:ident, $name:expr, as_action, ?) => { + Some($args.widget_use.attrs.ast_optional::($name)? + .clone() + .unwrap_or_else(|| yuck::config::attr_value::AttrValue::Action(yuck::config::attr_value::Action::Noop))) + }; // The attribute is explicitly marked as optional - the value should be provided to the prop function body as Option - (@get_value $args:ident, $name:expr, ?) => { + (@get_value $args:ident, $name:expr, $typecast_func:ident, ?) => { $args.widget_use.attrs.ast_optional::($name)?.clone() }; // The attribute has a default value - (@get_value $args:ident, $name:expr, = $default:expr) => { + (@get_value $args:ident, $name:expr, $_typecast_func:ident, = $default:expr) => { Some($args.widget_use.attrs.ast_optional::($name)? .clone() .unwrap_or_else(|| yuck::config::attr_value::AttrValue::SimplExpr(simplexpr::SimplExpr::synth_literal($default)))) }; // The attribute is required - the prop will only be ran if this attribute is actually provided. - (@get_value $args:ident, $name:expr,) => { + (@get_value $args:ident, $name:expr, $typecast_func:ident,) => { Some($args.widget_use.attrs.ast_required::($name)?.clone()) } } diff --git a/crates/eww/src/widgets/widget_definitions.rs b/crates/eww/src/widgets/widget_definitions.rs index e8251cd14..b7bbf5676 100644 --- a/crates/eww/src/widgets/widget_definitions.rs +++ b/crates/eww/src/widgets/widget_definitions.rs @@ -307,8 +307,6 @@ fn build_gtk_checkbox(bargs: &mut BuilderArgs) -> Result { // @prop onunchecked - similar to onchecked but when the widget is unchecked prop(timeout: as_duration = Duration::from_millis(200), onchecked: as_action?, onunchecked: as_action?) { let scope_sender = graph.event_sender.clone(); - let onchecked = onchecked.unwrap_or(ExecutableAction::Noop); - let onunchecked = onunchecked.unwrap_or(ExecutableAction::Noop); connect_signal_handler!(gtk_widget, gtk_widget.connect_toggled(move |gtk_widget| { run_action(scope_sender.clone(), calling_scope, timeout, if gtk_widget.is_active() { &onchecked } else { &onunchecked }, &[""]); })); @@ -444,20 +442,11 @@ fn build_gtk_button(bargs: &mut BuilderArgs) -> Result { ) { let scope_sender = graph.event_sender.clone(); gtk_widget.add_events(gdk::EventMask::BUTTON_PRESS_MASK); - let onclick = onclick.unwrap_or(ExecutableAction::Noop); - let onmiddleclick = onmiddleclick.unwrap_or(ExecutableAction::Noop); - let onrightclick = onrightclick.unwrap_or(ExecutableAction::Noop); connect_signal_handler!(gtk_widget, gtk_widget.connect_button_press_event(move |_, evt| { match evt.button() { - 1 => { - run_action(scope_sender.clone(), calling_scope, timeout, &onclick, &[""]); - }, - 2 => { - run_action(scope_sender.clone(), calling_scope, timeout, &onmiddleclick, &[""]); - }, - 3 => { - run_action(scope_sender.clone(), calling_scope, timeout, &onrightclick, &[""]); - }, + 1 => run_action(scope_sender.clone(), calling_scope, timeout, &onclick, &[""]), + 2 => run_action(scope_sender.clone(), calling_scope, timeout, &onmiddleclick, &[""]), + 3 => run_action(scope_sender.clone(), calling_scope, timeout, &onrightclick, &[""]), _ => {}, } gtk::Inhibit(false) From 7bbaed1657ae2eab062acab73d959968ecb30ae3 Mon Sep 17 00:00:00 2001 From: elkowar <5300871+elkowar@users.noreply.github.com> Date: Wed, 27 Apr 2022 19:50:07 +0200 Subject: [PATCH 08/10] Rework event placeholders to be json objects --- crates/eww/src/widgets/def_widget_macro.rs | 4 +- crates/eww/src/widgets/mod.rs | 99 ++++++++------------ crates/eww/src/widgets/widget_definitions.rs | 58 +++++++----- crates/simplexpr/src/eval.rs | 9 +- crates/yuck/src/config/attr_value.rs | 23 +++-- crates/yuck/src/parser/from_ast.rs | 6 ++ 6 files changed, 104 insertions(+), 95 deletions(-) diff --git a/crates/eww/src/widgets/def_widget_macro.rs b/crates/eww/src/widgets/def_widget_macro.rs index 2a9c57a0f..43b9236de 100644 --- a/crates/eww/src/widgets/def_widget_macro.rs +++ b/crates/eww/src/widgets/def_widget_macro.rs @@ -79,8 +79,8 @@ macro_rules! def_widget { (@value_depending_on_type $values:expr, $attr_name:ident : as_action) => { match $attr_name { - Some(yuck::config::attr_value::AttrValue::Action(action)) => Some(action.eval_exprs(&$values)?), - Some(yuck::config::attr_value::AttrValue::SimplExpr(expr)) => Some(ExecutableAction::Shell(expr.eval(&$values)?.as_string()?)), + Some(yuck::config::attr_value::AttrValue::Action(action)) => Some(action.clone().resolve_to_executable(&$values)), + Some(yuck::config::attr_value::AttrValue::SimplExpr(expr)) => Some(ExecutableAction::Shell(expr.clone().resolve_refs_lenient(&$values))), _ => None, } }; diff --git a/crates/eww/src/widgets/mod.rs b/crates/eww/src/widgets/mod.rs index 32961c89d..52116a237 100644 --- a/crates/eww/src/widgets/mod.rs +++ b/crates/eww/src/widgets/mod.rs @@ -1,5 +1,7 @@ use std::process::Command; +use eww_shared_util::VarName; +use simplexpr::dynval::DynVal; use yuck::config::attr_value::ExecutableAction; use crate::state::scope_graph::{ScopeGraphEvent, ScopeIndex}; @@ -11,16 +13,45 @@ pub mod graph; pub mod transform; pub mod widget_definitions; -/// Run a command that was provided as an attribute. -/// This command may use placeholders which will be replaced by the values of the arguments given. -/// This can either be the placeholder `{}`, which will be replaced by the first argument, -/// Or a placeholder like `{0}`, `{1}`, etc, which will refer to the respective argument. -pub(self) fn run_command(timeout: std::time::Duration, cmd: &str, args: &[T]) -where - T: 'static + std::fmt::Display + Send + Sync + Clone, -{ +#[macro_export] +macro_rules! action_args { + ($($key:literal => $value:expr),* $(,)?) => { + serde_json::json!({ + $($key.to_string(): $value),* + }) + } +} + +/// Run an action +pub(self) fn run_action( + sender: tokio::sync::mpsc::UnboundedSender, + scope: ScopeIndex, + timeout: std::time::Duration, + action: &ExecutableAction, + args: &serde_json::Value, +) { + let result: anyhow::Result<()> = try { + let event_arg = maplit::hashmap! { VarName("event".to_string()) => DynVal::try_from(args)? }; + match action { + ExecutableAction::Update(varname, value) => { + let value = value.eval(&event_arg)?; + sender.send(ScopeGraphEvent::UpdateValue(scope, varname.clone(), value.clone()))?; + } + ExecutableAction::Shell(command) => { + let command = command.eval(&event_arg)?; + run_command(timeout, command.to_string()); + } + ExecutableAction::Noop => {} + } + }; + if let Err(e) = result { + log::error!("{}", e); + } +} + +/// Run a command with a given timeout +fn run_command(timeout: std::time::Duration, cmd: String) { use wait_timeout::ChildExt; - let cmd = replace_placeholders(cmd, args); std::thread::spawn(move || { log::debug!("Running command from widget: {}", cmd); let child = Command::new("/bin/sh").arg("-c").arg(&cmd).spawn(); @@ -28,7 +59,7 @@ where Ok(mut child) => match child.wait_timeout(timeout) { // child timed out Ok(None) => { - log::error!("WARNING: command {} timed out", &cmd); + log::warn!(": command {} timed out", &cmd); let _ = child.kill(); let _ = child.wait(); } @@ -39,51 +70,3 @@ where } }); } - -fn replace_placeholders(cmd: &str, args: &[T]) -> String -where - T: 'static + std::fmt::Display + Send + Sync + Clone, -{ - if !args.is_empty() { - let cmd = cmd.replace("{}", &format!("{}", args[0])); - args.iter().enumerate().fold(cmd.to_string(), |acc, (i, arg)| acc.replace(&format!("{{{}}}", i), &format!("{}", arg))) - } else { - cmd.to_string() - } -} - -#[cfg(test)] -mod test { - use super::*; - #[test] - fn test_replace_placeholders() { - assert_eq!("foo", replace_placeholders("foo", &[""]),); - assert_eq!("foo hi", replace_placeholders("foo {}", &["hi"]),); - assert_eq!("foo hi", replace_placeholders("foo {}", &["hi", "ho"]),); - assert_eq!("bar foo baz", replace_placeholders("{0} foo {1}", &["bar", "baz"]),); - assert_eq!("baz foo bar", replace_placeholders("{1} foo {0}", &["bar", "baz"]),); - } -} - -pub(self) fn run_action( - sender: tokio::sync::mpsc::UnboundedSender, - scope: ScopeIndex, - timeout: std::time::Duration, - action: &ExecutableAction, - args: &[T], -) where - T: 'static + std::fmt::Display + Send + Sync + Clone, -{ - match action { - ExecutableAction::Update(varname, value) => { - let res = sender.send(ScopeGraphEvent::UpdateValue(scope, varname.clone(), value.clone())); - if let Err(e) = res { - log::error!("{}", e); - } - } - ExecutableAction::Shell(command) => { - run_command(timeout, command, args); - } - ExecutableAction::Noop => {} - } -} diff --git a/crates/eww/src/widgets/widget_definitions.rs b/crates/eww/src/widgets/widget_definitions.rs index b7bbf5676..884e28e00 100644 --- a/crates/eww/src/widgets/widget_definitions.rs +++ b/crates/eww/src/widgets/widget_definitions.rs @@ -1,7 +1,7 @@ #![allow(clippy::option_map_unit_fn)] use super::{build_widget::BuilderArgs, circular_progressbar::*, transform::*}; use crate::{ - def_widget, enum_parse, + def_widget, action_args, enum_parse, error::DiagError, error_handling_ctx, util::{list_difference, unindent}, @@ -11,11 +11,11 @@ use anyhow::{anyhow, Context, Result}; use codespan_reporting::diagnostic::Severity; use eww_shared_util::Spanned; use gdk::{ModifierType, NotifyType}; +use glib::signal::SignalHandlerId; use gtk::{self, glib, prelude::*, DestDefaults, TargetEntry, TargetList}; use itertools::Itertools; use once_cell::sync::Lazy; use std::hash::Hasher; -use glib::signal::SignalHandlerId; use std::{ cell::RefCell, @@ -63,7 +63,6 @@ macro_rules! connect_signal_handler { }}; } - // TODO figure out how to // TODO https://developer.gnome.org/gtk3/stable/GtkFixed.html @@ -226,7 +225,8 @@ pub(super) fn resolve_range_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Ran gtk_widget.set_sensitive(true); gtk_widget.add_events(gdk::EventMask::PROPERTY_CHANGE_MASK); connect_signal_handler!(gtk_widget, gtk_widget.connect_value_changed(move |gtk_widget| { - run_action(scope_sender.clone(), calling_scope, timeout, &onchange, &[gtk_widget.value()]); + let args = action_args! { "value" => gtk_widget.value() }; + run_action(scope_sender.clone(), calling_scope, timeout, &onchange, &args); })); } }); @@ -262,7 +262,8 @@ fn build_gtk_combo_box_text(bargs: &mut BuilderArgs) -> Result gtk_widget.active_text().unwrap_or_else(|| "".into()).to_string() }; + run_action(scope_sender.clone(), calling_scope, timeout, &onchange, &args); })); }, }); @@ -308,7 +309,8 @@ fn build_gtk_checkbox(bargs: &mut BuilderArgs) -> Result { prop(timeout: as_duration = Duration::from_millis(200), onchecked: as_action?, onunchecked: as_action?) { let scope_sender = graph.event_sender.clone(); connect_signal_handler!(gtk_widget, gtk_widget.connect_toggled(move |gtk_widget| { - run_action(scope_sender.clone(), calling_scope, timeout, if gtk_widget.is_active() { &onchecked } else { &onunchecked }, &[""]); + let action = if gtk_widget.is_active() { &onchecked } else { &onunchecked }; + run_action(scope_sender.clone(), calling_scope, timeout, action, &action_args!()); })); } }); @@ -330,7 +332,8 @@ fn build_gtk_color_button(bargs: &mut BuilderArgs) -> Result { prop(timeout: as_duration = Duration::from_millis(200), onchange: as_action) { let scope_sender = graph.event_sender.clone(); connect_signal_handler!(gtk_widget, gtk_widget.connect_color_set(move |gtk_widget| { - run_action(scope_sender.clone(), calling_scope, timeout, &onchange, &[gtk_widget.rgba()]); + let args = action_args! { "value" => format!("{}", gtk_widget.rgba()) }; + run_action(scope_sender.clone(), calling_scope, timeout, &onchange, &args); })); } }); @@ -352,7 +355,8 @@ fn build_gtk_color_chooser(bargs: &mut BuilderArgs) -> Result format!("{}", *color) }; + run_action(scope_sender.clone(), calling_scope, timeout, &onchange, &args); })); } }); @@ -416,7 +420,8 @@ fn build_gtk_input(bargs: &mut BuilderArgs) -> Result { prop(timeout: as_duration = Duration::from_millis(200), onchange: as_action) { let scope_sender = graph.event_sender.clone(); connect_signal_handler!(gtk_widget, gtk_widget.connect_changed(move |gtk_widget| { - run_action(scope_sender.clone(), calling_scope, timeout, &onchange, &[gtk_widget.text().to_string()]); + let args = action_args! { "value" => gtk_widget.text().to_string() }; + run_action(scope_sender.clone(), calling_scope, timeout, &onchange, &args); })); } }); @@ -444,9 +449,9 @@ fn build_gtk_button(bargs: &mut BuilderArgs) -> Result { gtk_widget.add_events(gdk::EventMask::BUTTON_PRESS_MASK); connect_signal_handler!(gtk_widget, gtk_widget.connect_button_press_event(move |_, evt| { match evt.button() { - 1 => run_action(scope_sender.clone(), calling_scope, timeout, &onclick, &[""]), - 2 => run_action(scope_sender.clone(), calling_scope, timeout, &onmiddleclick, &[""]), - 3 => run_action(scope_sender.clone(), calling_scope, timeout, &onrightclick, &[""]), + 1 => run_action(scope_sender.clone(), calling_scope, timeout, &onclick, &action_args!()), + 2 => run_action(scope_sender.clone(), calling_scope, timeout, &onmiddleclick, &action_args!()), + 3 => run_action(scope_sender.clone(), calling_scope, timeout, &onrightclick, &action_args!()), _ => {}, } gtk::Inhibit(false) @@ -592,7 +597,8 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { connect_signal_handler!(gtk_widget, gtk_widget.connect_scroll_event(move |_, evt| { let delta = evt.delta().1; if delta != 0f64 { // Ignore the first event https://bugzilla.gnome.org/show_bug.cgi?id=675959 - run_action(scope_sender.clone(), calling_scope, timeout, &onscroll, &[if delta < 0f64 { "up" } else { "down" }]); + let args = action_args! { "direction" => if delta < 0f64 { "up" } else { "down" } }; + run_action(scope_sender.clone(), calling_scope, timeout, &onscroll, &args); } gtk::Inhibit(false) })); @@ -604,7 +610,8 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { gtk_widget.add_events(gdk::EventMask::ENTER_NOTIFY_MASK); connect_signal_handler!(gtk_widget, gtk_widget.connect_enter_notify_event(move |_, evt| { if evt.detail() != NotifyType::Inferior { - run_action(scope_sender.clone(), calling_scope, timeout, &onhover, &[evt.position().0, evt.position().1]); + let args = action_args! { "x" => evt.position().0, "y" => evt.position().1 }; + run_action(scope_sender.clone(), calling_scope, timeout, &onhover, &args); } gtk::Inhibit(false) })); @@ -616,7 +623,8 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { gtk_widget.add_events(gdk::EventMask::LEAVE_NOTIFY_MASK); connect_signal_handler!(gtk_widget, gtk_widget.connect_leave_notify_event(move |_, evt| { if evt.detail() != NotifyType::Inferior { - run_action(scope_sender.clone(), calling_scope, timeout, &onhoverlost, &[evt.position().0, evt.position().1]); + let args = action_args! { "x" => evt.position().0, "y" => evt.position().1 }; + run_action(scope_sender.clone(), calling_scope, timeout, &onhoverlost, &args); } gtk::Inhibit(false) })); @@ -660,9 +668,11 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { ); connect_signal_handler!(gtk_widget, gtk_widget.connect_drag_data_received(move |_, _, _x, _y, selection_data, _target_type, _timestamp| { if let Some(data) = selection_data.uris().first(){ - run_action(scope_sender.clone(), calling_scope, timeout, &ondropped, &[data.to_string(), "file".to_string()]); + let args = action_args! { "data" => data.to_string(), "type" => "file" }; + run_action(scope_sender.clone(), calling_scope, timeout, &ondropped, &args); } else if let Some(data) = selection_data.text(){ - run_action(scope_sender.clone(), calling_scope, timeout, &ondropped, &[data.to_string(), "text".to_string()]); + let args = action_args! { "data" => data.to_string(), "type" => "text" }; + run_action(scope_sender.clone(), calling_scope, timeout, &ondropped, &args); } })); }, @@ -803,14 +813,12 @@ fn build_gtk_calendar(bargs: &mut BuilderArgs) -> Result { prop(timeout: as_duration = Duration::from_millis(200), onclick: as_action) { let scope_sender = graph.event_sender.clone(); connect_signal_handler!(gtk_widget, gtk_widget.connect_day_selected(move |w| { - log::warn!("BREAKING CHANGE: The date is now provided via three values, set by the placeholders {{0}}, {{1}} and {{2}}. If you're currently using the onclick date, you will need to change this."); - run_action( - scope_sender.clone(), - calling_scope, - timeout, - &onclick, - &[w.day(), w.month(), w.year()] - ) + let args = action_args! { + "day" => w.day(), + "month" => w.month(), + "year" => w.year(), + }; + run_action(scope_sender.clone(), calling_scope, timeout, &onclick, &args); })); } diff --git a/crates/simplexpr/src/eval.rs b/crates/simplexpr/src/eval.rs index 7f74ed113..eb6aa9cc3 100644 --- a/crates/simplexpr/src/eval.rs +++ b/crates/simplexpr/src/eval.rs @@ -118,6 +118,14 @@ impl SimplExpr { }) } + /// resolve variable references in the expression. Will leave unresolved variables in place + pub fn resolve_refs_lenient(self, variables: &HashMap) -> Self { + self.map_var_refs(|span, name| match variables.get(&name) { + Some(value) => SimplExpr::Literal(value.clone()), + None => SimplExpr::VarRef(span, name), + }) + } + pub fn var_refs_with_span(&self) -> Vec<(Span, &VarName)> { use SimplExpr::*; match self { @@ -154,7 +162,6 @@ impl SimplExpr { } } - pub fn eval(&self, values: &HashMap) -> Result { let span = self.span(); let value = match self { diff --git a/crates/yuck/src/config/attr_value.rs b/crates/yuck/src/config/attr_value.rs index ccbcd98e1..7f9512482 100644 --- a/crates/yuck/src/config/attr_value.rs +++ b/crates/yuck/src/config/attr_value.rs @@ -35,26 +35,31 @@ pub enum Action { } impl Action { - pub fn eval_exprs(&self, values: &HashMap) -> Result { - Ok(match self { - Self::Update(varname, expr) => ExecutableAction::Update(varname.clone(), expr.eval(values)?), - Self::Shell(expr) => ExecutableAction::Shell(expr.eval(values)?.as_string()?), + pub fn resolve_to_executable(self, values: &HashMap) -> ExecutableAction { + match self { + Self::Update(varname, expr) => ExecutableAction::Update(varname, expr.resolve_refs_lenient(values)), + Self::Shell(expr) => ExecutableAction::Shell(expr.resolve_refs_lenient(values)), Self::Noop => ExecutableAction::Noop, - }) + } } + // TODO the special case for event here is super ugly + /// Returns all variable references in this action, EXCEPT a variable called "event", + /// as that variable is specifically filled in when evaluating the event. + /// see [`eww::widgets::run_action`] pub fn collect_var_refs(&self) -> Vec { - match self { + let refs = match self { Self::Update(_, expr) => expr.collect_var_refs(), Self::Shell(expr) => expr.collect_var_refs(), Self::Noop => vec![], - } + }; + refs.into_iter().filter(|x| x.0 != "event").collect() } } #[derive(Debug, Clone, Eq, PartialEq)] pub enum ExecutableAction { - Update(VarName, DynVal), - Shell(String), + Update(VarName, SimplExpr), + Shell(SimplExpr), Noop, } diff --git a/crates/yuck/src/parser/from_ast.rs b/crates/yuck/src/parser/from_ast.rs index 65c999ce4..af018a3cc 100644 --- a/crates/yuck/src/parser/from_ast.rs +++ b/crates/yuck/src/parser/from_ast.rs @@ -14,6 +14,7 @@ use std::{ collections::{HashMap, LinkedList}, iter::FromIterator, str::FromStr, + time::Duration, }; pub trait FromAst: Sized { @@ -72,6 +73,11 @@ impl FromAst for Action { iter.expect_done()?; Ok(Action::Update(VarName(varname), value)) } + "shell" => { + let (value_span, value) = iter.expect_simplexpr()?; + iter.expect_done()?; + Ok(Action::Shell(value)) + } _ => Err(AstError::UnknownAction(span, action)), } } From 69ce65953458e6f9b4c7c0a8559726933b3e5860 Mon Sep 17 00:00:00 2001 From: elkowar <5300871+elkowar@users.noreply.github.com> Date: Wed, 27 Apr 2022 19:55:20 +0200 Subject: [PATCH 09/10] a few comments --- crates/yuck/src/config/attr_value.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/yuck/src/config/attr_value.rs b/crates/yuck/src/config/attr_value.rs index 7f9512482..1067eb6e7 100644 --- a/crates/yuck/src/config/attr_value.rs +++ b/crates/yuck/src/config/attr_value.rs @@ -27,6 +27,7 @@ impl AttrValue { } } +/// an action as it is provided by the user. These actions contain Expressions which may reference variables. #[derive(Debug, Clone, Eq, PartialEq)] pub enum Action { Update(VarName, SimplExpr), @@ -57,6 +58,8 @@ impl Action { } } +/// an action ready for execution. +/// The expressions in this struct may only reference the variable "event", and _must_ be fully resolved otherwise. #[derive(Debug, Clone, Eq, PartialEq)] pub enum ExecutableAction { Update(VarName, SimplExpr), From ce575e903ccd5e7408e80e554dc8c60e4776ba59 Mon Sep 17 00:00:00 2001 From: elkowar <5300871+elkowar@users.noreply.github.com> Date: Wed, 27 Apr 2022 20:10:47 +0200 Subject: [PATCH 10/10] Adjust widget documentation to match new action system --- crates/eww/src/widgets/widget_definitions.rs | 39 +++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/crates/eww/src/widgets/widget_definitions.rs b/crates/eww/src/widgets/widget_definitions.rs index 884e28e00..6ae52732c 100644 --- a/crates/eww/src/widgets/widget_definitions.rs +++ b/crates/eww/src/widgets/widget_definitions.rs @@ -219,7 +219,7 @@ pub(super) fn resolve_range_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Ran // @prop max - the maximum value prop(max: as_f64) { gtk_widget.adjustment().set_upper(max)}, // @prop timeout - timeout of the command - // @prop onchange - command executed once the value is changes. The placeholder `{}`, used in the command will be replaced by the new value. + // @prop onchange - executed once the value changes. Event object: `{"value": }` prop(timeout: as_duration = Duration::from_millis(200), onchange: as_action) { let scope_sender = graph.event_sender.clone(); gtk_widget.set_sensitive(true); @@ -258,7 +258,7 @@ fn build_gtk_combo_box_text(bargs: &mut BuilderArgs) -> Result}` prop(timeout: as_duration = Duration::from_millis(200), onchange: as_action) { let scope_sender = graph.event_sender.clone(); connect_signal_handler!(gtk_widget, gtk_widget.connect_changed(move |gtk_widget| { @@ -304,8 +304,8 @@ fn build_gtk_checkbox(bargs: &mut BuilderArgs) -> Result { let calling_scope = bargs.calling_scope.clone(); def_widget!(bargs, graph, gtk_widget, { // @prop timeout - timeout of the command - // @prop onchecked - action (command) to be executed when checked by the user - // @prop onunchecked - similar to onchecked but when the widget is unchecked + // @prop onchecked - executed when checked by the user + // @prop onunchecked - executed when unchecked by the user prop(timeout: as_duration = Duration::from_millis(200), onchecked: as_action?, onunchecked: as_action?) { let scope_sender = graph.event_sender.clone(); connect_signal_handler!(gtk_widget, gtk_widget.connect_toggled(move |gtk_widget| { @@ -327,7 +327,7 @@ fn build_gtk_color_button(bargs: &mut BuilderArgs) -> Result { // @prop use-alpha - bool to whether or not use alpha prop(use_alpha: as_bool) {gtk_widget.set_use_alpha(use_alpha);}, - // @prop onchange - runs the code when the color was selected + // @prop onchange - executed when a color was selected. Event object: `{"value": }` // @prop timeout - timeout of the command prop(timeout: as_duration = Duration::from_millis(200), onchange: as_action) { let scope_sender = graph.event_sender.clone(); @@ -350,7 +350,7 @@ fn build_gtk_color_chooser(bargs: &mut BuilderArgs) -> Result}` // @prop timeout - timeout of the command prop(timeout: as_duration = Duration::from_millis(200), onchange: as_action) { let scope_sender = graph.event_sender.clone(); @@ -415,7 +415,7 @@ fn build_gtk_input(bargs: &mut BuilderArgs) -> Result { gtk_widget.set_text(&value); }, - // @prop onchange - Command to run when the text changes. The placeholder `{}` will be replaced by the value + // @prop onchange - Executed when the text changes. Event object: `{"value": }` // @prop timeout - timeout of the command prop(timeout: as_duration = Duration::from_millis(200), onchange: as_action) { let scope_sender = graph.event_sender.clone(); @@ -435,16 +435,11 @@ fn build_gtk_button(bargs: &mut BuilderArgs) -> Result { let calling_scope = bargs.calling_scope.clone(); def_widget!(bargs, graph, gtk_widget, { - // @prop onclick - a command that get's run when the button is clicked - // @prop onmiddleclick - a command that get's run when the button is middleclicked - // @prop onrightclick - a command that get's run when the button is rightclicked + // @prop onclick - executed when the button is left-clicked + // @prop onmiddleclick - executed when the button is left-clicked + // @prop onrightclick - executed when the button is left-clicked // @prop timeout - timeout of the command - prop( - timeout: as_duration = Duration::from_millis(200), - onclick: as_action?, - onmiddleclick: as_action?, - onrightclick: as_action? - ) { + prop(timeout: as_duration = Duration::from_millis(200), onclick: as_action?, onmiddleclick: as_action?, onrightclick: as_action?) { let scope_sender = graph.event_sender.clone(); gtk_widget.add_events(gdk::EventMask::BUTTON_PRESS_MASK); connect_signal_handler!(gtk_widget, gtk_widget.connect_button_press_event(move |_, evt| { @@ -589,7 +584,7 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { def_widget!(bargs, graph, gtk_widget, { // @prop timeout - timeout of the command - // @prop onscroll - event to execute when the user scrolls with the mouse over the widget. The placeholder `{}` used in the command will be replaced with either `up` or `down`. + // @prop onscroll - executed when the user scrolls with the mouse over the widget. Event object: `{"direction": <"up" or "down">}` prop(timeout: as_duration = Duration::from_millis(200), onscroll: as_action) { let scope_sender = graph.event_sender.clone(); gtk_widget.add_events(gdk::EventMask::SCROLL_MASK); @@ -604,7 +599,7 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { })); }, // @prop timeout - timeout of the command - // @prop onhover - event to execute when the user hovers over the widget + // @prop onhover - executed when the user hovers over the widget. Event object: `{"x": , ""}` prop(timeout: as_duration = Duration::from_millis(200), onhover: as_action) { let scope_sender = graph.event_sender.clone(); gtk_widget.add_events(gdk::EventMask::ENTER_NOTIFY_MASK); @@ -617,7 +612,7 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { })); }, // @prop timeout - timeout of the command - // @prop onhoverlost - event to execute when the user losts hovers over the widget + // @prop onhoverlost - executed when the users mouse leaves the widget. Event object: `{"x": , ""}` prop(timeout: as_duration = Duration::from_millis(200), onhoverlost: as_action) { let scope_sender = graph.event_sender.clone(); gtk_widget.add_events(gdk::EventMask::LEAVE_NOTIFY_MASK); @@ -655,7 +650,7 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { })); }, // @prop timeout - timeout of the command - // @prop on_dropped - Command to execute when something is dropped on top of this element. The placeholder `{}` used in the command will be replaced with the uri to the dropped thing. + // @prop ondropped - executed when something is dropped on top of this element. Event object: `{"data": , "type": <"file" or "text">}` prop(timeout: as_duration = Duration::from_millis(200), ondropped: as_action) { let scope_sender = graph.event_sender.clone(); gtk_widget.drag_dest_set( @@ -808,7 +803,7 @@ fn build_gtk_calendar(bargs: &mut BuilderArgs) -> Result { prop(show_day_names: as_bool) { gtk_widget.set_show_day_names(show_day_names) }, // @prop show-week-numbers - show week numbers prop(show_week_numbers: as_bool) { gtk_widget.set_show_week_numbers(show_week_numbers) }, - // @prop onclick - command to run when the user selects a date. The `{0}` placeholder will be replaced by the selected day, `{1}` will be replaced by the month, and `{2}` by the year. + // @prop onclick - executed when the user selects a date. Event object `{"day": , "month": , "year": }` // @prop timeout - timeout of the command prop(timeout: as_duration = Duration::from_millis(200), onclick: as_action) { let scope_sender = graph.event_sender.clone(); @@ -833,7 +828,7 @@ fn build_gtk_calendar(bargs: &mut BuilderArgs) -> Result { fn build_transform(bargs: &mut BuilderArgs) -> Result { let w = Transform::new(); def_widget!(bargs, _g, w, { - // @prop rotation - the percentage to rotate + // @prop rotate - the percentage to rotate prop(rotate: as_f64) { w.set_property("rotate", rotate)?; }, // @prop translate-x - the amount to translate in the x direction (px or %) prop(translate_x: as_string) { w.set_property("translate-x", translate_x)?; },