From 10422b8f4a2c1fbe11566a028be0bd27355d3d75 Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Wed, 22 Apr 2026 15:08:12 +0200 Subject: [PATCH] header_bar: add WindowControlsPosition (macOS-style left controls) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new public enum `WindowControlsPosition { Start, End }` and a matching field on `HeaderBar`, allowing window controls (close / minimize / maximize) to be packed on the start side of the headerbar (macOS style, icon order close → minimize → maximize) instead of the default end side (Linux / GNOME style, minimize → maximize → close). Wiring: - `crate::widget::WindowControlsPosition` re-exported alongside `HeaderBar`. - `HeaderBar::controls_position(Option)` setter; when left unset, falls back to `crate::config::window_controls_position()` (reads `CosmicTk.window_controls_position`), mirroring how `density` falls back to `header_size()`. - New `CosmicTk.window_controls_position` field with default `End` for backwards compatibility; serde-friendly enum so existing configs keep working via `#[serde(default)]` semantics. Tested with cosmic-yoterm, cosmic-settings, cosmic-edit, cosmic-files rebuilt against this libcosmic via a local `[patch]` override. Config changes picked up live through the existing cosmic-config subscription. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/config/mod.rs | 12 +++++ src/widget/header_bar.rs | 104 ++++++++++++++++++++++++++++++--------- src/widget/mod.rs | 2 +- 3 files changed, 94 insertions(+), 24 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 9807961..1691caa 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -4,6 +4,7 @@ //! Configurations available to libcosmic applications. use crate::cosmic_theme::Density; +use crate::widget::WindowControlsPosition; use cosmic_config::cosmic_config_derive::CosmicConfigEntry; use cosmic_config::{Config, CosmicConfigEntry}; use serde::{Deserialize, Serialize}; @@ -67,6 +68,12 @@ pub fn header_size() -> Density { COSMIC_TK.read().unwrap().header_size } +/// Position of the window control buttons (close / minimize / maximize). +#[allow(clippy::missing_panics_doc)] +pub fn window_controls_position() -> WindowControlsPosition { + COSMIC_TK.read().unwrap().window_controls_position +} + /// Interface density. #[allow(clippy::missing_panics_doc)] pub fn interface_density() -> Density { @@ -109,6 +116,10 @@ pub struct CosmicTk { /// Mono font family pub monospace_font: FontConfig, + + /// Side on which window control buttons (close / minimize / maximize) + /// are placed. `End` = right (Linux / GNOME), `Start` = left (macOS). + pub window_controls_position: WindowControlsPosition, } impl Default for CosmicTk { @@ -132,6 +143,7 @@ impl Default for CosmicTk { stretch: iced::font::Stretch::Normal, style: iced::font::Style::Normal, }, + window_controls_position: WindowControlsPosition::default(), } } } diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 556466f..82a7e8e 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -27,9 +27,32 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { sharp_corners: false, is_ssd: false, on_double_click: None, + transparent: false, + controls_position: None, } } +/// Position of the window control buttons (close/min/max) within the headerbar. +#[derive( + Clone, + Copy, + Debug, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, +)] +pub enum WindowControlsPosition { + /// Controls packed at the start (left on LTR) — macOS style. + /// Internal icon order becomes close → minimize → maximize. + Start, + /// Controls packed at the end (right on LTR) — Linux / GNOME style. + /// Internal icon order is minimize → maximize → close. + #[default] + End, +} + #[derive(Setters)] pub struct HeaderBar<'a, Message> { /// Defines the title of the window @@ -88,6 +111,17 @@ pub struct HeaderBar<'a, Message> { /// HeaderBar used for server-side decorations is_ssd: bool, + + /// Whether the headerbar should be transparent + transparent: bool, + + /// Side on which the window control buttons (close / minimize / maximize) + /// are rendered. `None` falls back to the user's CosmicTk config + /// (`crate::config::window_controls_position()`). `Some` overrides it. + /// `End` matches Linux / GNOME conventions; `Start` provides macOS-style + /// left-side controls. + #[setters(strip_option)] + controls_position: Option, } impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { @@ -370,12 +404,20 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { let is_ssd = self.is_ssd; // Take ownership of the regions to be packed. - let start = std::mem::take(&mut self.start); + let mut start = std::mem::take(&mut self.start); let center = std::mem::take(&mut self.center); let mut end = std::mem::take(&mut self.end); - // Also packs the window controls at the very end. - end.push(self.window_controls(space_xxs)); + // Pack window controls on the configured side (reads CosmicTk + // config when the builder did not set an explicit override). + let controls_position = self + .controls_position + .unwrap_or_else(crate::config::window_controls_position); + let controls = self.window_controls(space_xxs, controls_position); + match controls_position { + WindowControlsPosition::End => end.push(controls), + WindowControlsPosition::Start => start.insert(0, controls), + } let padding = if is_ssd { [2, 8, 2, 8] @@ -445,7 +487,11 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { } /// Creates the widget for window controls. - fn window_controls(&mut self, spacing: u16) -> Element<'a, Message> { + fn window_controls( + &mut self, + spacing: u16, + controls_position: WindowControlsPosition, + ) -> Element<'a, Message> { macro_rules! icon { ($name:expr, $size:expr, $on_press:expr) => {{ widget::icon::from_name($name) @@ -458,25 +504,37 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { }}; } - widget::row::with_capacity(3) - .push_maybe( - self.on_minimize - .take() - .map(|m| icon!("window-minimize-symbolic", 16, m)), - ) - .push_maybe(self.on_maximize.take().map(|m| { - if self.maximized { - icon!("window-restore-symbolic", 16, m) - } else { - icon!("window-maximize-symbolic", 16, m) - } - })) - .push_maybe( - self.on_close - .take() - .map(|m| icon!("window-close-symbolic", 16, m)), - ) - .spacing(spacing) + let minimize = self + .on_minimize + .take() + .map(|m| icon!("window-minimize-symbolic", 16, m)); + let maximize = self.on_maximize.take().map(|m| { + if self.maximized { + icon!("window-restore-symbolic", 16, m) + } else { + icon!("window-maximize-symbolic", 16, m) + } + }); + let close = self + .on_close + .take() + .map(|m| icon!("window-close-symbolic", 16, m)); + + // Icon order follows the OS convention for the chosen side: + // End → minimize, maximize, close (Linux / GNOME) + // Start → close, minimize, maximize (macOS) + let row = widget::row::with_capacity(3); + let row = match controls_position { + WindowControlsPosition::End => row + .push_maybe(minimize) + .push_maybe(maximize) + .push_maybe(close), + WindowControlsPosition::Start => row + .push_maybe(close) + .push_maybe(minimize) + .push_maybe(maximize), + }; + row.spacing(spacing) .align_y(iced::Alignment::Center) .into() } diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 5089b99..331d6c2 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -225,7 +225,7 @@ pub use grid::{Grid, grid}; mod header_bar; #[doc(inline)] -pub use header_bar::{HeaderBar, header_bar}; +pub use header_bar::{HeaderBar, WindowControlsPosition, header_bar}; pub mod icon; #[doc(inline)]