This commit is contained in:
elkowar 2020-09-19 11:12:24 +02:00
commit 3cbd81219b
7 changed files with 2115 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1808
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

27
Cargo.toml Normal file
View file

@ -0,0 +1,27 @@
[package]
name = "eww"
version = "0.1.0"
authors = ["elkowar <5300871+elkowar@users.noreply.github.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
gtk = { version = "0.9", features = [ "v3_16" ] }
gdk = { version = "", features = ["v3_16"] }
gio = { version = "", features = ["v2_44"] }
hocon = { version = "0.3", features = [ "serde-support" ]}
try_match = "0.2.2"
anyhow = "1.0"
#thiserror = "1.0"
[target.x86_64-unknown-linux-gnu]
rustflags = [
"-C", "link-arg=-fuse-ld=lld",
]

24
src/config/hocon_ext.rs Normal file
View file

@ -0,0 +1,24 @@
use hocon::Hocon;
use std::collections::HashMap;
pub trait HoconExt: Sized {
fn as_hash(&self) -> Option<&HashMap<String, Self>>;
fn as_array(&self) -> Option<&Vec<Self>>;
}
impl HoconExt for Hocon {
// TODO take owned self here?
fn as_hash(&self) -> Option<&HashMap<String, Self>> {
match self {
Hocon::Hash(x) => Some(x),
_ => None,
}
}
fn as_array(&self) -> Option<&Vec<Self>> {
match self {
Hocon::Array(x) => Some(x),
_ => None,
}
}
}

173
src/config/mod.rs Normal file
View file

@ -0,0 +1,173 @@
use anyhow::*;
use hocon::*;
use hocon_ext::HoconExt;
use std::collections::HashMap;
use try_match::try_match;
pub mod hocon_ext;
#[derive(Debug, PartialEq)]
pub struct WidgetDefinition {
pub name: String,
pub structure: ElementUse,
}
#[derive(Debug, PartialEq)]
pub enum ElementUse {
Widget(WidgetUse),
Text(String),
}
#[derive(Debug, PartialEq)]
pub struct WidgetUse {
pub name: String,
pub children: Vec<ElementUse>,
pub num_attrs: HashMap<String, f64>,
pub str_attrs: HashMap<String, String>,
}
impl WidgetUse {
pub fn new(name: String, children: Vec<ElementUse>) -> Self {
WidgetUse {
name,
children,
num_attrs: HashMap::new(),
str_attrs: HashMap::new(),
}
}
}
impl From<WidgetUse> for ElementUse {
fn from(other: WidgetUse) -> ElementUse {
ElementUse::Widget(other)
}
}
pub fn parse_widget_definition(text: &str) -> Result<WidgetDefinition> {
let hocon = parse_hocon(text)?;
let definition = hocon
.as_hash()
.ok_or_else(|| anyhow!("{:?} is not a hash", text))?;
Ok(WidgetDefinition {
name: definition["name"]
.as_string()
.context("name was not a string")?,
structure: parse_element_use(definition.get("structure").unwrap().clone())?,
})
}
pub fn parse_element_use(hocon: Hocon) -> Result<ElementUse> {
match hocon {
Hocon::String(s) => Ok(ElementUse::Text(s)),
Hocon::Hash(hash) => parse_widget_use(hash).map(ElementUse::Widget),
_ => Err(anyhow!("{:?} is not a valid element", hocon)),
}
}
pub fn parse_widget_use(data: HashMap<String, Hocon>) -> Result<WidgetUse> {
let (widget_name, widget_config) = data.into_iter().next().unwrap();
let widget_config = widget_config.as_hash().unwrap();
// TODO allow for `layout_horizontal: [ elements ]` shorthand
let children = match &widget_config.get("children") {
Some(Hocon::String(text)) => Ok(vec![ElementUse::Text(text.to_string())]),
Some(Hocon::Array(children)) => children
.clone()
.into_iter()
.map(parse_element_use)
.collect::<Result<Vec<_>>>(),
None => Ok(Vec::new()),
_ => Err(anyhow!(
"children must be either a list of elements or a string, but was {:?}"
)),
}?;
let str_attrs: HashMap<String, String> = widget_config
.into_iter()
.filter_map(|(key, value)| {
Some((
key.clone(),
try_match!(Hocon::String(x) = value).ok()?.clone(),
))
})
.collect();
let num_attrs: HashMap<String, f64> = widget_config
.iter()
.filter_map(|(key, value)| {
Some((
key.to_string(),
try_match!(Hocon::Integer(x) = value)
.map(|&x| x as f64)
.or_else(|_| try_match!(Hocon::Real(x) = value).map(|&x| x as f64))
.ok()?,
))
})
.collect();
Ok(WidgetUse {
name: widget_name.to_string(),
children,
str_attrs,
num_attrs,
})
}
pub fn parse_hocon(s: &str) -> Result<Hocon> {
Ok(HoconLoader::new().load_str(s)?.hocon()?)
}
#[cfg(test)]
mod test {
use super::*;
const EXAMPLE_CONFIG: &'static str = r#"{
name: "example_widget"
structure {
layout_horizontal {
children: [
{ text { children: "hi", color: "red" } }
{ text: {} }
]
}
}
}"#;
#[test]
fn test_parse() {
assert_eq!(
parse_element_use(Hocon::String("hi".to_string())).unwrap(),
ElementUse::Text("hi".to_string())
);
}
#[test]
fn test_parse_widget_definition() {
let expected = WidgetDefinition {
name: "example_widget".to_string(),
structure: ElementUse::Widget(WidgetUse {
name: "layout_horizontal".to_string(),
children: vec![
ElementUse::Widget(WidgetUse::new(
"text".to_string(),
vec![ElementUse::Text("hi".to_string())],
)),
ElementUse::Widget(WidgetUse::new("text".to_string(), vec![])),
],
}),
};
let parsed_hocon = parse_hocon("{ text: { children: \"hi\" } }").unwrap();
assert_eq!(
parse_element_use(parsed_hocon).unwrap(),
ElementUse::Widget(WidgetUse::new(
"text".to_string(),
vec![ElementUse::Text("hi".to_string())]
))
);
assert_eq!(parse_widget_definition(EXAMPLE_CONFIG).unwrap(), expected);
}
}

