// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

use smithay_client_toolkit::
{
	compositor::CompositorHandler,
	delegate_compositor, delegate_subcompositor, delegate_foreign_toplevel_list, delegate_layer,
	delegate_output, delegate_registry, delegate_seat, delegate_keyboard,
	delegate_pointer, delegate_touch, delegate_shm, delegate_xdg_popup,
	delegate_xdg_shell, delegate_xdg_window, delegate_session_lock,
	foreign_toplevel_list::{ ForeignToplevelList, ForeignToplevelListHandler },
	output::{ OutputHandler, OutputState },
	registry::{ ProvidesRegistryState, RegistryState },
	registry_handlers,
	seat::{ Capability, SeatHandler, SeatState },
	shell::
	{
		WaylandSurface,
		wlr_layer::{ LayerShellHandler, LayerSurface, LayerSurfaceConfigure },
		xdg::XdgSurface,
		xdg::popup::{ Popup, PopupConfigure, PopupHandler },
		xdg::window::{ DecorationMode, Window, WindowConfigure, WindowHandler },
	},
	shm::{ Shm, ShmHandler },
	session_lock::{ SessionLock, SessionLockHandler, SessionLockSurface, SessionLockSurfaceConfigure },
};
use smithay_client_toolkit::reexports::protocols::ext::foreign_toplevel_list::v1::client::ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1;
use smithay_client_toolkit::reexports::client::
{
	protocol::
	{
		wl_callback::{ self, WlCallback },
		wl_output::{ self, WlOutput },
		wl_surface::WlSurface,
		wl_seat::WlSeat,
	},
	Connection, Dispatch, Proxy, QueueHandle,
};
use wayland_protocols::wp::text_input::zv3::client::
{
	zwp_text_input_manager_v3::ZwpTextInputManagerV3,
	zwp_text_input_v3::{ self, ZwpTextInputV3 },
};

use crate::app::{ App, ToplevelEvent };
use super::app_data::AppData;

/// Adopt a new buffer scale on `ss`: resize whichever rendering target is
/// active and request a redraw. Returns the new physical size, or `None`
/// when the factor is unchanged.
fn apply_surface_scale<Msg: Clone>(
	ss:         &mut super::SurfaceState<Msg>,
	shm:        &Shm,
	surface:    &WlSurface,
	new_factor: i32,
) -> Option<( u32, u32 )>
{
	if new_factor == ss.scale_factor { return None; }
	ss.scale_factor = new_factor;
	surface.set_buffer_scale( new_factor );
	if let Some( ref mut canvas ) = ss.canvas
	{
		canvas.set_dpi_scale( new_factor as f32 );
	}
	let pw = ss.width  * new_factor as u32;
	let ph = ss.height * new_factor as u32;
	// The two targets are mutually exclusive so only one branch runs.
	if let Some( ref es ) = ss.egl_surface
	{
		es.resize( pw as i32, ph as i32 );
		// Canvas FBO reallocation is deferred to the draw path: it needs
		// `eglMakeCurrent` first.
	} else {
		ss.pool = Some(
			smithay_client_toolkit::shm::slot::SlotPool::new(
				( pw * ph * 4 ) as usize, shm,
			).expect( "pool" ),
		);
		if let Some( ref mut canvas ) = ss.canvas
		{
			canvas.resize( pw, ph );
		}
	}
	ss.request_redraw();
	Some( ( pw, ph ) )
}

impl<A: App> CompositorHandler for AppData<A>
{
	fn scale_factor_changed( &mut self, _: &Connection, _: &QueueHandle<Self>, surface: &WlSurface, new_factor: i32 )
	{
		if new_factor <= 0 { return; }
		let Some( focus ) = self.focus_for_surface( surface ) else
		{
			// Secondary per-output surfaces live outside the focus map.
			let shm = &self.shm;
			if let Some( ( _, ss ) ) = self.secondaries.iter_mut()
				.find( |( _, ss )| ss.surface.try_wl_surface() == Some( surface ) )
			{
				apply_surface_scale( ss, shm, surface, new_factor );
			}
			return;
		};
		let ( pw, ph ) = {
			let shm = &self.shm;
			let ss  = match focus
			{
				super::SurfaceFocus::Main         => &mut self.main,
				super::SurfaceFocus::Overlay( id ) => match self.overlays.get_mut( &id )
				{
					Some( s ) => s,
					None      => return,
				},
			};
			match apply_surface_scale( ss, shm, surface, new_factor )
			{
				Some( dims ) => dims,
				None         => return,
			}
		};
		// Anchors and baked overlay geometry are in the old scale's
		// physical pixels — drop the tooltip and rebuild the specs.
		self.cancel_tooltip();
		self.overlays_dirty = true;
		// Notify the app of the new physical dimensions. The previous
		// `on_resize` it received was scaled with the OLD factor, so any
		// app-side state keyed off those pixels is now stale. Only fire
		// for the main surface — overlay resizes don't go through
		// `App::on_resize`.
		if matches!( focus, super::SurfaceFocus::Main )
		{
			crate::types::set_viewport_size( pw, ph );
			self.app.on_scale_changed( new_factor as u32 );
			self.app.on_resize( pw, ph );
			self.dirty_caches();
		}
	}

