init
This commit is contained in:
commit
3cbd81219b
7 changed files with 2115 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
1808
Cargo.lock
generated
Normal file
1808
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
27
Cargo.toml
Normal file
27
Cargo.toml
Normal 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
24
src/config/hocon_ext.rs
Normal 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
173
src/config/mod.rs
Normal 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
82
src/main.rs
Normal 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(>k::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
0
src/widgets/mod.rs
Normal file
Loading…
Add table
Reference in a new issue