diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/handlers/compositor.rs | 1 | ||||
| -rw-r--r-- | src/handlers/xdg_shell.rs | 59 | ||||
| -rw-r--r-- | src/layout/mod.rs | 18 | ||||
| -rw-r--r-- | src/layout/tile.rs | 11 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 33 | ||||
| -rw-r--r-- | src/niri.rs | 29 | ||||
| -rw-r--r-- | src/utils/mod.rs | 1 | ||||
| -rw-r--r-- | src/utils/transaction.rs | 185 | ||||
| -rw-r--r-- | src/window/mapped.rs | 70 |
9 files changed, 390 insertions, 17 deletions
diff --git a/src/handlers/compositor.rs b/src/handlers/compositor.rs index 140d00e0..e833494c 100644 --- a/src/handlers/compositor.rs +++ b/src/handlers/compositor.rs @@ -52,6 +52,7 @@ impl CompositorHandler for State { fn commit(&mut self, surface: &WlSurface) { let _span = tracy_client::span!("CompositorHandler::commit"); + trace!(surface = ?surface.id(), "commit"); on_commit_buffer_handler::<Self>(surface); self.backend.early_import(surface); diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index 5f84da3b..ec0b99e3 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -34,6 +34,7 @@ use smithay::wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState}; use smithay::{ delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_foreign, delegate_xdg_shell, }; +use tracing::field::Empty; use crate::input::resize_grab::ResizeGrab; use crate::input::DOUBLE_CLICK_TIME; @@ -1003,6 +1004,8 @@ fn unconstrain_with_padding( pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId { add_pre_commit_hook::<State, _>(toplevel.wl_surface(), move |state, _dh, surface| { let _span = tracy_client::span!("mapped toplevel pre-commit"); + let span = + trace_span!("toplevel pre-commit", surface = %surface.id(), serial = Empty).entered(); let Some((mapped, _)) = state.niri.layout.find_window_and_output_mut(surface) else { error!("pre-commit hook for mapped surfaces must be removed upon unmapping"); @@ -1032,31 +1035,73 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId (got_unmapped, dmabuf, role.configure_serial) }); - let mut dmabuf_blocker = - dmabuf.and_then(|dmabuf| dmabuf.generate_blocker(Interest::READ).ok()); + let mut transaction_for_dmabuf = None; + let mut animate = false; + if let Some(serial) = commit_serial { + if !span.is_disabled() { + span.record("serial", format!("{serial:?}")); + } + + trace!("taking pending transaction"); + if let Some(transaction) = mapped.take_pending_transaction(serial) { + // Transaction can be already completed if it ran past the deadline. + let disable = state.niri.config.borrow().debug.disable_transactions; + if !transaction.is_completed() && !disable { + // Register the deadline even if this is the last pending, since dmabuf + // rendering can still run over the deadline. + transaction.register_deadline_timer(&state.niri.event_loop); + + let is_last = transaction.is_last(); + + // If this is the last transaction, we don't need to add a separate + // notification, because the transaction will complete in our dmabuf blocker + // callback, which already calls blocker_cleared(), or by the end of this + // function, in which case there would be no blocker in the first place. + if !is_last { + // Waiting for some other surface; register a notification and add a + // transaction blocker. + if let Some(client) = surface.client() { + transaction.add_notification( + state.niri.blocker_cleared_tx.clone(), + client.clone(), + ); + add_blocker(surface, transaction.blocker()); + } + } + + // Delay dropping (and completing) the transaction until the dmabuf is ready. + // If there's no dmabuf, this will be dropped by the end of this pre-commit + // hook. + transaction_for_dmabuf = Some(transaction); + } + } - let animate = if let Some(serial) = commit_serial { - mapped.should_animate_commit(serial) + animate = mapped.should_animate_commit(serial); } else { error!("commit on a mapped surface without a configured serial"); - false }; - if let Some((blocker, source)) = dmabuf_blocker.take() { + if let Some((blocker, source)) = + dmabuf.and_then(|dmabuf| dmabuf.generate_blocker(Interest::READ).ok()) + { if let Some(client) = surface.client() { let res = state .niri .event_loop .insert_source(source, move |_, _, state| { + // This surface is now ready for the transaction. + drop(transaction_for_dmabuf.take()); + let display_handle = state.niri.display_handle.clone(); state .client_compositor_state(&client) .blocker_cleared(state, &display_handle); + Ok(()) }); if res.is_ok() { add_blocker(surface, blocker); - trace!("added toplevel dmabuf blocker"); + trace!("added dmabuf blocker"); } } } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index abcf9d43..9fbd1623 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -52,6 +52,7 @@ use crate::render_helpers::snapshot::RenderSnapshot; use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement}; use crate::render_helpers::texture::TextureBuffer; use crate::render_helpers::{BakedBuffer, RenderTarget, SplitElements}; +use crate::utils::transaction::Transaction; use crate::utils::{output_size, round_logical_in_physical_max1, ResizeEdge}; use crate::window::ResolvedWindowRules; @@ -153,7 +154,12 @@ pub trait LayoutElement { self.render(renderer, location, scale, alpha, target).popups } - fn request_size(&mut self, size: Size<i32, Logical>, animate: bool); + fn request_size( + &mut self, + size: Size<i32, Logical>, + animate: bool, + transaction: Option<Transaction>, + ); fn request_fullscreen(&self, size: Size<i32, Logical>); fn min_size(&self) -> Size<i32, Logical>; fn max_size(&self) -> Size<i32, Logical>; @@ -237,6 +243,7 @@ pub struct Options { // Debug flags. pub disable_resize_throttling: bool, + pub disable_transactions: bool, } impl Default for Options { @@ -255,6 +262,7 @@ impl Default for Options { default_width: None, animations: Default::default(), disable_resize_throttling: false, + disable_transactions: false, } } } @@ -292,6 +300,7 @@ impl Options { default_width, animations: config.animations.clone(), disable_resize_throttling: config.debug.disable_resize_throttling, + disable_transactions: config.debug.disable_transactions, } } @@ -2636,7 +2645,12 @@ mod tests { SplitElements::default() } - fn request_size(&mut self, size: Size<i32, Logical>, _animate: bool) { + fn request_size( + &mut self, + size: Size<i32, Logical>, + _animate: bool, + _transaction: Option<Transaction>, + ) { self.0.requested_size.set(Some(size)); self.0.pending_fullscreen.set(false); } diff --git a/src/layout/tile.rs b/src/layout/tile.rs index a701e96f..11c266d4 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -23,6 +23,7 @@ use crate::render_helpers::resize::ResizeRenderElement; use crate::render_helpers::snapshot::RenderSnapshot; use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement}; use crate::render_helpers::{render_to_encompassing_texture, RenderTarget}; +use crate::utils::transaction::Transaction; /// Toplevel window with decorations. #[derive(Debug)] @@ -503,7 +504,12 @@ impl<W: LayoutElement> Tile<W> { activation_region.contains(point) } - pub fn request_tile_size(&mut self, mut size: Size<f64, Logical>, animate: bool) { + pub fn request_tile_size( + &mut self, + mut size: Size<f64, Logical>, + animate: bool, + transaction: Option<Transaction>, + ) { // Can't go through effective_border_width() because we might be fullscreen. if !self.border.is_off() { let width = self.border.width(); @@ -514,7 +520,8 @@ impl<W: LayoutElement> Tile<W> { // The size request has to be i32 unfortunately, due to Wayland. We floor here instead of // round to avoid situations where proportionally-sized columns don't fit on the screen // exactly. - self.window.request_size(size.to_i32_floor(), animate); + self.window + .request_size(size.to_i32_floor(), animate, transaction); } pub fn tile_width_for_window_width(&self, size: f64) -> f64 { diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index 8f0ae6e3..7548dc10 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -22,6 +22,7 @@ use crate::niri_render_elements; use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::RenderTarget; use crate::utils::id::IdCounter; +use crate::utils::transaction::Transaction; use crate::utils::{output_size, send_scale_transform, ResizeEdge}; use crate::window::ResolvedWindowRules; @@ -2740,6 +2741,27 @@ impl<W: LayoutElement> Workspace<W> { } } + let intent = if self.options.disable_resize_throttling { + ConfigureIntent::CanSend + } else if self.options.disable_transactions { + // When transactions are disabled, we don't use combined throttling, but rather + // compute throttling individually below. + ConfigureIntent::CanSend + } else { + col.tiles + .iter() + .fold(ConfigureIntent::NotNeeded, |intent, tile| { + match (intent, tile.window().configure_intent()) { + (_, ConfigureIntent::ShouldSend) => ConfigureIntent::ShouldSend, + (ConfigureIntent::NotNeeded, tile_intent) => tile_intent, + (ConfigureIntent::CanSend, ConfigureIntent::Throttled) => { + ConfigureIntent::Throttled + } + (intent, _) => intent, + } + }) + }; + for (tile_idx, tile) in col.tiles.iter_mut().enumerate() { let win = tile.window_mut(); @@ -2759,7 +2781,13 @@ impl<W: LayoutElement> Workspace<W> { ); win.set_bounds(bounds); - let intent = win.configure_intent(); + // If transactions are disabled, also disable combined throttling, for more + // intuitive behavior. + let intent = if self.options.disable_transactions { + win.configure_intent() + } else { + intent + }; if matches!( intent, @@ -3167,13 +3195,14 @@ impl<W: LayoutElement> Column<W> { assert_eq!(auto_tiles_left, 0); } + let transaction = Transaction::new(); for (tile, h) in zip(&mut self.tiles, heights) { let WindowHeight::Fixed(height) = h else { unreachable!() }; let size = Size::from((width, height)); - tile.request_tile_size(size, animate); + tile.request_tile_size(size, animate, Some(transaction.clone())); } } diff --git a/src/niri.rs b/src/niri.rs index 33ca7d6d..27696749 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -4,6 +4,7 @@ use std::ffi::OsString; use std::path::PathBuf; use std::rc::Rc; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::{self, Receiver, Sender}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use std::{env, mem, thread}; @@ -61,14 +62,14 @@ use smithay::reexports::wayland_server::backend::{ }; use smithay::reexports::wayland_server::protocol::wl_shm; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; -use smithay::reexports::wayland_server::{Display, DisplayHandle, Resource}; +use smithay::reexports::wayland_server::{Client, Display, DisplayHandle, Resource}; use smithay::utils::{ ClockSource, IsAlive as _, Logical, Monotonic, Physical, Point, Rectangle, Scale, Size, Transform, SERIAL_COUNTER, }; use smithay::wayland::compositor::{ - with_states, with_surface_tree_downward, CompositorClientState, CompositorState, HookId, - SurfaceData, TraversalAction, + with_states, with_surface_tree_downward, CompositorClientState, CompositorHandler, + CompositorState, HookId, SurfaceData, TraversalAction, }; use smithay::wayland::cursor_shape::CursorShapeManagerState; use smithay::wayland::dmabuf::DmabufState; @@ -192,6 +193,10 @@ pub struct Niri { // Dmabuf readiness pre-commit hook for a surface. pub dmabuf_pre_commit_hook: HashMap<WlSurface, HookId>, + /// Clients to notify about their blockers being cleared. + pub blocker_cleared_tx: Sender<Client>, + pub blocker_cleared_rx: Receiver<Client>, + pub output_state: HashMap<Output, OutputState>, pub output_by_name: HashMap<String, Output>, @@ -517,6 +522,10 @@ impl State { fn refresh(&mut self) { let _span = tracy_client::span!("State::refresh"); + // Handle commits for surfaces whose blockers cleared this cycle. This should happen before + // layout.refresh() since this is where these surfaces handle commits. + self.notify_blocker_cleared(); + // These should be called periodically, before flushing the clients. self.niri.layout.refresh(); self.niri.cursor_manager.check_cursor_image_surface_alive(); @@ -535,6 +544,15 @@ impl State { self.niri.refresh_mapped_cast_outputs(); } + fn notify_blocker_cleared(&mut self) { + let dh = self.niri.display_handle.clone(); + while let Ok(client) = self.niri.blocker_cleared_rx.try_recv() { + trace!("calling blocker_cleared"); + self.client_compositor_state(&client) + .blocker_cleared(self, &dh); + } + } + pub fn move_cursor(&mut self, location: Point<f64, Logical>) { let under = self.niri.surface_under_and_global_space(location); self.niri @@ -1523,6 +1541,8 @@ impl Niri { let layout = Layout::new(&config_); + let (blocker_cleared_tx, blocker_cleared_rx) = mpsc::channel(); + let compositor_state = CompositorState::new_v6::<State>(&display_handle); let xdg_shell_state = XdgShellState::new_with_capabilities::<State>( &display_handle, @@ -1737,6 +1757,8 @@ impl Niri { unmapped_windows: HashMap::new(), root_surface: HashMap::new(), dmabuf_pre_commit_hook: HashMap::new(), + blocker_cleared_tx, + blocker_cleared_rx, monitors_active: true, devices: HashSet::new(), @@ -2497,6 +2519,7 @@ impl Niri { RedrawState::Queued | RedrawState::WaitingForEstimatedVBlankAndQueued(_) ) }) { + trace!("redrawing output"); let output = output.clone(); self.redraw(backend, &output); } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 03654877..5fb1d163 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -23,6 +23,7 @@ use smithay::wayland::fractional_scale::with_fractional_scale; pub mod id; pub mod scale; pub mod spawning; +pub mod transaction; pub mod watcher; pub static IS_SYSTEMD_SERVICE: AtomicBool = AtomicBool::new(false); diff --git a/src/utils/transaction.rs b/src/utils/transaction.rs new file mode 100644 index 00000000..03dd6a2e --- /dev/null +++ b/src/utils/transaction.rs @@ -0,0 +1,185 @@ +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::atomic::AtomicBool; +use std::sync::mpsc::Sender; +use std::sync::{Arc, Mutex, Weak}; +use std::time::{Duration, Instant}; + +use atomic::Ordering; +use calloop::ping::{make_ping, Ping}; +use calloop::timer::{TimeoutAction, Timer}; +use calloop::LoopHandle; +use smithay::reexports::wayland_server::Client; +use smithay::wayland::compositor::{Blocker, BlockerState}; + +/// Default time limit, after which the transaction completes. +/// +/// Serves to avoid hanging when a client fails to respond to a configure promptly. +const TIME_LIMIT: Duration = Duration::from_millis(300); + +/// Transaction between Wayland clients. +/// +/// How to use it: +/// 1. Create a transaction with [`Transaction::new()`]. +/// 2. Clone it as many times as you need. +/// 3. Before adding the transaction as a commit blocker, remember to call +/// [`Transaction::add_notification()`] to receive a notification when the transaction completes. +/// 4. Before adding the transaction as a commit blocker, remember to call +/// [`Transaction::register_deadline_timer()`] to make sure the transaction completes when +/// reaching the deadline. +/// 5. In your surface pre-commit handler, if the transaction corresponding to that commit isn't +/// ready, get a blocker with [`Transaction::blocker()`] and add it to the surface. +#[derive(Debug, Clone)] +pub struct Transaction { + inner: Arc<Inner>, + deadline: Rc<RefCell<Deadline>>, +} + +/// Blocker for a [`Transaction`]. +#[derive(Debug)] +pub struct TransactionBlocker(Weak<Inner>); + +#[derive(Debug)] +enum Deadline { + NotRegistered(Instant), + Registered { remove: Ping }, +} + +#[derive(Debug)] +struct Inner { + /// Whether the transaction is completed. + completed: AtomicBool, + /// Notifications to send out upon completing the transaction. + notifications: Mutex<Option<(Sender<Client>, Vec<Client>)>>, +} + +impl Transaction { + /// Creates a new transaction. + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { + inner: Arc::new(Inner::new()), + deadline: Rc::new(RefCell::new(Deadline::NotRegistered( + Instant::now() + TIME_LIMIT, + ))), + } + } + + /// Gets a blocker for this transaction. + pub fn blocker(&self) -> TransactionBlocker { + trace!(transaction = ?Arc::as_ptr(&self.inner), "generating blocker"); + TransactionBlocker(Arc::downgrade(&self.inner)) + } + + /// Adds a notification for when this transaction completes. + pub fn add_notification(&self, sender: Sender<Client>, client: Client) { + if self.is_completed() { + error!("tried to add notification to a completed transaction"); + return; + } + + let mut guard = self.inner.notifications.lock().unwrap(); + guard.get_or_insert((sender, Vec::new())).1.push(client); + } + + /// Registers this transaction's deadline timer on an event loop. + pub fn register_deadline_timer<T: 'static>(&self, event_loop: &LoopHandle<'static, T>) { + let mut cell = self.deadline.borrow_mut(); + if let Deadline::NotRegistered(deadline) = *cell { + let timer = Timer::from_deadline(deadline); + let inner = Arc::downgrade(&self.inner); + let token = event_loop + .insert_source(timer, move |_, _, _| { + let _span = trace_span!("deadline timer", transaction = ?Weak::as_ptr(&inner)) + .entered(); + + if let Some(inner) = inner.upgrade() { + trace!("deadline reached, completing transaction"); + inner.complete(); + } else { + // We should remove the timer automatically. But this callback can still + // just happen to run while the ping callback is scheduled, leading to this + // branch being legitimately taken. + trace!("transaction completed without removing the timer"); + } + + TimeoutAction::Drop + }) + .unwrap(); + + // Add a ping source that will be used to remove the timer automatically. + let (ping, source) = make_ping().unwrap(); + let loop_handle = event_loop.clone(); + event_loop + .insert_source(source, move |_, _, _| { + loop_handle.remove(token); + }) + .unwrap(); + + *cell = Deadline::Registered { remove: ping }; + } + } + + /// Returns whether this transaction has already completed. + pub fn is_completed(&self) -> bool { + self.inner.is_completed() + } + + /// Returns whether this is the last instance of this transaction. + pub fn is_last(&self) -> bool { + Arc::strong_count(&self.inner) == 1 + } +} + +impl Drop for Transaction { + fn drop(&mut self) { + let _span = trace_span!("drop", transaction = ?Arc::as_ptr(&self.inner)).entered(); + + if self.is_last() { + // If this was the last transaction, complete it. + trace!("last transaction dropped, completing"); + self.inner.complete(); + + // Also remove the timer. + if let Deadline::Registered { remove } = &*self.deadline.borrow() { + remove.ping(); + }; + } + } +} + +impl Blocker for TransactionBlocker { + fn state(&self) -> BlockerState { + if self.0.upgrade().map_or(true, |x| x.is_completed()) { + BlockerState::Released + } else { + BlockerState::Pending + } + } +} + +impl Inner { + fn new() -> Self { + Self { + completed: AtomicBool::new(false), + notifications: Mutex::new(None), + } + } + + fn is_completed(&self) -> bool { + self.completed.load(Ordering::Relaxed) + } + + fn complete(&self) { + self.completed.store(true, Ordering::Relaxed); + + let mut guard = self.notifications.lock().unwrap(); + if let Some((sender, clients)) = guard.take() { + for client in clients { + if let Err(err) = sender.send(client) { + warn!("error sending blocker notification: {err:?}"); + }; + } + } + } +} diff --git a/src/window/mapped.rs b/src/window/mapped.rs index 6a1fe045..54c045b3 100644 --- a/src/window/mapped.rs +++ b/src/window/mapped.rs @@ -32,6 +32,7 @@ use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderEleme use crate::render_helpers::surface::render_snapshot_from_surface_tree; use crate::render_helpers::{BakedBuffer, RenderTarget, SplitElements}; use crate::utils::id::IdCounter; +use crate::utils::transaction::Transaction; use crate::utils::{send_scale_transform, ResizeEdge}; #[derive(Debug)] @@ -71,6 +72,12 @@ pub struct Mapped { /// Snapshot right before an animated commit. animation_snapshot: Option<LayoutElementRenderSnapshot>, + /// Transaction that the next configure should take part in, if any. + transaction_for_next_configure: Option<Transaction>, + + /// Pending transactions that have not been added as blockers for this window yet. + pending_transactions: Vec<(Serial, Transaction)>, + /// State of an ongoing interactive resize. interactive_resize: Option<InteractiveResize>, @@ -141,6 +148,8 @@ impl Mapped { animate_next_configure: false, animate_serials: Vec::new(), animation_snapshot: None, + transaction_for_next_configure: None, + pending_transactions: Vec::new(), interactive_resize: None, last_interactive_resize_start: Cell::new(None), } @@ -255,6 +264,40 @@ impl Mapped { self.animation_snapshot = Some(self.render_snapshot(renderer)); } + pub fn take_pending_transaction(&mut self, commit_serial: Serial) -> Option<Transaction> { + let mut rv = None; + + // Pending transactions are appended in order by serial, so we can loop from the start + // until we hit a serial that is too new. + while let Some((serial, _)) = self.pending_transactions.first() { + // In this loop, we will complete the transaction corresponding to the commit, as well + // as all transactions corresponding to previous serials. This can happen when we + // request resizes too quickly, and the surface only responds to the last one. + // + // Note that in this case, completing the previous transactions can result in an + // inconsistent visual state, if another window is waiting for this window to assume a + // specific size (in a previous transaction), which is now different (in this commit). + // + // However, there isn't really a good way to deal with that. We cannot cancel any + // transactions because we need to keep sending frame callbacks, and cancelling a + // transaction will make the corresponding frame callbacks get lost, and the window + // will hang. + // + // This is why resize throttling (implemented separately) is important: it prevents + // visually inconsistent states by way of never having more than one transaction in + // flight. + if commit_serial.is_no_older_than(serial) { + let (_, transaction) = self.pending_transactions.remove(0); + // Previous transaction is dropped here, signaling completion. + rv = Some(transaction); + } else { + break; + } + } + + rv + } + pub fn last_interactive_resize_start(&self) -> &Cell<Option<(Duration, ResizeEdge)>> { &self.last_interactive_resize_start } @@ -442,7 +485,12 @@ impl LayoutElement for Mapped { } } - fn request_size(&mut self, size: Size<i32, Logical>, animate: bool) { + fn request_size( + &mut self, + size: Size<i32, Logical>, + animate: bool, + transaction: Option<Transaction>, + ) { let changed = self.toplevel().with_pending_state(|state| { let changed = state.size != Some(size); state.size = Some(size); @@ -453,6 +501,15 @@ impl LayoutElement for Mapped { if changed && animate { self.animate_next_configure = true; } + + // Store the transaction regardless of whether the size changed. This is because with 3+ + // windows in a column, the size may change among windows 1 and 2 and then right away among + // windows 2 and 3, and we want all windows 1, 2 and 3 to use the last transaction, rather + // than window 1 getting stuck with the previous transaction that is immediately released + // by 2. + if let Some(transaction) = transaction { + self.transaction_for_next_configure = Some(transaction); + } } fn request_fullscreen(&self, size: Size<i32, Logical>) { @@ -627,11 +684,21 @@ impl LayoutElement for Mapped { } fn send_pending_configure(&mut self) { + let _span = + trace_span!("send_pending_configure", surface = ?self.toplevel().wl_surface().id()) + .entered(); + if let Some(serial) = self.toplevel().send_pending_configure() { + trace!(?serial, "sending configure"); + if self.animate_next_configure { self.animate_serials.push(serial); } + if let Some(transaction) = self.transaction_for_next_configure.take() { + self.pending_transactions.push((serial, transaction)); + } + self.interactive_resize = match self.interactive_resize.take() { Some(InteractiveResize::WaitingForLastConfigure(data)) => { Some(InteractiveResize::WaitingForLastCommit { data, serial }) @@ -648,6 +715,7 @@ impl LayoutElement for Mapped { } self.animate_next_configure = false; + self.transaction_for_next_configure = None; } fn is_fullscreen(&self) -> bool { |
