Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support custom header buttons layouts #30

Merged
merged 3 commits into from
Jul 1, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 171 additions & 50 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,196 @@ 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(&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 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 left_most(&self) -> &Button {
if let Some(minimize) = self.minimize.as_ref() {
minimize
} else if let Some(maximize) = self.maximize.as_ref() {
maximize
} else {
&self.close
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 right_buttons_start_x(&self) -> Option<f32> {
match self.buttons_right.last() {
Some(button) => Some(button.x()),
None => None,
}
PolyMeilex marked this conversation as resolved.
Show resolved Hide resolved
}

pub fn left_buttons_end_x(&self) -> Option<f32> {
match self.buttons_left.last() {
Some(button) => Some(button.end_x()),
None => None,
}
PolyMeilex marked this conversation as resolved.
Show resolved Hide resolved
}

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);
}
}
}

pub fn iter(&self) -> std::array::IntoIter<Option<Button>, 3> {
[
Some(self.close.clone()),
self.maximize.clone(),
self.minimize.clone(),
]
.into_iter()
fn parse_button_layout(sides: Option<(String, String)>) -> Option<ButtonLayout> {
if sides.is_none() {
return None;
}

let (left_side, right_side) = sides.unwrap();
PolyMeilex marked this conversation as resolved.
Show resolved Hide resolved

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.len() == 0 && buttons_right.len() == 0 {
PolyMeilex marked this conversation as resolved.
Show resolved Hide resolved
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) {
PolyMeilex marked this conversation as resolved.
Show resolved Hide resolved
let button_kind = match button {
"close" => Some(ButtonKind::Close),
"maximize" => Some(ButtonKind::Maximize),
"minimize" => Some(ButtonKind::Minimize),
_ => None,
};

if button_kind.is_none() {
warn!("Found unknown button type, ignoring");
continue;
}
let kind = button_kind.unwrap();
PolyMeilex marked this conversation as resolved.
Show resolved Hide resolved
buttons.push(Button::new(kind));
}

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

buttons
PolyMeilex marked this conversation as resolved.
Show resolved Hide resolved
}

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 +234,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 +380,9 @@ pub enum ButtonKind {
Maximize,
Minimize,
}

#[derive(Debug, Copy, Clone, PartialEq)]
pub enum Side {
Left,
Right,
}
34 changes: 34 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,37 @@ 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 config_string = config_string.unwrap();
ilya-m32 marked this conversation as resolved.
Show resolved Hide resolved

let sides_split: Vec<_> = config_string
// Taking last word
.rsplit(" ")
PolyMeilex marked this conversation as resolved.
Show resolved Hide resolved
.nth(0)?
PolyMeilex marked this conversation as resolved.
Show resolved Hide resolved
// Split by left/right side
.split(":")
PolyMeilex marked this conversation as resolved.
Show resolved Hide resolved
// Only two sides
.take(2)
.collect();

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