Project 4: Wayland Panel/Bar (Layer Shell)

Build a Wayland panel using the layer-shell protocol that anchors to the top of the screen, renders text, and updates on a timer.

Quick Reference

Attribute Value
Difficulty Level 3: Intermediate
Time Estimate 1-2 weeks
Main Programming Language C (Alternatives: C++, Rust, Zig)
Alternative Programming Languages C++, Rust, Zig
Coolness Level Level 3: Genuinely Clever
Business Potential Level 2: Practical Utility
Prerequisites Project 1, basic drawing, timers, event loops
Key Topics layer-shell protocol, anchors/exclusive zone, rendering text, output scaling

1. Learning Objectives

By completing this project, you will:

  1. Use the wlr-layer-shell protocol to create a top-level panel.
  2. Negotiate size and anchors with the compositor and respect configure events.
  3. Render text and simple shapes into wl_shm buffers.
  4. Handle output scale correctly so text is crisp on HiDPI displays.
  5. Update the panel on a timer without wasting CPU.

2. All Theory Needed (Per-Concept Breakdown)

2.1 Layer-Shell Protocol Semantics

Fundamentals

Layer-shell is a Wayland protocol that allows clients to create surfaces that are not regular windows. It is designed for panels, docks, wallpapers, and overlays. The protocol lets you specify which layer you belong to (background, bottom, top, overlay), how you anchor to the edges of the output, and whether you reserve space via an exclusive zone. The compositor uses this information to place your surface and keep normal windows from overlapping when appropriate. Understanding these semantics is crucial because the compositor, not the client, decides final size and placement. You should also understand version negotiation for unstable protocols and how output selection determines where the panel appears.

Deep Dive into the concept

Layer-shell is defined in wlr-layer-shell-unstable-v1 and is widely supported by wlroots-based compositors. You create a normal wl_surface, then call zwlr_layer_shell_v1_get_layer_surface to obtain a layer surface. The compositor sends configure events with a size and a serial. You must acknowledge those configure events before attaching a buffer, just like xdg-shell. The key difference is in the constraints you can express: anchors, margins, size hints, and exclusive zones.

Anchors define which edges of the output your surface sticks to. If you set TOP, LEFT, and RIGHT, your surface spans the width of the output and sits at the top. If you only set TOP and LEFT, your surface occupies a corner and the compositor does not stretch it. If you set no anchors, the surface is centered by default. This anchoring model is simple but powerful. It allows panels, docks, and notifications to be positioned without complex window management logic.

The exclusive zone is one of the most important concepts. It tells the compositor that your surface reserves space and that other windows should avoid it. If your panel is 30 pixels tall and you set exclusive_zone=30, maximized windows will start below the panel. If exclusive_zone=0, your panel is an overlay and windows can appear beneath it. If exclusive_zone=-1, you ask the compositor to decide. These values directly affect how the desktop feels, so you should expose them as configuration options or at least document them.

Layer ordering matters. BACKGROUND is used for wallpapers. BOTTOM is often used for docks that should stay above wallpaper but below normal windows. TOP is for panels and status bars. OVERLAY is for transient UI like volume OSD or screen locks. The compositor sorts surfaces by layer, then by creation order or explicit z-order rules. Knowing this helps you reason about why your panel appears above or below other elements.

Another subtlety is output selection. The layer-shell request takes an optional wl_output. If you pass NULL, the compositor chooses an output. If you pass a specific output, your panel attaches there. For multi-monitor setups, you typically create one panel per output. You also need to handle output hotplug: if an output disappears, you must destroy the corresponding panel surface. This is easy to ignore in a single-monitor development environment, but real users will notice.

Finally, layer-shell is unstable. That means the protocol can change. In practice it is stable enough for real use, but you should still code defensively and handle version negotiation. Bind to the highest version you support, and do not call requests you do not understand. This is the same discipline you used with xdg-shell but applied to layer surfaces.