	fn transform_changed( &mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlSurface, _: wl_output::Transform ) {}

	fn frame(
		&mut self,
		_conn:    &Connection,
		_qh:      &QueueHandle<Self>,
		_surface: &WlSurface,
		_time:    u32,
	)
	{
		// Do NOT set needs_redraw here — that would create an infinite 60 fps loop
		// (commit → frame callback → redraw → commit → ...).  Redraws are driven
		// exclusively by input events, poll_external messages, and configure events.
	}

	fn surface_enter( &mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlSurface, _: &WlOutput ) {}

	fn surface_leave( &mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlSurface, _: &WlOutput ) {}
}

impl<A: App> LayerShellHandler for AppData<A>
{
	fn closed(
		&mut self,
		_conn: &Connection,
		_qh:   &QueueHandle<Self>,
		layer: &LayerSurface,
	)
	{
		if self.is_secondary_surface( layer.wl_surface() )
		{
			self.secondaries.retain( |( _, ss )| ss.surface.try_wl_surface() != Some( layer.wl_surface() ) );
			return;
		}
		match self.focus_for_surface( layer.wl_surface() )
		{
			Some( super::SurfaceFocus::Main ) | None =>
			{
				if self.app.on_close_requested()
				{
					self.exit_requested = true;
				}
			}
			Some( super::SurfaceFocus::Overlay( id ) ) =>
			{
				// Compositor asked us to destroy this overlay. Drop it
				// synchronously *and* rewrite every per-device focus that
				// pointed at it — otherwise the next event in this same
				// dispatch (touch up, IME done, pointer leave) lands on a
				// freed surface and `surface()` / `surface_mut()` panic.
				// `reconcile_overlays` will still run afterwards and skip
				// re-creating the entry as long as the app stops returning
				// its id from `overlays()`.
				self.discard_overlay( id );
			}
		}
	}

	fn configure(
		&mut self,
		_conn:     &Connection,
		_qh:       &QueueHandle<Self>,
		layer:     &LayerSurface,
		configure: LayerSurfaceConfigure,
		_serial:   u32,
	)
	{
		let ( w, h ) = configure.new_size;
		let ( w, h ) = ( w.max( 1 ), h.max( 1 ) );
		// Secondaries live outside the focus map; without this branch their
		// configure would fall into the `None` arm and resize the main
		// surface instead.
		for ( _, ss ) in self.secondaries.iter_mut()
		{
			if ss.surface.try_wl_surface() == Some( layer.wl_surface() )
			{
				ss.on_configure( &self.shm, self.egl_context.as_ref(), w, h );
				return;
			}
		}
		match self.focus_for_surface( layer.wl_surface() )
		{
			Some( super::SurfaceFocus::Main ) | None =>
			{
				self.on_configure( w, h );
			}
			Some( super::SurfaceFocus::Overlay( id ) ) =>
			{
				if let Some( ss ) = self.overlays.get_mut( &id )
				{
					ss.on_configure( &self.shm, self.egl_context.as_ref(), w, h );
				}
			}
		}
	}
}

impl<A: App> WindowHandler for AppData<A>
{
	fn request_close(
		&mut self,
		_conn:   &Connection,
		_qh:     &QueueHandle<Self>,
		_window: &Window,
	)
	{
		if self.app.on_close_requested()
		{
			self.exit_requested = true;
		}
	}

