aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2024-02-23 13:57:56 +0400
committerIvan Molodetskikh <yalterz@gmail.com>2024-02-23 14:01:32 +0400
commit2317021a7c4a7296606533d38f1fdce96826f7dc (patch)
tree10ff4760e21b9379853065f3472ae75d3986f183
parentaf6485cd8c85665b15ef8d2c812da17604ca4e32 (diff)
downloadniri-2317021a7c4a7296606533d38f1fdce96826f7dc.tar.gz
niri-2317021a7c4a7296606533d38f1fdce96826f7dc.tar.bz2
niri-2317021a7c4a7296606533d38f1fdce96826f7dc.zip
Implement explicit unmapped window state tracking
-rw-r--r--niri-visual-tests/src/cases/layout.rs5
-rw-r--r--src/handlers/compositor.rs62
-rw-r--r--src/handlers/xdg_shell.rs259
-rw-r--r--src/layout/mod.rs40
-rw-r--r--src/layout/workspace.rs20
-rw-r--r--src/lib.rs1
-rw-r--r--src/niri.rs3
-rw-r--r--src/window.rs71
8 files changed, 308 insertions, 153 deletions
diff --git a/niri-visual-tests/src/cases/layout.rs b/niri-visual-tests/src/cases/layout.rs
index ca8edcec..be611cc5 100644
--- a/niri-visual-tests/src/cases/layout.rs
+++ b/niri-visual-tests/src/cases/layout.rs
@@ -145,8 +145,7 @@ impl Layout {
}
fn add_window(&mut self, window: TestWindow, width: Option<ColumnWidth>) {
- self.layout
- .add_window(window.clone(), width.map(Some), false);
+ self.layout.add_window(window.clone(), width, false);
if window.communicate() {
self.layout.update_window(&window);
}
@@ -160,7 +159,7 @@ impl Layout {
width: Option<ColumnWidth>,
) {
self.layout
- .add_window_right_of(right_of, window.clone(), width.map(Some), false);
+ .add_window_right_of(right_of, window.clone(), width, false);
if window.communicate() {
self.layout.update_window(&window);
}
diff --git a/src/handlers/compositor.rs b/src/handlers/compositor.rs
index 2d6a6406..a8ef5e2c 100644
--- a/src/handlers/compositor.rs
+++ b/src/handlers/compositor.rs
@@ -16,9 +16,9 @@ use smithay::wayland::dmabuf::get_dmabuf;
use smithay::wayland::shm::{ShmHandler, ShmState};
use smithay::{delegate_compositor, delegate_shm};
-use super::xdg_shell::{initial_configure_sent, resolve_window_rules};
use crate::niri::{ClientState, State};
use crate::utils::clone2;
+use crate::window::{InitialConfigureState, Unmapped};
impl CompositorHandler for State {
fn compositor_state(&mut self) -> &mut CompositorState {
@@ -105,29 +105,41 @@ impl CompositorHandler for State {
if is_mapped {
// The toplevel got mapped.
- let window = entry.remove();
+ let Unmapped { window, state } = entry.remove();
+
window.on_commit();
+ let (width, output) =
+ if let InitialConfigureState::Configured { 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, output)
+ } else {
+ error!("window map must happen after initial configure");
+ (None, None)
+ };
+
let parent = window
.toplevel()
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
- .map(|(win, _)| win.clone());
-
- let (width, output) = {
- let config = self.niri.config.borrow();
- let rules = resolve_window_rules(&config.window_rules, window.toplevel());
- let output = rules
- .open_on_output
- .and_then(|name| self.niri.output_by_name.get(name))
- .cloned();
- (rules.default_width, output)
- };
+ // Only consider the parent if we configured the window for the same
+ // output.
+ //
+ // Normally when we're following the parent, the configured output will be
+ // None. If the configured output is set, that means it was set explicitly
+ // by a window rule or a fullscreen request.
+ .filter(|(_, parent_output)| {
+ output.is_none() || output.as_ref() == Some(*parent_output)
+ })
+ .map(|(window, _)| window.clone());
let win = window.clone();
- // Open dialogs immediately to the right of their parent window.
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, false)
} else if let Some(output) = &output {
self.niri
@@ -146,17 +158,10 @@ impl CompositorHandler for State {
}
// The toplevel remains unmapped.
- let window = entry.get().clone();
-
- // Send the initial configure in an idle, in case the client sent some more info
- // after the initial commit.
- if !initial_configure_sent(window.toplevel()) {
- self.niri.event_loop.insert_idle(move |state| {
- if !window.toplevel().alive() {
- return;
- }
- state.send_initial_configure_if_needed(&window);
- });
+ let unmapped = entry.get();
+ if unmapped.needs_initial_configure() {
+ let toplevel = unmapped.window.toplevel().clone();
+ self.queue_initial_configure(toplevel);
}
return;
}
@@ -178,7 +183,12 @@ impl CompositorHandler for State {
if !is_mapped {
// The toplevel got unmapped.
self.niri.layout.remove_window(&window);
- self.niri.unmapped_windows.insert(surface.clone(), window);
+
+ // Newly-unmapped toplevels must perform the initial commit-configure sequence
+ // afresh.
+ let unmapped = Unmapped::new(window);
+ self.niri.unmapped_windows.insert(surface.clone(), unmapped);
+
self.niri.queue_redraw(output);
return;
}
diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs
index c4ac6a06..04d4cc50 100644
--- a/src/handlers/xdg_shell.rs
+++ b/src/handlers/xdg_shell.rs
@@ -27,19 +27,7 @@ use smithay::{delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_she
use crate::layout::workspace::ColumnWidth;
use crate::niri::{PopupGrabState, State};
use crate::utils::clone2;
-
-#[derive(Debug, Default)]
-pub struct ResolvedWindowRule<'a> {
- /// Default width for this window.
- ///
- /// - `None`: unset.
- /// - `Some(None)`: set to empty.
- /// - `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<&'a str>,
-}
+use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped};
fn window_matches(role: &XdgToplevelSurfaceRoleAttributes, m: &Match) -> bool {
if let Some(app_id_re) = &m.app_id {
@@ -63,13 +51,13 @@ fn window_matches(role: &XdgToplevelSurfaceRoleAttributes, m: &Match) -> bool {
true
}
-pub fn resolve_window_rules<'a>(
- rules: &'a [WindowRule],
+pub fn resolve_window_rules(
+ rules: &[WindowRule],
toplevel: &ToplevelSurface,
-) -> ResolvedWindowRule<'a> {
+) -> ResolvedWindowRules {
let _span = tracy_client::span!("resolve_window_rules");
- let mut resolved = ResolvedWindowRule::default();
+ let mut resolved = ResolvedWindowRules::default();
with_states(toplevel.wl_surface(), |states| {
let role = states
@@ -79,6 +67,8 @@ pub fn resolve_window_rules<'a>(
.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;
@@ -97,9 +87,11 @@ pub fn resolve_window_rules<'a>(
}
if let Some(x) = rule.open_on_output.as_deref() {
- resolved.open_on_output = Some(x);
+ open_on_output = Some(x);
}
}
+
+ resolved.open_on_output = open_on_output.map(|x| x.to_owned());
});
resolved
@@ -112,10 +104,8 @@ impl XdgShellHandler for State {
fn new_toplevel(&mut self, surface: ToplevelSurface) {
let wl_surface = surface.wl_surface().clone();
- let window = Window::new(surface);
-
- // At the moment of creation, xdg toplevels must have no buffer.
- let existing = self.niri.unmapped_windows.insert(wl_surface, window);
+ let unmapped = Unmapped::new(Window::new(surface));
+ let existing = self.niri.unmapped_windows.insert(wl_surface, unmapped);
assert!(existing.is_none());
}
@@ -273,53 +263,42 @@ impl XdgShellHandler for State {
surface: ToplevelSurface,
wl_output: Option<wl_output::WlOutput>,
) {
- if surface
- .current_state()
- .capabilities
- .contains(xdg_toplevel::WmCapabilities::Fullscreen)
+ if let Some((window, current_output)) = self
+ .niri
+ .layout
+ .find_window_and_output(surface.wl_surface())
{
- if let Some((window, current_output)) = self
- .niri
- .layout
- .find_window_and_output(surface.wl_surface())
- {
- let window = window.clone();
+ let window = 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);
- }
+ 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);
}
+ }
- self.niri.layout.set_fullscreen(&window, true);
- } else if let Some(window) = self.niri.unmapped_windows.get(surface.wl_surface()) {
- let config = self.niri.config.borrow();
- let rules = resolve_window_rules(&config.window_rules, window.toplevel());
-
- // FIXME: take requested output into account (will need to thread this through to
- // send_initial_configure_if_needed and commit handler).
- let output = rules
- .open_on_output
- .and_then(|name| self.niri.output_by_name.get(name));
- let mon = output.map(|o| self.niri.layout.monitor_for_output(o).unwrap());
- let ws = mon
- .map(|mon| mon.active_workspace_ref())
- .or_else(|| self.niri.layout.active_workspace());
-
- if let Some(ws) = ws {
- window.toplevel().with_pending_state(|state| {
- state.size = Some(ws.view_size());
- state.states.set(xdg_toplevel::State::Fullscreen);
- });
+ self.niri.layout.set_fullscreen(&window, true);
+
+ // A configure is required in response to this event regardless if there are pending
+ // changes.
+ surface.send_configure();
+ } else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(surface.wl_surface()) {
+ match &mut unmapped.state {
+ InitialConfigureState::NotConfigured { wants_fullscreen } => {
+ *wants_fullscreen = Some(wl_output.as_ref().and_then(Output::from_resource));
+
+ // The required configure will be the initial configure.
}
- }
- }
+ InitialConfigureState::Configured { .. } => {
+ // FIXME: implement this once I figure out a good way without code duplication.
- // A configure is required in response to this event. However, if an initial configure
- // wasn't sent, then we will send this as part of the initial configure later.
- if initial_configure_sent(&surface) {
+ // We already sent the initial configure, so we need to reconfigure.
+ surface.send_configure();
+ }
+ }
+ } else {
+ error!("couldn't find the toplevel in fullscreen_request()");
surface.send_configure();
}
}
@@ -332,24 +311,27 @@ impl XdgShellHandler for State {
{
let window = window.clone();
self.niri.layout.set_fullscreen(&window, false);
- } else if let Some(window) = self.niri.unmapped_windows.get(surface.wl_surface()) {
- let config = self.niri.config.borrow();
- let rules = resolve_window_rules(&config.window_rules, window.toplevel());
-
- let output = rules
- .open_on_output
- .and_then(|name| self.niri.output_by_name.get(name));
- let mon = output.map(|o| self.niri.layout.monitor_for_output(o).unwrap());
- let ws = mon
- .map(|mon| mon.active_workspace_ref())
- .or_else(|| self.niri.layout.active_workspace());
-
- if let Some(ws) = ws {
- window.toplevel().with_pending_state(|state| {
- state.size = Some(ws.new_window_size(rules.default_width));
- state.states.unset(xdg_toplevel::State::Fullscreen);
- });
+
+ // A configure is required in response to this event regardless if there are pending
+ // changes.
+ surface.send_configure();
+ } else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(surface.wl_surface()) {
+ match &mut unmapped.state {
+ InitialConfigureState::NotConfigured { wants_fullscreen } => {
+ *wants_fullscreen = None;
+
+ // The required configure will be the initial configure.
+ }
+ InitialConfigureState::Configured { .. } => {
+ // FIXME: implement this once I figure out a good way without code duplication.
+
+ // We already sent the initial configure, so we need to reconfigure.
+ surface.send_configure();
+ }
}
+ } else {
+ error!("couldn't find the toplevel in unfullscreen_request()");
+ surface.send_configure();
}
}
@@ -385,6 +367,14 @@ impl XdgShellHandler for State {
self.niri.queue_redraw(output.clone());
}
}
+
+ fn app_id_changed(&mut self, toplevel: ToplevelSurface) {
+ self.update_window_rules(&toplevel);
+ }
+
+ fn title_changed(&mut self, toplevel: ToplevelSurface) {
+ self.update_window_rules(&toplevel);
+ }
}
delegate_xdg_shell!(State);
@@ -440,7 +430,7 @@ impl KdeDecorationHandler for State {
delegate_kde_decoration!(State);
-pub fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
+fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
with_states(toplevel.wl_surface(), |states| {
states
.data_map
@@ -453,28 +443,82 @@ pub fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
}
impl State {
- pub fn send_initial_configure_if_needed(&mut self, window: &Window) {
- let toplevel = window.toplevel();
- if initial_configure_sent(toplevel) {
+ pub fn send_initial_configure(&mut self, toplevel: &ToplevelSurface) {
+ let _span = tracy_client::span!("State::send_initial_configure");
+
+ let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) else {
+ error!("window must be present in unmapped_windows in send_initial_configure()");
return;
- }
+ };
- let _span = tracy_client::span!("State::send_initial_configure_if_needed");
+ let Unmapped { window, state } = unmapped;
+
+ let InitialConfigureState::NotConfigured { wants_fullscreen } = state else {
+ error!("window must not be already configured in send_initial_configure()");
+ return;
+ };
let config = self.niri.config.borrow();
let rules = resolve_window_rules(&config.window_rules, toplevel);
- let output = rules
+ // Pick the target monitor. First, check if we had an output set in the window rules.
+ let mon = rules
.open_on_output
- .and_then(|name| self.niri.output_by_name.get(name));
- let mon = output.map(|o| self.niri.layout.monitor_for_output(o).unwrap());
+ .as_deref()
+ .and_then(|name| self.niri.output_by_name.get(name))
+ .and_then(|o| self.niri.layout.monitor_for_output(o));
+
+ // If not, check if the window requested one for fullscreen.
+ let mon = mon.or_else(|| {
+ wants_fullscreen
+ .as_ref()
+ .and_then(|x| x.as_ref())
+ // The monitor might not exist if the output was disconnected.
+ .and_then(|o| self.niri.layout.monitor_for_output(o))
+ });
+
+ // If not, check if this is a dialog with a parent, to place it next to the parent.
+ let mon = mon.map(|mon| (mon, false)).or_else(|| {
+ toplevel
+ .parent()
+ .and_then(|parent| self.niri.layout.find_window_and_output(&parent))
+ .map(|(_win, output)| output)
+ .and_then(|o| self.niri.layout.monitor_for_output(o))
+ .map(|mon| (mon, true))
+ });
+
+ // If not, use the active monitor.
+ let mon = mon.or_else(|| {
+ self.niri
+ .layout
+ .active_monitor_ref()
+ .map(|mon| (mon, false))
+ });
+
+ // If we're following the parent, don't set the target output, so that when the window is
+ // mapped, it fetches the possibly changed parent's output again, and shows up there.
+ let output = mon
+ .filter(|(_, parent)| !parent)
+ .map(|(mon, _)| mon.output.clone());
+ let mon = mon.map(|(mon, _)| mon);
+
+ let mut width = None;
+
+ // Tell the surface the preferred size and bounds for its likely output.
let ws = mon
.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace());
- // Tell the surface the preferred size and bounds for its likely output.
if let Some(ws) = ws {
- ws.configure_new_window(window, rules.default_width);
+ // Set a fullscreen state if requested.
+ if wants_fullscreen.is_some() {
+ toplevel.with_pending_state(|state| {
+ state.states.set(xdg_toplevel::State::Fullscreen);
+ });
+ }
+
+ width = ws.resolve_default_width(rules.default_width);
+ ws.configure_new_window(window, width);
}
// If the user prefers no CSD, it's a reasonable assumption that they would prefer to get
@@ -488,9 +532,32 @@ impl State {
});
}
+ // Set the configured settings.
+ *state = InitialConfigureState::Configured {
+ rules,
+ width,
+ output,
+ };
+
toplevel.send_configure();
}
+ pub fn queue_initial_configure(&self, toplevel: ToplevelSurface) {
+ // Send the initial configure in an idle, in case the client sent some more info after the
+ // initial commit.
+ self.niri.event_loop.insert_idle(move |state| {
+ if !toplevel.alive() {
+ return;
+ }
+
+ if let Some(unmapped) = state.niri.unmapped_windows.get(toplevel.wl_surface()) {
+ if unmapped.needs_initial_configure() {
+ state.send_initial_configure(&toplevel);
+ }
+ }
+ });
+ }
+
/// Should be called on `WlSurface::commit`
pub fn popups_handle_commit(&mut self, surface: &WlSurface) {
self.niri.popups.commit(surface);
@@ -611,6 +678,16 @@ impl State {
}
}
}
+
+ pub fn update_window_rules(&mut self, toplevel: &ToplevelSurface) {
+ let resolve = || resolve_window_rules(&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();
+ }
+ }
+ }
}
fn unconstrain_with_padding(
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index edc0d9de..c7239a2b 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -531,15 +531,10 @@ impl<W: LayoutElement> Layout<W> {
pub fn add_window(
&mut self,
window: W,
- width: Option<Option<ColumnWidth>>,
+ width: Option<ColumnWidth>,
is_full_width: bool,
) -> Option<&Output> {
- let width = match width {
- Some(Some(width)) => Some(width),
- Some(None) => None,
- None => self.options.default_width,
- }
- .unwrap_or_else(|| ColumnWidth::Fixed(window.size().w));
+ let width = width.unwrap_or_else(|| ColumnWidth::Fixed(window.size().w));
match &mut self.monitor_set {
MonitorSet::Normal {
@@ -587,15 +582,10 @@ impl<W: LayoutElement> Layout<W> {
&mut self,
right_of: &W,
window: W,
- width: Option<Option<ColumnWidth>>,
+ width: Option<ColumnWidth>,
is_full_width: bool,
) -> Option<&Output> {
- let width = match width {
- Some(Some(width)) => Some(width),
- Some(None) => None,
- None => self.options.default_width,
- }
- .unwrap_or_else(|| ColumnWidth::Fixed(window.size().w));
+ let width = width.unwrap_or_else(|| ColumnWidth::Fixed(window.size().w));
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
@@ -623,15 +613,10 @@ impl<W: LayoutElement> Layout<W> {
&mut self,
output: &Output,
window: W,
- width: Option<Option<ColumnWidth>>,
+ width: Option<ColumnWidth>,
is_full_width: bool,
) {
- let width = match width {
- Some(Some(width)) => Some(width),
- Some(None) => None,
- None => self.options.default_width,
- }
- .unwrap_or_else(|| ColumnWidth::Fixed(window.size().w));
+ let width = width.unwrap_or_else(|| ColumnWidth::Fixed(window.size().w));
let MonitorSet::Normal {
monitors,
@@ -931,6 +916,19 @@ impl<W: LayoutElement> Layout<W> {
Some(&mut monitors[*active_monitor_idx])
}
+ pub fn active_monitor_ref(&self) -> Option<&Monitor<W>> {
+ let MonitorSet::Normal {
+ monitors,
+ active_monitor_idx,
+ ..
+ } = &self.monitor_set
+ else {
+ return None;
+ };
+
+ Some(&monitors[*active_monitor_idx])
+ }
+
pub fn monitor_for_output(&self, output: &Output) -> Option<&Monitor<W>> {
let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
return None;
diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs
index b9016098..79889ef2 100644
--- a/src/layout/workspace.rs
+++ b/src/layout/workspace.rs
@@ -322,17 +322,19 @@ impl<W: LayoutElement> Workspace<W> {
))
}
- pub fn new_window_size(
+ pub fn resolve_default_width(
&self,
default_width: Option<Option<ColumnWidth>>,
- ) -> Size<i32, Logical> {
- let default_width = match default_width {
+ ) -> Option<ColumnWidth> {
+ match default_width {
Some(Some(width)) => Some(width),
Some(None) => None,
None => self.options.default_width,
- };
+ }
+ }
- let width = if let Some(width) = default_width {
+ pub fn new_window_size(&self, width: Option<ColumnWidth>) -> Size<i32, Logical> {
+ let width = if let Some(width) = width {
let mut width = width.resolve(&self.options, self.working_area.size.w);
if !self.options.border.off {
width -= self.options.border.width as i32 * 2;
@@ -350,11 +352,7 @@ impl<W: LayoutElement> Workspace<W> {
Size::from((width, max(height, 1)))
}
- pub fn configure_new_window(
- &self,
- window: &Window,
- default_width: Option<Option<ColumnWidth>>,
- ) {
+ 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);
}
@@ -363,7 +361,7 @@ impl<W: LayoutElement> Workspace<W> {
if state.states.contains(xdg_toplevel::State::Fullscreen) {
state.size = Some(self.view_size);
} else {
- state.size = Some(self.new_window_size(default_width));
+ state.size = Some(self.new_window_size(width));
}
state.bounds = Some(self.toplevel_bounds());
diff --git a/src/lib.rs b/src/lib.rs
index 26f1277b..fdf588b3 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -17,6 +17,7 @@ pub mod protocols;
pub mod render_helpers;
pub mod ui;
pub mod utils;
+pub mod window;
#[cfg(not(feature = "xdp-gnome-screencast"))]
pub mod dummy_pw_utils;
diff --git a/src/niri.rs b/src/niri.rs
index 4e89e269..ed52a2ae 100644
--- a/src/niri.rs
+++ b/src/niri.rs
@@ -107,6 +107,7 @@ use crate::ui::screenshot_ui::{ScreenshotUi, ScreenshotUiRenderElement};
use crate::utils::{
center, get_monotonic_time, make_screenshot_path, output_size, write_png_rgba8,
};
+use crate::window::Unmapped;
use crate::{animation, niri_render_elements};
const CLEAR_COLOR: [f32; 4] = [0.2, 0.2, 0.2, 1.];
@@ -132,7 +133,7 @@ pub struct Niri {
pub global_space: Space<Window>,
// Windows which don't have a buffer attached yet.
- pub unmapped_windows: HashMap<WlSurface, Window>,
+ pub unmapped_windows: HashMap<WlSurface, Unmapped>,
pub output_state: HashMap<Output, OutputState>,
pub output_by_name: HashMap<String, Output>,
diff --git a/src/window.rs b/src/window.rs
new file mode 100644
index 00000000..68d1e3a0
--- /dev/null
+++ b/src/window.rs
@@ -0,0 +1,71 @@
+use smithay::desktop::Window;
+use smithay::output::Output;
+
+use crate::layout::workspace::ColumnWidth;
+
+#[derive(Debug)]
+pub struct Unmapped {
+ pub window: Window,
+ pub state: InitialConfigureState,
+}
+
+#[derive(Debug)]
+pub enum InitialConfigureState {
+ /// The window has not been initially configured yet.
+ NotConfigured {
+ /// Whether the window requested to be fullscreened, and the requested output, if any.
+ wants_fullscreen: Option<Option<Output>>,
+ },
+ /// The window has been configured.
+ Configured {
+ /// Up-to-date rules.
+ ///
+ /// We start tracking window rules when sending the initial configure, since they don't
+ /// affect anything before that.
+ rules: ResolvedWindowRules,
+
+ /// Resolved default width for this window.
+ ///
+ /// `None` means that the window will pick its own width.
+ width: Option<ColumnWidth>,
+
+ /// Output to open this window on.
+ ///
+ /// This can be `None` in cases like:
+ ///
+ /// - There are no outputs connected.
+ /// - This is a dialog with a parent, and there was no explicit output set, so this dialog
+ /// should fetch the parent's current output again upon mapping.
+ output: Option<Output>,
+ },
+}
+
+/// 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>,
+}
+
+impl Unmapped {
+ /// Wraps a newly created window that hasn't been initially configured yet.
+ pub fn new(window: Window) -> Self {
+ Self {
+ window,
+ state: InitialConfigureState::NotConfigured {
+ wants_fullscreen: None,
+ },
+ }
+ }
+
+ pub fn needs_initial_configure(&self) -> bool {
+ matches!(self.state, InitialConfigureState::NotConfigured { .. })
+ }
+}