Skip to content

Commit

Permalink
fix(arc): handle click outside background angle range (lvgl#4586) (lv…
Browse files Browse the repository at this point in the history
  • Loading branch information
C47D authored Oct 19, 2023
1 parent 454e454 commit 17c580f
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 11 deletions.
163 changes: 156 additions & 7 deletions src/widgets/lv_arc.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
#define MY_CLASS &lv_arc_class

#define VALUE_UNSET INT16_MIN
#define CLICK_OUTSIDE_BG_ANGLES ((uint32_t) 0x00U)
#define CLICK_INSIDE_BG_ANGLES ((uint32_t) 0x01U)
#define CLICK_CLOSER_TO_MAX_END ((uint32_t) 0x00U)
#define CLICK_CLOSER_TO_MIN_END ((uint32_t) 0x01U)

/**********************
* TYPEDEFS
Expand All @@ -40,6 +44,7 @@ static lv_coord_t get_angle(const lv_obj_t * obj);
static void get_knob_area(lv_obj_t * arc, const lv_point_t * center, lv_coord_t r, lv_area_t * knob_area);
static void value_update(lv_obj_t * arc);
static lv_coord_t knob_get_extra_size(lv_obj_t * obj);
static bool lv_arc_angle_within_bg_bounds(lv_obj_t * obj, const uint32_t angle, const uint32_t tolerance_deg);

/**********************
* STATIC VARIABLES
Expand Down Expand Up @@ -399,6 +404,7 @@ static void lv_arc_constructor(const lv_obj_class_t * class_p, lv_obj_t * obj)
arc->chg_rate = 720;
arc->last_tick = lv_tick_get();
arc->last_angle = arc->indic_angle_end;
arc->in_out = CLICK_OUTSIDE_BG_ANGLES;

lv_obj_add_flag(obj, LV_OBJ_FLAG_CLICKABLE);
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLL_CHAIN | LV_OBJ_FLAG_SCROLLABLE);
Expand Down Expand Up @@ -478,36 +484,62 @@ static void lv_arc_event(const lv_obj_class_t * class_p, lv_event_t * e)
angle -= arc->rotation;
angle -= arc->bg_angle_start; /*Make the angle relative to the start angle*/

/* If we click near the bg_angle_start the angle will be close to 360° instead of an small angle */
if(angle < 0) angle += 360;

int16_t deg_range = bg_end - arc->bg_angle_start;
const uint32_t circumference = (uint32_t)((2U * r * 314U) / 100U); /* Equivalent to: 2r * 3.14, avoiding floats */
const uint32_t tolerance_deg = (360U * LV_DPX(50U)) / circumference;
const uint32_t min_close_prev = (uint32_t) arc->min_close;

const bool is_angle_within_bg_bounds = lv_arc_angle_within_bg_bounds(obj, (uint32_t) angle, tolerance_deg);
if(!is_angle_within_bg_bounds) {
return;
}

int16_t deg_range = bg_end - arc->bg_angle_start;
int16_t last_angle_rel = arc->last_angle - arc->bg_angle_start;
int16_t delta_angle = angle - last_angle_rel;

/*Do not allow big jumps.
/*Do not allow big jumps (jumps bigger than 280°).
*It's mainly to avoid jumping to the opposite end if the "dead" range between min. and max. is crossed.
*Check which end was closer on the last valid press (arc->min_close) and prefer that end*/
if(LV_ABS(delta_angle) > 280) {
if(arc->min_close) angle = 0;
else angle = deg_range;
}
else {
if(angle < deg_range / 2)arc->min_close = 1;
else arc->min_close = 0;
/* Check if click was outside the background arc start and end angles */
else if(CLICK_OUTSIDE_BG_ANGLES == arc->in_out) {
if(arc->min_close) angle = -deg_range;
else angle = deg_range;
}
else { /* Keep the angle value */ }

/* Prevent big jumps when the click goes from start to end angle in the invisible
* part of the background arc without being released */
if(((min_close_prev == CLICK_CLOSER_TO_MIN_END) && (arc->min_close == CLICK_CLOSER_TO_MAX_END))
&& ((CLICK_OUTSIDE_BG_ANGLES == arc->in_out) && (LV_ABS(delta_angle) > 280))) {
angle = 0;
}
else if(((min_close_prev == CLICK_CLOSER_TO_MAX_END) && (arc->min_close == CLICK_CLOSER_TO_MIN_END))
&& (CLICK_OUTSIDE_BG_ANGLES == arc->in_out)) {
angle = deg_range;
}
else { /* Keep the angle value */ }

/*Calculate the slew rate limited angle based on change rate (degrees/sec)*/
delta_angle = angle - last_angle_rel;

uint32_t delta_tick = lv_tick_elaps(arc->last_tick);
int16_t delta_angle_max = (arc->chg_rate * delta_tick) / 1000;
/* delta_angle_max can never be signed. delta_tick is always signed, same for ch_rate */
const uint16_t delta_angle_max = (arc->chg_rate * delta_tick) / 1000;

if(delta_angle > delta_angle_max) {
delta_angle = delta_angle_max;
}
else if(delta_angle < -delta_angle_max) {
delta_angle = -delta_angle_max;
}
else { /* Nothing to do */ }

angle = last_angle_rel + delta_angle; /*Apply the limited angle change*/

Expand Down Expand Up @@ -564,7 +596,7 @@ static void lv_arc_event(const lv_obj_class_t * class_p, lv_event_t * e)
}
}
else if(code == LV_EVENT_HIT_TEST) {
lv_hit_test_info_t * info = lv_event_get_param(e);;
lv_hit_test_info_t * info = lv_event_get_param(e);

lv_point_t p;
lv_coord_t r;
Expand Down Expand Up @@ -868,4 +900,121 @@ static lv_coord_t knob_get_extra_size(lv_obj_t * obj)
return LV_MAX(knob_shadow_size, knob_outline_size);
}

