diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2024-05-10 16:58:53 +0400 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2024-05-10 20:23:08 +0400 |
| commit | 9dfa121b8e31082314d1c9347a60ef2e596494cb (patch) | |
| tree | 6e9b90f2f1d54213444fb89b4739e0c8948ed1e9 /src | |
| parent | c4ebb9f58e7ea1d2e688d3ee9483a7a1b3dd52b4 (diff) | |
| download | niri-9dfa121b8e31082314d1c9347a60ef2e596494cb.tar.gz niri-9dfa121b8e31082314d1c9347a60ef2e596494cb.tar.bz2 niri-9dfa121b8e31082314d1c9347a60ef2e596494cb.zip | |
Implement interactive mouse resizing
Diffstat (limited to 'src')
| -rw-r--r-- | src/handlers/compositor.rs | 18 | ||||
| -rw-r--r-- | src/handlers/xdg_shell.rs | 44 | ||||
| -rw-r--r-- | src/input.rs | 41 | ||||
| -rw-r--r-- | src/layout/mod.rs | 161 | ||||
| -rw-r--r-- | src/layout/monitor.rs | 43 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 353 | ||||
| -rw-r--r-- | src/lib.rs | 1 | ||||
| -rw-r--r-- | src/niri.rs | 4 | ||||
| -rw-r--r-- | src/resize_grab.rs | 171 | ||||
| -rw-r--r-- | src/utils/mod.rs | 28 | ||||
| -rw-r--r-- | src/window/mapped.rs | 78 |
11 files changed, 875 insertions, 67 deletions
diff --git a/src/handlers/compositor.rs b/src/handlers/compositor.rs index 0bdd7690..5f39d47c 100644 --- a/src/handlers/compositor.rs +++ b/src/handlers/compositor.rs @@ -13,6 +13,7 @@ use smithay::wayland::compositor::{ SurfaceAttributes, }; use smithay::wayland::dmabuf::get_dmabuf; +use smithay::wayland::shell::xdg::XdgToplevelSurfaceData; use smithay::wayland::shm::{ShmHandler, ShmState}; use smithay::{delegate_compositor, delegate_shm}; @@ -237,8 +238,21 @@ impl CompositorHandler for State { return; } + let serial = with_states(surface, |states| { + let role = states + .data_map + .get::<XdgToplevelSurfaceData>() + .unwrap() + .lock() + .unwrap(); + role.configure_serial + }); + if serial.is_none() { + error!("commit on a mapped surface without a configured serial"); + } + // The toplevel remains mapped. - self.niri.layout.update_window(&window); + self.niri.layout.update_window(&window, serial); // Popup placement depends on window size which might have changed. self.update_reactive_popups(&window, &output); @@ -256,7 +270,7 @@ impl CompositorHandler for State { let window = mapped.window.clone(); let output = output.clone(); window.on_commit(); - self.niri.layout.update_window(&window); + self.niri.layout.update_window(&window, None); self.niri.queue_redraw(&output); return; } diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index 9864ccdc..5e255d17 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -11,6 +11,7 @@ use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::{se use smithay::reexports::wayland_server::protocol::wl_output; use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::reexports::wayland_server::Resource; use smithay::utils::{Logical, Rectangle, Serial}; use smithay::wayland::compositor::{ add_pre_commit_hook, send_surface_state, with_states, BufferAssignment, HookId, @@ -31,6 +32,7 @@ use smithay::{ use crate::layout::workspace::ColumnWidth; use crate::niri::{PopupGrabState, State}; +use crate::resize_grab::ResizeGrab; use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped, WindowRef}; impl XdgShellHandler for State { @@ -60,12 +62,44 @@ impl XdgShellHandler for State { fn resize_request( &mut self, - _surface: ToplevelSurface, + surface: ToplevelSurface, _seat: WlSeat, - _serial: Serial, - _edges: ResizeEdge, + serial: Serial, + edges: ResizeEdge, ) { - // FIXME + let pointer = self.niri.seat.get_pointer().unwrap(); + if !pointer.has_grab(serial) { + return; + } + + let Some(start_data) = pointer.grab_start_data() else { + return; + }; + + let Some((focus, _)) = &start_data.focus else { + return; + }; + + let wl_surface = surface.wl_surface(); + if !focus.id().same_client_as(&wl_surface.id()) { + return; + } + + let Some((mapped, _)) = self.niri.layout.find_window_and_output(wl_surface) else { + return; + }; + + let window = mapped.window.clone(); + if !self + .niri + .layout + .interactive_resize_begin(window.clone(), edges.into()) + { + return; + } + + let grab = ResizeGrab::new(start_data, window); + pointer.set_grab(self, grab, serial, Focus::Clear); } fn reposition_request( @@ -799,7 +833,7 @@ impl State { drop(config); let output = output.cloned(); let window = mapped.window.clone(); - self.niri.layout.update_window(&window); + self.niri.layout.update_window(&window, None); if let Some(output) = output { self.niri.queue_redraw(&output); diff --git a/src/input.rs b/src/input.rs index 6a75960a..bb970dbf 100644 --- a/src/input.rs +++ b/src/input.rs @@ -10,16 +10,17 @@ use niri_ipc::LayoutSwitchTarget; use smithay::backend::input::{ AbsolutePositionEvent, Axis, AxisSource, ButtonState, Device, DeviceCapability, Event, GestureBeginEvent, GestureEndEvent, GesturePinchUpdateEvent as _, GestureSwipeUpdateEvent as _, - InputBackend, InputEvent, KeyState, KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, - PointerMotionEvent, ProximityState, TabletToolButtonEvent, TabletToolEvent, + InputBackend, InputEvent, KeyState, KeyboardKeyEvent, MouseButton, PointerAxisEvent, + PointerButtonEvent, PointerMotionEvent, ProximityState, TabletToolButtonEvent, TabletToolEvent, TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, TouchEvent, }; use smithay::backend::libinput::LibinputInputBackend; use smithay::input::keyboard::{keysyms, FilterResult, Keysym, ModifiersState}; use smithay::input::pointer::{ - AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent, + AxisFrame, ButtonEvent, CursorImageStatus, Focus, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, - GestureSwipeEndEvent, GestureSwipeUpdateEvent, MotionEvent, RelativeMotionEvent, + GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData, + MotionEvent, RelativeMotionEvent, }; use smithay::input::touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent}; use smithay::utils::{Logical, Point, SERIAL_COUNTER}; @@ -27,6 +28,7 @@ use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerCons use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait}; use crate::niri::State; +use crate::resize_grab::ResizeGrab; use crate::ui::screenshot_ui::ScreenshotUi; use crate::utils::spawning::spawn; use crate::utils::{center, get_monotonic_time}; @@ -1073,6 +1075,37 @@ impl State { let window = mapped.window.clone(); self.niri.layout.activate_window(&window); + // Check if we need to start an interactive resize. + if event.button() == Some(MouseButton::Right) && !pointer.is_grabbed() { + let mods = self.niri.seat.get_keyboard().unwrap().modifier_state(); + let mod_down = match self.backend.mod_key() { + CompositorMod::Super => mods.logo, + CompositorMod::Alt => mods.alt, + }; + if mod_down { + let location = pointer.current_location(); + let (output, pos_within_output) = self.niri.output_under(location).unwrap(); + let edges = self + .niri + .layout + .resize_edges_under(output, pos_within_output) + .unwrap(); + if self + .niri + .layout + .interactive_resize_begin(window.clone(), edges) + { + let start_data = PointerGrabStartData { + focus: None, + button: event.button_code(), + location, + }; + let grab = ResizeGrab::new(start_data, window); + pointer.set_grab(self, grab, serial, Focus::Clear); + } + } + } + // FIXME: granular. self.niri.queue_redraw_all(); } else if let Some(output) = self.niri.output_under_cursor() { diff --git a/src/layout/mod.rs b/src/layout/mod.rs index b93dfb4d..ad241d94 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -43,7 +43,7 @@ use smithay::backend::renderer::element::Id; use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture}; use smithay::output::Output; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; -use smithay::utils::{Logical, Point, Scale, Size, Transform}; +use smithay::utils::{Logical, Point, Scale, Serial, Size, Transform}; use self::monitor::Monitor; pub use self::monitor::MonitorRenderElement; @@ -52,7 +52,7 @@ use crate::niri_render_elements; use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::snapshot::RenderSnapshot; use crate::render_helpers::{BakedBuffer, RenderTarget, SplitElements}; -use crate::utils::output_size; +use crate::utils::{output_size, ResizeEdge}; use crate::window::ResolvedWindowRules; pub mod closing_window; @@ -74,9 +74,16 @@ niri_render_elements! { pub type LayoutElementRenderSnapshot = RenderSnapshot<BakedBuffer<TextureBuffer<GlesTexture>>, BakedBuffer<SolidColorBuffer>>; +#[derive(Debug, Clone, Copy)] +pub struct InteractiveResizeData { + pub edges: ResizeEdge, + pub original_view_offset: i32, + pub original_column_width: i32, +} + pub trait LayoutElement { /// Type that can be used as a unique ID of this element. - type Id: PartialEq; + type Id: PartialEq + std::fmt::Debug; /// Unique ID of this element. fn id(&self) -> &Self::Id; @@ -166,6 +173,10 @@ pub trait LayoutElement { fn animation_snapshot(&self) -> Option<&LayoutElementRenderSnapshot>; fn take_animation_snapshot(&mut self) -> Option<LayoutElementRenderSnapshot>; + + fn set_interactive_resize(&mut self, data: Option<InteractiveResizeData>); + fn cancel_interactive_resize(&mut self); + fn interactive_resize_data(&mut self, serial: Serial) -> Option<InteractiveResizeData>; } #[derive(Debug)] @@ -669,13 +680,13 @@ impl<W: LayoutElement> Layout<W> { rv } - pub fn update_window(&mut self, window: &W::Id) { + pub fn update_window(&mut self, window: &W::Id, serial: Option<Serial>) { match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { for ws in &mut mon.workspaces { if ws.has_window(window) { - ws.update_window(window); + ws.update_window(window, serial); return; } } @@ -684,7 +695,7 @@ impl<W: LayoutElement> Layout<W> { MonitorSet::NoOutputs { workspaces, .. } => { for ws in workspaces { if ws.has_window(window) { - ws.update_window(window); + ws.update_window(window, serial); return; } } @@ -1225,6 +1236,19 @@ impl<W: LayoutElement> Layout<W> { mon.window_under(pos_within_output) } + pub fn resize_edges_under( + &self, + output: &Output, + pos_within_output: Point<f64, Logical>, + ) -> Option<ResizeEdge> { + let MonitorSet::Normal { monitors, .. } = &self.monitor_set else { + return None; + }; + + let mon = monitors.iter().find(|mon| &mon.output == output)?; + mon.resize_edges_under(pos_within_output) + } + #[cfg(test)] fn verify_invariants(&self) { use std::collections::HashSet; @@ -1760,6 +1784,79 @@ impl<W: LayoutElement> Layout<W> { None } + pub fn interactive_resize_begin(&mut self, window: W::Id, edges: ResizeEdge) -> bool { + match &mut self.monitor_set { + MonitorSet::Normal { monitors, .. } => { + for mon in monitors { + for ws in &mut mon.workspaces { + if ws.has_window(&window) { + return ws.interactive_resize_begin(window, edges); + } + } + } + } + MonitorSet::NoOutputs { workspaces, .. } => { + for ws in workspaces { + if ws.has_window(&window) { + return ws.interactive_resize_begin(window, edges); + } + } + } + } + + false + } + + pub fn interactive_resize_update( + &mut self, + window: &W::Id, + delta: Point<f64, Logical>, + ) -> bool { + match &mut self.monitor_set { + MonitorSet::Normal { monitors, .. } => { + for mon in monitors { + for ws in &mut mon.workspaces { + if ws.has_window(window) { + return ws.interactive_resize_update(window, delta); + } + } + } + } + MonitorSet::NoOutputs { workspaces, .. } => { + for ws in workspaces { + if ws.has_window(window) { + return ws.interactive_resize_update(window, delta); + } + } + } + } + + false + } + + pub fn interactive_resize_end(&mut self, window: &W::Id) { + match &mut self.monitor_set { + MonitorSet::Normal { monitors, .. } => { + for mon in monitors { + for ws in &mut mon.workspaces { + if ws.has_window(window) { + ws.interactive_resize_end(Some(window)); + return; + } + } + } + } + MonitorSet::NoOutputs { workspaces, .. } => { + for ws in workspaces { + if ws.has_window(window) { + ws.interactive_resize_end(Some(window)); + return; + } + } + } + } + } + pub fn move_workspace_down(&mut self) { let Some(monitor) = self.active_monitor() else { return; @@ -2107,6 +2204,14 @@ mod tests { fn take_animation_snapshot(&mut self) -> Option<LayoutElementRenderSnapshot> { None } + + fn set_interactive_resize(&mut self, _data: Option<InteractiveResizeData>) {} + + fn cancel_interactive_resize(&mut self) {} + + fn interactive_resize_data(&mut self, _serial: Serial) -> Option<InteractiveResizeData> { + None + } } fn arbitrary_bbox() -> impl Strategy<Value = Rectangle<i32, Logical>> { @@ -2148,6 +2253,20 @@ mod tests { prop_oneof![(-10f64..10f64), (-50000f64..50000f64),] } + fn arbitrary_resize_edge() -> impl Strategy<Value = ResizeEdge> { + prop_oneof![ + Just(ResizeEdge::RIGHT), + Just(ResizeEdge::BOTTOM), + Just(ResizeEdge::LEFT), + Just(ResizeEdge::TOP), + Just(ResizeEdge::BOTTOM_RIGHT), + Just(ResizeEdge::BOTTOM_LEFT), + Just(ResizeEdge::TOP_RIGHT), + Just(ResizeEdge::TOP_LEFT), + Just(ResizeEdge::empty()), + ] + } + #[derive(Debug, Clone, Copy, Arbitrary)] enum Op { AddOutput(#[proptest(strategy = "1..=5usize")] usize), @@ -2237,6 +2356,24 @@ mod tests { WorkspaceSwitchGestureEnd { cancelled: bool, }, + InteractiveResizeBegin { + #[proptest(strategy = "1..=5usize")] + window: usize, + #[proptest(strategy = "arbitrary_resize_edge()")] + edges: ResizeEdge, + }, + InteractiveResizeUpdate { + #[proptest(strategy = "1..=5usize")] + window: usize, + #[proptest(strategy = "-20000f64..20000f64")] + dx: f64, + #[proptest(strategy = "-20000f64..20000f64")] + dy: f64, + }, + InteractiveResizeEnd { + #[proptest(strategy = "1..=5usize")] + window: usize, + }, } impl Op { @@ -2455,7 +2592,8 @@ mod tests { } if update { - layout.update_window(&id); + // FIXME: serial. + layout.update_window(&id, None); } } Op::MoveWorkspaceToOutput(id) => { @@ -2495,6 +2633,15 @@ mod tests { Op::WorkspaceSwitchGestureEnd { cancelled } => { layout.workspace_switch_gesture_end(cancelled); } + Op::InteractiveResizeBegin { window, edges } => { + layout.interactive_resize_begin(window, edges); + } + Op::InteractiveResizeUpdate { window, dx, dy } => { + layout.interactive_resize_update(&window, Point::from((dx, dy))); + } + Op::InteractiveResizeEnd { window } => { + layout.interactive_resize_end(&window); + } } } } diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs index 8d695139..3833a61a 100644 --- a/src/layout/monitor.rs +++ b/src/layout/monitor.rs @@ -19,7 +19,7 @@ use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::RenderTarget; use crate::rubber_band::RubberBand; use crate::swipe_tracker::SwipeTracker; -use crate::utils::output_size; +use crate::utils::{output_size, ResizeEdge}; /// Amount of touchpad movement to scroll the height of one workspace. const WORKSPACE_GESTURE_MOVEMENT: f64 = 300.; @@ -722,6 +722,47 @@ impl<W: LayoutElement> Monitor<W> { } } + pub fn resize_edges_under(&self, pos_within_output: Point<f64, Logical>) -> Option<ResizeEdge> { + match &self.workspace_switch { + Some(switch) => { + let size = output_size(&self.output); + + let render_idx = switch.current_idx(); + let before_idx = render_idx.floor(); + let after_idx = render_idx.ceil(); + + let offset = ((render_idx - before_idx) * size.h as f64).round() as i32; + + if after_idx < 0. || before_idx as usize >= self.workspaces.len() { + return None; + } + + let after_idx = after_idx as usize; + + let (idx, ws_offset) = if pos_within_output.y < (size.h - offset) as f64 { + if before_idx < 0. { + return None; + } + + (before_idx as usize, Point::from((0, offset))) + } else { + if after_idx >= self.workspaces.len() { + return None; + } + + (after_idx, Point::from((0, -size.h + offset))) + }; + + let ws = &self.workspaces[idx]; + ws.resize_edges_under(pos_within_output + ws_offset.to_f64()) + } + None => { + let ws = &self.workspaces[self.active_workspace_idx]; + ws.resize_edges_under(pos_within_output) + } + } + } + pub fn render_above_top_layer(&self) -> bool { // Render above the top layer only if the view is stationary. if self.workspace_switch.is_some() { diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index a14f804e..1b6b14ff 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -10,19 +10,19 @@ use smithay::desktop::{layer_map_for_output, Window}; use smithay::output::Output; 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, Size}; +use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size}; use smithay::wayland::compositor::send_surface_state; use super::closing_window::{ClosingWindow, ClosingWindowRenderElement}; use super::tile::{Tile, TileRenderElement}; -use super::{LayoutElement, Options}; +use super::{InteractiveResizeData, LayoutElement, Options}; use crate::animation::Animation; use crate::niri_render_elements; use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::RenderTarget; use crate::swipe_tracker::SwipeTracker; use crate::utils::id::IdCounter; -use crate::utils::output_size; +use crate::utils::{output_size, ResizeEdge}; use crate::window::ResolvedWindowRules; /// Amount of touchpad movement to scroll the view for the width of one working area. @@ -57,6 +57,9 @@ pub struct Workspace<W: LayoutElement> { /// Index of the currently active column, if any. pub active_column_idx: usize, + /// Ongoing interactive resize. + interactive_resize: Option<InteractiveResize<W>>, + /// Offset of the view computed from the active column. /// /// Any gaps, including left padding from work area left exclusive zone, is handled @@ -128,6 +131,13 @@ struct ViewGesture { static_view_offset: i32, } +#[derive(Debug)] +struct InteractiveResize<W: LayoutElement> { + window: W::Id, + original_window_size: Size<i32, Logical>, + data: InteractiveResizeData, +} + /// Width of a column. #[derive(Debug, Clone, Copy, PartialEq)] pub enum ColumnWidth { @@ -257,6 +267,7 @@ impl<W: LayoutElement> Workspace<W> { output: Some(output), columns: vec![], active_column_idx: 0, + interactive_resize: None, view_offset: 0, view_offset_adj: None, activate_prev_column_on_removal: None, @@ -275,6 +286,7 @@ impl<W: LayoutElement> Workspace<W> { working_area: Rectangle::from_loc_and_size((0, 0), (1280, 720)), columns: vec![], active_column_idx: 0, + interactive_resize: None, view_offset: 0, view_offset_adj: None, activate_prev_column_on_removal: None, @@ -697,6 +709,7 @@ impl<W: LayoutElement> Workspace<W> { // A different column was activated; reset the flag. self.activate_prev_column_on_removal = None; self.view_offset_before_fullscreen = None; + self.interactive_resize = None; } pub fn has_windows(&self) -> bool { @@ -957,6 +970,13 @@ impl<W: LayoutElement> Workspace<W> { tile.window().output_leave(output); } + // Stop interactive resize. + if let Some(resize) = &self.interactive_resize { + if tile.window().id() == &resize.window { + self.interactive_resize = None; + } + } + if column.tiles.is_empty() { if column_idx + 1 == self.active_column_idx { // The previous column, that we were going to activate upon removal of the active @@ -1055,6 +1075,17 @@ impl<W: LayoutElement> Workspace<W> { } } + // Stop interactive resize. + if let Some(resize) = &self.interactive_resize { + if column + .tiles + .iter() + .any(|tile| tile.window().id() == &resize.window) + { + self.interactive_resize = None; + } + } + if column_idx + 1 == self.active_column_idx { // The previous column, that we were going to activate upon removal of the active // column, has just been itself removed. @@ -1109,7 +1140,7 @@ impl<W: LayoutElement> Workspace<W> { .into_window() } - pub fn update_window(&mut self, window: &W::Id) { + pub fn update_window(&mut self, window: &W::Id, serial: Option<Serial>) { let (col_idx, column) = self .columns .iter_mut() @@ -1153,31 +1184,64 @@ impl<W: LayoutElement> Workspace<W> { } } - if col_idx == self.active_column_idx - && !matches!(self.view_offset_adj, Some(ViewOffsetAdjustment::Gesture(_))) - { - // We might need to move the view to ensure the resized window is still visible. - let current_x = self.view_pos(); - - // Upon unfullscreening, restore the view offset. - let is_fullscreen = self.columns[col_idx].tiles[tile_idx].is_fullscreen(); - if was_fullscreen && !is_fullscreen { - if let Some(prev_offset) = self.view_offset_before_fullscreen.take() { - self.animate_view_offset(current_x, col_idx, prev_offset); + if col_idx == self.active_column_idx { + let col = &mut self.columns[col_idx]; + let tile = &mut col.tiles[tile_idx]; + let window = tile.window_mut(); + + let resize = serial.and_then(|serial| window.interactive_resize_data(serial)); + + // If this was the last resize commit, this function will now return None. This way we + // can animate the window into view after the last resize commit. + let resize_still_ongoing = serial + .and_then(|serial| window.interactive_resize_data(serial)) + .is_some(); + + if let Some(resize) = resize { + // If this is an interactive resize commit of an active window, then we need to + // either preserve the view offset or adjust it accordingly. + let centered = self.options.center_focused_column == CenterFocusedColumn::Always; + + let width = col.width(); + if centered { + self.view_offset = + -(self.working_area.size.w - width) / 2 - self.working_area.loc.x; + } else if resize.edges.contains(ResizeEdge::LEFT) { + self.view_offset = + resize.original_view_offset + width - resize.original_column_width; } + + // We *could* compute the right offsets here to preserve the gesture but it's + // pretty edge-case-y so it's fine to just cancel it. + self.view_offset_adj = None; } - // Synchronize the horizontal view movement with the resize so that it looks nice. This - // is especially important for always-centered view. - let config = if started_resize_anim { - self.options.animations.window_resize.anim - } else { - self.options.animations.horizontal_view_movement.0 - }; + if !resize_still_ongoing + && !matches!(self.view_offset_adj, Some(ViewOffsetAdjustment::Gesture(_))) + { + // We might need to move the view to ensure the resized window is still visible. + let current_x = self.view_pos(); - // FIXME: we will want to skip the animation in some cases here to make continuously - // resizing windows not look janky. - self.animate_view_offset_to_column_with_config(current_x, col_idx, None, config); + // Upon unfullscreening, restore the view offset. + let is_fullscreen = self.columns[col_idx].tiles[tile_idx].is_fullscreen(); + if was_fullscreen && !is_fullscreen { + if let Some(prev_offset) = self.view_offset_before_fullscreen.take() { + self.animate_view_offset(current_x, col_idx, prev_offset); + } + } + + // Synchronize the horizontal view movement with the resize so that it looks nice. + // This is especially important for always-centered view. + let config = if started_resize_anim { + self.options.animations.window_resize.anim + } else { + self.options.animations.horizontal_view_movement.0 + }; + + // FIXME: we will want to skip the animation in some cases here to make continuously + // resizing windows not look janky. + self.animate_view_offset_to_column_with_config(current_x, col_idx, None, config); + } } } @@ -1332,10 +1396,11 @@ impl<W: LayoutElement> Workspace<W> { column.verify_invariants(); } + let col = &self.columns[self.active_column_idx]; + // When we have an unfullscreen view offset stored, the active column should have a // fullscreen tile. if self.view_offset_before_fullscreen.is_some() { - let col = &self.columns[self.active_column_idx]; assert!( col.is_fullscreen || col.tiles.iter().any(|tile| { @@ -1344,6 +1409,16 @@ impl<W: LayoutElement> Workspace<W> { ); } } + + if let Some(resize) = &self.interactive_resize { + assert!( + self.columns + .iter() + .flat_map(|col| &col.tiles) + .any(|tile| tile.window().id() == &resize.window), + "interactive resize window must be present on the workspace" + ); + } } pub fn focus_left(&mut self) { @@ -1394,7 +1469,8 @@ impl<W: LayoutElement> Workspace<W> { let current_col_x = self.column_x(self.active_column_idx); let next_col_x = self.column_x(self.active_column_idx + 1); - let column = self.columns.remove(self.active_column_idx); + let mut column = self.columns.remove(self.active_column_idx); + cancel_resize_if_this_column(&mut self.interactive_resize, &mut column); self.columns.insert(new_idx, column); // Preserve the camera position when moving to the left. @@ -1696,6 +1772,11 @@ impl<W: LayoutElement> Workspace<W> { self.active_column_idx, self.options.animations.horizontal_view_movement.0, ); + + if !self.columns.is_empty() { + let col = &mut self.columns[self.active_column_idx]; + cancel_resize_if_this_column(&mut self.interactive_resize, col); + } } fn view_pos(&self) -> i32 { @@ -1818,12 +1899,48 @@ impl<W: LayoutElement> Workspace<W> { }) } + pub fn resize_edges_under(&self, pos: Point<f64, Logical>) -> Option<ResizeEdge> { + if self.columns.is_empty() { + return None; + } + + self.tiles_in_render_order().find_map(|(tile, tile_pos)| { + let pos_within_tile = pos - tile_pos.to_f64(); + + // This logic should be consistent with window_under() in when it returns Some vs. + // None. + if tile.is_in_input_region(pos_within_tile) + || tile.is_in_activation_region(pos_within_tile) + { + let size = tile.tile_size().to_f64(); + + let mut edges = ResizeEdge::empty(); + if pos_within_tile.x < size.w / 3. { + edges |= ResizeEdge::LEFT; + } else if 2. * size.w / 3. < pos_within_tile.x { + edges |= ResizeEdge::RIGHT; + } + if pos_within_tile.y < size.h / 3. { + edges |= ResizeEdge::TOP; + } else if 2. * size.h / 3. < pos_within_tile.y { + edges |= ResizeEdge::BOTTOM; + } + return Some(edges); + } + + None + }) + } + pub fn toggle_width(&mut self) { if self.columns.is_empty() { return; } - self.columns[self.active_column_idx].toggle_width(); + let col = &mut self.columns[self.active_column_idx]; + col.toggle_width(); + + cancel_resize_if_this_column(&mut self.interactive_resize, col); } pub fn toggle_full_width(&mut self) { @@ -1831,7 +1948,10 @@ impl<W: LayoutElement> Workspace<W> { return; } - self.columns[self.active_column_idx].toggle_full_width(); + let col = &mut self.columns[self.active_column_idx]; + col.toggle_full_width(); + + cancel_resize_if_this_column(&mut self.interactive_resize, col); } pub fn set_column_width(&mut self, change: SizeChange) { @@ -1839,7 +1959,10 @@ impl<W: LayoutElement> Workspace<W> { return; } - self.columns[self.active_column_idx].set_column_width(change); + let col = &mut self.columns[self.active_column_idx]; + col.set_column_width(change, None, true); + + cancel_resize_if_this_column(&mut self.interactive_resize, col); } pub fn set_window_height(&mut self, change: SizeChange) { @@ -1847,7 +1970,10 @@ impl<W: LayoutElement> Workspace<W> { return; } - self.columns[self.active_column_idx].set_window_height(change); + let col = &mut self.columns[self.active_column_idx]; + col.set_window_height(change, None, true); + + cancel_resize_if_this_column(&mut self.interactive_resize, col); } pub fn set_fullscreen(&mut self, window: &W::Id, is_fullscreen: bool) { @@ -1867,6 +1993,8 @@ impl<W: LayoutElement> Workspace<W> { let mut col = &mut self.columns[col_idx]; + cancel_resize_if_this_co |