	fn configure(
		&mut self,
		_conn:     &Connection,
		_qh:       &QueueHandle<Self>,
		window:    &Window,
		configure: WindowConfigure,
		_serial:   u32,
	)
	{
		// Mutter ignores set_fullscreen sent before the surface is
		// mapped, so reapply on the first configure.
		if self.pending_fullscreen
		{
			window.set_fullscreen( None );
			self.pending_fullscreen = false;
		}
		if self.pending_size_hint_unpin
		{
			window.set_max_size( None );
			self.pending_size_hint_unpin = false;
		}
		let tb = if configure.decoration_mode == DecorationMode::Server { 0.0 }
			else { self.main.titlebar_base };
		if self.main.titlebar_height != tb
		{
			self.main.titlebar_height = tb;
			self.main.content_dirty   = true;
			self.view_dirty           = true;
		}
		let ( hint_w, hint_h ) = self.app.window_size_hint().unwrap_or( ( 800, 600 ) );
		let w = configure.new_size.0.map( |v| v.get() ).unwrap_or( hint_w );
		let h = configure.new_size.1.map( |v| v.get() ).unwrap_or( hint_h );
		window.xdg_surface().set_window_geometry( 0, 0, w as i32, h as i32 );
		self.on_configure( w, h );
	}
}

impl<A: App> ShmHandler for AppData<A>
{
	fn shm_state( &mut self ) -> &mut Shm
	{
		&mut self.shm
	}
}

impl<A: App> OutputHandler for AppData<A>
{
	fn output_state( &mut self ) -> &mut OutputState
	{
		&mut self.output_state
	}

	fn new_output( &mut self, _: &Connection, qh: &QueueHandle<Self>, output: WlOutput )
	{
		// If we were waiting for an output to assign the layer surface to, create it now.
		// Same for any overlays that were created before an output existed.
		if let Some( ref layer_shell ) = self.layer_shell
		{
			if self.main.surface.materialize( &self.compositor_state, layer_shell, qh, &output )
			{
				self.main_output = Some( output.clone() );
			}
			for ss in self.overlays.values_mut()
			{
				ss.surface.materialize( &self.compositor_state, layer_shell, qh, &output );
			}
			// A background shell covers every output: each one beyond the
			// main's gets its own background layer surface, painted with
			// `App::secondary_view` — the wallpaper without any chrome.
			if self.app.shell_mode() == crate::app::ShellMode::Layer( crate::app::Layer::Background )
				&& self.main_output.is_some()
				&& self.main_output.as_ref() != Some( &output )
				&& !self.secondaries.iter().any( |( o, _ )| o == &output )
			{
				use smithay_client_toolkit::shell::wlr_layer;
				let surface       = self.compositor_state.create_surface( qh );
				let layer_surface = layer_shell.create_layer_surface(
					qh,
					surface,
					wlr_layer::Layer::Background,
					Some( "ltk-secondary" ),
					Some( &output ),
				);
				layer_surface.set_anchor(
					wlr_layer::Anchor::TOP | wlr_layer::Anchor::BOTTOM
						| wlr_layer::Anchor::LEFT | wlr_layer::Anchor::RIGHT,
				);
				layer_surface.set_exclusive_zone( 0 );
				layer_surface.set_keyboard_interactivity( wlr_layer::KeyboardInteractivity::None );
				layer_surface.set_size( 0, 0 );
				layer_surface.commit();
				self.secondaries.push( (
					output.clone(),
					super::SurfaceState::new( super::SurfaceKind::Layer( layer_surface ), 0.0, String::new() ),
				) );
			}
		}
		// An output hotplugged while the session is locked would be blanked by
		// the compositor until it gets a lock surface of its own.
		if let Some( ref lock ) = self.session_lock
		{
			if matches!( self.main.surface, super::SurfaceKind::Lock( .. ) )
				&& self.main_output.as_ref() != Some( &output )
				&& !self.secondaries.iter().any( |( o, _ )| o == &output )
			{
				let surface      = self.compositor_state.create_surface( qh );
				let lock_surface = lock.create_lock_surface( surface, &output, qh );
				self.secondaries.push( (
					output,
					super::SurfaceState::new( super::SurfaceKind::Lock( lock_surface ), 0.0, String::new() ),
				) );
			}
		}
	}

	fn update_output(    &mut self, _: &Connection, _: &QueueHandle<Self>, _: WlOutput ) {}