A practical detail is size negotiation. The compositor may send width or height as 0 to indicate it wants you to choose a size. For a panel, you usually pick a fixed height and set width to the output width. You should also understand margins: they inset the surface from the edges, which is useful for floating overlays. The namespace string you pass to get_layer_surface is not just cosmetic; compositors use it to apply policies, so choose a stable name like "panel" rather than a random string.

How this fit on projects

This concept is the core of the project and directly impacts Section 3.2 (functional requirements), Section 3.7 (real-world outcome), and Section 5.10 (implementation phases).

Definitions & key terms

  • layer-shell -> Wayland protocol for non-window surfaces
  • anchor -> edges of output a surface attaches to
  • exclusive zone -> reserved area that normal windows avoid
  • layer -> background, bottom, top, overlay
  • configure -> event with size and serial from compositor

Mental Model Diagram (ASCII)

Output
+----------------------------------------------------+
| TOP layer: panel (exclusive zone 30)              |
+----------------------------------------------------+
| normal windows (avoid exclusive zone)             |
|                                                    |
|                                                    |
+----------------------------------------------------+

How It Works (Step-by-Step)

  1. Bind zwlr_layer_shell_v1 from registry.
  2. Create wl_surface.
  3. Call get_layer_surface with layer=TOP, output, and namespace.
  4. Set anchors and size hints.
  5. Commit and wait for configure.
  6. Ack configure, attach buffer, commit.

Invariants:

  • Must ack configure before attaching a buffer.
  • Exclusive zone controls window avoidance.

Failure modes:

  • Missing layer-shell global -> panel never appears.
  • Not handling configure -> surface stays zero size.

Minimal Concrete Example

struct zwlr_layer_surface_v1 *layer =
  zwlr_layer_shell_v1_get_layer_surface(shell, surface, output,
      ZWLR_LAYER_SHELL_V1_LAYER_TOP, "panel");
zwlr_layer_surface_v1_set_anchor(layer,
    ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
    ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT |
    ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT);
zwlr_layer_surface_v1_set_exclusive_zone(layer, 30);

Common Misconceptions

  • “The panel decides its final size.” -> The compositor decides final size.
  • “Exclusive zone is just padding.” -> It changes window placement policy.

Check-Your-Understanding Questions

  1. What does exclusive_zone=0 mean?
  2. Why must you ack configure before attaching a buffer?
  3. How do you create panels on multiple outputs?

Check-Your-Understanding Answers

  1. It means the panel is an overlay and does not reserve space.
  2. It synchronizes size and state with the compositor.
  3. Create one layer surface per wl_output and track them.

Real-World Applications

  • Waybar, Yambar, and panels in wlroots compositors use layer-shell.

Where You’ll Apply It

References

  • wlr-layer-shell protocol spec
  • wlroots documentation for layer-shell

Key Insights

Layer-shell is a contract with the compositor about placement and policy, not just drawing.

Summary

Layer-shell gives panels and overlays a reliable placement model via anchors and exclusive zones.

Homework/Exercises to Practice the Concept

  1. Implement a bottom dock by switching anchors and layer.
  2. Toggle exclusive zone between 0 and bar height.
  3. Create a panel on a secondary output.

Solutions to the Homework/Exercises

  1. Use layer=BOTTOM and anchor BOTTOM LEFT RIGHT.
  2. Call set_exclusive_zone with 0 or height.
  3. Iterate wl_output globals and create one surface per output.

2.2 Rendering Text and Shapes in wl_shm Buffers

Fundamentals

A panel must draw text, icons, and separators. Without a toolkit, you will draw into wl_shm buffers directly. This requires understanding pixel formats, stride, and basic rasterization. Even simple text rendering can be done with a bitmap font or a tiny software rasterizer. The key is to separate drawing from Wayland plumbing, so you can update content without rewriting protocol code. Even with a bitmap font, you must handle baseline, padding, and spacing so text remains readable, and caching glyphs avoids repeated blits. Color contrast and simple anti-aliasing rules also matter for legibility. A small color palette keeps the panel coherent across themes.

Deep Dive into the concept

