diff --git a/Cargo.lock b/Cargo.lock index b0fc32e..9e7970f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -463,6 +463,7 @@ name = "eww_shared_util" version = "0.1.0" dependencies = [ "derive_more", + "ref-cast", "serde", ] @@ -1744,6 +1745,26 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "ref-cast" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300f2a835d808734ee295d45007adacb9ebb29dd3ae2424acfa17930cae541da" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c38e3aecd2b21cb3959637b883bb3714bc7e43f0268b9a29d3743ee3e55cdd2" +dependencies = [ + "proc-macro2", + "quote 1.0.10", + "syn 1.0.81", +] + [[package]] name = "regex" version = "1.5.4" diff --git a/crates/eww/Cargo.toml b/crates/eww/Cargo.toml index f08f951..d31bd69 100644 --- a/crates/eww/Cargo.toml +++ b/crates/eww/Cargo.toml @@ -1,14 +1,16 @@ +cargo-features = ["edition2021"] [package] name = "eww" version = "0.2.0" authors = ["elkowar <5300871+elkowar@users.noreply.github.com>"] -edition = "2018" +edition = "2021" description = "Widget system for everyone!" license = "MIT" repository = "https://github.com/elkowar/eww" homepage = "https://github.com/elkowar/eww" + [features] default = ["x11"] x11 = ["gdkx11", "x11rb", "yuck/x11"] diff --git a/crates/eww/src/app.rs b/crates/eww/src/app.rs index 7ac0802..6c9d051 100644 --- a/crates/eww/src/app.rs +++ b/crates/eww/src/app.rs @@ -1,21 +1,25 @@ use crate::{ config, daemon_response::DaemonResponseSender, - display_backend, error_handling_ctx, eww_state, + display_backend, error_handling_ctx, gtk::prelude::{ContainerExt, CssProviderExt, GtkWindowExt, StyleContextExt, WidgetExt}, - script_var_handler::*, - EwwPaths, + script_var_handler::ScriptVarHandlerHandle, + state::scope_graph::{ScopeGraph, ScopeGraphEvent, ScopeIndex}, + EwwPaths, *, }; -use anyhow::*; -use debug_stub_derive::*; use eww_shared_util::VarName; use itertools::Itertools; use simplexpr::dynval::DynVal; -use std::collections::{HashMap, HashSet}; +use std::{ + cell::RefCell, + collections::{HashMap, HashSet}, + rc::Rc, +}; use tokio::sync::mpsc::UnboundedSender; use yuck::{ config::{ script_var_definition::ScriptVarDefinition, + window_definition::WindowDefinition, window_geometry::{AnchorPoint, WindowGeometry}, }, value::Coords, @@ -58,13 +62,15 @@ pub enum DaemonCommand { sender: DaemonResponseSender, }, PrintDebug(DaemonResponseSender), + PrintGraph(DaemonResponseSender), PrintWindows(DaemonResponseSender), } #[derive(Debug, Clone)] pub struct EwwWindow { pub name: String, - pub definition: config::EwwWindowDefinition, + pub definition: yuck::config::window_definition::WindowDefinition, + pub scope_index: ScopeIndex, pub gtk_window: gtk::Window, } @@ -74,9 +80,8 @@ impl EwwWindow { } } -#[derive(DebugStub)] pub struct App { - pub eww_state: eww_state::EwwState, + pub scope_graph: Rc>, pub eww_config: config::EwwConfig, pub open_windows: HashMap, /// Window names that are supposed to be open, but failed. @@ -84,14 +89,24 @@ pub struct App { pub failed_windows: HashSet, pub css_provider: gtk::CssProvider, - #[debug_stub = "ScriptVarHandler(...)"] pub app_evt_send: UnboundedSender, - #[debug_stub = "ScriptVarHandler(...)"] pub script_var_handler: ScriptVarHandlerHandle, pub paths: EwwPaths, } +impl std::fmt::Debug for App { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("App") + .field("scope_graph", &*self.scope_graph.borrow()) + .field("eww_config", &self.eww_config) + .field("open_windows", &self.open_windows) + .field("failed_windows", &self.failed_windows) + .field("paths", &self.paths) + .finish() + } +} + impl App { /// Handle a DaemonCommand event. pub fn handle_command(&mut self, event: DaemonCommand) { @@ -104,7 +119,7 @@ impl App { } DaemonCommand::UpdateVars(mappings) => { for (var_name, new_value) in mappings { - self.update_state(var_name, new_value); + self.update_global_state(var_name, new_value); } } DaemonCommand::ReloadConfigAndCss(sender) => { @@ -170,18 +185,21 @@ impl App { sender.respond_with_error_list(errors)?; } DaemonCommand::PrintState { all, sender } => { - let vars = self.eww_state.get_variables().iter(); - let output = if all { - vars.map(|(key, value)| format!("{}: {}", key, value)).join("\n") - } else { - vars.filter(|(x, _)| self.eww_state.referenced_vars().any(|var| x == &var)) - .map(|(key, value)| format!("{}: {}", key, value)) - .join("\n") - }; + let scope_graph = self.scope_graph.borrow(); + let used_globals_names = scope_graph.currently_used_globals(); + let output = scope_graph + .scope_at(scope_graph.root_index) + .expect("No global scope in scopegraph") + .data + .iter() + .filter(|(key, _)| all || used_globals_names.contains(*key)) + .map(|(key, value)| format!("{}: {}", key, value)) + .join("\n"); sender.send_success(output)? } DaemonCommand::GetVar { name, sender } => { - let vars = self.eww_state.get_variables(); + let scope_graph = &*self.scope_graph.borrow(); + let vars = &scope_graph.scope_at(scope_graph.root_index).expect("No root scope in graph").data; match vars.get(name.as_str()) { Some(x) => sender.send_success(x.to_string())?, None => sender.send_failure(format!("Variable not found \"{}\"", name))?, @@ -203,6 +221,7 @@ impl App { let output = format!("{:#?}", &self); sender.send_success(output)? } + DaemonCommand::PrintGraph(sender) => sender.send_success(self.scope_graph.borrow().visualize())?, } }; @@ -219,13 +238,20 @@ impl App { gtk::main_quit(); } - fn update_state(&mut self, fieldname: VarName, value: DynVal) { - self.eww_state.update_variable(fieldname.clone(), value); + fn update_global_state(&mut self, fieldname: VarName, value: DynVal) { + let result = self.scope_graph.borrow_mut().update_global_value(&fieldname, value); + if let Err(err) = result { + error_handling_ctx::print_error(err); + } if let Ok(linked_poll_vars) = self.eww_config.get_poll_var_link(&fieldname) { linked_poll_vars.iter().filter_map(|name| self.eww_config.get_script_var(name).ok()).for_each(|var| { if let ScriptVarDefinition::Poll(poll_var) = var { - match poll_var.run_while_expr.eval(self.eww_state.get_variables()).map(|v| v.as_bool()) { + let scope_graph = self.scope_graph.borrow(); + let run_while = scope_graph + .evaluate_simplexpr_in_scope(scope_graph.root_index, &poll_var.run_while_expr) + .map(|v| v.as_bool()); + match run_while { Ok(Ok(true)) => self.script_var_handler.add(var.clone()), Ok(Ok(false)) => self.script_var_handler.stop_for_variable(poll_var.name.clone()), Ok(Err(err)) => error_handling_ctx::print_error(anyhow!(err)), @@ -236,25 +262,28 @@ impl App { } } - fn close_window(&mut self, window_name: &String) -> Result<()> { - for unused_var in self.variables_only_used_in(window_name) { + fn close_window(&mut self, window_name: &str) -> Result<()> { + let eww_window = self + .open_windows + .remove(window_name) + .with_context(|| format!("Tried to close window named '{}', but no such window was open", window_name))?; + + self.scope_graph.borrow_mut().remove_scope(eww_window.scope_index); + + eww_window.close(); + + let unused_variables = self.scope_graph.borrow().currently_unused_globals(); + for unused_var in unused_variables { log::debug!("stopping for {}", &unused_var); self.script_var_handler.stop_for_variable(unused_var.clone()); } - self.open_windows - .remove(window_name) - .with_context(|| format!("Tried to close window named '{}', but no such window was open", window_name))? - .close(); - - self.eww_state.clear_window_state(window_name); - Ok(()) } fn open_window( &mut self, - window_name: &String, + window_name: &str, pos: Option, size: Option, monitor: Option, @@ -270,24 +299,44 @@ impl App { let mut window_def = self.eww_config.get_window(window_name)?.clone(); window_def.geometry = window_def.geometry.map(|x| x.override_if_given(anchor, pos, size)); - let root_widget = - window_def.widget.render(&mut self.eww_state, window_name, self.eww_config.get_widget_definitions())?; + let root_index = self.scope_graph.borrow().root_index; + + let window_scope = self.scope_graph.borrow_mut().register_new_scope( + window_name.to_string(), + Some(root_index), + root_index, + HashMap::new(), + )?; + + let root_widget = crate::widgets::build_widget::build_gtk_widget( + &mut *self.scope_graph.borrow_mut(), + Rc::new(self.eww_config.get_widget_definitions().clone()), + window_scope, + window_def.widget.clone(), + None, + )?; root_widget.style_context().add_class(&window_name.to_string()); let monitor_geometry = get_monitor_geometry(monitor.or(window_def.monitor_number))?; - let eww_window = initialize_window(monitor_geometry, root_widget, window_def)?; - - self.open_windows.insert(window_name.clone(), eww_window); + let eww_window = initialize_window(monitor_geometry, root_widget, window_def, window_scope)?; // initialize script var handlers for variables that where not used before opening this window. - // TODO somehow make this less shit - for newly_used_var in - self.variables_only_used_in(window_name).filter_map(|var| self.eww_config.get_script_var(var).ok()) - { - self.script_var_handler.add(newly_used_var.clone()); + // TODO maybe this could be handled by having a track_newly_used_variables function in the scope tree? + for used_var in self.scope_graph.borrow().variables_used_in_self_or_subscopes_of(eww_window.scope_index) { + if let Ok(script_var) = self.eww_config.get_script_var(&used_var) { + self.script_var_handler.add(script_var.clone()); + } } + + eww_window.gtk_window.connect_destroy({ + let scope_graph_sender = self.scope_graph.borrow().event_sender.clone(); + move |_| { + let _ = scope_graph_sender.send(ScopeGraphEvent::RemoveScope(eww_window.scope_index)); + } + }); + self.open_windows.insert(window_name.to_string(), eww_window); }; if let Err(err) = open_result { @@ -300,22 +349,20 @@ impl App { /// Load the given configuration, reloading all script-vars and attempting to reopen all windows that where opened. pub fn load_config(&mut self, config: config::EwwConfig) -> Result<()> { - // TODO the reload procedure is kinda bad. - // It should probably instead prepare a new eww_state and everything, and then swap the instances once everything has worked. - log::info!("Reloading windows"); - // refresh script-var poll stuff + self.script_var_handler.stop_all(); + self.script_var_handler = script_var_handler::init(self.app_evt_send.clone()); log::trace!("loading config: {:#?}", config); self.eww_config = config; - self.eww_state.clear_all_window_states(); + self.scope_graph.borrow_mut().clear(self.eww_config.generate_initial_state()?); let window_names: Vec = self.open_windows.keys().cloned().chain(self.failed_windows.iter().cloned()).dedup().collect(); for window_name in &window_names { - self.open_window(&window_name, None, None, None, None)?; + self.open_window(window_name, None, None, None, None)?; } Ok(()) } @@ -324,36 +371,13 @@ impl App { self.css_provider.load_from_data(css.as_bytes())?; Ok(()) } - - /// Get all variable names that are currently referenced in any of the open windows. - pub fn get_currently_used_variables(&self) -> impl Iterator { - self.open_windows.keys().flat_map(move |window_name| self.eww_state.vars_referenced_in(window_name)) - } - - /// Get all variables mapped to a list of windows they are being used in. - pub fn currently_used_variables<'a>(&'a self) -> HashMap<&'a VarName, Vec<&'a String>> { - let mut vars: HashMap<&'a VarName, Vec<_>> = HashMap::new(); - for window_name in self.open_windows.keys() { - for var in self.eww_state.vars_referenced_in(window_name) { - vars.entry(var).and_modify(|l| l.push(window_name)).or_insert_with(|| vec![window_name]); - } - } - vars - } - - /// Get all variables that are only used in the given window. - pub fn variables_only_used_in<'a>(&'a self, window: &'a String) -> impl Iterator { - self.currently_used_variables() - .into_iter() - .filter(move |(_, wins)| wins.len() == 1 && wins.contains(&window)) - .map(|(var, _)| var) - } } fn initialize_window( monitor_geometry: gdk::Rectangle, root_widget: gtk::Widget, - window_def: config::EwwWindowDefinition, + window_def: WindowDefinition, + window_scope: ScopeIndex, ) -> Result { let window = display_backend::initialize_window(&window_def, monitor_geometry) .with_context(|| format!("monitor {} is unavailable", window_def.monitor_number.unwrap()))?; @@ -395,7 +419,7 @@ fn initialize_window( window.show_all(); - Ok(EwwWindow { name: window_def.name.clone(), definition: window_def, gtk_window: window }) + Ok(EwwWindow { name: window_def.name.clone(), definition: window_def, gtk_window: window, scope_index: window_scope }) } /// Apply the provided window-positioning rules to the window. diff --git a/crates/eww/src/client.rs b/crates/eww/src/client.rs index e5d98ec..61ac5a2 100644 --- a/crates/eww/src/client.rs +++ b/crates/eww/src/client.rs @@ -24,6 +24,9 @@ pub fn handle_client_only_action(paths: &EwwPaths, action: ActionClientOnly) -> Ok(()) } + +/// Connect to the daemon and send the given request. +/// Returns the response from the daemon, or None if the daemon did not provide any useful response. An Ok(None) response does _not_ indicate failure. pub fn do_server_call(stream: &mut UnixStream, action: &opts::ActionWithServer) -> Result> { log::debug!("Forwarding options to server"); stream.set_nonblocking(false).context("Failed to set stream to non-blocking")?; diff --git a/crates/eww/src/config/eww_config.rs b/crates/eww/src/config/eww_config.rs index 9d23120..33d0b04 100644 --- a/crates/eww/src/config/eww_config.rs +++ b/crates/eww/src/config/eww_config.rs @@ -2,16 +2,17 @@ use anyhow::*; use eww_shared_util::VarName; use std::{collections::HashMap, path::Path}; use yuck::config::{ - file_provider::YuckFiles, script_var_definition::ScriptVarDefinition, widget_definition::WidgetDefinition, Config, + file_provider::YuckFiles, script_var_definition::ScriptVarDefinition, widget_definition::WidgetDefinition, + window_definition::WindowDefinition, Config, }; use simplexpr::dynval::DynVal; use crate::error_handling_ctx; -use super::{script_var, EwwWindowDefinition}; +use super::script_var; -/// Load an [EwwConfig] from a given file, resetting and applying the global YuckFiles object in [error_handling_ctx]. +/// Load an [EwwConfig] from a given file, resetting and applying the global YuckFiles object in [`crate::error_handling_ctx`]. pub fn read_from_file(path: impl AsRef) -> Result { error_handling_ctx::clear_files(); EwwConfig::read_from_file(&mut error_handling_ctx::YUCK_FILES.write().unwrap(), path) @@ -21,7 +22,7 @@ pub fn read_from_file(path: impl AsRef) -> Result { #[derive(Debug, Clone)] pub struct EwwConfig { widgets: HashMap, - windows: HashMap, + windows: HashMap, initial_variables: HashMap, script_vars: HashMap, @@ -65,10 +66,7 @@ impl EwwConfig { }); Ok(EwwConfig { - windows: window_definitions - .into_iter() - .map(|(name, window)| Ok((name, EwwWindowDefinition::generate(&widget_definitions, window)?))) - .collect::>>()?, + windows: window_definitions, widgets: widget_definitions, initial_variables: var_definitions.into_iter().map(|(k, v)| (k, v.initial_value)).collect(), script_vars, @@ -87,11 +85,11 @@ impl EwwConfig { Ok(vars) } - pub fn get_windows(&self) -> &HashMap { + pub fn get_windows(&self) -> &HashMap { &self.windows } - pub fn get_window(&self, name: &str) -> Result<&EwwWindowDefinition> { + pub fn get_window(&self, name: &str) -> Result<&WindowDefinition> { self.windows.get(name).with_context(|| { format!( "No window named '{}' exists in config.\nThis may also be caused by your config failing to load properly, \ diff --git a/crates/eww/src/config/window_definition.rs b/crates/eww/src/config/window_definition.rs index 750e8fb..e69de29 100644 --- a/crates/eww/src/config/window_definition.rs +++ b/crates/eww/src/config/window_definition.rs @@ -1,39 +0,0 @@ -use std::collections::HashMap; - -use anyhow::*; -use yuck::config::{ - backend_window_options::BackendWindowOptions, - widget_definition::WidgetDefinition, - window_definition::{WindowDefinition, WindowStacking}, - window_geometry::WindowGeometry, -}; - -use crate::widgets::widget_node; - -/// Full window-definition containing the fully expanded widget tree. -/// **Use this** rather than `[RawEwwWindowDefinition]`. -#[derive(Debug, Clone)] -pub struct EwwWindowDefinition { - pub name: String, - - pub geometry: Option, - pub stacking: WindowStacking, - pub monitor_number: Option, - pub widget: Box, - pub resizable: bool, - pub backend_options: BackendWindowOptions, -} - -impl EwwWindowDefinition { - pub fn generate(defs: &HashMap, window: WindowDefinition) -> Result { - Ok(EwwWindowDefinition { - name: window.name, - geometry: window.geometry, - stacking: window.stacking, - monitor_number: window.monitor_number, - resizable: window.resizable, - widget: widget_node::generate_generic_widget_node(defs, &HashMap::new(), window.widget)?, - backend_options: window.backend_options, - }) - } -} diff --git a/crates/eww/src/display_backend.rs b/crates/eww/src/display_backend.rs index 56b5387..9806450 100644 --- a/crates/eww/src/display_backend.rs +++ b/crates/eww/src/display_backend.rs @@ -2,9 +2,9 @@ pub use platform::*; #[cfg(not(any(feature = "x11", feature = "wayland")))] mod platform { - use crate::config::EwwWindowDefinition; + use yuck::config::window_definition::{WindowDefinition, WindowStacking}; - pub fn initialize_window(_window_def: &EwwWindowDefinition, _monitor: gdk::Rectangle) -> Option { + pub fn initialize_window(_window_def: &WindowDefinition, _monitor: gdk::Rectangle) -> Option { Some(gtk::Window::new(gtk::WindowType::Toplevel)) } } @@ -13,11 +13,9 @@ mod platform { mod platform { use gdk; use gtk::prelude::*; - use yuck::config::{window_definition::WindowStacking, window_geometry::AnchorAlignment}; + use yuck::config::{window_definition::{WindowStacking, WindowDefinition}, window_geometry::AnchorAlignment}; - use crate::config::EwwWindowDefinition; - - pub fn initialize_window(window_def: &EwwWindowDefinition, monitor: gdk::Rectangle) -> Option { + pub fn initialize_window(window_def: &WindowDefinition, monitor: gdk::Rectangle) -> Option { let window = gtk::Window::new(gtk::WindowType::Toplevel); // Initialising a layer shell surface gtk_layer_shell::init_for_window(&window); @@ -104,12 +102,10 @@ mod platform { }; use yuck::config::{ backend_window_options::{Side, WindowType}, - window_definition::WindowStacking, + window_definition::{WindowDefinition, WindowStacking}, }; - use crate::config::EwwWindowDefinition; - - pub fn initialize_window(window_def: &EwwWindowDefinition, _monitor: gdk::Rectangle) -> Option { + pub fn initialize_window(window_def: &WindowDefinition, _monitor: gdk::Rectangle) -> Option { let window_type = if window_def.backend_options.wm_ignore { gtk::WindowType::Popup } else { gtk::WindowType::Toplevel }; let window = gtk::Window::new(window_type); let wm_class_name = format!("eww-{}", window_def.name); @@ -126,7 +122,7 @@ mod platform { Some(window) } - pub fn set_xprops(window: >k::Window, monitor: gdk::Rectangle, window_def: &EwwWindowDefinition) -> Result<()> { + pub fn set_xprops(window: >k::Window, monitor: gdk::Rectangle, window_def: &WindowDefinition) -> Result<()> { let backend = X11Backend::new()?; backend.set_xprops_for(window, monitor, window_def)?; Ok(()) @@ -150,7 +146,7 @@ mod platform { &self, window: >k::Window, monitor_rect: gdk::Rectangle, - window_def: &EwwWindowDefinition, + window_def: &WindowDefinition, ) -> Result<()> { let gdk_window = window.window().context("Couldn't get gdk window from gtk window")?; let win_id = diff --git a/crates/eww/src/error_handling_ctx.rs b/crates/eww/src/error_handling_ctx.rs index b965b4a..1e54119 100644 --- a/crates/eww/src/error_handling_ctx.rs +++ b/crates/eww/src/error_handling_ctx.rs @@ -63,15 +63,6 @@ pub fn anyhow_err_to_diagnostic(err: &anyhow::Error) -> Option } } -// pub fn print_diagnostic(diagnostic: codespan_reporting::diagnostic::Diagnostic) { -// match stringify_diagnostic(diagnostic.clone()) { -// Ok(diag) => { -// eprintln!("{}", diag); -//} -// Err(_) => { -// log::error!("{:?}", diagnostic); -//} - pub fn stringify_diagnostic(mut diagnostic: codespan_reporting::diagnostic::Diagnostic) -> anyhow::Result { diagnostic.labels.drain_filter(|label| Span(label.range.start, label.range.end, label.file_id).is_dummy()); diff --git a/crates/eww/src/eww_state.rs b/crates/eww/src/eww_state.rs deleted file mode 100644 index 7bba210..0000000 --- a/crates/eww/src/eww_state.rs +++ /dev/null @@ -1,143 +0,0 @@ -use anyhow::*; -use eww_shared_util::{AttrName, VarName}; -use std::{collections::HashMap, sync::Arc}; - -use simplexpr::{dynval::DynVal, SimplExpr}; - -use crate::error_handling_ctx; - -/// Handler that gets executed to apply the necessary parts of the eww state to -/// a gtk widget. These are created and initialized in EwwState::resolve. -pub struct StateChangeHandler { - func: Box) -> Result<()> + 'static>, - unresolved_values: HashMap, -} - -impl StateChangeHandler { - fn used_variables(&self) -> impl Iterator { - self.unresolved_values.iter().flat_map(|(_, value)| value.var_refs()).map(|(_, value)| value) - } - - /// Run the StateChangeHandler. - /// [`state`] should be the global [EwwState::state]. - fn run_with_state(&self, state: &HashMap) { - let resolved_attrs = self - .unresolved_values - .clone() - .into_iter() - .map(|(attr_name, value)| Ok((attr_name, value.eval(state)?))) - .collect::>(); - - match resolved_attrs { - Ok(resolved_attrs) => { - if let Err(err) = (self.func)(resolved_attrs).context("Error while updating UI after state change") { - error_handling_ctx::print_error(err); - } - } - Err(err) => { - error_handling_ctx::print_error(err); - } - } - } -} - -/// Collection of [StateChangeHandler]s -/// State specific to one window. -/// stores the state_change handlers that are used for that window. -#[derive(Default)] -pub struct EwwWindowState { - state_change_handlers: HashMap>>, -} - -impl EwwWindowState { - /// register a new [`StateChangeHandler`] - fn put_handler(&mut self, handler: StateChangeHandler) { - let handler = Arc::new(handler); - for var_name in handler.used_variables() { - self.state_change_handlers.entry(var_name.clone()).or_insert_with(Vec::new).push(handler.clone()); - } - } -} - -/// Stores the actual state of eww, including the variable state and the -/// window-specific state-change handlers. -#[derive(Default)] -pub struct EwwState { - windows: HashMap, - variables_state: HashMap, -} - -impl std::fmt::Debug for EwwState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "EwwState {{ state: {:?} }}", self.variables_state) - } -} - -impl EwwState { - pub fn from_default_vars(defaults: HashMap) -> Self { - EwwState { variables_state: defaults, ..EwwState::default() } - } - - pub fn get_variables(&self) -> &HashMap { - &self.variables_state - } - - /// remove all state stored specific to one window - pub fn clear_window_state(&mut self, window_name: &str) { - self.windows.remove(window_name); - } - - /// remove all state that is specific to any window - pub fn clear_all_window_states(&mut self) { - self.windows.clear(); - } - - /// Update the value of a variable, running all registered - /// [StateChangeHandler]s. - pub fn update_variable(&mut self, key: VarName, value: DynVal) { - self.variables_state.insert(key.clone(), value); - - // run all of the handlers - self.windows - .values() - .filter_map(|window_state| window_state.state_change_handlers.get(&key)) - .flatten() - .for_each(|handler| handler.run_with_state(&self.variables_state)); - } - - /// Look up a single variable in the eww state, returning an `Err` when the value is not found. - pub fn lookup(&self, var_name: &VarName) -> Result<&DynVal> { - self.variables_state.get(var_name).with_context(|| format!("Unknown variable '{}' referenced", var_name)) - } - - /// resolves a value if possible, using the current eww_state. - pub fn resolve_once<'a>(&'a self, value: &'a SimplExpr) -> Result { - Ok(value.clone().eval(&self.variables_state)?) - } - - /// Resolve takes a function that applies a set of fully resolved attribute - /// values to it's gtk widget. - pub fn resolve) -> Result<()> + 'static + Clone>( - &mut self, - window_name: &str, - required_attributes: HashMap, - set_value: F, - ) { - let handler = StateChangeHandler { func: Box::new(set_value), unresolved_values: required_attributes }; - - handler.run_with_state(&self.variables_state); - - // only store the handler if at least one variable is being used - if handler.used_variables().next().is_some() { - self.windows.entry(window_name.to_string()).or_insert_with(EwwWindowState::default).put_handler(handler); - } - } - - pub fn referenced_vars(&self) -> impl Iterator { - self.windows.values().flat_map(|w| w.state_change_handlers.keys()) - } - - pub fn vars_referenced_in(&self, window_name: &str) -> std::collections::HashSet<&VarName> { - self.windows.get(window_name).map(|window| window.state_change_handlers.keys().collect()).unwrap_or_default() - } -} diff --git a/crates/eww/src/main.rs b/crates/eww/src/main.rs index 030e587..bbe9a38 100644 --- a/crates/eww/src/main.rs +++ b/crates/eww/src/main.rs @@ -6,6 +6,7 @@ #![feature(result_cloned)] #![feature(try_blocks)] #![feature(nll)] +#![allow(rustdoc::private_intra_doc_links)] extern crate gio; extern crate gtk; @@ -31,12 +32,12 @@ mod daemon_response; pub mod display_backend; pub mod error; mod error_handling_ctx; -pub mod eww_state; pub mod geometry; pub mod ipc_server; pub mod opts; pub mod script_var_handler; pub mod server; +pub mod state; pub mod util; pub mod widgets; @@ -58,51 +59,25 @@ fn main() { .unwrap_or_else(EwwPaths::default) .context("Failed to initialize eww paths")?; + let should_restart = match &opts.action { + opts::Action::Daemon => opts.restart, + opts::Action::WithServer(action) => opts.restart && action.can_start_daemon(), + opts::Action::ClientOnly(_) => false, + }; + if should_restart { + let response = handle_server_command(&paths, &ActionWithServer::KillServer, 1); + if let Ok(Some(response)) = response { + handle_daemon_response(response); + } + std::thread::sleep(std::time::Duration::from_millis(200)); + } + let would_show_logs = match opts.action { opts::Action::ClientOnly(action) => { client::handle_client_only_action(&paths, action)?; false } - // a running daemon is necessary for this command - opts::Action::WithServer(action) if action.can_start_daemon() && !opts.no_daemonize => { - if opts.restart { - let _ = handle_server_command(&paths, &ActionWithServer::KillServer, 1); - std::thread::sleep(std::time::Duration::from_millis(200)); - } - - // attempt to just send the command to a running daemon - if let Err(err) = handle_server_command(&paths, &action, 5) { - // connecting to the daemon failed. Thus, start the daemon here! - log::warn!("Failed to connect to daemon: {}", err); - log::info!("Initializing eww server. ({})", paths.get_ipc_socket_file().display()); - let _ = std::fs::remove_file(paths.get_ipc_socket_file()); - if !opts.show_logs { - println!("Run `{} logs` to see any errors while editing your configuration.", eww_binary_name); - } - - let (command, response_recv) = action.into_daemon_command(); - // start the daemon and give it the command - let fork_result = server::initialize_server(paths.clone(), Some(command), true)?; - let is_parent = fork_result == ForkResult::Parent; - if let (Some(recv), true) = (response_recv, is_parent) { - listen_for_daemon_response(recv); - } - is_parent - } else { - true - } - } - opts::Action::WithServer(ActionWithServer::KillServer) => { - handle_server_command(&paths, &ActionWithServer::KillServer, 1)?; - false - } - - opts::Action::WithServer(action) => { - handle_server_command(&paths, &action, 5)?; - true - } - // make sure that there isn't already a Eww daemon running. opts::Action::Daemon if check_server_running(paths.get_ipc_socket_file()) => { eprintln!("Eww server already running."); @@ -118,6 +93,45 @@ fn main() { let fork_result = server::initialize_server(paths.clone(), None, !opts.no_daemonize)?; opts.no_daemonize || fork_result == ForkResult::Parent } + + opts::Action::WithServer(ActionWithServer::KillServer) => { + if let Some(response) = handle_server_command(&paths, &ActionWithServer::KillServer, 1)? { + handle_daemon_response(response); + } + false + } + + // a running daemon is necessary for this command + opts::Action::WithServer(action) => { + // attempt to just send the command to a running daemon + match handle_server_command(&paths, &action, 5) { + Ok(Some(response)) => { + handle_daemon_response(response); + true + } + Ok(None) => true, + + Err(err) if action.can_start_daemon() && !opts.no_daemonize => { + // connecting to the daemon failed. Thus, start the daemon here! + log::warn!("Failed to connect to daemon: {}", err); + log::info!("Initializing eww server. ({})", paths.get_ipc_socket_file().display()); + let _ = std::fs::remove_file(paths.get_ipc_socket_file()); + if !opts.show_logs { + println!("Run `{} logs` to see any errors while editing your configuration.", eww_binary_name); + } + + let (command, response_recv) = action.into_daemon_command(); + // start the daemon and give it the command + let fork_result = server::initialize_server(paths.clone(), Some(command), true)?; + let is_parent = fork_result == ForkResult::Parent; + if let (Some(recv), true) = (response_recv, is_parent) { + listen_for_daemon_response(recv); + } + is_parent + } + Err(err) => Err(err)?, + } + } }; if would_show_logs && opts.show_logs { client::handle_client_only_action(&paths, opts::ActionClientOnly::Logs)?; @@ -139,21 +153,22 @@ fn listen_for_daemon_response(mut recv: DaemonResponseReceiver) { }) } -fn handle_server_command(paths: &EwwPaths, action: &ActionWithServer, connect_attempts: usize) -> Result<()> { +/// attempt to send a command to the daemon and send it the given action repeatedly. +fn handle_server_command(paths: &EwwPaths, action: &ActionWithServer, connect_attempts: usize) -> Result> { log::debug!("Trying to find server process at socket {}", paths.get_ipc_socket_file().display()); let mut stream = attempt_connect(&paths.get_ipc_socket_file(), connect_attempts).context("Failed to connect to daemon")?; log::debug!("Connected to Eww server ({}).", &paths.get_ipc_socket_file().display()); - let response = client::do_server_call(&mut stream, action).context("Error while forwarding command to server")?; - if let Some(response) = response { - match response { - DaemonResponse::Success(x) => println!("{}", x), - DaemonResponse::Failure(x) => { - eprintln!("{}", x); - bail!("Error, server command failed"); - } + client::do_server_call(&mut stream, action).context("Error while forwarding command to server") +} + +fn handle_daemon_response(res: DaemonResponse) { + match res { + DaemonResponse::Success(x) => println!("{}", x), + DaemonResponse::Failure(x) => { + eprintln!("{}", x); + std::process::exit(1); } } - Ok(()) } fn attempt_connect(socket_path: impl AsRef, attempts: usize) -> Option { diff --git a/crates/eww/src/opts.rs b/crates/eww/src/opts.rs index 9b6f70d..b4e0061 100644 --- a/crates/eww/src/opts.rs +++ b/crates/eww/src/opts.rs @@ -161,6 +161,10 @@ pub enum ActionWithServer { /// and to provide additional context to the eww developers if you are filing a bug. #[structopt(name = "debug")] ShowDebug, + + /// Print out the scope graph structure in graphviz dot format. + #[structopt(name = "graph")] + ShowGraph, } impl Opt { @@ -227,6 +231,7 @@ impl ActionWithServer { return with_response_channel(|sender| app::DaemonCommand::GetVar { name, sender }) } ActionWithServer::ShowDebug => return with_response_channel(app::DaemonCommand::PrintDebug), + ActionWithServer::ShowGraph => return with_response_channel(app::DaemonCommand::PrintGraph), }; (command, None) } diff --git a/crates/eww/src/script_var_handler.rs b/crates/eww/src/script_var_handler.rs index ade0425..edcc0c1 100644 --- a/crates/eww/src/script_var_handler.rs +++ b/crates/eww/src/script_var_handler.rs @@ -43,6 +43,7 @@ pub fn init(evt_send: UnboundedSender) -> ScriptVarHandlerHandle } ScriptVarHandlerMsg::StopAll => { handler.stop_all(); + break; } }, else => break, diff --git a/crates/eww/src/server.rs b/crates/eww/src/server.rs index 4f79b77..6c3817f 100644 --- a/crates/eww/src/server.rs +++ b/crates/eww/src/server.rs @@ -1,17 +1,12 @@ use crate::{ app::{self, DaemonCommand}, - config, daemon_response, error_handling_ctx, - eww_state::*, - ipc_server, script_var_handler, util, EwwPaths, + config, daemon_response, error_handling_ctx, ipc_server, script_var_handler, + state::scope_graph::ScopeGraph, + util, EwwPaths, }; use anyhow::*; -use std::{ - collections::{HashMap, HashSet}, - os::unix::io::AsRawFd, - path::Path, - sync::{atomic::Ordering, Arc}, -}; +use std::{cell::RefCell, collections::{HashMap, HashSet}, os::unix::io::AsRawFd, path::Path, rc::Rc, sync::{atomic::Ordering, Arc}}; use tokio::sync::mpsc::*; pub fn initialize_server(paths: EwwPaths, action: Option, should_daemonize: bool) -> Result { @@ -61,8 +56,13 @@ pub fn initialize_server(paths: EwwPaths, action: Option, should_ log::debug!("Initializing script var handler"); let script_var_handler = script_var_handler::init(ui_send.clone()); + let (scope_graph_evt_send, mut scope_graph_evt_recv) = tokio::sync::mpsc::unbounded_channel(); + let mut app = app::App { - eww_state: EwwState::from_default_vars(eww_config.generate_initial_state()?), + scope_graph: Rc::new(RefCell::new(ScopeGraph::from_global_vars( + eww_config.generate_initial_state()?, + scope_graph_evt_send, + ))), eww_config, open_windows: HashMap::new(), failed_windows: HashSet::new(), @@ -88,8 +88,17 @@ pub fn initialize_server(paths: EwwPaths, action: Option, should_ if let Some(action) = action { app.handle_command(action); } - while let Some(event) = ui_recv.recv().await { - app.handle_command(event); + + loop { + tokio::select! { + Some(scope_graph_evt) = scope_graph_evt_recv.recv() => { + app.scope_graph.borrow_mut().handle_scope_graph_event(scope_graph_evt); + }, + Some(ui_event) = ui_recv.recv() => { + app.handle_command(ui_event); + } + else => break, + } } }); diff --git a/crates/eww/src/state/mod.rs b/crates/eww/src/state/mod.rs new file mode 100644 index 0000000..07e9d72 --- /dev/null +++ b/crates/eww/src/state/mod.rs @@ -0,0 +1,6 @@ +mod one_to_n_elements_map; +pub mod scope; +pub mod scope_graph; + +#[cfg(test)] +mod test; diff --git a/crates/eww/src/state/one_to_n_elements_map.rs b/crates/eww/src/state/one_to_n_elements_map.rs new file mode 100644 index 0000000..fc4d5a6 --- /dev/null +++ b/crates/eww/src/state/one_to_n_elements_map.rs @@ -0,0 +1,140 @@ +use anyhow::*; +use std::collections::{HashMap, HashSet}; + +/// A map that represents a structure of a 1-n relationship with edges that contain data. +#[derive(Debug)] +pub struct OneToNElementsMap { + pub(super) child_to_parent: HashMap, + pub(super) parent_to_children: HashMap>, +} + +impl OneToNElementsMap { + pub fn new() -> Self { + OneToNElementsMap { child_to_parent: HashMap::new(), parent_to_children: HashMap::new() } + } + + pub fn clear(&mut self) { + self.child_to_parent.clear(); + self.parent_to_children.clear() + } + + pub fn insert(&mut self, child: I, parent: I, edge: T) -> Result<()> { + if self.child_to_parent.contains_key(&child) { + bail!("this child already has a parent"); + } + self.child_to_parent.insert(child, (parent, edge)); + self.parent_to_children.entry(parent).or_default().insert(child); + Ok(()) + } + + pub fn remove(&mut self, scope: I) { + if let Some(children) = self.parent_to_children.remove(&scope) { + for child in &children { + self.child_to_parent.remove(child); + } + } + if let Some((parent, _)) = self.child_to_parent.remove(&scope) { + if let Some(children_of_parent) = self.parent_to_children.get_mut(&parent) { + children_of_parent.remove(&scope); + } + } + } + + pub fn get_parent_of(&self, index: I) -> Option { + self.child_to_parent.get(&index).map(|(parent, _)| *parent) + } + + pub fn get_parent_edge_of(&self, index: I) -> Option<&(I, T)> { + self.child_to_parent.get(&index) + } + + pub fn get_parent_edge_mut(&mut self, index: I) -> Option<&mut (I, T)> { + self.child_to_parent.get_mut(&index) + } + + #[allow(unused)] + pub fn get_children_of(&self, index: I) -> HashSet { + self.parent_to_children.get(&index).cloned().unwrap_or_default() + } + + /// Return the children and edges to those children of a given scope + pub fn get_children_edges_of(&self, index: I) -> Vec<(I, &T)> { + let mut result = Vec::new(); + if let Some(children) = self.parent_to_children.get(&index) { + for child_scope in children { + let (_, edge) = self.child_to_parent.get(child_scope).expect("OneToNElementsMap got into inconsistent state"); + result.push((*child_scope, edge)); + } + } + result + } + + pub fn validate(&self) -> Result<()> { + for (parent, children) in &self.parent_to_children { + for child in children { + if let Some((parent_2, _)) = self.child_to_parent.get(child) { + if parent_2 != parent { + bail!( + "parent_to_child stored mapping from {:?} to {:?}, but child_to_parent contained mapping to {:?} \ + instead", + parent, + child, + parent_2 + ); + } + } else { + bail!("parent_to_child stored mapping from {:?} to {:?}, which was not found in child_to_parent"); + } + } + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + pub fn test_add_scope() { + let mut map = OneToNElementsMap::new(); + map.insert(1, 2, "a".to_string()).unwrap(); + map.insert(2, 3, "b".to_string()).unwrap(); + map.insert(3, 4, "c".to_string()).unwrap(); + map.insert(5, 4, "d".to_string()).unwrap(); + + assert_eq!(map.get_parent_of(1), Some(2)); + assert_eq!(map.get_parent_of(2), Some(3)); + assert_eq!(map.get_parent_of(3), Some(4)); + assert_eq!(map.get_parent_of(4), None); + assert_eq!(map.get_parent_of(5), Some(4)); + + + assert_eq!(map.get_children_of(4), HashSet::from_iter(vec![3, 5])); + assert_eq!(map.get_children_of(3), HashSet::from_iter(vec![2])); + assert_eq!(map.get_children_of(2), HashSet::from_iter(vec![1])); + assert_eq!(map.get_children_of(1), HashSet::new()); + } + + #[test] + pub fn test_remove_scope() { + let mut map = OneToNElementsMap::new(); + map.insert(1, 2, "a".to_string()).unwrap(); + map.insert(2, 3, "b".to_string()).unwrap(); + map.insert(3, 4, "c".to_string()).unwrap(); + map.insert(5, 4, "d".to_string()).unwrap(); + + map.remove(4); + + assert_eq!(map.get_parent_of(1), Some(2)); + assert_eq!(map.get_parent_of(2), Some(3)); + assert_eq!(map.get_parent_of(3), None); + assert_eq!(map.get_parent_of(4), None); + assert_eq!(map.get_parent_of(5), None); + + assert_eq!(map.get_children_of(3), HashSet::from_iter(vec![2])); + assert_eq!(map.get_children_of(2), HashSet::from_iter(vec![1])); + assert_eq!(map.get_children_of(1), HashSet::new()); + + } +} diff --git a/crates/eww/src/state/scope.rs b/crates/eww/src/state/scope.rs new file mode 100644 index 0000000..5ed280d --- /dev/null +++ b/crates/eww/src/state/scope.rs @@ -0,0 +1,39 @@ +use anyhow::Result; +use std::{collections::HashMap, rc::Rc}; + +use eww_shared_util::VarName; +use simplexpr::dynval::DynVal; + +use super::scope_graph::{ScopeGraph, ScopeIndex}; + +#[derive(Debug)] +pub struct Scope { + pub name: String, + pub ancestor: Option, + pub data: HashMap, + /// The listeners that react to value changes in this scope. + /// **Note** that there might be VarNames referenced here that are not defined in this scope. + /// In those cases it is necessary to look into the scopes this scope is inheriting from. + pub listeners: HashMap>>, + pub node_index: ScopeIndex, +} + +impl Scope { + /// Initializes a scope **incompletely**. The [`Self::node_index`] is not set correctly, and needs to be + /// set to the index of the node in the scope graph that connects to this scope. + pub(super) fn new(name: String, created_by: Option, data: HashMap) -> Self { + Self { name, ancestor: created_by, data, listeners: HashMap::new(), node_index: ScopeIndex(0) } + } +} + +pub type ListenerFn = Box) -> Result<()>>; + +pub struct Listener { + pub needed_variables: Vec, + pub f: ListenerFn, +} +impl std::fmt::Debug for Listener { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Listener").field("needed_variables", &self.needed_variables).field("f", &"function").finish() + } +} diff --git a/crates/eww/src/state/scope_graph.rs b/crates/eww/src/state/scope_graph.rs new file mode 100644 index 0000000..310ec1e --- /dev/null +++ b/crates/eww/src/state/scope_graph.rs @@ -0,0 +1,724 @@ +use std::{ + collections::{HashMap, HashSet}, + rc::Rc, +}; + +use anyhow::*; +use eww_shared_util::{AttrName, VarName}; +use simplexpr::{dynval::DynVal, SimplExpr}; +use tokio::sync::mpsc::UnboundedSender; + +use crate::error_handling_ctx; + +use super::scope::{Listener, Scope}; + +#[derive(Hash, Eq, PartialEq, Copy, Clone)] +pub struct ScopeIndex(pub usize); + +impl std::fmt::Debug for ScopeIndex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ScopeIndex({})", self.0) + } +} +impl ScopeIndex { + fn advance(&mut self) { + self.0 += 1; + } +} + +pub enum ScopeGraphEvent { + RemoveScope(ScopeIndex), +} + +/// A graph structure of scopes where each scope may inherit from another scope, +/// and can provide attributes to arbitrarily many descendant scopes. +/// +/// ## Some terminology +/// **Subscope / Superscope**: Subscopes are scopes that _inherit_ from their superscope. +/// This means that they have access to all the variables defined in that scope as well. +/// The variables a subscope references from it's superscope are listed in the [`internal::Inherits`]. +/// In most cases, scopes inherit from the global scope. +/// +/// **Descendant / Ancestor**: Descendants of a scope are the scopes that are used +/// _within_ that ancestor scope. This means that a descendant scope's widgets will aways be +/// used as children of the ancestors widgets. +/// Any scope can have 0 or 1 ancestor, and any arbitrary amount of descendants. +/// An ancestor scope can provide attributes to it's descendants, which will be +/// listed in the respective [`internal::ProvidedAttr`]s. +/// +/// Invariants: +/// - every scope inherits from exactly 0 or 1 scopes. +/// - any scope may provide 0-n attributes to 0-n descendants. +/// - There must not be inheritance loops +/// - Inheritance is transitive - if a is subscope of b, and b is subscope of c, a has access to variables from c. +/// - In case of transitive inheritance, all steps need to explicitly store the referenced variables. This means that +/// if A is subscope of B, and B is subscope of C, and A references a variable "foo" from C, then this reference +/// needs to be stored in both the inheritance connection A -> B and B -> C +#[derive(Debug)] +pub struct ScopeGraph { + pub(self) graph: internal::ScopeGraphInternal, + pub root_index: ScopeIndex, + // TODO this should be factored out, it doesn't really belong into this module / struct. + pub event_sender: UnboundedSender, +} + +impl ScopeGraph { + pub fn from_global_vars(vars: HashMap, event_sender: UnboundedSender) -> Self { + let mut graph = internal::ScopeGraphInternal::new(); + let root_index = graph.add_scope(Scope { + name: "global".to_string(), + ancestor: None, + data: vars, + listeners: HashMap::new(), + node_index: ScopeIndex(0), + }); + if let Some(scope) = graph.scope_at_mut(root_index) { + scope.node_index = root_index; + } + Self { graph, root_index, event_sender } + } + + pub fn update_global_value(&mut self, var_name: &VarName, value: DynVal) -> Result<()> { + self.update_value(self.root_index, var_name, value) + } + + pub fn handle_scope_graph_event(&mut self, evt: ScopeGraphEvent) { + match evt { + ScopeGraphEvent::RemoveScope(scope_index) => { + self.remove_scope(scope_index); + } + } + } + + /// Fully reinitialize the scope graph. Completely removes all state, and resets the ScopeIndex uniqueness. + pub fn clear(&mut self, vars: HashMap) { + self.graph.clear(); + let root_index = self.graph.add_scope(Scope { + name: "global".to_string(), + ancestor: None, + data: vars, + listeners: HashMap::new(), + node_index: ScopeIndex(0), + }); + if let Some(scope) = self.graph.scope_at_mut(root_index) { + scope.node_index = root_index; + } + self.root_index = root_index; + } + + pub fn remove_scope(&mut self, scope_index: ScopeIndex) { + self.graph.remove_scope(scope_index); + } + + pub fn validate(&self) -> Result<()> { + self.graph.validate() + } + + pub fn visualize(&self) -> String { + self.graph.visualize() + } + + pub fn currently_used_globals(&self) -> HashSet { + self.variables_used_in_self_or_subscopes_of(self.root_index) + } + + pub fn currently_unused_globals(&self) -> HashSet { + let used_variables = self.currently_used_globals(); + let global_scope = self.graph.scope_at(self.root_index).expect("No root scope in graph"); + global_scope.data.keys().cloned().collect::>().difference(&used_variables).cloned().collect() + } + + pub fn scope_at(&self, index: ScopeIndex) -> Option<&Scope> { + self.graph.scope_at(index) + } + + /// Evaluate a [SimplExpr] in a given scope. This will return `Err` if any referenced variables + /// are not available in the scope. If evaluation fails for other reasons (bad types, etc) + /// this will print a warning and return an empty string instead. + pub fn evaluate_simplexpr_in_scope(&self, index: ScopeIndex, expr: &SimplExpr) -> Result { + let needed_vars = self.lookup_variables_in_scope(index, &expr.collect_var_refs())?; + // TODORW + // TODO allowing it to fail here is painfully ugly + match expr.eval(&needed_vars) { + Ok(value) => Ok(value), + Err(err) => { + error_handling_ctx::print_error(anyhow!(err)); + Ok(DynVal::from("")) + } + } + } + + /// Register a new scope in the graph. + /// This will look up and resolve variable references in attributes to set up the correct [`internal::ProvidedAttr`] relationships. + pub fn register_new_scope( + &mut self, + name: String, + superscope: Option, + calling_scope: ScopeIndex, + attributes: HashMap, + ) -> Result { + let mut scope_variables = HashMap::new(); + + // First get the current values. If nothing here fails, we know that everything is in scope. + for (attr_name, attr_value) in &attributes { + let current_value = self.evaluate_simplexpr_in_scope(calling_scope, attr_value)?; + scope_variables.insert(attr_name.clone().into(), current_value); + } + + // Now that we're sure that we have all of the values, we can make changes to the scopegraph without + // risking getting it into an inconsistent state by adding a scope that can't get fully instantiated + // and aborting that operation prematurely. + let new_scope = Scope::new(name, Some(calling_scope), scope_variables); + + let new_scope_index = self.graph.add_scope(new_scope); + if let Some(superscope) = superscope { + self.graph.add_inheritance_relation(new_scope_index, superscope); + } + if let Some(scope) = self.graph.scope_at_mut(new_scope_index) { + scope.node_index = new_scope_index; + } + + for (attr_name, expression) in attributes { + let expression_var_refs = expression.collect_var_refs(); + if !expression_var_refs.is_empty() { + self.graph.register_scope_provides_attr( + calling_scope, + new_scope_index, + internal::ProvidedAttr { attr_name, expression }, + ); + for used_variable in expression_var_refs { + self.register_scope_referencing_variable(calling_scope, used_variable)?; + } + } + } + + #[cfg(debug_assertions)] + self.validate()?; + + Ok(new_scope_index) + } + + /// Register a listener. This listener will get called when any of the required variables change. + /// If there are no required_variables in the listener, nothing gets registered, but the listener + /// gets called once. + /// This should be used to update the gtk widgets that are in a scope. + /// This also calls the listener initially. + pub fn register_listener(&mut self, scope_index: ScopeIndex, listener: Listener) -> Result<()> { + if listener.needed_variables.is_empty() { + if let Err(err) = (*listener.f)(self, HashMap::new()).context("Error while updating UI after state change") { + error_handling_ctx::print_error(err); + } + } else { + for required_var in &listener.needed_variables { + self.register_scope_referencing_variable(scope_index, required_var.clone())?; + } + let scope = self.graph.scope_at_mut(scope_index).context("Scope not in graph")?; + let listener = Rc::new(listener); + for required_var in &listener.needed_variables { + scope.listeners.entry(required_var.clone()).or_default().push(listener.clone()); + } + + let required_variables = self.lookup_variables_in_scope(scope_index, &listener.needed_variables)?; + if let Err(err) = (*listener.f)(self, required_variables).context("Error while updating UI after state change") { + error_handling_ctx::print_error(err); + } + + #[cfg(debug_assertions)] + self.validate()?; + } + + Ok(()) + } + + /// Register the fact that a scope is referencing a given variable. + /// If the scope contains the variable itself, this is a No-op. Otherwise, will add that reference to the inherited scope relation. + pub fn register_scope_referencing_variable(&mut self, scope_index: ScopeIndex, var_name: VarName) -> Result<()> { + if !self.graph.scope_at(scope_index).context("scope not in graph")?.data.contains_key(&var_name) { + let superscope = + self.graph.superscope_of(scope_index).with_context(|| format!("Variable {} not in scope", var_name))?; + self.graph.add_reference_to_inherits_edge(scope_index, var_name.clone())?; + self.register_scope_referencing_variable(superscope, var_name)?; + } + Ok(()) + } + + pub fn update_value(&mut self, original_scope_index: ScopeIndex, updated_var: &VarName, new_value: DynVal) -> Result<()> { + let scope_index = self + .find_scope_with_variable(original_scope_index, updated_var) + .with_context(|| format!("Variable {} not scope", updated_var))?; + + if let Some(entry) = self.graph.scope_at_mut(scope_index).and_then(|scope| scope.data.get_mut(updated_var)) { + *entry = new_value; + } + + self.notify_value_changed(scope_index, updated_var)?; + + #[cfg(debug_assertions)] + self.graph.validate()?; + + Ok(()) + } + + /// Notify a scope that a value has been changed. This triggers the listeners and notifies further subscopes scopes recursively. + pub fn notify_value_changed(&mut self, scope_index: ScopeIndex, updated_var: &VarName) -> Result<()> { + // Update scopes that reference the changed variable in their attribute expressions. + let edges: Vec<(ScopeIndex, internal::ProvidedAttr)> = + self.graph.scopes_getting_attr_using(scope_index, updated_var).into_iter().map(|(a, b)| (a, b.clone())).collect(); + for (referencing_scope, edge) in edges { + if let Err(err) = self.evaluate_simplexpr_in_scope(scope_index, &edge.expression).and_then(|updated_attr_value| { + self.update_value(referencing_scope, edge.attr_name.to_var_name_ref(), updated_attr_value) + }) { + error_handling_ctx::print_error(err); + } + } + + // Trigger the listeners from this scope + self.call_listeners_in_scope(scope_index, updated_var)?; + + // Now find subscopes that reference this variable + let affected_subscopes = self.graph.subscopes_referencing(scope_index, updated_var); + for affected_subscope in affected_subscopes { + self.notify_value_changed(affected_subscope, updated_var)?; + } + Ok(()) + } + + /// Call all of the listeners in a given `scope_index` that are affected by a change to the `updated_var`. + fn call_listeners_in_scope(&mut self, scope_index: ScopeIndex, updated_var: &VarName) -> Result<()> { + let scope = self.graph.scope_at(scope_index).context("Scope not in graph")?; + if let Some(triggered_listeners) = scope.listeners.get(updated_var) { + for listener in triggered_listeners.clone() { + let required_variables = self.lookup_variables_in_scope(scope_index, &listener.needed_variables)?; + if let Err(err) = (*listener.f)(self, required_variables).context("Error while updating UI after state change") { + error_handling_ctx::print_error(err); + } + } + } + Ok(()) + } + + /// Find the closest available scope that contains variable with the given name. + pub fn find_scope_with_variable(&self, index: ScopeIndex, var_name: &VarName) -> Option { + let scope = self.graph.scope_at(index)?; + if scope.data.contains_key(var_name) { + Some(index) + } else { + self.find_scope_with_variable(self.graph.superscope_of(index)?, var_name) + } + } + + /// Find the value of a variable in the closest available scope that contains a variable with that name. + pub fn lookup_variable_in_scope(&self, index: ScopeIndex, var_name: &VarName) -> Option<&DynVal> { + self.find_scope_with_variable(index, var_name) + .and_then(|scope| self.graph.scope_at(scope)) + .map(|x| x.data.get(var_name).unwrap()) + } + + /// Get all variables that are used in the given scope or in any descendants of that scope. + /// If called with an index not in the tree, will return an empty set of variables. + pub fn variables_used_in_self_or_subscopes_of(&self, index: ScopeIndex) -> HashSet { + if let Some(scope) = self.scope_at(index) { + let mut variables: HashSet = scope.listeners.keys().cloned().collect(); + for (_, provided_attrs) in self.graph.descendant_edges_of(index) { + for attr in provided_attrs { + variables.extend(attr.expression.collect_var_refs()); + } + } + for (_, edge) in self.graph.subscope_edges_of(index) { + variables.extend(edge.references.clone()); + } + + // get all the variables that the current scope references from it's superscope + if let Some((_, edge)) = self.graph.superscope_edge_of(index) { + variables.extend(edge.references.clone()) + } + + // look through all descendants of this scope + for (descendant, _) in self.graph.descendant_edges_of(index) { + let used_in_descendant = self.variables_used_in_self_or_subscopes_of(descendant); + + // only include those variables that are not shadowed by the descendant itself + let descendant_scope = self.scope_at(descendant).unwrap(); + variables.extend(used_in_descendant.difference(&descendant_scope.data.keys().cloned().collect()).cloned()); + } + + variables + } else { + HashSet::new() + } + } + + /// like [Self::lookup_variable_in_scope], but looks up a set of variables and stores them in a HashMap. + pub fn lookup_variables_in_scope(&self, scope_index: ScopeIndex, vars: &[VarName]) -> Result> { + vars.iter() + .map(|required_var_name| { + let value = self + .lookup_variable_in_scope(scope_index, required_var_name) + .with_context(|| format!("Variable {} neither in scope nor any superscope", required_var_name))?; + + Ok((required_var_name.clone(), value.clone())) + }) + .collect::>() + } +} + +mod internal { + use super::{super::one_to_n_elements_map::OneToNElementsMap, *}; + + /// a --provides attribute [`Self::attr_name`] calculated via [`Self::expression`] to--> b + #[derive(Debug, Eq, PartialEq, Clone)] + pub struct ProvidedAttr { + pub attr_name: AttrName, + pub expression: SimplExpr, + } + + /// a -- inherits scope of --> b + /// If a inherits from b, and references variable V, V may either be available in b or in scopes that b inherits from. + #[derive(Debug, Eq, PartialEq, Clone)] + pub struct Inherits { + /// The variable names the subscope references from the superscope + pub references: HashSet, + } + + /// The internal graph representation of the [`ScopeGraph`]. + /// Unlike the public ScopeGraph, this may temporarily be in an inconsistent state while changes are being made. + #[derive(Debug)] + pub struct ScopeGraphInternal { + last_index: ScopeIndex, + scopes: HashMap, + + /// Edges from ancestors to descendants + pub(super) hierarchy_relations: OneToNElementsMap>, + + /// Edges from superscopes to subscopes. + pub(super) inheritance_relations: OneToNElementsMap, + } + + impl ScopeGraphInternal { + pub fn new() -> Self { + Self { + last_index: ScopeIndex(0), + scopes: HashMap::new(), + inheritance_relations: OneToNElementsMap::new(), + hierarchy_relations: OneToNElementsMap::new(), + } + } + + pub fn clear(&mut self) { + self.scopes.clear(); + self.inheritance_relations.clear(); + self.hierarchy_relations.clear(); + } + + pub fn add_scope(&mut self, scope: Scope) -> ScopeIndex { + let idx = self.last_index; + if let Some(ancestor) = scope.ancestor { + let _ = self.hierarchy_relations.insert(idx, ancestor, Vec::new()); + } + self.scopes.insert(idx, scope); + self.last_index.advance(); + idx + } + + pub fn descendant_edges_of(&self, index: ScopeIndex) -> Vec<(ScopeIndex, &Vec)> { + self.hierarchy_relations.get_children_edges_of(index) + } + + pub fn subscope_edges_of(&self, index: ScopeIndex) -> Vec<(ScopeIndex, &Inherits)> { + self.inheritance_relations.get_children_edges_of(index) + } + + pub fn superscope_edge_of(&self, index: ScopeIndex) -> Option<&(ScopeIndex, Inherits)> { + self.inheritance_relations.get_parent_edge_of(index) + } + + pub fn remove_scope(&mut self, index: ScopeIndex) { + self.scopes.remove(&index); + if let Some(descendants) = self.hierarchy_relations.parent_to_children.get(&index).cloned() { + for descendant in descendants { + self.remove_scope(descendant); + } + } + self.hierarchy_relations.remove(index); + self.inheritance_relations.remove(index); + } + + pub fn add_inheritance_relation(&mut self, a: ScopeIndex, b: ScopeIndex) { + self.inheritance_relations.insert(a, b, Inherits { references: HashSet::new() }).unwrap(); + } + + /// Register that a given scope `a` provides an attribute to it's descendant `b`. + pub fn register_scope_provides_attr(&mut self, a: ScopeIndex, b: ScopeIndex, edge: ProvidedAttr) { + if let Some((superscope, edges)) = self.hierarchy_relations.get_parent_edge_mut(b) { + assert_eq!(*superscope, a, "Hierarchy map had a different superscope for a given scope than what was given here"); + edges.push(edge); + } else { + log::error!( + "Tried to register a provided attribute edge between two scopes that are not connected in the hierarchy map" + ); + } + } + + pub fn scope_at(&self, index: ScopeIndex) -> Option<&Scope> { + self.scopes.get(&index) + } + + pub fn scope_at_mut(&mut self, index: ScopeIndex) -> Option<&mut Scope> { + self.scopes.get_mut(&index) + } + + /// List all subscopes that reference a given variable directly (-> the variable is in the [Inherits::references]) + pub fn subscopes_referencing(&self, index: ScopeIndex, var_name: &VarName) -> Vec { + self.inheritance_relations + .get_children_edges_of(index) + .iter() + .filter(|(_, edge)| edge.references.contains(var_name)) + .map(|(scope, _)| *scope) + .collect() + } + + pub fn superscope_of(&self, index: ScopeIndex) -> Option { + self.inheritance_relations.get_parent_of(index) + } + + /// List the scopes that are provided some attribute referencing `var_name` by the given scope `index`. + pub fn scopes_getting_attr_using(&self, index: ScopeIndex, var_name: &VarName) -> Vec<(ScopeIndex, &ProvidedAttr)> { + let edge_mappings = self.hierarchy_relations.get_children_edges_of(index); + edge_mappings + .iter() + .flat_map(|(k, v)| v.iter().map(move |edge| (*k, edge))) + .filter(|(_, edge)| edge.expression.references_var(var_name)) + .collect() + } + + /// Register that a given scope references a variable from it's direct superscope. + /// If the given scope does not have a superscope, this will return an `Err`. + pub fn add_reference_to_inherits_edge(&mut self, subscope: ScopeIndex, var_name: VarName) -> Result<()> { + let (_, edge) = self + .inheritance_relations + .get_parent_edge_mut(subscope) + .with_context(|| format!("Given scope {:?} does not have any superscope", subscope))?; + edge.references.insert(var_name); + Ok(()) + } + + pub fn validate(&self) -> Result<()> { + for (child_scope, (parent_scope, _edge)) in &self.hierarchy_relations.child_to_parent { + if !self.scopes.contains_key(child_scope) { + bail!("hierarchy_relations lists key that is not in graph"); + } + if !self.scopes.contains_key(parent_scope) { + bail!("hierarchy_relations values lists scope that is not in graph"); + } + } + for (child_scope, (parent_scope_idx, edge)) in &self.inheritance_relations.child_to_parent { + if !self.scopes.contains_key(child_scope) { + bail!("inheritance_relations lists key that is not in graph"); + } + if let Some(parent_scope) = self.scopes.get(parent_scope_idx) { + // check that everything the scope references from it's parent is actually + // accessible by the parent, meaning it either stores it directly or + // inherits it itself + for var in &edge.references { + let parent_has_access_to_var = parent_scope.data.contains_key(var) + || self + .inheritance_relations + .child_to_parent + .get(parent_scope_idx) + .map_or(false, |(_, e)| e.references.contains(var)); + if !parent_has_access_to_var { + bail!("scope inherited variable that parent scope doesn't have access to"); + } + } + } else { + bail!("inheritance_relations values lists scope that is not in graph"); + } + } + + self.hierarchy_relations.validate()?; + self.inheritance_relations.validate()?; + + Ok(()) + } + + pub fn visualize(&self) -> String { + let mut output = String::new(); + output.push_str("digraph {\n"); + + for (scope_index, scope) in &self.scopes { + output.push_str(&format!( + " \"{:?}\"[label=\"{}\\n{}\"]\n", + scope_index, + scope.name, + format!( + "data: {:?}, listeners: {:?}", + scope.data.iter().filter(|(k, _v)| !k.0.starts_with("EWW")).collect::>(), + scope + .listeners + .iter() + .map(|(k, v)| format!( + "on {}: {:?}", + k.0, + v.iter() + .map(|l| format!("{:?}", l.needed_variables.iter().map(|x| x.0.clone()).collect::>())) + .collect::>() + )) + .collect::>() + ) + .replace("\"", "'") + )); + if let Some(created_by) = scope.ancestor { + output.push_str(&format!(" \"{:?}\" -> \"{:?}\"[label=\"ancestor\"]\n", created_by, scope_index)); + } + } + + for (child, (parent, edges)) in &self.hierarchy_relations.child_to_parent { + for edge in edges { + output.push_str(&format!( + " \"{:?}\" -> \"{:?}\" [color = \"red\", label = \"{}\"]\n", + parent, + child, + format!(":{} `{:?}`", edge.attr_name, edge.expression).replace("\"", "'") + )); + } + } + for (child, (parent, edge)) in &self.inheritance_relations.child_to_parent { + output.push_str(&format!( + " \"{:?}\" -> \"{:?}\" [color = \"blue\", label = \"{}\"]\n", + child, + parent, + format!("inherits({:?})", edge.references).replace("\"", "'") + )); + } + + output.push('}'); + output + } + } +} + +#[cfg(test)] +mod test { + use maplit::{hashmap, hashset}; + + use super::*; + + #[test] + fn test_nested_inheritance() { + let globals = hashmap! { + "global".into() => "hi".into(), + }; + + let (send, _recv) = tokio::sync::mpsc::unbounded_channel(); + + let mut scope_graph = ScopeGraph::from_global_vars(globals, send); + let root_scope = scope_graph.root_index; + + let widget1_scope = scope_graph.register_new_scope("1".into(), Some(root_scope), root_scope, hashmap! {}).unwrap(); + let widget2_scope = scope_graph.register_new_scope("2".into(), Some(widget1_scope), widget1_scope, hashmap! {}).unwrap(); + scope_graph.register_scope_referencing_variable(widget2_scope, "global".into()).unwrap(); + + let inheritance_child_to_parent = scope_graph.graph.inheritance_relations.child_to_parent; + assert!(inheritance_child_to_parent.get(&widget2_scope).unwrap().1.references.contains("global")); + assert!(inheritance_child_to_parent.get(&widget1_scope).unwrap().1.references.contains("global")); + } + + #[test] + fn test_lookup_variable_in_scope() { + let globals = hashmap! { + "global".into() => "hi".into(), + }; + + let (send, _recv) = tokio::sync::mpsc::unbounded_channel(); + + let mut scope_graph = ScopeGraph::from_global_vars(globals, send); + + let widget_1_scope = scope_graph + .register_new_scope("1".to_string(), Some(scope_graph.root_index), scope_graph.root_index, hashmap! {}) + .unwrap(); + let widget_2_scope = + scope_graph.register_new_scope("2".to_string(), Some(widget_1_scope), widget_1_scope, hashmap! {}).unwrap(); + let widget_no_parent_scope = scope_graph.register_new_scope("2".to_string(), None, widget_1_scope, hashmap! {}).unwrap(); + + scope_graph.register_scope_referencing_variable(widget_2_scope, "global".into()).unwrap(); + + assert_eq!(scope_graph.lookup_variable_in_scope(widget_2_scope, &"global".into()).unwrap(), &"hi".into()); + assert_eq!(scope_graph.lookup_variable_in_scope(widget_1_scope, &"global".into()).unwrap(), &"hi".into()); + assert_eq!(scope_graph.lookup_variable_in_scope(widget_no_parent_scope, &"global".into()), None); + } + + /// tests the following graph structure: + /// ``` + /// ┌───────────────────────────────────────────────────┐ + /// │ widget2 │ + /// │ data: [('shadowed_var', 'hi')] │ ──────────────────┐ + /// └───────────────────────────────────────────────────┘ │ + /// ▲ │ + /// │ ancestor │ + /// │ │ + /// ┌───────────────────────────────────────────────────┐ │ + /// │ window │ │ + /// ┌────────▶ │ data: [] │ ─┐ │ + /// │ └───────────────────────────────────────────────────┘ │ │ + /// │ │ │ │ + /// │ │ ancestor │ │ + /// │ ▼ │ │ + /// │ ┌───────────────────────────────────────────────────┐ │ │ + /// │ │ widget │ │ │ + /// │ ancestor │ data: [] │ │ inherits({}) │ + /// │ └───────────────────────────────────────────────────┘ │ │ + /// │ │ │ │ + /// │ │ inherits({'the_var'}) │ │ + /// │ ▼ │ │ + /// │ ┌───────────────────────────────────────────────────┐ │ │ + /// │ │ global │ │ │ + /// └───────── │ data: [('shadowed_var', 'hi'), ('the_var', 'hi')] │ ◀┘ │ + /// └───────────────────────────────────────────────────┘ │ + /// ▲ inherits({}) │ + /// └─────────────────────────────────────────────────────────────────────┘ + /// ``` + #[test] + fn test_variables_used_in_self_or_subscopes_of() { + let globals = hashmap! { + "the_var".into() => "hi".into(), + "shadowed_var".into() => "hi".into(), + }; + + let (send, _recv) = tokio::sync::mpsc::unbounded_channel(); + + let mut scope_graph = ScopeGraph::from_global_vars(globals, send); + + let window_scope = scope_graph + .register_new_scope("window".to_string(), Some(scope_graph.root_index), scope_graph.root_index, hashmap! {}) + .unwrap(); + let widget_scope = scope_graph + .register_new_scope("widget".to_string(), Some(scope_graph.root_index), window_scope, hashmap! {}) + .unwrap(); + let _widget_with_local_var_scope = scope_graph + .register_new_scope( + "widget2".to_string(), + Some(scope_graph.root_index), + window_scope, + hashmap! { "shadowed_var".into() => SimplExpr::synth_literal("hi") }, + ) + .unwrap(); + + scope_graph.register_scope_referencing_variable(widget_scope, "the_var".into()).unwrap(); + + assert_eq!( + scope_graph.variables_used_in_self_or_subscopes_of(scope_graph.root_index), + hashset!["the_var".into()], + "Wrong variables assumed to be used by global" + ); + assert_eq!( + scope_graph.variables_used_in_self_or_subscopes_of(window_scope), + hashset!["the_var".into()], + "Wrong variables assumed to be used by window" + ); + assert_eq!( + scope_graph.variables_used_in_self_or_subscopes_of(widget_scope), + hashset!["the_var".into()], + "Wrong variables assumed to be used by widget" + ); + } +} diff --git a/crates/eww/src/state/test.rs b/crates/eww/src/state/test.rs new file mode 100644 index 0000000..a23b847 --- /dev/null +++ b/crates/eww/src/state/test.rs @@ -0,0 +1,158 @@ +use super::scope::Listener; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; + +use eww_shared_util::{Span, VarName}; +use maplit::hashmap; +use simplexpr::{dynval::DynVal, SimplExpr}; + +use crate::state::scope_graph::ScopeGraph; + +pub fn create_fn_verificator() -> (Arc, Box) { + let check = Arc::new(AtomicBool::new(false)); + let check_moved = check.clone(); + let f = Box::new(move || check_moved.store(true, Ordering::Relaxed)); + (check, f) +} + +#[allow(unused)] +macro_rules! make_listener { + (|$($varname:expr => $name:ident),*| $body:block) => { + Listener { + needed_variables: vec![$($varname),*], + f: Box::new(move |_, values| { + $( + let $name = values.get(&$varname).unwrap(); + )* + $body + Ok(()) + }) + } + }; + (@short |$($varname:ident),*| $body:block) => { + make_listener!(|$(VarName(stringify!($varname).to_string()) => $varname),*| $body) + } +} + +#[test] +pub fn test_delete_scope() { + let globals = hashmap! { + VarName("global_1".to_string()) => DynVal::from("hi"), + }; + + let (send, _recv) = tokio::sync::mpsc::unbounded_channel(); + + let mut scope_graph = ScopeGraph::from_global_vars(globals, send); + + let widget_foo_scope = scope_graph + .register_new_scope( + "foo".to_string(), + Some(scope_graph.root_index), + scope_graph.root_index, + hashmap! { + "arg_1".into() => SimplExpr::var_ref(Span::DUMMY, "global_1"), + }, + ) + .unwrap(); + let widget_bar_scope = scope_graph + .register_new_scope( + "bar".to_string(), + Some(scope_graph.root_index), + widget_foo_scope, + hashmap! { + "arg_3".into() => SimplExpr::var_ref(Span::DUMMY, "arg_1"), + }, + ) + .unwrap(); + + scope_graph.validate().unwrap(); + + scope_graph.remove_scope(widget_bar_scope); + scope_graph.validate().unwrap(); + assert!(scope_graph.scope_at(widget_bar_scope).is_none()); +} + +#[test] +fn test_state_updates() { + let globals = hashmap! { + "global_1".into() => DynVal::from("hi"), + "global_2".into() => DynVal::from("hey"), + }; + + let (send, _recv) = tokio::sync::mpsc::unbounded_channel(); + + let mut scope_graph = ScopeGraph::from_global_vars(globals, send); + + let widget_foo_scope = scope_graph + .register_new_scope( + "foo".to_string(), + Some(scope_graph.root_index), + scope_graph.root_index, + hashmap! { + "arg_1".into() => SimplExpr::var_ref(Span::DUMMY, "global_1"), + "arg_2".into() => SimplExpr::synth_string("static value"), + }, + ) + .unwrap(); + let widget_bar_scope = scope_graph + .register_new_scope( + "bar".to_string(), + Some(scope_graph.root_index), + widget_foo_scope, + hashmap! { + "arg_3".into() => SimplExpr::Concat(Span::DUMMY, vec![ + SimplExpr::var_ref(Span::DUMMY, "arg_1"), + SimplExpr::synth_literal("static_value"), + ]) + }, + ) + .unwrap(); + + let (foo_verify, foo_f) = create_fn_verificator(); + + scope_graph + .register_listener( + widget_foo_scope, + make_listener!(@short |arg_1| { + println!("foo: arg_1 changed to {}", arg_1); + if arg_1 == &"pog".into() { + foo_f() + } + }), + ) + .unwrap(); + let (bar_verify, bar_f) = create_fn_verificator(); + scope_graph + .register_listener( + widget_bar_scope, + make_listener!(@short |arg_3| { + println!("bar: arg_3 changed to {}", arg_3); + if arg_3 == &"pogstatic_value".into() { + bar_f() + } + }), + ) + .unwrap(); + + let (bar_2_verify, bar_2_f) = create_fn_verificator(); + scope_graph + .register_listener( + widget_bar_scope, + make_listener!(@short |global_2| { + println!("bar: global_2 changed to {}", global_2); + if global_2 == &"new global 2".into() { + bar_2_f() + } + }), + ) + .unwrap(); + + scope_graph.update_value(scope_graph.root_index, &"global_1".into(), "pog".into()).unwrap(); + assert!(foo_verify.load(Ordering::Relaxed), "update in foo did not trigger properly"); + assert!(bar_verify.load(Ordering::Relaxed), "update in bar did not trigger properly"); + + scope_graph.update_value(scope_graph.root_index, &"global_2".into(), "new global 2".into()).unwrap(); + assert!(bar_2_verify.load(Ordering::Relaxed), "inherited global update did not trigger properly"); +} diff --git a/crates/eww/src/widgets/build_widget.rs b/crates/eww/src/widgets/build_widget.rs new file mode 100644 index 0000000..39c7e7f --- /dev/null +++ b/crates/eww/src/widgets/build_widget.rs @@ -0,0 +1,263 @@ +use anyhow::*; +use codespan_reporting::diagnostic::Severity; +use eww_shared_util::AttrName; +use gdk::prelude::Cast; +use gtk::{ + prelude::{BoxExt, ContainerExt, WidgetExt, WidgetExtManual}, + Orientation, +}; +use itertools::Itertools; +use simplexpr::SimplExpr; +use std::{collections::HashMap, rc::Rc}; +use yuck::{ + config::{widget_definition::WidgetDefinition, widget_use::WidgetUse}, + gen_diagnostic, +}; + +use crate::{ + error::DiagError, + error_handling_ctx, + state::{ + scope::Listener, + scope_graph::{ScopeGraph, ScopeGraphEvent, ScopeIndex}, + }, + widgets::widget_definitions, +}; + +use super::widget_definitions::{resolve_orientable_attrs, resolve_range_attrs, resolve_widget_attrs}; + +pub struct BuilderArgs<'a> { + pub calling_scope: ScopeIndex, + pub widget_use: WidgetUse, + pub scope_graph: &'a mut ScopeGraph, + pub unhandled_attrs: Vec, + pub widget_defs: Rc>, + pub custom_widget_invocation: Option>, +} + +// TODO in case of custom widgets, we should add a validation step where +// warnings for unknown attributes (attributes not expected by the widget) are emitted. + +/// Build a [`gtk::Widget`] out of a [`WidgetUse`]. +/// This will set up scopes in the [`ScopeGraph`], register all the listeners there, +/// and recursively generate all the widgets and child widgets. +pub fn build_gtk_widget( + graph: &mut ScopeGraph, + widget_defs: Rc>, + calling_scope: ScopeIndex, + mut widget_use: WidgetUse, + custom_widget_invocation: Option>, +) -> Result { + if let Some(custom_widget) = widget_defs.clone().get(&widget_use.name) { + let widget_use_attributes = custom_widget + .expected_args + .iter() + .map(|spec| { + let expr = if spec.optional { + widget_use + .attrs + .ast_optional::(&spec.name.0)? + .unwrap_or_else(|| SimplExpr::literal(spec.span, "".to_string())) + } else { + widget_use.attrs.ast_required::(&spec.name.0)? + }; + Ok((spec.name.clone(), expr)) + }) + .collect::>>()?; + + let root_index = graph.root_index; + let new_scope_index = + graph.register_new_scope(widget_use.name, Some(root_index), calling_scope, widget_use_attributes)?; + + let gtk_widget = build_gtk_widget( + graph, + widget_defs, + new_scope_index, + custom_widget.widget.clone(), + Some(Rc::new(CustomWidgetInvocation { scope: calling_scope, children: widget_use.children })), + )?; + + let scope_graph_sender = graph.event_sender.clone(); + + gtk_widget.connect_destroy(move |_| { + let _ = scope_graph_sender.send(ScopeGraphEvent::RemoveScope(new_scope_index)); + }); + Ok(gtk_widget) + } else { + build_builtin_gtk_widget(graph, widget_defs, calling_scope, widget_use, custom_widget_invocation) + } +} + +/// 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`]. +/// +/// Also registers all the necessary scopes in the scope graph +/// +/// This may return `Err` in case there was an actual error while parsing or +/// resolving the widget, Or `Ok(None)` if the widget_use just didn't match any +/// widget name. +fn build_builtin_gtk_widget( + graph: &mut ScopeGraph, + widget_defs: Rc>, + calling_scope: ScopeIndex, + widget_use: WidgetUse, + custom_widget_invocation: Option>, +) -> Result { + let mut bargs = BuilderArgs { + unhandled_attrs: widget_use.attrs.attrs.keys().cloned().collect(), + scope_graph: graph, + calling_scope, + widget_use, + widget_defs, + custom_widget_invocation, + }; + let gtk_widget = widget_definitions::widget_use_to_gtk_widget(&mut bargs)?; + + if let Some(gtk_container) = gtk_widget.dynamic_cast_ref::() { + validate_container_children_count(gtk_container, &bargs.widget_use)?; + + // Only populate children if there haven't been any children added anywhere else + // TODO this is somewhat hacky + if gtk_container.children().is_empty() { + populate_widget_children( + bargs.scope_graph, + bargs.widget_defs.clone(), + calling_scope, + gtk_container, + bargs.widget_use.children.clone(), + bargs.custom_widget_invocation.clone(), + )?; + } + } + + if let Some(w) = gtk_widget.dynamic_cast_ref() { + resolve_range_attrs(&mut bargs, w)?; + } + if let Some(w) = gtk_widget.dynamic_cast_ref() { + resolve_orientable_attrs(&mut bargs, w)?; + }; + resolve_widget_attrs(&mut bargs, >k_widget)?; + + if !bargs.unhandled_attrs.is_empty() { + let diag = error_handling_ctx::stringify_diagnostic(gen_diagnostic! { + kind = Severity::Warning, + msg = format!("Unknown attributes {}", bargs.unhandled_attrs.iter().map(|x| x.to_string()).join(", ")), + label = bargs.widget_use.span => "Found in here" + })?; + eprintln!("{}", diag); + } + Ok(gtk_widget) +} + +/// If a gtk widget can take children (→ it is a [`gtk::Container`]) we need to add the provided `widget_use_children` +/// into that container. Those children might be uses of the special `children`-[`WidgetUse`], which will get expanded here, too. +fn populate_widget_children( + tree: &mut ScopeGraph, + widget_defs: Rc>, + calling_scope: ScopeIndex, + gtk_container: >k::Container, + widget_use_children: Vec, + custom_widget_invocation: Option>, +) -> Result<()> { + for child in widget_use_children { + if child.name == "children" { + let custom_widget_invocation = custom_widget_invocation.clone().context("Not in a custom widget invocation")?; + build_children_special_widget( + tree, + widget_defs.clone(), + calling_scope, + child, + gtk_container, + custom_widget_invocation, + )?; + } else { + let child_widget = + build_gtk_widget(tree, widget_defs.clone(), calling_scope, child, custom_widget_invocation.clone())?; + gtk_container.add(&child_widget); + } + } + Ok(()) +} + +/// Handle an invocation of the special `children` [`WidgetUse`]. +/// This widget expands to multiple other widgets, thus we require the `gtk_container` we should expand the widgets into. +/// The `custom_widget_invocation` will be used here to evaluate the provided children in their +/// original scope and expand them into the given container. +fn build_children_special_widget( + tree: &mut ScopeGraph, + widget_defs: Rc>, + calling_scope: ScopeIndex, + mut widget_use: WidgetUse, + gtk_container: >k::Container, + custom_widget_invocation: Rc, +) -> Result<()> { + assert_eq!(&widget_use.name, "children"); + + if let Some(nth) = widget_use.attrs.ast_optional::("nth")? { + // This should be a custom gtk::Bin subclass,.. + let child_container = gtk::Box::new(Orientation::Horizontal, 0); + child_container.set_homogeneous(true); + gtk_container.add(&child_container); + + tree.register_listener( + calling_scope, + Listener { + needed_variables: nth.collect_var_refs(), + f: Box::new({ + move |tree, values| { + let nth_value = nth.eval(&values)?.as_i32()?; + let nth_child_widget_use = custom_widget_invocation + .children + .get(nth_value as usize) + .with_context(|| format!("No child at index {}", nth_value))?; + let new_child_widget = build_gtk_widget( + tree, + widget_defs.clone(), + custom_widget_invocation.scope, + nth_child_widget_use.clone(), + None, + )?; + for old_child in child_container.children() { + unsafe { + old_child.destroy(); + } + } + child_container.set_child(Some(&new_child_widget)); + new_child_widget.show(); + Ok(()) + } + }), + }, + )?; + } else { + for child in &custom_widget_invocation.children { + let child_widget = build_gtk_widget(tree, widget_defs.clone(), custom_widget_invocation.scope, child.clone(), None)?; + gtk_container.add(&child_widget); + } + } + Ok(()) +} + +/// When a custom widget gets used, some context about that invocation needs to be +/// remembered whilst building it's content. If the body of the custom widget uses a `children` +/// widget, the children originally passed to the widget need to be set. +/// This struct represents that context +pub struct CustomWidgetInvocation { + /// The scope the custom widget was invoked in + scope: ScopeIndex, + /// The children the custom widget was given. These should be evaluated in [`Self::scope`] + children: Vec, +} + +/// Make sure that [`gtk::Bin`] widgets only get a single child. +fn validate_container_children_count(container: >k::Container, widget_use: &WidgetUse) -> Result<(), DiagError> { + if container.dynamic_cast_ref::().is_some() && widget_use.children.len() > 1 { + Err(DiagError::new(gen_diagnostic! { + kind = Severity::Error, + msg = format!("{} can only have one child", widget_use.name), + label = widget_use.children_span() => format!("Was given {} children here", widget_use.children.len()) + })) + } else { + Ok(()) + } +} diff --git a/crates/eww/src/widgets/circular_progressbar.rs b/crates/eww/src/widgets/circular_progressbar.rs index 3b2eac1..28682ef 100644 --- a/crates/eww/src/widgets/circular_progressbar.rs +++ b/crates/eww/src/widgets/circular_progressbar.rs @@ -228,7 +228,7 @@ impl WidgetImpl for CircProgPriv { cr.clip(); // Children widget - widget.propagate_draw(child, &cr); + widget.propagate_draw(child, cr); cr.reset_clip(); cr.restore()?; diff --git a/crates/eww/src/widgets/def_widget_macro.rs b/crates/eww/src/widgets/def_widget_macro.rs new file mode 100644 index 0000000..14beee3 --- /dev/null +++ b/crates/eww/src/widgets/def_widget_macro.rs @@ -0,0 +1,98 @@ +#[macro_export] +macro_rules! def_widget { + ($args:ident, $scope_graph:ident, $gtk_widget:ident, { + $( + prop($( + $attr_name:ident : $typecast_func:ident $(? $(@ $optional:tt @)?)? $(= $default:expr)? + ),*) $code:block + ),+ $(,)? + }) => { + $({ + $( + $args.unhandled_attrs.retain(|a| &a.0 != &::std::stringify!($attr_name).replace('_', "-")); + )* + + // Map of all attributes to their provided expressions. + // 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 { + ::maplit::hashmap! { + $( + eww_shared_util::AttrName(::std::stringify!($attr_name).to_owned()) => + def_widget!(@get_value $args, &::std::stringify!($attr_name).replace('_', "-"), $(? $($optional)?)? $(= $default)?) + ),* + } + }; + + // Only proceed if any attributes from this `prop` where actually provided + if let Ok(attr_map) = attr_map { + if attr_map.values().any(|x| x.is_some()) { + + // 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()) + .collect(); + + $args.scope_graph.register_listener( + $args.calling_scope, + 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| { + let $gtk_widget = gdk::glib::clone::Upgrade::upgrade(&$gtk_widget).unwrap(); + // values is a map of all the variables that are required to evaluate the + // attributes expression. + + + // We first initialize all the local variables for all the expected attributes in scope + $( + // get the simplexprs from the attr_map + 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 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)?)?); + )* + + // And then run the provided code with those attributes in scope. + $code + Ok(()) + } + }), + }, + )?; + } + } + })+ + }; + + (@unwrap_if_required $value:ident ?) => { }; + (@unwrap_if_required $value:ident) => { + let $value = $value.unwrap(); + }; + + // 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() + }; + + // 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))) + }; + + // 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()) + } +} diff --git a/crates/eww/src/widgets/mod.rs b/crates/eww/src/widgets/mod.rs index bfa1c6a..ec54fe6 100644 --- a/crates/eww/src/widgets/mod.rs +++ b/crates/eww/src/widgets/mod.rs @@ -1,23 +1,14 @@ -use crate::{error_handling_ctx, eww_state::*}; -use anyhow::*; -use codespan_reporting::diagnostic::Severity; -use eww_shared_util::AttrName; -use gtk::prelude::*; -use itertools::Itertools; -use std::collections::HashMap; -use yuck::{config::widget_definition::WidgetDefinition, gen_diagnostic}; - use std::process::Command; -use widget_definitions::*; +pub mod build_widget; pub mod circular_progressbar; pub mod widget_definitions; -pub mod widget_node; +pub mod def_widget_macro; const CMD_STRING_PLACEHODLER: &str = "{}"; /// Run a command that was provided as an attribute. This command may use a -/// placeholder ('{}') which will be replaced by the value provided as [`arg`] +/// placeholder ('{}') which will be replaced by the value provided as `arg` pub(self) fn run_command(timeout: std::time::Duration, cmd: &str, arg: T) { use wait_timeout::ChildExt; let cmd = cmd.to_string(); @@ -40,128 +31,3 @@ pub(self) fn run_command(timeout: } }); } - -struct BuilderArgs<'a, 'b, 'c, 'd, 'e> { - eww_state: &'a mut EwwState, - widget: &'b widget_node::Generic, - unhandled_attrs: Vec<&'c AttrName>, - window_name: &'d str, - widget_definitions: &'e HashMap, -} - -/// build a [`gtk::Widget`] out of a [`element::WidgetUse`] that uses a -/// **builtin widget**. User defined widgets are handled by -/// [widget_use_to_gtk_widget]. -/// -/// Also registers all the necessary handlers in the `eww_state`. -/// -/// This may return `Err` in case there was an actual error while parsing or -/// resolving the widget, Or `Ok(None)` if the widget_use just didn't match any -/// widget name. -fn build_builtin_gtk_widget( - eww_state: &mut EwwState, - window_name: &str, - widget_definitions: &HashMap, - widget: &widget_node::Generic, -) -> Result> { - let mut bargs = - BuilderArgs { eww_state, widget, window_name, unhandled_attrs: widget.attrs.keys().collect(), widget_definitions }; - let gtk_widget = widget_to_gtk_widget(&mut bargs)?; - - // run resolve functions for superclasses such as range, orientable, and widget - - if let Some(gtk_widget) = gtk_widget.dynamic_cast_ref::() { - resolve_container_attrs(&mut bargs, gtk_widget); - if gtk_widget.children().is_empty() { - for child in &widget.children { - let child_widget = child.render(bargs.eww_state, window_name, widget_definitions).with_context(|| { - format!( - "{}error while building child '{:#?}' of '{}'", - format!("{} | ", widget.span), - &child, - >k_widget.widget_name() - ) - })?; - gtk_widget.add(&child_widget); - child_widget.show(); - } - } - } - - if let Some(w) = gtk_widget.dynamic_cast_ref() { - resolve_range_attrs(&mut bargs, w) - } - if let Some(w) = gtk_widget.dynamic_cast_ref() { - resolve_orientable_attrs(&mut bargs, w) - }; - resolve_widget_attrs(&mut bargs, >k_widget); - - if !bargs.unhandled_attrs.is_empty() { - let diag = error_handling_ctx::stringify_diagnostic(gen_diagnostic! { - kind = Severity::Warning, - msg = format!("Unknown attributes {}", bargs.unhandled_attrs.iter().map(|x| x.to_string()).join(", ")), - label = widget.span => "Found in here" - })?; - eprintln!("{}", diag); - } - - Ok(Some(gtk_widget)) -} - -#[macro_export] -macro_rules! resolve_block { - ($args:ident, $gtk_widget:ident, { - $( - prop( $( $attr_name:ident : $typecast_func:ident $(= $default:expr)?),*) $code:block - ),+ $(,)? - }) => { - $({ - $( - $args.unhandled_attrs.retain(|a| &a.0 != &::std::stringify!($attr_name).replace('_', "-")); - )* - - let attr_map: Result<_> = try { - ::maplit::hashmap! { - $( - eww_shared_util::AttrName(::std::stringify!($attr_name).to_owned()) => - resolve_block!(@get_value $args, &::std::stringify!($attr_name).replace('_', "-"), $(= $default)?) - ),* - } - }; - if let Ok(attr_map) = attr_map { - $args.eww_state.resolve( - $args.window_name, - attr_map, - { - let $gtk_widget = $gtk_widget.clone(); - move |attrs| { - $( - let $attr_name = attrs.get( ::std::stringify!($attr_name) ).context("something went terribly wrong....")?.$typecast_func()?; - )* - $code - Ok(()) - } - } - ); - } - })+ - }; - - (@get_value $args:ident, $name:expr, = $default:expr) => { - $args.widget.get_attr($name).cloned().unwrap_or(simplexpr::SimplExpr::synth_literal($default)) - }; - - (@get_value $args:ident, $name:expr,) => { - $args.widget.get_attr($name)?.clone() - } -} - -#[allow(unused)] -macro_rules! log_errors { - ($body:expr) => {{ - let result = try { $body }; - if let Err(e) = result { - log::warn!("{}", e); - } - }}; -} diff --git a/crates/eww/src/widgets/widget_definitions.rs b/crates/eww/src/widgets/widget_definitions.rs index 814c1ea..f4189bc 100644 --- a/crates/eww/src/widgets/widget_definitions.rs +++ b/crates/eww/src/widgets/widget_definitions.rs @@ -1,14 +1,16 @@ #![allow(clippy::option_map_unit_fn)] -use super::{circular_progressbar::*, run_command, BuilderArgs}; +use super::{build_widget::BuilderArgs, circular_progressbar::*, run_command}; use crate::{ - enum_parse, error::DiagError, error_handling_ctx, eww_state, resolve_block, util::list_difference, widgets::widget_node, + def_widget, enum_parse, error::DiagError, error_handling_ctx, util::list_difference, widgets::build_widget::build_gtk_widget, }; use anyhow::*; use codespan_reporting::diagnostic::Severity; +use eww_shared_util::Spanned; use gdk::NotifyType; use gtk::{self, glib, prelude::*}; use itertools::Itertools; use once_cell::sync::Lazy; + use std::{ cell::RefCell, cmp::Ordering, @@ -18,20 +20,30 @@ use std::{ }; use yuck::{ config::validate::ValidationError, - error::{AstError, AstResult, AstResultExt}, + error::{AstError, AstResult}, gen_diagnostic, parser::from_ast::FromAst, }; -type EventHandlerId = Rc>>; +/// Connect a gtk signal handler inside of this macro to ensure that when the same code gets run multiple times, +/// the previously connected singal handler first gets disconnected. +macro_rules! connect_single_handler { + ($widget:ident, $connect_expr:expr) => {{ + static ID: Lazy>> = Lazy::new(|| std::sync::Mutex::new(None)); + let old = ID.lock().unwrap().replace($connect_expr); + if let Some(old) = old { + $widget.disconnect(old); + } + }}; +} // TODO figure out how to // TODO https://developer.gnome.org/gtk3/stable/GtkFixed.html //// widget definitions -pub(super) fn widget_to_gtk_widget(bargs: &mut BuilderArgs) -> Result { - let gtk_widget = match bargs.widget.name.as_str() { +pub(super) fn widget_use_to_gtk_widget(bargs: &mut BuilderArgs) -> Result { + let gtk_widget = match bargs.widget_use.name.as_str() { "box" => build_gtk_box(bargs)?.upcast(), "centerbox" => build_center_box(bargs)?.upcast(), "eventbox" => build_gtk_event_box(bargs)?.upcast(), @@ -50,11 +62,10 @@ pub(super) fn widget_to_gtk_widget(bargs: &mut BuilderArgs) -> Result build_gtk_combo_box_text(bargs)?.upcast(), "checkbox" => build_gtk_checkbox(bargs)?.upcast(), "revealer" => build_gtk_revealer(bargs)?.upcast(), - "if-else" => build_if_else(bargs)?.upcast(), _ => { return Err(AstError::ValidationError(ValidationError::UnknownWidget( - bargs.widget.name_span, - bargs.widget.name.to_string(), + bargs.widget_use.name_span, + bargs.widget_use.name.to_string(), )) .into()) } @@ -69,14 +80,14 @@ static DEPRECATED_ATTRS: Lazy> = /// attributes that apply to all widgets /// @widget widget /// @desc these properties apply to _all_ widgets, and can be used anywhere! -pub(super) fn resolve_widget_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Widget) { +pub(super) fn resolve_widget_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Widget) -> Result<()> { let deprecated: HashSet<_> = DEPRECATED_ATTRS.to_owned(); let contained_deprecated: Vec<_> = bargs.unhandled_attrs.drain_filter(|a| deprecated.contains(&a.0 as &str)).collect(); if !contained_deprecated.is_empty() { let diag = error_handling_ctx::stringify_diagnostic(gen_diagnostic! { kind = Severity::Error, msg = "Unsupported attributes provided", - label = bargs.widget.span => "Found in here", + label = bargs.widget_use.span => "Found in here", note = format!("The attribute(s) ({}) has/have been removed, as GTK does not support it consistently. Instead, use eventbox to wrap this widget and set the attribute there. See #251 (https://github.com/elkowar/eww/issues/251) for more details.", contained_deprecated.iter().join(", ")), }).unwrap(); eprintln!("{}", diag); @@ -84,19 +95,24 @@ pub(super) fn resolve_widget_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Wi let css_provider = gtk::CssProvider::new(); - if let Ok(visible) = - bargs.widget.get_attr("visible").and_then(|v| bargs.eww_state.resolve_once(v)?.as_bool().map_err(|e| anyhow!(e))) - { - connect_first_map(gtk_widget, move |w| { - if visible { - w.show(); - } else { - w.hide(); - } - }) + let visible_result: Result<_> = try { + let visible_expr = bargs.widget_use.attrs.attrs.get("visible").map(|x| x.value.as_simplexpr()).transpose()?; + if let Some(visible_expr) = visible_expr { + let visible = bargs.scope_graph.evaluate_simplexpr_in_scope(bargs.calling_scope, &visible_expr)?.as_bool()?; + connect_first_map(gtk_widget, move |w| { + if visible { + w.show(); + } else { + w.hide(); + } + }); + } + }; + if let Err(err) = visible_result { + error_handling_ctx::print_error(err); } - resolve_block!(bargs, gtk_widget, { + def_widget!(bargs, _g, gtk_widget, { // @prop class - css class name prop(class: as_string) { let old_classes = gtk_widget.style_context().list_classes(); @@ -119,9 +135,13 @@ pub(super) fn resolve_widget_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Wi // @prop hexpand - should this widget expand horizontally. Default: false. prop(hexpand: as_bool = false) { gtk_widget.set_hexpand(hexpand) }, // @prop width - width of this element. note that this can not restrict the size if the contents stretch it - prop(width: as_f64) { gtk_widget.set_size_request(width as i32, gtk_widget.allocated_height()) }, // @prop height - height of this element. note that this can not restrict the size if the contents stretch it - prop(height: as_f64) { gtk_widget.set_size_request(gtk_widget.allocated_width(), height as i32) }, + prop(width: as_i32?, height: as_i32?) { + gtk_widget.set_size_request( + width.unwrap_or_else(|| gtk_widget.allocated_width()), + height.unwrap_or_else(|| gtk_widget.allocated_height()) + ); + }, // @prop active - If this widget can be interacted with prop(active: as_bool = true) { gtk_widget.set_sensitive(active) }, // @prop tooltip - tooltip text (on hover) @@ -130,7 +150,6 @@ pub(super) fn resolve_widget_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Wi }, // @prop visible - visibility of the widget prop(visible: as_bool = true) { - // TODO how do i call this only after the widget has been mapped? this is actually an issue,.... if visible { gtk_widget.show(); } else { gtk_widget.hide(); } }, // @prop style - inline css style applied to the widget @@ -140,16 +159,11 @@ pub(super) fn resolve_widget_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Wi gtk_widget.style_context().add_provider(&css_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION) }, }); -} - -/// @widget !container -pub(super) fn resolve_container_attrs(_bargs: &mut BuilderArgs, _gtk_widget: >k::Container) { - // resolve_block!(bargs, gtk_widget, {}); + Ok(()) } /// @widget !range -pub(super) fn resolve_range_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Range) { - let on_change_handler_id: EventHandlerId = Rc::new(RefCell::new(None)); +pub(super) fn resolve_range_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Range) -> Result<()> { gtk_widget.set_sensitive(false); // only allow changing the value via the value property if the user isn't currently dragging @@ -163,7 +177,7 @@ pub(super) fn resolve_range_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Ran gtk::Inhibit(false) })); - resolve_block!(bargs, gtk_widget, { + def_widget!(bargs, _g, gtk_widget, { // @prop value - the value prop(value: as_f64) { if !*is_being_dragged.borrow() { @@ -179,56 +193,30 @@ pub(super) fn resolve_range_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Ran prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) { gtk_widget.set_sensitive(true); gtk_widget.add_events(gdk::EventMask::PROPERTY_CHANGE_MASK); - let old_id = on_change_handler_id.replace(Some( - gtk_widget.connect_value_changed(move |gtk_widget| { - run_command(timeout, &onchange, gtk_widget.value()); - }) - )); - old_id.map(|id| gtk_widget.disconnect(id)); - + connect_single_handler!(gtk_widget, gtk_widget.connect_value_changed(move |gtk_widget| { + run_command(timeout, &onchange, gtk_widget.value()); + })); } }); + Ok(()) } /// @widget !orientable -pub(super) fn resolve_orientable_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Range) { - resolve_block!(bargs, gtk_widget, { +pub(super) fn resolve_orientable_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Range) -> Result<()> { + def_widget!(bargs, _g, gtk_widget, { // @prop orientation - orientation of the widget. Possible values: $orientation prop(orientation: as_string) { gtk_widget.set_orientation(parse_orientation(&orientation)?) }, }); + Ok(()) } // concrete widgets -fn build_if_else(bargs: &mut BuilderArgs) -> Result { - if bargs.widget.children.len() != 2 { - bail!("if-widget needs to have exactly two children, but had {}", bargs.widget.children.len()); - } - let gtk_widget = gtk::Box::new(gtk::Orientation::Vertical, 0); - let (yes_widget, no_widget) = (bargs.widget.children[0].clone(), bargs.widget.children[1].clone()); - - let yes_widget = yes_widget.render(bargs.eww_state, bargs.window_name, bargs.widget_definitions)?; - let no_widget = no_widget.render(bargs.eww_state, bargs.window_name, bargs.widget_definitions)?; - - resolve_block!(bargs, gtk_widget, { - prop(cond: as_bool) { - gtk_widget.children().iter().for_each(|w| gtk_widget.remove(w)); - if cond { - gtk_widget.add(&yes_widget) - } else { - gtk_widget.add(&no_widget) - } - } - }); - Ok(gtk_widget) -} - /// @widget combo-box-text /// @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(); - let on_change_handler_id: EventHandlerId = Rc::new(RefCell::new(None)); - resolve_block!(bargs, gtk_widget, { + def_widget!(bargs, _g, gtk_widget, { // @prop items - Items that should be displayed in the combo box prop(items: as_vec) { gtk_widget.remove_all(); @@ -239,12 +227,9 @@ fn build_gtk_combo_box_text(bargs: &mut BuilderArgs) -> Result Result Result { let gtk_widget = gtk::Expander::new(None); - resolve_block!(bargs, gtk_widget, { + def_widget!(bargs, _g, gtk_widget, { // @prop name - name of the expander prop(name: as_string) {gtk_widget.set_label(Some(&name));}, // @prop expanded - sets if the tree is expanded @@ -266,7 +251,7 @@ fn build_gtk_expander(bargs: &mut BuilderArgs) -> Result { /// @desc A widget that can reveal a child with an animation. fn build_gtk_revealer(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::Revealer::new(); - resolve_block!(bargs, gtk_widget, { + def_widget!(bargs, _g, gtk_widget, { // @prop transition - the name of the transition. Possible values: $transition prop(transition: as_string = "crossfade") { gtk_widget.set_transition_type(parse_transition(&transition)?); }, // @prop reveal - sets if the child is revealed or not @@ -281,18 +266,14 @@ fn build_gtk_revealer(bargs: &mut BuilderArgs) -> 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(); - let on_change_handler_id: EventHandlerId = Rc::new(RefCell::new(None)); - resolve_block!(bargs, gtk_widget, { + def_widget!(bargs, _g, 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 = "") { - let old_id = on_change_handler_id.replace(Some( - gtk_widget.connect_toggled(move |gtk_widget| { - run_command(timeout, if gtk_widget.is_active() { &onchecked } else { &onunchecked }, ""); - }) - )); - old_id.map(|id| gtk_widget.disconnect(id)); + connect_single_handler!(gtk_widget, gtk_widget.connect_toggled(move |gtk_widget| { + run_command(timeout, if gtk_widget.is_active() { &onchecked } else { &onunchecked }, ""); + })); } }); @@ -303,20 +284,16 @@ 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(); - let on_change_handler_id: EventHandlerId = Rc::new(RefCell::new(None)); - resolve_block!(bargs, gtk_widget, { + def_widget!(bargs, _g, 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) { - let old_id = on_change_handler_id.replace(Some( - gtk_widget.connect_color_set(move |gtk_widget| { - run_command(timeout, &onchange, gtk_widget.rgba()); - }) - )); - old_id.map(|id| gtk_widget.disconnect(id)); + connect_single_handler!(gtk_widget, gtk_widget.connect_color_set(move |gtk_widget| { + run_command(timeout, &onchange, gtk_widget.rgba()); + })); } }); @@ -327,20 +304,16 @@ 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(); - let on_change_handler_id: EventHandlerId = Rc::new(RefCell::new(None)); - resolve_block!(bargs, gtk_widget, { + def_widget!(bargs, _g, 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) { - let old_id = on_change_handler_id.replace(Some( - gtk_widget.connect_color_activated(move |_a, color| { - run_command(timeout, &onchange, *color); - }) - )); - old_id.map(|id| gtk_widget.disconnect(id)); + connect_single_handler!(gtk_widget, gtk_widget.connect_color_activated(move |_a, color| { + run_command(timeout, &onchange, *color); + })); } }); @@ -352,16 +325,17 @@ fn build_gtk_color_chooser(bargs: &mut BuilderArgs) -> Result Result { let gtk_widget = gtk::Scale::new(gtk::Orientation::Horizontal, Some(>k::Adjustment::new(0.0, 0.0, 100.0, 1.0, 1.0, 1.0))); - resolve_block!(bargs, gtk_widget, { + def_widget!(bargs, _g, gtk_widget, { // @prop flipped - flip the direction prop(flipped: as_bool) { gtk_widget.set_inverted(flipped) }, // @prop marks - draw marks prop(marks: as_string) { gtk_widget.clear_marks(); - for mark in marks.split(","){ + for mark in marks.split(',') { gtk_widget.add_mark(mark.trim().parse()?, gtk::PositionType::Bottom, None) - }}, + } + }, // @prop draw-value - draw the value of the property prop(draw_value: as_bool = false) { gtk_widget.set_draw_value(draw_value) }, @@ -373,7 +347,7 @@ fn build_gtk_scale(bargs: &mut BuilderArgs) -> Result { /// @desc A progress bar fn build_gtk_progress(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::ProgressBar::new(); - resolve_block!(bargs, gtk_widget, { + def_widget!(bargs, _g, gtk_widget, { // @prop flipped - flip the direction prop(flipped: as_bool) { gtk_widget.set_inverted(flipped) }, @@ -390,8 +364,7 @@ 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(); - let on_change_handler_id: EventHandlerId = Rc::new(RefCell::new(None)); - resolve_block!(bargs, gtk_widget, { + def_widget!(bargs, _g, gtk_widget, { // @prop value - the content of the text field prop(value: as_string) { gtk_widget.set_text(&value); @@ -400,12 +373,9 @@ 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) { - let old_id = on_change_handler_id.replace(Some( - gtk_widget.connect_changed(move |gtk_widget| { - run_command(timeout, &onchange, gtk_widget.text().to_string()); - }) - )); - old_id.map(|id| gtk_widget.disconnect(id)); + connect_single_handler!(gtk_widget, gtk_widget.connect_changed(move |gtk_widget| { + run_command(timeout, &onchange, gtk_widget.text().to_string()); + })); } }); Ok(gtk_widget) @@ -415,9 +385,8 @@ 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 on_click_handler_id: EventHandlerId = Rc::new(RefCell::new(None)); - resolve_block!(bargs, gtk_widget, { + def_widget!(bargs, _g, 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 @@ -429,18 +398,15 @@ fn build_gtk_button(bargs: &mut BuilderArgs) -> Result { onrightclick: as_string = "" ) { gtk_widget.add_events(gdk::EventMask::BUTTON_PRESS_MASK); - let old_id = on_click_handler_id.replace(Some( - gtk_widget.connect_button_press_event(move |_, evt| { - match evt.button() { - 1 => run_command(timeout, &onclick, ""), - 2 => run_command(timeout, &onmiddleclick, ""), - 3 => run_command(timeout, &onrightclick, ""), - _ => {}, - } - gtk::Inhibit(false) - }) - )); - old_id.map(|id| gtk_widget.disconnect(id)); + connect_single_handler!(gtk_widget, gtk_widget.connect_button_press_event(move |_, evt| { + match evt.button() { + 1 => run_command(timeout, &onclick, ""), + 2 => run_command(timeout, &onmiddleclick, ""), + 3 => run_command(timeout, &onrightclick, ""), + _ => {}, + } + gtk::Inhibit(false) + })); } }); @@ -451,16 +417,16 @@ fn build_gtk_button(bargs: &mut BuilderArgs) -> Result { /// @desc A widget displaying an image fn build_gtk_image(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::Image::new(); - resolve_block!(bargs, gtk_widget, { + def_widget!(bargs, _g, gtk_widget, { // @prop path - path to the image file - // @prop width - width of the image - // @prop height - height of the image - prop(path: as_string, width: as_i32 = 10000, height: as_i32 = 10000) { + // @prop image-width - width of the image + // @prop image-height - height of the image + prop(path: as_string, image_width: as_i32 = -1, image_height: as_i32 = -1) { if path.ends_with(".gif") { let pixbuf_animation = gtk::gdk_pixbuf::PixbufAnimation::from_file(std::path::PathBuf::from(path))?; gtk_widget.set_from_animation(&pixbuf_animation); } else { - let pixbuf = gtk::gdk_pixbuf::Pixbuf::from_file_at_size(std::path::PathBuf::from(path), width, height)?; + let pixbuf = gtk::gdk_pixbuf::Pixbuf::from_file_at_size(std::path::PathBuf::from(path), image_width, image_height)?; gtk_widget.set_from_pixbuf(Some(&pixbuf)); } } @@ -472,7 +438,7 @@ fn build_gtk_image(bargs: &mut BuilderArgs) -> Result { /// @desc the main layout container fn build_gtk_box(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::Box::new(gtk::Orientation::Horizontal, 0); - resolve_block!(bargs, gtk_widget, { + def_widget!(bargs, _g, gtk_widget, { // @prop spacing - spacing between elements prop(spacing: as_i32 = 0) { gtk_widget.set_spacing(spacing) }, // @prop orientation - orientation of the box. possible values: $orientation @@ -487,17 +453,17 @@ fn build_gtk_box(bargs: &mut BuilderArgs) -> Result { /// @desc a box that must contain exactly three children, which will be layed out at the start, center and end of the container. fn build_center_box(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::Box::new(gtk::Orientation::Horizontal, 0); - resolve_block!(bargs, gtk_widget, { + def_widget!(bargs, _g, gtk_widget, { // @prop orientation - orientation of the centerbox. possible values: $orientation prop(orientation: as_string) { gtk_widget.set_orientation(parse_orientation(&orientation)?) }, }); - match bargs.widget.children.len().cmp(&3) { + match bargs.widget_use.children.len().cmp(&3) { Ordering::Less => { - Err(DiagError::new(gen_diagnostic!("centerbox must contain exactly 3 elements", bargs.widget.span)).into()) + Err(DiagError::new(gen_diagnostic!("centerbox must contain exactly 3 elements", bargs.widget_use.span)).into()) } Ordering::Greater => { - let (_, additional_children) = bargs.widget.children.split_at(3); + let (_, additional_children) = bargs.widget_use.children.split_at(3); // we know that there is more than three children, so unwrapping on first and left here is fine. let first_span = additional_children.first().unwrap().span(); let last_span = additional_children.last().unwrap().span(); @@ -508,11 +474,15 @@ fn build_center_box(bargs: &mut BuilderArgs) -> Result { .into()) } Ordering::Equal => { - let mut children = bargs - .widget - .children - .iter() - .map(|child| child.render(bargs.eww_state, bargs.window_name, bargs.widget_definitions)); + let mut children = bargs.widget_use.children.iter().map(|child| { + build_gtk_widget( + bargs.scope_graph, + bargs.widget_defs.clone(), + bargs.calling_scope, + child.clone(), + bargs.custom_widget_invocation.clone(), + ) + }); // we know that we have exactly three children here, so we can unwrap here. let (first, center, end) = children.next_tuple().unwrap(); let (first, center, end) = (first?, center?, end?); @@ -532,12 +502,6 @@ fn build_center_box(bargs: &mut BuilderArgs) -> Result { fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::EventBox::new(); - let on_scroll_handler_id: EventHandlerId = Rc::new(RefCell::new(None)); - let on_hover_handler_id: EventHandlerId = Rc::new(RefCell::new(None)); - let on_hover_leave_handler_id: EventHandlerId = Rc::new(RefCell::new(None)); - let cursor_hover_enter_handler_id: EventHandlerId = Rc::new(RefCell::new(None)); - let cursor_hover_leave_handler_id: EventHandlerId = Rc::new(RefCell::new(None)); - // Support :hover selector gtk_widget.connect_enter_notify_event(|gtk_widget, evt| { if evt.detail() != NotifyType::Inferior { @@ -553,81 +517,67 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { gtk::Inhibit(false) }); - resolve_block!(bargs, gtk_widget, { + def_widget!(bargs, _g, 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) { gtk_widget.add_events(gdk::EventMask::SCROLL_MASK); gtk_widget.add_events(gdk::EventMask::SMOOTH_SCROLL_MASK); - let old_id = on_scroll_handler_id.replace(Some( - 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" }); - } - gtk::Inhibit(false) - }) - )); - old_id.map(|id| gtk_widget.disconnect(id)); + connect_single_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" }); + } + 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) { gtk_widget.add_events(gdk::EventMask::ENTER_NOTIFY_MASK); - let old_id = on_hover_handler_id.replace(Some( - gtk_widget.connect_enter_notify_event(move |_, evt| { - if evt.detail() != NotifyType::Inferior { - run_command(timeout, &onhover, format!("{} {}", evt.position().0, evt.position().1)); - } - gtk::Inhibit(false) - }) - )); - old_id.map(|id| gtk_widget.disconnect(id)); + connect_single_handler!(gtk_widget, gtk_widget.connect_enter_notify_event(move |_, evt| { + if evt.detail() != NotifyType::Inferior { + run_command(timeout, &onhover, format!("{} {}", 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) { gtk_widget.add_events(gdk::EventMask::LEAVE_NOTIFY_MASK); - let old_id = on_hover_leave_handler_id.replace(Some( - gtk_widget.connect_leave_notify_event(move |_, evt| { - if evt.detail() != NotifyType::Inferior { - run_command(timeout, &onhoverlost, format!("{} {}", evt.position().0, evt.position().1)); - } - gtk::Inhibit(false) - }) - )); - old_id.map(|id| gtk_widget.disconnect(id)); + connect_single_handler!(gtk_widget, gtk_widget.connect_leave_notify_event(move |_, evt| { + if evt.detail() != NotifyType::Inferior { + run_command(timeout, &onhoverlost, format!("{} {}", evt.position().0, evt.position().1)); + } + gtk::Inhibit(false) + })); }, // @prop cursor - Cursor to show while hovering (see [gtk3-cursors](https://docs.gtk.org/gdk3/ctor.Cursor.new_from_name.html) for possible names) prop(cursor: as_string) { gtk_widget.add_events(gdk::EventMask::ENTER_NOTIFY_MASK); gtk_widget.add_events(gdk::EventMask::LEAVE_NOTIFY_MASK); - cursor_hover_enter_handler_id.replace(Some( - gtk_widget.connect_enter_notify_event(move |widget, _evt| { - if _evt.detail() != NotifyType::Inferior { - let display = gdk::Display::default(); - let gdk_window = widget.window(); - if let (Some(display), Some(gdk_window)) = (display, gdk_window) { - gdk_window.set_cursor(gdk::Cursor::from_name(&display, &cursor).as_ref()); - } + connect_single_handler!(gtk_widget, gtk_widget.connect_enter_notify_event(move |widget, _evt| { + if _evt.detail() != NotifyType::Inferior { + let display = gdk::Display::default(); + let gdk_window = widget.window(); + if let (Some(display), Some(gdk_window)) = (display, gdk_window) { + gdk_window.set_cursor(gdk::Cursor::from_name(&display, &cursor).as_ref()); } - gtk::Inhibit(false) - }) - )).map(|id| gtk_widget.disconnect(id)); - - cursor_hover_leave_handler_id.replace(Some( - gtk_widget.connect_leave_notify_event(move |widget, _evt| { - if _evt.detail() != NotifyType::Inferior { - let gdk_window = widget.window(); - if let Some(gdk_window) = gdk_window { - gdk_window.set_cursor(None); - } + } + gtk::Inhibit(false) + })); + connect_single_handler!(gtk_widget, gtk_widget.connect_leave_notify_event(move |widget, _evt| { + if _evt.detail() != NotifyType::Inferior { + let gdk_window = widget.window(); + if let Some(gdk_window) = gdk_window { + gdk_window.set_cursor(None); } - gtk::Inhibit(false) - }) - )).map(|id| gtk_widget.disconnect(id)); - }, + } + gtk::Inhibit(false) + })); + } }); Ok(gtk_widget) } @@ -637,7 +587,7 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { fn build_gtk_label(bargs: &mut BuilderArgs) -> Result { let gtk_widget = gtk::Label::new(None); - resolve_block!(bargs, gtk_widget, { + def_widget!(bargs, _g, gtk_widget, { // @prop text - the text to display // @prop limit-width - maximum count of characters to display // @prop show_truncated - show whether the text was truncated @@ -670,19 +620,20 @@ fn build_gtk_literal(bargs: &mut BuilderArgs) -> Result { gtk_widget.set_widget_name("literal"); // TODO these clones here are dumdum - let window_name = bargs.window_name.to_string(); - let widget_definitions = bargs.widget_definitions.clone(); - let literal_use_span = bargs.widget.span; + let literal_use_span = bargs.widget_use.span; // the file id the literal-content has been stored under, for error reporting. let literal_file_id: Rc>> = Rc::new(RefCell::new(None)); - resolve_block!(bargs, gtk_widget, { + let widget_defs = bargs.widget_defs.clone(); + let calling_scope = bargs.calling_scope; + + def_widget!(bargs, scope_graph, gtk_widget, { // @prop content - inline yuck that will be rendered as a widget. prop(content: as_string) { gtk_widget.children().iter().for_each(|w| gtk_widget.remove(w)); if !content.is_empty() { - let widget_node_result: AstResult<_> = try { + let content_widget_use: AstResult<_> = try { let ast = { let mut yuck_files = error_handling_ctx::YUCK_FILES.write().unwrap(); let (span, asts) = yuck_files.load_str("".to_string(), content)?; @@ -692,12 +643,12 @@ fn build_gtk_literal(bargs: &mut BuilderArgs) -> Result { yuck::parser::require_single_toplevel(span, asts)? }; - let content_widget_use = yuck::config::widget_use::WidgetUse::from_ast(ast)?; - widget_node::generate_generic_widget_node(&widget_definitions, &HashMap::new(), content_widget_use)? + yuck::config::widget_use::WidgetUse::from_ast(ast)? }; + let content_widget_use = content_widget_use?; - let widget_node = widget_node_result.context_label(literal_use_span, "Error in the literal used here")?; - let child_widget = widget_node.render(&mut eww_state::EwwState::default(), &window_name, &widget_definitions) + // TODO a literal should create a new scope, that I'm not even sure should inherit from root + let child_widget = build_gtk_widget(scope_graph, widget_defs.clone(), calling_scope, content_widget_use, None) .map_err(|e| AstError::ErrorContext { label_span: literal_use_span, context: "Error in the literal used here".to_string(), @@ -715,8 +666,7 @@ 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(); - let on_click_handler_id: EventHandlerId = Rc::new(RefCell::new(None)); - resolve_block!(bargs, gtk_widget, { + def_widget!(bargs, _g, gtk_widget, { // @prop day - the selected day prop(day: as_f64) { gtk_widget.set_day(day as i32) }, // @prop month - the selected month @@ -734,16 +684,13 @@ fn build_gtk_calendar(bargs: &mut BuilderArgs) -> Result { // @prop onclick - command to run when the user selects a date. The `{}` placeholder will be replaced by the selected date. // @prop timeout - timeout of the command prop(timeout: as_duration = Duration::from_millis(200), onclick: as_string) { - let old_id = on_click_handler_id.replace(Some( - gtk_widget.connect_day_selected(move |w| { - run_command( - timeout, - &onclick, - format!("{}.{}.{}", w.day(), w.month(), w.year()) - ) - }) - )); - old_id.map(|id| gtk_widget.disconnect(id)); + connect_single_handler!(gtk_widget, gtk_widget.connect_day_selected(move |w| { + run_command( + timeout, + &onclick, + format!("{}.{}.{}", w.day(), w.month(), w.year()) + ) + })); } }); @@ -755,7 +702,7 @@ fn build_gtk_calendar(bargs: &mut BuilderArgs) -> Result { /// @desc A widget that displays a circular progress bar fn build_circular_progress_bar(bargs: &mut BuilderArgs) -> Result { let w = CircProg::new(); - resolve_block!(bargs, w, { + def_widget!(bargs, _g, w, { // @prop value - the value, between 0 - 100 prop(value: as_f64) { w.set_property("value", value)?; }, // @prop start-angle - the angle that the circle should start at @@ -799,13 +746,17 @@ fn parse_align(o: &str) -> Result { } } +/// Connect a function to the first map event of a widget. After that first map, the handler will get disconnected. fn connect_first_map, F: Fn(&W) + 'static>(widget: &W, func: F) { - // TODO it would be better to actually remove the connect_map after first map, - // but that would be highly annoying to implement... - let is_first_map = std::rc::Rc::new(std::cell::RefCell::new(true)); - widget.connect_map(move |w| { - if is_first_map.replace(false) { - func(w); + let signal_handler_id = std::rc::Rc::new(std::cell::RefCell::new(None)); + + signal_handler_id.borrow_mut().replace(widget.connect_map({ + let signal_handler_id = signal_handler_id.clone(); + move |w| { + if let Some(signal_handler_id) = signal_handler_id.borrow_mut().take() { + w.disconnect(signal_handler_id); + } + func(w) } - }); + })); } diff --git a/crates/eww/src/widgets/widget_node.rs b/crates/eww/src/widgets/widget_node.rs deleted file mode 100644 index 5378336..0000000 --- a/crates/eww/src/widgets/widget_node.rs +++ /dev/null @@ -1,182 +0,0 @@ -use crate::eww_state::EwwState; -use anyhow::*; -use dyn_clone; -use eww_shared_util::{AttrName, Span, Spanned, VarName}; -use simplexpr::SimplExpr; -use std::collections::HashMap; -use yuck::{ - config::{validate::ValidationError, widget_definition::WidgetDefinition, widget_use::WidgetUse}, - error::{AstError, AstResult}, -}; - -pub trait WidgetNode: Spanned + std::fmt::Debug + dyn_clone::DynClone + Send + Sync { - fn get_name(&self) -> &str; - - /// Generate a [gtk::Widget] from a [element::WidgetUse]. - /// - /// Also registers all the necessary state-change handlers in the eww_state. - /// - /// This may return `Err` in case there was an actual error while parsing - /// or when the widget_use did not match any widget name - fn render( - &self, - eww_state: &mut EwwState, - window_name: &str, - widget_definitions: &HashMap, - ) -> Result; -} - -dyn_clone::clone_trait_object!(WidgetNode); - -#[derive(Debug, Clone)] -pub struct UserDefined { - name: String, - span: Span, - content: Box, -} - -impl WidgetNode for UserDefined { - fn get_name(&self) -> &str { - &self.name - } - - fn render( - &self, - eww_state: &mut EwwState, - window_name: &str, - widget_definitions: &HashMap, - ) -> Result { - self.content.render(eww_state, window_name, widget_definitions) - } -} - -impl Spanned for UserDefined { - fn span(&self) -> Span { - self.span - } -} - -#[derive(Debug, Clone)] -pub struct Generic { - pub name: String, - pub name_span: Span, - pub span: Span, - pub children: Vec>, - pub attrs: HashMap, -} - -impl Generic { - pub fn get_attr(&self, key: &str) -> Result<&SimplExpr> { - Ok(self.attrs.get(key).ok_or_else(|| { - AstError::ValidationError(ValidationError::MissingAttr { - widget_name: self.name.to_string(), - arg_name: AttrName(key.to_string()), - use_span: self.span, - // TODO set this when available - arg_list_span: None, - }) - })?) - } - - /// returns all the variables that are referenced in this widget - pub fn referenced_vars(&self) -> impl Iterator { - self.attrs.iter().flat_map(|(_, value)| value.var_refs()).map(|(_, value)| value) - } -} - -impl WidgetNode for Generic { - fn get_name(&self) -> &str { - &self.name - } - - fn render( - &self, - eww_state: &mut EwwState, - window_name: &str, - widget_definitions: &HashMap, - ) -> Result { - Ok(crate::widgets::build_builtin_gtk_widget(eww_state, window_name, widget_definitions, self)?.ok_or_else(|| { - AstError::ValidationError(ValidationError::UnknownWidget(self.name_span, self.get_name().to_string())) - })?) - } -} -impl Spanned for Generic { - fn span(&self) -> Span { - self.span - } -} - -pub fn generate_generic_widget_node( - defs: &HashMap, - local_env: &HashMap, - w: WidgetUse, -) -> AstResult> { - if let Some(def) = defs.get(&w.name) { - let children_span = w.children_span(); - let mut new_local_env = w - .attrs - .attrs - .into_iter() - .map(|(name, value)| Ok((VarName(name.0), value.value.as_simplexpr()?.resolve_one_level(local_env)))) - .collect::>>()?; - - // handle default value for optional arguments - for expected in def.expected_args.iter().filter(|x| x.optional) { - let var_name = VarName(expected.name.clone().0); - new_local_env.entry(var_name).or_insert_with(|| SimplExpr::literal(expected.span, String::new())); - } - - let definition_content = replace_children_placeholder_in(children_span, def.widget.clone(), &w.children)?; - - let content = generate_generic_widget_node(defs, &new_local_env, definition_content)?; - Ok(Box::new(UserDefined { name: w.name, span: w.span, content })) - } else { - Ok(Box::new(Generic { - name: w.name, - name_span: w.name_span, - span: w.span, - attrs: w - .attrs - .attrs - .into_iter() - .map(|(name, value)| Ok((name, value.value.as_simplexpr()?.resolve_one_level(local_env)))) - .collect::>>()?, - - children: w - .children - .into_iter() - .map(|child| generate_generic_widget_node(defs, local_env, child)) - .collect::>>()?, - })) - } -} - -/// Replaces all the `children` placeholders in the given [`widget`](w) using the provided [`children`](provided_children). -fn replace_children_placeholder_in(use_span: Span, mut w: WidgetUse, provided_children: &[WidgetUse]) -> AstResult { - // Take the current children from the widget and replace them with an empty vector that we will now add widgets to again. - let child_count = w.children.len(); - let widget_children = std::mem::replace(&mut w.children, Vec::with_capacity(child_count)); - - for mut child in widget_children.into_iter() { - if child.name == "children" { - // Note that we use `primitive_optional` here, meaning that the value for `nth` must be static. - // We'll be able to make this dynamic after the state management structure rework - if let Some(nth) = child.attrs.primitive_optional::("nth")? { - // If a single child is referenced, push that single widget into the children - let selected_child: &WidgetUse = provided_children - .get(nth) - .ok_or_else(|| AstError::MissingNode(use_span).context_label(child.span, "required here"))?; - w.children.push(selected_child.clone()); - } else { - // otherwise append all provided children - w.children.append(&mut provided_children.to_vec()); - } - } else { - // If this isn't a `children`-node, then recursively go into it and replace the children there. - // If there are no children referenced in there, this will append the widget unchanged. - let child = replace_children_placeholder_in(use_span, child, provided_children)?; - w.children.push(child); - } - } - Ok(w) -} diff --git a/crates/eww_shared_util/Cargo.toml b/crates/eww_shared_util/Cargo.toml index 3293833..d46137b 100644 --- a/crates/eww_shared_util/Cargo.toml +++ b/crates/eww_shared_util/Cargo.toml @@ -6,3 +6,4 @@ edition = "2018" [dependencies] serde = {version = "1.0", features = ["derive"]} derive_more = "0.99" +ref-cast = "1.0.6" diff --git a/crates/eww_shared_util/src/wrappers.rs b/crates/eww_shared_util/src/wrappers.rs index dabdfe8..4220c7b 100644 --- a/crates/eww_shared_util/src/wrappers.rs +++ b/crates/eww_shared_util/src/wrappers.rs @@ -1,10 +1,22 @@ use derive_more::*; +use ref_cast::RefCast; use serde::{Deserialize, Serialize}; /// The name of a variable #[repr(transparent)] #[derive( - Clone, Hash, PartialEq, Eq, Serialize, Deserialize, AsRef, From, FromStr, Display, DebugCustom, + Clone, + Hash, + PartialEq, + Eq, + Serialize, + Deserialize, + AsRef, + From, + FromStr, + Display, + DebugCustom, + RefCast, )] #[debug(fmt = "VarName({})", .0)] pub struct VarName(pub String); @@ -15,20 +27,49 @@ impl std::borrow::Borrow for VarName { } } +impl AttrName { + pub fn to_attr_name_ref(&self) -> &AttrName { + AttrName::ref_cast(&self.0) + } +} + impl From<&str> for VarName { fn from(s: &str) -> Self { VarName(s.to_owned()) } } +impl From for VarName { + fn from(x: AttrName) -> Self { + VarName(x.0) + } +} + /// The name of an attribute #[repr(transparent)] #[derive( - Clone, Hash, PartialEq, Eq, Serialize, Deserialize, AsRef, From, FromStr, Display, DebugCustom, + Clone, + Hash, + PartialEq, + Eq, + Serialize, + Deserialize, + AsRef, + From, + FromStr, + Display, + DebugCustom, + RefCast, )] #[debug(fmt="AttrName({})", .0)] pub struct AttrName(pub String); +impl AttrName { + pub fn to_var_name_ref(&self) -> &VarName { + VarName::ref_cast(&self.0) + } +} + impl std::borrow::Borrow for AttrName { fn borrow(&self) -> &str { &self.0 @@ -40,3 +81,9 @@ impl From<&str> for AttrName { AttrName(s.to_owned()) } } + +impl From for AttrName { + fn from(x: VarName) -> Self { + AttrName(x.0) + } +} diff --git a/crates/simplexpr/src/ast.rs b/crates/simplexpr/src/ast.rs index 6da9546..4b56cfb 100644 --- a/crates/simplexpr/src/ast.rs +++ b/crates/simplexpr/src/ast.rs @@ -77,40 +77,53 @@ impl SimplExpr { Self::Literal(DynVal(s, span)) } - /// Construct a synthetic simplexpr from a literal string, without adding any relevant span information (uses [DUMMY_SPAN]) - pub fn synth_string(s: String) -> Self { - Self::Literal(DynVal(s, Span::DUMMY)) + /// Construct a synthetic simplexpr from a literal string, without adding any relevant span information (uses [`Span::DUMMY`]) + pub fn synth_string(s: impl Into) -> Self { + Self::Literal(DynVal(s.into(), Span::DUMMY)) } - /// Construct a synthetic simplexpr from a literal dynval, without adding any relevant span information (uses [DUMMY_SPAN]) + /// Construct a synthetic simplexpr from a literal dynval, without adding any relevant span information (uses [`Span::DUMMY`]) pub fn synth_literal>(s: T) -> Self { Self::Literal(s.into()) } - pub fn collect_var_refs_into(&self, dest: &mut Vec) { + pub fn var_ref(span: Span, n: impl Into) -> Self { + Self::VarRef(span, n.into()) + } + + pub fn references_var(&self, var: &VarName) -> bool { + use SimplExpr::*; match self { - SimplExpr::VarRef(_, x) => dest.push(x.clone()), - SimplExpr::UnaryOp(_, _, x) => x.as_ref().collect_var_refs_into(dest), - SimplExpr::BinOp(_, l, _, r) => { - l.as_ref().collect_var_refs_into(dest); - r.as_ref().collect_var_refs_into(dest); + Literal(_) => false, + Concat(_, x) | FunctionCall(_, _, x) | JsonArray(_, x) => x.iter().any(|x| x.references_var(var)), + JsonObject(_, x) => x.iter().any(|(k, v)| k.references_var(var) || v.references_var(var)), + JsonAccess(_, a, b) | BinOp(_, a, _, b) => a.references_var(var) || b.references_var(var), + UnaryOp(_, _, x) => x.references_var(var), + IfElse(_, a, b, c) => a.references_var(var) || b.references_var(var) || c.references_var(var), + VarRef(_, x) => x == var, + } + } + + pub fn collect_var_refs_into(&self, dest: &mut Vec) { + use SimplExpr::*; + match self { + VarRef(_, x) => dest.push(x.clone()), + UnaryOp(_, _, x) => x.as_ref().collect_var_refs_into(dest), + BinOp(_, a, _, b) | JsonAccess(_, a, b) => { + a.as_ref().collect_var_refs_into(dest); + b.as_ref().collect_var_refs_into(dest); } - SimplExpr::IfElse(_, a, b, c) => { + IfElse(_, a, b, c) => { a.as_ref().collect_var_refs_into(dest); b.as_ref().collect_var_refs_into(dest); c.as_ref().collect_var_refs_into(dest); } - SimplExpr::JsonAccess(_, value, index) => { - value.as_ref().collect_var_refs_into(dest); - index.as_ref().collect_var_refs_into(dest); - } - SimplExpr::FunctionCall(_, _, args) => args.iter().for_each(|x: &SimplExpr| x.collect_var_refs_into(dest)), - SimplExpr::JsonArray(_, values) => values.iter().for_each(|v| v.collect_var_refs_into(dest)), - SimplExpr::JsonObject(_, entries) => entries.iter().for_each(|(k, v)| { + JsonArray(_, xs) | FunctionCall(_, _, xs) | Concat(_, xs) => xs.iter().for_each(|x| x.collect_var_refs_into(dest)), + JsonObject(_, entries) => entries.iter().for_each(|(k, v)| { k.collect_var_refs_into(dest); v.collect_var_refs_into(dest); }), - _ => (), + Literal(_) => {} }; } diff --git a/crates/simplexpr/src/eval.rs b/crates/simplexpr/src/eval.rs index b2af1f8..769bb93 100644 --- a/crates/simplexpr/src/eval.rs +++ b/crates/simplexpr/src/eval.rs @@ -115,27 +115,27 @@ impl SimplExpr { }) } - pub fn var_refs(&self) -> Vec<(Span, &VarName)> { + pub fn var_refs_with_span(&self) -> Vec<(Span, &VarName)> { use SimplExpr::*; match self { Literal(..) => Vec::new(), VarRef(span, name) => vec![(*span, name)], - Concat(_, elems) => elems.iter().flat_map(|x| x.var_refs().into_iter()).collect(), + Concat(_, elems) => elems.iter().flat_map(|x| x.var_refs_with_span().into_iter()).collect(), BinOp(_, box a, _, box b) | JsonAccess(_, box a, box b) => { - let mut refs = a.var_refs(); - refs.extend(b.var_refs().iter()); + let mut refs = a.var_refs_with_span(); + refs.extend(b.var_refs_with_span().iter()); refs } - UnaryOp(_, _, box x) => x.var_refs(), + UnaryOp(_, _, box x) => x.var_refs_with_span(), IfElse(_, box a, box b, box c) => { - let mut refs = a.var_refs(); - refs.extend(b.var_refs().iter()); - refs.extend(c.var_refs().iter()); + let mut refs = a.var_refs_with_span(); + refs.extend(b.var_refs_with_span().iter()); + refs.extend(c.var_refs_with_span().iter()); refs } - FunctionCall(_, _, args) => args.iter().flat_map(|a| a.var_refs()).collect(), - JsonArray(_, values) => values.iter().flat_map(|v| v.var_refs()).collect(), - JsonObject(_, entries) => entries.iter().flat_map(|(k, v)| k.var_refs().into_iter().chain(v.var_refs())).collect(), + FunctionCall(_, _, args) => args.iter().flat_map(|a| a.var_refs_with_span()).collect(), + JsonArray(_, values) => values.iter().flat_map(|v| v.var_refs_with_span()).collect(), + JsonObject(_, entries) => entries.iter().flat_map(|(k, v)| k.var_refs_with_span().into_iter().chain(v.var_refs_with_span())).collect(), } } diff --git a/crates/simplexpr/src/parser/lalrpop_helpers.rs b/crates/simplexpr/src/parser/lalrpop_helpers.rs index b1d3ea6..42b06dd 100644 --- a/crates/simplexpr/src/parser/lalrpop_helpers.rs +++ b/crates/simplexpr/src/parser/lalrpop_helpers.rs @@ -1,6 +1,6 @@ use eww_shared_util::Span; -use crate::{dynval::DynVal, SimplExpr}; +use crate::SimplExpr; use super::lexer::{LexicalError, Sp, StrLitSegment, Token}; @@ -19,7 +19,7 @@ pub fn parse_stringlit( let (lo, seg, hi) = segs.remove(0); let span = Span(lo, hi, file_id); match seg { - StrLitSegment::Literal(lit) => Ok(SimplExpr::Literal(DynVal(lit, span))), + StrLitSegment::Literal(lit) => Ok(SimplExpr::literal(span, lit)), StrLitSegment::Interp(toks) => { let token_stream = toks.into_iter().map(Ok); parser.parse(file_id, token_stream) @@ -32,7 +32,7 @@ pub fn parse_stringlit( let span = Span(lo, hi, file_id); match segment { StrLitSegment::Literal(lit) if lit.is_empty() => None, - StrLitSegment::Literal(lit) => Some(Ok(SimplExpr::Literal(DynVal(lit, span)))), + StrLitSegment::Literal(lit) => Some(Ok(SimplExpr::literal(span, lit))), StrLitSegment::Interp(toks) => { let token_stream = toks.into_iter().map(Ok); Some(parser.parse(file_id, token_stream)) diff --git a/crates/yuck/src/config/file_provider.rs b/crates/yuck/src/config/file_provider.rs index 0967f02..fc23889 100644 --- a/crates/yuck/src/config/file_provider.rs +++ b/crates/yuck/src/config/file_provider.rs @@ -56,7 +56,7 @@ impl YuckFile { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct YuckFiles { files: HashMap, latest_id: usize, @@ -64,7 +64,7 @@ pub struct YuckFiles { impl YuckFiles { pub fn new() -> Self { - Self { files: HashMap::new(), latest_id: 0 } + Self::default() } } diff --git a/crates/yuck/src/config/validate.rs b/crates/yuck/src/config/validate.rs index ffb8e7c..1af9592 100644 --- a/crates/yuck/src/config/validate.rs +++ b/crates/yuck/src/config/validate.rs @@ -90,7 +90,7 @@ pub fn validate_variables_in_widget_use( let values = widget.attrs.attrs.values(); let unknown_var = values.filter_map(|value| value.value.as_simplexpr().ok()).find_map(|expr: SimplExpr| { let span = expr.span(); - expr.var_refs() + expr.var_refs_with_span() .iter() .cloned() .map(|(span, var_ref)| (span, var_ref.clone())) diff --git a/crates/yuck/src/config/widget_use.rs b/crates/yuck/src/config/widget_use.rs index a754147..a63dc89 100644 --- a/crates/yuck/src/config/widget_use.rs +++ b/crates/yuck/src/config/widget_use.rs @@ -7,7 +7,7 @@ use crate::{ error::{AstError, AstResult}, parser::{ast::Ast, ast_iterator::AstIterator, from_ast::FromAst}, }; -use eww_shared_util::{AttrName, Span, VarName}; +use eww_shared_util::{AttrName, Span, Spanned, VarName}; use super::attributes::Attributes; @@ -45,6 +45,12 @@ impl FromAst for WidgetUse { } } +impl Spanned for WidgetUse { + fn span(&self) -> Span { + self.span + } +} + fn label_from_simplexpr(value: SimplExpr, span: Span) -> WidgetUse { WidgetUse { name: "label".to_string(), diff --git a/crates/yuck/src/parser/from_ast.rs b/crates/yuck/src/parser/from_ast.rs index 0d9c6f9..5cc5397 100644 --- a/crates/yuck/src/parser/from_ast.rs +++ b/crates/yuck/src/parser/from_ast.rs @@ -29,7 +29,7 @@ impl FromAst for String { } /// A trait that allows creating a type from the tail of a list-node. -/// I.e. to parse (foo [a b] (c d)), [from_tail] would just get [a b] (c d). +/// I.e. to parse (foo [a b] (c d)), [`FromAstElementContent::from_tail`] would just get [a b] (c d). pub trait FromAstElementContent: Sized { const ELEMENT_NAME: &'static str; fn from_tail>(span: Span, iter: AstIterator) -> AstResult; @@ -50,7 +50,7 @@ impl FromAst for T { impl FromAst for SimplExpr { fn from_ast(e: Ast) -> AstResult { match e { - Ast::Symbol(span, x) => Ok(SimplExpr::VarRef(span, VarName(x))), + Ast::Symbol(span, x) => Ok(SimplExpr::var_ref(span, x)), Ast::SimplExpr(span, x) => Ok(x), _ => Err(AstError::NotAValue(e.span(), e.expr_type())), } diff --git a/docs/src/configuration.md b/docs/src/configuration.md index d3e39ba..35340c2 100644 --- a/docs/src/configuration.md +++ b/docs/src/configuration.md @@ -129,9 +129,7 @@ As you may have noticed, we are using a couple predefined widgets here. These ar ### Rendering children in your widgets -(Note that this feature is currently considered **unstable**. The API might change, and there might be bugs here. If you encounter any, please do report them.) - -As your configuration grows, you might want to improve the structure of your config by factoring out functionality into basic reusable widgets. +As your configuration grows, you might want to improve the structure of you config by factoring out functionality into basic reusable widgets. Eww allows you to create custom wrapper widgets that can themselves take children, just like some of the built-in widgets like `box` or `button` can. For this, use the `children` placeholder: ```lisp @@ -154,10 +152,6 @@ You can also create more complex structure by referring to specific children wit (box :class "first" (children :nth 0)) (box :class "second" (children :nth 1)))) ``` -**NOTE**: It is currently not possible to dynamically change which child is shown. -This means that `nth` needs to be set to a static value and cannot refer to a variable. - - ## Adding dynamic content