First commit. Version 0.1.0
This commit is contained in:
2037
src/event_loop/app_data.rs
Normal file
2037
src/event_loop/app_data.rs
Normal file
File diff suppressed because it is too large
Load Diff
495
src/event_loop/handlers.rs
Normal file
495
src/event_loop/handlers.rs
Normal file
@@ -0,0 +1,495 @@
|
||||
// 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_layer, delegate_output, delegate_registry,
|
||||
delegate_seat, delegate_keyboard, delegate_pointer, delegate_touch, delegate_shm,
|
||||
delegate_xdg_popup, delegate_xdg_shell, delegate_xdg_window,
|
||||
output::{ OutputHandler, OutputState },
|
||||
registry::{ ProvidesRegistryState, RegistryState },
|
||||
registry_handlers,
|
||||
seat::{ Capability, SeatHandler, SeatState },
|
||||
shell::
|
||||
{
|
||||
WaylandSurface,
|
||||
wlr_layer::{ LayerShellHandler, LayerSurface, LayerSurfaceConfigure },
|
||||
xdg::popup::{ Popup, PopupConfigure, PopupHandler },
|
||||
xdg::window::{ Window, WindowConfigure, WindowHandler },
|
||||
},
|
||||
shm::{ Shm, ShmHandler },
|
||||
};
|
||||
use smithay_client_toolkit::reexports::client::
|
||||
{
|
||||
protocol::
|
||||
{
|
||||
wl_callback::{ self, WlCallback },
|
||||
wl_output::{ self, WlOutput },
|
||||
wl_surface::WlSurface,
|
||||
wl_seat::WlSeat,
|
||||
},
|
||||
Connection, Dispatch, 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;
|
||||
use super::app_data::AppData;
|
||||
|
||||
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 { 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,
|
||||
},
|
||||
};
|
||||
if new_factor == ss.scale_factor { return; }
|
||||
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;
|
||||
// Resize whichever rendering target is active. The two 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();
|
||||
( pw, ph )
|
||||
};
|
||||
// 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 )
|
||||
{
|
||||
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,
|
||||
)
|
||||
{
|
||||
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. Remove it;
|
||||
// the next reconcile will not recreate it unless the app
|
||||
// still returns its id from `App::overlays()`.
|
||||
self.overlays.remove( &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 ) );
|
||||
match self.focus_for_surface( layer.wl_surface() )
|
||||
{
|
||||
Some( super::SurfaceFocus::Main ) | None =>
|
||||
{
|
||||
self.on_configure( w, h );
|
||||
self.app.on_resize( 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;
|
||||
}
|
||||
let w = configure.new_size.0.map( |v| v.get() ).unwrap_or( 800 );
|
||||
let h = configure.new_size.1.map( |v| v.get() ).unwrap_or( 600 );
|
||||
self.on_configure( w, h );
|
||||
self.app.on_resize( 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
|
||||
{
|
||||
self.main.surface.materialize( &self.compositor_state, layer_shell, qh, &output );
|
||||
for ss in self.overlays.values_mut()
|
||||
{
|
||||
ss.surface.materialize( &self.compositor_state, layer_shell, qh, &output );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_output( &mut self, _: &Connection, _: &QueueHandle<Self>, _: WlOutput ) {}
|
||||
fn output_destroyed( &mut self, _: &Connection, _: &QueueHandle<Self>, _: WlOutput ) {}
|
||||
}
|
||||
|
||||
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,
|
||||
) {}
|
||||
|
||||
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() =>
|
||||
{
|
||||
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; }
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
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 );
|
||||
}
|
||||
self.overlays.remove( &id );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Delegate macros ---
|
||||
|
||||
delegate_compositor!( @<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_xdg_shell!( @<A: App> AppData<A> );
|
||||
delegate_xdg_window!( @<A: App> AppData<A> );
|
||||
delegate_xdg_popup!( @<A: App> AppData<A> );
|
||||
delegate_registry!( @<A: App> AppData<A> );
|
||||
|
||||
impl<A: App> ProvidesRegistryState for AppData<A>
|
||||
{
|
||||
fn registry( &mut self ) -> &mut RegistryState
|
||||
{
|
||||
&mut self.registry_state
|
||||
}
|
||||
registry_handlers![ OutputState, SeatState ];
|
||||
}
|
||||
|
||||
// --- 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
|
||||
{
|
||||
state.view_dirty = true;
|
||||
state.main.request_redraw();
|
||||
}
|
||||
}
|
||||
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, .. } =>
|
||||
{
|
||||
for _ in 0..before_length
|
||||
{
|
||||
state.handle_backspace( focus );
|
||||
}
|
||||
}
|
||||
zwp_text_input_v3::Event::Done { .. } =>
|
||||
{
|
||||
state.surface_mut( focus ).request_redraw();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
814
src/event_loop/mod.rs
Normal file
814
src/event_loop/mod.rs
Normal file
@@ -0,0 +1,814 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-only
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
pub( crate ) mod app_data;
|
||||
mod handlers;
|
||||
|
||||
pub( crate ) use app_data::{ AppData, LayerConfig, SurfaceFocus, SurfaceKind, SurfaceState };
|
||||
|
||||
use smithay_client_toolkit::
|
||||
{
|
||||
compositor::{ CompositorState, Surface },
|
||||
output::OutputState,
|
||||
registry::RegistryState,
|
||||
seat::SeatState,
|
||||
shell::
|
||||
{
|
||||
WaylandSurface,
|
||||
wlr_layer::LayerShell,
|
||||
xdg::
|
||||
{
|
||||
XdgPositioner, XdgShell, XdgSurface,
|
||||
popup::Popup,
|
||||
window::WindowDecorations,
|
||||
},
|
||||
},
|
||||
shm::Shm,
|
||||
};
|
||||
use smithay_client_toolkit::reexports::client::Connection;
|
||||
use smithay_client_toolkit::reexports::calloop::EventLoop;
|
||||
use smithay_client_toolkit::reexports::calloop_wayland_source::WaylandSource;
|
||||
use calloop::timer::{ Timer, TimeoutAction };
|
||||
use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3;
|
||||
use wayland_protocols::xdg::shell::client::xdg_positioner::
|
||||
{
|
||||
Anchor as PositionerAnchor,
|
||||
ConstraintAdjustment,
|
||||
Gravity,
|
||||
};
|
||||
|
||||
use crate::app::{ App, InvalidationScope, SurfaceTarget };
|
||||
use crate::draw::draw_frame;
|
||||
use crate::types::{ Point, Rect };
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Reasons [`crate::try_run`] (and therefore [`crate::run`]) can fail
|
||||
/// to bring up the event loop.
|
||||
///
|
||||
/// Every variant maps to a fatal failure during init: the Wayland
|
||||
/// connection, the calloop event loop, or one of the protocol bindings
|
||||
/// the runtime cannot operate without (`wl_compositor`, `wl_shm`,
|
||||
/// `xdg_wm_base`). Once init succeeds, runtime errors during the dispatch
|
||||
/// loop still panic — they are non-recoverable from the caller's point
|
||||
/// of view since the surface is already on screen.
|
||||
///
|
||||
/// Embedders that want a software-rendered fallback or that need to
|
||||
/// degrade gracefully should call [`crate::try_run`] and match on the
|
||||
/// variants instead of letting [`crate::run`] panic.
|
||||
#[ derive( Debug ) ]
|
||||
pub enum RunError
|
||||
{
|
||||
/// `WAYLAND_DISPLAY` is unset, the socket is missing, or the
|
||||
/// compositor refused the handshake. Includes the detailed reason
|
||||
/// from the underlying `wayland-client` error.
|
||||
NoWaylandConnection( String ),
|
||||
/// The Wayland registry could not be enumerated. Almost always a
|
||||
/// compositor / driver bug — the registry is the first thing every
|
||||
/// Wayland client touches.
|
||||
RegistryInit( String ),
|
||||
/// `calloop`'s `EventLoop::try_new` or its Wayland source insertion
|
||||
/// failed (typically an `io` error talking to the kernel).
|
||||
EventLoop( String ),
|
||||
/// A required Wayland protocol is missing from the compositor.
|
||||
/// `name` is the wire-format protocol name; `detail` is the
|
||||
/// underlying bind error.
|
||||
MissingProtocol
|
||||
{
|
||||
name: &'static str,
|
||||
detail: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RunError
|
||||
{
|
||||
fn fmt( &self, f: &mut std::fmt::Formatter<'_> ) -> std::fmt::Result
|
||||
{
|
||||
match self
|
||||
{
|
||||
Self::NoWaylandConnection( d ) =>
|
||||
write!( f, "Wayland connection failed: {d}" ),
|
||||
Self::RegistryInit( d ) =>
|
||||
write!( f, "Wayland registry init failed: {d}" ),
|
||||
Self::EventLoop( d ) =>
|
||||
write!( f, "event-loop setup failed: {d}" ),
|
||||
Self::MissingProtocol { name, detail } =>
|
||||
write!( f, "Wayland protocol `{name}` unavailable: {detail}" ),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RunError {}
|
||||
|
||||
/// Run the application, panicking on init failure. Thin wrapper over
|
||||
/// [`try_run`] kept for backwards-compatibility — embedders that need
|
||||
/// to recover from a missing compositor or a stripped-down driver
|
||||
/// should call [`try_run`] instead.
|
||||
pub( crate ) fn run<A: App>( app: A )
|
||||
{
|
||||
if let Err( e ) = try_run( app )
|
||||
{
|
||||
panic!( "ltk::run failed during init: {e}" );
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the application, returning a typed error on init failure.
|
||||
/// The dispatch loop's runtime errors still panic — they are non-
|
||||
/// recoverable once the surface is on screen, and the surface state
|
||||
/// machine cannot be unwound cleanly from this entry point.
|
||||
pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
|
||||
{
|
||||
let conn = Connection::connect_to_env()
|
||||
.map_err( |e| RunError::NoWaylandConnection( format!( "{e}" ) ) )?;
|
||||
let ( globals, queue ) =
|
||||
smithay_client_toolkit::reexports::client::globals::registry_queue_init( &conn )
|
||||
.map_err( |e| RunError::RegistryInit( format!( "{e:?}" ) ) )?;
|
||||
let qh = queue.handle();
|
||||
|
||||
let mut event_loop: EventLoop<AppData<A>> = EventLoop::try_new()
|
||||
.map_err( |e| RunError::EventLoop( format!( "EventLoop::try_new: {e}" ) ) )?;
|
||||
WaylandSource::new( conn.clone(), queue )
|
||||
.insert( event_loop.handle() )
|
||||
.map_err( |e| RunError::EventLoop( format!( "WaylandSource::insert: {e:?}" ) ) )?;
|
||||
|
||||
let compositor = CompositorState::bind( &globals, &qh )
|
||||
.map_err( |e| RunError::MissingProtocol { name: "wl_compositor", detail: format!( "{e:?}" ) } )?;
|
||||
let shm = Shm::bind( &globals, &qh )
|
||||
.map_err( |e| RunError::MissingProtocol { name: "wl_shm", detail: format!( "{e:?}" ) } )?;
|
||||
|
||||
// Try to bring EGL up. On failure (no libEGL, no compatible config,
|
||||
// LTK_FORCE_SOFTWARE=1, ES2/ES3 context creation refused…) we log the
|
||||
// reason and every surface falls back to the SHM path.
|
||||
let egl_context = match crate::egl_context::EglContext::new( &conn )
|
||||
{
|
||||
Ok( ctx ) => Some( std::sync::Arc::new( ctx ) ),
|
||||
Err( reason ) =>
|
||||
{
|
||||
crate::egl_context::log_software_fallback( &reason );
|
||||
None
|
||||
}
|
||||
};
|
||||
crate::render::set_software_render( egl_context.is_none() );
|
||||
|
||||
// Bind layer-shell up front. Both the main surface (when requested via
|
||||
// ShellMode::Layer) and every overlay returned by App::overlays() share
|
||||
// this single binding.
|
||||
let layer_shell_opt: Option<LayerShell> = LayerShell::bind( &globals, &qh ).ok();
|
||||
|
||||
// Backwards compatibility: window_config() overrides shell_mode()
|
||||
let force_window = app.window_config()
|
||||
.map( |( t, id )| ( t.to_string(), id.to_string() ) );
|
||||
|
||||
let bind_xdg = |globals: &smithay_client_toolkit::reexports::client::globals::GlobalList, qh: &smithay_client_toolkit::reexports::client::QueueHandle<AppData<A>>|
|
||||
-> Result<XdgShell, RunError>
|
||||
{
|
||||
XdgShell::bind( globals, qh )
|
||||
.map_err( |e| RunError::MissingProtocol { name: "xdg_wm_base", detail: format!( "{e:?}" ) } )
|
||||
};
|
||||
|
||||
// Pinning min == max is the standard idiom on xdg-shell for "I
|
||||
// want this exact size". Skipped when going fullscreen — Mutter
|
||||
// rejects the fullscreen request if a min_size constrains it.
|
||||
let apply_size_hint = |window: &smithay_client_toolkit::shell::xdg::window::Window|
|
||||
{
|
||||
if app.start_fullscreen() { return; }
|
||||
match app.window_size_hint()
|
||||
{
|
||||
Some( ( w, h ) ) =>
|
||||
{
|
||||
// `set_min_size` doubles as the suggested initial size:
|
||||
// most compositors send the first configure with this
|
||||
// value, after which the surface remains user-resizable
|
||||
// (the runtime adopts whatever size the compositor
|
||||
// supplies on subsequent configures). `set_max_size` is
|
||||
// deliberately *not* called — pinning min == max would
|
||||
// lock the toplevel and refuse user-initiated resize.
|
||||
window.set_min_size( Some( ( w, h ) ) );
|
||||
}
|
||||
None =>
|
||||
{
|
||||
window.set_min_size( Some( ( 360, 480 ) ) );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let apply_fullscreen = |window: &smithay_client_toolkit::shell::xdg::window::Window|
|
||||
{
|
||||
if app.start_fullscreen()
|
||||
{
|
||||
window.set_fullscreen( None );
|
||||
}
|
||||
};
|
||||
|
||||
let ( surface_kind, xdg_shell ) = if let Some( ( ref title, ref app_id ) ) = force_window
|
||||
{
|
||||
let xdg = bind_xdg( &globals, &qh )?;
|
||||
let surface = compositor.create_surface( &qh );
|
||||
let window = xdg.create_window( surface, WindowDecorations::RequestServer, &qh );
|
||||
window.set_title( title.as_str() );
|
||||
window.set_app_id( app_id.as_str() );
|
||||
apply_size_hint( &window );
|
||||
apply_fullscreen( &window );
|
||||
window.commit();
|
||||
( SurfaceKind::Window( window ), Some( xdg ) )
|
||||
} else {
|
||||
// Use shell_mode() to determine surface type
|
||||
use crate::app::ShellMode;
|
||||
match app.shell_mode()
|
||||
{
|
||||
ShellMode::Window => {
|
||||
let xdg = bind_xdg( &globals, &qh )?;
|
||||
let surface = compositor.create_surface( &qh );
|
||||
let window = xdg.create_window( surface, WindowDecorations::RequestServer, &qh );
|
||||
window.set_title( "ltk" );
|
||||
window.set_app_id( "ltk" );
|
||||
apply_size_hint( &window );
|
||||
window.commit();
|
||||
( SurfaceKind::Window( window ), Some( xdg ) )
|
||||
}
|
||||
ShellMode::Layer( layer ) => {
|
||||
if layer_shell_opt.is_some()
|
||||
{
|
||||
// Defer surface creation until new_output fires: if we create the layer
|
||||
// surface before the compositor has any output ready (e.g. sway startup),
|
||||
// the compositor cannot assign it and logs an error.
|
||||
let cfg = LayerConfig {
|
||||
layer: layer.to_wlr_layer(),
|
||||
exclusive_zone: app.exclusive_zone(),
|
||||
anchor: app.layer_anchor(),
|
||||
size: app.layer_size(),
|
||||
keyboard_exclusive: app.keyboard_exclusive(),
|
||||
namespace: "ltk-sctk",
|
||||
};
|
||||
( SurfaceKind::Pending( cfg ), None )
|
||||
} else {
|
||||
eprintln!( "ltk: wlr-layer-shell not available, falling back to xdg window" );
|
||||
let xdg = bind_xdg( &globals, &qh )?;
|
||||
let surface = compositor.create_surface( &qh );
|
||||
let window = xdg.create_window( surface, WindowDecorations::RequestServer, &qh );
|
||||
window.set_title( "ltk" );
|
||||
window.set_app_id( "ltk" );
|
||||
apply_size_hint( &window );
|
||||
window.commit();
|
||||
( SurfaceKind::Window( window ), Some( xdg ) )
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let text_input_manager: Option<ZwpTextInputManagerV3> = globals.bind( &qh, 1..=1, () ).ok();
|
||||
|
||||
let debug_layout = std::env::var( "LTK_DEBUG_LAYOUT" ).is_ok();
|
||||
|
||||
let titlebar_height = if force_window.is_some() { 36.0 } else { 0.0 };
|
||||
let titlebar_title = force_window.map( |( t, _ )| t ).unwrap_or_default();
|
||||
|
||||
let pending_fullscreen = app.start_fullscreen();
|
||||
|
||||
let mut data = AppData
|
||||
{
|
||||
app,
|
||||
registry_state: RegistryState::new( &globals ),
|
||||
seat_state: SeatState::new( &globals, &qh ),
|
||||
output_state: OutputState::new( &globals, &qh ),
|
||||
compositor_state: compositor,
|
||||
shm,
|
||||
egl_context,
|
||||
xdg_shell,
|
||||
layer_shell: layer_shell_opt,
|
||||
keyboard: None,
|
||||
pointer: None,
|
||||
touch: None,
|
||||
pointer_pos: Point::default(),
|
||||
cursor_shape_manager:
|
||||
smithay_client_toolkit::seat::pointer::cursor_shape::CursorShapeManager::bind( &globals, &qh ).ok(),
|
||||
cursor_shape_device: None,
|
||||
last_pointer_enter_serial: 0,
|
||||
current_cursor_shape: None,
|
||||
text_input_manager,
|
||||
text_input: None,
|
||||
shift_pressed: false,
|
||||
ctrl_pressed: false,
|
||||
loop_handle: event_loop.handle(),
|
||||
compositor_repeat_rate: 0,
|
||||
compositor_repeat_delay: 0,
|
||||
key_repeat: None,
|
||||
button_repeat: None,
|
||||
clipboard: String::new(),
|
||||
last_press_time: None,
|
||||
last_press_pos: None,
|
||||
debug_layout,
|
||||
pending_msgs: Vec::new(),
|
||||
pending_drag_inits: Vec::new(),
|
||||
qh: qh.clone(),
|
||||
last_pointer_serial: 0,
|
||||
last_input_serial: 0,
|
||||
exit_requested: false,
|
||||
pending_fullscreen,
|
||||
main: SurfaceState::<A::Message>::new( surface_kind, titlebar_height, titlebar_title ),
|
||||
overlays: std::collections::HashMap::new(),
|
||||
pointer_focus: SurfaceFocus::Main,
|
||||
keyboard_focus: SurfaceFocus::Main,
|
||||
touch_focus: std::collections::HashMap::new(),
|
||||
cached_view: None,
|
||||
cached_overlays: None,
|
||||
view_dirty: true,
|
||||
overlays_dirty: true,
|
||||
};
|
||||
|
||||
// Register a calloop channel so the app can send messages from any thread.
|
||||
// Messages sent through the Sender wake the event loop immediately.
|
||||
{
|
||||
let ( sender, channel ) = calloop::channel::channel::<A::Message>();
|
||||
event_loop.handle()
|
||||
.insert_source(
|
||||
channel,
|
||||
|event, _, data: &mut AppData<A>|
|
||||
{
|
||||
if let calloop::channel::Event::Msg( msg ) = event
|
||||
{
|
||||
// Just queue the message — the run loop will run
|
||||
// `App::invalidate_after` and apply the resulting
|
||||
// scope, which is what decides which surfaces (if
|
||||
// any) actually need to redraw.
|
||||
data.pending_msgs.push( msg );
|
||||
}
|
||||
},
|
||||
)
|
||||
.map_err( |e| RunError::EventLoop( format!( "channel insert_source: {e:?}" ) ) )?;
|
||||
data.app.set_channel_sender( sender );
|
||||
}
|
||||
|
||||
// Register a periodic timer if the app wants one (e.g. clock tick every second).
|
||||
// The timer fires independently of Wayland events, waking the event loop on schedule.
|
||||
if let Some( dur ) = data.app.poll_interval()
|
||||
{
|
||||
event_loop.handle()
|
||||
.insert_source(
|
||||
Timer::from_duration( dur ),
|
||||
|_, _, data: &mut AppData<A>|
|
||||
{
|
||||
let msgs = data.app.poll_external();
|
||||
data.pending_msgs.extend( msgs );
|
||||
let next = data.app.poll_interval()
|
||||
.unwrap_or( std::time::Duration::from_secs( 1 ) );
|
||||
TimeoutAction::ToDuration( next )
|
||||
},
|
||||
)
|
||||
.map_err( |e| RunError::EventLoop( format!( "poll timer insert_source: {e:?}" ) ) )?;
|
||||
}
|
||||
|
||||
while !data.exit_requested
|
||||
{
|
||||
// Sleep until something interesting fires:
|
||||
// * Wayland event (input, configure, frame callback, …)
|
||||
// * calloop timer (poll_external)
|
||||
// * calloop channel (App::set_channel_sender)
|
||||
// Pacing is driven by `wl_surface.frame` callbacks, so an idle /
|
||||
// off-screen / VRR display blocks indefinitely instead of polling
|
||||
// at a fixed rate. The one exception is a pending long-press:
|
||||
// cap the wait at its deadline so a perfectly still press still
|
||||
// fires on time.
|
||||
let timeout = data.next_long_press_wakeup();
|
||||
event_loop.dispatch( timeout, &mut data ).expect( "dispatch" );
|
||||
// Any surface whose press has now crossed `long_press_duration`
|
||||
// emits its stored message and flips into drag mode for the rest
|
||||
// of the gesture.
|
||||
data.check_long_press_deadlines();
|
||||
|
||||
// Poll external messages (immediate async results, e.g. PAM auth channel)
|
||||
let ext: Vec<_> = data.app.poll_external();
|
||||
data.pending_msgs.extend( ext );
|
||||
|
||||
// Process pending messages, folding their per-message invalidation
|
||||
// scopes into a single decision before applying it.
|
||||
let msgs: Vec<_> = data.pending_msgs.drain( .. ).collect();
|
||||
let had_msgs = !msgs.is_empty();
|
||||
if had_msgs
|
||||
{
|
||||
let mut scope = InvalidationScope::Only( Vec::new() );
|
||||
for msg in msgs
|
||||
{
|
||||
scope = scope.union( data.app.invalidate_after( &msg ) );
|
||||
data.app.update( msg );
|
||||
}
|
||||
apply_invalidation( &mut data, scope );
|
||||
// `update()` may have flipped the busy / loading flag
|
||||
// the app reads from inside `cursor_override`. Re-sync
|
||||
// the pointer cursor so the change propagates without
|
||||
// waiting for the next motion event.
|
||||
let pf = data.pointer_focus;
|
||||
data.dispatch_cursor_shape( pf );
|
||||
}
|
||||
// Seed `on_drag_move` with the long-press origin for any drag that
|
||||
// just started. Must run *after* `update()` so the app's drag state
|
||||
// has already been set up by the paired long-press message — the
|
||||
// coords would otherwise hit a dragging_app=None shell and be lost.
|
||||
let drag_inits: Vec<_> = data.pending_drag_inits.drain( .. ).collect();
|
||||
if !drag_inits.is_empty()
|
||||
{
|
||||
for origin in drag_inits
|
||||
{
|
||||
data.app.on_drag_move( origin.x, origin.y );
|
||||
}
|
||||
data.view_dirty = true;
|
||||
data.overlays_dirty = true;
|
||||
data.main.request_redraw();
|
||||
}
|
||||
// After update() the app state is the source of truth — discard any
|
||||
// pending text values so that the next keystroke reads the fresh state
|
||||
// instead of a stale pre-update buffer (e.g. password cleared on auth failure).
|
||||
if !data.main.pending_text_values.is_empty()
|
||||
{
|
||||
data.main.pending_text_values.clear();
|
||||
}
|
||||
for ss in data.overlays.values_mut()
|
||||
{
|
||||
ss.pending_text_values.clear();
|
||||
}
|
||||
|
||||
// Reconcile the overlay set against the app's current specs: drop any
|
||||
// overlays whose id disappeared, create new ones for ids that just
|
||||
// appeared. Specs are re-queried next frame for drawing.
|
||||
reconcile_overlays( &mut data );
|
||||
|
||||
// Draw any surface that's configured, dirty, and not already waiting
|
||||
// on a frame callback. The compositor decides our cadence — when no
|
||||
// surface qualifies we just loop back to `dispatch(None)` and sleep.
|
||||
let any_drawable = ( data.main.configured && data.main.needs_redraw && !data.main.frame_pending )
|
||||
|| data.overlays.values().any( |ss| ss.configured && ss.needs_redraw && !ss.frame_pending );
|
||||
if any_drawable
|
||||
{
|
||||
// Rebuild while motion is in progress and on the first frame
|
||||
// after it ends, so the settle frame paints at full quality
|
||||
// instead of freezing one step short of the target.
|
||||
// `wants_low_quality_paint` is the source of truth so
|
||||
// finger-tracked drags get the same treatment. Slider /
|
||||
// scroll drags are owned by the runtime gesture machine,
|
||||
// not by `App::update`, so OR with the gesture state to
|
||||
// pick up that motion signal too.
|
||||
let runtime_slider_motion =
|
||||
data.main.gesture.dragging_slider.is_some()
|
||||
|| data.overlays.values().any( |ss| ss.gesture.dragging_slider.is_some() );
|
||||
if runtime_slider_motion
|
||||
{
|
||||
data.view_dirty = true;
|
||||
data.overlays_dirty = true;
|
||||
}
|
||||
if data.view_dirty
|
||||
{
|
||||
data.cached_view = Some( data.app.view() );
|
||||
data.view_dirty = false;
|
||||
}
|
||||
if data.overlays_dirty
|
||||
{
|
||||
data.cached_overlays = Some( data.app.overlays() );
|
||||
data.overlays_dirty = false;
|
||||
}
|
||||
draw_frame( &mut data );
|
||||
}
|
||||
|
||||
// Focus the widget with the requested WidgetId if the app requests it
|
||||
if let Some( id ) = data.app.take_focus_request()
|
||||
{
|
||||
let found = data.main.widget_rects.iter()
|
||||
.find( |w| w.id == Some( id ) )
|
||||
.map( |w| w.flat_idx );
|
||||
if let Some( flat_idx ) = found
|
||||
{
|
||||
let qh = data.qh.clone();
|
||||
data.set_focus( SurfaceFocus::Main, Some( flat_idx ), &qh );
|
||||
data.main.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok( () )
|
||||
}
|
||||
|
||||
/// Apply a folded [`InvalidationScope`] from one message-batch iteration:
|
||||
/// dirty the relevant cache(s) so the next draw rebuilds the view via
|
||||
/// [`App::view`] / [`App::overlays`], and call `request_redraw` on each
|
||||
/// affected surface so the run loop's "anything to draw" check picks it up.
|
||||
///
|
||||
/// `InvalidationScope::All` (the default returned by `App::invalidate_after`)
|
||||
/// matches the pre-hook behaviour of broadcasting to every surface.
|
||||
fn apply_invalidation<A: App>( data: &mut AppData<A>, scope: InvalidationScope )
|
||||
{
|
||||
match scope
|
||||
{
|
||||
InvalidationScope::All =>
|
||||
{
|
||||
data.view_dirty = true;
|
||||
data.overlays_dirty = true;
|
||||
data.main.request_redraw();
|
||||
for ss in data.overlays.values_mut()
|
||||
{
|
||||
ss.request_redraw();
|
||||
}
|
||||
}
|
||||
InvalidationScope::Only( targets ) =>
|
||||
{
|
||||
for t in targets
|
||||
{
|
||||
match t
|
||||
{
|
||||
SurfaceTarget::Main =>
|
||||
{
|
||||
data.view_dirty = true;
|
||||
data.main.request_redraw();
|
||||
}
|
||||
SurfaceTarget::Overlay( id ) =>
|
||||
{
|
||||
// `overlays()` returns a single Vec, so any
|
||||
// per-overlay change forces the whole list to be
|
||||
// re-queried. Coarser than per-overlay caching but
|
||||
// matches the existing API shape.
|
||||
data.overlays_dirty = true;
|
||||
if let Some( ss ) = data.overlays.get_mut( &id )
|
||||
{
|
||||
ss.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pure overlay-id diff. Given the current set of live overlay ids and the
|
||||
/// list the app just returned from [`crate::app::App::overlays`], compute
|
||||
/// `( added, removed )`.
|
||||
///
|
||||
/// `added` preserves the order of `next` (so creation order is deterministic);
|
||||
/// `removed` is unordered (driven by HashMap iteration in the caller).
|
||||
pub fn diff_overlay_ids(
|
||||
current: impl IntoIterator<Item = crate::app::OverlayId>,
|
||||
next: &[ crate::app::OverlayId ],
|
||||
) -> ( Vec<crate::app::OverlayId>, Vec<crate::app::OverlayId> )
|
||||
{
|
||||
let current_set: HashSet<crate::app::OverlayId> = current.into_iter().collect();
|
||||
let next_set: HashSet<crate::app::OverlayId> = next.iter().copied().collect();
|
||||
|
||||
let added: Vec<_> = next.iter().copied().filter( |id| !current_set.contains( id ) ).collect();
|
||||
let removed: Vec<_> = current_set.into_iter().filter( |id| !next_set.contains( id ) ).collect();
|
||||
( added, removed )
|
||||
}
|
||||
|
||||
/// Sync `data.overlays` with the app's current `App::overlays()` spec list:
|
||||
/// destroy any overlay whose id disappeared and create a fresh `SurfaceState`
|
||||
/// for every id that just appeared. Created surfaces are materialized
|
||||
/// immediately if an output is already available; otherwise they stay
|
||||
/// [`SurfaceKind::Pending`] and the `new_output` handler will bring them up.
|
||||
fn reconcile_overlays<A: App>( data: &mut AppData<A> )
|
||||
{
|
||||
let specs = data.app.overlays();
|
||||
let next_ids: Vec<_> = specs.iter().map( |s| s.id ).collect();
|
||||
let wanted: HashSet<crate::app::OverlayId> = next_ids.iter().copied().collect();
|
||||
|
||||
// Drop overlays that disappeared from the spec list. If the overlay had
|
||||
// an in-flight drag (long-press fired), migrate that state to `main` so
|
||||
// the motion / release that follows still routes through the app's drag
|
||||
// handlers — apps typically hide the overlay *because* of the long-press,
|
||||
// and we must not lose the drag state with the surface.
|
||||
let removed: Vec<_> = data.overlays.keys()
|
||||
.filter( |id| !wanted.contains( id ) )
|
||||
.copied()
|
||||
.collect();
|
||||
for id in removed
|
||||
{
|
||||
if let Some( ss ) = data.overlays.remove( &id )
|
||||
{
|
||||
if ss.gesture.long_press_fired
|
||||
{
|
||||
data.main.gesture.long_press_fired = true;
|
||||
data.main.gesture.long_press_origin = ss.gesture.long_press_origin;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear any stale per-device focus pointing at a destroyed overlay.
|
||||
if let SurfaceFocus::Overlay( id ) = data.pointer_focus
|
||||
{
|
||||
if !data.overlays.contains_key( &id ) { data.pointer_focus = SurfaceFocus::Main; }
|
||||
}
|
||||
if let SurfaceFocus::Overlay( id ) = data.keyboard_focus
|
||||
{
|
||||
if !data.overlays.contains_key( &id ) { data.keyboard_focus = SurfaceFocus::Main; }
|
||||
}
|
||||
// Rewrite touch_focus entries pointing at a destroyed overlay to Main
|
||||
// rather than dropping them. Dropping would make subsequent motion/up
|
||||
// events for the same touch id default to Main *without* the long-press
|
||||
// drag state, turning a release into a stray tap.
|
||||
for f in data.touch_focus.values_mut()
|
||||
{
|
||||
if let SurfaceFocus::Overlay( id ) = *f
|
||||
{
|
||||
if !data.overlays.contains_key( &id ) { *f = SurfaceFocus::Main; }
|
||||
}
|
||||
}
|
||||
|
||||
// Create overlays that just appeared. Uses field-level borrow splitting so
|
||||
// the shared borrows of `layer_shell` / `xdg_shell` / `compositor_state`
|
||||
// / `output_state` / `qh` can coexist with the mutable borrow of
|
||||
// `overlays` inside the loop.
|
||||
let layer_shell_opt = data.layer_shell.as_ref();
|
||||
let xdg_shell_opt = data.xdg_shell.as_ref();
|
||||
let output_opt = data.output_state.outputs().next();
|
||||
let cs = &data.compositor_state;
|
||||
let qh = &data.qh;
|
||||
let grab_seat = data.seat_state.seats().next();
|
||||
let grab_serial = data.last_input_serial;
|
||||
// Snapshot the parent xdg_surface (only an xdg toplevel can host
|
||||
// xdg-popups; layer-shell parents would need a different code path
|
||||
// via `LayerSurface::get_popup`, which we do not currently support).
|
||||
let parent_xdg = match data.main.surface
|
||||
{
|
||||
SurfaceKind::Window( ref w ) => Some( w.xdg_surface().clone() ),
|
||||
_ => None,
|
||||
};
|
||||
let parent_scale = data.main.scale_factor.max( 1 ) as f32;
|
||||
// Snapshot the previous-frame anchor lookup table so we can resolve
|
||||
// `anchor_widget_id` → `Rect` without holding a borrow on `data.main`
|
||||
// across the overlay-mut loop below.
|
||||
let main_widget_rects = &data.main.widget_rects;
|
||||
let overlays_m = &mut data.overlays;
|
||||
for spec in &specs
|
||||
{
|
||||
if let Some( ss ) = overlays_m.get_mut( &spec.id )
|
||||
{
|
||||
// Already-live overlay: propagate size changes to the layer
|
||||
// surface so apps can animate an overlay's dimensions (e.g. a
|
||||
// slide-down panel whose height grows each frame). The very
|
||||
// first size was applied at `materialize` time and recorded in
|
||||
// `last_requested_size`, so we only commit when it actually
|
||||
// differs. Commit is needed after `set_size` so the compositor
|
||||
// sends a configure; the usual `on_configure` path picks up the
|
||||
// new dimensions and drives the redraw. Popups don't grow /
|
||||
// shrink mid-life — close and reopen instead.
|
||||
if spec.size != ss.last_requested_size
|
||||
{
|
||||
if let SurfaceKind::Layer( ref layer_surface ) = ss.surface
|
||||
{
|
||||
layer_surface.set_size( spec.size.0, spec.size.1 );
|
||||
layer_surface.commit();
|
||||
ss.last_requested_size = spec.size;
|
||||
}
|
||||
}
|
||||
// Compare the anchor at integer logical-pixel resolution:
|
||||
// floating-point jitter would otherwise call `reposition`
|
||||
// every frame, which several compositors react to by
|
||||
// dropping the popup grab.
|
||||
if let SurfaceKind::Popup( ref popup ) = ss.surface
|
||||
{
|
||||
if let ( Some( anchor_id ), Some( xdg_shell ) ) = ( spec.anchor_widget_id, xdg_shell_opt )
|
||||
{
|
||||
if let Some( anchor_rect ) = main_widget_rects.iter()
|
||||
.find( |w| w.id == Some( anchor_id ) )
|
||||
.map( |w| w.rect )
|
||||
{
|
||||
let to_logical = | r: Rect |
|
||||
{
|
||||
(
|
||||
( r.x / parent_scale ).round() as i32,
|
||||
( r.y / parent_scale ).round() as i32,
|
||||
( r.width / parent_scale ).round().max( 1.0 ) as i32,
|
||||
( r.height / parent_scale ).round().max( 1.0 ) as i32,
|
||||
)
|
||||
};
|
||||
let new_q = to_logical( anchor_rect );
|
||||
let moved = ss.last_popup_anchor.map( |r| to_logical( r ) != new_q ).unwrap_or( true );
|
||||
if moved
|
||||
{
|
||||
if let Ok( positioner ) = XdgPositioner::new( xdg_shell )
|
||||
{
|
||||
let ( ax, ay, aw, ah ) = new_q;
|
||||
let ( spec_w, spec_h ) = spec.size;
|
||||
let popup_w = if spec_w == 0 { aw } else { spec_w.max( 1 ) as i32 };
|
||||
let popup_h = spec_h.max( 1 ) as i32;
|
||||
positioner.set_size( popup_w, popup_h );
|
||||
positioner.set_anchor_rect( ax, ay, aw, ah );
|
||||
positioner.set_anchor( PositionerAnchor::Bottom );
|
||||
positioner.set_gravity( Gravity::Bottom );
|
||||
positioner.set_constraint_adjustment(
|
||||
ConstraintAdjustment::FlipY | ConstraintAdjustment::SlideX,
|
||||
);
|
||||
ss.popup_reposition_token = ss.popup_reposition_token.wrapping_add( 1 );
|
||||
popup.reposition( &positioner, ss.popup_reposition_token );
|
||||
ss.last_popup_anchor = Some( anchor_rect );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// `anchor_widget_id = Some(_)` → xdg-popup path; `None` →
|
||||
// wlr-layer-shell path.
|
||||
if let Some( anchor_id ) = spec.anchor_widget_id
|
||||
{
|
||||
let Some( xdg_shell ) = xdg_shell_opt else
|
||||
{
|
||||
eprintln!( "ltk: ignoring popup overlay {:?} — main surface is not an xdg-shell window", spec.id );
|
||||
continue;
|
||||
};
|
||||
let Some( ref parent ) = parent_xdg else
|
||||
{
|
||||
eprintln!( "ltk: ignoring popup overlay {:?} — no xdg parent surface", spec.id );
|
||||
continue;
|
||||
};
|
||||
let Some( anchor_rect ) = main_widget_rects.iter()
|
||||
.find( |w| w.id == Some( anchor_id ) )
|
||||
.map( |w| w.rect )
|
||||
else
|
||||
{
|
||||
eprintln!( "ltk: popup overlay {:?} could not find anchor widget id {:?}", spec.id, anchor_id );
|
||||
continue;
|
||||
};
|
||||
let positioner = match XdgPositioner::new( xdg_shell )
|
||||
{
|
||||
Ok( p ) => p,
|
||||
Err( e ) =>
|
||||
{
|
||||
eprintln!( "ltk: XdgPositioner::new failed for popup {:?}: {e}", spec.id );
|
||||
continue;
|
||||
}
|
||||
};
|
||||
// Convert the requested popup size and the anchor rect from
|
||||
// physical pixels (the layout coordinate space) to logical
|
||||
// pixels — the positioner expresses everything in window
|
||||
// geometry, which is logical. `size.0 == 0` is the
|
||||
// "match anchor width" convention (paralleling the
|
||||
// layer-shell `0 = fill` semantic): the popup is sized to
|
||||
// the trigger pill so combos / selects render flush with
|
||||
// the field they belong to.
|
||||
let ax = ( anchor_rect.x / parent_scale ).round() as i32;
|
||||
let ay = ( anchor_rect.y / parent_scale ).round() as i32;
|
||||
let aw = ( anchor_rect.width / parent_scale ).round().max( 1.0 ) as i32;
|
||||
let ah = ( anchor_rect.height / parent_scale ).round().max( 1.0 ) as i32;
|
||||
let ( spec_w, spec_h ) = spec.size;
|
||||
let popup_w = if spec_w == 0 { aw } else { spec_w.max( 1 ) as i32 };
|
||||
let popup_h = spec_h.max( 1 ) as i32;
|
||||
positioner.set_size( popup_w, popup_h );
|
||||
positioner.set_anchor_rect( ax, ay, aw, ah );
|
||||
positioner.set_anchor( PositionerAnchor::Bottom );
|
||||
positioner.set_gravity( Gravity::Bottom );
|
||||
positioner.set_constraint_adjustment(
|
||||
ConstraintAdjustment::FlipY | ConstraintAdjustment::SlideX,
|
||||
);
|
||||
// xdg_popup.grab must be issued before the first commit
|
||||
// (error 0 `invalid_grab` otherwise). `Popup::new` commits
|
||||
// internally, so the lower-level `from_surface` path is
|
||||
// the only one that lets the grab land in time.
|
||||
let surface = match Surface::new( cs, qh )
|
||||
{
|
||||
Ok( s ) => s,
|
||||
Err( e ) =>
|
||||
{
|
||||
eprintln!( "ltk: Surface::new failed for popup overlay {:?}: {e}", spec.id );
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let popup = match Popup::from_surface( Some( parent ), &positioner, qh, surface, xdg_shell )
|
||||
{
|
||||
Ok( p ) => p,
|
||||
Err( e ) =>
|
||||
{
|
||||
eprintln!( "ltk: Popup::from_surface failed for overlay {:?}: {e}", spec.id );
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Some( ref seat ) = grab_seat
|
||||
{
|
||||
popup.xdg_popup().grab( seat, grab_serial );
|
||||
}
|
||||
popup.wl_surface().commit();
|
||||
let mut ss = SurfaceState::<A::Message>::new( SurfaceKind::Popup( popup ), 0.0, String::new() );
|
||||
ss.last_requested_size = spec.size;
|
||||
ss.last_popup_anchor = Some( anchor_rect );
|
||||
overlays_m.insert( spec.id, ss );
|
||||
continue;
|
||||
}
|
||||
// wlr-layer-shell path.
|
||||
let Some( layer_shell ) = layer_shell_opt else
|
||||
{
|
||||
eprintln!( "ltk: ignoring layer-shell overlay {:?} — wlr-layer-shell not available", spec.id );
|
||||
continue;
|
||||
};
|
||||
let cfg = LayerConfig
|
||||
{
|
||||
layer: spec.layer.to_wlr_layer(),
|
||||
exclusive_zone: spec.exclusive_zone,
|
||||
anchor: spec.anchor,
|
||||
size: spec.size,
|
||||
keyboard_exclusive: spec.keyboard_exclusive,
|
||||
namespace: "ltk-overlay",
|
||||
};
|
||||
let mut surface = SurfaceKind::Pending( cfg );
|
||||
if let Some( ref output ) = output_opt
|
||||
{
|
||||
surface.materialize( cs, layer_shell, qh, output );
|
||||
}
|
||||
let mut ss = SurfaceState::<A::Message>::new( surface, 0.0, String::new() );
|
||||
ss.last_requested_size = spec.size;
|
||||
overlays_m.insert( spec.id, ss );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user