Software rendering for a panel is usually sufficient because the panel is small. You can create a wl_shm buffer at the panel’s pixel size and draw into it with a simple CPU loop. Text rendering can be implemented with a bitmap font: each glyph is a small bitmap, and you blit it into the buffer. This avoids complex font libraries and keeps the project manageable. You should still handle alpha blending so text looks smooth; even a simple alpha blend between text color and background improves readability.

The buffer format matters. Use WL_SHM_FORMAT_XRGB8888 or ARGB8888. If you want transparency (for overlays), use ARGB8888 and set an alpha channel. Your compositor must support that format. Stride should be width * 4, and size is stride * height. If you handle HiDPI, remember that the buffer dimensions are in buffer pixels, not logical pixels. That means you must multiply by output scale before allocating.

Separating layout from rendering is important. Compute positions of text and icons in logical coordinates, then multiply by scale to get buffer coordinates. If you mix these, you will get blurry or misaligned text. A good approach is to build a small layout structure: left section (workspace list), center section (clock), right section (status icons). Then render each section to the buffer in order. This makes updates predictable and helps you add features later.

Another challenge is partial updates. When the clock changes, you do not need to redraw everything, but you can if the panel is small. For a basic implementation, redraw the entire buffer on each update. For extra credit, compute a damage rectangle and only redraw the changed region. wl_surface_damage_buffer lets you specify the damaged region in buffer coordinates. This is optional, but it teaches you the same damage concepts used by compositors.

Finally, consider font choice. A built-in bitmap font keeps dependencies minimal. If you use a library like FreeType or Pango, you will gain better typography but increase complexity. For this project, a simple bitmap font is enough. The goal is not perfect typography, but a working panel that teaches you how to render pixels and update content in response to events.

If you want better text without a full library, you can add a tiny font metrics table. Track each glyph’s width and baseline so you can align text properly. This makes the clock look centered instead of jittering. Another small improvement is glyph caching: precompute the pixel masks for each glyph so you can blit quickly. The panel is small, so this is not required for performance, but it keeps rendering deterministic and simplifies your draw loop. For icons, you can use a small sprite atlas stored as raw pixel arrays and blit them the same way you blit glyphs.

One more detail is color handling. Most Wayland compositors expect premultiplied alpha for ARGB buffers. That means your color channels should already be multiplied by the alpha value. If you ignore this, translucent text can look darker than expected. Even for opaque panels, understanding this helps when you add transparency or shadows later.

How this fit on projects

This concept is used in Section 3.2 (functional requirements), Section 4.4 (data structures), and Section 5.10 (implementation phases).

Definitions & key terms

  • bitmap font -> font represented as pixel bitmaps
  • stride -> bytes per row of pixels
  • alpha blending -> combining foreground and background with transparency
  • damage -> region that needs redraw

Mental Model Diagram (ASCII)

Buffer (width x height)
+----------------------------------+
| [workspaces]   [clock]   [status]|
+----------------------------------+

How It Works (Step-by-Step)

  1. Allocate wl_shm buffer at panel size.
  2. Clear buffer to background color.
  3. Render text glyphs into buffer.
  4. Attach buffer and commit.
  5. On update, redraw and commit again.

Invariants:

  • Buffer size must match configured size.
  • Render coordinates must respect scale.

Failure modes:

  • Wrong stride -> garbled text.
  • Ignoring alpha -> ugly text edges.

Minimal Concrete Example

for (int y = 0; y < height; y++) {
  for (int x = 0; x < width; x++) {
    pixels[y * (stride/4) + x] = 0xFF202020; // background
  }
}
blit_glyph(pixels, stride, x, y, glyph_A, fg_color);

Common Misconceptions

  • “Text rendering requires a full GUI toolkit.” -> A bitmap font works fine.
  • “Damage is optional.” -> It is optional but improves performance.

Check-Your-Understanding Questions

  1. Why use ARGB8888 for transparency?
  2. Why must layout be in logical units and rendering in buffer units?
  3. What happens if you draw before the first configure?

Check-Your-Understanding Answers

  1. It allows an alpha channel for blending.
  2. Logical units allow consistent layout across scales.
  3. The compositor may ignore or disconnect you.

Real-World Applications

  • Many panels use wl_shm for text and icons to keep dependencies low.