	fn output_destroyed( &mut self, _: &Connection, _: &QueueHandle<Self>, output: WlOutput )
	{
		self.secondaries.retain( |( o, _ )| o != &output );
		if self.main_output.as_ref() == Some( &output )
		{
			self.main_output = None;
		}
	}
}

impl<A: App> SeatHandler for AppData<A>
{
	fn seat_state( &mut self ) -> &mut SeatState
	{
		&mut self.seat_state
	}

	fn new_seat(
		&mut self, _conn: &Connection, qh: &QueueHandle<Self>, seat: WlSeat,
	)
	{
		// First seat → first data device. We only need one device
		// regardless of how many seats appear (the typical desktop
		// session has exactly one); additional seats reuse the same
		// device-bound clipboard semantics by construction because
		// `wl_data_device_manager.get_data_device` takes the seat as
		// argument and the compositor manages routing.
		if self.data_device.is_none()
		{
			if let Some( ref ddm ) = self.data_device_manager
			{
				self.data_device = Some( ddm.get_data_device( qh, &seat ) );
			}
		}
	}

	fn new_capability(
		&mut self,
		_conn:      &Connection,
		qh:         &QueueHandle<Self>,
		seat:       WlSeat,
		capability: Capability,
	)
	{
		match capability
		{
			Capability::Keyboard if self.keyboard.is_none() =>
			{
				self.keyboard = Some(
					self.seat_state
						.get_keyboard( qh, &seat, None )
						.expect( "keyboard" ),
				);
			}
			Capability::Pointer if self.pointer.is_none() =>
			{
				let pointer = self.seat_state
					.get_pointer( qh, &seat )
					.expect( "pointer" );
				// Create a per-pointer cursor-shape device when the
				// compositor advertises wp_cursor_shape_v1. The device
				// outlives the WlPointer we just created and is what
				// `set_shape(serial, shape)` is called on.
				if let Some( ref mgr ) = self.cursor_shape_manager
				{
					self.cursor_shape_device = Some( mgr.get_shape_device( &pointer, qh ) );
				}
				self.pointer = Some( pointer );
			}
			Capability::Touch if self.touch.is_none() =>
			{
				// Touch coming back (resume on devices that power the
				// touchscreen down at suspend): clear anything the removal
				// path didn't, so the first post-resume gesture starts clean.
				if self.reset_touch_state()
				{
					eprintln!( "[ltk] touch capability re-added — cleared touch state stranded across the gap" );
				}
				self.touch = Some(
					self.seat_state
						.get_touch( qh, &seat )
						.expect( "touch" ),
				);
			}
			_ => {}
		}
	}

	fn remove_capability(
		&mut self,
		_conn:      &Connection,
		_qh:        &QueueHandle<Self>,
		_seat:      WlSeat,
		capability: Capability,
	)
	{
		match capability
		{
			Capability::Keyboard => { self.keyboard = None; }
			Capability::Pointer  => { self.pointer  = None; }
			Capability::Touch    =>
			{
				self.touch = None;
				// The touchscreen is gone before any pending up / cancel for
				// the last touch could arrive; drop the stale gesture state
				// now rather than stranding it until the next clean release.
				if self.reset_touch_state()
				{
					eprintln!( "[ltk] touch capability removed mid-gesture — reset stale touch state" );
				}
			}
			_ => {}
		}
	}

	fn remove_seat(
		&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _seat: WlSeat,
	) {}
}

impl<A: App> PopupHandler for AppData<A>
{
	fn configure(
		&mut self,
		_conn:  &Connection,
		_qh:    &QueueHandle<Self>,
		popup:  &Popup,
		config: PopupConfigure,
	)
	{
		let ( w, h ) = ( config.width.max( 1 ) as u32, config.height.max( 1 ) as u32 );
		if let Some( super::SurfaceFocus::Overlay( id ) ) = self.focus_for_surface( popup.wl_surface() )
		{
			if let Some( ss ) = self.overlays.get_mut( &id )
			{
				ss.on_configure( &self.shm, self.egl_context.as_ref(), w, h );
			}
		}
	}

