Skip to content

Commit

Permalink
Support custom header buttons layouts (PolyMeilex#30)
Browse files Browse the repository at this point in the history
* Support custom header buttons layouts

* Support custom header buttons layouts: review tweaks

* Safe unwrap layout config function

Co-authored-by: Bartłomiej Maryńczak <marynczak.bartlomiej@gmail.com>

---------

Co-authored-by: Ilya Malyavin <ilya-m64@protonmail.com>
Co-authored-by: Bartłomiej Maryńczak <marynczak.bartlomiej@gmail.com>
  • Loading branch information
3 people authored Jul 1, 2023
1 parent 4b61252 commit 68035e5
Show file tree
Hide file tree
Showing 3 changed files with 257 additions and 98 deletions.
208 changes: 159 additions & 49 deletions src/buttons.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use log::warn;
use tiny_skia::{FillRule, PathBuilder, PixmapMut, Rect, Stroke, Transform};

use smithay_client_toolkit::shell::xdg::window::{WindowManagerCapabilities, WindowState};
Expand All @@ -11,86 +12,185 @@ const BUTTON_SPACING: f32 = 13.;

#[derive(Debug)]
pub(crate) struct Buttons {
pub close: Button,
pub maximize: Option<Button>,
pub minimize: Option<Button>,
// Sorted by order vec of buttons for the left and right sides
buttons_left: Vec<Button>,
buttons_right: Vec<Button>,
layout_config: Option<(String, String)>,
}

type ButtonLayout = (Vec<Button>, Vec<Button>);

impl Default for Buttons {
fn default() -> Self {
let (buttons_left, buttons_right) = Buttons::get_default_buttons_layout();

Self {
close: Button::new(ButtonKind::Close),
maximize: Some(Button::new(ButtonKind::Maximize)),
minimize: Some(Button::new(ButtonKind::Minimize)),
buttons_left,
buttons_right,
layout_config: None,
}
}
}

impl Buttons {
pub fn new(layout_config: Option<(String, String)>) -> Self {
match Buttons::parse_button_layout(layout_config.clone()) {
Some((buttons_left, buttons_right)) => Self {
buttons_left,
buttons_right,
layout_config,
},
_ => Self::default(),
}
}

/// Rearrange the buttons with the new width.
pub fn arrange(&mut self, width: u32) {
let mut x = width as f32 - BUTTON_MARGIN;

for button in [
Some(&mut self.close),
self.maximize.as_mut(),
self.minimize.as_mut(),
]
.into_iter()
.flatten()
{
pub fn arrange(&mut self, width: u32, margin_h: f32) {
let mut left_x = BUTTON_MARGIN + margin_h;
let mut right_x = width as f32 - BUTTON_MARGIN;

for button in &mut self.buttons_left {
button.offset = left_x;

// Add the button size plus spacing
left_x += BUTTON_SIZE + BUTTON_SPACING;
}

for button in &mut self.buttons_right {
// Subtract the button size.
x -= BUTTON_SIZE;
right_x -= BUTTON_SIZE;

// Update it's
button.offset = x;
// Update it
button.offset = right_x;

// Subtract spacing for the next button.
x -= BUTTON_SPACING;
right_x -= BUTTON_SPACING;
}
}

/// Find the coordinate of the button.
pub fn find_button(&self, x: f64, y: f64) -> Location {
let x = x as f32;
let y = y as f32;
if self.close.contains(x, y) {
Location::Button(ButtonKind::Close)
} else if self.maximize.as_ref().map(|b| b.contains(x, y)) == Some(true) {
Location::Button(ButtonKind::Maximize)
} else if self.minimize.as_ref().map(|b| b.contains(x, y)) == Some(true) {
Location::Button(ButtonKind::Minimize)
} else {
Location::Head
let buttons = self.buttons_left.iter().chain(self.buttons_right.iter());

for button in buttons {
if button.contains(x, y) {
return Location::Button(button.kind);
}
}

Location::Head
}

pub fn update_wm_capabilities(&mut self, wm_capabilites: WindowManagerCapabilities) {
let supports_maximize = wm_capabilites.contains(WindowManagerCapabilities::MAXIMIZE);
let supports_minimize = wm_capabilites.contains(WindowManagerCapabilities::MINIMIZE);

self.update_buttons(supports_maximize, supports_minimize);
}

pub fn update_buttons(&mut self, supports_maximize: bool, supports_minimize: bool) {
let is_supported = |button: &Button| match button.kind {
ButtonKind::Close => true,
ButtonKind::Maximize => supports_maximize,
ButtonKind::Minimize => supports_minimize,
};

let (buttons_left, buttons_right) =
Buttons::parse_button_layout(self.layout_config.clone())
.unwrap_or_else(Buttons::get_default_buttons_layout);

self.buttons_left = buttons_left.into_iter().filter(is_supported).collect();
self.buttons_right = buttons_right.into_iter().filter(is_supported).collect();
}

pub fn update(&mut self, wm_capabilites: WindowManagerCapabilities) {
self.maximize = wm_capabilites
.contains(WindowManagerCapabilities::MAXIMIZE)
.then_some(Button::new(ButtonKind::Maximize));
self.minimize = wm_capabilites
.contains(WindowManagerCapabilities::MINIMIZE)
.then_some(Button::new(ButtonKind::Minimize));
pub fn right_buttons_start_x(&self) -> Option<f32> {
self.buttons_right.last().map(|button| button.x())
}

pub fn left_most(&self) -> &Button {
if let Some(minimize) = self.minimize.as_ref() {
minimize
} else if let Some(maximize) = self.maximize.as_ref() {
maximize
pub fn left_buttons_end_x(&self) -> Option<f32> {
self.buttons_left.last().map(|button| button.end_x())
}

pub fn draw(
&self,
start_x: f32,
end_x: f32,
scale: f32,
colors: &ColorMap,
mouse_location: Location,
pixmap: &mut PixmapMut,
resizable: bool,
state: &WindowState,
) {
let left_buttons_right_limit =
self.right_buttons_start_x().unwrap_or(end_x).min(end_x) - BUTTON_SPACING;
let buttons_left = self.buttons_left.iter().map(|x| (x, Side::Left));
let buttons_right = self.buttons_right.iter().map(|x| (x, Side::Right));

for (button, side) in buttons_left.chain(buttons_right) {
let is_visible = button.x() > start_x && button.end_x() < end_x
// If we have buttons from both sides and they overlap, prefer the right side
&& (side == Side::Right || button.end_x() < left_buttons_right_limit);

if is_visible {
button.draw(scale, colors, mouse_location, pixmap, resizable, state);
}
}
}

fn parse_button_layout(sides: Option<(String, String)>) -> Option<ButtonLayout> {
let Some((left_side, right_side)) = sides else {
return None;
};

let buttons_left = Buttons::parse_button_layout_side(left_side, Side::Left);
let buttons_right = Buttons::parse_button_layout_side(right_side, Side::Right);

if buttons_left.is_empty() && buttons_right.is_empty() {
warn!("No valid buttons found in configuration");
return None;
}

Some((buttons_left, buttons_right))
}

fn parse_button_layout_side(config: String, side: Side) -> Vec<Button> {
let mut buttons: Vec<Button> = vec![];

for button in config.split(',').take(3) {
let button_kind = match button {
"close" => Some(ButtonKind::Close),
"maximize" => Some(ButtonKind::Maximize),
"minimize" => Some(ButtonKind::Minimize),
_ => None,
};

let Some(kind) = button_kind else {
warn!("Found unknown button type, ignoring");
continue;
};
buttons.push(Button::new(kind));
}

// For the right side, we need to revert the order
if side == Side::Right {
buttons.into_iter().rev().collect()
} else {
&self.close
buttons
}
}

pub fn iter(&self) -> std::array::IntoIter<Option<Button>, 3> {
[
Some(self.close.clone()),
self.maximize.clone(),
self.minimize.clone(),
]
.into_iter()
fn get_default_buttons_layout() -> ButtonLayout {
(
vec![],
vec![
Button::new(ButtonKind::Close),
Button::new(ButtonKind::Maximize),
Button::new(ButtonKind::Minimize),
],
)
}
}

Expand Down Expand Up @@ -123,6 +223,10 @@ impl Button {
BUTTON_MARGIN + self.radius()
}

pub fn end_x(&self) -> f32 {
self.offset + BUTTON_SIZE
}

fn contains(&self, x: f32, y: f32) -> bool {
x > self.offset
&& x < self.offset + BUTTON_SIZE
Expand Down Expand Up @@ -265,3 +369,9 @@ pub enum ButtonKind {
Maximize,
Minimize,
}

#[derive(Debug, Copy, Clone, PartialEq)]
pub enum Side {
Left,
Right,
}
32 changes: 32 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,35 @@ pub(crate) fn prefer_dark() -> bool {

matches!(stdout, Some(s) if s.trim().ends_with("uint32 1"))
}

/// Query system configuration for buttons layout.
/// Should be updated to use standard xdg-desktop-portal specs once available
/// https://github.com/flatpak/xdg-desktop-portal/pull/996
pub(crate) fn get_button_layout_config() -> Option<(String, String)> {
let config_string = Command::new("dbus-send")
.arg("--reply-timeout=100")
.arg("--print-reply=literal")
.arg("--dest=org.freedesktop.portal.Desktop")
.arg("/org/freedesktop/portal/desktop")
.arg("org.freedesktop.portal.Settings.Read")
.arg("string:org.gnome.desktop.wm.preferences")
.arg("string:button-layout")
.output()
.ok()
.and_then(|out| String::from_utf8(out.stdout).ok())?;

let sides_split: Vec<_> = config_string
// Taking last word
.rsplit(' ')
.next()?
// Split by left/right side
.split(':')
// Only two sides
.take(2)
.collect();

match sides_split.as_slice() {
[left, right] => Some((left.to_string(), right.to_string())),
_ => None,
}
}
Loading

0 comments on commit 68035e5

Please sign in to comment.