Advanced window positioning with anchors (#55)

* Implement anchoring

* adjust documentation
This commit is contained in:
ElKowar 2020-11-01 17:33:57 +01:00 committed by GitHub
parent 0f68a76507
commit 9146905314
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 269 additions and 136 deletions

View file

@ -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! 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 * rustc
* cargo (nightly toolchain) * 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 ### Building

View file

@ -188,77 +188,23 @@ To use that it would look like this:
``` ```
### The `<windows>` block {#windows-block} ### The `<windows>` block {#windows-block}
This is the part the Eww reads and loads. The `<windows>` config should look something like this: All different windows you might want to use are defined in the `<windows>` block.
The `<windows>` config should look something like this:
```xml ```xml
<windows> <windows>
<window name="main_window" stacking="fg"> <window name="main_window" stacking="fg">
<size x="300" y="300" /> <geometry anchor="top left" x="300px" y="50%" width="25%" height="20px">
<pos x="0" y="500" />
<widget> <widget>
<main/> <main/>
</widget> </widget>
</window> </window>
</windows> </windows>
``` ```
`<window name="main_window">` 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 `<window>` to be `<window name="apple">` 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`. The window block contains multiple elements to configure the window.
`foreground` or `fg` *always* stays above windows. - `<geometry>` is used to specify the position and size of the window.
`background` or `bg` *always* stays behind windows. So it will stay on your desktop. - `<widget>` will contain the widget that is shown in the window.
If you were to remove the `stacking="fg"` it would default it to `fg`. 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`.
You can also have multiple windows in one document by doing:
```xml
<windows>
<window name="main_window">
<size x="300" y="300" />
<pos x="0" y="500" />
<widget>
<main/>
</widget>
</window>
<window name="main_window2">
<size x="400" y="600"/>
<pos x="0" y="0"/>
<widget>
<main2/>
</widget>
</window>
</windows>
```
---
- `<size>` sets x-y size of the widget.
- `<pos>` sets x-y position of the widget.
- `<widget>` is the part which you say which `<def>` eww should run. So if we take the example config from before:
```xml
<definitions>
<def name="clock">
<box>
The time is: {{my_time}} currently.
</box>
</def>
<def name="main">
<box>
<clock my_time="{{date}}"/>
</box>
</def>
</definitions>
```
and then look at
```xml
<widget>
<main/>
</widget>
```
we will see that eww will run `<def name="main">` and not `<def name="clock">`.

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
config, config,
config::{window_definition::WindowName, WindowStacking}, config::{window_definition::WindowName, AnchorPoint, WindowStacking},
eww_state, eww_state,
script_var_handler::*, script_var_handler::*,
util, util,
@ -25,6 +25,7 @@ pub enum EwwCommand {
window_name: WindowName, window_name: WindowName,
pos: Option<Coords>, pos: Option<Coords>,
size: Option<Coords>, size: Option<Coords>,
anchor: Option<AnchorPoint>,
}, },
CloseWindow { CloseWindow {
window_name: WindowName, window_name: WindowName,
@ -75,8 +76,13 @@ impl App {
script_var_process::on_application_death(); script_var_process::on_application_death();
std::process::exit(0); std::process::exit(0);
} }
EwwCommand::OpenWindow { window_name, pos, size } => { EwwCommand::OpenWindow {
self.open_window(&window_name, pos, size)?; window_name,
pos,
size,
anchor,
} => {
self.open_window(&window_name, pos, size, anchor)?;
} }
EwwCommand::CloseWindow { window_name } => { EwwCommand::CloseWindow { window_name } => {
self.close_window(&window_name)?; self.close_window(&window_name)?;
@ -115,7 +121,13 @@ impl App {
Ok(()) Ok(())
} }
fn open_window(&mut self, window_name: &WindowName, pos: Option<Coords>, size: Option<Coords>) -> Result<()> { fn open_window(
&mut self,
window_name: &WindowName,
pos: Option<Coords>,
size: Option<Coords>,
anchor: Option<config::AnchorPoint>,
) -> Result<()> {
// remove and close existing window with the same name // remove and close existing window with the same name
let _ = self.close_window(window_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); let monitor_geometry = display.get_default_screen().get_monitor_geometry(*screen_number);
window_def.position = pos.unwrap_or(window_def.position); window_def.geometry.offset = pos.unwrap_or(window_def.geometry.offset);
window_def.size = size.unwrap_or(window_def.size); 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 { let window = if window_def.focusable {
gtk::Window::new(gtk::WindowType::Toplevel) gtk::Window::new(gtk::WindowType::Toplevel)
@ -172,6 +185,16 @@ impl App {
root_widget.get_style_context().add_class(&window_name.to_string()); root_widget.get_style_context().add_class(&window_name.to_string());
window.add(root_widget); 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(); window.show_all();
let gdk_window = window.get_window().context("couldn't get gdk window from gtk window")?; 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(); let windows = self.windows.clone();
for (window_name, window) in windows { for (window_name, window) in windows {
let old_pos = window.definition.position;
let old_size = window.definition.size;
window.gtk_window.close(); 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(()) Ok(())
} }
@ -233,25 +254,3 @@ fn on_screen_changed(window: &gtk::Window, _old_screen: Option<&gdk::Screen>) {
}); });
window.set_visual(visual.as_ref()); 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,
},
}
}

View file

@ -28,12 +28,9 @@ impl WidgetDefinition {
); );
} }
let size: Option<_> = Option::zip(xml.attr("width").ok(), xml.attr("height").ok());
let size: Option<Result<_>> = size.map(|(x, y)| Ok((x.parse()?, y.parse()?)));
WidgetDefinition { WidgetDefinition {
name: xml.attr("name")?.to_owned(), 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()?)?, structure: WidgetUse::from_xml_node(xml.only_child()?)?,
} }
} }