Where You’ll Apply It

References

  • freetype docs (optional)
  • Wayland wl_shm protocol

Key Insights

Simple software rendering is enough for panels; correctness matters more than speed.

Summary

Render into wl_shm buffers with correct stride and scale to draw crisp text.

Homework/Exercises to Practice the Concept

  1. Render a fixed-width bitmap font and display a clock.
  2. Implement a simple separator line between sections.
  3. Add a transparent background using ARGB8888.

Solutions to the Homework/Exercises

  1. Store glyph bitmaps and blit them at fixed positions.
  2. Draw a 1-pixel line in a lighter color.
  3. Set alpha < 255 for background pixels.

2.3 Output Scaling, HiDPI, and Multi-Monitor Layout

Fundamentals

Wayland separates logical size from buffer size. Outputs can have a scale factor (2x, 1.5x) which means one logical pixel maps to multiple buffer pixels. If you ignore scale, your panel will appear blurry or the wrong size on HiDPI monitors. You must listen for output scale events and adjust buffer dimensions accordingly. Multi-monitor setups also require you to create one panel per output and track their individual scales and sizes. Scaling can change at runtime when outputs change, so treat scale as dynamic state, not a constant. Each output may also have different refresh and geometry. This affects crispness and hit testing.

Deep Dive into the concept

HiDPI support is not optional in modern desktops. On a 2x scale output, a logical width of 1920 corresponds to a buffer width of 3840. Your panel may be 30 logical pixels tall, but the buffer must be 60 pixels tall. If you draw only 30 pixels, the compositor will scale it up, causing blur. To be crisp, you must render at buffer resolution. That means your rendering code must accept a scale parameter and multiply coordinates.

The output scale is communicated via wl_output events. You must bind to wl_output and listen for scale and mode. When you get a scale change, you must recreate your buffer at the new size and redraw. In addition, you should set the buffer scale on the wl_surface with wl_surface_set_buffer_scale. This tells the compositor how to interpret buffer pixels relative to logical coordinates.

Multi-monitor handling is similar to the compositor side: you maintain a list of outputs, and for each output you create a layer surface. Each output may have a different scale and size, so each panel has its own buffer. When outputs are hotplugged, you must destroy or create panels accordingly. This adds complexity but is essential for real-world use. The good news is that most of the code can be shared; you just store per-output state in a struct.

Another subtlety is fractional scaling. Some compositors support fractional scale protocols, where scale is not an integer. In that case, you still render at the nearest integer buffer scale and use viewporter or fractional-scale protocols to get crisp results. For this project, you can ignore fractional scaling or treat it as an extension. The important lesson is that scale exists and must be handled explicitly.

Output transforms also matter. Outputs can be rotated or flipped, and the compositor reports a transform. If you ignore it, your panel may appear in the wrong orientation on rotated displays. Most panels simply rely on the compositor to handle transforms, but if you want pixel-perfect placement you should be aware of it. Additionally, output logical coordinates are not necessarily (0,0) for all outputs; multi-monitor layouts often place outputs side by side in a larger logical space. Even though a layer surface anchors to a specific output, understanding the global layout helps when you later add inter-output interactions.

In real setups, outputs can appear and disappear dynamically. When that happens, you should destroy the corresponding layer surface and buffers, and remove any cached scale state. If you use xdg-output to obtain output names and logical sizes, you can present clearer logs and help users map panels to physical monitors. This is not required for the minimal project, but it is a practical addition and reinforces the idea that outputs are dynamic entities.

If you ever move the panel between outputs (for example, when a laptop lid closes and the external monitor becomes primary), you must treat this as a full reconfiguration: update the output pointer, recompute logical size, and recreate the buffer with the new scale. This is where keeping per-output state in a struct pays off, because you can tear down and rebuild cleanly without leaking old buffers.

How this fit on projects

This concept is used in Section 3.2, Section 3.6 (edge cases), and Section 5.10 (implementation phases) for buffer allocation and rendering.

