ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
color_picker.h
Go to the documentation of this file.
1#pragma once
2
3#include "lvgl_esphome.h"
5
6#include <algorithm>
7#include <array>
8#include <cmath>
9#include <cstdio>
10#include <numbers>
11
12#ifdef USE_LVGL_COLOR_PICKER
13
14namespace esphome::lvgl {
15
16// The hue gradients need seven stops; gradient.py raises the limit when a colour picker is in use.
17static_assert(LV_GRADIENT_MAX_STOPS >= 7, "color_picker needs LV_GRADIENT_MAX_STOPS >= 7");
18
20 public:
30
39
40 explicit LvColorPickerType(uint8_t sliders, bool use_wheel) : sliders_mask_(sliders), use_wheel_(use_wheel) {}
41
42 // Returns null for a slider the widget was not built with.
43 lv_obj_t *get_slider(size_t index) const { return this->sliders_[index]; }
44
45 void set_tint_knobs(bool tint_knobs) { this->tint_knobs_ = tint_knobs; }
46
47 void set_color(lv_color_t color) {
48 lv_color32_t c32 = lv_color_to_32(color, LV_OPA_COVER);
49 this->state_ = Color(c32.red, c32.green, c32.blue);
50 if (this->obj != nullptr)
51 this->update_color_();
52 }
53 Color get_color() const { return this->state_; }
54
55 void set_obj(lv_obj_t *outer) override {
58 // The far end of these two follows the current hue.
59 init_bar(&this->saturation_bar_, LV_GRAD_DIR_HOR, lv_color_white(), lv_color_hex(0xFF0000));
60 init_bar(&this->brightness_bar_, LV_GRAD_DIR_HOR, lv_color_black(), lv_color_white());
61
62 auto channels = static_cast<int32_t>(this->enabled_(SLIDER_RED)) +
63 static_cast<int32_t>(this->enabled_(SLIDER_GREEN)) +
64 static_cast<int32_t>(this->enabled_(SLIDER_BLUE));
65
66 // No layout on the outer object, so LV_SIZE_CONTENT uses the size from self_size_cb.
67 lv_obj_add_event_cb(outer, self_size_cb, LV_EVENT_GET_SELF_SIZE, nullptr);
68 lv_obj_add_event_cb(outer, size_changed_cb, LV_EVENT_SIZE_CHANGED, this);
69 lv_obj_set_style_pad_all(outer, KNOB_OVERHANG, LV_PART_MAIN);
70
71 int32_t left_width = 0;
72 lv_obj_t *left = nullptr;
73 if (this->enabled_(SLIDER_HUE)) {
74 left_width = HUE_WIDTH_PCT;
75 left = this->create_slider_(outer, SLIDER_HUE, "Hue", true, &hue_bar, 360);
76 lv_obj_set_size(left, lv_pct(HUE_WIDTH_PCT), lv_pct(100));
77 }
78#ifdef USE_LVGL_COLOR_PICKER_WHEEL
79 else if (this->use_wheel_) {
80 left_width = WHEEL_WIDTH_PCT;
81 left = this->create_wheel_(outer);
82 lv_obj_set_size(left, lv_pct(WHEEL_WIDTH_PCT), lv_pct(WHEEL_WIDTH_PCT));
83 }
84#endif
85 if (left != nullptr) {
86 lv_obj_set_style_align(left, LV_ALIGN_LEFT_MID, LV_PART_MAIN);
87 // A narrow hue column may be slimmer than its label.
88 lv_obj_set_flag(left, LV_OBJ_FLAG_OVERFLOW_VISIBLE, true);
89 }
90
91 // The text rows keep their natural height and the bars between them take what is left,
92 // so a small widget gets thinner bars rather than clipped text.
93 auto *middle = create_flex(outer, LV_FLEX_FLOW_COLUMN, LV_FLEX_ALIGN_START);
94 lv_obj_set_style_align(middle, LV_ALIGN_RIGHT_MID, LV_PART_MAIN);
95 lv_obj_set_style_pad_row(middle, KNOB_OVERHANG, LV_PART_MAIN);
96 lv_obj_set_size(middle, lv_pct(100 - left_width), lv_pct(100));
97
98 // A dark bar needs light text, unlike every other value label.
99 if (this->enabled_(SLIDER_BRIGHTNESS))
100 this->create_row_slider_(middle, SLIDER_BRIGHTNESS, "Brightness", &this->brightness_bar_, lv_color_white());
101
102 auto *inner = create_flex(middle, LV_FLEX_FLOW_ROW, LV_FLEX_ALIGN_START);
103 lv_obj_set_width(inner, lv_pct(100));
104 lv_obj_set_flex_grow(inner, 1);
105
106 // The channel bars keep a fixed width each, so dropping one widens the swatch.
107 auto channels_width = CHANNEL_WIDTH_PCT * channels;
108 auto *indicator_container = create_flex(inner, LV_FLEX_FLOW_COLUMN, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER);
109 lv_obj_set_size(indicator_container, lv_pct(CONTENT_WIDTH_PCT - channels_width), lv_pct(100));
110 lv_obj_set_style_pad_ver(indicator_container, KNOB_OVERHANG, LV_PART_MAIN);
111 lv_obj_set_style_pad_row(indicator_container, KNOB_OVERHANG, LV_PART_MAIN);
112
113 this->color_text_ = lv_label_create(indicator_container);
114 this->color_indicator_ = lv_obj_create(indicator_container);
115 lv_obj_set_style_border_color(this->color_indicator_, lv_color_hex(0x808080), LV_PART_MAIN);
116 lv_obj_set_style_border_width(this->color_indicator_, 1, LV_PART_MAIN);
117 lv_obj_set_style_radius(this->color_indicator_, 0, LV_PART_MAIN);
118 lv_obj_set_width(this->color_indicator_, lv_pct(90));
119 lv_obj_set_flex_grow(this->color_indicator_, 1);
120
121 if (channels != 0) {
122 auto *rgb_container = create_flex(inner, LV_FLEX_FLOW_ROW, LV_FLEX_ALIGN_SPACE_BETWEEN);
123 lv_obj_set_size(rgb_container, lv_pct(channels_width), lv_pct(100));
124 struct ChannelInfo {
125 SliderIndex index;
126 const char *name;
127 const lv_grad_dsc_t *grad;
128 };
129 const std::array<ChannelInfo, 3> channel_info{{
130 {SLIDER_RED, "R", &red_bar},
131 {SLIDER_GREEN, "G", &green_bar},
132 {SLIDER_BLUE, "B", &blue_bar},
133 }};
134 for (const auto &channel : channel_info) {
135 if (!this->enabled_(channel.index))
136 continue;
137 auto *container = this->create_slider_(rgb_container, channel.index, channel.name, true, channel.grad, 255);
138 lv_obj_set_size(container, lv_pct(100 / channels), lv_pct(100));
139 this->add_value_label_(channel.index, lv_color_black());
140 }
141 }
142
143 if (this->enabled_(SLIDER_SATURATION))
144 this->create_row_slider_(middle, SLIDER_SATURATION, "Saturation", &this->saturation_bar_, lv_color_black());
145
146 this->update_color_();
147
148 for (size_t index = 0; index < SLIDER_COUNT; index++) {
149 if (this->sliders_[index] == nullptr)
150 continue;
151 lv_obj_add_event_cb(this->sliders_[index], index <= SLIDER_BRIGHTNESS ? hsv_changed_cb : rgb_changed_cb,
152 LV_EVENT_VALUE_CHANGED, this);
153 }
154
155 bubble_events(outer);
156 }
157
158 protected:
159 // LVGL keeps only pointers to gradients and styles, so one copy serves every colour picker.
160 static void init_shared() {
161 if (shared_ready)
162 return;
163 shared_ready = true;
164
165 // Clockwise from red at 3 o'clock, matching how lv_color_hsv_to_rgb() reads the angle.
166 std::array<lv_color_t, 7> hues{lv_color_hex(0xFF0000), lv_color_hex(0xFFFF00), lv_color_hex(0x00FF00),
167 lv_color_hex(0x00FFFF), lv_color_hex(0x0000FF), lv_color_hex(0xFF00FF),
168 lv_color_hex(0xFF0000)};
169#ifdef USE_LVGL_COLOR_PICKER_WHEEL
170 lv_grad_conical_init(&wheel_hue_grad, lv_pct(50), lv_pct(50), 0, 360, LV_GRAD_EXTEND_PAD);
171 lv_grad_init_stops(&wheel_hue_grad, hues.data(), nullptr, nullptr, static_cast<int>(hues.size()));
172 // White at the centre fading out at the rim, giving saturation over the hue wheel.
173 const lv_color_t whites[] = {lv_color_white(), lv_color_white()};
174 const lv_opa_t white_opas[] = {LV_OPA_COVER, LV_OPA_TRANSP};
175 lv_grad_radial_init(&wheel_white_grad, lv_pct(50), lv_pct(50), lv_pct(100), lv_pct(50), LV_GRAD_EXTEND_PAD);
176 lv_grad_init_stops(&wheel_white_grad, whites, white_opas, nullptr, 2);
177#endif
178 // A vertical slider has its minimum at the bottom, so the hue bar runs the other way.
179 std::reverse(hues.begin(), hues.end());
180 lv_grad_vertical_init(&hue_bar);
181 lv_grad_init_stops(&hue_bar, hues.data(), nullptr, nullptr, static_cast<int>(hues.size()));
182
183 init_bar(&red_bar, LV_GRAD_DIR_VER, lv_color_hex(0xFF0000), lv_color_white());
184 init_bar(&green_bar, LV_GRAD_DIR_VER, lv_color_hex(0x00FF00), lv_color_white());
185 init_bar(&blue_bar, LV_GRAD_DIR_VER, lv_color_hex(0x0000FF), lv_color_white());
186
187 lv_style_init(&plain_box);
188 lv_style_set_bg_opa(&plain_box, LV_OPA_TRANSP);
189 lv_style_set_border_width(&plain_box, 0);
190 lv_style_set_pad_all(&plain_box, 0);
191 lv_style_set_pad_row(&plain_box, 0);
192 lv_style_set_pad_column(&plain_box, 0);
193
196 }
197
198 static void init_bar(lv_grad_dsc_t *grad, lv_grad_dir_t dir, lv_color_t from, lv_color_t to) {
199 const lv_color_t colors[] = {from, to};
200 grad->dir = dir;
201 lv_grad_init_stops(grad, colors, nullptr, nullptr, 2);
202 }
203
204 static void init_slider_styles(lv_style_t *bar, lv_style_t *knob, bool vertical) {
205 lv_style_init(bar);
206 lv_style_set_align(bar, LV_ALIGN_CENTER);
207 lv_style_set_bg_opa(bar, LV_OPA_COVER);
208 lv_style_set_pad_all(bar, 0);
209 lv_style_set_radius(bar, 0);
210 // A horizontal bar's thickness is set by update_bar_thickness_().
211 if (vertical) {
212 lv_style_set_width(bar, lv_pct(50));
213 lv_style_set_height(bar, lv_pct(90));
214 } else {
215 lv_style_set_width(bar, lv_pct(90));
216 }
217
218 lv_style_init(knob);
219 lv_style_set_bg_color(knob, lv_color_white());
220 lv_style_set_border_width(knob, 1);
221 lv_style_set_outline_color(knob, lv_color_black());
222 lv_style_set_radius(knob, 0);
223 // Both axes are set, or the theme picks a knob size the layout has not left room for.
224 int32_t along = -KNOB_TRIM;
225 int32_t across = KNOB_OVERHANG;
226 lv_style_set_pad_top(knob, vertical ? along : across);
227 lv_style_set_pad_bottom(knob, vertical ? along : across);
228 lv_style_set_pad_left(knob, vertical ? across : along);
229 lv_style_set_pad_right(knob, vertical ? across : along);
230 }
231
232 // A borderless, unpadded container.
233 static lv_obj_t *create_box(lv_obj_t *parent) {
234 auto *obj = lv_obj_create(parent);
235 lv_obj_add_style(obj, &plain_box, LV_PART_MAIN);
236 lv_obj_set_flag(obj, LV_OBJ_FLAG_SCROLLABLE, false);
237 return obj;
238 }
239
240 static lv_obj_t *create_flex(lv_obj_t *parent, lv_flex_flow_t flow, lv_flex_align_t main_place,
241 lv_flex_align_t track_place = LV_FLEX_ALIGN_START) {
242 auto *obj = create_box(parent);
243 lv_obj_set_layout(obj, LV_LAYOUT_FLEX);
244 lv_obj_set_flex_flow(obj, flow);
245 lv_obj_set_flex_align(obj, main_place, LV_FLEX_ALIGN_CENTER, track_place);
246 return obj;
247 }
248
249 // Preferred size, in lines of the text font. The widget is square.
250 static constexpr int SELF_SIZE_FONT_MULTIPLE = 16;
251
252 // At either end of its travel half the knob lies outside the bar. KNOB_TRIM pulls it back
253 // along the bar, and KNOB_OVERHANG makes it stand out across the bar.
254 static constexpr int KNOB_TRIM = 4;
255 static constexpr int KNOB_OVERHANG = 4;
256
257 // Layout proportions, as percentages.
258 static constexpr int32_t HUE_WIDTH_PCT = 15;
259 static constexpr int32_t CHANNEL_WIDTH_PCT = 19;
260 static constexpr int32_t CONTENT_WIDTH_PCT = 99;
261 static constexpr int32_t WHEEL_WIDTH_PCT = 45;
262 static constexpr int32_t WHEEL_HANDLE_SIZE = 14;
263
264 bool enabled_(SliderIndex index) const { return (this->sliders_mask_ & (1 << index)) != 0; }
265 bool has_(SliderIndex index) const { return this->sliders_[index] != nullptr; }
274
275 // A slider the widget was built without keeps the value held internally.
276 int32_t slider_value_(SliderIndex index, int32_t fallback) const {
277 return this->has_(index) ? lv_slider_get_value(this->sliders_[index]) : fallback;
278 }
279
280 void set_slider_value_(SliderIndex index, int32_t value) {
281 if (this->has_(index))
282 lv_slider_set_value(this->sliders_[index], value, LV_ANIM_OFF);
283 }
284
285 // Builds a slider with a name label above it, and returns its container.
286 lv_obj_t *create_slider_(lv_obj_t *parent, SliderIndex index, const char *name, bool vertical,
287 const lv_grad_dsc_t *grad, int32_t max) {
288 // A column keeps the label on its own row however large the font is.
289 auto *container = create_flex(parent, LV_FLEX_FLOW_COLUMN, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER);
290 if (vertical) {
291 // How far the knob reaches past the ends depends on the font, so follow style changes.
292 update_knob_clearance(container);
293 lv_obj_add_event_cb(container, knob_clearance_cb, LV_EVENT_STYLE_CHANGED, nullptr);
294 } else {
295 lv_obj_set_style_pad_bottom(container, KNOB_OVERHANG, LV_PART_MAIN);
296 lv_obj_set_style_pad_row(container, KNOB_OVERHANG, LV_PART_MAIN);
297 }
298 lv_label_set_text(lv_label_create(container), name);
299
300 auto *slider = lv_slider_create(container);
301 this->sliders_[index] = slider;
302 lv_obj_add_style(slider, vertical ? &slider_vert : &slider_horz, LV_PART_MAIN);
303 lv_obj_set_style_bg_grad(slider, grad, LV_PART_MAIN);
304 lv_obj_set_style_bg_opa(slider, LV_OPA_TRANSP, LV_PART_INDICATOR);
305 lv_obj_add_style(slider, vertical ? &slider_vert_knob : &slider_horz_knob, LV_PART_KNOB);
306 if (vertical)
307 lv_obj_set_flex_grow(slider, 1);
308 lv_slider_set_range(slider, 0, max);
309 lv_slider_set_mode(slider, LV_SLIDER_MODE_NORMAL);
310 return container;
311 }
312
313 void create_row_slider_(lv_obj_t *parent, SliderIndex index, const char *name, const lv_grad_dsc_t *grad,
314 lv_color_t text_color) {
315 auto *container = this->create_slider_(parent, index, name, false, grad, 100);
316 lv_obj_set_size(container, lv_pct(100), LV_SIZE_CONTENT);
317 this->add_value_label_(index, text_color);
318 }
319
320#ifdef USE_LVGL_COLOR_PICKER_WHEEL
321 // A full-size circle filled with a gradient.
322 static lv_obj_t *create_disc(lv_obj_t *parent, const lv_grad_dsc_t *grad) {
323 auto *disc = create_box(parent);
324 lv_obj_set_size(disc, lv_pct(100), lv_pct(100));
325 lv_obj_set_style_align(disc, LV_ALIGN_CENTER, LV_PART_MAIN);
326 lv_obj_set_style_radius(disc, LV_RADIUS_CIRCLE, LV_PART_MAIN);
327 lv_obj_set_style_bg_opa(disc, LV_OPA_COVER, LV_PART_MAIN);
328 lv_obj_set_style_bg_grad(disc, grad, LV_PART_MAIN);
329 lv_obj_set_flag(disc, LV_OBJ_FLAG_SCROLLABLE, false);
330 return disc;
331 }
332
333 // Builds the colour wheel that replaces the hue and saturation sliders, and returns its container.
334 lv_obj_t *create_wheel_(lv_obj_t *parent) {
335 auto *container = create_box(parent);
336 this->wheel_ = create_disc(container, &wheel_hue_grad);
337 // Not clickable, so touches fall through to the wheel underneath.
338 auto *overlay = create_disc(this->wheel_, &wheel_white_grad);
339 lv_obj_set_flag(overlay, LV_OBJ_FLAG_CLICKABLE, false);
340
341 this->wheel_handle_ = lv_obj_create(this->wheel_);
342 lv_obj_set_size(this->wheel_handle_, WHEEL_HANDLE_SIZE, WHEEL_HANDLE_SIZE);
343 lv_obj_set_style_align(this->wheel_handle_, LV_ALIGN_CENTER, LV_PART_MAIN);
344 lv_obj_set_style_radius(this->wheel_handle_, LV_RADIUS_CIRCLE, LV_PART_MAIN);
345 lv_obj_set_style_bg_opa(this->wheel_handle_, LV_OPA_TRANSP, LV_PART_MAIN);
346 lv_obj_set_style_border_width(this->wheel_handle_, 2, LV_PART_MAIN);
347 lv_obj_set_style_border_color(this->wheel_handle_, lv_color_white(), LV_PART_MAIN);
348 lv_obj_set_flag(this->wheel_handle_, LV_OBJ_FLAG_CLICKABLE, false);
349 lv_obj_set_flag(this->wheel_handle_, LV_OBJ_FLAG_SCROLLABLE, false);
350
351 // PRESSED as well, so a tap takes effect without the finger moving.
352 lv_obj_add_event_cb(this->wheel_, wheel_touch_cb, LV_EVENT_PRESSED, this);
353 lv_obj_add_event_cb(this->wheel_, wheel_touch_cb, LV_EVENT_PRESSING, this);
354 // The handle can only be placed once the wheel has been laid out.
355 lv_obj_add_event_cb(this->wheel_, wheel_size_cb, LV_EVENT_SIZE_CHANGED, this);
356 return container;
357 }
358#endif
359
360 void add_value_label_(SliderIndex index, lv_color_t color) {
361 auto *slider = this->sliders_[index];
362 lv_obj_set_flag(slider, LV_OBJ_FLAG_OVERFLOW_VISIBLE, true);
363 auto *label = lv_label_create(slider);
364 this->values_[index] = label;
365 lv_obj_set_style_align(label, LV_ALIGN_CENTER, LV_PART_MAIN);
366 lv_obj_set_style_text_color(label, color, LV_PART_MAIN);
367 }
368
369 // Leaves room at the ends of a vertical slider for the overhanging knob: half a line of text.
370 static void update_knob_clearance(lv_obj_t *container) {
371 const auto *font = lv_obj_get_style_text_font(container, LV_PART_MAIN);
372 auto clearance = static_cast<int32_t>(lv_font_get_line_height(font)) / 2;
373 if (clearance == lv_obj_get_style_pad_bottom(container, LV_PART_MAIN))
374 return;
375 lv_obj_set_style_pad_bottom(container, clearance, LV_PART_MAIN);
376 lv_obj_set_style_pad_row(container, clearance, LV_PART_MAIN);
377 }
378
379 static void knob_clearance_cb(lv_event_t *event) { update_knob_clearance(lv_event_get_current_target_obj(event)); }
380
381 static LvColorPickerType *from_event(lv_event_t *event) {
382 return static_cast<LvColorPickerType *>(lv_event_get_user_data(event));
383 }
384 static void hsv_changed_cb(lv_event_t *event) { from_event(event)->update_hsv_(); }
385 static void rgb_changed_cb(lv_event_t *event) { from_event(event)->update_rgb_(); }
386#ifdef USE_LVGL_COLOR_PICKER_WHEEL
387 static void wheel_touch_cb(lv_event_t *event) { from_event(event)->update_from_wheel_touch_(); }
388 static void wheel_size_cb(lv_event_t *event) { from_event(event)->update_wheel_handle_(); }
389#endif
390 static void size_changed_cb(lv_event_t *event) { from_event(event)->update_bar_thickness_(); }
391
392 // Gives the horizontal bars the thickness of the vertical ones, half a hue column, so every
393 // knob is the same size. A knob is sized from the thickness of its bar.
395 auto thickness = lv_obj_get_content_width(this->obj) * HUE_WIDTH_PCT / 200;
396 for (auto index : {SLIDER_SATURATION, SLIDER_BRIGHTNESS}) {
397 if (this->has_(index) && lv_obj_get_style_height(this->sliders_[index], LV_PART_MAIN) != thickness)
398 lv_obj_set_style_height(this->sliders_[index], thickness, LV_PART_MAIN);
399 }
400 }
401
402 // LVGL also counts this size when working out the scroll area, so a widget given a fixed
403 // width must not claim it, or it would scroll whenever it is smaller.
404 static void self_size_cb(lv_event_t *event) {
405 auto *obj = lv_event_get_current_target_obj(event);
406 if (lv_obj_get_style_width(obj, LV_PART_MAIN) != LV_SIZE_CONTENT)
407 return;
408 const auto *font = lv_obj_get_style_text_font(obj, LV_PART_MAIN);
409 auto self_size = static_cast<int32_t>(lv_font_get_line_height(font)) * SELF_SIZE_FONT_MULTIPLE;
410 auto *point = static_cast<lv_point_t *>(lv_event_get_param(event));
411 point->x = self_size;
412 point->y = self_size;
413 }
414
415 void update_text_() const {
416 // "#" + 3x "%02X" + null
417 char buf[8];
418 snprintf(buf, sizeof(buf), "#%02X%02X%02X", static_cast<unsigned>(this->state_.r),
419 static_cast<unsigned>(this->state_.g), static_cast<unsigned>(this->state_.b));
420 lv_label_set_text(this->color_text_, buf);
421 }
422
423 void update_hue_bar_(SliderIndex index, lv_grad_dsc_t *grad, lv_color_t color) {
424 if (!this->has_(index))
425 return;
426 grad->stops[1].color = color;
427 lv_obj_invalidate(this->sliders_[index]);
428 }
429
430 void update_values_() const {
431 for (size_t index = 0; index < SLIDER_COUNT; index++) {
432 if (this->values_[index] == nullptr)
433 continue;
434 // up to 3 digits + null
435 char buf[4];
436 snprintf(buf, sizeof(buf), "%d", static_cast<int>(lv_slider_get_value(this->sliders_[index])));
437 lv_label_set_text(this->values_[index], buf);
438 }
439 }
440
441 void tint_knob_(SliderIndex index, lv_color_t color) const {
442 if (this->has_(index))
443 lv_obj_set_style_bg_color(this->sliders_[index], color, LV_PART_KNOB);
444 }
445
446 // Tints each knob with the colour of its bar at the current value. The brightness knob
447 // shows the selected colour instead, since its bar leaves out saturation.
448 void update_knobs_(lv_color_t full_hue) const {
449 if (!this->tint_knobs_)
450 return;
451 this->tint_knob_(SLIDER_HUE, full_hue);
452 this->tint_knob_(SLIDER_SATURATION, lv_color_hsv_to_rgb(this->hue_, this->saturation_, 100));
453 this->tint_knob_(SLIDER_BRIGHTNESS, this->state_);
454 this->tint_knob_(SLIDER_RED, lv_color_make(255, 255 - this->state_.r, 255 - this->state_.r));
455 this->tint_knob_(SLIDER_GREEN, lv_color_make(255 - this->state_.g, 255, 255 - this->state_.g));
456 this->tint_knob_(SLIDER_BLUE, lv_color_make(255 - this->state_.b, 255 - this->state_.b, 255));
457 }
458
459#ifdef USE_LVGL_COLOR_PICKER_WHEEL
460 // Returns the wheel's radius, which is zero until it has been laid out.
461 double wheel_radius_(lv_area_t *area) const {
462 lv_obj_get_coords(this->wheel_, area);
463 return static_cast<double>(area->x2 - area->x1) / 2.0;
464 }
465
467 if (this->wheel_handle_ == nullptr)
468 return;
469 lv_area_t area;
470 auto radius = this->wheel_radius_(&area);
471 if (radius <= 0)
472 return;
473 auto angle_rad = static_cast<double>(this->hue_) * (std::numbers::pi / 180.0);
474 auto distance = (static_cast<double>(this->saturation_) / 100.0) * radius;
475 lv_obj_set_style_translate_x(this->wheel_handle_, static_cast<int32_t>(std::lround(distance * std::cos(angle_rad))),
476 LV_PART_MAIN);
477 lv_obj_set_style_translate_y(this->wheel_handle_, static_cast<int32_t>(std::lround(distance * std::sin(angle_rad))),
478 LV_PART_MAIN);
479 }
480
481 // The angle from the centre gives the hue, the distance the saturation. Brightness is unchanged.
483 lv_indev_t *indev = lv_indev_active();
484 if (indev == nullptr)
485 return;
486 lv_point_t point;
487 lv_indev_get_point(indev, &point);
488 lv_area_t area;
489 auto radius = this->wheel_radius_(&area);
490 if (radius <= 0)
491 return;
492 auto dx = point.x - (area.x1 + area.x2) / 2.0;
493 auto dy = point.y - (area.y1 + area.y2) / 2.0;
494 auto angle = std::atan2(dy, dx) * (180.0 / std::numbers::pi);
495 if (angle < 0)
496 angle += 360.0;
497 this->hue_ = static_cast<uint16_t>(std::lround(angle)) % 360;
498 auto distance = std::sqrt(dx * dx + dy * dy);
499 this->saturation_ = static_cast<uint8_t>(std::lround(std::min(100.0, distance * 100.0 / radius)));
500 this->brightness_ = static_cast<uint8_t>(this->slider_value_(SLIDER_BRIGHTNESS, this->brightness_));
501 this->apply_hsv_();
502 // Only sliders send this themselves; without it on_value would not fire for the wheel.
503 lv_obj_send_event(this->wheel_, LV_EVENT_VALUE_CHANGED, nullptr);
504 }
505#endif
506
507 // Lets events from the internal sliders reach the widget, where the triggers listen. Every
508 // object in between needs the flag; the widget itself does not, so events stop there.
509 static void bubble_events(lv_obj_t *obj) {
510 for (uint32_t i = 0; i < lv_obj_get_child_count(obj); i++) {
511 auto *child = lv_obj_get_child(obj, i);
512 lv_obj_set_flag(child, LV_OBJ_FLAG_EVENT_BUBBLE, true);
513 bubble_events(child);
514 }
515 }
516
517 void refresh_() {
518 lv_obj_set_style_bg_color(this->color_indicator_, this->state_, LV_PART_MAIN);
519 auto full_hue = lv_color_hsv_to_rgb(this->hue_, 100, 100);
520 this->update_hue_bar_(SLIDER_SATURATION, &this->saturation_bar_, full_hue);
521 this->update_hue_bar_(SLIDER_BRIGHTNESS, &this->brightness_bar_, full_hue);
522 this->update_text_();
523 this->update_values_();
524 this->update_knobs_(full_hue);
525#ifdef USE_LVGL_COLOR_PICKER_WHEEL
526 this->update_wheel_handle_();
527#endif
528 }
529
530 // Sets the colour from the hue, saturation and brightness, and moves the RGB sliders to match.
531 void apply_hsv_() {
532 lv_color32_t c32 =
533 lv_color_to_32(lv_color_hsv_to_rgb(this->hue_, this->saturation_, this->brightness_), LV_OPA_COVER);
534 this->state_ = Color(c32.red, c32.green, c32.blue);
535 this->set_slider_value_(SLIDER_RED, c32.red);
536 this->set_slider_value_(SLIDER_GREEN, c32.green);
537 this->set_slider_value_(SLIDER_BLUE, c32.blue);
538 this->refresh_();
539 }
540
541 void update_hsv_() {
542 this->hue_ = static_cast<uint16_t>(this->slider_value_(SLIDER_HUE, this->hue_));
543 this->saturation_ = static_cast<uint8_t>(this->slider_value_(SLIDER_SATURATION, this->saturation_));
544 this->brightness_ = static_cast<uint8_t>(this->slider_value_(SLIDER_BRIGHTNESS, this->brightness_));
545 this->apply_hsv_();
546 }
547
548 void update_rgb_() {
549 this->state_ = Color(static_cast<uint8_t>(this->slider_value_(SLIDER_RED, this->state_.r)),
550 static_cast<uint8_t>(this->slider_value_(SLIDER_GREEN, this->state_.g)),
551 static_cast<uint8_t>(this->slider_value_(SLIDER_BLUE, this->state_.b)));
552 auto hsv = lv_color_rgb_to_hsv(this->state_.r, this->state_.g, this->state_.b);
553 this->hue_ = hsv.h;
554 this->saturation_ = hsv.s;
555 this->brightness_ = hsv.v;
556 // A grey, white or black colour leaves a picker stuck there if nothing it has can move the
557 // saturation, or the brightness, away from zero.
558 bool stuck = false;
559 if (this->saturation_ == 0 && !this->can_change_saturation_()) {
560 this->saturation_ = 100;
561 stuck = true;
562 }
563 if (this->brightness_ == 0 && !this->can_change_brightness_()) {
564 this->brightness_ = 100;
565 stuck = true;
566 }
567 this->set_slider_value_(SLIDER_HUE, this->hue_);
570 if (stuck) {
571 this->apply_hsv_();
572 } else {
573 this->refresh_();
574 }
575 }
576
578 this->set_slider_value_(SLIDER_RED, this->state_.r);
581 this->update_rgb_();
582 lv_obj_invalidate(this->obj);
583 }
584
585 // Shared by every colour picker; set up once by init_shared().
586 inline static bool shared_ready{false};
587 inline static lv_grad_dsc_t hue_bar{};
588 inline static lv_grad_dsc_t red_bar{};
589 inline static lv_grad_dsc_t green_bar{};
590 inline static lv_grad_dsc_t blue_bar{};
591#ifdef USE_LVGL_COLOR_PICKER_WHEEL
592 inline static lv_grad_dsc_t wheel_hue_grad{};
593 inline static lv_grad_dsc_t wheel_white_grad{};
594#endif
595 inline static lv_style_t plain_box{};
596 inline static lv_style_t slider_vert{};
597 inline static lv_style_t slider_vert_knob{};
598 inline static lv_style_t slider_horz{};
599 inline static lv_style_t slider_horz_knob{};
600
601 // Per widget, since their far end follows the chosen hue.
602 lv_grad_dsc_t saturation_bar_{};
603 lv_grad_dsc_t brightness_bar_{};
604 Color state_{0x80, 0x80, 0x80};
605 std::array<lv_obj_t *, SLIDER_COUNT> sliders_{};
606 std::array<lv_obj_t *, SLIDER_COUNT> values_{};
607 lv_obj_t *color_indicator_{};
608 lv_obj_t *color_text_{};
609 // Kept alongside state_ so a slider left out of the widget still has a value.
610 uint16_t hue_{0};
611 uint8_t saturation_{0};
612 uint8_t brightness_{50};
613 const uint8_t sliders_mask_;
614 const bool use_wheel_;
615 bool tint_knobs_{true};
616#ifdef USE_LVGL_COLOR_PICKER_WHEEL
617 // Null unless use_wheel_ is set.
618 lv_obj_t *wheel_{};
619 lv_obj_t *wheel_handle_{};
620#endif
621};
622
623} // namespace esphome::lvgl
624
625#endif // USE_LVGL_COLOR_PICKER
LvColorPickerType(uint8_t sliders, bool use_wheel)
void update_hue_bar_(SliderIndex index, lv_grad_dsc_t *grad, lv_color_t color)
static constexpr int KNOB_TRIM
static void bubble_events(lv_obj_t *obj)
static constexpr int32_t WHEEL_WIDTH_PCT
bool has_(SliderIndex index) const
std::array< lv_obj_t *, SLIDER_COUNT > sliders_
int32_t slider_value_(SliderIndex index, int32_t fallback) const
lv_obj_t * create_wheel_(lv_obj_t *parent)
static constexpr int32_t WHEEL_HANDLE_SIZE
lv_obj_t * create_slider_(lv_obj_t *parent, SliderIndex index, const char *name, bool vertical, const lv_grad_dsc_t *grad, int32_t max)
static lv_obj_t * create_disc(lv_obj_t *parent, const lv_grad_dsc_t *grad)
static lv_grad_dsc_t wheel_hue_grad
static void hsv_changed_cb(lv_event_t *event)
static constexpr int SELF_SIZE_FONT_MULTIPLE
std::array< lv_obj_t *, SLIDER_COUNT > values_
lv_obj_t * get_slider(size_t index) const
static lv_obj_t * create_box(lv_obj_t *parent)
static constexpr int32_t CONTENT_WIDTH_PCT
static void knob_clearance_cb(lv_event_t *event)
double wheel_radius_(lv_area_t *area) const
static lv_obj_t * create_flex(lv_obj_t *parent, lv_flex_flow_t flow, lv_flex_align_t main_place, lv_flex_align_t track_place=LV_FLEX_ALIGN_START)
static constexpr int32_t CHANNEL_WIDTH_PCT
void tint_knob_(SliderIndex index, lv_color_t color) const
static void wheel_touch_cb(lv_event_t *event)
static constexpr int32_t HUE_WIDTH_PCT
bool enabled_(SliderIndex index) const
void set_color(lv_color_t color)
void add_value_label_(SliderIndex index, lv_color_t color)
static void update_knob_clearance(lv_obj_t *container)
void update_knobs_(lv_color_t full_hue) const
static lv_grad_dsc_t wheel_white_grad
void set_tint_knobs(bool tint_knobs)
static lv_grad_dsc_t blue_bar
static void size_changed_cb(lv_event_t *event)
static lv_grad_dsc_t green_bar
static LvColorPickerType * from_event(lv_event_t *event)
void set_slider_value_(SliderIndex index, int32_t value)
static void init_bar(lv_grad_dsc_t *grad, lv_grad_dir_t dir, lv_color_t from, lv_color_t to)
void set_obj(lv_obj_t *outer) override
static void wheel_size_cb(lv_event_t *event)
void create_row_slider_(lv_obj_t *parent, SliderIndex index, const char *name, const lv_grad_dsc_t *grad, lv_color_t text_color)
static void rgb_changed_cb(lv_event_t *event)
static void init_slider_styles(lv_style_t *bar, lv_style_t *knob, bool vertical)
static void self_size_cb(lv_event_t *event)
static constexpr int KNOB_OVERHANG
virtual void set_obj(lv_obj_t *lv_obj)
static void uint32_t
uint8_t g
Definition color.h:34
uint8_t b
Definition color.h:38
uint8_t r
Definition color.h:30