advance shell integration and session support
This commit is contained in:
1 parent
0420242350
commit
e85fc1e032
27 files changed
+1165
-201
No files matched your search
@@ -10,6 +10,48 @@ const DEFAULT_BACKGROUND: [u8; 3] = [11, 15, 20];
|
||||
pub(crate) struct ShellConfig {
|
||||
pub(crate) font_family: Option<String>,
|
||||
pub(crate) wallpaper: WallpaperConfig,
|
||||
pub(crate) theme: ShellTheme,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct ShellTheme {
|
||||
pub(crate) surface_base: [u8; 4],
|
||||
pub(crate) text_primary: [u8; 4],
|
||||
pub(crate) text_muted: [u8; 4],
|
||||
pub(crate) accent: [u8; 4],
|
||||
pub(crate) border: [u8; 4],
|
||||
pub(crate) shadow: [u8; 4],
|
||||
pub(crate) bar_height: f32,
|
||||
pub(crate) bar_margin_top: i32,
|
||||
pub(crate) bar_margin_horizontal: i32,
|
||||
pub(crate) bar_radius: f32,
|
||||
pub(crate) panel_padding: f32,
|
||||
pub(crate) control_gap: f32,
|
||||
pub(crate) shadow_offset_y: f32,
|
||||
pub(crate) shadow_blur: f32,
|
||||
pub(crate) shadow_opacity: f32,
|
||||
}
|
||||
|
||||
impl Default for ShellTheme {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
surface_base: [17, 24, 33, 255],
|
||||
text_primary: [244, 247, 251, 255],
|
||||
text_muted: [127, 138, 152, 255],
|
||||
accent: [91, 140, 255, 255],
|
||||
border: [255, 255, 255, 24],
|
||||
shadow: [0, 0, 0, 85],
|
||||
bar_height: 38.0,
|
||||
bar_margin_top: 4,
|
||||
bar_margin_horizontal: 10,
|
||||
bar_radius: 19.0,
|
||||
panel_padding: 12.0,
|
||||
control_gap: 8.0,
|
||||
shadow_offset_y: 4.0,
|
||||
shadow_blur: 18.0,
|
||||
shadow_opacity: 0.20,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
@@ -35,6 +77,12 @@ struct FereseConfig {
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct ThemeConfig {
|
||||
#[serde(default)]
|
||||
colors: ThemeColorsConfig,
|
||||
#[serde(default)]
|
||||
geometry: ThemeGeometryConfig,
|
||||
#[serde(default)]
|
||||
shadow: ThemeShadowConfig,
|
||||
#[serde(default)]
|
||||
typography: TypographyConfig,
|
||||
#[serde(default)]
|
||||
@@ -46,6 +94,90 @@ struct TypographyConfig {
|
||||
font_family: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ThemeColorsConfig {
|
||||
#[serde(default = "default_surface_base")]
|
||||
surface_base: String,
|
||||
#[serde(default = "default_text_primary")]
|
||||
text_primary: String,
|
||||
#[serde(default = "default_text_muted")]
|
||||
text_muted: String,
|
||||
#[serde(default = "default_accent")]
|
||||
accent: String,
|
||||
#[serde(default = "default_border")]
|
||||
border: String,
|
||||
#[serde(default = "default_shadow")]
|
||||
shadow: String,
|
||||
}
|
||||
|
||||
impl Default for ThemeColorsConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
surface_base: default_surface_base(),
|
||||
text_primary: default_text_primary(),
|
||||
text_muted: default_text_muted(),
|
||||
accent: default_accent(),
|
||||
border: default_border(),
|
||||
shadow: default_shadow(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ThemeGeometryConfig {
|
||||
#[serde(default = "default_bar_height")]
|
||||
top_bar_height: f32,
|
||||
#[serde(default = "default_bar_margin_top")]
|
||||
top_bar_margin_top: i32,
|
||||
#[serde(default = "default_bar_margin_horizontal")]
|
||||
top_bar_margin_horizontal: i32,
|
||||
#[serde(default = "default_bar_radius")]
|
||||
top_bar_radius: f32,
|
||||
#[serde(default = "default_panel_padding")]
|
||||
panel_padding: f32,
|
||||
#[serde(default = "default_control_gap")]
|
||||
control_gap: f32,
|
||||
}
|
||||
|
||||
impl Default for ThemeGeometryConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
top_bar_height: default_bar_height(),
|
||||
top_bar_margin_top: default_bar_margin_top(),
|
||||
top_bar_margin_horizontal: default_bar_margin_horizontal(),
|
||||
top_bar_radius: default_bar_radius(),
|
||||
panel_padding: default_panel_padding(),
|
||||
control_gap: default_control_gap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct ThemeShadowConfig {
|
||||
#[serde(default)]
|
||||
soft: SoftShadowConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SoftShadowConfig {
|
||||
#[serde(default = "default_shadow_offset_y")]
|
||||
offset_y: f32,
|
||||
#[serde(default = "default_shadow_blur")]
|
||||
blur: f32,
|
||||
#[serde(default = "default_shadow_opacity")]
|
||||
opacity: f32,
|
||||
}
|
||||
|
||||
impl Default for SoftShadowConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
offset_y: default_shadow_offset_y(),
|
||||
blur: default_shadow_blur(),
|
||||
opacity: default_shadow_opacity(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn load() -> ShellConfig {
|
||||
let Some(path) = config_path() else {
|
||||
return ShellConfig::default();
|
||||
@@ -55,10 +187,15 @@ pub(crate) fn load() -> ShellConfig {
|
||||
};
|
||||
|
||||
match toml::from_str::<FereseConfig>(&source) {
|
||||
Ok(config) => ShellConfig {
|
||||
font_family: config.theme.typography.font_family,
|
||||
wallpaper: config.theme.background,
|
||||
},
|
||||
Ok(config) => {
|
||||
let theme = shell_theme(&config.theme);
|
||||
|
||||
ShellConfig {
|
||||
font_family: config.theme.typography.font_family,
|
||||
wallpaper: config.theme.background,
|
||||
theme,
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("ferese-shell: failed to read {}: {error}", path.display());
|
||||
ShellConfig::default()
|
||||
@@ -66,6 +203,112 @@ pub(crate) fn load() -> ShellConfig {
|
||||
}
|
||||
}
|
||||
|
||||
fn shell_theme(theme: &ThemeConfig) -> ShellTheme {
|
||||
let defaults = ShellTheme::default();
|
||||
|
||||
ShellTheme {
|
||||
surface_base: parse_color(&theme.colors.surface_base).unwrap_or(defaults.surface_base),
|
||||
text_primary: parse_color(&theme.colors.text_primary).unwrap_or(defaults.text_primary),
|
||||
text_muted: parse_color(&theme.colors.text_muted).unwrap_or(defaults.text_muted),
|
||||
accent: parse_color(&theme.colors.accent).unwrap_or(defaults.accent),
|
||||
border: parse_color(&theme.colors.border).unwrap_or(defaults.border),
|
||||
shadow: parse_color(&theme.colors.shadow).unwrap_or(defaults.shadow),
|
||||
bar_height: positive_or(theme.geometry.top_bar_height, defaults.bar_height),
|
||||
bar_margin_top: theme.geometry.top_bar_margin_top.max(0),
|
||||
bar_margin_horizontal: theme.geometry.top_bar_margin_horizontal.max(0),
|
||||
bar_radius: nonnegative_or(theme.geometry.top_bar_radius, defaults.bar_radius),
|
||||
panel_padding: nonnegative_or(theme.geometry.panel_padding, defaults.panel_padding),
|
||||
control_gap: nonnegative_or(theme.geometry.control_gap, defaults.control_gap),
|
||||
shadow_offset_y: finite_or(theme.shadow.soft.offset_y, defaults.shadow_offset_y),
|
||||
shadow_blur: nonnegative_or(theme.shadow.soft.blur, defaults.shadow_blur),
|
||||
shadow_opacity: theme.shadow.soft.opacity.clamp(0.0, 1.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_color(value: &str) -> Option<[u8; 4]> {
|
||||
let value = value.strip_prefix('#')?;
|
||||
if value.len() != 6 && value.len() != 8 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let red = u8::from_str_radix(&value[0..2], 16).ok()?;
|
||||
let green = u8::from_str_radix(&value[2..4], 16).ok()?;
|
||||
let blue = u8::from_str_radix(&value[4..6], 16).ok()?;
|
||||
let alpha = if value.len() == 8 {
|
||||
u8::from_str_radix(&value[6..8], 16).ok()?
|
||||
} else {
|
||||
255
|
||||
};
|
||||
|
||||
Some([red, green, blue, alpha])
|
||||
}
|
||||
|
||||
fn positive_or(value: f32, fallback: f32) -> f32 {
|
||||
if value.is_finite() && value > 0.0 {
|
||||
value
|
||||
} else {
|
||||
fallback
|
||||
}
|
||||
}
|
||||
|
||||
fn nonnegative_or(value: f32, fallback: f32) -> f32 {
|
||||
if value.is_finite() && value >= 0.0 {
|
||||
value
|
||||
} else {
|
||||
fallback
|
||||
}
|
||||
}
|
||||
|
||||
fn finite_or(value: f32, fallback: f32) -> f32 {
|
||||
if value.is_finite() { value } else { fallback }
|
||||
}
|
||||
|
||||
fn default_surface_base() -> String {
|
||||
"#111821".to_owned()
|
||||
}
|
||||
fn default_text_primary() -> String {
|
||||
"#F4F7FB".to_owned()
|
||||
}
|
||||
fn default_text_muted() -> String {
|
||||
"#7F8A98".to_owned()
|
||||
}
|
||||
fn default_accent() -> String {
|
||||
"#5B8CFF".to_owned()
|
||||
}
|
||||
fn default_border() -> String {
|
||||
"#FFFFFF18".to_owned()
|
||||
}
|
||||
fn default_shadow() -> String {
|
||||
"#00000055".to_owned()
|
||||
}
|
||||
const fn default_bar_height() -> f32 {
|
||||
38.0
|
||||
}
|
||||
const fn default_bar_margin_top() -> i32 {
|
||||
4
|
||||
}
|
||||
const fn default_bar_margin_horizontal() -> i32 {
|
||||
10
|
||||
}
|
||||
const fn default_bar_radius() -> f32 {
|
||||
19.0
|
||||
}
|
||||
const fn default_panel_padding() -> f32 {
|
||||
12.0
|
||||
}
|
||||
const fn default_control_gap() -> f32 {
|
||||
8.0
|
||||
}
|
||||
const fn default_shadow_offset_y() -> f32 {
|
||||
4.0
|
||||
}
|
||||
const fn default_shadow_blur() -> f32 {
|
||||
18.0
|
||||
}
|
||||
const fn default_shadow_opacity() -> f32 {
|
||||
0.20
|
||||
}
|
||||
|
||||
pub(crate) const fn default_background() -> [u8; 3] {
|
||||
DEFAULT_BACKGROUND
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ pub(crate) struct WindowSnapshot {
|
||||
pub(crate) app_id: String,
|
||||
pub(crate) title: String,
|
||||
pub(crate) focused: bool,
|
||||
pub(crate) fullscreen: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct ShellControl {
|
||||
@@ -56,11 +57,13 @@ pub(crate) struct ShellControl {
|
||||
|
||||
pub(crate) struct ControlPoll {
|
||||
pub(crate) snapshot: Option<ShellSnapshot>,
|
||||
pub(crate) overview_active: Option<bool>,
|
||||
pub(crate) disconnected: bool,
|
||||
}
|
||||
|
||||
enum ControlUpdate {
|
||||
Snapshot(ShellSnapshot),
|
||||
OverviewState(bool),
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
@@ -93,12 +96,14 @@ impl ShellControl {
|
||||
pub(crate) fn poll(&self) -> ControlPoll {
|
||||
let mut poll = ControlPoll {
|
||||
snapshot: None,
|
||||
overview_active: None,
|
||||
disconnected: false,
|
||||
};
|
||||
|
||||
loop {
|
||||
match self.updates.try_recv() {
|
||||
Ok(ControlUpdate::Snapshot(snapshot)) => poll.snapshot = Some(snapshot),
|
||||
Ok(ControlUpdate::OverviewState(active)) => poll.overview_active = Some(active),
|
||||
Ok(ControlUpdate::Disconnected) | Err(TryRecvError::Disconnected) => {
|
||||
poll.disconnected = true;
|
||||
return poll;
|
||||
@@ -115,8 +120,12 @@ impl ShellControl {
|
||||
let _ = self.connection.flush();
|
||||
}
|
||||
|
||||
pub(crate) fn enter_overview(&self) {
|
||||
self.shell.enter_overview();
|
||||
pub(crate) fn set_overview_active(&self, active: bool) {
|
||||
if active {
|
||||
self.shell.enter_overview();
|
||||
} else {
|
||||
self.shell.exit_overview();
|
||||
}
|
||||
let _ = self.connection.flush();
|
||||
}
|
||||
}
|
||||
@@ -214,17 +223,20 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
|
||||
title,
|
||||
state: window_state,
|
||||
} => {
|
||||
let focused = matches!(
|
||||
window_state,
|
||||
WEnum::Value(flags)
|
||||
if flags.contains(ferese_shell_v1::WindowState::Focused)
|
||||
);
|
||||
let (focused, fullscreen) = match window_state {
|
||||
WEnum::Value(flags) => (
|
||||
flags.contains(ferese_shell_v1::WindowState::Focused),
|
||||
flags.contains(ferese_shell_v1::WindowState::Fullscreen),
|
||||
),
|
||||
WEnum::Unknown(_) => (false, false),
|
||||
};
|
||||
|
||||
state.pending.windows.push(WindowSnapshot {
|
||||
workspace: join_id(workspace_hi, workspace_lo),
|
||||
app_id,
|
||||
title,
|
||||
focused,
|
||||
fullscreen,
|
||||
});
|
||||
}
|
||||
ferese_shell_v1::Event::SnapshotEnd { serial }
|
||||
@@ -234,6 +246,9 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
|
||||
.sender
|
||||
.send(ControlUpdate::Snapshot(state.pending.clone()));
|
||||
}
|
||||
ferese_shell_v1::Event::OverviewState { active } => {
|
||||
let _ = state.sender.send(ControlUpdate::OverviewState(active != 0));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
+248
-77
@@ -9,6 +9,7 @@ use cosmic::iced::alignment;
|
||||
use cosmic::iced::event::{self, PlatformSpecific, wayland};
|
||||
use cosmic::iced::platform_specific::{
|
||||
runtime::wayland::layer_surface::{IcedMargin, IcedOutput, SctkLayerSurfaceSettings},
|
||||
shell::commands::layer_surface::set_input_zone,
|
||||
shell::commands::layer_surface::{Anchor, KeyboardInteractivity, Layer},
|
||||
};
|
||||
use cosmic::iced::{
|
||||
@@ -16,7 +17,7 @@ use cosmic::iced::{
|
||||
window,
|
||||
};
|
||||
use cosmic::theme;
|
||||
use cosmic::widget::{button, container, image, row, text};
|
||||
use cosmic::widget::{button, container, icon, image, row, text};
|
||||
use ferese_protocols::effects::v1::client::{
|
||||
ferese_effects_manager_v1::FereseEffectsManagerV1,
|
||||
ferese_surface_effects_v1::{self, FereseSurfaceEffectsV1},
|
||||
@@ -28,21 +29,41 @@ use wayland_client::{
|
||||
protocol::{wl_registry, wl_surface},
|
||||
};
|
||||
|
||||
use crate::config::{ShellConfig, WallpaperMode};
|
||||
use crate::config::{ShellConfig, ShellTheme, WallpaperMode};
|
||||
use crate::control::{ShellControl, ShellSnapshot};
|
||||
|
||||
const APP_ID: &str = "dev.ferese.Shell";
|
||||
const BAR_HEIGHT: u32 = 32;
|
||||
const BAR_MARGIN: i32 = 4;
|
||||
const EXCLUSIVE_ZONE: i32 = BAR_HEIGHT as i32;
|
||||
const BAR_TEXT_SIZE: u16 = (BAR_HEIGHT * 3 / 8) as u16;
|
||||
const BAR_ICON_SIZE: u16 = (BAR_HEIGHT / 2) as u16;
|
||||
const CONTROL_HEIGHT: f32 = BAR_HEIGHT as f32 * 0.75;
|
||||
const WORKSPACE_HIT_WIDTH: f32 = BAR_HEIGHT as f32 * 0.6875;
|
||||
const ACTIVE_MARKER_WIDTH: f32 = BAR_HEIGHT as f32 * 0.5;
|
||||
const ACTIVE_MARKER_HEIGHT: f32 = BAR_HEIGHT as f32 * 0.125;
|
||||
const DOT_MARKER_SIZE: f32 = BAR_HEIGHT as f32 * 0.1875;
|
||||
const EMPTY_MARKER_SIZE: f32 = BAR_HEIGHT as f32 * 0.125;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct BarMetrics {
|
||||
height: f32,
|
||||
text_size: u16,
|
||||
icon_size: u16,
|
||||
control_height: f32,
|
||||
workspace_hit_width: f32,
|
||||
active_marker_width: f32,
|
||||
active_marker_height: f32,
|
||||
dot_marker_size: f32,
|
||||
empty_marker_size: f32,
|
||||
}
|
||||
|
||||
impl From<ShellTheme> for BarMetrics {
|
||||
fn from(theme: ShellTheme) -> Self {
|
||||
let height = theme.bar_height;
|
||||
|
||||
Self {
|
||||
height,
|
||||
text_size: (height * 0.368).round() as u16,
|
||||
icon_size: (height * 0.553).round() as u16,
|
||||
control_height: height * 0.79,
|
||||
workspace_hit_width: height * 0.68,
|
||||
active_marker_width: height * 0.47,
|
||||
active_marker_height: height * 0.13,
|
||||
dot_marker_size: height * 0.18,
|
||||
empty_marker_size: height * 0.13,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> cosmic::iced::Result {
|
||||
let config = config::load();
|
||||
@@ -66,6 +87,7 @@ struct FereseShell {
|
||||
config: ShellConfig,
|
||||
control: Option<ShellControl>,
|
||||
snapshot: ShellSnapshot,
|
||||
overview_active: bool,
|
||||
clock: String,
|
||||
effects: Option<EffectsBinding>,
|
||||
}
|
||||
@@ -75,7 +97,7 @@ enum Message {
|
||||
Event(Event, window::Id),
|
||||
Tick,
|
||||
ActivateWorkspace(u64),
|
||||
EnterOverview,
|
||||
ToggleOverview,
|
||||
}
|
||||
|
||||
impl cosmic::Application for FereseShell {
|
||||
@@ -96,6 +118,8 @@ impl cosmic::Application for FereseShell {
|
||||
fn init(core: Core, config: Self::Flags) -> (Self, Task<Self::Message>) {
|
||||
let bar_surface_id = window::Id::unique();
|
||||
let wallpaper_surface_id = window::Id::unique();
|
||||
let shell_theme = config.theme;
|
||||
let bar = BarMetrics::from(shell_theme);
|
||||
let app = Self {
|
||||
core,
|
||||
bar_surface_id,
|
||||
@@ -106,6 +130,7 @@ impl cosmic::Application for FereseShell {
|
||||
})
|
||||
.ok(),
|
||||
snapshot: ShellSnapshot::default(),
|
||||
overview_active: false,
|
||||
clock: current_time(),
|
||||
effects: None,
|
||||
};
|
||||
@@ -135,14 +160,14 @@ impl cosmic::Application for FereseShell {
|
||||
output: IcedOutput::Active,
|
||||
namespace: "ferese-shell-top-bar".to_owned(),
|
||||
margin: IcedMargin {
|
||||
top: BAR_MARGIN,
|
||||
right: 10,
|
||||
top: shell_theme.bar_margin_top,
|
||||
right: shell_theme.bar_margin_horizontal,
|
||||
bottom: 0,
|
||||
left: 10,
|
||||
left: shell_theme.bar_margin_horizontal,
|
||||
},
|
||||
size: Some((None, Some(BAR_HEIGHT))),
|
||||
size: Some((None, Some(bar.height.round() as u32))),
|
||||
size_limits: Limits::NONE,
|
||||
exclusive_zone: EXCLUSIVE_ZONE,
|
||||
exclusive_zone: bar.height.round() as i32,
|
||||
..Default::default()
|
||||
},
|
||||
Some(Box::new(Self::view_layer)),
|
||||
@@ -173,8 +198,26 @@ impl cosmic::Application for FereseShell {
|
||||
if poll.disconnected {
|
||||
return cosmic::iced::exit();
|
||||
}
|
||||
if let Some(active) = poll.overview_active {
|
||||
self.overview_active = active;
|
||||
}
|
||||
if let Some(snapshot) = poll.snapshot {
|
||||
let was_hidden = bar_hidden(&self.snapshot);
|
||||
|
||||
self.snapshot = snapshot;
|
||||
|
||||
let hidden = bar_hidden(&self.snapshot);
|
||||
if hidden != was_hidden {
|
||||
if let Some(effects) = &self.effects
|
||||
&& let Err(error) = effects.set_visible(!hidden)
|
||||
{
|
||||
eprintln!("ferese-shell: could not update panel material: {error}");
|
||||
}
|
||||
|
||||
let input_zone = if hidden { Some(Vec::new()) } else { None };
|
||||
|
||||
return set_input_zone(self.bar_surface_id, input_zone);
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
@@ -185,9 +228,10 @@ impl cosmic::Application for FereseShell {
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::EnterOverview => {
|
||||
Message::ToggleOverview => {
|
||||
self.overview_active = !self.overview_active;
|
||||
if let Some(control) = &self.control {
|
||||
control.enter_overview();
|
||||
control.set_overview_active(self.overview_active);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
@@ -234,13 +278,20 @@ impl FereseShell {
|
||||
return;
|
||||
}
|
||||
|
||||
match EffectsBinding::attach(surface) {
|
||||
match EffectsBinding::attach(surface, !bar_hidden(&self.snapshot)) {
|
||||
Ok(binding) => self.effects = Some(binding),
|
||||
Err(error) => eprintln!("ferese-shell: panel material unavailable: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn view_layer(&self) -> Element<'_, cosmic::Action<Message>> {
|
||||
if bar_hidden(&self.snapshot) {
|
||||
return container(text(""))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into();
|
||||
}
|
||||
|
||||
let focused_output = self
|
||||
.snapshot
|
||||
.outputs
|
||||
@@ -248,16 +299,18 @@ impl FereseShell {
|
||||
.find(|output| output.focused)
|
||||
.or_else(|| self.snapshot.outputs.first());
|
||||
let focused_output_id = focused_output.map(|output| output.id);
|
||||
let shell_theme = self.config.theme;
|
||||
let bar = BarMetrics::from(shell_theme);
|
||||
let mut workspace_row = row::with_capacity(self.snapshot.workspaces.len() + 1)
|
||||
.spacing(1)
|
||||
.align_y(cosmic::iced::Alignment::Center);
|
||||
|
||||
workspace_row = workspace_row.push(
|
||||
button::suggested("F")
|
||||
.font_size(BAR_TEXT_SIZE)
|
||||
.height(CONTROL_HEIGHT)
|
||||
.padding([2, 9])
|
||||
.on_press(cosmic::Action::App(Message::EnterOverview)),
|
||||
button::custom(overview_control(bar, shell_theme))
|
||||
.height(bar.control_height)
|
||||
.padding(0)
|
||||
.class(theme::Button::Transparent)
|
||||
.on_press(cosmic::Action::App(Message::ToggleOverview)),
|
||||
);
|
||||
for workspace in self
|
||||
.snapshot
|
||||
@@ -270,9 +323,9 @@ impl FereseShell {
|
||||
.windows
|
||||
.iter()
|
||||
.any(|window| window.workspace == workspace.id);
|
||||
let indicator = workspace_indicator(workspace.active, occupied);
|
||||
let indicator = workspace_indicator(workspace.active, occupied, bar, shell_theme);
|
||||
let workspace_button = button::custom(indicator)
|
||||
.height(CONTROL_HEIGHT)
|
||||
.height(bar.control_height)
|
||||
.padding(0)
|
||||
.class(theme::Button::Transparent)
|
||||
.on_press(cosmic::Action::App(Message::ActivateWorkspace(
|
||||
@@ -293,16 +346,16 @@ impl FereseShell {
|
||||
})
|
||||
.map(display_app_name)
|
||||
.unwrap_or_else(|| "Desktop".to_owned());
|
||||
let left = row![workspace_row, text(app_name).size(BAR_TEXT_SIZE)]
|
||||
.spacing(12)
|
||||
let left = row![workspace_row, text(app_name).size(bar.text_size)]
|
||||
.spacing(shell_theme.control_gap)
|
||||
.align_y(cosmic::iced::Alignment::Center);
|
||||
let center = text(&self.clock).size(BAR_TEXT_SIZE);
|
||||
let center = text(&self.clock).size(bar.text_size);
|
||||
let right = row![
|
||||
text("◌").size(BAR_ICON_SIZE),
|
||||
text("♪").size(BAR_ICON_SIZE),
|
||||
text("●").size(BAR_ICON_SIZE),
|
||||
symbolic_icon(include_bytes!("../assets/icons/wifi.svg"), bar.icon_size),
|
||||
symbolic_icon(include_bytes!("../assets/icons/volume.svg"), bar.icon_size),
|
||||
symbolic_icon(include_bytes!("../assets/icons/battery.svg"), bar.icon_size),
|
||||
]
|
||||
.spacing(10)
|
||||
.spacing(shell_theme.control_gap)
|
||||
.align_y(cosmic::iced::Alignment::Center);
|
||||
let content = row![
|
||||
container(left)
|
||||
@@ -321,8 +374,8 @@ impl FereseShell {
|
||||
container(content)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.padding([0, 12])
|
||||
.class(theme::Container::custom(bar_style))
|
||||
.padding([0, shell_theme.panel_padding.round() as u16])
|
||||
.class(theme::Container::custom(move |_| bar_style(shell_theme)))
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -347,51 +400,98 @@ impl FereseShell {
|
||||
}
|
||||
}
|
||||
|
||||
fn workspace_indicator(active: bool, occupied: bool) -> Element<'static, cosmic::Action<Message>> {
|
||||
let (width, height, style) = if active {
|
||||
fn symbolic_icon(source: &'static [u8], size: u16) -> icon::Icon {
|
||||
icon::from_svg_bytes(source)
|
||||
.symbolic(true)
|
||||
.icon()
|
||||
.size(size)
|
||||
}
|
||||
|
||||
fn overview_control(
|
||||
bar: BarMetrics,
|
||||
shell_theme: ShellTheme,
|
||||
) -> Element<'static, cosmic::Action<Message>> {
|
||||
container(symbolic_icon(
|
||||
include_bytes!("../assets/icons/overview.svg"),
|
||||
bar.icon_size,
|
||||
))
|
||||
.width(bar.control_height)
|
||||
.height(bar.control_height)
|
||||
.align_x(alignment::Horizontal::Center)
|
||||
.align_y(alignment::Vertical::Center)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
icon_color: Some(color(shell_theme.text_primary)),
|
||||
background: Some(Background::Color(color(shell_theme.accent))),
|
||||
border: Border {
|
||||
color: color_with_opacity(shell_theme.text_primary, 0.14),
|
||||
width: 1.0,
|
||||
radius: (bar.control_height * 0.34).into(),
|
||||
},
|
||||
snap: true,
|
||||
..container::Style::default()
|
||||
}))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn bar_hidden(snapshot: &ShellSnapshot) -> bool {
|
||||
let active_workspace = snapshot
|
||||
.outputs
|
||||
.iter()
|
||||
.find(|output| output.focused)
|
||||
.or_else(|| snapshot.outputs.first())
|
||||
.map(|output| output.active_workspace);
|
||||
|
||||
active_workspace.is_some_and(|workspace| {
|
||||
snapshot
|
||||
.windows
|
||||
.iter()
|
||||
.any(|window| window.workspace == workspace && window.fullscreen)
|
||||
})
|
||||
}
|
||||
|
||||
fn workspace_indicator(
|
||||
active: bool,
|
||||
occupied: bool,
|
||||
bar: BarMetrics,
|
||||
shell_theme: ShellTheme,
|
||||
) -> Element<'static, cosmic::Action<Message>> {
|
||||
let (width, height, color, radius) = if active {
|
||||
(
|
||||
ACTIVE_MARKER_WIDTH,
|
||||
ACTIVE_MARKER_HEIGHT,
|
||||
active_workspace_style as fn(&cosmic::Theme) -> container::Style,
|
||||
bar.active_marker_width,
|
||||
bar.active_marker_height,
|
||||
color(shell_theme.accent),
|
||||
2.0,
|
||||
)
|
||||
} else if occupied {
|
||||
(
|
||||
DOT_MARKER_SIZE,
|
||||
DOT_MARKER_SIZE,
|
||||
occupied_workspace_style as fn(&cosmic::Theme) -> container::Style,
|
||||
bar.dot_marker_size,
|
||||
bar.dot_marker_size,
|
||||
color_with_opacity(shell_theme.text_muted, 0.78),
|
||||
3.0,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
EMPTY_MARKER_SIZE,
|
||||
EMPTY_MARKER_SIZE,
|
||||
empty_workspace_style as fn(&cosmic::Theme) -> container::Style,
|
||||
bar.empty_marker_size,
|
||||
bar.empty_marker_size,
|
||||
color_with_opacity(shell_theme.text_muted, 0.42),
|
||||
2.0,
|
||||
)
|
||||
};
|
||||
let marker = container(text(""))
|
||||
.width(width)
|
||||
.height(height)
|
||||
.class(theme::Container::custom(style));
|
||||
.class(theme::Container::custom(move |_| {
|
||||
workspace_marker_style(color, radius)
|
||||
}));
|
||||
|
||||
container(marker)
|
||||
.width(WORKSPACE_HIT_WIDTH)
|
||||
.height(CONTROL_HEIGHT)
|
||||
.width(bar.workspace_hit_width)
|
||||
.height(bar.control_height)
|
||||
.align_x(alignment::Horizontal::Center)
|
||||
.align_y(alignment::Vertical::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn active_workspace_style(_theme: &cosmic::Theme) -> container::Style {
|
||||
workspace_marker_style(Color::from_rgb8(91, 140, 255), 2.0)
|
||||
}
|
||||
|
||||
fn occupied_workspace_style(_theme: &cosmic::Theme) -> container::Style {
|
||||
workspace_marker_style(Color::from_rgba8(221, 228, 240, 0.68), 3.0)
|
||||
}
|
||||
|
||||
fn empty_workspace_style(_theme: &cosmic::Theme) -> container::Style {
|
||||
workspace_marker_style(Color::from_rgba8(221, 228, 240, 0.25), 2.0)
|
||||
}
|
||||
|
||||
fn workspace_marker_style(color: Color, radius: f32) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(color)),
|
||||
@@ -426,25 +526,37 @@ fn current_time() -> String {
|
||||
Zoned::now().strftime("%H:%M").to_string()
|
||||
}
|
||||
|
||||
fn bar_style(_theme: &cosmic::Theme) -> container::Style {
|
||||
fn bar_style(theme: ShellTheme) -> container::Style {
|
||||
let mut shadow = color(theme.shadow);
|
||||
shadow.a *= theme.shadow_opacity;
|
||||
|
||||
container::Style {
|
||||
icon_color: Some(Color::from_rgb8(235, 239, 247)),
|
||||
text_color: Some(Color::from_rgb8(235, 239, 247)),
|
||||
background: Some(Background::Color(Color::from_rgba8(18, 21, 28, 0.88))),
|
||||
icon_color: Some(color(theme.text_primary)),
|
||||
text_color: Some(color(theme.text_primary)),
|
||||
background: None,
|
||||
border: Border {
|
||||
color: Color::from_rgba8(255, 255, 255, 0.14),
|
||||
color: color(theme.border),
|
||||
width: 1.0,
|
||||
radius: 16.0.into(),
|
||||
radius: theme.bar_radius.into(),
|
||||
},
|
||||
shadow: Shadow {
|
||||
color: Color::from_rgba8(0, 0, 0, 0.32),
|
||||
offset: Vector::new(0.0, 4.0),
|
||||
blur_radius: 18.0,
|
||||
color: shadow,
|
||||
offset: Vector::new(0.0, theme.shadow_offset_y),
|
||||
blur_radius: theme.shadow_blur,
|
||||
},
|
||||
snap: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn color([red, green, blue, alpha]: [u8; 4]) -> Color {
|
||||
Color::from_rgba8(red, green, blue, f32::from(alpha) / 255.0)
|
||||
}
|
||||
|
||||
fn color_with_opacity(mut value: [u8; 4], opacity: f32) -> Color {
|
||||
value[3] = (f32::from(value[3]) * opacity).round() as u8;
|
||||
color(value)
|
||||
}
|
||||
|
||||
fn wallpaper_fallback_style(_theme: &cosmic::Theme) -> container::Style {
|
||||
let [red, green, blue] = config::default_background();
|
||||
|
||||
@@ -455,13 +567,17 @@ fn wallpaper_fallback_style(_theme: &cosmic::Theme) -> container::Style {
|
||||
}
|
||||
|
||||
struct EffectsBinding {
|
||||
connection: Connection,
|
||||
_manager: FereseEffectsManagerV1,
|
||||
_surface: FereseSurfaceEffectsV1,
|
||||
surface: FereseSurfaceEffectsV1,
|
||||
_queue: EventQueue<EffectsState>,
|
||||
}
|
||||
|
||||
impl EffectsBinding {
|
||||
fn attach(surface: &wl_surface::WlSurface) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
fn attach(
|
||||
surface: &wl_surface::WlSurface,
|
||||
visible: bool,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let backend = surface
|
||||
.backend()
|
||||
.upgrade()
|
||||
@@ -472,15 +588,32 @@ impl EffectsBinding {
|
||||
let manager = globals.bind::<FereseEffectsManagerV1, _, _>(&qh, 1..=1, ())?;
|
||||
let effects = manager.get_surface_effects(surface, &qh, ());
|
||||
|
||||
effects.set_role(ferese_surface_effects_v1::Role::Panel);
|
||||
if visible {
|
||||
effects.set_role(ferese_surface_effects_v1::Role::Panel);
|
||||
} else {
|
||||
effects.clear_role();
|
||||
}
|
||||
connection.flush()?;
|
||||
|
||||
Ok(Self {
|
||||
connection,
|
||||
_manager: manager,
|
||||
_surface: effects,
|
||||
surface: effects,
|
||||
_queue: queue,
|
||||
})
|
||||
}
|
||||
|
||||
fn set_visible(&self, visible: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if visible {
|
||||
self.surface
|
||||
.set_role(ferese_surface_effects_v1::Role::Panel);
|
||||
} else {
|
||||
self.surface.clear_role();
|
||||
}
|
||||
|
||||
self.connection.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct EffectsState;
|
||||
@@ -499,3 +632,41 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for EffectsState {
|
||||
|
||||
delegate_noop!(EffectsState: ignore FereseEffectsManagerV1);
|
||||
delegate_noop!(EffectsState: ignore FereseSurfaceEffectsV1);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::control::{OutputSnapshot, WindowSnapshot};
|
||||
|
||||
#[test]
|
||||
fn fullscreen_on_the_active_workspace_hides_the_bar() {
|
||||
let snapshot = snapshot_with_fullscreen_window(7);
|
||||
|
||||
assert!(bar_hidden(&snapshot));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fullscreen_on_an_inactive_workspace_keeps_the_bar_visible() {
|
||||
let snapshot = snapshot_with_fullscreen_window(8);
|
||||
|
||||
assert!(!bar_hidden(&snapshot));
|
||||
}
|
||||
|
||||
fn snapshot_with_fullscreen_window(workspace: u64) -> ShellSnapshot {
|
||||
ShellSnapshot {
|
||||
outputs: vec![OutputSnapshot {
|
||||
id: 1,
|
||||
active_workspace: 7,
|
||||
focused: true,
|
||||
}],
|
||||
workspaces: Vec::new(),
|
||||
windows: vec![WindowSnapshot {
|
||||
workspace,
|
||||
app_id: "dev.ferese.Test".to_owned(),
|
||||
title: "Test".to_owned(),
|
||||
focused: true,
|
||||
fullscreen: true,
|
||||
}],
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user