aboutsummaryrefslogtreecommitdiff
path: root/src/layout
diff options
context:
space:
mode:
Diffstat (limited to 'src/layout')
-rw-r--r--src/layout/floating.rs815
-rw-r--r--src/layout/mod.rs236
-rw-r--r--src/layout/monitor.rs53
-rw-r--r--src/layout/scrolling.rs41
-rw-r--r--src/layout/workspace.rs412
5 files changed, 1492 insertions, 65 deletions
diff --git a/src/layout/floating.rs b/src/layout/floating.rs
new file mode 100644
index 00000000..75345eed
--- /dev/null
+++ b/src/layout/floating.rs
@@ -0,0 +1,815 @@
+use std::cmp::{max, min};
+use std::iter::zip;
+use std::rc::Rc;
+
+use niri_ipc::SizeChange;
+use smithay::backend::renderer::gles::GlesRenderer;
+use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size};
+
+use super::closing_window::{ClosingWindow, ClosingWindowRenderElement};
+use super::scrolling::ColumnWidth;
+use super::tile::{Tile, TileRenderElement, TileRenderSnapshot};
+use super::workspace::InteractiveResize;
+use super::{ConfigureIntent, InteractiveResizeData, LayoutElement, Options, RemovedTile};
+use crate::animation::{Animation, Clock};
+use crate::niri_render_elements;
+use crate::render_helpers::renderer::NiriRenderer;
+use crate::render_helpers::RenderTarget;
+use crate::utils::transaction::TransactionBlocker;
+use crate::utils::ResizeEdge;
+use crate::window::ResolvedWindowRules;
+
+/// Space for floating windows.
+#[derive(Debug)]
+pub struct FloatingSpace<W: LayoutElement> {
+ /// Tiles in top-to-bottom order.
+ tiles: Vec<Tile<W>>,
+
+ /// Extra per-tile data.
+ data: Vec<Data>,
+
+ /// Id of the active window.
+ ///
+ /// The active window is not necessarily the topmost window. Focus-follows-mouse should
+ /// activate a window, but not bring it to the top, because that's very annoying.
+ ///
+ /// This is always set to `Some()` when `tiles` isn't empty.
+ active_window_id: Option<W::Id>,
+
+ /// Ongoing interactive resize.
+ interactive_resize: Option<InteractiveResize<W>>,
+
+ /// Windows in the closing animation.
+ closing_windows: Vec<ClosingWindow>,
+
+ /// Working area for this space.
+ working_area: Rectangle<f64, Logical>,
+
+ /// Scale of the output the space is on (and rounds its sizes to).
+ scale: f64,
+
+ /// Clock for driving animations.
+ clock: Clock,
+
+ /// Configurable properties of the layout.
+ options: Rc<Options>,
+}
+
+niri_render_elements! {
+ FloatingSpaceRenderElement<R> => {
+ Tile = TileRenderElement<R>,
+ ClosingWindow = ClosingWindowRenderElement,
+ }
+}
+
+/// Size-relative units.
+struct SizeFrac;
+
+/// Extra per-tile data.
+#[derive(Debug, Clone, Copy, PartialEq)]
+struct Data {
+ /// Position relative to the working area.
+ pos: Point<f64, SizeFrac>,
+
+ /// Cached position in logical coordinates.
+ ///
+ /// Not rounded to physical pixels.
+ logical_pos: Point<f64, Logical>,
+
+ /// Cached actual size of the tile.
+ size: Size<f64, Logical>,
+
+ /// Working area used for conversions.
+ working_area: Rectangle<f64, Logical>,
+}
+
+impl Data {
+ pub fn new<W: LayoutElement>(
+ working_area: Rectangle<f64, Logical>,
+ tile: &Tile<W>,
+ logical_pos: Point<f64, Logical>,
+ ) -> Self {
+ let mut rv = Self {
+ pos: Point::default(),
+ logical_pos: Point::default(),
+ size: Size::default(),
+ working_area,
+ };
+ rv.update(tile);
+ rv.set_logical_pos(logical_pos);
+ rv
+ }
+
+ fn recompute_logical_pos(&mut self) {
+ let mut logical_pos = Point::from((self.pos.x, self.pos.y));
+ logical_pos.x *= self.working_area.size.w;
+ logical_pos.y *= self.working_area.size.h;
+ logical_pos += self.working_area.loc;
+ self.logical_pos = logical_pos;
+ }
+
+ pub fn update_config(&mut self, working_area: Rectangle<f64, Logical>) {
+ if self.working_area == working_area {
+ return;
+ }
+
+ self.working_area = working_area;
+ self.recompute_logical_pos();
+ }
+
+ pub fn update<W: LayoutElement>(&mut self, tile: &Tile<W>) {
+ self.size = tile.tile_size();
+ }
+
+ pub fn set_logical_pos(&mut self, logical_pos: Point<f64, Logical>) {
+ let pos = logical_pos - self.working_area.loc;
+ let mut pos = Point::from((pos.x, pos.y));
+ pos.x /= f64::max(self.working_area.size.w, 1.0);
+ pos.y /= f64::max(self.working_area.size.h, 1.0);
+
+ self.pos = pos;
+
+ // This should get close to the same result as what we started with.
+ self.recompute_logical_pos();
+ }
+
+ #[cfg(test)]
+ fn verify_invariants(&self) {
+ let mut temp = *self;
+ temp.recompute_logical_pos();
+ assert_eq!(
+ self.logical_pos, temp.logical_pos,
+ "cached logical pos must be up to date"
+ );
+ }
+}
+
+impl<W: LayoutElement> FloatingSpace<W> {
+ pub fn new(
+ working_area: Rectangle<f64, Logical>,
+ scale: f64,
+ clock: Clock,
+ options: Rc<Options>,
+ ) -> Self {
+ Self {
+ tiles: Vec::new(),
+ data: Vec::new(),
+ active_window_id: None,
+ interactive_resize: None,
+ closing_windows: Vec::new(),
+ working_area,
+ scale,
+ clock,
+ options,
+ }
+ }
+
+ pub fn update_config(
+ &mut self,
+ working_area: Rectangle<f64, Logical>,
+ scale: f64,
+ options: Rc<Options>,
+ ) {
+ for (tile, data) in zip(&mut self.tiles, &mut self.data) {
+ tile.update_config(scale, options.clone());
+ data.update(tile);
+ data.update_config(working_area);
+ }
+
+ self.working_area = working_area;
+ self.scale = scale;
+ self.options = options;
+ }
+
+ pub fn update_shaders(&mut self) {
+ for tile in &mut self.tiles {
+ tile.update_shaders();
+ }
+ }
+
+ pub fn advance_animations(&mut self) {
+ for tile in &mut self.tiles {
+ tile.advance_animations();
+ }
+
+ self.closing_windows.retain_mut(|closing| {
+ closing.advance_animations();
+ closing.are_animations_ongoing()
+ });
+ }
+
+ pub fn are_animations_ongoing(&self) -> bool {
+ self.tiles.iter().any(Tile::are_animations_ongoing) || !self.closing_windows.is_empty()
+ }
+
+ pub fn update_render_elements(&mut self, is_active: bool, view_rect: Rectangle<f64, Logical>) {
+ let active = self.active_window_id.clone();
+ for (tile, offset) in self.tiles_with_offsets_mut() {
+ let id = tile.window().id();
+ let is_active = is_active && Some(id) == active.as_ref();
+
+ let mut tile_view_rect = view_rect;
+ tile_view_rect.loc -= offset + tile.render_offset();
+ tile.update(is_active, tile_view_rect);
+ }
+ }
+
+ pub fn tiles(&self) -> impl Iterator<Item = &Tile<W>> + '_ {
+ self.tiles.iter()
+ }
+
+ pub fn tiles_mut(&mut self) -> impl Iterator<Item = &mut Tile<W>> + '_ {
+ self.tiles.iter_mut()
+ }
+
+ pub fn tiles_with_offsets(&self) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>)> + '_ {
+ let offsets = self.data.iter().map(|d| d.logical_pos);
+ zip(&self.tiles, offsets)
+ }
+
+ pub fn tiles_with_offsets_mut(
+ &mut self,
+ ) -> impl Iterator<Item = (&mut Tile<W>, Point<f64, Logical>)> + '_ {
+ let offsets = self.data.iter().map(|d| d.logical_pos);
+ zip(&mut self.tiles, offsets)
+ }
+
+ pub fn tiles_with_render_positions(
+ &self,
+ ) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>)> {
+ let scale = self.scale;
+ self.tiles_with_offsets().map(move |(tile, offset)| {
+ let pos = offset + tile.render_offset();
+ // Round to physical pixels.
+ let pos = pos.to_physical_precise_round(scale).to_logical(scale);
+ (tile, pos)
+ })
+ }
+
+ pub fn tiles_with_render_positions_mut(
+ &mut self,
+ round: bool,
+ ) -> impl Iterator<Item = (&mut Tile<W>, Point<f64, Logical>)> {
+ let scale = self.scale;
+ self.tiles_with_offsets_mut().map(move |(tile, offset)| {
+ let mut pos = offset + tile.render_offset();
+ // Round to physical pixels.
+ if round {
+ pos = pos.to_physical_precise_round(scale).to_logical(scale);
+ }
+ (tile, pos)
+ })
+ }
+
+ pub fn toplevel_bounds(&self, rules: &ResolvedWindowRules) -> Size<i32, Logical> {
+ let border_config = rules.border.resolve_against(self.options.border);
+ compute_toplevel_bounds(border_config, self.working_area.size)
+ }
+
+ /// Returns the geometry of the active tile relative to and clamped to the working area.
+ ///
+ /// During animations, assumes the final tile position.
+ pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
+ let (tile, offset) = self.tiles_with_offsets().next()?;
+
+ let tile_size = tile.tile_size();
+ let tile_rect = Rectangle::from_loc_and_size(offset, tile_size);
+
+ self.working_area.intersection(tile_rect)
+ }
+
+ pub fn popup_target_rect(&self, id: &W::Id) -> Option<Rectangle<f64, Logical>> {
+ for (tile, pos) in self.tiles_with_offsets() {
+ if tile.window().id() == id {
+ // TODO: intersect with working area width.
+ let width = tile.window_size().w;
+ let height = self.working_area.size.h;
+
+ let mut target = Rectangle::from_loc_and_size((0., 0.), (width, height));
+ target.loc.y -= pos.y;
+ target.loc.y -= tile.window_loc().y;
+
+ return Some(target);
+ }
+ }
+ None
+ }
+
+ fn idx_of(&self, id: &W::Id) -> Option<usize> {
+ self.tiles.iter().position(|tile| tile.window().id() == id)
+ }
+
+ fn contains(&self, id: &W::Id) -> bool {
+ self.idx_of(id).is_some()
+ }
+
+ pub fn active_window(&self) -> Option<&W> {
+ let id = self.active_window_id.as_ref()?;
+ self.tiles
+ .iter()
+ .find(|tile| tile.window().id() == id)
+ .map(Tile::window)
+ }
+
+ pub fn has_window(&self, id: &W::Id) -> bool {
+ self.tiles.iter().any(|tile| tile.window().id() == id)
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.tiles.is_empty()
+ }
+
+ pub fn add_tile(&mut self, tile: Tile<W>, pos: Option<Point<f64, Logical>>, activate: bool) {
+ self.add_tile_at(0, tile, pos, activate);
+ }
+
+ fn add_tile_at(
+ &mut self,
+ idx: usize,
+ mut tile: Tile<W>,
+ pos: Option<Point<f64, Logical>>,
+ activate: bool,
+ ) {
+ tile.update_config(self.scale, self.options.clone());
+
+ if tile.window().is_pending_fullscreen() {
+ tile.window_mut()
+ .request_size(Size::from((0, 0)), true, None);
+ }
+
+ if activate || self.tiles.is_empty() {
+ self.active_window_id = Some(tile.window().id().clone());
+ }
+
+ let mut pos = pos.unwrap_or_else(|| {
+ let area_size = self.working_area.size.to_point();
+ let tile_size = tile.tile_size().to_point();
+ let offset = (area_size - tile_size).downscale(2.);
+ self.working_area.loc + offset
+ });
+ // TODO: also nudge pos + size inside the area.
+ // TODO: smart padding (if doesn't fit, try without padding).
+ pos.x = f64::max(pos.x, self.working_area.loc.x + 8.);
+ pos.y = f64::max(pos.y, self.working_area.loc.y + 8.);
+
+ let data = Data::new(self.working_area, &tile, pos);
+ self.data.insert(idx, data);
+ self.tiles.insert(idx, tile);
+ }
+
+ pub fn add_tile_above(&mut self, above: &W::Id, tile: Tile<W>) {
+ // Activate the new window if above was active.
+ let activate = Some(above) == self.active_window_id.as_ref();
+
+ let idx = self.idx_of(above).unwrap();
+
+ let above_pos = self.data[idx].logical_pos;
+ let above_size = self.data[idx].size;
+ let pos = above_pos + (above_size.to_point() - tile.tile_size().to_point()).downscale(2.);
+
+ self.add_tile_at(idx, tile, Some(pos), activate);
+ }
+
+ pub fn remove_active_tile(&mut self) -> Option<RemovedTile<W>> {
+ let id = self.active_window_id.clone()?;
+ Some(self.remove_tile(&id))
+ }
+
+ pub fn remove_tile(&mut self, id: &W::Id) -> RemovedTile<W> {
+ let idx = self.idx_of(id).unwrap();
+ self.remove_tile_by_idx(idx)
+ }
+
+ fn remove_tile_by_idx(&mut self, idx: usize) -> RemovedTile<W> {
+ let tile = self.tiles.remove(idx);
+ self.data.remove(idx);
+
+ if self.tiles.is_empty() {
+ self.active_window_id = None;
+ } else if Some(tile.window().id()) == self.active_window_id.as_ref() {
+ // The active tile was removed, make the topmost tile active.
+ self.active_window_id = Some(self.tiles[0].window().id().clone());
+ }
+
+ // Stop interactive resize.
+ if let Some(resize) = &self.interactive_resize {
+ if tile.window().id() == &resize.window {
+ self.interactive_resize = None;
+ }
+ }
+
+ let width = ColumnWidth::Fixed(tile.window_size().w);
+ RemovedTile {
+ tile,
+ // TODO
+ width,
+ is_full_width: false,
+ is_floating: true,
+ }
+ }
+
+ pub fn activate_window_without_raising(&mut self, id: &W::Id) -> bool {
+ if !self.contains(id) {
+ return false;
+ }
+
+ self.active_window_id = Some(id.clone());
+ true
+ }
+
+ pub fn activate_window(&mut self, id: &W::Id) -> bool {
+ let Some(idx) = self.idx_of(id) else {
+ return false;
+ };
+
+ let tile = self.tiles.remove(idx);
+ let data = self.data.remove(idx);
+ self.tiles.insert(0, tile);
+ self.data.insert(0, data);
+ self.active_window_id = Some(id.clone());
+
+ true
+ }
+
+ pub fn start_close_animation_for_window(
+ &mut self,
+ renderer: &mut GlesRenderer,
+ id: &W::Id,
+ blocker: TransactionBlocker,
+ ) {
+ let (tile, tile_pos) = self
+ .tiles_with_render_positions_mut(false)
+ .find(|(tile, _)| tile.window().id() == id)
+ .unwrap();
+
+ let Some(snapshot) = tile.take_unmap_snapshot() else {
+ return;
+ };
+
+ let tile_size = tile.tile_size();
+
+ 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 anim = Animation::new(
+ self.clock.clone(),
+ 0.,
+ 1.,
+ 0.,
+ self.options.animations.window_close.anim,
+ );
+
+ let blocker = if self.options.disable_transactions {
+ TransactionBlocker::completed()
+ } else {
+ blocker
+ };
+
+ let scale = Scale::from(self.scale);
+ let res = ClosingWindow::new(
+ renderer, snapshot, scale, tile_size, tile_pos, blocker, anim,
+ );
+ match res {
+ Ok(closing) => {
+ self.closing_windows.push(closing);
+ }
+ Err(err) => {
+ warn!("error creating a closing window animation: {err:?}");
+ }
+ }
+ }
+
+ pub fn set_window_width(&mut self, id: Option<&W::Id>, change: SizeChange, animate: bool) {
+ let Some(id) = id.or(self.active_window_id.as_ref()) else {
+ return;
+ };
+ let idx = self.idx_of(id).unwrap();
+
+ let SizeChange::SetFixed(mut win_width) = change else {
+ // TODO
+ return;
+ };
+
+ let tile = &mut self.tiles[idx];
+ let win = tile.window_mut();
+ let min_w = win.min_size().w;
+ let max_w = win.max_size().w;
+
+ if max_w > 0 {
+ win_width = min(win_width, max_w);
+ }
+ if min_w > 0 {
+ win_width = max(win_width, min_w);
+ }
+ win_width = max(1, win_width);
+
+ let win_height = win
+ .requested_size()
+ .map(|size| size.h)
+ // If we requested height = 0, then switch to the current height.
+ .filter(|h| *h != 0)
+ .unwrap_or_else(|| win.size().h);
+ let win_size = Size::from((win_width, win_height));
+ win.request_size(win_size, animate, None);
+ }
+
+ pub fn set_window_height(&mut self, id: Option<&W::Id>, change: SizeChange, animate: bool) {
+ let Some(id) = id.or(self.active_window_id.as_ref()) else {
+ return;
+ };
+ let idx = self.idx_of(id).unwrap();
+
+ let SizeChange::SetFixed(mut win_height) = change else {
+ // TODO
+ return;
+ };
+
+ let tile = &mut self.tiles[idx];
+ let win = tile.window_mut();
+ let min_h = win.min_size().h;
+ let max_h = win.max_size().h;
+
+ if max_h > 0 {
+ win_height = min(win_height, max_h);
+ }
+ if min_h > 0 {
+ win_height = max(win_height, min_h);
+ }
+ win_height = max(1, win_height);
+
+ let win_width = win
+ .requested_size()
+ .map(|size| size.w)
+ // If we requested width = 0, then switch to the current width.
+ .filter(|w| *w != 0)
+ .unwrap_or_else(|| win.size().w);
+ let win_size = Size::from((win_width, win_height));
+ win.request_size(win_size, animate, None);
+ }
+
+ pub fn update_window(&mut self, id: &W::Id, serial: Option<Serial>) -> bool {
+ let Some(tile_idx) = self.idx_of(id) else {
+ return false;
+ };
+
+ let tile = &mut self.tiles[tile_idx];
+ let data = &mut self.data[tile_idx];
+
+ let resize = tile.window_mut().interactive_resize_data();
+
+ // Do this before calling update_window() so it can get up-to-date info.
+ if let Some(serial) = serial {
+ tile.window_mut().update_interactive_resize(serial);
+ }
+
+ let prev_size = data.size;
+
+ tile.update_window();
+ data.update(tile);
+
+ // When resizing by top/left edge, update the position accordingly.
+ if let Some(resize) = resize {
+ let mut offset = Point::from((0., 0.));
+ if resize.edges.contains(ResizeEdge::LEFT) {
+ offset.x += prev_size.w - data.size.w;
+ }
+ if resize.edges.contains(ResizeEdge::TOP) {
+ offset.y += prev_size.h - data.size.h;
+ }
+ data.set_logical_pos(data.logical_pos + offset);
+ }
+
+ true
+ }
+
+ pub fn render_elements<R: NiriRenderer>(
+ &self,
+ renderer: &mut R,
+ view_rect: Rectangle<f64, Logical>,
+ scale: Scale<f64>,
+ target: RenderTarget,
+ ) -> Vec<FloatingSpaceRenderElement<R>> {
+ let mut rv = Vec::new();
+
+ // Draw the closing windows on top of the other windows.
+ //
+ // FIXME: I guess this should rather preserve the stacking order when the window is closed.
+ for closing in self.closing_windows.iter().rev() {
+ let elem = closing.render(renderer.as_gles_renderer(), view_rect, scale, target);
+ rv.push(elem.into());
+ }
+
+ let active = self.active_window_id.clone();
+ for (tile, tile_pos) in self.tiles_with_render_positions() {
+ // For the active tile, draw the focus ring.
+ let focus_ring = Some(tile.window().id()) == active.as_ref();
+
+ rv.extend(
+ tile.render(renderer, tile_pos, scale, focus_ring, target)
+ .map(Into::into),
+ );
+ }
+
+ rv
+ }
+
+ pub fn interactive_resize_begin(&mut self, window: W::Id, edges: ResizeEdge) -> bool {
+ if self.interactive_resize.is_some() {
+ return false;
+ }
+
+ let tile = self
+ .tiles
+ .iter_mut()
+ .find(|tile| tile.window().id() == &window)
+ .unwrap();
+
+ let original_window_size = tile.window_size();
+
+ let resize = InteractiveResize {
+ window,
+ original_window_size,
+ data: InteractiveResizeData { edges },
+ };
+ self.interactive_resize = Some(resize);
+
+ true
+ }
+
+ pub fn interactive_resize_update(
+ &mut self,
+ window: &W::Id,
+ delta: Point<f64, Logical>,
+ ) -> bool {
+ let Some(resize) = &self.interactive_resize else {
+ return false;
+ };
+
+ if window != &resize.window {
+ return false;
+ }
+
+ let original_window_size = resize.original_window_size;
+ let edges = resize.data.edges;
+
+ if edges.intersects(ResizeEdge::LEFT_RIGHT) {
+ let mut dx = delta.x;
+ if edges.contains(ResizeEdge::LEFT) {
+ dx = -dx;
+ };
+
+ let window_width = (original_window_size.w + dx).round() as i32;
+ self.set_window_width(Some(window), SizeChange::SetFixed(window_width), false);
+ }
+
+ if edges.intersects(ResizeEdge::TOP_BOTTOM) {
+ let mut dy = delta.y;
+ if edges.contains(ResizeEdge::TOP) {
+ dy = -dy;
+ };
+
+ let window_height = (original_window_size.h + dy).round() as i32;
+ self.set_window_height(Some(window), SizeChange::SetFixed(window_height), false);
+ }
+
+ true
+ }
+
+ pub fn interactive_resize_end(&mut self, window: Option<&W::Id>) {
+ let Some(resize) = &self.interactive_resize else {
+ return;
+ };
+
+ if let Some(window) = window {
+ if window != &resize.window {
+ return;
+ }
+ }
+
+ self.interactive_resize = None;
+ }
+
+ pub fn refresh(&mut self, is_active: bool) {
+ let active = self.active_window_id.clone();
+ for tile in &mut self.tiles {
+ let win = tile.window_mut();
+
+ win.set_active_in_column(true);
+
+ let is_active = is_active && Some(win.id()) == active.as_ref();
+ win.set_activated(is_active);
+
+ let resize_data = self
+ .interactive_resize
+ .as_ref()
+ .filter(|resize| &resize.window == win.id())
+ .map(|resize| resize.data);
+ win.set_interactive_resize(resize_data);
+
+ let border_config = win.rules().border.resolve_against(self.options.border);
+ let bounds = compute_toplevel_bounds(border_config, self.working_area.size);
+ win.set_bounds(bounds);
+
+ // If transactions are disabled, also disable combined throttling, for more
+ // intuitive behavior.
+ let intent = if self.options.disable_resize_throttling {
+ ConfigureIntent::CanSend
+ } else {
+ win.configure_intent()
+ };
+
+ if matches!(
+ intent,
+ ConfigureIntent::CanSend | ConfigureIntent::ShouldSend
+ ) {
+ win.send_pending_configure();
+ }
+
+ win.refresh();
+ }
+ }
+
+ #[cfg(test)]
+ pub fn working_area(&self) -> Rectangle<f64, Logical> {
+ self.working_area
+ }
+
+ #[cfg(test)]
+ pub fn scale(&self) -> f64 {
+ self.scale
+ }
+
+ #[cfg(test)]
+ pub fn clock(&self) -> &Clock {
+ &self.clock
+ }
+
+ #[cfg(test)]
+ pub fn options(&self) -> &Rc<Options> {
+ &self.options
+ }
+
+ #[cfg(test)]
+ pub fn verify_invariants(&self) {
+ assert!(self.scale > 0.);
+ assert!(self.scale.is_finite());
+ assert_eq!(self.tiles.len(), self.data.len());
+
+ for (tile, data) in zip(&self.tiles, &self.data) {
+ assert!(Rc::ptr_eq(&self.options, &tile.options));
+ assert_eq!(self.clock, tile.clock);
+ assert_eq!(self.scale, tile.scale());
+ tile.verify_invariants();
+
+ assert!(
+ !tile.window().is_pending_fullscreen(),
+ "floating windows cannot be fullscreen"
+ );
+
+ data.verify_invariants();
+
+ let mut data2 = *data;
+ data2.update(tile);
+ data2.update_config(self.working_area);
+ assert_eq!(data, &data2, "tile data must be up to date");
+ }
+
+ if let Some(id) = &self.active_window_id {
+ assert!(!self.tiles.is_empty());
+ assert!(self.contains(id), "active window must be present in tiles");
+ } else {
+ assert!(self.tiles.is_empty());
+ }
+
+ if let Some(resize) = &self.interactive_resize {
+ assert!(
+ self.contains(&resize.window),
+ "interactive resize window must be present in tiles"
+ );
+ }
+ }
+}
+
+fn compute_toplevel_bounds(
+ border_config: niri_config::Border,
+ working_area_size: Size<f64, Logical>,
+) -> Size<i32, Logical> {
+ let mut border = 0.;
+ if !border_config.off {
+ border = border_config.width.0 * 2.;
+ }
+
+ Size::from((
+ f64::max(working_area_size.w - border, 1.),
+ f64::max(working_area_size.h - border, 1.),
+ ))
+ .to_i32_floor()
+}
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 90fcaab6..48a7954a 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -66,6 +66,7 @@ use crate::utils::{output_matches_name, output_size, round_logical_in_physical_m
use crate::window::ResolvedWindowRules;
pub mod closing_window;
+pub mod floating;
pub mod focus_ring;
pub mod insert_hint_element;
pub mod monitor;
@@ -357,6 +358,8 @@ pub struct RemovedTile<W: LayoutElement> {
width: ColumnWidth,
/// Whether the column the tile was in was full-width.
is_full_width: bool,
+ /// Whether the tile was floating.
+ is_floating: bool,
}
/// Whether to activate a newly added window.
@@ -744,6 +747,7 @@ impl<W: LayoutElement> Layout<W> {
}
}
+ // TODO: pos
pub fn add_window_by_idx(
&mut self,
monitor_idx: usize,
@@ -752,6 +756,7 @@ impl<W: LayoutElement> Layout<W> {
activate: bool,
width: ColumnWidth,
is_full_width: bool,
+ is_floating: bool,
) {
let MonitorSet::Normal {
monitors,
@@ -762,7 +767,14 @@ impl<W: LayoutElement> Layout<W> {
panic!()
};
- monitors[monitor_idx].add_window(workspace_idx, window, activate, width, is_full_width);
+ monitors[monitor_idx].add_window(
+ workspace_idx,
+ window,
+ activate,
+ width,
+ is_full_width,
+ is_floating,
+ );
if activate {
*active_monitor_idx = monitor_idx;
@@ -776,6 +788,7 @@ impl<W: LayoutElement> Layout<W> {
window: W,
width: Option<ColumnWidth>,
is_full_width: bool,
+ is_floating: bool,
activate: ActivateWindow,
) -> Option<&Output> {
let width = self.resolve_default_width(&window, width);
@@ -814,7 +827,7 @@ impl<W: LayoutElement> Layout<W> {
true
});
- mon.add_window(ws_idx, window, activate, width, is_full_width);
+ mon.add_window(ws_idx, window, activate, width, is_full_width, is_floating);
Some(&mon.output)
}
MonitorSet::NoOutputs { workspaces } => {
@@ -827,7 +840,7 @@ impl<W: LayoutElement> Layout<W> {
})
.unwrap();
let activate = activate.map_smart(|| true);
- ws.add_window(window, activate, width, is_full_width);
+ ws.add_window(window, activate, width, is_full_width, is_floating);
None
}
}
@@ -864,6 +877,7 @@ impl<W: LayoutElement> Layout<W> {
window: W,
width: Option<ColumnWidth>,
is_full_width: bool,
+ is_floating: bool,
activate: ActivateWindow,
) -> Option<&Output> {
let width = self.resolve_default_width(&window, width);
@@ -888,6 +902,7 @@ impl<W: LayoutElement> Layout<W> {
activate,
width,
is_full_width,
+ is_floating,
);
Some(&mon.output)
}
@@ -902,7 +917,7 @@ impl<W: LayoutElement> Layout<W> {
&mut workspaces[0]
};
let activate = activate.map_smart(|| true);
- ws.add_window(window, activate, width, is_full_width);
+ ws.add_window(window, activate, width, is_full_width, is_floating);
None
}
}
@@ -919,16 +934,24 @@ impl<W: LayoutElement> Layout<W> {
window: W,
width: Option<ColumnWidth>,
is_full_width: bool,
+ is_floating: bool,
) -> Option<&Output> {
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
if right_of == move_.tile.window().id() {
let output = move_.output.clone();
let activate = ActivateWindow::default();
if self.monitor_for_output(&output).is_some() {
- self.add_window_on_output(&output, window, width, is_full_width, activate);
+ self.add_window_on_output(
+ &output,
+ window,
+ width,
+ is_full_width,
+ is_floating,
+ activate,
+ );
return Some(&self.monitor_for_output(&output).unwrap().output);
} else {
- return self.add_window(window, width, is_full_width, activate);
+ return self.add_window(window, width, is_full_width, is_floating, activate);
}
}
}
@@ -942,7 +965,7 @@ impl<W: LayoutElement> Layout<W> {
.find(|mon| mon.workspaces.iter().any(|ws| ws.has_window(right_of)))
.unwrap();
- mon.add_window_right_of(right_of, window, width, is_full_width);
+ mon.add_window_right_of(right_of, window, width, is_full_width, is_floating);
Some(&mon.output)
}
MonitorSet::NoOutputs { workspaces } => {
@@ -950,7 +973,7 @@ impl<W: LayoutElement> Layout<W> {
.iter_mut()
.find(|ws| ws.has_window(right_of))
.unwrap();
- ws.add_window_right_of(right_of, window, width, is_full_width);
+ ws.add_window_right_of(right_of, window, width, is_full_width, is_floating);
None
}
}
@@ -963,6 +986,7 @@ impl<W: LayoutElement> Layout<W> {
window: W,
width: Option<ColumnWidth>,
is_full_width: bool,
+ is_floating: bool,
activate: ActivateWindow,
) {
let width = self.resolve_default_width(&window, width);
@@ -998,6 +1022,7 @@ impl<W: LayoutElement> Layout<W> {
activate,
width,
is_full_width,
+ is_floating,
);
}
@@ -1024,6 +1049,7 @@ impl<W: LayoutElement> Layout<W> {
tile: move_.tile,
width: move_.width,
is_full_width: move_.is_full_width,
+ is_floating: false,
});
}
}
@@ -1390,6 +1416,42 @@ impl<W: LayoutElement> Layout<W> {
}
}
+ pub fn activate_window_without_raising(&mut self, window: &W::Id) {
+ if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
+ if move_.tile.window().id() == window {
+ return;
+ }
+ }
+
+ let MonitorSet::Normal {
+ monitors,
+ active_monitor_idx,
+ ..
+ } = &mut self.monitor_set
+ else {
+ return;
+ };
+
+ for (monitor_idx, mon) in monitors.iter_mut().enumerate() {
+ for (workspace_idx, ws) in mon.workspaces.iter_mut().enumerate() {
+ if ws.activate_window_without_raising(window) {
+ *active_monitor_idx = monitor_idx;
+
+ // If currently in the middle of a vertical swipe between the target workspace
+ // and some other, don't switch the workspace.
+ match &mon.workspace_switch {
+ Some(WorkspaceSwitch::Gesture(gesture))
+ if gesture.current_idx.floor() == workspace_idx as f64
+ || gesture.current_idx.ceil() == workspace_idx as f64 => {}
+ _ => mon.switch_workspace(workspace_idx),
+ }
+
+ return;
+ }
+ }
+ }
+ }
+
pub fn activate_output(&mut self, output: &Output) {
let MonitorSet::Normal {
monitors,
@@ -2596,6 +2658,36 @@ impl<W: LayoutElement> Layout<W> {
workspace.reset_window_height(window);
}
+ pub fn toggle_window_floating(&mut self, window: Option<&W::Id>) {
+ if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
+ if window.is_none() || window == Some(move_.tile.window().id()) {
+ return;