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:
- Use wlr-layer-shell protocol - Create surfaces anchored to screen edges with exclusive zones
- Render with Cairo - Draw 2D graphics (shapes, text) to Wayland buffers
- Implement damage tracking - Efficiently update only changed regions
- Handle multi-monitor - Create per-output panel instances
- Integrate with compositor IPC - Get workspace/window information
- 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:
- 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
- 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
- Unlike xdg-toplevel, layer surfaces bind to a specific
- 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
- Layer Surface: Create TOP layer surface anchored to top edge
- Exclusive Zone: Reserve 30px, windows maximize below panel
- Multi-Monitor: One panel instance per connected output
- Widgets:
- Clock (updates every second)
- Workspaces (from compositor IPC)
- Window title (focused window)
- CPU/Memory usage
- Battery status (laptops)
- 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:
- Connect to Wayland, get registry
- Bind to wl_compositor, wl_shm, zwlr_layer_shell_v1
- Enumerate wl_output globals
- For each output, create layer surface
-
Set anchor (TOP LEFT RIGHT), size (0, 30), exclusive zone (30) - Create buffer, fill with solid color
- 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:
- Create Cairo surface from shared memory
- Implement render_panel() function
- Draw background color
- Add clock widget (static text for now)
- 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:
- Create timerfd for 1-second interval
- Integrate with Wayland event loop
- Update clock widget each second
- Update system info every 2 seconds
- 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:
- Read CPU usage from /proc/stat
- Read memory from /proc/meminfo
- Read battery from /sys/class/power_supply/
- Create widgets for each
- 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:
- Connect to Sway IPC socket (or compositor-specific)
- Subscribe to workspace events
- Query current workspace state
- Render workspace indicators
- 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.