diff --git a/src/app.rs b/src/app.rs index 09a946153..b21172978 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,7 +1,7 @@ use crate::{ HEIGHT, components::{Centerbox, menu::MenuType}, - config::{self, BarSurface, Config, ModuleName, Modules, WorkspaceIndicatorFormat}, + config::{self, Config, ModuleName, Modules, WorkspaceIndicatorFormat}, get_log_spec, i18n::{Localizer, init_localizer}, ipc::IpcCommand, @@ -24,7 +24,7 @@ use crate::{ osd::{self, Osd}, outputs::{HasOutput, Outputs}, services::{ReadOnlyService, xdg_icons}, - theme::{AshellTheme, BarLayout, backdrop_color, darken_color, init_theme, use_theme}, + theme::{AshellTheme, BarLayout, darken_color, init_theme, use_theme}, }; use flexi_logger::LoggerHandle; use iced::futures::StreamExt; @@ -84,7 +84,7 @@ impl App { ) -> impl FnOnce() -> (Self, Task) { move || { let mut outputs = Outputs::new( - BarLayout::from_appearance(&config.appearance.bar), + BarLayout::new(config.appearance.bar), config.position, config.layer, config.appearance.scale_factor, @@ -238,7 +238,7 @@ impl App { ); let (bar_position, bar_layout, scale_factor) = use_theme(|t| (t.bar_position, t.bar_layout(), t.scale_factor)); - let new_layout = BarLayout::from_appearance(&config.appearance.bar); + let new_layout = BarLayout::new(config.appearance.bar); if self.general_config.outputs != config.outputs || bar_position != config.position || bar_layout != new_layout @@ -588,61 +588,63 @@ impl App { let [left, center, right] = self.modules_section(id); - let (space, bar_surface, menu, animations_enabled, bar_radius, blur) = - use_theme(|t| { - ( - t.space, - t.bar_surface, - t.menu, - t.animations_enabled, - t.bar_border_radius(), - t.blur, - ) - }); + let (space, bar, menu, animations_enabled, theme_radius, blur) = use_theme(|t| { + ( + t.space, + t.bar, + t.menu, + t.animations_enabled, + t.radius, + t.blur, + ) + }); + let (bar_bg_opacity, bar_border, bar_inset) = + (bar.opacity.background, bar.border, bar.inset); + let radius = bar_border.radius.resolve(theme_radius); + + let has_inset = bar_inset > 0.; + let centerbox = Centerbox::new([left, center, right]) .animated(animations_enabled) .spacing(space.xxs) .width(Length::Fill) .align_items(Alignment::Center) - .height(if bar_surface == BarSurface::Transparent { - HEIGHT - } else { + .height(if has_inset { HEIGHT - space.xs as f64 - } as f32) - .padding(if bar_surface == BarSurface::Transparent { - [space.xxs, space.xxs] } else { + HEIGHT + } as f32) + .padding(if has_inset { [0.0, 0.0] + } else { + [space.xxs, space.xxs] }); let menu_is_open = self.outputs.menu_is_open(); - let bar_style = move |t: &Theme| container::Style { - background: match bar_surface { - BarSurface::Solid => Some({ - let bg = t.palette().background; - if menu_is_open { - darken_color(bg, menu.backdrop) - } else { - bg - } - .into() - }), - BarSurface::Transparent => { - if menu_is_open { - Some(backdrop_color(menu.backdrop).into()) - } else { - None - } - } - }, - border: iced::Border { - radius: bar_radius, + let bar_style = move |t: &Theme| { + let bg = t.palette().background.scale_alpha(bar_bg_opacity); + + container::Style { + background: { + Some( + if menu_is_open { + darken_color(bg, menu.backdrop) + } else { + bg + } + .into(), + ) + }, + border: iced::Border { + radius, + color: bar_border.color.get_base(), + width: bar_border.width, + }, ..Default::default() - }, - ..Default::default() + } }; // In Transparent the individual module groups carry the blur. - let status_bar: Element<'_, Message> = if blur && bar_surface == BarSurface::Solid { + let status_bar: Element<'_, Message> = if blur && bar_bg_opacity > 0. { blur_container(centerbox).style(bar_style).into() } else { container(centerbox).style(bar_style).into() diff --git a/src/components/menu.rs b/src/components/menu.rs index 94c022e7b..581b05f52 100644 --- a/src/components/menu.rs +++ b/src/components/menu.rs @@ -1,6 +1,6 @@ use crate::app::{self, App}; use crate::components::{self, ButtonUIRef}; -use crate::config::{BarSurface, Position}; +use crate::config::Position; use crate::theme::{backdrop_color, use_theme}; use iced::alignment::Vertical; use iced::widget::container::Style; @@ -301,19 +301,21 @@ impl App { content: Element<'a, app::Message>, button_ui_ref: ButtonUIRef, ) -> Element<'a, app::Message> { - let (space, radius, bar_surface, bar_position, menu_backdrop, blur) = use_theme(|t| { - ( - t.space, - t.radius, - t.bar_surface, - t.bar_position, - t.menu.backdrop, - t.blur, - ) - }); + let (space, radius, bar_inset, bar_position, menu_backdrop, menu_opacity, blur) = + use_theme(|t| { + ( + t.space, + t.radius, + t.bar.inset, + t.bar_position, + t.menu.backdrop, + t.menu.opacity, + t.blur, + ) + }); let menu_style = move |theme: &Theme| Style { - background: Some(theme.palette().background.into()), + background: Some(theme.palette().background.scale_alpha(menu_opacity).into()), border: Border { color: theme.extended_palette().background.weakest.color, width: 1., @@ -337,11 +339,7 @@ impl App { components::MenuWrapper::new(button_ui_ref.position.x, menu_body) .padding({ - let v_padding = match bar_surface { - BarSurface::Solid => 2, - BarSurface::Transparent => 0, - }; - + let v_padding = if bar_inset > 0. { 2 } else { 0 }; Padding::new(0.) .top(if bar_position == Position::Top { v_padding diff --git a/src/components/mod.rs b/src/components/mod.rs index 82c98abdd..b9324084f 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -8,6 +8,7 @@ pub mod menu; mod menu_wrapper; mod module_group; mod module_item; +mod module_view; pub mod password_dialog; mod position_button; mod quick_setting_button; @@ -25,6 +26,7 @@ pub use menu::MenuSize; pub use menu_wrapper::*; pub use module_group::*; pub use module_item::*; +pub use module_view::*; pub use position_button::*; pub use quick_setting_button::*; pub use slider_control::*; diff --git a/src/components/module_group.rs b/src/components/module_group.rs index a329971a3..1fb6e5962 100644 --- a/src/components/module_group.rs +++ b/src/components/module_group.rs @@ -1,35 +1,64 @@ -use crate::{config::BarSurface, theme::use_theme}; +use crate::{ + config::{BorderAppearance, ModuleAppearance}, + theme::use_theme, +}; use iced::{ - Border, Color, Element, + Border, Element, widget::{blur_container, container}, }; -/// Wraps content with the appropriate bar surface container. -/// -/// - `Solid` → pass through as-is (the bar itself carries the background) -/// - `Transparent` → wrap in a container with background color + rounded border, -/// using `blur_container` when compositor blur is enabled -pub fn module_group<'a, Msg: 'static>(content: Element<'a, Msg>) -> Element<'a, Msg> { - let (bar_surface, radius, blur) = - use_theme(|theme| (theme.bar_surface, theme.radius, theme.blur)); +/// Wraps content in a container styled from theme +pub fn module_group<'a, Msg: 'static>( + content: Element<'a, Msg>, + module_apperance: ModuleAppearance, +) -> Element<'a, Msg> { + let (theme_space, theme_radius, module_opacity, module_border, blur) = use_theme(|theme| { + ( + theme.space, + theme.radius, + theme.bar.opacity.module, + theme.bar.module_border, + theme.blur, + ) + }); - match bar_surface { - BarSurface::Solid => content, - BarSurface::Transparent => { - let style = move |iced_theme: &iced::Theme| container::Style { - background: Some(iced_theme.palette().background.into()), - border: Border { - width: 0.0, - radius: radius.lg.into(), - color: Color::TRANSPARENT, - }, - ..container::Style::default() - }; - if blur { - blur_container(content).style(style).into() - } else { - container(content).style(style).into() + let border = module_apperance.border.map_or_else( + || Border { + width: module_border.width, + color: module_border.color.get_base(), + radius: module_border.radius.resolve(theme_radius), + }, + |BorderAppearance { + width, + radius, + color, + }| { + Border { + width, + radius: radius.resolve(theme_radius), + color: color.get_base(), } + }, + ); + let opacity = module_apperance.opacity.unwrap_or(module_opacity); + + let padding = theme_space.resolve(module_apperance.padding); + let style = move |iced_theme: &iced::Theme| { + let background = module_apperance.background.map_or_else( + || iced_theme.palette().background.scale_alpha(opacity), + |b| b.get_base().scale_alpha(opacity), + ); + + container::Style { + background: Some(background.into()), + border, + ..container::Style::default() } + }; + + if blur { + blur_container(content).padding(padding).style(style).into() + } else { + container(content).padding(padding).style(style).into() } } diff --git a/src/components/module_item.rs b/src/components/module_item.rs index 05e6f69c8..163872ca2 100644 --- a/src/components/module_item.rs +++ b/src/components/module_item.rs @@ -1,4 +1,4 @@ -use crate::{components::position_button, theme::use_theme}; +use crate::{components::position_button, config::ModuleAppearance, theme::use_theme}; use iced::{Alignment, Element, Length, widget::container}; use super::ButtonUIRef; @@ -9,6 +9,7 @@ use super::ButtonUIRef; /// When no press handler is set, renders as a plain container. pub struct ModuleItem<'a, Msg> { content: Element<'a, Msg>, + appearance: Option, on_press: Option, on_press_with_position: Option Msg + 'a>>, on_right_press: Option, @@ -17,9 +18,13 @@ pub struct ModuleItem<'a, Msg> { on_scroll_down: Option, } -pub fn module_item<'a, Msg: 'static + Clone>(content: Element<'a, Msg>) -> ModuleItem<'a, Msg> { +pub fn module_item<'a, Msg: 'static + Clone>( + content: Element<'a, Msg>, + appearance: Option, +) -> ModuleItem<'a, Msg> { ModuleItem { content, + appearance, on_press: None, on_press_with_position: None, on_right_press: None, @@ -64,7 +69,7 @@ impl<'a, Msg: 'static + Clone> ModuleItem<'a, Msg> { impl<'a, Msg: 'static + Clone> From> for Element<'a, Msg> { fn from(item: ModuleItem<'a, Msg>) -> Self { let (space, module_button_style) = - use_theme(|theme| (theme.space, theme.module_button_style())); + use_theme(|theme| (theme.space, theme.module_button_style(item.appearance))); let has_action = item.on_press.is_some() || item.on_press_with_position.is_some(); diff --git a/src/components/module_view.rs b/src/components/module_view.rs new file mode 100644 index 000000000..b809b56f2 --- /dev/null +++ b/src/components/module_view.rs @@ -0,0 +1,175 @@ +use iced::Row; +use iced::alignment::Vertical; +use iced::{Element, Length}; + +use crate::modules::OnModulePress; + +pub struct ModuleView<'a, Msg> { + pub content: ModuleContent<'a, Msg>, +} + +pub enum ModuleContent<'a, Msg> { + Row(ModuleRow<'a, Msg>), + Element(Element<'a, Msg>), +} + +impl<'a, Msg> std::fmt::Debug for ModuleContent<'a, Msg> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ModuleContent::Element(_) => f.write_str("Element(..)"), + ModuleContent::Row(row) => f + .debug_struct("Row") + .field("children", &row.children.len()) + .field("spacing", &row.spacing) + .field("align_y", &row.align_y) + .finish(), + } + } +} + +impl<'a, Msg: 'a> ModuleView<'a, Msg> { + pub fn new(content: impl Into>) -> Self { + Self { + content: content.into(), + } + } + + pub fn into_element(self) -> Element<'a, Msg> { + match self.content { + ModuleContent::Element(element) => element, + ModuleContent::Row(row) => row.into_element(), + } + } + + pub fn map(self, f: impl Fn(Msg) -> NewMsg + Clone + 'a) -> ModuleView<'a, NewMsg> + where + NewMsg: 'a, + { + ModuleView::new(match self.content { + ModuleContent::Element(element) => ModuleContent::Element(element.map(f)), + ModuleContent::Row(row) => ModuleContent::Row(row.map(f)), + }) + } + + pub fn map_elements(self, f: impl Fn(Element<'a, Msg>) -> Element<'a, Msg> + Clone) -> Self { + Self { + content: match self.content { + ModuleContent::Element(element) => ModuleContent::Element(f(element)), + ModuleContent::Row(mut row) => { + row.children = row + .children + .into_iter() + .map(|child| f.clone()(child)) + .collect(); + ModuleContent::Row(row) + } + }, + } + } +} + +impl<'a, Msg> From> for ModuleView<'a, Msg> { + fn from(element: Element<'a, Msg>) -> Self { + Self { + content: ModuleContent::Element(element), + } + } +} + +impl<'a, Msg> From> for ModuleView<'a, Msg> { + fn from(row: ModuleRow<'a, Msg>) -> Self { + Self { + content: ModuleContent::Row(row), + } + } +} + +pub struct ModuleRow<'a, Msg> { + pub children: Vec>, + pub spacing: f32, + pub align_y: Vertical, + pub height: Length, +} + +impl<'a, Msg: 'a> ModuleRow<'a, Msg> { + #[allow(unused)] + pub fn new() -> Self { + Self { + children: Vec::new(), + spacing: 0.0, + align_y: Vertical::Center, + height: Length::Fill, + } + } + + pub fn with_children(children: I) -> Self + where + I: IntoIterator, + I::Item: Into>, + { + let children = children.into_iter(); + let mut row = Self::with_capacity(children.size_hint().0); + + row.children.extend(children.map(Into::into)); + + row + } + + pub fn with_capacity(capacity: usize) -> Self { + Self { + children: Vec::with_capacity(capacity), + spacing: 0.0, + align_y: Vertical::Center, + height: Length::Fill, + } + } + + pub fn push(mut self, child: impl Into>) -> Self { + self.children.push(child.into()); + self + } + + pub fn spacing(mut self, spacing: f32) -> Self { + self.spacing = spacing; + self + } + + pub fn align_y(mut self, align: impl Into) -> Self { + self.align_y = align.into(); + self + } + + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + pub fn into_element(self) -> Element<'a, Msg> { + Row::with_children(self.children) + .height(self.height) + .align_y(self.align_y) + .spacing(self.spacing) + .into() + } + + pub fn map(self, f: impl Fn(Msg) -> NewMsg + Clone + 'a) -> ModuleRow<'a, NewMsg> + where + NewMsg: 'a, + { + ModuleRow { + children: self + .children + .into_iter() + .map(|child| child.map(f.clone())) + .collect(), + spacing: self.spacing, + align_y: self.align_y, + height: self.height, + } + } +} + +pub struct ModuleResult<'a, Msg: 'static> { + pub view: ModuleView<'a, Msg>, + pub action: Option, +} diff --git a/src/components/sub_menu_wrapper.rs b/src/components/sub_menu_wrapper.rs index 07377c91d..74fc6c329 100644 --- a/src/components/sub_menu_wrapper.rs +++ b/src/components/sub_menu_wrapper.rs @@ -2,11 +2,20 @@ use crate::theme::use_theme; use iced::{Background, Border, Element, Length, Theme, widget::container}; pub fn sub_menu_wrapper<'a, Msg: 'static>(content: Element<'a, Msg>) -> Element<'a, Msg> { - let (radius, space) = use_theme(|theme| (theme.radius, theme.space)); + let (bg_opacity, radius, space) = + use_theme(|theme| (theme.menu.opacity, theme.radius, theme.space)); container(content) .style(move |theme: &Theme| container::Style { - background: Background::Color(theme.extended_palette().background.weak.color).into(), + background: Background::Color( + theme + .extended_palette() + .background + .weak + .color + .scale_alpha(bg_opacity), + ) + .into(), border: Border::default().rounded(radius.lg), ..container::Style::default() }) diff --git a/src/config.rs b/src/config.rs index 0363b9351..d255428b9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,7 @@ use crate::app::Message; use crate::i18n::{UnitSystem, unit_system}; use crate::services::upower::PeripheralDeviceKind; +use crate::theme::Radius; use crate::utils::celsius_to_fahrenheit; use hex_color::HexColor; use iced::futures::StreamExt; @@ -92,6 +93,32 @@ impl Config { self.settings.validate(); self.media_player.validate(); } + + fn translate_deprecated(&mut self) { + if let Some(surface) = self.appearance.bar.surface { + let bar_appearance = &mut self.appearance.bar; + + log::warn!( + "`appearance.bar.surface` is deprecated. \ + configure `appearance.bar` directly instead — see the docs for equivalents." + ); + + match surface { + BarSurface::Solid => { + bar_appearance.opacity.module = 0.; + bar_appearance.module_border.width = 0.; + bar_appearance.inset = 8.; + bar_appearance.opacity.background = 1.; + } + BarSurface::Transparent => { + bar_appearance.opacity.background = 0.; + bar_appearance.inset = 0.; + } + } + } + + // other deprecated stuffs + } } #[derive(Deserialize, Clone, Debug)] @@ -796,7 +823,7 @@ fn hex_to_pair(hex: HexColor, text: Option, text_fallback: Color) -> p ) } -#[derive(Deserialize, Clone, Copy, Debug)] +#[derive(Deserialize, Clone, Copy, Debug, PartialEq)] #[serde(untagged)] pub enum AppearanceColor { Simple(HexColor), @@ -993,6 +1020,25 @@ impl<'de> Deserialize<'de> for BarRadius { } } +impl BarRadius { + pub fn resolve(&self, scale: Radius) -> iced::border::Radius { + iced::border::Radius { + top_left: scale.resolve(self.top_left), + top_right: scale.resolve(self.top_right), + bottom_left: scale.resolve(self.bottom_left), + bottom_right: scale.resolve(self.bottom_right), + } + } + pub fn new(size: RadiusSize) -> Self { + Self { + top_left: size, + top_right: size, + bottom_left: size, + bottom_right: size, + } + } +} + /// Per-edge margin selection, deserialized with CSS `margin` shorthand: /// 1 value = all edges, 2 = `[vertical, horizontal]`, 4 = `[top, right, bottom, left]`. #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] @@ -1016,30 +1062,134 @@ impl<'de> Deserialize<'de> for BarMargin { } } -#[derive(Deserialize, Default, Clone, Copy, Debug, PartialEq)] +#[derive(Deserialize, Clone, Copy, Debug, PartialEq)] #[serde(default)] -pub struct BarAppearance { - pub surface: BarSurface, +pub struct BorderAppearance { pub radius: BarRadius, + pub width: f32, + pub color: AppearanceColor, +} + +impl Default for BorderAppearance { + fn default() -> Self { + Self { + radius: BarRadius::new(RadiusSize::Lg), + width: 0f32, + color: AppearanceColor::Simple(HexColor::default()), + } + } +} + +#[derive(Deserialize, Clone, Copy, Debug, PartialEq)] +#[serde(default)] +pub struct OpacityAppearance { + #[serde(deserialize_with = "opacity_deserializer")] + pub button: f32, + #[serde(deserialize_with = "opacity_deserializer")] + pub background: f32, + #[serde(deserialize_with = "opacity_deserializer")] + pub module: f32, +} +impl Default for OpacityAppearance { + fn default() -> Self { + Self { + button: default_opacity(), + background: 0., + module: default_opacity(), + } + } +} + +#[derive(Deserialize, Clone, Copy, Debug, PartialEq)] +#[serde(default)] +pub struct BarAppearance { + pub opacity: OpacityAppearance, + pub module_border: BorderAppearance, + pub border: BorderAppearance, pub margin: BarMargin, + + pub inset: f32, + + /// Deprecated - set bar appearance directly instead. + pub surface: Option, +} + +impl BarAppearance {} + +impl Default for BarAppearance { + fn default() -> Self { + Self { + border: BorderAppearance { + radius: BarRadius::new(RadiusSize::None), + ..Default::default() + }, + opacity: OpacityAppearance::default(), + module_border: BorderAppearance::default(), + margin: BarMargin::default(), + inset: 0.0, + surface: None, + } + } } -#[derive(Deserialize, Default, Clone, Copy, Debug)] +#[derive(Deserialize, Clone, Copy, Debug)] #[serde(default)] pub struct MenuAppearance { + #[serde(deserialize_with = "opacity_deserializer")] + pub opacity: f32, pub backdrop: f32, } +impl Default for MenuAppearance { + fn default() -> Self { + Self { + opacity: default_opacity(), + backdrop: f32::default(), + } + } +} + +#[derive(Deserialize, Default, Clone, Copy, Debug, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ModuleGroup { + #[default] + Combined, // modules and members + Individual, // only members (have its own container) + None, // no containers +} + +#[derive(Deserialize, Copy, Clone, Debug)] +#[serde(default)] +pub struct ModuleAppearance { + pub spacing: SpaceSize, + pub grouping: ModuleGroup, + pub padding: SpaceSize, + pub border: Option, + pub background: Option, + pub opacity: Option, + pub text_color: Option, +} + +impl Default for ModuleAppearance { + fn default() -> Self { + Self { + spacing: SpaceSize::Xxs, + padding: SpaceSize::None, + grouping: ModuleGroup::default(), + border: None, + background: None, + opacity: None, + text_color: None, + } + } +} + #[derive(Deserialize, Clone, Debug)] #[serde(default)] pub struct Appearance { pub font_name: Option, #[serde(deserialize_with = "scale_factor_deserializer")] pub scale_factor: f64, - /// Opacity of every surface ashell draws. Applied once, to the palette, so - /// every background colour carries it and every text colour does not. - #[serde(deserialize_with = "opacity_deserializer")] - pub opacity: f32, pub bar: BarAppearance, pub menu: MenuAppearance, pub background_color: BackgroundAppearanceColor, @@ -1050,6 +1200,9 @@ pub struct Appearance { pub text_color: AppearanceColor, pub workspace_colors: Vec, pub special_workspace_colors: Option>, + + pub modules: HashMap, + pub grouped: ModuleAppearance, /// Blur the wallpaper behind ashell's translucent surfaces via /// `ext-background-effect-v1`. No-op where the protocol is unsupported. pub blur: BlurMode, @@ -1129,9 +1282,16 @@ impl Default for Appearance { Self { font_name: None, scale_factor: 1.0, - opacity: default_opacity(), bar: BarAppearance::default(), menu: MenuAppearance::default(), + grouped: ModuleAppearance::default(), + modules: HashMap::from([( + ModuleName::Settings, + ModuleAppearance { + spacing: SpaceSize::Xs, + ..Default::default() + }, + )]), background_color: BackgroundAppearanceColor::Complete { base: HexColor::rgb(26, 27, 38), weakest: None, @@ -1173,7 +1333,7 @@ pub enum Layer { Overlay, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum ModuleName { Updates, Workspaces, @@ -1206,18 +1366,18 @@ impl<'de> Deserialize<'de> for ModuleName { E: serde::de::Error, { Ok(match value { - "Updates" => ModuleName::Updates, - "Workspaces" => ModuleName::Workspaces, - "WindowTitle" => ModuleName::WindowTitle, - "SystemInfo" => ModuleName::SystemInfo, - "KeyboardLayout" => ModuleName::KeyboardLayout, - "KeyboardSubmap" => ModuleName::KeyboardSubmap, - "Tray" => ModuleName::Tray, - "Notifications" => ModuleName::Notifications, - "Tempo" => ModuleName::Tempo, - "Privacy" => ModuleName::Privacy, - "Settings" => ModuleName::Settings, - "MediaPlayer" => ModuleName::MediaPlayer, + "updates" | "Updates" => ModuleName::Updates, + "workspaces" | "Workspaces" => ModuleName::Workspaces, + "window_title" | "WindowTitle" => ModuleName::WindowTitle, + "system_info" | "SystemInfo" => ModuleName::SystemInfo, + "keyboard_layout" | "KeyboardLayout" => ModuleName::KeyboardLayout, + "keyboard_submap" | "KeyboardSubmap" => ModuleName::KeyboardSubmap, + "tray" | "Tray" => ModuleName::Tray, + "notifications" | "Notifications" => ModuleName::Notifications, + "tempo" | "Tempo" => ModuleName::Tempo, + "privacy" | "Privacy" => ModuleName::Privacy, + "settings" | "Settings" => ModuleName::Settings, + "media_player" | "MediaPlayer" => ModuleName::MediaPlayer, other => ModuleName::Custom(other.to_string()), }) } @@ -1455,6 +1615,9 @@ fn read_config(path: &Path) -> Result> { info!("Config file loaded successfully"); let mut config: Config = config; config.validate(); + + config.translate_deprecated(); + Ok(config) } Err(e) => { diff --git a/src/main.rs b/src/main.rs index 835c422cb..a3fa2fcd8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -242,8 +242,8 @@ fn main() -> iced::Result { Font::DEFAULT }; - let bar_layout = BarLayout::from_appearance(&config.appearance.bar); - let height = Outputs::get_height(bar_layout.surface, config.appearance.scale_factor); + let bar_layout = BarLayout::new(config.appearance.bar); + let height = Outputs::get_height(bar_layout.appearance.inset, config.appearance.scale_factor); let iced_layer = match config.layer { config::Layer::Top => Layer::Top, diff --git a/src/modules/custom_module.rs b/src/modules/custom_module.rs index 12c97f125..598b5b7a6 100644 --- a/src/modules/custom_module.rs +++ b/src/modules/custom_module.rs @@ -1,5 +1,8 @@ use crate::{ - components::icons::{DynamicIcon, StaticIcon, icon}, + components::{ + ModuleContent, ModuleRow, ModuleView, + icons::{DynamicIcon, StaticIcon, icon}, + }, config::CustomModuleDef, theme::use_theme, utils::launcher::execute_command, @@ -8,7 +11,7 @@ use iced::widget::canvas; use iced::{ Element, Length, Subscription, Theme, stream::channel, - widget::{Space, Stack, row, text}, + widget::{Space, Stack, text}, }; use iced::{ mouse::Cursor, @@ -119,7 +122,7 @@ impl Custom { } } - pub fn view(&'_ self) -> Element<'_, Message> { + pub fn view<'a>(&'a self) -> ModuleView<'a, Message> { let space = use_theme(|theme| theme.space); match self.config.r#type { crate::config::CustomModuleType::Text => self @@ -128,12 +131,18 @@ impl Custom { .as_ref() .and_then(|text_content| { if !text_content.is_empty() { - Some(text(text_content.clone()).into()) + Some(ModuleView::new(ModuleContent::Element( + text(text_content.clone()).into(), + ))) } else { None } }) - .unwrap_or_else(|| Space::new().width(Length::Shrink).into()), + .unwrap_or_else(|| { + ModuleView::new(ModuleContent::Element( + Space::new().width(Length::Shrink).into(), + )) + }), crate::config::CustomModuleType::Button => { let mut icon_element = self.config.icon.as_ref().map_or_else( || icon(StaticIcon::None), @@ -160,7 +169,7 @@ impl Custom { false }; - let icon_with_alert = if show_alert { + let icon_with_alert: Element<'a, Message> = if show_alert { let alert_canvas = canvas(AlertIndicator) .width(Length::Fixed(space.xs)) // Size of the dot .height(Length::Fixed(space.xs)); @@ -180,18 +189,22 @@ impl Custom { padded_icon_container.into() // No alert, just the padded icon }; - let maybe_text_element = self.data.text.as_ref().and_then(|text_content| { - if !text_content.is_empty() { - Some(text(text_content.clone())) - } else { - None - } - }); + let maybe_text_element: Option> = + self.data.text.as_ref().and_then(|text_content| { + if !text_content.is_empty() { + Some(text(text_content.clone()).into()) + } else { + None + } + }); if let Some(text_element) = maybe_text_element { - row![icon_with_alert, text_element].spacing(space.xs).into() + ModuleView::new(ModuleContent::Row( + ModuleRow::with_children(vec![icon_with_alert, text_element]) + .spacing(space.xs), + )) } else { - icon_with_alert + ModuleView::new(ModuleContent::Element(icon_with_alert)) } } } diff --git a/src/modules/keyboard_layout.rs b/src/modules/keyboard_layout.rs index d5cedbb44..94ea06e9a 100644 --- a/src/modules/keyboard_layout.rs +++ b/src/modules/keyboard_layout.rs @@ -1,11 +1,12 @@ use crate::{ + components::{ModuleContent, ModuleView}, config::KeyboardLayoutModuleConfig, services::{ ReadOnlyService, Service, ServiceEvent, compositor::{CompositorCommand, CompositorService}, }, }; -use iced::{Element, Subscription, Task, widget::text}; +use iced::{Subscription, Task, widget::text}; #[derive(Debug, Clone)] pub enum Message { @@ -56,7 +57,7 @@ impl KeyboardLayout { } } - pub fn view(&self) -> Option> { + pub fn view(&self) -> Option> { let service = self.service.as_ref()?; let active_layout = &service.keyboard_layout; @@ -73,7 +74,7 @@ impl KeyboardLayout { // Returns plain text matching original implementation style. // (Assuming parent container or mouse area handles interactions if any) - Some(text(label).into()) + Some(ModuleView::new(ModuleContent::Element(text(label).into()))) } pub fn subscription(&self) -> Subscription { diff --git a/src/modules/keyboard_submap.rs b/src/modules/keyboard_submap.rs index 0c3c82aa3..a9b8dd507 100644 --- a/src/modules/keyboard_submap.rs +++ b/src/modules/keyboard_submap.rs @@ -1,5 +1,8 @@ -use crate::services::{ReadOnlyService, ServiceEvent, compositor::CompositorService}; -use iced::{Element, Subscription, widget::text}; +use crate::{ + components::{ModuleContent, ModuleView}, + services::{ReadOnlyService, ServiceEvent, compositor::CompositorService}, +}; +use iced::{Subscription, widget::text}; #[derive(Debug, Clone)] pub enum Message { @@ -30,11 +33,11 @@ impl KeyboardSubmap { } } - pub fn view(&self) -> Option> { + pub fn view(&self) -> Option> { let submap = self.service.as_ref()?.submap.as_ref()?; if !submap.is_empty() { - Some(text(submap).into()) + Some(ModuleView::new(ModuleContent::Element(text(submap).into()))) } else { None } diff --git a/src/modules/media_player.rs b/src/modules/media_player.rs index 66165b427..6b9be6dda 100644 --- a/src/modules/media_player.rs +++ b/src/modules/media_player.rs @@ -1,7 +1,8 @@ use crate::{ - components::divider, - components::icons::{StaticIcon, icon, icon_button}, - components::{ButtonSize, MenuSize}, + components::{ + ButtonSize, MenuSize, ModuleContent, ModuleRow, ModuleView, divider, + icons::{StaticIcon, icon, icon_button}, + }, config::{ MediaPlayerFormat, MediaPlayerModuleConfig, MediaPlayerTextField, MediaPlayerVisualizer, }, @@ -242,10 +243,11 @@ impl MediaPlayer { } pub fn menu_view<'a>(&'a self, is_closing: bool) -> Element<'a, Message> { - let (space, font_size, radius, palette) = use_theme(|theme| { + let (space, font_size, bg_opacity, radius, palette) = use_theme(|theme| { ( theme.space, theme.font_size, + theme.menu.opacity, theme.radius, theme.iced_theme.palette(), ) @@ -395,7 +397,12 @@ impl MediaPlayer { container(body) .style(move |app_theme: &Theme| container::Style { background: Background::Color( - app_theme.extended_palette().background.weak.color, + app_theme + .extended_palette() + .background + .weak + .color + .scale_alpha(bg_opacity), ) .into(), border: Border::default().rounded(radius.lg), @@ -468,7 +475,7 @@ impl MediaPlayer { } } - pub fn view(&'_ self) -> Option> { + pub fn view(&'_ self) -> Option> { let (space, font_size, palette) = use_theme(|theme| (theme.space, theme.font_size, theme.iced_theme.palette())); self.active_player().map(|player| { @@ -495,7 +502,7 @@ impl MediaPlayer { let beside_visualizer = || { container( - Canvas::new(VisualizerCanvas { + Canvas::::new(VisualizerCanvas { bars: self.bars.clone(), low: palette.primary, mid: palette.warning, @@ -524,37 +531,51 @@ impl MediaPlayer { .center_y(Length::Fill) .into() }; - Stack::new() - .push(base) - .push_under( - Canvas::new(VisualizerCanvas { - bars: self.bars.clone(), - low: palette.primary, - mid: palette.warning, - high: palette.danger, - opacity: 0.1, - radius: 0.0, - min_bar_width: VISUALIZER_BAR_MIN_WIDTH, - max_bar_width: VISUALIZER_BG_BAR_MAX_WIDTH, - gap: VISUALIZER_BAR_GAP, - inset: space.xxs, - }) - .width(Length::Fill) - .height(Length::Fill), - ) - .into() + ModuleView::new(ModuleContent::Element( + Stack::new() + .push(base) + .push_under( + Canvas::new(VisualizerCanvas { + bars: self.bars.clone(), + low: palette.primary, + mid: palette.warning, + high: palette.danger, + opacity: 0.1, + radius: 0.0, + min_bar_width: VISUALIZER_BAR_MIN_WIDTH, + max_bar_width: VISUALIZER_BG_BAR_MAX_WIDTH, + gap: VISUALIZER_BAR_GAP, + inset: space.xxs, + }) + .width(Length::Fill) + .height(Length::Fill), + ) + .into(), + )) + } + Some(MediaPlayerVisualizer::Before) if active => { + ModuleView::new(ModuleContent::Row( + ModuleRow::with_children(vec![ + Element::from(beside_visualizer()), + Element::from(content), + ]) + .align_y(Vertical::Center) + .spacing(space.xs) + .height(Length::Fill), + )) + } + Some(MediaPlayerVisualizer::After) if active => { + ModuleView::new(ModuleContent::Row( + ModuleRow::with_children(vec![ + Element::from(content), + Element::from(beside_visualizer()), + ]) + .align_y(Vertical::Center) + .spacing(space.xs) + .height(Length::Fill), + )) } - Some(MediaPlayerVisualizer::Before) if active => row![beside_visualizer(), content] - .align_y(Vertical::Center) - .spacing(space.xs) - .height(Length::Fill) - .into(), - Some(MediaPlayerVisualizer::After) if active => row![content, beside_visualizer()] - .align_y(Vertical::Center) - .spacing(space.xs) - .height(Length::Fill) - .into(), - _ => content.into(), + _ => ModuleView::new(ModuleContent::Element(content.into())), } }) } diff --git a/src/modules/mod.rs b/src/modules/mod.rs index a8a6adb3e..1eb18cc4b 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -1,9 +1,9 @@ use crate::{ app::{App, Message}, - components::animated_size, - components::menu::MenuType, - components::{module_group, module_item}, - config::{ModuleDef, ModuleName}, + components::{ + ModuleItem, ModuleResult, animated_size, menu::MenuType, module_group, module_item, + }, + config::{ModuleAppearance, ModuleDef, ModuleGroup, ModuleName}, theme::use_theme, }; use iced::{Alignment, Element, Length, Subscription, SurfaceId, widget::Row}; @@ -83,74 +83,86 @@ impl App { .collect() } + fn apply_module_action<'a>( + &self, + mut item: ModuleItem<'a, Message>, + action: Option, + id: SurfaceId, + ) -> ModuleItem<'a, Message> { + if let Some(action) = action { + match action { + OnModulePress::Action(msg) => { + item = item.on_press(*msg); + } + OnModulePress::ToggleMenu(menu_type) => { + item = item.on_press_with_position(move |button_ui_ref| { + Message::ToggleMenu(menu_type.clone(), id, button_ui_ref) + }); + } + OnModulePress::ToggleMenuWithExtra { + menu_type, + on_right_press, + on_scroll_up, + on_scroll_down, + } => { + item = item.on_press_with_position(move |button_ui_ref| { + Message::ToggleMenu(menu_type.clone(), id, button_ui_ref) + }); + if let Some(msg) = on_right_press { + item = item.on_right_press(*msg); + } + if let Some(msg) = on_scroll_up { + item = item.on_scroll_up(*msg); + } + if let Some(msg) = on_scroll_down { + item = item.on_scroll_down(*msg); + } + } + OnModulePress::CustomAction { + on_press, + on_right_press, + on_middle_press, + on_scroll_up, + on_scroll_down, + } => { + item = item.on_press(*on_press); + if let Some(msg) = on_right_press { + item = item.on_right_press(*msg); + } + if let Some(msg) = on_middle_press { + item = item.on_middle_press(*msg); + } + if let Some(msg) = on_scroll_up { + item = item.on_scroll_up(*msg); + } + if let Some(msg) = on_scroll_down { + item = item.on_scroll_down(*msg); + } + } + } + }; + + item + } + fn build_module_item<'a>( &'a self, id: SurfaceId, + module_appearance: Option, content: Element<'a, Message>, action: Option, ) -> Element<'a, Message> { - let content = if use_theme(|t| t.animations_enabled) { + let animated = use_theme(|t| t.animations_enabled); + + let content = if animated { animated_size(content).into() } else { content }; - match action { - Some(action) => { - let mut item = module_item(content); - match action { - OnModulePress::Action(msg) => { - item = item.on_press(*msg); - } - OnModulePress::ToggleMenu(menu_type) => { - item = item.on_press_with_position(move |button_ui_ref| { - Message::ToggleMenu(menu_type.clone(), id, button_ui_ref) - }); - } - OnModulePress::ToggleMenuWithExtra { - menu_type, - on_right_press, - on_scroll_up, - on_scroll_down, - } => { - item = item.on_press_with_position(move |button_ui_ref| { - Message::ToggleMenu(menu_type.clone(), id, button_ui_ref) - }); - if let Some(msg) = on_right_press { - item = item.on_right_press(*msg); - } - if let Some(msg) = on_scroll_up { - item = item.on_scroll_up(*msg); - } - if let Some(msg) = on_scroll_down { - item = item.on_scroll_down(*msg); - } - } - OnModulePress::CustomAction { - on_press, - on_right_press, - on_middle_press, - on_scroll_up, - on_scroll_down, - } => { - item = item.on_press(*on_press); - if let Some(msg) = on_right_press { - item = item.on_right_press(*msg); - } - if let Some(msg) = on_middle_press { - item = item.on_middle_press(*msg); - } - if let Some(msg) = on_scroll_up { - item = item.on_scroll_up(*msg); - } - if let Some(msg) = on_scroll_down { - item = item.on_scroll_down(*msg); - } - } - } - item.into() - } - None => module_item(content).into(), - } + + let item = module_item(content, module_appearance); + + self.apply_module_action(item, action, id).into() } fn single_module_wrapper<'a>( @@ -158,8 +170,48 @@ impl App { id: SurfaceId, module_name: &'a ModuleName, ) -> Option> { + let module_appearance = use_theme(|t| t.module_appearance()(module_name)); + let grouping = module_appearance.grouping; + self.get_module_view(id, module_name) - .map(|(content, action)| module_group(self.build_module_item(id, content, action))) + .map(move |module_result| { + let ModuleResult { + action, + view: content, + } = module_result; + + match grouping { + ModuleGroup::Individual => { + let content = content.map_elements(|child| { + // maybe we could further edit individuals + self.build_module_item( + id, + Some(module_appearance), + child, + action.clone(), + ) + }); + + content.into_element() + } + ModuleGroup::Combined => { + let item_appearance = ModuleAppearance { + opacity: Some(0.), + ..module_appearance + }; + let item = self.build_module_item( + id, + Some(item_appearance), + content.into_element(), + action, + ); + module_group(item, module_appearance) + } + ModuleGroup::None => { + self.build_module_item(id, None, content.into_element(), action) + } + } + }) } fn group_module_wrapper<'a>( @@ -167,29 +219,67 @@ impl App { id: SurfaceId, group: &'a [ModuleName], ) -> Option> { - let modules: Vec<_> = group + let (theme_space, module_appearance, group_appearance) = + use_theme(|t| (t.space, t.module_appearance(), t.grouped)); + + let module_items: Vec<_> = group .iter() - .filter_map(|module| self.get_module_view(id, module)) + .filter_map(|module| self.get_module_view(id, module).map(|view| (module, view))) .collect(); - if modules.is_empty() { - None - } else { - let items = Row::with_children( - modules - .into_iter() - .map(|(content, action)| self.build_module_item(id, content, action)) - .collect::>(), - ); - Some(module_group(items.into())) + if module_items.is_empty() { + return None; } + + let items = module_items + .into_iter() + .map(|(module_name, module_result)| { + let ModuleResult { + action, + view: content, + } = module_result; + + let appearance = module_appearance(module_name); + + let grouping = appearance.grouping; + + match grouping { + ModuleGroup::Individual => { + let content = content.map_elements(|child| { + self.build_module_item(id, Some(appearance), child, action.clone()) + }); + + content.into_element() + } + ModuleGroup::Combined => { + let item = self.build_module_item( + id, + Some(appearance), + content.into_element(), + action, + ); + // module_group(item, module_appearance) + // we should allow more customisation but for now leave it + item + } + ModuleGroup::None => { + self.build_module_item(id, Some(appearance), content.into_element(), action) + } + } + }) + .collect::>(); + + let spacing = theme_space.resolve(group_appearance.spacing); + let row = Row::with_children(items).spacing(spacing); + + Some(module_group(row.into(), group_appearance)) } fn get_module_view<'a>( &'a self, id: SurfaceId, module_name: &'a ModuleName, - ) -> Option<(Element<'a, Message>, Option)> { + ) -> Option> { match module_name { ModuleName::Custom(name) => self.custom.get(name).map(|custom| { let action = match custom.module_type() { @@ -248,52 +338,48 @@ impl App { }) } }; - ( - custom.view().map(|msg| Message::Custom(name.clone(), msg)), + + ModuleResult { + view: custom.view().map(|msg| Message::Custom(name.clone(), msg)), action, - ) + } }), - ModuleName::Updates => self.updates.as_ref().map(|updates| { - ( - updates.view().map(Message::Updates), - Some(OnModulePress::ToggleMenu(MenuType::Updates)), - ) + ModuleName::Updates => self.updates.as_ref().map(|updates| ModuleResult { + view: updates.view().map(Message::Updates), + action: Some(OnModulePress::ToggleMenu(MenuType::Updates)), }), - ModuleName::Workspaces => Some(( - self.workspaces + ModuleName::Workspaces => Some(ModuleResult { + view: self + .workspaces .view(id, &self.outputs) .map(Message::Workspaces), - None, - )), - ModuleName::WindowTitle => self.window_title.get_value().map(|title| { - ( - self.window_title.view(title).map(Message::WindowTitle), - None, - ) + action: None, }), - ModuleName::SystemInfo => Some(( - self.system_info.view().map(Message::SystemInfo), - Some(OnModulePress::ToggleMenu(MenuType::SystemInfo)), - )), - ModuleName::KeyboardLayout => self.keyboard_layout.view().map(|view| { - ( - view.map(Message::KeyboardLayout), - Some(OnModulePress::Action(Box::new(Message::KeyboardLayout( - keyboard_layout::Message::ChangeLayout, - )))), - ) + ModuleName::WindowTitle => self.window_title.get_value().map(|title| ModuleResult { + view: self.window_title.view(title).map(Message::WindowTitle), + action: None, }), - ModuleName::KeyboardSubmap => self - .keyboard_submap - .view() - .map(|view| (view.map(Message::KeyboardSubmap), None)), - ModuleName::Tray => self - .tray - .view(id) - .map(|view| (view.map(Message::Tray), None)), - ModuleName::Tempo => Some(( - self.tempo.view().map(Message::Tempo), - Some(OnModulePress::ToggleMenuWithExtra { + ModuleName::SystemInfo => Some(ModuleResult { + view: self.system_info.view().map(Message::SystemInfo), + action: Some(OnModulePress::ToggleMenu(MenuType::SystemInfo)), + }), + ModuleName::KeyboardLayout => self.keyboard_layout.view().map(|view| ModuleResult { + view: view.map(Message::KeyboardLayout), + action: Some(OnModulePress::Action(Box::new(Message::KeyboardLayout( + keyboard_layout::Message::ChangeLayout, + )))), + }), + ModuleName::KeyboardSubmap => self.keyboard_submap.view().map(|view| ModuleResult { + view: view.map(Message::KeyboardSubmap), + action: None, + }), + ModuleName::Tray => self.tray.view(id).map(|view| ModuleResult { + view: view.map(Message::Tray), + action: None, + }), + ModuleName::Tempo => Some(ModuleResult { + view: self.tempo.view().map(Message::Tempo), + action: Some(OnModulePress::ToggleMenuWithExtra { menu_type: MenuType::Tempo, on_right_press: Some(Box::new(Message::Tempo(tempo::Message::CycleFormat))), on_scroll_up: Some(Box::new(Message::Tempo(tempo::Message::CycleTimezone( @@ -303,25 +389,23 @@ impl App { tempo::TimezoneDirection::Backward, )))), }), - )), - ModuleName::Privacy => self - .privacy - .view() - .map(|view| (view.map(Message::Privacy), None)), - ModuleName::MediaPlayer => self.media_player.view().map(|view| { - ( - view.map(Message::MediaPlayer), - Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), - ) }), - ModuleName::Settings => Some(( - self.settings.view(id).map(Message::Settings), - Some(OnModulePress::ToggleMenu(MenuType::Settings)), - )), - ModuleName::Notifications => Some(( - self.notifications.view().map(Message::Notifications), - Some(OnModulePress::ToggleMenu(MenuType::Notifications)), - )), + ModuleName::Privacy => self.privacy.view().map(|view| ModuleResult { + view: view.map(Message::Privacy), + action: None, + }), + ModuleName::MediaPlayer => self.media_player.view().map(|view| ModuleResult { + view: view.map(Message::MediaPlayer), + action: Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), + }), + ModuleName::Settings => Some(ModuleResult { + view: self.settings.view(id).map(Message::Settings), + action: Some(OnModulePress::ToggleMenu(MenuType::Settings)), + }), + ModuleName::Notifications => Some(ModuleResult { + view: self.notifications.view().map(Message::Notifications), + action: Some(OnModulePress::ToggleMenu(MenuType::Notifications)), + }), } } diff --git a/src/modules/notifications.rs b/src/modules/notifications.rs index 6a7d18b7d..16d78513c 100644 --- a/src/modules/notifications.rs +++ b/src/modules/notifications.rs @@ -1,8 +1,10 @@ use crate::{ - components::collapsible::{self, collapsible}, - components::icons::{StaticIcon, icon, icon_button}, - components::slide::{self, SlideDirection, slide}, - components::{ButtonHierarchy, ButtonKind, ButtonSize, MenuSize}, + components::{ + ButtonHierarchy, ButtonKind, ButtonSize, MenuSize, ModuleContent, ModuleView, + collapsible::{self, collapsible}, + icons::{StaticIcon, icon, icon_button}, + slide::{self, SlideDirection, slide}, + }, config::{NotificationsModuleConfig, ToastPosition}, services::{ ReadOnlyService, ServiceEvent, @@ -496,11 +498,13 @@ impl Notifications { border, ..iced::widget::button::Style::default() }; + let bg = if style == NotificationStyle::Toast { iced_theme.extended_palette().background.base.color } else { iced_theme.extended_palette().background.weak.color }; + button_style.background = Some( if matches!(status, iced::widget::button::Status::Hovered) { crate::theme::hovered(iced_theme, bg) @@ -626,12 +630,14 @@ impl Notifications { .into() } - pub fn view(&'_ self) -> Element<'_, Message> { - if !self.notifications.is_empty() { + pub fn view(&'_ self) -> ModuleView<'_, Message> { + let element = if !self.notifications.is_empty() { icon(StaticIcon::BellBadge).into() } else { icon(StaticIcon::Bell).into() - } + }; + + ModuleView::new(ModuleContent::Element(element)) } pub fn menu_view<'a>(&'a self) -> Element<'a, Message> { diff --git a/src/modules/privacy.rs b/src/modules/privacy.rs index bcc3e6341..0cbb85d37 100644 --- a/src/modules/privacy.rs +++ b/src/modules/privacy.rs @@ -1,10 +1,13 @@ use crate::{ - components::icons::{StaticIcon, icon}, + components::{ + ModuleContent, ModuleView, + icons::{StaticIcon, icon}, + }, services::{ReadOnlyService, ServiceEvent, privacy::PrivacyService}, theme::use_theme, }; use iced::{ - Alignment, Element, Subscription, + Alignment, Subscription, widget::{Row, container}, }; @@ -35,12 +38,12 @@ impl Privacy { } } - pub fn view(&'_ self) -> Option> { + pub fn view(&'_ self) -> Option> { let space = use_theme(|theme| theme.space); if let Some(service) = self.service.as_ref() && !service.no_access() { - Some( + Some(ModuleView::new(ModuleContent::Element( container( Row::with_capacity(3) .push( @@ -58,7 +61,7 @@ impl Privacy { ..Default::default() }) .into(), - ) + ))) } else { None } diff --git a/src/modules/settings/mod.rs b/src/modules/settings/mod.rs index fdaf703a2..61f856626 100644 --- a/src/modules/settings/mod.rs +++ b/src/modules/settings/mod.rs @@ -8,12 +8,12 @@ use tokio::time::timeout; use crate::{ components::{ - ButtonUIRef, MenuSize, collapsible, + ButtonUIRef, MenuSize, ModuleContent, ModuleRow, ModuleView, collapsible, icons::{DynamicIcon, Icon, StaticIcon, icon, icon_button}, menu::MenuType, password_dialog, position_button, quick_setting_button, sub_menu_wrapper, }, - config::{Position, SettingsCustomButton, SettingsIndicator, SettingsModuleConfig}, + config::{ModuleName, Position, SettingsCustomButton, SettingsIndicator, SettingsModuleConfig}, modules::settings::{ audio::{AudioSettings, AudioSettingsConfig}, bluetooth::{BluetoothSettings, BluetoothSettingsConfig}, @@ -760,9 +760,12 @@ impl Settings { .into() } - pub fn view<'a>(&'a self, id: SurfaceId) -> Element<'a, Message> { - let space = use_theme(|t| t.space); - let mut row = Row::with_capacity(self.indicators.len()); + pub fn view<'a>(&'a self, id: SurfaceId) -> ModuleView<'a, Message> { + let (theme_space, appearance) = + use_theme(|t| (t.space, t.module_appearance()(&ModuleName::Settings))); + let space = theme_space.resolve(appearance.spacing); + + let mut row: Vec> = Vec::with_capacity(self.indicators.len()); for indicator in &self.indicators { let element: Option> = match indicator { @@ -809,7 +812,7 @@ impl Settings { for (index, element) in peripherals.into_iter().enumerate() { let element = element.map(Message::Power); if self.enable_tooltips { - row = row.push( + row.push( position_button(element) .width(Length::Shrink) .height(Length::Shrink) @@ -821,10 +824,11 @@ impl Settings { .on_unhover(Message::TooltipUnhover( id, MenuType::PeripheralBatteryTooltip(index), - )), + )) + .into(), ); } else { - row = row.push(element); + row.push(element); } } None @@ -860,22 +864,25 @@ impl Settings { }; if let Some((hover_msg, menu_type)) = tooltip_config { - row = row.push( + row.push( position_button(element) .width(Length::Shrink) .height(Length::Shrink) .padding(0) .style(transparent_button_style) .on_hover_with_position(move |ui_ref| hover_msg(ui_ref, id)) - .on_unhover(Message::TooltipUnhover(id, menu_type)), + .on_unhover(Message::TooltipUnhover(id, menu_type)) + .into(), ); } else { - row = row.push(element); + row.push(element); } } } - row.spacing(space.xs).into() + ModuleView::new(ModuleContent::Row( + ModuleRow::with_children(row).spacing(space), + )) } pub fn subscription(&self) -> Subscription { diff --git a/src/modules/system_info.rs b/src/modules/system_info.rs index 24c1a0d5e..e5b581d94 100644 --- a/src/modules/system_info.rs +++ b/src/modules/system_info.rs @@ -1,10 +1,11 @@ use crate::{ - components::MenuSize, - components::divider, - components::icons::{StaticIcon, icon}, + components::{ + MenuSize, ModuleContent, ModuleRow, ModuleView, divider, + icons::{StaticIcon, icon}, + }, config::{ - CpuFormat, DiskFormat, MemoryFormat, SystemInfoIndicator, SystemInfoModuleConfig, - SystemInfoTemperature, TemperatureSensor, TemperatureSensorType, + CpuFormat, DiskFormat, MemoryFormat, ModuleName, SystemInfoIndicator, + SystemInfoModuleConfig, SystemInfoTemperature, TemperatureSensor, TemperatureSensorType, }, i18n::{UnitSystem, unit_system}, t, @@ -14,7 +15,7 @@ use crate::{ use iced::{ Alignment, Element, Length, Subscription, Theme, time::every, - widget::{Column, Row, column, container, row, text}, + widget::{Column, column, container, row, text}, }; use iced_anim::{AnimationBuilder, transition::Easing}; use itertools::Itertools; @@ -636,8 +637,13 @@ impl SystemInfo { .into() } - pub fn view(&'_ self) -> Element<'_, Message> { - let space = use_theme(|t| t.space); + pub fn view<'a>(&'a self) -> ModuleView<'a, Message> { + let (theme_space, appearance) = + use_theme(|t| (t.space, t.module_appearance()(&ModuleName::SystemInfo))); + let space_sizing = appearance.spacing; + + let space = theme_space.resolve(space_sizing); + let indicators = self.config.indicators.iter().filter_map(|i| match i { SystemInfoIndicator::Cpu => Some(Self::indicator_info_element( StaticIcon::Cpu, @@ -773,10 +779,11 @@ impl SystemInfo { }), }); - Row::with_children(indicators) - .align_y(Alignment::Center) - .spacing(space.xxs) - .into() + ModuleView::new(ModuleContent::Row( + ModuleRow::with_children(indicators) + .spacing(space) + .align_y(Alignment::Center), + )) } pub fn subscription(&self) -> Subscription { diff --git a/src/modules/tempo/mod.rs b/src/modules/tempo/mod.rs index d5703a9b5..0c3791baa 100644 --- a/src/modules/tempo/mod.rs +++ b/src/modules/tempo/mod.rs @@ -15,8 +15,8 @@ use log::{debug, warn}; use self::weather::{Location, WeatherData, fetch_location, fetch_weather_data}; use crate::{ - components::MenuSize, - config::{TempoModuleConfig, WeatherIndicator}, + components::{MenuSize, ModuleRow, ModuleView}, + config::{ModuleName, TempoModuleConfig, WeatherIndicator}, i18n::{language_subtag, unit_system}, theme::use_theme, }; @@ -164,15 +164,18 @@ impl Tempo { } } - pub fn view(&'_ self) -> Element<'_, Message> { - let space = use_theme(|t| t.space); + pub fn view(&'_ self) -> ModuleView<'_, Message> { + let (theme_space, appearance) = + use_theme(|t| (t.space, t.module_appearance()(&ModuleName::Tempo))); let display_text = self.time_str(self.current_format(), self.current_timezone_index, None); - Row::with_capacity(2) + let spacing = theme_space.resolve(appearance.spacing); + + ModuleRow::with_capacity(2) .push(self.weather_indicator()) .push(text(display_text)) .align_y(Vertical::Center) - .spacing(space.sm) + .spacing(spacing) .into() } diff --git a/src/modules/tempo/weather.rs b/src/modules/tempo/weather.rs index 33026b003..d3b48b357 100644 --- a/src/modules/tempo/weather.rs +++ b/src/modules/tempo/weather.rs @@ -21,7 +21,8 @@ use super::{Message, Tempo}; impl Tempo { pub(super) fn weather<'a>(&'a self) -> Option> { - let (space, font_size, radius) = use_theme(|t| (t.space, t.font_size, t.radius)); + let (space, font_size, bg_opacity, radius) = + use_theme(|t| (t.space, t.font_size, t.menu.opacity, t.radius)); let locale = chrono_locale(); let units = unit_system(); let temp = units.temperature_symbol(); @@ -146,7 +147,12 @@ impl Tempo { .padding(space.md) .style(move |app_theme: &Theme| container::Style { background: Background::Color( - app_theme.extended_palette().background.weak.color, + app_theme + .extended_palette() + .background + .weak + .color + .scale_alpha(bg_opacity), ) .into(), border: Border::default().rounded(radius.lg), @@ -204,7 +210,12 @@ impl Tempo { .padding(space.sm) .style(move |app_theme: &Theme| container::Style { background: Background::Color( - app_theme.extended_palette().background.weak.color, + app_theme + .extended_palette() + .background + .weak + .color + .scale_alpha(bg_opacity), ) .into(), border: Border::default().rounded(radius.lg), @@ -267,7 +278,12 @@ impl Tempo { .padding(space.sm) .style(move |app_theme: &Theme| container::Style { background: Background::Color( - app_theme.extended_palette().background.weak.color, + app_theme + .extended_palette() + .background + .weak + .color + .scale_alpha(bg_opacity), ) .into(), border: Border::default().rounded(iced::border::Radius { diff --git a/src/modules/tray.rs b/src/modules/tray.rs index cfde81816..43e80b230 100644 --- a/src/modules/tray.rs +++ b/src/modules/tray.rs @@ -1,9 +1,9 @@ use crate::{ - components::divider, - components::icons::{StaticIcon, icon}, components::{ - ButtonHierarchy, ButtonKind, ButtonUIRef, IconPosition, MenuSize, position_button, - styled_button, + ButtonHierarchy, ButtonKind, ButtonUIRef, IconPosition, MenuSize, ModuleContent, + ModuleView, divider, + icons::{StaticIcon, icon}, + position_button, styled_button, }, config::{TrayClickAction, TrayModuleConfig}, services::{ @@ -257,7 +257,7 @@ impl TrayModule { } } - pub fn view<'a>(&'a self, id: SurfaceId) -> Option> { + pub fn view<'a>(&'a self, id: SurfaceId) -> Option> { let (space, font_size, button_style) = use_theme(|theme| { ( theme.space, @@ -271,7 +271,7 @@ impl TrayModule { .as_ref() .filter(|s| s.data.iter().any(|item| !self.is_blocklisted(&item.name))) .map(|service| { - Into::>::into( + let element = Into::>::into( Row::with_children( service .data @@ -312,7 +312,9 @@ impl TrayModule { ) .padding([2.0, 0.]) .align_y(Alignment::Center), - ) + ); + + ModuleView::new(ModuleContent::Element(element)) }) } diff --git a/src/modules/updates.rs b/src/modules/updates.rs index 19f411f83..1e9619927 100644 --- a/src/modules/updates.rs +++ b/src/modules/updates.rs @@ -1,8 +1,10 @@ use crate::{ - components::divider, - components::icons::{StaticIcon, icon}, - components::spinning_icon::spinning_icon, - components::{IconPosition, MenuSize, styled_button}, + components::{ + IconPosition, MenuSize, ModuleContent, ModuleView, divider, + icons::{StaticIcon, icon}, + spinning_icon::spinning_icon, + styled_button, + }, config::UpdatesModuleConfig, t, theme::use_theme, @@ -169,7 +171,7 @@ impl Updates { } } - pub fn view(&'_ self) -> Element<'_, Message> { + pub fn view(&'_ self) -> ModuleView<'_, Message> { let (space, font_size, animated) = use_theme(|theme| (theme.space, theme.font_size, theme.animations_enabled)); let is_checking = matches!(self.state, State::Checking); @@ -192,7 +194,7 @@ impl Updates { content = content.push(text(self.updates.len())); } - content.into() + ModuleView::new(ModuleContent::Element(content.into())) } pub fn menu_view<'a>(&'a self, id: SurfaceId) -> Element<'a, Message> { diff --git a/src/modules/window_title.rs b/src/modules/window_title.rs index 147bd529a..ce5218db5 100644 --- a/src/modules/window_title.rs +++ b/src/modules/window_title.rs @@ -1,11 +1,12 @@ use crate::{ + components::{ModuleContent, ModuleView}, config::{WindowTitleConfig, WindowTitleMode}, services::{ReadOnlyService, ServiceEvent, compositor::CompositorService}, theme::use_theme, utils::truncate_text, }; use iced::{ - Element, Subscription, + Subscription, widget::{container, text}, }; @@ -90,8 +91,8 @@ impl WindowTitle { self.value.clone() } - pub fn view(&'_ self, title: String) -> Element<'_, Message> { - use_theme(|theme| { + pub fn view(&'_ self, title: String) -> ModuleView<'_, Message> { + let element = use_theme(|theme| { container( text(title) .size(theme.font_size.sm) @@ -99,7 +100,9 @@ impl WindowTitle { ) .clip(true) .into() - }) + }); + + ModuleView::new(ModuleContent::Element(element)) } pub fn subscription(&self) -> Subscription { diff --git a/src/modules/workspaces.rs b/src/modules/workspaces.rs index 75ce4ce46..732357959 100644 --- a/src/modules/workspaces.rs +++ b/src/modules/workspaces.rs @@ -1,8 +1,8 @@ use crate::{ - components::icons::icon, + components::{ModuleContent, ModuleView, icons::icon}, config::{ - AppearanceColor, InvertScrollDirection, WorkspaceIndicatorFormat, WorkspaceVisibilityMode, - WorkspacesModuleConfig, + AppearanceColor, InvertScrollDirection, ModuleName, WorkspaceIndicatorFormat, + WorkspaceVisibilityMode, WorkspacesModuleConfig, }, outputs::Outputs, services::{ @@ -509,10 +509,14 @@ impl Workspaces { } } - pub fn view<'a>(&'a self, id: SurfaceId, outputs: &Outputs) -> Element<'a, Message> { + pub fn view<'a>(&'a self, id: SurfaceId, outputs: &Outputs) -> ModuleView<'a, Message> { let monitor_name = outputs.get_monitor_name(id); let row = use_theme(|theme| { + let appearance = theme.module_appearance()(&ModuleName::Workspaces); + let (theme_space, space_sizing) = (theme.space, appearance.spacing); + let space = theme_space.resolve(space_sizing); + Row::with_children( self.ui_workspaces .iter() @@ -685,60 +689,60 @@ impl Workspaces { }) .collect::>(), ) - .spacing(theme.space.xxs) + .spacing(space) }); let scroll_monitor = monitor_name.map(str::to_owned); - MouseArea::new(row) - .on_scroll(move |direction| { - let scroll = |dir: i32| Message::Scroll(dir, scroll_monitor.clone()); - match direction { - iced::mouse::ScrollDelta::Lines { y, .. } => { - if y.is_sign_positive() { - match self.config.invert_scroll_direction { - Some(InvertScrollDirection::All | InvertScrollDirection::Mouse) => { - scroll(-1) - } - Some(InvertScrollDirection::Trackpad) => scroll(1), - None => scroll(1), + let element = MouseArea::new(row).on_scroll(move |direction| { + let scroll = |dir: i32| Message::Scroll(dir, scroll_monitor.clone()); + match direction { + iced::mouse::ScrollDelta::Lines { y, .. } => { + if y.is_sign_positive() { + match self.config.invert_scroll_direction { + Some(InvertScrollDirection::All | InvertScrollDirection::Mouse) => { + scroll(-1) } - } else { - match self.config.invert_scroll_direction { - Some(InvertScrollDirection::All | InvertScrollDirection::Mouse) => { - scroll(1) - } - Some(InvertScrollDirection::Trackpad) => scroll(-1), - None => scroll(-1), + Some(InvertScrollDirection::Trackpad) => scroll(1), + None => scroll(1), + } + } else { + match self.config.invert_scroll_direction { + Some(InvertScrollDirection::All | InvertScrollDirection::Mouse) => { + scroll(1) } + Some(InvertScrollDirection::Trackpad) => scroll(-1), + None => scroll(-1), } } - iced::mouse::ScrollDelta::Pixels { y, .. } => { - let sensibility = 3.; - - if self.scroll_accumulator.abs() < sensibility { - Message::ScrollAccumulator(y) - } else if self.scroll_accumulator.is_sign_positive() { - match self.config.invert_scroll_direction { - Some( - InvertScrollDirection::All | InvertScrollDirection::Trackpad, - ) => scroll(-1), - Some(InvertScrollDirection::Mouse) => scroll(1), - None => scroll(1), + } + iced::mouse::ScrollDelta::Pixels { y, .. } => { + let sensibility = 3.; + + if self.scroll_accumulator.abs() < sensibility { + Message::ScrollAccumulator(y) + } else if self.scroll_accumulator.is_sign_positive() { + match self.config.invert_scroll_direction { + Some(InvertScrollDirection::All | InvertScrollDirection::Trackpad) => { + scroll(-1) } - } else { - match self.config.invert_scroll_direction { - Some( - InvertScrollDirection::All | InvertScrollDirection::Trackpad, - ) => scroll(1), - Some(InvertScrollDirection::Mouse) => scroll(-1), - None => scroll(-1), + Some(InvertScrollDirection::Mouse) => scroll(1), + None => scroll(1), + } + } else { + match self.config.invert_scroll_direction { + Some(InvertScrollDirection::All | InvertScrollDirection::Trackpad) => { + scroll(1) } + Some(InvertScrollDirection::Mouse) => scroll(-1), + None => scroll(-1), } } } - }) - .into() + } + }); + + ModuleView::new(ModuleContent::Element(element.into())) } pub fn subscription(&self) -> Subscription { diff --git a/src/outputs.rs b/src/outputs.rs index 9cdc6dcf9..7a29512bb 100644 --- a/src/outputs.rs +++ b/src/outputs.rs @@ -9,7 +9,7 @@ use crate::{ HEIGHT, components::ButtonUIRef, components::menu::{Menu, MenuType, OpenMenu}, - config::{self, BarSurface, Position}, + config::{self, Position}, theme::BarLayout, }; @@ -141,13 +141,8 @@ impl Outputs { Menu::with_animations(self.animations_enabled) } - pub fn get_height(surface: BarSurface, scale_factor: f64) -> f64 { - (HEIGHT - - match surface { - BarSurface::Solid => 8., - BarSurface::Transparent => 0., - }) - * scale_factor + pub fn get_height(inset: f32, scale_factor: f64) -> f64 { + (HEIGHT - inset as f64) * scale_factor } /// Layer-shell outer margin scaled to physical pixels, ordered @@ -161,7 +156,7 @@ impl Outputs { /// Space reserved on the anchored edge: the bar height plus the margin that /// pushes the bar away from that edge. pub fn exclusive_zone(layout: BarLayout, position: Position, scale_factor: f64) -> i32 { - let height = Self::get_height(layout.surface, scale_factor); + let height = Self::get_height(layout.appearance.inset, scale_factor); let (top, _, bottom, _) = Self::margin(layout, scale_factor); height as i32 + match position { @@ -177,7 +172,7 @@ impl Outputs { layer: config::Layer, scale_factor: f64, ) -> (SurfaceId, Task) { - let height = Self::get_height(layout.surface, scale_factor); + let height = Self::get_height(layout.appearance.inset, scale_factor); let iced_layer = match layer { config::Layer::Top => Layer::Top, @@ -552,7 +547,7 @@ impl Outputs { ); shell_info.layout = layout; shell_info.scale_factor = scale_factor; - let height = Self::get_height(layout.surface, scale_factor); + let height = Self::get_height(layout.appearance.inset, scale_factor); tasks.push(Task::batch(vec![ set_size(shell_info.id, (0, height as u32)), set_exclusive_zone( @@ -857,7 +852,8 @@ impl Outputs { if *oid == Some(target) { info.as_ref().and_then(|i| { i.output_logical_height.map(|h| { - let bar = Self::get_height(i.layout.surface, i.scale_factor) as u32; + let bar = + Self::get_height(i.layout.appearance.inset, i.scale_factor) as u32; h.saturating_sub(bar) }) }) diff --git a/src/theme.rs b/src/theme.rs index 09edc1a2f..7164bce1b 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,14 +1,16 @@ -use std::cell::RefCell; +use std::sync::Arc; +use std::{cell::RefCell, collections::HashMap}; +use crate::config::BorderAppearance; use crate::{ components::button::{ButtonHierarchy, ButtonKind}, config::{ - Appearance, AppearanceColor, BackgroundLevel, BarAppearance, BarMargin, BarRadius, - BarSurface, MenuAppearance, Position, RadiusSize, SpaceSize, + Appearance, AppearanceColor, BackgroundLevel, BarAppearance, MenuAppearance, + ModuleAppearance, ModuleName, Position, RadiusSize, SpaceSize, }, }; use iced::{ - Background, Border, Color, Theme, border, + Background, Border, Color, Theme, theme::{Palette, palette}, widget::{ button::{self, Status}, @@ -106,19 +108,17 @@ impl Radius { /// ordered `(top, right, bottom, left)`. #[derive(Debug, Clone, Copy, PartialEq)] pub struct BarLayout { - pub surface: BarSurface, pub margin: (f32, f32, f32, f32), + pub appearance: BarAppearance, } impl BarLayout { - pub fn from_appearance(bar: &BarAppearance) -> Self { - Self::new(bar.surface, bar.margin) - } - - fn new(surface: BarSurface, margin: BarMargin) -> Self { + pub fn new(appearance: BarAppearance) -> Self { + let margin = appearance.margin; let space = Space::default(); + Self { - surface, + appearance, margin: ( space.resolve(margin.top), space.resolve(margin.right), @@ -159,12 +159,15 @@ impl Default for FontSize { pub struct AshellTheme { pub iced_theme: Theme, pub space: Space, + + pub modules: Arc>, + pub grouped: ModuleAppearance, + + pub bar: BarAppearance, + pub radius: Radius, pub font_size: FontSize, pub bar_position: Position, - pub bar_surface: BarSurface, - pub bar_radius: BarRadius, - pub bar_margin: BarMargin, pub menu: MenuAppearance, pub workspace_colors: Vec, pub special_workspace_colors: Option>, @@ -207,63 +210,12 @@ pub fn hovered(theme: &Theme, color: Color) -> Color { over(theme.palette().text.scale_alpha(HOVER_OVERLAY), color) } -/// Apply the opacity to every background colour, leaving foregrounds opaque. -/// -/// In a [`palette::Pair`], `color` is painted behind content and `text` on top -/// of it, so the split needs no list of special cases and no call site has to -/// ask for a translucent colour. -fn with_opacity(extended: palette::Extended, opacity: f32) -> palette::Extended { - let pair = |p: palette::Pair| palette::Pair { - color: p.color.scale_alpha(opacity), - text: p.text, - }; - - palette::Extended { - background: palette::Background { - base: pair(extended.background.base), - weakest: pair(extended.background.weakest), - weaker: pair(extended.background.weaker), - weak: pair(extended.background.weak), - neutral: pair(extended.background.neutral), - strong: pair(extended.background.strong), - stronger: pair(extended.background.stronger), - strongest: pair(extended.background.strongest), - }, - primary: palette::Primary { - base: pair(extended.primary.base), - weak: pair(extended.primary.weak), - strong: pair(extended.primary.strong), - }, - secondary: palette::Secondary { - base: pair(extended.secondary.base), - weak: pair(extended.secondary.weak), - strong: pair(extended.secondary.strong), - }, - success: palette::Success { - base: pair(extended.success.base), - weak: pair(extended.success.weak), - strong: pair(extended.success.strong), - }, - warning: palette::Warning { - base: pair(extended.warning.base), - weak: pair(extended.warning.weak), - strong: pair(extended.warning.strong), - }, - danger: palette::Danger { - base: pair(extended.danger.base), - weak: pair(extended.danger.weak), - strong: pair(extended.danger.strong), - }, - is_dark: extended.is_dark, - } -} - -fn build_iced_theme(appearance: &Appearance, opacity: f32) -> Theme { +fn build_iced_theme(appearance: &Appearance) -> Theme { Theme::custom_with_fn( "local".to_string(), Palette { // The one colour here that is paint; the accents are read as ink. - background: appearance.background_color.get_base().scale_alpha(opacity), + background: appearance.background_color.get_base(), text: appearance.text_color.get_base(), primary: appearance.primary_color.get_base(), success: appearance.success_color.get_base(), @@ -273,14 +225,8 @@ fn build_iced_theme(appearance: &Appearance, opacity: f32) -> Theme { |palette| { let text = palette.text; let bg_text = appearance.background_color.get_text().unwrap_or(text); - // `mix` interpolates alpha too, so deriving from the translucent - // colour would spread assorted alphas across the variants. - let background = Color { - a: 1.0, - ..palette.background - }; - let default_bg = palette::Background::new(background, bg_text); + let default_bg = palette::Background::new(palette.background, bg_text); let bg = |level, fallback| { appearance .background_color @@ -290,26 +236,26 @@ fn build_iced_theme(appearance: &Appearance, opacity: f32) -> Theme { let default_primary = palette::Primary::generate( palette.primary, - background, + palette.background, appearance.primary_color.get_text().unwrap_or(text), ); let default_success = palette::Success::generate( palette.success, - background, + palette.background, appearance.success_color.get_text().unwrap_or(text), ); let default_warning = palette::Warning::generate( palette.warning, - background, + palette.background, appearance.warning_color.get_text().unwrap_or(text), ); let default_danger = palette::Danger::generate( palette.danger, - background, + palette.background, appearance.danger_color.get_text().unwrap_or(text), ); - let built = palette::Extended { + palette::Extended { background: palette::Background { base: default_bg.base, weakest: bg(BackgroundLevel::Weakest, default_bg.weakest), @@ -331,7 +277,7 @@ fn build_iced_theme(appearance: &Appearance, opacity: f32) -> Theme { .get_strong_pair(text) .unwrap_or(default_primary.strong), }, - secondary: palette::Secondary::generate(background, text), + secondary: palette::Secondary::generate(palette.background, text), success: palette::Success { base: default_success.base, weak: appearance @@ -366,9 +312,7 @@ fn build_iced_theme(appearance: &Appearance, opacity: f32) -> Theme { .unwrap_or(default_danger.strong), }, is_dark: true, - }; - - with_opacity(built, opacity) + } }, ) } @@ -383,16 +327,27 @@ fn base_theme_from_appearance( radius: Radius::default(), font_size: FontSize::default(), bar_position, - bar_surface: appearance.bar.surface, - bar_radius: appearance.bar.radius, - bar_margin: appearance.bar.margin, + + bar: appearance.bar, menu: appearance.menu, + + modules: Arc::new(appearance.modules.clone()), + grouped: appearance.grouped, + workspace_colors: appearance.workspace_colors.clone(), special_workspace_colors: appearance.special_workspace_colors.clone(), scale_factor: appearance.scale_factor, animations_enabled, - blur: appearance.blur.enabled(appearance.opacity), - iced_theme: build_iced_theme(appearance, appearance.opacity), + // Auto against the most translucent surface; the two opacities collapse + // into one in the opacity refactor. + blur: appearance.blur.enabled( + appearance + .bar + .opacity + .background + .min(appearance.menu.opacity), + ), + iced_theme: build_iced_theme(appearance), } } @@ -406,16 +361,7 @@ impl AshellTheme { } pub fn bar_layout(&self) -> BarLayout { - BarLayout::new(self.bar_surface, self.bar_margin) - } - - pub fn bar_border_radius(&self) -> border::Radius { - border::Radius { - top_left: self.radius.resolve(self.bar_radius.top_left), - top_right: self.radius.resolve(self.bar_radius.top_right), - bottom_right: self.radius.resolve(self.bar_radius.bottom_right), - bottom_left: self.radius.resolve(self.bar_radius.bottom_left), - } + BarLayout::new(self.bar) } pub fn button_style( @@ -427,6 +373,7 @@ impl AshellTheme { ButtonKind::Transparent => self.radius.sm, ButtonKind::Solid | ButtonKind::Outline => self.radius.xl, }; + let btn_opacity = self.bar.opacity.button; move |theme: &Theme, status: Status| { let palette = theme.palette(); @@ -458,7 +405,7 @@ impl AshellTheme { match (kind, status) { (ButtonKind::Solid, Status::Active) => button::Style { - background: Some(base_bg.into()), + background: Some(base_bg.scale_alpha(btn_opacity).into()), border: Border { width: 0.0, radius: radius.into(), @@ -468,7 +415,7 @@ impl AshellTheme { ..button::Style::default() }, (ButtonKind::Solid, Status::Hovered) => button::Style { - background: Some(hover_bg.into()), + background: Some(hover_bg.scale_alpha(btn_opacity).into()), border: Border { width: 0.0, radius: radius.into(), @@ -523,7 +470,7 @@ impl AshellTheme { }, // Transparent at rest, so hover adds an overlay, not a background. (ButtonKind::Outline, Status::Hovered) => button::Style { - background: Some(palette.text.scale_alpha(HOVER_OVERLAY).into()), + background: Some(base_bg.scale_alpha(btn_opacity).into()), border: Border { width: 2.0, radius: radius.into(), @@ -537,7 +484,9 @@ impl AshellTheme { let disabled_opacity = 0.3; match kind { ButtonKind::Solid => button::Style { - background: Some(base_bg.scale_alpha(disabled_opacity).into()), + background: Some( + base_bg.scale_alpha(btn_opacity * disabled_opacity).into(), + ), border: Border { width: 0.0, radius: radius.into(), @@ -588,6 +537,7 @@ impl AshellTheme { active: f32, ) -> impl Fn(&Theme, Status) -> button::Style + use<> { let radius_lg = self.radius.lg; + let bg_opacity = self.bar.opacity.background; move |theme: &Theme, status: Status| { let mut base = button::Style { background: None, @@ -607,7 +557,15 @@ impl AshellTheme { Status::Active => base, // Transparent at rest, so hover adds an overlay, not a background. Status::Hovered => { - base.background = Some(theme.palette().text.scale_alpha(HOVER_OVERLAY).into()); + base.background = Some( + theme + .extended_palette() + .background + .weak + .color + .scale_alpha(bg_opacity) + .into(), + ); base.text_color = theme.palette().text; base } @@ -620,11 +578,12 @@ impl AshellTheme { &self, active: f32, ) -> impl Fn(&Theme, Status) -> button::Style + use<> { + let bg_opacity = self.bar.opacity.background; let radius = self.radius.xl; move |theme: &Theme, status: Status| { let inactive_bg = theme.extended_palette().background.weak.color; - let active_bg = theme.extended_palette().primary.base.color; - let bg = lerp_color(inactive_bg, active_bg, active); + let active_bg = theme.palette().primary; + let bg = lerp_color(inactive_bg, active_bg, active).scale_alpha(bg_opacity); let mut base = button::Style { background: Some(bg.into()), @@ -645,7 +604,11 @@ impl AshellTheme { Status::Hovered => { let inactive_hover = theme.extended_palette().background.strong.color; let active_hover = theme.extended_palette().primary.weak.color; - base.background = Some(lerp_color(inactive_hover, active_hover, active).into()); + base.background = Some( + lerp_color(inactive_hover, active_hover, active) + .scale_alpha(bg_opacity) + .into(), + ); base } _ => base, @@ -797,30 +760,100 @@ impl AshellTheme { /// Module button style: transparent base with hover highlight. /// The module-group background is handled by `module_group`, not the button. - pub fn module_button_style(&self) -> impl Fn(&Theme, Status) -> button::Style + use<> { - let radius_lg = self.radius.lg; + pub fn module_button_style( + &self, + appearance: Option, + ) -> impl Fn(&Theme, Status) -> button::Style + use<> { + let (theme_radius, border, _module_padding, module_opacity) = ( + self.radius, + self.bar.module_border, + self.space.xxs, + self.bar.opacity.module, + ); + + let border = appearance.and_then(|a| a.border).map_or_else( + || Border { + width: 0.0, + radius: border.radius.resolve(theme_radius), + color: Color::TRANSPARENT, + }, + |BorderAppearance { + radius, + width, + color, + }| { + Border { + width, + color: color.get_base(), + radius: radius.resolve(theme_radius), + } + }, + ); + + let btn_opacity = self.bar.opacity.button; move |theme, status| { + let opacity = appearance.and_then(|a| a.opacity).unwrap_or(module_opacity); + + let background = Some(appearance.and_then(|a| a.background).map_or_else( + || theme.palette().background.scale_alpha(opacity).into(), + |background| background.get_base().scale_alpha(opacity).into(), + )); + + let text_color = appearance + .and_then(|a| a.text_color) + .map_or_else(|| theme.palette().text, |text_color| text_color.get_base()); + let mut base = button::Style { - background: None, - border: Border { - width: 0.0, - radius: radius_lg.into(), - color: Color::TRANSPARENT, - }, - text_color: theme.palette().text, + background, + border, + text_color, + ..button::Style::default() }; + match status { Status::Active => base, // The group pill already carries the opacity; overlay on it. Status::Hovered => { - base.background = Some(theme.palette().text.scale_alpha(HOVER_OVERLAY).into()); + base.background = Some( + appearance + .and_then(|a| a.background) + .map_or_else( + || { + theme + .extended_palette() + .background + .weak + .color + .scale_alpha(btn_opacity) + }, + |background| { + background + .get_pair(BackgroundLevel::Weak, background.get_base()) + .map_or_else( + || theme.extended_palette().background.weak.color, + |c| c.color, + ) + .scale_alpha(btn_opacity) + }, + ) + .into(), + ); + base } _ => base, } } } + + pub fn module_appearance(&self) -> impl Fn(&ModuleName) -> ModuleAppearance + use<> { + let modules = Arc::clone(&self.modules); + move |module_name| { + let module = modules.get(module_name); + module.copied().unwrap_or_default() + } + } } pub fn backdrop_color(backdrop: f32) -> Color {