Compare commits

...

34 Commits

Author SHA1 Message Date
Rasmus Eneman
6e4cdba49f
Merge 5e5b18f7a4 into 2f73dd5b59 2024-08-15 01:20:50 +08:00
Ivan Molodetskikh
2f73dd5b59 wiki: Use real em-dash 2024-08-14 18:33:43 +03:00
Ivan Molodetskikh
c658424c9f wiki: Document invisible state 2024-08-14 18:32:50 +03:00
Ivan Molodetskikh
bb58f2d162 wiki: Clarify named workspaces example 2024-08-14 18:18:05 +03:00
Fea
f54297f242 flake: Update flake inputs 2024-08-14 10:49:54 +03:00
Fea
b72d946062 Fix nix build 2024-08-14 10:49:54 +03:00
Rasmus Eneman
5e5b18f7a4
Extract move location calcs to methods 2024-08-08 11:51:25 +02:00
Rasmus Eneman
facb81b7e8
Implement window_under for moving window 2024-08-05 17:21:59 +02:00
Rasmus Eneman
c04c36c341
Take into account gaps in x direction for insert hints 2024-08-05 17:16:40 +02:00
Rasmus Eneman
25991a685b
Clear insert hint if moved window get closed 2024-08-05 16:29:47 +02:00
Rasmus Eneman
81da4725c4
Handle resizing windows during move 2024-08-05 16:29:24 +02:00
Rasmus Eneman
65198f406e
Disable triple-click moves 2024-08-02 09:44:22 +02:00
Rasmus Eneman
b5e4ab5881
Only buffer is not fallback buffer 2024-08-02 09:39:52 +02:00
Rasmus Eneman
ba743171f5
Make layout render method name clearer 2024-08-02 09:38:16 +02:00
Rasmus Eneman
2bdc7ae78d
Check window id in (store|clear)_unmap_snapshot 2024-08-02 09:35:28 +02:00
Rasmus Eneman
e17c156151
Remove unnecessary clone derive 2024-08-02 09:31:51 +02:00
Rasmus Eneman
784adbb092
Disable insert hint by default 2024-08-02 09:30:40 +02:00
Rasmus Eneman
3a6cfd828b
Comment + whitespace 2024-08-02 09:30:25 +02:00
Rasmus Eneman
432e1e273a
Fix insert hint position on empty workspace and with gaps 2024-07-22 11:34:03 +02:00
Rasmus Eneman
9a019449b1
Merge remote-tracking branch 'upstream/main' into interactive-move 2024-07-22 11:21:31 +02:00
Rasmus Eneman
ea22684fc1
Fix insert hint left after switching workspace during move 2024-07-18 23:14:40 +02:00
Rasmus Eneman
2c46b02a39
Replace auto scrolling workspace with manual using middle mouse button 2024-07-18 22:41:50 +02:00
Rasmus Eneman
3e098bd88e
Use Tile for moving window 2024-07-18 16:57:07 +02:00
Rasmus Eneman
b673b6ddeb
Keep focus on moving window 2024-07-18 11:41:20 +02:00
Rasmus Eneman
5a18ed73fb
Show visual hint where window will be placed 2024-07-18 10:49:43 +02:00
Rasmus Eneman
26f5921618
Fix activation when placing in column 2024-07-18 10:49:43 +02:00
Rasmus Eneman
6a7ca85e3e
Handle multiple outputs 2024-07-18 10:49:43 +02:00
Rasmus Eneman
a36f8923a7
Fix animations of drop in column 2024-07-18 10:49:43 +02:00
Rasmus Eneman
539bd241f4
Render moving window 2024-07-18 10:49:43 +02:00
Rasmus Eneman
2cf2c45a40
Remove moving window from workspace 2024-07-18 10:49:43 +02:00
Rasmus Eneman
2b3acf6a16
Insert windows where they where dropped 2024-07-15 19:59:12 +02:00
Rasmus Eneman
217ccb8c56
Add animations when picking up window 2024-07-15 18:48:52 +02:00
Rasmus Eneman
9b2f138c5f
Fix updating other positions correctly while moving 2024-07-15 18:34:16 +02:00
Rasmus Eneman
264de6f3d0
Support initiating an interactive move 2024-07-15 16:11:53 +02:00
13 changed files with 1081 additions and 30 deletions