Definitions & key terms

  • logical size -> size in compositor coordinate space
  • buffer size -> size in pixel coordinates
  • scale factor -> ratio between logical and buffer pixels
  • wl_surface_set_buffer_scale -> informs compositor of buffer scale

Mental Model Diagram (ASCII)

Logical panel: 1920x30 @ scale 2
Buffer size:  3840x60

How It Works (Step-by-Step)

  1. Bind wl_output and read scale.
  2. Multiply logical size by scale for buffer size.
  3. Set buffer scale on surface.
  4. Render at buffer resolution.
  5. Recreate buffers on scale change.

Invariants:

  • Buffer size = logical size * scale.
  • Surface buffer scale must match buffer resolution.

Failure modes:

  • Ignoring scale -> blurry text.
  • Not recreating buffers -> stretched artifacts.

Minimal Concrete Example

wl_surface_set_buffer_scale(surface, scale);
int buf_w = logical_w * scale;
int buf_h = logical_h * scale;

Common Misconceptions

  • “The compositor will handle scaling for me.” -> It will, but results are blurry.
  • “Scale never changes.” -> It can change with output configuration.

Check-Your-Understanding Questions

  1. Why must buffer size be multiplied by scale?
  2. What happens if you forget wl_surface_set_buffer_scale?
  3. How do you handle multiple outputs with different scales?

Check-Your-Understanding Answers

  1. To render at native resolution and avoid blur.
  2. The compositor interprets buffer pixels incorrectly.
  3. Store per-output state and create one panel per output.

Real-World Applications

  • Panels in GNOME/KDE handle scaling per output.

Where You’ll Apply It

References

  • Wayland output protocol
  • fractional-scale protocol (optional)

Key Insights

HiDPI correctness is a buffer allocation problem, not just a visual tweak.

Summary

Handle output scale by adjusting buffer size and setting buffer scale on surfaces.

Homework/Exercises to Practice the Concept

  1. Simulate scale=2 and verify crisp text.
  2. Add a log line showing logical vs buffer size.
  3. Create panels for two outputs with different scales.

Solutions to the Homework/Exercises

  1. Multiply all coordinates by 2 and render again.
  2. Print logical and buffer sizes on configure.
  3. Create separate panel structs per output.

2.4 Timers, Event Loops, and Efficient Updates

Fundamentals

Panels often update on a schedule: clocks tick, battery changes, network status changes. You need timers that integrate with the Wayland event loop. On Linux, timerfd is a convenient way to generate periodic events. The key is to update only when necessary and avoid busy loops. A well-behaved panel should be mostly idle when nothing changes. Use CLOCK_MONOTONIC to avoid jumps when system time changes, and keep timer handling outside render callbacks. Dirty flags and batched updates keep redraws under control, and you can coalesce multiple changes into one redraw. Combining timer events with input or IPC updates prevents unnecessary redraws.

Deep Dive into the concept

Wayland is event-driven, so your panel should follow the same model. You can integrate timerfd with poll or epoll alongside the Wayland socket. When the timer fires, set a flag to redraw and then render on the next iteration. This keeps your application responsive without wasting CPU. Another approach is to use wl_event_loop on the compositor side, but for clients a simple poll loop is enough.

Efficient updates matter. If you redraw every 16 ms, your panel will consume CPU even when static. Instead, update only when data changes. For a clock, updating once per second is sufficient. For status indicators, update on events rather than a fixed timer. The simplest model is a single timer for the clock plus explicit redraws for other events.

When you redraw, you should use wl_surface_damage_buffer to specify the region that changed. For example, if only the clock changed, damage that rectangle. This reduces compositor work. In practice, redrawing the entire panel is acceptable because it is small, but learning damage semantics here reinforces compositor-level concepts.

Timers also introduce determinism issues. If your tests depend on wall clock time, they become flaky. You should structure your code so you can inject a fixed time for tests, or at least provide a demo mode with a fixed clock string. This is required for reproducible golden-path outputs.

Timerfd has its own semantics: you must read the timerfd to clear the event, and multiple expirations can coalesce into a single wakeup. Your code should read the 64-bit counter and treat any nonzero value as “time to update”. Combined with a dirty flag, this allows you to update at most once per loop even if several timer ticks occurred. This pattern keeps your panel responsive without cascading redraws and avoids bursty CPU usage when the system is busy.

