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!
@ -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

View file

@ -188,77 +188,23 @@ To use that it would look like this:
```
### 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
<windows>
<window name="main_window" stacking="fg">
<size x="300" y="300" />
<pos x="0" y="500" />
<geometry anchor="top left" x="300px" y="50%" width="25%" height="20px">
<widget>
<main/>
</widget>
</window>
</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`.
`foreground` or `fg` *always* stays above windows.
`background` or `bg` *always* stays behind windows. So it will stay on your desktop.
The window block contains multiple elements to configure the window.
- `<geometry>` is used to specify the position and size of the window.
- `<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`.
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">`.
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`.

View file

@ -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<Coords>,
size: Option<Coords>,
anchor: Option<AnchorPoint>,
},
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<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
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: &gtk::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,
},
}
}

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 {
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()?)?,
}
}

View file

@ -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 {

View file

@ -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<i32>,
pub widget: WidgetUse,
@ -21,22 +19,18 @@ pub struct EwwWindowDefinition {
impl EwwWindowDefinition {
pub fn from_xml_element(xml: XmlElement) -> Result<Self> {
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<str> for WindowName {
@ -97,9 +92,3 @@ impl std::borrow::Borrow<str> for WindowName {
&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> {
with_text_pos_context! { self =>
let mut children_iter = self.children();

View file

@ -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<Coords>,
/// Anchorpoint of the window, formatted like "top right"
#[structopt(short, long)]
anchor: Option<AnchorPoint>,
},
/// 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 => {

View file

@ -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<Self, Self::Err> {
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)]