State refactor (#121)

* Add widget_node trait and generic impl

* Use WidgetNode stuff in some places

* Use WidgetNode stuff in rendering code

* Refactor
This commit is contained in:
ElKowar 2021-02-26 21:03:53 +01:00 committed by GitHub
parent 6251a2bc1d
commit a18901f187
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 403 additions and 287 deletions

7
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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));

View file

@ -35,11 +35,6 @@ impl WidgetDefinition {
}
}
}
/// returns all the variables that are referenced in this widget
pub fn referenced_vars(&self) -> impl Iterator<Item = &VarName> {
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<Item = &VarName> {
self.attrs.iter().flat_map(|(_, value)| value.var_refs())
}
}
#[cfg(test)]

View file

@ -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<String, WidgetDefinition>,
windows: HashMap<WindowName, EwwWindowDefinition>,
initial_variables: HashMap<VarName, PrimitiveValue>,
script_vars: HashMap<VarName, ScriptVar>,
pub filepath: PathBuf,
}
impl EwwConfig {
pub fn merge_includes(mut eww_config: EwwConfig, includes: Vec<EwwConfig>) -> Result<EwwConfig> {
pub fn read_from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
Ok(Self::generate(RawEwwConfig::read_from_file(path)?)?)
}
pub fn generate(conf: RawEwwConfig) -> Result<Self> {
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::<Result<HashMap<_, _>>>()?,
})
}
// TODO this is kinda ugly
pub fn generate_initial_state(&self) -> Result<HashMap<VarName, PrimitiveValue>> {
let mut vars = self
.script_vars
.iter()
.map(|var| Ok((var.0.clone(), var.1.initial_value()?)))
.collect::<Result<HashMap<_, _>>>()?;
vars.extend(self.initial_variables.clone());
Ok(vars)
}
pub fn get_windows(&self) -> &HashMap<WindowName, EwwWindowDefinition> {
&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<String, WidgetDefinition>,
windows: HashMap<WindowName, RawEwwWindowDefinition>,
initial_variables: HashMap<VarName, PrimitiveValue>,
script_vars: HashMap<VarName, ScriptVar>,
pub filepath: PathBuf,
}
impl RawEwwConfig {
pub fn merge_includes(mut eww_config: RawEwwConfig, includes: Vec<RawEwwConfig>) -> Result<RawEwwConfig> {
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::<Result<Vec<_>>>()
.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::<Result<Vec<_>>>()?,
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<HashMap<VarName, PrimitiveValue>> {
let mut vars = self
.script_vars
.iter()
.map(|var| Ok((var.0.clone(), var.1.initial_value()?)))
.collect::<Result<HashMap<_, _>>>()?;
vars.extend(self.get_default_vars().clone());
Ok(vars)
}
pub fn get_widgets(&self) -> &HashMap<String, WidgetDefinition> {
&self.widgets
}
pub fn get_windows(&self) -> &HashMap<WindowName, EwwWindowDefinition> {
&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<VarName, PrimitiveValue> {
&self.initial_variables
}
pub fn get_script_vars(&self) -> Vec<ScriptVar> {
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<VarName, PrimitiveValue>, HashMap<VarName, ScriptVar>)> {
@ -176,12 +202,11 @@ fn parse_variables_block(xml: XmlElement) -> Result<(HashMap<VarName, PrimitiveV
for node in xml.child_elements() {
match node.tag_name() {
"var" => {
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<VarName, PrimitiveV
Ok((normal_vars, script_vars))
}
/// 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.
fn join_path_pretty<P: AsRef<std::path::Path>, P2: AsRef<std::path::Path>>(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<K: std::cmp::Eq + std::hash::Hash + Clone, V, T: IntoIterator<Item = (K, V)>>(
a: &mut HashMap<K, V>,
b: T,
) -> Vec<K> {
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);

View file

@ -41,7 +41,9 @@ impl ScriptVar {
pub fn initial_value(&self) -> Result<PrimitiveValue> {
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())),
}
}

View file

@ -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<i32>,
pub widget: Box<dyn widget_node::WidgetNode>,
pub struts: StrutDefinition,
pub focusable: bool,
}
impl EwwWindowDefinition {
pub fn generate(defs: &HashMap<String, WidgetDefinition>, window: RawEwwWindowDefinition) -> Result<Self> {
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<Self> {
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<StrutDefinition> = xml
.child("reserve")
.ok()
.map(parse_strut_definition)
.map(StrutDefinition::from_xml_element)
.transpose()
.context("Failed to parse <reserve>")?;
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<Item = &VarName> {
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<Side> {
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<StrutDefinition> {
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<Side> {
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<Self> {
Ok(StrutDefinition {
side: xml.attr("side")?.parse()?,
dist: xml.attr("distance")?.parse()?,
})
}
}

View file

@ -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<dyn Fn(HashMap<AttrName, PrimitiveValue>) -> Result<()> + 'static>,
unresolved_values: Vec<(AttrName, AttrValue)>,
unresolved_values: HashMap<AttrName, AttrValue>,
}
impl StateChangeHandler {
@ -24,19 +24,16 @@ impl StateChangeHandler {
fn run_with_state(&self, state: &HashMap<VarName, PrimitiveValue>) {
let resolved_attrs = self
.unresolved_values
.iter()
.cloned()
.clone()
.into_iter()
.map(|(attr_name, value)| Ok((attr_name, value.resolve_fully(state)?)))
.collect::<Result<_>>();
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<VarName, AttrValue>,
value: &'a AttrValue,
) -> Result<PrimitiveValue> {
/// 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<PrimitiveValue> {
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<F: Fn(HashMap<AttrName, PrimitiveValue>) -> Result<()> + 'static + Clone>(
&mut self,
window_name: &WindowName,
local_env: &HashMap<VarName, AttrValue>,
required_attributes: HashMap<AttrName, AttrValue>,
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);
}
}

View file

@ -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<P: AsRef<std::path::Path>, P2: AsRef<std::path::Path>>(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<K: std::cmp::Eq + std::hash::Hash + Clone, V, T: IntoIterator<Item = (K, V)>>(
a: &mut std::collections::HashMap<K, V>,
b: T,
) -> Vec<K> {
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<String> {

View file

@ -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<VarName, AttrValue>) -> 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<VarName, PrimitiveValue>) -> Result<PrimitiveValue> {
self.into_iter()
.map(|element| match element {

View file

@ -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<T: std::fmt::Display>(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<VarName, AttrValue>,
widget: &'c element::WidgetUse,
widget: &'b widget_node::Generic,
unhandled_attrs: Vec<&'c AttrName>,
window_name: &'d WindowName,
widget_definitions: &'e HashMap<String, element::WidgetDefinition>,
}
/// 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<String, element::WidgetDefinition>,
eww_state: &mut EwwState,
window_name: &WindowName,
local_env: &HashMap<VarName, AttrValue>,
widget: &element::WidgetUse,
) -> Result<gtk::Widget> {
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<String, element::WidgetDefinition>,
eww_state: &mut EwwState,
window_name: &WindowName,
local_env: &HashMap<VarName, AttrValue>,
widget: &element::WidgetUse,
widget: &widget_node::Generic,
) -> Result<Option<gtk::Widget>> {
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::<gtk::Container>() {
resolve_container_attrs(&mut bargs, &gtk_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| {
$(

View file

@ -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: &gtk::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<gtk::Box> {
// 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<gtk::Box> {
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();
}

162
src/widgets/widget_node.rs Normal file
View file

@ -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<gtk::Widget>;
}
dyn_clone::clone_trait_object!(WidgetNode);
#[derive(Debug, Clone)]
pub struct UserDefined {
name: String,
text_pos: Option<roxmltree::TextPos>,
content: Box<dyn WidgetNode>,
}
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<gtk::Widget> {
self.content.render(eww_state, window_name)
}
}
#[derive(Debug, Clone)]
pub struct Generic {
pub name: String,
pub text_pos: Option<roxmltree::TextPos>,
pub children: Vec<Box<dyn WidgetNode>>,
pub attrs: HashMap<AttrName, AttrValue>,
}
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<Item = &VarName> {
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<gtk::Widget> {
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<String, WidgetDefinition>,
local_env: &HashMap<VarName, AttrValue>,
w: WidgetUse,
) -> Result<Box<dyn WidgetNode>> {
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::<HashMap<_, _>>();
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::<Result<Vec<_>>>()?,
}))
}
}
#[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#"<def name="foo"><box><box>{{nested1}}{{raw1}}</box></box></def>"#;
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#"<def name="bar"><box><foo nested1="{{nested2}}" raw1="raw value"/></box></def>"#;
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#"<bar nested2="{{in_root}}"/>"#;
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")
}
}