View File

@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1720226507,
"narHash": "sha256-yHVvNsgrpyNTXZBEokL8uyB2J6gB1wEx0KOJzoeZi1A=",
"lastModified": 1722960479,
"narHash": "sha256-NhCkJJQhD5GUib8zN9JrmYGMwt4lCRp6ZVNzIiYCl0Y=",
"owner": "ipetkov",
"repo": "crane",
"rev": "0aed560c5c0a61c9385bddff471a13036203e11c",
"rev": "4c6c77920b8d44cd6660c1621dea6b3fc4b4c4f4",
"type": "github"
},
"original": {
@ -28,11 +28,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1719815435,
"narHash": "sha256-K2xFp142onP35jcx7li10xUxNVEVRWjAdY8DSuR7Naw=",
"lastModified": 1722493751,
"narHash": "sha256-l7/yMehbrL5d4AI8E2hKtNlT50BlUAau4EKTgPg9KcY=",
"owner": "nix-community",
"repo": "fenix",
"rev": "ebfe2c639111d7e82972a12711206afaeeda2450",
"rev": "60ab4a085ef6ee40f2ef7921ca4061084dd8cf26",
"type": "github"
},
"original": {
@ -77,11 +77,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1720368505,
"narHash": "sha256-5r0pInVo5d6Enti0YwUSQK4TebITypB42bWy5su3MrQ=",
"lastModified": 1723572004,
"narHash": "sha256-U5gKtbKuPahB02iGeGHFPlKr/HqrvSsHlEDEXoVyaPc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ab82a9612aa45284d4adf69ee81871a389669a9e",
"rev": "19674872444bb3e0768249e724d99c8649c3bd78",
"type": "github"
},
"original": {
@ -103,11 +103,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1719760370,
"narHash": "sha256-fsxAuW6RxKZYjAP3biUC6C4vaYFhDfWv8lp1Tmx3ZCY=",
"lastModified": 1722449213,
"narHash": "sha256-1na4m2PNH99syz2g/WQ+Hr3RfY7k4H8NBnmkr5dFDXw=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "ea7fdada6a0940b239ddbde2048a4d7dac1efe1e",
"rev": "c8e41d95061543715b30880932ec3dc24c42d7ae",
"type": "github"
},
"original": {

View File

@ -33,7 +33,7 @@
system: let
pkgs = nixpkgs.legacyPackages.${system};
toolchain = fenix.packages.${system}.complete.toolchain;
craneLib = crane.lib.${system}.overrideToolchain toolchain;
craneLib = (crane.mkLib pkgs).overrideToolchain toolchain;
craneArgs = {
pname = "niri";
@ -80,6 +80,7 @@
];
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath craneArgs.runtimeDependencies; # Needed for tests to find libxkbcommon
};
cargoArtifacts = craneLib.buildDepsOnly craneArgs;

View File

