diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs
index 250e61cb..7d070ad7 100644
--- a/cosmic-theme/src/model/theme.rs
+++ b/cosmic-theme/src/model/theme.rs
@@ -140,10 +140,20 @@ impl Theme {
Config::new(LIGHT_THEME_ID, Self::VERSION)
}
- #[inline]
- /// get the built in light theme
+ /// Get the built-in light theme with customized defaults.
+ ///
+ /// This produces a clean, minimal light theme with pure white backgrounds,
+ /// generous window rounding, and subtle gray surfaces.
pub fn light_default() -> Self {
- LIGHT_PALETTE.clone().into()
+ let mut builder = ThemeBuilder::light()
+ .bg_color(Srgba::new(1.0, 1.0, 1.0, 1.0))
+ .primary_container_bg(Srgba::new(0.96, 0.96, 0.96, 1.0))
+ .corner_radii(CornerRadii {
+ radius_window: [16.0; 4],
+ ..CornerRadii::default()
+ });
+ builder.secondary_container_bg = Some(Srgba::new(0.93, 0.93, 0.93, 1.0));
+ builder.build()
}
#[inline]
diff --git a/res/icons/window-close.svg b/res/icons/window-close.svg
new file mode 100644
index 00000000..34853139
--- /dev/null
+++ b/res/icons/window-close.svg
@@ -0,0 +1,4 @@
+
diff --git a/res/icons/window-maximize.svg b/res/icons/window-maximize.svg
new file mode 100644
index 00000000..709c7166
--- /dev/null
+++ b/res/icons/window-maximize.svg
@@ -0,0 +1,10 @@
+
diff --git a/res/icons/window-minimize.svg b/res/icons/window-minimize.svg
new file mode 100644
index 00000000..da61d59d
--- /dev/null
+++ b/res/icons/window-minimize.svg
@@ -0,0 +1,3 @@
+
diff --git a/res/icons/window-restore.svg b/res/icons/window-restore.svg
new file mode 100644
index 00000000..4aca309e
--- /dev/null
+++ b/res/icons/window-restore.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 67636dac..00b905bf 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -301,6 +301,11 @@ where
None
}
+ /// Returns an optional icon handle to display in the header bar before the title.
+ fn app_icon(&self) -> Option {
+ None
+ }
+
/// Attaches elements to the start section of the header.
fn header_start(&self) -> Vec> {
Vec::new()
@@ -684,9 +689,18 @@ impl ApplicationExt for App {
border_padding,
])
}));
+ let content_inset = if maximized { 0 } else { 16 };
let content: Element<_> = if content_container {
- content_col
+ let inner: Element<_> = content_col
.apply(container)
+ .padding([7, 0, 0, 0])
+ .width(iced::Length::Fill)
+ .height(iced::Length::Fill)
+ .class(crate::theme::Container::ContentArea)
+ .into();
+
+ container(inner)
+ .padding([8, content_inset, content_inset, content_inset])
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container")))
@@ -699,10 +713,7 @@ impl ApplicationExt for App {
let window_corner_radius = if sharp_corners {
crate::theme::active().cosmic().radius_0()
} else {
- crate::theme::active()
- .cosmic()
- .radius_s()
- .map(|x| if x < 4.0 { x } else { x + 4.0 })
+ crate::theme::active().cosmic().radius_window()
};
let view_column = crate::widget::column::with_capacity(2)
@@ -719,6 +730,10 @@ impl ApplicationExt for App {
.on_double_click(crate::Action::Cosmic(Action::Maximize))
.is_condensed(is_condensed);
+ if let Some(icon) = self.app_icon() {
+ header = header.app_icon(icon);
+ }
+
if self.nav_model().is_some() {
let toggle = crate::widget::nav_bar_toggle()
.active(core.nav_bar_active())
diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs
index 36870cff..0fb5ae57 100644
--- a/src/theme/style/iced.rs
+++ b/src/theme/style/iced.rs
@@ -389,6 +389,7 @@ pub enum Container<'a> {
WindowBackground,
Background,
Card,
+ ContentArea,
ContextDrawer,
Custom(Box iced_container::Style + 'a>),
Dialog,
@@ -595,6 +596,18 @@ impl iced_container::Catalog for Theme {
shadow: Shadow::default(),
},
+ Container::ContentArea => iced_container::Style {
+ icon_color: Some(Color::from(cosmic.background.on)),
+ text_color: Some(Color::from(cosmic.background.on)),
+ background: Some(iced::Background::Color(cosmic.background.base.into())),
+ border: Border {
+ radius: cosmic.corner_radii.radius_s.into(),
+ width: 1.0,
+ color: cosmic.background.divider.into(),
+ },
+ shadow: Shadow::default(),
+ },
+
Container::Card => {
let cosmic = self.cosmic();
diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs
index c5bde28f..b3d4af66 100644
--- a/src/widget/header_bar.rs
+++ b/src/widget/header_bar.rs
@@ -7,12 +7,13 @@ use apply::Apply;
use derive_setters::Setters;
use iced::Length;
use iced_core::{Vector, Widget, widget::tree};
-use std::{borrow::Cow, cmp};
+use std::borrow::Cow;
#[must_use]
pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> {
HeaderBar {
title: Cow::Borrowed(""),
+ app_icon: None,
on_close: None,
on_drag: None,
on_maximize: None,
@@ -38,6 +39,10 @@ pub struct HeaderBar<'a, Message> {
#[setters(skip)]
title: Cow<'a, str>,
+ /// Optional app icon displayed before the title
+ #[setters(skip)]
+ app_icon: Option,
+
/// A message emitted when the close button is pressed.
#[setters(strip_option)]
on_close: Option,
@@ -106,6 +111,13 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
self
}
+ /// Sets the app icon displayed before the title
+ #[must_use]
+ pub fn app_icon(mut self, icon: widget::icon::Handle) -> Self {
+ self.app_icon = Some(icon);
+ self
+ }
+
/// Pushes an element to the start region.
#[must_use]
pub fn start(mut self, widget: impl Into> + 'a) -> Self {
@@ -314,7 +326,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
} = theme::spacing();
// 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);
@@ -324,20 +336,46 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
// Also packs the window controls at the very end.
end.push(self.window_controls());
+ // Build the title element (with optional app icon) and place it in the start region.
+ if !self.title.is_empty() && !self.is_condensed {
+ let mut title = Cow::default();
+ std::mem::swap(&mut title, &mut self.title);
+
+ let title_text: Element<'a, Message> =
+ widget::text::heading(title).into();
+
+ let title_element: Element<'a, Message> = if let Some(icon_handle) = self.app_icon.take() {
+ widget::row::with_capacity(2)
+ .push(
+ widget::icon::icon(icon_handle)
+ .size(20),
+ )
+ .push(title_text)
+ .spacing(space_xxs)
+ .align_y(iced::Alignment::Center)
+ .into()
+ } else {
+ title_text
+ };
+
+ start.push(title_element);
+ }
+
// Center content depending on window border
+ // Non-maximized windows use larger horizontal padding to clear rounded corners.
let padding = match self.density.unwrap_or_else(crate::config::header_size) {
Density::Compact => {
if self.maximized {
[4, 8, 4, 8]
} else {
- [3, 7, 4, 7]
+ [3, 16, 4, 16]
}
}
_ => {
if self.maximized {
[8, 8, 8, 8]
} else {
- [7, 7, 8, 7]
+ [7, 16, 8, 16]
}
}
};
@@ -359,7 +397,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
.round() as u16)
.max(1);
let (left_portion, right_portion) =
- if center.is_empty() && (self.title.is_empty() || self.is_condensed) {
+ if center.is_empty() {
let left_to_right_ratio = left_len as f32 / right_len.max(1) as f32;
let right_to_left_ratio = right_len as f32 / left_len.max(1) as f32;
if right_to_left_ratio > 2. || left_len < 1 {
@@ -372,10 +410,9 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
} else {
(portion, portion)
};
- let title_portion = cmp::max(left_portion, right_portion) * 2;
// Creates the headerbar widget.
let mut widget = widget::row::with_capacity(3)
- // If elements exist in the start region, append them here.
+ // Start region: includes app icon + title + user start elements.
.push(
widget::row::with_children(start)
.spacing(space_xxxs)
@@ -384,8 +421,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
.align_x(iced::Alignment::Start)
.width(Length::FillPortion(left_portion)),
)
- // If elements exist in the center region, use them here.
- // This will otherwise use the title as a widget if a title was defined.
+ // Center region: only explicit center elements.
.push_maybe(if !center.is_empty() {
Some(
widget::row::with_children(center)
@@ -395,10 +431,8 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
.center_x(Length::Fill)
.into(),
)
- } else if !self.title.is_empty() && !self.is_condensed {
- Some(self.title_widget(title_portion))
} else {
- None
+ None::>
})
.push(
widget::row::with_children(end)
@@ -440,22 +474,17 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
widget.into()
}
- fn title_widget(&mut self, title_portion: u16) -> Element<'a, Message> {
- let mut title = Cow::default();
- std::mem::swap(&mut title, &mut self.title);
-
- widget::text::heading(title)
- .apply(widget::container)
- .center(Length::FillPortion(title_portion))
- .into()
- }
-
/// Creates the widget for window controls.
fn window_controls(&mut self) -> Element<'a, Message> {
+ const ICON_MINIMIZE: &[u8] = include_bytes!("../../res/icons/window-minimize.svg");
+ const ICON_MAXIMIZE: &[u8] = include_bytes!("../../res/icons/window-maximize.svg");
+ const ICON_RESTORE: &[u8] = include_bytes!("../../res/icons/window-restore.svg");
+ const ICON_CLOSE: &[u8] = include_bytes!("../../res/icons/window-close.svg");
+
macro_rules! icon {
- ($name:expr, $size:expr, $on_press:expr) => {{
+ ($svg_bytes:expr, $size:expr, $on_press:expr) => {{
let icon = {
- widget::icon::from_name($name)
+ widget::icon::from_svg_bytes($svg_bytes)
.apply(widget::button::icon)
.padding(8)
};
@@ -471,19 +500,19 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
.push_maybe(
self.on_minimize
.take()
- .map(|m: Message| icon!("window-minimize-symbolic", 16, m)),
+ .map(|m: Message| icon!(ICON_MINIMIZE, 16, m)),
)
.push_maybe(self.on_maximize.take().map(|m| {
if self.maximized {
- icon!("window-restore-symbolic", 16, m)
+ icon!(ICON_RESTORE, 16, m)
} else {
- icon!("window-maximize-symbolic", 16, m)
+ icon!(ICON_MAXIMIZE, 16, m)
}
}))
.push_maybe(
self.on_close
.take()
- .map(|m| icon!("window-close-symbolic", 16, m)),
+ .map(|m| icon!(ICON_CLOSE, 16, m)),
)
.spacing(theme::spacing().space_xxs)
.apply(widget::container)