exwlshell

repository·master·Indexed 40 days ago

https://github.com/waycrate/exwlshelleventloop

A collection of tools and bindings for creating Wayland shell components such as docks, panels, and session locks. It provides an extra shell binding for layershell with a winit-like event loop and includes iced-exwlshell for integration with the Iced GUI framework, supporting LayerShell, SessionLock, and InputPanel protocols.

Tokens
35.3K
Snippets
95
Records
143
Agent score
97%

What's inside exwlshell

  1. Overview of waycrate_xkbkeycode

    master
    The waycrate_xkbkeycode crate provides a simplified interface for handling xkbcommon in Rust. It is primarily designed to be used as a dependency in layershellev and sessionlockev projects to manage keyboard state and keycodes without the complexity of direct xkbcommon integration. Much of the implementation logic is derived from winit.
  2. Overview of exwlshelleventloop

    master

    exwlshelleventloop is a project designed to enable the creation of Wayland shell programs (such as docks, panels, and session locks) using the iced GUI framework. It provides event loops and bindings for ext-session-lock and layershell, following a pattern similar to winit where messages are stored and handled via callbacks.

    The project is composed of several specialized subprojects that handle different aspects of Wayland shell integration and Iced bindings.

  3. Handle new surfaces and monitor changes

    master

    To interact with the Wayland environment, iced-exwlshell provides two ways to track surfaces and monitor information:

    1. Synchronous Surface Mapping: Use the on_new_shell hook in your Settings. This allows you to map a newly created surface to one of your own Message types before its first frame is drawn.
    2. Asynchronous Broadcasting: For asynchronous updates, pass a sender from iced_wayland_subscriber::shell::channel() to Settings::shell_broadcast.

    Subscribing to this broadcast provides information about:

    • Monitors: via OutputAdded, OutputUpdated, and OutputRemoved events.
    • Window Placement: via WindowOutputChanged, which reports which monitor a specific window is currently on.

    To detect when a window is removed, listen to standard iced window events.

  4. How to integrate custom Wayland events into Iced

    master

    Because iced does not natively support adding custom system actions or events, this project uses a technique involving generic type restrictions on the Message type. This allows messages to be transformed into events or actions during the event loop.

    To use this, you must:

    1. Use the provided macros.
    2. Add extra fields to your application's Message enum to accommodate these custom events/actions.
  5. Use `on_new_shell` for synchronous surface handling

    master

    Because the shell broadcast is asynchronous, a subscription might deliver a NewShell event after your view(id) function has already attempted to run for a new window.

    If your application's first frame depends on knowing the shell type or window info, use the .on_new_shell hook on the daemon builder. This hook runs synchronously at the moment of surface creation and feeds the information into your update function before anything is drawn.

    // Use this on the daemon builder when timing matters
    .on_new_shell(|info: ShellInfo| {
        matches!(info.shell, ShellType::SessionLock).then(|| Message::LockAppeared(info.window))
    })
  6. Subprojects in exwlshelleventloop

    master

    The project is divided into four main crates, each serving a specific purpose in the Wayland/Iced ecosystem:

    • waycrate_xkbkeycode: Handles xkbcommon events, heavily inspired by winit.
    • exwlshellev: A unified event loop containing support for layershell, sessionlock, and input-panel. This serves as the foundation for iced_exwlshell.
    • iced_exwlshell: Provides full bindings for iced, allowing you to build shell programs like locks, docks, and more. It supports opening new layershell windows, popups, ext-virtual-keyboard, and sessionlock.
    • iced_wayland_subscriber: Provides iced subscriptions for various Wayland events, such as ext-workspace and output changes, allowing your application to listen to system-level Wayland events.
  7. Untitled record

    master

    The daemon function provides a high-level way to create a program by passing it the core application logic. It manages the lifecycle of the application and integrates with the Wayland shell.

    To use daemon, you provide:

    1. A factory function to create your application state.
    2. A namespace function.
    3. An update function.
    4. A view function.

    You can then chain configuration methods like .title(), .subscription(), .on_new_shell(), and .settings() before calling .run().

    Monitoring Shell Surfaces

    There are two primary ways to react to shell surfaces (windows/layers) created by the runtime:

    1. on_new_shell (Synchronous): This hook runs synchronously before a surface's first frame is rendered. This is critical for use cases like session locks, where the initial frame must be correct to prevent flickering or security bypasses.
    2. Settings::shell_broadcast (Asynchronous): This is the asynchronous counterpart. You pass the sending end of iced_wayland_subscriber::shell::channel() to the Settings object, and keep the receiving end (ShellReceiver) in your application state. You can then use this receiver in your subscription to listen for surface and monitor changes as they happen.

    Example Usage

    use iced_exwlshell::daemon;
    use iced_exwlshell::settings::{Settings, LayerShellSettings};
    use iced_wayland_subscriber::shell::{ShellReceiver, ShellEvent};
    
    // ... inside main or setup ...
    
    // 1. Create the channel
    let (shell_broadcast, shell_events) = iced_wayland_subscriber::shell::channel();
    
    // 2. Initialize daemon
    daemon(
        move || MyCounter::new("hello", shell_events.clone()),
        MyCounter::namespace,
        MyCounter::update,
        MyCounter::view,
    )
    .on_new_shell(|info: ShellInfo| {
        // Handle synchronous events like session locks
        matches!(info.shell, ShellType::SessionLock).then(|| Message::LockAppeared(info.window))
    })
    .settings(Settings {
        shell_broadcast,
        // ... other settings
        ..Default::default()
    })
    .run()
    use iced_exwlshell::daemon;
    use iced_exwlshell::settings::{Settings, LayerShellSettings};
    use iced_wayland_subscriber::shell::{ShellReceiver, ShellEvent};
    
    // The runtime gets the sending end, the application keeps the receiving one.
    let (shell_broadcast, shell_events) = iced_wayland_subscriber::shell::channel();
    
    daemon(
        move || Counter::new("hello", shell_events.clone()),
        Counter::namespace,
        Counter::update,
        Counter::view,
    )
    .title(Counter::title)
    .subscription(Counter::subscription)
    .on_new_shell(|info: ShellInfo| {
        matches!(info.shell, ShellType::SessionLock).then(|| Message::LockAppeared(info.window))
    })
    .settings(Settings {
        layer_settings: LayerShellSettings {
            size: LayerSize::fill_width(400),
            exclusive_zone: 400,
            anchor: Anchor::Bottom | Anchor::Left | Anchor::Right,
            start_mode: StartMode::AllScreens,
            ..Default::default()
        },
        with_connection: Some(connection.into()),
        shell_broadcast,
        ..Default::default()
    })
    .run()
  8. Handle Shell events via hooks and channels

    master

    The ShellInfo trait and NewShell variant have been replaced by two distinct mechanisms:

    1. Synchronous Hook (Before first frame)

    Use .on_new_shell on the daemon to react to new shells before the surface's first frame.

    daemon(...).on_new_shell(|info: ShellInfo| {
        matches!(info.shell, ShellType::SessionLock)
            .then(|| Message::LockAppeared(info.window))
    })

    2. Asynchronous Channel (Runtime-wide)

    Use iced_wayland_subscriber::shell::channel() to create a broadcast/event pair. This works for iced_layershell, iced_exwlshell, and iced_sessionlock.

    let (shell_broadcast, shell_events) = iced_wayland_subscriber::shell::channel();
    
    application(move || App::new(shell_events.clone()), ...)
        .settings(Settings { shell_broadcast, ..Default::default() })

    ShellEvent Variants:

    • NewShell(info) (contains info.window, info.shell)
    • Closed(id)
    • WindowOutputChanged { window, output }
    • OutputAdded(info)
    • OutputUpdated(info)
    • OutputRemoved(info)

    ShellType Variants: LayerShell, PopUp, XdgTopLevel, InputPanel, SessionLock.

    Note: Late subscribers receive a replay of current monitors, live shells, and their outputs before new events.

    self.shell_events.listen().filter_map(|event| match event {
        ShellEvent::NewShell(info) => {
            // Handle new shell
            Some(Message::HandleNew(info.window, info.shell))
        },
        ShellEvent::Closed(id) => Some(Message::WindowClosed(id)),
        _ => None,
    })
  9. Use Shell Broadcast to subscribe to Wayland events

    master

    The shell::channel() function provides a sender/receiver pair used to bridge the shell runtime and your iced application. You must pass the sender to the runtime via Settings::shell_broadcast and keep the receiver in your application to subscribe to Wayland events like new surfaces, closed windows, and monitor changes.

    Subscribing late is safe: the channel replays current monitors, live shells, and their outputs to new subscribers before any new events are emitted, ensuring the application state is synchronized even if it wasn't listening from the first frame.

    Available ShellEvents

    EventMeaning
    NewShell(ShellInfo)A new surface was created. Contains window: Id and shell: ShellType.
    Closed(Id)A window has been closed.
    WindowOutputChanged { window, output }A window moved to a different monitor (None if unknown).
    OutputAdded(OutputInfo)A monitor was connected.
    OutputUpdated(OutputInfo)Monitor mode, scale, name, or position changed.
    OutputRemoved(OutputInfo)A monitor was disconnected.

    ShellType allows you to distinguish between LayerShell, PopUp, XdgTopLevel, InputPanel, and SessionLock without manually tracking IDs.

    use iced_layershell::daemon;
    use iced_layershell::settings::Settings;
    use iced_wayland_subscriber::shell::{ShellEvent, ShellReceiver};
    
    pub fn main() -> Result<(), iced_layershell::Error> {
        // 1. Create the channel
        let (shell_broadcast, shell_events) = iced_wayland_subscriber::shell::channel();
    
        daemon(
            move || Counter::new(shell_events.clone()),
            Counter::namespace,
            Counter::update,
            Counter::view,
        )
        .subscription(Counter::subscription)
        .settings(Settings {
            shell_broadcast, // 2. Pass sender to settings
            ..Default::default()
        })
        .run()
    }
    
    impl Counter {
        fn subscription(&self) -> iced::Subscription<Message> {
            // 3. Listen to events in your subscription
            self.shell_events.listen().map(Message::Shell)
        }
    }
  10. Use Workspaces feature in iced_wayland_subscriber

    master

    Workspaces support is available behind the workspace feature flag in iced_wayland_subscriber.

    Setup:

    1. Enable the feature in Cargo.toml:
      iced_wayland_subscriber = { version = "...", features = ["workspace"] }
    2. Listen to workspace events:
      iced_wayland_subscriber::workspace::listen(connection.clone())

    Behavior:

    • WorkspaceEvent::Updated carries an Arc<WorkspaceSnapshot>.
    • Unsupported events do not end the subscription; they wait for the compositor to register the global.
    • Finished events end the subscription.
    • Requests: You can perform activate, deactivate, remove, assign, and create_workspace on the snapshot. These are capability-gated; check WorkspaceCapabilities or GroupCapabilities before showing UI elements. If a snapshot outlives its protocol objects, requests return RequestError::Gone.
    iced_wayland_subscriber = { version = "...", features = ["workspace"] }
  11. Install iced-exwlshell

    master

    To use iced-exwlshell, add it to your Cargo.toml. By default, the macros feature is enabled, which provides the to_exwlshell_message proc-macro. If you do not need the macro but still want features like Linux theme detection, you can disable default features and enable linux-theme-detection specifically.

    # Standard installation with macros enabled
    iced_exwlshell = "..."
    
    # Minimal installation without macros (e.g., to reduce dependencies)
    iced_exwlshell = { version = "...", default-features = false, features = ["linux-theme-detection"] }
  12. Migrate Popup settings and positioning

    master

    When using IcedNewPopupSettings, coordinate tuples have been replaced by PixelSize.

    • IcedNewPopupSettings::new(parent, (width, height), (x, y)) $\rightarrow$ IcedNewPopupSettings::new(parent, PixelSize::px(width, height), (x, y))
    • IcedNewPopupSettings::at_position(parent, (width, height), pos) $\rightarrow$ IcedNewPopupSettings::at_position(parent, PixelSize::px(width, height), pos)

    Similar changes apply to on_current_surface and at_position_on_current_surface.

    New functionality includes PopUpRepositionSettings and the PopUpReposition message.

    // New Popup initialization pattern
    IcedNewPopupSettings::new(
        parent, 
        PixelSize::px(100, 100), 
        (0, 0), 
        PixelSize::px(1, 1)
    )