/**
* Check if angle is within arc background bounds
*
* In order to avoid unexpected value update of the arc value when the user clicks
* outside of the arc background we need to check if the angle (of the clicked point)
* is within the bounds of the background.
*
* A tolerance (extra room) also should be taken into consideration.
*
* E.g. Arc with start angle of 0° and end angle of 90°, the background is only visible in
* that range, from 90° to 360° the background is invisible. Click in 150° should not update
* the arc value, click within the arc angle range should.
*
* IMPORTANT NOTE: angle is always relative to bg_angle_start, e.g. if bg_angle_start is 30
* and we click a bit to the left, angle is 10, not the expected 40.
*
* @param obj Pointer to lv_arc
* @param angle Angle to be checked
* @param tolerance_deg Tolerance
*
* @return true if angle is within arc background bounds, false otherwise
*/
static bool lv_arc_angle_within_bg_bounds(lv_obj_t * obj, const uint32_t angle, const uint32_t tolerance_deg)
{
LV_ASSERT_OBJ(obj, MY_CLASS);
lv_arc_t * arc = (lv_arc_t *)obj;

uint32_t smaller_angle = 0;
uint32_t bigger_angle = 0;

/* Determine which background angle is smaller and bigger */
if(arc->bg_angle_start < arc->bg_angle_end) {
bigger_angle = arc->bg_angle_end;
smaller_angle = arc->bg_angle_start;
}
else {
bigger_angle = 360U - arc->bg_angle_end;
smaller_angle = arc->bg_angle_start;
}

/* Angle is between both background angles */
if((smaller_angle <= angle) && (angle <= bigger_angle)) {

if(((bigger_angle - smaller_angle) / 2U) >= angle) {
arc->min_close = 1;
}
else {
arc->min_close = 0;
}

arc->in_out = CLICK_INSIDE_BG_ANGLES;

return true;
}
/* Distance between background start and end angles is less than tolerance,
* consider the click inside the arc */
else if(((smaller_angle - tolerance_deg) <= 0U) &&
(360U - (bigger_angle + (smaller_angle - tolerance_deg)))) {

arc->min_close = 1;
arc->in_out = CLICK_INSIDE_BG_ANGLES;
return true;
}
else { /* Case handled below */ }

/* Legends:
* 0° = angle 0
* 360° = angle 360
* T: Tolerance
* A: Angle
* S: Arc background start angle
* E: Arc background end angle
*
* Start angle is bigger or equal to tolerance */
if((smaller_angle >= tolerance_deg)
/* (360° - T) --- A --- 360° */
&& ((angle >= (360U - tolerance_deg)) && (angle <= 360U))) {

arc->min_close = 1;
arc->in_out = CLICK_OUTSIDE_BG_ANGLES;
return true;
}
/* Tolerance is bigger than bg start angle */
else if((smaller_angle < tolerance_deg)
/* (360° - (T - S)) --- A --- 360° */
&& (((360U - (tolerance_deg - smaller_angle)) <= angle)) && (angle <= 360U)) {

arc->min_close = 1;
arc->in_out = CLICK_OUTSIDE_BG_ANGLES;
return true;
}
/* 360° is bigger than background end angle + tolerance */
else if((360U >= (bigger_angle + tolerance_deg))
/* E --- A --- (E + T) */
&& ((bigger_angle <= (angle + smaller_angle)) &&
((angle + smaller_angle) <= (bigger_angle + tolerance_deg)))) {

arc->min_close = 0;
arc->in_out = CLICK_OUTSIDE_BG_ANGLES;
return true;
}
/* Background end angle + tolerance is bigger than 360° and bg_start_angle + tolerance is not near 0° + ((bg_end_angle + tolerance) - 360°)
* Here we can assume background is not near 0° because of the first two initial checks */
else if((360U < (bigger_angle + tolerance_deg))
&& (angle <= 0U + ((bigger_angle + tolerance_deg) - 360U)) && (angle > bigger_angle)) {

arc->min_close = 0;
arc->in_out = CLICK_OUTSIDE_BG_ANGLES;
return true;
}
else {
/* Nothing to do */
}

return false;
}