	fn done(
		&mut self,
		_conn: &Connection,
		_qh:   &QueueHandle<Self>,
		popup: &Popup,
	)
	{
		if let Some( super::SurfaceFocus::Overlay( id ) ) = self.focus_for_surface( popup.wl_surface() )
		{
			if let Some( msg ) = self.overlay_dismiss_msg( id )
			{
				self.pending_msgs.push( msg );
			}
			// Synchronous teardown plus per-device focus cleanup. See
			// `discard_overlay` for the panic case this prevents.
			self.discard_overlay( id );
		}
	}
}

// --- Delegate macros ---

impl<A: App> SessionLockHandler for AppData<A>
{
	fn locked( &mut self, _conn: &Connection, qh: &QueueHandle<Self>, session_lock: SessionLock )
	{
		// Every output needs its own lock surface — the compositor blanks any
		// output left without one. The first hosts the app's main surface;
		// the rest render `App::secondary_view`.
		let outputs: Vec<WlOutput> = self.output_state.outputs().collect();
		let mut outputs = outputs.into_iter();
		if let Some( output ) = outputs.next()
		{
			let surface      = self.compositor_state.create_surface( qh );
			let lock_surface = session_lock.create_lock_surface( surface, &output, qh );
			self.main.surface = super::SurfaceKind::Lock( lock_surface );
			self.main_output  = Some( output );
		}
		for output in outputs
		{
			let surface      = self.compositor_state.create_surface( qh );
			let lock_surface = session_lock.create_lock_surface( surface, &output, qh );
			self.secondaries.push( (
				output,
				super::SurfaceState::new( super::SurfaceKind::Lock( lock_surface ), 0.0, String::new() ),
			) );
		}
		self.session_lock = Some( session_lock );
	}

	fn finished( &mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _session_lock: SessionLock )
	{
		// Compositor refused the lock or ended it; nothing left to show.
		self.exit_requested = true;
	}

	fn configure(
		&mut self,
		_conn:     &Connection,
		_qh:       &QueueHandle<Self>,
		surface:   SessionLockSurface,
		configure: SessionLockSurfaceConfigure,
		_serial:   u32,
	)
	{
		let ( w, h ) = configure.new_size;
		let ( w, h ) = ( w.max( 1 ), h.max( 1 ) );
		let wl = surface.wl_surface();
		if self.main.surface.try_wl_surface() == Some( wl )
		{
			self.on_configure( w, h );
			return;
		}
		for ( _, ss ) in self.secondaries.iter_mut()
		{
			if ss.surface.try_wl_surface() == Some( wl )
			{
				ss.on_configure( &self.shm, self.egl_context.as_ref(), w, h );
				return;
			}
		}
	}
}

delegate_compositor!(  @<A: App> AppData<A> );
delegate_subcompositor!( @<A: App> AppData<A> );
delegate_output!(      @<A: App> AppData<A> );
delegate_shm!(         @<A: App> AppData<A> );
delegate_seat!(        @<A: App> AppData<A> );
delegate_keyboard!(    @<A: App> AppData<A> );
delegate_pointer!(     @<A: App> AppData<A> );
delegate_touch!(       @<A: App> AppData<A> );
delegate_layer!(       @<A: App> AppData<A> );
delegate_session_lock!( @<A: App> AppData<A> );
delegate_xdg_shell!(   @<A: App> AppData<A> );
delegate_xdg_window!(  @<A: App> AppData<A> );
delegate_xdg_popup!(   @<A: App> AppData<A> );
delegate_foreign_toplevel_list!( @<A: App> AppData<A> );
delegate_registry!(    @<A: App> AppData<A> );
smithay_client_toolkit::delegate_activation!( @<A: App> AppData<A> );
smithay_client_toolkit::delegate_data_device!( @<A: App> AppData<A> );

// --- `xdg-activation-v1` handler ---
//
// A token carrying a `RequestData::app_id` was requested by the app via
// [`App::take_activation_requests`]: echo the tag + token back through
// [`App::on_activation_token`] so the app can pass it to a child it is
// launching. A token with no tag was issued for our own surface, so we
// self-activate.
impl<A: App> smithay_client_toolkit::activation::ActivationHandler for AppData<A>
{
	type RequestData = smithay_client_toolkit::activation::RequestData;
	fn new_token( &mut self, token: String, data: &Self::RequestData )
	{
		use smithay_client_toolkit::activation::RequestDataExt;
		if let Some( tag ) = data.app_id()
		{
			if let Some( msg ) = self.app.on_activation_token( tag.to_string(), token )
			{
				self.pending_msgs.push( msg );
			}
			return;
		}
		if let ( Some( ref activation ), Some( wl ) ) = ( self.activation_state.as_ref(), self.main.surface.try_wl_surface() )
		{
			activation.activate::<Self>( &wl, token );
		}
	}
}

