feat: add floating and fullscreen modes
This commit is contained in:
1 parent
28d845ee64
commit
9ad6761c4d
5 files changed
+471
-33
No files matched your search
@@ -152,6 +152,32 @@ impl XdgShellHandler for Ferese {
|
||||
);
|
||||
}
|
||||
|
||||
fn fullscreen_request(
|
||||
&mut self,
|
||||
surface: ToplevelSurface,
|
||||
_output: Option<smithay::reexports::wayland_server::protocol::wl_output::WlOutput>,
|
||||
) {
|
||||
if let Some(window) = 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_fullscreen(window, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
|
||||
if let Some(window) = 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_fullscreen(window, false);
|
||||
}
|
||||
}
|
||||
|
||||
fn grab(&mut self, surface: PopupSurface, seat: wl_seat::WlSeat, serial: Serial) {
|
||||
let Some(seat) = Seat::from_resource(&seat) else {
|
||||
return;
|
||||
|
||||
@@ -84,6 +84,22 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
FilterResult::Intercept(())
|
||||
} else if !modifiers.shift
|
||||
&& matches!(symbol, keysyms::KEY_f | keysyms::KEY_F)
|
||||
{
|
||||
if state == KeyState::Pressed {
|
||||
data.intercepted_keys.insert(keycode);
|
||||
data.toggle_focused_fullscreen();
|
||||
}
|
||||
|
||||
FilterResult::Intercept(())
|
||||
} else if modifiers.shift && symbol == keysyms::KEY_space {
|
||||
if state == KeyState::Pressed {
|
||||
data.intercepted_keys.insert(keycode);
|
||||
data.toggle_focused_floating();
|
||||
}
|
||||
|
||||
FilterResult::Intercept(())
|
||||
} else {
|
||||
FilterResult::Forward
|
||||
|
||||
+123
-18
@@ -6,7 +6,7 @@ use std::{
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use ferese_core::WorkspaceSet;
|
||||
use ferese_core::{WindowPlacement, WorkspaceSet};
|
||||
use ferese_layout::{Axis, Direction, GapConfig, Rect, WindowId};
|
||||
|
||||
use smithay::{
|
||||
@@ -14,6 +14,7 @@ use smithay::{
|
||||
input::{Seat, SeatState},
|
||||
reexports::{
|
||||
calloop::{EventLoop, Interest, LoopSignal, Mode, PostAction, generic::Generic},
|
||||
wayland_protocols::xdg::shell::server::xdg_toplevel,
|
||||
wayland_server::{
|
||||
Display, DisplayHandle,
|
||||
backend::{ClientData, ClientId, DisconnectReason},
|
||||
@@ -168,6 +169,7 @@ impl Ferese {
|
||||
pub fn add_tiled_window(&mut self, window: Window) {
|
||||
let id = WindowId(self.next_window_id);
|
||||
self.next_window_id += 1;
|
||||
let focus_new_window = self.workspaces.active().fullscreen.is_none();
|
||||
|
||||
let axis = self
|
||||
.output_bounds()
|
||||
@@ -186,8 +188,10 @@ impl Ferese {
|
||||
}
|
||||
|
||||
self.window_ids.insert(window.clone(), id);
|
||||
if focus_new_window {
|
||||
self.focused_window = Some(id);
|
||||
self.space.map_element(window, (0, 0), true);
|
||||
}
|
||||
self.space.map_element(window, (0, 0), focus_new_window);
|
||||
self.relayout();
|
||||
}
|
||||
|
||||
@@ -211,19 +215,7 @@ impl Ferese {
|
||||
let Some(bounds) = self.output_bounds() else {
|
||||
return;
|
||||
};
|
||||
let active = self.workspaces.active_id();
|
||||
let inactive = self
|
||||
.window_ids
|
||||
.iter()
|
||||
.filter(|(_, id)| self.workspaces.workspace_for_window(**id) != Some(active))
|
||||
.map(|(window, _)| window.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for window in inactive {
|
||||
self.space.unmap_elem(&window);
|
||||
}
|
||||
|
||||
let geometry = match self
|
||||
let tiled_geometry = match self
|
||||
.workspaces
|
||||
.active()
|
||||
.layout
|
||||
@@ -235,13 +227,46 @@ impl Ferese {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let active = self.workspaces.active_id();
|
||||
let fullscreen = self.workspaces.active().fullscreen;
|
||||
let hidden = self
|
||||
.window_ids
|
||||
.iter()
|
||||
.filter(|(_, id)| {
|
||||
self.workspaces.workspace_for_window(**id) != Some(active)
|
||||
|| fullscreen.is_some_and(|fullscreen| fullscreen != **id)
|
||||
})
|
||||
.map(|(window, _)| window.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for window in hidden {
|
||||
self.space.unmap_elem(&window);
|
||||
}
|
||||
|
||||
let placements = self
|
||||
.window_ids
|
||||
.iter()
|
||||
.filter_map(|(window, id)| geometry.get(id).map(|rect| (window.clone(), *rect)))
|
||||
.filter_map(|(window, id)| {
|
||||
if self.workspaces.workspace_for_window(*id) != Some(active) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let rect = if fullscreen == Some(*id) {
|
||||
bounds
|
||||
} else if fullscreen.is_some() {
|
||||
return None;
|
||||
} else {
|
||||
match self.workspaces.placement(*id)? {
|
||||
WindowPlacement::Tiled => *tiled_geometry.get(id)?,
|
||||
WindowPlacement::Floating { rect } => rect,
|
||||
}
|
||||
};
|
||||
|
||||
Some((window.clone(), rect, fullscreen == Some(*id)))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (window, rect) in placements {
|
||||
for (window, rect, is_fullscreen) in placements {
|
||||
let location = (rect.x.round() as i32, rect.y.round() as i32);
|
||||
let size = (
|
||||
(rect.width.round() as i32).max(1),
|
||||
@@ -250,7 +275,15 @@ impl Ferese {
|
||||
|
||||
self.space.map_element(window.clone(), location, false);
|
||||
if let Some(toplevel) = window.toplevel() {
|
||||
toplevel.with_pending_state(|state| state.size = Some(size.into()));
|
||||
toplevel.with_pending_state(|state| {
|
||||
state.size = Some(size.into());
|
||||
|
||||
if is_fullscreen {
|
||||
state.states.set(xdg_toplevel::State::Fullscreen);
|
||||
} else {
|
||||
state.states.unset(xdg_toplevel::State::Fullscreen);
|
||||
}
|
||||
});
|
||||
toplevel.send_pending_configure();
|
||||
}
|
||||
}
|
||||
@@ -347,6 +380,66 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_focused_floating(&mut self) {
|
||||
let Some(window) = self.focused_window else {
|
||||
return;
|
||||
};
|
||||
let Some(bounds) = self.output_bounds() else {
|
||||
return;
|
||||
};
|
||||
let floating_rect = match self.workspaces.placement(window) {
|
||||
Some(WindowPlacement::Tiled) => self
|
||||
.workspaces
|
||||
.active()
|
||||
.layout
|
||||
.geometry_with_gaps(bounds, GapConfig::default())
|
||||
.ok()
|
||||
.and_then(|geometry| geometry.get(&window).copied())
|
||||
.unwrap_or_else(|| centered_floating_rect(bounds)),
|
||||
Some(WindowPlacement::Floating { rect }) => rect,
|
||||
None => return,
|
||||
};
|
||||
let axis = self
|
||||
.workspaces
|
||||
.active()
|
||||
.layout
|
||||
.automatic_axis(Some(window), bounds)
|
||||
.unwrap_or(Axis::Horizontal);
|
||||
|
||||
if let Err(error) = self
|
||||
.workspaces
|
||||
.toggle_floating(window, floating_rect, axis, 0.5)
|
||||
{
|
||||
tracing::error!(%error, ?window, "failed to toggle floating window");
|
||||
return;
|
||||
}
|
||||
|
||||
self.relayout();
|
||||
}
|
||||
|
||||
pub fn toggle_focused_fullscreen(&mut self) {
|
||||
let Some(window) = self.focused_window else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(error) = self.workspaces.toggle_fullscreen(window) {
|
||||
tracing::error!(%error, ?window, "failed to toggle fullscreen window");
|
||||
return;
|
||||
}
|
||||
|
||||
self.relayout();
|
||||
}
|
||||
|
||||
pub fn set_window_fullscreen(&mut self, window: WindowId, enabled: bool) {
|
||||
match self.workspaces.set_fullscreen(window, enabled) {
|
||||
Ok(true) => self.relayout(),
|
||||
Ok(false) => {}
|
||||
Err(error) => {
|
||||
tracing::error!(%error, ?window, enabled, "failed to set fullscreen window")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn switch_workspace(&mut self, index: u32) {
|
||||
let focus = match self.workspaces.switch_to_numeric(index) {
|
||||
Ok(focus) => focus,
|
||||
@@ -419,6 +512,18 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
Rect::new(
|
||||
bounds.x + (bounds.width - width) / 2.0,
|
||||
bounds.y + (bounds.height - height) / 2.0,
|
||||
width,
|
||||
height,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ClientState {
|
||||
pub compositor_state: CompositorClientState,
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
mod workspace;
|
||||
|
||||
pub use workspace::{Workspace, WorkspaceError, WorkspaceId, WorkspaceSet};
|
||||
pub use workspace::{WindowPlacement, Workspace, WorkspaceError, WorkspaceId, WorkspaceSet};
|
||||
@@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
use ferese_layout::{Axis, LayoutError, LayoutTree, WindowId};
|
||||
use ferese_layout::{Axis, LayoutError, LayoutTree, Rect, WindowId};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct WorkspaceId(pub u64);
|
||||
@@ -12,7 +12,15 @@ pub struct Workspace {
|
||||
pub id: WorkspaceId,
|
||||
pub name: String,
|
||||
pub layout: LayoutTree,
|
||||
pub floating: Vec<WindowId>,
|
||||
pub last_focused: Option<WindowId>,
|
||||
pub fullscreen: Option<WindowId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum WindowPlacement {
|
||||
Tiled,
|
||||
Floating { rect: Rect },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -61,6 +69,7 @@ pub struct WorkspaceSet {
|
||||
workspaces: HashMap<WorkspaceId, Workspace>,
|
||||
names: HashMap<String, WorkspaceId>,
|
||||
window_workspaces: HashMap<WindowId, WorkspaceId>,
|
||||
placements: HashMap<WindowId, WindowPlacement>,
|
||||
next_id: u64,
|
||||
}
|
||||
|
||||
@@ -71,7 +80,9 @@ impl Default for WorkspaceSet {
|
||||
id: active,
|
||||
name: "1".to_owned(),
|
||||
layout: LayoutTree::default(),
|
||||
floating: Vec::new(),
|
||||
last_focused: None,
|
||||
fullscreen: None,
|
||||
};
|
||||
let workspaces = HashMap::from([(active, workspace)]);
|
||||
let names = HashMap::from([("1".to_owned(), active)]);
|
||||
@@ -81,6 +92,7 @@ impl Default for WorkspaceSet {
|
||||
workspaces,
|
||||
names,
|
||||
window_workspaces: HashMap::new(),
|
||||
placements: HashMap::new(),
|
||||
next_id: 2,
|
||||
}
|
||||
}
|
||||
@@ -111,12 +123,25 @@ impl WorkspaceSet {
|
||||
self.window_workspaces.get(&window).copied()
|
||||
}
|
||||
|
||||
pub fn placement(&self, window: WindowId) -> Option<WindowPlacement> {
|
||||
self.placements.get(&window).copied()
|
||||
}
|
||||
|
||||
pub fn focus_window(&mut self, window: WindowId) -> Result<(), WorkspaceError> {
|
||||
if self.workspace_for_window(window) != Some(self.active) {
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"focused window is not on the active workspace",
|
||||
));
|
||||
}
|
||||
if self
|
||||
.active()
|
||||
.fullscreen
|
||||
.is_some_and(|fullscreen| fullscreen != window)
|
||||
{
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"focused window is hidden by fullscreen",
|
||||
));
|
||||
}
|
||||
|
||||
self.active_mut().last_focused = Some(window);
|
||||
|
||||
@@ -144,7 +169,9 @@ impl WorkspaceSet {
|
||||
id,
|
||||
name,
|
||||
layout: LayoutTree::default(),
|
||||
floating: Vec::new(),
|
||||
last_focused: None,
|
||||
fullscreen: None,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -156,7 +183,7 @@ impl WorkspaceSet {
|
||||
self.active = self.ensure_numeric(index)?;
|
||||
|
||||
debug_assert!(self.validate().is_ok());
|
||||
Ok(self.active().last_focused)
|
||||
Ok(self.active().fullscreen.or(self.active().last_focused))
|
||||
}
|
||||
|
||||
pub fn insert_window(
|
||||
@@ -173,8 +200,11 @@ impl WorkspaceSet {
|
||||
workspace
|
||||
.layout
|
||||
.insert(window, workspace.last_focused, axis, ratio)?;
|
||||
if workspace.fullscreen.is_none() {
|
||||
workspace.last_focused = Some(window);
|
||||
}
|
||||
self.window_workspaces.insert(window, self.active);
|
||||
self.placements.insert(window, WindowPlacement::Tiled);
|
||||
|
||||
debug_assert!(self.validate().is_ok());
|
||||
Ok(())
|
||||
@@ -190,10 +220,20 @@ impl WorkspaceSet {
|
||||
.get_mut(&workspace_id)
|
||||
.ok_or(WorkspaceError::UnknownWorkspace(workspace_id))?;
|
||||
|
||||
workspace.layout.remove(window)?;
|
||||
match self.placements.remove(&window) {
|
||||
Some(WindowPlacement::Tiled) => workspace.layout.remove(window)?,
|
||||
Some(WindowPlacement::Floating { .. }) => {
|
||||
workspace.floating.retain(|candidate| *candidate != window);
|
||||
}
|
||||
None => return Err(LayoutError::UnknownWindow(window).into()),
|
||||
}
|
||||
|
||||
if workspace.fullscreen == Some(window) {
|
||||
workspace.fullscreen = None;
|
||||
}
|
||||
|
||||
if workspace.last_focused == Some(window) {
|
||||
workspace.last_focused = first_window(&workspace.layout);
|
||||
workspace.last_focused = first_window(workspace);
|
||||
}
|
||||
|
||||
debug_assert!(self.validate().is_ok());
|
||||
@@ -216,30 +256,135 @@ impl WorkspaceSet {
|
||||
return Ok(destination_id);
|
||||
}
|
||||
|
||||
let placement = self
|
||||
.placements
|
||||
.get(&window)
|
||||
.copied()
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let source = self
|
||||
.workspaces
|
||||
.get_mut(&source_id)
|
||||
.ok_or(WorkspaceError::UnknownWorkspace(source_id))?;
|
||||
source.layout.remove(window)?;
|
||||
|
||||
match placement {
|
||||
WindowPlacement::Tiled => source.layout.remove(window)?,
|
||||
WindowPlacement::Floating { .. } => {
|
||||
source.floating.retain(|candidate| *candidate != window);
|
||||
}
|
||||
}
|
||||
|
||||
let was_fullscreen = source.fullscreen == Some(window);
|
||||
if was_fullscreen {
|
||||
source.fullscreen = None;
|
||||
}
|
||||
|
||||
if source.last_focused == Some(window) {
|
||||
source.last_focused = first_window(&source.layout);
|
||||
source.last_focused = first_window(source);
|
||||
}
|
||||
|
||||
let destination = self
|
||||
.workspaces
|
||||
.get_mut(&destination_id)
|
||||
.ok_or(WorkspaceError::UnknownWorkspace(destination_id))?;
|
||||
destination
|
||||
.layout
|
||||
.insert(window, destination.last_focused, axis, ratio)?;
|
||||
match placement {
|
||||
WindowPlacement::Tiled => {
|
||||
let focused = tiled_focus(destination);
|
||||
destination.layout.insert(window, focused, axis, ratio)?;
|
||||
}
|
||||
WindowPlacement::Floating { .. } => destination.floating.push(window),
|
||||
}
|
||||
if was_fullscreen {
|
||||
destination.fullscreen = Some(window);
|
||||
}
|
||||
if destination.fullscreen.is_none() || was_fullscreen {
|
||||
destination.last_focused = Some(window);
|
||||
}
|
||||
self.window_workspaces.insert(window, destination_id);
|
||||
|
||||
debug_assert!(self.validate().is_ok());
|
||||
Ok(destination_id)
|
||||
}
|
||||
|
||||
pub fn toggle_floating(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
floating_rect: Rect,
|
||||
axis: Axis,
|
||||
ratio: f64,
|
||||
) -> Result<WindowPlacement, WorkspaceError> {
|
||||
let workspace_id = self
|
||||
.workspace_for_window(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let workspace = self
|
||||
.workspaces
|
||||
.get_mut(&workspace_id)
|
||||
.ok_or(WorkspaceError::UnknownWorkspace(workspace_id))?;
|
||||
let placement = self
|
||||
.placements
|
||||
.get_mut(&window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
|
||||
*placement = match *placement {
|
||||
WindowPlacement::Tiled => {
|
||||
workspace.layout.remove(window)?;
|
||||
workspace.floating.push(window);
|
||||
WindowPlacement::Floating {
|
||||
rect: normalized_floating_rect(floating_rect),
|
||||
}
|
||||
}
|
||||
WindowPlacement::Floating { .. } => {
|
||||
workspace.floating.retain(|candidate| *candidate != window);
|
||||
let focused = tiled_focus(workspace);
|
||||
workspace.layout.insert(window, focused, axis, ratio)?;
|
||||
WindowPlacement::Tiled
|
||||
}
|
||||
};
|
||||
|
||||
let result = *placement;
|
||||
|
||||
debug_assert!(self.validate().is_ok());
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn toggle_fullscreen(&mut self, window: WindowId) -> Result<bool, WorkspaceError> {
|
||||
let enabled = self
|
||||
.workspace_for_window(window)
|
||||
.and_then(|workspace| self.workspaces.get(&workspace))
|
||||
.is_none_or(|workspace| workspace.fullscreen != Some(window));
|
||||
|
||||
self.set_fullscreen(window, enabled)?;
|
||||
Ok(enabled)
|
||||
}
|
||||
|
||||
pub fn set_fullscreen(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
enabled: bool,
|
||||
) -> Result<bool, WorkspaceError> {
|
||||
let workspace_id = self
|
||||
.workspace_for_window(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let workspace = self
|
||||
.workspaces
|
||||
.get_mut(&workspace_id)
|
||||
.ok_or(WorkspaceError::UnknownWorkspace(workspace_id))?;
|
||||
|
||||
let changed = if enabled {
|
||||
let changed = workspace.fullscreen != Some(window);
|
||||
workspace.fullscreen = Some(window);
|
||||
workspace.last_focused = Some(window);
|
||||
changed
|
||||
} else if workspace.fullscreen == Some(window) {
|
||||
workspace.fullscreen = None;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
debug_assert!(self.validate().is_ok());
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), WorkspaceError> {
|
||||
if !self.workspaces.contains_key(&self.active) {
|
||||
return Err(WorkspaceError::InvalidState("active workspace is missing"));
|
||||
@@ -262,24 +407,55 @@ impl WorkspaceSet {
|
||||
workspace.layout.validate()?;
|
||||
|
||||
for window in workspace.layout.window_ids() {
|
||||
if !seen_windows.insert(window) || self.window_workspaces.get(&window) != Some(id) {
|
||||
if !seen_windows.insert(window)
|
||||
|| self.window_workspaces.get(&window) != Some(id)
|
||||
|| self.placements.get(&window) != Some(&WindowPlacement::Tiled)
|
||||
{
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"window ownership is inconsistent",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut seen_floating = HashSet::new();
|
||||
|
||||
for window in &workspace.floating {
|
||||
if !seen_floating.insert(*window)
|
||||
|| !seen_windows.insert(*window)
|
||||
|| self.window_workspaces.get(window) != Some(id)
|
||||
|| !matches!(
|
||||
self.placements.get(window),
|
||||
Some(WindowPlacement::Floating { .. })
|
||||
)
|
||||
{
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"floating window ownership is inconsistent",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if workspace
|
||||
.last_focused
|
||||
.is_some_and(|window| !workspace.layout.contains(window))
|
||||
.is_some_and(|window| self.window_workspaces.get(&window) != Some(id))
|
||||
{
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"workspace focus is inconsistent",
|
||||
));
|
||||
}
|
||||
|
||||
if workspace
|
||||
.fullscreen
|
||||
.is_some_and(|window| self.window_workspaces.get(&window) != Some(id))
|
||||
{
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"fullscreen window ownership is inconsistent",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if seen_windows.len() != self.window_workspaces.len() {
|
||||
if seen_windows.len() != self.window_workspaces.len()
|
||||
|| self.placements.len() != self.window_workspaces.len()
|
||||
{
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"window index retains stale entries",
|
||||
));
|
||||
@@ -289,8 +465,32 @@ impl WorkspaceSet {
|
||||
}
|
||||
}
|
||||
|
||||
fn first_window(layout: &LayoutTree) -> Option<WindowId> {
|
||||
layout.window_ids().min_by_key(|window| window.0)
|
||||
fn first_window(workspace: &Workspace) -> Option<WindowId> {
|
||||
workspace
|
||||
.layout
|
||||
.window_ids()
|
||||
.chain(workspace.floating.iter().copied())
|
||||
.min_by_key(|window| window.0)
|
||||
}
|
||||
|
||||
fn tiled_focus(workspace: &Workspace) -> Option<WindowId> {
|
||||
workspace
|
||||
.last_focused
|
||||
.filter(|window| workspace.layout.contains(*window))
|
||||
.or_else(|| workspace.layout.window_ids().min_by_key(|window| window.0))
|
||||
}
|
||||
|
||||
fn normalized_floating_rect(rect: Rect) -> Rect {
|
||||
Rect::new(
|
||||
finite_or_zero(rect.x),
|
||||
finite_or_zero(rect.y),
|
||||
finite_or_zero(rect.width).max(1.0),
|
||||
finite_or_zero(rect.height).max(1.0),
|
||||
)
|
||||
}
|
||||
|
||||
fn finite_or_zero(value: f64) -> f64 {
|
||||
if value.is_finite() { value } else { 0.0 }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -392,4 +592,95 @@ mod tests {
|
||||
);
|
||||
assert!(workspaces.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn floating_toggle_removes_and_restores_tiled_membership() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
let rect = Rect::new(10.0, 20.0, 640.0, 480.0);
|
||||
|
||||
assert_eq!(
|
||||
workspaces
|
||||
.toggle_floating(WindowId(1), rect, Axis::Horizontal, 0.5)
|
||||
.unwrap(),
|
||||
WindowPlacement::Floating { rect }
|
||||
);
|
||||
assert!(!workspaces.active().layout.contains(WindowId(1)));
|
||||
assert_eq!(workspaces.active().floating, vec![WindowId(1)]);
|
||||
|
||||
assert_eq!(
|
||||
workspaces
|
||||
.toggle_floating(WindowId(1), rect, Axis::Horizontal, 0.5)
|
||||
.unwrap(),
|
||||
WindowPlacement::Tiled
|
||||
);
|
||||
assert!(workspaces.active().layout.contains(WindowId(1)));
|
||||
assert!(workspaces.active().floating.is_empty());
|
||||
assert!(workspaces.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fullscreen_preserves_underlying_placement() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
let before = workspaces.placement(WindowId(1));
|
||||
|
||||
assert!(workspaces.toggle_fullscreen(WindowId(1)).unwrap());
|
||||
assert_eq!(workspaces.active().fullscreen, Some(WindowId(1)));
|
||||
assert_eq!(workspaces.placement(WindowId(1)), before);
|
||||
assert!(!workspaces.set_fullscreen(WindowId(1), true).unwrap());
|
||||
|
||||
assert!(!workspaces.toggle_fullscreen(WindowId(1)).unwrap());
|
||||
assert_eq!(workspaces.active().fullscreen, None);
|
||||
assert_eq!(workspaces.placement(WindowId(1)), before);
|
||||
assert!(!workspaces.set_fullscreen(WindowId(1), false).unwrap());
|
||||
assert!(workspaces.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapping_under_fullscreen_does_not_change_focus() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.toggle_fullscreen(WindowId(1)).unwrap();
|
||||
workspaces
|
||||
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(workspaces.active().last_focused, Some(WindowId(1)));
|
||||
assert_eq!(workspaces.active().fullscreen, Some(WindowId(1)));
|
||||
assert!(workspaces.active().layout.contains(WindowId(2)));
|
||||
assert!(workspaces.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switching_to_fullscreen_restores_the_visible_window() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.toggle_fullscreen(WindowId(1)).unwrap();
|
||||
workspaces.switch_to_numeric(2).unwrap();
|
||||
workspaces
|
||||
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces
|
||||
.move_window_to_numeric(WindowId(2), 1, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(workspaces.switch_to_numeric(1).unwrap(), Some(WindowId(1)));
|
||||
assert_eq!(workspaces.active().last_focused, Some(WindowId(1)));
|
||||
assert_eq!(
|
||||
workspaces.focus_window(WindowId(2)),
|
||||
Err(WorkspaceError::InvalidState(
|
||||
"focused window is hidden by fullscreen"
|
||||
))
|
||||
);
|
||||
assert!(workspaces.validate().is_ok());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user