A robust loop should also manage Wayland flushing. After you update your buffer and call wl_surface_commit, you should call wl_display_flush so the compositor sees your requests promptly. If you use poll, make sure you handle the case where you have outgoing data but no incoming events; otherwise you can block and delay updates. A common pattern is: dispatch pending events, render if dirty, flush, then poll for new events. Keep all file descriptors non-blocking and treat EAGAIN as “nothing to do”, not as a hard error. These details matter when your panel runs for hours.

Deterministic testing is easier if you separate “state update” from “render”. For example, build a function that updates the clock string given a timestamp, and then render that string separately. In tests, provide a fixed timestamp and verify the rendered buffer contents or a checksum. In demo mode, you can freeze the time string entirely. This ensures that your golden path output and screenshots are stable across machines and time zones, which is essential for documentation and debugging. It also simplifies profiling and long-running stability checks. Keep logs concise for debugging.

How this fit on projects

This concept is used in Section 3.2 (functional requirements), Section 3.7 (deterministic demos), and Section 6 (testing strategy).

Definitions & key terms

  • timerfd -> Linux timer file descriptor
  • poll/epoll -> event loop mechanisms
  • damage -> region to redraw
  • deterministic output -> fixed outputs independent of system clock

Mental Model Diagram (ASCII)

[Wayland fd]   [timerfd]
      \          /
       \        /
        poll() loop
            |
            v
        redraw panel

How It Works (Step-by-Step)

  1. Create timerfd with 1-second interval.
  2. Use poll() on Wayland fd and timerfd.
  3. On timer, mark clock dirty.
  4. Redraw and commit on next loop iteration.

Invariants:

  • Redraw only when needed.
  • Event loop must dispatch Wayland events.

Failure modes:

  • Busy loop -> high CPU usage.
  • No timer -> clock never updates.

Minimal Concrete Example

int tfd = timerfd_create(CLOCK_MONOTONIC, 0);
struct itimerspec it = { .it_interval = {1,0}, .it_value = {1,0} };
timerfd_settime(tfd, 0, &it, NULL);

Common Misconceptions

  • “Redraw on every frame.” -> Panels should be event-driven.
  • “Timers are enough for all updates.” -> Some updates are event-based.

Check-Your-Understanding Questions

  1. Why is a busy loop bad for a panel?
  2. How do you integrate timerfd with Wayland dispatch?
  3. Why is deterministic time important for tests?

Check-Your-Understanding Answers

  1. It wastes CPU and power for little benefit.
  2. Use poll/epoll on both fds and dispatch when ready.
  3. It keeps demo outputs reproducible.

Real-World Applications

  • Status bars use timers for clocks and events for network/battery.

Where You’ll Apply It

References

  • timerfd man pages
  • Wayland event loop documentation

Key Insights

Panels should be mostly idle; update only when data changes.

Summary

Use timerfd and event-driven redraws to keep your panel efficient and predictable.

Homework/Exercises to Practice the Concept

  1. Implement a clock that updates every second.
  2. Add a manual “refresh” key that forces redraw.
  3. Add a debug mode with fixed time string.

Solutions to the Homework/Exercises

  1. Use timerfd and redraw on ticks.
  2. On key press, set dirty flag.
  3. Replace time() with a fixed string in demo mode.

3. Project Specification

3.1 What You Will Build

A panel application that:

  • Creates a layer-shell surface anchored to the top of the output
  • Reserves space with exclusive zone
  • Renders a background and a clock text
  • Updates once per second with low CPU usage

Excluded:

  • Complex widget toolkit
  • Animations or heavy effects

3.2 Functional Requirements

  1. Layer-shell surface: Create a layer surface at the TOP layer with anchors.
  2. Exclusive zone: Reserve panel height so windows avoid it.
  3. Rendering: Draw background and text into wl_shm buffer.
  4. Scaling: Handle output scale and render at buffer resolution.
  5. Timer updates: Update clock once per second without busy loop.

