From 6b7fa5d55ccd560a3c95b93caa2e945662953db8 Mon Sep 17 00:00:00 2001 From: ElKowar <5300871+elkowar@users.noreply.github.com> Date: Sat, 23 Apr 2022 12:54:48 +0200 Subject: [PATCH] Implement loop widget (#350) --- crates/eww/src/widgets/build_widget.rs | 142 ++++++++++++++++++++----- crates/simplexpr/src/dynval.rs | 1 + crates/yuck/src/config/validate.rs | 73 ++++++++----- crates/yuck/src/config/widget_use.rs | 112 ++++++++++++++++--- crates/yuck/src/error.rs | 8 +- crates/yuck/src/format_diagnostic.rs | 4 + crates/yuck/src/parser/ast_iterator.rs | 20 +++- docs/src/configuration.md | 18 +++- 8 files changed, 302 insertions(+), 76 deletions(-) diff --git a/crates/eww/src/widgets/build_widget.rs b/crates/eww/src/widgets/build_widget.rs index 7a71d4c..897a912 100644 --- a/crates/eww/src/widgets/build_widget.rs +++ b/crates/eww/src/widgets/build_widget.rs @@ -1,16 +1,20 @@ use anyhow::{Context, Result}; use codespan_reporting::diagnostic::Severity; -use eww_shared_util::AttrName; +use eww_shared_util::{AttrName, Spanned}; 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 maplit::hashmap; +use simplexpr::{dynval::DynVal, SimplExpr}; +use std::{cell::RefCell, collections::HashMap, rc::Rc}; use yuck::{ - config::{widget_definition::WidgetDefinition, widget_use::WidgetUse}, + config::{ + widget_definition::WidgetDefinition, + widget_use::{BasicWidgetUse, ChildrenWidgetUse, LoopWidgetUse, WidgetUse}, + }, gen_diagnostic, }; @@ -28,7 +32,7 @@ use super::widget_definitions::{resolve_orientable_attrs, resolve_range_attrs, r pub struct BuilderArgs<'a> { pub calling_scope: ScopeIndex, - pub widget_use: WidgetUse, + pub widget_use: BasicWidgetUse, pub scope_graph: &'a mut ScopeGraph, pub unhandled_attrs: Vec, pub widget_defs: Rc>, @@ -45,7 +49,26 @@ pub fn build_gtk_widget( graph: &mut ScopeGraph, widget_defs: Rc>, calling_scope: ScopeIndex, - mut widget_use: WidgetUse, + widget_use: WidgetUse, + custom_widget_invocation: Option>, +) -> Result { + match widget_use { + WidgetUse::Basic(widget_use) => { + build_basic_gtk_widget(graph, widget_defs, calling_scope, widget_use, custom_widget_invocation) + } + WidgetUse::Loop(_) | WidgetUse::Children(_) => Err(anyhow::anyhow!(DiagError::new(gen_diagnostic! { + msg = "This widget can only be used as a child of some container widget such as box", + label = widget_use.span(), + note = "Hint: try wrapping this in a `box`" + }))), + } +} + +fn build_basic_gtk_widget( + graph: &mut ScopeGraph, + widget_defs: Rc>, + calling_scope: ScopeIndex, + mut widget_use: BasicWidgetUse, custom_widget_invocation: Option>, ) -> Result { if let Some(custom_widget) = widget_defs.clone().get(&widget_use.name) { @@ -100,7 +123,7 @@ fn build_builtin_gtk_widget( graph: &mut ScopeGraph, widget_defs: Rc>, calling_scope: ScopeIndex, - widget_use: WidgetUse, + widget_use: BasicWidgetUse, custom_widget_invocation: Option>, ) -> Result { let mut bargs = BuilderArgs { @@ -160,25 +183,93 @@ fn populate_widget_children( custom_widget_invocation: Option>, ) -> Result<()> { for child in widget_use_children { - if child.name == "children" { - let custom_widget_invocation = custom_widget_invocation.clone().context("Not in a custom widget invocation")?; - build_children_special_widget( - tree, - widget_defs.clone(), - calling_scope, - child, - gtk_container, - custom_widget_invocation, - )?; - } else { - let child_widget = - build_gtk_widget(tree, widget_defs.clone(), calling_scope, child, custom_widget_invocation.clone())?; - gtk_container.add(&child_widget); + match child { + WidgetUse::Children(child) => { + build_children_special_widget( + tree, + widget_defs.clone(), + calling_scope, + child, + gtk_container, + custom_widget_invocation.clone().context("Not in a custom widget invocation")?, + )?; + } + WidgetUse::Loop(child) => { + build_loop_special_widget( + tree, + widget_defs.clone(), + calling_scope, + child, + gtk_container, + custom_widget_invocation.clone(), + )?; + } + _ => { + let child_widget = + build_gtk_widget(tree, widget_defs.clone(), calling_scope, child, custom_widget_invocation.clone())?; + gtk_container.add(&child_widget); + } } } Ok(()) } +fn build_loop_special_widget( + tree: &mut ScopeGraph, + widget_defs: Rc>, + calling_scope: ScopeIndex, + widget_use: LoopWidgetUse, + gtk_container: >k::Container, + custom_widget_invocation: Option>, +) -> Result<()> { + tree.register_listener( + calling_scope, + Listener { + needed_variables: widget_use.elements_expr.collect_var_refs(), + f: Box::new({ + let custom_widget_invocation = custom_widget_invocation.clone(); + let widget_defs = widget_defs.clone(); + let elements_expr = widget_use.elements_expr.clone(); + let elements_expr_span = widget_use.elements_expr_span.clone(); + let element_name = widget_use.element_name.clone(); + let body: WidgetUse = widget_use.body.as_ref().clone(); + let created_children = Rc::new(RefCell::new(Vec::::new())); + let gtk_container = gtk_container.clone(); + move |tree, values| { + let elements_value = elements_expr + .eval(&values)? + .as_json_value()? + .as_array() + .context("Not an array value")? + .into_iter() + .map(DynVal::from) + .collect_vec(); + let mut created_children = created_children.borrow_mut(); + for old_child in created_children.drain(..) { + unsafe { old_child.destroy() }; + } + for element in elements_value { + let scope = tree.register_new_scope( + format!("for {} = {}", element_name.0, element), + Some(calling_scope), + calling_scope, + hashmap! { + element_name.clone().into() => SimplExpr::Literal(DynVal(element.0, elements_expr_span)) + }, + )?; + let new_child_widget = + build_gtk_widget(tree, widget_defs.clone(), scope, body.clone(), custom_widget_invocation.clone())?; + gtk_container.add(&new_child_widget); + created_children.push(new_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 @@ -187,13 +278,12 @@ fn build_children_special_widget( tree: &mut ScopeGraph, widget_defs: Rc>, calling_scope: ScopeIndex, - mut widget_use: WidgetUse, + widget_use: ChildrenWidgetUse, gtk_container: >k::Container, custom_widget_invocation: Rc, ) -> Result<()> { - assert_eq!(&widget_use.name, "children"); - - if let Some(nth) = widget_use.attrs.ast_optional::("nth")? { + if let Some(nth) = widget_use.nth_expr { + // TODORW this might not be necessary, if I can keep a copy of the widget I can destroy it directly, no need to go through the container. // This should be a custom gtk::Bin subclass,.. let child_container = gtk::Box::new(Orientation::Horizontal, 0); child_container.set_homogeneous(true); @@ -250,7 +340,7 @@ pub struct CustomWidgetInvocation { } /// Make sure that [`gtk::Bin`] widgets only get a single child. -fn validate_container_children_count(container: >k::Container, widget_use: &WidgetUse) -> Result<(), DiagError> { +fn validate_container_children_count(container: >k::Container, widget_use: &BasicWidgetUse) -> Result<(), DiagError> { if container.dynamic_cast_ref::().is_some() && widget_use.children.len() > 1 { Err(DiagError::new(gen_diagnostic! { kind = Severity::Error, diff --git a/crates/simplexpr/src/dynval.rs b/crates/simplexpr/src/dynval.rs index ab1faae..a22b914 100644 --- a/crates/simplexpr/src/dynval.rs +++ b/crates/simplexpr/src/dynval.rs @@ -185,6 +185,7 @@ impl DynVal { } } + // TODO this should return Result> and use json parsing pub fn as_vec(&self) -> Result> { if self.0.is_empty() { Ok(Vec::new()) diff --git a/crates/yuck/src/config/validate.rs b/crates/yuck/src/config/validate.rs index 1af9592..054e66c 100644 --- a/crates/yuck/src/config/validate.rs +++ b/crates/yuck/src/config/validate.rs @@ -7,7 +7,11 @@ use crate::{ parser::{ast::Ast, ast_iterator::AstIterator, from_ast::FromAst}, }; -use super::{widget_definition::WidgetDefinition, widget_use::WidgetUse, Config}; +use super::{ + widget_definition::WidgetDefinition, + widget_use::{BasicWidgetUse, WidgetUse}, + Config, +}; use eww_shared_util::{AttrName, Span, Spanned, VarName}; #[derive(Debug, thiserror::Error)] @@ -71,37 +75,52 @@ pub fn validate_variables_in_widget_use( widget: &WidgetUse, is_in_definition: bool, ) -> Result<(), ValidationError> { - let matching_definition = defs.get(&widget.name); - if let Some(matching_def) = matching_definition { - let missing_arg = matching_def - .expected_args - .iter() - .find(|expected| !expected.optional && !widget.attrs.attrs.contains_key(&expected.name)); - if let Some(missing_arg) = missing_arg { - return Err(ValidationError::MissingAttr { - widget_name: widget.name.clone(), - arg_name: missing_arg.name.clone(), - arg_list_span: Some(matching_def.args_span), - use_span: widget.attrs.span, - }); + if let WidgetUse::Basic(widget) = widget { + let matching_definition = defs.get(&widget.name); + if let Some(matching_def) = matching_definition { + let missing_arg = matching_def + .expected_args + .iter() + .find(|expected| !expected.optional && !widget.attrs.attrs.contains_key(&expected.name)); + if let Some(missing_arg) = missing_arg { + return Err(ValidationError::MissingAttr { + widget_name: widget.name.clone(), + arg_name: missing_arg.name.clone(), + arg_list_span: Some(matching_def.args_span), + use_span: widget.attrs.span, + }); + } + } + 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_with_span() + .iter() + .cloned() + .map(|(span, var_ref)| (span, var_ref.clone())) + .find(|(_, var_ref)| !variables.contains(var_ref)) + }); + if let Some((span, var)) = unknown_var { + return Err(ValidationError::UnknownVariable { span, name: var, in_definition: is_in_definition }); } - } - 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_with_span() + for child in widget.children.iter() { + let _ = validate_variables_in_widget_use(defs, variables, child, is_in_definition)?; + } + } else if let WidgetUse::Loop(widget) = widget { + let unknown_var = widget + .elements_expr + .var_refs_with_span() .iter() .cloned() .map(|(span, var_ref)| (span, var_ref.clone())) - .find(|(_, var_ref)| !variables.contains(var_ref)) - }); - if let Some((span, var)) = unknown_var { - return Err(ValidationError::UnknownVariable { span, name: var, in_definition: is_in_definition }); - } - - for child in widget.children.iter() { - let _ = validate_variables_in_widget_use(defs, variables, child, is_in_definition)?; + .find(|(_, var_ref)| var_ref != &widget.element_name && !variables.contains(var_ref)); + if let Some((span, var)) = unknown_var { + return Err(ValidationError::UnknownVariable { span, name: var, in_definition: is_in_definition }); + } + let mut variables = variables.clone(); + variables.insert(widget.element_name.clone()); + let _ = validate_variables_in_widget_use(defs, &variables, &widget.body, is_in_definition)?; } Ok(()) diff --git a/crates/yuck/src/config/widget_use.rs b/crates/yuck/src/config/widget_use.rs index a63dc89..a8a9193 100644 --- a/crates/yuck/src/config/widget_use.rs +++ b/crates/yuck/src/config/widget_use.rs @@ -4,15 +4,41 @@ use simplexpr::SimplExpr; use crate::{ config::attributes::AttrEntry, - error::{AstError, AstResult}, - parser::{ast::Ast, ast_iterator::AstIterator, from_ast::FromAst}, + error::{AstError, AstResult, AstResultExt, FormFormatError}, + parser::{ + ast::Ast, + ast_iterator::AstIterator, + from_ast::{FromAst, FromAstElementContent}, + }, }; use eww_shared_util::{AttrName, Span, Spanned, VarName}; use super::attributes::Attributes; #[derive(Debug, PartialEq, Eq, Clone, serde::Serialize)] -pub struct WidgetUse { +pub enum WidgetUse { + Basic(BasicWidgetUse), + Loop(LoopWidgetUse), + Children(ChildrenWidgetUse), +} + +#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize)] +pub struct LoopWidgetUse { + pub element_name: VarName, + pub elements_expr: SimplExpr, + pub elements_expr_span: Span, + pub body: Box, + pub span: Span, +} + +#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize)] +pub struct ChildrenWidgetUse { + pub span: Span, + pub nth_expr: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize)] +pub struct BasicWidgetUse { pub name: String, pub attrs: Attributes, pub children: Vec, @@ -20,39 +46,79 @@ pub struct WidgetUse { pub name_span: Span, } -impl WidgetUse { +impl BasicWidgetUse { pub fn children_span(&self) -> Span { if self.children.is_empty() { self.span.point_span_at_end().shifted(-1) } else { - self.children.first().unwrap().span.to(self.children.last().unwrap().span) + self.children.first().unwrap().span().to(self.children.last().unwrap().span()) } } + + fn from_iter>( + span: Span, + name: String, + name_span: Span, + mut iter: AstIterator, + ) -> AstResult { + let attrs = iter.expect_key_values()?; + let children = iter.map(WidgetUse::from_ast).collect::>>()?; + Ok(Self { name, attrs, children, span, name_span }) + } +} + +impl FromAstElementContent for LoopWidgetUse { + const ELEMENT_NAME: &'static str = "for"; + + fn from_tail>(span: Span, mut iter: AstIterator) -> AstResult { + let (element_name_span, element_name) = iter.expect_symbol()?; + let (in_string_span, in_string) = iter.expect_symbol()?; + if in_string != "in" { + return Err(AstError::FormFormatError(FormFormatError::ExpectedInInForLoop(in_string_span, in_string))); + } + let (elements_span, elements_expr) = iter.expect_simplexpr()?; + let body = iter.expect_any().note("Expected a loop body").and_then(WidgetUse::from_ast)?; + iter.expect_done()?; + Ok(Self { + element_name: VarName(element_name), + elements_expr, + body: Box::new(body), + span, + elements_expr_span: elements_span, + }) + } +} + +impl FromAstElementContent for ChildrenWidgetUse { + const ELEMENT_NAME: &'static str = "children"; + + fn from_tail>(span: Span, mut iter: AstIterator) -> AstResult { + let mut attrs = iter.expect_key_values()?; + let nth_expr = attrs.ast_optional("nth")?; + iter.expect_done()?; + Ok(Self { span, nth_expr }) + } } impl FromAst for WidgetUse { fn from_ast(e: Ast) -> AstResult { let span = e.span(); if let Ok(value) = e.clone().as_simplexpr() { - Ok(label_from_simplexpr(value, span)) + Ok(WidgetUse::Basic(label_from_simplexpr(value, span))) } else { let mut iter = e.try_ast_iter()?; let (name_span, name) = iter.expect_symbol()?; - let attrs = iter.expect_key_values()?; - let children = iter.map(WidgetUse::from_ast).collect::>>()?; - Ok(Self { name, attrs, children, span, name_span }) + match name.as_ref() { + LoopWidgetUse::ELEMENT_NAME => Ok(WidgetUse::Loop(LoopWidgetUse::from_tail(span, iter)?)), + ChildrenWidgetUse::ELEMENT_NAME => Ok(WidgetUse::Children(ChildrenWidgetUse::from_tail(span, iter)?)), + _ => Ok(WidgetUse::Basic(BasicWidgetUse::from_iter(span, name, name_span, iter)?)), + } } } } -impl Spanned for WidgetUse { - fn span(&self) -> Span { - self.span - } -} - -fn label_from_simplexpr(value: SimplExpr, span: Span) -> WidgetUse { - WidgetUse { +fn label_from_simplexpr(value: SimplExpr, span: Span) -> BasicWidgetUse { + BasicWidgetUse { name: "label".to_string(), name_span: span.point_span(), attrs: Attributes::new( @@ -68,3 +134,15 @@ fn label_from_simplexpr(value: SimplExpr, span: Span) -> WidgetUse { span, } } + +macro_rules! impl_spanned { + ($($super:ident => $name:ident),*) => { + $(impl Spanned for $name { fn span(&self) -> Span { self.span } })* + impl Spanned for WidgetUse { + fn span(&self) -> Span { + match self { $(WidgetUse::$super(widget) => widget.span),* } + } + } + } +} +impl_spanned!(Basic => BasicWidgetUse, Loop => LoopWidgetUse, Children => ChildrenWidgetUse); diff --git a/crates/yuck/src/error.rs b/crates/yuck/src/error.rs index c5f1980..104cab9 100644 --- a/crates/yuck/src/error.rs +++ b/crates/yuck/src/error.rs @@ -170,13 +170,17 @@ pub enum FormFormatError { #[error("Widget definition has more than one child widget")] WidgetDefMultipleChildren(Span), + + #[error("Expected 'in' in this position, but got '{}'", .1)] + ExpectedInInForLoop(Span, String), } impl Spanned for FormFormatError { fn span(&self) -> Span { match self { - FormFormatError::WidgetDefArglistMissing(span) => *span, - FormFormatError::WidgetDefMultipleChildren(span) => *span, + FormFormatError::WidgetDefArglistMissing(span) + | FormFormatError::WidgetDefMultipleChildren(span) + | FormFormatError::ExpectedInInForLoop(span, _) => *span, } } } diff --git a/crates/yuck/src/format_diagnostic.rs b/crates/yuck/src/format_diagnostic.rs index b6a28c1..7da4ed7 100644 --- a/crates/yuck/src/format_diagnostic.rs +++ b/crates/yuck/src/format_diagnostic.rs @@ -298,6 +298,10 @@ impl ToDiagnostic for FormFormatError { To include multiple elements, wrap these elements in a single container widget such as `box`.\n\ This is necessary as eww can't know how you want these elements to be layed out otherwise." }, + FormFormatError::ExpectedInInForLoop(span, got) => gen_diagnostic! { + msg = self, + label = span, + }, } } } diff --git a/crates/yuck/src/parser/ast_iterator.rs b/crates/yuck/src/parser/ast_iterator.rs index cee69dc..12577ac 100644 --- a/crates/yuck/src/parser/ast_iterator.rs +++ b/crates/yuck/src/parser/ast_iterator.rs @@ -40,9 +40,9 @@ macro_rules! return_or_put_back { impl> AstIterator { return_or_put_back! { - fn expect_symbol -> AstType::Symbol, (Span, String) = Ast::Symbol(span, x) => (span, x) - fn expect_list -> AstType::List, (Span, Vec) = Ast::List(span, x) => (span, x) - fn expect_array -> AstType::Array, (Span, Vec) = Ast::Array(span, x) => (span, x) + fn expect_symbol -> AstType::Symbol, (Span, String) = Ast::Symbol(span, x) => (span, x) + fn expect_list -> AstType::List, (Span, Vec) = Ast::List(span, x) => (span, x) + fn expect_array -> AstType::Array, (Span, Vec) = Ast::Array(span, x) => (span, x) } pub fn expect_literal(&mut self) -> AstResult<(Span, DynVal)> { @@ -67,6 +67,20 @@ impl> AstIterator { self.next().or_missing(self.remaining_span.point_span()) } + pub fn expect_simplexpr(&mut self) -> AstResult<(Span, SimplExpr)> { + let expr_type = AstType::SimplExpr; + match self.expect_any()? { + Ast::SimplExpr(span, expr) => Ok((span, expr)), + Ast::Symbol(span, var) => Ok((span, SimplExpr::VarRef(span, VarName(var)))), + other => { + let span = other.span(); + let actual_type = other.expr_type(); + self.put_back(other); + Err(AstError::WrongExprType(span, expr_type, actual_type)) + } + } + } + pub fn expect_done(&mut self) -> AstResult<()> { if let Some(next) = self.next() { self.put_back(next); diff --git a/docs/src/configuration.md b/docs/src/configuration.md index 33b2473..b23da4a 100644 --- a/docs/src/configuration.md +++ b/docs/src/configuration.md @@ -239,7 +239,7 @@ For this, you can make use of one of eww's most powerful features: the `literal` (defvar variable_containing_yuck "(box (button 'foo') (button 'bar'))") -; then, inside your widget, use: +; Then, inside your widget, use: (literal :content variable_containing_yuck) ``` @@ -248,6 +248,22 @@ Eww then reads the provided value and renders the resulting widget. Whenever it Note that this is not all that efficient. Make sure to only use `literal` when necessary! +## Generating a list of widgets from JSON using `for` + +If you want to display a list of values, you can use the `for`-Element to fill a container with a list of elements generated from a JSON-array. +```lisp +(defvar my-json "[1, 2, 3]") + +; Then, inside your widget, you can use +(box + (for entry in my-json + (button :onclick "notify-send 'click' 'button ${entry}'" + entry))) +``` + +This can be useful in many situations, for example when generating a workspace list from a JSON representation of your workspaces. +In many cases, this can be used instead of `literal`, and should most likely be preferred in those cases. + ## Splitting up your configuration As time passes, your configuration might grow larger and larger. Luckily, you can easily split up your configuration into multiple files!