
The Calculator sample demonstrates how to implement a complex view using recursive composition of the standard EFL UI components and containers in a UI component hierarchy. It aims to explain how such an application can be developed using view and model parts.
The sample uses UI components (such as elm_conformant and elm_naviframe) for the view management, containers (such as elm_grid and elm_table) for UI component management inside the view, and UI components (such as elm_bg, elm_button, and elm_label) for the content inside view.
The following figure illustrates the main screen of the Calculator (portrait and landscape), the wireframe structure, and the UI component tree.
Figure: Calculator screen



Implementation
The _on_create_cb() callback function initializes the application model and user interface.
static Evas_Object *_add_naviframe(app_data *app)
{
Evas_Object *result = NULL;
Evas_Object *parent = win_get_host_layout(app->win);
if (parent)
{
result = ui_utils_navi_add(parent, _navi_back_cb, app);
if (result)
{
win_set_layout(app->win, result);
}
}
return result;
}
static bool _on_create_cb(void *user_data)
{
app_data *app = user_data;
RETVM_IF(!app, false, "ad is NULL");
result_type result = RES_OK;
app->model = calc_create(&result);
RETVM_IF(!app->model, false, "calc instance creation failed: %d", result);
app->win = win_create();
RETVM_IF(!app->win, false, "Window creation failed.");
app->navi = _add_naviframe(app);
RETVM_IF(!app->navi, false, "Naviframe creation failed.");
main_view_add(app);
return true;
}
The win_create() function creates the window which consists of the indicator (elm_conformant) and the background with a red color to easily see if the view does not have its own background component.
window_obj *win_create()
{
window_obj *obj = calloc(1, sizeof(window_obj));
RETVM_IF(!obj, NULL, "Cannot allocate memory");
obj->win = elm_win_add(NULL, APP_NAME, ELM_WIN_BASIC);
elm_win_wm_rotation_available_rotations_set(obj->win, AVAILABLE_ROTATIONS, AVAILABLE_ROTATIONS_LENGTH);
elm_win_conformant_set(obj->win, EINA_TRUE);
evas_object_show(obj->win);
obj->conform = elm_conformant_add(obj->win);
evas_object_size_hint_weight_set(obj->conform, EVAS_HINT_EXPAND, EVAS_HINT_EXPAND);
elm_win_resize_object_add(obj->win, obj->conform);
evas_object_show(obj->conform);
obj->bg = elm_bg_add(obj->conform);
elm_bg_color_set(obj->bg, WIN_FAIL_BG_COLOR);
evas_object_size_hint_weight_set(obj->bg, EVAS_HINT_EXPAND, EVAS_HINT_EXPAND);
evas_object_show(obj->bg);
elm_object_part_content_set(obj->conform, "elm.swallow.bg", obj->bg);
return obj;
}
The main_view_add() function creates the main view layout and all its UI components and pushes itself to the naviframe. This function also registers the rotation change callback with the evas_object_smart_callback_add() function, the callback on model display change with the calc_set_display_change_cb() function and the region format change callback by assigning a function pointer and data to the main application structure.
void main_view_add(app_data *app)
{
RETM_IF(!app, "app is NULL");
main_view_data *data = calloc(1, sizeof(main_view_data));
RETM_IF(!data, "Failed to allocate the instance");
result_type result = _main_view_init(data, app);
if (RES_OK != result)
{
_main_view_del(data);
}
}
result_type _main_view_init(main_view_data *data, app_data *app)
{
RETVM_IF(!app->model, RES_ILLEGAL_ARGUMENT, "app->model is NULL");
RETVM_IF(!app->win, RES_ILLEGAL_ARGUMENT, "app->win is NULL");
RETVM_IF(!app->win->win, RES_ILLEGAL_ARGUMENT, "app->win->win is NULL");
RETVM_IF(!app->navi, RES_ILLEGAL_ARGUMENT, "app->navi is NULL");
data->app = app;
data->model = app->model;
data->window = app->win->win;
data->orientation = elm_win_rotation_get(data->window);
result_type result = RES_INTERNAL_ERROR;
result = _main_view_make_layout(data, app->navi);
RETVM_IF(RES_OK != result, result, "Failed to make layout: %d", result);
result = _main_view_make_widgets(data);
RETVM_IF(RES_OK != result, result, "Failed to make widgets: %d", result);
_main_view_repack_widgets(data);
Elm_Object_Item *navi_item = elm_naviframe_item_push(app->navi, STR_MAIN_TITLE, NULL, NULL, data->layout, NULL);
RETVM_IF(!navi_item, RES_OUT_OF_MEMORY, "Failed to push naviframe item");
evas_object_smart_callback_add(data->window, "wm,rotation,changed", _main_view_win_rotate_cb, data);
calc_set_display_change_cb(data->model, _main_view_display_changed_cb, data);
app->region_fmt_change_cb = _main_view_region_fmt_changed_cb;
app->region_fmt_change_cb_data = data;
return RES_OK;
}
The _main_view_make_layout() function creates the main view layout components:
- Table: Holds other UI components and adds border padding around the view
- Grid: Positions all UI components on the view in a scalable manner
- Background: Colors the view with the default background color
Figure: Calculator main view layout

