refactor: split every monolithic module into focused submodules
Each source file that had grown beyond a single concern is replaced by an identically-named directory containing focused submodules. `src/event_loop/mod.rs` (878 lines) becomes a directory with clipboard, context_menu, cursor_shape, drag, focus, handlers, invalidation, overlays_reconcile, repeat, run, surface, text_editing, and tooltip. Every widget, input handler, and theme component follows the same split. Public interfaces are unchanged — only the internal file layout moves. image bumped from 0.25.2 to 0.25.9.
This commit is contained in:
289
src/event_loop/overlays_reconcile.rs
Normal file
289
src/event_loop/overlays_reconcile.rs
Normal file
@@ -0,0 +1,289 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-only
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use smithay_client_toolkit::
|
||||
{
|
||||
compositor::Surface,
|
||||
shell::
|
||||
{
|
||||
WaylandSurface,
|
||||
xdg::
|
||||
{
|
||||
XdgPositioner, XdgSurface,
|
||||
popup::Popup,
|
||||
},
|
||||
},
|
||||
};
|
||||
use wayland_protocols::xdg::shell::client::xdg_positioner::
|
||||
{
|
||||
Anchor as PositionerAnchor,
|
||||
ConstraintAdjustment,
|
||||
Gravity,
|
||||
};
|
||||
|
||||
use crate::app::App;
|
||||
use crate::types::Rect;
|
||||
use super::{ AppData, LayerConfig, SurfaceFocus, SurfaceKind, SurfaceState };
|
||||
|
||||
/// 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.
|
||||
pub( super ) fn reconcile_overlays<A: App>( data: &mut AppData<A> )
|
||||
{
|
||||
let mut specs = data.app.overlays();
|
||||
if let Some( ts ) = data.tooltip_overlay() { specs.push( ts ); }
|
||||
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;
|
||||
ss.layer_anchor = Some( spec.anchor );
|
||||
overlays_m.insert( spec.id, ss );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user