mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-21 02:01:55 +07:00
Implement interactive mouse resizing
This commit is contained in:
@@ -186,7 +186,7 @@ impl TestCase for Layout {
|
||||
self.layout.update_output_size(&self.output);
|
||||
for win in &self.windows {
|
||||
if win.communicate() {
|
||||
self.layout.update_window(win.id());
|
||||
self.layout.update_window(win.id(), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ use std::cell::RefCell;
|
||||
use std::cmp::{max, min};
|
||||
use std::rc::Rc;
|
||||
|
||||
use niri::layout::{LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot};
|
||||
use niri::layout::{
|
||||
InteractiveResizeData, LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot,
|
||||
};
|
||||
use niri::render_helpers::renderer::NiriRenderer;
|
||||
use niri::render_helpers::{RenderTarget, SplitElements};
|
||||
use niri::window::ResolvedWindowRules;
|
||||
@@ -10,7 +12,7 @@ use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRen
|
||||
use smithay::backend::renderer::element::{Id, Kind};
|
||||
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};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestWindowInner {
|
||||
@@ -237,4 +239,12 @@ impl LayoutElement for TestWindow {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
+37
-4
@@ -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() {
|
||||
|
||||
+154
-7
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+42
-1
@@ -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() {
|
||||
|
||||
+307
-44
@@ -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();
|
||||
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();
|
||||
|
||||
// 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);
|
||||
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_column(&mut self.interactive_resize, col);
|
||||
|
||||
if is_fullscreen && col.tiles.len() > 1 {
|
||||
// This wasn't the only window in its column; extract it into a separate column.
|
||||
let target_window_was_focused =
|
||||
@@ -2188,8 +2316,124 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn interactive_resize_begin(&mut self, window: W::Id, edges: ResizeEdge) -> bool {
|
||||
let col = self
|
||||
.columns
|
||||
.iter_mut()
|
||||
.find(|col| col.contains(&window))
|
||||
.unwrap();
|
||||
|
||||
if col.is_fullscreen {
|
||||
return false;
|
||||
}
|
||||
|
||||
let tile = col
|
||||
.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,
|
||||
original_view_offset: self.view_offset,
|
||||
original_column_width: col.width(),
|
||||
},
|
||||
};
|
||||
self.interactive_resize = Some(resize);
|
||||
|
||||
// Stop ongoing animation.
|
||||
self.view_offset_adj = None;
|
||||
|
||||
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 col = self
|
||||
.columns
|
||||
.iter_mut()
|
||||
.find(|col| col.contains(window))
|
||||
.unwrap();
|
||||
|
||||
let tile_idx = col
|
||||
.tiles
|
||||
.iter()
|
||||
.position(|tile| tile.window().id() == window)
|
||||
.unwrap();
|
||||
|
||||
if resize.data.edges.intersects(ResizeEdge::LEFT_RIGHT) {
|
||||
let mut dx = delta.x;
|
||||
if resize.data.edges.contains(ResizeEdge::LEFT) {
|
||||
dx = -dx;
|
||||
};
|
||||
|
||||
if self.options.center_focused_column == CenterFocusedColumn::Always {
|
||||
dx *= 2.;
|
||||
}
|
||||
|
||||
let window_width = (f64::from(resize.original_window_size.w) + dx).round() as i32;
|
||||
col.set_column_width(SizeChange::SetFixed(window_width), Some(tile_idx), false);
|
||||
}
|
||||
|
||||
if resize.data.edges.intersects(ResizeEdge::TOP_BOTTOM) {
|
||||
// Prevent the simplest case of weird resizing (top edge when this is the topmost
|
||||
// window).
|
||||
if !(resize.data.edges.contains(ResizeEdge::TOP) && tile_idx == 0) {
|
||||
let mut dy = delta.y;
|
||||
if resize.data.edges.contains(ResizeEdge::TOP) {
|
||||
dy = -dy;
|
||||
};
|
||||
|
||||
// FIXME: some smarter height distribution would be nice here so that vertical
|
||||
// resizes work as expected in more cases.
|
||||
|
||||
let window_height = (f64::from(resize.original_window_size.h) + dy).round() as i32;
|
||||
col.set_window_height(SizeChange::SetFixed(window_height), Some(tile_idx), 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) {
|
||||
for (col_idx, col) in self.columns.iter_mut().enumerate() {
|
||||
let mut col_resize_data = None;
|
||||
if let Some(resize) = &self.interactive_resize {
|
||||
if col.contains(&resize.window) {
|
||||
col_resize_data = Some(resize.data);
|
||||
}
|
||||
}
|
||||
|
||||
for (tile_idx, tile) in col.tiles.iter_mut().enumerate() {
|
||||
let win = tile.window_mut();
|
||||
let active = is_active
|
||||
@@ -2197,6 +2441,8 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
&& col.active_tile_idx == tile_idx;
|
||||
win.set_activated(active);
|
||||
|
||||
win.set_interactive_resize(col_resize_data);
|
||||
|
||||
let border_config = win.rules().border.resolve_against(self.options.border);
|
||||
let bounds = compute_toplevel_bounds(
|
||||
border_config,
|
||||
@@ -2310,10 +2556,10 @@ impl<W: LayoutElement> Column<W> {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_width(&mut self, width: ColumnWidth) {
|
||||
fn set_width(&mut self, width: ColumnWidth, animate: bool) {
|
||||
self.width = width;
|
||||
self.is_full_width = false;
|
||||
self.update_tile_sizes(true);
|
||||
self.update_tile_sizes(animate);
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self, current_time: Duration) {
|
||||
@@ -2695,7 +2941,7 @@ impl<W: LayoutElement> Column<W> {
|
||||
}
|
||||
};
|
||||
let width = ColumnWidth::Preset(idx);
|
||||
self.set_width(width);
|
||||
self.set_width(width, true);
|
||||
}
|
||||
|
||||
fn toggle_full_width(&mut self) {
|
||||
@@ -2703,7 +2949,7 @@ impl<W: LayoutElement> Column<W> {
|
||||
self.update_tile_sizes(true);
|
||||
}
|
||||
|
||||
fn set_column_width(&mut self, change: SizeChange) {
|
||||
fn set_column_width(&mut self, change: SizeChange, tile_idx: Option<usize>, animate: bool) {
|
||||
let width = if self.is_full_width {
|
||||
ColumnWidth::Proportion(1.)
|
||||
} else {
|
||||
@@ -2724,9 +2970,10 @@ impl<W: LayoutElement> Column<W> {
|
||||
let width = match (current, change) {
|
||||
(_, SizeChange::SetFixed(fixed)) => {
|
||||
// As a special case, setting a fixed column width will compute it in such a way
|
||||
// that the active window gets that width. This is the intention behind the ability
|
||||
// to set a fixed size.
|
||||
let tile = &self.tiles[self.active_tile_idx];
|
||||
// that the specified (usually active) window gets that width. This is the
|
||||
// intention behind the ability to set a fixed size.
|
||||
let tile_idx = tile_idx.unwrap_or(self.active_tile_idx);
|
||||
let tile = &self.tiles[tile_idx];
|
||||
ColumnWidth::Fixed(tile.tile_width_for_window_width(fixed).clamp(1, MAX_PX))
|
||||
}
|
||||
(_, SizeChange::SetProportion(proportion)) => {
|
||||
@@ -2749,12 +2996,13 @@ impl<W: LayoutElement> Column<W> {
|
||||
(ColumnWidth::Preset(_), _) => unreachable!(),
|
||||
};
|
||||
|
||||
self.set_width(width);
|
||||
self.set_width(width, animate);
|
||||
}
|
||||
|
||||
fn set_window_height(&mut self, change: SizeChange) {
|
||||
let current = self.heights[self.active_tile_idx];
|
||||
let tile = &self.tiles[self.active_tile_idx];
|
||||
fn set_window_height(&mut self, change: SizeChange, tile_idx: Option<usize>, animate: bool) {
|
||||
let tile_idx = tile_idx.unwrap_or(self.active_tile_idx);
|
||||
let current = self.heights[tile_idx];
|
||||
let tile = &self.tiles[tile_idx];
|
||||
let current_window_px = match current {
|
||||
WindowHeight::Auto => tile.window_size().h,
|
||||
WindowHeight::Fixed(height) => height,
|
||||
@@ -2787,7 +3035,7 @@ impl<W: LayoutElement> Column<W> {
|
||||
};
|
||||
|
||||
// Clamp it against the window height constraints.
|
||||
let win = &self.tiles[self.active_tile_idx].window();
|
||||
let win = &self.tiles[tile_idx].window();
|
||||
let min_h = win.min_size().h;
|
||||
let max_h = win.max_size().h;
|
||||
|
||||
@@ -2798,8 +3046,8 @@ impl<W: LayoutElement> Column<W> {
|
||||
window_height = window_height.max(min_h);
|
||||
}
|
||||
|
||||
self.heights[self.active_tile_idx] = WindowHeight::Fixed(window_height.clamp(1, MAX_PX));
|
||||
self.update_tile_sizes(true);
|
||||
self.heights[tile_idx] = WindowHeight::Fixed(window_height.clamp(1, MAX_PX));
|
||||
self.update_tile_sizes(animate);
|
||||
}
|
||||
|
||||
fn set_fullscreen(&mut self, is_fullscreen: bool) {
|
||||
@@ -2917,3 +3165,18 @@ fn compute_toplevel_bounds(
|
||||
max(working_area_size.h - gaps * 2 - border, 1),
|
||||
))
|
||||
}
|
||||
|
||||
fn cancel_resize_if_this_column<W: LayoutElement>(
|
||||
interactive_resize: &mut Option<InteractiveResize<W>>,
|
||||
column: &mut Column<W>,
|
||||
) {
|
||||
if let Some(resize) = interactive_resize {
|
||||
if column.contains(&resize.window) {
|
||||
*interactive_resize = None;
|
||||
|
||||
for tile in &mut column.tiles {
|
||||
tile.window_mut().cancel_interactive_resize();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ pub mod layout;
|
||||
pub mod niri;
|
||||
pub mod protocols;
|
||||
pub mod render_helpers;
|
||||
pub mod resize_grab;
|
||||
pub mod rubber_band;
|
||||
pub mod scroll_tracker;
|
||||
pub mod swipe_tracker;
|
||||
|
||||
+2
-2
@@ -995,7 +995,7 @@ impl State {
|
||||
}
|
||||
});
|
||||
for win in windows {
|
||||
self.niri.layout.update_window(&win);
|
||||
self.niri.layout.update_window(&win, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2414,7 +2414,7 @@ impl Niri {
|
||||
drop(config);
|
||||
|
||||
for win in windows {
|
||||
self.layout.update_window(&win);
|
||||
self.layout.update_window(&win, None);
|
||||
win.toplevel()
|
||||
.expect("no X11 support")
|
||||
.send_pending_configure();
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, 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 ResizeGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
window: Window,
|
||||
}
|
||||
|
||||
impl ResizeGrab {
|
||||
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
|
||||
Self { start_data, window }
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
state.niri.layout.interactive_resize_end(&self.window);
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerGrab<State> for ResizeGrab {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<i32, Logical>)>,
|
||||
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 ongoing = data
|
||||
.niri
|
||||
.layout
|
||||
.interactive_resize_update(&self.window, delta);
|
||||
if ongoing {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The resize 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<(<State as SeatHandler>::PointerFocus, Point<i32, Logical>)>,
|
||||
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);
|
||||
|
||||
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<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,13 @@ use std::sync::atomic::AtomicBool;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{ensure, Context};
|
||||
use bitflags::bitflags;
|
||||
use directories::UserDirs;
|
||||
use git_version::git_version;
|
||||
use niri_config::Config;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::rustix::time::{clock_gettime, ClockId};
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size, Transform};
|
||||
|
||||
pub mod id;
|
||||
@@ -21,6 +23,32 @@ pub mod watcher;
|
||||
|
||||
pub static IS_SYSTEMD_SERVICE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
bitflags! {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ResizeEdge: u32 {
|
||||
const TOP = 0b0001;
|
||||
const BOTTOM = 0b0010;
|
||||
const LEFT = 0b0100;
|
||||
const RIGHT = 0b1000;
|
||||
|
||||
const TOP_LEFT = Self::TOP.bits() | Self::LEFT.bits();
|
||||
const BOTTOM_LEFT = Self::BOTTOM.bits() | Self::LEFT.bits();
|
||||
|
||||
const TOP_RIGHT = Self::TOP.bits() | Self::RIGHT.bits();
|
||||
const BOTTOM_RIGHT = Self::BOTTOM.bits() | Self::RIGHT.bits();
|
||||
|
||||
const LEFT_RIGHT = Self::LEFT.bits() | Self::RIGHT.bits();
|
||||
const TOP_BOTTOM = Self::TOP.bits() | Self::BOTTOM.bits();
|
||||
}
|
||||
}
|
||||
|
||||
impl From<xdg_toplevel::ResizeEdge> for ResizeEdge {
|
||||
#[inline]
|
||||
fn from(x: xdg_toplevel::ResizeEdge) -> Self {
|
||||
Self::from_bits(x as u32).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn version() -> String {
|
||||
format!(
|
||||
"{} ({})",
|
||||
|
||||
+77
-1
@@ -19,7 +19,9 @@ use smithay::wayland::compositor::{
|
||||
use smithay::wayland::shell::xdg::{SurfaceCachedState, ToplevelSurface};
|
||||
|
||||
use super::{ResolvedWindowRules, WindowRef};
|
||||
use crate::layout::{LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot};
|
||||
use crate::layout::{
|
||||
InteractiveResizeData, LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot,
|
||||
};
|
||||
use crate::niri::WindowOffscreenId;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::render_helpers::snapshot::RenderSnapshot;
|
||||
@@ -56,6 +58,23 @@ pub struct Mapped {
|
||||
|
||||
/// Snapshot right before an animated commit.
|
||||
animation_snapshot: Option<LayoutElementRenderSnapshot>,
|
||||
|
||||
/// State of an ongoing interactive resize.
|
||||
interactive_resize: Option<InteractiveResize>,
|
||||
}
|
||||
|
||||
/// Interactive resize state.
|
||||
#[derive(Debug)]
|
||||
enum InteractiveResize {
|
||||
/// The resize is ongoing.
|
||||
Ongoing(InteractiveResizeData),
|
||||
/// The resize has stopped and we're waiting to send the last configure.
|
||||
WaitingForLastConfigure(InteractiveResizeData),
|
||||
/// We had sent the last resize configure and are waiting for the corresponding commit.
|
||||
WaitingForLastCommit {
|
||||
data: InteractiveResizeData,
|
||||
serial: Serial,
|
||||
},
|
||||
}
|
||||
|
||||
impl Mapped {
|
||||
@@ -70,6 +89,7 @@ impl Mapped {
|
||||
animate_next_configure: false,
|
||||
animate_serials: Vec::new(),
|
||||
animation_snapshot: None,
|
||||
interactive_resize: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,6 +447,20 @@ impl LayoutElement for Mapped {
|
||||
if self.animate_next_configure {
|
||||
self.animate_serials.push(serial);
|
||||
}
|
||||
|
||||
self.interactive_resize = match self.interactive_resize.take() {
|
||||
Some(InteractiveResize::WaitingForLastConfigure(data)) => {
|
||||
Some(InteractiveResize::WaitingForLastCommit { data, serial })
|
||||
}
|
||||
x => x,
|
||||
}
|
||||
} else {
|
||||
self.interactive_resize = match self.interactive_resize.take() {
|
||||
// We probably started and stopped resizing in the same loop cycle without anything
|
||||
// changing.
|
||||
Some(InteractiveResize::WaitingForLastConfigure { .. }) => None,
|
||||
x => x,
|
||||
}
|
||||
}
|
||||
|
||||
self.animate_next_configure = false;
|
||||
@@ -459,4 +493,46 @@ impl LayoutElement for Mapped {
|
||||
fn take_animation_snapshot(&mut self) -> Option<LayoutElementRenderSnapshot> {
|
||||
self.animation_snapshot.take()
|
||||
}
|
||||
|
||||
fn set_interactive_resize(&mut self, data: Option<InteractiveResizeData>) {
|
||||
self.toplevel().with_pending_state(|state| {
|
||||
if data.is_some() {
|
||||
state.states.set(xdg_toplevel::State::Resizing);
|
||||
} else {
|
||||
state.states.unset(xdg_toplevel::State::Resizing);
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(data) = data {
|
||||
self.interactive_resize = Some(InteractiveResize::Ongoing(data));
|
||||
} else {
|
||||
self.interactive_resize = match self.interactive_resize.take() {
|
||||
Some(InteractiveResize::Ongoing(data)) => {
|
||||
Some(InteractiveResize::WaitingForLastConfigure(data))
|
||||
}
|
||||
x => x,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel_interactive_resize(&mut self) {
|
||||
self.set_interactive_resize(None);
|
||||
self.interactive_resize = None;
|
||||
}
|
||||
|
||||
fn interactive_resize_data(&mut self, commit_serial: Serial) -> Option<InteractiveResizeData> {
|
||||
let resize = self.interactive_resize.as_ref()?;
|
||||
match resize {
|
||||
InteractiveResize::Ongoing(data) | InteractiveResize::WaitingForLastConfigure(data) => {
|
||||
Some(*data)
|
||||
}
|
||||
InteractiveResize::WaitingForLastCommit { data, serial } => {
|
||||
let rv = Some(*data);
|
||||
if commit_serial.is_no_older_than(serial) {
|
||||
self.interactive_resize = None;
|
||||
}
|
||||
rv
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user