impl<A: App> ProvidesRegistryState for AppData<A>
{
	fn registry( &mut self ) -> &mut RegistryState
	{
		&mut self.registry_state
	}
	registry_handlers![ OutputState, SeatState ];
}

// --- `ext-foreign-toplevel-list-v1` handler ---
//
// SCTK does the wire-level dispatch (open / close / done / app_id /
// title / identifier events arrive on the inner `ForeignToplevelList`
// helper); we surface the high-level open / update / close callbacks
// to apps through [`App::on_toplevel_event`]. Returned messages join
// `pending_msgs` and flow through the regular `update` cycle, so the
// invalidation pipeline (`App::invalidate_after`) decides which
// surfaces to redraw — apps that only need the dock to refresh can
// scope to `SurfaceTarget::Main` and skip overlays.
//
// The `id` field reported to apps is the Wayland protocol id of the
// handle proxy — unique per session and stable for the handle's
// lifetime, the same value paired across `Opened` and the matching
// `Closed`.
/// Cascade for the per-toplevel display string crustace-style shells use to
/// register a window: prefer `app_id` (sets `.desktop` matching), fall back
/// to `title` (visible to the user), and finally to the protocol-issued
/// `identifier` (always present, unique per handle). Without the cascade,
/// any client that never set `app_id` (e.g. winit-windowed compositors
/// before they bind their xdg surface) was silently invisible to the dock.
fn toplevel_display_id(
	list:   &ForeignToplevelList,
	handle: &ExtForeignToplevelHandleV1,
) -> String
{
	// Only ever surface the client's `app_id`. The protocol-level
	// `identifier` is a server-internal random opaque token, not an
	// app handle; smithay creates the toplevel handle with an empty
	// `app_id` and `init_new_instance` flushes a `done` immediately,
	// so apps subscribing through this binding would otherwise see
	// the random identifier as their "app id" until the client's
	// real `set_app_id` lands. `title` is also unsuitable — it
	// changes every time the window's title text updates.
	let Some( info ) = list.info( handle ) else { return String::new(); };
	info.app_id
}

impl<A: App> ForeignToplevelListHandler for AppData<A>
{
	fn foreign_toplevel_list_state( &mut self ) -> &mut ForeignToplevelList
	{
		&mut self.foreign_toplevel_list
	}

	fn new_toplevel(
		&mut self,
		_conn:  &Connection,
		_qh:    &QueueHandle<Self>,
		handle: ExtForeignToplevelHandleV1,
	)
	{
		let id     = handle.id().protocol_id();
		let app_id = toplevel_display_id( &self.foreign_toplevel_list, &handle );
		if let Some( msg ) = self.app.on_toplevel_event( ToplevelEvent::Opened { id, app_id } )
		{
			self.pending_msgs.push( msg );
		}
	}

	fn update_toplevel(
		&mut self,
		_conn:  &Connection,
		_qh:    &QueueHandle<Self>,
		handle: ExtForeignToplevelHandleV1,
	)
	{
		// Re-emit `Opened` with the latest info. Compositors usually
		// only fire this on title changes; the `app_id` is committed
		// once on the initial `done` and never replaced. Apps whose
		// `RunningApps`-style bookkeeping is keyed by `(id, app_id)`
		// can treat repeated opens as idempotent and pay nothing here,
		// but the app_id-can-change-mid-life case still produces a
		// correct sequence (old refcount drops, new one bumps).
		let id     = handle.id().protocol_id();
		let app_id = toplevel_display_id( &self.foreign_toplevel_list, &handle );
		if let Some( msg ) = self.app.on_toplevel_event( ToplevelEvent::Opened { id, app_id } )
		{
			self.pending_msgs.push( msg );
		}
	}

	fn toplevel_closed(
		&mut self,
		_conn:  &Connection,
		_qh:    &QueueHandle<Self>,
		handle: ExtForeignToplevelHandleV1,
	)
	{
		let id = handle.id().protocol_id();
		if let Some( msg ) = self.app.on_toplevel_event( ToplevelEvent::Closed { id } )
		{
			self.pending_msgs.push( msg );
		}
	}
}