3.3 Non-Functional Requirements

  • Performance: CPU usage near idle when static.
  • Reliability: No protocol errors on configure/ack.
  • Usability: Panel visible on all outputs with correct size.

3.4 Example Usage / Output

$ ./panel --height 30 --exclusive 30
[panel] output=HDMI-A-1 scale=2
[panel] size=1920x30 (buffer 3840x60)

3.5 Data Formats / Schemas / Protocols

  • wlr-layer-shell-unstable-v1
  • wl_shm buffers with XRGB8888 or ARGB8888

3.6 Edge Cases

  • configure width/height 0 -> use default
  • output scale change -> recreate buffer
  • layer-shell missing -> exit with clear error

3.7 Real World Outcome

3.7.1 How to Run (Copy/Paste)

cc -O2 -Wall -o panel panel.c \
  $(pkg-config --cflags --libs wayland-client wayland-protocols)

./panel --height 30 --exclusive 30

3.7.2 Golden Path Demo (Deterministic)

  • Fixed height: 30
  • Fixed time string: “12:34”
  • Fixed background: #202020

3.7.3 If CLI: Exact Terminal Transcript

$ ./panel --height 30 --exclusive 30 --demo-time "12:34"
[panel] connected to wayland-0
[panel] output=HDMI-A-1 scale=1
[panel] configured size=1920x30
[panel] render clock="12:34"

Exit codes:

  • 0 on clean exit
  • 1 if layer-shell not available

3.7.4 If GUI / Desktop

ASCII wireframe:

+--------------------------------------------------------------+
|  [Workspaces]                12:34                [Net][Bat]|
+--------------------------------------------------------------+

3.7.5 Failure Demo (Deterministic)

$ ./panel
[Error] zwlr_layer_shell_v1 not advertised
[Exit] code=1

4. Solution Architecture

4.1 High-Level Design

Wayland socket
   |
   v
Panel client
  - layer-shell surface
  - buffer renderer
  - timer updates

4.2 Key Components

| Component | Responsibility | Key Decisions | |———–|—————-|—————| | Layer-shell handler | Configure anchors and exclusive zone | TOP layer, full width | | Renderer | Draw background and text | bitmap font | | Timer loop | Update clock | timerfd + poll |

4.3 Data Structures (No Full Code)

struct panel {
    struct wl_surface *surface;
    struct zwlr_layer_surface_v1 *layer;
    int logical_w, logical_h;
    int scale;
    struct wl_buffer *buffer;
    void *pixels;
    bool dirty;
};

4.4 Algorithm Overview

Key Algorithm: Configure-Driven Redraw

  1. Receive configure with size.
  2. Ack configure.
  3. Allocate buffer at size * scale.
  4. Draw panel content.
  5. Attach and commit.

Complexity Analysis:

  • Time: O(width * height) per redraw
  • Space: O(width * height * 4)

5. Implementation Guide

5.1 Development Environment Setup

sudo apt install libwayland-dev wayland-protocols

5.2 Project Structure

