header_bar: add WindowControlsPosition (macOS-style left controls)

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<WindowControlsPosition>)` 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) <noreply@anthropic.com>
This commit is contained in:
Lionel DARNIS 2026-04-22 15:08:12 +02:00
parent 87510782ae
commit 10422b8f4a
3 changed files with 94 additions and 24 deletions

View file

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

View file

@ -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<WindowControlsPosition>,
}
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()
}

View file

@ -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)]