From 91469053142bf1983ad18010e5b2f669d28f5704 Mon Sep 17 00:00:00 2001 From: ElKowar <5300871+elkowar@users.noreply.github.com> Date: Sun, 1 Nov 2020 17:33:57 +0100 Subject: [PATCH] Advanced window positioning with anchors (#55) * Implement anchoring * adjust documentation --- docs/content/main/_index.md | 9 +- docs/content/main/configuration.md | 86 +++------------- src/app.rs | 63 ++++++------ src/config/element.rs | 5 +- src/config/mod.rs | 2 + src/config/window_definition.rs | 35 +++---- src/config/window_geometry.rs | 151 +++++++++++++++++++++++++++++ src/config/xml_ext.rs | 14 +++ src/opts.rs | 18 +++- src/value/coords.rs | 22 ++++- 10 files changed, 269 insertions(+), 136 deletions(-) create mode 100644 src/config/window_geometry.rs diff --git a/docs/content/main/_index.md b/docs/content/main/_index.md index beb7258..c4e82fb 100644 --- a/docs/content/main/_index.md +++ b/docs/content/main/_index.md @@ -5,7 +5,10 @@ sort_by = "weight" +++ -Eww (ElKowar's Wacky Widgets, pronounced with sufficient amounts of disgust) Is a widgeting system made in [rust](https://www.rust-lang.org/), which let's you create your own widgets similarly to how you can in AwesomeWM. The key difference: It is independent of your window manager! +Eww (ElKowar's Wacky Widgets, pronounced with sufficient amounts of disgust) +is a widgeting system made in [rust](https://www.rust-lang.org/), +which let's you create your own widgets similarly to how you can in AwesomeWM. +The key difference: It is independent of your window manager! Configured in XML and themed using CSS, it is easy to customize and provides all the flexibility you need! @@ -17,7 +20,9 @@ Configured in XML and themed using CSS, it is easy to customize and provides all * rustc * cargo (nightly toolchain) -Rather than with your system package manager, I recommend installing it using [rustup](https://rustup.rs/), as this makes it easy to use the nightly toolchain, which is necessary to build eww. +Rather than with your system package manager, +I recommend installing it using [rustup](https://rustup.rs/), +as this makes it easy to use the nightly toolchain necessary to build eww. ### Building diff --git a/docs/content/main/configuration.md b/docs/content/main/configuration.md index b809094..aa98a32 100644 --- a/docs/content/main/configuration.md +++ b/docs/content/main/configuration.md @@ -28,11 +28,11 @@ Your config structure should look like this: - + - + @@ -75,7 +75,7 @@ Example: ```xml - date +%H:%M + date +%H:%M ``` @@ -141,7 +141,7 @@ This part: ```xml - The time is: {{my_time}} currently. + The time is: {{my_time}} currently. ``` @@ -156,7 +156,7 @@ So if we look at: ```xml - + ``` @@ -172,7 +172,7 @@ It doesn't have to be `{{my_time}}` either, it can be anything. ```xml - The time is: {{very_long_list_of_animals}} currently. + The time is: {{very_long_list_of_animals}} currently. ``` @@ -182,83 +182,29 @@ To use that it would look like this: ```xml - + ``` ### The `` block {#windows-block} -This is the part the Eww reads and loads. The `` config should look something like this: +All different windows you might want to use are defined in the `` block. +The `` config should look something like this: ```xml - - - -
- - - -``` -`` is the part that eww runs when you start it. In this example you would run eww by doing: -```bash -./eww open main_window -``` -but if renamed the `` to be `` we would run eww by doing: -```bash -./eww open apple -``` - -The `stacking="fg"` says where the widget will be stacked. Possible values here are `foreground`, `fg`, `background` and `bg`. -`foreground` or `fg` *always* stays above windows. -`background` or `bg` *always* stays behind windows. So it will stay on your desktop. - -If you were to remove the `stacking="fg"` it would default it to `fg`. - -You can also have multiple windows in one document by doing: - -```xml - - - - +
- - - - - - - ``` ---- -- `` sets x-y size of the widget. -- `` sets x-y position of the widget. -- `` is the part which you say which `` eww should run. So if we take the example config from before: -```xml - - - - The time is: {{my_time}} currently. - - - - - - - - -``` -and then look at -```xml - -
- -``` -we will see that eww will run `` and not ``. +The window block contains multiple elements to configure the window. +- `` is used to specify the position and size of the window. +- `` will contain the widget that is shown in the window. + +The `stacking="fg"` specifies if the window should appear on top of or behind other windows. +Possible values here are `foreground`, `fg`, `background` and `bg`. It defaults to `fg`. diff --git a/src/app.rs b/src/app.rs index e040299..24007e4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,6 @@ use crate::{ config, - config::{window_definition::WindowName, WindowStacking}, + config::{window_definition::WindowName, AnchorPoint, WindowStacking}, eww_state, script_var_handler::*, util, @@ -25,6 +25,7 @@ pub enum EwwCommand { window_name: WindowName, pos: Option, size: Option, + anchor: Option, }, CloseWindow { window_name: WindowName, @@ -75,8 +76,13 @@ impl App { script_var_process::on_application_death(); std::process::exit(0); } - EwwCommand::OpenWindow { window_name, pos, size } => { - self.open_window(&window_name, pos, size)?; + EwwCommand::OpenWindow { + window_name, + pos, + size, + anchor, + } => { + self.open_window(&window_name, pos, size, anchor)?; } EwwCommand::CloseWindow { window_name } => { self.close_window(&window_name)?; @@ -115,7 +121,13 @@ impl App { Ok(()) } - fn open_window(&mut self, window_name: &WindowName, pos: Option, size: Option) -> Result<()> { + fn open_window( + &mut self, + window_name: &WindowName, + pos: Option, + size: Option, + anchor: Option, + ) -> Result<()> { // remove and close existing window with the same name let _ = self.close_window(window_name); @@ -135,10 +147,11 @@ impl App { let monitor_geometry = display.get_default_screen().get_monitor_geometry(*screen_number); - window_def.position = pos.unwrap_or(window_def.position); - window_def.size = size.unwrap_or(window_def.size); + window_def.geometry.offset = pos.unwrap_or(window_def.geometry.offset); + window_def.geometry.size = size.unwrap_or(window_def.geometry.size); + window_def.geometry.anchor_point = anchor.unwrap_or(window_def.geometry.anchor_point); - let actual_window_rect = get_window_rectangle_in_screen(monitor_geometry, window_def.position, window_def.size); + let actual_window_rect = window_def.geometry.get_window_rectangle(monitor_geometry); let window = if window_def.focusable { gtk::Window::new(gtk::WindowType::Toplevel) @@ -172,6 +185,16 @@ impl App { root_widget.get_style_context().add_class(&window_name.to_string()); window.add(root_widget); + // Handle the fact that the gtk window will have a different size than specified, + // as it is sized according to how much space it's contents require. + // This is necessary to handle different anchors correctly in case the size was wrong. + let (gtk_window_width, gtk_window_height) = window.get_size(); + window_def.geometry.size = Coords { + x: NumWithUnit::Pixels(gtk_window_width), + y: NumWithUnit::Pixels(gtk_window_height), + }; + let actual_window_rect = window_def.geometry.get_window_rectangle(monitor_geometry); + window.show_all(); let gdk_window = window.get_window().context("couldn't get gdk window from gtk window")?; @@ -210,10 +233,8 @@ impl App { let windows = self.windows.clone(); for (window_name, window) in windows { - let old_pos = window.definition.position; - let old_size = window.definition.size; window.gtk_window.close(); - self.open_window(&window_name, Some(old_pos.into()), Some(old_size.into()))?; + self.open_window(&window_name, None, None, None)?; } Ok(()) } @@ -233,25 +254,3 @@ fn on_screen_changed(window: >k::Window, _old_screen: Option<&gdk::Screen>) { }); window.set_visual(visual.as_ref()); } - -/// Calculate the window rectangle given the configured window [`pos`] and [`size`], which might be relative to the screen size. -fn get_window_rectangle_in_screen(screen_rect: gdk::Rectangle, pos: Coords, size: Coords) -> gdk::Rectangle { - gdk::Rectangle { - x: match pos.x { - NumWithUnit::Percent(n) => ((screen_rect.width as f64 / 100.0) * n as f64) as i32, - NumWithUnit::Pixels(n) => screen_rect.x + n, - }, - y: match pos.y { - NumWithUnit::Percent(n) => ((screen_rect.height as f64 / 100.0) * n as f64) as i32, - NumWithUnit::Pixels(n) => screen_rect.y + n, - }, - width: match size.x { - NumWithUnit::Percent(n) => ((screen_rect.width as f64 / 100.0) * n as f64) as i32, - NumWithUnit::Pixels(n) => n, - }, - height: match size.y { - NumWithUnit::Percent(n) => ((screen_rect.height as f64 / 100.0) * n as f64) as i32, - NumWithUnit::Pixels(n) => n, - }, - } -} diff --git a/src/config/element.rs b/src/config/element.rs index c8e3614..db06204 100644 --- a/src/config/element.rs +++ b/src/config/element.rs @@ -28,12 +28,9 @@ impl WidgetDefinition { ); } - let size: Option<_> = Option::zip(xml.attr("width").ok(), xml.attr("height").ok()); - let size: Option> = size.map(|(x, y)| Ok((x.parse()?, y.parse()?))); - WidgetDefinition { name: xml.attr("name")?.to_owned(), - size: size.transpose()?, + size: Option::zip(xml.parse_optional_attr("width")?, xml.parse_optional_attr("height")?), structure: WidgetUse::from_xml_node(xml.only_child()?)?, } } diff --git a/src/config/mod.rs b/src/config/mod.rs index 56829f2..757c9d2 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -11,10 +11,12 @@ pub mod element; pub mod eww_config; pub mod script_var; pub mod window_definition; +pub mod window_geometry; pub mod xml_ext; pub use eww_config::*; pub use script_var::*; pub use window_definition::*; +pub use window_geometry::*; #[macro_export] macro_rules! ensure_xml_tag_is { diff --git a/src/config/window_definition.rs b/src/config/window_definition.rs index cf7dfa6..d692b66 100644 --- a/src/config/window_definition.rs +++ b/src/config/window_definition.rs @@ -1,16 +1,14 @@ -use crate::{ensure_xml_tag_is, value::Coords}; +use crate::ensure_xml_tag_is; use anyhow::*; use derive_more::*; use serde::{Deserialize, Serialize}; use smart_default::SmartDefault; use super::*; -use std::fmt; #[derive(Debug, Clone, PartialEq)] pub struct EwwWindowDefinition { - pub position: Coords, - pub size: Coords, + pub geometry: EwwWindowGeometry, pub stacking: WindowStacking, pub screen_number: Option, pub widget: WidgetUse, @@ -21,22 +19,18 @@ pub struct EwwWindowDefinition { impl EwwWindowDefinition { pub fn from_xml_element(xml: XmlElement) -> Result { ensure_xml_tag_is!(xml, "window"); + let stacking: WindowStacking = xml.parse_optional_attr("stacking")?.unwrap_or_default(); + let screen_number = xml.parse_optional_attr("screen")?; + let focusable = xml.parse_optional_attr("focusable")?; - let size_node = xml.child("size")?; - let size = Coords::from_strs(size_node.attr("x")?, size_node.attr("y")?)?; - let pos_node = xml.child("pos")?; - let position = Coords::from_strs(pos_node.attr("x")?, pos_node.attr("y")?)?; - - let stacking = xml.attr("stacking").ok().map(|x| x.parse()).transpose()?.unwrap_or_default(); - let screen_number = xml.attr("screen").ok().map(|x| x.parse()).transpose()?; - let focusable = xml.attr("focusable").ok().map(|x| x.parse()).transpose()?; let struts = xml.child("struts").ok().map(Struts::from_xml_element).transpose()?; - let widget = WidgetUse::from_xml_node(xml.child("widget")?.only_child()?)?; Ok(EwwWindowDefinition { - position, - size, - widget, + geometry: match xml.child("geometry") { + Ok(node) => EwwWindowGeometry::from_xml_element(node)?, + Err(_) => EwwWindowGeometry::default(), + }, + widget: WidgetUse::from_xml_node(xml.child("widget")?.only_child()?)?, stacking, screen_number, focusable: focusable.unwrap_or(false), @@ -89,7 +83,8 @@ impl std::str::FromStr for WindowStacking { } #[repr(transparent)] -#[derive(Clone, Hash, PartialEq, Eq, AsRef, FromStr, Display, Serialize, Deserialize, Default, From)] +#[derive(Clone, Hash, PartialEq, Eq, AsRef, FromStr, Display, Serialize, Deserialize, Default, From, DebugCustom)] +#[debug(fmt = "WindowName(\".0\")")] pub struct WindowName(String); impl std::borrow::Borrow for WindowName { @@ -97,9 +92,3 @@ impl std::borrow::Borrow for WindowName { &self.0 } } - -impl fmt::Debug for WindowName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "WindowName(\"{}\")", self.0) - } -} diff --git a/src/config/window_geometry.rs b/src/config/window_geometry.rs new file mode 100644 index 0000000..a19989e --- /dev/null +++ b/src/config/window_geometry.rs @@ -0,0 +1,151 @@ +use crate::value::Coords; +use anyhow::*; +use serde::{Deserialize, Serialize}; +use smart_default::SmartDefault; + +use std::fmt; + +use super::xml_ext::XmlElement; + +#[derive(Debug, derive_more::Display, Clone, Copy, Eq, PartialEq, SmartDefault, Serialize, Deserialize)] +pub enum AnchorAlignment { + #[display("start")] + #[default] + START, + #[display("center")] + CENTER, + #[display("end")] + END, +} + +impl AnchorAlignment { + pub fn from_x_alignment(s: &str) -> Result { + match s { + "l" | "left" => Ok(AnchorAlignment::START), + "c" | "center" => Ok(AnchorAlignment::CENTER), + "r" | "right" => Ok(AnchorAlignment::END), + _ => bail!( + r#"couldn't parse '{}' as x-alignment. Must be one of "left", "center", "right""#, + s + ), + } + } + + pub fn from_y_alignment(s: &str) -> Result { + match s { + "t" | "top" => Ok(AnchorAlignment::START), + "c" | "center" => Ok(AnchorAlignment::CENTER), + "b" | "bottom" => Ok(AnchorAlignment::END), + _ => bail!( + r#"couldn't parse '{}' as y-alignment. Must be one of "top", "center", "bottom""#, + s + ), + } + } + + pub fn alignment_to_coordinate(&self, size_inner: i32, size_container: i32) -> i32 { + match self { + AnchorAlignment::START => 0, + AnchorAlignment::CENTER => (size_container / 2) - (size_inner / 2), + AnchorAlignment::END => size_container - size_inner, + } + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, Serialize, Deserialize)] +pub struct AnchorPoint { + x: AnchorAlignment, + y: AnchorAlignment, +} + +impl std::fmt::Display for AnchorPoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use AnchorAlignment::*; + match (self.x, self.y) { + (CENTER, CENTER) => write!(f, "center"), + (x, y) => write!( + f, + "{} {}", + match x { + START => "left", + CENTER => "center", + END => "right", + }, + match y { + START => "top", + CENTER => "center", + END => "bottom", + } + ), + } + } +} + +impl std::str::FromStr for AnchorPoint { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + if s == "center" { + Ok(AnchorPoint { + x: AnchorAlignment::CENTER, + y: AnchorAlignment::CENTER, + }) + } else { + let (first, second) = s + .split_once(' ') + .context("Failed to parse anchor: Must either be \"center\" or be formatted like \"top left\"")?; + let x_y_result: Result<_> = try { + AnchorPoint { + x: AnchorAlignment::from_x_alignment(first)?, + y: AnchorAlignment::from_y_alignment(second)?, + } + }; + x_y_result.or_else(|_| { + Ok(AnchorPoint { + x: AnchorAlignment::from_x_alignment(second)?, + y: AnchorAlignment::from_y_alignment(first)?, + }) + }) + } + } +} + +#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)] +pub struct EwwWindowGeometry { + pub anchor_point: AnchorPoint, + pub offset: Coords, + pub size: Coords, +} + +impl EwwWindowGeometry { + pub fn from_xml_element(xml: XmlElement) -> Result { + Ok(EwwWindowGeometry { + anchor_point: xml.parse_optional_attr("anchor")?.unwrap_or_default(), + size: Coords { + x: xml.parse_optional_attr("width")?.unwrap_or_default(), + y: xml.parse_optional_attr("height")?.unwrap_or_default(), + }, + offset: Coords { + x: xml.parse_optional_attr("x")?.unwrap_or_default(), + y: xml.parse_optional_attr("y")?.unwrap_or_default(), + }, + }) + } +} + +impl std::fmt::Display for EwwWindowGeometry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}-{} ({})", self.offset, self.size, self.anchor_point) + } +} + +impl EwwWindowGeometry { + /// Calculate the window rectangle given the configured window geometry + pub fn get_window_rectangle(&self, screen_rect: gdk::Rectangle) -> gdk::Rectangle { + let (offset_x, offset_y) = self.offset.relative_to(screen_rect.width, screen_rect.height); + let (width, height) = self.size.relative_to(screen_rect.width, screen_rect.height); + let x = screen_rect.x + offset_x + self.anchor_point.x.alignment_to_coordinate(width, screen_rect.width); + let y = screen_rect.y + offset_y + self.anchor_point.y.alignment_to_coordinate(height, screen_rect.height); + gdk::Rectangle { x, y, width, height } + } +} diff --git a/src/config/xml_ext.rs b/src/config/xml_ext.rs index 4908520..3635f4f 100644 --- a/src/config/xml_ext.rs +++ b/src/config/xml_ext.rs @@ -177,6 +177,20 @@ impl<'a, 'b> XmlElement<'a, 'b> { } } + pub fn optional_attr Result>(&self, key: &str, parse: F) -> Result> { + match self.0.attribute(key) { + Some(value) => parse(value).map(Some), + None => Ok(None), + } + } + + pub fn parse_optional_attr>(&self, key: &str) -> Result, E> { + match self.0.attribute(key) { + Some(value) => value.parse::().map(Some), + None => Ok(None), + } + } + pub fn only_child(&self) -> Result { with_text_pos_context! { self => let mut children_iter = self.children(); diff --git a/src/opts.rs b/src/opts.rs index 1a4bf5e..5de2963 100644 --- a/src/opts.rs +++ b/src/opts.rs @@ -4,7 +4,7 @@ use structopt::StructOpt; use crate::{ app, - config::WindowName, + config::{AnchorPoint, WindowName}, value::{Coords, PrimitiveValue, VarName}, }; @@ -57,6 +57,10 @@ pub enum ActionWithServer { /// The size of the window to open #[structopt(short, long)] size: Option, + + /// Anchorpoint of the window, formatted like "top right" + #[structopt(short, long)] + anchor: Option, }, /// Close the window with the given name @@ -92,7 +96,17 @@ impl ActionWithServer { ActionWithServer::Update { mappings } => { app::EwwCommand::UpdateVars(mappings.into_iter().map(|x| x.into()).collect()) } - ActionWithServer::OpenWindow { window_name, pos, size } => app::EwwCommand::OpenWindow { window_name, pos, size }, + ActionWithServer::OpenWindow { + window_name, + pos, + size, + anchor, + } => app::EwwCommand::OpenWindow { + window_name, + pos, + size, + anchor, + }, ActionWithServer::CloseWindow { window_name } => app::EwwCommand::CloseWindow { window_name }, ActionWithServer::KillServer => app::EwwCommand::KillServer, ActionWithServer::ShowState => { diff --git a/src/value/coords.rs b/src/value/coords.rs index 4b9f3eb..04348c6 100644 --- a/src/value/coords.rs +++ b/src/value/coords.rs @@ -1,15 +1,17 @@ use anyhow::*; use derive_more::*; use serde::{Deserialize, Serialize}; +use smart_default::SmartDefault; use std::{fmt, str::FromStr}; -#[derive(Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Display, DebugCustom)] +#[derive(Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Display, DebugCustom, SmartDefault)] pub enum NumWithUnit { #[display(fmt = "{}%", .0)] #[debug(fmt = "{}%", .0)] Percent(i32), #[display(fmt = "{}px", .0)] #[debug(fmt = "{}px", .0)] + #[default] Pixels(i32), } @@ -18,7 +20,7 @@ impl FromStr for NumWithUnit { fn from_str(s: &str) -> Result { lazy_static::lazy_static! { - static ref PATTERN: regex::Regex = regex::Regex::new("^(\\d+)(.*)$").unwrap(); + static ref PATTERN: regex::Regex = regex::Regex::new("^(-?\\d+)(.*)$").unwrap(); }; let captures = PATTERN.captures(s).with_context(|| format!("could not parse '{}'", s))?; @@ -32,7 +34,7 @@ impl FromStr for NumWithUnit { } } -#[derive(Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Display)] +#[derive(Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Display, Default)] #[display(fmt = "{}X{}", x, y)] pub struct Coords { pub x: NumWithUnit, @@ -64,6 +66,20 @@ impl Coords { y: y.parse().with_context(|| format!("Failed to parse '{}'", y))?, }) } + + /// resolve the possibly relative coordinates relative to a given containers size + pub fn relative_to(&self, width: i32, height: i32) -> (i32, i32) { + ( + match self.x { + NumWithUnit::Percent(n) => ((width as f64 / 100.0) * n as f64) as i32, + NumWithUnit::Pixels(n) => n, + }, + match self.y { + NumWithUnit::Percent(n) => ((height as f64 / 100.0) * n as f64) as i32, + NumWithUnit::Pixels(n) => n, + }, + ) + } } #[cfg(test)]