use smithay_client_toolkit::
{
compositor::CompositorState,
output::OutputState,
registry::RegistryState,
seat::SeatState,
shell::
{
WaylandSurface,
wlr_layer::LayerShell,
xdg::
{
XdgShell,
window::WindowDecorations,
},
},
shm::Shm,
subcompositor::SubcompositorState,
};
use smithay_client_toolkit::reexports::client::Connection;
use smithay_client_toolkit::reexports::calloop::EventLoop;
use smithay_client_toolkit::reexports::calloop_wayland_source::WaylandSource;
use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3;
use crate::app::App;
use crate::event_loop::{ AppData, RunError, Sources, SurfaceState };
use super::{ LayerConfig, SurfaceKind, WaylandPlatform, WaylandSurfaceState };
use super::overlays_reconcile::reconcile_overlays;
use super::subsurface::reconcile_subsurfaces;
pub( crate ) fn run<A: App>( app: A )
{
if let Err( e ) = try_run( app )
{
panic!( "ltk::run failed during init: {e}" );
}
}
pub( crate ) fn try_run<A: App>( mut 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:?}" ) ) )?;
super::session::install_signal_source( &event_loop.handle() )?;
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:?}" ) } )?;
let subcompositor = SubcompositorState::bind( compositor.wl_compositor().clone(), &globals, &qh ).ok();
let egl_context = match crate::egl_context::EglContext::new( &conn )
{
#[ allow( clippy::arc_with_non_send_sync ) ]
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() );
let layer_shell_opt: Option<LayerShell> = LayerShell::bind( &globals, &qh ).ok();
let force_window = app.window_config()
.map( |( t, id )| ( t.to_string(), id.to_string() ) );
let session_enabled = force_window.is_some() || matches!( app.shell_mode(), crate::app::ShellMode::Window );
let mut session = if session_enabled
{
super::session::SessionRuntime::bootstrap( &mut app )
} else {
super::session::SessionRuntime::disabled()
};
if session_enabled
{
session.bind( &globals, &qh );
}
let app_id = app.app_id().to_string();
if let Some( ( _, ref cfg_id ) ) = force_window
{
if cfg_id != &app_id
{
eprintln!( "ltk: window_config app_id {cfg_id:?} differs from App::app_id {app_id:?}; App::app_id wins" );
}
}
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:?}" ) } )
};
let prefitted_size_hint = app.window_size_hint()
.filter( |_| !app.start_fullscreen() )
.and_then( |hint| super::output_probe::output_logical_size( &conn, &globals ).map( |output| super::window_fit::fit_within( hint, super::window_fit::output_share( output ) ) ) );
let apply_size_hint = |window: &smithay_client_toolkit::shell::xdg::window::Window|
{
if app.start_fullscreen() { return; }
match prefitted_size_hint.or( app.window_size_hint() )
{
Some( ( w, h ) ) =>
{
window.set_min_size( Some( ( w, h ) ) );
window.set_max_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 make_window = |xdg: &XdgShell, title: &str, session: &mut super::session::SessionRuntime, attach: bool|
{
let surface = compositor.create_surface( &qh );
let window = xdg.create_window( surface, WindowDecorations::RequestServer, &qh );
window.set_title( title );
window.set_app_id( app_id.as_str() );
apply_size_hint( &window );
apply_fullscreen( &window );
if attach
{
session.attach_toplevel( &window, &qh );
}
window.commit();
window
};
let ( surface_kind, xdg_shell ) = if let Some( ( ref title, _ ) ) = force_window
{
let xdg = bind_xdg( &globals, &qh )?;
let window = make_window( &xdg, title.as_str(), &mut session, true );
( SurfaceKind::Window( window ), Some( xdg ) )
} else {
use crate::app::ShellMode;
match app.shell_mode()
{
ShellMode::SessionLock => {
( SurfaceKind::PendingLock, None )
}
ShellMode::Window => {
let xdg = bind_xdg( &globals, &qh )?;
let window = make_window( &xdg, &app.window_title(), &mut session, true );
( SurfaceKind::Window( window ), Some( xdg ) )
}
ShellMode::Layer( layer ) => {
if layer_shell_opt.is_some()
{
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 window = make_window( &xdg, &app.window_title(), &mut session, false );
( SurfaceKind::Window( window ), Some( xdg ) )
}
}
}
};
let xdg_shell = xdg_shell.or_else( || XdgShell::bind( &globals, &qh ).ok() );
let session_lock =
if force_window.is_none() && matches!( app.shell_mode(), crate::app::ShellMode::SessionLock )
{
smithay_client_toolkit::session_lock::SessionLockState::new( &globals, &qh )
.lock( &qh )
.ok()
} else {
None
};
let text_input_manager: Option<ZwpTextInputManagerV3> = globals.bind( &qh, 1..=1, () ).ok();
let builtin_osk = text_input_manager.is_none() || std::env::var_os( "LTK_BUILTIN_OSK" ).is_some_and( |v| v == "1" );
let data_device_manager =
smithay_client_toolkit::data_device_manager::DataDeviceManagerState::bind( &globals, &qh ).ok();
let primary = super::primary_selection::PrimarySelection::new(
smithay_client_toolkit::primary_selection::PrimarySelectionManagerState::bind( &globals, &qh ).ok() );
let ( clipboard_inbox_tx, clipboard_inbox_rx ) = std::sync::mpsc::channel();
let ( drop_inbox_tx, drop_inbox_rx ) = super::data_device::drop_inbox();
let a11y = crate::a11y::A11yState::try_new( &app_id, &app_id );
let activation_state =
smithay_client_toolkit::activation::ActivationState::bind( &globals, &qh ).ok();
let activation_token_pending = std::env::var( "XDG_ACTIVATION_TOKEN" ).ok().filter( |t| !t.is_empty() );
if activation_token_pending.is_some()
{
unsafe { std::env::remove_var( "XDG_ACTIVATION_TOKEN" ); }
}
let foreign_toplevel_list = smithay_client_toolkit::foreign_toplevel_list::ForeignToplevelList::new( &globals, &qh );
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_size_hint_unpin = !app.start_fullscreen() && app.window_size_hint().is_some() && app.window_resizable();
let pending_size_hint_fit = !app.start_fullscreen() && app.window_size_hint().is_some() && prefitted_size_hint.is_none();
let platform = WaylandPlatform
{
registry_state: RegistryState::new( &globals ),
seat_state: SeatState::new( &globals, &qh ),
output_state: OutputState::new( &globals, &qh ),
compositor_state: compositor,
subcompositor,
shm,
egl_context,
session_lock,
xdg_shell,
layer_shell: layer_shell_opt,
keyboard: None,
pointer: None,
touch: None,
cursor_shape_manager:
smithay_client_toolkit::seat::pointer::cursor_shape::CursorShapeManager::bind( &globals, &qh ).ok(),
cursor_shape_device: None,
last_pointer_enter_serial: 0,
last_pointer_serial: 0,
last_input_serial: 0,
text_input_manager,
text_input: None,
activation_state,
activation_token_pending,
session,
data_device_manager,
data_device: None,
clipboard_source: None,
primary,
clipboard_inbox_tx,
clipboard_inbox_rx,
drop_position: None,
drop_mime: None,
drop_inbox_tx,
drop_inbox_rx,
pending_fullscreen: app.start_fullscreen(),
pending_size_hint_unpin,
pending_size_hint_fit,
fitted_size_hint: prefitted_size_hint,
canvas_pool: Vec::new(),
foreign_toplevel_list,
main_output: None,
subsurfaces: std::collections::HashMap::new(),
subsurface_gles_canvas: None,
qh: qh.clone(),
};
let mut main_wl = WaylandSurfaceState::from( surface_kind );
main_wl.titlebar_base = titlebar_height;
let main = SurfaceState::new( main_wl, titlebar_height, titlebar_title );
let mut data = AppData::new( app, platform, main, Sources::new( event_loop.handle() ), builtin_osk, a11y );
data.install_sources()?;
if data.platform.session.store.is_some()
{
super::session::install_save_timer( &event_loop.handle() )?;
}
while !data.exit_requested
{
let timeout = data.next_wakeup();
match event_loop.dispatch( timeout, &mut data )
{
Ok( () ) => {},
Err( calloop::Error::IoError( ref e ) )
if matches!( e.kind(), std::io::ErrorKind::BrokenPipe | std::io::ErrorKind::ConnectionReset ) =>
{
if let Some( pe ) = conn.protocol_error()
{
eprintln!(
"ltk: wayland protocol error — interface={} object_id={} code={}: {}",
pe.object_interface, pe.object_id, pe.code, pe.message
);
}
else
{
eprintln!( "ltk: wayland connection lost ({e}); exiting" );
}
data.exit_requested = true;
continue;
}
Err( calloop::Error::OtherError( ref e ) ) =>
{
let mut src: Option<&dyn std::error::Error> = Some( e.as_ref() );
let mut is_closed = false;
while let Some( err ) = src
{
if let Some( io ) = err.downcast_ref::<std::io::Error>()
{
if matches!( io.kind(),
std::io::ErrorKind::BrokenPipe | std::io::ErrorKind::ConnectionReset )
{
is_closed = true;
break;
}
}
src = err.source();
}
if is_closed
{
eprintln!( "ltk: wayland connection lost; exiting" );
data.exit_requested = true;
continue;
}
panic!( "dispatch: {e:?}" );
}
Err( e ) => panic!( "dispatch: {e:?}" ),
}
while let Ok( text ) = data.platform.clipboard_inbox_rx.try_recv()
{
data.clipboard = text;
}
if let Some( text ) = data.platform.primary.drain()
{
data.primary_selection = text;
}
while let Ok( p ) = data.platform.drop_inbox_rx.try_recv()
{
data.app.on_drop_received( p.x as f32, p.y as f32, &p.mime, &p.text );
}
request_activation_tokens( &mut data );
data.before_draw();
if data.app.requested_exit()
{
if let Some( lock ) = data.platform.session_lock.take()
{
lock.unlock();
let _ = conn.roundtrip();
}
}
reconcile_overlays( &mut data );
if !data.first_frame_committed
{
reconcile_subsurfaces( &mut data );
}
let drawn = data.draw_due();
reconcile_subsurfaces( &mut data );
data.after_draw( drawn );
}
data.platform.session.on_exit( &data.app );
Ok( () )
}
fn request_activation_tokens<A: App>( data: &mut AppData<A> )
{
for tag in data.app.take_activation_requests()
{
match data.platform.activation_state
{
Some( ref activation ) =>
{
let req = smithay_client_toolkit::activation::RequestData {
app_id: Some( tag ),
seat_and_serial: None,
surface: data.main.platform.surface.try_wl_surface().cloned(),
};
activation.request_token::<AppData<A>>( &data.platform.qh, req );
}
None =>
{
if let Some( msg ) = data.app.on_activation_token( tag, String::new() )
{
data.pending_msgs.push( msg );
}
}
}
}
}