lvgl-crystal

LVGL example in Crystal

lvgl-crystal

Crystal project scaffold for building LVGL GUI examples on a Debian Linux target.

This repository is intended as a practical starting point for:

  • experimenting with LVGL concepts in Crystal,
  • building small GUI demos and widgets,
  • validating Linux framebuffer/SDL-style integration strategies,
  • and documenting reproducible Debian setup steps.

Project Goals

  • Keep example code small and easy to run.
  • Provide a repeatable Debian setup for contributors.
  • Make it straightforward to add new GUI examples under a consistent structure.
  • Keep a strict 1-to-1 mapping between local examples and LVGL 9.4 upstream examples, and include source links in example comments.
  • Keep examples browseable in API docs through Examples::DocsGallery (image + source link per example).

Requirements

  • Debian 12+ (or compatible Debian-based distro)
  • Crystal 1.19.1 (or newer) with Shards
  • Git

Installation

  1. Clone the repository:

    git clone https://github.com/embedconsult/lvgl-crystal.git
    cd lvgl-crystal
    
  2. Install Crystal (Debian package or official install script).

  3. Install project dependencies:

    shards install
    
  4. Prepare LVGL and backend libraries for your Debian target.

Note: exact LVGL binding and backend wiring depends on your selected strategy (native C bindings, generated bindings, or wrapper shard).

Usage

Environment assumptions (Linux and macOS)

  • LVGL_BACKEND=headless is wired and recommended for CI/spec environments.
  • Headless execution requires LVGL test-module symbols to exist in lib/lvgl/build/crystal/liblvgl.so (built with -DLV_USE_TEST=1).
  • Runtime defaults to LVGL_BACKEND=macos on macOS and LVGL_BACKEND=sdl on other platforms when unset.
  • LVGL_BACKEND=wayland is wired when the loaded liblvgl.so provides native Wayland driver support (-DLV_USE_WAYLAND=1).
  • LVGL_BACKEND=sdl is wired when the loaded liblvgl.so provides native SDL driver support (-DLV_USE_SDL=1).
  • LVGL_BACKEND=framebuffer is wired when the loaded liblvgl.so provides native Linux framebuffer + evdev driver symbols (-DLV_USE_LINUX_FBDEV=1, -DLV_USE_EVDEV=1).
  • LVGL_BACKEND=macos is wired as a macOS profile over LVGL's SDL driver symbols (-DLV_USE_SDL=1), with optional LVGL_MACOS_WIDTH and LVGL_MACOS_HEIGHT overrides.

On Linux (Debian/Ubuntu), build prerequisites:

sudo apt-get update
sudo apt-get install -y build-essential clang lld pkg-config libsdl2-dev
# Install Crystal 1.19.1 and Shards

On macOS (MacPorts), install equivalent toolchain prerequisites:

sudo port install crystal shards pkgconfig libsdl2

Generate reference images from macro-collected example metadata

LVGL_BACKEND=headless crystal run scripts/generate_example_images.cr

