aboutsummaryrefslogtreecommitdiff
path: root/src/layout/workspace.rs
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2023-12-24 15:10:09 +0400
committerIvan Molodetskikh <yalterz@gmail.com>2023-12-24 15:10:09 +0400
commited3080d908001bf468789b8f47f893e00306135d (patch)
treeabf3c12eba9e0759fa199781280d33c62891e508 /src/layout/workspace.rs
parent461ce5f3631c99e928280935d92f084f4b641b9e (diff)
downloadniri-ed3080d908001bf468789b8f47f893e00306135d.tar.gz
niri-ed3080d908001bf468789b8f47f893e00306135d.tar.bz2
niri-ed3080d908001bf468789b8f47f893e00306135d.zip
Split layout mod into files
No functional change intended.
Diffstat (limited to 'src/layout/workspace.rs')
-rw-r--r--src/layout/workspace.rs1441
1 files changed, 1441 insertions, 0 deletions
diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs
new file mode 100644
index 00000000..16424ffb
--- /dev/null
+++ b/src/layout/workspace.rs
@@ -0,0 +1,1441 @@
+use std::cmp::{max, min};
+use std::iter::zip;
+use std::rc::Rc;
+use std::time::Duration;
+
+use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
+use smithay::backend::renderer::element::AsRenderElements;
+use smithay::backend::renderer::gles::GlesRenderer;
+use smithay::backend::renderer::ImportAll;
+use smithay::desktop::space::SpaceElement;
+use smithay::desktop::{layer_map_for_output, Window};
+use smithay::output::Output;
+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
+use smithay::render_elements;
+use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
+
+use super::focus_ring::{FocusRing, FocusRingRenderElement};
+use super::{LayoutElement, Options};
+use crate::animation::Animation;
+use crate::config::{PresetWidth, SizeChange, Struts};
+use crate::utils::output_size;
+
+#[derive(Debug)]
+pub struct Workspace<W: LayoutElement> {
+ /// The original output of this workspace.
+ ///
+ /// Most of the time this will be the workspace's current output, however, after an output
+ /// disconnection, it may remain pointing to the disconnected output.
+ pub original_output: OutputId,
+
+ /// Current output of this workspace.
+ output: Option<Output>,
+
+ /// Latest known view size for this workspace.
+ ///
+ /// This should be computed from the current workspace output size, or, if all outputs have
+ /// been disconnected, preserved until a new output is connected.
+ view_size: Size<i32, Logical>,
+
+ /// Latest known working area for this workspace.
+ ///
+ /// This is similar to view size, but takes into account things like layer shell exclusive
+ /// zones.
+ working_area: Rectangle<i32, Logical>,
+
+ /// Columns of windows on this workspace.
+ pub columns: Vec<Column<W>>,
+
+ /// Index of the currently active column, if any.
+ pub active_column_idx: usize,
+
+ /// Focus ring buffer and parameters.
+ focus_ring: FocusRing,
+
+ /// Offset of the view computed from the active column.
+ ///
+ /// Any gaps, including left padding from work area left exclusive zone, is handled
+ /// with this view offset (rather than added as a constant elsewhere in the code). This allows
+ /// for natural handling of fullscreen windows, which must ignore work area padding.
+ view_offset: i32,
+
+ /// Animation of the view offset, if one is currently ongoing.
+ view_offset_anim: Option<Animation>,
+
+ /// Whether to activate the previous, rather than the next, column upon column removal.
+ ///
+ /// When a new column is created and removed with no focus changes in-between, it is more
+ /// natural to activate the previously-focused column. This variable tracks that.
+ ///
+ /// Since we only create-and-activate columns immediately to the right of the active column (in
+ /// contrast to tabs in Firefox, for example), we can track this as a bool, rather than an
+ /// index of the previous column to activate.
+ activate_prev_column_on_removal: bool,
+
+ /// Configurable properties of the layout.
+ pub options: Rc<Options>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct OutputId(String);
+
+render_elements! {
+ #[derive(Debug)]
+ pub WorkspaceRenderElement<R> where R: ImportAll;
+ Wayland = WaylandSurfaceRenderElement<R>,
+ FocusRing = FocusRingRenderElement,
+}
+
+/// Width of a column.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum ColumnWidth {
+ /// Proportion of the current view width.
+ Proportion(f64),
+ /// One of the proportion presets.
+ ///
+ /// This is separate from Proportion in order to be able to reliably cycle between preset
+ /// proportions.
+ Preset(usize),
+ /// Fixed width in logical pixels.
+ Fixed(i32),
+}
+
+/// Height of a window in a column.
+///
+/// Proportional height is intentionally omitted. With column widths you frequently want e.g. two
+/// columns side-by-side with 50% width each, and you want them to remain this way when moving to a
+/// differently sized monitor. Windows in a column, however, already auto-size to fill the available
+/// height, giving you this behavior. The only reason to set a different window height, then, is
+/// when you want something in the window to fit exactly, e.g. to fit 30 lines in a terminal, which
+/// corresponds to the `Fixed` variant.
+///
+/// This does not preclude the usual set of binds to set or resize a window proportionally. Just,
+/// they are converted to, and stored as fixed height right away, so that once you resize a window
+/// to fit the desired content, it can never become smaller than that when moving between monitors.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum WindowHeight {
+ /// Automatically computed height, evenly distributed across the column.
+ Auto,
+ /// Fixed height in logical pixels.
+ Fixed(i32),
+}
+
+#[derive(Debug)]
+pub struct Column<W: LayoutElement> {
+ /// Windows in this column.
+ ///
+ /// Must be non-empty.
+ pub windows: Vec<W>,
+
+ /// Heights of the windows.
+ ///
+ /// Must have the same number of elements as `windows`.
+ heights: Vec<WindowHeight>,
+
+ /// Index of the currently active window.
+ pub active_window_idx: usize,
+
+ /// Desired width of this column.
+ ///
+ /// If the column is full-width or full-screened, this is the width that should be restored
+ /// upon unfullscreening and untoggling full-width.
+ pub width: ColumnWidth,
+
+ /// Whether this column is full-width.
+ pub is_full_width: bool,
+
+ /// Whether this column contains a single full-screened window.
+ pub is_fullscreen: bool,
+
+ /// Latest known view size for this column's workspace.
+ view_size: Size<i32, Logical>,
+
+ /// Latest known working area for this column's workspace.
+ working_area: Rectangle<i32, Logical>,
+
+ /// Configurable properties of the layout.
+ options: Rc<Options>,
+}
+
+impl OutputId {
+ pub fn new(output: &Output) -> Self {
+ Self(output.name())
+ }
+}
+
+impl ColumnWidth {
+ fn resolve(self, options: &Options, view_width: i32) -> i32 {
+ match self {
+ ColumnWidth::Proportion(proportion) => {
+ ((view_width - options.gaps) as f64 * proportion).floor() as i32 - options.gaps
+ }
+ ColumnWidth::Preset(idx) => options.preset_widths[idx].resolve(options, view_width),
+ ColumnWidth::Fixed(width) => width,
+ }
+ }
+}
+
+impl From<PresetWidth> for ColumnWidth {
+ fn from(value: PresetWidth) -> Self {
+ match value {
+ PresetWidth::Proportion(p) => Self::Proportion(p.clamp(0., 10000.)),
+ PresetWidth::Fixed(f) => Self::Fixed(f.clamp(1, 100000)),
+ }
+ }
+}
+
+impl<W: LayoutElement> Workspace<W> {
+ pub fn new(output: Output, options: Rc<Options>) -> Self {
+ let working_area = compute_working_area(&output, options.struts);
+ Self {
+ original_output: OutputId::new(&output),
+ view_size: output_size(&output),
+ working_area,
+ output: Some(output),
+ columns: vec![],
+ active_column_idx: 0,
+ focus_ring: FocusRing::new(options.focus_ring),
+ view_offset: 0,
+ view_offset_anim: None,
+ activate_prev_column_on_removal: false,
+ options,
+ }
+ }
+
+ pub fn new_no_outputs(options: Rc<Options>) -> Self {
+ Self {
+ output: None,
+ original_output: OutputId(String::new()),
+ view_size: Size::from((1280, 720)),
+ working_area: Rectangle::from_loc_and_size((0, 0), (1280, 720)),
+ columns: vec![],
+ active_column_idx: 0,
+ focus_ring: FocusRing::new(options.focus_ring),
+ view_offset: 0,
+ view_offset_anim: None,
+ activate_prev_column_on_removal: false,
+ options,
+ }
+ }
+
+ pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
+ match &mut self.view_offset_anim {
+ Some(anim) => {
+ anim.set_current_time(current_time);
+ self.view_offset = anim.value().round() as i32;
+ if anim.is_done() {
+ self.view_offset_anim = None;
+ }
+ }
+ None => (),
+ }
+
+ let view_pos = self.view_pos();
+
+ // This shall one day become a proper animation.
+ if !self.columns.is_empty() {
+ let col = &self.columns[self.active_column_idx];
+ let active_win = &col.windows[col.active_window_idx];
+ let geom = active_win.geometry();
+ let has_ssd = active_win.has_ssd();
+
+ let win_pos = Point::from((
+ self.column_x(self.active_column_idx) - view_pos,
+ col.window_y(col.active_window_idx),
+ ));
+
+ self.focus_ring.update(win_pos, geom.size, has_ssd);
+ self.focus_ring.set_active(is_active);
+ }
+ }
+
+ pub fn are_animations_ongoing(&self) -> bool {
+ self.view_offset_anim.is_some()
+ }
+
+ pub fn update_config(&mut self, options: Rc<Options>) {
+ self.focus_ring.update_config(options.focus_ring);
+ // The focus ring buffer will be updated in a subsequent update_animations call.
+
+ for column in &mut self.columns {
+ column.update_config(options.clone());
+ }
+
+ self.options = options;
+ }
+
+ pub fn windows(&self) -> impl Iterator<Item = &W> + '_ {
+ self.columns.iter().flat_map(|col| col.windows.iter())
+ }
+
+ pub fn set_output(&mut self, output: Option<Output>) {
+ if self.output == output {
+ return;
+ }
+
+ if let Some(output) = self.output.take() {
+ for win in self.windows() {
+ win.output_leave(&output);
+ }
+ }
+
+ self.output = output;
+
+ if let Some(output) = &self.output {
+ let working_area = compute_working_area(output, self.options.struts);
+ self.set_view_size(output_size(output), working_area);
+
+ for win in self.windows() {
+ self.enter_output_for_window(win);
+ }
+ }
+ }
+
+ fn enter_output_for_window(&self, window: &W) {
+ if let Some(output) = &self.output {
+ prepare_for_output(window, output);
+
+ // FIXME: proper overlap.
+ window.output_enter(
+ output,
+ Rectangle::from_loc_and_size((0, 0), (i32::MAX, i32::MAX)),
+ );
+ }
+ }
+
+ pub fn set_view_size(
+ &mut self,
+ size: Size<i32, Logical>,
+ working_area: Rectangle<i32, Logical>,
+ ) {
+ if self.view_size == size && self.working_area == working_area {
+ return;
+ }
+
+ self.view_size = size;
+ self.working_area = working_area;
+
+ for col in &mut self.columns {
+ col.set_view_size(self.view_size, self.working_area);
+ }
+ }
+
+ fn toplevel_bounds(&self) -> Size<i32, Logical> {
+ Size::from((
+ max(self.working_area.size.w - self.options.gaps * 2, 1),
+ max(self.working_area.size.h - self.options.gaps * 2, 1),
+ ))
+ }
+
+ pub fn configure_new_window(&self, window: &Window) {
+ let width = if let Some(width) = self.options.default_width {
+ max(1, width.resolve(&self.options, self.working_area.size.w))
+ } else {
+ 0
+ };
+
+ let height = self.working_area.size.h - self.options.gaps * 2;
+ let size = Size::from((width, max(height, 1)));
+
+ let bounds = self.toplevel_bounds();
+
+ if let Some(output) = self.output.as_ref() {
+ prepare_for_output(window, output);
+ }
+
+ window.toplevel().with_pending_state(|state| {
+ state.size = Some(size);
+ state.bounds = Some(bounds);
+ });
+ }
+
+ fn compute_new_view_offset_for_column(&self, current_x: i32, idx: usize) -> i32 {
+ if self.columns[idx].is_fullscreen {
+ return 0;
+ }
+
+ let new_col_x = self.column_x(idx);
+
+ let final_x = if let Some(anim) = &self.view_offset_anim {
+ current_x - self.view_offset + anim.to().round() as i32
+ } else {
+ current_x
+ };
+
+ let new_offset = compute_new_view_offset(
+ final_x + self.working_area.loc.x,
+ self.working_area.size.w,
+ new_col_x,
+ self.columns[idx].width(),
+ self.options.gaps,
+ );
+
+ // Non-fullscreen windows are always offset at least by the working area position.
+ new_offset - self.working_area.loc.x
+ }
+
+ fn animate_view_offset_to_column(&mut self, current_x: i32, idx: usize) {
+ let new_view_offset = self.compute_new_view_offset_for_column(current_x, idx);
+
+ let new_col_x = self.column_x(idx);
+ let from_view_offset = current_x - new_col_x;
+ self.view_offset = from_view_offset;
+
+ // If we're already animating towards that, don't restart it.
+ if let Some(anim) = &self.view_offset_anim {
+ if anim.value().round() as i32 == self.view_offset
+ && anim.to().round() as i32 == new_view_offset
+ {
+ return;
+ }
+ }
+
+ // If our view offset is already this, we don't need to do anything.
+ if self.view_offset == new_view_offset {
+ self.view_offset_anim = None;
+ return;
+ }
+
+ self.view_offset_anim = Some(Animation::new(
+ self.view_offset as f64,
+ new_view_offset as f64,
+ Duration::from_millis(250),
+ ));
+ }
+
+ fn activate_column(&mut self, idx: usize) {
+ if self.active_column_idx == idx {
+ return;
+ }
+
+ let current_x = self.view_pos();
+ self.animate_view_offset_to_column(current_x, idx);
+
+ self.active_column_idx = idx;
+
+ // A different column was activated; reset the flag.
+ self.activate_prev_column_on_removal = false;
+ }
+
+ pub fn has_windows(&self) -> bool {
+ self.windows().next().is_some()
+ }
+
+ pub fn has_window(&self, window: &W) -> bool {
+ self.windows().any(|win| win == window)
+ }
+
+ pub fn find_wl_surface(&self, wl_surface: &WlSurface) -> Option<&W> {
+ self.windows().find(|win| win.is_wl_surface(wl_surface))
+ }
+
+ /// Computes the X position of the windows in the given column, in logical coordinates.
+ fn column_x(&self, column_idx: usize) -> i32 {
+ let mut x = 0;
+
+ for column in self.columns.iter().take(column_idx) {
+ x += column.width() + self.options.gaps;
+ }
+
+ x
+ }
+
+ pub fn add_window(
+ &mut self,
+ window: W,
+ activate: bool,
+ width: ColumnWidth,
+ is_full_width: bool,
+ ) {
+ self.enter_output_for_window(&window);
+
+ let was_empty = self.columns.is_empty();
+
+ let idx = if self.columns.is_empty() {
+ 0
+ } else {
+ self.active_column_idx + 1
+ };
+
+ let column = Column::new(
+ window,
+ self.view_size,
+ self.working_area,
+ self.options.clone(),
+ width,
+ is_full_width,
+ );
+ self.columns.insert(idx, column);
+
+ if activate {
+ // If this is the first window on an empty workspace, skip the animation from whatever
+ // view_offset was left over.
+ if was_empty {
+ // Try to make the code produce a left-aligned offset, even in presence of left
+ // exclusive zones.
+ self.view_offset = self.compute_new_view_offset_for_column(self.column_x(0), 0);
+ self.view_offset_anim = None;
+ }
+
+ self.activate_column(idx);
+ self.activate_prev_column_on_removal = true;
+ }
+ }
+
+ pub fn remove_window(&mut self, window: &W) {
+ if let Some(output) = &self.output {
+ window.output_leave(output);
+ }
+
+ let column_idx = self
+ .columns
+ .iter()
+ .position(|col| col.contains(window))
+ .unwrap();
+ let column = &mut self.columns[column_idx];
+
+ let window_idx = column.windows.iter().position(|win| win == window).unwrap();
+ column.windows.remove(window_idx);
+ column.heights.remove(window_idx);
+ if column.windows.is_empty() {
+ if column_idx + 1 == self.active_column_idx {
+ // The previous column, that we were going to activate upon removal of the active
+ // column, has just been itself removed.
+ self.activate_prev_column_on_removal = false;
+ }
+
+ // FIXME: activate_column below computes current view position to compute the new view
+ // position, which can include the column we're removing here. This leads to unwanted
+ // view jumps.
+ self.columns.remove(column_idx);
+ if self.columns.is_empty() {
+ return;
+ }
+
+ if self.active_column_idx > column_idx
+ || (self.active_column_idx == column_idx && self.activate_prev_column_on_removal)
+ {
+ // A column to the left was removed; preserve the current position.
+ // FIXME: preserve activate_prev_column_on_removal.
+ // Or, the active column was removed, and we needed to activate the previous column.
+ self.activate_column(self.active_column_idx.saturating_sub(1));
+ } else {
+ self.activate_column(min(self.active_column_idx, self.columns.len() - 1));
+ }
+
+ return;
+ }
+
+ column.active_window_idx = min(column.active_window_idx, column.windows.len() - 1);
+ column.update_window_sizes();
+ }
+
+ pub fn update_window(&mut self, window: &W) {
+ let (idx, column) = self
+ .columns
+ .iter_mut()
+ .enumerate()
+ .find(|(_, col)| col.contains(window))
+ .unwrap();
+ column.update_window_sizes();
+
+ if idx == self.active_column_idx {
+ // We might need to move the view to ensure the resized window is still visible.
+ let current_x = self.view_pos();
+ self.animate_view_offset_to_column(current_x, idx);
+ }
+ }
+
+ pub fn activate_window(&mut self, window: &W) {
+ let column_idx = self
+ .columns
+ .iter()
+ .position(|col| col.contains(window))
+ .unwrap();
+ let column = &mut self.columns[column_idx];
+
+ column.activate_window(window);
+ self.activate_column(column_idx);
+ }
+
+ #[cfg(test)]
+ pub fn verify_invariants(&self) {
+ assert!(self.view_size.w > 0);
+ assert!(self.view_size.h > 0);
+
+ if !self.columns.is_empty() {
+ assert!(self.active_column_idx < self.columns.len());
+
+ for column in &self.columns {
+ column.verify_invariants();
+ }
+ }
+ }
+
+ pub fn focus_left(&mut self) {
+ self.activate_column(self.active_column_idx.saturating_sub(1));
+ }
+
+ pub fn focus_right(&mut self) {
+ if self.columns.is_empty() {
+ return;
+ }
+
+ self.activate_column(min(self.active_column_idx + 1, self.columns.len() - 1));
+ }
+
+ pub fn focus_down(&mut self) {
+ if self.columns.is_empty() {
+ return;
+ }
+
+ self.columns[self.active_column_idx].focus_down();
+ }
+
+ pub fn focus_up(&mut self) {
+ if self.columns.is_empty() {
+ return;
+ }
+
+ self.columns[self.active_column_idx].focus_up();
+ }
+
+ pub fn move_left(&mut self) {
+ let new_idx = self.active_column_idx.saturating_sub(1);
+ if self.active_column_idx == new_idx {
+ return;
+ }
+
+ let current_x = self.view_pos();
+
+ self.columns.swap(self.active_column_idx, new_idx);
+
+ self.view_offset =
+ self.compute_new_view_offset_for_column(current_x, self.active_column_idx);
+
+ self.activate_column(new_idx);
+ }
+
+ pub fn move_right(&mut self) {
+ if self.columns.is_empty() {
+ return;
+ }
+
+ let new_idx = min(self.active_column_idx + 1, self.columns.len() - 1);
+ if self.active_column_idx == new_idx {
+ return;
+ }
+
+ let current_x = self.view_pos();
+
+ self.columns.swap(self.active_column_idx, new_idx);
+
+ self.view_offset =
+ self.compute_new_view_offset_for_column(current_x, self.active_column_idx);
+
+ self.activate_column(new_idx);
+ }
+
+ pub fn move_down(&mut self) {
+ if self.columns.is_empty() {
+ return;
+ }
+
+ self.columns[self.active_column_idx].move_down();
+ }
+
+ pub fn move_up(&mut self) {
+ if self.columns.is_empty() {
+ return;
+ }
+
+ self.columns[self.active_column_idx].move_up();
+ }
+
+ pub fn consume_into_column(&mut self) {
+ if self.columns.len() < 2 {
+ return;
+ }
+
+ if self.active_column_idx == self.columns.len() - 1 {
+ return;
+ }
+
+ let source_column_idx = self.active_column_idx + 1;
+
+ let source_column = &mut self.columns[source_column_idx];
+ let window = source_column.windows[0].clone();
+ self.remove_window(&window);
+
+ let target_column = &mut self.columns[self.active_column_idx];
+ target_column.add_window(window);
+ }
+
+ pub fn expel_from_column(&mut self) {
+ if self.columns.is_empty() {
+ return;
+ }
+
+ let source_column = &mut self.columns[self.active_column_idx];
+ if source_column.windows.len() == 1 {
+ return;
+ }
+
+ let width = source_column.width;
+ let is_full_width = source_column.is_full_width;
+ let window = source_column.windows[source_column.active_window_idx].clone();
+ self.remove_window(&window);
+
+ self.add_window(window, true, width, is_full_width);
+ }
+
+ pub fn center_column(&mut self) {
+ if self.columns.is_empty() {
+ return;
+ }
+
+ let col = &self.columns[self.active_column_idx];
+ if col.is_fullscreen {
+ return;
+ }
+
+ let width = col.width();
+
+ // If the column is wider than the working area, then on commit it will be shifted to left
+ // edge alignment by the usual positioning code, so there's no use in doing anything here.
+ if self.working_area.size.w <= width {
+ return;
+ }
+
+ let new_view_offset = -(self.working_area.size.w - width) / 2 - self.working_area.loc.x;
+
+ // If we're already animating towards that, don't restart it.
+ if let Some(anim) = &self.view_offset_anim {
+ if anim.to().round() as i32 == new_view_offset {
+ return;
+ }
+ }
+
+ // If our view offset is already this, we don't need to do anything.
+ if self.view_offset == new_view_offset {
+ return;
+ }
+
+ self.view_offset_anim = Some(Animation::new(
+ self.view_offset as f64,
+ new_view_offset as f64,
+ Duration::from_millis(250),
+ ));
+ }
+
+ fn view_pos(&self) -> i32 {
+ self.column_x(self.active_column_idx) + self.view_offset
+ }
+
+ pub fn window_under(&self, pos: Point<f64, Logical>) -> Option<(&W, Point<i32, Logical>)> {
+ if self.columns.is_empty() {
+ return None;
+ }
+
+ let view_pos = self.view_pos();
+
+ // Prefer the active window since it's drawn on top.
+ let col = &self.columns[self.active_column_idx];
+ let active_win = &col.windows[col.active_window_idx];
+ let geom = active_win.geometry();
+ let buf_pos = Point::from((
+ self.column_x(self.active_column_idx) - view_pos,
+ col.window_y(col.active_window_idx),
+ )) - geom.loc;
+ if active_win.is_in_input_region(&(pos - buf_pos.to_f64())) {
+ return Some((active_win, buf_pos));
+ }
+
+ let mut x = -view_pos;
+ for col in &self.columns {
+ for (win, y) in zip(&col.windows, col.window_ys()) {
+ if win == active_win {
+ // Already handled it above.
+ continue;
+ }
+
+ let geom = win.geometry();
+ let buf_pos = Point::from((x, y)) - geom.loc;
+ if win.is_in_input_region(&(pos - buf_pos.to_f64())) {
+ return Some((win, buf_pos));
+ }
+ }
+
+ x += col.width() + self.options.gaps;
+ }
+
+ None
+ }
+
+ pub fn toggle_width(&mut self) {
+ if self.columns.is_empty() {
+ return;
+ }
+
+ self.columns[self.active_column_idx].toggle_width();
+ }
+
+ pub fn toggle_full_width(&mut self) {
+ if self.columns.is_empty() {
+ return;
+ }
+
+ self.columns[self.active_column_idx].toggle_full_width();
+ }
+
+ pub fn set_column_width(&mut self, change: SizeChange) {
+ if self.columns.is_empty() {
+ return;
+ }
+
+ self.columns[self.active_column_idx].set_column_width(change);
+ }
+
+ pub fn set_window_height(&mut self, change: SizeChange) {
+ if self.columns.is_empty() {
+ return;
+ }
+
+ self.columns[self.active_column_idx].set_window_height(change);
+ }
+
+ pub fn set_fullscreen(&mut self, window: &W, is_fullscreen: bool) {
+ let (mut col_idx, win_idx) = self
+ .columns
+ .iter()
+ .enumerate()
+ .find_map(|(col_idx, col)| {
+ col.windows
+ .iter()
+ .position(|w| w == window)
+ .map(|win_idx| (col_idx, win_idx))
+ })
+ .unwrap();
+
+ let mut col = &mut self.columns[col_idx];
+
+ if is_fullscreen && col.windows.len() > 1 {
+ // This wasn't the only window in its column; extract it into a separate column.
+ let target_window_was_focused =
+ self.active_column_idx == col_idx && col.active_window_idx == win_idx;
+ let window = col.windows.remove(win_idx);
+ col.heights.remove(win_idx);
+ col.active_window_idx = min(col.active_window_idx, col.windows.len() - 1);
+ col.update_window_sizes();
+ let width = col.width;
+ let is_full_width = col.is_full_width;
+
+ col_idx += 1;
+ self.columns.insert(
+ col_idx,
+ Column::new(
+ window,
+ self.view_size,
+ self.working_area,
+ self.options.clone(),
+ width,
+ is_full_width,
+ ),
+ );
+ if self.active_column_idx >= col_idx || target_window_was_focused {
+ self.active_column_idx += 1;
+ }
+ col = &mut self.columns[col_idx];
+ }
+
+ col.set_fullscreen(is_fullscreen);
+ }
+
+ pub fn toggle_fullscreen(&mut self, window: &W) {
+ let col = self
+ .columns
+ .iter_mut()
+ .find(|col| col.windows.contains(window))
+ .unwrap();
+ let value = !col.is_fullscreen;
+ self.set_fullscreen(window, value);
+ }
+
+ pub fn render_above_top_layer(&self) -> bool {
+ // Render above the top layer if we're on a fullscreen window and the view is stationary.
+ if self.columns.is_empty() {
+ return false;
+ }
+
+ if self.view_offset_anim.is_some() {
+ return false;
+ }
+
+ self.columns[self.active_column_idx].is_fullscreen
+ }
+}
+
+impl Workspace<Window> {
+ pub fn refresh(&self) {
+ let bounds = self.toplevel_bounds();
+
+ for (col_idx, col) in self.columns.iter().enumerate() {
+ for (win_idx, win) in col.windows.iter().enumerate() {
+ let active = self.active_column_idx == col_idx && col.active_window_idx == win_idx;
+ win.set_activated(active);
+
+ win.toplevel().with_pending_state(|state| {
+ state.bounds = Some(bounds);
+ });
+
+ win.toplevel().send_pending_configure();
+ win.refresh();
+ }
+ }
+ }
+
+ pub fn render_elements(
+ &self,
+ renderer: &mut GlesRenderer,
+ ) -> Vec<WorkspaceRenderElement<GlesRenderer>> {
+ if self.columns.is_empty() {
+ return vec![];
+ }
+
+ // FIXME: workspaces should probably cache their last used scale so they can be correctly
+ // rendered even with no outputs connected.
+ let output_scale = self
+ .output
+ .as_ref()
+ .map(|o| Scale::from(o.current_scale().fractional_scale()))
+ .unwrap_or(Scale::from(1.));
+
+ let mut rv = vec![];
+ let view_pos = self.view_pos();
+
+ // Draw the active window on top.
+ let col = &self.columns[self.active_column_idx];
+ let active_win = &col.windows[col.active_window_idx];
+ let win_pos = Point::from((
+ self.column_x(self.active_column_idx) - view_pos,
+ col.window_y(col.active_window_idx),
+ ));
+
+ // Draw the window itself.
+ let geom = active_win.geometry();
+ let buf_pos = win_pos - geom.loc;
+ rv.extend(active_win.render_elements(
+ renderer,
+ buf_pos.to_physical_precise_round(output_scale),
+ output_scale,
+ 1.,
+ ));
+
+ // Draw the focus ring.
+ rv.extend(self.focus_ring.render(output_scale).map(Into::into));
+
+ let mut x = -view_pos;
+ for col in &self.columns {
+ for (win, y) in zip(&col.windows, col.window_ys()) {
+ if win == active_win {
+ // Already handled it above.
+ continue;
+ }
+
+ let geom = win.geometry();
+ let buf_pos = Point::from((x, y)) - geom.loc;
+ rv.extend(win.render_elements(
+ renderer,
+ buf_pos.to_physical_precise_round(output_scale),
+ output_scale,
+ 1.,
+ ));
+ }
+
+ x += col.width() + self.options.gaps;
+ }
+
+ rv
+ }
+}
+
+impl<W: LayoutElement> Column<W> {
+ fn new(
+ window: W,
+ view_size: Size<i32, Logical>,
+ working_area: Rectangle<i32, Logical>,
+ options: Rc<Options>,
+ width: ColumnWidth,
+ is_full_width: bool,
+ ) -> Self {
+ let mut rv = Self {
+ windows: vec![],
+ heights: vec![],
+ active_window_idx: 0,
+ width,
+ is_full_width,
+ is_fullscreen: false,
+ view_size,
+ working_area,
+ options,
+ };
+
+ rv.add_window(window);
+
+ rv
+ }
+
+ fn set_view_size(&mut self, size: Size<i32, Logical>, working_area: Rectangle<i32, Logical>) {
+ if self.view_size == size && self.working_area == working_area {
+ return;
+ }
+
+ self.view_size = size;
+ self.working_area = working_area;
+
+ self.update_window_sizes();
+ }
+
+ fn update_config(&mut self, options: Rc<Options>) {
+ let mut update_sizes = false;
+
+ // If preset widths changed, make our width non-preset.
+ if self.options.preset_widths != options.preset_widths {
+ if let ColumnWidth::Preset(idx) = self.width {
+ self.width = self.options.preset_widths[idx];
+ }
+ }
+
+ if self.options.gaps != options.gaps {
+ update_sizes = true;
+ }
+
+ self.options = options;
+
+ if update_sizes {
+ self.update_window_sizes();
+ }
+ }
+
+ fn set_width(&mut self, width: ColumnWidth) {
+ self.width = width;
+ self.is_full_width = false;
+ self.update_window_sizes();
+ }
+
+ fn contains(&self, window: &W) -> bool {
+ self.windows.iter().any(|win| win == window)
+ }
+
+ fn activate_window(&mut self, window: &W) {
+ let idx = self.windows.iter().position(|win| win == window).unwrap();
+ self.active_window_idx = idx;
+ }
+
+ fn add_window(&mut self, window: W) {
+ self.is_fullscreen = false;
+ self.windows.push(window);
+ self.heights.push(WindowHeight::Auto);
+ self.update_window_sizes();
+ }
+
+ fn update_window_sizes(&mut self) {
+ if self.is_fullscreen {
+ self.windows[0].request_fullscreen(self.view_size);
+ return;
+ }
+
+ let min_size: Vec<_> = self.windows.iter().map(LayoutElement::min_size).collect();
+ let max_size: Vec<_> = self.windows.iter().map(LayoutElement::max_size).collect();
+
+ // Compute the column width.
+ let min_width = min_size
+ .iter()