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
@@ -110,6 +110,7 @@ pub struct ThemeSettings {
|
||||
pub border_color: RgbaColor,
|
||||
pub accent_color: RgbaColor,
|
||||
pub shadow_color: RgbaColor,
|
||||
pub surface_base_color: RgbaColor,
|
||||
pub window_radius: f64,
|
||||
pub shadow_offset_y: f64,
|
||||
pub shadow_blur: f64,
|
||||
@@ -146,6 +147,8 @@ struct ThemeMaterialConfig {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ThemeColorsConfig {
|
||||
#[serde(default = "default_surface_base_color")]
|
||||
surface_base: String,
|
||||
#[serde(default = "default_border_color")]
|
||||
border: String,
|
||||
#[serde(default = "default_accent_color")]
|
||||
@@ -157,6 +160,7 @@ struct ThemeColorsConfig {
|
||||
impl Default for ThemeColorsConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
surface_base: default_surface_base_color(),
|
||||
border: default_border_color(),
|
||||
accent: default_accent_color(),
|
||||
shadow: default_shadow_color(),
|
||||
@@ -235,12 +239,14 @@ struct BindingModifiers {
|
||||
pub enum BindingAction {
|
||||
Spawn(Vec<String>),
|
||||
Close,
|
||||
Exit,
|
||||
Focus(ferese_layout::Direction),
|
||||
Move(ferese_layout::Direction),
|
||||
Resize(ferese_layout::Direction),
|
||||
SwitchWorkspace(u8),
|
||||
MoveToWorkspace(u8),
|
||||
ToggleFullscreen,
|
||||
ToggleMaximized,
|
||||
ToggleLayout,
|
||||
CycleColumnWidth,
|
||||
CenterColumn,
|
||||
@@ -803,6 +809,10 @@ impl Config {
|
||||
border_color: parse_color(&self.theme.colors.border, "colors.border")?,
|
||||
accent_color: parse_color(&self.theme.colors.accent, "colors.accent")?,
|
||||
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,
|
||||
shadow_offset_y,
|
||||
shadow_blur,
|
||||
@@ -1059,6 +1069,10 @@ fn parse_action(
|
||||
no_argument()?;
|
||||
Ok(BindingAction::Close)
|
||||
}
|
||||
"exit" => {
|
||||
no_argument()?;
|
||||
Ok(BindingAction::Exit)
|
||||
}
|
||||
"focus" => Ok(BindingAction::Focus(parse_direction(required_argument()?)?)),
|
||||
"move" => Ok(BindingAction::Move(parse_direction(required_argument()?)?)),
|
||||
"resize" => Ok(BindingAction::Resize(
|
||||
@@ -1070,6 +1084,10 @@ fn parse_action(
|
||||
"move-to-workspace" => Ok(BindingAction::MoveToWorkspace(parse_workspace(
|
||||
required_argument()?,
|
||||
)?)),
|
||||
"toggle-maximized" => {
|
||||
no_argument()?;
|
||||
Ok(BindingAction::ToggleMaximized)
|
||||
}
|
||||
"toggle-fullscreen" => {
|
||||
no_argument()?;
|
||||
Ok(BindingAction::ToggleFullscreen)
|
||||
@@ -1133,7 +1151,8 @@ fn default_bindings() -> Vec<BindingConfig> {
|
||||
let mut bindings = vec![
|
||||
binding("Super+Enter", "spawn", Some("terminal")),
|
||||
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+R", "cycle-column-width", None),
|
||||
binding("Super+C", "center-column", None),
|
||||
@@ -1141,6 +1160,7 @@ fn default_bindings() -> Vec<BindingConfig> {
|
||||
binding("Super+]", "expel", None),
|
||||
binding("Super+Shift+Space", "toggle-floating", 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")] {
|
||||
@@ -1283,13 +1303,17 @@ const fn default_inner_gap() -> f64 {
|
||||
}
|
||||
|
||||
const fn default_outer_gap() -> f64 {
|
||||
10.0
|
||||
4.0
|
||||
}
|
||||
|
||||
fn default_border_color() -> String {
|
||||
"#FFFFFF18".to_owned()
|
||||
}
|
||||
|
||||
fn default_surface_base_color() -> String {
|
||||
"#111821".to_owned()
|
||||
}
|
||||
|
||||
fn default_accent_color() -> String {
|
||||
"#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,]),
|
||||
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]),
|
||||
surface_base_color: RgbaColor([17.0 / 255.0, 24.0 / 255.0, 33.0 / 255.0, 1.0,]),
|
||||
window_radius: 12.0,
|
||||
shadow_offset_y: -2.0,
|
||||
shadow_blur: 24.0,
|
||||
@@ -1628,7 +1653,21 @@ mod tests {
|
||||
let input = config.input_settings().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| {
|
||||
binding.modifiers.logo
|
||||
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Return)
|
||||
@@ -1650,7 +1689,7 @@ mod tests {
|
||||
|
||||
let input = replaced.input_settings().unwrap();
|
||||
let bindings = replaced.bindings(&input).unwrap();
|
||||
assert_eq!(bindings.len(), 40);
|
||||
assert_eq!(bindings.len(), 42);
|
||||
assert!(bindings.iter().any(|binding| {
|
||||
binding.action
|
||||
== BindingAction::Spawn(vec![
|
||||
@@ -1662,7 +1701,7 @@ mod tests {
|
||||
|
||||
let input = unbound.input_settings().unwrap();
|
||||
let bindings = unbound.bindings(&input).unwrap();
|
||||
assert_eq!(bindings.len(), 39);
|
||||
assert_eq!(bindings.len(), 41);
|
||||
assert!(
|
||||
!bindings
|
||||
.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(
|
||||
&mut self,
|
||||
surface: ToplevelSurface,
|
||||
|
||||
+45
-6
@@ -44,6 +44,14 @@ impl Ferese {
|
||||
}
|
||||
|
||||
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 state == KeyState::Pressed {
|
||||
data.intercepted_keys.insert(keycode);
|
||||
@@ -168,11 +176,29 @@ impl Ferese {
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
|
||||
if self.overview.is_active() {
|
||||
if event.state() == ButtonState::Pressed
|
||||
&& let Some(window) = self.window_under_visual(pointer.current_location())
|
||||
&& let Some(id) = self.window_ids.get(&window).copied()
|
||||
{
|
||||
self.select_overview_window(id);
|
||||
let position = pointer.current_location();
|
||||
if self.layer_under(position).is_some() {
|
||||
pointer.button(
|
||||
self,
|
||||
&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;
|
||||
@@ -465,6 +491,7 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
BindingAction::Close => self.close_focused_window(),
|
||||
BindingAction::Exit => self.loop_signal.stop(),
|
||||
BindingAction::Focus(direction) => self.focus_direction(direction),
|
||||
BindingAction::Move(direction) => self.move_direction(direction),
|
||||
BindingAction::Resize(direction) => self.resize_direction(direction),
|
||||
@@ -475,6 +502,7 @@ impl Ferese {
|
||||
self.move_focused_to_workspace(u32::from(workspace));
|
||||
}
|
||||
BindingAction::ToggleFullscreen => self.toggle_focused_fullscreen(),
|
||||
BindingAction::ToggleMaximized => self.toggle_focused_maximized(),
|
||||
BindingAction::ToggleLayout => self.toggle_layout_mode(),
|
||||
BindingAction::CycleColumnWidth => self.cycle_focused_column_width(),
|
||||
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
|
||||
}
|
||||
|
||||
fn overview_escape(symbol: u32, overview_active: bool) -> bool {
|
||||
overview_active && symbol == keysyms::KEY_Escape
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
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]
|
||||
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_layout::Direction;
|
||||
use serde_json::{Value, json};
|
||||
use smithay::reexports::calloop::{EventLoop, channel};
|
||||
use smithay::reexports::calloop::{EventLoop, LoopSignal, channel};
|
||||
use smithay::utils::Transform;
|
||||
|
||||
use crate::{Ferese, config::OutputTransform};
|
||||
@@ -84,15 +84,20 @@ pub(crate) fn init(
|
||||
}
|
||||
})?;
|
||||
|
||||
let signal = event_loop.get_signal();
|
||||
thread::Builder::new()
|
||||
.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");
|
||||
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));
|
||||
|
||||
loop {
|
||||
@@ -125,11 +130,12 @@ fn accept_connections(listener: UnixListener, sender: channel::SyncSender<IpcCal
|
||||
continue;
|
||||
};
|
||||
let sender = sender.clone();
|
||||
let signal = signal.clone();
|
||||
if let Err(error) = thread::Builder::new()
|
||||
.name("ferese-ipc-client".to_owned())
|
||||
.spawn(move || {
|
||||
let _permit = permit;
|
||||
serve_connection(stream, sender);
|
||||
serve_connection(stream, sender, signal);
|
||||
})
|
||||
{
|
||||
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 {
|
||||
let request = match read_frame(&mut stream) {
|
||||
let request: Request = match read_frame(&mut stream) {
|
||||
Ok(request) => request,
|
||||
Err(ferese_ipc::FrameError::Io(error))
|
||||
if matches!(
|
||||
@@ -174,6 +184,7 @@ fn serve_connection(mut stream: UnixStream, sender: channel::SyncSender<IpcCall>
|
||||
}
|
||||
};
|
||||
let (response, receiver) = sync_channel(1);
|
||||
let exit_requested = request.command == "exit";
|
||||
let call = IpcCall { request, response };
|
||||
if sender.try_send(call).is_err() {
|
||||
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 {
|
||||
return;
|
||||
};
|
||||
let exit_accepted = exit_requested && response.error.is_none();
|
||||
if let Err(error) = write_frame(&mut stream, &response) {
|
||||
tracing::debug!(%error, "IPC client disconnected before receiving its response");
|
||||
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> {
|
||||
match command {
|
||||
"exit" => {} // The IPC worker stops the loop after writing the response.
|
||||
"focus" => self.focus_direction(direction_arg(args)?),
|
||||
"move" => self.move_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)?),
|
||||
"toggle-floating" => self.toggle_focused_floating(),
|
||||
"toggle-fullscreen" => self.toggle_focused_fullscreen(),
|
||||
"toggle-maximized" => self.toggle_focused_maximized(),
|
||||
"toggle-layout" => self.toggle_layout_mode(),
|
||||
"cycle-column-width" => self.cycle_focused_column_width(),
|
||||
"center-column" => self.center_focused_column(),
|
||||
@@ -608,7 +628,9 @@ mod tests {
|
||||
fn malformed_ipc_connection_is_closed_without_reaching_the_request_queue() {
|
||||
let (mut client, server) = UnixStream::pair().unwrap();
|
||||
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";
|
||||
|
||||
client
|
||||
|
||||
+93
-6
@@ -6,7 +6,7 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ferese_animation::{AnimatedValue, ClientSize, SpringConfig, WindowGeometry};
|
||||
use ferese_animation::{AnimatedValue, ClientSize, PresentationMode, SpringConfig, WindowGeometry};
|
||||
use ferese_core::{
|
||||
LayoutMode, OutputGeometry, OutputId, OutputWorkspaceMap, WindowPlacement, WorkspaceId,
|
||||
WorkspaceSet,
|
||||
@@ -118,6 +118,7 @@ pub struct Ferese {
|
||||
output_identity_ids: HashMap<String, OutputId>,
|
||||
pub window_ids: HashMap<Window, WindowId>,
|
||||
pub window_geometry: HashMap<WindowId, WindowGeometry>,
|
||||
maximized_windows: HashSet<WindowId>,
|
||||
window_stack: crate::stacking::WindowStack,
|
||||
pub(crate) window_borders: HashMap<WindowId, crate::winit::WindowBorderBuffers>,
|
||||
pub(crate) window_shadows: HashMap<WindowId, crate::winit::WindowShadowBuffers>,
|
||||
@@ -446,6 +447,7 @@ impl Ferese {
|
||||
output_identity_ids: HashMap::new(),
|
||||
window_ids: HashMap::new(),
|
||||
window_geometry: HashMap::new(),
|
||||
maximized_windows: HashSet::new(),
|
||||
window_stack: crate::stacking::WindowStack::default(),
|
||||
window_borders: HashMap::new(),
|
||||
window_shadows: HashMap::new(),
|
||||
@@ -619,6 +621,9 @@ impl Ferese {
|
||||
let map = layer_map_for_output(output);
|
||||
|
||||
for requested in layers {
|
||||
if *requested == Layer::Top && self.output_has_fullscreen(output) {
|
||||
continue;
|
||||
}
|
||||
for layer in map.layers_on(*requested).rev() {
|
||||
let geometry = map.layer_geometry(layer)?;
|
||||
let layer_position = output_position - geometry.loc.to_f64();
|
||||
@@ -901,6 +906,7 @@ impl Ferese {
|
||||
|
||||
self.space.unmap_elem(window);
|
||||
self.window_geometry.remove(&id);
|
||||
self.maximized_windows.remove(&id);
|
||||
self.window_stack.remove(id);
|
||||
self.window_borders.remove(&id);
|
||||
self.window_shadows.remove(&id);
|
||||
@@ -996,8 +1002,12 @@ impl Ferese {
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_maximized =
|
||||
self.maximized_windows.contains(id) && workspace_fullscreen != Some(*id);
|
||||
let rect = if workspace_fullscreen == Some(*id) {
|
||||
fullscreen_bounds
|
||||
} else if is_maximized {
|
||||
maximized_rect(bounds, self.gap_config.outer)
|
||||
} else {
|
||||
match self.workspaces.placement(*id) {
|
||||
Some(WindowPlacement::Tiled) => {
|
||||
@@ -1017,7 +1027,7 @@ impl Ferese {
|
||||
);
|
||||
|
||||
visible.insert(*id);
|
||||
let scrolling = if !is_fullscreen && !is_floating {
|
||||
let scrolling = if !is_fullscreen && !is_maximized && !is_floating {
|
||||
viewport_target
|
||||
.zip(viewport_current)
|
||||
.map(|(target, current)| (workspace_id, rect.x + target, current))
|
||||
@@ -1030,6 +1040,7 @@ impl Ferese {
|
||||
*id,
|
||||
rect,
|
||||
is_fullscreen,
|
||||
is_maximized,
|
||||
is_floating,
|
||||
scrolling,
|
||||
pending_column_width_cycles.contains(id) && viewport_target_changed,
|
||||
@@ -1053,7 +1064,9 @@ impl Ferese {
|
||||
let mut scrolling_world_x = HashMap::new();
|
||||
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.
|
||||
if scrolling.is_none() {
|
||||
self.viewport_coupled_widths.remove(&id);
|
||||
@@ -1064,7 +1077,14 @@ impl Ferese {
|
||||
.window_geometry
|
||||
.entry(id)
|
||||
.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 {
|
||||
geometry.advance(Duration::ZERO, self.spring_config, false);
|
||||
requested_size = geometry.presentation_size_request(now).or(requested_size);
|
||||
@@ -1132,6 +1152,11 @@ impl Ferese {
|
||||
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_changed = if tiled {
|
||||
state.states.set(xdg_toplevel::State::TiledLeft)
|
||||
@@ -1145,7 +1170,7 @@ impl Ferese {
|
||||
| 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
|
||||
} else {
|
||||
DecorationMode::ServerSide
|
||||
@@ -1153,7 +1178,7 @@ impl Ferese {
|
||||
let decoration_changed = 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 {
|
||||
@@ -1600,6 +1625,42 @@ impl Ferese {
|
||||
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) {
|
||||
let Some(focused) = self.focused_window else {
|
||||
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 {
|
||||
let width = (bounds.width * 0.6).max(1.0);
|
||||
let height = (bounds.height * 0.6).max(1.0);
|
||||
@@ -2017,6 +2090,20 @@ impl ClientData for ClientState {
|
||||
mod tests {
|
||||
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]
|
||||
fn transient_geometry_is_centered_and_bounded_by_its_parent() {
|
||||
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},
|
||||
output::{Mode, Output, PhysicalProperties, Scale, Subpixel},
|
||||
reexports::calloop::EventLoop,
|
||||
reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind as PresentationKind,
|
||||
reexports::wayland_server::Resource,
|
||||
reexports::calloop::EventLoop,
|
||||
utils::{
|
||||
Buffer, Clock, Logical, Monotonic, Physical, Point, Rectangle, Scale as RenderScale, Size,
|
||||
Transform,
|
||||
@@ -871,12 +871,12 @@ fn output_elements(
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
elements.extend(layer_elements(
|
||||
state,
|
||||
renderer,
|
||||
output,
|
||||
&[Layer::Overlay, Layer::Top],
|
||||
));
|
||||
let upper_layers: &[Layer] = if state.output_has_fullscreen(output) {
|
||||
&[Layer::Overlay]
|
||||
} else {
|
||||
&[Layer::Overlay, Layer::Top]
|
||||
};
|
||||
elements.extend(layer_elements(state, renderer, output, upper_layers));
|
||||
let windows = state
|
||||
.space
|
||||
.elements()
|
||||
@@ -1452,30 +1452,26 @@ fn layer_elements(
|
||||
|
||||
let mut elements = Vec::new();
|
||||
for (geometry, layer) in layers {
|
||||
let target = geometry.to_physical_precise_round(scale);
|
||||
let surface_elements = AsRenderElements::<GlesRenderer>::render_elements::<
|
||||
WaylandSurfaceRenderElement<GlesRenderer>,
|
||||
>(&layer, renderer, target.loc, scale.into(), 1.0);
|
||||
let source_bounds = surface_elements
|
||||
.iter()
|
||||
.map(|element| element.geometry(scale.into()))
|
||||
.reduce(Rectangle::merge);
|
||||
let content_scale = source_bounds
|
||||
.map(|source| layer_content_scale(source.size, target.size))
|
||||
.unwrap_or_else(|| RenderScale::from(1.0));
|
||||
let scale_origin = source_bounds.map_or(target.loc, |source| source.loc);
|
||||
let crop = target.intersection(output_crop).unwrap_or_default();
|
||||
elements.extend(
|
||||
AsRenderElements::<GlesRenderer>::render_elements::<
|
||||
WaylandSurfaceRenderElement<GlesRenderer>,
|
||||
>(
|
||||
&layer,
|
||||
renderer,
|
||||
geometry.loc.to_physical_precise_round(scale),
|
||||
scale.into(),
|
||||
1.0,
|
||||
)
|
||||
.into_iter()
|
||||
.filter_map(|element| {
|
||||
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| {
|
||||
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)
|
||||
}));
|
||||
CropRenderElement::from_element(element, scale, output_crop).map(Into::into)
|
||||
}),
|
||||
);
|
||||
|
||||
if let Some(material) = material_element(state, renderer, output, &layer, geometry) {
|
||||
elements.push(material);
|
||||
@@ -1485,20 +1481,6 @@ fn layer_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(
|
||||
state: &mut Ferese,
|
||||
renderer: &mut GlesRenderer,
|
||||
@@ -1519,7 +1501,8 @@ fn material_element(
|
||||
expanded_blur_region(geometry, material.blur.ceil() as i32, output_logical);
|
||||
let sample_physical = sample_geometry.to_physical_precise_round(scale);
|
||||
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 {
|
||||
geometry,
|
||||
tint,
|
||||
@@ -1831,8 +1814,7 @@ mod tests {
|
||||
|
||||
use super::{
|
||||
color_with_alpha, expanded_blur_region, framebuffer_capture_rect, framebuffer_clip_rect,
|
||||
layer_content_scale, normalized_scale, rounded_visual_rect, scaled_visual_rect,
|
||||
shadow_bounds,
|
||||
normalized_scale, rounded_visual_rect, scaled_visual_rect, shadow_bounds,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -1885,21 +1867,6 @@ mod tests {
|
||||
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]
|
||||
fn clip_rect_matches_smithay_gles_projection() {
|
||||
let geometry = Rectangle::<i32, Physical>::new((10, 5).into(), (30, 40).into());
|
||||
|
||||
Reference in new issue
Block a user