Project 4: Wayland Panel/Bar (Layer Shell)

Project 4: Wayland Panel/Bar (Layer Shell)

Project Overview

Attribute Value
Difficulty Advanced (Level 3)
Time Estimate 2-3 weeks
Programming Language C
Knowledge Area Linux Desktop / Wayland Protocol
Main Book โ€œThe Wayland Bookโ€ by Drew DeVault
Coolness Level Level 3: Genuinely Clever
Business Potential Micro-SaaS / Pro Tool

What youโ€™ll build: A status bar/panel using the wlr-layer-shell protocolโ€”something like waybar or yambar but simpler, showing time, workspaces, and system info.

Why it teaches shell concepts: Layer shell lets you create surfaces that exist outside the normal window hierarchy (panels, overlays, backgrounds). This teaches you how desktop environments are built on top of Wayland.


Learning Objectives

By completing this project, you will be able to:

  1. Use wlr-layer-shell protocol - Create surfaces anchored to screen edges with exclusive zones
  2. Render with Cairo - Draw 2D graphics (shapes, text) to Wayland buffers
  3. Implement damage tracking - Efficiently update only changed regions
  4. Handle multi-monitor - Create per-output panel instances
  5. Integrate with compositor IPC - Get workspace/window information
  6. Build timer-based updates - Update clock, system stats periodically

The Core Question Youโ€™re Answering

โ€œHow do desktop environments create UI elements that exist โ€˜outsideโ€™ the normal window hierarchyโ€”panels that other windows canโ€™t overlap, backgrounds that sit behind everything, and overlays that appear above all windows?โ€

Before Waylandโ€™s layer shell, this was compositor-specific. X11 had _NET_WM_WINDOW_TYPE_DOCK hints, but enforcement was inconsistent. Waylandโ€™s layer shell protocol solves this by defining explicit layers:

  • Background layer: Wallpapers, desktop backgrounds
  • Bottom layer: Desktop icons, widgets that sit below windows
  • Top layer: Panels, status bars that sit above windows
  • Overlay layer: Notifications, lock screens, on-screen displays

By building a panel, you answer these deeper questions:

  1. Why does layer shell exist?
    • Normal windows canโ€™t reliably reserve screen space
    • The compositor needs to know about panels to adjust window geometry
    • Security: overlay layer is compositor-controlled for lock screens
  2. How does per-output binding work?
    • Unlike xdg-toplevel, layer surfaces bind to a specific wl_output
    • Enables different panels on different monitors
    • Compositor handles output hotplug
  3. What is an exclusive zone?
    • Reserves pixels from the edge for the panel
    • Compositor reduces โ€œusable areaโ€ for windows
    • Windows maximize within usable area, not full screen

Deep Theoretical Foundation

1. Layer Shell Protocol

The layer shell defines four layers with fixed stacking order:

LAYER SHELL STACKING ORDER
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

Top of screen (closest to user)
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
โ”‚                                                                     โ”‚
โ”‚  OVERLAY LAYER (ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY)                 โ”‚
โ”‚  - Lock screens                                                    โ”‚
โ”‚  - Critical notifications                                          โ”‚
โ”‚  - On-screen keyboards                                             โ”‚
โ”‚                                                                     โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                     โ”‚
โ”‚  TOP LAYER (ZWLR_LAYER_SHELL_V1_LAYER_TOP)                         โ”‚
โ”‚  - Panels / Status bars  โ—„โ”€โ”€ Your panel goes here                 โ”‚
โ”‚  - Popup menus from panels                                         โ”‚
โ”‚  - Application launchers                                           โ”‚
โ”‚                                                                     โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                     โ”‚
โ”‚  NORMAL WINDOWS (xdg-toplevel)                                     โ”‚
โ”‚  - All regular application windows                                 โ”‚
โ”‚  - Stacked by focus/z-order                                        โ”‚
โ”‚                                                                     โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                     โ”‚
โ”‚  BOTTOM LAYER (ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM)                   โ”‚
โ”‚  - Desktop widgets                                                 โ”‚
โ”‚  - Desktop icons                                                   โ”‚
โ”‚                                                                     โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                     โ”‚
โ”‚  BACKGROUND LAYER (ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND)           โ”‚
โ”‚  - Wallpapers                                                      โ”‚
โ”‚  - Desktop backgrounds                                             โ”‚
โ”‚                                                                     โ”‚
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Bottom of screen (furthest from user)