View file

@ -11,10 +11,12 @@ pub mod element;
pub mod eww_config; pub mod eww_config;
pub mod script_var; pub mod script_var;
pub mod window_definition; pub mod window_definition;
pub mod window_geometry;
pub mod xml_ext; pub mod xml_ext;
pub use eww_config::*; pub use eww_config::*;
pub use script_var::*; pub use script_var::*;
pub use window_definition::*; pub use window_definition::*;
pub use window_geometry::*;
#[macro_export] #[macro_export]
macro_rules! ensure_xml_tag_is { macro_rules! ensure_xml_tag_is {

View file

@ -1,16 +1,14 @@
use crate::{ensure_xml_tag_is, value::Coords}; use crate::ensure_xml_tag_is;
use anyhow::*; use anyhow::*;
use derive_more::*; use derive_more::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use smart_default::SmartDefault; use smart_default::SmartDefault;
use super::*; use super::*;
use std::fmt;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct EwwWindowDefinition { pub struct EwwWindowDefinition {
pub position: Coords, pub geometry: EwwWindowGeometry,
pub size: Coords,
pub stacking: WindowStacking, pub stacking: WindowStacking,
pub screen_number: Option<i32>, pub screen_number: Option<i32>,
pub widget: WidgetUse, pub widget: WidgetUse,
@ -21,22 +19,18 @@ pub struct EwwWindowDefinition {
impl EwwWindowDefinition { impl EwwWindowDefinition {
pub fn from_xml_element(xml: XmlElement) -> Result<Self> { pub fn from_xml_element(xml: XmlElement) -> Result<Self> {
ensure_xml_tag_is!(xml, "window"); 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 struts = xml.child("struts").ok().map(Struts::from_xml_element).transpose()?;
let widget = WidgetUse::from_xml_node(xml.child("widget")?.only_child()?)?;
Ok(EwwWindowDefinition { Ok(EwwWindowDefinition {
position, geometry: match xml.child("geometry") {
size, Ok(node) => EwwWindowGeometry::from_xml_element(node)?,
widget, Err(_) => EwwWindowGeometry::default(),
},
widget: WidgetUse::from_xml_node(xml.child("widget")?.only_child()?)?,
stacking, stacking,
screen_number, screen_number,
focusable: focusable.unwrap_or(false), focusable: focusable.unwrap_or(false),
@ -89,7 +83,8 @@ impl std::str::FromStr for WindowStacking {
} }
#[repr(transparent)] #[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); pub struct WindowName(String);
impl std::borrow::Borrow<str> for WindowName { impl std::borrow::Borrow<str> for WindowName {
@ -97,9 +92,3 @@ impl std::borrow::Borrow<str> for WindowName {
&self.0 &self.0
} }
} }
impl fmt::Debug for WindowName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "WindowName(\"{}\")", self.0)
}
}

View file

@ -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<AnchorAlignment> {
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<AnchorAlignment> {
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<Self, Self::Err> {
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<Self> {
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 }
}
}

View file

@ -177,6 +177,20 @@ impl<'a, 'b> XmlElement<'a, 'b> {
} }
} }
pub fn optional_attr<O, F: FnOnce(&str) -> Result<O>>(&self, key: &str, parse: F) -> Result<Option<O>> {
match self.0.attribute(key) {
Some(value) => parse(value).map(Some),
None => Ok(None),
}
}
pub fn parse_optional_attr<E, O: std::str::FromStr<Err = E>>(&self, key: &str) -> Result<Option<O>, E> {
match self.0.attribute(key) {
Some(value) => value.parse::<O>().map(Some),
None => Ok(None),
}
}
pub fn only_child(&self) -> Result<XmlNode> { pub fn only_child(&self) -> Result<XmlNode> {
with_text_pos_context! { self => with_text_pos_context! { self =>
let mut children_iter = self.children(); let mut children_iter = self.children();

View file

@ -4,7 +4,7 @@ use structopt::StructOpt;
use crate::{ use crate::{
app, app,
config::WindowName, config::{AnchorPoint, WindowName},
value::{Coords, PrimitiveValue, VarName}, value::{Coords, PrimitiveValue, VarName},
}; };
@ -57,6 +57,10 @@ pub enum ActionWithServer {
/// The size of the window to open /// The size of the window to open
#[structopt(short, long)] #[structopt(short, long)]
size: Option<Coords>, size: Option<Coords>,
/// Anchorpoint of the window, formatted like "top right"
#[structopt(short, long)]
anchor: Option<AnchorPoint>,
}, },
/// Close the window with the given name /// Close the window with the given name
@ -92,7 +96,17 @@ impl ActionWithServer {
ActionWithServer::Update { mappings } => { ActionWithServer::Update { mappings } => {
app::EwwCommand::UpdateVars(mappings.into_iter().map(|x| x.into()).collect()) 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::CloseWindow { window_name } => app::EwwCommand::CloseWindow { window_name },
ActionWithServer::KillServer => app::EwwCommand::KillServer, ActionWithServer::KillServer => app::EwwCommand::KillServer,
ActionWithServer::ShowState => { ActionWithServer::ShowState => {

View file

@ -1,15 +1,17 @@
use anyhow::*; use anyhow::*;
use derive_more::*; use derive_more::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use smart_default::SmartDefault;
use std::{fmt, str::FromStr}; 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 { pub enum NumWithUnit {
#[display(fmt = "{}%", .0)] #[display(fmt = "{}%", .0)]
#[debug(fmt = "{}%", .0)] #[debug(fmt = "{}%", .0)]
Percent(i32), Percent(i32),
#[display(fmt = "{}px", .0)] #[display(fmt = "{}px", .0)]
#[debug(fmt = "{}px", .0)] #[debug(fmt = "{}px", .0)]
#[default]
Pixels(i32), Pixels(i32),
} }
@ -18,7 +20,7 @@ impl FromStr for NumWithUnit {
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
lazy_static::lazy_static! { 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))?; 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)] #[display(fmt = "{}X{}", x, y)]
pub struct Coords { pub struct Coords {
pub x: NumWithUnit, pub x: NumWithUnit,
@ -64,6 +66,20 @@ impl Coords {
y: y.parse().with_context(|| format!("Failed to parse '{}'", y))?, 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)] #[cfg(test)]