aboutsummaryrefslogtreecommitdiff
path: root/src/layout/workspace.rs
diff options
context:
space:
mode:
authorRasmus Eneman <rasmus@eneman.eu>2024-07-15 15:51:48 +0200
committerIvan Molodetskikh <yalterz@gmail.com>2024-10-27 23:07:39 -0700
commite887ee93a30390b641bf647d694a1424f7ce4592 (patch)
tree94a76c90c2433ad3a0d92015d7ca6ba569ab2979 /src/layout/workspace.rs
parentd640e8515899e552b845cf8f901ebeb126bb12a5 (diff)
downloadniri-e887ee93a30390b641bf647d694a1424f7ce4592.tar.gz
niri-e887ee93a30390b641bf647d694a1424f7ce4592.tar.bz2
niri-e887ee93a30390b641bf647d694a1424f7ce4592.zip
Implement interactive window move
Diffstat (limited to 'src/layout/workspace.rs')
-rw-r--r--src/layout/workspace.rs302
1 files changed, 274 insertions, 28 deletions
diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs
index 2fd70978..84920bde 100644
--- a/src/layout/workspace.rs
+++ b/src/layout/workspace.rs
@@ -8,6 +8,7 @@ use niri_config::{
};
use niri_ipc::SizeChange;
use ordered_float::NotNan;
+use smithay::backend::renderer::element::Kind;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::desktop::{layer_map_for_output, Window};
use smithay::output::Output;
@@ -16,12 +17,13 @@ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size, Transform};
use super::closing_window::{ClosingWindow, ClosingWindowRenderElement};
-use super::tile::{Tile, TileRenderElement};
+use super::tile::{Tile, TileRenderElement, TileRenderSnapshot};
use super::{ConfigureIntent, InteractiveResizeData, LayoutElement, Options, RemovedTile};
use crate::animation::Animation;
use crate::input::swipe_tracker::SwipeTracker;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
+use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::RenderTarget;
use crate::utils::id::IdCounter;
use crate::utils::transaction::{Transaction, TransactionBlocker};
@@ -106,6 +108,12 @@ pub struct Workspace<W: LayoutElement> {
/// Windows in the closing animation.
closing_windows: Vec<ClosingWindow>,
+ /// Indication where an interactively-moved window is about to be placed.
+ insert_hint: Option<InsertHint>,
+
+ /// Buffer for the insert hint.
+ insert_hint_buffer: SolidColorBuffer,
+
/// Configurable properties of the layout as received from the parent monitor.
pub(super) base_options: Rc<Options>,
@@ -119,6 +127,19 @@ pub struct Workspace<W: LayoutElement> {
id: WorkspaceId,
}
+#[derive(Debug, PartialEq)]
+pub enum InsertPosition {
+ NewColumn(usize),
+ InColumn(usize, usize),
+}
+
+#[derive(Debug)]
+pub struct InsertHint {
+ pub position: InsertPosition,
+ pub width: ColumnWidth,
+ pub is_full_width: bool,
+}
+
#[derive(Debug, Clone)]
pub struct OutputId(String);
@@ -418,6 +439,8 @@ impl<W: LayoutElement> Workspace<W> {
activate_prev_column_on_removal: None,
view_offset_before_fullscreen: None,
closing_windows: vec![],
+ insert_hint: None,
+ insert_hint_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
base_options,
options,
name: config.map(|c| c.name.0),
@@ -456,6 +479,8 @@ impl<W: LayoutElement> Workspace<W> {
activate_prev_column_on_removal: None,
view_offset_before_fullscreen: None,
closing_windows: vec![],
+ insert_hint: None,
+ insert_hint_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
base_options,
options,
name: config.map(|c| c.name.0),
@@ -534,6 +559,13 @@ impl<W: LayoutElement> Workspace<W> {
let view_rect = Rectangle::from_loc_and_size(col_pos, view_size);
col.update_render_elements(is_active, view_rect);
}
+
+ if let Some(insert_hint) = &self.insert_hint {
+ if let Some(area) = self.insert_hint_area(insert_hint) {
+ self.insert_hint_buffer
+ .update(area.size, self.options.insert_hint.color.to_array_premul());
+ }
+ }
}
pub fn update_config(&mut self, base_options: Rc<Options>) {
@@ -565,10 +597,11 @@ impl<W: LayoutElement> Workspace<W> {
}
pub fn windows_mut(&mut self) -> impl Iterator<Item = &mut W> + '_ {
- self.columns
- .iter_mut()
- .flat_map(|col| col.tiles.iter_mut())
- .map(Tile::window_mut)
+ self.tiles_mut().map(Tile::window_mut)
+ }
+
+ pub fn tiles_mut(&mut self) -> impl Iterator<Item = &mut Tile<W>> + '_ {
+ self.columns.iter_mut().flat_map(|col| col.tiles.iter_mut())
}
pub fn current_output(&self) -> Option<&Output> {
@@ -731,14 +764,17 @@ impl<W: LayoutElement> Workspace<W> {
});
}
- fn compute_new_view_offset_for_column_fit(&self, current_x: f64, idx: usize) -> f64 {
- let col = &self.columns[idx];
- if col.is_fullscreen {
+ fn compute_new_view_offset_fit(
+ &self,
+ current_x: f64,
+ col_x: f64,
+ width: f64,
+ is_fullscreen: bool,
+ ) -> f64 {
+ if is_fullscreen {
return 0.;
}
- let new_col_x = self.column_x(idx);
-
let final_x = if let Some(ViewOffsetAdjustment::Animation(anim)) = &self.view_offset_adj {
current_x - self.view_offset + anim.to()
} else {
@@ -748,8 +784,8 @@ impl<W: LayoutElement> Workspace<W> {
let new_offset = compute_new_view_offset(
final_x + self.working_area.loc.x,
self.working_area.size.w,
- new_col_x,
- col.width(),
+ col_x,
+ width,
self.options.gaps,
);
@@ -757,22 +793,45 @@ impl<W: LayoutElement> Workspace<W> {
new_offset - self.working_area.loc.x
}
- fn compute_new_view_offset_for_column_centered(&self, current_x: f64, idx: usize) -> f64 {
- let col = &self.columns[idx];
- if col.is_fullscreen {
- return self.compute_new_view_offset_for_column_fit(current_x, idx);
+ fn compute_new_view_offset_centered(
+ &self,
+ current_x: f64,
+ col_x: f64,
+ width: f64,
+ is_fullscreen: bool,
+ ) -> f64 {
+ if is_fullscreen {
+ return self.compute_new_view_offset_fit(current_x, col_x, width, is_fullscreen);
}
- let width = col.width();
-
// Columns wider than the view are left-aligned (the fit code can deal with that).
if self.working_area.size.w <= width {
- return self.compute_new_view_offset_for_column_fit(current_x, idx);
+ return self.compute_new_view_offset_fit(current_x, col_x, width, is_fullscreen);
}
-(self.working_area.size.w - width) / 2. - self.working_area.loc.x
}
+ fn compute_new_view_offset_for_column_fit(&self, current_x: f64, idx: usize) -> f64 {
+ let col = &self.columns[idx];
+ self.compute_new_view_offset_fit(
+ current_x,
+ self.column_x(idx),
+ col.width(),
+ col.is_fullscreen,
+ )
+ }
+
+ fn compute_new_view_offset_for_column_centered(&self, current_x: f64, idx: usize) -> f64 {
+ let col = &self.columns[idx];
+ self.compute_new_view_offset_centered(
+ current_x,
+ self.column_x(idx),
+ col.width(),
+ col.is_fullscreen,
+ )
+ }
+
fn compute_new_view_offset_for_column(
&self,
current_x: f64,
@@ -958,6 +1017,71 @@ impl<W: LayoutElement> Workspace<W> {
self.windows_mut().find(|win| win.is_wl_surface(wl_surface))
}
+ pub fn set_insert_hint(&mut self, insert_hint: InsertHint) {
+ if self.options.insert_hint.off {
+ return;
+ }
+ self.insert_hint = Some(insert_hint);
+ }
+
+ pub fn clear_insert_hint(&mut self) {
+ self.insert_hint = None;
+ }
+
+ pub fn get_insert_position(&self, pos: Point<f64, Logical>) -> InsertPosition {
+ if self.columns.is_empty() {
+ return InsertPosition::NewColumn(0);
+ }
+
+ let x = pos.x + self.view_pos();
+
+ // Aim for the center of the gap.
+ let x = x + self.options.gaps / 2.;
+ let y = pos.y + self.options.gaps / 2.;
+
+ // Insert position is before the first column.
+ if x < 0. {
+ return InsertPosition::NewColumn(0);
+ }
+
+ // Find the closest gap between columns.
+ let (closest_col_idx, col_x) = self
+ .column_xs(self.data.iter().copied())
+ .enumerate()
+ .min_by_key(|(_, col_x)| NotNan::new((col_x - x).abs()).unwrap())
+ .unwrap();
+
+ // Find the column containing the position.
+ let (col_idx, _) = self
+ .column_xs(self.data.iter().copied())
+ .enumerate()
+ .take_while(|(_, col_x)| *col_x <= x)
+ .last()
+ .unwrap_or((0, 0.));
+
+ // Insert position is past the last column.
+ if col_idx == self.columns.len() {
+ return InsertPosition::NewColumn(closest_col_idx);
+ }
+
+ // Find the closest gap between tiles.
+ let col = &self.columns[col_idx];
+ let (closest_tile_idx, tile_off) = col
+ .tile_offsets()
+ .enumerate()
+ .min_by_key(|(_, tile_off)| NotNan::new((tile_off.y - y).abs()).unwrap())
+ .unwrap();
+
+ // Return the closest among the vertical and the horizontal gap.
+ let vert_dist = (col_x - x).abs();
+ let hor_dist = (tile_off.y - y).abs();
+ if vert_dist <= hor_dist {
+ InsertPosition::NewColumn(closest_col_idx)
+ } else {
+ InsertPosition::InColumn(col_idx, closest_tile_idx)
+ }
+ }
+
pub fn add_window(
&mut self,
col_idx: Option<usize>,
@@ -970,7 +1094,7 @@ impl<W: LayoutElement> Workspace<W> {
self.add_tile(col_idx, tile, activate, width, is_full_width, None);
}
- fn add_tile(
+ pub fn add_tile(
&mut self,
col_idx: Option<usize>,
tile: Tile<W>,
@@ -1488,8 +1612,6 @@ impl<W: LayoutElement> Workspace<W> {
window: &W::Id,
blocker: TransactionBlocker,
) {
- let output_scale = Scale::from(self.scale.fractional_scale());
-
let (tile, mut tile_pos) = self
.tiles_with_render_positions_mut(false)
.find(|(tile, _)| tile.window().id() == window)
@@ -1537,6 +1659,19 @@ impl<W: LayoutElement> Workspace<W> {
tile_pos.x -= offset;
}
+ self.start_close_animation_for_tile(renderer, snapshot, tile_size, tile_pos, blocker);
+ }
+
+ pub fn start_close_animation_for_tile(
+ &mut self,
+ renderer: &mut GlesRenderer,
+ snapshot: TileRenderSnapshot,
+ tile_size: Size<f64, Logical>,
+ tile_pos: Point<f64, Logical>,
+ blocker: TransactionBlocker,
+ ) {
+ let output_scale = Scale::from(self.scale.fractional_scale());
+
let anim = Animation::new(0., 1., 0., self.options.animations.window_close.anim);
let blocker = if self.options.disable_transactions {
@@ -1565,7 +1700,7 @@ impl<W: LayoutElement> Workspace<W> {
}
#[cfg(test)]
- pub fn verify_invariants(&self) {
+ pub fn verify_invariants(&self, move_win_id: Option<&W::Id>) {
use approx::assert_abs_diff_eq;
let scale = self.scale.fractional_scale();
@@ -1601,7 +1736,11 @@ impl<W: LayoutElement> Workspace<W> {
);
}
- for (_, tile_pos) in self.tiles_with_render_positions() {
+ for (tile, tile_pos) in self.tiles_with_render_positions() {
+ if Some(tile.window().id()) != move_win_id {
+ assert_eq!(tile.interactive_move_offset, Point::from((0., 0.)));
+ }
+
let rounded_pos = tile_pos.to_physical_precise_round(scale).to_logical(scale);
// Tile positions must be rounded to physical pixels.
@@ -2046,7 +2185,7 @@ impl<W: LayoutElement> Workspace<W> {
cancel_resize_for_column(&mut self.interactive_resize, col);
}
- fn view_pos(&self) -> f64 {
+ pub fn view_pos(&self) -> f64 {
self.column_x(self.active_column_idx) + self.view_offset
}
@@ -2123,7 +2262,9 @@ impl<W: LayoutElement> Workspace<W> {
zip(tiles, offsets)
}
- fn tiles_with_render_positions(&self) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>)> {
+ pub fn tiles_with_render_positions(
+ &self,
+ ) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>)> {
let scale = self.scale.fractional_scale();
let view_off = Point::from((-self.view_pos(), 0.));
self.columns_in_render_order()
@@ -2139,7 +2280,7 @@ impl<W: LayoutElement> Workspace<W> {
})
}
- fn tiles_with_render_positions_mut(
+ pub fn tiles_with_render_positions_mut(
&mut self,
round: bool,
) -> impl Iterator<Item = (&mut Tile<W>, Point<f64, Logical>)> {
@@ -2162,6 +2303,96 @@ impl<W: LayoutElement> Workspace<W> {
})
}
+ fn insert_hint_area(&self, insert_hint: &InsertHint) -> Option<Rectangle<f64, Logical>> {
+ let mut hint_area = match insert_hint.position {
+ InsertPosition::NewColumn(column_index) => {
+ if column_index == 0 || column_index == self.columns.len() {
+ let size =
+ Size::from((300., self.working_area.size.h - self.options.gaps * 2.));
+ let mut loc = Point::from((
+ self.column_x(column_index),
+ self.working_area.loc.y + self.options.gaps,
+ ));
+ if column_index == 0 && !self.columns.is_empty() {
+ loc.x -= size.w + self.options.gaps;
+ }
+ Rectangle::from_loc_and_size(loc, size)
+ } else if column_index > self.columns.len() {
+ error!("insert hint column index is out of range");
+ return None;
+ } else {
+ let size =
+ Size::from((300., self.working_area.size.h - self.options.gaps * 2.));
+ let loc = Point::from((
+ self.column_x(column_index) - size.w / 2. - self.options.gaps / 2.,
+ self.working_area.loc.y + self.options.gaps,
+ ));
+ Rectangle::from_loc_and_size(loc, size)
+ }
+ }
+ InsertPosition::InColumn(column_index, tile_index) => {
+ if column_index > self.columns.len() {
+ error!("insert hint column index is out of range");
+ return None;
+ }
+ if tile_index > self.columns[column_index].tiles.len() {
+ error!("insert hint tile index is out of range");
+ return None;
+ }
+
+ let (height, y) = if tile_index == 0 {
+ (150., self.columns[column_index].tile_offset(tile_index).y)
+ } else if tile_index == self.columns[column_index].tiles.len() {
+ (
+ 150.,
+ self.columns[column_index].tile_offset(tile_index).y
+ - self.options.gaps
+ - 150.,
+ )
+ } else {
+ (
+ 300.,
+ self.columns[column_index].tile_offset(tile_index).y
+ - self.options.gaps / 2.
+ - 150.,
+ )
+ };
+
+ let size = Size::from((self.data[column_index].width, height));
+ let loc = Point::from((self.column_x(column_index), y));
+ Rectangle::from_loc_and_size(loc, size)
+ }
+ };
+
+ // First window on an empty workspace will cancel out any view offset. Replicate this
+ // effect here.
+ if self.columns.is_empty() {
+ let view_offset = if self.is_centering_focused_column() {
+ self.compute_new_view_offset_centered(0., 0., hint_area.size.w, false)
+ } else {
+ self.compute_new_view_offset_fit(0., 0., hint_area.size.w, false)
+ };
+ hint_area.loc.x -= view_offset;
+ } else {
+ hint_area.loc.x -= self.view_pos();
+ }
+
+ let view_size = self.view_size();
+
+ // Make sure the hint is at least partially visible.
+ if matches!(insert_hint.position, InsertPosition::NewColumn(_)) {
+ hint_area.loc.x = hint_area.loc.x.max(-hint_area.size.w / 2.);
+ hint_area.loc.x = hint_area.loc.x.min(view_size.w - hint_area.size.w / 2.);
+ }
+
+ // Round to physical pixels.
+ hint_area = hint_area
+ .to_physical_precise_round(self.scale.fractional_scale())
+ .to_logical(self.scale.fractional_scale());
+
+ Some(hint_area)
+ }
+
/// Returns the geometry of the active tile relative to and clamped to the view.
///
/// During animations, assumes the final view position.
@@ -2438,7 +2669,22 @@ impl<W: LayoutElement> Workspace<W> {
let mut rv = vec![];
- // Draw the closing windows on top.
+ // Draw the insert hint.
+ if let Some(insert_hint) = &self.insert_hint {
+ if let Some(area) = self.insert_hint_area(insert_hint) {
+ rv.push(
+ TileRenderElement::SolidColor(SolidColorRenderElement::from_buffer(
+ &self.insert_hint_buffer,
+ area.loc,
+ 1.,
+ Kind::Unspecified,
+ ))
+ .into(),
+ );
+ }
+ }
+
+ // Draw the closing windows on top of the other windows.
let view_rect = Rectangle::from_loc_and_size((self.view_pos(), 0.), self.view_size);
for closing in self.closing_windows.iter().rev() {
let elem = closing.render(renderer.as_gles_renderer(), view_rect, output_scale, target);