From 3a535a53e84b050faede33d7b735991d9dd84f47 Mon Sep 17 00:00:00 2001 From: Rasmus Eneman Date: Mon, 15 Jul 2024 15:51:48 +0200 Subject: [PATCH 1/6] Implement interactive window move --- niri-config/src/lib.rs | 24 ++ resources/default-config.kdl | 7 + src/handlers/xdg_shell.rs | 62 ++++- src/input/mod.rs | 54 ++++- src/input/move_grab.rs | 217 +++++++++++++++++ src/ipc/server.rs | 6 +- src/layout/mod.rs | 449 ++++++++++++++++++++++++++++++++++- src/layout/workspace.rs | 212 ++++++++++++++++- src/niri.rs | 15 ++ src/window/mapped.rs | 10 + 10 files changed, 1035 insertions(+), 21 deletions(-) create mode 100644 src/input/move_grab.rs diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index dd81e665..bbbb217b 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -391,6 +391,8 @@ pub struct Layout { pub focus_ring: FocusRing, #[knuffel(child, default)] pub border: Border, + #[knuffel(child, default)] + pub insert_hint: InsertHint, #[knuffel(child, unwrap(children), default)] pub preset_column_widths: Vec, #[knuffel(child)] @@ -412,6 +414,7 @@ impl Default for Layout { Self { focus_ring: Default::default(), border: Default::default(), + insert_hint: Default::default(), preset_column_widths: Default::default(), default_column_width: Default::default(), center_focused_column: Default::default(), @@ -558,6 +561,23 @@ impl From for Border { } } +#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] +pub struct InsertHint { + #[knuffel(child)] + pub off: bool, + #[knuffel(child, default = Self::default().color)] + pub color: Color, +} + +impl Default for InsertHint { + fn default() -> Self { + Self { + off: false, + color: Color::from_rgba8_unpremul(127, 200, 255, 128), + } + } +} + /// RGB color in [0, 1] with unpremultiplied alpha. #[derive(Debug, Default, Clone, Copy, PartialEq)] pub struct Color { @@ -3146,6 +3166,10 @@ mod tests { active_gradient: None, inactive_gradient: None, }, + insert_hint: InsertHint { + off: false, + color: Color::from_rgba8_unpremul(255, 200, 127, 255), + }, preset_column_widths: vec![ PresetSize::Proportion(0.25), PresetSize::Proportion(0.5), diff --git a/resources/default-config.kdl b/resources/default-config.kdl index 4dce86f7..f9060bde 100644 --- a/resources/default-config.kdl +++ b/resources/default-config.kdl @@ -181,6 +181,13 @@ layout { // inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" } + insert-hint { + off + + color "#7fc8ff88" + } + + // Struts shrink the area occupied by windows, similarly to layer-shell panels. // You can think of them as a kind of outer gaps. They are set in logical pixels. // Left and right struts will cause the next window to the side to always be visible. diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index be7dc813..3a8bef4b 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -6,7 +6,7 @@ use smithay::desktop::{ PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window, WindowSurfaceType, }; -use smithay::input::pointer::Focus; +use smithay::input::pointer::{Focus, PointerGrab}; use smithay::output::Output; use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_positioner::ConstraintAdjustment; @@ -36,6 +36,7 @@ use smithay::{ }; use tracing::field::Empty; +use crate::input::move_grab::MoveGrab; use crate::input::resize_grab::ResizeGrab; use crate::input::DOUBLE_CLICK_TIME; use crate::layout::workspace::ColumnWidth; @@ -65,8 +66,63 @@ impl XdgShellHandler for State { } } - fn move_request(&mut self, _surface: ToplevelSurface, _seat: WlSeat, _serial: Serial) { - // FIXME + fn move_request(&mut self, surface: ToplevelSurface, _seat: WlSeat, serial: Serial) { + 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, output)) = self.niri.layout.find_window_and_output(wl_surface) else { + return; + }; + + let window = mapped.window.clone(); + let output = output.clone(); + + // See if we got a double move-click gesture. + let time = get_monotonic_time(); + let last_cell = mapped.last_interactive_move_start(); + let last = last_cell.get(); + last_cell.set(Some(time)); + if let Some(last_time) = last { + if time.saturating_sub(last_time) <= DOUBLE_CLICK_TIME { + // Allow quick move after a triple click. + last_cell.set(None); + + // FIXME: don't activate once we can pass specific windows to actions. + self.niri.layout.activate_window(&window); + self.niri.layer_shell_on_demand_focus = None; + // FIXME: granular. + self.niri.queue_redraw_all(); + return; + } + } + + let grab = MoveGrab::new(start_data, window.clone()); + + if !self + .niri + .layout + .interactive_move_begin(window, output, grab.start_data().location) + { + return; + } + + pointer.set_grab(self, grab, serial, Focus::Clear); + self.niri.pointer_grab_ongoing = true; } fn resize_request( diff --git a/src/input/mod.rs b/src/input/mod.rs index a89682b9..ad380cf2 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -28,6 +28,7 @@ use smithay::utils::{Logical, Point, Rectangle, SERIAL_COUNTER}; use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraint}; use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait}; +use self::move_grab::MoveGrab; use self::resize_grab::ResizeGrab; use self::spatial_movement_grab::SpatialMovementGrab; use crate::niri::State; @@ -35,6 +36,7 @@ use crate::ui::screenshot_ui::ScreenshotUi; use crate::utils::spawning::spawn; use crate::utils::{center, get_monotonic_time, ResizeEdge}; +pub mod move_grab; pub mod resize_grab; pub mod scroll_tracker; pub mod spatial_movement_grab; @@ -1528,8 +1530,58 @@ impl State { if let Some(mapped) = self.niri.window_under_cursor() { let window = mapped.window.clone(); + // Check if we need to start an interactive move. + if event.button() == Some(MouseButton::Left) && !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, _) = self.niri.output_under(location).unwrap(); + let output = output.clone(); + + // See and ignore if we got a double move-click gesture. + // FIXME: deduplicate with move_request in xdg-shell somehow. + let time = get_monotonic_time(); + let last_cell = mapped.last_interactive_move_start(); + let last = last_cell.get(); + last_cell.set(Some(time)); + if let Some(last_time) = last { + if time.saturating_sub(last_time) <= DOUBLE_CLICK_TIME { + self.niri.layout.activate_window(&window); + // FIXME: granular. + self.niri.queue_redraw_all(); + return; + } + } + + self.niri.layout.activate_window(&window); + + if self + .niri + .layout + .interactive_move_begin(window.clone(), output, location) + { + let start_data = PointerGrabStartData { + focus: None, + button: event.button_code(), + location, + }; + let grab = MoveGrab::new(start_data, window.clone()); + pointer.set_grab(self, grab, serial, Focus::Clear); + self.niri.pointer_grab_ongoing = true; + self.niri + .cursor_manager + .set_cursor_image(CursorImageStatus::Named(CursorIcon::Move)); + // FIXME: granular. + self.niri.queue_redraw_all(); + } + } + } // Check if we need to start an interactive resize. - if event.button() == Some(MouseButton::Right) && !pointer.is_grabbed() { + else 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, diff --git a/src/input/move_grab.rs b/src/input/move_grab.rs new file mode 100644 index 00000000..27e3bdc0 --- /dev/null +++ b/src/input/move_grab.rs @@ -0,0 +1,217 @@ +use std::time::Duration; + +use smithay::backend::input::ButtonState; +use smithay::desktop::Window; +use smithay::input::pointer::{ + AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent, + GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, + GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData, + MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent, +}; +use smithay::input::SeatHandler; +use smithay::utils::{IsAlive, Logical, Point}; + +use crate::niri::State; + +pub struct MoveGrab { + start_data: PointerGrabStartData, + last_location: Point, + window: Window, + is_moving: bool, +} + +impl MoveGrab { + pub fn new(start_data: PointerGrabStartData, window: Window) -> Self { + Self { + last_location: start_data.location, + start_data, + window, + is_moving: false, + } + } + + fn on_ungrab(&mut self, state: &mut State) { + state.niri.layout.interactive_move_end(&self.window); + state.niri.pointer_grab_ongoing = false; + state + .niri + .cursor_manager + .set_cursor_image(CursorImageStatus::default_named()); + } +} + +impl PointerGrab for MoveGrab { + fn motion( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + _focus: Option<(::PointerFocus, Point)>, + event: &MotionEvent, + ) { + // While the grab is active, no client has pointer focus. + handle.motion(data, None, event); + + if self.window.alive() { + let delta = event.location - self.start_data.location; + let output = data + .niri + .output_under(event.location) + .map(|(output, _)| output) + .cloned(); + let ongoing = data + .niri + .layout + .interactive_move_update(&self.window, output, delta); + if ongoing { + let event_delta = event.location - self.last_location; + self.last_location = event.location; + let timestamp = Duration::from_millis(u64::from(event.time)); + if self.is_moving { + data.niri + .layout + .view_offset_gesture_update(-event_delta.x, timestamp, false); + } + return; + } + } + + // The move is no longer ongoing. + handle.unset_grab(self, data, event.serial, event.time, true); + } + + fn relative_motion( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + _focus: Option<(::PointerFocus, Point)>, + event: &RelativeMotionEvent, + ) { + // While the grab is active, no client has pointer focus. + handle.relative_motion(data, None, event); + } + + fn button( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &ButtonEvent, + ) { + handle.button(data, event); + + // MouseButton::Middle + if event.button == 0x112 { + if event.state == ButtonState::Pressed { + let output = data + .niri + .output_under(handle.current_location()) + .map(|(output, _)| output) + .cloned(); + if let Some(output) = output { + self.is_moving = true; + data.niri.layout.view_offset_gesture_begin(&output, false); + } + } else if event.state == ButtonState::Released { + self.is_moving = false; + data.niri.layout.view_offset_gesture_end(false, None); + } + } + + if handle.current_pressed().is_empty() { + // No more buttons are pressed, release the grab. + handle.unset_grab(self, data, event.serial, event.time, true); + } + } + + fn axis( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + details: AxisFrame, + ) { + handle.axis(data, details); + } + + fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) { + handle.frame(data); + } + + fn gesture_swipe_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeBeginEvent, + ) { + handle.gesture_swipe_begin(data, event); + } + + fn gesture_swipe_update( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeUpdateEvent, + ) { + handle.gesture_swipe_update(data, event); + } + + fn gesture_swipe_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeEndEvent, + ) { + handle.gesture_swipe_end(data, event); + } + + fn gesture_pinch_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchBeginEvent, + ) { + handle.gesture_pinch_begin(data, event); + } + + fn gesture_pinch_update( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchUpdateEvent, + ) { + handle.gesture_pinch_update(data, event); + } + + fn gesture_pinch_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchEndEvent, + ) { + handle.gesture_pinch_end(data, event); + } + + fn gesture_hold_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureHoldBeginEvent, + ) { + handle.gesture_hold_begin(data, event); + } + + fn gesture_hold_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureHoldEndEvent, + ) { + handle.gesture_hold_end(data, event); + } + + fn start_data(&self) -> &PointerGrabStartData { + &self.start_data + } + + fn unset(&mut self, data: &mut State) { + self.on_ungrab(data); + } +} diff --git a/src/ipc/server.rs b/src/ipc/server.rs index 79af18d5..608bb629 100644 --- a/src/ipc/server.rs +++ b/src/ipc/server.rs @@ -551,12 +551,12 @@ impl State { } let Some(ipc_win) = state.windows.get(&id) else { - let window = make_ipc_window(mapped, Some(ws_id)); + let window = make_ipc_window(mapped, ws_id); events.push(Event::WindowOpenedOrChanged { window }); return; }; - let workspace_id = Some(ws_id.get()); + let workspace_id = ws_id.map(|id| id.get()); let mut changed = ipc_win.workspace_id != workspace_id; let wl_surface = mapped.toplevel().wl_surface(); @@ -572,7 +572,7 @@ impl State { }); if changed { - let window = make_ipc_window(mapped, Some(ws_id)); + let window = make_ipc_window(mapped, ws_id); events.push(Event::WindowOpenedOrChanged { window }); return; } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 0a1063fb..60c22b9c 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -43,12 +43,14 @@ use smithay::backend::renderer::element::Id; use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture}; use smithay::output::{self, Output}; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; -use smithay::utils::{Logical, Point, Scale, Serial, Size, Transform}; +use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size, Transform}; use workspace::WorkspaceId; pub use self::monitor::MonitorRenderElement; use self::monitor::{Monitor, WorkspaceSwitch}; -use self::workspace::{compute_working_area, Column, ColumnWidth, OutputId, Workspace}; +use self::tile::{Tile, TileRenderElement}; +use self::workspace::{compute_working_area, Column, ColumnWidth, InsertHint, OutputId, Workspace}; +use crate::layout::workspace::InsertPosition; use crate::niri_render_elements; use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::snapshot::RenderSnapshot; @@ -79,6 +81,21 @@ niri_render_elements! { pub type LayoutElementRenderSnapshot = RenderSnapshot>, BakedBuffer>; +#[derive(Debug)] +pub struct InteractiveMoveData { + /// The window being moved. + pub(self) window: Tile, + /// Output where the window is currently located/rendered. + pub(self) output: Output, + /// Window column width. + pub(self) width: ColumnWidth, + /// Whether the window column was full-width. + pub(self) is_full_width: bool, + pub(self) initial_pointer_location: Point, + pub(self) pointer_offset: Point, + pub(self) window_offset: Point, +} + #[derive(Debug, Clone, Copy)] pub struct InteractiveResizeData { pub(self) edges: ResizeEdge, @@ -210,6 +227,8 @@ pub trait LayoutElement { pub struct Layout { /// Monitors and workspaes in the layout. monitor_set: MonitorSet, + /// Ongoing interactive move. + interactive_move: Option>, /// Configurable properties of the layout. options: Rc, } @@ -240,6 +259,7 @@ pub struct Options { pub struts: Struts, pub focus_ring: niri_config::FocusRing, pub border: niri_config::Border, + pub insert_hint: niri_config::InsertHint, pub center_focused_column: CenterFocusedColumn, pub always_center_single_column: bool, /// Column widths that `toggle_width()` switches between. @@ -261,6 +281,7 @@ impl Default for Options { struts: Default::default(), focus_ring: Default::default(), border: Default::default(), + insert_hint: Default::default(), center_focused_column: Default::default(), always_center_single_column: false, preset_column_widths: vec![ @@ -314,6 +335,7 @@ impl Options { struts: layout.struts, focus_ring: layout.focus_ring, border: layout.border, + insert_hint: layout.insert_hint, center_focused_column: layout.center_focused_column, always_center_single_column: layout.always_center_single_column, preset_column_widths, @@ -336,6 +358,25 @@ impl Options { } } +impl InteractiveMoveData { + pub fn get_pointer_loc(&self) -> Point { + self.initial_pointer_location + self.pointer_offset + - self.output.current_location().to_f64() + } + + pub fn get_precise_window_loc(&self) -> Point { + let scale = Scale::from(self.output.current_scale().fractional_scale()); + let window_render_loc = + self.initial_pointer_location + self.pointer_offset + self.window_offset + - self.output.current_location().to_f64(); + + // Round to physical pixels + window_render_loc + .to_physical_precise_round(scale) + .to_logical(scale) + } +} + impl Layout { pub fn new(config: &Config) -> Self { Self::with_options_and_workspaces(config, Options::from_config(config)) @@ -344,6 +385,7 @@ impl Layout { pub fn with_options(options: Options) -> Self { Self { monitor_set: MonitorSet::NoOutputs { workspaces: vec![] }, + interactive_move: None, options: Rc::new(options), } } @@ -359,6 +401,7 @@ impl Layout { Self { monitor_set: MonitorSet::NoOutputs { workspaces }, + interactive_move: None, options: opts, } } @@ -744,6 +787,18 @@ impl Layout { } pub fn remove_window(&mut self, window: &W::Id, transaction: Transaction) -> Option { + if self + .interactive_move + .as_ref() + .map_or(false, |move_| move_.window.window().id() == window) + { + let move_ = self.interactive_move.take().unwrap(); + if let Some(workspace) = self.workspace_for_output_mut(&move_.output) { + workspace.clear_insert_hint(); + } + return Some(move_.window.into_window()); + } + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { @@ -790,12 +845,31 @@ impl Layout { } pub fn update_window(&mut self, window: &W::Id, serial: Option) { + if let Some(move_) = &mut self.interactive_move { + if move_.window.window().id() == window { + move_.window.update_window(); + return; + } + } + 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, serial); + + if let Some(move_) = &mut self.interactive_move { + if move_.output == mon.output { + let position = ws.get_insert_position(move_.get_pointer_loc()); + ws.set_insert_hint(InsertHint { + position, + width: move_.width, + is_full_width: move_.is_full_width, + }); + } + } + return; } } @@ -813,6 +887,12 @@ impl Layout { } pub fn find_window_and_output(&self, wl_surface: &WlSurface) -> Option<(&W, &Output)> { + if let Some(move_) = &self.interactive_move { + if move_.window.window().is_wl_surface(wl_surface) { + return Some((move_.window.window(), &move_.output)); + } + } + if let MonitorSet::Normal { monitors, .. } = &self.monitor_set { for mon in monitors { for ws in &mon.workspaces { @@ -918,6 +998,12 @@ impl Layout { &mut self, wl_surface: &WlSurface, ) -> Option<(&mut W, Option<&Output>)> { + if let Some(move_) = &mut self.interactive_move { + if move_.window.window().is_wl_surface(wl_surface) { + return Some((move_.window.window_mut(), Some(&move_.output))); + } + } + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { @@ -941,6 +1027,12 @@ impl Layout { } pub fn window_loc(&self, window: &W::Id) -> Option> { + if let Some(move_) = &self.interactive_move { + if move_.window.window().id() == window { + return Some(move_.window.window_loc()); + } + } + match &self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { @@ -1036,6 +1128,10 @@ impl Layout { } pub fn activate_window(&mut self, window: &W::Id) { + if self.interactive_move.is_some() { + return; + } + let MonitorSet::Normal { monitors, active_monitor_idx, @@ -1125,6 +1221,10 @@ impl Layout { } pub fn active_window(&self) -> Option<(&W, &Output)> { + if let Some(move_) = &self.interactive_move { + return Some((move_.window.window(), &move_.output)); + } + let MonitorSet::Normal { monitors, active_monitor_idx, @@ -1150,17 +1250,27 @@ impl Layout { panic!() }; + let moving_window = self + .interactive_move + .as_ref() + .map(|move_| move_.window.window()); let mon = monitors.iter().find(|mon| &mon.output == output).unwrap(); - mon.workspaces.iter().flat_map(|ws| ws.windows()) + moving_window + .into_iter() + .chain(mon.workspaces.iter().flat_map(|ws| ws.windows())) } - pub fn with_windows(&self, mut f: impl FnMut(&W, Option<&Output>, WorkspaceId)) { + pub fn with_windows(&self, mut f: impl FnMut(&W, Option<&Output>, Option)) { + if let Some(move_) = &self.interactive_move { + f(move_.window.window(), Some(&move_.output), None); + } + match &self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { for ws in &mon.workspaces { for win in ws.windows() { - f(win, Some(&mon.output), ws.id()); + f(win, Some(&mon.output), Some(ws.id())); } } } @@ -1168,7 +1278,7 @@ impl Layout { MonitorSet::NoOutputs { workspaces } => { for ws in workspaces { for win in ws.windows() { - f(win, None, ws.id()); + f(win, None, Some(ws.id())); } } } @@ -1176,6 +1286,10 @@ impl Layout { } pub fn with_windows_mut(&mut self, mut f: impl FnMut(&mut W, Option<&Output>)) { + if let Some(move_) = &mut self.interactive_move { + f(move_.window.window_mut(), Some(&move_.output)); + } + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { @@ -1244,6 +1358,18 @@ impl Layout { }) } + fn workspace_for_output_mut(&mut self, output: &Output) -> Option<&mut Workspace> { + match self.monitor_set { + MonitorSet::Normal { + ref mut monitors, .. + } => monitors + .iter_mut() + .find(|monitor| monitor.output == *output) + .map(|monitor| monitor.active_workspace()), + _ => None, + } + } + pub fn outputs(&self) -> impl Iterator + '_ { let monitors = if let MonitorSet::Normal { monitors, .. } = &self.monitor_set { &monitors[..] @@ -1593,6 +1719,7 @@ impl Layout { return; }; monitor.switch_workspace_up(); + self.update_insert_hint(); } pub fn switch_workspace_down(&mut self) { @@ -1600,6 +1727,7 @@ impl Layout { return; }; monitor.switch_workspace_down(); + self.update_insert_hint(); } pub fn switch_workspace(&mut self, idx: usize) { @@ -1607,6 +1735,7 @@ impl Layout { return; }; monitor.switch_workspace(idx); + self.update_insert_hint(); } pub fn switch_workspace_auto_back_and_forth(&mut self, idx: usize) { @@ -1614,6 +1743,7 @@ impl Layout { return; }; monitor.switch_workspace_auto_back_and_forth(idx); + self.update_insert_hint(); } pub fn switch_workspace_previous(&mut self) { @@ -1621,6 +1751,7 @@ impl Layout { return; }; monitor.switch_workspace_previous(); + self.update_insert_hint(); } pub fn consume_into_column(&mut self) { @@ -1645,6 +1776,10 @@ impl Layout { } pub fn focus(&self) -> Option<&W> { + if let Some(move_) = &self.interactive_move { + return Some(move_.window.window()); + } + let MonitorSet::Normal { monitors, active_monitor_idx, @@ -1669,6 +1804,20 @@ impl Layout { output: &Output, pos_within_output: Point, ) -> Option<(&W, Option>)> { + if let Some(move_) = &self.interactive_move { + let window_render_loc = move_.get_precise_window_loc(); + let pos_within_window = pos_within_output - window_render_loc; + + if move_.window.is_in_input_region(pos_within_window) { + let pos_within_surface = window_render_loc + move_.window.buf_loc(); + return Some((move_.window.window(), Some(pos_within_surface))); + } else if move_.window.is_in_activation_region(pos_within_window) { + return Some((move_.window.window(), None)); + } + + return None; + }; + let MonitorSet::Normal { monitors, .. } = &self.monitor_set else { return None; }; @@ -1856,6 +2005,10 @@ impl Layout { pub fn advance_animations(&mut self, current_time: Duration) { let _span = tracy_client::span!("Layout::advance_animations"); + if let Some(move_) = &mut self.interactive_move { + move_.window.advance_animations(current_time); + } + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { @@ -1873,6 +2026,15 @@ impl Layout { pub fn update_render_elements(&mut self, output: Option<&Output>) { let _span = tracy_client::span!("Layout::update_render_elements"); + if let Some(move_) = &mut self.interactive_move { + if output.map_or(true, |output| move_.output == *output) { + let pos_within_output = move_.get_precise_window_loc(); + let view_rect = + Rectangle::from_loc_and_size(pos_within_output, output_size(&move_.output)); + move_.window.update(true, view_rect); + } + } + let MonitorSet::Normal { monitors, active_monitor_idx, @@ -1885,12 +2047,18 @@ impl Layout { for (idx, mon) in monitors.iter_mut().enumerate() { if output.map_or(true, |output| mon.output == *output) { - mon.update_render_elements(idx == *active_monitor_idx); + mon.update_render_elements( + self.interactive_move.is_none() && idx == *active_monitor_idx, + ); } } } pub fn update_shaders(&mut self) { + if let Some(move_) = &mut self.interactive_move { + move_.window.update_shaders(); + } + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { @@ -1907,6 +2075,22 @@ impl Layout { } } + fn update_insert_hint(&mut self) { + let Some(move_) = self.interactive_move.take() else { + return; + }; + let pointer_loc = move_.get_pointer_loc(); + if let Some(workspace) = self.workspace_for_output_mut(&move_.output) { + let position = workspace.get_insert_position(pointer_loc); + workspace.set_insert_hint(InsertHint { + position, + width: move_.width, + is_full_width: move_.is_full_width, + }); + } + self.interactive_move = Some(move_); + } + pub fn ensure_named_workspace(&mut self, ws_config: &WorkspaceConfig) { if self.find_workspace_by_name(&ws_config.name.0).is_some() { return; @@ -2384,6 +2568,146 @@ impl Layout { None } + pub fn interactive_move_begin( + &mut self, + window: W::Id, + output: Output, + pointer_location: Point, + ) -> bool { + let window = match &mut self.monitor_set { + MonitorSet::Normal { monitors, .. } => monitors + .iter_mut() + .flat_map(|mon| &mut mon.workspaces) + .filter(|ws| ws.has_window(&window)) + .map(|ws| ws.remove_window_with_col_info(&window, Transaction::new())) + .next(), + MonitorSet::NoOutputs { workspaces, .. } => workspaces + .iter_mut() + .filter(|ws| ws.has_window(&window)) + .map(|ws| ws.remove_window_with_col_info(&window, Transaction::new())) + .next(), + }; + + let Some((render_pos, width, is_full_width, window)) = window else { + return false; + }; + + window.output_enter(&output); + window.set_preferred_scale_transform(output.current_scale(), output.current_transform()); + let pos_within_output = pointer_location - output.current_location().to_f64(); + + self.interactive_move = Some(InteractiveMoveData { + window: Tile::new( + window, + output.current_scale().fractional_scale(), + self.options.clone(), + ), + output, + width, + is_full_width, + initial_pointer_location: pointer_location, + pointer_offset: Point::from((0., 0.)), + window_offset: render_pos - pos_within_output, + }); + + true + } + + pub fn interactive_move_update( + &mut self, + window: &W::Id, + output: Option, + delta: Point, + ) -> bool { + let Some(output) = output else { + return false; + }; + let Some(mut move_) = self.interactive_move.take() else { + return false; + }; + + if window != move_.window.window().id() { + return false; + } + + if output != move_.output { + if let Some(workspace) = self.workspace_for_output_mut(&move_.output) { + workspace.clear_insert_hint(); + } + move_.window.window().output_leave(&move_.output); + move_.window.window().output_enter(&output); + move_ + .window + .window() + .set_preferred_scale_transform(output.current_scale(), output.current_transform()); + move_.window.update_config( + output.current_scale().fractional_scale(), + self.options.clone(), + ); + move_.output = output.clone(); + self.focus_output(&output); + } + + if let Some(workspace) = self.workspace_for_output_mut(&output) { + let position = workspace.get_insert_position(move_.get_pointer_loc()); + workspace.set_insert_hint(InsertHint { + position, + width: move_.width, + is_full_width: move_.is_full_width, + }); + } + + move_.pointer_offset = delta; + self.interactive_move = Some(move_); + + true + } + + pub fn interactive_move_end(&mut self, window: &W::Id) { + let Some(ref mut move_) = self.interactive_move else { + return; + }; + + if window != move_.window.window().id() { + return; + } + + let workspace = match self.monitor_set { + MonitorSet::Normal { + ref mut monitors, + active_monitor_idx, + .. + } => { + let workspace = monitors + .iter_mut() + .find(|monitor| monitor.output == move_.output) + .map(|monitor| monitor.active_workspace()); + if workspace.is_some() { + workspace + } else { + Some(monitors[active_monitor_idx].active_workspace()) + } + } + MonitorSet::NoOutputs { + ref mut workspaces, .. + } => workspaces.first_mut(), + } + .unwrap(); + + let move_ = self.interactive_move.take().unwrap(); + let width = ColumnWidth::Fixed(move_.window.window().size().w as f64); + workspace.clear_insert_hint(); + let position = workspace.get_insert_position(move_.get_pointer_loc()); + match position { + InsertPosition::NewColumn(column_idx) => { + workspace.add_tile(Some(column_idx), move_.window, true, width, false, None) + } + InsertPosition::InColumn(column_idx, tile_idx) => { + workspace.add_tile_to_column(column_idx, Some(tile_idx), move_.window, true) + } + } + } + pub fn interactive_resize_begin(&mut self, window: W::Id, edges: ResizeEdge) -> bool { match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { @@ -2505,6 +2829,15 @@ impl Layout { pub fn store_unmap_snapshot(&mut self, renderer: &mut GlesRenderer, window: &W::Id) { let _span = tracy_client::span!("Layout::store_unmap_snapshot"); + if let Some(move_) = &mut self.interactive_move { + if move_.window.window().id() == window { + move_.window.store_unmap_snapshot_if_empty( + renderer, + move_.output.current_scale().fractional_scale().into(), + ); + } + } + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { @@ -2528,6 +2861,13 @@ impl Layout { } pub fn clear_unmap_snapshot(&mut self, window: &W::Id) { + if let Some(move_) = &mut self.interactive_move { + if move_.window.window().id() == window { + let _ = move_.window.take_unmap_snapshot(); + return; + } + } + match &mut self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { @@ -2580,9 +2920,47 @@ impl Layout { } } + pub fn render_floating_for_output( + &self, + renderer: &mut R, + target: RenderTarget, + output: &Output, + ) -> Vec> { + let mut rv = vec![]; + + if let Some(ref move_) = self.interactive_move { + if &move_.output == output { + let scale = Scale::from(move_.output.current_scale().fractional_scale()); + let window_render_loc = move_.get_precise_window_loc(); + + rv.extend( + move_ + .window + .render(renderer, window_render_loc, scale, true, target), + ); + } + } + + rv + } + pub fn refresh(&mut self) { let _span = tracy_client::span!("Layout::refresh"); + if let Some(move_) = &mut self.interactive_move { + let win = move_.window.window_mut(); + + win.set_active_in_column(true); + win.set_activated(true); + + win.set_interactive_resize(None); + + win.set_bounds(output_size(&move_.output).to_i32_round()); + + win.send_pending_configure(); + win.refresh(); + } + match &mut self.monitor_set { MonitorSet::Normal { monitors, @@ -2592,7 +2970,10 @@ impl Layout { for (idx, mon) in monitors.iter_mut().enumerate() { let is_active = idx == *active_monitor_idx; for (ws_idx, ws) in mon.workspaces.iter_mut().enumerate() { - ws.refresh(is_active); + ws.refresh( + is_active && ws_idx == mon.active_workspace_idx, + self.interactive_move.is_none(), + ); // Cancel the view offset gesture after workspace switches, moves, etc. if ws_idx != mon.active_workspace_idx { @@ -2603,7 +2984,7 @@ impl Layout { } MonitorSet::NoOutputs { workspaces, .. } => { for ws in workspaces { - ws.refresh(false); + ws.refresh(false, false); ws.view_offset_gesture_end(false, None); } } @@ -2929,6 +3310,10 @@ mod tests { prop_oneof![(-10f64..10f64), (-50000f64..50000f64),] } + fn arbitrary_move_point() -> impl Strategy> { + any::<(f64, f64)>().prop_map(|(x, y)| Point::from((x, y))) + } + fn arbitrary_resize_edge() -> impl Strategy { prop_oneof![ Just(ResizeEdge::RIGHT), @@ -3117,6 +3502,28 @@ mod tests { cancelled: bool, is_touchpad: Option, }, + InteractiveMoveBegin { + #[proptest(strategy = "1..=5usize")] + window: usize, + #[proptest(strategy = "1..=5usize")] + output_idx: usize, + #[proptest(strategy = "arbitrary_move_point()")] + point: Point, + }, + InteractiveMoveUpdate { + #[proptest(strategy = "1..=5usize")] + window: usize, + #[proptest(strategy = "1..=5usize")] + output_idx: usize, + #[proptest(strategy = "-20000f64..20000f64")] + dx: f64, + #[proptest(strategy = "-20000f64..20000f64")] + dy: f64, + }, + InteractiveMoveEnd { + #[proptest(strategy = "1..=5usize")] + window: usize, + }, InteractiveResizeBegin { #[proptest(strategy = "1..=5usize")] window: usize, @@ -3610,6 +4017,30 @@ mod tests { } => { layout.workspace_switch_gesture_end(cancelled, is_touchpad); } + Op::InteractiveMoveBegin { + window, + output_idx, + point, + } => { + let name = format!("output{output_idx}"); + let Some(output) = layout.outputs().find(|o| o.name() == name).cloned() else { + return; + }; + layout.interactive_move_begin(window, output, point); + } + Op::InteractiveMoveUpdate { + window, + output_idx, + dx, + dy, + } => { + let name = format!("output{output_idx}"); + let output = layout.outputs().find(|o| o.name() == name).cloned(); + layout.interactive_move_update(&window, output, Point::from((dx, dy))); + } + Op::InteractiveMoveEnd { window } => { + layout.interactive_move_end(&window); + } Op::InteractiveResizeBegin { window, edges } => { layout.interactive_resize_begin(window, edges); } diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index 0348dfa7..87e24e7c 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -8,6 +8,7 @@ use niri_config::{ }; use niri_ipc::SizeChange; use ordered_float::NotNan; +use smithay::backend::renderer::element::Kind; use smithay::backend::renderer::gles::GlesRenderer; use smithay::desktop::{layer_map_for_output, Window}; use smithay::output::Output; @@ -22,6 +23,7 @@ use crate::animation::Animation; use crate::input::swipe_tracker::SwipeTracker; use crate::niri_render_elements; use crate::render_helpers::renderer::NiriRenderer; +use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement}; use crate::render_helpers::RenderTarget; use crate::utils::id::IdCounter; use crate::utils::transaction::{Transaction, TransactionBlocker}; @@ -106,6 +108,9 @@ pub struct Workspace { /// Windows in the closing animation. closing_windows: Vec, + /// Indication where a window is about to be placed. + insert_hint: Option, + /// Configurable properties of the layout as received from the parent monitor. pub(super) base_options: Rc, @@ -119,6 +124,19 @@ pub struct Workspace { id: WorkspaceId, } +#[derive(Debug, PartialEq)] +pub enum InsertPosition { + NewColumn(usize), + InColumn(usize, usize), +} + +#[derive(Debug, PartialEq)] +pub struct InsertHint { + pub position: InsertPosition, + pub width: ColumnWidth, + pub is_full_width: bool, +} + #[derive(Debug, Clone)] pub struct OutputId(String); @@ -418,6 +436,7 @@ impl Workspace { activate_prev_column_on_removal: None, view_offset_before_fullscreen: None, closing_windows: vec![], + insert_hint: None, base_options, options, name: config.map(|c| c.name.0), @@ -456,6 +475,7 @@ impl Workspace { activate_prev_column_on_removal: None, view_offset_before_fullscreen: None, closing_windows: vec![], + insert_hint: None, base_options, options, name: config.map(|c| c.name.0), @@ -958,6 +978,87 @@ impl Workspace { self.windows_mut().find(|win| win.is_wl_surface(wl_surface)) } + pub fn set_insert_hint(&mut self, insert_hint: InsertHint) { + if self.options.insert_hint.off { + return; + } + self.insert_hint = Some(insert_hint); + } + + pub fn clear_insert_hint(&mut self) { + self.insert_hint = None; + } + + pub fn get_insert_position(&self, pos: Point) -> InsertPosition { + if self.columns.is_empty() { + return InsertPosition::NewColumn(0); + } + let Some((target_window, direction)) = + self.tiles_with_render_positions() + .find_map(|(tile, tile_pos)| { + let pos_within_tile = pos - tile_pos; + + 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((tile.window().id(), edges)); + } + + None + }) + else { + return InsertPosition::NewColumn(if pos.x < self.column_x(0) { + 0 + } else if pos.x + > self.column_x(self.columns.len() - 1) + self.data.last().unwrap().width + { + self.columns.len() + } else if pos.x < self.view_size().w / 2. { + self.active_column_idx + } else { + self.active_column_idx + 1 + }); + }; + + let mut target_column_idx = self + .columns + .iter() + .position(|col| col.contains(target_window)) + .unwrap(); + + if direction.contains(ResizeEdge::LEFT) || direction.contains(ResizeEdge::RIGHT) { + if direction.contains(ResizeEdge::RIGHT) { + target_column_idx += 1; + } + InsertPosition::NewColumn(target_column_idx) + } else if direction.contains(ResizeEdge::TOP) || direction.contains(ResizeEdge::BOTTOM) { + let mut target_window_idx = self.columns[target_column_idx] + .tiles + .iter() + .position(|tile| tile.window().id() == target_window) + .unwrap(); + if direction.contains(ResizeEdge::BOTTOM) { + target_window_idx += 1; + } + InsertPosition::InColumn(target_column_idx, target_window_idx) + } else { + InsertPosition::NewColumn(target_column_idx) + } + } + pub fn add_window( &mut self, col_idx: Option, @@ -970,7 +1071,7 @@ impl Workspace { self.add_tile(col_idx, tile, activate, width, is_full_width, None); } - fn add_tile( + pub fn add_tile( &mut self, col_idx: Option, tile: Tile, @@ -1128,6 +1229,7 @@ impl Workspace { transaction: Transaction, anim_config: Option, ) -> Tile { + self.insert_hint = None; let offset = self.column_x(column_idx + 1) - self.column_x(column_idx); let column = &mut self.columns[column_idx]; @@ -1323,16 +1425,33 @@ impl Workspace { } pub fn remove_window(&mut self, window: &W::Id, transaction: Transaction) -> W { + self.remove_window_with_col_info(window, transaction).3 + } + + pub fn remove_window_with_col_info( + &mut self, + window: &W::Id, + transaction: Transaction, + ) -> (Point, ColumnWidth, bool, W) { let column_idx = self .columns .iter() .position(|col| col.contains(window)) .unwrap(); let column = &self.columns[column_idx]; + let width = column.width; + let is_full_width = column.is_full_width; let window_idx = column.position(window).unwrap(); - self.remove_tile_by_idx(column_idx, window_idx, transaction, None) - .into_window() + let (_, render_pos) = self + .tiles_with_render_positions() + .find(|(tile, _)| tile.window().id() == window) + .unwrap(); + let window = self + .remove_tile_by_idx(column_idx, window_idx, transaction, None) + .into_window(); + + (render_pos, width, is_full_width, window) } pub fn update_window(&mut self, window: &W::Id, serial: Option) { @@ -1802,6 +1921,7 @@ impl Workspace { if self.columns.is_empty() { return; } + self.insert_hint = None; let (source_col_idx, source_tile_idx) = if let Some(window) = window { self.columns @@ -1908,6 +2028,7 @@ impl Workspace { if self.columns.is_empty() { return; } + self.insert_hint = None; let (source_col_idx, source_tile_idx) = if let Some(window) = window { self.columns @@ -2007,6 +2128,7 @@ impl Workspace { if self.active_column_idx == self.columns.len() - 1 { return; } + self.insert_hint = None; let target_column_idx = self.active_column_idx; let source_column_idx = self.active_column_idx + 1; @@ -2202,6 +2324,63 @@ impl Workspace { }) } + fn insert_hint_area(&self, insert_hint: &InsertHint) -> Option> { + let mut hint_area = match insert_hint.position { + InsertPosition::NewColumn(column_index) => { + if column_index == 0 || column_index == self.columns.len() { + let size = Size::from(( + insert_hint.width.resolve(&self.options, self.view_size.w), + self.working_area.size.h - self.options.gaps * 2., + )); + let mut loc = Point::from(( + self.column_x(column_index), + self.working_area.loc.y + self.options.gaps, + )); + if column_index == 0 && !self.columns.is_empty() { + loc.x -= size.w + self.options.gaps; + } + Rectangle::from_loc_and_size(loc, size) + } else { + let size = + Size::from((300., self.working_area.size.h - self.options.gaps * 2.)); + let loc = Point::from(( + self.column_x(column_index) - size.w / 2. - self.options.gaps / 2., + self.working_area.loc.y + self.options.gaps, + )); + Rectangle::from_loc_and_size(loc, size) + } + } + InsertPosition::InColumn(column_index, tile_index) => { + let size = Size::from((self.data.get(column_index)?.width, 300.)); + let loc = Point::from(( + self.column_x(column_index), + (self.columns.get(column_index)?.tile_offset(tile_index).y - size.h / 2.) + .max(self.options.gaps) + .min(self.working_area.size.h - size.h - self.options.gaps), + )); + Rectangle::from_loc_and_size(loc, size) + } + }; + + let view_area = + Rectangle::from_loc_and_size(Point::from((self.view_pos(), 0.)), self.view_size()); + + // Make sure the hint is at least partially visible. + hint_area.loc.x = hint_area + .loc + .x + .max(view_area.loc.x + 150. - hint_area.size.w); + hint_area.loc.x = hint_area + .loc + .x + .min(view_area.loc.x + view_area.size.w - 150.); + hint_area = hint_area + .to_physical_precise_round(self.scale.fractional_scale()) + .to_logical(self.scale.fractional_scale()); + + Some(hint_area) + } + /// Returns the geometry of the active tile relative to and clamped to the view. /// /// During animations, assumes the final view position. @@ -2487,6 +2666,26 @@ impl Workspace { rv.push(elem.into()); } + if let Some(insert_hint) = &self.insert_hint { + if let Some(mut area) = self.insert_hint_area(insert_hint) { + area.loc.x -= self.view_pos(); + + let buffer = SolidColorBuffer::new( + area.size, + self.options.insert_hint.color.to_array_premul(), + ); + rv.push( + TileRenderElement::SolidColor(SolidColorRenderElement::from_buffer( + &buffer, + area.loc, + 1., + Kind::Unspecified, + )) + .into(), + ); + } + } + if self.columns.is_empty() { return rv; } @@ -2885,7 +3084,10 @@ impl Workspace { self.interactive_resize = None; } - pub fn refresh(&mut self, is_active: bool) { + pub fn refresh(&mut self, is_active: bool, is_focusable: bool) { + if !is_active { + self.clear_insert_hint(); + } for (col_idx, col) in self.columns.iter_mut().enumerate() { let mut col_resize_data = None; if let Some(resize) = &self.interactive_resize { @@ -2922,7 +3124,7 @@ impl Workspace { win.set_active_in_column(active_in_column); let active = is_active && self.active_column_idx == col_idx && active_in_column; - win.set_activated(active); + win.set_activated(active && is_focusable); win.set_interactive_resize(col_resize_data); diff --git a/src/niri.rs b/src/niri.rs index 7c3052ec..0dcaa3c6 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -116,6 +116,7 @@ use crate::input::{ apply_libinput_settings, mods_with_finger_scroll_binds, mods_with_wheel_binds, TabletData, }; use crate::ipc::server::IpcServer; +use crate::layout::tile::TileRenderElement; use crate::layout::workspace::WorkspaceId; use crate::layout::{Layout, LayoutElement as _, MonitorRenderElement}; use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState}; @@ -3045,6 +3046,9 @@ impl Niri { // Get monitor elements. let mon = self.layout.monitor_for_output(output).unwrap(); let monitor_elements = mon.render_elements(renderer, target); + let floating_elements = self + .layout + .render_floating_for_output(renderer, target, output); // Get layer-shell elements. let layer_map = layer_map_for_output(output); @@ -3077,8 +3081,18 @@ impl Niri { if mon.render_above_top_layer() { elements.extend(monitor_elements.into_iter().map(OutputRenderElements::from)); extend_from_layer(&mut elements, Layer::Top); + elements.extend( + floating_elements + .into_iter() + .map(OutputRenderElements::from), + ); } else { extend_from_layer(&mut elements, Layer::Top); + elements.extend( + floating_elements + .into_iter() + .map(OutputRenderElements::from), + ); elements.extend(monitor_elements.into_iter().map(OutputRenderElements::from)); } @@ -4766,6 +4780,7 @@ impl ClientData for ClientState { niri_render_elements! { OutputRenderElements => { Monitor = MonitorRenderElement, + Tile = TileRenderElement, Wayland = WaylandSurfaceRenderElement, NamedPointer = MemoryRenderBufferRenderElement, SolidColor = SolidColorRenderElement, diff --git a/src/window/mapped.rs b/src/window/mapped.rs index 3477a2f1..156baa53 100644 --- a/src/window/mapped.rs +++ b/src/window/mapped.rs @@ -78,6 +78,11 @@ pub struct Mapped { /// Pending transactions that have not been added as blockers for this window yet. pending_transactions: Vec<(Serial, Transaction)>, + /// Last time interactive move was started. + /// + /// Used for double-move-click tracking. + last_interactive_move_start: Cell>, + /// State of an ongoing interactive resize. interactive_resize: Option, @@ -150,6 +155,7 @@ impl Mapped { animation_snapshot: None, transaction_for_next_configure: None, pending_transactions: Vec::new(), + last_interactive_move_start: Cell::new(None), interactive_resize: None, last_interactive_resize_start: Cell::new(None), } @@ -298,6 +304,10 @@ impl Mapped { rv } + pub fn last_interactive_move_start(&self) -> &Cell> { + &self.last_interactive_move_start + } + pub fn last_interactive_resize_start(&self) -> &Cell> { &self.last_interactive_resize_start } From d47f7ba726cda1449ed98421b059341f42115545 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Mon, 7 Oct 2024 20:28:13 +0300 Subject: [PATCH 2/6] Remove insert-hint from default config --- resources/default-config.kdl | 7 ------- 1 file changed, 7 deletions(-) diff --git a/resources/default-config.kdl b/resources/default-config.kdl index f9060bde..4dce86f7 100644 --- a/resources/default-config.kdl +++ b/resources/default-config.kdl @@ -181,13 +181,6 @@ layout { // inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" } - insert-hint { - off - - color "#7fc8ff88" - } - - // Struts shrink the area occupied by windows, similarly to layer-shell panels. // You can think of them as a kind of outer gaps. They are set in logical pixels. // Left and right struts will cause the next window to the side to always be visible. From 4eec2b6b6959add328bb9372ec8e57a76944edbd Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Mon, 7 Oct 2024 20:47:26 +0300 Subject: [PATCH 3/6] Add missing test config lines --- niri-config/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index bbbb217b..08e253c5 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -2986,6 +2986,10 @@ mod tests { } center-focused-column "on-overflow" + + insert-hint { + color "rgb(255, 200, 127)" + } } spawn-at-startup "alacritty" "-e" "fish" From 57c409c2b29b5fac6b9c30ec7e45eff0dbac1d81 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Mon, 7 Oct 2024 20:47:46 +0300 Subject: [PATCH 4/6] Add boxed_union proptest-derive feature Our Op enum grew large enough to trigger a stack overflow in proptest-derive's generated code. Thankfully, this feature works around the problem. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5ae38d7d..afd67a21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,7 +109,7 @@ features = [ approx = "0.5.1" k9.workspace = true proptest = "1.5.0" -proptest-derive = "0.5.0" +proptest-derive = { version = "0.5.0", features = ["boxed_union"] } xshell = "0.2.6" [features] From 8acb36dcb2e5c88b83ee59ab59ba082d4948fd45 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Tue, 8 Oct 2024 10:43:37 +0300 Subject: [PATCH 5/6] Remove double-move-click logic We can add it when/if we need it. --- src/handlers/xdg_shell.rs | 19 ------------------- src/input/mod.rs | 17 ----------------- src/window/mapped.rs | 10 ---------- 3 files changed, 46 deletions(-) diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index 3a8bef4b..8809bf46 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -92,25 +92,6 @@ impl XdgShellHandler for State { let window = mapped.window.clone(); let output = output.clone(); - // See if we got a double move-click gesture. - let time = get_monotonic_time(); - let last_cell = mapped.last_interactive_move_start(); - let last = last_cell.get(); - last_cell.set(Some(time)); - if let Some(last_time) = last { - if time.saturating_sub(last_time) <= DOUBLE_CLICK_TIME { - // Allow quick move after a triple click. - last_cell.set(None); - - // FIXME: don't activate once we can pass specific windows to actions. - self.niri.layout.activate_window(&window); - self.niri.layer_shell_on_demand_focus = None; - // FIXME: granular. - self.niri.queue_redraw_all(); - return; - } - } - let grab = MoveGrab::new(start_data, window.clone()); if !self diff --git a/src/input/mod.rs b/src/input/mod.rs index ad380cf2..e66e4aa6 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -1542,21 +1542,6 @@ impl State { let (output, _) = self.niri.output_under(location).unwrap(); let output = output.clone(); - // See and ignore if we got a double move-click gesture. - // FIXME: deduplicate with move_request in xdg-shell somehow. - let time = get_monotonic_time(); - let last_cell = mapped.last_interactive_move_start(); - let last = last_cell.get(); - last_cell.set(Some(time)); - if let Some(last_time) = last { - if time.saturating_sub(last_time) <= DOUBLE_CLICK_TIME { - self.niri.layout.activate_window(&window); - // FIXME: granular. - self.niri.queue_redraw_all(); - return; - } - } - self.niri.layout.activate_window(&window); if self @@ -1575,8 +1560,6 @@ impl State { self.niri .cursor_manager .set_cursor_image(CursorImageStatus::Named(CursorIcon::Move)); - // FIXME: granular. - self.niri.queue_redraw_all(); } } } diff --git a/src/window/mapped.rs b/src/window/mapped.rs index 156baa53..3477a2f1 100644 --- a/src/window/mapped.rs +++ b/src/window/mapped.rs @@ -78,11 +78,6 @@ pub struct Mapped { /// Pending transactions that have not been added as blockers for this window yet. pending_transactions: Vec<(Serial, Transaction)>, - /// Last time interactive move was started. - /// - /// Used for double-move-click tracking. - last_interactive_move_start: Cell>, - /// State of an ongoing interactive resize. interactive_resize: Option, @@ -155,7 +150,6 @@ impl Mapped { animation_snapshot: None, transaction_for_next_configure: None, pending_transactions: Vec::new(), - last_interactive_move_start: Cell::new(None), interactive_resize: None, last_interactive_resize_start: Cell::new(None), } @@ -304,10 +298,6 @@ impl Mapped { rv } - pub fn last_interactive_move_start(&self) -> &Cell> { - &self.last_interactive_move_start - } - pub fn last_interactive_resize_start(&self) -> &Cell> { &self.last_interactive_resize_start } From 69b270bc072e8fabfc201cf5e62bbbc720525fba Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Tue, 8 Oct 2024 12:07:14 +0300 Subject: [PATCH 6/6] A few misc fixes --- src/layout/mod.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 60c22b9c..3a87ba9e 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -787,16 +787,14 @@ impl Layout { } pub fn remove_window(&mut self, window: &W::Id, transaction: Transaction) -> Option { - if self - .interactive_move - .as_ref() - .map_or(false, |move_| move_.window.window().id() == window) - { - let move_ = self.interactive_move.take().unwrap(); - if let Some(workspace) = self.workspace_for_output_mut(&move_.output) { - workspace.clear_insert_hint(); + if let Some(move_) = &self.interactive_move { + if move_.window.window().id() == window { + let move_ = self.interactive_move.take().unwrap(); + if let Some(workspace) = self.workspace_for_output_mut(&move_.output) { + workspace.clear_insert_hint(); + } + return Some(move_.window.into_window()); } - return Some(move_.window.into_window()); } match &mut self.monitor_set { @@ -1253,11 +1251,14 @@ impl Layout { let moving_window = self .interactive_move .as_ref() - .map(|move_| move_.window.window()); + .filter(|move_| move_.output == *output) + .map(|move_| move_.window.window()) + .into_iter(); + let mon = monitors.iter().find(|mon| &mon.output == output).unwrap(); - moving_window - .into_iter() - .chain(mon.workspaces.iter().flat_map(|ws| ws.windows())) + let mon_windows = mon.workspaces.iter().flat_map(|ws| ws.windows()); + + moving_window.chain(mon_windows) } pub fn with_windows(&self, mut f: impl FnMut(&W, Option<&Output>, Option)) { @@ -2831,10 +2832,9 @@ impl Layout { if let Some(move_) = &mut self.interactive_move { if move_.window.window().id() == window { - move_.window.store_unmap_snapshot_if_empty( - renderer, - move_.output.current_scale().fractional_scale().into(), - ); + let scale = Scale::from(move_.output.current_scale().fractional_scale()); + move_.window.store_unmap_snapshot_if_empty(renderer, scale); + return; } }