mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-22 02:01:55 +07:00
1076 lines
34 KiB
Rust
1076 lines
34 KiB
Rust
use std::cmp::min;
|
|
use std::iter::zip;
|
|
use std::rc::Rc;
|
|
use std::time::Duration;
|
|
|
|
use smithay::backend::renderer::element::utils::{
|
|
CropRenderElement, Relocate, RelocateRenderElement,
|
|
};
|
|
use smithay::output::Output;
|
|
use smithay::utils::{Logical, Point, Rectangle, Size};
|
|
|
|
use super::scrolling::{Column, ColumnWidth};
|
|
use super::tile::Tile;
|
|
use super::workspace::{
|
|
OutputId, Workspace, WorkspaceAddWindowTarget, WorkspaceId, WorkspaceRenderElement,
|
|
};
|
|
use super::{ActivateWindow, HitType, LayoutElement, Options};
|
|
use crate::animation::{Animation, Clock};
|
|
use crate::input::swipe_tracker::SwipeTracker;
|
|
use crate::render_helpers::renderer::NiriRenderer;
|
|
use crate::render_helpers::RenderTarget;
|
|
use crate::rubber_band::RubberBand;
|
|
use crate::utils::transaction::Transaction;
|
|
use crate::utils::{output_size, ResizeEdge};
|
|
|
|
/// Amount of touchpad movement to scroll the height of one workspace.
|
|
const WORKSPACE_GESTURE_MOVEMENT: f64 = 300.;
|
|
|
|
const WORKSPACE_GESTURE_RUBBER_BAND: RubberBand = RubberBand {
|
|
stiffness: 0.5,
|
|
limit: 0.05,
|
|
};
|
|
|
|
#[derive(Debug)]
|
|
pub struct Monitor<W: LayoutElement> {
|
|
/// Output for this monitor.
|
|
pub(super) output: Output,
|
|
/// Cached name of the output.
|
|
output_name: String,
|
|
// Must always contain at least one.
|
|
pub(super) workspaces: Vec<Workspace<W>>,
|
|
/// Index of the currently active workspace.
|
|
pub(super) active_workspace_idx: usize,
|
|
/// ID of the previously active workspace.
|
|
pub(super) previous_workspace_id: Option<WorkspaceId>,
|
|
/// In-progress switch between workspaces.
|
|
pub(super) workspace_switch: Option<WorkspaceSwitch>,
|
|
/// Clock for driving animations.
|
|
pub(super) clock: Clock,
|
|
/// Configurable properties of the layout.
|
|
pub(super) options: Rc<Options>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum WorkspaceSwitch {
|
|
Animation(Animation),
|
|
Gesture(WorkspaceSwitchGesture),
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct WorkspaceSwitchGesture {
|
|
/// Index of the workspace where the gesture was started.
|
|
center_idx: usize,
|
|
/// Fractional workspace index where the gesture was started.
|
|
///
|
|
/// Can differ from center_idx when starting a gesture in the middle between workspaces, for
|
|
/// example by "catching" an animation.
|
|
start_idx: f64,
|
|
/// Current, fractional workspace index.
|
|
pub(super) current_idx: f64,
|
|
tracker: SwipeTracker,
|
|
/// Whether the gesture is controlled by the touchpad.
|
|
is_touchpad: bool,
|
|
}
|
|
|
|
/// Where to put a newly added window.
|
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
|
pub enum MonitorAddWindowTarget<'a, W: LayoutElement> {
|
|
/// No particular preference.
|
|
#[default]
|
|
Auto,
|
|
/// On this workspace.
|
|
Workspace {
|
|
/// Id of the target workspace.
|
|
id: WorkspaceId,
|
|
/// Override where the window will open as a new column.
|
|
column_idx: Option<usize>,
|
|
},
|
|
/// Next to this existing window.
|
|
NextTo(&'a W::Id),
|
|
}
|
|
|
|
pub type MonitorRenderElement<R> =
|
|
RelocateRenderElement<CropRenderElement<WorkspaceRenderElement<R>>>;
|
|
|
|
impl WorkspaceSwitch {
|
|
pub fn current_idx(&self) -> f64 {
|
|
match self {
|
|
WorkspaceSwitch::Animation(anim) => anim.value(),
|
|
WorkspaceSwitch::Gesture(gesture) => gesture.current_idx,
|
|
}
|
|
}
|
|
|
|
pub fn target_idx(&self) -> f64 {
|
|
match self {
|
|
WorkspaceSwitch::Animation(anim) => anim.to(),
|
|
WorkspaceSwitch::Gesture(gesture) => gesture.current_idx,
|
|
}
|
|
}
|
|
|
|
pub fn offset(&mut self, delta: isize) {
|
|
match self {
|
|
WorkspaceSwitch::Animation(anim) => anim.offset(delta as f64),
|
|
WorkspaceSwitch::Gesture(gesture) => {
|
|
if delta >= 0 {
|
|
gesture.center_idx += delta as usize;
|
|
} else {
|
|
gesture.center_idx -= (-delta) as usize;
|
|
}
|
|
gesture.start_idx += delta as f64;
|
|
gesture.current_idx += delta as f64;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if the workspace switch is [`Animation`].
|
|
///
|
|
/// [`Animation`]: WorkspaceSwitch::Animation
|
|
#[must_use]
|
|
fn is_animation(&self) -> bool {
|
|
matches!(self, Self::Animation(..))
|
|
}
|
|
}
|
|
|
|
impl WorkspaceSwitchGesture {
|
|
fn min_max(&self, workspace_count: usize) -> (f64, f64) {
|
|
let min = self.center_idx.saturating_sub(1) as f64;
|
|
let max = (self.center_idx + 1).min(workspace_count - 1) as f64;
|
|
(min, max)
|
|
}
|
|
}
|
|
|
|
impl<W: LayoutElement> Monitor<W> {
|
|
pub fn new(
|
|
output: Output,
|
|
workspaces: Vec<Workspace<W>>,
|
|
clock: Clock,
|
|
options: Rc<Options>,
|
|
) -> Self {
|
|
Self {
|
|
output_name: output.name(),
|
|
output,
|
|
workspaces,
|
|
active_workspace_idx: 0,
|
|
previous_workspace_id: None,
|
|
workspace_switch: None,
|
|
clock,
|
|
options,
|
|
}
|
|
}
|
|
|
|
pub fn output(&self) -> &Output {
|
|
&self.output
|
|
}
|
|
|
|
pub fn output_name(&self) -> &String {
|
|
&self.output_name
|
|
}
|
|
|
|
pub fn active_workspace_idx(&self) -> usize {
|
|
self.active_workspace_idx
|
|
}
|
|
|
|
pub fn active_workspace_ref(&self) -> &Workspace<W> {
|
|
&self.workspaces[self.active_workspace_idx]
|
|
}
|
|
|
|
pub fn find_named_workspace(&self, workspace_name: &str) -> Option<&Workspace<W>> {
|
|
self.workspaces.iter().find(|ws| {
|
|
ws.name
|
|
.as_ref()
|
|
.is_some_and(|name| name.eq_ignore_ascii_case(workspace_name))
|
|
})
|
|
}
|
|
|
|
pub fn find_named_workspace_index(&self, workspace_name: &str) -> Option<usize> {
|
|
self.workspaces.iter().position(|ws| {
|
|
ws.name
|
|
.as_ref()
|
|
.is_some_and(|name| name.eq_ignore_ascii_case(workspace_name))
|
|
})
|
|
}
|
|
|
|
pub fn active_workspace(&mut self) -> &mut Workspace<W> {
|
|
&mut self.workspaces[self.active_workspace_idx]
|
|
}
|
|
|
|
pub fn windows(&self) -> impl Iterator<Item = &W> {
|
|
self.workspaces.iter().flat_map(|ws| ws.windows())
|
|
}
|
|
|
|
pub fn has_window(&self, window: &W::Id) -> bool {
|
|
self.windows().any(|win| win.id() == window)
|
|
}
|
|
|
|
pub fn add_workspace_top(&mut self) {
|
|
let ws = Workspace::new(
|
|
self.output.clone(),
|
|
self.clock.clone(),
|
|
self.options.clone(),
|
|
);
|
|
self.workspaces.insert(0, ws);
|
|
self.active_workspace_idx += 1;
|
|
|
|
if let Some(switch) = &mut self.workspace_switch {
|
|
switch.offset(1);
|
|
}
|
|
}
|
|
|
|
pub fn add_workspace_bottom(&mut self) {
|
|
let ws = Workspace::new(
|
|
self.output.clone(),
|
|
self.clock.clone(),
|
|
self.options.clone(),
|
|
);
|
|
self.workspaces.push(ws);
|
|
}
|
|
|
|
fn activate_workspace(&mut self, idx: usize) {
|
|
if self.active_workspace_idx == idx {
|
|
return;
|
|
}
|
|
|
|
// FIXME: also compute and use current velocity.
|
|
let current_idx = self
|
|
.workspace_switch
|
|
.as_ref()
|
|
.map(|s| s.current_idx())
|
|
.unwrap_or(self.active_workspace_idx as f64);
|
|
|
|
self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id());
|
|
|
|
self.active_workspace_idx = idx;
|
|
|
|
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
|
|
self.clock.clone(),
|
|
current_idx,
|
|
idx as f64,
|
|
0.,
|
|
self.options.animations.workspace_switch.0,
|
|
)));
|
|
}
|
|
|
|
pub fn add_window(
|
|
&mut self,
|
|
window: W,
|
|
target: MonitorAddWindowTarget<W>,
|
|
activate: ActivateWindow,
|
|
width: ColumnWidth,
|
|
is_full_width: bool,
|
|
is_floating: bool,
|
|
) {
|
|
// Currently, everything a workspace sets on a Tile is the same across all workspaces of a
|
|
// monitor. So we can use any workspace, not necessarily the exact target workspace.
|
|
let tile = self.workspaces[0].make_tile(window);
|
|
|
|
self.add_tile(tile, target, activate, width, is_full_width, is_floating);
|
|
}
|
|
|
|
pub fn add_column(&mut self, mut workspace_idx: usize, column: Column<W>, activate: bool) {
|
|
let workspace = &mut self.workspaces[workspace_idx];
|
|
|
|
workspace.add_column(column, activate);
|
|
|
|
// After adding a new window, workspace becomes this output's own.
|
|
if workspace.name().is_none() {
|
|
workspace.original_output = OutputId::new(&self.output);
|
|
}
|
|
|
|
if workspace_idx == self.workspaces.len() - 1 {
|
|
self.add_workspace_bottom();
|
|
}
|
|
if self.options.empty_workspace_above_first && workspace_idx == 0 {
|
|
self.add_workspace_top();
|
|
workspace_idx += 1;
|
|
}
|
|
|
|
if activate {
|
|
self.activate_workspace(workspace_idx);
|
|
}
|
|
}
|
|
|
|
pub fn add_tile(
|
|
&mut self,
|
|
tile: Tile<W>,
|
|
target: MonitorAddWindowTarget<W>,
|
|
activate: ActivateWindow,
|
|
width: ColumnWidth,
|
|
is_full_width: bool,
|
|
is_floating: bool,
|
|
) {
|
|
let (mut workspace_idx, target) = match target {
|
|
MonitorAddWindowTarget::Auto => {
|
|
(self.active_workspace_idx, WorkspaceAddWindowTarget::Auto)
|
|
}
|
|
MonitorAddWindowTarget::Workspace { id, column_idx } => {
|
|
let idx = self.workspaces.iter().position(|ws| ws.id() == id).unwrap();
|
|
let target = if let Some(column_idx) = column_idx {
|
|
WorkspaceAddWindowTarget::NewColumnAt(column_idx)
|
|
} else {
|
|
WorkspaceAddWindowTarget::Auto
|
|
};
|
|
(idx, target)
|
|
}
|
|
MonitorAddWindowTarget::NextTo(win_id) => {
|
|
let idx = self
|
|
.workspaces
|
|
.iter_mut()
|
|
.position(|ws| ws.has_window(win_id))
|
|
.unwrap();
|
|
(idx, WorkspaceAddWindowTarget::NextTo(win_id))
|
|
}
|
|
};
|
|
|
|
let workspace = &mut self.workspaces[workspace_idx];
|
|
|
|
workspace.add_tile(tile, target, activate, width, is_full_width, is_floating);
|
|
|
|
// After adding a new window, workspace becomes this output's own.
|
|
if workspace.name().is_none() {
|
|
workspace.original_output = OutputId::new(&self.output);
|
|
}
|
|
|
|
if workspace_idx == self.workspaces.len() - 1 {
|
|
// Insert a new empty workspace.
|
|
self.add_workspace_bottom();
|
|
}
|
|
|
|
if self.options.empty_workspace_above_first && workspace_idx == 0 {
|
|
self.add_workspace_top();
|
|
workspace_idx += 1;
|
|
}
|
|
|
|
if activate.map_smart(|| false) {
|
|
self.activate_workspace(workspace_idx);
|
|
}
|
|
}
|
|
|
|
pub fn add_tile_to_column(
|
|
&mut self,
|
|
workspace_idx: usize,
|
|
column_idx: usize,
|
|
tile_idx: Option<usize>,
|
|
tile: Tile<W>,
|
|
activate: bool,
|
|
) {
|
|
let workspace = &mut self.workspaces[workspace_idx];
|
|
|
|
workspace.add_tile_to_column(column_idx, tile_idx, tile, activate);
|
|
|
|
// After adding a new window, workspace becomes this output's own.
|
|
if workspace.name().is_none() {
|
|
workspace.original_output = OutputId::new(&self.output);
|
|
}
|
|
|
|
// Since we're adding window to an existing column, the workspace isn't empty, and
|
|
// therefore cannot be the last one, so we never need to insert a new empty workspace.
|
|
|
|
if activate {
|
|
self.activate_workspace(workspace_idx);
|
|
}
|
|
}
|
|
|
|
pub fn clean_up_workspaces(&mut self) {
|
|
assert!(self.workspace_switch.is_none());
|
|
|
|
let range_start = if self.options.empty_workspace_above_first {
|
|
1
|
|
} else {
|
|
0
|
|
};
|
|
for idx in (range_start..self.workspaces.len() - 1).rev() {
|
|
if self.active_workspace_idx == idx {
|
|
continue;
|
|
}
|
|
|
|
if !self.workspaces[idx].has_windows_or_name() {
|
|
self.workspaces.remove(idx);
|
|
if self.active_workspace_idx > idx {
|
|
self.active_workspace_idx -= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Special case handling when empty_workspace_above_first is set and all workspaces
|
|
// are empty.
|
|
if self.options.empty_workspace_above_first && self.workspaces.len() == 2 {
|
|
assert!(!self.workspaces[0].has_windows_or_name());
|
|
assert!(!self.workspaces[1].has_windows_or_name());
|
|
self.workspaces.remove(1);
|
|
self.active_workspace_idx = 0;
|
|
}
|
|
}
|
|
|
|
pub fn unname_workspace(&mut self, id: WorkspaceId) -> bool {
|
|
let Some(ws) = self.workspaces.iter_mut().find(|ws| ws.id() == id) else {
|
|
return false;
|
|
};
|
|
|
|
ws.unname();
|
|
true
|
|
}
|
|
|
|
pub fn move_down_or_to_workspace_down(&mut self) {
|
|
if !self.active_workspace().move_down() {
|
|
self.move_to_workspace_down();
|
|
}
|
|
}
|
|
|
|
pub fn move_up_or_to_workspace_up(&mut self) {
|
|
if !self.active_workspace().move_up() {
|
|
self.move_to_workspace_up();
|
|
}
|
|
}
|
|
|
|
pub fn focus_window_or_workspace_down(&mut self) {
|
|
if !self.active_workspace().focus_down() {
|
|
self.switch_workspace_down();
|
|
}
|
|
}
|
|
|
|
pub fn focus_window_or_workspace_up(&mut self) {
|
|
if !self.active_workspace().focus_up() {
|
|
self.switch_workspace_up();
|
|
}
|
|
}
|
|
|
|
pub fn move_to_workspace_up(&mut self) {
|
|
let source_workspace_idx = self.active_workspace_idx;
|
|
|
|
let new_idx = source_workspace_idx.saturating_sub(1);
|
|
if new_idx == source_workspace_idx {
|
|
return;
|
|
}
|
|
let new_id = self.workspaces[new_idx].id();
|
|
|
|
let workspace = &mut self.workspaces[source_workspace_idx];
|
|
let Some(removed) = workspace.remove_active_tile(Transaction::new()) else {
|
|
return;
|
|
};
|
|
|
|
self.add_tile(
|
|
removed.tile,
|
|
MonitorAddWindowTarget::Workspace {
|
|
id: new_id,
|
|
column_idx: None,
|
|
},
|
|
ActivateWindow::Yes,
|
|
removed.width,
|
|
removed.is_full_width,
|
|
removed.is_floating,
|
|
);
|
|
}
|
|
|
|
pub fn move_to_workspace_down(&mut self) {
|
|
let source_workspace_idx = self.active_workspace_idx;
|
|
|
|
let new_idx = min(source_workspace_idx + 1, self.workspaces.len() - 1);
|
|
if new_idx == source_workspace_idx {
|
|
return;
|
|
}
|
|
let new_id = self.workspaces[new_idx].id();
|
|
|
|
let workspace = &mut self.workspaces[source_workspace_idx];
|
|
let Some(removed) = workspace.remove_active_tile(Transaction::new()) else {
|
|
return;
|
|
};
|
|
|
|
self.add_tile(
|
|
removed.tile,
|
|
MonitorAddWindowTarget::Workspace {
|
|
id: new_id,
|
|
column_idx: None,
|
|
},
|
|
ActivateWindow::Yes,
|
|
removed.width,
|
|
removed.is_full_width,
|
|
removed.is_floating,
|
|
);
|
|
}
|
|
|
|
pub fn move_to_workspace(
|
|
&mut self,
|
|
window: Option<&W::Id>,
|
|
idx: usize,
|
|
activate: ActivateWindow,
|
|
) {
|
|
let source_workspace_idx = if let Some(window) = window {
|
|
self.workspaces
|
|
.iter()
|
|
.position(|ws| ws.has_window(window))
|
|
.unwrap()
|
|
} else {
|
|
self.active_workspace_idx
|
|
};
|
|
|
|
let new_idx = min(idx, self.workspaces.len() - 1);
|
|
if new_idx == source_workspace_idx {
|
|
return;
|
|
}
|
|
let new_id = self.workspaces[new_idx].id();
|
|
|
|
let activate = activate.map_smart(|| {
|
|
window.map_or(true, |win| {
|
|
self.active_window().map(|win| win.id()) == Some(win)
|
|
})
|
|
});
|
|
|
|
let workspace = &mut self.workspaces[source_workspace_idx];
|
|
let transaction = Transaction::new();
|
|
let removed = if let Some(window) = window {
|
|
workspace.remove_tile(window, transaction)
|
|
} else if let Some(removed) = workspace.remove_active_tile(transaction) {
|
|
removed
|
|
} else {
|
|
return;
|
|
};
|
|
|
|
self.add_tile(
|
|
removed.tile,
|
|
MonitorAddWindowTarget::Workspace {
|
|
id: new_id,
|
|
column_idx: None,
|
|
},
|
|
if activate {
|
|
ActivateWindow::Yes
|
|
} else {
|
|
ActivateWindow::No
|
|
},
|
|
removed.width,
|
|
removed.is_full_width,
|
|
removed.is_floating,
|
|
);
|
|
|
|
if self.workspace_switch.is_none() {
|
|
self.clean_up_workspaces();
|
|
}
|
|
}
|
|
|
|
pub fn move_column_to_workspace_up(&mut self) {
|
|
let source_workspace_idx = self.active_workspace_idx;
|
|
|
|
let new_idx = source_workspace_idx.saturating_sub(1);
|
|
if new_idx == source_workspace_idx {
|
|
return;
|
|
}
|
|
|
|
let workspace = &mut self.workspaces[source_workspace_idx];
|
|
if workspace.floating_is_active() {
|
|
self.move_to_workspace_up();
|
|
return;
|
|
}
|
|
|
|
let Some(column) = workspace.remove_active_column() else {
|
|
return;
|
|
};
|
|
|
|
self.add_column(new_idx, column, true);
|
|
}
|
|
|
|
pub fn move_column_to_workspace_down(&mut self) {
|
|
let source_workspace_idx = self.active_workspace_idx;
|
|
|
|
let new_idx = min(source_workspace_idx + 1, self.workspaces.len() - 1);
|
|
if new_idx == source_workspace_idx {
|
|
return;
|
|
}
|
|
|
|
let workspace = &mut self.workspaces[source_workspace_idx];
|
|
if workspace.floating_is_active() {
|
|
self.move_to_workspace_down();
|
|
return;
|
|
}
|
|
|
|
let Some(column) = workspace.remove_active_column() else {
|
|
return;
|
|
};
|
|
|
|
self.add_column(new_idx, column, true);
|
|
}
|
|
|
|
pub fn move_column_to_workspace(&mut self, idx: usize) {
|
|
let source_workspace_idx = self.active_workspace_idx;
|
|
|
|
let new_idx = min(idx, self.workspaces.len() - 1);
|
|
if new_idx == source_workspace_idx {
|
|
return;
|
|
}
|
|
|
|
let workspace = &mut self.workspaces[source_workspace_idx];
|
|
if workspace.floating_is_active() {
|
|
self.move_to_workspace(None, idx, ActivateWindow::Smart);
|
|
return;
|
|
}
|
|
|
|
let Some(column) = workspace.remove_active_column() else {
|
|
return;
|
|
};
|
|
|
|
self.add_column(new_idx, column, true);
|
|
}
|
|
|
|
pub fn switch_workspace_up(&mut self) {
|
|
self.activate_workspace(self.active_workspace_idx.saturating_sub(1));
|
|
}
|
|
|
|
pub fn switch_workspace_down(&mut self) {
|
|
self.activate_workspace(min(
|
|
self.active_workspace_idx + 1,
|
|
self.workspaces.len() - 1,
|
|
));
|
|
}
|
|
|
|
fn previous_workspace_idx(&self) -> Option<usize> {
|
|
let id = self.previous_workspace_id?;
|
|
self.workspaces.iter().position(|w| w.id() == id)
|
|
}
|
|
|
|
pub fn switch_workspace(&mut self, idx: usize) {
|
|
self.activate_workspace(min(idx, self.workspaces.len() - 1));
|
|
}
|
|
|
|
pub fn switch_workspace_auto_back_and_forth(&mut self, idx: usize) {
|
|
let idx = min(idx, self.workspaces.len() - 1);
|
|
|
|
if idx == self.active_workspace_idx {
|
|
if let Some(prev_idx) = self.previous_workspace_idx() {
|
|
self.switch_workspace(prev_idx);
|
|
}
|
|
} else {
|
|
self.switch_workspace(idx);
|
|
}
|
|
}
|
|
|
|
pub fn switch_workspace_previous(&mut self) {
|
|
if let Some(idx) = self.previous_workspace_idx() {
|
|
self.switch_workspace(idx);
|
|
}
|
|
}
|
|
|
|
pub fn active_window(&self) -> Option<&W> {
|
|
self.active_workspace_ref().active_window()
|
|
}
|
|
|
|
pub fn advance_animations(&mut self) {
|
|
if let Some(WorkspaceSwitch::Animation(anim)) = &mut self.workspace_switch {
|
|
if anim.is_done() {
|
|
self.workspace_switch = None;
|
|
self.clean_up_workspaces();
|
|
}
|
|
}
|
|
|
|
for ws in &mut self.workspaces {
|
|
ws.advance_animations();
|
|
}
|
|
}
|
|
|
|
pub(super) fn are_animations_ongoing(&self) -> bool {
|
|
self.workspace_switch
|
|
.as_ref()
|
|
.is_some_and(|s| s.is_animation())
|
|
|| self.workspaces.iter().any(|ws| ws.are_animations_ongoing())
|
|
}
|
|
|
|
pub fn are_transitions_ongoing(&self) -> bool {
|
|
self.workspace_switch.is_some()
|
|
|| self
|
|
.workspaces
|
|
.iter()
|
|
.any(|ws| ws.are_transitions_ongoing())
|
|
}
|
|
|
|
pub fn update_render_elements(&mut self, is_active: bool) {
|
|
for (ws, _) in self.workspaces_with_render_geo_mut() {
|
|
ws.update_render_elements(is_active);
|
|
}
|
|
}
|
|
|
|
pub fn update_config(&mut self, options: Rc<Options>) {
|
|
if self.options.empty_workspace_above_first != options.empty_workspace_above_first
|
|
&& self.workspaces.len() > 1
|
|
{
|
|
if options.empty_workspace_above_first {
|
|
self.add_workspace_top();
|
|
} else if self.workspace_switch.is_none() && self.active_workspace_idx != 0 {
|
|
self.workspaces.remove(0);
|
|
self.active_workspace_idx = self.active_workspace_idx.saturating_sub(1);
|
|
}
|
|
}
|
|
|
|
for ws in &mut self.workspaces {
|
|
ws.update_config(options.clone());
|
|
}
|
|
|
|
self.options = options;
|
|
}
|
|
|
|
pub fn update_shaders(&mut self) {
|
|
for ws in &mut self.workspaces {
|
|
ws.update_shaders();
|
|
}
|
|
}
|
|
|
|
pub fn move_workspace_down(&mut self) {
|
|
let mut new_idx = min(self.active_workspace_idx + 1, self.workspaces.len() - 1);
|
|
if new_idx == self.active_workspace_idx {
|
|
return;
|
|
}
|
|
|
|
self.workspaces.swap(self.active_workspace_idx, new_idx);
|
|
|
|
if new_idx == self.workspaces.len() - 1 {
|
|
// Insert a new empty workspace.
|
|
self.add_workspace_bottom();
|
|
}
|
|
|
|
if self.options.empty_workspace_above_first && self.active_workspace_idx == 0 {
|
|
self.add_workspace_top();
|
|
new_idx += 1;
|
|
}
|
|
|
|
let previous_workspace_id = self.previous_workspace_id;
|
|
self.activate_workspace(new_idx);
|
|
self.workspace_switch = None;
|
|
self.previous_workspace_id = previous_workspace_id;
|
|
|
|
self.clean_up_workspaces();
|
|
}
|
|
|
|
pub fn move_workspace_up(&mut self) {
|
|
let mut new_idx = self.active_workspace_idx.saturating_sub(1);
|
|
if new_idx == self.active_workspace_idx {
|
|
return;
|
|
}
|
|
|
|
self.workspaces.swap(self.active_workspace_idx, new_idx);
|
|
|
|
if self.active_workspace_idx == self.workspaces.len() - 1 {
|
|
// Insert a new empty workspace.
|
|
self.add_workspace_bottom();
|
|
}
|
|
|
|
if self.options.empty_workspace_above_first && new_idx == 0 {
|
|
self.add_workspace_top();
|
|
new_idx += 1;
|
|
}
|
|
|
|
let previous_workspace_id = self.previous_workspace_id;
|
|
self.activate_workspace(new_idx);
|
|
self.workspace_switch = None;
|
|
self.previous_workspace_id = previous_workspace_id;
|
|
|
|
self.clean_up_workspaces();
|
|
}
|
|
|
|
pub fn move_workspace_to_idx(&mut self, old_idx: usize, new_idx: usize) {
|
|
if self.workspaces.len() <= old_idx {
|
|
return;
|
|
}
|
|
|
|
let mut new_idx = new_idx.clamp(0, self.workspaces.len() - 1);
|
|
if old_idx == new_idx {
|
|
return;
|
|
}
|
|
|
|
let ws = self.workspaces.remove(old_idx);
|
|
self.workspaces.insert(new_idx, ws);
|
|
|
|
if new_idx > old_idx {
|
|
if new_idx == self.workspaces.len() - 1 {
|
|
// Insert a new empty workspace.
|
|
self.add_workspace_bottom();
|
|
}
|
|
|
|
if self.options.empty_workspace_above_first && old_idx == 0 {
|
|
self.add_workspace_top();
|
|
new_idx += 1;
|
|
}
|
|
} else {
|
|
if old_idx == self.workspaces.len() - 1 {
|
|
// Insert a new empty workspace.
|
|
self.add_workspace_bottom();
|
|
}
|
|
|
|
if self.options.empty_workspace_above_first && new_idx == 0 {
|
|
self.add_workspace_top();
|
|
new_idx += 1;
|
|
}
|
|
}
|
|
|
|
// Only refocus the workspace if it was already focused
|
|
if self.active_workspace_idx == old_idx {
|
|
self.active_workspace_idx = new_idx;
|
|
// If the workspace order was switched so that the current workspace moved down the
|
|
// workspace stack, focus correctly
|
|
} else if new_idx <= self.active_workspace_idx && old_idx > self.active_workspace_idx {
|
|
self.active_workspace_idx += 1;
|
|
} else if new_idx >= self.active_workspace_idx && old_idx < self.active_workspace_idx {
|
|
self.active_workspace_idx = self.active_workspace_idx.saturating_sub(1);
|
|
}
|
|
|
|
self.workspace_switch = None;
|
|
|
|
self.clean_up_workspaces();
|
|
}
|
|
|
|
/// Returns the geometry of the active tile relative to and clamped to the output.
|
|
///
|
|
/// During animations, assumes the final view position.
|
|
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
|
|
let mut rect = self.active_workspace_ref().active_tile_visual_rectangle()?;
|
|
|
|
if let Some(switch) = &self.workspace_switch {
|
|
let size = output_size(&self.output).to_f64();
|
|
|
|
let offset = switch.target_idx() - self.active_workspace_idx as f64;
|
|
let offset = offset * size.h;
|
|
|
|
let clip_rect = Rectangle::new(Point::from((0., -offset)), size);
|
|
rect = rect.intersection(clip_rect)?;
|
|
}
|
|
|
|
Some(rect)
|
|
}
|
|
|
|
pub fn workspaces_render_geo(&self) -> impl Iterator<Item = Rectangle<f64, Logical>> {
|
|
let scale = self.output.current_scale().fractional_scale();
|
|
let size = output_size(&self.output);
|
|
|
|
// Ceil the workspace size in physical pixels.
|
|
let ws_size = size.to_physical_precise_ceil(scale).to_logical(scale);
|
|
|
|
let render_idx = if let Some(switch) = &self.workspace_switch {
|
|
switch.current_idx()
|
|
} else {
|
|
self.active_workspace_idx as f64
|
|
};
|
|
|
|
let first_ws_y = -render_idx * ws_size.h;
|
|
|
|
(0..self.workspaces.len()).map(move |idx| {
|
|
let y = first_ws_y + idx as f64 * ws_size.h;
|
|
let loc = Point::from((0., y));
|
|
let loc = loc.to_physical_precise_round(scale).to_logical(scale);
|
|
Rectangle::new(loc, ws_size)
|
|
})
|
|
}
|
|
|
|
pub fn workspaces_with_render_geo(
|
|
&self,
|
|
) -> impl Iterator<Item = (&Workspace<W>, Rectangle<f64, Logical>)> {
|
|
let output_size = output_size(&self.output);
|
|
let output_geo = Rectangle::new(Point::from((0., 0.)), output_size);
|
|
|
|
let geo = self.workspaces_render_geo();
|
|
zip(self.workspaces.iter(), geo)
|
|
// Cull out workspaces outside the output.
|
|
.filter(move |(_ws, geo)| geo.intersection(output_geo).is_some())
|
|
}
|
|
|
|
pub fn workspaces_with_render_geo_mut(
|
|
&mut self,
|
|
) -> impl Iterator<Item = (&mut Workspace<W>, Rectangle<f64, Logical>)> {
|
|
let output_size = output_size(&self.output);
|
|
let output_geo = Rectangle::new(Point::from((0., 0.)), output_size);
|
|
|
|
let geo = self.workspaces_render_geo();
|
|
zip(self.workspaces.iter_mut(), geo)
|
|
// Cull out workspaces outside the output.
|
|
.filter(move |(_ws, geo)| geo.intersection(output_geo).is_some())
|
|
}
|
|
|
|
pub fn workspace_under(
|
|
&self,
|
|
pos_within_output: Point<f64, Logical>,
|
|
) -> Option<(&Workspace<W>, Rectangle<f64, Logical>)> {
|
|
let size = output_size(&self.output);
|
|
let (ws, geo) = self.workspaces_with_render_geo().find_map(|(ws, geo)| {
|
|
// Extend width to entire output.
|
|
let loc = Point::from((0., geo.loc.y));
|
|
let size = Size::from((size.w, geo.size.h));
|
|
let bounds = Rectangle::new(loc, size);
|
|
|
|
bounds.contains(pos_within_output).then_some((ws, geo))
|
|
})?;
|
|
Some((ws, geo))
|
|
}
|
|
|
|
pub fn window_under(&self, pos_within_output: Point<f64, Logical>) -> Option<(&W, HitType)> {
|
|
let (ws, geo) = self.workspace_under(pos_within_output)?;
|
|
let (win, hit) = ws.window_under(pos_within_output - geo.loc)?;
|
|
Some((win, hit.offset_win_pos(geo.loc)))
|
|
}
|
|
|
|
pub fn resize_edges_under(&self, pos_within_output: Point<f64, Logical>) -> Option<ResizeEdge> {
|
|
let (ws, geo) = self.workspace_under(pos_within_output)?;
|
|
ws.resize_edges_under(pos_within_output - geo.loc)
|
|
}
|
|
|
|
pub fn render_above_top_layer(&self) -> bool {
|
|
// Render above the top layer only if the view is stationary.
|
|
if self.workspace_switch.is_some() {
|
|
return false;
|
|
}
|
|
|
|
let ws = &self.workspaces[self.active_workspace_idx];
|
|
ws.render_above_top_layer()
|
|
}
|
|
|
|
pub fn render_elements<'a, R: NiriRenderer>(
|
|
&'a self,
|
|
renderer: &'a mut R,
|
|
target: RenderTarget,
|
|
focus_ring: bool,
|
|
) -> impl Iterator<Item = MonitorRenderElement<R>> + 'a {
|
|
let _span = tracy_client::span!("Monitor::render_elements");
|
|
|
|
let scale = self.output.current_scale().fractional_scale();
|
|
let size = output_size(&self.output);
|
|
// Ceil the height in physical pixels.
|
|
let height = (size.h * scale).ceil() as i32;
|
|
|
|
// Crop the elements to prevent them overflowing, currently visible during a workspace
|
|
// switch.
|
|
//
|
|
// HACK: crop to infinite bounds at least horizontally where we
|
|
// know there's no workspace joining or monitor bounds, otherwise
|
|
// it will cut pixel shaders and mess up the coordinate space.
|
|
// There's also a damage tracking bug which causes glitched
|
|
// rendering for maximized GTK windows.
|
|
//
|
|
// FIXME: use proper bounds after fixing the Crop element.
|
|
let crop_bounds = if self.workspace_switch.is_some() {
|
|
Rectangle::new(
|
|
Point::from((-i32::MAX / 2, 0)),
|
|
Size::from((i32::MAX, height)),
|
|
)
|
|
} else {
|
|
Rectangle::new(
|
|
Point::from((-i32::MAX / 2, -i32::MAX / 2)),
|
|
Size::from((i32::MAX, i32::MAX)),
|
|
)
|
|
};
|
|
|
|
self.workspaces_with_render_geo()
|
|
.flat_map(move |(ws, geo)| {
|
|
ws.render_elements(renderer, target, focus_ring)
|
|
.filter_map(move |elem| {
|
|
CropRenderElement::from_element(elem, scale, crop_bounds)
|
|
})
|
|
.map(move |elem| {
|
|
RelocateRenderElement::from_element(
|
|
elem,
|
|
// The offset we get from workspaces_with_render_positions() is already
|
|
// rounded to physical pixels, but it's in the logical coordinate
|
|
// space, so we need to convert it to physical.
|
|
geo.loc.to_physical_precise_round(scale),
|
|
Relocate::Relative,
|
|
)
|
|
})
|
|
})
|
|
}
|
|
|
|
pub fn workspace_switch_gesture_begin(&mut self, is_touchpad: bool) {
|
|
let center_idx = self.active_workspace_idx;
|
|
let current_idx = self
|
|
.workspace_switch
|
|
.as_ref()
|
|
.map(|s| s.current_idx())
|
|
.unwrap_or(center_idx as f64);
|
|
|
|
let gesture = WorkspaceSwitchGesture {
|
|
center_idx,
|
|
start_idx: current_idx,
|
|
current_idx,
|
|
tracker: SwipeTracker::new(),
|
|
is_touchpad,
|
|
};
|
|
self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture));
|
|
}
|
|
|
|
pub fn workspace_switch_gesture_update(
|
|
&mut self,
|
|
delta_y: f64,
|
|
timestamp: Duration,
|
|
is_touchpad: bool,
|
|
) -> Option<bool> {
|
|
let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else {
|
|
return None;
|
|
};
|
|
|
|
if gesture.is_touchpad != is_touchpad {
|
|
return None;
|
|
}
|
|
|
|
gesture.tracker.push(delta_y, timestamp);
|
|
|
|
let total_height = if gesture.is_touchpad {
|
|
WORKSPACE_GESTURE_MOVEMENT
|
|
} else {
|
|
self.workspaces[0].view_size().h
|
|
};
|
|
let pos = gesture.tracker.pos() / total_height;
|
|
|
|
let (min, max) = gesture.min_max(self.workspaces.len());
|
|
let new_idx = gesture.start_idx + pos;
|
|
let new_idx = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx);
|
|
|
|
if gesture.current_idx == new_idx {
|
|
return Some(false);
|
|
}
|
|
|
|
gesture.current_idx = new_idx;
|
|
Some(true)
|
|
}
|
|
|
|
pub fn workspace_switch_gesture_end(&mut self, is_touchpad: Option<bool>) -> bool {
|
|
let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else {
|
|
return false;
|
|
};
|
|
|
|
if is_touchpad.is_some_and(|x| gesture.is_touchpad != x) {
|
|
return false;
|
|
}
|
|
|
|
// Take into account any idle time between the last event and now.
|
|
let now = self.clock.now_unadjusted();
|
|
gesture.tracker.push(0., now);
|
|
|
|
let total_height = if gesture.is_touchpad {
|
|
WORKSPACE_GESTURE_MOVEMENT
|
|
} else {
|
|
self.workspaces[0].view_size().h
|
|
};
|
|
|
|
let mut velocity = gesture.tracker.velocity() / total_height;
|
|
let current_pos = gesture.tracker.pos() / total_height;
|
|
let pos = gesture.tracker.projected_end_pos() / total_height;
|
|
|
|
let (min, max) = gesture.min_max(self.workspaces.len());
|
|
let new_idx = gesture.start_idx + pos;
|
|
|
|
let new_idx = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx);
|
|
let new_idx = new_idx.round() as usize;
|
|
|
|
velocity *= WORKSPACE_GESTURE_RUBBER_BAND.clamp_derivative(
|
|
min,
|
|
max,
|
|
gesture.start_idx + current_pos,
|
|
);
|
|
|
|
self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id());
|
|
|
|
self.active_workspace_idx = new_idx;
|
|
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
|
|
self.clock.clone(),
|
|
gesture.current_idx,
|
|
new_idx as f64,
|
|
velocity,
|
|
self.options.animations.workspace_switch.0,
|
|
)));
|
|
|
|
true
|
|
}
|
|
}
|