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:
parent
a322516f33
commit
5c3319351c
3 changed files with 90 additions and 24 deletions
|
|
@ -4,6 +4,7 @@
|
||||||
//! Configurations available to libcosmic applications.
|
//! Configurations available to libcosmic applications.
|
||||||
|
|
||||||
use crate::cosmic_theme::Density;
|
use crate::cosmic_theme::Density;
|
||||||
|
use crate::widget::WindowControlsPosition;
|
||||||
use cosmic_config::cosmic_config_derive::CosmicConfigEntry;
|
use cosmic_config::cosmic_config_derive::CosmicConfigEntry;
|
||||||
use cosmic_config::{Config, CosmicConfigEntry};
|
use cosmic_config::{Config, CosmicConfigEntry};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -67,6 +68,12 @@ pub fn header_size() -> Density {
|
||||||
COSMIC_TK.read().unwrap().header_size
|
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.
|
/// Interface density.
|
||||||
#[allow(clippy::missing_panics_doc)]
|
#[allow(clippy::missing_panics_doc)]
|
||||||
pub fn interface_density() -> Density {
|
pub fn interface_density() -> Density {
|
||||||
|
|
@ -109,6 +116,10 @@ pub struct CosmicTk {
|
||||||
|
|
||||||
/// Mono font family
|
/// Mono font family
|
||||||
pub monospace_font: FontConfig,
|
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 {
|
impl Default for CosmicTk {
|
||||||
|
|
@ -132,6 +143,7 @@ impl Default for CosmicTk {
|
||||||
stretch: iced::font::Stretch::Normal,
|
stretch: iced::font::Stretch::Normal,
|
||||||
style: iced::font::Style::Normal,
|
style: iced::font::Style::Normal,
|
||||||
},
|
},
|
||||||
|
window_controls_position: WindowControlsPosition::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,31 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> {
|
||||||
is_ssd: false,
|
is_ssd: false,
|
||||||
on_double_click: None,
|
on_double_click: None,
|
||||||
transparent: false,
|
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)]
|
#[derive(Setters)]
|
||||||
pub struct HeaderBar<'a, Message> {
|
pub struct HeaderBar<'a, Message> {
|
||||||
/// Defines the title of the window
|
/// Defines the title of the window
|
||||||
|
|
@ -91,6 +113,14 @@ pub struct HeaderBar<'a, Message> {
|
||||||
|
|
||||||
/// Whether the headerbar should be transparent
|
/// Whether the headerbar should be transparent
|
||||||
transparent: bool,
|
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> {
|
impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
||||||
|
|
@ -372,12 +402,20 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
||||||
} = theme::spacing();
|
} = theme::spacing();
|
||||||
|
|
||||||
// Take ownership of the regions to be packed.
|
// 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 center = std::mem::take(&mut self.center);
|
||||||
let mut end = std::mem::take(&mut self.end);
|
let mut end = std::mem::take(&mut self.end);
|
||||||
|
|
||||||
// Also packs the window controls at the very end.
|
// Pack window controls on the configured side (reads CosmicTk
|
||||||
end.push(self.window_controls(space_xxs));
|
// 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 self.is_ssd {
|
let padding = if self.is_ssd {
|
||||||
[2, 8, 2, 8]
|
[2, 8, 2, 8]
|
||||||
|
|
@ -447,7 +485,11 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates the widget for window controls.
|
/// 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 {
|
macro_rules! icon {
|
||||||
($name:expr, $size:expr, $on_press:expr) => {{
|
($name:expr, $size:expr, $on_press:expr) => {{
|
||||||
widget::icon::from_name($name)
|
widget::icon::from_name($name)
|
||||||
|
|
@ -460,25 +502,37 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
widget::row::with_capacity(3)
|
let minimize = self
|
||||||
.push_maybe(
|
.on_minimize
|
||||||
self.on_minimize
|
.take()
|
||||||
.take()
|
.map(|m| icon!("window-minimize-symbolic", 16, m));
|
||||||
.map(|m| icon!("window-minimize-symbolic", 16, m)),
|
let maximize = self.on_maximize.take().map(|m| {
|
||||||
)
|
if self.maximized {
|
||||||
.push_maybe(self.on_maximize.take().map(|m| {
|
icon!("window-restore-symbolic", 16, m)
|
||||||
if self.maximized {
|
} else {
|
||||||
icon!("window-restore-symbolic", 16, m)
|
icon!("window-maximize-symbolic", 16, m)
|
||||||
} else {
|
}
|
||||||
icon!("window-maximize-symbolic", 16, m)
|
});
|
||||||
}
|
let close = self
|
||||||
}))
|
.on_close
|
||||||
.push_maybe(
|
.take()
|
||||||
self.on_close
|
.map(|m| icon!("window-close-symbolic", 16, m));
|
||||||
.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)
|
||||||
.spacing(spacing)
|
// 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)
|
.align_y(iced::Alignment::Center)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,7 @@ pub use grid::{Grid, grid};
|
||||||
|
|
||||||
mod header_bar;
|
mod header_bar;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use header_bar::{HeaderBar, header_bar};
|
pub use header_bar::{HeaderBar, WindowControlsPosition, header_bar};
|
||||||
|
|
||||||
pub mod icon;
|
pub mod icon;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue