How to create a radial menu?

What do you want to achieve?

A radial menu like you’d see in a video game.

What have you tried so far?

I have modified the Pie Chart with clickable slices using Arcs from examples.

The problem I’m having is the accuracy for events like click event doesn’t behave how I would expect when the width of the arc indicator is below half the size of the arc.

Code to reproduce

#include "radial_menu.h"

#define CHART_SIZE 160
#define SEGMENT_THICKNESS 40
#define SLICE_OFFSET 20

typedef struct {
	int start_angle;
	int end_angle;
	int mid_angle;
	lv_point_t home;
	bool out;
} segment_info_t;

typedef struct {
	lv_obj_t* obj;
	int start_x;
	int start_y;
	int end_x;
	int end_y;
} segment_anim_data_t;

static float angle_accum = 0.0f;
static segment_info_t* active_info = NULL;
static lv_obj_t* active_arc = NULL;

static void anim_move_cb(void* var, int32_t v) {
	segment_anim_data_t* d = (segment_anim_data_t*)var;

	int32_t x = d->start_x + ((d->end_x - d->start_x) * v) / 100;
	int32_t y = d->start_y + ((d->end_y - d->start_y) * v) / 100;
	lv_obj_set_pos(d->obj, x, y);
}

static void anim_cleanup_cb(lv_anim_t* a) {
	lv_free(a->var);
}

static void arc_click_cb(lv_event_t* e) {
	lv_obj_t* arc = lv_event_get_target_obj(e);
	segment_info_t* info = (segment_info_t*)lv_event_get_user_data(e);

	int32_t x_off = (SLICE_OFFSET * lv_trigo_cos(info->mid_angle)) >> LV_TRIGO_SHIFT;
	int32_t y_off = (SLICE_OFFSET * lv_trigo_sin(info->mid_angle)) >> LV_TRIGO_SHIFT;

	if (active_info && active_info != info && active_info->out) {
		segment_anim_data_t* anim_back = (segment_anim_data_t*)lv_malloc(sizeof(segment_anim_data_t));
		anim_back->obj = active_arc;
		anim_back->start_x = lv_obj_get_x(active_arc) - SLICE_OFFSET;
		anim_back->start_y = lv_obj_get_y(active_arc) - SLICE_OFFSET;
		anim_back->end_x = active_info->home.x;
		anim_back->end_y = active_info->home.y;

		active_info->out = false;

		lv_anim_t a;
		lv_anim_init(&a);
		lv_anim_set_var(&a, anim_back);
		lv_anim_set_exec_cb(&a, anim_move_cb);
		lv_anim_set_time(&a, 50);
		lv_anim_set_values(&a, 0, 100);
		lv_anim_set_deleted_cb(&a, anim_cleanup_cb);
		lv_anim_start(&a);
	}

	int target_x, target_y;
	if (info->out) {
		target_x = info->home.x;
		target_y = info->home.y;
		info->out = false;
		active_info = NULL;
		active_arc = NULL;
	}
	else {
		target_x = info->home.x + x_off;
		target_y = info->home.y + y_off;
		info->out = true;
		active_info = info;
		active_arc = arc;
	}

	segment_anim_data_t* anim_data = (segment_anim_data_t*)lv_malloc(sizeof(segment_anim_data_t));
	anim_data->obj = arc;
	anim_data->start_x = lv_obj_get_x(arc) - SLICE_OFFSET;
	anim_data->start_y = lv_obj_get_y(arc) - SLICE_OFFSET;
	anim_data->end_x = target_x;
	anim_data->end_y = target_y;

	lv_anim_t a;
	lv_anim_init(&a);
	lv_anim_set_var(&a, anim_data);
	lv_anim_set_exec_cb(&a, anim_move_cb);
	lv_anim_set_time(&a, 50);
	lv_anim_set_values(&a, 0, 100);
	lv_anim_set_deleted_cb(&a, anim_cleanup_cb);
	lv_anim_start(&a);
}

