Trackball rewrite (#600)

- Implement generic trackball settings
- Refactor T-Deck trackball driver into generic driver at `Drivers/gpio-trackball-module`
- Automatically bind/unbind trackball devices with LVGL
- Automatically load settings for trackball devices on boot
This commit is contained in:
Ken Van Hoeylandt
2026-07-30 13:38:19 +02:00
committed by GitHub
parent de6fc3b346
commit c729e8340f
39 changed files with 1194 additions and 666 deletions
@@ -0,0 +1,26 @@
#pragma once
#include <new>
struct Device;
/**
* Common driver-data wrapper attached to every LVGL display/indev created by this module: bundles
* the kernel Device* with an optional device-type-specific context blob (LvglDisplayCtx,
* LvglPointerCtx, LvglTrackballCtx, ...), so each device type doesn't have to store its own Device*
* redundantly. Owns context: any resources referenced through it (buffers, cursor objects, etc.)
* must be released by the caller before the wrapper is deleted, since context is trivially
* destructible and only its own memory is freed here.
*/
struct LvglDeviceContext {
struct Device* device = nullptr;
void* context;
explicit LvglDeviceContext(void* context) : context(context) {}
~LvglDeviceContext() {
if (context != nullptr) {
::operator delete(context);
}
}
};
@@ -0,0 +1,107 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <lvgl.h>
#include <tactility/device.h>
#include <tactility/error.h>
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Trackball operating mode.
*/
enum LvglTrackballMode {
/** Navigation via enc_diff (scroll wheel behavior). No cursor is shown. */
LVGL_TRACKBALL_MODE_ENCODER,
/** Mouse cursor via point.x/point.y. A cursor is shown if an image was set via
* lvgl_trackball_set_cursor_image(). */
LVGL_TRACKBALL_MODE_POINTER
};
/**
* @brief Runtime-adjustable trackball behavior. See lvgl_trackball_set_settings()/
* lvgl_trackball_get_settings().
*/
struct LvglTrackballSettings {
enum LvglTrackballMode mode;
/** When false, movement and button presses are drained from the device and discarded:
* the indev reports no movement and stays released. */
bool enabled;
/** Encoder mode: steps per raw trackball tick. Must be >= 1. */
uint8_t encoder_sensitivity;
/** Pointer mode: pixels per raw trackball tick. Must be >= 1. */
uint8_t pointer_sensitivity;
};
/**
* @brief Creates an lv_indev_t bound to the given TRACKBALL_TYPE device and registers a read
* callback that polls the device through its TrackballApi.
*
* The indev starts in LVGL_TRACKBALL_MODE_ENCODER, enabled, with default sensitivities (see
* lvgl_trackball_settings_get_default()). Call lvgl_trackball_set_settings() to change this,
* e.g. to switch to pointer mode - each device added this way can independently run with or
* without pointer support.
*
* @warning Caller must hold the LVGL lock (see lvgl_lock() in lvgl_module.h) — call this from
* LvglModuleConfig.on_start, or after calling lvgl_lock() explicitly.
*
* @param[in] device a device of type TRACKBALL_TYPE
* @param[in] display the display this indev should be associated with, or NULL to leave it unset
* @param[out] out_indev the created indev, valid only when ERROR_NONE is returned
* @retval ERROR_NONE on success
* @retval ERROR_INVALID_ARGUMENT if device or out_indev is NULL, or device is not of type TRACKBALL_TYPE
* @retval ERROR_OUT_OF_MEMORY if allocation failed
*/
error_t lvgl_trackball_add(struct Device* device, lv_display_t* display, lv_indev_t** out_indev);
/**
* @brief Removes an indev previously created with lvgl_trackball_add(), including its cursor
* object (if one was showing).
* @warning Caller must hold the LVGL lock.
*/
void lvgl_trackball_remove(lv_indev_t* indev);
/**
* @return the default (all fields explicitly set) LvglTrackballSettings: encoder mode, enabled,
* encoder_sensitivity 1, pointer_sensitivity 10.
*/
struct LvglTrackballSettings lvgl_trackball_settings_get_default(void);
/**
* @brief Applies settings to an indev previously created with lvgl_trackball_add(). Switching
* `mode` shows/hides the cursor (pointer mode) and re-centers it on the indev's display.
* @warning Caller must hold the LVGL lock.
* @param[in] indev an indev previously created by lvgl_trackball_add()
* @param[in] settings the settings to apply
* @retval ERROR_NONE on success
* @retval ERROR_INVALID_ARGUMENT if indev or settings is NULL, or either sensitivity is 0
*/
error_t lvgl_trackball_set_settings(lv_indev_t* indev, const struct LvglTrackballSettings* settings);
/**
* @brief Retrieves the settings currently active on indev.
* @warning Caller must hold the LVGL lock.
* @return true when indev is a trackball indev (out_settings is filled), false otherwise
*/
bool lvgl_trackball_get_settings(lv_indev_t* indev, struct LvglTrackballSettings* out_settings);
/**
* @brief Sets the image shown as the mouse cursor while indev is in LVGL_TRACKBALL_MODE_POINTER.
* lvgl-module has no bundled cursor asset (it doesn't depend on any particular asset layout), so
* pointer mode has no visible cursor until a caller supplies one here; the indev still moves and
* reports point.x/point.y either way. Pass NULL to remove the cursor.
* @warning Caller must hold the LVGL lock.
* @param[in] indev an indev previously created by lvgl_trackball_add()
* @param[in] image_src an LVGL image source (as accepted by lv_image_set_src()), or NULL
*/
void lvgl_trackball_set_cursor_image(lv_indev_t* indev, const void* image_src);
#ifdef __cplusplus
}
#endif
@@ -1,119 +0,0 @@
#include <lvgl/devices/display.h>
#include <lvgl/devices/keyboard.h>
#include <lvgl/devices/pointer.h>
#include <lvgl/lvgl.h>
#include <tactility/device.h>
#include <tactility/drivers/display.h>
#include <tactility/drivers/keyboard.h>
#include <tactility/drivers/pointer.h>
#include <tactility/log.h>
#include <lvgl.h>
#define TAG "lvgl"
void lvgl_devices_attach() {
lvgl_lock();
lv_disp_t* lvgl_display = NULL;
bool display_updates_slowly = false;
struct Device* kernel_display_device = NULL;
device_get_first_by_type(&DISPLAY_TYPE, &kernel_display_device);
// Placeholder drivers (boards not yet migrated to the kernel display driver) register with a
// NULL api: they exist so the devicetree node resolves, but have nothing for LVGL to bind to.
if (kernel_display_device != NULL && device_get_driver(kernel_display_device)->api == NULL) {
device_put(kernel_display_device);
kernel_display_device = NULL;
}
if (kernel_display_device != NULL) {
uint16_t vres = display_get_resolution_y(kernel_display_device);
enum DisplayColorFormat color_format = display_get_color_format(kernel_display_device);
bool swap_bytes = color_format == DISPLAY_COLOR_FORMAT_RGB565_SWAPPED ||
color_format == DISPLAY_COLOR_FORMAT_BGR565_SWAPPED ||
color_format == DISPLAY_COLOR_FORMAT_BGR565;
bool display_requires_full_frame = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME);
display_updates_slowly = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_SLOW_REFRESH);
// Without CAP_SWAP_XY the driver can't rotate 90/270 in hardware (display_swap_xy() is
// null and silently skipped by lvgl_display_apply_rotation()) - LVGL would still switch
// its own logical w/h for those rotations, mismatching the panel's fixed physical
// orientation (e.g. RGB/DPI panels, whose video timing is fixed at panel-init time).
// sw_rotate makes LVGL rotate the rendered pixels in software instead, so the driver
// itself is never asked to do something it can't.
bool can_hw_rotate = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_CAP_SWAP_XY) &&
display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_CAP_MIRROR);
struct LvglDisplayConfig lvgl_display_config = {
.buffer_height = vres > 10 ? vres / 10 : vres,
.sw_rotate = !can_hw_rotate,
.swap_bytes = swap_bytes,
.force_full_frame = display_requires_full_frame
};
if (lvgl_display_add(kernel_display_device, &lvgl_display_config, &lvgl_display) == ERROR_NONE) {
LOG_I(TAG, "Bound %s to LVGL", kernel_display_device->name);
} else {
LOG_E(TAG, "Failed to bind %s to LVGL", kernel_display_device->name);
}
device_put(kernel_display_device);
}
struct Device* kernel_pointer_device = NULL;
device_get_first_by_type(&POINTER_TYPE, &kernel_pointer_device);
if (kernel_pointer_device != NULL && device_get_driver(kernel_pointer_device)->api == NULL) {
device_put(kernel_pointer_device);
kernel_pointer_device = NULL;
}
if (kernel_pointer_device != NULL) {
lv_indev_t* lvgl_pointer_device;
if (lvgl_pointer_add(kernel_pointer_device, lvgl_display, &lvgl_pointer_device) == ERROR_NONE) {
LOG_I(TAG, "Bound %s to LVGL", kernel_pointer_device->name);
// Slow panels cause taps to be missed due to the long update time, prevent that
if (display_updates_slowly ) {
lv_indev_set_long_press_time(lvgl_pointer_device, 2000);
}
} else {
LOG_E(TAG, "Failed to bind %s to LVG", kernel_pointer_device->name);
}
device_put(kernel_pointer_device);
}
struct Device* kernel_keyboard_device = NULL;
device_get_first_by_type(&KEYBOARD_TYPE, &kernel_keyboard_device);
lv_indev_t* lvgl_keyboard_device;
if (kernel_keyboard_device != NULL) {
if (lvgl_keyboard_add(kernel_keyboard_device, lvgl_display, &lvgl_keyboard_device) == ERROR_NONE) {
LOG_I(TAG, "Bound %s to LVGL", kernel_keyboard_device->name);
} else {
LOG_E(TAG, "Failed to bind %s to LVGL", kernel_keyboard_device->name);
}
device_put(kernel_keyboard_device);
}
lvgl_unlock();
}
void lvgl_devices_detach() {
lvgl_lock();
lv_indev_t* device = lv_indev_get_next(NULL);
while (device != NULL) {
lv_indev_type_t type = lv_indev_get_type(device);
if (type == LV_INDEV_TYPE_POINTER) {
lvgl_pointer_remove(device);
} else if (type == LV_INDEV_TYPE_KEYPAD) {
lvgl_keyboard_remove(device);
} else {
lv_indev_delete(device);
}
// Always get the first item, because getting the next one doesn't work as the current pointer just became corrupt
device = lv_indev_get_next(NULL);
}
lv_disp_t* display = lv_disp_get_next(NULL);
while (display != NULL) {
lvgl_display_remove(display);
display = lv_disp_get_next(NULL);
}
lvgl_unlock();
}
@@ -0,0 +1,185 @@
#include <lvgl/devices/display.h>
#include <lvgl/devices/device_context.h>
#include <lvgl/devices/keyboard.h>
#include <lvgl/devices/pointer.h>
#include <lvgl/devices/trackball.h>
#include <lvgl/lvgl.h>
#include <tactility/device.h>
#include <tactility/drivers/display.h>
#include <tactility/drivers/keyboard.h>
#include <tactility/drivers/pointer.h>
#include <tactility/drivers/trackball.h>
#include <tactility/log.h>
#include <lvgl.h>
#include <cstddef>
constexpr auto* TAG = "lvgl";
// Boards are not expected to expose more devices of a single type than this; device_for_each_of_type()
// callbacks run under the device ledger lock, so devices are collected here and processed afterwards.
constexpr auto LVGL_DEVICES_MAX_PER_TYPE = 8;
struct LvglDeviceList {
struct Device* devices[LVGL_DEVICES_MAX_PER_TYPE];
size_t count;
};
extern "C" {
static bool lvgl_device_list_collect(struct Device* device, void* context) {
struct LvglDeviceList* list = (struct LvglDeviceList*)context;
if (list->count < LVGL_DEVICES_MAX_PER_TYPE) {
list->devices[list->count++] = device;
}
return true;
}
void lvgl_devices_attach() {
lvgl_lock();
lv_disp_t* lvgl_display = NULL;
bool display_updates_slowly = false;
struct LvglDeviceList display_devices = {0};
device_for_each_of_type(&DISPLAY_TYPE, &display_devices, lvgl_device_list_collect);
for (size_t i = 0; i < display_devices.count; i++) {
struct Device* kernel_display_device = display_devices.devices[i];
// Placeholder drivers (boards not yet migrated to the kernel display driver) register with a
// NULL api: they exist so the devicetree node resolves, but have nothing for LVGL to bind to.
if (device_get_driver(kernel_display_device)->api == NULL) {
continue;
}
uint16_t vres = display_get_resolution_y(kernel_display_device);
enum DisplayColorFormat color_format = display_get_color_format(kernel_display_device);
bool swap_bytes = color_format == DISPLAY_COLOR_FORMAT_RGB565_SWAPPED ||
color_format == DISPLAY_COLOR_FORMAT_BGR565_SWAPPED ||
color_format == DISPLAY_COLOR_FORMAT_BGR565;
bool display_requires_full_frame = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME);
// Without CAP_SWAP_XY the driver can't rotate 90/270 in hardware (display_swap_xy() is
// null and silently skipped by lvgl_display_apply_rotation()) - LVGL would still switch
// its own logical w/h for those rotations, mismatching the panel's fixed physical
// orientation (e.g. RGB/DPI panels, whose video timing is fixed at panel-init time).
// sw_rotate makes LVGL rotate the rendered pixels in software instead, so the driver
// itself is never asked to do something it can't.
bool can_hw_rotate = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_CAP_SWAP_XY) &&
display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_CAP_MIRROR);
struct LvglDisplayConfig lvgl_display_config = {
.buffer_height = (uint16_t)(vres > 10 ? vres / 10 : vres),
.sw_rotate = !can_hw_rotate,
.swap_bytes = swap_bytes,
.force_full_frame = display_requires_full_frame
};
lv_disp_t* added_display = NULL;
if (lvgl_display_add(kernel_display_device, &lvgl_display_config, &added_display) == ERROR_NONE) {
LOG_I(TAG, "Bound %s to LVGL", kernel_display_device->name);
// Pointers/keyboards below bind to the first display bound here, matching that display's
// refresh behavior.
if (lvgl_display == NULL) {
lvgl_display = added_display;
display_updates_slowly = display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_SLOW_REFRESH);
}
} else {
LOG_E(TAG, "Failed to bind %s to LVGL", kernel_display_device->name);
}
}
struct LvglDeviceList pointer_devices = {0};
device_for_each_of_type(&POINTER_TYPE, &pointer_devices, lvgl_device_list_collect);
for (size_t i = 0; i < pointer_devices.count; i++) {
struct Device* kernel_pointer_device = pointer_devices.devices[i];
if (device_get_driver(kernel_pointer_device)->api == NULL) {
continue;
}
lv_indev_t* lvgl_pointer_device;
if (lvgl_pointer_add(kernel_pointer_device, lvgl_display, &lvgl_pointer_device) == ERROR_NONE) {
LOG_I(TAG, "Bound %s to LVGL", kernel_pointer_device->name);
// Slow panels cause taps to be missed due to the long update time, prevent that
if (display_updates_slowly ) {
lv_indev_set_long_press_time(lvgl_pointer_device, 2000);
}
} else {
LOG_E(TAG, "Failed to bind %s to LVG", kernel_pointer_device->name);
}
}
struct LvglDeviceList keyboard_devices = {0};
device_for_each_of_type(&KEYBOARD_TYPE, &keyboard_devices, lvgl_device_list_collect);
for (size_t i = 0; i < keyboard_devices.count; i++) {
struct Device* kernel_keyboard_device = keyboard_devices.devices[i];
lv_indev_t* lvgl_keyboard_device;
if (lvgl_keyboard_add(kernel_keyboard_device, lvgl_display, &lvgl_keyboard_device) == ERROR_NONE) {
LOG_I(TAG, "Bound %s to LVGL", kernel_keyboard_device->name);
} else {
LOG_E(TAG, "Failed to bind %s to LVGL", kernel_keyboard_device->name);
}
}
struct Device* kernel_trackball_device = NULL;
device_get_first_by_type(&TRACKBALL_TYPE, &kernel_trackball_device);
if (kernel_trackball_device != NULL) {
lv_indev_t* lvgl_trackball_device;
if (lvgl_trackball_add(kernel_trackball_device, lvgl_display, &lvgl_trackball_device) == ERROR_NONE) {
LOG_I(TAG, "Bound %s to LVGL", kernel_trackball_device->name);
} else {
LOG_E(TAG, "Failed to bind %s to LVGL", kernel_trackball_device->name);
}
device_put(kernel_trackball_device);
}
lvgl_unlock();
}
void lvgl_devices_detach() {
lvgl_lock();
lv_indev_t* indev = lv_indev_get_next(NULL);
while (indev != NULL) {
lv_indev_type_t type = lv_indev_get_type(indev);
if (type == LV_INDEV_TYPE_POINTER || type == LV_INDEV_TYPE_ENCODER) {
bool handled = false;
void* driver_data = lv_indev_get_driver_data(indev);
if (driver_data != nullptr) {
// Trackball is a special pointer type as it can operate as a mouse or as a device that changes widget focus.
LvglDeviceContext* context = static_cast<LvglDeviceContext*>(driver_data);
if (context->device != nullptr) {
const DeviceType* device_type = device_get_type(context->device);
if (device_type == &TRACKBALL_TYPE) {
lvgl_trackball_remove(indev);
handled = true;
} else if (device_type == &POINTER_TYPE) {
lvgl_pointer_remove(indev);
handled = true;
} else {
LOG_E(TAG, "Unknown pointer device with possible memory leak of driver data");
}
} else {
LOG_W(TAG, "Unknown pointer device (no data attached)");
}
}
if (!handled) {
lv_indev_delete(indev);
}
} else if (type == LV_INDEV_TYPE_KEYPAD) {
lvgl_keyboard_remove(indev);
} else {
lv_indev_delete(indev);
}
// Always get the first item, because getting the next one doesn't work as the current pointer just became corrupt
indev = lv_indev_get_next(NULL);
}
lv_disp_t* display = lv_disp_get_next(NULL);
while (display != NULL) {
lvgl_display_remove(display);
display = lv_disp_get_next(NULL);
}
lvgl_unlock();
}
} // extern C
@@ -8,16 +8,17 @@
#include <tactility/drivers/display.h>
#include <tactility/log.h>
#include <lvgl/devices/device_context.h>
#include <stdlib.h>
#ifdef ESP_PLATFORM
#include <esp_heap_caps.h>
#endif
#define TAG "lvgl_display"
constexpr auto* TAG = "lvgl_display";
struct LvglDisplayCtx {
struct Device* device;
void* buf1;
void* buf2;
bool owns_buffers; // false when buf1/buf2 point at the device's own frame buffer(s)
@@ -108,7 +109,9 @@ static bool lvgl_display_map_color_format(enum DisplayColorFormat in, lv_color_f
}
}
static void lvgl_display_apply_rotation(struct LvglDisplayCtx* ctx, lv_display_rotation_t rotation) {
static void lvgl_display_apply_rotation(struct LvglDeviceContext* wrapper, lv_display_rotation_t rotation) {
struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)wrapper->context;
// SW-rotated displays stay in their base orientation; rotation is applied per-flush instead.
if (ctx->sw_rotate) {
return;
@@ -144,10 +147,10 @@ static void lvgl_display_apply_rotation(struct LvglDisplayCtx* ctx, lv_display_r
}
if (ctx->has_swap_xy_cap) {
display_swap_xy(ctx->device, swap_xy);
display_swap_xy(wrapper->device, swap_xy);
}
if (ctx->has_mirror_cap) {
display_mirror(ctx->device, mirror_x, mirror_y);
display_mirror(wrapper->device, mirror_x, mirror_y);
}
if (ctx->has_set_gap_cap) {
// set_gap() takes its (x,y) in whatever axes the panel is currently drawn with, not the
@@ -155,14 +158,14 @@ static void lvgl_display_apply_rotation(struct LvglDisplayCtx* ctx, lv_display_r
bool gap_axes_swapped = swap_xy != ctx->base_swap_xy;
int32_t gap_x = gap_axes_swapped ? ctx->base_gap_y : ctx->base_gap_x;
int32_t gap_y = gap_axes_swapped ? ctx->base_gap_x : ctx->base_gap_y;
display_set_gap(ctx->device, gap_x, gap_y);
display_set_gap(wrapper->device, gap_x, gap_y);
}
}
static void lvgl_display_rotation_event_cb(lv_event_t* e) {
struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)lv_event_get_user_data(e);
struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_event_get_user_data(e);
lv_display_t* disp = (lv_display_t*)lv_event_get_current_target(e);
lvgl_display_apply_rotation(ctx, lv_display_get_rotation(disp));
lvgl_display_apply_rotation(wrapper, lv_display_get_rotation(disp));
}
// Returns which of buf1/buf2 (the real frame buffers, when !owns_buffers) color_map falls inside.
@@ -206,7 +209,8 @@ static void* lvgl_display_try_ppa_rotate(struct LvglDisplayCtx* ctx, const uint8
}
static void lvgl_display_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* color_map) {
struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)lv_display_get_driver_data(disp);
struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_display_get_driver_data(disp);
struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)wrapper->context;
bool is_i1 = lv_display_get_color_format(disp) == LV_COLOR_FORMAT_I1;
int32_t x1 = area->x1;
@@ -280,8 +284,8 @@ static void lvgl_display_flush_cb(lv_display_t* disp, const lv_area_t* area, uin
} else {
fb_base = (uint8_t*)lvgl_display_fb_base(ctx, color_map);
}
uint16_t hres = display_get_resolution_x(ctx->device);
uint16_t vres = display_get_resolution_y(ctx->device);
uint16_t hres = display_get_resolution_x(wrapper->device);
uint16_t vres = display_get_resolution_y(wrapper->device);
if (rotating) {
// fb_base now holds the whole completed frame, but still in LVGL's *logical*
@@ -303,13 +307,13 @@ static void lvgl_display_flush_cb(lv_display_t* disp, const lv_area_t* area, uin
}
}
display_draw_bitmap(ctx->device, 0, 0, hres, vres, fb_base);
display_draw_bitmap(wrapper->device, 0, 0, hres, vres, fb_base);
}
} else if (ctx->owns_buffers) {
// PARTIAL mode: each flush_cb call is one independent, complete tile into a buffer that
// gets reused for the next tile, so present it immediately rather than waiting.
// LVGL's area is inclusive; DisplayApi's draw_bitmap wants an exclusive end.
display_draw_bitmap(ctx->device, x1, y1, x2 + 1, y2 + 1, color_map);
display_draw_bitmap(wrapper->device, x1, y1, x2 + 1, y2 + 1, color_map);
}
// DisplayApi has no async completion callback, so draw_bitmap is synchronous.
lv_display_flush_ready(disp);
@@ -335,11 +339,17 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig*
uint8_t fb_count = display_get_frame_buffer_count(device);
uint8_t bpp = lv_color_format_get_size(lv_color_format);
struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)calloc(1, sizeof(struct LvglDisplayCtx));
struct LvglDisplayCtx* ctx = new(std::nothrow) LvglDisplayCtx();
if (ctx == NULL) {
return ERROR_OUT_OF_MEMORY;
}
ctx->device = device;
struct LvglDeviceContext* wrapper = new(std::nothrow) LvglDeviceContext(ctx);
if (wrapper == NULL) {
delete ctx;
return ERROR_OUT_OF_MEMORY;
}
wrapper->device = device;
ctx->byte_swap = config->swap_bytes;
ctx->sw_rotate = config->sw_rotate;
// Only relevant when sw_rotate is set - lvgl_display_try_ppa_rotate() also checks
@@ -388,7 +398,7 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig*
buf_size_bytes = (size_t)((hres + 7) / 8) * vres + 8;
ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes);
if (ctx->buf1 == NULL) {
free(ctx);
delete wrapper;
return ERROR_OUT_OF_MEMORY;
}
ctx->owns_buffers = true;
@@ -408,14 +418,14 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig*
ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes);
if (ctx->buf1 == NULL) {
free(ctx);
delete wrapper;
return ERROR_OUT_OF_MEMORY;
}
if (config->double_buffer) {
ctx->buf2 = lvgl_display_alloc_buffer(buf_size_bytes);
if (ctx->buf2 == NULL) {
lvgl_display_free_buffer(ctx->buf1);
free(ctx);
delete wrapper;
return ERROR_OUT_OF_MEMORY;
}
}
@@ -432,7 +442,7 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig*
lvgl_display_free_buffer(ctx->buf1);
lvgl_display_free_buffer(ctx->buf2);
}
free(ctx);
delete wrapper;
return ERROR_OUT_OF_MEMORY;
}
}
@@ -444,7 +454,7 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig*
lvgl_display_free_buffer(ctx->buf2);
}
lvgl_display_free_buffer(ctx->rotate_buf);
free(ctx);
delete wrapper;
return ERROR_OUT_OF_MEMORY;
}
@@ -452,11 +462,11 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig*
lv_display_set_color_format(disp, lv_color_format);
lv_display_set_buffers(disp, ctx->buf1, ctx->buf2, buf_size_bytes, render_mode);
lv_display_set_flush_cb(disp, lvgl_display_flush_cb);
lv_display_set_driver_data(disp, ctx);
lv_display_add_event_cb(disp, lvgl_display_rotation_event_cb, LV_EVENT_RESOLUTION_CHANGED, ctx);
lv_display_set_driver_data(disp, wrapper);
lv_display_add_event_cb(disp, lvgl_display_rotation_event_cb, LV_EVENT_RESOLUTION_CHANGED, wrapper);
// Apply once explicitly, independent of whether LV_EVENT_RESOLUTION_CHANGED fires on creation.
lvgl_display_apply_rotation(ctx, lv_display_get_rotation(disp));
lvgl_display_apply_rotation(wrapper, lv_display_get_rotation(disp));
*out_display = disp;
return ERROR_NONE;
@@ -467,10 +477,11 @@ void lvgl_display_remove(lv_display_t* display) {
return;
}
struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)lv_display_get_driver_data(display);
struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_display_get_driver_data(display);
lv_display_delete(display);
if (ctx != NULL) {
if (wrapper != NULL) {
struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)wrapper->context;
if (ctx->owns_buffers) {
if (ctx->buf1 != NULL) {
lvgl_display_free_buffer(ctx->buf1);
@@ -485,6 +496,6 @@ void lvgl_display_remove(lv_display_t* display) {
if (ctx->ppa_handle != NULL) {
lvgl_ppa_delete(ctx->ppa_handle);
}
free(ctx);
delete wrapper;
}
}
+15 -20
View File
@@ -1,16 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
#include <lvgl/devices/keyboard.h>
#include <lvgl/devices/device_context.h>
#include <lvgl/lvgl.h>
#include <tactility/drivers/keyboard.h>
#include <stdlib.h>
#include <vector>
struct LvglKeyboardCtx {
Device* device;
};
static LvglSoftwareKeyboard last_software_keyboard = {
.object = nullptr
};
@@ -39,10 +35,10 @@ void lvgl_keyboard_on_stop_lvgl() {
}
static void lvgl_keyboard_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
LvglKeyboardCtx* ctx = static_cast<struct LvglKeyboardCtx*>(lv_indev_get_driver_data(indev));
auto* wrapper = static_cast<LvglDeviceContext*>(lv_indev_get_driver_data(indev));
KeyboardKeyData key_data = {};
if (keyboard_read_key(ctx->device, &key_data) != ERROR_NONE) {
if (keyboard_read_key(wrapper->device, &key_data) != ERROR_NONE) {
data->state = LV_INDEV_STATE_RELEASED;
data->continue_reading = false;
return;
@@ -62,21 +58,21 @@ error_t lvgl_keyboard_add(struct Device* device, lv_display_t* display, lv_indev
return ERROR_INVALID_ARGUMENT;
}
LvglKeyboardCtx* ctx = static_cast<struct LvglKeyboardCtx*>(malloc(sizeof(struct LvglKeyboardCtx)));
if (ctx == NULL) {
auto* wrapper = new(std::nothrow) LvglDeviceContext(nullptr);
if (wrapper == NULL) {
return ERROR_OUT_OF_MEMORY;
}
ctx->device = device;
wrapper->device = device;
lv_indev_t* indev = lv_indev_create();
if (indev == NULL) {
free(ctx);
delete wrapper;
return ERROR_OUT_OF_MEMORY;
}
lv_indev_set_type(indev, LV_INDEV_TYPE_KEYPAD);
lv_indev_set_read_cb(indev, lvgl_keyboard_read_cb);
lv_indev_set_driver_data(indev, ctx);
lv_indev_set_driver_data(indev, wrapper);
if (display != NULL) {
lv_indev_set_display(indev, display);
}
@@ -90,9 +86,9 @@ void lvgl_keyboard_remove(lv_indev_t* indev) {
return;
}
LvglKeyboardCtx* ctx = (struct LvglKeyboardCtx*)lv_indev_get_driver_data(indev);
auto* wrapper = static_cast<LvglDeviceContext*>(lv_indev_get_driver_data(indev));
lv_indev_delete(indev);
free(ctx);
delete wrapper;
}
void lvgl_keyboard_enable(lv_indev_t* indev) {
@@ -115,22 +111,21 @@ bool lvgl_hardware_keyboard_is_available() {
}
void lvgl_hardware_keyboard_add_custom(lv_indev_t* indev) {
LvglKeyboardCtx* ctx = static_cast<struct LvglKeyboardCtx*>(malloc(sizeof(struct LvglKeyboardCtx)));
if (ctx == nullptr) {
auto* wrapper = new(std::nothrow) LvglDeviceContext(nullptr);
if (wrapper == nullptr) {
return;
}
ctx->device = nullptr;
lv_indev_set_driver_data(indev, ctx);
lv_indev_set_driver_data(indev, wrapper);
lvgl_keyboard_enable(indev);
}
void lvgl_hardware_keyboard_remove_custom(lv_indev_t* indev) {
lvgl_keyboard_disable(indev);
auto* data = lv_indev_get_driver_data(indev);
auto* wrapper = static_cast<LvglDeviceContext*>(lv_indev_get_driver_data(indev));
lv_indev_set_driver_data(indev, nullptr);
free(data); // LvglKeyboardCtx*
delete wrapper;
}
static void textarea_show_keyboard(lv_event_t* event) {
@@ -1,14 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
#include <lvgl/devices/pointer.h>
#include <lvgl/devices/device_context.h>
#include <tactility/drivers/pointer.h>
#include <stdlib.h>
#define TAG "lvgl_pointer"
constexpr auto* TAG = "lvgl_pointer";
struct LvglPointerCtx {
struct Device* device;
bool calibration_enabled;
struct LvglPointerCalibration calibration;
};
@@ -60,13 +58,13 @@ static void lvgl_pointer_calibration_apply(
// fixed pixel dimensions, not a rotation. This function has no notion of LVGL rotation at all:
// calibration corrects the raw sensor's fixed physical mapping, which never changes with on-screen
// orientation, so it doesn't belong anywhere near rotation math.
static bool lvgl_pointer_read_calibrated(struct LvglPointerCtx* ctx, int32_t native_x_max, int32_t native_y_max, uint16_t* x, uint16_t* y) {
if (pointer_read_data(ctx->device, LVGL_POINTER_READ_TIMEOUT) != ERROR_NONE) {
static bool lvgl_pointer_read_calibrated(struct Device* device, struct LvglPointerCtx* ctx, int32_t native_x_max, int32_t native_y_max, uint16_t* x, uint16_t* y) {
if (pointer_read_data(device, LVGL_POINTER_READ_TIMEOUT) != ERROR_NONE) {
return false;
}
uint8_t point_count = 0;
if (!pointer_get_touched_points(ctx->device, x, y, NULL, &point_count, 1) || point_count == 0) {
if (!pointer_get_touched_points(device, x, y, NULL, &point_count, 1) || point_count == 0) {
return false;
}
@@ -82,7 +80,8 @@ static bool lvgl_pointer_read_calibrated(struct LvglPointerCtx* ctx, int32_t nat
// logical space - unconditionally, since native-space coordinates always need this regardless of
// whether calibration is enabled.
static void lvgl_pointer_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)lv_indev_get_driver_data(indev);
struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev);
struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)wrapper->context;
lv_display_t* display = lv_indev_get_display(indev);
// lv_display_get_original_*_resolution() is the native (LV_DISPLAY_ROTATION_0) size,
@@ -92,7 +91,7 @@ static void lvgl_pointer_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
uint16_t x = 0;
uint16_t y = 0;
if (!lvgl_pointer_read_calibrated(ctx, native_x_max, native_y_max, &x, &y)) {
if (!lvgl_pointer_read_calibrated(wrapper->device, ctx, native_x_max, native_y_max, &x, &y)) {
data->state = LV_INDEV_STATE_RELEASED;
return;
}
@@ -110,21 +109,26 @@ error_t lvgl_pointer_add(struct Device* device, lv_display_t* display, lv_indev_
return ERROR_INVALID_ARGUMENT;
}
struct LvglPointerCtx* ctx = calloc(1, sizeof(struct LvglPointerCtx));
struct LvglPointerCtx* ctx = new(std::nothrow) LvglPointerCtx();
if (ctx == NULL) {
return ERROR_OUT_OF_MEMORY;
}
ctx->device = device;
struct LvglDeviceContext* wrapper = new(std::nothrow) LvglDeviceContext(ctx);
if (wrapper == NULL) {
delete ctx;
return ERROR_OUT_OF_MEMORY;
}
wrapper->device = device;
lv_indev_t* indev = lv_indev_create();
if (indev == NULL) {
free(ctx);
delete wrapper;
return ERROR_OUT_OF_MEMORY;
}
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, lvgl_pointer_read_cb);
lv_indev_set_driver_data(indev, ctx);
lv_indev_set_driver_data(indev, wrapper);
if (display != NULL) {
lv_indev_set_display(indev, display);
}
@@ -145,7 +149,8 @@ error_t lvgl_pointer_set_calibration(lv_indev_t* indev, const struct LvglPointer
if (indev == NULL) {
return ERROR_INVALID_ARGUMENT;
}
struct LvglPointerCtx* ctx = lv_indev_get_driver_data(indev);
struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev);
struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)wrapper->context;
if (calibration == NULL) {
ctx->calibration_enabled = false;
@@ -164,7 +169,8 @@ bool lvgl_pointer_get_calibration(lv_indev_t* indev, struct LvglPointerCalibrati
if (indev == NULL || out_calibration == NULL) {
return false;
}
struct LvglPointerCtx* ctx = lv_indev_get_driver_data(indev);
struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev);
struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)wrapper->context;
if (!ctx->calibration_enabled) {
return false;
}
@@ -177,10 +183,10 @@ void lvgl_pointer_remove(lv_indev_t* indev) {
return;
}
struct LvglPointerCtx* ctx = lv_indev_get_driver_data(indev);
struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev);
if (default_pointer_indev == indev) {
default_pointer_indev = NULL;
}
lv_indev_delete(indev);
free(ctx);
delete wrapper;
}
@@ -0,0 +1,255 @@
// SPDX-License-Identifier: Apache-2.0
#include <lvgl/devices/trackball.h>
#include <lvgl/devices/device_context.h>
#include <lvgl/lvgl.h>
#include <tactility/drivers/trackball.h>
#include <cstdint>
constexpr auto* TAG = "lvgl_trackball";
struct LvglTrackballCtx {
LvglTrackballSettings settings;
int32_t cursor_x;
int32_t cursor_y;
lv_obj_t* cursor;
const void* cursor_image_src;
};
static inline int32_t clamp(int32_t value, int32_t min_value, int32_t max_value) {
if (value < min_value) return min_value;
if (value > max_value) return max_value;
return value;
}
static void recenter_cursor(LvglTrackballCtx* ctx, lv_indev_t* indev) {
lv_display_t* display = lv_indev_get_display(indev);
// lv_display_get_original_*_resolution() is the native (LV_DISPLAY_ROTATION_0) size,
// unaffected by the display's current rotation, matching lvgl/devices/pointer.c's approach.
ctx->cursor_x = display != nullptr ? lv_display_get_original_horizontal_resolution(display) / 2 : 0;
ctx->cursor_y = display != nullptr ? lv_display_get_original_vertical_resolution(display) / 2 : 0;
}
// Creates the cursor object on first use and binds it to indev via lv_indev_set_cursor() exactly
// once - LVGL's lv_indev_set_cursor() unconditionally reparents whatever it's given (even NULL:
// it does not special-case that, see hide_cursor() below), so it must only ever be called here,
// with a freshly-created, non-null object whose parent already matches (lv_layer_sys()), making
// lv_obj_set_parent()'s "parent == obj->parent" check a safe no-op. Once bound, later image/
// enabled changes update the existing object in place instead of re-binding.
static void show_cursor(LvglTrackballCtx* ctx, lv_indev_t* indev) {
if (ctx->cursor_image_src == nullptr) {
return;
}
if (ctx->cursor == nullptr) {
ctx->cursor = lv_image_create(lv_layer_sys());
if (ctx->cursor == nullptr) {
return;
}
lv_obj_remove_flag(ctx->cursor, LV_OBJ_FLAG_CLICKABLE);
lv_indev_set_cursor(indev, ctx->cursor);
}
lv_image_set_src(ctx->cursor, ctx->cursor_image_src);
if (ctx->settings.enabled) {
lv_obj_remove_flag(ctx->cursor, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(ctx->cursor, LV_OBJ_FLAG_HIDDEN);
}
}
// Only hides the cursor object - never deletes it
static void hide_cursor(LvglTrackballCtx* ctx) {
if (ctx->cursor == nullptr) {
return;
}
lv_obj_add_flag(ctx->cursor, LV_OBJ_FLAG_HIDDEN);
}
static void lvgl_trackball_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
auto* wrapper = static_cast<LvglDeviceContext*>(lv_indev_get_driver_data(indev));
auto* ctx = static_cast<LvglTrackballCtx*>(wrapper->context);
// Always drain accumulated movement so it doesn't jump on re-enable, but discard it while disabled.
int32_t dx = 0;
int32_t dy = 0;
trackball_read_delta(wrapper->device, &dx, &dy);
if (!ctx->settings.enabled) {
dx = 0;
dy = 0;
}
lv_display_t* display = lv_indev_get_display(indev);
if (ctx->settings.mode == LVGL_TRACKBALL_MODE_ENCODER) {
int32_t ticks = (dx + dy) * static_cast<int32_t>(ctx->settings.encoder_sensitivity);
data->enc_diff = static_cast<int16_t>(clamp(ticks, INT16_MIN, INT16_MAX));
if (ticks != 0) {
lv_display_trigger_activity(display);
}
} else {
int32_t max_x = display != nullptr ? lv_display_get_original_horizontal_resolution(display) - 1 : 0;
int32_t max_y = display != nullptr ? lv_display_get_original_vertical_resolution(display) - 1 : 0;
ctx->cursor_x = clamp(ctx->cursor_x + dx * static_cast<int32_t>(ctx->settings.pointer_sensitivity), 0, max_x);
ctx->cursor_y = clamp(ctx->cursor_y + dy * static_cast<int32_t>(ctx->settings.pointer_sensitivity), 0, max_y);
data->point.x = static_cast<int16_t>(ctx->cursor_x);
data->point.y = static_cast<int16_t>(ctx->cursor_y);
}
bool pressed = false;
if (ctx->settings.enabled) {
trackball_get_button_pressed(wrapper->device, &pressed);
}
data->state = pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
if (pressed) {
lv_display_trigger_activity(display);
}
}
extern "C" {
LvglTrackballSettings lvgl_trackball_settings_get_default() {
return LvglTrackballSettings {
.mode = LVGL_TRACKBALL_MODE_ENCODER,
.enabled = true,
.encoder_sensitivity = 1,
.pointer_sensitivity = 10,
};
}
error_t lvgl_trackball_add(struct Device* device, lv_display_t* display, lv_indev_t** out_indev) {
if (device == nullptr || out_indev == nullptr) {
return ERROR_INVALID_ARGUMENT;
}
if (device_get_type(device) != &TRACKBALL_TYPE) {
return ERROR_INVALID_ARGUMENT;
}
auto* ctx = new(std::nothrow) LvglTrackballCtx();
if (ctx == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
ctx->settings = lvgl_trackball_settings_get_default();
auto* wrapper = new(std::nothrow) LvglDeviceContext(ctx);
if (wrapper == nullptr) {
delete ctx;
return ERROR_OUT_OF_MEMORY;
}
wrapper->device = device;
lv_indev_t* indev = lv_indev_create();
if (indev == nullptr) {
delete wrapper;
return ERROR_OUT_OF_MEMORY;
}
lv_indev_set_type(indev, LV_INDEV_TYPE_ENCODER);
lv_indev_set_read_cb(indev, lvgl_trackball_read_cb);
lv_indev_set_driver_data(indev, wrapper);
if (display != nullptr) {
lv_indev_set_display(indev, display);
}
recenter_cursor(ctx, indev);
*out_indev = indev;
return ERROR_NONE;
}
void lvgl_trackball_remove(lv_indev_t* indev) {
if (indev == nullptr) {
return;
}
auto* wrapper = static_cast<LvglDeviceContext*>(lv_indev_get_driver_data(indev));
check(wrapper);
auto* ctx = static_cast<LvglTrackballCtx*>(wrapper->context);
check(ctx);
if (ctx->cursor != nullptr) {
lv_obj_delete(ctx->cursor);
}
lv_indev_delete(indev);
delete wrapper;
}
error_t lvgl_trackball_set_settings(lv_indev_t* indev, const struct LvglTrackballSettings* settings) {
if (indev == nullptr || settings == nullptr) {
return ERROR_INVALID_ARGUMENT;
}
if (
(settings->mode != LVGL_TRACKBALL_MODE_ENCODER && settings->mode != LVGL_TRACKBALL_MODE_POINTER) ||
settings->encoder_sensitivity == 0 ||
settings->pointer_sensitivity == 0
) {
return ERROR_INVALID_ARGUMENT;
}
auto* wrapper = static_cast<LvglDeviceContext*>(lv_indev_get_driver_data(indev));
check(wrapper);
auto* ctx = static_cast<LvglTrackballCtx*>(wrapper->context);
check(ctx);
bool mode_changed = ctx->settings.mode != settings->mode;
ctx->settings = *settings;
if (mode_changed) {
if (settings->mode == LVGL_TRACKBALL_MODE_POINTER) {
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
recenter_cursor(ctx, indev);
show_cursor(ctx, indev);
} else {
hide_cursor(ctx);
lv_indev_set_type(indev, LV_INDEV_TYPE_ENCODER);
}
}
// Cursor visibility only tracks the enabled toggle in pointer mode - in encoder mode it must
// stay hidden regardless of enabled, otherwise this unconditionally un-hides the cursor
// hide_cursor() just hid above (enabled is independent of mode, and defaults to true).
if (ctx->cursor != nullptr && ctx->settings.mode == LVGL_TRACKBALL_MODE_POINTER) {
if (ctx->settings.enabled) {
lv_obj_remove_flag(ctx->cursor, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(ctx->cursor, LV_OBJ_FLAG_HIDDEN);
}
}
return ERROR_NONE;
}
bool lvgl_trackball_get_settings(lv_indev_t* indev, struct LvglTrackballSettings* out_settings) {
if (indev == nullptr || out_settings == nullptr) {
return false;
}
auto* wrapper = static_cast<LvglDeviceContext*>(lv_indev_get_driver_data(indev));
check(wrapper);
auto* ctx = static_cast<LvglTrackballCtx*>(wrapper->context);
check(ctx);
*out_settings = ctx->settings;
return true;
}
void lvgl_trackball_set_cursor_image(lv_indev_t* indev, const void* image_src) {
if (indev == nullptr) {
return;
}
auto* wrapper = static_cast<LvglDeviceContext*>(lv_indev_get_driver_data(indev));
check(wrapper);
auto* ctx = static_cast<LvglTrackballCtx*>(wrapper->context);
check(ctx);
ctx->cursor_image_src = image_src;
if (ctx->settings.mode == LVGL_TRACKBALL_MODE_POINTER) {
if (image_src == nullptr) {
hide_cursor(ctx);
} else {
show_cursor(ctx, indev);
}
}
}
} // extern "C"