diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2024-02-23 13:57:56 +0400 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2024-02-23 14:01:32 +0400 |
| commit | 2317021a7c4a7296606533d38f1fdce96826f7dc (patch) | |
| tree | 10ff4760e21b9379853065f3472ae75d3986f183 | |
| parent | af6485cd8c85665b15ef8d2c812da17604ca4e32 (diff) | |
| download | niri-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.rs | 5 | ||||
| -rw-r--r-- | src/handlers/compositor.rs | 62 | ||||
| -rw-r--r-- | src/handlers/xdg_shell.rs | 259 | ||||
| -rw-r--r-- | src/layout/mod.rs | 40 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 20 | ||||
| -rw-r--r-- | src/lib.rs | 1 | ||||
| -rw-r--r-- | src/niri.rs | 3 | ||||
| -rw-r--r-- | src/window.rs | 71 |
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()); @@ -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 { .. }) + } +} |
