Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Wayland Layer Shell

Guido uses the Wayland layer shell protocol for positioning widgets on the desktop. This enables status bars, panels, overlays, and multi-surface applications.

Surface Configuration

Each surface is configured using SurfaceConfig:

#![allow(unused)]
fn main() {
App::new().run(|app| {
    let _surface_id = app.add_surface(
        SurfaceConfig::new()
            .width(1920)
            .height(32)
            .anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT)
            .layer(Layer::Top)
            .keyboard_interactivity(KeyboardInteractivity::OnDemand)
            .namespace("my-status-bar")
            .background_color(Color::rgb(0.1, 0.1, 0.15)),
        || view,
    );
});
}

Note: run() takes a setup closure where you add surfaces. add_surface() returns a SurfaceId that can be used to get a SurfaceHandle for dynamic property modification.

Layers

Control where your surface appears in the stacking order:

#![allow(unused)]
fn main() {
SurfaceConfig::new().layer(Layer::Top)
}
LayerDescription
BackgroundBelow all windows
BottomAbove background, below windows
TopAbove windows (default)
OverlayAbove everything

Use Cases

  • Background: Desktop widgets, wallpaper effects
  • Bottom: Dock bars (below windows but above background)
  • Top: Status bars, panels (above windows)
  • Overlay: Notifications, lock screens

Keyboard Interactivity

Control how the surface receives keyboard focus:

#![allow(unused)]
fn main() {
SurfaceConfig::new().keyboard_interactivity(KeyboardInteractivity::OnDemand)
}
ModeDescription
NoneSurface never receives keyboard focus
OnDemandSurface receives focus when clicked (default)
ExclusiveSurface grabs keyboard focus exclusively

Use Cases

  • None: Status bars that only respond to mouse
  • OnDemand: Panels with text input fields
  • Exclusive: Lock screens, app launchers, modal dialogs

Anchoring

Control which screen edges the surface attaches to:

#![allow(unused)]
fn main() {
SurfaceConfig::new().anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT)
}
AnchorEffect
TOPAttach to top edge
BOTTOMAttach to bottom edge
LEFTAttach to left edge
RIGHTAttach to right edge

Common Patterns

Top status bar (full width):

#![allow(unused)]
fn main() {
SurfaceConfig::new()
    .anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT)
    .height(32)
}

Bottom dock (full width):

#![allow(unused)]
fn main() {
SurfaceConfig::new()
    .anchor(Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT)
    .height(48)
}

Left sidebar (full height):

#![allow(unused)]
fn main() {
SurfaceConfig::new()
    .anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT)
    .width(64)
}

Corner widget (top-right):

#![allow(unused)]
fn main() {
SurfaceConfig::new()
    .anchor(Anchor::TOP | Anchor::RIGHT)
    .width(200)
    .height(100)
}

Centered floating (no anchors):

#![allow(unused)]
fn main() {
// No anchor = centered on screen
SurfaceConfig::new()
    .width(400)
    .height(300)
}

Size Behavior

Size depends on anchoring:

  • Anchored dimension: Expands to fill (e.g., width when LEFT+RIGHT anchored)
  • Unanchored dimension: Uses specified size
  • No anchors: Uses exact size, centered on screen
#![allow(unused)]
fn main() {
// Width fills screen, height is 32px
SurfaceConfig::new()
    .anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT)
    .height(32)

// Both dimensions specified, widget is 200x100
SurfaceConfig::new()
    .anchor(Anchor::TOP | Anchor::RIGHT)
    .width(200)
    .height(100)
}

Namespace

Identify your surface to the compositor:

#![allow(unused)]
fn main() {
SurfaceConfig::new().namespace("my-app-name")
}

Some compositors use this for:

  • Workspace rules
  • Blur effects
  • Per-app settings

Exclusive Zones

Reserve screen space (windows won’t overlap):

#![allow(unused)]
fn main() {
SurfaceConfig::new()
    .anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT)
    .height(32)
    .exclusive_zone(32)  // Reserve 32px at top
}

Without exclusive zone, windows can cover the surface.

Multi-Surface Applications

Guido supports creating multiple surfaces within a single application. All surfaces share the same reactive state, allowing for coordinated updates.

Multiple Static Surfaces

Define multiple surfaces at startup:

fn main() {
    App::new().run(|app| {
        // Shared reactive state
        let count = create_signal(0);

        // Top status bar
        app.add_surface(
            SurfaceConfig::new()
                .height(32)
                .anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT)
                .layer(Layer::Top)
                .namespace("status-bar")
                .background_color(Color::rgb(0.1, 0.1, 0.15)),
            move || {
                container()
                    .height(fill())
                    .layout(
                        Flex::row()
                            .main_alignment(MainAlignment::SpaceBetween)
                            .cross_alignment(CrossAlignment::Center)
                    )
                    .padding([0.0, 16.0])
                    .child(text("Status Bar"))
                    .child(text(move || format!("Count: {}", count.get())))
            },
        );

        // Bottom dock
        app.add_surface(
            SurfaceConfig::new()
                .height(48)
                .anchor(Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT)
                .layer(Layer::Top)
                .namespace("dock")
                .background_color(Color::rgb(0.15, 0.15, 0.2)),
            move || {
                container()
                    .height(fill())
                    .layout(
                        Flex::row()
                            .spacing(16.0)
                            .main_alignment(MainAlignment::Center)
                            .cross_alignment(CrossAlignment::Center)
                    )
                    .child(
                        container()
                            .padding([8.0, 16.0])
                            .background(Color::rgb(0.3, 0.3, 0.4))
                            .corner_radius(8.0)
                            .hover_state(|s| s.lighter(0.1))
                            .on_click(move || count.update(|c| *c += 1))
                            .child(text("+").color(Color::WHITE))
                    )
            },
        );
    });
}

Key Points

  • Shared State: All surfaces share the same reactive signals
  • Independent Widget Trees: Each surface has its own widget tree
  • Fill Layout: Use height(fill()) to make containers expand to fill the surface

Dynamic Surfaces

Create and destroy surfaces at runtime using spawn_surface():

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    App::new().run(|app| {
        let popup_handle: Rc<RefCell<Option<SurfaceHandle>>> = Rc::new(RefCell::new(None));
        let popup_clone = popup_handle.clone();

        app.add_surface(
            SurfaceConfig::new()
                .height(32)
                .anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT),
            move || {
                container()
                    .child(
                        container()
                            .padding(8.0)
                            .hover_state(|s| s.lighter(0.1))
                            .on_click({
                                let popup_handle = popup_clone.clone();
                                move || {
                                    let mut handle = popup_handle.borrow_mut();
                                    if let Some(h) = handle.take() {
                                        // Close existing popup
                                        h.close();
                                    } else {
                                        // Create new popup
                                        let new_handle = spawn_surface(
                                            SurfaceConfig::new()
                                                .width(200)
                                                .height(300)
                                                .anchor(Anchor::TOP | Anchor::RIGHT)
                                                .layer(Layer::Overlay)
                                                .keyboard_interactivity(KeyboardInteractivity::Exclusive),
                                            || {
                                                container()
                                                    .padding(16.0)
                                                    .child(text("Popup Content"))
                                            }
                                        );
                                        *handle = Some(new_handle);
                                    }
                                }
                            })
                            .child(text("Toggle Popup"))
                    )
            },
        );
    });
}

SurfaceHandle API

The SurfaceHandle allows controlling a surface after creation:

#![allow(unused)]
fn main() {
impl SurfaceHandle {
    /// Close and destroy the surface
    pub fn close(&self);

    /// Get the surface ID
    pub fn id(&self) -> SurfaceId;

    /// Change the layer (Background, Bottom, Top, Overlay)
    pub fn set_layer(&self, layer: Layer);

    /// Change keyboard interactivity mode
    pub fn set_keyboard_interactivity(&self, mode: KeyboardInteractivity);

    /// Change anchor edges
    pub fn set_anchor(&self, anchor: Anchor);

    /// Change surface size
    pub fn set_size(&self, width: u32, height: u32);

    /// Change exclusive zone
    pub fn set_exclusive_zone(&self, zone: i32);

    /// Change margins
    pub fn set_margin(&self, top: i32, right: i32, bottom: i32, left: i32);
}
}

Getting a Handle for Existing Surfaces

Use surface_handle() to get a handle for any surface by its ID:

#![allow(unused)]
fn main() {
App::new().run(|app| {
    // Store the ID when adding the surface
    let status_bar_id = app.add_surface(config, move || {
        container()
            .on_click(move || {
                // Get handle and modify properties dynamically
                let handle = surface_handle(status_bar_id);
                handle.set_layer(Layer::Overlay);
                handle.set_keyboard_interactivity(KeyboardInteractivity::Exclusive);
            })
            .child(text("Click to promote to overlay"))
    });
});
}