#endif
9 changes: 5 additions & 4 deletions src/widgets/lv_arc.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ typedef struct {
int16_t value; /*Current value of the arc*/
int16_t min_value; /*Minimum value of the arc*/
int16_t max_value; /*Maximum value of the arc*/
uint16_t dragging : 1;
uint16_t type : 2;
uint16_t min_close : 1; /*1: the last pressed angle was closer to minimum end*/
uint16_t chg_rate; /*Drag angle rate of change of the arc (degrees/sec)*/
uint32_t dragging : 1;
uint32_t type : 2;
uint32_t min_close : 1; /*1: the last pressed angle was closer to minimum end*/
uint32_t in_out : 1; /* 1: The click was within the background arc angles. 0: Click outside */
uint32_t chg_rate; /*Drag angle rate of change of the arc (degrees/sec)*/
uint32_t last_tick; /*Last dragging event timestamp of the arc*/
int16_t last_angle; /*Last dragging angle of the arc*/
} lv_arc_t;
Expand Down
47 changes: 47 additions & 0 deletions tests/src/test_cases/test_arc.c
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,53 @@ void test_arc_click_area_with_adv_hittest(void)
TEST_ASSERT_EQUAL_UINT32(0, event_cnt);
}

/* Check value doesn't go to max when clicking on the other side of the arc */
void test_arc_click_sustained_from_start_to_end_does_not_set_value_to_max(void)
{
arc = lv_arc_create(lv_scr_act());
lv_arc_set_value(arc, 0);

lv_obj_set_size(arc, 100, 100);
lv_obj_center(arc);
lv_obj_add_event_cb(arc, dummy_event_cb, LV_EVENT_PRESSED, NULL);
event_cnt = 0;

/* Click close to start angle */
event_cnt = 0;
lv_test_mouse_move_to(376, 285);
lv_test_mouse_press();
lv_test_indev_wait(50);
lv_test_mouse_release();
lv_test_indev_wait(50);

TEST_ASSERT_EQUAL_UINT32(1, event_cnt);
TEST_ASSERT_EQUAL_UINT32(lv_arc_get_value(arc), lv_arc_get_min_value(arc));

/* Click close to end angle */
event_cnt = 0;

lv_test_mouse_move_to(376, 285);
lv_test_mouse_press();
lv_test_indev_wait(50);
lv_test_mouse_move_to(415, 281);
lv_test_indev_wait(50);
lv_test_mouse_release();
lv_test_indev_wait(50);

TEST_ASSERT_EQUAL_UINT32(1, event_cnt);
TEST_ASSERT_NOT_EQUAL_UINT32(lv_arc_get_value(arc), lv_arc_get_max_value(arc));

TEST_ASSERT_EQUAL_SCREENSHOT("arc_2.png");
}

void test_arc_basic_render(void)
{
arc = lv_arc_create(lv_scr_act());
lv_obj_set_size(arc, 100, 100);
lv_obj_center(arc);
TEST_ASSERT_EQUAL_SCREENSHOT("arc_1.png");
}

static void dummy_event_cb(lv_event_t * e)
{
LV_UNUSED(e);
Expand Down

0 comments on commit 17c580f

Please sign in to comment.