Difference between revisions of "Getting started with Yolk"

From Dooba Wiki
Jump to navigation Jump to search
 
Line 441: Line 441:
 
* <syntaxhighlight lang="c" inline>uint8_t yolk_file_browser_compute_path(struct yolk_file_browser *b, char *path, uint8_t index, char *buf, uint8_t *len, uint8_t *otype);</syntaxhighlight>
 
* <syntaxhighlight lang="c" inline>uint8_t yolk_file_browser_compute_path(struct yolk_file_browser *b, char *path, uint8_t index, char *buf, uint8_t *len, uint8_t *otype);</syntaxhighlight>
  
This will compute the complete path for element '''index''' within '''path''' and place the resulting string in '''buf''' and the length of the string in '''len'''. The '''buf''' argument must point to a buffer of at least '''VFS_PATH_MAXLEN''' bytes - 100 by default). The type of the resulting file system object (file or directory) will be placed in '''otype'''.
+
This will compute the complete path for element '''index''' within '''path''' and place the resulting string in '''buf''' and the length of the string in '''len'''. The '''buf''' argument must point to a buffer of at least '''VFS_PATH_MAXLEN''' bytes - 150 by default). The type of the resulting file system object (file or directory) will be placed in '''otype'''.
  
 
The possible object types are defined in [[VFS_basics#Object_types_.28OTYPE.29|VFS - Object types]].
 
The possible object types are defined in [[VFS_basics#Object_types_.28OTYPE.29|VFS - Object types]].

Latest revision as of 17:13, 10 August 2019

Introduction

Yolk is a GUI (Graphical User Interface) framework. It is tailored for microcontroller-based embedded systems.

Having color TFT displays or even small OLED displays opens up the possibility of presenting elegant user interfaces, rather than just blinking a few LEDs.

Built on top of the Graphics Framework, the Yolk UI framework allows us to very quickly build some simple interfaces based on 'components' such as menus or dialogs.

How it works

At the very least, Yolk requires a generic display through which the GUI will be presented.

Visually, the UI will be rendered according to the currently configured style.

Because some displays include a backlight, Yolk also provides automatic backlight management.

A typical application would then invoke components - typical UI "views" such as menus or dialogs - to actually present information to the user. These components then trigger callbacks when the user interacts with them.

Plugins can be attached / detached at any moment to provide additional features, whether visual, logical, or both. Plugins are automatically updated and drawn by Yolk.

To interact with the GUI, an input layer can be added.

Drawing

At every iteration of the main loop (everytime 'yolk_update' is called - either directly by the user or automatically through the substrate), the main drawing routine is run:

  • Draw background color/image
  • Draw active component (calling 'draw' method of component)
  • Draw attached plugins (calling 'draw' method of plugins)

Input

At every iteration of the main loop, Yolk also uses the input layer to get the current state of all inputs, packed together into a 32-bit integer (note: this means that we can not have more than 32 individual inputs).

This 32-bit 'input state' value is actually not a direct immediate image of the input at a given moment, but a "cleaned-up" version. Indeed, Yolk takes care of de-bouncing the input bits and handling re-triggering.

Pressing a button for a long time will not produce a constant "on" value, but a series of equally-spaced individual pulses.

Using the library

As always, we will need to add the 'yolk' library to our dependencies in dfe.conf:

name: example_app
type: app
mmcu: atmega1284p
freq: 10000000
deps:
  - yolk

Minimal initialization

As a minimum, Yolk requires a display (struct gfx_dsp *) to work with.

Let's start with a basic example using some substrate (assuming a 'dsp0' display is available):

1 # Use Yolk UI
2 uses :yolk, name: 'ui0', dsp: 'dsp0', style: :tinymono

Notice how we need to specify a visual style for our UI (:tinymono in this case).

Basic styles are provided that fit the majority of available displays:

  • :tinymono - A minimalist style for small OLED displays with resolutions around 128x64
  • :tftbasic - A simple style for medium-sized TFT LCD displays with resolutions around 128x160

Component Data Size

Components usually need to store some data - whether it's items for a menu or text for a simple dialog.

Yolk provides some buffer space to components to store their data. Although it's set to 1024 bytes by default, we can change that and decide how much space we want to allocate for component data.

Why does it matter?

While the default size of 1024 bytes is generally a good fit for most applications, some specific cases may have different requirements.

For example, if we really need to save on memory and can live with fewer menu items, reducing this value to maybe 256 or 128 bytes can be an interesting option.

There might also be cases when we really need a lot of space for some custom component, and actually need to increase that value up to maybe 2048 bytes.

To change the component data size, we just need to specify it in our substrate:

1 # Use Yolk UI
2 uses :yolk, name: 'ui0', dsp: 'dsp0', style: :tinymono, cdata_size: 256

The default value (1024 bytes) can accommodate for at least 20 menu items (can be a lot more depending on the lengths of item names), or around 1000 characters of text (some bytes are used to store button info and title) for a typical dialog.

General layout

Yolk keeps things simple yet flexible.

One component can be active at any moment, and is allocated a region of the display called the 'component area'.

Here is an example of a typical 128x160 TFT color display showing a menu:

Yolk layout simple.png

By default, this 'component area' is the entire display. The application can change the height and the starting Y offset of this area, but not the width.

This can be useful to add overlays like status bars around the component area.

Below is an example similar to what we saw above, but with the addition of a status bar above the component area:

Yolk layout sbar.png

While components may make use this area however they wish, the general recommendation is to stick to the same structure:

  • Component head containing an icon and title.
  • Component body

To make it easier to follow this pattern, Yolk provides void yolk_ui_draw_component_head(struct yolk *yk, uint16_t icon, char *title, uint16_t title_len);, which automatically draws this header based on the current style.

We can then use the macros below to get the body area:

  • yolk_ui_body_x(yk)
  • yolk_ui_body_y(yk)
  • yolk_ui_body_w(yk)
  • yolk_ui_body_h(yk)

Note: while the component area's width is always the full width of the display, the X and W macros above should be used when designing custom components as they take margins into account.

Components

Components are really the business end of the Yolk UI framework. They are the "views" that a user will see and interact with.

Only one component can be "active" at any moment. Yolk will update and draw the component at every iteration of the main loop. Yolk will also re-enter the component if the display configuration changes (such as when rotating the display orientation), allowing the component to "re-organise" itself if needed.

A component can therefore implement up to three methods:

  • void update(struct yolk *yk, uint32_t input, void *data, uint16_t data_size);
  • void draw(struct yolk *yk, void *data, uint16_t data_size);
  • void reenter(struct yolk *yk, void *data, uint16_t data_size);

Typically, a component would also provide at the very least an 'enter' method, which would take care of initializing itself with any parameters provided and tell Yolk to use it as the active component.

The 'update' method

At every iteration of the main loop, Yolk calls the 'update' method of whatever component is active. It passes in the pointer and size of the component data buffer.

The component's 'update' method can use the component data buffer however it needs, but must be careful not to overflow it (duh).

Within this method the component should check the current input state and update itself (internal states) accordingly.

The 'draw' method

As part of the main draw routine executed once per iteration of the main loop, Yolk will call the 'draw' method of whatever component is active. Just like the 'update' method, it passes in the pointer and size of the component data buffer.

The draw method should be fairly straightforward, and always draw everything for the component. Indeed, because Yolk makes use of the invalidation stack, the Graphics Framework will take care of filtering what actually needs to be re-drawn according to what has been invalidated.

The 'reenter' method

When the display configuration changes, any component that saves layout info (positions / offsets / sizes) should be allowed to "re-organize" itself - basically re-calculate any saved value that may need to be changed according to the new display configuration. The main example of this is when rotating the orientation of the display.

Whenever something changes in the display configuration, Yolk will call the 'reenter' method on the active component to allow it to re-calculate anything it needs.

Example 'counter' component

Below is a bare-bones example of a custom component that increases a counter and displays its value, but only refreshing the display every 1000 steps:

 1 #include <gfx/txt.h>
 2 #include <yolk/yolk.h>
 3 #include <yolk/ui.h>
 4 #include <yolk/icons.h>
 5 
 6 // Title for our component
 7 #define EXAMPLE_COMPONENT_TITLE    "Counter"
 8 
 9 // Example component data structure
10 struct example_component_data
11 {
12     // Counter
13     uint16_t counter;
14 };
15 
16 // This method draws our component
17 void example_component_draw(struct yolk *yk, struct yolk_dialog_data *data, uint16_t data_size)
18 {
19     // Draw a standard component head
20     yolk_ui_draw_component_head(yk, YOLK_ICON_SYSTEM, EXAMPLE_COMPONENT_TITLE, strlen(EXAMPLE_COMPONENT_TITLE));
21 
22     // Draw our counter to the component area
23     gfx_txt_f(yk->dsp, yolk_ui_body_x(yk), yolk_ui_body_y(yk), yolk_ui_body_w(yk), yolk_ui_body_h(yk), yolk_txt_font(yk), yolk_txt_col(yk), "C: %i", data->counter);
24 }
25 
26 // This method takes care of increasing the counter and invalidating the display every 1000 steps
27 void example_component_update(struct yolk *yk, struct yolk_dialog_data *data, uint16_t data_size)
28 {
29     // Increment counter
30     data->counter = data->counter + 1;
31 
32     // Invalidate counter display every 1000 steps
33     if(data->counter % 1000)    { gfx_dsp_invalidate(yk->dsp, yolk_ui_body_x(yk), yolk_ui_body_y(yk), yolk_ui_body_w(yk), yolk_txt_font(yk)->fh); }
34 }
35 
36 // Enter (make our component active)
37 void example_component_enter(struct yolk *yk)
38 {
39     struct example_component_data *data;
40 
41     // Initialize our component's data
42     data = (struct example_component_data *)(yk->component_data);
43     data->counter = 0;
44 
45     // Invalidate UI Component Area
46     yolk_ui_invalidate_component(yk);
47 
48     // Set Active Component
49     yolk_ui_set_component(yk, (yolk_component_cb_t)example_component_update, (yolk_component_cb_t)example_component_draw);
50 }

There are a few things to mention here.

First, as we saw earlier, Yolk provides a component data buffer so components can store their internal data.

This is accessible through yk->component_data, and is typically used to store a custom structure, such as struct example_component_data in our example.

When we invalidate the counter display every 1000 steps, while we could simply just invalidate the entire component area by calling yolk_ui_invalidate_component(yk); (like in the 'enter' method), it is actually better to invalidate less, to avoid re-drawing things we don't need. The only thing that is changing here is the counter's text, for which we should ideally calculate the exact width and height.

For the purposes of this simple example however, we can simplify this by thinking that the counter's text will never span more than one line. This is why we invalidate the full width of the component area (yolk_ui_body_w(yk)), but only the character height of the text font (yolk_txt_font(yk)->fh).

An important detail here is that our component data structure (struct example_component_data) is ridiculously small (it only contains a two-byte integer). As such, we don't need to worry about the size of Yolk's component area. If our component needed to store more information, we would have to watch out for potential overflows.

Built-in components

Yolk includes some built-in components, let's go over each of those and see what we can do with them.

Dialog

Dialog is the simplest component to understand and use. It allows displaying a simple message to the user.

To open a dialog, we can use void yolk_dialog_enter(struct yolk *yk, yolk_dialog_cb_t update_cb, void *user, uint16_t icon, char *title_fmt, char *text_fmt, ...);.

Example:

 1 #include <yolk/icons.h>
 2 #include <yolk/components/dialog.h>
 3 
 4 void dialog_update_cb(struct yolk *yk, void *user)
 5 {
 6     // Detect Button Press
 7     if((yolk_in_btn_ok(yk) || yolk_in_center(yk))    { yolk_dialog_enter(yk, 0, 0, YOLK_ICON_OK, "Thank you!", "You pressed a button..."); }
 8 }
 9 
10 void init()
11 {
12     // ...
13 
14     // Display Dialog
15     yolk_dialog_enter(ui0, dialog_update_cb, 0, YOLK_ICON_SYSTEM, "Hello, %t!", "This is a %s dialog.", "world", strlen("world"), "nice");
16 
17     // ...
18 }

The 'update_cb' callback is defined as such: void update_cb(struct yolk *yk, void *user);. If defined, this callback will be run at every iteration of the main loop as long as this dialog is the active component.

The 'user' parameter will be passed to the 'update_cb' if defined.

At any moment, we can change the text being displayed by calling void yolk_dialog_set_text(struct yolk *yk, char *fmt, ...);.

Standard Action Buttons

The dialog component can also be created with "standard action buttons". These are buttons like "Ok", "Cancel", "Yes", or "No".

This feature allows displaying these buttons below the dialog text. This also simplifies triggering actions for standard keys.

Instead of using 'yolk_dialog_enter' as before, we can use void yolk_dialog_enter_std_btn(struct yolk *yk, yolk_dialog_cb_t update_cb, uint8_t btn, yolk_dialog_cb_t action_1, yolk_dialog_cb_t action_2, void *user, uint16_t icon, char *title_fmt, char *text_fmt, ...); to enter a dialog with standard buttons and associated actions.

The btn parameter can be any of:

  • YOLK_DIALOG_STD_BTN_NONE
  • YOLK_DIALOG_STD_BTN_YES_NO
  • YOLK_DIALOG_STD_BTN_OK_CANCEL
  • YOLK_DIALOG_STD_BTN_OK

The action1 and action2 parameters are callbacks that will be triggered when the user triggers the matching default action.

Menu

Menus are simple lists of items that can be scrolled through by the user, where each item can have an icon, a title, and an 32-bit integer value.

Menus also have a callback that will be run whenever a menu event occurs. The callback signature is void menu_event_cb(struct yolk *yk, void *user, uint8_t event);. That event will be passed to the menu callback.

Below is the list of possible events:

  • YOLK_MENU_EVENT_UP moving up
  • YOLK_MENU_EVENT_DOWN moving down
  • YOLK_MENU_EVENT_SELECT selecting
  • YOLK_MENU_EVENT_BACK going back
  • YOLK_MENU_EVENT_OPTION pressing the 'option' button
  • YOLK_MENU_EVENT_CANCEL pressing the 'cancel' button
  • YOLK_MENU_EVENT_RIGHT pressing the 'right' button

Creating a menu happens in two steps: first we must create the menu, then we can add items to it.

We can use void yolk_menu_enter(struct yolk *yk, yolk_menu_event_cb_t event_cb, void *user, uint16_t icon, char *title_fmt, ...);.

As always, the 'user' parameter given when creating the menu will be passed back to the callback whenever it is run.

Items can then be added by calling void yolk_menu_add_item(struct yolk *yk, uint32_t value, uint16_t icon, char *fmt, ...);.

Example:

 1 #include <yolk/icons.h>
 2 #include <yolk/components/dialog.h>
 3 #include <yolk/components/menu.h>
 4 
 5 void menu_event_cb(struct yolk *yk, void *user, uint8_t event)
 6 {
 7     uint32_t v;
 8 
 9     // Only react to 'select' event
10     if(event != YOLK_MENU_EVENT_SELECT)    { return; }
11 
12     // Describe what was selected
13     v = yolk_menu_selected_item_value(yk);
14     yolk_dialog_enter(yk, 0, 0, YOLK_ICON_OK, "Thank you!", "You selected: %I (%H)", v, v);
15 }
16 
17 void init()
18 {
19     // ...
20 
21     // Create Menu
22     yolk_menu_enter(ui0, menu_event_cb, 0, YOLK_ICON_SYSTEM, "Simple menu");
23 
24     // Add some items
25     yolk_menu_add_item(ui0, 99, YOLK_ICON_FILE, "Hello!");
26     yolk_menu_add_item(ui0, 42, YOLK_ICON_FILE, "Foobar");
27     yolk_menu_add_item(ui0, 0xffffffff, YOLK_ICON_FILE, "Blabla");
28 
29     // ...
30 }

Whenever we want to get the currently selected item, we can use yolk_menu_selected_item(yk). To get the value, we can do yolk_menu_selected_item(yk)->value or directly yolk_menu_selected_item_value(yk).

Spinwait

The spinwait is a general waiting screen. It is similar to the dialog component presented earlier in many ways. It allows displaying a simple message to the user along with a spinner, and even a progress bar if needed (by passing 1 for uint8_t show_progress when calling 'yolk_spinwait_enter').

To open a dialog, we can use void yolk_spinwait_enter(struct yolk *yk, yolk_spinwait_update_cb_t update_cb, void *user, uint16_t icon, uint8_t show_progress, char *title_fmt, char *text_fmt, ...);.

Example:

 1 #include <yolk/icons.h>
 2 #include <yolk/components/dialog.h>
 3 #include <yolk/components/spinwait.h>
 4 
 5 void spinwait_update_cb(struct yolk *yk, void *user)
 6 {
 7     // Detect Button Press
 8     if((yolk_in_btn_ok(yk) || yolk_in_center(yk))    { yolk_dialog_enter(yk, 0, 0, YOLK_ICON_ERROR, "Hey!", "I told you to wait!"); }
 9 }
10 
11 void init()
12 {
13     // ...
14 
15     // Display Spinwait
16     yolk_spinwait_enter(ui0, spinwait_update_cb, 0, YOLK_ICON_SYSTEM, 0, "Please wait", "Computing some stuff...");
17 
18     // ...
19 }

The 'update_cb' callback is defined as such: void update_cb(struct yolk *yk, void *user);. It works just like for the dialog component. If defined, this callback will be run at every iteration of the main loop as long as this spinwait is the active component.

The 'user' parameter will be passed to the 'update_cb' if defined.

At any moment, we can change the text being displayed by calling void yolk_spinwait_set_text(struct yolk *yk, char *fmt, ...);.

We can also set the progress percentage by calling void yolk_spinwait_set_progress(struct yolk *yk, uint8_t progress);.

Finally, we can change the visibility of the progress bar with void yolk_spinwait_set_progress_visible(struct yolk *yk, uint8_t visible);.

Utilities

Utilities combine ui components to provide more advanced features.

File browser

Yolk includes a basic file browser utility. This can be used for many applications, let's see how it works.

A struct yolk_file_browser object is used to store state information.

The file browser behaves somewhat similarly to a basic component - it provides a yolk_file_browser_enter method to enter the file browser and then calls a callback whenever an event is triggered. It relies on the menu component internally and this is why it operates almost the same way.

The basic entry methods is:

  • uint8_t yolk_file_browser_enter(struct yolk_file_browser *b, struct yolk *yk, char *root, char *ext, yolk_file_browser_event_cb_t event_cb, void *user, uint16_t icon, char *title_fmt, ...);

A variant is available for when the 'root' parameter is not a NULL-terminated string, and therefore needs the ability to be given a 'root_len' argument:

  • uint8_t yolk_file_browser_enter_n(struct yolk_file_browser *b, struct yolk *yk, char *root, uint8_t root_len, char *ext, uint8_t ext_len, yolk_file_browser_event_cb_t event_cb, void *user, uint16_t icon, char *title_fmt, ...);

Another variant is also available for when a va_list needs to be passed for the title format string instead of direct arguments (this variant also requires a root_len argument):

  • uint8_t yolk_file_browser_enter_n_v(struct yolk_file_browser *b, struct yolk *yk, char *root, uint8_t root_len, char *ext, uint8_t ext_len, yolk_file_browser_event_cb_t event_cb, void *user, uint16_t icon, char *title_fmt, va_list ap);

These methods enter a file browser through the UI provided in the struct yolk *yk argument, using the supplied struct yolk_file_browser *b to store state information.

They return 0 if the file browser was successfully opened, and 1 if anything went wrong.

The file browser will start browsing at the path designated by the root argument. This should be a valid VFS path (such as "sdc0p0:/foo/bar") or an empty string ("").

The ext argument defines a file extension filter that can be applied to only display certain files (directories are never filtered). This argument must be a string, either an extension filter (such as ".mp3") or an empty string ("").

The yolk_file_browser_event_cb_t event_cb argument is a callback that will be run whenever an event occurs. The user argument will be passed to the event callback every time it is run.

The callback is defined as: void (*yolk_file_browser_event_cb_t)(struct yolk_file_browser *b, struct yolk *yk, void *user, uint8_t event);

Browsing / Reacting to events

Some methods are provided to make it easier to handle events.

To browse the currently selected item:

  • uint8_t yolk_file_browser_browse_selected(struct yolk_file_browser *b);

To browse "back" (up one level in the file system):

  • uint8_t yolk_file_browser_browse_back(struct yolk_file_browser *b);

Computing paths

Some methods are also provided to make it easier to compute paths and browse things programmatically.

To compute the path for the n-th element in a given directory (while respecting the ext filter):

  • uint8_t yolk_file_browser_compute_path(struct yolk_file_browser *b, char *path, uint8_t index, char *buf, uint8_t *len, uint8_t *otype);

This will compute the complete path for element index within path and place the resulting string in buf and the length of the string in len. The buf argument must point to a buffer of at least VFS_PATH_MAXLEN bytes - 150 by default). The type of the resulting file system object (file or directory) will be placed in otype.

The possible object types are defined in VFS - Object types.

Usage

Below is an example of how to use the file browser:

 1 #include <yolk/icons.h>
 2 #include <yolk/components/dialog.h>
 3 #include <yolk/util/file_browser.h>
 4 
 5 // File Browser object
 6 struct yolk_file_browser yfb;
 7 
 8 void fb_event_cb(struct yolk_file_browser *b, struct yolk *yk, void *user, uint8_t event)
 9 {
10     // Check Event
11     if((event == YOLK_MENU_EVENT_SELECT) || (event == YOLK_MENU_EVENT_RIGHT))
12     {
13         // "Select" event - Check whether object is a file or directory
14         if(b->selected_otype == VFS_OTYPE_FILE)         { /* A file was selected! Do something with 'b->selected_path' / 'b->selected_path_len' */ }
15         else
16         {
17             // A directory was selected - try to browse into it
18             if(yolk_file_browser_browse_selected(b))    { yolk_dialog_enter(yk, 0, 0, YOLK_ICON_ERROR, "Error :(", "Failed to browse directory!"); }
19         }
20     }
21     else if(event == YOLK_MENU_EVENT_BACK)
22     {
23         // "Back" event - Try to browse back
24         if(yolk_file_browser_browse_back(b))            { yolk_dialog_enter(yk, 0, 0, YOLK_ICON_ERROR, "Error :(", "Failed to browse back!"); }
25     }
26     else                                                { /* NoOp */ }
27 }
28 
29 void init()
30 {
31     // ...
32 
33     // Enter File Browser (without any extension filter)
34     if(yolk_file_browser_enter(&yfb, ui0, "", ".mp3", fb_event_cb, 0, YOLK_ICON_FOLDER, "My files"))
35     {
36         // Error
37         yolk_dialog_enter(ui0, 0, 0, YOLK_ICON_ERROR, "Error :(", "Failed to open file browser!");
38     }
39 
40     // ...
41 }

Plugins

Plugins are additional features that can be attached to (and detached from) Yolk at any moment.

A plugin is materialized by a struct yolk_plugin object.

Just like components, three callbacks can be defined for a plugin:

  • void update(struct yolk *yk, void *user);
  • void draw(struct yolk *yk, void *user);
  • void reenter(struct yolk *yk, void *user);

These work just the same as components.

Included plugins

Yolk is provided with some basic plugins.

Let's quickly explore the available plugins included.

Status Bar

The status bar plugin, as the name implies, simplifies the process of adding a small overlay either above or below the component area to display some general information.

It is provided through the <yolk/util/sbar.h> header file.

First, we need to allocate a 'yolk_sbar' structure which will hold the information for our status bar. Then, we can 'attach' and 'detach' our plugin by calling:

// Attach Status Bar void yolk_sbar_attach(struct yolk_sbar *sb, struct yolk *yk, uint8_t position); // Detach Status Bar void yolk_sbar_detach(struct yolk_sbar *sb);

The 'position' argument can be either YOLK_SBAR_POS_TOP (put the status bar above the component area) or YOLK_SBAR_POS_BOTTOM (put the status bar below the component area).

By default, the status bar will not actually draw anything. The status bar will take care of resizing the component area and calculating inner offsets for text and icons:

// Status Bar Structure
struct yolk_sbar
{
    // ...

    // Offsets & Size
    uint16_t y;
    uint16_t h;
    uint16_t y_icon;
    uint16_t x_icon_pbar;
    uint16_t x_icon_pwr;
    uint16_t x_pbar;
    uint16_t y_pbar;
    uint16_t w_pbar;
    uint16_t y_text;
    uint16_t x_text_pwr;
    uint16_t w_text_pwr;

    // ...
}
Power icon

The status bar can automatically manage a power icon (battery level & charging status) if we provide two simple callbacks by calling void yolk_sbar_set_pwr_callbacks(struct yolk_sbar *sb, yolk_sbar_get_bat_pct_t get_bat_pct, yolk_sbar_is_charging_t is_charging, void *user);:

#include <yolk/util/sbar.h>

// Status bar storage
struct yolk_sbar example_sbar;

uint8_t example_get_bat_pct(void *user)
{
    // Here we should return the battery percentage
    return 78;
}

uint8_t example_is_charging(void *user)
{
    // Here we should return 1 if battery is charging, 0 otherwise.
    return 0;
}

void init()
{
    // ...

    // Attach status bar above component area
    yolk_sbar_attach(&example_sbar, ui0, YOLK_SBAR_POS_TOP);

    // Configure power callbacks
    yolk_sbar_set_pwr_callbacks(&example_sbar, example_get_bat_pct, example_is_charging, 0);

    // ...
}

The last parameter ('user') to yolk_sbar_set_pwr_callbacks will be passed back to the callbacks (get_bat_pct & is_charging).

Progress bar

The status bar plugin also allows displaying a generic progress bar along with an associated icon. This can be set to display only for a limited time and then disappear automatically after that.

This feature greatly simplifies things like displaying progress of a background task or changes to the output volume of an audio system.

We have two methods to display a progress in the status bar:

// Set Progress (with default short timer)
void yolk_sbar_set_progress(struct yolk_sbar *sb, uint8_t progress, uint16_t icon);

// Set Progress (with timer - in cycles - 0xffff = always on)
void yolk_sbar_set_progress_t(struct yolk_sbar *sb, uint8_t progress, uint16_t icon, uint16_t timer);

Some typical timer values are defined:

  • YOLK_SBAR_PROGRESS_TIMER_SHORT (5000 cycles)
  • YOLK_SBAR_PROGRESS_TIMER_MEDIUM (10000 cycles)
  • YOLK_SBAR_PROGRESS_TIMER_LONG (50000 cycles)
  • YOLK_SBAR_PROGRESS_TIMER_INFINITE (0xffff cycles)

The 'timer' argument can be set to 0xffff (or YOLK_SBAR_PROGRESS_TIMER_INFINITE) to have the progress bar always-on.

Auto-Off

In many applications, it is not desirable to have the display constantly on. Turning off the display after some inactivity timeout greatly reduces battery use.

An "auto-off" plugin is provided to accomplish just that. Simply attach the plugin and set the desired inactivity timeout, and it will take care of turning off the UI after some time (if no user input is detected). It will also turn the display back on as soon as any user input is detected.

The plugin uses the yolk_ui_on(struct yolk *yk) and yolk_ui_off(struct yolk *yk) methods to turn the UI on or off. This means that no user input will be processed while the UI is off.

It is provided through the <yolk/util/auto_off.h> header file.

Two methods are provided to attach the plugin:

  • void yolk_auto_off_attach(struct yolk_auto_off *aoff, struct yolk *yk); sets up a default inactivity timeout of 50000 cycles
  • void yolk_auto_off_attach_t(struct yolk_auto_off *aoff, struct yolk *yk, uint32_t time); allows specifying the desired inactivity timeout.

Example:

#include <yolk/util/auto_off.h>

// Auto-off storage
struct yolk_auto_off example_aoff;

void init()
{
    // ...

    // Attach auto-off plugin with default timeout
    yolk_auto_off_attach(&example_aoff, ui0);

    // ...
}

UI style

As we've seen above, our UI needs a style - but what is that exactly?

A 'style' is a combination of parameters and assets which together define the visual aspect of our user interface:

  • margins
  • colors
  • fonts
  • tilesets
  • wallpaper

Whenever a component (such as menu or dialog) needs to draw itself, it uses the current style defined for the UI.

In the 'most basic' example from the minimal initialization section, we only specified the style as :tinymono, which is a built-in style from Yolk.

Instead, we could also define every aspect of the style ourselves:

 1 # Use Yolk UI
 2 uses :yolk, name: 'ui0', dsp: 'dsp0', style: {
 3     
 4     # Margins
 5     ui_margin: 0,
 6 
 7     # Fonts
 8     font_ttl: :gfx_font_basic6x8_data,
 9     font_txt: :gfx_font_basic6x8_data,
10 
11     # Text Colors
12     col_ttl: 'gfx_col(255, 255, 255)',
13     col_ttl_shadow: 'gfx_col(0, 0, 0)',
14     col_txt: 'gfx_col(255, 255, 255)',
15     col_sel: 'gfx_col(255, 255, 255)',
16     col_csr: 'gfx_col(255, 255, 255)',
17     col_csr_shadow: 'gfx_col(0, 0, 0)',
18 
19     # Tilesets
20     tileset_frame: :gfx_tileset_frame_tinymono_data,
21     tileset_spinner: :gfx_tileset_spinner_tinymono_data,
22     tileset_pbar: :gfx_tileset_pbar_tinymono_data,
23     tileset_icon: :yolk_icon_tileset_tinymono_data,
24 
25     # Solid Black Wallpaper
26     wp: { solid: 'gfx_col(0, 0, 0)' }
27 }

Even better, we can combine both, and "overload" a built-in style with only the specific details we want to change:

 1 # Use Yolk UI
 2 uses :yolk, name: 'ui0', dsp: 'dsp0', style: {
 3     
 4     # Overload :tinymono
 5     base: :tinymono,
 6 
 7     # Use bigger fonts
 8     font_ttl: :gfx_font_basic8x16_data,
 9     font_txt: :gfx_font_basic6x12_data
10 }

Style parameters

Below is the complete list of available style parameters:

Parameter Description Value type Example(s)
ui_margin General margin used between UI elements (in pixels) Numeric ui_margin: 2
font_ttl Font used to draw titles Symbol (reference to program-space data) font_ttl: :gfx_font_basic6x8_data

(See included fonts for built-in fonts provided by the GFX framework)

font_txt Font used to draw general text Symbol (reference to program-space data) font_txt: :gfx_font_basic6x8_data

(See included fonts for built-in fonts provided by the GFX framework)

col_ttl Color value used for titles String (direct C code) col_ttl: 'gfx_col(255, 255, 255)'

col_ttl: 'gfx_col_h(0xffffff)'

col_ttl_shadow Color value used for title shadows String (direct C code) col_ttl_shadow: 'gfx_col(127, 0, 127)'

col_ttl_shadow: 'gfx_col_h(0xff00ff)'

col_txt Color value used for general text String (direct C code) col_txt: 'gfx_col(127, 127, 127)'

col_txt: 'gfx_col_h(0x7f7f7f)'

col_sel Color value used for selections / highlights String (direct C code) col_sel: 'gfx_col(255, 127, 0)'

col_sel: 'gfx_col_h(0xff7f00)'

col_csr Color value used for cursors String (direct C code) col_csr: 'gfx_col(255, 255, 255)'

col_csr: 'gfx_col_h(0xffffff)'

col_csr_shadow Color value used for cursor shadows String (direct C code) col_csr_shadow: 'gfx_col(127, 127, 127)'

col_csr_shadow: 'gfx_col_h(0x7f7f7f)'

tileset_frame Tileset used to draw frames Symbol (reference to program-space data) tileset_frame: :gfx_tileset_frame_tinymono_data

(See included frames for built-in frames provided by the GFX framework)

tileset_spinner Tileset used to draw spinners Symbol (reference to program-space data) tileset_spinner: :gfx_tileset_spinner_tinymono_data

(See included spinners for built-in spinners provided by the GFX framework)

tileset_pbar Tileset used to draw progress bars Symbol (reference to program-space data) tileset_pbar: :gfx_tileset_pbar_tinymono_data

(See included progress bars for built-in progress bars provided by the GFX framework)

tileset_icon Tileset used to draw icons Symbol (reference to program-space data) tileset_icon: :yolk_icon_tileset_tinymono_data
wp Wallpaper to be drawn behind UI components Hash containing exactly one element.
  • :solid (solid color value - direct C code)
  • :file (image file path in VFS)
  • :flash (reference to image in program-space data).
wp: { solid: 'gfx_col(0, 0, 0)' }

wp: { file: 'sdc0p0:/example/wp.img' }

wp: { flash: :some_image_data }

Icons

Icons are packed together in tilesets. This allows loading custom icon sets to create unique styles.

Yolk already includes an icon tileset, available in two versions:

  • yolk_icon_tileset_data
  • yolk_icon_tileset_tinymono_data

Yolk icon tileset.png Yolk icon tileset tinymono.png

The icons are used by generic UI components and therefore need to follow a specific order in the tileset.

The table below defines the mapping:

Icon name Index
YOLK_ICON_NONE 255
YOLK_ICON_NET_NONE 0
YOLK_ICON_NET_HOME 1
YOLK_ICON_NET_ROAMING 2
YOLK_ICON_NET_UNKNOWN 3
YOLK_ICON_NEW_SMS 4
YOLK_ICON_SMS 5
YOLK_ICON_UNREAD 6
YOLK_ICON_RCVD 7
YOLK_ICON_SENT 8
YOLK_ICON_CONTACT 9
YOLK_ICON_CALL 10
YOLK_ICON_CALL_IN 11
YOLK_ICON_CALL_OUT 12
YOLK_ICON_HANGUP 13
YOLK_ICON_VIBRATE 14
YOLK_ICON_SOUND 15
YOLK_ICON_SYSTEM 16
YOLK_ICON_MICROSD 17
YOLK_ICON_MUSIC 18
YOLK_ICON_PLAY 19
YOLK_ICON_PAUSE 20
YOLK_ICON_EDIT 21
YOLK_ICON_CROSS 22
YOLK_ICON_NEW 23
YOLK_ICON_OK 24
YOLK_ICON_ERROR 25
YOLK_ICON_FOLDER 26
YOLK_ICON_FILE 27
YOLK_ICON_DOOBA 28
YOLK_ICON_ARROW_L 29
YOLK_ICON_ARROW_R 30
YOLK_ICON_BATTERY_HIGH 31
YOLK_ICON_BATTERY_MED 32
YOLK_ICON_BATTERY_LOW 33
YOLK_ICON_BATTERY_CHARGING 34

Backlight management

Most displays will include a backlight in the form of one or more LEDs. Yolk will manage the backlight if we tell it how via the substrate.

Two operation modes are available:

  • basic on/off (:dio) simply turns the backlight on or off depending on the UI usage
  • dimming (:pwm) can use PWM to control the intensity of the backlight

Here is an example with :dio mode:

1 # Use Yolk UI
2 uses :yolk, name: 'ui0', dsp: 'dsp0', bklt: { mode: :dio, pin: 3 }

And another example using :pwm mode:

1 # Use Yolk UI
2 uses :yolk, name: 'ui0', dsp: 'dsp0', bklt: { mode: :pwm, pin: 11 }

PWM mode allows setting the default 'on' level ('base') as well as the steps per cycle ('speed'). Default is { base: 255, speed: 1 }.

Here is an example overriding these defaults:

1 # Use Yolk UI
2 uses :yolk, name: 'ui0', dsp: 'dsp0', bklt: { mode: :pwm, pin: 11, base: 128, speed: 5 }

UI Rotation

Yolk allows easily rotating the orientation of the entire user interface.

This includes rotating the display, as well as rotating the bits for generic "up", "down", "left", and "right" inputs.

We can rotate the UI at any moment using the following methods:

  • void yolk_ui_rotate_left(struct yolk *yk);
  • void yolk_ui_rotate_right(struct yolk *yk);

Calling any of these will trigger a UI re-enter - the 'reenter' method will be called on all plugins as well as the active component. The entire display will be invalidated.

Input layer

Most applications featuring a user interface will usually also need to handle user input.

Yolk allows building an input layer through either DIO or MCP23008 I2C I/O expander(s).

Yolk allows us to define how the input state is collected (DIO pins or MCP23008 I2C addresses), as well as mapping between certain input bits and generic buttons such as directions, 'ok', 'cancel', ...

1 # Use Yolk UI
2 uses :yolk, name: 'ui0', dsp: 'dsp0', input:
3 {
4     mask: 0x7f,
5     bit_nums: { up: 0, down: 4, left: 2, right: 3, center: 1 },
6     driver: { type: :ioexp, addrs: [0x04] }
7 }

The :mask parameter defines the bitmask for the input bits (which bits from the 32-bit input state will actually be used).

The :driver parameter is where we configure the actual DIO input pins or MCP23008 expander(s).

When using the :ioexp input driver, we must specify the :addrs parameter as an array of I2C addresses for the MCP23008 I/O expanders we are using.

When using the :dio input driver, we must specify the :pins parameter as an array of DIO pin numbers.

DIO pins or I/O expanders will be concatenated in the order they are specified to produce the 32-bit input state.

Depending on your personal preference, generic controls (such as 'up', 'down', 'left', 'right', etc...) can be mapped to input bits in one of two ways:

  • bit index (:bit_nums parameter)
  • bit mask (:bits parameter)

Example with bit indices:

1 uses :yolk, name: 'ui0', dsp: 'dsp0', input:
2 {
3     mask: 0x7f,
4     bit_nums: { up: 0, down: 4, left: 2, right: 3, center: 1 },
5     driver: { type: :ioexp, addrs: [0x04] }
6 }

Identical example with bit masks:

1 uses :yolk, name: 'ui0', dsp: 'dsp0', input:
2 {
3     mask: 0x7f,
4     bit_nums: { up: 0x00000001, down: 0x00010000, left: 0x00000100, right: 0x00001000, center: 0x00000010 },
5     driver: { type: :ioexp, addrs: [0x04] }
6 }

Below is the list of mappable controls:

  • :up
  • :down
  • :left
  • :right
  • :center (used as the main selection button)
  • :u1 (user button number 1 - mapped to "ok")
  • :u2 (user button number 2 - mapped to "cancel")
  • :n0 to :n9 (numpad buttons 0 to 9)
  • :star (numpad star button)
  • :hash (numpad hash/pound button)

Reading input

Components will need to read the input state to detect user actions.

The input state is stored in 'yolk->input->cstate'. At any moment, we can check this value to handle user input.

Warning! The input state is given in negative logic (active-low). This means that an input bit set to '0' means the corresponding button is pressed.

Shortcuts

The generic macro below allows checking any input bit easily:

  • yolk_in(struct yolk *yk, uint32_t bit)

To simplify checking the input state for generic controls, Yolk provides a set of shortcuts:

  • yolk_in_up(struct yolk *yk)
  • yolk_in_down(struct yolk *yk)
  • yolk_in_left(struct yolk *yk)
  • yolk_in_right(struct yolk *yk)
  • yolk_in_center(struct yolk *yk)
  • yolk_in_ok(struct yolk *yk)
  • yolk_in_cancel(struct yolk *yk)
  • yolk_in_u1(struct yolk *yk)
  • yolk_in_u2(struct yolk *yk)
  • yolk_in_num_0(struct yolk *yk)
  • yolk_in_num_1(struct yolk *yk)
  • yolk_in_num_2(struct yolk *yk)
  • yolk_in_num_3(struct yolk *yk)
  • yolk_in_num_4(struct yolk *yk)
  • yolk_in_num_5(struct yolk *yk)
  • yolk_in_num_6(struct yolk *yk)
  • yolk_in_num_7(struct yolk *yk)
  • yolk_in_num_8(struct yolk *yk)
  • yolk_in_num_9(struct yolk *yk)
  • yolk_in_num_0(struct yolk *yk)
  • yolk_in_num_star(struct yolk *yk)
  • yolk_in_num_hash(struct yolk *yk)