From 4a6d91980cb269e0ac900cb340f11dd56f455312 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Fri, 25 Sep 2026 07:47:56 +0100 Subject: [PATCH] feat: add modern shell top bar --- Cargo.lock | 10 + Cargo.toml | 3 + README.md | 13 + compositor/src/main.rs | 61 ++++- compositor/src/winit.rs | 76 +++++- shell/ferese-shell/Cargo.toml | 12 + shell/ferese-shell/src/control.rs | 254 +++++++++++++++++++ shell/ferese-shell/src/main.rs | 371 ++++++++++++++++++++++++++++ tools/ferese-shell-spike/Cargo.toml | 2 +- 9 files changed, 782 insertions(+), 20 deletions(-) create mode 100644 shell/ferese-shell/Cargo.toml create mode 100644 shell/ferese-shell/src/control.rs create mode 100644 shell/ferese-shell/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 5b55c80..20fd399 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1637,6 +1637,16 @@ dependencies = [ "wayland-server", ] +[[package]] +name = "ferese-shell" +version = "0.1.0" +dependencies = [ + "ferese-protocols", + "jiff", + "libcosmic", + "wayland-client", +] + [[package]] name = "ferese-shell-control-probe" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 03a50b6..333afb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "crates/ferese-ipc", "crates/ferese-layout", "crates/ferese-protocols", + "shell/ferese-shell", "tools/ferese-effects-probe", "tools/ferese-malformed-client", "tools/ferese-shell-control-probe", @@ -23,7 +24,9 @@ rust-version = "1.87" [workspace.dependencies] bitflags = "2.9.4" calloop = { version = "0.14.4", features = ["signals"] } +jiff = "0.2.37" libc = "0.2.176" +libcosmic = { git = "https://github.com/pop-os/libcosmic", rev = "03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0", default-features = false, features = ["multi-window", "tokio", "wayland"] } smithay = { version = "0.7.0", default-features = false, features = [ "backend_drm", "backend_gbm", diff --git a/README.md b/README.md index 4390f23..a492719 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,19 @@ their client surfaces, `Super+H/J/K/L` moves the selection using the presented preview geometry, and clicking a preview activates it without forwarding the selection click to the application. +The first production shell slice provides a floating libcosmic top bar backed +by the private shell-control connection. Build and preview it nested with: + +```bash +cargo build -p ferese -p ferese-shell +target/debug/ferese --backend nested --grant-effects --grant-shell-control -- \ + target/debug/ferese-shell +``` + +The Ferese mark enters overview, workspace pills switch workspaces, the clock +remains geometrically centered, and focused-application/output metadata updates +from compositor snapshots. + Keyboard and direct-session touchpad settings are applied at startup: ```toml diff --git a/compositor/src/main.rs b/compositor/src/main.rs index 300d3f4..1712bea 100644 --- a/compositor/src/main.rs +++ b/compositor/src/main.rs @@ -17,7 +17,13 @@ mod state; mod window_rules; mod winit; -use std::{error::Error, io, process::Command}; +use std::{ + error::Error, + io, + process::{Child, Command}, + thread, + time::Duration, +}; use calloop::signals::{Signal, Signals}; use smithay::reexports::{calloop::EventLoop, wayland_server::Display}; @@ -70,9 +76,14 @@ fn main() -> Result<(), Box> { unsafe { std::env::set_var("WAYLAND_DISPLAY", &state.socket_name) }; info!(socket = ?state.socket_name, backend = ?launch.backend, "Ferese is accepting Wayland clients"); - spawn_client(&mut state, launch.client, launch.client_capabilities); - event_loop.run(None, &mut state, |_| {})?; - Ok(()) + let mut child = spawn_client(&mut state, launch.client, launch.client_capabilities); + let result = event_loop.run(None, &mut state, |_| {}); + + if let Some(child) = &mut child { + terminate_child(child); + } + + result.map_err(Into::into) } fn init_logging() { @@ -85,10 +96,10 @@ fn spawn_client( state: &mut Ferese, mut args: Vec, capabilities: private_client::ClientCapabilities, -) { +) -> Option { if args.is_empty() { info!("no client requested; pass one after `--`, for example `-- foot`"); - return; + return None; } let program = args.remove(0); @@ -101,14 +112,42 @@ fn spawn_client( Ok(connection) => Some(connection), Err(error) => { warn!(program = ?program, %error, "failed to create private Wayland connection"); - return; + return None; } } }; - match command.spawn() { - Ok(child) => info!(program = ?program, pid = child.id(), "spawned Wayland client"), - Err(error) => warn!(program = ?program, %error, "failed to spawn Wayland client"), - } + let child = match command.spawn() { + Ok(child) => { + info!(program = ?program, pid = child.id(), "spawned Wayland client"); + Some(child) + } + Err(error) => { + warn!(program = ?program, %error, "failed to spawn Wayland client"); + None + } + }; drop(private_connection); + child +} + +fn terminate_child(child: &mut Child) { + if child.try_wait().ok().flatten().is_some() { + return; + } + + // SAFETY: the process ID comes from the live Child owned by Ferese. + unsafe { + libc::kill(child.id() as i32, libc::SIGTERM); + } + + for _ in 0..20 { + if child.try_wait().ok().flatten().is_some() { + return; + } + thread::sleep(Duration::from_millis(10)); + } + + let _ = child.kill(); + let _ = child.wait(); } diff --git a/compositor/src/winit.rs b/compositor/src/winit.rs index 8de867c..f2abcac 100644 --- a/compositor/src/winit.rs +++ b/compositor/src/winit.rs @@ -1371,13 +1371,15 @@ fn framebuffer_clip_rect( output_size: smithay::utils::Size, transform: Transform, ) -> [f32; 4] { - let transformed = transform.transform_rect_in(geometry, &output_size); - let transformed_output_size = transform.transform_size(output_size); - let bottom = transformed_output_size.h - transformed.loc.y - transformed.size.h; + // Match GlesFrame's projection: Smithay already accounts for GL's Y axis. + // An extra bottom-left conversion here mirrors shader clips independently + // of the surface geometry (notably with the nested Flipped180 output). + let element_size = transform.transform_size(output_size); + let transformed = transform.transform_rect_in(geometry, &element_size); [ transformed.loc.x as f32, - bottom as f32, + transformed.loc.y as f32, transformed.size.w as f32, transformed.size.h as f32, ] @@ -1861,12 +1863,12 @@ mod tests { } #[test] - fn clip_rect_uses_opengl_bottom_left_origin() { + fn clip_rect_matches_smithay_gles_projection() { let geometry = Rectangle::::new((10, 5).into(), (30, 40).into()); assert_eq!( framebuffer_clip_rect(geometry, (100, 80).into(), Transform::Normal), - [10.0, 35.0, 30.0, 40.0] + [10.0, 5.0, 30.0, 40.0] ); } @@ -1876,14 +1878,72 @@ mod tests { assert_eq!( framebuffer_clip_rect(geometry, (100, 80).into(), Transform::Flipped180), - [10.0, 5.0, 30.0, 40.0] + [10.0, 35.0, 30.0, 40.0] ); assert_eq!( framebuffer_clip_rect(geometry, (100, 80).into(), Transform::_180), - [60.0, 5.0, 30.0, 40.0] + [60.0, 35.0, 30.0, 40.0] ); } + #[test] + fn panel_clearance_clip_matches_gles_projection_at_fractional_scales() { + for scale in [1.0, 1.25, 1.75, 2.0] { + for transform in [ + Transform::Normal, + Transform::_90, + Transform::_180, + Transform::_270, + Transform::Flipped, + Transform::Flipped90, + Transform::Flipped180, + Transform::Flipped270, + ] { + let output_size = smithay::utils::Size::::from(( + (1280.0 * scale) as i32, + (800.0 * scale) as i32, + )); + let element_size = transform.transform_size(output_size); + let logical_height = f64::from(element_size.h) / scale; + // Bar and exclusion end at 40; the 10px outer gap ends at 50. + let geometry = Rectangle::::new( + (10.0, 50.0).into(), + (400.0, logical_height - 60.0).into(), + ) + .to_physical_precise_round(scale); + let clip = framebuffer_clip_rect(geometry, output_size, transform); + // Independently reproduce GlesFrame's orthographic projection, + // transform matrix, GL flip, and viewport mapping for each corner. + let matrix = transform.matrix(); + let mut xs = Vec::new(); + let mut ys = Vec::new(); + for x in [geometry.loc.x, geometry.loc.x + geometry.size.w] { + for y in [geometry.loc.y, geometry.loc.y + geometry.size.h] { + let nx = 2.0 * x as f32 / element_size.w as f32 - 1.0; + let ny = 1.0 - 2.0 * y as f32 / element_size.h as f32; + let tx = matrix[0][0] * nx + matrix[1][0] * ny; + let ty = matrix[0][1] * nx + matrix[1][1] * ny; + xs.push((tx + 1.0) * output_size.w as f32 / 2.0); + ys.push((1.0 - ty) * output_size.h as f32 / 2.0); + } + } + let min_x = xs.iter().copied().fold(f32::INFINITY, f32::min); + let max_x = xs.iter().copied().fold(f32::NEG_INFINITY, f32::max); + let min_y = ys.iter().copied().fold(f32::INFINITY, f32::min); + let max_y = ys.iter().copied().fold(f32::NEG_INFINITY, f32::max); + for (actual, expected) in + clip.into_iter() + .zip([min_x, min_y, max_x - min_x, max_y - min_y]) + { + assert!( + (actual - expected).abs() < 0.001, + "{transform:?}, scale={scale}: {actual} != {expected}" + ); + } + } + } + } + #[test] fn blur_sampling_expands_around_the_visible_surface() { let visible = Rectangle::::new((100, 80).into(), (400, 300).into()); diff --git a/shell/ferese-shell/Cargo.toml b/shell/ferese-shell/Cargo.toml new file mode 100644 index 0000000..1d659cc --- /dev/null +++ b/shell/ferese-shell/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "ferese-shell" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version = "1.93" + +[dependencies] +ferese-protocols = { path = "../../crates/ferese-protocols", default-features = false, features = ["client"] } +jiff.workspace = true +libcosmic.workspace = true +wayland-client.workspace = true diff --git a/shell/ferese-shell/src/control.rs b/shell/ferese-shell/src/control.rs new file mode 100644 index 0000000..c9cfb7e --- /dev/null +++ b/shell/ferese-shell/src/control.rs @@ -0,0 +1,254 @@ +use std::{ + error::Error, + os::{ + fd::{FromRawFd, RawFd}, + unix::net::UnixStream, + }, + sync::mpsc::{self, Receiver, Sender, TryRecvError}, + thread, +}; + +use ferese_protocols::shell::v1::client::{ + ferese_shell_manager_v1::FereseShellManagerV1, + ferese_shell_v1::{self, FereseShellV1}, +}; +use wayland_client::{ + Connection, Dispatch, QueueHandle, WEnum, delegate_noop, + globals::{GlobalListContents, registry_queue_init}, + protocol::wl_registry, +}; + +#[derive(Clone, Debug, Default)] +pub(crate) struct ShellSnapshot { + pub(crate) outputs: Vec, + pub(crate) workspaces: Vec, + pub(crate) windows: Vec, +} + +#[derive(Clone, Debug)] +pub(crate) struct OutputSnapshot { + pub(crate) id: u64, + pub(crate) name: String, + pub(crate) active_workspace: u64, + pub(crate) focused: bool, +} + +#[derive(Clone, Debug)] +pub(crate) struct WorkspaceSnapshot { + pub(crate) id: u64, + pub(crate) output: Option, + pub(crate) name: String, + pub(crate) active: bool, +} + +#[derive(Clone, Debug)] +pub(crate) struct WindowSnapshot { + pub(crate) workspace: u64, + pub(crate) app_id: String, + pub(crate) title: String, + pub(crate) focused: bool, +} + +pub(crate) struct ShellControl { + connection: Connection, + _manager: FereseShellManagerV1, + shell: FereseShellV1, + updates: Receiver, +} + +pub(crate) struct ControlPoll { + pub(crate) snapshot: Option, + pub(crate) disconnected: bool, +} + +enum ControlUpdate { + Snapshot(ShellSnapshot), + Disconnected, +} + +impl ShellControl { + pub(crate) fn connect() -> Result> { + let connection = control_connection()?; + let (globals, mut queue) = registry_queue_init::(&connection)?; + let qh = queue.handle(); + let manager = globals.bind::(&qh, 1..=1, ())?; + let shell = manager.get_shell(&qh, ()); + let (sender, updates) = mpsc::channel(); + let mut state = ControlState::new(sender); + + connection.flush()?; + thread::Builder::new() + .name("ferese-shell-control".to_owned()) + .spawn(move || { + while queue.blocking_dispatch(&mut state).is_ok() {} + let _ = state.sender.send(ControlUpdate::Disconnected); + })?; + + Ok(Self { + connection, + _manager: manager, + shell, + updates, + }) + } + + pub(crate) fn poll(&self) -> ControlPoll { + let mut poll = ControlPoll { + snapshot: None, + disconnected: false, + }; + + loop { + match self.updates.try_recv() { + Ok(ControlUpdate::Snapshot(snapshot)) => poll.snapshot = Some(snapshot), + Ok(ControlUpdate::Disconnected) | Err(TryRecvError::Disconnected) => { + poll.disconnected = true; + return poll; + } + Err(TryRecvError::Empty) => return poll, + } + } + } + + pub(crate) fn activate_workspace(&self, id: u64) { + let (hi, lo) = split_id(id); + + self.shell.activate_workspace(hi, lo); + let _ = self.connection.flush(); + } + + pub(crate) fn enter_overview(&self) { + self.shell.enter_overview(); + let _ = self.connection.flush(); + } +} + +fn control_connection() -> Result> { + let raw_fd = std::env::var_os("FERESE_SHELL_CONTROL_SOCKET") + .ok_or("Ferese did not provide a shell-control connection")?; + let fd = raw_fd + .to_str() + .ok_or("FERESE_SHELL_CONTROL_SOCKET is not valid UTF-8")? + .parse::()?; + // SAFETY: Ferese passes ownership of this inherited descriptor to the + // shell. This function is called once and consumes the descriptor. + let socket = unsafe { UnixStream::from_raw_fd(fd) }; + + Ok(Connection::from_socket(socket)?) +} + +struct ControlState { + sender: Sender, + pending: ShellSnapshot, + serial: Option, +} + +impl ControlState { + fn new(sender: Sender) -> Self { + Self { + sender, + pending: ShellSnapshot::default(), + serial: None, + } + } +} + +impl Dispatch for ControlState { + fn event( + _state: &mut Self, + _registry: &wl_registry::WlRegistry, + _event: wl_registry::Event, + _data: &GlobalListContents, + _connection: &Connection, + _qh: &QueueHandle, + ) { + } +} + +impl Dispatch for ControlState { + fn event( + state: &mut Self, + _shell: &FereseShellV1, + event: ferese_shell_v1::Event, + _data: &(), + _connection: &Connection, + _qh: &QueueHandle, + ) { + match event { + ferese_shell_v1::Event::SnapshotBegin { serial } => { + state.serial = Some(serial); + state.pending = ShellSnapshot::default(); + } + ferese_shell_v1::Event::Output { + output_hi, + output_lo, + name, + active_workspace_hi, + active_workspace_lo, + focused, + } => state.pending.outputs.push(OutputSnapshot { + id: join_id(output_hi, output_lo), + name, + active_workspace: join_id(active_workspace_hi, active_workspace_lo), + focused: focused != 0, + }), + ferese_shell_v1::Event::Workspace { + workspace_hi, + workspace_lo, + output_hi, + output_lo, + name, + active, + } => { + let output = join_id(output_hi, output_lo); + + state.pending.workspaces.push(WorkspaceSnapshot { + id: join_id(workspace_hi, workspace_lo), + output: (output != 0).then_some(output), + name, + active: active != 0, + }); + } + ferese_shell_v1::Event::Window { + window_hi: _, + window_lo: _, + workspace_hi, + workspace_lo, + app_id, + title, + state: window_state, + } => { + let focused = matches!( + window_state, + WEnum::Value(flags) + if flags.contains(ferese_shell_v1::WindowState::Focused) + ); + + state.pending.windows.push(WindowSnapshot { + workspace: join_id(workspace_hi, workspace_lo), + app_id, + title, + focused, + }); + } + ferese_shell_v1::Event::SnapshotEnd { serial } + if state.serial.take() == Some(serial) => + { + let _ = state + .sender + .send(ControlUpdate::Snapshot(state.pending.clone())); + } + _ => {} + } + } +} + +delegate_noop!(ControlState: ignore FereseShellManagerV1); + +fn split_id(id: u64) -> (u32, u32) { + ((id >> 32) as u32, id as u32) +} + +fn join_id(hi: u32, lo: u32) -> u64 { + (u64::from(hi) << 32) | u64::from(lo) +} diff --git a/shell/ferese-shell/src/main.rs b/shell/ferese-shell/src/main.rs new file mode 100644 index 0000000..53c6353 --- /dev/null +++ b/shell/ferese-shell/src/main.rs @@ -0,0 +1,371 @@ +mod control; + +use std::time::Duration; + +use cosmic::Element; +use cosmic::app::{Core, Settings, Task}; +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::{Anchor, KeyboardInteractivity, Layer}, +}; +use cosmic::iced::{ + Background, Border, Color, Event, Length, Shadow, Subscription, Vector, window, +}; +use cosmic::theme; +use cosmic::widget::{button, container, row, text}; +use ferese_protocols::effects::v1::client::{ + ferese_effects_manager_v1::FereseEffectsManagerV1, + ferese_surface_effects_v1::{self, FereseSurfaceEffectsV1}, +}; +use jiff::Zoned; +use wayland_client::{ + Connection, Dispatch, EventQueue, Proxy, QueueHandle, delegate_noop, + globals::{GlobalListContents, registry_queue_init}, + protocol::{wl_registry, wl_surface}, +}; + +use crate::control::{ShellControl, ShellSnapshot}; + +const APP_ID: &str = "dev.ferese.Shell"; +const BAR_HEIGHT: u32 = 32; +const BAR_MARGIN: i32 = 8; +const EXCLUSIVE_ZONE: i32 = BAR_HEIGHT as i32; + +fn main() -> cosmic::iced::Result { + let settings = Settings::default() + .no_main_window(true) + .client_decorations(false) + .transparent(true) + .is_daemon(true); + + cosmic::app::run::(settings, ()) +} + +struct FereseShell { + core: Core, + surface_id: window::Id, + control: Option, + snapshot: ShellSnapshot, + clock: String, + effects: Option, +} + +#[derive(Clone, Debug)] +enum Message { + Event(Event, window::Id), + Tick, + ActivateWorkspace(u64), + EnterOverview, +} + +impl cosmic::Application for FereseShell { + type Executor = cosmic::executor::Default; + type Flags = (); + type Message = Message; + + const APP_ID: &'static str = APP_ID; + + fn core(&self) -> &Core { + &self.core + } + + fn core_mut(&mut self) -> &mut Core { + &mut self.core + } + + fn init(core: Core, _flags: Self::Flags) -> (Self, Task) { + let surface_id = window::Id::unique(); + let app = Self { + core, + surface_id, + control: ShellControl::connect() + .map_err(|error| { + eprintln!("ferese-shell: shell control unavailable: {error}"); + }) + .ok(), + snapshot: ShellSnapshot::default(), + clock: current_time(), + effects: None, + }; + let action = cosmic::surface::action::app_layer_shell::( + |_| Default::default(), + move |_| SctkLayerSurfaceSettings { + id: surface_id, + layer: Layer::Top, + keyboard_interactivity: KeyboardInteractivity::None, + anchor: Anchor::TOP | Anchor::LEFT | Anchor::RIGHT, + output: IcedOutput::Active, + namespace: "ferese-shell-top-bar".to_owned(), + margin: IcedMargin { + top: BAR_MARGIN, + right: 10, + bottom: 0, + left: 10, + }, + size: Some((None, Some(BAR_HEIGHT))), + exclusive_zone: EXCLUSIVE_ZONE, + ..Default::default() + }, + Some(Box::new(Self::view_layer)), + ); + + (app, cosmic::task::message(cosmic::Action::Surface(action))) + } + + fn subscription(&self) -> Subscription { + Subscription::batch([ + event::listen_with(|event, _status, id| Some(Message::Event(event, id))), + cosmic::iced::time::every(Duration::from_millis(250)).map(|_| Message::Tick), + ]) + } + + fn update(&mut self, message: Self::Message) -> Task { + match message { + Message::Event(event, id) => self.handle_event(event, id), + Message::Tick => { + self.clock = current_time(); + if let Some(control) = &self.control { + let poll = control.poll(); + + if poll.disconnected { + return cosmic::iced::exit(); + } + if let Some(snapshot) = poll.snapshot { + self.snapshot = snapshot; + } + } + Task::none() + } + Message::ActivateWorkspace(id) => { + if let Some(control) = &self.control { + control.activate_workspace(id); + } + Task::none() + } + Message::EnterOverview => { + if let Some(control) = &self.control { + control.enter_overview(); + } + Task::none() + } + } + } + + fn view(&self) -> Element<'_, Self::Message> { + text("").into() + } + + fn view_window(&self, _id: window::Id) -> Element<'_, Self::Message> { + text("").into() + } +} + +impl FereseShell { + fn handle_event(&mut self, event: Event, id: window::Id) -> Task { + match event { + Event::Window(window::Event::Closed) if id == self.surface_id => cosmic::iced::exit(), + Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame( + _, + surface, + frame_id, + ))) if frame_id == self.surface_id => { + self.attach_effects(&surface); + Task::none() + } + Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Layer( + _, + surface, + layer_id, + ))) if layer_id == self.surface_id => { + self.attach_effects(&surface); + Task::none() + } + _ => Task::none(), + } + } + + fn attach_effects(&mut self, surface: &wl_surface::WlSurface) { + if self.effects.is_some() { + return; + } + + match EffectsBinding::attach(surface) { + Ok(binding) => self.effects = Some(binding), + Err(error) => eprintln!("ferese-shell: panel material unavailable: {error}"), + } + } + + fn view_layer(&self) -> Element<'_, cosmic::Action> { + let focused_output = self + .snapshot + .outputs + .iter() + .find(|output| output.focused) + .or_else(|| self.snapshot.outputs.first()); + let focused_output_id = focused_output.map(|output| output.id); + let mut workspace_row = row::with_capacity(self.snapshot.workspaces.len() + 1) + .spacing(2) + .align_y(cosmic::iced::Alignment::Center); + + workspace_row = workspace_row.push( + button::suggested("F") + .font_size(12) + .height(24) + .padding([2, 9]) + .on_press(cosmic::Action::App(Message::EnterOverview)), + ); + for workspace in self + .snapshot + .workspaces + .iter() + .filter(|workspace| workspace.output == focused_output_id) + { + let button = if workspace.active { + button::suggested(workspace.name.clone()) + } else { + button::text(workspace.name.clone()) + }; + + workspace_row = + workspace_row.push(button.font_size(12).height(24).padding([2, 8]).on_press( + cosmic::Action::App(Message::ActivateWorkspace(workspace.id)), + )); + } + + let app_name = self + .snapshot + .windows + .iter() + .find(|window| { + window.focused + && focused_output + .is_none_or(|output| window.workspace == output.active_workspace) + }) + .map(display_app_name) + .unwrap_or_else(|| "Desktop".to_owned()); + let left = row![workspace_row, text(app_name).size(12)] + .spacing(12) + .align_y(cosmic::iced::Alignment::Center); + let center = text(&self.clock).size(13); + let output_hint = focused_output + .map(|output| output.name.as_str()) + .unwrap_or("No display"); + let right = row![ + text("◌").size(16), + text("♪").size(15), + text(output_hint).size(11), + text("●").size(10), + ] + .spacing(10) + .align_y(cosmic::iced::Alignment::Center); + let content = row![ + container(left) + .width(Length::FillPortion(1)) + .align_x(alignment::Horizontal::Left), + container(center) + .width(Length::FillPortion(1)) + .align_x(alignment::Horizontal::Center), + container(right) + .width(Length::FillPortion(1)) + .align_x(alignment::Horizontal::Right), + ] + .align_y(cosmic::iced::Alignment::Center) + .height(Length::Fill); + + container(content) + .width(Length::Fill) + .height(Length::Fill) + .padding([0, 12]) + .class(theme::Container::custom(bar_style)) + .into() + } +} + +fn display_app_name(window: &control::WindowSnapshot) -> String { + let identity = window + .app_id + .rsplit('.') + .find(|part| !part.is_empty()) + .unwrap_or(&window.app_id); + + if identity.is_empty() { + window.title.clone() + } else { + let mut characters = identity.chars(); + characters + .next() + .map(|first| first.to_uppercase().collect::() + characters.as_str()) + .unwrap_or_else(|| "Desktop".to_owned()) + } +} + +fn current_time() -> String { + Zoned::now().strftime("%H:%M").to_string() +} + +fn bar_style(_theme: &cosmic::Theme) -> container::Style { + 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))), + border: Border { + color: Color::from_rgba8(255, 255, 255, 0.14), + width: 1.0, + radius: 16.0.into(), + }, + shadow: Shadow { + color: Color::from_rgba8(0, 0, 0, 0.32), + offset: Vector::new(0.0, 4.0), + blur_radius: 18.0, + }, + snap: true, + } +} + +struct EffectsBinding { + _manager: FereseEffectsManagerV1, + _surface: FereseSurfaceEffectsV1, + _queue: EventQueue, +} + +impl EffectsBinding { + fn attach(surface: &wl_surface::WlSurface) -> Result> { + let backend = surface + .backend() + .upgrade() + .ok_or("libcosmic Wayland connection is no longer alive")?; + let connection = Connection::from_backend(backend); + let (globals, queue) = registry_queue_init::(&connection)?; + let qh = queue.handle(); + let manager = globals.bind::(&qh, 1..=1, ())?; + let effects = manager.get_surface_effects(surface, &qh, ()); + + effects.set_role(ferese_surface_effects_v1::Role::Panel); + connection.flush()?; + + Ok(Self { + _manager: manager, + _surface: effects, + _queue: queue, + }) + } +} + +struct EffectsState; + +impl Dispatch for EffectsState { + fn event( + _state: &mut Self, + _registry: &wl_registry::WlRegistry, + _event: wl_registry::Event, + _data: &GlobalListContents, + _connection: &Connection, + _qh: &QueueHandle, + ) { + } +} + +delegate_noop!(EffectsState: ignore FereseEffectsManagerV1); +delegate_noop!(EffectsState: ignore FereseSurfaceEffectsV1); diff --git a/tools/ferese-shell-spike/Cargo.toml b/tools/ferese-shell-spike/Cargo.toml index ab1d199..c16c15e 100644 --- a/tools/ferese-shell-spike/Cargo.toml +++ b/tools/ferese-shell-spike/Cargo.toml @@ -7,5 +7,5 @@ rust-version = "1.93" [dependencies] ferese-protocols = { path = "../../crates/ferese-protocols", default-features = false, features = ["client"] } -libcosmic = { git = "https://github.com/pop-os/libcosmic", rev = "03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0", default-features = false, features = ["multi-window", "tokio", "wayland"] } +libcosmic.workspace = true wayland-client.workspace = true