aboutsummaryrefslogtreecommitdiff
path: root/src/layout/monitor.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/layout/monitor.rs')
-rw-r--r--src/layout/monitor.rs565
1 files changed, 565 insertions, 0 deletions
diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs
new file mode 100644
index 00000000..0fe7af00
--- /dev/null
+++ b/src/layout/monitor.rs
@@ -0,0 +1,565 @@
+use std::cmp::min;
+use std::rc::Rc;
+use std::time::Duration;
+
+use smithay::backend::renderer::element::utils::{
+ CropRenderElement, Relocate, RelocateRenderElement,
+};
+use smithay::backend::renderer::gles::GlesRenderer;
+use smithay::desktop::Window;
+use smithay::output::Output;
+use smithay::utils::{Logical, Point, Rectangle, Scale};
+
+use super::workspace::{
+ compute_working_area, ColumnWidth, OutputId, Workspace, WorkspaceRenderElement,
+};
+use super::{LayoutElement, Options};
+use crate::animation::Animation;
+use crate::config::SizeChange;
+use crate::utils::output_size;
+
+#[derive(Debug)]
+pub struct Monitor<W: LayoutElement> {
+ /// Output for this monitor.
+ pub output: Output,
+ // Must always contain at least one.
+ pub workspaces: Vec<Workspace<W>>,
+ /// Index of the currently active workspace.
+ pub active_workspace_idx: usize,
+ /// In-progress switch between workspaces.
+ pub workspace_switch: Option<WorkspaceSwitch>,
+ /// Configurable properties of the layout.
+ pub 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.
+ pub center_idx: usize,
+ /// Current, fractional workspace index.
+ pub current_idx: f64,
+}
+
+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,
+ }
+ }
+
+ /// Returns `true` if the workspace switch is [`Animation`].
+ ///
+ /// [`Animation`]: WorkspaceSwitch::Animation
+ #[must_use]
+ fn is_animation(&self) -> bool {
+ matches!(self, Self::Animation(..))
+ }
+}
+
+impl<W: LayoutElement> Monitor<W> {
+ pub fn new(output: Output, workspaces: Vec<Workspace<W>>, options: Rc<Options>) -> Self {
+ Self {
+ output,
+ workspaces,
+ active_workspace_idx: 0,
+ workspace_switch: None,
+ options,
+ }
+ }
+
+ pub fn active_workspace(&mut self) -> &mut Workspace<W> {
+ &mut self.workspaces[self.active_workspace_idx]
+ }
+
+ fn activate_workspace(&mut self, idx: usize) {
+ if self.active_workspace_idx == idx {
+ return;
+ }
+
+ let current_idx = self
+ .workspace_switch
+ .as_ref()
+ .map(|s| s.current_idx())
+ .unwrap_or(self.active_workspace_idx as f64);
+
+ self.active_workspace_idx = idx;
+
+ self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
+ current_idx,
+ idx as f64,
+ Duration::from_millis(250),
+ )));
+ }
+
+ pub fn add_window(
+ &mut self,
+ workspace_idx: usize,
+ window: W,
+ activate: bool,
+ width: ColumnWidth,
+ is_full_width: bool,
+ ) {
+ let workspace = &mut self.workspaces[workspace_idx];
+
+ workspace.add_window(window.clone(), activate, width, is_full_width);
+
+ // After adding a new window, workspace becomes this output's own.
+ workspace.original_output = OutputId::new(&self.output);
+
+ if workspace_idx == self.workspaces.len() - 1 {
+ // Insert a new empty workspace.
+ let ws = Workspace::new(self.output.clone(), self.options.clone());
+ self.workspaces.push(ws);
+ }
+
+ if activate {
+ self.activate_workspace(workspace_idx);
+ }
+ }
+
+ fn clean_up_workspaces(&mut self) {
+ assert!(self.workspace_switch.is_none());
+
+ for idx in (0..self.workspaces.len() - 1).rev() {
+ if self.active_workspace_idx == idx {
+ continue;
+ }
+
+ if !self.workspaces[idx].has_windows() {
+ self.workspaces.remove(idx);
+ if self.active_workspace_idx > idx {
+ self.active_workspace_idx -= 1;
+ }
+ }
+ }
+ }
+
+ pub fn move_left(&mut self) {
+ self.active_workspace().move_left();
+ }
+
+ pub fn move_right(&mut self) {
+ self.active_workspace().move_right();
+ }
+
+ pub fn move_down(&mut self) {
+ self.active_workspace().move_down();
+ }
+
+ pub fn move_up(&mut self) {
+ self.active_workspace().move_up();
+ }
+
+ pub fn move_down_or_to_workspace_down(&mut self) {
+ let workspace = self.active_workspace();
+ if workspace.columns.is_empty() {
+ return;
+ }
+ let column = &mut workspace.columns[workspace.active_column_idx];
+ let curr_idx = column.active_window_idx;
+ let new_idx = min(column.active_window_idx + 1, column.windows.len() - 1);
+ if curr_idx == new_idx {
+ self.move_to_workspace_down();
+ } else {
+ workspace.move_down();
+ }
+ }
+
+ pub fn move_up_or_to_workspace_up(&mut self) {
+ let workspace = self.active_workspace();
+ if workspace.columns.is_empty() {
+ return;
+ }
+ let curr_idx = workspace.columns[workspace.active_column_idx].active_window_idx;
+ let new_idx = curr_idx.saturating_sub(1);
+ if curr_idx == new_idx {
+ self.move_to_workspace_up();
+ } else {
+ workspace.move_up();
+ }
+ }
+
+ pub fn focus_left(&mut self) {
+ self.active_workspace().focus_left();
+ }
+
+ pub fn focus_right(&mut self) {
+ self.active_workspace().focus_right();
+ }
+
+ pub fn focus_down(&mut self) {
+ self.active_workspace().focus_down();
+ }
+
+ pub fn focus_up(&mut self) {
+ self.active_workspace().focus_up();
+ }
+
+ pub fn focus_window_or_workspace_down(&mut self) {
+ let workspace = self.active_workspace();
+ if workspace.columns.is_empty() {
+ self.switch_workspace_down();
+ } else {
+ let column = &workspace.columns[workspace.active_column_idx];
+ let curr_idx = column.active_window_idx;
+ let new_idx = min(column.active_window_idx + 1, column.windows.len() - 1);
+ if curr_idx == new_idx {
+ self.switch_workspace_down();
+ } else {
+ workspace.focus_down();
+ }
+ }
+ }
+
+ pub fn focus_window_or_workspace_up(&mut self) {
+ let workspace = self.active_workspace();
+ if workspace.columns.is_empty() {
+ self.switch_workspace_up();
+ } else {
+ let curr_idx = workspace.columns[workspace.active_column_idx].active_window_idx;
+ let new_idx = curr_idx.saturating_sub(1);
+ if curr_idx == new_idx {
+ self.switch_workspace_up();
+ } else {
+ workspace.focus_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 workspace = &mut self.workspaces[source_workspace_idx];
+ if workspace.columns.is_empty() {
+ return;
+ }
+
+ let column = &mut workspace.columns[workspace.active_column_idx];
+ let width = column.width;
+ let is_full_width = column.is_full_width;
+ let window = column.windows[column.active_window_idx].clone();
+ workspace.remove_window(&window);
+
+ self.add_window(new_idx, window, true, width, is_full_width);
+ }
+
+ 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 workspace = &mut self.workspaces[source_workspace_idx];
+ if workspace.columns.is_empty() {
+ return;
+ }
+
+ let column = &mut workspace.columns[workspace.active_column_idx];
+ let width = column.width;
+ let is_full_width = column.is_full_width;
+ let window = column.windows[column.active_window_idx].clone();
+ workspace.remove_window(&window);
+
+ self.add_window(new_idx, window, true, width, is_full_width);
+ }
+
+ pub fn move_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.columns.is_empty() {
+ return;
+ }
+
+ let column = &mut workspace.columns[workspace.active_column_idx];
+ let width = column.width;
+ let is_full_width = column.is_full_width;
+ let window = column.windows[column.active_window_idx].clone();
+ workspace.remove_window(&window);
+
+ self.add_window(new_idx, window, true, width, is_full_width);
+
+ // Don't animate this action.
+ self.workspace_switch = None;
+
+ self.clean_up_workspaces();
+ }
+
+ 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,
+ ));
+ }
+
+ pub fn switch_workspace(&mut self, idx: usize) {
+ self.activate_workspace(min(idx, self.workspaces.len() - 1));
+ // Don't animate this action.
+ self.workspace_switch = None;
+
+ self.clean_up_workspaces();
+ }
+
+ pub fn consume_into_column(&mut self) {
+ self.active_workspace().consume_into_column();
+ }
+
+ pub fn expel_from_column(&mut self) {
+ self.active_workspace().expel_from_column();
+ }
+
+ pub fn center_column(&mut self) {
+ self.active_workspace().center_column();
+ }
+
+ pub fn focus(&self) -> Option<&W> {
+ let workspace = &self.workspaces[self.active_workspace_idx];
+ if !workspace.has_windows() {
+ return None;
+ }
+
+ let column = &workspace.columns[workspace.active_column_idx];
+ Some(&column.windows[column.active_window_idx])
+ }
+
+ pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
+ if let Some(WorkspaceSwitch::Animation(anim)) = &mut self.workspace_switch {
+ anim.set_current_time(current_time);
+ if anim.is_done() {
+ self.workspace_switch = None;
+ self.clean_up_workspaces();
+ }
+ }
+
+ for ws in &mut self.workspaces {
+ ws.advance_animations(current_time, is_active);
+ }
+ }
+
+ pub 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_animations_ongoing())
+ }
+
+ pub fn update_config(&mut self, options: Rc<Options>) {
+ for ws in &mut self.workspaces {
+ ws.update_config(options.clone());
+ }
+
+ if self.options.struts != options.struts {
+ let view_size = output_size(&self.output);
+ let working_area = compute_working_area(&self.output, options.struts);
+
+ for ws in &mut self.workspaces {
+ ws.set_view_size(view_size, working_area);
+ }
+ }
+
+ self.options = options;
+ }
+
+ pub fn toggle_width(&mut self) {
+ self.active_workspace().toggle_width();
+ }
+
+ pub fn toggle_full_width(&mut self) {
+ self.active_workspace().toggle_full_width();
+ }
+
+ pub fn set_column_width(&mut self, change: SizeChange) {
+ self.active_workspace().set_column_width(change);
+ }
+
+ pub fn set_window_height(&mut self, change: SizeChange) {
+ self.active_workspace().set_window_height(change);
+ }
+
+ pub fn move_workspace_down(&mut self) {
+ let 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.
+ let ws = Workspace::new(self.output.clone(), self.options.clone());
+ self.workspaces.push(ws);
+ }
+
+ self.activate_workspace(new_idx);
+ self.workspace_switch = None;
+
+ self.clean_up_workspaces();
+ }
+
+ pub fn move_workspace_up(&mut self) {
+ let 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.
+ let ws = Workspace::new(self.output.clone(), self.options.clone());
+ self.workspaces.push(ws);
+ }
+
+ self.activate_workspace(new_idx);
+ self.workspace_switch = None;
+
+ self.clean_up_workspaces();
+ }
+
+ pub fn window_under(
+ &self,
+ pos_within_output: Point<f64, Logical>,
+ ) -> Option<(&W, Point<i32, Logical>)> {
+ match &self.workspace_switch {
+ Some(switch) => {
+ let size = output_size(&self.output);
+
+ let render_idx = switch.current_idx();
+ let before_idx = render_idx.floor() as usize;
+ let after_idx = render_idx.ceil() as usize;
+
+ let offset = ((render_idx - before_idx as f64) * size.h as f64).round() as i32;
+
+ let (idx, ws_offset) = if pos_within_output.y < (size.h - offset) as f64 {
+ (before_idx, Point::from((0, offset)))
+ } else {
+ (after_idx, Point::from((0, -size.h + offset)))
+ };
+
+ let ws = &self.workspaces[idx];
+ let (win, win_pos) = ws.window_under(pos_within_output + ws_offset.to_f64())?;
+ Some((win, win_pos - ws_offset))
+ }
+ None => {
+ let ws = &self.workspaces[self.active_workspace_idx];
+ ws.window_under(pos_within_output)
+ }
+ }
+ }
+
+ 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()
+ }
+}
+
+impl Monitor<Window> {
+ pub fn render_elements(
+ &self,
+ renderer: &mut GlesRenderer,
+ ) -> Vec<MonitorRenderElement<GlesRenderer>> {
+ let _span = tracy_client::span!("Monitor::render_elements");
+
+ let output_scale = Scale::from(self.output.current_scale().fractional_scale());
+ let output_transform = self.output.current_transform();
+ let output_mode = self.output.current_mode().unwrap();
+ let size = output_transform.transform_size(output_mode.size);
+
+ match &self.workspace_switch {
+ Some(switch) => {
+ let render_idx = switch.current_idx();
+ let before_idx = render_idx.floor() as usize;
+ let after_idx = render_idx.ceil() as usize;
+
+ let offset = ((render_idx - before_idx as f64) * size.h as f64).round() as i32;
+
+ let before = self.workspaces[before_idx].render_elements(renderer);
+ let after = self.workspaces[after_idx].render_elements(renderer);
+
+ let before = before.into_iter().filter_map(|elem| {
+ Some(RelocateRenderElement::from_element(
+ CropRenderElement::from_element(
+ elem,
+ output_scale,
+ Rectangle::from_extemities((0, offset), (size.w, size.h)),
+ )?,
+ (0, -offset),
+ Relocate::Relative,
+ ))
+ });
+ let after = after.into_iter().filter_map(|elem| {
+ Some(RelocateRenderElement::from_element(
+ CropRenderElement::from_element(
+ elem,
+ output_scale,
+ Rectangle::from_extemities((0, 0), (size.w, offset)),
+ )?,
+ (0, -offset + size.h),
+ Relocate::Relative,
+ ))
+ });
+ before.chain(after).collect()
+ }
+ None => {
+ let elements = self.workspaces[self.active_workspace_idx].render_elements(renderer);
+ elements
+ .into_iter()
+ .filter_map(|elem| {
+ Some(RelocateRenderElement::from_element(
+ CropRenderElement::from_element(
+ elem,
+ output_scale,
+ // HACK: set infinite crop bounds due to a damage tracking bug
+ // which causes glitched rendering for maximized GTK windows.
+ // FIXME: use proper bounds after fixing the Crop element.
+ Rectangle::from_loc_and_size(
+ (-i32::MAX / 2, -i32::MAX / 2),
+ (i32::MAX, i32::MAX),
+ ),
+ // Rectangle::from_loc_and_size((0, 0), size),
+ )?,
+ (0, 0),
+ Relocate::Relative,
+ ))
+ })
+ .collect()
+ }
+ }
+ }
+}