Generated artifacts:

  • docs/images/*.png
  • macro-driven applet image/docs references in src/examples.cr

These generated images are intentionally not committed to the repository.

To verify CI consistency (metadata annotations + macro-generated docs index):

crystal run scripts/check_example_docs_index.cr

Run the LVGL examples menu (launches one selected applet at a time):

crystal run src/examples.cr

Build the default shard target:

shards build

Suggested Example Layout

As you add demos, keep examples organized by topic:

src/
  lvgl.cr
  lvgl/
    widgets/
  examples/
    basics/
    widgets/
    animations/

Debian Notes for LVGL Targets

Typical Linux dependencies you may need (adjust for your backend):

sudo apt-get update
sudo apt-get install -y build-essential pkg-config libsdl2-dev

For native Wayland backend builds (LVGL_BACKEND=wayland), include Wayland client development libraries when building LVGL:

sudo apt-get update
sudo apt-get install -y libwayland-dev wayland-protocols libxkbcommon-dev

For framebuffer backend builds (LVGL_BACKEND=framebuffer), ensure Linux framebuffer/evdev support is enabled in LVGL and run with access to your target devices (defaults are /dev/fb0 and /dev/input/event0, configurable via LVGL_FBDEV_DEVICE and LVGL_EVDEV_DEVICE).

For framebuffer or DRM/KMS targets, install the corresponding development packages and grant the required runtime permissions.

Internal Runtime Ownership Model

If you follow the Applet pattern and don't make LVGL calls outside of the provided fibers, then you should adhere to these requirements by default.

  • Use a single-owner fiber model for LVGL calls whenever possible.
  • Lvgl::Object/widget constructors auto-start runtime on first use (Lvgl::Runtime.start, idempotent).
  • Prefer deterministic cleanup with Lvgl::Runtime.shutdown during app teardown.
  • Treat direct LibLvgl.lv_init / LibLvgl.lv_deinit calls as low-level escape hatches.

Minimal lifecycle example (auto-start via first object):

root = Lvgl::Object.new(nil)
# ... create and manipulate widgets from the same UI fiber ...
Lvgl::Runtime.shutdown

If your app prefers explicit bring-up, calling Lvgl::Runtime.start before creating objects is still safe and idempotent.

LVGL Scheduler Concurrency Contract

Lvgl::Scheduler centralizes lv_tick_inc and lv_timer_handler behind one UI event-loop helper.

  • Only the UI fiber may call LVGL APIs or mutate LVGL objects/widgets.
  • Background fibers must marshal UI changes with scheduler.schedule { ... }.
  • The UI fiber periodically runs scheduler.step (or drain_scheduled_work, tick_inc, and timer_handler explicitly).

Minimal pattern:

scheduler = Lvgl::Scheduler.new(
  tick_period_ms: Lvgl::Scheduler::DEFAULT_TICK_PERIOD_MS,
  max_sleep_ms: Lvgl::Scheduler::DEFAULT_MAX_SLEEP_MS,
)

# background fiber
spawn do
  scheduler.schedule do
    # safe: executed later on the UI fiber
    label.set_text("Updated from worker")
  end
end

# UI fiber loop
loop do
  sleep scheduler.step.milliseconds
end

If this ownership rule is violated and multiple fibers touch LVGL directly, LVGL global/object state can race and produce undefined behavior including UI corruption and hard crashes.

Documentation style

  • API documentation conventions are described in DOCUMENTATION_STYLE_GUIDE.md.
  • Crystal-doc generation and gallery layout customization analysis is documented in CRYSTAL_DOCS_LAYOUT_ANALYSIS.md.
  • Prefer thorough, self-contained API docs so users can understand wrappers without leaving this repo.
  • Apply this guide to comments in src/lvgl/*.cr and src/lvgl/widgets/*.cr.
  • Optional docs lint check:
    crystal run scripts/check_public_docs.cr
    

Binding ownership

  • Raw C bindings are declared in src/lvgl/raw.cr under LibLvgl.
  • Canonical object wrappers live in src/lvgl/object.cr and widget wrappers live in src/lvgl/widgets/*.
  • New LibLvgl.lv_* usage should be introduced through one canonical Crystal method and reused from that method rather than adding direct raw calls in multiple files.
  • When you need a new symbol, add the wrapper at the closest ownership layer (Lvgl::Object, Lvgl::Widgets::*, backend adapter, or runtime helper), then call that wrapper from higher-level code.

Backend Adapter Profiles

The Crystal bindings now expose backend adapter profiles under src/lvgl/backend/:

  • FramebufferBackend (native Linux framebuffer + evdev profile)
  • HeadlessTestBackend (recommended for specs/CI)
  • MacosBackend (macOS profile using LVGL SDL driver symbols)
  • SdlBackend (native SDL window/profile when LVGL is built with SDL support)
  • WaylandBackend (native Wayland window/profile when LVGL is built with Wayland support)

LVGL_BACKEND selects the profile (macos by default on macOS, sdl elsewhere).

Headless test backend (Debian-first)

This repository is pinned to LVGL shard version 9.4.0, and the CI headless path uses the 9.4 test-module APIs from the source headers:

  • lib/lvgl/src/others/test/lv_test_display.h (lv_test_display_create)
  • lib/lvgl/src/others/test/lv_test_indev.h (lv_test_indev_create_all, lv_test_indev_delete_all)

Docs cross-reference used during implementation:

To run headless runtime specs in CI-like Debian environments, enable LVGL test symbols in the shared library:

sudo apt-get update
sudo apt-get install -y build-essential clang lld pkg-config

shards install
# ./scripts/build_lvgl_headless_test.sh (builds test + SDL support)
crystal spec

If test-module symbols are not available in the shared LVGL build (-DLV_USE_TEST=1), runtime-dependent specs are skipped with a clear reason from spec/support/lvgl_harness.cr.

SDL is wired to LVGL's native SDL driver symbols and opens an SDL window when those symbols are present in liblvgl.so. SDL window size can be configured with LVGL_SDL_WIDTH and LVGL_SDL_HEIGHT (defaults: 800x480). On macOS, LVGL_BACKEND=macos reuses those same SDL symbols and accepts LVGL_MACOS_WIDTH and LVGL_MACOS_HEIGHT (falls back to LVGL_SDL_*). Wayland is wired to LVGL's native Wayland driver symbols and opens a Wayland window when those symbols are present in liblvgl.so. Wayland window size can be configured with LVGL_WAYLAND_WIDTH and LVGL_WAYLAND_HEIGHT (defaults: 800x480). Framebuffer backend binds to LVGL_FBDEV_DEVICE and LVGL_EVDEV_DEVICE (defaults: /dev/fb0 and /dev/input/event0).

Development

Run specs:

crystal spec

Run the direct LibLvgl callsite duplicate check:

crystal run scripts/check_lvgl_callsites.cr

Format Crystal code:

crystal tool format

Generate API documentation locally:

crystal docs

CI/CD

  • GitHub Actions validates formatting/specs, builds binary artifacts, and publishes docs to GitHub Pages from main.
  • GitLab CI/CD validates formatting/specs, stores binary artifacts, and publishes docs using the pages job from the default branch.

TODO items

  • Add busybox-style symlink dispatch so one executable can select and run a single applet by invocation name.

Contributing

  1. Create a feature branch.
  2. Add or update one example at a time.
  3. Include run instructions for every new example.
  4. Run crystal spec and crystal tool format before submitting.

Contributors

Repository

lvgl-crystal

Owner
Statistic
  • 0
  • 0
  • 0
  • 1
  • 2
  • 8 days ago
  • February 11, 2026
License

MIT License

Links
Synced at

Tue, 03 Mar 2026 23:04:09 GMT

Languages