panel/
|-- src/
|   |-- panel.c
|   |-- render.c
|   `-- font.c
|-- assets/
|   `-- font8x8.h
`-- Makefile

5.3 The Core Question You’re Answering

“How do I create non-window UI elements in Wayland and keep them crisp and efficient?”

5.4 Concepts You Must Understand First

  1. Layer-shell protocol semantics
  2. wl_shm rendering and stride
  3. Output scaling
  4. Event-driven timers

5.5 Questions to Guide Your Design

  1. Will the panel reserve space or overlay windows?
  2. How will you handle output scale changes?
  3. How will you update the clock without wasting CPU?

5.6 Thinking Exercise

Draw a diagram of your panel layout and identify logical vs buffer coordinates.

5.7 The Interview Questions They’ll Ask

  1. What is layer-shell and how does it differ from xdg-shell?
  2. How do you avoid blurry text on HiDPI displays?
  3. Why should a panel be event-driven?

5.8 Hints in Layers

Hint 1: Start with a fixed-size panel Hardcode width and height, then later follow configure size.

Hint 2: Use a bitmap font It keeps dependencies minimal and predictable.

5.9 Books That Will Help

| Topic | Book | Chapter | |——-|——|———| | Wayland protocols | “The Wayland Book” | layer-shell sections | | Software rendering | “Computer Graphics from Scratch” | bitmap rendering |

5.10 Implementation Phases

Phase 1: Foundation (3-4 days)

Goals:

  • Bind layer-shell
  • Create surface and handle configure

Tasks:

  1. Create layer surface and anchors.
  2. Handle configure/ack.

Checkpoint: Panel appears with solid background.

Phase 2: Core Functionality (4-5 days)

Goals:

  • Render text
  • Timer updates

Tasks:

  1. Draw bitmap font clock.
  2. Add timerfd and update loop.

Checkpoint: Clock updates once per second.

Phase 3: Polish and Edge Cases (2-3 days)

Goals:

  • HiDPI scaling
  • Multi-monitor support

Tasks:

  1. Handle output scale events.
  2. Create one panel per output.

Checkpoint: Panels are crisp on all outputs.

5.11 Key Implementation Decisions

| Decision | Options | Recommendation | Rationale | |———-|———|—————-|———–| | Rendering | bitmap font, FreeType | bitmap font | minimal dependencies | | Updates | timer loop, event-driven | timerfd + event-driven | efficient | | Exclusive zone | 0, height | height | reserve space for panel |


6. Testing Strategy

6.1 Test Categories

| Category | Purpose | Examples | |———-|———|———-| | Unit Tests | Validate rendering math | stride, scale multiply | | Integration Tests | Protocol flow | configure -> ack -> commit | | Edge Case Tests | Multi-output | different scales |

6.2 Critical Test Cases

  1. Scale change: buffer resized and redrawn.
  2. Missing layer-shell: exits with error.
  3. Timer update: clock changes without high CPU.

6.3 Test Data

height=30
scale=2
clock="12:34"

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

| Pitfall | Symptom | Solution | |———|———|———-| | Not acking configure | panel never appears | ack serial before commit | | Ignoring scale | blurry text | multiply size by scale | | Redrawing in loop | high CPU | use timerfd |

7.2 Debugging Strategies

  • Use WAYLAND_DEBUG=1 to verify configure/ack/commit.
  • Log output scale and buffer size on each redraw.

7.3 Performance Traps

  • Redrawing more often than needed wastes CPU.

8. Extensions & Challenges

8.1 Beginner Extensions

  • Add a second text section (e.g., workspace list).

8.2 Intermediate Extensions

  • Add a clickable area with pointer events.

8.3 Advanced Extensions

  • Add layer-shell configuration options (overlay mode, per-output).

9. Real-World Connections

9.1 Industry Applications

  • Panels in Sway and Waybar are built on layer-shell.
  • Waybar: popular layer-shell panel
  • Yambar: minimal panel in C

9.3 Interview Relevance

  • Explain difference between xdg-shell and layer-shell.

10. Resources

10.1 Essential Reading

  • wlr-layer-shell protocol spec
  • “The Wayland Book” sections on layer-shell

10.2 Video Resources

  • Wayland compositor and panel talks

10.3 Tools & Documentation

  • wayland-info: inspect supported globals

11. Self-Assessment Checklist

11.1 Understanding

  • I can explain anchors and exclusive zone.
  • I can render text into a wl_shm buffer.
  • I can handle output scale correctly.

11.2 Implementation

  • Panel appears and reserves space.
  • Clock updates on timer without high CPU.
  • No protocol errors in WAYLAND_DEBUG.

11.3 Growth

  • I can extend the panel with new widgets.
  • I can handle multiple outputs.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • Panel appears at top of screen with clock text.
  • Exclusive zone prevents windows from covering panel.
  • Timer updates clock once per second.

Full Completion:

  • All minimum criteria plus:
  • HiDPI scaling handled correctly.
  • Multi-monitor panels supported.

Excellence (Going Above & Beyond):

  • Add configurable widgets and click handling.
  • Provide a config file for appearance.