// --- Dispatch impls for zwp_text_input_v3 ---

impl<A: App> Dispatch<ZwpTextInputManagerV3, ()> for AppData<A>
{
	fn event(
		_state: &mut Self,
		_proxy: &ZwpTextInputManagerV3,
		_event: <ZwpTextInputManagerV3 as smithay_client_toolkit::reexports::client::Proxy>::Event,
		_data:  &(),
		_conn:  &Connection,
		_qh:    &QueueHandle<Self>,
	)
	{
		// no events from manager
	}
}

// Frame-callback routing.
//
// Each `draw_*` path requests a `wl_surface.frame` with `SurfaceFocus` user-
// data identifying which `SurfaceState` it belongs to. The compositor fires
// the callback when the surface is ready for its next commit (display refresh,
// VRR cadence, "screen is on", …). Clearing `frame_pending` unblocks the run
// loop so the next iteration is allowed to draw that surface again.
//
// While the app is animating we re-arm the surface for redraw inline so the
// loop keeps ticking at the compositor's pace without needing a fixed-period
// timer.
impl<A: App> Dispatch<WlCallback, super::SurfaceFocus> for AppData<A>
{
	fn event(
		state:  &mut Self,
		_proxy: &WlCallback,
		_event: wl_callback::Event,
		focus:  &super::SurfaceFocus,
		_conn:  &Connection,
		_qh:    &QueueHandle<Self>,
	)
	{
		let is_animating = state.app.is_animating();
		match *focus
		{
			super::SurfaceFocus::Main =>
			{
				state.main.frame_pending = false;
				if is_animating
				{
					if state.app.subsurface_motion_only()
					{
						// The main buffer is unchanged; only the subsurface
						// moves (repositioned by the per-frame reconcile).
						// Keep the vsync cadence with a bare frame callback +
						// commit — no buffer attach, no re-raster.
						if let Some( wl ) = state.main.surface.try_wl_surface().cloned()
						{
							let _ = wl.frame( &state.qh, super::SurfaceFocus::Main );
							wl.commit();
							state.main.frame_pending = true;
						}
					}
					else
					{
						// Perf guardrails: run the opt-in diagnostics and apply
						// the software animation-rate cap (~30 Hz). A throttled
						// frame keeps the vsync cadence with a bare callback but
						// skips the re-raster.
						let software = crate::render::is_software_render();
						let cap      = state.app.cap_software_animation();
						if state.perf.animated_frame( software, cap )
						{
							state.view_dirty = true;
							state.main.request_redraw();
						}
						else if let Some( wl ) = state.main.surface.try_wl_surface().cloned()
						{
							let _ = wl.frame( &state.qh, super::SurfaceFocus::Main );
							wl.commit();
							state.main.frame_pending = true;
						}
					}
				}
				else
				{
					state.perf.animation_stopped();
				}
			}
			super::SurfaceFocus::Overlay( id ) =>
			{
				if let Some( ss ) = state.overlays.get_mut( &id )
				{
					ss.frame_pending = false;
					if is_animating
					{
						state.overlays_dirty = true;
						ss.request_redraw();
					}
				}
			}
		}
	}
}

impl<A: App> Dispatch<ZwpTextInputV3, ()> for AppData<A>
{
	fn event(
		state:  &mut Self,
		_proxy: &ZwpTextInputV3,
		event:  zwp_text_input_v3::Event,
		_data:  &(),
		_conn:  &Connection,
		_qh:    &QueueHandle<Self>,
	)
	{
		let focus = state.keyboard_focus;
		match event
		{
			zwp_text_input_v3::Event::CommitString { text } =>
			{
				if let Some( text ) = text
				{
					if !text.is_empty()
					{
						state.handle_text_insert( focus, &text );
					}
				}
			}
			zwp_text_input_v3::Event::DeleteSurroundingText { before_length, after_length } =>
			{
				state.handle_delete_surrounding( focus, before_length, after_length );
			}
			zwp_text_input_v3::Event::Done { .. } =>
			{
				if let Some( ss ) = state.try_surface_mut( focus )
				{
					ss.request_redraw();
				}
			}
			zwp_text_input_v3::Event::Enter { .. } =>
			{
				state.reenable_text_input();
			}
			_ => {}
		}
	}
}