static void create_segment(lv_obj_t* parent, const int segments) {
	float segment_angle = 360.0f / segments;
	float gap = 3.0f;
	int start = (int)(angle_accum + 0.4f + gap / 2);
	angle_accum += segment_angle;
	int end = (int)(angle_accum + 0.5f - gap / 2);
	if (end > 360) end = 360;

	lv_obj_t* arc = lv_arc_create(parent);
	lv_obj_set_size(arc, CHART_SIZE, CHART_SIZE);
	lv_obj_center(arc);

	lv_arc_set_mode(arc, LV_ARC_MODE_NORMAL);
	lv_arc_set_bg_start_angle(arc, start);
	lv_arc_set_bg_end_angle(arc, end);

	lv_obj_set_style_arc_width(arc, SEGMENT_THICKNESS, 0);
	///lv_obj_set_style_arc_width(arc, CHART_SIZE / 2, 0);
	lv_obj_set_style_arc_width(arc, 0, LV_PART_INDICATOR);

	lv_obj_set_style_arc_color(arc, lv_color_hex(0xff0000), 0);
	lv_obj_set_style_arc_rounded(arc, false, 0);
	lv_obj_remove_style(arc, NULL, LV_PART_KNOB);
	lv_obj_add_flag(arc, LV_OBJ_FLAG_ADV_HITTEST);

	/* for visualizing bounding box to debug */
	lv_obj_set_style_border_width(arc, 1, 0);
	lv_obj_set_style_border_color(arc, lv_color_hex(0x00FF00), 0);
	lv_obj_set_style_bg_opa(arc, LV_OPA_20, 0);
	lv_obj_set_style_bg_color(arc, lv_color_hex(0x0000FF), 0);

	segment_info_t* info = (segment_info_t*) lv_malloc(sizeof(segment_info_t));
	info->start_angle = start;
	info->end_angle = end;
	info->mid_angle = start + ((end - start) / 2);
	info->out = false;
	info->home.x = lv_obj_get_x(arc);
	info->home.y = lv_obj_get_y(arc);
	lv_obj_add_event_cb(arc, arc_click_cb, LV_EVENT_CLICKED, info);
}

lv_obj_t* radial_menu_create(lv_obj_t* parent, const int segments) {
	lv_obj_t* root = lv_obj_create(parent);
	lv_obj_set_size(root, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
	lv_obj_center(root);
	lv_obj_set_flex_flow(root, LV_FLEX_FLOW_ROW);
	lv_obj_set_flex_align(root, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);

	lv_obj_set_style_pad_all(root, 0, 0);
	lv_obj_set_style_border_width(root, 0, 0);
	lv_obj_set_style_border_color(root, lv_color_hex(0xFF0000), 0);
	lv_obj_set_style_bg_opa(root, LV_OPA_TRANSP, 0);
	lv_obj_remove_flag(root, LV_OBJ_FLAG_SCROLLABLE);

	/* slices container */
	lv_obj_t* slices_container = lv_obj_create(root);
	lv_obj_set_size(slices_container, CHART_SIZE + 2 * SLICE_OFFSET, CHART_SIZE + 2 * SLICE_OFFSET);
	lv_obj_set_style_pad_all(slices_container, 0, 0);
	lv_obj_set_style_margin_all(slices_container, 0, 0);
	lv_obj_set_style_border_width(slices_container, 0, 0);
	lv_obj_set_style_border_color(slices_container, lv_color_hex(0x00FF00), 0);
	lv_obj_set_style_bg_opa(slices_container, LV_OPA_TRANSP, 0);
	lv_obj_remove_flag(slices_container, LV_OBJ_FLAG_SCROLLABLE);

	/* create segments */
	angle_accum = 0.0f;
	for (int i = 0; i < segments; ++i) {
		create_segment(slices_container, segments);
	}

	return root;
}

Screenshot and/or video

Current radial menu behavior

Environment

  • MCU/MPU/Board: WaveShare ESP32-C6 1.47inch Touch Display
  • LVGL version: v9.3

After reading the documentation more closely I believe the issue is related to how lv_arc has tolerance added if LV_OBJ_FLAG_ADV_HITTEST is enabled. A tolerance of lv_dpx(50) pixels is applied to each angle, extending the hit-test range along the Arc’s length. This is causing overlap of each Arc segments.

I will attempt to create my own hit test because I don’t believe LVGL currently allows me to override/remove the added tolerances on an Arc that has the flag LV_OBJ_FLAG_ADV_HITTEST.