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
@@ -1,12 +1,16 @@
|
|||||||
# Ferese
|
# Ferese
|
||||||
|
|
||||||
Ferese is an experimental Wayland scrolling compositor written in Rust with
|
Ferese is a Wayland scrolling compositor written in Rust with
|
||||||
Smithay. Scrolling columns are the default, tree tiling remains available as an
|
Smithay. Scrolling columns are the default, tree tiling remains available as an
|
||||||
optional per-workspace mode, and geometry changes use timestamp-driven
|
optional per-workspace mode, and geometry changes use timestamp-driven
|
||||||
animation. See the
|
animation. See the
|
||||||
[window-manager specification](docs/ferese-window-manager-spec.md) for the
|
[window-manager specification](docs/ferese-window-manager-spec.md) for the
|
||||||
current architecture and milestones.
|
current architecture and milestones.
|
||||||
|
|
||||||
|
For a versioned installation alongside Plasma in SDDM, see
|
||||||
|
[installation and recovery](docs/installation.md). `Super+Shift+E` or
|
||||||
|
`feresectl exit` logs out immediately; save your work first.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Install the native dependencies required by Smithay's winit and GLES backends,
|
Install the native dependencies required by Smithay's winit and GLES backends,
|
||||||
@@ -24,6 +28,10 @@ cargo run -p ferese -- foot
|
|||||||
|
|
||||||
Set `RUST_LOG=ferese=debug` for detailed compositor logging.
|
Set `RUST_LOG=ferese=debug` for detailed compositor logging.
|
||||||
|
|
||||||
|
`Super+F` toggles decorated maximization inside the workspace (bar and outer
|
||||||
|
gaps retained). `Super+Shift+F` toggles true fullscreen (no bar or window
|
||||||
|
decorations). Leaving fullscreen restores the previous maximized/tiled state.
|
||||||
|
|
||||||
Set `FERESE_TRACE_PERFORMANCE=1` to emit five-second per-output summaries under
|
Set `FERESE_TRACE_PERFORMANCE=1` to emit five-second per-output summaries under
|
||||||
the `ferese::render` tracing target. Summaries include rendered frames, damaged
|
the `ferese::render` tracing target. Summaries include rendered frames, damaged
|
||||||
pixels, average and longest render time, DRM missed-deadline totals, blur-active
|
pixels, average and longest render time, DRM missed-deadline totals, blur-active
|
||||||
@@ -76,6 +84,7 @@ them for a lone tiled window:
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[layout]
|
[layout]
|
||||||
|
outer_gap = 4.0
|
||||||
smart_gaps = true
|
smart_gaps = true
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -245,7 +254,7 @@ cargo run -p feresectl -- get-outputs
|
|||||||
```
|
```
|
||||||
|
|
||||||
The v0 IPC slice also supports `move`, `resize`, `move-to-workspace`,
|
The v0 IPC slice also supports `move`, `resize`, `move-to-workspace`,
|
||||||
`toggle-floating`, `toggle-fullscreen`, `toggle-layout`, `cycle-column-width`,
|
`toggle-floating`, `toggle-maximized`, `toggle-fullscreen`, `toggle-layout`, `cycle-column-width`,
|
||||||
`center-column`, `consume`, `expel`, `close`, `get-focused-window`, and
|
`center-column`, `consume`, `expel`, `close`, `get-focused-window`, and
|
||||||
`get-outputs`. Requests use versioned, 1 MiB-limited length-prefixed JSON. The
|
`get-outputs`. Requests use versioned, 1 MiB-limited length-prefixed JSON. The
|
||||||
runtime directory and socket are restricted to modes `0700` and `0600`, and the
|
runtime directory and socket are restricted to modes `0700` and `0600`, and the
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ pub struct ThemeSettings {
|
|||||||
pub border_color: RgbaColor,
|
pub border_color: RgbaColor,
|
||||||
pub accent_color: RgbaColor,
|
pub accent_color: RgbaColor,
|
||||||
pub shadow_color: RgbaColor,
|
pub shadow_color: RgbaColor,
|
||||||
|
pub surface_base_color: RgbaColor,
|
||||||
pub window_radius: f64,
|
pub window_radius: f64,
|
||||||
pub shadow_offset_y: f64,
|
pub shadow_offset_y: f64,
|
||||||
pub shadow_blur: f64,
|
pub shadow_blur: f64,
|
||||||
@@ -146,6 +147,8 @@ struct ThemeMaterialConfig {
|
|||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct ThemeColorsConfig {
|
struct ThemeColorsConfig {
|
||||||
|
#[serde(default = "default_surface_base_color")]
|
||||||
|
surface_base: String,
|
||||||
#[serde(default = "default_border_color")]
|
#[serde(default = "default_border_color")]
|
||||||
border: String,
|
border: String,
|
||||||
#[serde(default = "default_accent_color")]
|
#[serde(default = "default_accent_color")]
|
||||||
@@ -157,6 +160,7 @@ struct ThemeColorsConfig {
|
|||||||
impl Default for ThemeColorsConfig {
|
impl Default for ThemeColorsConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
surface_base: default_surface_base_color(),
|
||||||
border: default_border_color(),
|
border: default_border_color(),
|
||||||
accent: default_accent_color(),
|
accent: default_accent_color(),
|
||||||
shadow: default_shadow_color(),
|
shadow: default_shadow_color(),
|
||||||
@@ -235,12 +239,14 @@ struct BindingModifiers {
|
|||||||
pub enum BindingAction {
|
pub enum BindingAction {
|
||||||
Spawn(Vec<String>),
|
Spawn(Vec<String>),
|
||||||
Close,
|
Close,
|
||||||
|
Exit,
|
||||||
Focus(ferese_layout::Direction),
|
Focus(ferese_layout::Direction),
|
||||||
Move(ferese_layout::Direction),
|
Move(ferese_layout::Direction),
|
||||||
Resize(ferese_layout::Direction),
|
Resize(ferese_layout::Direction),
|
||||||
SwitchWorkspace(u8),
|
SwitchWorkspace(u8),
|
||||||
MoveToWorkspace(u8),
|
MoveToWorkspace(u8),
|
||||||
ToggleFullscreen,
|
ToggleFullscreen,
|
||||||
|
ToggleMaximized,
|
||||||
ToggleLayout,
|
ToggleLayout,
|
||||||
CycleColumnWidth,
|
CycleColumnWidth,
|
||||||
CenterColumn,
|
CenterColumn,
|
||||||
@@ -803,6 +809,10 @@ impl Config {
|
|||||||
border_color: parse_color(&self.theme.colors.border, "colors.border")?,
|
border_color: parse_color(&self.theme.colors.border, "colors.border")?,
|
||||||
accent_color: parse_color(&self.theme.colors.accent, "colors.accent")?,
|
accent_color: parse_color(&self.theme.colors.accent, "colors.accent")?,
|
||||||
shadow_color: parse_color(&self.theme.colors.shadow, "colors.shadow")?,
|
shadow_color: parse_color(&self.theme.colors.shadow, "colors.shadow")?,
|
||||||
|
surface_base_color: parse_color(
|
||||||
|
&self.theme.colors.surface_base,
|
||||||
|
"colors.surface_base",
|
||||||
|
)?,
|
||||||
window_radius,
|
window_radius,
|
||||||
shadow_offset_y,
|
shadow_offset_y,
|
||||||
shadow_blur,
|
shadow_blur,
|
||||||
@@ -1059,6 +1069,10 @@ fn parse_action(
|
|||||||
no_argument()?;
|
no_argument()?;
|
||||||
Ok(BindingAction::Close)
|
Ok(BindingAction::Close)
|
||||||
}
|
}
|
||||||
|
"exit" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::Exit)
|
||||||
|
}
|
||||||
"focus" => Ok(BindingAction::Focus(parse_direction(required_argument()?)?)),
|
"focus" => Ok(BindingAction::Focus(parse_direction(required_argument()?)?)),
|
||||||
"move" => Ok(BindingAction::Move(parse_direction(required_argument()?)?)),
|
"move" => Ok(BindingAction::Move(parse_direction(required_argument()?)?)),
|
||||||
"resize" => Ok(BindingAction::Resize(
|
"resize" => Ok(BindingAction::Resize(
|
||||||
@@ -1070,6 +1084,10 @@ fn parse_action(
|
|||||||
"move-to-workspace" => Ok(BindingAction::MoveToWorkspace(parse_workspace(
|
"move-to-workspace" => Ok(BindingAction::MoveToWorkspace(parse_workspace(
|
||||||
required_argument()?,
|
required_argument()?,
|
||||||
)?)),
|
)?)),
|
||||||
|
"toggle-maximized" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::ToggleMaximized)
|
||||||
|
}
|
||||||
"toggle-fullscreen" => {
|
"toggle-fullscreen" => {
|
||||||
no_argument()?;
|
no_argument()?;
|
||||||
Ok(BindingAction::ToggleFullscreen)
|
Ok(BindingAction::ToggleFullscreen)
|
||||||
@@ -1133,7 +1151,8 @@ fn default_bindings() -> Vec<BindingConfig> {
|
|||||||
let mut bindings = vec![
|
let mut bindings = vec![
|
||||||
binding("Super+Enter", "spawn", Some("terminal")),
|
binding("Super+Enter", "spawn", Some("terminal")),
|
||||||
binding("Super+Q", "close", None),
|
binding("Super+Q", "close", None),
|
||||||
binding("Super+F", "toggle-fullscreen", None),
|
binding("Super+F", "toggle-maximized", None),
|
||||||
|
binding("Super+Shift+F", "toggle-fullscreen", None),
|
||||||
binding("Super+M", "toggle-layout", None),
|
binding("Super+M", "toggle-layout", None),
|
||||||
binding("Super+R", "cycle-column-width", None),
|
binding("Super+R", "cycle-column-width", None),
|
||||||
binding("Super+C", "center-column", None),
|
binding("Super+C", "center-column", None),
|
||||||
@@ -1141,6 +1160,7 @@ fn default_bindings() -> Vec<BindingConfig> {
|
|||||||
binding("Super+]", "expel", None),
|
binding("Super+]", "expel", None),
|
||||||
binding("Super+Shift+Space", "toggle-floating", None),
|
binding("Super+Shift+Space", "toggle-floating", None),
|
||||||
binding("Super+Tab", "toggle-overview", None),
|
binding("Super+Tab", "toggle-overview", None),
|
||||||
|
binding("Super+Shift+E", "exit", None),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (key, direction) in [("H", "left"), ("J", "down"), ("K", "up"), ("L", "right")] {
|
for (key, direction) in [("H", "left"), ("J", "down"), ("K", "up"), ("L", "right")] {
|
||||||
@@ -1283,13 +1303,17 @@ const fn default_inner_gap() -> f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fn default_outer_gap() -> f64 {
|
const fn default_outer_gap() -> f64 {
|
||||||
10.0
|
4.0
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_border_color() -> String {
|
fn default_border_color() -> String {
|
||||||
"#FFFFFF18".to_owned()
|
"#FFFFFF18".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_surface_base_color() -> String {
|
||||||
|
"#111821".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
fn default_accent_color() -> String {
|
fn default_accent_color() -> String {
|
||||||
"#5B8CFF".to_owned()
|
"#5B8CFF".to_owned()
|
||||||
}
|
}
|
||||||
@@ -1574,6 +1598,7 @@ mod tests {
|
|||||||
border_color: RgbaColor([17.0 / 255.0, 34.0 / 255.0, 51.0 / 255.0, 68.0 / 255.0,]),
|
border_color: RgbaColor([17.0 / 255.0, 34.0 / 255.0, 51.0 / 255.0, 68.0 / 255.0,]),
|
||||||
accent_color: RgbaColor([170.0 / 255.0, 187.0 / 255.0, 0.8, 1.0]),
|
accent_color: RgbaColor([170.0 / 255.0, 187.0 / 255.0, 0.8, 1.0]),
|
||||||
shadow_color: RgbaColor([1.0 / 255.0, 2.0 / 255.0, 3.0 / 255.0, 128.0 / 255.0]),
|
shadow_color: RgbaColor([1.0 / 255.0, 2.0 / 255.0, 3.0 / 255.0, 128.0 / 255.0]),
|
||||||
|
surface_base_color: RgbaColor([17.0 / 255.0, 24.0 / 255.0, 33.0 / 255.0, 1.0,]),
|
||||||
window_radius: 12.0,
|
window_radius: 12.0,
|
||||||
shadow_offset_y: -2.0,
|
shadow_offset_y: -2.0,
|
||||||
shadow_blur: 24.0,
|
shadow_blur: 24.0,
|
||||||
@@ -1628,7 +1653,21 @@ mod tests {
|
|||||||
let input = config.input_settings().unwrap();
|
let input = config.input_settings().unwrap();
|
||||||
let bindings = config.bindings(&input).unwrap();
|
let bindings = config.bindings(&input).unwrap();
|
||||||
|
|
||||||
assert_eq!(bindings.len(), 40);
|
assert_eq!(bindings.len(), 42);
|
||||||
|
for (shift, action) in [
|
||||||
|
(false, BindingAction::ToggleMaximized),
|
||||||
|
(true, BindingAction::ToggleFullscreen),
|
||||||
|
] {
|
||||||
|
assert!(bindings.iter().any(|binding| binding.modifiers.logo
|
||||||
|
&& binding.modifiers.shift == shift
|
||||||
|
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_f)
|
||||||
|
&& binding.action == action));
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
bindings
|
||||||
|
.iter()
|
||||||
|
.any(|binding| binding.action == BindingAction::Exit)
|
||||||
|
);
|
||||||
assert!(bindings.iter().any(|binding| {
|
assert!(bindings.iter().any(|binding| {
|
||||||
binding.modifiers.logo
|
binding.modifiers.logo
|
||||||
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Return)
|
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Return)
|
||||||
@@ -1650,7 +1689,7 @@ mod tests {
|
|||||||
|
|
||||||
let input = replaced.input_settings().unwrap();
|
let input = replaced.input_settings().unwrap();
|
||||||
let bindings = replaced.bindings(&input).unwrap();
|
let bindings = replaced.bindings(&input).unwrap();
|
||||||
assert_eq!(bindings.len(), 40);
|
assert_eq!(bindings.len(), 42);
|
||||||
assert!(bindings.iter().any(|binding| {
|
assert!(bindings.iter().any(|binding| {
|
||||||
binding.action
|
binding.action
|
||||||
== BindingAction::Spawn(vec![
|
== BindingAction::Spawn(vec![
|
||||||
@@ -1662,7 +1701,7 @@ mod tests {
|
|||||||
|
|
||||||
let input = unbound.input_settings().unwrap();
|
let input = unbound.input_settings().unwrap();
|
||||||
let bindings = unbound.bindings(&input).unwrap();
|
let bindings = unbound.bindings(&input).unwrap();
|
||||||
assert_eq!(bindings.len(), 39);
|
assert_eq!(bindings.len(), 41);
|
||||||
assert!(
|
assert!(
|
||||||
!bindings
|
!bindings
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -198,6 +198,28 @@ impl XdgShellHandler for Ferese {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn maximize_request(&mut self, surface: ToplevelSurface) {
|
||||||
|
if let Some(id) = self.window_ids.iter().find_map(|(window, id)| {
|
||||||
|
window
|
||||||
|
.toplevel()
|
||||||
|
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
|
||||||
|
.then_some(*id)
|
||||||
|
}) {
|
||||||
|
self.set_window_maximized(id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unmaximize_request(&mut self, surface: ToplevelSurface) {
|
||||||
|
if let Some(id) = self.window_ids.iter().find_map(|(window, id)| {
|
||||||
|
window
|
||||||
|
.toplevel()
|
||||||
|
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
|
||||||
|
.then_some(*id)
|
||||||
|
}) {
|
||||||
|
self.set_window_maximized(id, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn fullscreen_request(
|
fn fullscreen_request(
|
||||||
&mut self,
|
&mut self,
|
||||||
surface: ToplevelSurface,
|
surface: ToplevelSurface,
|
||||||
|
|||||||
+45
-6
@@ -44,6 +44,14 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let symbol = keysym.modified_sym().raw();
|
let symbol = keysym.modified_sym().raw();
|
||||||
|
if overview_escape(symbol, data.overview.is_active()) {
|
||||||
|
if state == KeyState::Pressed {
|
||||||
|
data.intercepted_keys.insert(keycode);
|
||||||
|
data.set_overview_active(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FilterResult::Intercept(());
|
||||||
|
}
|
||||||
if emergency_shortcut_escape(symbol, modifiers.ctrl, modifiers.alt) {
|
if emergency_shortcut_escape(symbol, modifiers.ctrl, modifiers.alt) {
|
||||||
if state == KeyState::Pressed {
|
if state == KeyState::Pressed {
|
||||||
data.intercepted_keys.insert(keycode);
|
data.intercepted_keys.insert(keycode);
|
||||||
@@ -168,11 +176,29 @@ impl Ferese {
|
|||||||
let serial = SERIAL_COUNTER.next_serial();
|
let serial = SERIAL_COUNTER.next_serial();
|
||||||
|
|
||||||
if self.overview.is_active() {
|
if self.overview.is_active() {
|
||||||
if event.state() == ButtonState::Pressed
|
let position = pointer.current_location();
|
||||||
&& let Some(window) = self.window_under_visual(pointer.current_location())
|
if self.layer_under(position).is_some() {
|
||||||
&& let Some(id) = self.window_ids.get(&window).copied()
|
pointer.button(
|
||||||
{
|
self,
|
||||||
self.select_overview_window(id);
|
&ButtonEvent {
|
||||||
|
button: event.button_code(),
|
||||||
|
state: event.state(),
|
||||||
|
serial,
|
||||||
|
time: event.time() as u32,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
pointer.frame(self);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.state() == ButtonState::Pressed {
|
||||||
|
if let Some(window) = self.window_under_visual(position)
|
||||||
|
&& let Some(id) = self.window_ids.get(&window).copied()
|
||||||
|
{
|
||||||
|
self.select_overview_window(id);
|
||||||
|
} else {
|
||||||
|
self.set_overview_active(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -465,6 +491,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
BindingAction::Close => self.close_focused_window(),
|
BindingAction::Close => self.close_focused_window(),
|
||||||
|
BindingAction::Exit => self.loop_signal.stop(),
|
||||||
BindingAction::Focus(direction) => self.focus_direction(direction),
|
BindingAction::Focus(direction) => self.focus_direction(direction),
|
||||||
BindingAction::Move(direction) => self.move_direction(direction),
|
BindingAction::Move(direction) => self.move_direction(direction),
|
||||||
BindingAction::Resize(direction) => self.resize_direction(direction),
|
BindingAction::Resize(direction) => self.resize_direction(direction),
|
||||||
@@ -475,6 +502,7 @@ impl Ferese {
|
|||||||
self.move_focused_to_workspace(u32::from(workspace));
|
self.move_focused_to_workspace(u32::from(workspace));
|
||||||
}
|
}
|
||||||
BindingAction::ToggleFullscreen => self.toggle_focused_fullscreen(),
|
BindingAction::ToggleFullscreen => self.toggle_focused_fullscreen(),
|
||||||
|
BindingAction::ToggleMaximized => self.toggle_focused_maximized(),
|
||||||
BindingAction::ToggleLayout => self.toggle_layout_mode(),
|
BindingAction::ToggleLayout => self.toggle_layout_mode(),
|
||||||
BindingAction::CycleColumnWidth => self.cycle_focused_column_width(),
|
BindingAction::CycleColumnWidth => self.cycle_focused_column_width(),
|
||||||
BindingAction::CenterColumn => self.center_focused_column(),
|
BindingAction::CenterColumn => self.center_focused_column(),
|
||||||
@@ -503,9 +531,20 @@ fn emergency_shortcut_escape(symbol: u32, ctrl: bool, alt: bool) -> bool {
|
|||||||
ctrl && alt && symbol == keysyms::KEY_Escape
|
ctrl && alt && symbol == keysyms::KEY_Escape
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn overview_escape(symbol: u32, overview_active: bool) -> bool {
|
||||||
|
overview_active && symbol == keysyms::KEY_Escape
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{emergency_shortcut_escape, virtual_terminal};
|
use super::{emergency_shortcut_escape, overview_escape, virtual_terminal};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_closes_only_an_active_overview() {
|
||||||
|
assert!(overview_escape(keysyms::KEY_Escape, true));
|
||||||
|
assert!(!overview_escape(keysyms::KEY_Escape, false));
|
||||||
|
assert!(!overview_escape(keysyms::KEY_q, true));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn emergency_escape_requires_control_alt_escape() {
|
fn emergency_escape_requires_control_alt_escape() {
|
||||||
|
|||||||
+29
-7
@@ -16,7 +16,7 @@ use ferese_core::LayoutMode;
|
|||||||
use ferese_ipc::{Request, Response, VERSION, read_frame, write_frame};
|
use ferese_ipc::{Request, Response, VERSION, read_frame, write_frame};
|
||||||
use ferese_layout::Direction;
|
use ferese_layout::Direction;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use smithay::reexports::calloop::{EventLoop, channel};
|
use smithay::reexports::calloop::{EventLoop, LoopSignal, channel};
|
||||||
use smithay::utils::Transform;
|
use smithay::utils::Transform;
|
||||||
|
|
||||||
use crate::{Ferese, config::OutputTransform};
|
use crate::{Ferese, config::OutputTransform};
|
||||||
@@ -84,15 +84,20 @@ pub(crate) fn init(
|
|||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let signal = event_loop.get_signal();
|
||||||
thread::Builder::new()
|
thread::Builder::new()
|
||||||
.name("ferese-ipc-listener".to_owned())
|
.name("ferese-ipc-listener".to_owned())
|
||||||
.spawn(move || accept_connections(listener, sender))?;
|
.spawn(move || accept_connections(listener, sender, signal))?;
|
||||||
|
|
||||||
tracing::info!(path = %path.display(), "Ferese IPC is accepting connections");
|
tracing::info!(path = %path.display(), "Ferese IPC is accepting connections");
|
||||||
Ok(guard)
|
Ok(guard)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accept_connections(listener: UnixListener, sender: channel::SyncSender<IpcCall>) {
|
fn accept_connections(
|
||||||
|
listener: UnixListener,
|
||||||
|
sender: channel::SyncSender<IpcCall>,
|
||||||
|
signal: LoopSignal,
|
||||||
|
) {
|
||||||
let active_connections = Arc::new(AtomicUsize::new(0));
|
let active_connections = Arc::new(AtomicUsize::new(0));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -125,11 +130,12 @@ fn accept_connections(listener: UnixListener, sender: channel::SyncSender<IpcCal
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let sender = sender.clone();
|
let sender = sender.clone();
|
||||||
|
let signal = signal.clone();
|
||||||
if let Err(error) = thread::Builder::new()
|
if let Err(error) = thread::Builder::new()
|
||||||
.name("ferese-ipc-client".to_owned())
|
.name("ferese-ipc-client".to_owned())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
let _permit = permit;
|
let _permit = permit;
|
||||||
serve_connection(stream, sender);
|
serve_connection(stream, sender, signal);
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
tracing::warn!(%error, "could not start IPC connection worker");
|
tracing::warn!(%error, "could not start IPC connection worker");
|
||||||
@@ -154,9 +160,13 @@ fn try_acquire_connection(active: &Arc<AtomicUsize>) -> Option<ConnectionPermit>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serve_connection(mut stream: UnixStream, sender: channel::SyncSender<IpcCall>) {
|
fn serve_connection(
|
||||||
|
mut stream: UnixStream,
|
||||||
|
sender: channel::SyncSender<IpcCall>,
|
||||||
|
signal: LoopSignal,
|
||||||
|
) {
|
||||||
loop {
|
loop {
|
||||||
let request = match read_frame(&mut stream) {
|
let request: Request = match read_frame(&mut stream) {
|
||||||
Ok(request) => request,
|
Ok(request) => request,
|
||||||
Err(ferese_ipc::FrameError::Io(error))
|
Err(ferese_ipc::FrameError::Io(error))
|
||||||
if matches!(
|
if matches!(
|
||||||
@@ -174,6 +184,7 @@ fn serve_connection(mut stream: UnixStream, sender: channel::SyncSender<IpcCall>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let (response, receiver) = sync_channel(1);
|
let (response, receiver) = sync_channel(1);
|
||||||
|
let exit_requested = request.command == "exit";
|
||||||
let call = IpcCall { request, response };
|
let call = IpcCall { request, response };
|
||||||
if sender.try_send(call).is_err() {
|
if sender.try_send(call).is_err() {
|
||||||
tracing::warn!("disconnecting IPC client because the request queue is full");
|
tracing::warn!("disconnecting IPC client because the request queue is full");
|
||||||
@@ -182,10 +193,17 @@ fn serve_connection(mut stream: UnixStream, sender: channel::SyncSender<IpcCall>
|
|||||||
let Ok(response) = receiver.recv() else {
|
let Ok(response) = receiver.recv() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let exit_accepted = exit_requested && response.error.is_none();
|
||||||
if let Err(error) = write_frame(&mut stream, &response) {
|
if let Err(error) = write_frame(&mut stream, &response) {
|
||||||
tracing::debug!(%error, "IPC client disconnected before receiving its response");
|
tracing::debug!(%error, "IPC client disconnected before receiving its response");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if exit_accepted {
|
||||||
|
// Acknowledge before stopping so feresectl never races process exit.
|
||||||
|
signal.stop();
|
||||||
|
signal.wakeup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +221,7 @@ impl Ferese {
|
|||||||
|
|
||||||
fn dispatch_ipc_command(&mut self, command: &str, args: &Value) -> Result<Value, CommandError> {
|
fn dispatch_ipc_command(&mut self, command: &str, args: &Value) -> Result<Value, CommandError> {
|
||||||
match command {
|
match command {
|
||||||
|
"exit" => {} // The IPC worker stops the loop after writing the response.
|
||||||
"focus" => self.focus_direction(direction_arg(args)?),
|
"focus" => self.focus_direction(direction_arg(args)?),
|
||||||
"move" => self.move_direction(direction_arg(args)?),
|
"move" => self.move_direction(direction_arg(args)?),
|
||||||
"resize" => self.resize_direction(direction_arg(args)?),
|
"resize" => self.resize_direction(direction_arg(args)?),
|
||||||
@@ -210,6 +229,7 @@ impl Ferese {
|
|||||||
"move-to-workspace" => self.move_focused_to_workspace(workspace_arg(args)?),
|
"move-to-workspace" => self.move_focused_to_workspace(workspace_arg(args)?),
|
||||||
"toggle-floating" => self.toggle_focused_floating(),
|
"toggle-floating" => self.toggle_focused_floating(),
|
||||||
"toggle-fullscreen" => self.toggle_focused_fullscreen(),
|
"toggle-fullscreen" => self.toggle_focused_fullscreen(),
|
||||||
|
"toggle-maximized" => self.toggle_focused_maximized(),
|
||||||
"toggle-layout" => self.toggle_layout_mode(),
|
"toggle-layout" => self.toggle_layout_mode(),
|
||||||
"cycle-column-width" => self.cycle_focused_column_width(),
|
"cycle-column-width" => self.cycle_focused_column_width(),
|
||||||
"center-column" => self.center_focused_column(),
|
"center-column" => self.center_focused_column(),
|
||||||
@@ -608,7 +628,9 @@ mod tests {
|
|||||||
fn malformed_ipc_connection_is_closed_without_reaching_the_request_queue() {
|
fn malformed_ipc_connection_is_closed_without_reaching_the_request_queue() {
|
||||||
let (mut client, server) = UnixStream::pair().unwrap();
|
let (mut client, server) = UnixStream::pair().unwrap();
|
||||||
let (sender, _receiver) = channel::sync_channel(1);
|
let (sender, _receiver) = channel::sync_channel(1);
|
||||||
let worker = thread::spawn(move || serve_connection(server, sender));
|
let event_loop = EventLoop::<()>::try_new().unwrap();
|
||||||
|
let signal = event_loop.get_signal();
|
||||||
|
let worker = thread::spawn(move || serve_connection(server, sender, signal));
|
||||||
let invalid = b"not-json";
|
let invalid = b"not-json";
|
||||||
|
|
||||||
client
|
client
|
||||||
|
|||||||
+93
-6
@@ -6,7 +6,7 @@ use std::{
|
|||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use ferese_animation::{AnimatedValue, ClientSize, SpringConfig, WindowGeometry};
|
use ferese_animation::{AnimatedValue, ClientSize, PresentationMode, SpringConfig, WindowGeometry};
|
||||||
use ferese_core::{
|
use ferese_core::{
|
||||||
LayoutMode, OutputGeometry, OutputId, OutputWorkspaceMap, WindowPlacement, WorkspaceId,
|
LayoutMode, OutputGeometry, OutputId, OutputWorkspaceMap, WindowPlacement, WorkspaceId,
|
||||||
WorkspaceSet,
|
WorkspaceSet,
|
||||||
@@ -118,6 +118,7 @@ pub struct Ferese {
|
|||||||
output_identity_ids: HashMap<String, OutputId>,
|
output_identity_ids: HashMap<String, OutputId>,
|
||||||
pub window_ids: HashMap<Window, WindowId>,
|
pub window_ids: HashMap<Window, WindowId>,
|
||||||
pub window_geometry: HashMap<WindowId, WindowGeometry>,
|
pub window_geometry: HashMap<WindowId, WindowGeometry>,
|
||||||
|
maximized_windows: HashSet<WindowId>,
|
||||||
window_stack: crate::stacking::WindowStack,
|
window_stack: crate::stacking::WindowStack,
|
||||||
pub(crate) window_borders: HashMap<WindowId, crate::winit::WindowBorderBuffers>,
|
pub(crate) window_borders: HashMap<WindowId, crate::winit::WindowBorderBuffers>,
|
||||||
pub(crate) window_shadows: HashMap<WindowId, crate::winit::WindowShadowBuffers>,
|
pub(crate) window_shadows: HashMap<WindowId, crate::winit::WindowShadowBuffers>,
|
||||||
@@ -446,6 +447,7 @@ impl Ferese {
|
|||||||
output_identity_ids: HashMap::new(),
|
output_identity_ids: HashMap::new(),
|
||||||
window_ids: HashMap::new(),
|
window_ids: HashMap::new(),
|
||||||
window_geometry: HashMap::new(),
|
window_geometry: HashMap::new(),
|
||||||
|
maximized_windows: HashSet::new(),
|
||||||
window_stack: crate::stacking::WindowStack::default(),
|
window_stack: crate::stacking::WindowStack::default(),
|
||||||
window_borders: HashMap::new(),
|
window_borders: HashMap::new(),
|
||||||
window_shadows: HashMap::new(),
|
window_shadows: HashMap::new(),
|
||||||
@@ -619,6 +621,9 @@ impl Ferese {
|
|||||||
let map = layer_map_for_output(output);
|
let map = layer_map_for_output(output);
|
||||||
|
|
||||||
for requested in layers {
|
for requested in layers {
|
||||||
|
if *requested == Layer::Top && self.output_has_fullscreen(output) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
for layer in map.layers_on(*requested).rev() {
|
for layer in map.layers_on(*requested).rev() {
|
||||||
let geometry = map.layer_geometry(layer)?;
|
let geometry = map.layer_geometry(layer)?;
|
||||||
let layer_position = output_position - geometry.loc.to_f64();
|
let layer_position = output_position - geometry.loc.to_f64();
|
||||||
@@ -901,6 +906,7 @@ impl Ferese {
|
|||||||
|
|
||||||
self.space.unmap_elem(window);
|
self.space.unmap_elem(window);
|
||||||
self.window_geometry.remove(&id);
|
self.window_geometry.remove(&id);
|
||||||
|
self.maximized_windows.remove(&id);
|
||||||
self.window_stack.remove(id);
|
self.window_stack.remove(id);
|
||||||
self.window_borders.remove(&id);
|
self.window_borders.remove(&id);
|
||||||
self.window_shadows.remove(&id);
|
self.window_shadows.remove(&id);
|
||||||
@@ -996,8 +1002,12 @@ impl Ferese {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let is_maximized =
|
||||||
|
self.maximized_windows.contains(id) && workspace_fullscreen != Some(*id);
|
||||||
let rect = if workspace_fullscreen == Some(*id) {
|
let rect = if workspace_fullscreen == Some(*id) {
|
||||||
fullscreen_bounds
|
fullscreen_bounds
|
||||||
|
} else if is_maximized {
|
||||||
|
maximized_rect(bounds, self.gap_config.outer)
|
||||||
} else {
|
} else {
|
||||||
match self.workspaces.placement(*id) {
|
match self.workspaces.placement(*id) {
|
||||||
Some(WindowPlacement::Tiled) => {
|
Some(WindowPlacement::Tiled) => {
|
||||||
@@ -1017,7 +1027,7 @@ impl Ferese {
|
|||||||
);
|
);
|
||||||
|
|
||||||
visible.insert(*id);
|
visible.insert(*id);
|
||||||
let scrolling = if !is_fullscreen && !is_floating {
|
let scrolling = if !is_fullscreen && !is_maximized && !is_floating {
|
||||||
viewport_target
|
viewport_target
|
||||||
.zip(viewport_current)
|
.zip(viewport_current)
|
||||||
.map(|(target, current)| (workspace_id, rect.x + target, current))
|
.map(|(target, current)| (workspace_id, rect.x + target, current))
|
||||||
@@ -1030,6 +1040,7 @@ impl Ferese {
|
|||||||
*id,
|
*id,
|
||||||
rect,
|
rect,
|
||||||
is_fullscreen,
|
is_fullscreen,
|
||||||
|
is_maximized,
|
||||||
is_floating,
|
is_floating,
|
||||||
scrolling,
|
scrolling,
|
||||||
pending_column_width_cycles.contains(id) && viewport_target_changed,
|
pending_column_width_cycles.contains(id) && viewport_target_changed,
|
||||||
@@ -1053,7 +1064,9 @@ impl Ferese {
|
|||||||
let mut scrolling_world_x = HashMap::new();
|
let mut scrolling_world_x = HashMap::new();
|
||||||
placements.sort_by_key(|(_, id, ..)| self.window_stack.rank(*id));
|
placements.sort_by_key(|(_, id, ..)| self.window_stack.rank(*id));
|
||||||
|
|
||||||
for (window, id, rect, is_fullscreen, is_floating, scrolling, couple_width) in placements {
|
for (window, id, rect, is_fullscreen, is_maximized, is_floating, scrolling, couple_width) in
|
||||||
|
placements
|
||||||
|
{
|
||||||
// A viewport-coupled width must never override fullscreen/floating geometry.
|
// A viewport-coupled width must never override fullscreen/floating geometry.
|
||||||
if scrolling.is_none() {
|
if scrolling.is_none() {
|
||||||
self.viewport_coupled_widths.remove(&id);
|
self.viewport_coupled_widths.remove(&id);
|
||||||
@@ -1064,7 +1077,14 @@ impl Ferese {
|
|||||||
.window_geometry
|
.window_geometry
|
||||||
.entry(id)
|
.entry(id)
|
||||||
.or_insert_with(|| WindowGeometry::new(rect, committed_size));
|
.or_insert_with(|| WindowGeometry::new(rect, committed_size));
|
||||||
let mut requested_size = geometry.set_presentation_target(rect, is_fullscreen, now);
|
let mode = if is_fullscreen {
|
||||||
|
PresentationMode::Fullscreen
|
||||||
|
} else if is_maximized {
|
||||||
|
PresentationMode::Maximized
|
||||||
|
} else {
|
||||||
|
PresentationMode::Normal
|
||||||
|
};
|
||||||
|
let mut requested_size = geometry.set_presentation_mode(rect, mode, now);
|
||||||
if !self.animations_enabled {
|
if !self.animations_enabled {
|
||||||
geometry.advance(Duration::ZERO, self.spring_config, false);
|
geometry.advance(Duration::ZERO, self.spring_config, false);
|
||||||
requested_size = geometry.presentation_size_request(now).or(requested_size);
|
requested_size = geometry.presentation_size_request(now).or(requested_size);
|
||||||
@@ -1132,6 +1152,11 @@ impl Ferese {
|
|||||||
state.states.unset(xdg_toplevel::State::Fullscreen)
|
state.states.unset(xdg_toplevel::State::Fullscreen)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let maximized_changed = if is_maximized {
|
||||||
|
state.states.set(xdg_toplevel::State::Maximized)
|
||||||
|
} else {
|
||||||
|
state.states.unset(xdg_toplevel::State::Maximized)
|
||||||
|
};
|
||||||
let tiled = !is_floating && !is_fullscreen;
|
let tiled = !is_floating && !is_fullscreen;
|
||||||
let tiled_changed = if tiled {
|
let tiled_changed = if tiled {
|
||||||
state.states.set(xdg_toplevel::State::TiledLeft)
|
state.states.set(xdg_toplevel::State::TiledLeft)
|
||||||
@@ -1145,7 +1170,7 @@ impl Ferese {
|
|||||||
| state.states.unset(xdg_toplevel::State::TiledBottom)
|
| state.states.unset(xdg_toplevel::State::TiledBottom)
|
||||||
};
|
};
|
||||||
|
|
||||||
let decoration_mode = if is_floating && !is_fullscreen {
|
let decoration_mode = if is_floating && !is_fullscreen && !is_maximized {
|
||||||
DecorationMode::ClientSide
|
DecorationMode::ClientSide
|
||||||
} else {
|
} else {
|
||||||
DecorationMode::ServerSide
|
DecorationMode::ServerSide
|
||||||
@@ -1153,7 +1178,7 @@ impl Ferese {
|
|||||||
let decoration_changed = state.decoration_mode != Some(decoration_mode);
|
let decoration_changed = state.decoration_mode != Some(decoration_mode);
|
||||||
state.decoration_mode = Some(decoration_mode);
|
state.decoration_mode = Some(decoration_mode);
|
||||||
|
|
||||||
fullscreen_changed || tiled_changed || decoration_changed
|
fullscreen_changed || maximized_changed || tiled_changed || decoration_changed
|
||||||
});
|
});
|
||||||
|
|
||||||
if requested_size.is_some() || state_changed {
|
if requested_size.is_some() || state_changed {
|
||||||
@@ -1600,6 +1625,42 @@ impl Ferese {
|
|||||||
self.relayout();
|
self.relayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn toggle_focused_maximized(&mut self) {
|
||||||
|
let Some(window) = self.focused_window else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// Super+F from true fullscreen enters decorated maximization.
|
||||||
|
let fullscreen = self
|
||||||
|
.workspaces
|
||||||
|
.workspace_for_window(window)
|
||||||
|
.and_then(|workspace| self.workspaces.workspace(workspace))
|
||||||
|
.is_some_and(|workspace| workspace.fullscreen == Some(window));
|
||||||
|
let enabled = fullscreen || !self.maximized_windows.contains(&window);
|
||||||
|
self.set_window_maximized(window, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_window_maximized(&mut self, window: WindowId, enabled: bool) {
|
||||||
|
if self.workspaces.workspace_for_window(window).is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if enabled {
|
||||||
|
let _ = self.workspaces.set_fullscreen(window, false);
|
||||||
|
self.maximized_windows.insert(window);
|
||||||
|
} else {
|
||||||
|
self.maximized_windows.remove(&window);
|
||||||
|
}
|
||||||
|
self.relayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn output_has_fullscreen(&self, output: &Output) -> bool {
|
||||||
|
!self.overview.is_presenting()
|
||||||
|
&& self
|
||||||
|
.output_id(output)
|
||||||
|
.and_then(|id| self.output_workspaces.active_workspace(id))
|
||||||
|
.and_then(|workspace| self.workspaces.workspace(workspace))
|
||||||
|
.is_some_and(|workspace| workspace.fullscreen.is_some())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn close_focused_window(&mut self) {
|
pub fn close_focused_window(&mut self) {
|
||||||
let Some(focused) = self.focused_window else {
|
let Some(focused) = self.focused_window else {
|
||||||
return;
|
return;
|
||||||
@@ -1950,6 +2011,18 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn maximized_rect(bounds: Rect, gap: f64) -> Rect {
|
||||||
|
let gap = gap
|
||||||
|
.max(0.0)
|
||||||
|
.min(((bounds.width.min(bounds.height) - 1.0) / 2.0).max(0.0));
|
||||||
|
Rect::new(
|
||||||
|
bounds.x + gap,
|
||||||
|
bounds.y + gap,
|
||||||
|
(bounds.width - 2.0 * gap).max(1.0),
|
||||||
|
(bounds.height - 2.0 * gap).max(1.0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn centered_floating_rect(bounds: Rect) -> Rect {
|
fn centered_floating_rect(bounds: Rect) -> Rect {
|
||||||
let width = (bounds.width * 0.6).max(1.0);
|
let width = (bounds.width * 0.6).max(1.0);
|
||||||
let height = (bounds.height * 0.6).max(1.0);
|
let height = (bounds.height * 0.6).max(1.0);
|
||||||
@@ -2017,6 +2090,20 @@ impl ClientData for ClientState {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn maximization_respects_layer_exclusion_and_outer_gaps() {
|
||||||
|
assert_eq!(
|
||||||
|
maximized_rect(Rect::new(0.0, 48.0, 1280.0, 752.0), 10.0),
|
||||||
|
Rect::new(10.0, 58.0, 1260.0, 732.0)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
maximized_rect(Rect::new(1600.0, 36.0, 1280.0, 764.0), 6.0),
|
||||||
|
Rect::new(1606.0, 42.0, 1268.0, 752.0)
|
||||||
|
);
|
||||||
|
let tiny = maximized_rect(Rect::new(0.0, 0.0, 5.0, 3.0), 10.0);
|
||||||
|
assert!(tiny.width >= 1.0 && tiny.height >= 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transient_geometry_is_centered_and_bounded_by_its_parent() {
|
fn transient_geometry_is_centered_and_bounded_by_its_parent() {
|
||||||
let parent = Rect::new(100.0, 50.0, 1_000.0, 800.0);
|
let parent = Rect::new(100.0, 50.0, 1_000.0, 800.0);
|
||||||
|
|||||||
+29
-62
@@ -40,9 +40,9 @@ use smithay::{
|
|||||||
},
|
},
|
||||||
input::pointer::{CursorImageStatus, CursorImageSurfaceData},
|
input::pointer::{CursorImageStatus, CursorImageSurfaceData},
|
||||||
output::{Mode, Output, PhysicalProperties, Scale, Subpixel},
|
output::{Mode, Output, PhysicalProperties, Scale, Subpixel},
|
||||||
reexports::calloop::EventLoop,
|
|
||||||
reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind as PresentationKind,
|
reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind as PresentationKind,
|
||||||
reexports::wayland_server::Resource,
|
reexports::wayland_server::Resource,
|
||||||
|
reexports::calloop::EventLoop,
|
||||||
utils::{
|
utils::{
|
||||||
Buffer, Clock, Logical, Monotonic, Physical, Point, Rectangle, Scale as RenderScale, Size,
|
Buffer, Clock, Logical, Monotonic, Physical, Point, Rectangle, Scale as RenderScale, Size,
|
||||||
Transform,
|
Transform,
|
||||||
@@ -871,12 +871,12 @@ fn output_elements(
|
|||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
elements.extend(layer_elements(
|
let upper_layers: &[Layer] = if state.output_has_fullscreen(output) {
|
||||||
state,
|
&[Layer::Overlay]
|
||||||
renderer,
|
} else {
|
||||||
output,
|
&[Layer::Overlay, Layer::Top]
|
||||||
&[Layer::Overlay, Layer::Top],
|
};
|
||||||
));
|
elements.extend(layer_elements(state, renderer, output, upper_layers));
|
||||||
let windows = state
|
let windows = state
|
||||||
.space
|
.space
|
||||||
.elements()
|
.elements()
|
||||||
@@ -1452,30 +1452,26 @@ fn layer_elements(
|
|||||||
|
|
||||||
let mut elements = Vec::new();
|
let mut elements = Vec::new();
|
||||||
for (geometry, layer) in layers {
|
for (geometry, layer) in layers {
|
||||||
let target = geometry.to_physical_precise_round(scale);
|
elements.extend(
|
||||||
let surface_elements = AsRenderElements::<GlesRenderer>::render_elements::<
|
AsRenderElements::<GlesRenderer>::render_elements::<
|
||||||
WaylandSurfaceRenderElement<GlesRenderer>,
|
WaylandSurfaceRenderElement<GlesRenderer>,
|
||||||
>(&layer, renderer, target.loc, scale.into(), 1.0);
|
>(
|
||||||
let source_bounds = surface_elements
|
&layer,
|
||||||
.iter()
|
renderer,
|
||||||
.map(|element| element.geometry(scale.into()))
|
geometry.loc.to_physical_precise_round(scale),
|
||||||
.reduce(Rectangle::merge);
|
scale.into(),
|
||||||
let content_scale = source_bounds
|
1.0,
|
||||||
.map(|source| layer_content_scale(source.size, target.size))
|
)
|
||||||
.unwrap_or_else(|| RenderScale::from(1.0));
|
.into_iter()
|
||||||
let scale_origin = source_bounds.map_or(target.loc, |source| source.loc);
|
.filter_map(|element| {
|
||||||
let crop = target.intersection(output_crop).unwrap_or_default();
|
let origin = Point::<i32, Physical>::default();
|
||||||
|
let element = RescaleRenderElement::from_element(element, origin, 1.0);
|
||||||
|
let element =
|
||||||
|
RelocateRenderElement::from_element(element, origin, Relocate::Relative);
|
||||||
|
|
||||||
elements.extend(surface_elements.into_iter().filter_map(|element| {
|
CropRenderElement::from_element(element, scale, output_crop).map(Into::into)
|
||||||
let element = RescaleRenderElement::from_element(element, scale_origin, content_scale);
|
}),
|
||||||
let element = RelocateRenderElement::from_element(
|
);
|
||||||
element,
|
|
||||||
Point::<i32, Physical>::default(),
|
|
||||||
Relocate::Relative,
|
|
||||||
);
|
|
||||||
|
|
||||||
CropRenderElement::from_element(element, scale, crop).map(Into::into)
|
|
||||||
}));
|
|
||||||
|
|
||||||
if let Some(material) = material_element(state, renderer, output, &layer, geometry) {
|
if let Some(material) = material_element(state, renderer, output, &layer, geometry) {
|
||||||
elements.push(material);
|
elements.push(material);
|
||||||
@@ -1485,20 +1481,6 @@ fn layer_elements(
|
|||||||
elements
|
elements
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layer_content_scale(
|
|
||||||
source: Size<i32, Physical>,
|
|
||||||
target: Size<i32, Physical>,
|
|
||||||
) -> RenderScale<f64> {
|
|
||||||
if source.w <= 0 || source.h <= 0 || target.w <= 0 || target.h <= 0 {
|
|
||||||
return RenderScale::from(1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderScale::from((
|
|
||||||
f64::from(target.w) / f64::from(source.w),
|
|
||||||
f64::from(target.h) / f64::from(source.h),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn material_element(
|
fn material_element(
|
||||||
state: &mut Ferese,
|
state: &mut Ferese,
|
||||||
renderer: &mut GlesRenderer,
|
renderer: &mut GlesRenderer,
|
||||||
@@ -1519,7 +1501,8 @@ fn material_element(
|
|||||||
expanded_blur_region(geometry, material.blur.ceil() as i32, output_logical);
|
expanded_blur_region(geometry, material.blur.ceil() as i32, output_logical);
|
||||||
let sample_physical = sample_geometry.to_physical_precise_round(scale);
|
let sample_physical = sample_geometry.to_physical_precise_round(scale);
|
||||||
let transform = output.current_transform().invert();
|
let transform = output.current_transform().invert();
|
||||||
let tint = [0.067, 0.094, 0.129, material.opacity];
|
let [red, green, blue, _] = state.theme_settings.surface_base_color.0;
|
||||||
|
let tint = [red, green, blue, material.opacity];
|
||||||
let parameters = MaterialParameters {
|
let parameters = MaterialParameters {
|
||||||
geometry,
|
geometry,
|
||||||
tint,
|
tint,
|
||||||
@@ -1831,8 +1814,7 @@ mod tests {
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
color_with_alpha, expanded_blur_region, framebuffer_capture_rect, framebuffer_clip_rect,
|
color_with_alpha, expanded_blur_region, framebuffer_capture_rect, framebuffer_clip_rect,
|
||||||
layer_content_scale, normalized_scale, rounded_visual_rect, scaled_visual_rect,
|
normalized_scale, rounded_visual_rect, scaled_visual_rect, shadow_bounds,
|
||||||
shadow_bounds,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -1885,21 +1867,6 @@ mod tests {
|
|||||||
assert_eq!(normalized_scale(f64::INFINITY), 1.0);
|
assert_eq!(normalized_scale(f64::INFINITY), 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn layer_content_tracks_new_allocation_before_the_client_repaints() {
|
|
||||||
let scale = layer_content_scale((1_000, 40).into(), (1_600, 40).into());
|
|
||||||
|
|
||||||
assert_eq!(scale, Scale::from((1.6, 1.0)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn layer_content_scale_ignores_empty_allocations() {
|
|
||||||
assert_eq!(
|
|
||||||
layer_content_scale((0, 40).into(), (1_600, 40).into()),
|
|
||||||
Scale::from(1.0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clip_rect_matches_smithay_gles_projection() {
|
fn clip_rect_matches_smithay_gles_projection() {
|
||||||
let geometry = Rectangle::<i32, Physical>::new((10, 5).into(), (30, 40).into());
|
let geometry = Rectangle::<i32, Physical>::new((10, 5).into(), (30, 40).into());
|
||||||
|
|||||||
@@ -267,6 +267,13 @@ impl ClientGeometry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum PresentationMode {
|
||||||
|
Normal,
|
||||||
|
Maximized,
|
||||||
|
Fullscreen,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct WindowGeometry {
|
pub struct WindowGeometry {
|
||||||
pub logical: Rect,
|
pub logical: Rect,
|
||||||
@@ -274,11 +281,11 @@ pub struct WindowGeometry {
|
|||||||
pub client: ClientGeometry,
|
pub client: ClientGeometry,
|
||||||
pub decorations: f64,
|
pub decorations: f64,
|
||||||
pub presentation_changed: bool,
|
pub presentation_changed: bool,
|
||||||
fullscreen: bool,
|
mode: PresentationMode,
|
||||||
zoom: Option<ZoomTransition>,
|
zoom: Option<ZoomTransition>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fullscreen geometry and decorations use one clock, including on reversal.
|
/// Maximized/fullscreen geometry and decorations share a clock, including on reversal.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
struct ZoomTransition {
|
struct ZoomTransition {
|
||||||
from: Rect,
|
from: Rect,
|
||||||
@@ -300,7 +307,7 @@ impl WindowGeometry {
|
|||||||
},
|
},
|
||||||
decorations: 1.0,
|
decorations: 1.0,
|
||||||
presentation_changed: true,
|
presentation_changed: true,
|
||||||
fullscreen: false,
|
mode: PresentationMode::Normal,
|
||||||
zoom: None,
|
zoom: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,9 +326,26 @@ impl WindowGeometry {
|
|||||||
rect: Rect,
|
rect: Rect,
|
||||||
fullscreen: bool,
|
fullscreen: bool,
|
||||||
now: Duration,
|
now: Duration,
|
||||||
|
) -> Option<ClientSize> {
|
||||||
|
self.set_presentation_mode(
|
||||||
|
rect,
|
||||||
|
if fullscreen {
|
||||||
|
PresentationMode::Fullscreen
|
||||||
|
} else {
|
||||||
|
PresentationMode::Normal
|
||||||
|
},
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_presentation_mode(
|
||||||
|
&mut self,
|
||||||
|
rect: Rect,
|
||||||
|
mode: PresentationMode,
|
||||||
|
now: Duration,
|
||||||
) -> Option<ClientSize> {
|
) -> Option<ClientSize> {
|
||||||
let rect = normalized_rect(rect);
|
let rect = normalized_rect(rect);
|
||||||
if self.fullscreen != fullscreen || (self.zoom.is_some() && self.logical != rect) {
|
if self.mode != mode || (self.zoom.is_some() && self.logical != rect) {
|
||||||
let mut progress = AnimatedValue::new(0.0);
|
let mut progress = AnimatedValue::new(0.0);
|
||||||
progress.set_target(1.0);
|
progress.set_target(1.0);
|
||||||
let from = self.visual.current;
|
let from = self.visual.current;
|
||||||
@@ -350,7 +374,7 @@ impl WindowGeometry {
|
|||||||
progress,
|
progress,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
self.fullscreen = fullscreen;
|
self.mode = mode;
|
||||||
self.logical = rect;
|
self.logical = rect;
|
||||||
self.visual.set_target(rect);
|
self.visual.set_target(rect);
|
||||||
self.presentation_size_request(now)
|
self.presentation_size_request(now)
|
||||||
@@ -368,14 +392,14 @@ impl WindowGeometry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_fullscreen(&self) -> bool {
|
pub fn is_fullscreen(&self) -> bool {
|
||||||
self.fullscreen
|
self.mode == PresentationMode::Fullscreen
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn follow_pointer(&mut self, rect: Rect, now: Duration) -> Option<ClientSize> {
|
pub fn follow_pointer(&mut self, rect: Rect, now: Duration) -> Option<ClientSize> {
|
||||||
self.zoom = None;
|
self.zoom = None;
|
||||||
let requested_size = self.set_logical_target(rect, now);
|
let requested_size = self.set_logical_target(rect, now);
|
||||||
self.visual.snap();
|
self.visual.snap();
|
||||||
self.decorations = if self.fullscreen { 0.0 } else { 1.0 };
|
self.decorations = if self.is_fullscreen() { 0.0 } else { 1.0 };
|
||||||
self.presentation_changed = true;
|
self.presentation_changed = true;
|
||||||
requested_size
|
requested_size
|
||||||
}
|
}
|
||||||
@@ -400,7 +424,7 @@ impl WindowGeometry {
|
|||||||
) -> bool {
|
) -> bool {
|
||||||
if !animations_enabled {
|
if !animations_enabled {
|
||||||
self.visual.snap();
|
self.visual.snap();
|
||||||
self.decorations = if self.fullscreen { 0.0 } else { 1.0 };
|
self.decorations = if self.is_fullscreen() { 0.0 } else { 1.0 };
|
||||||
self.zoom = None;
|
self.zoom = None;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -437,7 +461,11 @@ impl WindowGeometry {
|
|||||||
};
|
};
|
||||||
self.decorations = lerp(
|
self.decorations = lerp(
|
||||||
zoom.decorations_from,
|
zoom.decorations_from,
|
||||||
if self.fullscreen { 0.0 } else { 1.0 },
|
if self.mode == PresentationMode::Fullscreen {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
},
|
||||||
);
|
);
|
||||||
if !active {
|
if !active {
|
||||||
self.zoom = None;
|
self.zoom = None;
|
||||||
@@ -544,6 +572,67 @@ fn finite_or(value: f64, fallback: f64) -> f64 {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn maximized_zoom_keeps_decorations_and_restores_through_fullscreen() {
|
||||||
|
let normal = Rect::new(800.0, 50.0, 600.0, 700.0);
|
||||||
|
let maximized = Rect::new(10.0, 50.0, 1580.0, 740.0);
|
||||||
|
let fullscreen = Rect::new(0.0, 0.0, 1600.0, 800.0);
|
||||||
|
let mut geometry = WindowGeometry::new(normal, None);
|
||||||
|
for (mode, rect) in [
|
||||||
|
(PresentationMode::Maximized, maximized),
|
||||||
|
(PresentationMode::Fullscreen, fullscreen),
|
||||||
|
(PresentationMode::Maximized, maximized),
|
||||||
|
(PresentationMode::Normal, normal),
|
||||||
|
] {
|
||||||
|
let before = geometry.visual.current;
|
||||||
|
let decorations = geometry.decorations;
|
||||||
|
geometry.set_presentation_mode(rect, mode, Duration::ZERO);
|
||||||
|
assert_eq!(geometry.visual.current, before);
|
||||||
|
assert_eq!(geometry.decorations, decorations);
|
||||||
|
for _ in 0..1000 {
|
||||||
|
if !geometry.advance(Duration::from_millis(8), SpringConfig::default(), true) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if mode != PresentationMode::Fullscreen && decorations == 1.0 {
|
||||||
|
assert_eq!(geometry.decorations, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(geometry.visual.current, rect);
|
||||||
|
assert_eq!(
|
||||||
|
geometry.decorations,
|
||||||
|
if mode == PresentationMode::Fullscreen {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert!(!geometry.is_zooming());
|
||||||
|
assert_eq!(
|
||||||
|
geometry.is_fullscreen(),
|
||||||
|
mode == PresentationMode::Fullscreen
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reversing_maximization_preserves_visual_position_and_decoration() {
|
||||||
|
let normal = Rect::new(800.0, 50.0, 600.0, 700.0);
|
||||||
|
let mut geometry = WindowGeometry::new(normal, None);
|
||||||
|
geometry.set_presentation_mode(
|
||||||
|
Rect::new(10.0, 50.0, 1580.0, 740.0),
|
||||||
|
PresentationMode::Maximized,
|
||||||
|
Duration::ZERO,
|
||||||
|
);
|
||||||
|
geometry.advance(Duration::from_millis(80), SpringConfig::default(), true);
|
||||||
|
let before = geometry.visual.current;
|
||||||
|
geometry.set_presentation_mode(normal, PresentationMode::Normal, Duration::from_millis(80));
|
||||||
|
assert_eq!(geometry.visual.current, before);
|
||||||
|
assert_eq!(geometry.decorations, 1.0);
|
||||||
|
geometry.advance(Duration::ZERO, SpringConfig::default(), false);
|
||||||
|
assert_eq!(geometry.visual.current, normal);
|
||||||
|
assert_eq!(geometry.decorations, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn zoom_finishes_without_a_visible_final_edge_jump() {
|
fn zoom_finishes_without_a_visible_final_edge_jump() {
|
||||||
let tiled = Rect::new(12.0, 12.0, 942.0, 1056.0);
|
let tiled = Rect::new(12.0, 12.0, 942.0, 1056.0);
|
||||||
|
|||||||
@@ -1031,7 +1031,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
first.geometry[&WindowId(1)],
|
first.geometry[&WindowId(1)],
|
||||||
Rect::new(10.0, 10.0, 980.0, 780.0)
|
Rect::new(4.0, 4.0, 992.0, 792.0)
|
||||||
);
|
);
|
||||||
workspaces.switch_to_numeric(2).unwrap();
|
workspaces.switch_to_numeric(2).unwrap();
|
||||||
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Scrolling);
|
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Scrolling);
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ impl Default for GapConfig {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner: 10.0,
|
inner: 10.0,
|
||||||
outer: 10.0,
|
outer: 4.0,
|
||||||
smart: false,
|
smart: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1178,8 +1178,8 @@ mod tests {
|
|||||||
let first = geometry[&WindowId(1)];
|
let first = geometry[&WindowId(1)];
|
||||||
let second = geometry[&WindowId(2)];
|
let second = geometry[&WindowId(2)];
|
||||||
|
|
||||||
assert_eq!(first.x, 10.0);
|
assert_eq!(first.x, 4.0);
|
||||||
assert_eq!(second.x + second.width, 90.0);
|
assert_eq!(second.x + second.width, 96.0);
|
||||||
assert_eq!(second.x - (first.x + first.width), 10.0);
|
assert_eq!(second.x - (first.x + first.width), 10.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1213,7 +1213,7 @@ mod tests {
|
|||||||
.geometry_with_gaps(Rect::new(0.0, 0.0, 100.0, 80.0), GapConfig::default())
|
.geometry_with_gaps(Rect::new(0.0, 0.0, 100.0, 80.0), GapConfig::default())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(geometry[&WindowId(1)], Rect::new(10.0, 10.0, 80.0, 60.0));
|
assert_eq!(geometry[&WindowId(1)], Rect::new(4.0, 4.0, 92.0, 72.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1571,7 +1571,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.geometry[&WindowId(1)],
|
result.geometry[&WindowId(1)],
|
||||||
Rect::new(10.0, 10.0, 60.0, 40.0)
|
Rect::new(4.0, 4.0, 60.0, 40.0)
|
||||||
);
|
);
|
||||||
assert_eq!(result.warnings.len(), 2);
|
assert_eq!(result.warnings.len(), 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -240,10 +240,10 @@ base_unit = 8.0
|
|||||||
|
|
||||||
window_radius = 14.0
|
window_radius = 14.0
|
||||||
|
|
||||||
top_bar_height = 32.0
|
top_bar_height = 38.0
|
||||||
top_bar_margin_top = 4.0
|
top_bar_margin_top = 4.0
|
||||||
top_bar_margin_horizontal = 10.0
|
top_bar_margin_horizontal = 10.0
|
||||||
top_bar_radius = 16.0
|
top_bar_radius = 19.0
|
||||||
|
|
||||||
launcher_width = 560.0
|
launcher_width = 560.0
|
||||||
launcher_radius = 20.0
|
launcher_radius = 20.0
|
||||||
@@ -724,8 +724,8 @@ noise = 0.012
|
|||||||
|
|
||||||
[theme.geometry]
|
[theme.geometry]
|
||||||
window_radius = 14.0
|
window_radius = 14.0
|
||||||
top_bar_height = 32.0
|
top_bar_height = 38.0
|
||||||
top_bar_radius = 16.0
|
top_bar_radius = 19.0
|
||||||
launcher_width = 560.0
|
launcher_width = 560.0
|
||||||
launcher_radius = 20.0
|
launcher_radius = 20.0
|
||||||
|
|
||||||
|
|||||||
@@ -1127,7 +1127,7 @@ mass = 1.0
|
|||||||
[layout]
|
[layout]
|
||||||
mode = "scrolling"
|
mode = "scrolling"
|
||||||
inner_gap = 10.0
|
inner_gap = 10.0
|
||||||
outer_gap = 10.0
|
outer_gap = 4.0
|
||||||
smart_gaps = false
|
smart_gaps = false
|
||||||
|
|
||||||
[scrolling]
|
[scrolling]
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Fedora / SDDM installation
|
||||||
|
|
||||||
|
Build and test in the checkout:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo test -p ferese -p feresectl -p ferese-animation -p ferese-core -p ferese-layout -p ferese-shell --locked
|
||||||
|
cargo build --release --locked -p ferese -p ferese-shell -p feresectl
|
||||||
|
sudo bash scripts/install-session.sh YOUR_RELEASE_ID
|
||||||
|
```
|
||||||
|
|
||||||
|
The installer adds **Ferese** to SDDM without changing Plasma or
|
||||||
|
the default session. It installs immutable, versioned directories in
|
||||||
|
`/usr/local/lib/ferese/releases/` and launchers in `/usr/local/bin/`.
|
||||||
|
`current` selects the installed build; `previous` records the prior selection
|
||||||
|
on upgrades. Installation refuses to replace unmanaged launchers/session files.
|
||||||
|
|
||||||
|
Keep your existing `~/.config/ferese/config.toml`. For a new configuration, copy
|
||||||
|
`/usr/local/lib/ferese/current/config.example.toml` there. The existing compositor
|
||||||
|
defaults handle connected monitors automatically; add output profiles only after
|
||||||
|
checking `feresectl get-outputs` on the actual DRM session.
|
||||||
|
|
||||||
|
Use `ferese-session --nested` for an installed-build preview. Log out of Plasma
|
||||||
|
and select Ferese in SDDM for real hardware use. `ferese-session` can also run
|
||||||
|
from a spare logged-in TTY. It must run as your normal user, never as root.
|
||||||
|
Foot and `dbus-run-session` must be installed.
|
||||||
|
|
||||||
|
Session logs are written under `${XDG_STATE_HOME:-~/.local/state}/ferese/`.
|
||||||
|
The launcher sets Wayland desktop variables, starts D-Bus if none is available,
|
||||||
|
and grants shell capabilities only to the compositor-launched shell process.
|
||||||
|
It does not enable public screencopy, privileged input methods, or shortcut
|
||||||
|
inhibition by default.
|
||||||
|
|
||||||
|
## Logout and recovery
|
||||||
|
|
||||||
|
Save your work, then press **Super+Shift+E** or run `feresectl exit` to log out.
|
||||||
|
Ferese stops its event loop and terminates its shell child during shutdown.
|
||||||
|
This is immediate logout, without a save-confirmation dialog.
|
||||||
|
|
||||||
|
If the compositor becomes unresponsive, switch to another TTY using
|
||||||
|
Ctrl+Alt+F1–F12 and terminate that specific Ferese process. Select Plasma on the
|
||||||
|
next login. Do not restart SDDM while a desktop contains unsaved work.
|
||||||
|
|
||||||
|
For a binary rollback, inspect `/usr/local/lib/ferese/previous` and select that
|
||||||
|
release with the `current` symlink while Ferese is stopped. Existing releases
|
||||||
|
are retained. To hide the login option, move
|
||||||
|
`/usr/share/wayland-sessions/ferese.desktop` out of that directory.
|
||||||
|
|
||||||
|
## First hardware session
|
||||||
|
|
||||||
|
Verify terminal/editor/browser, clipboard, fullscreen, fractional scaling,
|
||||||
|
monitor disconnect/reconnect, VT switching away/back, and logout. Suspend/resume
|
||||||
|
and session locking are not certified by the nested smoke test. Keep Plasma
|
||||||
|
available until real development sessions and these checks pass.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Nested live-resize regression check
|
||||||
|
|
||||||
|
The shell enables libcosmic's `wgpu` feature. Without it, iced uses tiny-skia
|
||||||
|
to resample and rasterize the wallpaper on the CPU. In an unoptimized build,
|
||||||
|
this blocks the UI/render thread shared with the top bar for seconds. Prompt
|
||||||
|
layer configures, buffer stretching, and compositor vsync cannot fix that
|
||||||
|
client-side bottleneck. Software rendering remains available as a fallback.
|
||||||
|
|
||||||
|
## Reproduce and compare
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --locked -p ferese -p ferese-shell
|
||||||
|
WAYLAND_DEBUG=1 FERESE_TRACE_PERFORMANCE=1 \
|
||||||
|
target/debug/ferese --backend nested --grant-effects --grant-shell-control -- \
|
||||||
|
target/debug/ferese-shell 2>/tmp/ferese-resize.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Use a configured wallpaper. Drag the host window's edges in both directions,
|
||||||
|
then maximize/restore it repeatedly. Check that the wallpaper and bar follow
|
||||||
|
the window, including at fractional host scaling. Repeat with
|
||||||
|
`ICED_BACKEND=wgpu` to require GPU rendering (rather than silently accepting
|
||||||
|
fallback), and with `ICED_BACKEND=tiny-skia` to compare software rendering.
|
||||||
|
Use a release build if software fallback is needed for regular use.
|
||||||
|
|
||||||
|
Trace the chain, not just the compositor's frame rate:
|
||||||
|
|
||||||
|
1. Host `xdg_toplevel.configure` becomes Winit `Resized`.
|
||||||
|
2. Ferese updates the output, arranges layers, and sends
|
||||||
|
`zwlr_layer_surface_v1.configure` for the new logical dimensions.
|
||||||
|
3. The shell acknowledges configure and eventually attaches/commits a new
|
||||||
|
buffer. A fast acknowledgement does **not** imply a finished render.
|
||||||
|
4. Committed buffers change surface damage; Ferese renders the damaged output
|
||||||
|
and submits through EGL to the host compositor.
|
||||||
|
|
||||||
|
`FERESE_TRACE_PERFORMANCE` measures Ferese's rendering/submission, not shell
|
||||||
|
rasterization. Wayland traces distinguish software SHM buffers from GPU
|
||||||
|
buffers (Mesa Vulkan on the tested system).
|
||||||
|
|
||||||
|
## Measured validation (KDE Wayland, 175% scale)
|
||||||
|
|
||||||
|
With temporary instrumentation, 24 host maximize/restore requests spaced
|
||||||
|
250 ms apart generated 26 Winit resize events including initialization. The
|
||||||
|
same debug binaries were tested with each renderer forced:
|
||||||
|
|
||||||
|
| Measurement | tiny-skia | wgpu |
|
||||||
|
| --- | ---: | ---: |
|
||||||
|
| Median configure acknowledgement | 1.35 ms | 2.27 ms |
|
||||||
|
| Median Ferese render + submit | 0.82 ms | 1.10 ms |
|
||||||
|
| Wallpaper buffer interval | 5919 ms (only two buffers) | 9.85 ms median |
|
||||||
|
| Bar buffers observed | 1 | 996 |
|
||||||
|
|
||||||
|
The CPU capture ran for 14 seconds and the GPU capture for 18 seconds;
|
||||||
|
counts are diagnostic, not equal-duration throughput benchmarks. GPU maximum
|
||||||
|
buffer intervals were about 200 ms for wallpaper and 158 ms for the bar, so
|
||||||
|
the median does not imply every frame met a refresh deadline. These figures
|
||||||
|
include protocol tracing overhead. The temporary resize/timing hooks were
|
||||||
|
removed after testing; no synthetic resizing runs in production.
|
||||||
|
|
||||||
|
The fix does not alter output coordinates, layer exclusion, damage tracking,
|
||||||
|
frame scheduling, or EGL swap settings. It also applies to the same shell
|
||||||
|
running under DRM, though this validation used the nested backend.
|
||||||
@@ -47,9 +47,9 @@ fn parse_args(args: impl IntoIterator<Item = String>) -> Result<(String, Value),
|
|||||||
.map_err(|_| format!("{} requires a positive workspace index", command))?;
|
.map_err(|_| format!("{} requires a positive workspace index", command))?;
|
||||||
json!({ "index": index })
|
json!({ "index": index })
|
||||||
}
|
}
|
||||||
"toggle-floating" | "toggle-fullscreen" | "toggle-layout" | "cycle-column-width"
|
"toggle-floating" | "toggle-fullscreen" | "toggle-maximized" | "toggle-layout"
|
||||||
| "center-column" | "consume" | "expel" | "close" | "get-focused-window"
|
| "cycle-column-width" | "center-column" | "consume" | "expel" | "close"
|
||||||
| "get-workspaces" | "get-outputs" => {
|
| "get-focused-window" | "get-workspaces" | "get-outputs" | "exit" => {
|
||||||
if !positional.is_empty() {
|
if !positional.is_empty() {
|
||||||
return Err(format!("{command} does not accept arguments"));
|
return Err(format!("{command} does not accept arguments"));
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ fn socket_path() -> Result<PathBuf, io::Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn usage() -> String {
|
fn usage() -> String {
|
||||||
"usage: feresectl <focus|move|resize> <direction>\n feresectl <workspace|move-to-workspace> <index>\n feresectl <toggle-floating|toggle-fullscreen|toggle-layout>\n feresectl <cycle-column-width|center-column|consume|expel|close>\n feresectl <get-focused-window|get-workspaces|get-outputs>".to_owned()
|
"usage: feresectl <focus|move|resize> <direction>\n feresectl <workspace|move-to-workspace> <index>\n feresectl <toggle-floating|toggle-maximized|toggle-fullscreen|toggle-layout>\n feresectl <cycle-column-width|center-column|consume|expel|close|exit>\n feresectl <get-focused-window|get-workspaces|get-outputs>".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -107,6 +107,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_unknown_or_missing_arguments() {
|
fn rejects_unknown_or_missing_arguments() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_args(["exit".to_owned()]).unwrap(),
|
||||||
|
("exit".to_owned(), json!({}))
|
||||||
|
);
|
||||||
|
assert!(parse_args(["exit".to_owned(), "extra".to_owned()]).is_err());
|
||||||
assert!(parse_args(["focus".to_owned()]).is_err());
|
assert!(parse_args(["focus".to_owned()]).is_err());
|
||||||
assert!(parse_args(["reload-config".to_owned()]).is_err());
|
assert!(parse_args(["reload-config".to_owned()]).is_err());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# User configuration: ~/.config/ferese/config.toml
|
||||||
|
# Unspecified monitors use automatic placement and preferred modes.
|
||||||
|
[commands]
|
||||||
|
terminal = ["foot"]
|
||||||
|
|
||||||
|
[input]
|
||||||
|
xkb_layout = "us"
|
||||||
|
focus_follows_mouse = false
|
||||||
|
|
||||||
|
[layout]
|
||||||
|
smart_gaps = false
|
||||||
|
|
||||||
|
# Super+Enter opens Foot. Super+Shift+E logs out.
|
||||||
|
# See README.md for monitor profiles and additional bindings.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
umask 077
|
||||||
|
|
||||||
|
backend=drm
|
||||||
|
if [[ ${1:-} == --nested ]]; then
|
||||||
|
backend=nested
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
if (($#)); then
|
||||||
|
echo 'usage: ferese-session [--nested]' >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
: "${XDG_RUNTIME_DIR:?A login session with XDG_RUNTIME_DIR is required}"
|
||||||
|
|
||||||
|
# Resolve once, so upgrading current cannot mix compositor and shell versions.
|
||||||
|
release_dir=$(dirname -- "$(readlink -f -- "$0")")
|
||||||
|
state_dir=${XDG_STATE_HOME:-$HOME/.local/state}/ferese
|
||||||
|
mkdir -p -- "$state_dir"
|
||||||
|
log_file=$state_dir/session-$(date -u +%Y%m%dT%H%M%SZ)-$$.log
|
||||||
|
echo "Ferese session log: $log_file" >&2
|
||||||
|
exec >>"$log_file" 2>&1
|
||||||
|
|
||||||
|
export XDG_CURRENT_DESKTOP=Ferese
|
||||||
|
export XDG_SESSION_DESKTOP=ferese
|
||||||
|
export XDG_SESSION_TYPE=wayland
|
||||||
|
export MOZ_ENABLE_WAYLAND=1
|
||||||
|
export QT_QPA_PLATFORM=wayland
|
||||||
|
export PATH="$release_dir:$PATH"
|
||||||
|
unset WAYLAND_SOCKET
|
||||||
|
if [[ $backend == drm ]]; then
|
||||||
|
unset DISPLAY WAYLAND_DISPLAY
|
||||||
|
fi
|
||||||
|
|
||||||
|
command=("$release_dir/ferese" --backend "$backend" --grant-effects
|
||||||
|
--grant-shell-control -- "$release_dir/ferese-shell")
|
||||||
|
if [[ -z ${DBUS_SESSION_BUS_ADDRESS:-} ]]; then
|
||||||
|
exec dbus-run-session -- "${command[@]}"
|
||||||
|
else
|
||||||
|
exec "${command[@]}"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=Ferese
|
||||||
|
Comment=Animation-first scrolling Wayland desktop
|
||||||
|
Exec=/usr/local/bin/ferese-session
|
||||||
|
TryExec=/usr/local/bin/ferese-session
|
||||||
|
Type=Application
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Install already-built binaries. Run from any directory with sudo.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
release_id=${1:?usage: install-session.sh RELEASE_ID}
|
||||||
|
[[ $release_id =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]] || exit 2
|
||||||
|
[[ $EUID == 0 ]] || { echo 'Run this installer with sudo.' >&2; exit 1; }
|
||||||
|
|
||||||
|
install_root=/usr/local/lib/ferese
|
||||||
|
release_dir=$install_root/releases/$release_id
|
||||||
|
session_entry=/usr/share/wayland-sessions/ferese.desktop
|
||||||
|
[[ ! -e $release_dir ]] || { echo "Release already exists: $release_dir" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Refuse to overwrite unrelated installations.
|
||||||
|
for name in ferese ferese-shell feresectl ferese-session; do
|
||||||
|
link=/usr/local/bin/$name
|
||||||
|
if [[ -e $link || -L $link ]]; then
|
||||||
|
[[ -L $link && $(readlink -- "$link") == "$install_root/current/$name" ]] || {
|
||||||
|
echo "Unmanaged path exists: $link" >&2; exit 1;
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for link in current previous; do
|
||||||
|
if [[ -e $install_root/$link || -L $install_root/$link ]]; then
|
||||||
|
[[ -L $install_root/$link && $(readlink -- "$install_root/$link") == releases/* ]] || {
|
||||||
|
echo "Unmanaged path exists: $install_root/$link" >&2; exit 1;
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ -e $session_entry ]]; then
|
||||||
|
cmp -s -- "$repo_dir/packaging/ferese.desktop" "$session_entry" || {
|
||||||
|
echo "Different session entry exists: $session_entry" >&2; exit 1;
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
for name in ferese ferese-shell feresectl; do
|
||||||
|
[[ -x $repo_dir/target/release/$name ]] || { echo "Missing release binary: $name" >&2; exit 1; }
|
||||||
|
done
|
||||||
|
desktop-file-validate "$repo_dir/packaging/ferese.desktop"
|
||||||
|
|
||||||
|
install -d -m 0755 -- "$release_dir" /usr/local/bin /usr/share/wayland-sessions
|
||||||
|
for name in ferese ferese-shell feresectl; do
|
||||||
|
install -m 0755 -- "$repo_dir/target/release/$name" "$release_dir/$name"
|
||||||
|
done
|
||||||
|
install -m 0755 -- "$repo_dir/packaging/ferese-session" "$release_dir/ferese-session"
|
||||||
|
install -m 0644 -- "$repo_dir/packaging/config.toml" "$release_dir/config.example.toml"
|
||||||
|
|
||||||
|
if [[ -L $install_root/current ]]; then
|
||||||
|
ln -sfn -- "$(readlink -- "$install_root/current")" "$install_root/previous"
|
||||||
|
fi
|
||||||
|
ln -s -- "releases/$release_id" "$install_root/current-$release_id"
|
||||||
|
mv -Tf -- "$install_root/current-$release_id" "$install_root/current"
|
||||||
|
for name in ferese ferese-shell feresectl ferese-session; do
|
||||||
|
ln -sfn -- "$install_root/current/$name" "/usr/local/bin/$name"
|
||||||
|
done
|
||||||
|
install -m 0644 -- "$repo_dir/packaging/ferese.desktop" "$session_entry"
|
||||||
|
echo "Installed Ferese $release_id. Select Ferese in SDDM."
|
||||||
@@ -8,7 +8,9 @@ rust-version = "1.93"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
ferese-protocols = { path = "../../crates/ferese-protocols", default-features = false, features = ["client"] }
|
ferese-protocols = { path = "../../crates/ferese-protocols", default-features = false, features = ["client"] }
|
||||||
jiff.workspace = true
|
jiff.workspace = true
|
||||||
libcosmic.workspace = true
|
# Wallpaper resampling must not block the bar on iced's shared UI thread.
|
||||||
|
# Keep tiny-skia available as libcosmic's fallback, but prefer GPU rendering.
|
||||||
|
libcosmic = { workspace = true, features = ["wgpu"] }
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
toml.workspace = true
|
toml.workspace = true
|
||||||
wayland-client.workspace = true
|
wayland-client.workspace = true
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none">
|
||||||
|
<rect x="1.4" y="5.5" width="15.4" height="9" rx="2.4" stroke="currentColor" stroke-width="1.8"/>
|
||||||
|
<path d="M18 8.1c.6.2 1 .8 1 1.5v.8c0 .7-.4 1.3-1 1.5V8.1Z" fill="currentColor"/>
|
||||||
|
<rect x="3.8" y="7.9" width="10.6" height="4.2" rx="1" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 343 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<rect x="1.5" y="2" width="7.2" height="6.2" rx="1.7"/>
|
||||||
|
<rect x="11.3" y="2" width="7.2" height="9.2" rx="1.7"/>
|
||||||
|
<rect x="1.5" y="10.8" width="7.2" height="7.2" rx="1.7"/>
|
||||||
|
<rect x="11.3" y="13.8" width="7.2" height="4.2" rx="1.7"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 328 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||||
|
<path d="M2 8.1v3.8c0 .8.6 1.4 1.4 1.4h2.1l4 3.3c.6.5 1.5.1 1.5-.7V4.1c0-.8-.9-1.2-1.5-.7l-4 3.3H3.4C2.6 6.7 2 7.3 2 8.1Z" fill="currentColor" stroke="none"/>
|
||||||
|
<path d="M13.4 7.2a4.1 4.1 0 0 1 0 5.6"/>
|
||||||
|
<path d="M16 4.8a7.4 7.4 0 0 1 0 10.4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 390 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2.15" stroke-linecap="round">
|
||||||
|
<path d="M2.1 7.2a12.1 12.1 0 0 1 15.8 0"/>
|
||||||
|
<path d="M5.2 10.5a7.4 7.4 0 0 1 9.6 0"/>
|
||||||
|
<path d="M8.3 13.7a2.7 2.7 0 0 1 3.4 0"/>
|
||||||
|
<circle cx="10" cy="16.6" r="1.25" fill="currentColor" stroke="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 352 B |
@@ -10,6 +10,48 @@ const DEFAULT_BACKGROUND: [u8; 3] = [11, 15, 20];
|
|||||||
pub(crate) struct ShellConfig {
|
pub(crate) struct ShellConfig {
|
||||||
pub(crate) font_family: Option<String>,
|
pub(crate) font_family: Option<String>,
|
||||||
pub(crate) wallpaper: WallpaperConfig,
|
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)]
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
@@ -35,6 +77,12 @@ struct FereseConfig {
|
|||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
struct ThemeConfig {
|
struct ThemeConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
colors: ThemeColorsConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
geometry: ThemeGeometryConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
shadow: ThemeShadowConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
typography: TypographyConfig,
|
typography: TypographyConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -46,6 +94,90 @@ struct TypographyConfig {
|
|||||||
font_family: Option<String>,
|
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 {
|
pub(crate) fn load() -> ShellConfig {
|
||||||
let Some(path) = config_path() else {
|
let Some(path) = config_path() else {
|
||||||
return ShellConfig::default();
|
return ShellConfig::default();
|
||||||
@@ -55,10 +187,15 @@ pub(crate) fn load() -> ShellConfig {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match toml::from_str::<FereseConfig>(&source) {
|
match toml::from_str::<FereseConfig>(&source) {
|
||||||
Ok(config) => ShellConfig {
|
Ok(config) => {
|
||||||
font_family: config.theme.typography.font_family,
|
let theme = shell_theme(&config.theme);
|
||||||
wallpaper: config.theme.background,
|
|
||||||
},
|
ShellConfig {
|
||||||
|
font_family: config.theme.typography.font_family,
|
||||||
|
wallpaper: config.theme.background,
|
||||||
|
theme,
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("ferese-shell: failed to read {}: {error}", path.display());
|
eprintln!("ferese-shell: failed to read {}: {error}", path.display());
|
||||||
ShellConfig::default()
|
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] {
|
pub(crate) const fn default_background() -> [u8; 3] {
|
||||||
DEFAULT_BACKGROUND
|
DEFAULT_BACKGROUND
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ pub(crate) struct WindowSnapshot {
|
|||||||
pub(crate) app_id: String,
|
pub(crate) app_id: String,
|
||||||
pub(crate) title: String,
|
pub(crate) title: String,
|
||||||
pub(crate) focused: bool,
|
pub(crate) focused: bool,
|
||||||
|
pub(crate) fullscreen: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct ShellControl {
|
pub(crate) struct ShellControl {
|
||||||
@@ -56,11 +57,13 @@ pub(crate) struct ShellControl {
|
|||||||
|
|
||||||
pub(crate) struct ControlPoll {
|
pub(crate) struct ControlPoll {
|
||||||
pub(crate) snapshot: Option<ShellSnapshot>,
|
pub(crate) snapshot: Option<ShellSnapshot>,
|
||||||
|
pub(crate) overview_active: Option<bool>,
|
||||||
pub(crate) disconnected: bool,
|
pub(crate) disconnected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ControlUpdate {
|
enum ControlUpdate {
|
||||||
Snapshot(ShellSnapshot),
|
Snapshot(ShellSnapshot),
|
||||||
|
OverviewState(bool),
|
||||||
Disconnected,
|
Disconnected,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,12 +96,14 @@ impl ShellControl {
|
|||||||
pub(crate) fn poll(&self) -> ControlPoll {
|
pub(crate) fn poll(&self) -> ControlPoll {
|
||||||
let mut poll = ControlPoll {
|
let mut poll = ControlPoll {
|
||||||
snapshot: None,
|
snapshot: None,
|
||||||
|
overview_active: None,
|
||||||
disconnected: false,
|
disconnected: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match self.updates.try_recv() {
|
match self.updates.try_recv() {
|
||||||
Ok(ControlUpdate::Snapshot(snapshot)) => poll.snapshot = Some(snapshot),
|
Ok(ControlUpdate::Snapshot(snapshot)) => poll.snapshot = Some(snapshot),
|
||||||
|
Ok(ControlUpdate::OverviewState(active)) => poll.overview_active = Some(active),
|
||||||
Ok(ControlUpdate::Disconnected) | Err(TryRecvError::Disconnected) => {
|
Ok(ControlUpdate::Disconnected) | Err(TryRecvError::Disconnected) => {
|
||||||
poll.disconnected = true;
|
poll.disconnected = true;
|
||||||
return poll;
|
return poll;
|
||||||
@@ -115,8 +120,12 @@ impl ShellControl {
|
|||||||
let _ = self.connection.flush();
|
let _ = self.connection.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn enter_overview(&self) {
|
pub(crate) fn set_overview_active(&self, active: bool) {
|
||||||
self.shell.enter_overview();
|
if active {
|
||||||
|
self.shell.enter_overview();
|
||||||
|
} else {
|
||||||
|
self.shell.exit_overview();
|
||||||
|
}
|
||||||
let _ = self.connection.flush();
|
let _ = self.connection.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,17 +223,20 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
|
|||||||
title,
|
title,
|
||||||
state: window_state,
|
state: window_state,
|
||||||
} => {
|
} => {
|
||||||
let focused = matches!(
|
let (focused, fullscreen) = match window_state {
|
||||||
window_state,
|
WEnum::Value(flags) => (
|
||||||
WEnum::Value(flags)
|
flags.contains(ferese_shell_v1::WindowState::Focused),
|
||||||
if flags.contains(ferese_shell_v1::WindowState::Focused)
|
flags.contains(ferese_shell_v1::WindowState::Fullscreen),
|
||||||
);
|
),
|
||||||
|
WEnum::Unknown(_) => (false, false),
|
||||||
|
};
|
||||||
|
|
||||||
state.pending.windows.push(WindowSnapshot {
|
state.pending.windows.push(WindowSnapshot {
|
||||||
workspace: join_id(workspace_hi, workspace_lo),
|
workspace: join_id(workspace_hi, workspace_lo),
|
||||||
app_id,
|
app_id,
|
||||||
title,
|
title,
|
||||||
focused,
|
focused,
|
||||||
|
fullscreen,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ferese_shell_v1::Event::SnapshotEnd { serial }
|
ferese_shell_v1::Event::SnapshotEnd { serial }
|
||||||
@@ -234,6 +246,9 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
|
|||||||
.sender
|
.sender
|
||||||
.send(ControlUpdate::Snapshot(state.pending.clone()));
|
.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::event::{self, PlatformSpecific, wayland};
|
||||||
use cosmic::iced::platform_specific::{
|
use cosmic::iced::platform_specific::{
|
||||||
runtime::wayland::layer_surface::{IcedMargin, IcedOutput, SctkLayerSurfaceSettings},
|
runtime::wayland::layer_surface::{IcedMargin, IcedOutput, SctkLayerSurfaceSettings},
|
||||||
|
shell::commands::layer_surface::set_input_zone,
|
||||||
shell::commands::layer_surface::{Anchor, KeyboardInteractivity, Layer},
|
shell::commands::layer_surface::{Anchor, KeyboardInteractivity, Layer},
|
||||||
};
|
};
|
||||||
use cosmic::iced::{
|
use cosmic::iced::{
|
||||||
@@ -16,7 +17,7 @@ use cosmic::iced::{
|
|||||||
window,
|
window,
|
||||||
};
|
};
|
||||||
use cosmic::theme;
|
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::{
|
use ferese_protocols::effects::v1::client::{
|
||||||
ferese_effects_manager_v1::FereseEffectsManagerV1,
|
ferese_effects_manager_v1::FereseEffectsManagerV1,
|
||||||
ferese_surface_effects_v1::{self, FereseSurfaceEffectsV1},
|
ferese_surface_effects_v1::{self, FereseSurfaceEffectsV1},
|
||||||
@@ -28,21 +29,41 @@ use wayland_client::{
|
|||||||
protocol::{wl_registry, wl_surface},
|
protocol::{wl_registry, wl_surface},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::config::{ShellConfig, WallpaperMode};
|
use crate::config::{ShellConfig, ShellTheme, WallpaperMode};
|
||||||
use crate::control::{ShellControl, ShellSnapshot};
|
use crate::control::{ShellControl, ShellSnapshot};
|
||||||
|
|
||||||
const APP_ID: &str = "dev.ferese.Shell";
|
const APP_ID: &str = "dev.ferese.Shell";
|
||||||
const BAR_HEIGHT: u32 = 32;
|
|
||||||
const BAR_MARGIN: i32 = 4;
|
#[derive(Clone, Copy)]
|
||||||
const EXCLUSIVE_ZONE: i32 = BAR_HEIGHT as i32;
|
struct BarMetrics {
|
||||||
const BAR_TEXT_SIZE: u16 = (BAR_HEIGHT * 3 / 8) as u16;
|
height: f32,
|
||||||
const BAR_ICON_SIZE: u16 = (BAR_HEIGHT / 2) as u16;
|
text_size: u16,
|
||||||
const CONTROL_HEIGHT: f32 = BAR_HEIGHT as f32 * 0.75;
|
icon_size: u16,
|
||||||
const WORKSPACE_HIT_WIDTH: f32 = BAR_HEIGHT as f32 * 0.6875;
|
control_height: f32,
|
||||||
const ACTIVE_MARKER_WIDTH: f32 = BAR_HEIGHT as f32 * 0.5;
|
workspace_hit_width: f32,
|
||||||
const ACTIVE_MARKER_HEIGHT: f32 = BAR_HEIGHT as f32 * 0.125;
|
active_marker_width: f32,
|
||||||
const DOT_MARKER_SIZE: f32 = BAR_HEIGHT as f32 * 0.1875;
|
active_marker_height: f32,
|
||||||
const EMPTY_MARKER_SIZE: f32 = BAR_HEIGHT as f32 * 0.125;
|
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 {
|
fn main() -> cosmic::iced::Result {
|
||||||
let config = config::load();
|
let config = config::load();
|
||||||
@@ -66,6 +87,7 @@ struct FereseShell {
|
|||||||
config: ShellConfig,
|
config: ShellConfig,
|
||||||
control: Option<ShellControl>,
|
control: Option<ShellControl>,
|
||||||
snapshot: ShellSnapshot,
|
snapshot: ShellSnapshot,
|
||||||
|
overview_active: bool,
|
||||||
clock: String,
|
clock: String,
|
||||||
effects: Option<EffectsBinding>,
|
effects: Option<EffectsBinding>,
|
||||||
}
|
}
|
||||||
@@ -75,7 +97,7 @@ enum Message {
|
|||||||
Event(Event, window::Id),
|
Event(Event, window::Id),
|
||||||
Tick,
|
Tick,
|
||||||
ActivateWorkspace(u64),
|
ActivateWorkspace(u64),
|
||||||
EnterOverview,
|
ToggleOverview,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl cosmic::Application for FereseShell {
|
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>) {
|
fn init(core: Core, config: Self::Flags) -> (Self, Task<Self::Message>) {
|
||||||
let bar_surface_id = window::Id::unique();
|
let bar_surface_id = window::Id::unique();
|
||||||
let wallpaper_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 {
|
let app = Self {
|
||||||
core,
|
core,
|
||||||
bar_surface_id,
|
bar_surface_id,
|
||||||
@@ -106,6 +130,7 @@ impl cosmic::Application for FereseShell {
|
|||||||
})
|
})
|
||||||
.ok(),
|
.ok(),
|
||||||
snapshot: ShellSnapshot::default(),
|
snapshot: ShellSnapshot::default(),
|
||||||
|
overview_active: false,
|
||||||
clock: current_time(),
|
clock: current_time(),
|
||||||
effects: None,
|
effects: None,
|
||||||
};
|
};
|
||||||
@@ -135,14 +160,14 @@ impl cosmic::Application for FereseShell {
|
|||||||
output: IcedOutput::Active,
|
output: IcedOutput::Active,
|
||||||
namespace: "ferese-shell-top-bar".to_owned(),
|
namespace: "ferese-shell-top-bar".to_owned(),
|
||||||
margin: IcedMargin {
|
margin: IcedMargin {
|
||||||
top: BAR_MARGIN,
|
top: shell_theme.bar_margin_top,
|
||||||
right: 10,
|
right: shell_theme.bar_margin_horizontal,
|
||||||
bottom: 0,
|
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,
|
size_limits: Limits::NONE,
|
||||||
exclusive_zone: EXCLUSIVE_ZONE,
|
exclusive_zone: bar.height.round() as i32,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
Some(Box::new(Self::view_layer)),
|
Some(Box::new(Self::view_layer)),
|
||||||
@@ -173,8 +198,26 @@ impl cosmic::Application for FereseShell {
|
|||||||
if poll.disconnected {
|
if poll.disconnected {
|
||||||
return cosmic::iced::exit();
|
return cosmic::iced::exit();
|
||||||
}
|
}
|
||||||
|
if let Some(active) = poll.overview_active {
|
||||||
|
self.overview_active = active;
|
||||||
|
}
|
||||||
if let Some(snapshot) = poll.snapshot {
|
if let Some(snapshot) = poll.snapshot {
|
||||||
|
let was_hidden = bar_hidden(&self.snapshot);
|
||||||
|
|
||||||
self.snapshot = 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()
|
Task::none()
|
||||||
@@ -185,9 +228,10 @@ impl cosmic::Application for FereseShell {
|
|||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::EnterOverview => {
|
Message::ToggleOverview => {
|
||||||
|
self.overview_active = !self.overview_active;
|
||||||
if let Some(control) = &self.control {
|
if let Some(control) = &self.control {
|
||||||
control.enter_overview();
|
control.set_overview_active(self.overview_active);
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
@@ -234,13 +278,20 @@ impl FereseShell {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
match EffectsBinding::attach(surface) {
|
match EffectsBinding::attach(surface, !bar_hidden(&self.snapshot)) {
|
||||||
Ok(binding) => self.effects = Some(binding),
|
Ok(binding) => self.effects = Some(binding),
|
||||||
Err(error) => eprintln!("ferese-shell: panel material unavailable: {error}"),
|
Err(error) => eprintln!("ferese-shell: panel material unavailable: {error}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_layer(&self) -> Element<'_, cosmic::Action<Message>> {
|
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
|
let focused_output = self
|
||||||
.snapshot
|
.snapshot
|
||||||
.outputs
|
.outputs
|
||||||
@@ -248,16 +299,18 @@ impl FereseShell {
|
|||||||
.find(|output| output.focused)
|
.find(|output| output.focused)
|
||||||
.or_else(|| self.snapshot.outputs.first());
|
.or_else(|| self.snapshot.outputs.first());
|
||||||
let focused_output_id = focused_output.map(|output| output.id);
|
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)
|
let mut workspace_row = row::with_capacity(self.snapshot.workspaces.len() + 1)
|
||||||
.spacing(1)
|
.spacing(1)
|
||||||
.align_y(cosmic::iced::Alignment::Center);
|
.align_y(cosmic::iced::Alignment::Center);
|
||||||
|
|
||||||
workspace_row = workspace_row.push(
|
workspace_row = workspace_row.push(
|
||||||
button::suggested("F")
|
button::custom(overview_control(bar, shell_theme))
|
||||||
.font_size(BAR_TEXT_SIZE)
|
.height(bar.control_height)
|
||||||
.height(CONTROL_HEIGHT)
|
.padding(0)
|
||||||
.padding([2, 9])
|
.class(theme::Button::Transparent)
|
||||||
.on_press(cosmic::Action::App(Message::EnterOverview)),
|
.on_press(cosmic::Action::App(Message::ToggleOverview)),
|
||||||
);
|
);
|
||||||
for workspace in self
|
for workspace in self
|
||||||
.snapshot
|
.snapshot
|
||||||
@@ -270,9 +323,9 @@ impl FereseShell {
|
|||||||
.windows
|
.windows
|
||||||
.iter()
|
.iter()
|
||||||
.any(|window| window.workspace == workspace.id);
|
.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)
|
let workspace_button = button::custom(indicator)
|
||||||
.height(CONTROL_HEIGHT)
|
.height(bar.control_height)
|
||||||
.padding(0)
|
.padding(0)
|
||||||
.class(theme::Button::Transparent)
|
.class(theme::Button::Transparent)
|
||||||
.on_press(cosmic::Action::App(Message::ActivateWorkspace(
|
.on_press(cosmic::Action::App(Message::ActivateWorkspace(
|
||||||
@@ -293,16 +346,16 @@ impl FereseShell {
|
|||||||
})
|
})
|
||||||
.map(display_app_name)
|
.map(display_app_name)
|
||||||
.unwrap_or_else(|| "Desktop".to_owned());
|
.unwrap_or_else(|| "Desktop".to_owned());
|
||||||
let left = row![workspace_row, text(app_name).size(BAR_TEXT_SIZE)]
|
let left = row![workspace_row, text(app_name).size(bar.text_size)]
|
||||||
.spacing(12)
|
.spacing(shell_theme.control_gap)
|
||||||
.align_y(cosmic::iced::Alignment::Center);
|
.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![
|
let right = row![
|
||||||
text("◌").size(BAR_ICON_SIZE),
|
symbolic_icon(include_bytes!("../assets/icons/wifi.svg"), bar.icon_size),
|
||||||
text("♪").size(BAR_ICON_SIZE),
|
symbolic_icon(include_bytes!("../assets/icons/volume.svg"), bar.icon_size),
|
||||||
text("●").size(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);
|
.align_y(cosmic::iced::Alignment::Center);
|
||||||
let content = row![
|
let content = row![
|
||||||
container(left)
|
container(left)
|
||||||
@@ -321,8 +374,8 @@ impl FereseShell {
|
|||||||
container(content)
|
container(content)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.padding([0, 12])
|
.padding([0, shell_theme.panel_padding.round() as u16])
|
||||||
.class(theme::Container::custom(bar_style))
|
.class(theme::Container::custom(move |_| bar_style(shell_theme)))
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,51 +400,98 @@ impl FereseShell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn workspace_indicator(active: bool, occupied: bool) -> Element<'static, cosmic::Action<Message>> {
|
fn symbolic_icon(source: &'static [u8], size: u16) -> icon::Icon {
|
||||||
let (width, height, style) = if active {
|
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,
|
bar.active_marker_width,
|
||||||
ACTIVE_MARKER_HEIGHT,
|
bar.active_marker_height,
|
||||||
active_workspace_style as fn(&cosmic::Theme) -> container::Style,
|
color(shell_theme.accent),
|
||||||
|
2.0,
|
||||||
)
|
)
|
||||||
} else if occupied {
|
} else if occupied {
|
||||||
(
|
(
|
||||||
DOT_MARKER_SIZE,
|
bar.dot_marker_size,
|
||||||
DOT_MARKER_SIZE,
|
bar.dot_marker_size,
|
||||||
occupied_workspace_style as fn(&cosmic::Theme) -> container::Style,
|
color_with_opacity(shell_theme.text_muted, 0.78),
|
||||||
|
3.0,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
EMPTY_MARKER_SIZE,
|
bar.empty_marker_size,
|
||||||
EMPTY_MARKER_SIZE,
|
bar.empty_marker_size,
|
||||||
empty_workspace_style as fn(&cosmic::Theme) -> container::Style,
|
color_with_opacity(shell_theme.text_muted, 0.42),
|
||||||
|
2.0,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let marker = container(text(""))
|
let marker = container(text(""))
|
||||||
.width(width)
|
.width(width)
|
||||||
.height(height)
|
.height(height)
|
||||||
.class(theme::Container::custom(style));
|
.class(theme::Container::custom(move |_| {
|
||||||
|
workspace_marker_style(color, radius)
|
||||||
|
}));
|
||||||
|
|
||||||
container(marker)
|
container(marker)
|
||||||
.width(WORKSPACE_HIT_WIDTH)
|
.width(bar.workspace_hit_width)
|
||||||
.height(CONTROL_HEIGHT)
|
.height(bar.control_height)
|
||||||
.align_x(alignment::Horizontal::Center)
|
.align_x(alignment::Horizontal::Center)
|
||||||
.align_y(alignment::Vertical::Center)
|
.align_y(alignment::Vertical::Center)
|
||||||
.into()
|
.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 {
|
fn workspace_marker_style(color: Color, radius: f32) -> container::Style {
|
||||||
container::Style {
|
container::Style {
|
||||||
background: Some(Background::Color(color)),
|
background: Some(Background::Color(color)),
|
||||||
@@ -426,25 +526,37 @@ fn current_time() -> String {
|
|||||||
Zoned::now().strftime("%H:%M").to_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 {
|
container::Style {
|
||||||
icon_color: Some(Color::from_rgb8(235, 239, 247)),
|
icon_color: Some(color(theme.text_primary)),
|
||||||
text_color: Some(Color::from_rgb8(235, 239, 247)),
|
text_color: Some(color(theme.text_primary)),
|
||||||
background: Some(Background::Color(Color::from_rgba8(18, 21, 28, 0.88))),
|
background: None,
|
||||||
border: Border {
|
border: Border {
|
||||||
color: Color::from_rgba8(255, 255, 255, 0.14),
|
color: color(theme.border),
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
radius: 16.0.into(),
|
radius: theme.bar_radius.into(),
|
||||||
},
|
},
|
||||||
shadow: Shadow {
|
shadow: Shadow {
|
||||||
color: Color::from_rgba8(0, 0, 0, 0.32),
|
color: shadow,
|
||||||
offset: Vector::new(0.0, 4.0),
|
offset: Vector::new(0.0, theme.shadow_offset_y),
|
||||||
blur_radius: 18.0,
|
blur_radius: theme.shadow_blur,
|
||||||
},
|
},
|
||||||
snap: true,
|
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 {
|
fn wallpaper_fallback_style(_theme: &cosmic::Theme) -> container::Style {
|
||||||
let [red, green, blue] = config::default_background();
|
let [red, green, blue] = config::default_background();
|
||||||
|
|
||||||
@@ -455,13 +567,17 @@ fn wallpaper_fallback_style(_theme: &cosmic::Theme) -> container::Style {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct EffectsBinding {
|
struct EffectsBinding {
|
||||||
|
connection: Connection,
|
||||||
_manager: FereseEffectsManagerV1,
|
_manager: FereseEffectsManagerV1,
|
||||||
_surface: FereseSurfaceEffectsV1,
|
surface: FereseSurfaceEffectsV1,
|
||||||
_queue: EventQueue<EffectsState>,
|
_queue: EventQueue<EffectsState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EffectsBinding {
|
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
|
let backend = surface
|
||||||
.backend()
|
.backend()
|
||||||
.upgrade()
|
.upgrade()
|
||||||
@@ -472,15 +588,32 @@ impl EffectsBinding {
|
|||||||
let manager = globals.bind::<FereseEffectsManagerV1, _, _>(&qh, 1..=1, ())?;
|
let manager = globals.bind::<FereseEffectsManagerV1, _, _>(&qh, 1..=1, ())?;
|
||||||
let effects = manager.get_surface_effects(surface, &qh, ());
|
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()?;
|
connection.flush()?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
connection,
|
||||||
_manager: manager,
|
_manager: manager,
|
||||||
_surface: effects,
|
surface: effects,
|
||||||
_queue: queue,
|
_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;
|
struct EffectsState;
|
||||||
@@ -499,3 +632,41 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for EffectsState {
|
|||||||
|
|
||||||
delegate_noop!(EffectsState: ignore FereseEffectsManagerV1);
|
delegate_noop!(EffectsState: ignore FereseEffectsManagerV1);
|
||||||
delegate_noop!(EffectsState: ignore FereseSurfaceEffectsV1);
|
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