@ -362,6 +362,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<PresetWidth>,
#[knuffel(child)]
@ -379,6 +381,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(),
@ -523,6 +526,23 @@ impl From<FocusRing> 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 {
@ -2918,6 +2938,10 @@ mod tests {
active_gradient: None,
inactive_gradient: None,
},
insert_hint: InsertHint {
off: false,
color: Color::from_rgba8_unpremul(0, 100, 200, 128),
},
preset_column_widths: vec![
PresetWidth::Proportion(0.25),
PresetWidth::Proportion(0.5),

View File

@ -178,6 +178,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.

View File

@ -5,7 +5,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;
@ -32,6 +32,7 @@ use smithay::{
delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_foreign, delegate_xdg_shell,
};
use crate::input::move_grab::MoveGrab;
use crate::input::resize_grab::ResizeGrab;
use crate::input::DOUBLE_CLICK_TIME;
use crate::layout::workspace::ColumnWidth;
@ -60,8 +61,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(

View File

@ -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;
@ -1332,8 +1334,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,

217
src/input/move_grab.rs Normal file
View File

@ -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<State>,
last_location: Point<f64, Logical>,
window: Window,
is_moving: bool,
}
impl MoveGrab {
pub fn new(start_data: PointerGrabStartData<State>, 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<State> for MoveGrab {
fn motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, 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 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<(<State as SeatHandler>::PointerFocus, Point<f64, 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);
// 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<State> {
&self.start_data
}
fn unset(&mut self, data: &mut State) {
self.on_ungrab(data);
}
}

View File

@ -41,11 +41,13 @@ 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};
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;
@ -75,6 +77,17 @@ niri_render_elements! {
pub type LayoutElementRenderSnapshot =
RenderSnapshot<BakedBuffer<TextureBuffer<GlesTexture>>, BakedBuffer<SolidColorBuffer>>;
#[derive(Debug)]
pub struct InteractiveMoveData<W: LayoutElement> {
pub window: Tile<W>,
pub output: Output,
pub width: ColumnWidth,
pub is_full_width: bool,
pub initial_pointer_location: Point<f64, Logical>,
pub pointer_offset: Point<f64, Logical>,
pub window_offset: Point<f64, Logical>,
}
#[derive(Debug, Clone, Copy)]
pub struct InteractiveResizeData {
pub edges: ResizeEdge,
@ -184,6 +197,8 @@ pub trait LayoutElement {
pub struct Layout<W: LayoutElement> {
/// Monitors and workspaes in the layout.
monitor_set: MonitorSet<W>,
/// Ongoing interactive move.
interactive_move: Option<InteractiveMoveData<W>>,
/// Configurable properties of the layout.
options: Rc<Options>,
}
@ -214,6 +229,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,
/// Column widths that `toggle_width()` switches between.
pub preset_widths: Vec<ColumnWidth>,
@ -229,6 +245,7 @@ impl Default for Options {
struts: Default::default(),
focus_ring: Default::default(),
border: Default::default(),
insert_hint: Default::default(),
center_focused_column: Default::default(),
preset_widths: vec![
ColumnWidth::Proportion(1. / 3.),
@ -269,6 +286,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,
preset_widths,
default_width,
@ -287,6 +305,25 @@ impl Options {
}
}
impl<W: LayoutElement> InteractiveMoveData<W> {
pub fn get_pointer_loc(&self) -> Point<f64, Logical> {
self.initial_pointer_location + self.pointer_offset
- self.output.current_location().to_f64()
}
pub fn get_precise_window_loc(&self) -> Point<f64, Logical> {
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<W: LayoutElement> Layout<W> {
pub fn new(config: &Config) -> Self {
Self::with_options_and_workspaces(config, Options::from_config(config))
@ -295,6 +332,7 @@ impl<W: LayoutElement> Layout<W> {
pub fn with_options(options: Options) -> Self {
Self {
monitor_set: MonitorSet::NoOutputs { workspaces: vec![] },
interactive_move: None,
options: Rc::new(options),
}
}
@ -310,6 +348,7 @@ impl<W: LayoutElement> Layout<W> {
Self {
monitor_set: MonitorSet::NoOutputs { workspaces },
interactive_move: None,
options: opts,
}
}
@ -727,6 +766,18 @@ impl<W: LayoutElement> Layout<W> {
pub fn remove_window(&mut self, window: &W::Id) -> Option<W> {
let mut rv = None;
if self
.interactive_move
.as_ref()
.map_or(false, |move_| move_.window.window().id() == window)
{
let move_ = self.interactive_move.take().unwrap();
rv = Some(move_.window.into_window());
if let Some(workspace) = self.workspace_for_output_mut(&move_.output) {
workspace.clear_insert_hint();
}
}
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
@ -773,12 +824,31 @@ impl<W: LayoutElement> Layout<W> {
}
pub fn update_window(&mut self, window: &W::Id, serial: Option<Serial>) {
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;
}
}
@ -796,6 +866,12 @@ impl<W: LayoutElement> Layout<W> {
}
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 {
@ -875,6 +951,12 @@ impl<W: LayoutElement> Layout<W> {
&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 {
@ -898,6 +980,12 @@ impl<W: LayoutElement> Layout<W> {
}
pub fn window_loc(&self, window: &W::Id) -> Option<Point<f64, Logical>> {
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 {
@ -964,6 +1052,10 @@ impl<W: LayoutElement> Layout<W> {
}
pub fn activate_window(&mut self, window: &W::Id) {
if self.interactive_move.is_some() {
return;
}
let MonitorSet::Normal {
monitors,
active_monitor_idx,
@ -1039,6 +1131,10 @@ impl<W: LayoutElement> Layout<W> {
}
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,
@ -1064,11 +1160,21 @@ impl<W: LayoutElement> Layout<W> {
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>)) {
if let Some(move_) = &self.interactive_move {
f(move_.window.window(), Some(&move_.output));
}
match &self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
@ -1090,6 +1196,10 @@ impl<W: LayoutElement> Layout<W> {
}
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 {
@ -1158,6 +1268,18 @@ impl<W: LayoutElement> Layout<W> {
})
}
fn workspace_for_output_mut(&mut self, output: &Output) -> Option<&mut Workspace<W>> {
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<Item = &Output> + '_ {
let monitors = if let MonitorSet::Normal { monitors, .. } = &self.monitor_set {
&monitors[..]
@ -1545,6 +1667,10 @@ impl<W: LayoutElement> Layout<W> {
}
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,
@ -1569,6 +1695,20 @@ impl<W: LayoutElement> Layout<W> {
output: &Output,
pos_within_output: Point<f64, Logical>,
) -> Option<(&W, Option<Point<f64, Logical>>)> {
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;
};
@ -1758,6 +1898,10 @@ impl<W: LayoutElement> Layout<W> {
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 {
@ -1775,6 +1919,15 @@ impl<W: LayoutElement> Layout<W> {
pub fn update_render_elements(&mut self, output: &Output) {
let _span = tracy_client::span!("Layout::update_render_elements");
if let Some(move_) = &mut self.interactive_move {
if 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,
@ -1787,7 +1940,9 @@ impl<W: LayoutElement> Layout<W> {
for (idx, mon) in monitors.iter_mut().enumerate() {
if mon.output == *output {
mon.update_render_elements(idx == *active_monitor_idx);
mon.update_render_elements(
self.interactive_move.is_none() && idx == *active_monitor_idx,
);
return;
}
}
@ -1796,6 +1951,12 @@ impl<W: LayoutElement> Layout<W> {
pub fn update_render_elements_all(&mut self) {
let _span = tracy_client::span!("Layout::update_render_elements_all");
if let Some(move_) = &mut self.interactive_move {
let view_rect =
Rectangle::from_loc_and_size(move_.get_precise_window_loc(), output_size(&move_.output));
move_.window.update(true, view_rect);
}
let MonitorSet::Normal {
monitors,
active_monitor_idx,
@ -1812,6 +1973,10 @@ impl<W: LayoutElement> Layout<W> {
}
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 {
@ -2246,6 +2411,149 @@ impl<W: LayoutElement> Layout<W> {
None
}
pub fn interactive_move_begin(
&mut self,
window: W::Id,
output: Output,
pointer_location: Point<f64, Logical>,
) -> 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))
.next(),
MonitorSet::NoOutputs { workspaces, .. } => workspaces
.iter_mut()
.filter(|ws| ws.has_window(&window))
.map(|ws| ws.remove_window_with_col_info(&window))
.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<Output>,
delta: Point<f64, Logical>,
) -> 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_window_at(column_idx, move_.window.into_window(), true, width, false)
}
InsertPosition::InColumn(column_idx, tile_idx) => workspace.add_window_in_column(
column_idx,
tile_idx,
move_.window.into_window(),
true,
),
}
}
pub fn interactive_resize_begin(&mut self, window: W::Id, edges: ResizeEdge) -> bool {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
@ -2367,6 +2675,15 @@ impl<W: LayoutElement> Layout<W> {
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 {
@ -2390,6 +2707,13 @@ impl<W: LayoutElement> Layout<W> {
}
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 {
@ -2441,9 +2765,47 @@ impl<W: LayoutElement> Layout<W> {
}
}
pub fn render_floating_for_output<R: NiriRenderer>(
&self,
renderer: &mut R,
target: RenderTarget,
output: &Output,
) -> Vec<TileRenderElement<R>> {
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,
@ -2453,7 +2815,10 @@ impl<W: LayoutElement> Layout<W> {
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 {
@ -2464,7 +2829,7 @@ impl<W: LayoutElement> Layout<W> {
}
MonitorSet::NoOutputs { workspaces, .. } => {
for ws in workspaces {
ws.refresh(false);
ws.refresh(false, false);
ws.view_offset_gesture_end(false, None);
}
}
@ -2731,6 +3096,10 @@ mod tests {
prop_oneof![(-10f64..10f64), (-50000f64..50000f64),]
}
fn arbitrary_move_point() -> impl Strategy<Value = Point<f64, Logical>> {
any::<(f64, f64)>().prop_map(|(x, y)| Point::from((x, y)))
}
fn arbitrary_resize_edge() -> impl Strategy<Value = ResizeEdge> {
prop_oneof![
Just(ResizeEdge::RIGHT),
@ -2889,6 +3258,28 @@ mod tests {
cancelled: bool,
is_touchpad: Option<bool>,
},
InteractiveMoveBegin {
#[proptest(strategy = "1..=5usize")]
window: usize,
#[proptest(strategy = "1..=5usize")]
output_idx: usize,
#[proptest(strategy = "arbitrary_move_point()")]
point: Point<f64, Logical>,
},
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,
@ -3337,6 +3728,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);
}

View File

@ -6,6 +6,7 @@ use std::time::Duration;
use niri_config::{CenterFocusedColumn, PresetWidth, Struts, Workspace as WorkspaceConfig};
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;
@ -20,6 +21,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::{output_size, send_scale_transform, ResizeEdge};
@ -103,6 +105,9 @@ pub struct Workspace<W: LayoutElement> {
/// Windows in the closing animation.
closing_windows: Vec<ClosingWindow>,
/// Indication where a window is about to be placed.
insert_hint: Option<InsertHint>,
/// Configurable properties of the layout as received from the parent monitor.
pub base_options: Rc<Options>,
@ -116,6 +121,19 @@ pub struct Workspace<W: LayoutElement> {
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, PartialEq, Eq)]
pub struct OutputId(String);
@ -370,6 +388,7 @@ impl<W: LayoutElement> Workspace<W> {
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),
@ -408,6 +427,7 @@ impl<W: LayoutElement> Workspace<W> {
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),
@ -883,6 +903,87 @@ impl<W: LayoutElement> Workspace<W> {
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<f64, Logical>) -> 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_at(
&mut self,
col_idx: usize,
@ -895,6 +996,53 @@ impl<W: LayoutElement> Workspace<W> {
self.add_tile_at(col_idx, tile, activate, width, is_full_width, None);
}
pub fn add_window_in_column(
&mut self,
col_idx: usize,
tile_idx: usize,
window: W,
activate: bool,
) {
self.enter_output_for_window(&window);
let tile = Tile::new(window, self.scale.fractional_scale(), self.options.clone());
let prev_next_x = self.column_x(self.active_column_idx + 1);
let target_column = &mut self.columns[col_idx];
let was_fullscreen = target_column.tiles[target_column.active_tile_idx].is_fullscreen();
let prev_offsets: Vec<_> = target_column.tile_offsets().skip(tile_idx).collect();
target_column.add_tile_at(tile_idx, tile, true);
if !was_fullscreen {
self.view_offset_before_fullscreen = None;
}
// Animate movement of other tiles.
let new_offsets: Vec<_> = target_column.tile_offsets().skip(tile_idx + 1).collect();
for ((prev_offset, new_offset), tile) in zip(
zip(prev_offsets, new_offsets),
&mut target_column.tiles[tile_idx + 1..],
) {
tile.animate_move_from(prev_offset - new_offset);
}
self.data[col_idx].update(target_column);
if activate {
target_column.active_tile_idx = tile_idx;
if self.active_column_idx != col_idx {
self.activate_column(col_idx);
}
}
// Consuming a window into a column could've increased its width if the new window had a
// larger min width. Move the next columns to account for this.
let offset_next = prev_next_x - self.column_x(self.active_column_idx + 1);
for col in &mut self.columns[self.active_column_idx + 1..] {
col.animate_move_from(offset_next);
}
}
fn add_tile_at(
&mut self,
col_idx: usize,
@ -1090,6 +1238,7 @@ impl<W: LayoutElement> Workspace<W> {
window_idx: usize,
anim_config: Option<niri_config::Animation>,
) -> Tile<W> {
self.insert_hint = None;
let offset = self.column_x(column_idx + 1) - self.column_x(column_idx);
let column = &mut self.columns[column_idx];
@ -1278,16 +1427,32 @@ impl<W: LayoutElement> Workspace<W> {
}
pub fn remove_window(&mut self, window: &W::Id) -> W {
self.remove_window_with_col_info(window).3
}
pub fn remove_window_with_col_info(
&mut self,
window: &W::Id,
) -> (Point<f64, Logical>, 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, 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, None)
.into_window();
(render_pos, width, is_full_width, window)
}
pub fn update_window(&mut self, window: &W::Id, serial: Option<Serial>) {
@ -1742,6 +1907,7 @@ impl<W: LayoutElement> Workspace<W> {
if self.columns.is_empty() {
return;
}
self.insert_hint = None;
let source_col_idx = self.active_column_idx;
let source_column = &self.columns[source_col_idx];
@ -1820,6 +1986,7 @@ impl<W: LayoutElement> Workspace<W> {
if self.columns.is_empty() {
return;
}
self.insert_hint = None;
let source_col_idx = self.active_column_idx;
let offset = self.column_x(source_col_idx) - self.column_x(source_col_idx + 1);
@ -1895,6 +2062,7 @@ impl<W: LayoutElement> Workspace<W> {
if self.active_column_idx == self.columns.len() - 1 {
return;
}
self.insert_hint = None;
let source_column_idx = self.active_column_idx + 1;
@ -2101,6 +2269,58 @@ impl<W: LayoutElement> Workspace<W> {
})
}
fn insert_hint_area(&self, insert_hint: &InsertHint) -> Option<Rectangle<f64, Logical>> {
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.,
));
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.);
Some(hint_area)
}
/// Returns the geometry of the active tile relative to and clamped to the view.
///
/// During animations, assumes the final view position.
@ -2348,6 +2568,26 @@ impl<W: LayoutElement> Workspace<W> {
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;
}
@ -2707,7 +2947,10 @@ impl<W: LayoutElement> Workspace<W> {
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 {
@ -2723,7 +2966,7 @@ impl<W: LayoutElement> Workspace<W> {
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);
@ -2936,9 +3179,14 @@ impl<W: LayoutElement> Column<W> {
}
fn add_tile(&mut self, tile: Tile<W>, animate: bool) {
self.add_tile_at(self.tiles.len(), tile, animate);
}
fn add_tile_at(&mut self, pos: usize, tile: Tile<W>, animate: bool) {
self.is_fullscreen = false;
self.data.push(TileData::new(&tile, WindowHeight::Auto));
self.tiles.push(tile);
self.data
.insert(pos, TileData::new(&tile, WindowHeight::Auto));
self.tiles.insert(pos, tile);
self.update_tile_sizes(animate);
}

View File

@ -115,6 +115,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::{Layout, LayoutElement as _, MonitorRenderElement};
use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState};
use crate::protocols::gamma_control::GammaControlManagerState;
@ -2932,6 +2933,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);
@ -2964,8 +2968,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));
}
@ -4601,6 +4615,7 @@ impl ClientData for ClientState {
niri_render_elements! {
OutputRenderElements<R> => {
Monitor = MonitorRenderElement<R>,
Tile = TileRenderElement<R>,
Wayland = WaylandSurfaceRenderElement<R>,
NamedPointer = MemoryRenderBufferRenderElement<R>,
SolidColor = SolidColorRenderElement,

View File

@ -69,6 +69,11 @@ pub struct Mapped {
/// Snapshot right before an animated commit.
animation_snapshot: Option<LayoutElementRenderSnapshot>,
/// Last time interactive move was started.
///
/// Used for double-move-click tracking.
last_interactive_move_start: Cell<Option<Duration>>,
/// State of an ongoing interactive resize.
interactive_resize: Option<InteractiveResize>,
@ -139,6 +144,7 @@ impl Mapped {
animate_next_configure: false,
animate_serials: Vec::new(),
animation_snapshot: None,
last_interactive_move_start: Cell::new(None),
interactive_resize: None,
last_interactive_resize_start: Cell::new(None),
}
@ -253,6 +259,10 @@ impl Mapped {
self.animation_snapshot = Some(self.render_snapshot(renderer));
}
pub fn last_interactive_move_start(&self) -> &Cell<Option<Duration>> {
&self.last_interactive_move_start
}
pub fn last_interactive_resize_start(&self) -> &Cell<Option<(Duration, ResizeEdge)>> {
&self.last_interactive_resize_start
}

View File

@ -24,7 +24,7 @@ workspace "chat" {
open-on-output "DP-2"
}
// Open Fractal on the "chat" workspace at niri startup.
// Open Fractal on the "chat" workspace, if it runs at niri startup.
window-rule {
match at-startup=true app-id=r#"^org\.gnome\.Fractal$"#
open-on-workspace "chat"

View File

@ -22,3 +22,9 @@ And here are some more principles I try to follow throughout niri.
1. Eye-candy features should not cause unreasonable excessive rendering.
- For example, clip-to-geometry will prevent direct scanout in many cases (since the window surface is not completely visible). But in the cases where the surface or the subsurface *is* completely visible (fully within the clipped region), it will still allow for direct scanout.
- For example, animations *can* cause damage and even draw to an offscreen every frame, because they are expected to be short (and can be disabled). However, something like the rounded corners shader should not offscreen or cause excessive damage every frame, because it is long-running and constantly active.
1. Be mindful of invisible state.
This is niri state that is not immediately apparent from looking at the screen. This is not bad per se, but you should carefully consider how to reduce the surprise factor.
- For example, when a monitor disconnects, all its workspaces move to another connected monitor. In order to be able to restore these workspaces when the first monitor connects again, these workspaces keep the knowledge of which was their *original monitor*—this is an example of invisible state, since you can't tell it in any way by looking at the screen. This can have surprising consequences: imagine disconnecting a monitor at home, going to work, completely rearranging the windows there, then coming back home, and suddenly some random workspaces end up on your home monitor. In order to reduce this surprise factor, whenever a new window appears on a workspace, that workspace resets its *original monitor* to its current monitor. This way, the workspaces you actively worked on remain where they were.
- For example, niri preserves the view position whenever a window appears, or whenever a window goes full-screen, to restore it afterward. This way, dealing with temporary things like dialogs opening and closing, or toggling full-screen, becomes less annoying, since it doesn't mess up the view position. This is also invisible state, as you cannot tell by looking at the screen where closing a window will restore the view position. If taken to the extreme (previous view position saved forever for every open window), this can be surprising, as closing long-running windows would result in the view shifting around pretty much randomly. To reduce this surprise factor, niri remembers only one last view position per workspace, and forgets this stored view position upon window focus change.