2. Anchoring and Exclusive Zones

Anchoring determines how the surface attaches to screen edges:

ANCHOR COMBINATIONS
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

TOP                          BOTTOM
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ”‚   โ”‚                        โ”‚
โ”‚                        โ”‚   โ”‚                        โ”‚
โ”‚                        โ”‚   โ”‚                        โ”‚
โ”‚                        โ”‚   โ”‚โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
Anchor: TOP|LEFT|RIGHT       Anchor: BOTTOM|LEFT|RIGHT
Size: (0, 30)                Size: (0, 30)
0 width = full width         0 width = full width

LEFT                         RIGHT
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚โ–ˆโ–ˆโ”‚                     โ”‚   โ”‚                     โ”‚โ–ˆโ–ˆโ”‚
โ”‚โ–ˆโ–ˆโ”‚                     โ”‚   โ”‚                     โ”‚โ–ˆโ–ˆโ”‚
โ”‚โ–ˆโ–ˆโ”‚                     โ”‚   โ”‚                     โ”‚โ–ˆโ–ˆโ”‚
โ”‚โ–ˆโ–ˆโ”‚                     โ”‚   โ”‚                     โ”‚โ–ˆโ–ˆโ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
Anchor: TOP|LEFT|BOTTOM      Anchor: TOP|RIGHT|BOTTOM
Size: (50, 0)                Size: (50, 0)
0 height = full height       0 height = full height

CORNER                       CENTER (unusual)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚โ–ˆโ–ˆโ–ˆโ–ˆโ”‚                   โ”‚   โ”‚                        โ”‚
โ”‚                        โ”‚   โ”‚      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”‚
โ”‚                        โ”‚   โ”‚      โ”‚โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ”‚        โ”‚
โ”‚                        โ”‚   โ”‚      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
Anchor: TOP|LEFT             Anchor: (none or one edge)
Size: (200, 100)             Size: (200, 100)


EXCLUSIVE ZONES
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

Without exclusive zone (exclusive_zone = 0):
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ Panel โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ”‚ โ† Panel
โ”‚โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ”‚ โ† Window overlaps!
โ”‚โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ”‚
โ”‚โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

With exclusive zone (exclusive_zone = 30):
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ Panel โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ”‚ โ† Panel (30px)
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ”‚ โ† Windows start here
โ”‚โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ”‚
โ”‚โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Usable area calculation:
  Original: (0, 0, 1920, 1080)
  After top panel (30px): (0, 30, 1920, 1050)
  After bottom panel (40px): (0, 30, 1920, 1010)

3. Cairo Graphics Library

Cairo provides 2D drawing operations:

CAIRO RENDERING PIPELINE
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

1. Create Cairo surface from shared memory buffer:

   void *shm_data = mmap(...);  // From wl_shm
   cairo_surface_t *cairo_surface =
       cairo_image_surface_create_for_data(
           shm_data,
           CAIRO_FORMAT_ARGB32,
           width,
           height,
           stride);

2. Create drawing context:

   cairo_t *cr = cairo_create(cairo_surface);

3. Draw operations:

   // Clear background
   cairo_set_source_rgba(cr, 0.1, 0.1, 0.1, 0.9);
   cairo_paint(cr);

   // Draw rectangle
   cairo_set_source_rgb(cr, 0.2, 0.6, 0.8);
   cairo_rectangle(cr, x, y, width, height);
   cairo_fill(cr);

   // Draw text (with Pango)
   PangoLayout *layout = pango_cairo_create_layout(cr);
   pango_layout_set_text(layout, "14:30:22", -1);
   pango_layout_set_font_description(layout, font_desc);
   cairo_move_to(cr, text_x, text_y);
   pango_cairo_show_layout(cr, layout);

