Compare commits

...

7 Commits

Author SHA1 Message Date
Ivan Molodetskikh
db49deb7fd Implement draw-border-with-background window rule 2024-03-19 18:29:13 +04:00
Ivan Molodetskikh
c61361de3c Implement window rule reloading and min/max size rules 2024-03-19 18:29:13 +04:00
Ivan Molodetskikh
3963f537a4 Wrap mapped windows in a Mapped 2024-03-19 18:29:13 +04:00
Ivan Molodetskikh
f31e105043 Make window a subdirectory 2024-03-19 18:29:13 +04:00
Ivan Molodetskikh
bbb4caeb8c Remove remaining Window-specific functions 2024-03-19 18:29:13 +04:00
Ivan Molodetskikh
d421e1fbf8 Move PartialEq from LayoutElement to an associated type 2024-03-19 18:29:13 +04:00
FluxTape
23ac3d7323
Workspace back and forth (#253)
* implement workspace back and forth

* Make our own ID counter instead of SerialCounter, use a newtype

* Rename FocusWorkspaceBackAndForth to FocusWorkspacePrevious

* Add focus-workspace-previous to tests

* Don't special case in switch_workspace_previous

* Minor clean up

* Add switch_workspace_auto_back_and_forth to tests

* Skip animation on switch_workspace_previous

* Preserve previous_workspace_id on workspace movement

* Make Workspace::id private with a getter

Reduce the chance it gets overwritten.

* Add test for workspace ID uniqueness

* Update previous workspace ID upon moving workspace across monitors

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2024-03-19 07:27:52 -07:00
20 changed files with 908 additions and 426 deletions

View File

@ -73,6 +73,8 @@ pub struct Input {
pub warp_mouse_to_focus: bool,
#[knuffel(child)]
pub focus_follows_mouse: bool,
#[knuffel(child)]
pub workspace_auto_back_and_forth: bool,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq)]
@ -672,6 +674,7 @@ pub struct WindowRule {
#[knuffel(children(name = "exclude"))]
pub excludes: Vec<Match>,
// Rules applied at initial configure.
#[knuffel(child)]
pub default_column_width: Option<DefaultColumnWidth>,
#[knuffel(child, unwrap(argument))]
@ -680,6 +683,19 @@ pub struct WindowRule {
pub open_maximized: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub open_fullscreen: Option<bool>,
// Rules applied dynamically.
#[knuffel(child, unwrap(argument))]
pub min_width: Option<u16>,
#[knuffel(child, unwrap(argument))]
pub min_height: Option<u16>,
#[knuffel(child, unwrap(argument))]
pub max_width: Option<u16>,
#[knuffel(child, unwrap(argument))]
pub max_height: Option<u16>,
#[knuffel(child, unwrap(argument))]
pub draw_border_with_background: Option<bool>,
}
#[derive(knuffel::Decode, Debug, Default, Clone)]
@ -766,6 +782,7 @@ pub enum Action {
FocusWorkspaceDown,
FocusWorkspaceUp,
FocusWorkspace(#[knuffel(argument)] u8),
FocusWorkspacePrevious,
MoveWindowToWorkspaceDown,
MoveWindowToWorkspaceUp,
MoveWindowToWorkspace(#[knuffel(argument)] u8),
@ -835,6 +852,7 @@ impl From<niri_ipc::Action> for Action {
niri_ipc::Action::FocusWorkspaceDown => Self::FocusWorkspaceDown,
niri_ipc::Action::FocusWorkspaceUp => Self::FocusWorkspaceUp,
niri_ipc::Action::FocusWorkspace { index } => Self::FocusWorkspace(index),
niri_ipc::Action::FocusWorkspacePrevious => Self::FocusWorkspacePrevious,
niri_ipc::Action::MoveWindowToWorkspaceDown => Self::MoveWindowToWorkspaceDown,
niri_ipc::Action::MoveWindowToWorkspaceUp => Self::MoveWindowToWorkspaceUp,
niri_ipc::Action::MoveWindowToWorkspace { index } => Self::MoveWindowToWorkspace(index),
@ -1599,6 +1617,7 @@ mod tests {
warp-mouse-to-focus
focus-follows-mouse
workspace-auto-back-and-forth
}
output "eDP-1" {
@ -1740,6 +1759,7 @@ mod tests {
disable_power_key_handling: true,
warp_mouse_to_focus: true,
focus_follows_mouse: true,
workspace_auto_back_and_forth: true,
},
outputs: vec![Output {
off: false,

View File

@ -123,6 +123,8 @@ pub enum Action {
#[cfg_attr(feature = "clap", arg())]
index: u8,
},
/// Focus the previous workspace.
FocusWorkspacePrevious,
/// Move the focused window to the workspace below.
MoveWindowToWorkspaceDown,
/// Move the focused window to the workspace above.

View File

@ -2,7 +2,7 @@ use std::collections::HashMap;
use std::time::Duration;
use niri::layout::workspace::ColumnWidth;
use niri::layout::Options;
use niri::layout::{LayoutElement as _, Options};
use niri::utils::get_monotonic_time;
use niri_config::Color;
use smithay::backend::renderer::element::RenderElement;
@ -73,12 +73,12 @@ impl Layout {
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
rv.layout.activate_window(&rv.windows[0]);
rv.layout.activate_window(&0);
rv.add_step(500, |l| {
let win = TestWindow::freeform(2);
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
l.layout.start_open_animation_for_window(&win);
l.layout.start_open_animation_for_window(win.id());
});
rv
@ -91,7 +91,7 @@ impl Layout {
rv.add_step(delay, move |l| {
let win = TestWindow::freeform(delay as usize);
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
l.layout.start_open_animation_for_window(&win);
l.layout.start_open_animation_for_window(win.id());
});
}
@ -105,7 +105,7 @@ impl Layout {
rv.add_step(delay, move |l| {
let win = TestWindow::freeform(delay as usize);
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.5)));
l.layout.start_open_animation_for_window(&win);
l.layout.start_open_animation_for_window(win.id());
});
}
@ -122,7 +122,7 @@ impl Layout {
let win = TestWindow::freeform(2);
let right_of = l.windows[0].clone();
l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.3)));
l.layout.start_open_animation_for_window(&win);
l.layout.start_open_animation_for_window(win.id());
});
rv
@ -138,7 +138,7 @@ impl Layout {
let win = TestWindow::freeform(2);
let right_of = l.windows[0].clone();
l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.5)));
l.layout.start_open_animation_for_window(&win);
l.layout.start_open_animation_for_window(win.id());
});
rv
@ -147,7 +147,7 @@ impl Layout {
fn add_window(&mut self, window: TestWindow, width: Option<ColumnWidth>) {
self.layout.add_window(window.clone(), width, false);
if window.communicate() {
self.layout.update_window(&window);
self.layout.update_window(window.id());
}
self.windows.push(window);
}
@ -159,9 +159,9 @@ impl Layout {
width: Option<ColumnWidth>,
) {
self.layout
.add_window_right_of(right_of, window.clone(), width, false);
.add_window_right_of(right_of.id(), window.clone(), width, false);
if window.communicate() {
self.layout.update_window(&window);
self.layout.update_window(window.id());
}
self.windows.push(window);
}
@ -183,7 +183,7 @@ impl TestCase for Layout {
self.layout.update_output_size(&self.output);
for win in &self.windows {
if win.communicate() {
self.layout.update_window(win);
self.layout.update_window(win.id());
}
}
}

View File

@ -4,6 +4,7 @@ use std::rc::Rc;
use niri::layout::{LayoutElement, LayoutElementRenderElement};
use niri::render_helpers::renderer::NiriRenderer;
use niri::window::ResolvedWindowRules;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::{Id, Kind};
use smithay::output::Output;
@ -12,7 +13,6 @@ use smithay::utils::{Logical, Point, Scale, Size, Transform};
#[derive(Debug)]
struct TestWindowInner {
id: usize,
size: Size<i32, Logical>,
requested_size: Option<Size<i32, Logical>>,
min_size: Size<i32, Logical>,
@ -24,7 +24,10 @@ struct TestWindowInner {
}
#[derive(Debug, Clone)]
pub struct TestWindow(Rc<RefCell<TestWindowInner>>);
pub struct TestWindow {
id: usize,
inner: Rc<RefCell<TestWindowInner>>,
}
impl TestWindow {
pub fn freeform(id: usize) -> Self {
@ -33,17 +36,19 @@ impl TestWindow {
let max_size = Size::from((0, 0));
let buffer = SolidColorBuffer::new(size, [0.15, 0.64, 0.41, 1.]);
Self(Rc::new(RefCell::new(TestWindowInner {
Self {
id,
size,
requested_size: None,
min_size,
max_size,
buffer,
pending_fullscreen: false,
csd_shadow_width: 0,
csd_shadow_buffer: SolidColorBuffer::new((0, 0), [0., 0., 0., 0.3]),
})))
inner: Rc::new(RefCell::new(TestWindowInner {
size,
requested_size: None,
min_size,
max_size,
buffer,
pending_fullscreen: false,
csd_shadow_width: 0,
csd_shadow_buffer: SolidColorBuffer::new((0, 0), [0., 0., 0., 0.3]),
})),
}
}
pub fn fixed_size(id: usize) -> Self {
@ -56,24 +61,24 @@ impl TestWindow {
}
pub fn set_min_size(&self, size: Size<i32, Logical>) {
self.0.borrow_mut().min_size = size;
self.inner.borrow_mut().min_size = size;
}
pub fn set_max_size(&self, size: Size<i32, Logical>) {
self.0.borrow_mut().max_size = size;
self.inner.borrow_mut().max_size = size;
}
pub fn set_color(&self, color: [f32; 4]) {
self.0.borrow_mut().buffer.set_color(color);
self.inner.borrow_mut().buffer.set_color(color);
}
pub fn set_csd_shadow_width(&self, width: i32) {
self.0.borrow_mut().csd_shadow_width = width;
self.inner.borrow_mut().csd_shadow_width = width;
}
pub fn communicate(&self) -> bool {
let mut rv = false;
let mut inner = self.0.borrow_mut();
let mut inner = self.inner.borrow_mut();
let mut new_size = inner.size;
@ -117,15 +122,15 @@ impl TestWindow {
}
}
impl PartialEq for TestWindow {
fn eq(&self, other: &Self) -> bool {
self.0.borrow().id == other.0.borrow().id
}
}
impl LayoutElement for TestWindow {
type Id = usize;
fn id(&self) -> &Self::Id {
&self.id
}
fn size(&self) -> Size<i32, Logical> {
self.0.borrow().size
self.inner.borrow().size
}
fn buf_loc(&self) -> Point<i32, Logical> {
@ -142,7 +147,7 @@ impl LayoutElement for TestWindow {
location: Point<i32, Logical>,
scale: Scale<f64>,
) -> Vec<LayoutElementRenderElement<R>> {
let inner = self.0.borrow();
let inner = self.inner.borrow();
vec![
SolidColorRenderElement::from_buffer(
@ -166,20 +171,20 @@ impl LayoutElement for TestWindow {
}
fn request_size(&self, size: Size<i32, Logical>) {
self.0.borrow_mut().requested_size = Some(size);
self.0.borrow_mut().pending_fullscreen = false;
self.inner.borrow_mut().requested_size = Some(size);
self.inner.borrow_mut().pending_fullscreen = false;
}
fn request_fullscreen(&self, _size: Size<i32, Logical>) {
self.0.borrow_mut().pending_fullscreen = true;
self.inner.borrow_mut().pending_fullscreen = true;
}
fn min_size(&self) -> Size<i32, Logical> {
self.0.borrow().min_size
self.inner.borrow().min_size
}
fn max_size(&self) -> Size<i32, Logical> {
self.0.borrow().max_size
self.inner.borrow().max_size
}
fn is_wl_surface(&self, _wl_surface: &WlSurface) -> bool {
@ -198,11 +203,24 @@ impl LayoutElement for TestWindow {
fn set_offscreen_element_id(&self, _id: Option<Id>) {}
fn set_activated(&self, _active: bool) {}
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
fn send_pending_configure(&self) {}
fn is_fullscreen(&self) -> bool {
false
}
fn is_pending_fullscreen(&self) -> bool {
self.0.borrow().pending_fullscreen
self.inner.borrow().pending_fullscreen
}
fn refresh(&self) {}
fn rules(&self) -> &ResolvedWindowRules {
static EMPTY: ResolvedWindowRules = ResolvedWindowRules::empty();
&EMPTY
}
}

View File

@ -73,6 +73,11 @@ input {
// Focus windows and outputs automatically when moving the mouse into them.
// focus-follows-mouse
// Uncomment this to enable workspace auto-back-and-forth.
// If enabled, switching to the same workspace by index twice will switch back to the
// previous workspace.
// workspace-auto-back-and-forth
}
// You can configure outputs by their name, which you can find
@ -118,6 +123,9 @@ layout {
// If you don't like that, you should uncomment `prefer-no-csd` below.
// Niri will draw focus ring and border *around* windows that agree to omit their
// client-side decorations.
//
// Alternatively, you can override it with a window rule called
// `draw-border-with-background`.
// You can change how the focus ring looks.
focus-ring {
@ -357,6 +365,8 @@ animations {
exclude app-id=r#"\.unwanted\."#
// Here are the properties that you can set on a window rule.
// These properties apply once, when a window first opens.
// You can override the default column width.
default-column-width { proportion 0.75; }
@ -372,6 +382,27 @@ animations {
open-fullscreen true
// You can also set this to false to prevent a window from opening fullscreen.
// open-fullscreen false
// The following properties apply dynamically while a window is open.
// You can amend the window's minimum and maximum size in logical pixels.
// Keep in mind that the window itself always has a final say in its size.
// These values instruct niri to never ask the window to be smaller than
// the minimum you set, or to be bigger than the maximum you set.
min-width 100
max-width 200
min-height 300
// Caveat: max-height will only apply to automatically-sized windows
// if it is equal to min-height. Either set this equal to min-height,
// or change the window height manually for this to apply.
max-height 300
// Override whether the border and the focus ring draw with a background.
// Set this to `true` to draw them as solid colored rectangles even for
// windows which agreed to omit their client-side decorations.
// Set this to `false` to draw them as borders around the window even for
// windows which use client-side decorations.
draw-border-with-background false
}
// Here's a useful example. Work around WezTerm's initial configure bug
@ -518,6 +549,9 @@ binds {
// Alternatively, there are commands to move just a single window:
// Mod+Ctrl+1 { move-window-to-workspace 1; }
// Switches focus between the current and the previous workspace.
// Mod+Tab { focus-workspace-previous; }
Mod+Comma { consume-window-into-column; }
Mod+Period { expel-window-from-column; }

View File

@ -17,8 +17,7 @@ use smithay::wayland::shm::{ShmHandler, ShmState};
use smithay::{delegate_compositor, delegate_shm};
use crate::niri::{ClientState, State};
use crate::utils::clone2;
use crate::window::{InitialConfigureState, Unmapped};
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
impl CompositorHandler for State {
fn compositor_state(&mut self) -> &mut CompositorState {
@ -109,22 +108,22 @@ impl CompositorHandler for State {
window.on_commit();
let (width, is_full_width, output) =
let (rules, width, is_full_width, output) =
if let InitialConfigureState::Configured {
rules,
width,
is_full_width,
output,
..
} = state
{
// Check that the output is still connected.
let output =
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
(width, is_full_width, output)
(rules, width, is_full_width, output)
} else {
error!("window map must happen after initial configure");
(None, false, None)
(ResolvedWindowRules::empty(), None, false, None)
};
let parent = window
@ -141,29 +140,30 @@ impl CompositorHandler for State {
.filter(|(_, parent_output)| {
output.is_none() || output.as_ref() == Some(*parent_output)
})
.map(|(window, _)| window.clone());
.map(|(mapped, _)| mapped.window.clone());
let window = window.clone();
let win = window.clone();
let mapped = Mapped::new(window, rules);
let window = mapped.window.clone();
let output = if let Some(p) = parent {
// Open dialogs immediately to the right of their parent window.
self.niri
.layout
.add_window_right_of(&p, win, width, is_full_width)
.add_window_right_of(&p, mapped, width, is_full_width)
} else if let Some(output) = &output {
self.niri
.layout
.add_window_on_output(output, win, width, is_full_width);
.add_window_on_output(output, mapped, width, is_full_width);
Some(output)
} else {
self.niri.layout.add_window(win, width, is_full_width)
self.niri.layout.add_window(mapped, width, is_full_width)
};
if let Some(output) = output.cloned() {
self.niri.layout.start_open_animation_for_window(&window);
let new_active_window = self.niri.layout.active_window().map(|(w, _)| w);
let new_active_window =
self.niri.layout.active_window().map(|(m, _)| &m.window);
if new_active_window == Some(&window) {
self.maybe_warp_cursor_to_focus();
}
@ -183,8 +183,9 @@ impl CompositorHandler for State {
}
// This is a commit of a previously-mapped root or a non-toplevel root.
if let Some(win_out) = self.niri.layout.find_window_and_output(surface) {
let (window, output) = clone2(win_out);
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(surface) {
let window = mapped.window.clone();
let output = output.clone();
window.on_commit();
@ -224,7 +225,9 @@ impl CompositorHandler for State {
// This is a commit of a non-root or a non-toplevel root.
let root_window_output = self.niri.layout.find_window_and_output(&root_surface);
if let Some((window, output)) = root_window_output.map(clone2) {
if let Some((mapped, output)) = root_window_output {
let window = mapped.window.clone();
let output = output.clone();
window.on_commit();
self.niri.layout.update_window(&window);
self.niri.queue_redraw(output);

View File

@ -144,7 +144,7 @@ impl InputMethodHandler for State {
self.niri
.layout
.find_window_and_output(parent)
.map(|(window, _)| window.geometry())
.map(|(mapped, _)| mapped.window.geometry())
.unwrap_or_default()
}
}
@ -333,25 +333,24 @@ impl ForeignToplevelHandler for State {
}
fn activate(&mut self, wl_surface: WlSurface) {
if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
let window = window.clone();
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
let window = mapped.window.clone();
self.niri.layout.activate_window(&window);
self.niri.queue_redraw_all();
}
}
fn close(&mut self, wl_surface: WlSurface) {
if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
window.toplevel().expect("no x11 support").send_close();
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
mapped.toplevel().send_close();
}
}
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>) {
if let Some((window, current_output)) = self.niri.layout.find_window_and_output(&wl_surface)
if let Some((mapped, current_output)) = self.niri.layout.find_window_and_output(&wl_surface)
{
if !window
if !mapped
.toplevel()
.expect("no x11 support")
.current_state()
.capabilities
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
@ -359,13 +358,13 @@ impl ForeignToplevelHandler for State {
return;
}
let window = window.clone();
let window = mapped.window.clone();
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
if &requested_output != current_output {
self.niri
.layout
.move_window_to_output(window.clone(), &requested_output);
.move_window_to_output(&window, &requested_output);
}
}
@ -374,8 +373,8 @@ impl ForeignToplevelHandler for State {
}
fn unset_fullscreen(&mut self, wl_surface: WlSurface) {
if let Some((window, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
let window = window.clone();
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
let window = mapped.window.clone();
self.niri.layout.set_fullscreen(&window, false);
}
}

View File

@ -1,4 +1,3 @@
use niri_config::{Match, WindowRule};
use smithay::desktop::{
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, LayerSurface,
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
@ -20,7 +19,7 @@ use smithay::wayland::shell::wlr_layer::Layer;
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
use smithay::wayland::shell::xdg::{
PopupSurface, PositionerState, ToplevelSurface, XdgPopupSurfaceData, XdgShellHandler,
XdgShellState, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
XdgShellState, XdgToplevelSurfaceData,
};
use smithay::wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState};
use smithay::{
@ -29,85 +28,8 @@ use smithay::{
use crate::layout::workspace::ColumnWidth;
use crate::niri::{PopupGrabState, State};
use crate::utils::clone2;
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped};
fn window_matches(role: &XdgToplevelSurfaceRoleAttributes, m: &Match) -> bool {
if let Some(app_id_re) = &m.app_id {
let Some(app_id) = &role.app_id else {
return false;
};
if !app_id_re.is_match(app_id) {
return false;
}
}
if let Some(title_re) = &m.title {
let Some(title) = &role.title else {
return false;
};
if !title_re.is_match(title) {
return false;
}
}
true
}
pub fn resolve_window_rules(
rules: &[WindowRule],
toplevel: &ToplevelSurface,
) -> ResolvedWindowRules {
let _span = tracy_client::span!("resolve_window_rules");
let mut resolved = ResolvedWindowRules::default();
with_states(toplevel.wl_surface(), |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
let mut open_on_output = None;
for rule in rules {
if !(rule.matches.is_empty() || rule.matches.iter().any(|m| window_matches(&role, m))) {
continue;
}
if rule.excludes.iter().any(|m| window_matches(&role, m)) {
continue;
}
if let Some(x) = rule
.default_column_width
.as_ref()
.map(|d| d.0.map(ColumnWidth::from))
{
resolved.default_width = Some(x);
}
if let Some(x) = rule.open_on_output.as_deref() {
open_on_output = Some(x);
}
if let Some(x) = rule.open_maximized {
resolved.open_maximized = Some(x);
}
if let Some(x) = rule.open_fullscreen {
resolved.open_fullscreen = Some(x);
}
}
resolved.open_on_output = open_on_output.map(|x| x.to_owned());
});
resolved
}
impl XdgShellHandler for State {
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
&mut self.niri.xdg_shell_state
@ -213,9 +135,7 @@ impl XdgShellHandler for State {
}
let layout_focus = self.niri.layout.focus();
if Some(&root)
!= layout_focus.map(|win| win.toplevel().expect("no x11 support").wl_surface())
{
if Some(&root) != layout_focus.map(|win| win.toplevel().wl_surface()) {
let _ = PopupManager::dismiss_popup(&root, &popup);
return;
}
@ -278,18 +198,18 @@ impl XdgShellHandler for State {
) {
let requested_output = wl_output.as_ref().and_then(Output::from_resource);
if let Some((window, current_output)) = self
if let Some((mapped, current_output)) = self
.niri
.layout
.find_window_and_output(toplevel.wl_surface())
{
let window = window.clone();
let window = mapped.window.clone();
if let Some(requested_output) = requested_output {
if &requested_output != current_output {
self.niri
.layout
.move_window_to_output(window.clone(), &requested_output);
.move_window_to_output(&window, &requested_output);
}
}
@ -358,12 +278,12 @@ impl XdgShellHandler for State {
}
fn unfullscreen_request(&mut self, toplevel: ToplevelSurface) {
if let Some((window, _)) = self
if let Some((mapped, _)) = self
.niri
.layout
.find_window_and_output(toplevel.wl_surface())
{
let window = window.clone();
let window = mapped.window.clone();
self.niri.layout.set_fullscreen(&window, false);
// A configure is required in response to this event regardless if there are pending
@ -453,14 +373,16 @@ impl XdgShellHandler for State {
.layout
.find_window_and_output(surface.wl_surface());
let Some((window, output)) = win_out.map(clone2) else {
let Some((mapped, output)) = win_out else {
// I have no idea how this can happen, but I saw it happen once, in a weird interaction
// involving laptop going to sleep and resuming.
error!("toplevel missing from both unmapped_windows and layout");
return;
};
let window = mapped.window.clone();
let output = output.clone();
let active_window = self.niri.layout.active_window().map(|(w, _)| w);
let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window);
let was_active = active_window == Some(&window);
self.niri.layout.remove_window(&window);
@ -575,7 +497,7 @@ impl State {
};
let config = self.niri.config.borrow();
let rules = resolve_window_rules(&config.window_rules, toplevel);
let rules = ResolvedWindowRules::compute(&config.window_rules, toplevel);
// Pick the target monitor. First, check if we had an output set in the window rules.
let mon = rules
@ -733,8 +655,8 @@ impl State {
};
// Figure out if the root is a window or a layer surface.
if let Some((window, output)) = self.niri.layout.find_window_and_output(&root) {
self.unconstrain_window_popup(popup, window, output);
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(&root) {
self.unconstrain_window_popup(popup, &mapped.window, output);
} else if let Some((layer_surface, output)) = self.niri.layout.outputs().find_map(|o| {
let map = layer_map_for_output(o);
let layer_surface = map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
@ -808,12 +730,30 @@ impl State {
}
pub fn update_window_rules(&mut self, toplevel: &ToplevelSurface) {
let resolve = || resolve_window_rules(&self.niri.config.borrow().window_rules, toplevel);
let resolve =
|| ResolvedWindowRules::compute(&self.niri.config.borrow().window_rules, toplevel);
if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
if let InitialConfigureState::Configured { rules, .. } = &mut unmapped.state {
*rules = resolve();
}
} else if let Some((mapped, output)) = self
.niri
.layout
.find_window_and_output_mut(toplevel.wl_surface())
{
let new_rules = resolve();
if mapped.rules != new_rules {
mapped.rules = new_rules;
let output = output.cloned();
let window = mapped.window.clone();
self.niri.layout.update_window(&window);
if let Some(output) = output {
self.niri.queue_redraw(output);
}
}
}
}
}

View File

@ -379,21 +379,24 @@ impl State {
}
Action::ScreenshotWindow => {
let active = self.niri.layout.active_window();
if let Some((window, output)) = active {
if let Some((mapped, output)) = active {
self.backend.with_primary_renderer(|renderer| {
if let Err(err) = self.niri.screenshot_window(renderer, output, window) {
if let Err(err) =
self.niri
.screenshot_window(renderer, output, &mapped.window)
{
warn!("error taking screenshot: {err:?}");
}
});
}
}
Action::CloseWindow => {
if let Some(window) = self.niri.layout.focus() {
window.toplevel().expect("no x11 support").send_close();
if let Some(mapped) = self.niri.layout.focus() {
mapped.toplevel().send_close();
}
}
Action::FullscreenWindow => {
let focus = self.niri.layout.focus().cloned();
let focus = self.niri.layout.focus().map(|m| m.window.clone());
if let Some(window) = focus {
self.niri.layout.toggle_fullscreen(&window);
// FIXME: granular
@ -569,11 +572,23 @@ impl State {
}
Action::FocusWorkspace(idx) => {
let idx = idx.saturating_sub(1) as usize;
self.niri.layout.switch_workspace(idx);
let config = &self.niri.config;
if config.borrow().input.workspace_auto_back_and_forth {
self.niri.layout.switch_workspace_auto_back_and_forth(idx);
} else {
self.niri.layout.switch_workspace(idx);
}
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWorkspacePrevious => {
self.niri.layout.switch_workspace_previous();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveWorkspaceDown => {
self.niri.layout.move_workspace_down();
// FIXME: granular
@ -1005,8 +1020,8 @@ impl State {
let button_state = event.state();
if ButtonState::Pressed == button_state {
if let Some(window) = self.niri.window_under_cursor() {
let window = window.clone();
if let Some(mapped) = self.niri.window_under_cursor() {
let window = mapped.window.clone();
self.niri.layout.activate_window(&window);
// FIXME: granular.
@ -1165,8 +1180,8 @@ impl State {
tool.tip_down(serial, event.time_msec());
if let Some(pos) = self.niri.tablet_cursor_location {
if let Some(window) = self.niri.window_under(pos) {
let window = window.clone();
if let Some(mapped) = self.niri.window_under(pos) {
let window = mapped.window.clone();
self.niri.layout.activate_window(&window);
// FIXME: granular.
@ -1523,8 +1538,8 @@ impl State {
.output_under(touch_location)
.next()
.cloned();
if let Some(window) = self.niri.window_under(touch_location) {
let window = window.clone();
if let Some(mapped) = self.niri.window_under(touch_location) {
let window = mapped.window.clone();
self.niri.layout.activate_window(&window);
// FIXME: granular.

View File

@ -38,24 +38,18 @@ use niri_config::{CenterFocusedColumn, Config, Struts};
use niri_ipc::SizeChange;
use smithay::backend::renderer::element::solid::SolidColorRenderElement;
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
use smithay::backend::renderer::element::{AsRenderElements, Id};
use smithay::desktop::space::SpaceElement;
use smithay::desktop::Window;
use smithay::backend::renderer::element::Id;
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_toplevel;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::shell::xdg::SurfaceCachedState;
use smithay::utils::{Logical, Point, Scale, Size, Transform};
use self::monitor::Monitor;
pub use self::monitor::MonitorRenderElement;
use self::workspace::{compute_working_area, Column, ColumnWidth, OutputId, Workspace};
use crate::niri::WindowOffscreenId;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::utils::output_size;
use crate::window::ResolvedWindowRules;
pub mod focus_ring;
pub mod monitor;
@ -69,7 +63,13 @@ niri_render_elements! {
}
}
pub trait LayoutElement: PartialEq {
pub trait LayoutElement {
/// Type that can be used as a unique ID of this element.
type Id: PartialEq;
/// Unique ID of this element.
fn id(&self) -> &Self::Id;
/// Visual size of the element.
///
/// This is what the user would consider the size, i.e. excluding CSD shadows and whatnot.
@ -107,6 +107,10 @@ pub trait LayoutElement: PartialEq {
fn output_enter(&self, output: &Output);
fn output_leave(&self, output: &Output);
fn set_offscreen_element_id(&self, id: Option<Id>);
fn set_activated(&self, active: bool);
fn set_bounds(&self, bounds: Size<i32, Logical>);
fn send_pending_configure(&self);
/// Whether the element is currently fullscreen.
///
@ -117,6 +121,11 @@ pub trait LayoutElement: PartialEq {
///
/// This *will* switch immediately after a [`LayoutElement::request_fullscreen()`] call.
fn is_pending_fullscreen(&self) -> bool;
fn rules(&self) -> &ResolvedWindowRules;
/// Runs periodic clean-up tasks.
fn refresh(&self);
}
#[derive(Debug)]
@ -216,120 +225,6 @@ impl Options {
}
}
impl LayoutElement for Window {
fn size(&self) -> Size<i32, Logical> {
self.geometry().size
}
fn buf_loc(&self) -> Point<i32, Logical> {
Point::from((0, 0)) - self.geometry().loc
}
fn is_in_input_region(&self, point: Point<f64, Logical>) -> bool {
let surface_local = point + self.geometry().loc.to_f64();
SpaceElement::is_in_input_region(self, &surface_local)
}
fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
) -> Vec<LayoutElementRenderElement<R>> {
let buf_pos = location - self.geometry().loc;
self.render_elements(
renderer,
buf_pos.to_physical_precise_round(scale),
scale,
1.,
)
}
fn request_size(&self, size: Size<i32, Logical>) {
self.toplevel()
.expect("no x11 support")
.with_pending_state(|state| {
state.size = Some(size);
state.states.unset(xdg_toplevel::State::Fullscreen);
});
}
fn request_fullscreen(&self, size: Size<i32, Logical>) {
self.toplevel()
.expect("no x11 support")
.with_pending_state(|state| {
state.size = Some(size);
state.states.set(xdg_toplevel::State::Fullscreen);
});
}
fn min_size(&self) -> Size<i32, Logical> {
with_states(
self.toplevel().expect("no x11 support").wl_surface(),
|state| {
let curr = state.cached_state.current::<SurfaceCachedState>();
curr.min_size
},
)
}
fn max_size(&self) -> Size<i32, Logical> {
with_states(
self.toplevel().expect("no x11 support").wl_surface(),
|state| {
let curr = state.cached_state.current::<SurfaceCachedState>();
curr.max_size
},
)
}
fn is_wl_surface(&self, wl_surface: &WlSurface) -> bool {
self.toplevel().expect("no x11 support").wl_surface() == wl_surface
}
fn set_preferred_scale_transform(&self, scale: i32, transform: Transform) {
self.with_surfaces(|surface, data| {
send_surface_state(surface, data, scale, transform);
});
}
fn has_ssd(&self) -> bool {
self.toplevel()
.expect("no x11 support")
.current_state()
.decoration_mode
== Some(zxdg_toplevel_decoration_v1::Mode::ServerSide)
}
fn output_enter(&self, output: &Output) {
let overlap = Rectangle::from_loc_and_size((0, 0), (i32::MAX, i32::MAX));
SpaceElement::output_enter(self, output, overlap)
}
fn output_leave(&self, output: &Output) {
SpaceElement::output_leave(self, output)
}
fn set_offscreen_element_id(&self, id: Option<Id>) {
let data = self.user_data().get_or_insert(WindowOffscreenId::default);
data.0.replace(id);
}
fn is_fullscreen(&self) -> bool {
self.toplevel()
.expect("no x11 support")
.current_state()
.states
.contains(xdg_toplevel::State::Fullscreen)
}
fn is_pending_fullscreen(&self) -> bool {
self.toplevel()
.expect("no x11 support")
.with_pending_state(|state| state.states.contains(xdg_toplevel::State::Fullscreen))
}
}
impl<W: LayoutElement> Layout<W> {
pub fn new(config: &Config) -> Self {
Self::with_options(Options::from_config(config))
@ -599,7 +494,7 @@ impl<W: LayoutElement> Layout<W> {
/// Returns an output that the window was added to, if there were any outputs.
pub fn add_window_right_of(
&mut self,
right_of: &W,
right_of: &W::Id,
window: W,
width: Option<ColumnWidth>,
is_full_width: bool,
@ -681,13 +576,15 @@ impl<W: LayoutElement> Layout<W> {
);
}
pub fn remove_window(&mut self, window: &W) {
pub fn remove_window(&mut self, window: &W::Id) -> Option<W> {
let mut rv = None;
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for (idx, ws) in mon.workspaces.iter_mut().enumerate() {
if ws.has_window(window) {
ws.remove_window(window);
rv = Some(ws.remove_window(window));
// Clean up empty workspaces that are not active and not last.
if !ws.has_windows()
@ -710,7 +607,7 @@ impl<W: LayoutElement> Layout<W> {
MonitorSet::NoOutputs { workspaces, .. } => {
for (idx, ws) in workspaces.iter_mut().enumerate() {
if ws.has_window(window) {
ws.remove_window(window);
rv = Some(ws.remove_window(window));
// Clean up empty workspaces.
if !ws.has_windows() {
@ -722,9 +619,11 @@ impl<W: LayoutElement> Layout<W> {
}
}
}
rv
}
pub fn update_window(&mut self, window: &W) {
pub fn update_window(&mut self, window: &W::Id) {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
@ -761,7 +660,33 @@ impl<W: LayoutElement> Layout<W> {
None
}
pub fn window_y(&self, window: &W) -> Option<i32> {
pub fn find_window_and_output_mut(
&mut self,
wl_surface: &WlSurface,
) -> Option<(&mut W, Option<&Output>)> {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
if let Some(window) = ws.find_wl_surface_mut(wl_surface) {
return Some((window, Some(&mon.output)));
}
}
}
}
MonitorSet::NoOutputs { workspaces } => {
for ws in workspaces {
if let Some(window) = ws.find_wl_surface_mut(wl_surface) {
return Some((window, None));
}
}
}
}
None
}
pub fn window_y(&self, window: &W::Id) -> Option<i32> {
match &self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
@ -810,7 +735,7 @@ impl<W: LayoutElement> Layout<W> {
}
}
pub fn activate_window(&mut self, window: &W) {
pub fn activate_window(&mut self, window: &W::Id) {
let MonitorSet::Normal {
monitors,
active_monitor_idx,
@ -932,6 +857,27 @@ impl<W: LayoutElement> Layout<W> {
}
}
pub fn with_windows_mut(&mut self, mut f: impl FnMut(&mut W, Option<&Output>)) {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
for win in ws.windows_mut() {
f(win, Some(&mon.output));
}
}
}
}
MonitorSet::NoOutputs { workspaces } => {
for ws in workspaces {
for win in ws.windows_mut() {
f(win, None);
}
}
}
}
}
fn active_monitor(&mut self) -> Option<&mut Monitor<W>> {
let MonitorSet::Normal {
monitors,
@ -1165,6 +1111,20 @@ impl<W: LayoutElement> Layout<W> {
monitor.switch_workspace(idx);
}
pub fn switch_workspace_auto_back_and_forth(&mut self, idx: usize) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.switch_workspace_auto_back_and_forth(idx);
}
pub fn switch_workspace_previous(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.switch_workspace_previous();
}
pub fn consume_into_column(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
@ -1221,8 +1181,12 @@ impl<W: LayoutElement> Layout<W> {
#[cfg(test)]
fn verify_invariants(&self) {
use std::collections::HashSet;
use crate::layout::monitor::WorkspaceSwitch;
let mut seen_workspace_id = HashSet::new();
let (monitors, &primary_idx, &active_monitor_idx) = match &self.monitor_set {
MonitorSet::Normal {
monitors,
@ -1241,6 +1205,11 @@ impl<W: LayoutElement> Layout<W> {
"workspace options must be synchronized with layout"
);
assert!(
seen_workspace_id.insert(workspace.id()),
"workspace id must be unique"
);
workspace.verify_invariants();
}
@ -1325,6 +1294,11 @@ impl<W: LayoutElement> Layout<W> {
"workspace options must be synchronized with layout"
);
assert!(
seen_workspace_id.insert(workspace.id()),
"workspace id must be unique"
);
workspace.verify_invariants();
}
}
@ -1465,7 +1439,7 @@ impl<W: LayoutElement> Layout<W> {
}
}
pub fn move_window_to_output(&mut self, window: W, output: &Output) {
pub fn move_window_to_output(&mut self, window: &W::Id, output: &Output) {
let mut width = None;
let mut is_full_width = false;
@ -1473,7 +1447,7 @@ impl<W: LayoutElement> Layout<W> {
for mon in &*monitors {
for ws in &mon.workspaces {
for col in &ws.columns {
if col.contains(&window) {
if col.contains(window) {
width = Some(col.width);
is_full_width = col.is_full_width;
break;
@ -1485,7 +1459,7 @@ impl<W: LayoutElement> Layout<W> {
let Some(width) = width else { return };
self.remove_window(&window);
let window = self.remove_window(window).unwrap();
if let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set {
let new_idx = monitors
@ -1529,6 +1503,8 @@ impl<W: LayoutElement> Layout<W> {
.unwrap();
let target = &mut monitors[target_idx];
target.previous_workspace_id = Some(target.workspaces[target.active_workspace_idx].id());
// Insert the workspace after the currently active one. Unless the currently active one is
// the last empty workspace, then insert before.
let target_ws_idx = min(target.active_workspace_idx + 1, target.workspaces.len() - 1);
@ -1540,7 +1516,7 @@ impl<W: LayoutElement> Layout<W> {
*active_monitor_idx = target_idx;
}
pub fn set_fullscreen(&mut self, window: &W, is_fullscreen: bool) {
pub fn set_fullscreen(&mut self, window: &W::Id, is_fullscreen: bool) {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
@ -1563,7 +1539,7 @@ impl<W: LayoutElement> Layout<W> {
}
}
pub fn toggle_fullscreen(&mut self, window: &W) {
pub fn toggle_fullscreen(&mut self, window: &W::Id) {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
@ -1716,14 +1692,14 @@ impl<W: LayoutElement> Layout<W> {
monitor.move_workspace_up();
}
pub fn start_open_animation_for_window(&mut self, window: &W) {
pub fn start_open_animation_for_window(&mut self, window: &W::Id) {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
for col in &mut ws.columns {
for tile in &mut col.tiles {
if tile.window() == window {
if tile.window().id() == window {
tile.start_open_animation();
return;
}
@ -1734,13 +1710,11 @@ impl<W: LayoutElement> Layout<W> {
}
MonitorSet::NoOutputs { workspaces, .. } => {
for ws in workspaces {
if ws.has_window(window) {
for col in &mut ws.columns {
for tile in &mut col.tiles {
if tile.window() == window {
tile.start_open_animation();
return;
}
for col in &mut ws.columns {
for tile in &mut col.tiles {
if tile.window().id() == window {
tile.start_open_animation();
return;
}
}
}
@ -1748,11 +1722,9 @@ impl<W: LayoutElement> Layout<W> {
}
}
}
}
impl Layout<Window> {
pub fn refresh(&mut self) {
let _span = tracy_client::span!("MonitorSet::refresh");
let _span = tracy_client::span!("Layout::refresh");
match &mut self.monitor_set {
MonitorSet::Normal {
@ -1795,6 +1767,7 @@ mod tests {
use proptest::prelude::*;
use proptest_derive::Arbitrary;
use smithay::output::{Mode, PhysicalProperties, Subpixel};
use smithay::utils::Rectangle;
use super::*;
@ -1859,13 +1832,13 @@ mod tests {
}
}
impl PartialEq for TestWindow {
fn eq(&self, other: &Self) -> bool {
self.0.id == other.0.id
}
}
impl LayoutElement for TestWindow {
type Id = usize;
fn id(&self) -> &Self::Id {
&self.0.id
}
fn size(&self) -> Size<i32, Logical> {
self.0.bbox.get().size
}
@ -1920,6 +1893,12 @@ mod tests {
fn set_offscreen_element_id(&self, _id: Option<Id>) {}
fn set_activated(&self, _active: bool) {}
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
fn send_pending_configure(&self) {}
fn is_fullscreen(&self) -> bool {
false
}
@ -1927,6 +1906,13 @@ mod tests {
fn is_pending_fullscreen(&self) -> bool {
self.0.pending_fullscreen.get()
}
fn refresh(&self) {}
fn rules(&self) -> &ResolvedWindowRules {
static EMPTY: ResolvedWindowRules = ResolvedWindowRules::empty();
&EMPTY
}
}
fn arbitrary_bbox() -> impl Strategy<Value = Rectangle<i32, Logical>> {
@ -2017,6 +2003,8 @@ mod tests {
FocusWorkspaceDown,
FocusWorkspaceUp,
FocusWorkspace(#[proptest(strategy = "0..=4usize")] usize),
FocusWorkspaceAutoBackAndForth(#[proptest(strategy = "0..=4usize")] usize),
FocusWorkspacePrevious,
MoveWindowToWorkspaceDown,
MoveWindowToWorkspaceUp,
MoveWindowToWorkspace(#[proptest(strategy = "0..=4usize")] usize),
@ -2176,24 +2164,14 @@ mod tests {
return;
}
let right_of_win = TestWindow::new(
right_of_id,
Rectangle::default(),
Size::default(),
Size::default(),
);
let win = TestWindow::new(id, bbox, min_max_size.0, min_max_size.1);
layout.add_window_right_of(&right_of_win, win, None, false);
layout.add_window_right_of(&right_of_id, win, None, false);
}
Op::CloseWindow(id) => {
let dummy =
TestWindow::new(id, Rectangle::default(), Size::default(), Size::default());
layout.remove_window(&dummy);
layout.remove_window(&id);
}
Op::FullscreenWindow(id) => {
let dummy =
TestWindow::new(id, Rectangle::default(), Size::default(), Size::default());
layout.toggle_fullscreen(&dummy);
layout.toggle_fullscreen(&id);
}
Op::FocusColumnLeft => layout.focus_left(),
Op::FocusColumnRight => layout.focus_right(),
@ -2219,6 +2197,10 @@ mod tests {
Op::FocusWorkspaceDown => layout.switch_workspace_down(),
Op::FocusWorkspaceUp => layout.switch_workspace_up(),
Op::FocusWorkspace(idx) => layout.switch_workspace(idx),
Op::FocusWorkspaceAutoBackAndForth(idx) => {
layout.switch_workspace_auto_back_and_forth(idx)
}
Op::FocusWorkspacePrevious => layout.switch_workspace_previous(),
Op::MoveWindowToWorkspaceDown => layout.move_to_workspace_down(),
Op::MoveWindowToWorkspaceUp => layout.move_to_workspace_up(),
Op::MoveWindowToWorkspace(idx) => layout.move_to_workspace(idx),
@ -2248,7 +2230,7 @@ mod tests {
Op::SetColumnWidth(change) => layout.set_column_width(change),
Op::SetWindowHeight(change) => layout.set_window_height(change),
Op::Communicate(id) => {
let mut window = None;
let mut update = false;
match &mut layout.monitor_set {
MonitorSet::Normal { monitors, .. } => {
'outer: for mon in monitors {
@ -2256,7 +2238,7 @@ mod tests {
for win in ws.windows() {
if win.0.id == id {
if win.communicate() {
window = Some(win.clone());
update = true;
}
break 'outer;
}
@ -2269,7 +2251,7 @@ mod tests {
for win in ws.windows() {
if win.0.id == id {
if win.communicate() {
window = Some(win.clone());
update = true;
}
break 'outer;
}
@ -2278,8 +2260,8 @@ mod tests {
}
}
if let Some(win) = window {
layout.update_window(&win);
if update {
layout.update_window(&id);
}
}
Op::MoveWorkspaceToOutput(id) => {

View File

@ -10,7 +10,8 @@ use smithay::output::Output;
use smithay::utils::{Logical, Point, Rectangle, Scale};
use super::workspace::{
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceRenderElement,
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceId,
WorkspaceRenderElement,
};
use super::{LayoutElement, Options};
use crate::animation::Animation;
@ -35,6 +36,8 @@ pub struct Monitor<W: LayoutElement> {
pub workspaces: Vec<Workspace<W>>,
/// Index of the currently active workspace.
pub active_workspace_idx: usize,
/// ID of the previously active workspace.
pub previous_workspace_id: Option<WorkspaceId>,
/// In-progress switch between workspaces.
pub workspace_switch: Option<WorkspaceSwitch>,
/// Configurable properties of the layout.
@ -89,6 +92,7 @@ impl<W: LayoutElement> Monitor<W> {
output,
workspaces,
active_workspace_idx: 0,
previous_workspace_id: None,
workspace_switch: None,
options,
}
@ -114,6 +118,8 @@ impl<W: LayoutElement> Monitor<W> {
.map(|s| s.current_idx())
.unwrap_or(self.active_workspace_idx as f64);
self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id());
self.active_workspace_idx = idx;
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
@ -153,7 +159,7 @@ impl<W: LayoutElement> Monitor<W> {
pub fn add_window_right_of(
&mut self,
right_of: &W,
right_of: &W::Id,
window: W,
width: ColumnWidth,
is_full_width: bool,
@ -461,6 +467,11 @@ impl<W: LayoutElement> Monitor<W> {
));
}
fn previous_workspace_idx(&self) -> Option<usize> {
let id = self.previous_workspace_id?;
self.workspaces.iter().position(|w| w.id() == id)
}
pub fn switch_workspace(&mut self, idx: usize) {
self.activate_workspace(min(idx, self.workspaces.len() - 1));
// Don't animate this action.
@ -469,6 +480,24 @@ impl<W: LayoutElement> Monitor<W> {
self.clean_up_workspaces();
}
pub fn switch_workspace_auto_back_and_forth(&mut self, idx: usize) {
let idx = min(idx, self.workspaces.len() - 1);
if idx == self.active_workspace_idx {
if let Some(prev_idx) = self.previous_workspace_idx() {
self.switch_workspace(prev_idx);
}
} else {
self.switch_workspace(idx);
}
}
pub fn switch_workspace_previous(&mut self) {
if let Some(idx) = self.previous_workspace_idx() {
self.switch_workspace(idx);
}
}
pub fn consume_into_column(&mut self) {
self.active_workspace().consume_into_column();
}
@ -564,8 +593,10 @@ impl<W: LayoutElement> Monitor<W> {
self.workspaces.push(ws);
}
let previous_workspace_id = self.previous_workspace_id;
self.activate_workspace(new_idx);
self.workspace_switch = None;
self.previous_workspace_id = previous_workspace_id;
self.clean_up_workspaces();
}
@ -584,8 +615,10 @@ impl<W: LayoutElement> Monitor<W> {
self.workspaces.push(ws);
}
let previous_workspace_id = self.previous_workspace_id;
self.activate_workspace(new_idx);
self.workspace_switch = None;
self.previous_workspace_id = previous_workspace_id;
self.clean_up_workspaces();
}
@ -833,6 +866,8 @@ impl<W: LayoutElement> Monitor<W> {
gesture.center_idx as f64 + current_pos,
);
self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id());
self.active_workspace_idx = new_idx;
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
gesture.current_idx,

View File

@ -85,11 +85,22 @@ impl<W: LayoutElement> Tile<W> {
}
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
let draw_border_with_background = self
.window
.rules()
.draw_border_with_background
.unwrap_or_else(|| !self.window.has_ssd());
self.border
.update(self.window.size(), self.window.has_ssd());
.update(self.window.size(), !draw_border_with_background);
self.border.set_active(is_active);
self.focus_ring.update(self.tile_size(), self.has_ssd());
let draw_focus_ring_with_background = if self.effective_border_width().is_some() {
false
} else {
draw_border_with_background
};
self.focus_ring
.update(self.tile_size(), !draw_focus_ring_with_background);
self.focus_ring.set_active(is_active);
match &mut self.open_animation {
@ -121,6 +132,10 @@ impl<W: LayoutElement> Tile<W> {
&self.window
}
pub fn window_mut(&mut self) -> &mut W {
&mut self.window
}
pub fn into_window(self) -> W {
self.window
}
@ -291,8 +306,15 @@ impl<W: LayoutElement> Tile<W> {
size
}
pub fn has_ssd(&self) -> bool {
self.effective_border_width().is_some() || self.window.has_ssd()
pub fn draw_border_with_background(&self) -> bool {
if self.effective_border_width().is_some() {
return false;
}
self.window
.rules()
.draw_border_with_background
.unwrap_or_else(|| !self.window.has_ssd())
}
fn render_inner<R: NiriRenderer>(

View File

@ -5,12 +5,12 @@ use std::time::Duration;
use niri_config::{CenterFocusedColumn, PresetWidth, Struts};
use niri_ipc::SizeChange;
use smithay::desktop::space::SpaceElement;
use smithay::desktop::{layer_map_for_output, Window};
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use smithay::wayland::compositor::send_surface_state;
use super::tile::{Tile, TileRenderElement};
use super::{LayoutElement, Options};
@ -18,6 +18,7 @@ use crate::animation::Animation;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::swipe_tracker::SwipeTracker;
use crate::utils::id::IdCounter;
use crate::utils::output_size;
/// Amount of touchpad movement to scroll the view for the width of one working area.
@ -76,11 +77,25 @@ pub struct Workspace<W: LayoutElement> {
/// Configurable properties of the layout.
pub options: Rc<Options>,
/// Unique ID of this workspace.
id: WorkspaceId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutputId(String);
static WORKSPACE_ID_COUNTER: IdCounter = IdCounter::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WorkspaceId(u32);
impl WorkspaceId {
fn next() -> WorkspaceId {
WorkspaceId(WORKSPACE_ID_COUNTER.next())
}
}
niri_render_elements! {
WorkspaceRenderElement<R> => {
Tile = TileRenderElement<R>,
@ -225,6 +240,7 @@ impl<W: LayoutElement> Workspace<W> {
view_offset_adj: None,
activate_prev_column_on_removal: None,
options,
id: WorkspaceId::next(),
}
}
@ -240,9 +256,14 @@ impl<W: LayoutElement> Workspace<W> {
view_offset_adj: None,
activate_prev_column_on_removal: None,
options,
id: WorkspaceId::next(),
}
}
pub fn id(&self) -> WorkspaceId {
self.id
}
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
if let Some(ViewOffsetAdjustment::Animation(anim)) = &mut self.view_offset_adj {
anim.set_current_time(current_time);
@ -279,6 +300,13 @@ impl<W: LayoutElement> Workspace<W> {
.map(Tile::window)
}
pub fn windows_mut(&mut self) -> impl Iterator<Item = &mut W> + '_ {
self.columns
.iter_mut()
.flat_map(|col| col.tiles.iter_mut())
.map(Tile::window_mut)
}
pub fn set_output(&mut self, output: Option<Output>) {
if self.output == output {
return;
@ -387,7 +415,11 @@ impl<W: LayoutElement> Workspace<W> {
pub fn configure_new_window(&self, window: &Window, width: Option<ColumnWidth>) {
if let Some(output) = self.output.as_ref() {
set_preferred_scale_transform(window, output);
let scale = output.current_scale().integer_scale();
let transform = output.current_transform();
window.with_surfaces(|surface, data| {
send_surface_state(surface, data, scale, transform);
});
}
window
@ -556,14 +588,18 @@ impl<W: LayoutElement> Workspace<W> {
self.windows().next().is_some()
}
pub fn has_window(&self, window: &W) -> bool {
self.windows().any(|win| win == window)
pub fn has_window(&self, window: &W::Id) -> bool {
self.windows().any(|win| win.id() == window)
}
pub fn find_wl_surface(&self, wl_surface: &WlSurface) -> Option<&W> {
self.windows().find(|win| win.is_wl_surface(wl_surface))
}
pub fn find_wl_surface_mut(&mut self, wl_surface: &WlSurface) -> Option<&mut W> {
self.windows_mut().find(|win| win.is_wl_surface(wl_surface))
}
/// Computes the X position of the windows in the given column, in logical coordinates.
fn column_x(&self, column_idx: usize) -> i32 {
let mut x = 0;
@ -637,7 +673,7 @@ impl<W: LayoutElement> Workspace<W> {
pub fn add_window_right_of(
&mut self,
right_of: &W,
right_of: &W::Id,
window: W,
width: ColumnWidth,
is_full_width: bool,
@ -814,7 +850,7 @@ impl<W: LayoutElement> Workspace<W> {
column
}
pub fn remove_window(&mut self, window: &W) {
pub fn remove_window(&mut self, window: &W::Id) -> W {
let column_idx = self
.columns
.iter()
@ -823,10 +859,10 @@ impl<W: LayoutElement> Workspace<W> {
let column = &self.columns[column_idx];
let window_idx = column.position(window).unwrap();
self.remove_window_by_idx(column_idx, window_idx);
self.remove_window_by_idx(column_idx, window_idx)
}
pub fn update_window(&mut self, window: &W) {
pub fn update_window(&mut self, window: &W::Id) {
let (idx, column) = self
.columns
.iter_mut()
@ -848,7 +884,7 @@ impl<W: LayoutElement> Workspace<W> {
}
}
pub fn activate_window(&mut self, window: &W) {
pub fn activate_window(&mut self, window: &W::Id) {
let column_idx = self
.columns
.iter()
@ -1212,7 +1248,7 @@ impl<W: LayoutElement> Workspace<W> {
self.columns[self.active_column_idx].set_window_height(change);
}
pub fn set_fullscreen(&mut self, window: &W, is_fullscreen: bool) {
pub fn set_fullscreen(&mut self, window: &W::Id, is_fullscreen: bool) {
let (mut col_idx, tile_idx) = self
.columns
.iter()
@ -1254,7 +1290,7 @@ impl<W: LayoutElement> Workspace<W> {
col.set_fullscreen(is_fullscreen);
}
pub fn toggle_fullscreen(&mut self, window: &W) {
pub fn toggle_fullscreen(&mut self, window: &W::Id) {
let col = self
.columns
.iter_mut()
@ -1513,9 +1549,7 @@ impl<W: LayoutElement> Workspace<W> {
true
}
}
impl Workspace<Window> {
pub fn refresh(&self, is_active: bool) {
let bounds = self.toplevel_bounds();
@ -1527,15 +1561,8 @@ impl Workspace<Window> {
&& col.active_tile_idx == tile_idx;
win.set_activated(active);
win.toplevel()
.expect("no x11 support")
.with_pending_state(|state| {
state.bounds = Some(bounds);
});
win.toplevel()
.expect("no x11 support")
.send_pending_configure();
win.set_bounds(bounds);
win.send_pending_configure();
win.refresh();
}
}
@ -1633,18 +1660,21 @@ impl<W: LayoutElement> Column<W> {
self.tiles.iter().any(Tile::are_animations_ongoing)
}
pub fn contains(&self, window: &W) -> bool {
self.tiles.iter().map(Tile::window).any(|win| win == window)
}
pub fn position(&self, window: &W) -> Option<usize> {
pub fn contains(&self, window: &W::Id) -> bool {
self.tiles
.iter()
.map(Tile::window)
.position(|win| win == window)
.any(|win| win.id() == window)
}
fn activate_window(&mut self, window: &W) {
pub fn position(&self, window: &W::Id) -> Option<usize> {
self.tiles
.iter()
.map(Tile::window)
.position(|win| win.id() == window)
}
fn activate_window(&mut self, window: &W::Id) {
let idx = self.position(window).unwrap();
self.active_tile_idx = idx;
}
@ -1657,11 +1687,11 @@ impl<W: LayoutElement> Column<W> {
self.update_tile_sizes();
}
fn update_window(&mut self, window: &W) {
fn update_window(&mut self, window: &W::Id) {
let tile = self
.tiles
.iter_mut()
.find(|tile| tile.window() == window)
.find(|tile| tile.window().id() == window)
.unwrap();
tile.update_window();
}

View File

@ -114,7 +114,7 @@ use crate::utils::spawning::CHILD_ENV;
use crate::utils::{
center, center_f64, get_monotonic_time, make_screenshot_path, output_size, write_png_rgba8,
};
use crate::window::Unmapped;
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
use crate::{animation, niri_render_elements};
const CLEAR_COLOR: [f32; 4] = [0.2, 0.2, 0.2, 1.];
@ -138,7 +138,7 @@ pub struct Niri {
// Each workspace corresponds to a Space. Each workspace generally has one Output mapped to it,
// however it may have none (when there are no outputs connected) or mutiple (when mirroring).
pub layout: Layout<Window>,
pub layout: Layout<Mapped>,
// This space does not actually contain any windows, but all outputs are mapped into it
// according to their global position.
@ -631,7 +631,7 @@ impl State {
self.niri
.layout
.focus()
.map(|win| win.toplevel().expect("no x11 support").wl_surface().clone())
.map(|win| win.toplevel().wl_surface().clone())
.map(|surface| KeyboardFocus::Layout {
surface: Some(surface),
})
@ -761,6 +761,7 @@ impl State {
let mut reload_xkb = None;
let mut libinput_config_changed = false;
let mut output_config_changed = false;
let mut window_rules_changed = false;
let mut old_config = self.niri.config.borrow_mut();
// Reload the cursor.
@ -802,6 +803,10 @@ impl State {
self.niri.hotkey_overlay.on_hotkey_config_updated();
}
if config.window_rules != old_config.window_rules {
window_rules_changed = true;
}
*old_config = config;
// Release the borrow.
@ -865,6 +870,30 @@ impl State {
}
}
if window_rules_changed {
let _span = tracy_client::span!("recompute window rules");
let window_rules = &self.niri.config.borrow().window_rules;
for unmapped in self.niri.unmapped_windows.values_mut() {
if let InitialConfigureState::Configured { rules, .. } = &mut unmapped.state {
*rules = ResolvedWindowRules::compute(
window_rules,
unmapped.window.toplevel().expect("no X11 support"),
);
}
}
let mut windows = vec![];
self.niri.layout.with_windows_mut(|mapped, _| {
mapped.rules = ResolvedWindowRules::compute(window_rules, mapped.toplevel());
windows.push(mapped.window.clone());
});
for win in windows {
self.niri.layout.update_window(&win);
}
}
// Can't really update xdg-decoration settings since we have to hide the globals for CSD
// due to the SDL2 bug... I don't imagine clients are prepared for the xdg-decoration
// global suddenly appearing? Either way, right now it's live-reloaded in a sense that new
@ -1591,7 +1620,7 @@ impl Niri {
///
/// The cursor may be inside the window's activation region, but not within the window's input
/// region.
pub fn window_under(&self, pos: Point<f64, Logical>) -> Option<&Window> {
pub fn window_under(&self, pos: Point<f64, Logical>) -> Option<&Mapped> {
if self.is_locked() || self.screenshot_ui.is_open() {
return None;
}
@ -1618,7 +1647,7 @@ impl Niri {
///
/// The cursor may be inside the window's activation region, but not within the window's input
/// region.
pub fn window_under_cursor(&self) -> Option<&Window> {
pub fn window_under_cursor(&self) -> Option<&Mapped> {
let pos = self.seat.get_pointer().unwrap().current_location();
self.window_under(pos)
}
@ -1684,8 +1713,9 @@ impl Niri {
let window_under = || {
self.layout
.window_under(output, pos_within_output)
.and_then(|(window, win_pos_within_output)| {
.and_then(|(mapped, win_pos_within_output)| {
let win_pos_within_output = win_pos_within_output?;
let window = &mapped.window;
window
.surface_under(
pos_within_output - win_pos_within_output.to_f64(),
@ -2409,7 +2439,8 @@ impl Niri {
// The reason to do this at all is that it keeps track of whether the surface is visible or
// not in a unified way with the pointer surfaces, which makes the logic elsewhere simpler.
for win in self.layout.windows_for_output(output) {
for mapped in self.layout.windows_for_output(output) {
let win = &mapped.window;
let offscreen_id = win
.user_data()
.get_or_insert(WindowOffscreenId::default)
@ -2480,8 +2511,8 @@ impl Niri {
// We can unconditionally send the current output's feedback to regular and layer-shell
// surfaces, as they can only be displayed on a single output at a time. Even if a surface
// is currently invisible, this is the DMABUF feedback that it should know about.
for win in self.layout.windows_for_output(output) {
win.send_dmabuf_feedback(
for mapped in self.layout.windows_for_output(output) {
mapped.window.send_dmabuf_feedback(
output,
|_, _| Some(output.clone()),
|surface, _| {
@ -2600,8 +2631,8 @@ impl Niri {
let frame_callback_time = get_monotonic_time();
for win in self.layout.windows_for_output(output) {
win.send_frame(
for mapped in self.layout.windows_for_output(output) {
mapped.window.send_frame(
output,
frame_callback_time,
FRAME_CALLBACK_THROTTLE,
@ -2666,8 +2697,8 @@ impl Niri {
let frame_callback_time = get_monotonic_time();
self.layout.with_windows(|win, _| {
win.send_frame(
self.layout.with_windows(|mapped, _| {
mapped.window.send_frame(
output,
frame_callback_time,
FRAME_CALLBACK_THROTTLE,
@ -2746,8 +2777,8 @@ impl Niri {
);
}
for win in self.layout.windows_for_output(output) {
win.take_presentation_feedback(
for mapped in self.layout.windows_for_output(output) {
mapped.window.take_presentation_feedback(
&mut feedback,
surface_primary_scanout_output,
|surface, _| {

View File

@ -95,8 +95,8 @@ pub fn refresh(state: &mut State) {
// Save the focused window for last, this way when the focus changes, we will first deactivate
// the previous window and only then activate the newly focused window.
let mut focused = None;
state.niri.layout.with_windows(|window, output| {
let wl_surface = window.toplevel().expect("no x11 support").wl_surface();
state.niri.layout.with_windows(|mapped, output| {
let wl_surface = mapped.toplevel().wl_surface();
with_states(wl_surface, |states| {
let role = states
@ -107,7 +107,7 @@ pub fn refresh(state: &mut State) {
.unwrap();
if state.niri.keyboard_focus.surface() == Some(wl_surface) {
focused = Some((window.clone(), output.cloned()));
focused = Some((mapped.window.clone(), output.cloned()));
} else {
refresh_toplevel(protocol_state, wl_surface, &role, output, false);
}

27
src/utils/id.rs Normal file
View File

@ -0,0 +1,27 @@
use std::sync::atomic::{AtomicU32, Ordering};
/// Counter that returns unique IDs.
///
/// Under the hood it uses a `u32` that will eventually wrap around. When incrementing it once a
/// second, it will wrap around after about 136 years.
pub struct IdCounter {
value: AtomicU32,
}
impl IdCounter {
pub const fn new() -> Self {
Self {
value: AtomicU32::new(0),
}
}
pub fn next(&self) -> u32 {
self.value.fetch_add(1, Ordering::SeqCst)
}
}
impl Default for IdCounter {
fn default() -> Self {
Self::new()
}
}

View File

@ -14,15 +14,12 @@ use smithay::output::Output;
use smithay::reexports::rustix::time::{clock_gettime, ClockId};
use smithay::utils::{Logical, Point, Rectangle, Size};
pub mod id;
pub mod spawning;
pub mod watcher;
pub static IS_SYSTEMD_SERVICE: AtomicBool = AtomicBool::new(false);
pub fn clone2<T: Clone, U: Clone>(t: (&T, &U)) -> (T, U) {
(t.0.clone(), t.1.clone())
}
pub fn version() -> String {
format!(
"{} ({})",

191
src/window/mapped.rs Normal file
View File

@ -0,0 +1,191 @@
use std::cmp::{max, min};
use smithay::backend::renderer::element::{AsRenderElements as _, Id};
use smithay::desktop::space::SpaceElement as _;
use smithay::desktop::Window;
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_toplevel;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::shell::xdg::{SurfaceCachedState, ToplevelSurface};
use super::ResolvedWindowRules;
use crate::layout::{LayoutElement, LayoutElementRenderElement};
use crate::niri::WindowOffscreenId;
use crate::render_helpers::renderer::NiriRenderer;
#[derive(Debug)]
pub struct Mapped {
pub window: Window,
/// Up-to-date rules.
pub rules: ResolvedWindowRules,
}
impl Mapped {
pub fn new(window: Window, rules: ResolvedWindowRules) -> Self {
Self { window, rules }
}
pub fn toplevel(&self) -> &ToplevelSurface {
self.window.toplevel().expect("no X11 support")
}
}
impl LayoutElement for Mapped {
type Id = Window;
fn id(&self) -> &Self::Id {
&self.window
}
fn size(&self) -> Size<i32, Logical> {
self.window.geometry().size
}
fn buf_loc(&self) -> Point<i32, Logical> {
Point::from((0, 0)) - self.window.geometry().loc
}
fn is_in_input_region(&self, point: Point<f64, Logical>) -> bool {
let surface_local = point + self.window.geometry().loc.to_f64();
self.window.is_in_input_region(&surface_local)
}
fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
) -> Vec<LayoutElementRenderElement<R>> {
let buf_pos = location - self.window.geometry().loc;
self.window.render_elements(
renderer,
buf_pos.to_physical_precise_round(scale),
scale,
1.,
)
}
fn request_size(&self, size: Size<i32, Logical>) {
self.toplevel().with_pending_state(|state| {
state.size = Some(size);
state.states.unset(xdg_toplevel::State::Fullscreen);
});
}
fn request_fullscreen(&self, size: Size<i32, Logical>) {
self.toplevel().with_pending_state(|state| {
state.size = Some(size);
state.states.set(xdg_toplevel::State::Fullscreen);
});
}
fn min_size(&self) -> Size<i32, Logical> {
let mut size = with_states(self.toplevel().wl_surface(), |state| {
let curr = state.cached_state.current::<SurfaceCachedState>();
curr.min_size
});
if let Some(x) = self.rules.min_width {
size.w = max(size.w, i32::from(x));
}
if let Some(x) = self.rules.min_height {
size.h = max(size.h, i32::from(x));
}
size
}
fn max_size(&self) -> Size<i32, Logical> {
let mut size = with_states(self.toplevel().wl_surface(), |state| {
let curr = state.cached_state.current::<SurfaceCachedState>();
curr.max_size
});
if let Some(x) = self.rules.max_width {
if size.w == 0 {
size.w = i32::from(x);
} else if x > 0 {
size.w = min(size.w, i32::from(x));
}
}
if let Some(x) = self.rules.max_height {
if size.h == 0 {
size.h = i32::from(x);
} else if x > 0 {
size.h = min(size.h, i32::from(x));
}
}
size
}
fn is_wl_surface(&self, wl_surface: &WlSurface) -> bool {
self.toplevel().wl_surface() == wl_surface
}
fn set_preferred_scale_transform(&self, scale: i32, transform: Transform) {
self.window.with_surfaces(|surface, data| {
send_surface_state(surface, data, scale, transform);
});
}
fn has_ssd(&self) -> bool {
self.toplevel().current_state().decoration_mode
== Some(zxdg_toplevel_decoration_v1::Mode::ServerSide)
}
fn output_enter(&self, output: &Output) {
let overlap = Rectangle::from_loc_and_size((0, 0), (i32::MAX, i32::MAX));
self.window.output_enter(output, overlap)
}
fn output_leave(&self, output: &Output) {
self.window.output_leave(output)
}
fn set_offscreen_element_id(&self, id: Option<Id>) {
let data = self
.window
.user_data()
.get_or_insert(WindowOffscreenId::default);
data.0.replace(id);
}
fn set_activated(&self, active: bool) {
self.window.set_activated(active);
}
fn set_bounds(&self, bounds: Size<i32, Logical>) {
self.toplevel().with_pending_state(|state| {
state.bounds = Some(bounds);
});
}
fn send_pending_configure(&self) {
self.toplevel().send_pending_configure();
}
fn is_fullscreen(&self) -> bool {
self.toplevel()
.current_state()
.states
.contains(xdg_toplevel::State::Fullscreen)
}
fn is_pending_fullscreen(&self) -> bool {
self.toplevel()
.with_pending_state(|state| state.states.contains(xdg_toplevel::State::Fullscreen))
}
fn refresh(&self) {
self.window.refresh();
}
fn rules(&self) -> &ResolvedWindowRules {
&self.rules
}
}

155
src/window/mod.rs Normal file
View File

@ -0,0 +1,155 @@
use niri_config::{Match, WindowRule};
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::{
ToplevelSurface, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
};
use crate::layout::workspace::ColumnWidth;
pub mod mapped;
pub use mapped::Mapped;
pub mod unmapped;
pub use unmapped::{InitialConfigureState, Unmapped};
/// Rules fully resolved for a window.
#[derive(Debug, PartialEq)]
pub struct ResolvedWindowRules {
/// Default width for this window.
///
/// - `None`: unset (global default should be used).
/// - `Some(None)`: set to empty (window picks its own width).
/// - `Some(Some(width))`: set to a particular width.
pub default_width: Option<Option<ColumnWidth>>,
/// Output to open this window on.
pub open_on_output: Option<String>,
/// Whether the window should open full-width.
pub open_maximized: Option<bool>,
/// Whether the window should open fullscreen.
pub open_fullscreen: Option<bool>,
/// Extra bound on the minimum window width.
pub min_width: Option<u16>,
/// Extra bound on the minimum window height.
pub min_height: Option<u16>,
/// Extra bound on the maximum window width.
pub max_width: Option<u16>,
/// Extra bound on the maximum window height.
pub max_height: Option<u16>,
/// Whether or not to draw the border with a solid background.
///
/// `None` means using the SSD heuristic.
pub draw_border_with_background: Option<bool>,
}
impl ResolvedWindowRules {
pub const fn empty() -> Self {
Self {
default_width: None,
open_on_output: None,
open_maximized: None,
open_fullscreen: None,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
draw_border_with_background: None,
}
}
pub fn compute(rules: &[WindowRule], toplevel: &ToplevelSurface) -> Self {
let _span = tracy_client::span!("ResolvedWindowRules::compute");
let mut resolved = ResolvedWindowRules::empty();
with_states(toplevel.wl_surface(), |states| {
let role = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
let mut open_on_output = None;
for rule in rules {
if !(rule.matches.is_empty()
|| rule.matches.iter().any(|m| window_matches(&role, m)))
{
continue;
}
if rule.excludes.iter().any(|m| window_matches(&role, m)) {
continue;
}
if let Some(x) = rule
.default_column_width
.as_ref()
.map(|d| d.0.map(ColumnWidth::from))
{
resolved.default_width = Some(x);
}
if let Some(x) = rule.open_on_output.as_deref() {
open_on_output = Some(x);
}
if let Some(x) = rule.open_maximized {
resolved.open_maximized = Some(x);
}
if let Some(x) = rule.open_fullscreen {
resolved.open_fullscreen = Some(x);
}
if let Some(x) = rule.min_width {
resolved.min_width = Some(x);
}
if let Some(x) = rule.min_height {
resolved.min_height = Some(x);
}
if let Some(x) = rule.max_width {
resolved.max_width = Some(x);
}
if let Some(x) = rule.max_height {
resolved.max_height = Some(x);
}
if let Some(x) = rule.draw_border_with_background {
resolved.draw_border_with_background = Some(x);
}
}
resolved.open_on_output = open_on_output.map(|x| x.to_owned());
});
resolved
}
}
fn window_matches(role: &XdgToplevelSurfaceRoleAttributes, m: &Match) -> bool {
if let Some(app_id_re) = &m.app_id {
let Some(app_id) = &role.app_id else {
return false;
};
if !app_id_re.is_match(app_id) {
return false;
}
}
if let Some(title_re) = &m.title {
let Some(title) = &role.title else {
return false;
};
if !title_re.is_match(title) {
return false;
}
}
true
}

View File

@ -1,6 +1,7 @@
use smithay::desktop::Window;
use smithay::output::Output;
use super::ResolvedWindowRules;
use crate::layout::workspace::ColumnWidth;
#[derive(Debug)]
@ -43,26 +44,6 @@ pub enum InitialConfigureState {
},
}
/// Rules fully resolved for a window.
#[derive(Debug, Default)]
pub struct ResolvedWindowRules {
/// Default width for this window.
///
/// - `None`: unset (global default should be used).
/// - `Some(None)`: set to empty (window picks its own width).
/// - `Some(Some(width))`: set to a particular width.
pub default_width: Option<Option<ColumnWidth>>,
/// Output to open this window on.
pub open_on_output: Option<String>,
/// Whether the window should open full-width.
pub open_maximized: Option<bool>,
/// Whether the window should open fullscreen.
pub open_fullscreen: Option<bool>,
}
impl Unmapped {
/// Wraps a newly created window that hasn't been initially configured yet.
pub fn new(window: Window) -> Self {