The view object is deleted when the main layout component (table) is deleted by its parent. The callback is registered using the evas_object_event_callback_add() function.
result_type _main_view_make_layout(main_view_data *data, Evas_Object *parent)
{
int scaled_border_padding = (int)utils_round(ELM_SCALE_SIZE(VIEW_BORDER_WIDTH));
// Create a single cell table so the internal grid can support padding
Evas_Object *layout = elm_table_add(parent);
RETVM_IF(!layout, RES_OUT_OF_MEMORY, "Failed to create elm_table");
data->layout = layout;
elm_table_homogeneous_set(layout, EINA_TRUE);
evas_object_size_hint_weight_set(layout, EVAS_HINT_EXPAND, EVAS_HINT_EXPAND);
evas_object_event_callback_add(layout, EVAS_CALLBACK_FREE, _main_view_layout_free_cb, data);
evas_object_show(layout);
// Add a background to the view with the default theme color
Evas_Object *bg = elm_bg_add(layout);
RETVM_IF(!bg, RES_OUT_OF_MEMORY, "Failed to create elm_bg");
elm_table_pack(layout, bg, 0, 0, 1, 1);
evas_object_size_hint_align_set(bg, EVAS_HINT_FILL, EVAS_HINT_FILL);
evas_object_show(bg);
// Create a grid for holding and aligning the UI components
Evas_Object *grid = elm_grid_add(layout);
RETVM_IF(!grid, RES_OUT_OF_MEMORY, "Failed to create elm_grid");
data->grid = grid;
elm_table_pack(layout, grid, 0, 0, 1, 1);
evas_object_size_hint_align_set(grid, EVAS_HINT_FILL, EVAS_HINT_FILL);
evas_object_size_hint_padding_set(grid,
scaled_border_padding, scaled_border_padding,
scaled_border_padding, scaled_border_padding);
evas_object_show(grid);
return RES_OK;
}
The _main_view_make_widgets() function creates all UI components (the display label and buttons), but does not pack them to the grid yet. For each UI component, a separate single cell table is created. This table is needed to add padding around a specific UI component, since the grid does not support padding for its elements.
You can use a single table instead of a grid (without additional single cell tables), but the grid is more stable as it resizes all UI components in a single calculation and does not stretch if the UI components are too large. To handle button events, a smart clicked callback is registered with the evas_object_smart_callback_add() function.
result_type _main_view_make_widgets(main_view_data *data)
{
int scaled_widget_padding = (int)utils_round(ELM_SCALE_SIZE(WIDGET_PADDING));
int scaled_number_padding = (int)utils_round(ELM_SCALE_SIZE(DISP_NUMBER_RIGHT_PADDING));
int btn_idx = 0;
for (int i = 0; i < WIDGETS_COUNT; ++i)
{
const widget_params *params = &WIDGET_PARAMS[i];
// For each UI component in the grid cell, create an additional single cell table
// This allows adding paddings for UI components because grid does not support paddings
Evas_Object *table = elm_table_add(data->grid);
RETVM_IF(!table, RES_OUT_OF_MEMORY, "Failed to create elm_table");
data->widgets[i] = table; // Registering the UI component in the array to pack it to the grid later
elm_table_homogeneous_set(table, EINA_TRUE);
evas_object_show(table);
bool is_button = (CALC_KEY_NONE != params->key);
Evas_Object *obj = is_button ? elm_button_add(table) : elm_label_add(table);
RETVM_IF(!obj, RES_OUT_OF_MEMORY, "Failed to create widget object");
elm_table_pack(table, obj, 0, 0, 1, 1);
if (is_button)
{
RETVM_IF(BUTTONS_COUNT == btn_idx, RES_INTERNAL_ERROR, "Too many buttons in params!");
main_view_btn_data *btn_data = &data->btns_data[btn_idx];
++btn_idx;
btn_data->view = data;
btn_data->key = params->key;
evas_object_smart_callback_add(obj, "clicked", _main_view_btn_click_cb, btn_data);
int text_size = (int)utils_round(ELM_SCALE_SIZE(((CALC_KEY_ERASE == params->key) ? ERASE_BTN_TEXT_SIZE : GENERAL_BTN_TEXT_SIZE)));
char buff[DISP_STR_SIZE] = {'\0'};
if (CALC_KEY_DEC == params->key)
{
snprintf(buff, DISP_STR_SIZE, params->text, text_size, calc_get_dec_point_str(data->model));
}
else
{
snprintf(buff, DISP_STR_SIZE, params->text, text_size);
}
elm_object_text_set(obj, buff);
evas_object_size_hint_align_set(obj, EVAS_HINT_FILL, EVAS_HINT_FILL);
evas_object_size_hint_padding_set(obj,
scaled_widget_padding, scaled_widget_padding,
scaled_widget_padding, scaled_widget_padding);
}
else
{
data->label = obj;
_main_view_update_disp_label(data);
evas_object_size_hint_align_set(obj, 1, EO_ALIGN_HINT_CENTER);
evas_object_size_hint_padding_set(obj,
scaled_widget_padding, scaled_widget_padding + scaled_number_padding,
scaled_widget_padding, scaled_widget_padding);
}
evas_object_show(obj);
}
return RES_OK;
}
The _main_view_repack_widgets() function clears the grid from UI components (if any) and packs it again according to the current screen orientation.
void _main_view_repack_widgets(main_view_data *data)
{
// Remove all UI components from the grid without deleting
elm_grid_clear(data->grid, EINA_FALSE);
// Obtain the UI component position parameters and resize the grid according to the orientation
const widget_pos *pos_array = NULL;
if ((((abs(data->orientation) + CIRCLE_HALF_QUARTER_DEG) / CIRCLE_QUARTER_DEG) & 1) == 0)
{
pos_array = PORT_WPOS;
elm_grid_size_set(data->grid, PORT_GRID_W, PORT_GRID_H);
}
else
{
pos_array = LAND_WPOS;
elm_grid_size_set(data->grid, LAND_GRID_W, LAND_GRID_H);
}
// Packing UI components to the grid using information from the array
for (int i = 0; i < WIDGETS_COUNT; ++i)
{
elm_grid_pack(data->grid, data->widgets[i],
pos_array[i].x, pos_array[i].y,
pos_array[i].w, pos_array[i].h);
}
}
The _main_view_layout_free_cb() function removes the view object from the memory when the main layout component is deleted by the parent.
void _main_view_layout_free_cb(void *data, Evas *e, Evas_Object *obj, void *event_info)
{
main_view_data *view_data = data;
// NULL the layout so you do not delete it twice in _main_view_del()
view_data->layout = NULL;
// Delete self when layout is deleted by parent object
_main_view_del(view_data);
}
The _main_view_display_changed_cb() function handles the model display change event and updates the display label. The current display value is retrieved from the model using the calc_get_display_str() function. The label is updated with the elm_object_text_set() function.
void _main_view_display_changed_cb(void *data)
{
_main_view_update_disp_label((main_view_data *)data);
}
void _main_view_update_disp_label(main_view_data *data)
{
RETM_IF(!data->label, "label is NULL");
int text_size = (int)utils_round(ELM_SCALE_SIZE(DISP_NUMBER_TEXT_SIZE));
char buff[DISP_STR_SIZE] = {'\0'};
snprintf(buff, DISP_STR_SIZE, "<font_size=%d>%s</font_size>",
text_size, calc_get_display_str(data->model));
elm_object_text_set(data->label, buff);
}
The _main_view_region_fmt_changed_cb() function handles the device region format change event. Before the view update, call the calc_update_region_fmt() function to update the model. After this, the decimal point button text can be updated using the result of the calc_get_dec_point_str() function.
void _main_view_region_fmt_changed_cb(void *data)
{
_main_view_update_region_fmt((main_view_data *)data);
}
void _main_view_update_region_fmt(main_view_data *data)
{
RETM_IF(!data->dec_btn, "dec_btn is NULL");
calc_update_region_fmt(data->model);
int text_size = (int)utils_round(ELM_SCALE_SIZE(GENERAL_BTN_TEXT_SIZE));
char buff[DISP_STR_SIZE] = {'\0'};
snprintf(buff, DISP_STR_SIZE, "<font_size=%d>%s</font_size>",
text_size, calc_get_dec_point_str(data->model));
elm_object_text_set(data->dec_btn, buff);
}
The _main_view_btn_click_cb() function handles the button clock event. The function is implemented mostly by calling the calc_handle_key_press() model function. The model method can generate the display change event described earlier.
void _main_view_btn_click_cb(void *data, Evas_Object *obj, void *event_info)
{
main_view_btn_data *btn_data = data;
_main_view_handle_key_press(btn_data->view, btn_data->key);
}
void _main_view_handle_key_press(main_view_data *data, key_type key)
{
result_type result = calc_handle_key_press(data->model, key);
if (RES_MAX_DIGITS_REACHED == result)
{
char msg[DISP_STR_SIZE] = {'\0'};
snprintf(msg, DISP_STR_SIZE, "<p align=center>"STR_POPUP_MAX_INPUT"</p>", DISP_MAX_DIGITS);
_main_view_create_msg_popup(data, msg);
}
}
The computation logic of the application is implemented in a separate calc.c module. Before using the module, create the calc object using the calc_create() function. The calc_handle_key_press() function handles a specific calculator key specified by the key_type enumeration.
Handling keys usually changes the calculator display string, which the calc_get_display_str() function can retrieve.
To listen on display string change events, the application can register a callback using the calc_set_display_change_cb() function. By registering this callback, the view can avoid manual checking of the display string value after each key handling.
// Create and initialize a calc object instance calc *calc_create(result_type *result); // Destroy and free the calc instance referenced void calc_destroy(calc *obj); // Register the callback to be invoked on display string change void calc_set_display_change_cb(calc *obj, notify_cb cb, void *cb_data); // Get current display string message const char *calc_get_display_str(calc *obj); // Handle calculator key press result_type calc_handle_key_press(calc *obj, key_type key); // Update region format dependent properties (decimal point character) void calc_update_region_fmt(calc *obj); // Get current decimal point character used by the object const char *calc_get_dec_point_str(calc *obj);
The _calc structure in the calc.c module defines the calc object structure. A pointer to this structure is passed to all functions of this module.
// Internal data structure of the calc object
struct _calc
{
// Error flag to indicate critical failure and block computations until reset
bool error;
// Major state variables of the calculator
bool dirty; // In the middle of expression calculation
bool num_is_new; // Indicates that number in num register is new (entered by user of by unary function)
bool in_edit_mode; // Indicates that the user is entering number to the display
key_type op; // Current operation to apply to operands
// Calculator registers used in the computations
double num; // Corresponds to the number on display (not actual in edit mode)
double acc; // Left operand of the operation, stores the result of the operation
double rep; // Special repeat register, used to repeat last operation on result key
// Display object to handle I/O
display disp;
// 1 char string to hold current decimal point character
char dec_point_str[CALC_DEC_POINT_STR_SIZE];
};
The following example shows how to implement the calc.c module functions:
result_type calc_handle_key_press(calc *obj, key_type key)
{
RETVM_IF(!obj, RES_INTERNAL_ERROR, "obj is NULL");
if (CALC_KEY_RESET == key)
{
_calc_reset(obj);
return RES_OK;
}
RETVM_IF(obj->error, RES_ILLEGAL_STATE, "Calculator is in error state");
result_type result = RES_INTERNAL_ERROR;
switch (key & CALC_KEY_TYPE_MASK)
{
case CALC_KEY_TYPE_NUM:
result = _calc_handle_num_key(obj, key);
break;
case CALC_KEY_TYPE_OP:
result = _calc_handle_op_key(obj, key);
break;
default:
result = _calc_handle_etc_key(obj, key);
break;
}
if (RES_OK != result)
{
_calc_handle_error(obj, result);
return result;
}
return RES_OK;
}
result_type _calc_handle_etc_key(calc *obj, key_type key)
{
switch (key)
{
case CALC_KEY_RESULT:
return _calc_handle_result_key(obj);
case CALC_KEY_SIGN:
return _calc_handle_sign_key(obj);
case CALC_KEY_ERASE:
return _calc_handle_erase_key(obj);
default:
return RES_ILLEGAL_ARGUMENT;
}
}
result_type _calc_handle_num_key(calc *obj, key_type key)
{
bool reset = false;
if (!obj->in_edit_mode)
{
obj->in_edit_mode = true;
reset = true;
}
return display_enter_key(&obj->disp, key, reset);
}
result_type _calc_handle_op_key(calc *obj, key_type key)
{
switch (key)
{
case CALC_KEY_ADD:
case CALC_KEY_SUB:
case CALC_KEY_MUL:
case CALC_KEY_DIV:
break;
default:
return RES_ILLEGAL_ARGUMENT;
}
_calc_end_edit_mode(obj);
result_type result = RES_OK;
if (!obj->dirty)
{
obj->dirty = true;
obj->acc = obj->num;
}
else if (obj->num_is_new)
{
result = _calc_perform_op(obj, obj->num);
}
obj->num_is_new = false;
obj->op = key;
return result;
}
result_type _calc_handle_result_key(calc *obj)
{
_calc_end_edit_mode(obj);
if (obj->dirty)
{
obj->dirty = false;
obj->rep = obj->num;
}
else if (obj->num_is_new)
{
obj->acc = obj->num;
}
obj->num_is_new = false;
return _calc_perform_op(obj, obj->rep);
}
result_type _calc_handle_sign_key(calc *obj)
{
if (obj->in_edit_mode)
{
display_negate(&obj->disp);
}
else
{
_calc_perform_negate_func(obj);
}
return RES_OK;
}
result_type _calc_handle_erase_key(calc *obj)
{
RETVM_IF(!obj->in_edit_mode, RES_ILLEGAL_STATE, "Not in edit mode");
return display_erase(&obj->disp);
}
void _calc_end_edit_mode(calc *obj)
{
if (obj->in_edit_mode)
{
obj->in_edit_mode = false;
obj->num = display_get_number(&obj->disp);
obj->num_is_new = true;
// This removes extra chars from the current display (for example: 2.300 -> 2.3; -0.0 -> 0)
display_set_number(&obj->disp, obj->num);
}
}
result_type _calc_perform_op(calc *obj, double rv)
{
switch (obj->op)
{
case CALC_KEY_ADD:
obj->acc += rv;
break;
case CALC_KEY_SUB:
obj->acc -= rv;
break;
case CALC_KEY_MUL:
obj->acc *= rv;
break;
case CALC_KEY_DIV:
if (fabs(rv) <= CALC_ZERO_EPS)
{
return ((fabs(obj->acc) <= CALC_ZERO_EPS) ? RES_UNDEFINED_VALUE : RES_DIVISION_BY_ZERO);
}
obj->acc /= rv;
break;
default:
return RES_INTERNAL_ERROR;
}
result_type result = display_set_number(&obj->disp, obj->acc);
RETVM_IF(RES_OK != result, result, "Failed to set display number: f", obj->acc);
// Get number back from display to get rounded value
obj->num = display_get_number(&obj->disp);
obj->acc = obj->num;
return RES_OK;
}
void _calc_perform_negate_func(calc *obj)
{
if (0.0 != obj->num)
{
obj->num = -obj->num;
(void)display_set_number(&obj->disp, obj->num);
}
obj->num_is_new = true;
}
The disp.c module handles user number input and rounds calculation results in a string representation. This module contains the following functions:
- display_reset() function resets the display to the initial state (0).
- display_set_number() function is used to set a number to the display after the computations in the model. The number is rounded and converted to a string.
- display_get_number() function converts a string number on the display to a double value.
- display_enter_key() and display_erase() functions are used to edit a number in the display.
- display_negate() function adds or removes a negative sign on the non-zero number on the display. If the number is zero, this function does nothing.
- display_set_str() function is used in special cases to set an error message to the display of the calculator. After this function, the display must be reset to work properly.
// Data structure of the display object
typedef struct
{
char str[DISP_STR_SIZE]; // String buffer of the display
int len; // Length of the string in the buffer
int dig_count; // Count of decimal digits in the string
bool is_neg; // Is number in the string negative
bool has_dec_point; // Does number in the string contain decimal point
notify_cb change_cb; // Callback to be invoked on display string change
void *change_cb_data; // Data pointer for the callback
char dec_point_char; // Current decimal point character used by the display
} display;
// Reset display to "0"
void display_reset(display *obj);
// Set new decimal pointer character to use
void display_set_dec_point_char(display *obj, char new_dec_point_char);
// Set double value number to the display
result_type display_set_number(display *obj, double value);
// Get number from display as double value
double display_get_number(display *obj);
// Process number key category to enter new characters to display
result_type display_enter_key(display *obj, key_type key, bool reset);
// Erase last entered character
result_type display_erase(display *obj);
// Negate number on display
void display_negate(display *obj);
// Set string message to display
void display_set_str(display *obj, const char *new_str);