4. Flush and cleanup:

   cairo_surface_flush(cairo_surface);
   cairo_destroy(cr);
   // Now shm_data contains rendered pixels

4. Damage Tracking

Efficient rendering only updates changed regions:

DAMAGE TRACKING STRATEGY
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

Full panel: 1920 x 30 pixels

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ [1][2][3โ—]โ”‚  Terminal  โ”‚     โ”‚ CPU:34% โ”‚ RAM:8G โ”‚ 14:30:22 โ”‚        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
     โ”‚            โ”‚                  โ”‚          โ”‚        โ”‚
  200px        400px              200px      150px    150px

Clock update (every 1 second):
  - Only damage region: (1570, 0, 150, 30)
  - Redraw only clock widget
  - 97% of panel unchanged

System info update (every 2 seconds):
  - Damage regions: (800, 0, 550, 30)
  - Redraw CPU, RAM, Network widgets
  - 70% of panel unchanged

Workspace change:
  - Damage region: (0, 0, 200, 30)
  - Redraw workspace indicator
  - 90% of panel unchanged

Implementation:

struct damage_region {
    int x, y, width, height;
    bool dirty;
};

void mark_widget_dirty(struct widget *w) {
    w->damage.dirty = true;
    schedule_redraw();
}

void render_frame() {
    for each widget {
        if (widget->damage.dirty) {
            cairo_save(cr);
            cairo_rectangle(cr, widget->x, widget->y, widget->w, widget->h);
            cairo_clip(cr);
            widget->render(cr);
            cairo_restore(cr);

            wl_surface_damage_buffer(surface,
                widget->x, widget->y, widget->w, widget->h);
            widget->damage.dirty = false;
        }
    }
    wl_surface_commit(surface);
}

Complete Project Specification

Functional Requirements

  1. Layer Surface: Create TOP layer surface anchored to top edge
  2. Exclusive Zone: Reserve 30px, windows maximize below panel
  3. Multi-Monitor: One panel instance per connected output
  4. Widgets:
    • Clock (updates every second)
    • Workspaces (from compositor IPC)
    • Window title (focused window)
    • CPU/Memory usage
    • Battery status (laptops)
  5. Interaction: Click workspaces to switch

Non-Functional Requirements

  • Minimal CPU usage when idle (damage tracking)
  • Graceful handling of output hotplug
  • Works with any wlroots-based compositor

Solution Architecture

Component Diagram

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                        wayland_panel                                 โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                     โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚                        main.c                                โ”‚   โ”‚
โ”‚  โ”‚                                                              โ”‚   โ”‚
โ”‚  โ”‚  struct panel_state {                                        โ”‚   โ”‚
โ”‚  โ”‚      struct wl_display *display;                            โ”‚   โ”‚
โ”‚  โ”‚      struct wl_registry *registry;                          โ”‚   โ”‚
โ”‚  โ”‚      struct wl_compositor *compositor;                      โ”‚   โ”‚
โ”‚  โ”‚      struct wl_shm *shm;                                    โ”‚   โ”‚
โ”‚  โ”‚      struct zwlr_layer_shell_v1 *layer_shell;               โ”‚   โ”‚
โ”‚  โ”‚      struct wl_list outputs;                                โ”‚   โ”‚
โ”‚  โ”‚      struct wl_list widgets;                                โ”‚   โ”‚
โ”‚  โ”‚  };                                                          โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                                                                     โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚  โ”‚    output.c     โ”‚  โ”‚   widgets.c     โ”‚  โ”‚     render.c        โ”‚ โ”‚
โ”‚  โ”‚                 โ”‚  โ”‚                 โ”‚  โ”‚                     โ”‚ โ”‚
โ”‚  โ”‚ struct output { โ”‚  โ”‚ struct widget { โ”‚  โ”‚ render_panel()      โ”‚ โ”‚
โ”‚  โ”‚  wl_output      โ”‚  โ”‚  x, y, w, h     โ”‚  โ”‚ damage_region()     โ”‚ โ”‚
โ”‚  โ”‚  layer_surface  โ”‚  โ”‚  render()       โ”‚  โ”‚ create_buffer()     โ”‚ โ”‚
โ”‚  โ”‚  buffer         โ”‚  โ”‚  update()       โ”‚  โ”‚ cairo_from_buffer() โ”‚ โ”‚
โ”‚  โ”‚  cairo_surface  โ”‚  โ”‚  on_click()     โ”‚  โ”‚                     โ”‚ โ”‚
โ”‚  โ”‚ }               โ”‚  โ”‚ }               โ”‚  โ”‚                     โ”‚ โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚                                                                     โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚                     widget implementations                   โ”‚   โ”‚
โ”‚  โ”‚                                                              โ”‚   โ”‚
โ”‚  โ”‚  clock.c       workspace.c    sysinfo.c      battery.c      โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                                                                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Output Structure

