Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
#include "ui/ColourWheel.h"
#include "rackglow.h"
#include <Logger.h>
#include <esp_heap_caps.h>
#include <cmath>
namespace {
// The knob overhangs the wheel square by half its width at full saturation;
// the parent container leaves padding for that (see ColourTab).
constexpr int32_t KNOB_SIZE = 24;
// Width of the soft edge at the rim, in pixels. A hard-thresholded circle at
// this size is visibly jagged without it.
constexpr float FEATHER_PX = 1.5f;
constexpr float DEG_PER_RAD = 180.0f / static_cast<float>(M_PI);
uint8_t lerp8(uint8_t from, uint8_t to, float t) {
return static_cast<uint8_t>(from + (to - from) * t + 0.5f);
}
// True while LVGL is scrolling something under the finger. The wheel sits in a
// tab page and the tabview swipes horizontally, so a drag across the wheel can
// turn into a tab change; nothing else guards that.
bool isScrollInProgress() {
lv_indev_t* indev = lv_indev_active();
return indev && lv_indev_get_scroll_obj(indev) != nullptr;
}
} // namespace
ColourWheel::~ColourWheel() {
// The canvas holds a draw_buf pointing at buf_, so the objects go first.
// Deleting the wrapper takes the canvas and the knob (and the event
// callbacks registered on it) with it.
if (wrapper_) {
lv_obj_delete(wrapper_);
wrapper_ = nullptr;
canvas_ = nullptr;
knob_ = nullptr;
}
if (buf_) {
heap_caps_free(buf_);
buf_ = nullptr;
}
callback_ = nullptr;
}
bool ColourWheel::initialize(lv_obj_t* parent, int32_t size, uint32_t backdrop) {
if (wrapper_) return true;
if (!parent || size <= 0) return false;
size_ = size;
radius_ = size / 2;
const size_t bufSize = LV_CANVAS_BUF_SIZE(size_, size_, 16, LV_DRAW_BUF_STRIDE_ALIGN);
buf_ = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, bufSize, MALLOC_CAP_SPIRAM);
if (!buf_) {
Logger.error("ColourWheel: %u byte PSRAM alloc failed (largest free block %u)",
static_cast<unsigned>(bufSize),
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)));
return false;
}
// A wrapper, so the knob is a SIBLING of the canvas rather than a child of
// an image widget, and so one object owns the whole square's input.
wrapper_ = lv_obj_create(parent);
lv_obj_set_size(wrapper_, size_, size_);
lv_obj_set_style_bg_opa(wrapper_, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(wrapper_, 0, 0);
lv_obj_set_style_pad_all(wrapper_, 0, 0);
lv_obj_remove_flag(wrapper_, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(wrapper_, LV_OBJ_FLAG_CLICKABLE);
// The knob hangs over the square at full saturation.
lv_obj_add_flag(wrapper_, LV_OBJ_FLAG_OVERFLOW_VISIBLE);
canvas_ = lv_canvas_create(wrapper_);
lv_canvas_set_buffer(canvas_, buf_, size_, size_, LV_COLOR_FORMAT_RGB565);
lv_obj_set_pos(canvas_, 0, 0);
lv_obj_remove_flag(canvas_, LV_OBJ_FLAG_CLICKABLE); // the wrapper owns input
paintWheel(backdrop);
knob_ = lv_obj_create(wrapper_);
lv_obj_set_size(knob_, KNOB_SIZE, KNOB_SIZE);
lv_obj_set_style_radius(knob_, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_border_width(knob_, UI_BORDER_THICK, 0);
lv_obj_set_style_border_color(knob_, lv_color_hex(UI_COLOR_WHITE), 0);
lv_obj_set_style_pad_all(knob_, 0, 0);
lv_obj_remove_flag(knob_, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_remove_flag(knob_, LV_OBJ_FLAG_CLICKABLE); // never swallow a drag
placeKnob();
// PRESSED positions the knob, PRESSING previews a drag live, RELEASED
// reports a plain tap (which produces no PRESSING). See handleTouch for why
// the press itself must not report.
lv_obj_add_event_cb(wrapper_, touchEventHandler, LV_EVENT_PRESSED, this);
lv_obj_add_event_cb(wrapper_, touchEventHandler, LV_EVENT_PRESSING, this);
lv_obj_add_event_cb(wrapper_, touchEventHandler, LV_EVENT_RELEASED, this);
return true;
}
void ColourWheel::paintWheel(uint32_t backdrop) {
const uint8_t bgR = (backdrop >> 16) & 0xFF;
const uint8_t bgG = (backdrop >> 8) & 0xFF;
const uint8_t bgB = backdrop & 0xFF;
const lv_color_t bg = lv_color_make(bgR, bgG, bgB);
const float centre = (size_ - 1) / 2.0f;
const float rad = radius_ - 1.0f;
for (int32_t y = 0; y < size_; ++y) {
const float dy = y - centre;
for (int32_t x = 0; x < size_; ++x) {
const float dx = x - centre;
const float dist = sqrtf(dx * dx + dy * dy);
if (dist > rad + FEATHER_PX) {
lv_canvas_set_px(canvas_, x, y, bg, LV_OPA_COVER);
continue;
}
float angle = atan2f(dy, dx) * DEG_PER_RAD;
if (angle < 0.0f) angle += 360.0f;
const float satF = (dist / rad) * 100.0f;
lv_color_t c = lv_color_hsv_to_rgb(static_cast<uint16_t>(angle),
static_cast<uint8_t>(satF > 100.0f ? 100.0f : satF),
100);
// Feather the rim into the backdrop instead of hard-clipping it.
if (dist > rad) {
const float t = (dist - rad) / FEATHER_PX;
c = lv_color_make(lerp8(c.red, bgR, t),
lerp8(c.green, bgG, t),
lerp8(c.blue, bgB, t));
}
lv_canvas_set_px(canvas_, x, y, c, LV_OPA_COVER);
}
}
// The pixels were written by the CPU, but the draw unit can reach PSRAM over
// DMA - without a writeback it can blit a stale line.
lv_draw_buf_t* drawBuf = lv_canvas_get_draw_buf(canvas_);
if (drawBuf) lv_draw_buf_flush_cache(drawBuf, nullptr);
lv_obj_invalidate(canvas_);
}
void ColourWheel::touchEventHandler(lv_event_t* event) {
auto* instance = static_cast<ColourWheel*>(lv_event_get_user_data(event));
if (instance) instance->handleTouch(lv_event_get_code(event));
}
void ColourWheel::handleTouch(lv_event_code_t code) {
if (!wrapper_) return;
lv_indev_t* indev = lv_indev_active();
if (!indev) return;
// The drag turned into a tab swipe. Report nothing, and put the knob back
// where the last reported colour was - the press that started the scroll
// already moved it.
if (isScrollInProgress()) {
hsv_ = committed_;
placeKnob();
return;
}
lv_point_t point;
lv_indev_get_point(indev, &point);
lv_area_t area;
lv_obj_get_coords(wrapper_, &area);
const float centre = (size_ - 1) / 2.0f;
const float dx = (point.x - area.x1) - centre;
const float dy = (point.y - area.y1) - centre;
const float rad = radius_ - 1.0f;
// Outside the circle still picks, clamped to the rim, so a drag that
// overshoots keeps tracking hue instead of freezing.
float dist = sqrtf(dx * dx + dy * dy);
if (dist > rad) dist = rad;
float angle = atan2f(dy, dx) * DEG_PER_RAD;
if (angle < 0.0f) angle += 360.0f;
const float satF = (dist / rad) * 100.0f;
hsv_.h = static_cast<uint16_t>(angle);
hsv_.s = static_cast<uint8_t>(satF > 100.0f ? 100.0f : satF);
hsv_.v = 100;
placeKnob();
// A press moves the knob but reports nothing: a press is also how a tab
// swipe begins, and at press time there is no scroll yet for the guard above
// to see. The colour goes out once the gesture is known - PRESSING (a drag,
// giving the live preview) or RELEASED (a tap).
if (code == LV_EVENT_PRESSED) return;
notifyChange();
}
void ColourWheel::setCallback(ColorChangeCallback callback) {
callback_ = callback;
}
void ColourWheel::setColor(const String& hexString) {
String clean = hexString;
if (clean.startsWith("#")) {
clean = clean.substring(1);
}
const long value = strtol(clean.c_str(), nullptr, 16);
setColor(static_cast<uint8_t>((value >> 16) & 0xFF),
static_cast<uint8_t>((value >> 8) & 0xFF),
static_cast<uint8_t>( value & 0xFF));
}
void ColourWheel::setColor(uint8_t r, uint8_t g, uint8_t b, bool notify) {
if (!wrapper_) return;
hsv_ = lv_color_rgb_to_hsv(r, g, b);
placeKnob();
if (notify) {
notifyChange();
} else {
commit();
}
}
String ColourWheel::getColorHex() const {
uint8_t r, g, b;
getColorRGB(r, g, b);
// Oversized on purpose: "#RRGGBB" needs 8, but %02X takes an int so GCC's
// -Wformat-truncation assumes the worst case.
char hex[16];
snprintf(hex, sizeof(hex), "#%02X%02X%02X", r, g, b);
return String(hex);
}
void ColourWheel::getColorRGB(uint8_t& r, uint8_t& g, uint8_t& b) const {
if (!wrapper_) {
r = g = b = 0;
return;
}
const lv_color_t c = lv_color_hsv_to_rgb(hsv_.h, hsv_.s, hsv_.v);
r = c.red;
g = c.green;
b = c.blue;
}
void ColourWheel::commit() {
committed_ = hsv_;
}
void ColourWheel::notifyChange() {
commit();
if (callback_) {
uint8_t r, g, b;
getColorRGB(r, g, b);
callback_(r, g, b);
}
}
void ColourWheel::placeKnob() {
if (!knob_) return;
const float centre = (size_ - 1) / 2.0f;
const float rad = radius_ - 1.0f;
const float theta = hsv_.h / DEG_PER_RAD;
const float dist = (hsv_.s / 100.0f) * rad;
lv_obj_set_pos(knob_,
static_cast<int32_t>(centre + cosf(theta) * dist) - KNOB_SIZE / 2,
static_cast<int32_t>(centre + sinf(theta) * dist) - KNOB_SIZE / 2);
// Gesture-rate, not tick-rate: the knob's fill shows the picked colour, so
// it has to change when the colour does. This isn't the style-thrash the
// LVGL rule warns about (that's restyling from an update tick).
lv_obj_set_style_bg_color(knob_, lv_color_hsv_to_rgb(hsv_.h, hsv_.s, hsv_.v), 0);
}
96336ed42This file is pinned to the selected release. Download the original to open it in your design tool.