From c5fffd6e2c48aa7fb8b45b8bdcd972bbd8ce900b Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Fri, 29 Nov 2024 21:11:02 +0300 Subject: Initial WIP floating window implementation --- src/layout/floating.rs | 815 ++++++++++++++++++++++++++++++++++++++++++++++++ src/layout/mod.rs | 236 ++++++++++++-- src/layout/monitor.rs | 53 +++- src/layout/scrolling.rs | 41 ++- src/layout/workspace.rs | 412 ++++++++++++++++++++++-- 5 files changed, 1492 insertions(+), 65 deletions(-) create mode 100644 src/layout/floating.rs (limited to 'src/layout') 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 { + /// Tiles in top-to-bottom order. + tiles: Vec>, + + /// Extra per-tile data. + data: Vec, + + /// 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, + + /// Ongoing interactive resize. + interactive_resize: Option>, + + /// Windows in the closing animation. + closing_windows: Vec, + + /// Working area for this space. + working_area: Rectangle, + + /// 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, +} + +niri_render_elements! { + FloatingSpaceRenderElement => { + Tile = TileRenderElement, + 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, + + /// Cached position in logical coordinates. + /// + /// Not rounded to physical pixels. + logical_pos: Point, + + /// Cached actual size of the tile. + size: Size, + + /// Working area used for conversions. + working_area: Rectangle, +} + +impl Data { + pub fn new( + working_area: Rectangle, + tile: &Tile, + logical_pos: Point, + ) -> 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) { + if self.working_area == working_area { + return; + } + + self.working_area = working_area; + self.recompute_logical_pos(); + } + + pub fn update(&mut self, tile: &Tile) { + self.size = tile.tile_size(); + } + + pub fn set_logical_pos(&mut self, logical_pos: Point) { + 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 FloatingSpace { + pub fn new( + working_area: Rectangle, + scale: f64, + clock: Clock, + options: Rc, + ) -> 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, + scale: f64, + options: Rc, + ) { + 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) { + 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> + '_ { + self.tiles.iter() + } + + pub fn tiles_mut(&mut self) -> impl Iterator> + '_ { + self.tiles.iter_mut() + } + + pub fn tiles_with_offsets(&self) -> impl Iterator, Point)> + '_ { + let offsets = self.data.iter().map(|d| d.logical_pos); + zip(&self.tiles, offsets) + } + + pub fn tiles_with_offsets_mut( + &mut self, + ) -> impl Iterator, Point)> + '_ { + let offsets = self.data.iter().map(|d| d.logical_pos); + zip(&mut self.tiles, offsets) + } + + pub fn tiles_with_render_positions( + &self, + ) -> impl Iterator, Point)> { + 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, Point)> { + 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 { + 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> { + 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> { + 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 { + 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, pos: Option>, activate: bool) { + self.add_tile_at(0, tile, pos, activate); + } + + fn add_tile_at( + &mut self, + idx: usize, + mut tile: Tile, + pos: Option>, + 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) { + // 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> { + let id = self.active_window_id.clone()?; + Some(self.remove_tile(&id)) + } + + pub fn remove_tile(&mut self, id: &W::Id) -> RemovedTile { + let idx = self.idx_of(id).unwrap(); + self.remove_tile_by_idx(idx) + } + + fn remove_tile_by_idx(&mut self, idx: usize) -> RemovedTile { + 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, + tile_pos: Point, + 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) -> 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( + &self, + renderer: &mut R, + view_rect: Rectangle, + scale: Scale, + target: RenderTarget, + ) -> Vec> { + 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, + ) -> 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 { + 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 { + &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, +) -> Size { + 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 { 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 Layout { } } + // TODO: pos pub fn add_window_by_idx( &mut self, monitor_idx: usize, @@ -752,6 +756,7 @@ impl Layout { activate: bool, width: ColumnWidth, is_full_width: bool, + is_floating: bool, ) { let MonitorSet::Normal { monitors, @@ -762,7 +767,14 @@ impl Layout { 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 Layout { window: W, width: Option, is_full_width: bool, + is_floating: bool, activate: ActivateWindow, ) -> Option<&Output> { let width = self.resolve_default_width(&window, width); @@ -814,7 +827,7 @@ impl Layout { 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 Layout { }) .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 Layout { window: W, width: Option, is_full_width: bool, + is_floating: bool, activate: ActivateWindow, ) -> Option<&Output> { let width = self.resolve_default_width(&window, width); @@ -888,6 +902,7 @@ impl Layout { activate, width, is_full_width, + is_floating, ); Some(&mon.output) } @@ -902,7 +917,7 @@ impl Layout { &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 Layout { window: W, width: Option, 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 Layout { .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 Layout { .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 Layout { window: W, width: Option, is_full_width: bool, + is_floating: bool, activate: ActivateWindow, ) { let width = self.resolve_default_width(&window, width); @@ -998,6 +1022,7 @@ impl Layout { activate, width, is_full_width, + is_floating, ); } @@ -1024,6 +1049,7 @@ impl Layout { tile: move_.tile, width: move_.width, is_full_width: move_.is_full_width, + is_floating: false, }); } } @@ -1390,6 +1416,42 @@ impl Layout { } } + 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 Layout { 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; + } + } + + let workspace = if let Some(window) = window { + Some( + self.workspaces_mut() + .find(|ws| ws.has_window(window)) + .unwrap(), + ) + } else { + self.active_workspace_mut() + }; + + let Some(workspace) = workspace else { + return; + }; + workspace.toggle_window_floating(window); + } + + pub fn switch_focus_floating_tiling(&mut self) { + let Some(workspace) = self.active_workspace_mut() else { + return; + }; + workspace.switch_focus_floating_tiling(); + } + pub fn focus_output(&mut self, output: &Output) { if let MonitorSet::Normal { monitors, @@ -2680,6 +2772,7 @@ impl Layout { activate, removed.width, removed.is_full_width, + removed.is_floating, ); let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set else { @@ -2706,6 +2799,12 @@ impl Layout { let current = &mut monitors[*active_monitor_idx]; let ws = current.active_workspace(); + + if ws.floating_is_active() { + self.move_to_output(None, output, None); + return; + } + let Some(column) = ws.remove_active_column() else { return; }; @@ -3040,10 +3139,17 @@ impl Layout { } .band(sq_dist / INTERACTIVE_MOVE_START_THRESHOLD); - let tile = self + let (is_floating, tile) = self .workspaces_mut() - .flat_map(|ws| ws.tiles_mut()) - .find(|tile| *tile.window().id() == window_id) + .find(|ws| ws.has_window(&window_id)) + .map(|ws| { + ( + ws.is_floating(&window_id), + ws.tiles_mut() + .find(|tile| *tile.window().id() == window_id) + .unwrap(), + ) + }) .unwrap(); tile.interactive_move_offset = pointer_delta.upscale(factor); @@ -3054,7 +3160,7 @@ impl Layout { pointer_ratio_within_window, }); - if sq_dist < INTERACTIVE_MOVE_START_THRESHOLD { + if !is_floating && sq_dist < INTERACTIVE_MOVE_START_THRESHOLD { return true; } @@ -3063,8 +3169,8 @@ impl Layout { // to the pointer. Otherwise, we just teleport it as the layout code is not aware // of monitor positions. // - // FIXME: with floating layer, the layout code will know about monitor positions, - // so this will be potentially animatable. + // FIXME: when and if the layout code knows about monitor positions, this will be + // potentially animatable. let mut tile_pos = None; if let MonitorSet::Normal { monitors, .. } = &self.monitor_set { if let Some((mon, (ws, ws_offset))) = monitors.iter().find_map(|mon| { @@ -3087,6 +3193,7 @@ impl Layout { mut tile, width, is_full_width, + is_floating: _, } = self.remove_window(window, Transaction::new()).unwrap(); tile.stop_move_animations(); @@ -3105,6 +3212,7 @@ impl Layout { // Unfullscreen and let the window pick a natural size. // + // TODO // When we have floating, we will want to always send a (0, 0) size here, not just // to unfullscreen. However, when implementing that, remember to check how GTK // tiled window size restoration works. It seems to remember *some* last size with @@ -3265,6 +3373,10 @@ impl Layout { true, ); } + InsertPosition::Floating => { + let pos = move_.tile_render_location() - offset; + mon.add_floating_tile(ws_idx, move_.tile, Some(pos), true); + } } // needed because empty_workspace_above_first could have modified the idx @@ -3911,6 +4023,8 @@ mod tests { (0f64..).prop_map(SizeChange::SetProportion), any::().prop_map(SizeChange::AdjustFixed), any::().prop_map(SizeChange::AdjustProportion), + // Interactive resize can have negative values here. + Just(SizeChange::SetFixed(-100)), ] } @@ -3925,11 +4039,19 @@ mod tests { } fn arbitrary_min_max_size() -> impl Strategy, Size)> { - (arbitrary_min_max(), arbitrary_min_max()).prop_map(|((min_w, max_w), (min_h, max_h))| { - let min_size = Size::from((min_w, min_h)); - let max_size = Size::from((max_w, max_h)); - (min_size, max_size) - }) + prop_oneof![ + 5 => (arbitrary_min_max(), arbitrary_min_max()).prop_map( + |((min_w, max_w), (min_h, max_h))| { + let min_size = Size::from((min_w, min_h)); + let max_size = Size::from((max_w, max_h)); + (min_size, max_size) + }, + ), + 1 => arbitrary_min_max().prop_map(|(w, h)| { + let size = Size::from((w, h)); + (size, size) + }), + ] } fn arbitrary_view_offset_gesture_delta() -> impl Strategy { @@ -4103,6 +4225,11 @@ mod tests { #[proptest(strategy = "proptest::option::of(1..=5usize)")] id: Option, }, + ToggleWindowFloating { + #[proptest(strategy = "proptest::option::of(1..=5usize)")] + id: Option, + }, + SwitchFocusFloatingTiling, Communicate(#[proptest(strategy = "1..=5usize")] usize), Refresh { is_active: bool, @@ -4294,7 +4421,8 @@ mod tests { } let win = TestWindow::new(id, bbox, min_max_size.0, min_max_size.1); - layout.add_window(win, None, false, ActivateWindow::default()); + let is_floating = min_max_size.0.h > 0 && min_max_size.0.h == min_max_size.1.h; + layout.add_window(win, None, false, is_floating, ActivateWindow::default()); } Op::AddWindowRightOf { id, @@ -4350,7 +4478,8 @@ mod tests { } let win = TestWindow::new(id, bbox, min_max_size.0, min_max_size.1); - layout.add_window_right_of(&right_of_id, win, None, false); + let is_floating = min_max_size.0.h > 0 && min_max_size.0.h == min_max_size.1.h; + layout.add_window_right_of(&right_of_id, win, None, false, is_floating); } Op::AddWindowToNamedWorkspace { id, @@ -4411,11 +4540,13 @@ mod tests { } let win = TestWindow::new(id, bbox, min_max_size.0, min_max_size.1); + let is_floating = min_max_size.0.h > 0 && min_max_size.0.h == min_max_size.1.h; layout.add_window_to_named_workspace( &ws_name, win, None, false, + is_floating, ActivateWindow::default(), ); } @@ -4575,6 +4706,13 @@ mod tests { let id = id.filter(|id| layout.has_window(id)); layout.reset_window_height(id.as_ref()); } + Op::ToggleWindowFloating { id } => { + let id = id.filter(|id| layout.has_window(id)); + layout.toggle_window_floating(id.as_ref()); + } + Op::SwitchFocusFloatingTiling => { + layout.switch_focus_floating_tiling(); + } Op::Communicate(id) => { let mut update = false; @@ -6191,6 +6329,62 @@ mod tests { check_ops(&ops); } + #[test] + fn set_width_fixed_negative() { + let ops = [ + Op::AddOutput(3), + Op::AddWindow { + id: 3, + bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)), + min_max_size: Default::default(), + }, + Op::ToggleWindowFloating { id: Some(3) }, + Op::SetColumnWidth(SizeChange::SetFixed(-100)), + ]; + check_ops(&ops); + } + + #[test] + fn set_height_fixed_negative() { + let ops = [ + Op::AddOutput(3), + Op::AddWindow { + id: 3, + bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)), + min_max_size: Default::default(), + }, + Op::ToggleWindowFloating { id: Some(3) }, + Op::SetWindowHeight { + id: None, + change: SizeChange::SetFixed(-100), + }, + ]; + check_ops(&ops); + } + + #[test] + fn interactive_resize_to_negative() { + let ops = [ + Op::AddOutput(3), + Op::AddWindow { + id: 3, + bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)), + min_max_size: Default::default(), + }, + Op::ToggleWindowFloating { id: Some(3) }, + Op::InteractiveResizeBegin { + window: 3, + edges: ResizeEdge::BOTTOM_RIGHT, + }, + Op::InteractiveResizeUpdate { + window: 3, + dx: -10000., + dy: -10000., + }, + ]; + check_ops(&ops); + } + #[test] fn windows_on_other_workspaces_remain_activated() { let ops = [ diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs index 67b09e20..7bfc1084 100644 --- a/src/layout/monitor.rs +++ b/src/layout/monitor.rs @@ -225,10 +225,11 @@ impl Monitor { activate: bool, width: ColumnWidth, is_full_width: bool, + is_floating: bool, ) { let workspace = &mut self.workspaces[workspace_idx]; - workspace.add_window(window, activate, width, is_full_width); + workspace.add_window(window, activate, width, is_full_width, is_floating); // After adding a new window, workspace becomes this output's own. workspace.original_output = OutputId::new(&self.output); @@ -253,6 +254,7 @@ impl Monitor { window: W, width: ColumnWidth, is_full_width: bool, + is_floating: bool, ) { let workspace_idx = self .workspaces @@ -261,7 +263,7 @@ impl Monitor { .unwrap(); let workspace = &mut self.workspaces[workspace_idx]; - workspace.add_window_right_of(right_of, window, width, is_full_width); + workspace.add_window_right_of(right_of, window, width, is_full_width, is_floating); // After adding a new window, workspace becomes this output's own. workspace.original_output = OutputId::new(&self.output); @@ -322,6 +324,35 @@ impl Monitor { } } + pub fn add_floating_tile( + &mut self, + mut workspace_idx: usize, + tile: Tile, + pos: Option>, + activate: bool, + ) { + let workspace = &mut self.workspaces[workspace_idx]; + + workspace.add_floating_tile(tile, pos, activate); + + // 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. + 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_to_column( &mut self, workspace_idx: usize, @@ -505,6 +536,7 @@ impl Monitor { true, removed.width, removed.is_full_width, + removed.is_floating, ); } @@ -527,6 +559,7 @@ impl Monitor { true, removed.width, removed.is_full_width, + removed.is_floating, ); } @@ -565,6 +598,7 @@ impl Monitor { activate, removed.width, removed.is_full_width, + removed.is_floating, ); if self.workspace_switch.is_none() { @@ -581,6 +615,11 @@ impl Monitor { } 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; }; @@ -597,6 +636,11 @@ impl Monitor { } 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; }; @@ -613,6 +657,11 @@ impl Monitor { } let workspace = &mut self.workspaces[source_workspace_idx]; + if workspace.floating_is_active() { + self.move_to_workspace(None, idx); + return; + } + let Some(column) = workspace.remove_active_column() else { return; }; diff --git a/src/layout/scrolling.rs b/src/layout/scrolling.rs index 30c20898..83e13c52 100644 --- a/src/layout/scrolling.rs +++ b/src/layout/scrolling.rs @@ -26,6 +26,11 @@ use crate::window::ResolvedWindowRules; /// Amount of touchpad movement to scroll the view for the width of one working area. const VIEW_GESTURE_WORKING_AREA_MOVEMENT: f64 = 1200.; +/// Maximum distance to the target position for an interactively moved window. +/// +/// If the distance is higher than this, the window will instead remain floating. +const WINDOW_INSERT_MAX_DISTANCE: f64 = 50.; + /// A scrollable-tiling space for windows. #[derive(Debug)] pub struct ScrollingSpace { @@ -102,6 +107,7 @@ niri_render_elements! { pub enum InsertPosition { NewColumn(usize), InColumn(usize, usize), + Floating, } #[derive(Debug)] @@ -359,6 +365,10 @@ impl ScrollingSpace { self.columns.iter_mut().flat_map(|col| col.tiles.iter_mut()) } + pub fn is_empty(&self) -> bool { + self.columns.is_empty() + } + pub fn active_window(&self) -> Option<&W> { if self.columns.is_empty() { return None; @@ -652,9 +662,9 @@ impl ScrollingSpace { self.insert_hint = None; } - pub fn get_insert_position(&self, pos: Point) -> InsertPosition { + fn compute_insert_position(&self, pos: Point) -> (InsertPosition, f64) { if self.columns.is_empty() { - return InsertPosition::NewColumn(0); + return (InsertPosition::NewColumn(0), pos.x.abs()); } let x = pos.x + self.view_pos(); @@ -665,7 +675,7 @@ impl ScrollingSpace { // Insert position is before the first column. if x < 0. { - return InsertPosition::NewColumn(0); + return (InsertPosition::NewColumn(0), -x); } // Find the closest gap between columns. @@ -685,7 +695,8 @@ impl ScrollingSpace { // Insert position is past the last column. if col_idx == self.columns.len() { - return InsertPosition::NewColumn(closest_col_idx); + // col_x should be what we expect if pos.x is past the last column. + return (InsertPosition::NewColumn(closest_col_idx), x - col_x); } // Find the closest gap between tiles. @@ -700,10 +711,21 @@ impl ScrollingSpace { 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) + (InsertPosition::NewColumn(closest_col_idx), vert_dist) } else { - InsertPosition::InColumn(col_idx, closest_tile_idx) + ( + InsertPosition::InColumn(col_idx, closest_tile_idx), + hor_dist, + ) + } + } + + pub fn get_insert_position(&self, pos: Point) -> InsertPosition { + let (position, distance) = self.compute_insert_position(pos); + if distance > WINDOW_INSERT_MAX_DISTANCE { + return InsertPosition::Floating; } + position } pub fn add_tile( @@ -894,6 +916,7 @@ impl ScrollingSpace { tile: column.tiles.remove(tile_idx), width: column.width, is_full_width: column.is_full_width, + is_floating: false, }; } @@ -929,6 +952,7 @@ impl ScrollingSpace { tile, width: column.width, is_full_width: column.is_full_width, + is_floating: false, }; column.active_tile_idx = min(column.active_tile_idx, column.tiles.len() - 1); @@ -1244,7 +1268,7 @@ impl ScrollingSpace { self.start_close_animation_for_tile(renderer, snapshot, tile_size, tile_pos, blocker); } - pub fn start_close_animation_for_tile( + fn start_close_animation_for_tile( &mut self, renderer: &mut GlesRenderer, snapshot: TileRenderSnapshot, @@ -1934,6 +1958,7 @@ impl ScrollingSpace { let loc = Point::from((self.column_x(column_index), y)); Rectangle::from_loc_and_size(loc, size) } + InsertPosition::Floating => return None, }; // First window on an empty workspace will cancel out any view offset. Replicate this @@ -2738,7 +2763,7 @@ impl ScrollingSpace { .iter() .flat_map(|col| &col.tiles) .any(|tile| tile.window().id() == &resize.window), - "interactive resize window must be present on the workspace" + "interactive resize window must be present in the layout" ); } } diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index c3870b8a..21ef208f 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -10,6 +10,7 @@ use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size, Transform}; +use super::floating::{FloatingSpace, FloatingSpaceRenderElement}; use super::scrolling::{ Column, ColumnWidth, InsertHint, InsertPosition, ScrollingSpace, ScrollingSpaceRenderElement, }; @@ -29,6 +30,12 @@ pub struct Workspace { /// The scrollable-tiling layout. scrolling: ScrollingSpace, + /// The floating layout. + floating: FloatingSpace, + + /// Whether the floating layout is active instead of the scrolling layout. + floating_is_active: bool, + /// The original output of this workspace. /// /// Most of the time this will be the workspace's current output, however, after an output @@ -112,6 +119,7 @@ impl WorkspaceId { niri_render_elements! { WorkspaceRenderElement => { Scrolling = ScrollingSpaceRenderElement, + Floating = FloatingSpaceRenderElement, } } @@ -170,8 +178,17 @@ impl Workspace { options.clone(), ); + let floating = FloatingSpace::new( + working_area, + scale.fractional_scale(), + clock.clone(), + options.clone(), + ); + Self { scrolling, + floating, + floating_is_active: false, original_output, scale, transform: output.current_transform(), @@ -213,8 +230,17 @@ impl Workspace { options.clone(), ); + let floating = FloatingSpace::new( + working_area, + scale.fractional_scale(), + clock.clone(), + options.clone(), + ); + Self { scrolling, + floating, + floating_is_active: false, output: None, scale, transform: Transform::Normal, @@ -255,18 +281,24 @@ impl Workspace { pub fn advance_animations(&mut self) { self.scrolling.advance_animations(); + self.floating.advance_animations(); } pub fn are_animations_ongoing(&self) -> bool { - self.scrolling.are_animations_ongoing() + self.scrolling.are_animations_ongoing() || self.floating.are_animations_ongoing() } pub fn are_transitions_ongoing(&self) -> bool { - self.scrolling.are_transitions_ongoing() + self.scrolling.are_transitions_ongoing() || self.floating.are_animations_ongoing() } pub fn update_render_elements(&mut self, is_active: bool) { - self.scrolling.update_render_elements(is_active); + self.scrolling + .update_render_elements(is_active && !self.floating_is_active); + + let view_rect = Rectangle::from_loc_and_size((0., 0.), self.view_size); + self.floating + .update_render_elements(is_active && self.floating_is_active, view_rect); } pub fn update_config(&mut self, base_options: Rc) { @@ -280,12 +312,19 @@ impl Workspace { options.clone(), ); + self.floating.update_config( + self.working_area, + self.scale.fractional_scale(), + options.clone(), + ); + self.base_options = base_options; self.options = options; } pub fn update_shaders(&mut self) { self.scrolling.update_shaders(); + self.floating.update_shaders(); } pub fn windows(&self) -> impl Iterator + '_ { @@ -297,11 +336,19 @@ impl Workspace { } pub fn tiles(&self) -> impl Iterator> + '_ { - self.scrolling.tiles() + let scrolling = self.scrolling.tiles(); + let floating = self.floating.tiles(); + scrolling.chain(floating) } pub fn tiles_mut(&mut self) -> impl Iterator> + '_ { - self.scrolling.tiles_mut() + let scrolling = self.scrolling.tiles_mut(); + let floating = self.floating.tiles_mut(); + scrolling.chain(floating) + } + + pub fn is_floating(&self, id: &W::Id) -> bool { + self.floating.has_window(id) } pub fn current_output(&self) -> Option<&Output> { @@ -309,7 +356,11 @@ impl Workspace { } pub fn active_window(&self) -> Option<&W> { - self.scrolling.active_window() + if self.floating_is_active { + self.floating.active_window() + } else { + self.scrolling.active_window() + } } pub fn is_active_fullscreen(&self) -> bool { @@ -391,6 +442,11 @@ impl Workspace { scale.fractional_scale(), self.options.clone(), ); + self.floating.update_config( + working_area, + scale.fractional_scale(), + self.options.clone(), + ); } if scale_transform_changed { @@ -410,6 +466,7 @@ impl Workspace { activate: bool, width: ColumnWidth, is_full_width: bool, + is_floating: bool, ) { let tile = Tile::new( window, @@ -417,7 +474,12 @@ impl Workspace { self.clock.clone(), self.options.clone(), ); - self.add_tile(None, tile, activate, width, is_full_width); + + if is_floating { + self.add_floating_tile(tile, None, activate); + } else { + self.add_tile(None, tile, activate, width, is_full_width); + } } pub fn add_tile( @@ -431,6 +493,24 @@ impl Workspace { self.enter_output_for_window(tile.window()); self.scrolling .add_tile(col_idx, tile, activate, width, is_full_width, None); + + if activate { + self.floating_is_active = false; + } + } + + pub fn add_floating_tile( + &mut self, + tile: Tile, + pos: Option>, + activate: bool, + ) { + self.enter_output_for_window(tile.window()); + self.floating.add_tile(tile, pos, activate); + + if activate || self.scrolling.is_empty() { + self.floating_is_active = true; + } } pub fn add_tile_to_column( @@ -443,6 +523,10 @@ impl Workspace { self.enter_output_for_window(tile.window()); self.scrolling .add_tile_to_column(col_idx, tile_idx, tile, activate); + + if activate { + self.floating_is_active = false; + } } pub fn add_window_right_of( @@ -451,6 +535,8 @@ impl Workspace { window: W, width: ColumnWidth, is_full_width: bool, + // TODO: smarter enum, so you can override is_floating = false for floating right_of. + is_floating: bool, ) { let tile = Tile::new( window, @@ -458,7 +544,7 @@ impl Workspace { self.clock.clone(), self.options.clone(), ); - self.add_tile_right_of(right_of, tile, width, is_full_width); + self.add_tile_right_of(right_of, tile, width, is_full_width, is_floating); } pub fn add_tile_right_of( @@ -467,10 +553,36 @@ impl Workspace { tile: Tile, width: ColumnWidth, is_full_width: bool, + is_floating: bool, ) { self.enter_output_for_window(tile.window()); - self.scrolling - .add_tile_right_of(right_of, tile, width, is_full_width); + + let floating_has_window = self.floating.has_window(right_of); + if is_floating || floating_has_window { + if floating_has_window { + self.floating.add_tile_above(right_of, tile); + } else { + let activate = self.scrolling.active_window().unwrap().id() == right_of; + // FIXME: use static pos + let (right_of_tile, render_pos) = self + .scrolling + .tiles_with_render_positions() + .find(|(tile, _)| tile.window().id() == right_of) + .unwrap(); + // Position the new tile in the center above the right_of tile. Think a dialog + // opening on top of a window. + let pos = render_pos + + (right_of_tile.tile_size().to_point() - tile.tile_size().to_point()) + .downscale(2.); + self.floating.add_tile(tile, Some(pos), activate); + if activate { + self.floating_is_active = true; + } + } + } else { + self.scrolling + .add_tile_right_of(right_of, tile, width, is_full_width); + } } pub fn add_column(&mut self, column: Column, activate: bool) { @@ -479,29 +591,66 @@ impl Workspace { } self.scrolling.add_column(None, column, activate, None); + + if activate { + self.floating_is_active = false; + } + } + + fn update_focus_floating_tiling_after_removing(&mut self, removed_from_floating: bool) { + if removed_from_floating { + if self.floating.is_empty() { + self.floating_is_active = false; + } + } else { + // Scrolling should remain focused if both are empty. + if self.scrolling.is_empty() && !self.floating.is_empty() { + self.floating_is_active = true; + } + } } pub fn remove_tile(&mut self, id: &W::Id, transaction: Transaction) -> RemovedTile { - let removed = self.scrolling.remove_tile(id, transaction); + let mut from_floating = false; + let removed = if self.floating.has_window(id) { + from_floating = true; + self.floating.remove_tile(id) + } else { + self.scrolling.remove_tile(id, transaction) + }; if let