struct panel_output {
    struct wl_list link;
    struct panel_state *state;

    struct wl_output *wl_output;
    char *name;  // "HDMI-A-1"
    int32_t width, height;
    int32_t scale;

    struct wl_surface *surface;
    struct zwlr_layer_surface_v1 *layer_surface;

    struct wl_buffer *buffer;
    void *buffer_data;
    cairo_surface_t *cairo_surface;

    bool configured;
    bool dirty;

    struct wl_listener destroy;
};

Widget Interface

struct widget {
    struct wl_list link;
    const char *name;

    // Geometry
    int x, y, width, height;
    bool flexible;  // Takes remaining space

    // Callbacks
    void (*init)(struct widget *w, struct panel_state *state);
    void (*update)(struct widget *w);  // Called periodically
    void (*render)(struct widget *w, cairo_t *cr);
    void (*click)(struct widget *w, int x, int y, uint32_t button);
    void (*destroy)(struct widget *w);

    // State
    bool dirty;
    void *data;
};

Phased Implementation Guide

Phase 1: Layer Surface Setup (Day 1-3)

Goal: Create a basic layer surface that appears at screen top

Steps:

  1. Connect to Wayland, get registry
  2. Bind to wl_compositor, wl_shm, zwlr_layer_shell_v1
  3. Enumerate wl_output globals
  4. For each output, create layer surface
  5. Set anchor (TOP LEFT RIGHT), size (0, 30), exclusive zone (30)
  6. Create buffer, fill with solid color
  7. Commit and see panel appear

Verification:

$ ./wayland_panel
# Gray bar should appear at top of each monitor
# Windows should maximize below it

Phase 2: Cairo Rendering (Day 4-6)

Goal: Render text and shapes with Cairo

Steps:

  1. Create Cairo surface from shared memory
  2. Implement render_panel() function
  3. Draw background color
  4. Add clock widget (static text for now)
  5. Use Pango for text rendering

Key Code:

void render_panel(struct panel_output *output) {
    cairo_t *cr = cairo_create(output->cairo_surface);

    // Background
    cairo_set_source_rgba(cr, 0.1, 0.1, 0.1, 0.9);
    cairo_paint(cr);

    // Render each widget
    struct widget *w;
    wl_list_for_each(w, &output->state->widgets, link) {
        cairo_save(cr);
        cairo_translate(cr, w->x, w->y);
        w->render(w, cr);
        cairo_restore(cr);
    }

    cairo_destroy(cr);
    cairo_surface_flush(output->cairo_surface);

    wl_surface_attach(output->surface, output->buffer, 0, 0);
    wl_surface_damage_buffer(output->surface, 0, 0, output->width, 30);
    wl_surface_commit(output->surface);
}

Phase 3: Timer-Based Updates (Day 7-9)

Goal: Update clock and system info periodically

Steps:

  1. Create timerfd for 1-second interval
  2. Integrate with Wayland event loop
  3. Update clock widget each second
  4. Update system info every 2 seconds
  5. Implement damage tracking

Timer Integration:

int timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
struct itimerspec its = {
    .it_interval = { .tv_sec = 1, .tv_nsec = 0 },
    .it_value = { .tv_sec = 1, .tv_nsec = 0 }
};
timerfd_settime(timer_fd, 0, &its, NULL);

// Add to poll loop alongside wl_display fd

Phase 4: System Information (Day 10-12)