Complete Examples

Status Bar

fn main() {
    App::new().run(|app| {
        app.add_surface(
            SurfaceConfig::new()
                .height(32)
                .anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT)
                .layer(Layer::Top)
                .exclusive_zone(Some(32))
                .namespace("status-bar")
                .background_color(Color::rgb(0.1, 0.1, 0.15)),
            || {
                container()
                    .height(fill())
                    .layout(
                        Flex::row()
                            .main_alignment(MainAlignment::SpaceBetween)
                            .cross_alignment(CrossAlignment::Center)
                    )
                    .children([
                        left_section(),
                        center_section(),
                        right_section(),
                    ])
            },
        );
    });
}

Dock

fn main() {
    App::new().run(|app| {
        app.add_surface(
            SurfaceConfig::new()
                .height(64)
                .anchor(Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT)
                .layer(Layer::Top)
                .exclusive_zone(Some(64))
                .namespace("dock")
                .background_color(Color::rgba(0.1, 0.1, 0.15, 0.9)),
            || {
                container()
                    .height(fill())
                    .layout(
                        Flex::row()
                            .spacing(8.0)
                            .main_alignment(MainAlignment::Center)
                            .cross_alignment(CrossAlignment::Center)
                    )
                    .children([
                        dock_icon("terminal"),
                        dock_icon("browser"),
                        dock_icon("files"),
                    ])
            },
        );
    });
}

Floating Overlay with Keyboard Focus

fn main() {
    App::new().run(|app| {
        app.add_surface(
            SurfaceConfig::new()
                .width(300)
                .height(100)
                .anchor(Anchor::TOP | Anchor::RIGHT)
                .layer(Layer::Overlay)
                .keyboard_interactivity(KeyboardInteractivity::Exclusive)
                .namespace("notification")
                .background_color(Color::TRANSPARENT),
            || {
                container()
                    .padding(20.0)
                    .background(Color::rgb(0.15, 0.15, 0.2))
                    .corner_radius(12.0)
                    .child(text("Notification").color(Color::WHITE))
            },
        );
    });
}

API Reference

SurfaceConfig

#![allow(unused)]
fn main() {
impl SurfaceConfig {
    pub fn new() -> Self;
    pub fn width(self, width: u32) -> Self;
    pub fn height(self, height: u32) -> Self;
    pub fn anchor(self, anchor: Anchor) -> Self;
    pub fn layer(self, layer: Layer) -> Self;
    pub fn keyboard_interactivity(self, mode: KeyboardInteractivity) -> Self;
    pub fn exclusive_zone(self, zone: Option<i32>) -> Self;
    pub fn namespace(self, namespace: impl Into<String>) -> Self;
    pub fn background_color(self, color: Color) -> Self;
}
}

App

#![allow(unused)]
fn main() {
impl App {
    pub fn new() -> Self;
    pub fn run(self, setup: impl FnOnce(&mut Self)) -> ExitReason;
    pub fn add_surface<W, F>(&mut self, config: SurfaceConfig, widget_fn: F) -> SurfaceId
    where
        W: Widget + 'static,
        F: FnOnce() -> W + 'static;
}
}

Dynamic Surface Creation

#![allow(unused)]
fn main() {
/// Spawn a new surface at runtime
pub fn spawn_surface<W, F>(config: SurfaceConfig, widget_fn: F) -> SurfaceHandle
where
    W: Widget + 'static,
    F: FnOnce() -> W + Send + 'static;

/// Get a handle for an existing surface by ID
pub fn surface_handle(id: SurfaceId) -> SurfaceHandle;
}

SurfaceHandle

#![allow(unused)]
fn main() {
impl SurfaceHandle {
    pub fn id(&self) -> SurfaceId;
    pub fn close(&self);
    pub fn set_layer(&self, layer: Layer);
    pub fn set_keyboard_interactivity(&self, mode: KeyboardInteractivity);
    pub fn set_anchor(&self, anchor: Anchor);
    pub fn set_size(&self, width: u32, height: u32);
    pub fn set_exclusive_zone(&self, zone: i32);
    pub fn set_margin(&self, top: i32, right: i32, bottom: i32, left: i32);
}
}