diff --git a/Cargo.lock b/Cargo.lock index ca5454d..7dde666 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -230,6 +230,12 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +[[package]] +name = "dyn-clone" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" + [[package]] name = "either" version = "1.6.1" @@ -258,6 +264,7 @@ dependencies = [ "bincode", "debug_stub_derive", "derive_more", + "dyn-clone", "extend", "futures-core", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index 5937eb4..dc8241d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,8 @@ futures-core = "0.3" futures-util = "0.3" tokio-util = "0.6" +dyn-clone = "1.0" + [target.'cfg(target_os="linux")'.dependencies] inotify = "0.9" diff --git a/src/app.rs b/src/app.rs index 045b1a7..b319872 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,8 +3,7 @@ use crate::{ config::{window_definition::WindowName, AnchorPoint, WindowStacking}, display_backend, eww_state, script_var_handler::*, - value::{AttrValue, Coords, NumWithUnit, PrimitiveValue, VarName}, - widgets, + value::{Coords, NumWithUnit, PrimitiveValue, VarName}, }; use anyhow::*; use debug_stub_derive::*; @@ -67,7 +66,7 @@ pub enum DaemonCommand { PrintWindows(DaemonResponseSender), } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct EwwWindow { pub name: WindowName, pub definition: config::EwwWindowDefinition, @@ -105,13 +104,14 @@ impl App { DaemonCommand::NoOp => {} DaemonCommand::UpdateVars(mappings) => { for (var_name, new_value) in mappings { - self.update_state(var_name, new_value)?; + self.update_state(var_name, new_value); } } DaemonCommand::ReloadConfigAndCss(sender) => { let mut errors = Vec::new(); - let config_result = config::EwwConfig::read_from_file(&self.config_file_path); + let config_result = + config::RawEwwConfig::read_from_file(&self.config_file_path).and_then(config::EwwConfig::generate); match config_result { Ok(new_config) => self.handle_command(DaemonCommand::UpdateConfig(new_config)), Err(e) => errors.push(e), @@ -211,7 +211,7 @@ impl App { gtk::main_quit(); } - fn update_state(&mut self, fieldname: VarName, value: PrimitiveValue) -> Result<()> { + fn update_state(&mut self, fieldname: VarName, value: PrimitiveValue) { self.eww_state.update_variable(fieldname, value) } @@ -247,13 +247,7 @@ impl App { let mut window_def = self.eww_config.get_window(window_name)?.clone(); window_def.geometry = window_def.geometry.override_if_given(anchor, pos, size); - let root_widget = widgets::widget_use_to_gtk_widget( - &self.eww_config.get_widgets(), - &mut self.eww_state, - window_name, - &maplit::hashmap! { "window_name".into() => AttrValue::from_primitive(window_name.to_string()) }, - &window_def.widget, - )?; + let root_widget = window_def.widget.render(&mut self.eww_state, window_name)?; root_widget.get_style_context().add_class(&window_name.to_string()); let monitor_geometry = get_monitor_geometry(window_def.screen_number.unwrap_or_else(get_default_monitor_index)); diff --git a/src/config/element.rs b/src/config/element.rs index b7924d8..a3ea17a 100644 --- a/src/config/element.rs +++ b/src/config/element.rs @@ -35,11 +35,6 @@ impl WidgetDefinition { } } } - - /// returns all the variables that are referenced in this widget - pub fn referenced_vars(&self) -> impl Iterator { - self.structure.referenced_vars() - } } #[derive(Debug, Clone, Default)] @@ -106,17 +101,6 @@ impl WidgetUse { self.text_pos = Some(text_pos); self } - - pub fn get_attr(&self, key: &str) -> Result<&AttrValue> { - self.attrs - .get(key) - .context(format!("attribute '{}' missing from widgetuse of '{}'", key, &self.name)) - } - - /// 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()) - } } #[cfg(test)] diff --git a/src/config/eww_config.rs b/src/config/eww_config.rs index ff0f0c2..87df525 100644 --- a/src/config/eww_config.rs +++ b/src/config/eww_config.rs @@ -9,22 +9,87 @@ use crate::{ use super::{ element::WidgetDefinition, xml_ext::{XmlElement, XmlNode}, - EwwWindowDefinition, ScriptVar, WindowName, + EwwWindowDefinition, RawEwwWindowDefinition, ScriptVar, WindowName, }; use std::path::PathBuf; +/// Eww configuration structure. #[derive(Debug, Clone)] -/// Structure to hold the eww config pub struct EwwConfig { - widgets: HashMap, windows: HashMap, initial_variables: HashMap, script_vars: HashMap, pub filepath: PathBuf, } - impl EwwConfig { - pub fn merge_includes(mut eww_config: EwwConfig, includes: Vec) -> Result { + pub fn read_from_file>(path: P) -> Result { + Ok(Self::generate(RawEwwConfig::read_from_file(path)?)?) + } + + pub fn generate(conf: RawEwwConfig) -> Result { + let RawEwwConfig { + windows, + initial_variables, + script_vars, + filepath, + widgets, + } = conf; + Ok(EwwConfig { + initial_variables, + script_vars, + filepath, + windows: windows + .into_iter() + .map(|(name, window)| { + Ok(( + name, + EwwWindowDefinition::generate(&widgets, window).context("Failed expand window definition")?, + )) + }) + .collect::>>()?, + }) + } + + // TODO this is kinda ugly + pub fn generate_initial_state(&self) -> Result> { + let mut vars = self + .script_vars + .iter() + .map(|var| Ok((var.0.clone(), var.1.initial_value()?))) + .collect::>>()?; + vars.extend(self.initial_variables.clone()); + Ok(vars) + } + + pub fn get_windows(&self) -> &HashMap { + &self.windows + } + + pub fn get_window(&self, name: &WindowName) -> Result<&EwwWindowDefinition> { + self.windows + .get(name) + .with_context(|| format!("No window named '{}' exists", name)) + } + + pub fn get_script_var(&self, name: &VarName) -> Result<&ScriptVar> { + self.script_vars + .get(name) + .with_context(|| format!("No script var named '{}' exists", name)) + } +} + +/// Raw Eww configuration, before expanding widget usages. +#[derive(Debug, Clone)] +pub struct RawEwwConfig { + widgets: HashMap, + windows: HashMap, + initial_variables: HashMap, + script_vars: HashMap, + pub filepath: PathBuf, +} + +impl RawEwwConfig { + pub fn merge_includes(mut eww_config: RawEwwConfig, includes: Vec) -> Result { let config_path = eww_config.filepath.clone(); let log_conflict = |what: &str, conflict: &str, included_path: &std::path::PathBuf| { eprintln!( @@ -37,16 +102,16 @@ impl EwwConfig { }; for included_config in includes { - for conflict in extend_safe(&mut eww_config.widgets, included_config.widgets) { + for conflict in util::extend_safe(&mut eww_config.widgets, included_config.widgets) { log_conflict("widget", &conflict, &included_config.filepath) } - for conflict in extend_safe(&mut eww_config.windows, included_config.windows) { + for conflict in util::extend_safe(&mut eww_config.windows, included_config.windows) { log_conflict("window", &conflict.to_string(), &included_config.filepath) } - for conflict in extend_safe(&mut eww_config.script_vars, included_config.script_vars) { + for conflict in util::extend_safe(&mut eww_config.script_vars, included_config.script_vars) { log_conflict("script-var", &conflict.to_string(), &included_config.filepath) } - for conflict in extend_safe(&mut eww_config.initial_variables, included_config.initial_variables) { + for conflict in util::extend_safe(&mut eww_config.initial_variables, included_config.initial_variables) { log_conflict("var", &conflict.to_string(), &included_config.filepath) } } @@ -60,16 +125,16 @@ impl EwwConfig { let root_node = XmlNode::from(document.root_element()); let root_element = root_node.as_element()?; - let (config, included_paths) = EwwConfig::from_xml_element(root_element.clone(), path.as_ref()) + let (config, included_paths) = Self::from_xml_element(root_element.clone(), path.as_ref()) .with_context(|| format!("Error parsing eww config file {}", path.as_ref().display()))?; let parsed_includes = included_paths .into_iter() - .map(|included_path| EwwConfig::read_from_file(included_path)) + .map(|included_path| Self::read_from_file(included_path)) .collect::>>() .with_context(|| format!("Included in {}", path.as_ref().display()))?; - EwwConfig::merge_includes(config, parsed_includes) + Self::merge_includes(config, parsed_includes) .context("Failed to merge included files into parent configuration file")? }; result.with_context(|| format!("Failed to load eww config file {}", path.as_ref().display())) @@ -83,7 +148,7 @@ impl EwwConfig { .child_elements() .map(|child| { crate::ensure_xml_tag_is!(child, "file"); - Ok(join_path_pretty(path, PathBuf::from(child.attr("path")?))) + Ok(util::join_path_pretty(path, PathBuf::from(child.attr("path")?))) }) .collect::>>()?, None => Default::default(), @@ -106,7 +171,7 @@ impl EwwConfig { Some(tag) => tag .child_elements() .map(|child| { - let def = EwwWindowDefinition::from_xml_element(&child).with_context(|| { + let def = RawEwwWindowDefinition::from_xml_element(&child).with_context(|| { format!("Error parsing window definition at {}:{}", path.display(), &child.text_pos()) })?; Ok((def.name.to_owned(), def)) @@ -120,7 +185,7 @@ impl EwwConfig { None => Default::default(), }; - let config = EwwConfig { + let config = RawEwwConfig { widgets: definitions, windows, initial_variables, @@ -129,45 +194,6 @@ impl EwwConfig { }; Ok((config, included_paths)) } - - // TODO this is kinda ugly - pub fn generate_initial_state(&self) -> Result> { - let mut vars = self - .script_vars - .iter() - .map(|var| Ok((var.0.clone(), var.1.initial_value()?))) - .collect::>>()?; - vars.extend(self.get_default_vars().clone()); - Ok(vars) - } - - pub fn get_widgets(&self) -> &HashMap { - &self.widgets - } - - pub fn get_windows(&self) -> &HashMap { - &self.windows - } - - pub fn get_window(&self, name: &WindowName) -> Result<&EwwWindowDefinition> { - self.windows - .get(name) - .with_context(|| format!("No window named '{}' exists", name)) - } - - pub fn get_default_vars(&self) -> &HashMap { - &self.initial_variables - } - - pub fn get_script_vars(&self) -> Vec { - self.script_vars.values().cloned().collect() - } - - pub fn get_script_var(&self, name: &VarName) -> Result<&ScriptVar> { - self.script_vars - .get(name) - .with_context(|| format!("No script var named '{}' exists", name)) - } } fn parse_variables_block(xml: XmlElement) -> Result<(HashMap, HashMap)> { @@ -176,12 +202,11 @@ fn parse_variables_block(xml: XmlElement) -> Result<(HashMap { - let var_name = VarName(node.attr("name")?.to_owned()); let value = node .only_child() .map(|c| c.as_text_or_sourcecode()) .unwrap_or_else(|_| String::new()); - normal_vars.insert(var_name, PrimitiveValue::from_string(value)); + normal_vars.insert(VarName(node.attr("name")?.to_owned()), PrimitiveValue::from_string(value)); } "script-var" => { let script_var = ScriptVar::from_xml_element(node)?; @@ -193,33 +218,9 @@ fn parse_variables_block(xml: XmlElement) -> Result<(HashMap, P2: AsRef>(a: P, b: P2) -> PathBuf { - let a = a.as_ref(); - let b = b.as_ref(); - if b.is_absolute() { - b.to_path_buf() - } else { - a.parent().unwrap().join(b.strip_prefix("./").unwrap_or(&b)) - } -} - -/// extends a hashmap, returning a list of keys that already where present in the hashmap. -fn extend_safe>( - a: &mut HashMap, - b: T, -) -> Vec { - b.into_iter() - .filter_map(|(k, v)| a.insert(k.clone(), v).map(|_| k.clone())) - .collect() -} - #[cfg(test)] mod test { - use crate::config::{EwwConfig, XmlNode}; + use crate::config::{RawEwwConfig, XmlNode}; use std::collections::HashMap; #[test] @@ -274,13 +275,13 @@ mod test { let document1 = roxmltree::Document::parse(&input1).unwrap(); let document2 = roxmltree::Document::parse(input2).unwrap(); - let config1 = EwwConfig::from_xml_element(XmlNode::from(document1.root_element()).as_element().unwrap().clone(), "") + let config1 = RawEwwConfig::from_xml_element(XmlNode::from(document1.root_element()).as_element().unwrap().clone(), "") .unwrap() .0; - let config2 = EwwConfig::from_xml_element(XmlNode::from(document2.root_element()).as_element().unwrap().clone(), "") + let config2 = RawEwwConfig::from_xml_element(XmlNode::from(document2.root_element()).as_element().unwrap().clone(), "") .unwrap() .0; - let base_config = EwwConfig { + let base_config = RawEwwConfig { widgets: HashMap::new(), windows: HashMap::new(), initial_variables: HashMap::new(), @@ -288,7 +289,7 @@ mod test { filepath: "test_path".into(), }; - let merged_config = EwwConfig::merge_includes(base_config, vec![config1, config2]).unwrap(); + let merged_config = RawEwwConfig::merge_includes(base_config, vec![config1, config2]).unwrap(); assert_eq!(merged_config.widgets.len(), 2); assert_eq!(merged_config.windows.len(), 2); diff --git a/src/config/script_var.rs b/src/config/script_var.rs index d71b18e..b50f8b3 100644 --- a/src/config/script_var.rs +++ b/src/config/script_var.rs @@ -41,7 +41,9 @@ impl ScriptVar { pub fn initial_value(&self) -> Result { match self { - ScriptVar::Poll(x) => Ok(run_command(&x.command)?), + ScriptVar::Poll(x) => { + run_command(&x.command).with_context(|| format!("Failed to compute initial value for {}", &self.name())) + } ScriptVar::Tail(_) => Ok(PrimitiveValue::from_string(String::new())), } } diff --git a/src/config/window_definition.rs b/src/config/window_definition.rs index 3a178c4..6527079 100644 --- a/src/config/window_definition.rs +++ b/src/config/window_definition.rs @@ -1,28 +1,42 @@ -use crate::{ensure_xml_tag_is, value::NumWithUnit}; +use crate::{ensure_xml_tag_is, value::NumWithUnit, widgets::widget_node}; use anyhow::*; use derive_more::*; use serde::{Deserialize, Serialize}; use smart_default::SmartDefault; +use std::collections::HashMap; use super::*; -#[derive(Debug, Clone, Copy, Eq, PartialEq, smart_default::SmartDefault)] -pub enum Side { - #[default] - Top, - Left, - Right, - Bottom, -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] -pub struct StrutDefinition { - pub side: Side, - pub dist: NumWithUnit, -} - -#[derive(Debug, Clone, PartialEq)] +/// Full window-definition containing the fully expanded widget tree. +/// **Use this** rather than `[RawEwwWindowDefinition]`. +#[derive(Debug, Clone)] pub struct EwwWindowDefinition { + pub name: WindowName, + pub geometry: EwwWindowGeometry, + pub stacking: WindowStacking, + pub screen_number: Option, + pub widget: Box, + pub struts: StrutDefinition, + pub focusable: bool, +} + +impl EwwWindowDefinition { + pub fn generate(defs: &HashMap, window: RawEwwWindowDefinition) -> Result { + Ok(EwwWindowDefinition { + name: window.name, + geometry: window.geometry, + stacking: window.stacking, + screen_number: window.screen_number, + widget: widget_node::generate_generic_widget_node(defs, &HashMap::new(), window.widget)?, + struts: window.struts, + focusable: window.focusable, + }) + } +} + +/// Window-definition storing the raw WidgetUse, as received directly from parsing. +#[derive(Debug, Clone, PartialEq)] +pub struct RawEwwWindowDefinition { pub name: WindowName, pub geometry: EwwWindowGeometry, pub stacking: WindowStacking, @@ -32,7 +46,7 @@ pub struct EwwWindowDefinition { pub focusable: bool, } -impl EwwWindowDefinition { +impl RawEwwWindowDefinition { pub fn from_xml_element(xml: &XmlElement) -> Result { ensure_xml_tag_is!(xml, "window"); let stacking: WindowStacking = xml.parse_optional_attr("stacking")?.unwrap_or_default(); @@ -44,11 +58,11 @@ impl EwwWindowDefinition { let struts: Option = xml .child("reserve") .ok() - .map(parse_strut_definition) + .map(StrutDefinition::from_xml_element) .transpose() .context("Failed to parse ")?; - Ok(EwwWindowDefinition { + Ok(RawEwwWindowDefinition { name: WindowName(xml.attr("name")?.to_owned()), geometry: match xml.child("geometry") { Ok(node) => EwwWindowGeometry::from_xml_element(node)?, @@ -61,30 +75,45 @@ impl EwwWindowDefinition { struts: struts.unwrap_or_default(), }) } +} - /// returns all the variables that are referenced in this window - pub fn referenced_vars(&self) -> impl Iterator { - self.widget.referenced_vars() +#[derive(Debug, Clone, Copy, Eq, PartialEq, smart_default::SmartDefault)] +pub enum Side { + #[default] + Top, + Left, + Right, + Bottom, +} +impl std::str::FromStr for Side { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "l" | "left" => Ok(Side::Left), + "r" | "right" => Ok(Side::Right), + "t" | "top" => Ok(Side::Top), + "b" | "bottom" => Ok(Side::Bottom), + _ => Err(anyhow!( + "Failed to parse {} as valid side. Must be one of \"left\", \"right\", \"top\", \"bottom\"", + s + )), + } } } -fn parse_strut_definition(xml: XmlElement) -> Result { - Ok(StrutDefinition { - side: parse_side(xml.attr("side")?)?, - dist: xml.attr("distance")?.parse()?, - }) +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] +pub struct StrutDefinition { + pub side: Side, + pub dist: NumWithUnit, } -fn parse_side(s: &str) -> Result { - match s { - "l" | "left" => Ok(Side::Left), - "r" | "right" => Ok(Side::Right), - "t" | "top" => Ok(Side::Top), - "b" | "bottom" => Ok(Side::Bottom), - _ => Err(anyhow!( - "Failed to parse {} as valid side. Must be one of \"left\", \"right\", \"top\", \"bottom\"", - s - )), +impl StrutDefinition { + pub fn from_xml_element(xml: XmlElement) -> Result { + Ok(StrutDefinition { + side: xml.attr("side")?.parse()?, + dist: xml.attr("distance")?.parse()?, + }) } } diff --git a/src/eww_state.rs b/src/eww_state.rs index dbfc76b..40bed5a 100644 --- a/src/eww_state.rs +++ b/src/eww_state.rs @@ -7,11 +7,11 @@ use std::{collections::HashMap, sync::Arc}; use crate::value::{AttrValue, PrimitiveValue}; -/// Handler that get's executed to apply the necessary parts of the eww state to +/// 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: Vec<(AttrName, AttrValue)>, + unresolved_values: HashMap, } impl StateChangeHandler { @@ -24,19 +24,16 @@ impl StateChangeHandler { fn run_with_state(&self, state: &HashMap) { let resolved_attrs = self .unresolved_values - .iter() - .cloned() + .clone() + .into_iter() .map(|(attr_name, value)| Ok((attr_name, value.resolve_fully(state)?))) .collect::>(); match resolved_attrs { Ok(resolved_attrs) => { - let result: Result<_> = (self.func)(resolved_attrs); - crate::print_result_err!("while updating UI based after state change", &result); - } - Err(err) => { - eprintln!("Error while resolving attributes: {:?}", err); + crate::print_result_err!("while updating UI based after state change", &(self.func)(resolved_attrs)) } + Err(err) => eprintln!("Error while resolving attributes: {:?}", err), } } } @@ -100,73 +97,56 @@ impl EwwState { /// Update the value of a variable, running all registered /// [StateChangeHandler]s. - pub fn update_variable(&mut self, key: VarName, value: PrimitiveValue) -> Result<()> { + pub fn update_variable(&mut self, key: VarName, value: PrimitiveValue) { self.variables_state.insert(key.clone(), value); - let handlers = self - .windows + // run all of the handlers + self.windows .values() .filter_map(|window_state| window_state.state_change_handlers.get(&key)) - .flatten(); - - for handler in handlers { - handler.run_with_state(&self.variables_state) - } - Ok(()) + .flatten() + .for_each(|handler| handler.run_with_state(&self.variables_state)); } - /// resolves a value if possible, using the current eww_state - /// Expects there to be at max one level of nesting var_refs from local-env. - /// This means that no elements in the local_env may be var-refs into the - /// local_env again, but only into the global state. - pub fn resolve_once<'a>( - &'a self, - local_env: &'a HashMap, - value: &'a AttrValue, - ) -> Result { + /// 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<&PrimitiveValue> { + 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 AttrValue) -> Result { value .iter() .map(|element| match element { AttrValueElement::Primitive(primitive) => Ok(primitive.clone()), - AttrValueElement::VarRef(var_name) => self - .variables_state - .get(var_name) - .cloned() - .or_else(|| local_env.get(var_name).and_then(|x| self.resolve_once(local_env, x).ok())) - .with_context(|| format!("Unknown variable '{}' referenced", var_name)), + AttrValueElement::VarRef(var_name) => self.lookup(var_name).cloned(), }) .collect() } /// Resolve takes a function that applies a set of fully resolved attribute - /// values to it's gtk widget. Expects there to be at max one level of - /// nesting var_refs from local-env. This means that no elements in the - /// local_env may be var-refs into the local_env again, but only into the - /// global state. + /// values to it's gtk widget. pub fn resolve) -> Result<()> + 'static + Clone>( &mut self, window_name: &WindowName, - local_env: &HashMap, required_attributes: HashMap, set_value: F, ) { let handler = StateChangeHandler { func: Box::new(set_value), - unresolved_values: required_attributes - .into_iter() - .map(|(attr_name, attr_value)| (attr_name, attr_value.resolve_one_level(local_env))) - .collect(), + 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() { - let window_state = self - .windows + self.windows .entry(window_name.clone()) - .or_insert_with(EwwWindowState::default); - window_state.put_handler(handler); + .or_insert_with(EwwWindowState::default) + .put_handler(handler); } } diff --git a/src/util.rs b/src/util.rs index 49c4a7d..a254017 100644 --- a/src/util.rs +++ b/src/util.rs @@ -50,6 +50,30 @@ macro_rules! loop_select { } } +/// Joins two paths while keeping it somewhat pretty. +/// If the second path is absolute, this will just return the second path. +/// If it is relative, it will return the second path joined onto the first path, removing any `./` if present. +/// TODO this is not yet perfect, as it will still leave ../ and multiple ./ etc,... check for a Path::simplify or something. +pub fn join_path_pretty, P2: AsRef>(a: P, b: P2) -> std::path::PathBuf { + let a = a.as_ref(); + let b = b.as_ref(); + if b.is_absolute() { + b.to_path_buf() + } else { + a.parent().unwrap().join(b.strip_prefix("./").unwrap_or(&b)) + } +} + +/// extends a hashmap, returning a list of keys that already where present in the hashmap. +pub fn extend_safe>( + a: &mut std::collections::HashMap, + b: T, +) -> Vec { + b.into_iter() + .filter_map(|(k, v)| a.insert(k.clone(), v).map(|_| k.clone())) + .collect() +} + /// read an scss file, replace all environment variable references within it and /// then parse it into css. pub fn parse_scss_from_file(path: &Path) -> Result { diff --git a/src/value/attr_value.rs b/src/value/attr_value.rs index d87c314..1c7545f 100644 --- a/src/value/attr_value.rs +++ b/src/value/attr_value.rs @@ -43,6 +43,9 @@ impl AttrValue { self.0.iter().filter_map(|x| x.as_var_ref()) } + /// resolve partially. + /// If a var-ref links to another var-ref, that other var-ref is used. + /// If a referenced variable is not found in the given hashmap, returns the var-ref unchanged. pub fn resolve_one_level(self, variables: &HashMap) -> AttrValue { self.into_iter() .flat_map(|entry| match entry { @@ -55,6 +58,9 @@ impl AttrValue { .collect() } + /// resolve fully. + /// As the variables here have to be primitive values, + /// this enforces that var-refs are not linking to other variables. pub fn resolve_fully(self, variables: &HashMap) -> Result { self.into_iter() .map(|element| match element { diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 52d893d..3b6ff19 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -1,17 +1,13 @@ -use crate::{ - config::{element, window_definition::WindowName}, - eww_state::*, - print_result_err, - value::{AttrName, AttrValue, VarName}, -}; +use crate::{config::window_definition::WindowName, eww_state::*, print_result_err, value::AttrName}; use anyhow::*; use gtk::prelude::*; use itertools::Itertools; -use std::{collections::HashMap, process::Command}; +use std::process::Command; use widget_definitions::*; pub mod widget_definitions; +pub mod widget_node; const CMD_STRING_PLACEHODLER: &str = "{}"; @@ -27,71 +23,11 @@ pub(self) fn run_command(cmd: &str, arg: T) { print_result_err!(format!("executing command {}", &cmd), command_result); } -struct BuilderArgs<'a, 'b, 'c, 'd, 'e> { +struct BuilderArgs<'a, 'b, 'c, 'd> { eww_state: &'a mut EwwState, - local_env: &'b HashMap, - widget: &'c element::WidgetUse, + widget: &'b widget_node::Generic, unhandled_attrs: Vec<&'c AttrName>, window_name: &'d WindowName, - widget_definitions: &'e HashMap, -} - -/// Generate a [gtk::Widget] from a [element::WidgetUse]. -/// The widget_use may be using a builtin widget, or a custom -/// [element::WidgetDefinition]. -/// -/// 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 -/// resolving the widget, Or `Ok(None)` if the widget_use just didn't match any -/// widget name. -pub fn widget_use_to_gtk_widget( - widget_definitions: &HashMap, - eww_state: &mut EwwState, - window_name: &WindowName, - local_env: &HashMap, - widget: &element::WidgetUse, -) -> Result { - let builtin_gtk_widget = build_builtin_gtk_widget(widget_definitions, eww_state, window_name, local_env, widget)?; - - let gtk_widget = if let Some(builtin_gtk_widget) = builtin_gtk_widget { - builtin_gtk_widget - } else if let Some(def) = widget_definitions.get(widget.name.as_str()) { - // let mut local_env = local_env.clone(); - - // the attributes that are set on the widget need to be resolved as far as - // possible. If an attribute is a variable reference, it must either reference a - // variable in the current local_env, or in the global state. As we are building - // widgets from the outer most to the most nested, we can resolve attributes at - // every step. This way, any definition that is affected by changes in the - // eww_state will be directly linked to the eww_state's value. Example: - // foo="{{in_eww_state}}" => attr_in_child="{{foo}}" => - // attr_in_nested_child="{{attr_in_child}}" will be resolved step by step. This - // code will first resolve attr_in_child to directly be - // attr_in_child={{in_eww_state}}. then, in the widget_use_to_gtk_widget call of - // that child element, attr_in_nested_child will again be resolved to point to - // the value of attr_in_child, and thus: attr_in_nested_child="{{in_eww_state}}" - let resolved_widget_attr_env = widget - .attrs - .clone() - .into_iter() - .map(|(attr_name, attr_value)| (VarName(attr_name.0), attr_value.resolve_one_level(local_env))) - .collect(); - - let custom_widget = widget_use_to_gtk_widget( - widget_definitions, - eww_state, - window_name, - &resolved_widget_attr_env, - &def.structure, - )?; - custom_widget.get_style_context().add_class(widget.name.as_str()); - custom_widget - } else { - bail!("unknown widget: '{}'", &widget.name); - }; - - Ok(gtk_widget) } /// build a [`gtk::Widget`] out of a [`element::WidgetUse`] that uses a @@ -104,19 +40,15 @@ pub fn widget_use_to_gtk_widget( /// resolving the widget, Or `Ok(None)` if the widget_use just didn't match any /// widget name. fn build_builtin_gtk_widget( - widget_definitions: &HashMap, eww_state: &mut EwwState, window_name: &WindowName, - local_env: &HashMap, - widget: &element::WidgetUse, + widget: &widget_node::Generic, ) -> Result> { let mut bargs = BuilderArgs { eww_state, - local_env, widget, window_name, unhandled_attrs: widget.attrs.keys().collect(), - widget_definitions, }; let gtk_widget = match widget_to_gtk_widget(&mut bargs) { Ok(Some(gtk_widget)) => gtk_widget, @@ -136,8 +68,7 @@ fn build_builtin_gtk_widget( if let Some(gtk_widget) = gtk_widget.dynamic_cast_ref::() { resolve_container_attrs(&mut bargs, >k_widget); for child in &widget.children { - let child_widget = widget_use_to_gtk_widget(widget_definitions, bargs.eww_state, window_name, local_env, child); - let child_widget = child_widget.with_context(|| { + let child_widget = child.render(bargs.eww_state, window_name).with_context(|| { format!( "{}error while building child '{:#?}' of '{}'", widget.text_pos.map(|x| format!("{} |", x)).unwrap_or_default(), @@ -193,7 +124,6 @@ macro_rules! resolve_block { if let Ok(attr_map) = attr_map { $args.eww_state.resolve( $args.window_name, - $args.local_env, attr_map, ::glib::clone!(@strong $gtk_widget => move |attrs| { $( diff --git a/src/widgets/widget_definitions.rs b/src/widgets/widget_definitions.rs index 31c5c46..d92dc34 100644 --- a/src/widgets/widget_definitions.rs +++ b/src/widgets/widget_definitions.rs @@ -1,8 +1,8 @@ use super::{run_command, BuilderArgs}; -use crate::{config, eww_state, resolve_block, value::AttrValue}; +use crate::{config, eww_state, resolve_block, value::AttrValue, widgets::widget_node}; use anyhow::*; use gtk::{prelude::*, ImageExt}; -use std::{cell::RefCell, rc::Rc}; +use std::{cell::RefCell, collections::HashMap, rc::Rc}; // TODO figure out how to // TODO https://developer.gnome.org/gtk3/stable/GtkFixed.html @@ -40,7 +40,7 @@ pub(super) fn resolve_widget_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Wi if let Ok(visible) = bargs .widget .get_attr("visible") - .and_then(|v| bargs.eww_state.resolve_once(bargs.local_env, v)?.as_bool()) + .and_then(|v| bargs.eww_state.resolve_once(v)?.as_bool()) { connect_first_map(gtk_widget, move |w| { if visible { @@ -410,7 +410,6 @@ fn build_gtk_literal(bargs: &mut BuilderArgs) -> Result { // TODO these clones here are dumdum let window_name = bargs.window_name.clone(); - let widget_definitions = bargs.widget_definitions.clone(); resolve_block!(bargs, gtk_widget, { // @prop content - inline Eww XML that will be rendered as a widget. prop(content: as_string) { @@ -418,13 +417,9 @@ fn build_gtk_literal(bargs: &mut BuilderArgs) -> Result { if !content.is_empty() { let document = roxmltree::Document::parse(&content).map_err(|e| anyhow!("Failed to parse eww xml literal: {:?}", e))?; let content_widget_use = config::element::WidgetUse::from_xml_node(document.root_element().into())?; - let child_widget = super::widget_use_to_gtk_widget( - &widget_definitions, - &mut eww_state::EwwState::default(), - &window_name, - &std::collections::HashMap::new(), - &content_widget_use, - )?; + + let widget_node = &*widget_node::generate_generic_widget_node(&HashMap::new(), &HashMap::new(), content_widget_use)?; + let child_widget = widget_node.render( &mut eww_state::EwwState::default(), &window_name)?; gtk_widget.add(&child_widget); child_widget.show(); } diff --git a/src/widgets/widget_node.rs b/src/widgets/widget_node.rs new file mode 100644 index 0000000..0fc98b7 --- /dev/null +++ b/src/widgets/widget_node.rs @@ -0,0 +1,162 @@ +use crate::{ + config::{ + element::{WidgetDefinition, WidgetUse}, + WindowName, + }, + eww_state::EwwState, + value::{AttrName, AttrValue, VarName}, +}; +use anyhow::*; +use dyn_clone; +use std::collections::HashMap; +pub trait WidgetNode: std::fmt::Debug + dyn_clone::DynClone + Send + Sync { + fn get_name(&self) -> &str; + fn get_text_pos(&self) -> Option<&roxmltree::TextPos>; + + /// 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 + /// resolving the widget, Or `Ok(None)` if the widget_use just didn't match any + /// widget name. + fn render(&self, eww_state: &mut EwwState, window_name: &WindowName) -> Result; +} + +dyn_clone::clone_trait_object!(WidgetNode); + +#[derive(Debug, Clone)] +pub struct UserDefined { + name: String, + text_pos: Option, + content: Box, +} + +impl WidgetNode for UserDefined { + fn get_name(&self) -> &str { + &self.name + } + + fn get_text_pos(&self) -> Option<&roxmltree::TextPos> { + self.text_pos.as_ref() + } + + fn render(&self, eww_state: &mut EwwState, window_name: &WindowName) -> Result { + self.content.render(eww_state, window_name) + } +} + +#[derive(Debug, Clone)] +pub struct Generic { + pub name: String, + pub text_pos: Option, + pub children: Vec>, + pub attrs: HashMap, +} + +impl Generic { + pub fn get_attr(&self, key: &str) -> Result<&AttrValue> { + self.attrs + .get(key) + .context(format!("attribute '{}' missing from use of '{}'", key, &self.name)) + } + + /// 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()) + } +} + +impl WidgetNode for Generic { + fn get_name(&self) -> &str { + &self.name + } + + fn get_text_pos(&self) -> Option<&roxmltree::TextPos> { + self.text_pos.as_ref() + } + + fn render(&self, eww_state: &mut EwwState, window_name: &WindowName) -> Result { + Ok(crate::widgets::build_builtin_gtk_widget(eww_state, window_name, &self)? + .with_context(|| format!("Unknown widget '{}'", self.get_name()))?) + } +} + +pub fn generate_generic_widget_node( + defs: &HashMap, + local_env: &HashMap, + w: WidgetUse, +) -> Result> { + if let Some(def) = defs.get(&w.name) { + ensure!(w.children.is_empty(), "User-defined widgets cannot be given children."); + + let new_local_env = w + .attrs + .into_iter() + .map(|(name, value)| (VarName(name.0), value.resolve_one_level(local_env))) + .collect::>(); + + let content = generate_generic_widget_node(defs, &new_local_env, def.structure.clone())?; + Ok(Box::new(UserDefined { + name: w.name, + text_pos: w.text_pos, + content, + })) + } else { + Ok(Box::new(Generic { + name: w.name, + text_pos: w.text_pos, + attrs: w + .attrs + .into_iter() + .map(|(name, value)| (name, value.resolve_one_level(local_env))) + .collect(), + children: w + .children + .into_iter() + .map(|child| generate_generic_widget_node(defs, local_env, child)) + .collect::>>()?, + })) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::config::xml_ext::*; + use maplit::hashmap; + use pretty_assertions::assert_eq; + #[test] + fn test_generic_generate() { + let w_def1 = { + let input = r#"{{nested1}}{{raw1}}"#; + let document = roxmltree::Document::parse(input).unwrap(); + let xml = XmlNode::from(document.root_element().clone()); + WidgetDefinition::from_xml_element(&xml.as_element().unwrap()).unwrap() + }; + let w_def2 = { + let input = r#""#; + let document = roxmltree::Document::parse(input).unwrap(); + let xml = XmlNode::from(document.root_element().clone()); + WidgetDefinition::from_xml_element(&xml.as_element().unwrap()).unwrap() + }; + let w_use = { + let input = r#""#; + let document = roxmltree::Document::parse(input).unwrap(); + let xml = XmlNode::from(document.root_element().clone()); + WidgetUse::from_xml_node(xml).unwrap() + }; + + let generic = generate_generic_widget_node( + &hashmap! { "foo".to_string() => w_def1, "bar".to_string() => w_def2 }, + &HashMap::new(), + w_use, + ) + .unwrap(); + + // TODO actually implement this test ._. + + dbg!(&generic); + // panic!("REEEEEEEEEE") + } +}