Goal: Display CPU, RAM, battery status

Steps:

  1. Read CPU usage from /proc/stat
  2. Read memory from /proc/meminfo
  3. Read battery from /sys/class/power_supply/
  4. Create widgets for each
  5. Update on timer

CPU Reading:

// Read /proc/stat
// cpu  user nice system idle iowait irq softirq steal guest guest_nice
// Calculate: usage = 100 * (total - idle) / total

Phase 5: Workspace Integration (Day 13-15)

Goal: Show and switch workspaces via compositor IPC

Steps:

  1. Connect to Sway IPC socket (or compositor-specific)
  2. Subscribe to workspace events
  3. Query current workspace state
  4. Render workspace indicators
  5. Handle click to switch

Sway IPC:

// Connect to $SWAYSOCK
// Send: {"type": "subscribe", "payload": ["workspace"]}
// Receive: workspace events as JSON
// Send: {"type": "run_command", "payload": ["workspace 2"]}

Testing Strategy

Visual Testing

Test Case Expected Result
Panel appears At top of all monitors
Exclusive zone Windows maximize below panel
Clock updates Shows correct time, updates each second
System info Shows reasonable values
Multi-monitor Independent panel per monitor
Monitor hotplug Panel appears/disappears with output

Performance Testing

# Monitor CPU usage
top -p $(pgrep wayland_panel)

# Should be <1% CPU when idle
# Brief spikes on updates, then back to idle

Common Pitfalls and Debugging

Problem: Panel appears but doesnโ€™t reserve space

Cause: Exclusive zone not set or set to 0 Fix: zwlr_layer_surface_v1_set_exclusive_zone(layer_surface, 30);

Problem: Panel doesnโ€™t appear on all monitors

Cause: Only creating layer surface for one output Fix: Iterate all wl_output globals, create surface for each

Problem: Text looks blurry

Cause: Not handling output scale factor Fix:

cairo_surface_set_device_scale(cairo_surface, scale, scale);
// Or use Pango with proper DPI settings

Problem: Updates cause flicker

Cause: Full redraw instead of damage tracking Fix: Only damage and redraw changed regions


Extensions and Challenges

Challenge 1: Custom Widget API

Create plugin system for user-defined widgets.

Challenge 2: Configuration File

Load widget layout, colors from config file.

Challenge 3: Popup Menus

Implement popup menus from panel (calendar, app launcher).

Challenge 4: Animations

Add smooth animations for workspace switching.

Challenge 5: Multi-Compositor Support

Support Sway, Wayfire, Hyprland IPC.


Real-World Connections

Production Wayland Panels

Panel Features
waybar JSON config, many modules, highly customizable
yambar YAML config, particle-based widgets
sfwbar GTK-based, desktop integration
eww Lisp-like config, scriptable widgets

Career Applications

  • Desktop Environment Development: GNOME Shell, KDE Plasma
  • Embedded Systems: HMI panels, kiosk displays
  • System Administration Tools: Status displays, monitoring
  • Accessibility: On-screen keyboards, magnifiers

Resources

Essential Reading

Resource Purpose
wlr-layer-shell-unstable-v1.xml Protocol definition
Cairo tutorial 2D graphics
Pango documentation Text rendering
TLPI Ch. 23 Timers
TLPI Ch. 12 /proc filesystem

Code References

  • waybar source (complex but complete)
  • wlroots examples/layer-shell.c
  • cairo samples

Self-Assessment Checklist

Before considering this project complete, verify you can:

  • Explain the four layer shell layers and their purposes
  • Create a layer surface anchored to any edge
  • Explain what an exclusive zone does
  • Render text with Cairo and Pango
  • Implement damage tracking to minimize redraws
  • Handle multi-monitor configurations
  • Read system information from /proc and /sys
  • Integrate timers with Wayland event loop
  • Connect to compositor IPC (at least Sway)
  • Add a new widget to your panel

Completing this project gives you the skills to build desktop shell components. You understand how panels, docks, notifications, and other โ€œspecialโ€ surfaces work in Wayland. Combined with P02 (Compositor), you can now build a complete desktop environment.