mirror of
https://github.com/YaLTeR/niri.git
synced 2024-09-11 20:37:25 +03:00
Compare commits
34 Commits
50aa31dc2f
...
6e4cdba49f
Author | SHA1 | Date | |
---|---|---|---|
|
6e4cdba49f | ||
|
2f73dd5b59 | ||
|
c658424c9f | ||
|
bb58f2d162 | ||
|
f54297f242 | ||
|
b72d946062 | ||
|
5e5b18f7a4 | ||
|
facb81b7e8 | ||
|
c04c36c341 | ||
|
25991a685b | ||
|
81da4725c4 | ||
|
65198f406e | ||
|
b5e4ab5881 | ||
|
ba743171f5 | ||
|
2bdc7ae78d | ||
|
e17c156151 | ||
|
784adbb092 | ||
|
3a6cfd828b | ||
|
432e1e273a | ||
|
9a019449b1 | ||
|
ea22684fc1 | ||
|
2c46b02a39 | ||
|
3e098bd88e | ||
|
b673b6ddeb | ||
|
5a18ed73fb | ||
|
26f5921618 | ||
|
6a7ca85e3e | ||
|
a36f8923a7 | ||
|
539bd241f4 | ||
|
2cf2c45a40 | ||
|
2b3acf6a16 | ||
|
217ccb8c56 | ||
|
9b2f138c5f | ||
|
264de6f3d0 |
24
flake.lock
24
flake.lock
@ -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": {
|
||||
|
@ -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;
|
||||
|
@ -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),
|
||||
|
@ -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.
|
||||
|
@ -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(
|
||||
|
@ -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
217
src/input/move_grab.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
15
src/niri.rs
15
src/niri.rs
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user