82
src/main.rs Normal file
View file

@ -0,0 +1,82 @@
extern crate gio;
extern crate gtk;
use anyhow::*;
use gdk::*;
use gio::prelude::*;
use gtk::prelude::*;
use gtk::{Adjustment, Application, ApplicationWindow, Button, Scale};
pub mod config;
pub mod widgets;
fn main() -> Result<()> {
let application = Application::new(Some("de.elkowar.eww"), Default::default())
.expect("failed to initialize GTK application");
application.connect_activate(|app| {
let window = ApplicationWindow::new(app);
window.set_title("Eww");
window.set_wmclass("noswallow", "noswallow");
window.set_type_hint(gdk::WindowTypeHint::Dock);
window.set_position(gtk::WindowPosition::Center);
let element = config::parse_element_use(
config::parse_hocon(
r#"{
layout_horizontal: {
children: [
"hi",
{ button: { children: "click me you" } }
{ slider: {} }
"hu"
]
}
}"#,
)
.unwrap(),
)
.unwrap();
window.add(&element_to_gtk_widget(&element).unwrap());
window.show_all();
});
application.run(&[]);
Ok(())
}
fn element_to_gtk_widget(element: &config::ElementUse) -> Option<gtk::Widget> {
match element {
config::ElementUse::Text(text) => Some(gtk::Label::new(Some(&text)).upcast()),
config::ElementUse::Widget(widget) => {
widget_use_to_gtk_container(&widget).or(widget_use_to_gtk_widget(&widget))
}
}
}
fn widget_use_to_gtk_container(widget: &config::WidgetUse) -> Option<gtk::Widget> {
let container_widget: gtk::Container = match widget.name.as_str() {
"layout_horizontal" => gtk::Box::new(gtk::Orientation::Horizontal, 0).upcast(),
"button" => gtk::Button::new().upcast(),
_ => return None,
};
for child in &widget.children {
container_widget.add(&element_to_gtk_widget(child)?);
}
Some(container_widget.upcast())
}
fn widget_use_to_gtk_widget(widget: &config::WidgetUse) -> Option<gtk::Widget> {
let new_widget: gtk::Widget = match widget.name.as_str() {
"slider" => gtk::Scale::new(
gtk::Orientation::Horizontal,
Some(&gtk::Adjustment::new(50.0, 0.0, 100.0, 1.0, 1.0, 1.0)),
)
.upcast(),
_ => return None,
};
Some(new_widget)
}

0
src/widgets/mod.rs Normal file
View file