State rework (#335)
This commit is contained in:
parent
0fa5c92e04
commit
6fec568176
34 changed files with 1955 additions and 948 deletions
21
Cargo.lock
generated
21
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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<RefCell<ScopeGraph>>,
|
||||
pub eww_config: config::EwwConfig,
|
||||
pub open_windows: HashMap<String, EwwWindow>,
|
||||
/// Window names that are supposed to be open, but failed.
|
||||
|
@ -84,14 +89,24 @@ pub struct App {
|
|||
pub failed_windows: HashSet<String>,
|
||||
pub css_provider: gtk::CssProvider,
|
||||
|
||||
#[debug_stub = "ScriptVarHandler(...)"]
|
||||
pub app_evt_send: UnboundedSender<DaemonCommand>,
|
||||
#[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))
|
||||
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")
|
||||
};
|
||||
.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<Coords>,
|
||||
size: Option<Coords>,
|
||||
monitor: Option<i32>,
|
||||
|
@ -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<String> =
|
||||
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<Item = &VarName> {
|
||||
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<Item = &'a VarName> {
|
||||
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<EwwWindow> {
|
||||
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.
|
||||
|
|
|
@ -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<Option<DaemonResponse>> {
|
||||
log::debug!("Forwarding options to server");
|
||||
stream.set_nonblocking(false).context("Failed to set stream to non-blocking")?;
|
||||
|
|
|
@ -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<Path>) -> Result<EwwConfig> {
|
||||
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<Path>) -> Result<EwwConfig> {
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct EwwConfig {
|
||||
widgets: HashMap<String, WidgetDefinition>,
|
||||
windows: HashMap<String, EwwWindowDefinition>,
|
||||
windows: HashMap<String, WindowDefinition>,
|
||||
initial_variables: HashMap<VarName, DynVal>,
|
||||
script_vars: HashMap<VarName, ScriptVarDefinition>,
|
||||
|
||||
|
@ -65,10 +66,7 @@ impl EwwConfig {
|
|||
});
|
||||
|
||||
Ok(EwwConfig {
|
||||
windows: window_definitions
|
||||
.into_iter()
|
||||
.map(|(name, window)| Ok((name, EwwWindowDefinition::generate(&widget_definitions, window)?)))
|
||||
.collect::<Result<HashMap<_, _>>>()?,
|
||||
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<String, EwwWindowDefinition> {
|
||||
pub fn get_windows(&self) -> &HashMap<String, WindowDefinition> {
|
||||
&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, \
|
||||
|
|
|
@ -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<WindowGeometry>,
|
||||
pub stacking: WindowStacking,
|
||||
pub monitor_number: Option<i32>,
|
||||
pub widget: Box<dyn widget_node::WidgetNode>,
|
||||
pub resizable: bool,
|
||||
pub backend_options: BackendWindowOptions,
|
||||
}
|
||||
|
||||
impl EwwWindowDefinition {
|
||||
pub fn generate(defs: &HashMap<String, WidgetDefinition>, window: WindowDefinition) -> Result<Self> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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<gtk::Window> {
|
||||
pub fn initialize_window(_window_def: &WindowDefinition, _monitor: gdk::Rectangle) -> Option<gtk::Window> {
|
||||
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<gtk::Window> {
|
||||
pub fn initialize_window(window_def: &WindowDefinition, monitor: gdk::Rectangle) -> Option<gtk::Window> {
|
||||
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<gtk::Window> {
|
||||
pub fn initialize_window(window_def: &WindowDefinition, _monitor: gdk::Rectangle) -> Option<gtk::Window> {
|
||||
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 =
|
||||
|
|
|
@ -63,15 +63,6 @@ pub fn anyhow_err_to_diagnostic(err: &anyhow::Error) -> Option<Diagnostic<usize>
|
|||
}
|
||||
}
|
||||
|
||||
// pub fn print_diagnostic(diagnostic: codespan_reporting::diagnostic::Diagnostic<usize>) {
|
||||
// match stringify_diagnostic(diagnostic.clone()) {
|
||||
// Ok(diag) => {
|
||||
// eprintln!("{}", diag);
|
||||
//}
|
||||
// Err(_) => {
|
||||
// log::error!("{:?}", diagnostic);
|
||||
//}
|
||||
|
||||
pub fn stringify_diagnostic(mut diagnostic: codespan_reporting::diagnostic::Diagnostic<usize>) -> anyhow::Result<String> {
|
||||
diagnostic.labels.drain_filter(|label| Span(label.range.start, label.range.end, label.file_id).is_dummy());
|
||||
|
||||
|
|
|
@ -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<dyn Fn(HashMap<AttrName, DynVal>) -> Result<()> + 'static>,
|
||||
unresolved_values: HashMap<AttrName, SimplExpr>,
|
||||
}
|
||||
|
||||
impl StateChangeHandler {
|
||||
fn used_variables(&self) -> impl Iterator<Item = &VarName> {
|
||||
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<VarName, DynVal>) {
|
||||
let resolved_attrs = self
|
||||
.unresolved_values
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(attr_name, value)| Ok((attr_name, value.eval(state)?)))
|
||||
.collect::<Result<_>>();
|
||||
|
||||
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<VarName, Vec<Arc<StateChangeHandler>>>,
|
||||
}
|
||||
|
||||
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<String, EwwWindowState>,
|
||||
variables_state: HashMap<VarName, DynVal>,
|
||||
}
|
||||
|
||||
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<VarName, DynVal>) -> Self {
|
||||
EwwState { variables_state: defaults, ..EwwState::default() }
|
||||
}
|
||||
|
||||
pub fn get_variables(&self) -> &HashMap<VarName, DynVal> {
|
||||
&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<DynVal> {
|
||||
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<F: Fn(HashMap<AttrName, DynVal>) -> Result<()> + 'static + Clone>(
|
||||
&mut self,
|
||||
window_name: &str,
|
||||
required_attributes: HashMap<AttrName, SimplExpr>,
|
||||
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<Item = &VarName> {
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -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,22 +153,23 @@ 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<Option<DaemonResponse>> {
|
||||
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 {
|
||||
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);
|
||||
bail!("Error, server command failed");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn attempt_connect(socket_path: impl AsRef<Path>, attempts: usize) -> Option<net::UnixStream> {
|
||||
for _ in 0..attempts {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ pub fn init(evt_send: UnboundedSender<DaemonCommand>) -> ScriptVarHandlerHandle
|
|||
}
|
||||
ScriptVarHandlerMsg::StopAll => {
|
||||
handler.stop_all();
|
||||
break;
|
||||
}
|
||||
},
|
||||
else => break,
|
||||
|
|
|
@ -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<DaemonCommand>, should_daemonize: bool) -> Result<ForkResult> {
|
||||
|
@ -61,8 +56,13 @@ pub fn initialize_server(paths: EwwPaths, action: Option<DaemonCommand>, 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<DaemonCommand>, 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,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
6
crates/eww/src/state/mod.rs
Normal file
6
crates/eww/src/state/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
mod one_to_n_elements_map;
|
||||
pub mod scope;
|
||||
pub mod scope_graph;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
140
crates/eww/src/state/one_to_n_elements_map.rs
Normal file
140
crates/eww/src/state/one_to_n_elements_map.rs
Normal file
|
@ -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<I, T> {
|
||||
pub(super) child_to_parent: HashMap<I, (I, T)>,
|
||||
pub(super) parent_to_children: HashMap<I, HashSet<I>>,
|
||||
}
|
||||
|
||||
impl<I: Copy + std::hash::Hash + std::cmp::Eq + std::fmt::Debug, T> OneToNElementsMap<I, T> {
|
||||
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<I> {
|
||||
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<I> {
|
||||
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());
|
||||
|
||||
}
|
||||
}
|
39
crates/eww/src/state/scope.rs
Normal file
39
crates/eww/src/state/scope.rs
Normal file
|
@ -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<ScopeIndex>,
|
||||
pub data: HashMap<VarName, DynVal>,
|
||||
/// 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<VarName, Vec<Rc<Listener>>>,
|
||||
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<ScopeIndex>, data: HashMap<VarName, DynVal>) -> Self {
|
||||
Self { name, ancestor: created_by, data, listeners: HashMap::new(), node_index: ScopeIndex(0) }
|
||||
}
|
||||
}
|
||||
|
||||
pub type ListenerFn = Box<dyn Fn(&mut ScopeGraph, HashMap<VarName, DynVal>) -> Result<()>>;
|
||||
|
||||
pub struct Listener {
|
||||
pub needed_variables: Vec<VarName>,
|
||||
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()
|
||||
}
|
||||
}
|
724
crates/eww/src/state/scope_graph.rs
Normal file
724
crates/eww/src/state/scope_graph.rs
Normal file
|
@ -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<ScopeGraphEvent>,
|
||||
}
|
||||
|
||||
impl ScopeGraph {
|
||||
pub fn from_global_vars(vars: HashMap<VarName, DynVal>, event_sender: UnboundedSender<ScopeGraphEvent>) -> 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<VarName, DynVal>) {
|
||||
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<VarName> {
|
||||
self.variables_used_in_self_or_subscopes_of(self.root_index)
|
||||
}
|
||||
|
||||
pub fn currently_unused_globals(&self) -> HashSet<VarName> {
|
||||
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::<HashSet<_>>().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<DynVal> {
|
||||
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<ScopeIndex>,
|
||||
calling_scope: ScopeIndex,
|
||||
attributes: HashMap<AttrName, SimplExpr>,
|
||||
) -> Result<ScopeIndex> {
|
||||
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<ScopeIndex> {
|
||||
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<VarName> {
|
||||
if let Some(scope) = self.scope_at(index) {
|
||||
let mut variables: HashSet<VarName> = 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<HashMap<VarName, DynVal>> {
|
||||
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::<Result<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
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<VarName>,
|
||||
}
|
||||
|
||||
/// 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<ScopeIndex, Scope>,
|
||||
|
||||
/// Edges from ancestors to descendants
|
||||
pub(super) hierarchy_relations: OneToNElementsMap<ScopeIndex, Vec<ProvidedAttr>>,
|
||||
|
||||
/// Edges from superscopes to subscopes.
|
||||
pub(super) inheritance_relations: OneToNElementsMap<ScopeIndex, Inherits>,
|
||||
}
|
||||
|
||||
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<ProvidedAttr>)> {
|
||||
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<ScopeIndex> {
|
||||
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<ScopeIndex> {
|
||||
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::<Vec<_>>(),
|
||||
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::<Vec<_>>()))
|
||||
.collect::<Vec<_>>()
|
||||
))
|
||||
.collect::<Vec<_>>()
|
||||
)
|
||||
.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"
|
||||
);
|
||||
}
|
||||
}
|
158
crates/eww/src/state/test.rs
Normal file
158
crates/eww/src/state/test.rs
Normal file
|
@ -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<AtomicBool>, Box<dyn Fn()>) {
|
||||
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");
|
||||
}
|
263
crates/eww/src/widgets/build_widget.rs
Normal file
263
crates/eww/src/widgets/build_widget.rs
Normal file
|
@ -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<AttrName>,
|
||||
pub widget_defs: Rc<HashMap<String, WidgetDefinition>>,
|
||||
pub custom_widget_invocation: Option<Rc<CustomWidgetInvocation>>,
|
||||
}
|
||||
|
||||
// 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<HashMap<String, WidgetDefinition>>,
|
||||
calling_scope: ScopeIndex,
|
||||
mut widget_use: WidgetUse,
|
||||
custom_widget_invocation: Option<Rc<CustomWidgetInvocation>>,
|
||||
) -> Result<gtk::Widget> {
|
||||
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::<SimplExpr>(&spec.name.0)?
|
||||
.unwrap_or_else(|| SimplExpr::literal(spec.span, "".to_string()))
|
||||
} else {
|
||||
widget_use.attrs.ast_required::<SimplExpr>(&spec.name.0)?
|
||||
};
|
||||
Ok((spec.name.clone(), expr))
|
||||
})
|
||||
.collect::<Result<HashMap<_, _>>>()?;
|
||||
|
||||
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<HashMap<String, WidgetDefinition>>,
|
||||
calling_scope: ScopeIndex,
|
||||
widget_use: WidgetUse,
|
||||
custom_widget_invocation: Option<Rc<CustomWidgetInvocation>>,
|
||||
) -> Result<gtk::Widget> {
|
||||
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::<gtk::Container>() {
|
||||
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<HashMap<String, WidgetDefinition>>,
|
||||
calling_scope: ScopeIndex,
|
||||
gtk_container: >k::Container,
|
||||
widget_use_children: Vec<WidgetUse>,
|
||||
custom_widget_invocation: Option<Rc<CustomWidgetInvocation>>,
|
||||
) -> 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<HashMap<String, WidgetDefinition>>,
|
||||
calling_scope: ScopeIndex,
|
||||
mut widget_use: WidgetUse,
|
||||
gtk_container: >k::Container,
|
||||
custom_widget_invocation: Rc<CustomWidgetInvocation>,
|
||||
) -> Result<()> {
|
||||
assert_eq!(&widget_use.name, "children");
|
||||
|
||||
if let Some(nth) = widget_use.attrs.ast_optional::<SimplExpr>("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<WidgetUse>,
|
||||
}
|
||||
|
||||
/// 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::<gtk::Bin>().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(())
|
||||
}
|
||||
}
|
|
@ -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()?;
|
||||
|
|
98
crates/eww/src/widgets/def_widget_macro.rs
Normal file
98
crates/eww/src/widgets/def_widget_macro.rs
Normal file
|
@ -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<HashMap<eww_shared_util::AttrName, Option<simplexpr::SimplExpr>>> = 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<eww_shared_util::VarName> = 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<T>, 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<T>
|
||||
(@get_value $args:ident, $name:expr, ?) => {
|
||||
$args.widget_use.attrs.ast_optional::<simplexpr::SimplExpr>($name)?.clone()
|
||||
};
|
||||
|
||||
// The attribute has a default value
|
||||
(@get_value $args:ident, $name:expr, = $default:expr) => {
|
||||
Some($args.widget_use.attrs.ast_optional::<simplexpr::SimplExpr>($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::<simplexpr::SimplExpr>($name)?.clone())
|
||||
}
|
||||
}
|
|
@ -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<T: 'static + std::fmt::Display + Send + Sync>(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<T: 'static + std::fmt::Display + Send + Sync>(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<String, WidgetDefinition>,
|
||||
}
|
||||
|
||||
/// 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<String, WidgetDefinition>,
|
||||
widget: &widget_node::Generic,
|
||||
) -> Result<Option<gtk::Widget>> {
|
||||
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::<gtk::Container>() {
|
||||
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);
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
|
|
@ -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<RefCell<Option<gtk::glib::SignalHandlerId>>>;
|
||||
/// 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<std::sync::Mutex<Option<gtk::glib::SignalHandlerId>>> = 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<gtk::Widget> {
|
||||
let gtk_widget = match bargs.widget.name.as_str() {
|
||||
pub(super) fn widget_use_to_gtk_widget(bargs: &mut BuilderArgs) -> Result<gtk::Widget> {
|
||||
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<gtk::Widge
|
|||
"combo-box-text" => 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<HashSet<&str>> =
|
|||
/// 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)))
|
||||
{
|
||||
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| {
|
||||
connect_single_handler!(gtk_widget, gtk_widget.connect_value_changed(move |gtk_widget| {
|
||||
run_command(timeout, &onchange, gtk_widget.value());
|
||||
})
|
||||
));
|
||||
old_id.map(|id| gtk_widget.disconnect(id));
|
||||
|
||||
}));
|
||||
}
|
||||
});
|
||||
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<gtk::Box> {
|
||||
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<gtk::ComboBoxText> {
|
||||
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<gtk::ComboBoxText
|
|||
// @prop timeout - timeout of the command
|
||||
// @prop onchange - runs the code when a item was selected, replacing {} with the item as a string
|
||||
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| {
|
||||
connect_single_handler!(gtk_widget, gtk_widget.connect_changed(move |gtk_widget| {
|
||||
run_command(timeout, &onchange, gtk_widget.active_text().unwrap_or_else(|| "".into()));
|
||||
})
|
||||
));
|
||||
old_id.map(|id| gtk_widget.disconnect(id));
|
||||
}));
|
||||
},
|
||||
});
|
||||
Ok(gtk_widget)
|
||||
|
@ -253,7 +238,7 @@ fn build_gtk_combo_box_text(bargs: &mut BuilderArgs) -> Result<gtk::ComboBoxText
|
|||
/// @desc A widget that can expand and collapse, showing/hiding it's children.
|
||||
fn build_gtk_expander(bargs: &mut BuilderArgs) -> Result<gtk::Expander> {
|
||||
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<gtk::Expander> {
|
|||
/// @desc A widget that can reveal a child with an animation.
|
||||
fn build_gtk_revealer(bargs: &mut BuilderArgs) -> Result<gtk::Revealer> {
|
||||
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<gtk::Revealer> {
|
|||
/// @desc A checkbox that can trigger events on checked / unchecked.
|
||||
fn build_gtk_checkbox(bargs: &mut BuilderArgs) -> Result<gtk::CheckButton> {
|
||||
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| {
|
||||
connect_single_handler!(gtk_widget, 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));
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -303,20 +284,16 @@ fn build_gtk_checkbox(bargs: &mut BuilderArgs) -> Result<gtk::CheckButton> {
|
|||
/// @desc A button opening a color chooser window
|
||||
fn build_gtk_color_button(bargs: &mut BuilderArgs) -> Result<gtk::ColorButton> {
|
||||
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| {
|
||||
connect_single_handler!(gtk_widget, gtk_widget.connect_color_set(move |gtk_widget| {
|
||||
run_command(timeout, &onchange, gtk_widget.rgba());
|
||||
})
|
||||
));
|
||||
old_id.map(|id| gtk_widget.disconnect(id));
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -327,20 +304,16 @@ fn build_gtk_color_button(bargs: &mut BuilderArgs) -> Result<gtk::ColorButton> {
|
|||
/// @desc A color chooser widget
|
||||
fn build_gtk_color_chooser(bargs: &mut BuilderArgs) -> Result<gtk::ColorChooserWidget> {
|
||||
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| {
|
||||
connect_single_handler!(gtk_widget, gtk_widget.connect_color_activated(move |_a, color| {
|
||||
run_command(timeout, &onchange, *color);
|
||||
})
|
||||
));
|
||||
old_id.map(|id| gtk_widget.disconnect(id));
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -352,16 +325,17 @@ fn build_gtk_color_chooser(bargs: &mut BuilderArgs) -> Result<gtk::ColorChooserW
|
|||
fn build_gtk_scale(bargs: &mut BuilderArgs) -> Result<gtk::Scale> {
|
||||
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<gtk::Scale> {
|
|||
/// @desc A progress bar
|
||||
fn build_gtk_progress(bargs: &mut BuilderArgs) -> Result<gtk::ProgressBar> {
|
||||
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<gtk::ProgressBar> {
|
|||
/// @desc An input field. For this to be useful, set `focusable="true"` on the window.
|
||||
fn build_gtk_input(bargs: &mut BuilderArgs) -> Result<gtk::Entry> {
|
||||
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<gtk::Entry> {
|
|||
// @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| {
|
||||
connect_single_handler!(gtk_widget, gtk_widget.connect_changed(move |gtk_widget| {
|
||||
run_command(timeout, &onchange, gtk_widget.text().to_string());
|
||||
})
|
||||
));
|
||||
old_id.map(|id| gtk_widget.disconnect(id));
|
||||
}));
|
||||
}
|
||||
});
|
||||
Ok(gtk_widget)
|
||||
|
@ -415,9 +385,8 @@ fn build_gtk_input(bargs: &mut BuilderArgs) -> Result<gtk::Entry> {
|
|||
/// @desc A button
|
||||
fn build_gtk_button(bargs: &mut BuilderArgs) -> Result<gtk::Button> {
|
||||
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,8 +398,7 @@ fn build_gtk_button(bargs: &mut BuilderArgs) -> Result<gtk::Button> {
|
|||
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| {
|
||||
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, ""),
|
||||
|
@ -438,9 +406,7 @@ fn build_gtk_button(bargs: &mut BuilderArgs) -> Result<gtk::Button> {
|
|||
_ => {},
|
||||
}
|
||||
gtk::Inhibit(false)
|
||||
})
|
||||
));
|
||||
old_id.map(|id| gtk_widget.disconnect(id));
|
||||
}));
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -451,16 +417,16 @@ fn build_gtk_button(bargs: &mut BuilderArgs) -> Result<gtk::Button> {
|
|||
/// @desc A widget displaying an image
|
||||
fn build_gtk_image(bargs: &mut BuilderArgs) -> Result<gtk::Image> {
|
||||
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<gtk::Image> {
|
|||
/// @desc the main layout container
|
||||
fn build_gtk_box(bargs: &mut BuilderArgs) -> Result<gtk::Box> {
|
||||
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<gtk::Box> {
|
|||
/// @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<gtk::Box> {
|
||||
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<gtk::Box> {
|
|||
.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<gtk::Box> {
|
|||
fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result<gtk::EventBox> {
|
||||
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,58 +517,48 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result<gtk::EventBox> {
|
|||
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| {
|
||||
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)
|
||||
})
|
||||
));
|
||||
old_id.map(|id| gtk_widget.disconnect(id));
|
||||
}));
|
||||
},
|
||||
// @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| {
|
||||
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)
|
||||
})
|
||||
));
|
||||
old_id.map(|id| gtk_widget.disconnect(id));
|
||||
}));
|
||||
},
|
||||
// @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| {
|
||||
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)
|
||||
})
|
||||
));
|
||||
old_id.map(|id| gtk_widget.disconnect(id));
|
||||
}));
|
||||
},
|
||||
// @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| {
|
||||
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();
|
||||
|
@ -613,11 +567,8 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result<gtk::EventBox> {
|
|||
}
|
||||
}
|
||||
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| {
|
||||
}));
|
||||
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 {
|
||||
|
@ -625,9 +576,8 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result<gtk::EventBox> {
|
|||
}
|
||||
}
|
||||
gtk::Inhibit(false)
|
||||
})
|
||||
)).map(|id| gtk_widget.disconnect(id));
|
||||
},
|
||||
}));
|
||||
}
|
||||
});
|
||||
Ok(gtk_widget)
|
||||
}
|
||||
|
@ -637,7 +587,7 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result<gtk::EventBox> {
|
|||
fn build_gtk_label(bargs: &mut BuilderArgs) -> Result<gtk::Label> {
|
||||
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::Box> {
|
|||
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<RefCell<Option<usize>>> = 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("<literal-content>".to_string(), content)?;
|
||||
|
@ -692,12 +643,12 @@ fn build_gtk_literal(bargs: &mut BuilderArgs) -> Result<gtk::Box> {
|
|||
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<gtk::Box> {
|
|||
/// @desc A widget that displays a calendar
|
||||
fn build_gtk_calendar(bargs: &mut BuilderArgs) -> Result<gtk::Calendar> {
|
||||
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<gtk::Calendar> {
|
|||
// @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| {
|
||||
connect_single_handler!(gtk_widget, 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));
|
||||
}));
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -755,7 +702,7 @@ fn build_gtk_calendar(bargs: &mut BuilderArgs) -> Result<gtk::Calendar> {
|
|||
/// @desc A widget that displays a circular progress bar
|
||||
fn build_circular_progress_bar(bargs: &mut BuilderArgs) -> Result<CircProg> {
|
||||
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<gtk::Align> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Connect a function to the first map event of a widget. After that first map, the handler will get disconnected.
|
||||
fn connect_first_map<W: IsA<gtk::Widget>, 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)
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -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<String, WidgetDefinition>,
|
||||
) -> Result<gtk::Widget>;
|
||||
}
|
||||
|
||||
dyn_clone::clone_trait_object!(WidgetNode);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UserDefined {
|
||||
name: String,
|
||||
span: Span,
|
||||
content: Box<dyn WidgetNode>,
|
||||
}
|
||||
|
||||
impl WidgetNode for UserDefined {
|
||||
fn get_name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn render(
|
||||
&self,
|
||||
eww_state: &mut EwwState,
|
||||
window_name: &str,
|
||||
widget_definitions: &HashMap<String, WidgetDefinition>,
|
||||
) -> Result<gtk::Widget> {
|
||||
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<Box<dyn WidgetNode>>,
|
||||
pub attrs: HashMap<AttrName, SimplExpr>,
|
||||
}
|
||||
|
||||
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<Item = &VarName> {
|
||||
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<String, WidgetDefinition>,
|
||||
) -> Result<gtk::Widget> {
|
||||
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<String, WidgetDefinition>,
|
||||
local_env: &HashMap<VarName, SimplExpr>,
|
||||
w: WidgetUse,
|
||||
) -> AstResult<Box<dyn WidgetNode>> {
|
||||
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::<AstResult<HashMap<VarName, _>>>()?;
|
||||
|
||||
// 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::<AstResult<HashMap<_, _>>>()?,
|
||||
|
||||
children: w
|
||||
.children
|
||||
.into_iter()
|
||||
.map(|child| generate_generic_widget_node(defs, local_env, child))
|
||||
.collect::<AstResult<Vec<_>>>()?,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<WidgetUse> {
|
||||
// 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::<usize, _>("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)
|
||||
}
|
|
@ -6,3 +6,4 @@ edition = "2018"
|
|||
[dependencies]
|
||||
serde = {version = "1.0", features = ["derive"]}
|
||||
derive_more = "0.99"
|
||||
ref-cast = "1.0.6"
|
||||
|
|
|
@ -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<str> 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<AttrName> 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<str> for AttrName {
|
||||
fn borrow(&self) -> &str {
|
||||
&self.0
|
||||
|
@ -40,3 +81,9 @@ impl From<&str> for AttrName {
|
|||
AttrName(s.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VarName> for AttrName {
|
||||
fn from(x: VarName) -> Self {
|
||||
AttrName(x.0)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String>) -> 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<T: Into<DynVal>>(s: T) -> Self {
|
||||
Self::Literal(s.into())
|
||||
}
|
||||
|
||||
pub fn collect_var_refs_into(&self, dest: &mut Vec<VarName>) {
|
||||
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);
|
||||
pub fn var_ref(span: Span, n: impl Into<VarName>) -> Self {
|
||||
Self::VarRef(span, n.into())
|
||||
}
|
||||
SimplExpr::IfElse(_, a, b, c) => {
|
||||
|
||||
pub fn references_var(&self, var: &VarName) -> bool {
|
||||
use SimplExpr::*;
|
||||
match self {
|
||||
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<VarName>) {
|
||||
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);
|
||||
}
|
||||
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(_) => {}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -56,7 +56,7 @@ impl YuckFile {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct YuckFiles {
|
||||
files: HashMap<usize, YuckFile>,
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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<I: Iterator<Item = Ast>>(span: Span, iter: AstIterator<I>) -> AstResult<Self>;
|
||||
|
@ -50,7 +50,7 @@ impl<T: FromAstElementContent> FromAst for T {
|
|||
impl FromAst for SimplExpr {
|
||||
fn from_ast(e: Ast) -> AstResult<Self> {
|
||||
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())),
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue