diff options
Diffstat (limited to 'src/layout')
| -rw-r--r-- | src/layout/floating.rs | 815 | ||||
| -rw-r--r-- | src/layout/mod.rs | 236 | ||||
| -rw-r--r-- | src/layout/monitor.rs | 53 | ||||
| -rw-r--r-- | src/layout/scrolling.rs | 41 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 412 |
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; |
