Migrate TactilityC and LVGL functionality (#588)
lvgl-module - Move and rename source and include files - Fix bug with missing lvgl unlock - New widgets: spinner, toolbar, sliderbox TactilityC - Removed tt_lock, tt_lvgl_\*
This commit is contained in:
committed by
GitHub
parent
cd2d9d6158
commit
b98a813f3c
@@ -0,0 +1,119 @@
|
||||
#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) {
|
||||
lv_display_delete(display);
|
||||
display = lv_disp_get_next(NULL);
|
||||
}
|
||||
|
||||
lvgl_unlock();
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <lvgl/devices/display.h>
|
||||
|
||||
#include <lvgl/ppa.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/display.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <esp_heap_caps.h>
|
||||
#endif
|
||||
|
||||
#define 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)
|
||||
// Mirrors what lvgl_display_add() passed to lv_display_set_render_mode() - there's no
|
||||
// lv_display_get_render_mode() to query it back from LVGL, so it's cached here instead.
|
||||
lv_display_render_mode_t render_mode;
|
||||
// The device's swap_xy/mirror_x/mirror_y at bind time, queried once and treated as the LV_DISPLAY_ROTATION_0 baseline.
|
||||
bool base_swap_xy;
|
||||
bool base_mirror_x;
|
||||
bool base_mirror_y;
|
||||
// The device's configured gap at bind time, in the same LV_DISPLAY_ROTATION_0 baseline frame
|
||||
// as base_swap_xy above. set_gap() is a raw (x,y) offset applied to whatever coordinates the
|
||||
// panel is currently being drawn with - it has no idea about swap_xy, so a rotation that flips
|
||||
// swap_xy relative to this baseline must swap gap_x/gap_y too (see lvgl_display_apply_rotation()).
|
||||
int32_t base_gap_x;
|
||||
int32_t base_gap_y;
|
||||
bool has_set_gap_cap;
|
||||
// When true, rotation is done in software in the flush callback instead of via display_swap_xy()/
|
||||
// display_mirror(); rotate_buf holds the rotated pixels and is sized like buf1.
|
||||
bool sw_rotate;
|
||||
void* rotate_buf;
|
||||
// Lazily created on the first sw_rotate flush that needs it (see lvgl_display_rotate_tile()).
|
||||
// Stays NULL - and every rotate falls back to rotate_buf/lv_draw_sw_rotate() - when the target
|
||||
// has no PPA (lvgl_ppa_is_supported()), the color format has no PPA color mode
|
||||
// (lvgl_ppa_supports_color_format()), or creating the PPA client/buffer failed once already.
|
||||
void* ppa_handle;
|
||||
bool ppa_unavailable;
|
||||
bool ppa_eligible;
|
||||
// Size of buf1/buf2 (each) - used by lvgl_display_fb_base() to range-check which real buffer
|
||||
// (for the fb-direct case) a given color_map pointer falls into.
|
||||
size_t buf_size_bytes;
|
||||
// Cached DISPLAY_CAPABILITY_CAP_SWAP_XY/CAP_MIRROR: swap_xy()/mirror() and their getters are
|
||||
// null on drivers that don't support them, so rotation handling must not call through blindly.
|
||||
bool has_swap_xy_cap;
|
||||
bool has_mirror_cap;
|
||||
// Mirrors LvglDisplayConfig::swap_bytes: the panel is big endian while the OS is little endian,
|
||||
// so we fix it in software. In the future, the driver should probably expose endianness requirements instead.
|
||||
bool byte_swap;
|
||||
};
|
||||
|
||||
static void* lvgl_display_alloc_buffer(size_t size_bytes) {
|
||||
#ifdef ESP_PLATFORM
|
||||
// Must match LV_DRAW_BUF_ALIGN (can be > 4 - e.g. 64, tied to the cache line size for
|
||||
// DMA2D/PPA coherency on some targets - see sdkconfig's CONFIG_LV_DRAW_BUF_ALIGN). A buffer
|
||||
// allocated less strictly than that fails lv_display_set_buffers()'s alignment assert, which
|
||||
// is configured to LV_ASSERT_HANDLER (while(1);) rather than a clean abort - i.e. a silent hang.
|
||||
void* buf = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size_bytes, MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
|
||||
if (buf == NULL) {
|
||||
buf = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size_bytes, MALLOC_CAP_DEFAULT);
|
||||
}
|
||||
return buf;
|
||||
#else
|
||||
return malloc(size_bytes);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void lvgl_display_free_buffer(void* buf) {
|
||||
#ifdef ESP_PLATFORM
|
||||
heap_caps_free(buf);
|
||||
#else
|
||||
free(buf);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Resolves the kernel-reported color format to an LVGL color format. RGB565 and BGR565 (and
|
||||
// their _SWAPPED variants) all render identically as far as LVGL is concerned - it has no native
|
||||
// concept of channel order, only byte order (see LvglDisplayConfig::swap_bytes for that axis).
|
||||
static bool lvgl_display_map_color_format(enum DisplayColorFormat in, lv_color_format_t* out) {
|
||||
switch (in) {
|
||||
case DISPLAY_COLOR_FORMAT_RGB565:
|
||||
case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED:
|
||||
case DISPLAY_COLOR_FORMAT_BGR565:
|
||||
case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED:
|
||||
*out = LV_COLOR_FORMAT_RGB565;
|
||||
return true;
|
||||
case DISPLAY_COLOR_FORMAT_RGB888:
|
||||
*out = LV_COLOR_FORMAT_RGB888;
|
||||
return true;
|
||||
case DISPLAY_COLOR_FORMAT_MONOCHROME:
|
||||
// Row-major, MSB-first 1bpp (matches LV_COLOR_FORMAT_I1's raw layout once the
|
||||
// palette header is stripped, see lvgl_display_flush_cb()) - any page/tile
|
||||
// reformatting a specific panel's GDDRAM needs is that driver's own concern
|
||||
// (e.g. ssd1306_draw_bitmap()'s row-to-page transpose).
|
||||
*out = LV_COLOR_FORMAT_I1;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void lvgl_display_apply_rotation(struct LvglDisplayCtx* ctx, lv_display_rotation_t rotation) {
|
||||
// SW-rotated displays stay in their base orientation; rotation is applied per-flush instead.
|
||||
if (ctx->sw_rotate) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool swap_xy = ctx->base_swap_xy;
|
||||
bool mirror_x = ctx->base_mirror_x;
|
||||
bool mirror_y = ctx->base_mirror_y;
|
||||
|
||||
switch (rotation) {
|
||||
case LV_DISPLAY_ROTATION_0:
|
||||
break;
|
||||
case LV_DISPLAY_ROTATION_90:
|
||||
swap_xy = !ctx->base_swap_xy;
|
||||
if (ctx->base_swap_xy) {
|
||||
mirror_x = !ctx->base_mirror_x;
|
||||
} else {
|
||||
mirror_y = !ctx->base_mirror_y;
|
||||
}
|
||||
break;
|
||||
case LV_DISPLAY_ROTATION_180:
|
||||
mirror_x = !ctx->base_mirror_x;
|
||||
mirror_y = !ctx->base_mirror_y;
|
||||
break;
|
||||
case LV_DISPLAY_ROTATION_270:
|
||||
swap_xy = !ctx->base_swap_xy;
|
||||
if (ctx->base_swap_xy) {
|
||||
mirror_y = !ctx->base_mirror_y;
|
||||
} else {
|
||||
mirror_x = !ctx->base_mirror_x;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (ctx->has_swap_xy_cap) {
|
||||
display_swap_xy(ctx->device, swap_xy);
|
||||
}
|
||||
if (ctx->has_mirror_cap) {
|
||||
display_mirror(ctx->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
|
||||
// baseline's - swap gap_x/gap_y whenever this rotation's swap_xy differs from the baseline.
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
static void lvgl_display_rotation_event_cb(lv_event_t* e) {
|
||||
struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)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));
|
||||
}
|
||||
|
||||
// Returns which of buf1/buf2 (the real frame buffers, when !owns_buffers) color_map falls inside.
|
||||
// Defaults to buf1, which also covers the single-frame-buffer (buf2 == NULL) case.
|
||||
static void* lvgl_display_fb_base(struct LvglDisplayCtx* ctx, const uint8_t* color_map) {
|
||||
if (ctx->buf2 != NULL && color_map >= (uint8_t*)ctx->buf2 &&
|
||||
color_map < (uint8_t*)ctx->buf2 + ctx->buf_size_bytes) {
|
||||
return ctx->buf2;
|
||||
}
|
||||
return ctx->buf1;
|
||||
}
|
||||
|
||||
// Tries to rotate the tightly-packed w x h block at in_buff via PPA, returning the PPA output
|
||||
// buffer on success or NULL if this tile/format/target can't use it - in which case the caller
|
||||
// must fall back to lv_draw_sw_rotate() into ctx->rotate_buf. Lazily creates the PPA client on the
|
||||
// first eligible call, sized to ctx->buf_size_bytes (the largest tile or full-frame buffer this
|
||||
// display will ever flush - see lvgl_display_add()); once creation fails once, ppa_unavailable
|
||||
// latches so later tiles don't retry it. Never asks PPA to byte-swap: lvgl_display_flush_cb()
|
||||
// applies ctx->byte_swap itself, once, at a single point regardless of which rotation path ran -
|
||||
// simpler than tracking whether it was already done by PPA (PARTIAL mode) vs. already done
|
||||
// per-tile before FULL mode's whole-frame rotate (see the two call sites).
|
||||
static void* lvgl_display_try_ppa_rotate(struct LvglDisplayCtx* ctx, const uint8_t* in_buff, int32_t w, int32_t h,
|
||||
lv_display_rotation_t rotation, lv_color_format_t color_format) {
|
||||
if (!ctx->ppa_eligible || ctx->ppa_unavailable) {
|
||||
return NULL;
|
||||
}
|
||||
// PPA reads pic_w/pic_h in pixels with no separate stride - only safe when the tile has no
|
||||
// row padding beyond w * bytes-per-pixel (see lvgl_ppa.h).
|
||||
uint32_t bpp = lv_color_format_get_size(color_format);
|
||||
if (lv_draw_buf_width_to_stride(w, color_format) != (uint32_t)w * bpp) {
|
||||
return NULL;
|
||||
}
|
||||
if (ctx->ppa_handle == NULL) {
|
||||
ctx->ppa_handle = lvgl_ppa_get_or_create(ctx->buf_size_bytes);
|
||||
if (ctx->ppa_handle == NULL) {
|
||||
ctx->ppa_unavailable = true;
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
return lvgl_ppa_rotate(ctx->ppa_handle, in_buff, w, h, rotation, color_format, false);
|
||||
}
|
||||
|
||||
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);
|
||||
bool is_i1 = lv_display_get_color_format(disp) == LV_COLOR_FORMAT_I1;
|
||||
|
||||
int32_t x1 = area->x1;
|
||||
int32_t y1 = area->y1;
|
||||
int32_t x2 = area->x2;
|
||||
int32_t y2 = area->y2;
|
||||
uint32_t area_size_px = (uint32_t)(x2 - x1 + 1) * (uint32_t)(y2 - y1 + 1);
|
||||
|
||||
lv_display_rotation_t rotation = lv_display_get_rotation(disp);
|
||||
bool rotating = ctx->sw_rotate && rotation != LV_DISPLAY_ROTATION_0;
|
||||
|
||||
// In FULL mode, a refresh cycle can call this once per still-unjoined invalidated area (see
|
||||
// the comment below) before the frame is complete - rotating per-tile here would only ever
|
||||
// reflect the last tile written, not the accumulated whole frame. Rotate the whole buffer in
|
||||
// one shot instead, right before presenting (see the FULL-mode branch below).
|
||||
if (rotating && ctx->render_mode != LV_DISPLAY_RENDER_MODE_FULL) {
|
||||
// sw_rotate is only ever requested for displays lacking real HW mirror/swap_xy capability
|
||||
// (see lvgl_devices.c), and lvgl_display_add() only binds fb-direct (owns_buffers == false)
|
||||
// when that capability is present - so this is always the owns_buffers == true case.
|
||||
lv_color_format_t color_format = lv_display_get_color_format(disp);
|
||||
int32_t w = x2 - x1 + 1;
|
||||
int32_t h = y2 - y1 + 1;
|
||||
|
||||
void* ppa_out = lvgl_display_try_ppa_rotate(ctx, color_map, w, h, rotation, color_format);
|
||||
if (ppa_out != NULL) {
|
||||
color_map = (uint8_t*)ppa_out;
|
||||
} else {
|
||||
uint32_t w_stride = lv_draw_buf_width_to_stride(w, color_format);
|
||||
uint32_t h_stride = lv_draw_buf_width_to_stride(h, color_format);
|
||||
if (rotation == LV_DISPLAY_ROTATION_180) {
|
||||
lv_draw_sw_rotate(color_map, ctx->rotate_buf, w, h, w_stride, w_stride, rotation, color_format);
|
||||
} else {
|
||||
lv_draw_sw_rotate(color_map, ctx->rotate_buf, w, h, w_stride, h_stride, rotation, color_format);
|
||||
}
|
||||
color_map = (uint8_t*)ctx->rotate_buf;
|
||||
}
|
||||
lv_area_t rotated_area = { x1, y1, x2, y2 };
|
||||
lv_display_rotate_area(disp, &rotated_area);
|
||||
x1 = rotated_area.x1;
|
||||
y1 = rotated_area.y1;
|
||||
x2 = rotated_area.x2;
|
||||
y2 = rotated_area.y2;
|
||||
}
|
||||
|
||||
if (ctx->byte_swap) {
|
||||
lv_draw_sw_rgb565_swap(color_map, area_size_px);
|
||||
}
|
||||
|
||||
if (ctx->render_mode == LV_DISPLAY_RENDER_MODE_FULL) {
|
||||
// FULL mode always redraws (and flushes) the whole display, but a refresh cycle can still
|
||||
// call this flush_cb once per still-unjoined invalidated area (lv_refr.c's
|
||||
// refr_invalid_areas()), each writing its own tile into the *same* shared buffer - only
|
||||
// the last call actually holds the complete frame. This applies equally whether that
|
||||
// buffer is one we own (owns_buffers, e.g. an I1 e-paper/OLED panel) or a real hardware
|
||||
// frame buffer (fb-direct, where display_draw_bitmap() - see rgb_display_draw_bitmap() -
|
||||
// additionally blocks for a full scan-out period whenever frame_buffer_count > 0).
|
||||
// Presenting on every call would send partially-rendered frames, and for fb-direct would
|
||||
// also pay that scan-out wait N times per refresh instead of once; defer to the last
|
||||
// flush and present the whole buffer in one call, mirroring esp_lvgl_port_disp.c's own
|
||||
// lv_disp_flush_is_last() gate for its direct/full render mode.
|
||||
if (lv_display_flush_is_last(disp)) {
|
||||
uint8_t* fb_base;
|
||||
if (ctx->owns_buffers) {
|
||||
fb_base = (uint8_t*)ctx->buf1;
|
||||
if (is_i1) {
|
||||
// LVGL reserves an 8-byte palette (2 x lv_color32_t) at the front of every I1
|
||||
// draw buffer; it's on the caller to skip it before treating the rest as
|
||||
// pixel data.
|
||||
fb_base += 8;
|
||||
}
|
||||
} 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);
|
||||
|
||||
if (rotating) {
|
||||
// fb_base now holds the whole completed frame, but still in LVGL's *logical*
|
||||
// (rotated) w/h - rotate it as one block into rotate_buf, matching the panel's
|
||||
// fixed physical w/h, before presenting.
|
||||
lv_color_format_t color_format = lv_display_get_color_format(disp);
|
||||
bool swapped_wh = rotation == LV_DISPLAY_ROTATION_90 || rotation == LV_DISPLAY_ROTATION_270;
|
||||
int32_t logical_w = swapped_wh ? (int32_t)vres : (int32_t)hres;
|
||||
int32_t logical_h = swapped_wh ? (int32_t)hres : (int32_t)vres;
|
||||
|
||||
void* ppa_out = lvgl_display_try_ppa_rotate(ctx, fb_base, logical_w, logical_h, rotation, color_format);
|
||||
if (ppa_out != NULL) {
|
||||
fb_base = (uint8_t*)ppa_out;
|
||||
} else {
|
||||
uint32_t src_stride = lv_draw_buf_width_to_stride(logical_w, color_format);
|
||||
uint32_t dest_stride = lv_draw_buf_width_to_stride(hres, color_format);
|
||||
lv_draw_sw_rotate(fb_base, ctx->rotate_buf, logical_w, logical_h, src_stride, dest_stride, rotation, color_format);
|
||||
fb_base = (uint8_t*)ctx->rotate_buf;
|
||||
}
|
||||
}
|
||||
|
||||
display_draw_bitmap(ctx->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);
|
||||
}
|
||||
// DisplayApi has no async completion callback, so draw_bitmap is synchronous.
|
||||
lv_display_flush_ready(disp);
|
||||
}
|
||||
|
||||
error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* config, lv_display_t** out_display) {
|
||||
if (device == NULL || config == NULL || out_display == NULL) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (device_get_type(device) != &DISPLAY_TYPE) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
lv_color_format_t lv_color_format;
|
||||
enum DisplayColorFormat kernel_color_format = display_get_color_format(device);
|
||||
if (!lvgl_display_map_color_format(kernel_color_format, &lv_color_format)) {
|
||||
LOG_E(TAG, "Unsupported color format %d (no LVGL equivalent)", (int)kernel_color_format);
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
uint16_t hres = display_get_resolution_x(device);
|
||||
uint16_t vres = display_get_resolution_y(device);
|
||||
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));
|
||||
if (ctx == NULL) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
ctx->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
|
||||
// ctx->ppa_eligible directly, so this is safe to compute unconditionally.
|
||||
ctx->ppa_eligible = lvgl_ppa_is_supported() && lvgl_ppa_supports_color_format(lv_color_format);
|
||||
if (config->sw_rotate && !ctx->ppa_eligible) {
|
||||
LOG_I(TAG, "PPA not available for this display (supported=%d, color_format=%d) - using lv_draw_sw_rotate()",
|
||||
(int)lvgl_ppa_is_supported(), (int)lv_color_format);
|
||||
}
|
||||
ctx->has_swap_xy_cap = display_has_capability(device, DISPLAY_CAPABILITY_CAP_SWAP_XY);
|
||||
ctx->has_mirror_cap = display_has_capability(device, DISPLAY_CAPABILITY_CAP_MIRROR);
|
||||
ctx->has_set_gap_cap = display_has_capability(device, DISPLAY_CAPABILITY_CAP_SET_GAP);
|
||||
// sw_rotate is excluded from fb-direct binding below: it writes rotated pixels into
|
||||
// ctx->rotate_buf, which lvgl_display_fb_base() doesn't recognize, so fb-direct must stay
|
||||
// off in that case as well.
|
||||
bool would_bind_fb_direct = fb_count > 0 && ctx->has_swap_xy_cap && ctx->has_mirror_cap && !ctx->sw_rotate;
|
||||
if (fb_count > 0 && !would_bind_fb_direct) {
|
||||
// Only re-enable capabilities the driver reported off above if it has a *dynamic*
|
||||
// has_capability() (DisplayApi.has_capability non-null, e.g. rgb_display_has_capability()/
|
||||
// st7701_has_capability()): that's specifically what a driver implements when a capability's
|
||||
// availability is state-dependent - here, off only because binding fb-direct would defeat
|
||||
// it (see rgb_display_has_capability()) - so once we're falling back to an owned buffer
|
||||
// instead (right below), the concern no longer applies. A driver with no dynamic
|
||||
// has_capability() reports fixed, hardware-level capabilities instead: if the bit's off,
|
||||
// swap_xy()/mirror() don't exist at all (null function pointers - see the DisplayApi
|
||||
// contract), so forcing them back on here would call through a null pointer.
|
||||
const struct DisplayApi* api = (const struct DisplayApi*)device_get_driver(device)->api;
|
||||
if (api->has_capability != NULL) {
|
||||
ctx->has_swap_xy_cap = true;
|
||||
ctx->has_mirror_cap = true;
|
||||
}
|
||||
}
|
||||
ctx->base_swap_xy = ctx->has_swap_xy_cap ? display_get_swap_xy(device) : false;
|
||||
ctx->base_mirror_x = ctx->has_mirror_cap ? display_get_mirror_x(device) : false;
|
||||
ctx->base_mirror_y = ctx->has_mirror_cap ? display_get_mirror_y(device) : false;
|
||||
ctx->base_gap_x = ctx->has_set_gap_cap ? display_get_gap_x(device) : 0;
|
||||
ctx->base_gap_y = ctx->has_set_gap_cap ? display_get_gap_y(device) : 0;
|
||||
|
||||
lv_display_render_mode_t render_mode;
|
||||
size_t buf_size_bytes;
|
||||
|
||||
if (lv_color_format == LV_COLOR_FORMAT_I1) {
|
||||
// I1 packs 8 pixels/byte row-wise and LVGL reserves an 8-byte palette header at the
|
||||
// buffer's start (see lvgl_display_flush_cb()). Always redraw the whole frame in one
|
||||
// owned buffer instead of computing partial-region byte offsets against that packing.
|
||||
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);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
ctx->owns_buffers = true;
|
||||
render_mode = LV_DISPLAY_RENDER_MODE_FULL;
|
||||
} else if (would_bind_fb_direct) {
|
||||
display_get_frame_buffer(device, 0, &ctx->buf1);
|
||||
if (fb_count > 1) {
|
||||
display_get_frame_buffer(device, 1, &ctx->buf2);
|
||||
}
|
||||
ctx->owns_buffers = false;
|
||||
render_mode = LV_DISPLAY_RENDER_MODE_FULL;
|
||||
buf_size_bytes = (size_t)hres * vres * bpp;
|
||||
} else {
|
||||
uint16_t buf_height = config->force_full_frame || config->buffer_height == 0
|
||||
? vres : config->buffer_height;
|
||||
buf_size_bytes = (size_t)hres * buf_height * bpp;
|
||||
|
||||
ctx->buf1 = lvgl_display_alloc_buffer(buf_size_bytes);
|
||||
if (ctx->buf1 == NULL) {
|
||||
free(ctx);
|
||||
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);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
}
|
||||
ctx->owns_buffers = true;
|
||||
render_mode = config->force_full_frame ? LV_DISPLAY_RENDER_MODE_FULL : LV_DISPLAY_RENDER_MODE_PARTIAL;
|
||||
}
|
||||
|
||||
ctx->buf_size_bytes = buf_size_bytes;
|
||||
|
||||
if (ctx->sw_rotate) {
|
||||
ctx->rotate_buf = lvgl_display_alloc_buffer(buf_size_bytes);
|
||||
if (ctx->rotate_buf == NULL) {
|
||||
if (ctx->owns_buffers) {
|
||||
lvgl_display_free_buffer(ctx->buf1);
|
||||
lvgl_display_free_buffer(ctx->buf2);
|
||||
}
|
||||
free(ctx);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
}
|
||||
|
||||
lv_display_t* disp = lv_display_create(hres, vres);
|
||||
if (disp == NULL) {
|
||||
if (ctx->owns_buffers) {
|
||||
lvgl_display_free_buffer(ctx->buf1);
|
||||
lvgl_display_free_buffer(ctx->buf2);
|
||||
}
|
||||
lvgl_display_free_buffer(ctx->rotate_buf);
|
||||
free(ctx);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
ctx->render_mode = render_mode;
|
||||
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);
|
||||
|
||||
// Apply once explicitly, independent of whether LV_EVENT_RESOLUTION_CHANGED fires on creation.
|
||||
lvgl_display_apply_rotation(ctx, lv_display_get_rotation(disp));
|
||||
|
||||
*out_display = disp;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
void lvgl_display_remove(lv_display_t* display) {
|
||||
if (display == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)lv_display_get_driver_data(display);
|
||||
lv_display_delete(display);
|
||||
|
||||
if (ctx != NULL) {
|
||||
if (ctx->owns_buffers) {
|
||||
if (ctx->buf1 != NULL) {
|
||||
lvgl_display_free_buffer(ctx->buf1);
|
||||
}
|
||||
if (ctx->buf2 != NULL) {
|
||||
lvgl_display_free_buffer(ctx->buf2);
|
||||
}
|
||||
}
|
||||
if (ctx->rotate_buf != NULL) {
|
||||
lvgl_display_free_buffer(ctx->rotate_buf);
|
||||
}
|
||||
if (ctx->ppa_handle != NULL) {
|
||||
lvgl_ppa_delete(ctx->ppa_handle);
|
||||
}
|
||||
free(ctx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <lvgl/devices/keyboard.h>
|
||||
|
||||
#include <tactility/drivers/keyboard.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
struct LvglKeyboardCtx {
|
||||
struct Device* device;
|
||||
};
|
||||
|
||||
static void lvgl_keyboard_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
|
||||
struct LvglKeyboardCtx* ctx = (struct LvglKeyboardCtx*)lv_indev_get_driver_data(indev);
|
||||
|
||||
struct KeyboardKeyData key_data = {0};
|
||||
if (keyboard_read_key(ctx->device, &key_data) != ERROR_NONE) {
|
||||
data->state = LV_INDEV_STATE_RELEASED;
|
||||
data->continue_reading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// KeyboardKeyData deliberately mirrors lv_indev_data_t's key/continue_reading fields, so no translation is needed.
|
||||
data->key = key_data.key;
|
||||
data->state = key_data.pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
|
||||
data->continue_reading = key_data.continue_reading;
|
||||
}
|
||||
|
||||
error_t lvgl_keyboard_add(struct Device* device, lv_display_t* display, lv_indev_t** out_indev) {
|
||||
if (device == NULL || out_indev == NULL) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (device_get_type(device) != &KEYBOARD_TYPE) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
struct LvglKeyboardCtx* ctx = (struct LvglKeyboardCtx*)malloc(sizeof(struct LvglKeyboardCtx));
|
||||
if (ctx == NULL) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
ctx->device = device;
|
||||
|
||||
lv_indev_t* indev = lv_indev_create();
|
||||
if (indev == NULL) {
|
||||
free(ctx);
|
||||
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);
|
||||
if (display != NULL) {
|
||||
lv_indev_set_display(indev, display);
|
||||
}
|
||||
|
||||
*out_indev = indev;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
void lvgl_keyboard_remove(lv_indev_t* indev) {
|
||||
if (indev == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct LvglKeyboardCtx* ctx = (struct LvglKeyboardCtx*)lv_indev_get_driver_data(indev);
|
||||
lv_indev_delete(indev);
|
||||
free(ctx);
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <lvgl/devices/pointer.h>
|
||||
|
||||
#include <tactility/drivers/pointer.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#define TAG "lvgl_pointer"
|
||||
|
||||
struct LvglPointerCtx {
|
||||
struct Device* device;
|
||||
bool calibration_enabled;
|
||||
struct LvglPointerCalibration calibration;
|
||||
};
|
||||
|
||||
// Bus reads are expected to complete quickly; bound the wait so a stalled controller can't block the LVGL indev poll.
|
||||
static const TickType_t LVGL_POINTER_READ_TIMEOUT = pdMS_TO_TICKS(10);
|
||||
|
||||
// Tracks the first indev created by lvgl_pointer_add() still alive, for lvgl_pointer_get_default().
|
||||
// Only ever set/cleared by lvgl_pointer_add()/lvgl_pointer_remove(), so it can never point at an
|
||||
// indev created by other code (e.g. the deprecated HAL's own LVGL pointer registration).
|
||||
static lv_indev_t* default_pointer_indev = NULL;
|
||||
|
||||
// Mirrors Tactility/Source/settings/TouchCalibrationSettings.cpp's isValid().
|
||||
static const int32_t LVGL_POINTER_CALIBRATION_MIN_RANGE = 20;
|
||||
|
||||
static bool lvgl_pointer_calibration_is_valid(const struct LvglPointerCalibration* calibration) {
|
||||
return calibration->x_max > calibration->x_min &&
|
||||
calibration->y_max > calibration->y_min &&
|
||||
(calibration->x_max - calibration->x_min) >= LVGL_POINTER_CALIBRATION_MIN_RANGE &&
|
||||
(calibration->y_max - calibration->y_min) >= LVGL_POINTER_CALIBRATION_MIN_RANGE;
|
||||
}
|
||||
|
||||
// Linear per-axis rescale of [x_min,x_max]/[y_min,y_max] onto [0,target_x_max]/[0,target_y_max],
|
||||
// clamped. Mirrors TouchCalibrationSettings.cpp's applyCalibration(). Kept as a standalone
|
||||
// function (not inlined into the read callback) so the math is isolated and easy to reason about.
|
||||
static void lvgl_pointer_calibration_apply(
|
||||
const struct LvglPointerCalibration* calibration,
|
||||
int32_t target_x_max,
|
||||
int32_t target_y_max,
|
||||
uint16_t* x,
|
||||
uint16_t* y
|
||||
) {
|
||||
int64_t mapped_x = ((int64_t)*x - calibration->x_min) * target_x_max /
|
||||
((int64_t)calibration->x_max - calibration->x_min);
|
||||
int64_t mapped_y = ((int64_t)*y - calibration->y_min) * target_y_max /
|
||||
((int64_t)calibration->y_max - calibration->y_min);
|
||||
|
||||
if (mapped_x < 0) mapped_x = 0;
|
||||
if (mapped_x > target_x_max) mapped_x = target_x_max;
|
||||
if (mapped_y < 0) mapped_y = 0;
|
||||
if (mapped_y > target_y_max) mapped_y = target_y_max;
|
||||
|
||||
*x = (uint16_t)mapped_x;
|
||||
*y = (uint16_t)mapped_y;
|
||||
}
|
||||
|
||||
// Reads the touch controller and applies calibration entirely in the graphics driver's own
|
||||
// native (LV_DISPLAY_ROTATION_0) coordinate space - native_x_max/native_y_max are just the panel's
|
||||
// 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) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t point_count = 0;
|
||||
if (!pointer_get_touched_points(ctx->device, x, y, NULL, &point_count, 1) || point_count == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ctx->calibration_enabled && native_x_max > 0 && native_y_max > 0) {
|
||||
lvgl_pointer_calibration_apply(&ctx->calibration, native_x_max, native_y_max, x, y);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// The actual LVGL indev read callback: wraps lvgl_pointer_read_calibrated() and, only here, applies
|
||||
// the rotation needed to place the (still native-space) point into the currently active LVGL
|
||||
// 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);
|
||||
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 - no rotation lookup needed to get it.
|
||||
int32_t native_x_max = display != NULL ? lv_display_get_original_horizontal_resolution(display) - 1 : 0;
|
||||
int32_t native_y_max = display != NULL ? lv_display_get_original_vertical_resolution(display) - 1 : 0;
|
||||
|
||||
uint16_t x = 0;
|
||||
uint16_t y = 0;
|
||||
if (!lvgl_pointer_read_calibrated(ctx, native_x_max, native_y_max, &x, &y)) {
|
||||
data->state = LV_INDEV_STATE_RELEASED;
|
||||
return;
|
||||
}
|
||||
|
||||
data->point.x = x;
|
||||
data->point.y = y;
|
||||
data->state = LV_INDEV_STATE_PRESSED;
|
||||
}
|
||||
|
||||
error_t lvgl_pointer_add(struct Device* device, lv_display_t* display, lv_indev_t** out_indev) {
|
||||
if (device == NULL || out_indev == NULL) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (device_get_type(device) != &POINTER_TYPE) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
struct LvglPointerCtx* ctx = calloc(1, sizeof(struct LvglPointerCtx));
|
||||
if (ctx == NULL) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
ctx->device = device;
|
||||
|
||||
lv_indev_t* indev = lv_indev_create();
|
||||
if (indev == NULL) {
|
||||
free(ctx);
|
||||
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);
|
||||
if (display != NULL) {
|
||||
lv_indev_set_display(indev, display);
|
||||
}
|
||||
|
||||
if (default_pointer_indev == NULL) {
|
||||
default_pointer_indev = indev;
|
||||
}
|
||||
|
||||
*out_indev = indev;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
lv_indev_t* lvgl_pointer_get_default(void) {
|
||||
return default_pointer_indev;
|
||||
}
|
||||
|
||||
error_t lvgl_pointer_set_calibration(lv_indev_t* indev, const struct LvglPointerCalibration* calibration) {
|
||||
if (indev == NULL) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
struct LvglPointerCtx* ctx = lv_indev_get_driver_data(indev);
|
||||
|
||||
if (calibration == NULL) {
|
||||
ctx->calibration_enabled = false;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
if (!lvgl_pointer_calibration_is_valid(calibration)) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
ctx->calibration = *calibration;
|
||||
ctx->calibration_enabled = true;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
bool lvgl_pointer_get_calibration(lv_indev_t* indev, struct LvglPointerCalibration* out_calibration) {
|
||||
if (indev == NULL || out_calibration == NULL) {
|
||||
return false;
|
||||
}
|
||||
struct LvglPointerCtx* ctx = lv_indev_get_driver_data(indev);
|
||||
if (!ctx->calibration_enabled) {
|
||||
return false;
|
||||
}
|
||||
*out_calibration = ctx->calibration;
|
||||
return true;
|
||||
}
|
||||
|
||||
void lvgl_pointer_remove(lv_indev_t* indev) {
|
||||
if (indev == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct LvglPointerCtx* ctx = lv_indev_get_driver_data(indev);
|
||||
if (default_pointer_indev == indev) {
|
||||
default_pointer_indev = NULL;
|
||||
}
|
||||
lv_indev_delete(indev);
|
||||
free(ctx);
|
||||
}
|
||||
Reference in New Issue
Block a user