List design

The goal of this list design document is to establish an appropriate architecture and API design for the list widgets in the Apertis platform.

Historically, the roller widget has provided a list widget on a cylinder with no conceptual beginning or end and is manipulated naturally by the user. For non-cylindrical lists there was a separate widget with a different API and different usage. The goal is to consolidate list operations into a base class and be able to use the same simple API to use both cylindrical lists and non-cylindrical lists.

The above shows an example of the roller widget in use inside the music application. There are multiple roller widgets for showing album, artist, and song. Although they are manipulated independently, their contents are linked.

Terminology and concepts

Vehicle

For the purposes of this document, a vehicle may be a car, car trailer, motorbike, bus, truck tractor, truck trailer, agricultural tractor, or agricultural trailer, amongst other things.

System

The system is the infotainment computer in its entirety in place inside the vehicle.

User

The user is the person using the system, be it the driver of the vehicle or a passenger in the vehicle.

Widget

A widget is a reusable part of the user interface which can be changed depending on location and function.

User interface

The user interface is the group of all widgets in place in a certain layout to represent a specific use-case.

Roller

The roller is a list widget named after a cylinder which revolves around its central horizontal axis. As a result of being a cylinder it has no specific start and finish and appears endless.

Application author

The application author is the developer tasked with writing an application using the widgets described in this document. They cannot modify the variant or the user interface library.

Variant

A variant is a customised version of the system by a particular system integrator. Usually variants are personalised with particular colour schemes and logos and potentially different widget behaviour.

Use cases

A variety of use cases for list design are given below.

Common API

An application author wants to add a list widget to their application. At that moment it is not known whether a simple list widget or a roller widget will suit the application better. Said application author doesn't want to have a high overhead in migrating code from one widget to another.

MVC separation

A group of application authors wants to be able to split the work involved in developing their application into teams such that, adhering to the interfaces provided, they can develop the different parts of the application and easily put them together at the end.

Data backend agnosticity

An application author wishes to display data stored in a database, and does not want to duplicate this data in an intermediary data structure in order to do so.

Kinetic scrolling

The user wants to be able to scroll lists using their finger in such a way that the visual response of the list is as expected. Additionally, the system integrator wants the user to have visual feedback when the start or end of a list is reached by the list bouncing up and back down (the elastic effect). However, another system integrator wants to disable this effect.

The user expectations include the following:

  • The user expects the scroll to only occur after a natural threshold of movement (as opposed to a tap), for the list to continue scrolling after having removed their finger, and for the rate of scroll to decrease with time.

  • The user expects to be able to stop a scroll by tapping and holding the scrolling area.

  • The user expects a flick gesture to re-accelerate a scroll without any visible stops in the animation.

  • The user expects video elements to continue playing during a scroll.

  • When there are not enough items to fill the entire height of the list area the user expects a scroll to occur but using the elastic effect fall back to the centre.

  • The user expects a horizontal scroll gesture to not also scroll in the vertical direction.

Roller focus handling

In the roller widget, the user wants the concept of focus to be highlighted by the list scrolling such that the focused row is in the vertical centre.

Additionally, the user wants to be able to easily focus another unfocused visible item in the list simply by pressing it.

Animations

The user wants to have a smooth and natural experience in using either list widget. If the scrolling stops half-way into an item and it is required that one item is focused (see Roller focus handling), they want the list to bounce a small scroll to focus said item.

Item launching

An application author wants to be able to perform some application-specific behaviour in response to the selecting of an item in the list. However, they want to provide some confirmation before launching an item in a list. They want the two step process to be:

  1. The desired item is focused by scrolling to it or tapping on it.

  2. The focused item is tapped again which confirms the intention to launch it.

The application author wants to add a header to the column to make it clear exactly what is in said column. (An example can be seen in List design in the music application.)

Another system integrator wants the column names to be shown above the widget in a footer instead of a header.

Roller rollover

In the roller widget, by definition, the user wants to scroll from the last item in the list back to the first without having to go all the way back up.

Additionally, the user wants the wrap around to be made more visually obvious with increased resistance when scrolling past the fold between end and start again.

Widget size

The application author wants any list widget to expand into space allocated to it by its layout manager. If there are not enough items in the list to fill all available space said application author wants the remaining space to be blank, but still used by the list widget.

Click activation

A system integrator wants to choose between single click and double click activation (see Item launching) for use in the list widgets. This is only expected once the item has already been focused (see also Roller focus handling).

The decision of single or double click is given to the system integrator instead of the application author in order to retain a consistent user experience.

Consistent focus

The user focuses an item in the list and a new item is added. The user expects the new item not to change the scroll position of the list and more importantly not to change the currently focused row.

Focus animation

An application author wants an item in the list to be able to perform an animation after being focused.

Mutable list

An application author wants to be able to change the contents of a list shown in the user interface after having created the widget and shown it in the user interface.

UI customisation

A system integrator wants to change the look and feel of a list widget without having to change any of the application code.

Blur effect

A system integrator wants items in the list to be slightly blurred when scrolling fast to exaggerate the scrolling effect. Another system integrator wants to disable this blur.

Scrollbar

A system integrator wants a scrollbar to be visible for controlling the scrolling of the list. Another system integrator doesn't want the scrollbar visible.

Hardware scroll

A system integrator wants to use hardware buttons to facilitate moving the focus up and down the list. The system integrator wants the list to scroll in pages and for the focus to remain in order. For example, a list contains items A, B, C, and D. When the down hardware button down is pressed, the page moves down to show items E, F, G, and H, and the focus moves to item E as it is the first on the page.

On-demand item resource loading

The music application lists hundreds of albums but the application author doesn't want the album art thumbnail to be loaded for every item immediately as it would take too long and slow the system down. Instead, said application author wants album art thumbnail to load only once visible and have a placeholder picture in place until then.

Scroll bubbles

A system integrator wants bubbles to appear when scrolling and disappear when scrolling has stopped.

Item headers

An application author wants to display items in a list but have a logical separation into sections. For example, in a music application, listing all tracks of an artist and separating by album.

Another application author wants said headers to stick to the top of the widget so they are always visible, even if the first item has been scrolled past and is no longer visible.

List with tens of thousands of items

An application author wants to display a list containing thousands of items, but does not want to incur the initial cost of instantiating all the items when creating the list.

Flow layout

An application author wants the list widget to render as a grid with multiple items on the same line. The following video shows such a grid layout.

Concurrent presentation of the same model in different list widgets

An application author wants to present the same model in two side-by-side list widgets, possibly with different filtering or sorting.

Non-use cases

A variety of non-use cases for the list design are given below.

Tree views

An application author wants to show the filesystem hierarchy in their application. They understand that multi-dimension models (or trees) where items can be children of other items are not supported by the Apertis list widgets (hence the name list).

List widget without a backing model

An application wants to display a list of items in the list widget, but does not wish to create a model and pass it to the list widget, and would rather use helper functions in the list widget, such as list_add_item(List *list, ListItem *item).

Such an interface is not considered necessary, at least for this version of the design document, because we want to encourage use of models so that the UI views themselves can be rearranged more easily.

If, in the future, such an interface was considered desirable, its API should be similar to the GtkListBox API, such as gtk_list_box_row_changed().

An application developer wants an actor to stick to the top or the bottom of the list widget, and always be visible regardless of scrolling.

This is best handled as a separate actor, sharing a common parent with the list widget.

Requirements

Common API

There should be a common API between both list widgets (see Common API). Changing from a list widget to a roller widget or the other way around should involve only trivial changes to the code leading to a change in behaviour.

MVC separation

The separation between components that use the list widgets should be functional and enable application authors and system integrators to swap out parts of applications easily and quickly (see MVC separation).

The implementation of the model should be of no impact to the functionality of the widget. As a result the widget should only refer to the model using an interface which all models can implement.

Data backend agnosticity

The widget should not require application authors to store their backing model in any particular way.

Kinetic scrolling

Both list widgets should support kinetic scrolling from user inputs (see Kinetic scrolling). That is, when the user scrolls using their finger, he or she can flick the list up or down and the scroll will continue after the finger is released and gradually slow down. This animation should feel natural to the user as if he or she is moving a wheel up or down, with minimal friction. The animation should also be easily stopped by tapping once.

Elastic effect

In the list widget with a defined start and finish, on trying to scroll there should be visual feedback that the start or finish of the list has been reached. This visual feedback should be accomplished using the elastic effect. That is, when the bottom is reached and further downward scrolling is attempted, an empty space slowly appears with resistance, and pops back when the user releases their finger.

This is not necessary on the roller widget because the list loops and there is no defined start and finish to the list.

It should be easy to turn this feature off as it may be undesired by the system integrator (see Kinetic scrolling).

Item focus

In both list and roller widgets there should be a concept of focus which only one item has at any one point. How to display which item has focus depends on the widget.

Roller focus handling

In the roller widget the focused item should always be in the vertical centre of the widget (see Roller focus handling). The focused item should visually change and even expand if necessary to demonstrate its focused state (see also Focus animation).

Changing which item is focused should be possible by clicking on another item in the list.

Animations

It should be possible to add animations to widgets to allow for moving the current scroll location of the list up or down (see Animations). This should be customisable by the system integrator and application author depending on the application in question but should retain the general look and feel across the entire system.

Item launching

Focused items (see Item focus) should be able to be launched using widget-specific bindings (clicks or touches) (see Click activation).

Header and footer

It should be possible to add a header to a list to provide more context as to what the information is showing (see Header and footer and the screenshot in List design). This should be customisable by the application author and should be consistent across the entire system.

Roller rollover

The rollover of the two list widgets should be different and customisable by the system integrator (see Roller rollover and UI customisation).

The roller widget should roll over from the end of the list back to the beginning again, like a cylinder would (see List design and Roller). Additionally the system integrator should be able to customise whether they want extra resistance in going back to the beginning. This is visual feedback to ensure the user knows they are returning to the beginning of the list.

The non-roller list widget should not have a rollover and should have a well-defined start and finish, with visual effects as appropriate (see Elastic effect).

Widget size

The list widgets should expand to fill out all space that has been provided to them (see Widget size). They should fill any space not required with a blank colour, specified by the variant UI customisation (see UI customisation).

Consistent focus

The focus of items in a list should remain consistent despite modification of the list contents (see Consistent focus). Adding items before or after the currently focused item shouldn't change its focused state.

Focus animation

The application author and system integrator should be able to specify whether there is an animation in selecting an item in a list (see Focus animation and UI customisation). This could mean expanding an item to make the item focused larger vertically and even display extra controls which were previously hidden under the fold.

During this animation, input should not be possible.

Mutable list

The items shown in the list widgets and their content should update dynamically when the model backing the widget is updated (see Mutable list). This should require no extra effort on the part of the application author.

UI customisation

Both list widgets should be visibly customisable in the same way the rest of the system is and should honour UI customisations made by the system integrator (see UI customisation). In this way, the list widgets should use CSS (see the UI Customisation Design document) for styling.

Blur effect

The list widget should support slightly blurring list items only when scrolling (see Blur effect). It should be easily to disable this feature by another system integrator who doesn't want the blur.

Scrollbar

The list widget should support showing and hiding a scrollbar as necessary (see Scrollbar). It should be easy to disable this feature by another system integrator who doesn't want to display a scrollbar.

Hardware scroll

The list widget should support scrolling using hardware buttons and therefore always have one item focused (Hardware scroll). Hardware button callbacks should use the adjustments on the list widget to change the subset of visible items and the appropriate list widget function for moving the focus to the next item. Hardware button signals are generated as described in the Hardkeys Design.

On-demand item resource loading

Items in the list need to know when they are visible (and not past the current scroll area) so they know when to load expensive resources, such as thumbnails from disk (see On-demand item resource loading).

Scroll bubbles

The scrollbar (see also Scrollbar) should support showing bubbles to show the scroll position (see Scroll bubbles). It should be possible to disable the bubble and change its appearance when necessary.

Item headers

It should be possible to add separating headers to sets of items in the list widgets (see Item headers). Said headers should also be sticky if specified.

Lazy list model

It should be possible to provide a ‘lazy list store’ to the widget, in which items would be created on demand, when they need to be displayed to the user.

This model could make memory usage and instantiation performance independent of the number of items in the model.

See List with tens of thousands of items

Flow layout

It should be possible for n items, each of the same width and height, to be packed in the same row of the list, where n is calculated as the floor of the list width divided by the item width. There is no need for the programmer to set n manually.

Reusable model

The underlying model should not have to be duplicated in order to present it in multiple list widgets at the same time.

See Concurrent presentation of the same model in different list widgets

Approach

Adapter interface

As required by Data backend agnosticity, the backing data model format should not be imposed by the list widget upon the application developer.

As such, an ‘adapter’ is required, similar to Android's ListAdapter, this adapter will make the bridge between the list widget and the data that backs the list, by formatting data from the underlying model as list item widgets for rendering.

The following diagram illustrates how this adapter helps decoupling the list widget from the underlying model.

list adapter flowchart

In the above example, we assume a program that simply displays a list widget exposing items stored in a database, and an adapter that stores strong references to the created list items, and will eventually cache them all if the list widget is fully scrolled down by the user. This is as opposed to the approach presented in Lazy list model where memory usage is also taken into account.

The ‘cursor’ is a representation of whatever database access API is in use, as most databases use cursor-based APIs for reading.

An interface for this adapter (the contents of the list widgets) is required such that it can be swapped out easily where necessary (see MVC separation, Lazy list model).

GLib recently (since version 2.44) added an interface for this very task. GListModel is an implementation-agnostic interface for representing lists in a single dimension. It does not support tree models (see Tree views) and contains everything required for the requirements specified in this document.

It should be noted that GListModel, which is for arbitrary containers, is entirely unrelated to the GList data structure, which is for doubly linked lists.

In addition to functions for accessing the contents of the adapter, there is also an items-changed signal for notifying the view (the list widget and list item widgets it contains; see MVC separation) that it should re-render as a result of something changing in the adapter.

GtkListBox

GtkListBox is a GTK+ widget added in 3.10 as a replacement for the very complicated GtkTreeView widget. GtkTreeView is used for displaying complex trees with customisable cell renderers, but more often lists are used instead of trees.

GtkListBox doesn't have a separate model backing it (but one can be used), and each item is a GtkListBoxRow (which is in turn a GtkWidget). This makes using the widget and modifying its contents especially easy using the GtkContainer functions. Items can be activated (see Item launching or selected (see Item focus).

GtkListBox has been used in many GNOME applications since its addition and has shown that its API is sufficient for most simple use cases, with a limited number of items.

However GtkListBox is not scalable, as its interface requires that all its rows be instantiated at initialisation, in order for example to add headers to sections, and still be able to scroll accurately to any random position in the list (random access).

As such, its API is only of a limited interest to us, particularly when it comes to Item headers or Filtering.

GtkFlowBox

GtkFlowBox is a GTK+ widget added in 3.12 as a complement to GtkListBox. Its API takes a similar approach to that of GtkListBox: it doesn't have a separate model backing it – but one can be used – and each item is a GtkFlowBoxChild which contains the content for that item.

As with GtkListBox, its API is interesting to us for its approach to reflowing children; see Column layout.

Widget size

The list widgets should expand to fill space assigned to them (see Widget size). This means that when there are too few items to fill space the remaining space should be filled appropriately, but when there are more items than can be shown at one time the list should be put into a scrolling container.

In Clutter, actors are made to expand to fill the space they have been assigned by setting the x-expand and y-expand properties on ClutterActor. For example:

/* this actor will expand into horizontal space, but not into vertical
 * space, allocated to it. */
clutter_actor_set_x_expand (first_actor, TRUE);
clutter_actor_set_y_expand (first_actor, FALSE);

/* this actor will expand into vertical space, but not into horizontal
 * space, allocated to it. */
clutter_actor_set_x_expand (second_actor, FALSE);
clutter_actor_set_y_expand (second_actor, TRUE);

/* this actor will stretch to fill all allocated space, the
 * default behaviour. */
clutter_actor_set_x_align (third_actor, CLUTTER_ACTOR_ALIGN_FILL);

/* this actor will be centered inside the allocation. */
clutter_actor_set_x_align (fourth_actor, CLUTTER_ACTOR_ALIGN_CENTER);

More details can be found in the ClutterActor documentation.

The list item widgets (as described in Adapter/Model implementation) are packed by the list widget and so application authors have no control over their expanding or alignment.

A suitable scrolling container to put a list widget into is the MxKineticScrollView as it provides kinetic scrolling (see Kinetic scrolling and Elastic effect) using touch events. Additionally, the MxKineticScrollView should be put into an MxScrollView to get a scrollbar where appropriate (see Scrollbar).

For support of MxKineticScrollView the list widgets should implement the MxScrollable interface, which allows getting and setting the adjustments, and is necessary in showing a viewport into the interface

The exact dimensions in pixels for the widget shouldn't be specified by the application author as it means changes to the appearance desired by a system integrator are much more difficult to achieve.

Adapter/Model implementation

As highlighted before (in Adapter interface), the list widget should make no assumption about how the backing data is stored. An adapter data structure should be provided, making the bridge between the backing data and the list widget, by returning list item actors for any given position.

The GListModel interface requires all its contained items to be GObjects with the same GType.

It is suggested that the items themselves are all instances of a new ListItem class, which will inherit from ClutterActor, and implement selection and activation logic.

GListStore is an object in GLib which implements GListModel. It provides functions for inserting and appending items to the model but no more. For small lists, it is suggested to either use GListStore directly or implement a thin subclass to give more type safety and better-adapted function signatures.

In these simple cases, GListStore will act as both the adapter and the backing model, as it is storing ListItem widgets. For more complicated use cases (where the same data source is being used by multiple list widgets), the adapter and backing model must be separated, and hence GListStore is not appropriate as the adapter in those cases. See Decoupled model.

Decoupled model

As shown in Adapter interface, the list widget will not directly interact with the underlying data model, but through an ‘adapter’.

The following diagram shows how the same underlying model may be queried by two different list widgets, and their corresponding adapters.

List widgets are the outermost boxes in the diagram; the adapters are the next boxes inwards; and the middle box is the shared data model (a database).

The ‘cursors’ in the above diagram are a representation of whatever database access API is in use, as most databases use cursor-based APIs for reading.

Lazy object creation

GListModel allows an implementation to create items lazily (only create or update items on screen and next to be displayed when a scroll is initiated) for performance reasons. This is recommended for applications with a very large number of items, so a new ListItem isn't required for every single item in the list at initialisation.

GListStore does not support lazy object creation so an alternative model will need to be implemented by applications which need to deal with huge models.

An example for this is provided in Alternative list adapter.

High-level helpers

Higher-level API should be provided in order to facilitate common usage scenarios, in the form of an adapter implementation.

This adapter should be instantiatable from various common data models, through constructors such as: list_adapter_new_from_g_list_model or list_adapter_new_from_g_list.

This default adapter should automatically generate an appropriate UI for the individual objects contained in the data model, with the only requirement that they all be instances of the same GObject subclass. This requirement should be clearly documented, as it won't be possible to enforce it at instantiation time for certain data models, such as GList, without iterating on all its nodes, thus forbidding the generic adapter from adopting a lazy loading strategy.

The default behaviour of the adapter should be to provide a UI for all the properties exposed by the objects (provided it knows how to handle them), but much like the Django admin site, it should be easy for the user to modify which of these properties are displayed, and the order in which they should be displayed, using a set_fields() method. The suggested set_fields() API would take a non-empty ordered list of property names for the properties of the objects in the model which the adapter should display. For example, if the model contains objects which represent music artists, and each of those objects has name, genre and photo properties, the adapter would try to create row widgets which display all three properties. If set_fields (['name', 'genre']) were called on the adapter, it would instead try to only display the name and genre for the artist (name first, genre second), and not their photo. The layout algorithm used for presenting these properties generically in the UI is not prescribed here.

This generic adapter should expose virtual methods to allow potential subclasses to provide their own list item widgets for properties that may or may not be handled by the default implementation, and to provide their own list item widgets for each of the GObjects in the model. These are represented by create_view_for_property() and create_view_for_object() on the API diagram.

The adapter should use weak references on the created list items, as examplified in Alternative list adapter.

Filtering and sorting should be implemented by this adapter, with the option for the user to provide implementations of the sorting and filtering methods, and to trigger the sorting and filtering of the adapter. It should be clearly documented that these operations may be expensive, as the adapter will have no other option than iterating over the whole model.

If developers want to use the list widget with an underlying model that allows more efficient sorting and filtering (for example a database), they should implement their own adapter.

Refer to the API diagram for a more formal view of the proposed API, and to the Generic adapter section for a practical usage example.

UI customisation

The list and list item widgets should be ApertisWidget subclasses (which are in turn ClutterActors) to take advantage of the GtkApertisStylable mixin that ApertisWidget uses. This adds support for styling the widget using CSS and other style providers which can be customised by system integrators.

As the list item widgets are customisable widgets, they can appear any way the application author wants. This means that it is up to the application author to decide on theming decisions. Apertis-provided list item widgets will clearly document the CSS classes that affect their appearance.

Sorting

Sorting the model is built into the GListStore. When adding a new item to the adapter g_list_store_insert_sorted is used with the third argument pointing to a function to help sort the model. All items can be sorted at once using the g_list_store_sort function, passing in the same or different sorting function.

When using an Alternative list adapter, sorting will need to be implemented on a case-by-case basis.

Filtering

As with GtkListBox when bound to a model, filtering should be implemented by updating the contents of the adapter.

The list widget will be connected to the adapter, and will update itself appropriately when notified of changes.

An example of this is shown in the next section, the following diagram illustrates the filtering process.

Adapter filtering

The ‘cursors’ in the above diagram are a representation of whatever database access API is in use, as most databases use cursor-based APIs for reading.

Header and footer

The header should be a regular ApertisWidget, passed to the list widget as a header property. It will be rendered above the body of the list widget. Similarly, an ApertisWidget passed to a footer property will be rendered below the body of the list widget. Using arbitrary widgets means the header’s appearance is easily customisable. Passing them to the list widget means that the list widget can set the widgets’ width to match that of the list.

Applications can set either, both, or neither, of the header and footer properties. If both are set, both a header and a footer are rendered, and the application may use different widgets in each.

Selections

As with GtkListBox, it should be possible to select either one or many items in the list (see Item focus). The application author can decide what is the behaviour of the list in question using the “selection mode”. The values for the selection mode are none (no items can be selected), single (at most one item can be selected), and multiple (any number of items can be selected). A multiple selection example can be found in the Multiple selection section.

The ListItem object has a read-write property for determining it can be selected or not. An example that sets this property can be found in the Non-selectable items section.

The selection signals exposed by the list widget will be identical to those exposed by GtkListBox, namely item-selected when an item is focused, item-activated when the item is subsequently activated, and in the case of multiple selection, the selected-items-changed signal will give the user the full picture of the current items selected by the user, by being emitted every time the current selection changes, and passing an updated list of selected list items to potential callback functions.

Item headers

Item headers are widgets used to separate items into logical groups (for example, when showing tracks of an artist, they could be grouped in albums with the album name as the item header).

Due to the requirement for lazy initialisation, the solution proposed by GtkListBox cannot be adopted here, as it implies that all the list items need to be instantiated ahead of time.

Our approach here is similar to the solution used with Android's ListAdapter: as the adapter is decoupled from the data model, and returns the actors that should be placed at a given position in the list, it may also account for such headers, which should be returned as unselectable ListItems at specific positions in the adapter.

We make no assumptions as to how implementations may choose to associate a selected list item with the data model: in the simple case, the index of a list item may be directly usable to access the backing object it represents, if the backing data is stored in an array, and no item header is inserted in the adapter.

In other cases, where for example the backing data is stored in a database, or item headers are inserted and offset the indices of the following items, implementations may choose to store a reference to the backing object using g_object_set_qdata or an external mechanism such as a GHashTable.

An example of item headers is shown in the following section.

Sticky item headers

As required for Lazy list model, when the list widget is scrolled to a random point, items surrounding the viewport may not be created yet.

It is proposed that a simple API be exposed to let application developers specify the sticky item at any point in the scrolling process, named list_set_sticky_func.

The implementation will, upon scrolling the list widget, pass this function the index of the top-most visible item, and expect it to return the ListItem that should be stickied (or NULL).

Blur effect

Given the MxKineticScrollView container does the actual scrolling, it is the best place to implement the desired blur effect (see Blur effect). Additionally, implementing it in the container means it can be implemented once instead of in every widget that needs to have a blur effect.

On-demand item resource loading

By default, list items should assume they are not in view and should not perform expensive operations until they have been signalled by the list widget that they are in view (see On-demand item resource loading). For example, a music application might have a long list of albums and each item has the album cover showing. Instead of loading every single album cover when creating the list, each list item should use a default dummy picture in its place. When the user scrolls the list, revealing previously hidden items, the album cover should be loaded and the default dummy picture replaced with the newly loaded picture.

The list widget should have a way of determining the item from given co-ordinates so that it can signal to said item when it comes into view after a scroll event. The list item object should only perform expensive operations when it has come into view, by reading and monitoring a property on itself. Once visible an item will not return to the not visible state.

Scroll bubbles

The bubble displayed when scrolling to indicate in which category the scroll is showing (see Scroll bubbles) should be added to MxScrollBar as that is the widget that controls changing the scroll position.

Roller subclass

The roller widget should be implemented as a subclass of the list widget.

The roller widget will implement the MxScrollable interface, setting appropriate increment values on its adjustments, in order to ensure the currently-focused row will always be aligned with the middle after scrolling.

As the roller subclass will implement rollover, the elastic effect when reaching the bottom of the list will not be used.

In addition, it will also, in its init method, use a ClutterEffect in order to render itself as a cylinder (or ideally, as a hexagonal prism).

Column layout

By default, the list widget will display as many items as fit per row, given the list’s width and the configured item width. Properties will be provided for:

  • A row-spacing property, setting the blank space between adjacent rows.
  • A column-spacing property, setting the blank space between adjacent columns.
  • An item-width property, setting the width of all columns.
  • An item-height property, setting the height of all rows.

Note that GtkFlowBox supports reflowing children of different sizes; in this design, we only consider children of equal sizes, which simplifies the API. It is equivalent to considering a GtkFlowBox with its homogeneous property set to true.

So, for example, to display items in a single column (one item per row), set the item-width to equal the list’s width. To implement a grid layout where some items may be missing and gaps should be left for them, implement a custom row widget which displays its own children in a grid; and display one such row widget per row of the list.

We suggest the default value for item-width is to track the list’s width, so that the list uses a single column unless otherwise specified.

Focus animation

A use-animations property will be exposed on the list items. Upon activation, an item with this property set to True will be animated to cover the whole width and height allocated to the list widget.

Application developers may connect to the item-activated signal in order to update the contents of the item, as shown in Item activation.

Once the set of possible and valuable animations has become clearer, API may be exposed to give more control to system integrators and application developers.

Item launching

In the roller subclass, the item-activated signal should only be emitted for actors in the currently focused row. This will ensure that only items that were scrolled to can be activated.

API diagram

Signals are shown with a hash beforehand (for example, #item-activated), with arguments listed afterwards. Properties are shown with a plus sign beforehand and without getters or setters (get_model(), set_model()) for brevity.

Example API usage

Basic usage

The following example creates a model, creates item actors for each artist available on the system, and adds them to the model. The exact API is purely an example but the approach to the API is to note.

As a simple example, this avoids creating a separate adapter and model, and instead creates a model which contains the list row widgets. In more complex examples, where data from the model is being used by multiple list widgets, the model and adapter are separate, and the entries in the model are not necessarily widget objects. See Generic adapter for such an example.

#include "sample-list.h"

/* In this example, our backing model is a simple GPtrArray */

typedef struct
{
  gchar *name;
} SampleArtist;

static void
free_sample_artist (SampleArtist *artist)
{
  g_free (artist->name);
  g_free (artist);
}

static SampleArtist *
create_sample_artist (const gchar *name)
{
  SampleArtist *res = g_new0 (SampleArtist, 1);

  res->name = g_strdup (name);

  return res;
}

static GPtrArray *
create_sample_artists (void)
{
  GPtrArray *res;

  res = g_ptr_array_new_with_free_func ((GDestroyNotify) free_sample_artist);

  g_ptr_array_add (res, create_sample_artist ("GYBE"));

  return res;
}

static void
create_sample_artist_item (SampleArtist *artist, GListStore *adapter)
{
  SampleListItem *item = sample_list_item_new ();
  ClutterActor *name_label =
      clutter_text_new_with_text (artist->name, "Sans 12");

  clutter_actor_add_child (CLUTTER_ACTOR (item), name_label);

  /* The list store takes ownership of the new artist item so we do not need to
   * unref it.
   */
  g_list_store_append (adapter, item);
}

SampleList *
create_sample_list (void)
{
  g_autoptr (GListStore) adapter;
  g_autoptr (SampleList) list;
  g_autoptr (GPtrArray) backend_artists = create_sample_artists ();

  adapter = g_list_store_new (SAMPLE_TYPE_LIST_ITEM);

  /* Populate the adapter with artist views */
  g_ptr_array_foreach (backend_artists, (GFunc) create_sample_artist_item,
                       adapter);

  /* Create the list widget and pass the adapter */
  list = sample_list_new ();

  /* This takes a reference to adapter */
  sample_list_set_adapter (list, G_LIST_MODEL (adapter));

  return g_steal_pointer (&list);
}

int
main (int ac, char **av)
{
  SampleList *list = create_sample_list ();

  /* We may now expose the list to the user, once that is done
   * we can drop our reference to the list. This will release the
   * last remaining reference to the adapter, which will in turn
   * release the last references to individual artist items.
   */
  g_object_unref (list);

  /* All references are released */

  return 0;
}

The object created by create_sample_artist_item is an instance of ClutterActor (or an instance of a subclass) which defines how the item will display in the list. In that case, it is as simple as packing in a ClutterText to display the name of the artist as a string. More likely it would use a ClutterBoxLayout layout manager to pack different ClutterActors into a horizontal line showing properties of the artist.

Item activation

The following example creates a list widget using the function defined in the previous section and connects to the item-activated signal to change the list item actor.

#include "sample-utils.h"

static void
list_item_activated_cb (SampleList *list,
                        SampleListItem *item,
                        gpointer user_data)
{
  /* Update the list item to show more information about the artist */
}

int
main (int ac, char **av)
{
  SampleList *list = create_sample_list ();

  g_signal_connect (list, "item-activated",
                    G_CALLBACK (list_item_activated_cb), NULL);

  /* Now display the list to the user, then ... */

  g_object_unref (list);

  return 0;
}

Filtering

The following example shows how to filter the list store bound to the list widget created using the function implemented in Basic usage to only display items with a specific property.

#include "sample-utils.h"

static gboolean
filter_artist (guint index)
{
  /* Here we could retrieve the artist at the specified index
   * from our backing data model, and return TRUE or FALSE
   * based on its attributes. For simplicity we only make it
   * so the first 20 items get displayed */
  return (index <= 20);
}

static void
filter_artists (GListStore *store)
{
  guint i, n_items;

  n_items = g_list_model_get_n_items (G_LIST_MODEL (store));

  for (i = 0; i < n_items; i++)
    {
      if (!filter_artist (i))
        g_list_store_remove (store, i);
    }
}

int
main (int ac, char **av)
{
  SampleList *list = create_sample_list ();

  filter_artists (G_LIST_STORE (sample_list_get_adapter (list)));

  /* now display to the list to the user, then ... */
  g_object_unref (list);

  return 0;
}

Multiple selection

The following example sets the selection mode to allow multiple items to be simultaneously selected.

#include "sample-utils.h"

static void
selected_items_changed_cb (SampleList *list)
{
  GArray *selected;

  selected = sample_list_get_selected_items (list);

  /* Now do something with the items */

  g_array_free (selected, TRUE);
}

int
main (int ac, char **av)
{
  SampleList *list = create_sample_list ();

  g_object_set (list, "selection-mode", SAMPLE_SELECTION_MULTIPLE, NULL);

  g_signal_connect (list, "selected-items-changed",
                    G_CALLBACK (selected_items_changed_cb), NULL);

  /* now display to the list to the user, then ... */
  g_object_unref (list);

  return 0;
}

Non-selectable items

The following example makes half the items in a list impossible to select.

#include "sample-utils.h"

static void
item_created_cb (SampleList *list, SampleListItem *item)
{
  if (sample_list_item_get_index (item) % 2)
    sample_list_item_set_is_selectable (item, TRUE);
}

int
main (int ac, char **av)
{
  SampleList *list = create_sample_list ();

  g_signal_connect (list, "item-created", G_CALLBACK (item_created_cb), NULL);

  /* now display to the list to the user, then ... */
  g_object_unref (list);

  return 0;
}

Header items

The following example adds alphabetical header items to the list.

#include "sample-list.h"

static SampleListItem *
create_sample_artist_item (const gchar *name)
{
  SampleListItem *res = sample_list_item_new ();
  ClutterActor *name_label = clutter_text_new_with_text (name, "Sans 12");

  clutter_actor_add_child (CLUTTER_ACTOR (res), name_label);

  return res;
}

static SampleListItem *
create_sample_header_item (const gchar *header_text)
{
  SampleListItem *res = sample_list_item_new ();
  ClutterActor *name_label =
      clutter_text_new_with_text (header_text, "Sans 12");

  clutter_actor_add_child (CLUTTER_ACTOR (res), name_label);
  g_object_set (res, "selectable", FALSE, NULL);

  return res;
}

static SampleList *
create_sample_list (void)
{
  GListStore *adapter;
  SampleList *list;

  adapter = g_list_store_new (SAMPLE_TYPE_LIST_ITEM);

  g_list_store_append (adapter, create_sample_header_item ("A"));
  g_list_store_append (adapter, create_sample_artist_item ("ABBA"));
  g_list_store_append (adapter, create_sample_header_item ("B"));
  g_list_store_append (adapter, create_sample_artist_item ("Bob Marley"));

  list = sample_list_new ();
  sample_list_set_adapter (list, G_LIST_MODEL (adapter));

  g_object_unref (adapter);

  return list;
}

int
main (int ac, char **av)
{
  SampleList *list = create_sample_list ();

  /* We may now expose the list to the user, once that is done ... */
  g_object_unref (list);

  return 0;
}

On-demand item resource loading

The following example shows how on-demand item resource loading could be implemented, using the model created in the first listing:

#include <gdk/gdk.h>
#include <cogl/cogl.h>

#include "sample-utils.h"

static ClutterContent *
create_empty_image (void)
{
  ClutterContent *res;
  static guint8 empty[] = { 0, 0, 0, 0xff };

  res = clutter_image_new ();

  /* Here, we simply set a black background as a placeholder,
   * other implementations may use an actual image.
   */
  clutter_image_set_data (CLUTTER_IMAGE (res), empty,
                          COGL_PIXEL_FORMAT_RGBA_8888, 1, 1, 1, NULL);

  return res;
}

static void
_pixbuf_loaded_cb (GObject *source, GAsyncResult *result, gpointer user_data)
{
  g_autoptr (GdkPixbuf) pixbuf;
  GError *error = NULL;

  pixbuf = gdk_pixbuf_new_from_stream_finish (result, &error);

  if (pixbuf)
    clutter_image_set_data (
        CLUTTER_IMAGE (user_data), gdk_pixbuf_get_pixels (pixbuf),
        gdk_pixbuf_get_has_alpha (pixbuf) ? COGL_PIXEL_FORMAT_RGBA_8888
                                          : COGL_PIXEL_FORMAT_RGB_888,
        gdk_pixbuf_get_width (pixbuf), gdk_pixbuf_get_height (pixbuf),
        gdk_pixbuf_get_rowstride (pixbuf), NULL);

  g_object_unref (user_data);
}

static void
list_item_showing_cb (SampleListItem *item,
                      GParamSpec *pspec,
                      gpointer user_data)
{
  ClutterContent *image;
  GError *error = NULL;
  g_autoptr (GFile) file;
  g_autoptr (GInputStream) stream;

  /* We already loaded the image for this item */
  if (clutter_actor_get_content (CLUTTER_ACTOR (item)) != NULL)
    return;

  file = g_file_new_for_path ("file:///some/path");
  stream = G_INPUT_STREAM (g_file_read (file, NULL, &error));

  if (!stream)
    return;

  /* We only load the image once it needs to be shown to the user */
  image = create_empty_image ();

  /* We take a reference to the image, to make sure it does not get
   * disposed before loading is done
   */
  gdk_pixbuf_new_from_stream_async (stream, NULL,
                                    (GAsyncReadyCallback) _pixbuf_loaded_cb,
                                    g_object_ref (image));

  /* For simplicity, we set the image as the content of the item,
   * more advanced usage would have us set it as the content of
   * a child actor */
  clutter_actor_set_content (CLUTTER_ACTOR (item), image);
}

static void
list_item_created_cb (SampleList *list,
                      SampleListItem *item,
                      gpointer user_data)
{
  g_signal_connect (item, "notify::showing", (GCallback) list_item_showing_cb,
                    NULL);
}

int
main (int ac, char **av)
{
  SampleList *list = create_sample_list ();

  /* Connect to the notify::showing signal from every list item. */
  g_signal_connect (list, "item-created", (GCallback) list_item_created_cb,
                    NULL);

  /* now display to the list to the user, then ... */
  g_object_unref (list);

  return 0;
}

Generic adapter

The following example shows how developers may take advantage of the proposed generic adapter.

#include "sample-list.h"

/* In this example, our backing model is a simple GList */

typedef GList BackingModel;

static void
backing_model_free (BackingModel *model)
{
  g_list_free_full (model, g_object_unref);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (BackingModel, backing_model_free)

static GList *
create_backing_model (void)
{
  GList *res = NULL;

  res = g_list_prepend (res, sample_artist_new ("GYBE"));

  return res;
}

int
main (int ac, char **av)
{
  SampleList *list = sample_list_new ();
  g_autoptr (BackingModel) backing_model = create_backing_model ();
  g_autoptr (SampleListAdapter) adapter;

  /* Create a generic adapter and pass it to the list widget.
   * In our case, the adapter will automatically create list
   * items containing a single ClutterText, representing the
   * name of the artist. The adapter essentially converts a GList of
   * SampleArtist objects to a GListModel of ClutterText objects.
   */
  adapter = sample_list_adapter_new_from_g_list (backing_model);
  sample_list_set_adapter (list, G_LIST_MODEL (adapter));

  /* We may now expose the list to the user, once that is done
   * we can drop our reference to the list. This will release the
   * last remaining reference to the adapter. */
  g_object_unref (list);

  /* All references are released */

  return 0;
}

Alternative list adapter

Authors of applications do not necessarily need to use a GListStore as their adapter class. Instead they can implement the GListModel interface, and pass it as the adapter for the list widget.

In the following example, we define and implement an adapter to optimise both initialisation performance by creating MyArtistItems only when required, and memory usage by letting the List widget assume ownership of the created items.

#include <glib.h>
#include <gio/gio.h>

#include "sample-list.h"

/* This is left undefined to keep the example short, modalities
 * from retrieving backing data will vary from one implementation
 * to another
 */
static guint get_chart_position_from_item (SampleListItem *item);

/* Example alternative adapter implementation
 *
 * SampleLazyList is an adapter for the list store, which adapts an underlying
 * data source to a GListModel of SampleListItem objects. In this example the
 * source of the underlying data (self->model) is left unspecified, but it
 * could be any presentation-agnostic data source, such as a database or
 * a GListModel containing non-widget objects. */

#define SAMPLE_TYPE_LAZY_LIST (sample_lazy_list_get_type ())
G_DECLARE_FINAL_TYPE (
    SampleLazyList, sample_lazy_list, SAMPLE, LAZY_LIST, GObject)

struct _SampleLazyList
{
  GObject parent_instance;

  /* cache, using a GSequence is adapted for really large models,
   * but implementations may use another data type such as GPtrArray
   * or GArray for smaller models to decrease memory overhead */
  GSequence *artists;

  /* The underlying data model. In theory it could have any type,
   * but in practice the adapter would declare a specific type that it
   * must have (for example, a connection object from an SQLite database),
   * and use that. */
  gpointer model;
};

static void sample_lazy_list_iface_init (GListModelInterface *iface);

G_DEFINE_TYPE_WITH_CODE (SampleLazyList,
                         sample_lazy_list,
                         G_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL,
                                                sample_lazy_list_iface_init));

static void
sample_lazy_list_finalize (GObject *obj)
{
  g_sequence_free (SAMPLE_LAZY_LIST (obj)->artists);

  G_OBJECT_CLASS (sample_lazy_list_parent_class)->finalize (obj);
}

static void
sample_lazy_list_class_init (SampleLazyListClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = sample_lazy_list_finalize;
}

static GType
sample_lazy_list_get_item_type (GListModel *list)
{
  return SAMPLE_TYPE_LIST_ITEM;
}

static guint
sample_lazy_list_get_n_items (GListModel *list)
{
  return 42; /* Arbitrary number, implementation-dependent */
}

static gint
sample_artist_item_find (gconstpointer p1,
                         gconstpointer p2,
                         gpointer user_data)
{
  guint pos1 = get_chart_position_from_item ((SampleListItem *) p1);
  guint pos2 = *(guint *) p2;

  return ((pos1 < pos2) ? -1 : ((pos1 == pos2) ? 0 : 1));
}

static gint
sample_artist_item_compare (gconstpointer p1,
                            gconstpointer p2,
                            gpointer user_data)
{
  guint pos1 = get_chart_position_from_item ((SampleListItem *) p1);
  guint pos2 = get_chart_position_from_item ((SampleListItem *) p2);

  return ((pos1 < pos2) ? -1 : ((pos1 == pos2) ? 0 : 1));
}

static void
sample_artist_item_unreffed (SampleListItem *item, SampleLazyList *self)
{
  GSequenceIter *iter;

  iter = g_sequence_lookup (self->artists, item, sample_artist_item_compare,
                            NULL);
  g_assert (iter);
  g_sequence_remove (iter);
}

/* Here, lookup the artist item from the self->artists sequence
 * and return it if it was found.
 *
 * Otherwise, construct a new artist item, add it to self->artists and
 * return it.
 *
 * If memory is constrained, let the created object inherit from
 * GInitiallyUnowned, take a weak reference on it and return that instead.
 *
 * When the weak reference gets notified, remove it from self->artists
 * in order to return a new object if required.
 */
static gpointer
get_from_cache (SampleLazyList *self, guint position)
{
  GSequenceIter *iter;
  SampleListItem *res;

  iter = g_sequence_lookup (self->artists, &position, sample_artist_item_find,
                            NULL);

  if (iter)
    {
      return g_sequence_get (iter);
    }

  res = sample_list_item_new ();

  /* Implementations should populate the view here, using data from some
   * underlying self->model. */

  /* ListItem will inherit from GInitiallyUnowned. Implementations may either
   * call g_object_ref_sink on the new list item or, as we do here, let List
   * assume ownership of the list item, and set a weak reference to be notified
   * when it is disposed.
   */
  g_sequence_insert_sorted (self->artists, res, sample_artist_item_compare,
                            NULL);
  g_object_weak_ref (G_OBJECT (res), (GWeakNotify) sample_artist_item_unreffed,
                     self);

  return res;
}

static gpointer
sample_lazy_list_get_item (GListModel *self, guint position)
{
  return get_from_cache (SAMPLE_LAZY_LIST (self), position);
}

static void
sample_lazy_list_iface_init (GListModelInterface *iface)
{
  iface->get_item_type = sample_lazy_list_get_item_type;
  iface->get_n_items = sample_lazy_list_get_n_items;
  iface->get_item = sample_lazy_list_get_item;
}

static void
sample_lazy_list_init (SampleLazyList *self)
{
  /* Passing NULL to the data_destroy function, as in this example
   * we do not keep full references on them */
  self->artists = g_sequence_new (NULL);
}

Requirements

This design fulfils the following requirements:

  • Common API — the list widget and roller widget have the same API and any roller-specific roller API is in its own class.

  • MVC separationGListModel is used as an adapter to the backing data, which storage format is not imposed to the user. The list widget and item widgets are separate objects.

  • Data backend agnosticity — Applications provide list items through an adapter, no requirement is made as to the storage format.

  • Kinetic scrolling — use MxKineticScrollView.

  • Elastic effect — use MxKineticScrollView.

  • Item focus — list items can be selected (Selections).

  • Roller focus handling — this roller-specific selecting behaviour can be added to the roller's class.

  • Animations — use MxKineticScrollView.

  • Item launching — the item-activated signal on the list widget will signal when an item is activated.

  • Header and footerApertisWidgets can be set as header or footer (Header and footer).

  • Roller rollover — this roller-specific rollover behaviour can be added to the roller's class.

  • Widget size — use ClutterLayoutManager and the ClutterActor properties (Widget size).

  • Consistent focus — the API asserts a consistent focus and ensures the implementation behaves when the model changes.

  • Focus animation — items are ClutterActors which can animate using regular Clutter API.

  • Mutable list — use GListStore.

  • UI customisation — subclass ApertisWidget and use the GtkStyleProviders.

  • Blur effect — add a motion-blur property to MxKineticScrollView and use that (Blur effect).

  • Scrollbar — use the scroll-visibility property on the MxScrollView container.

  • Hardware scroll — use the adjustment on the list widget to scroll down a page, and use the appropriate function to move the selection on.

  • [][On-demand item resource loading1] — ensure list widget can look up the item based on co-ordinates, and add a property to the list item object to denote whether it’s in view, which the list widget updates.

  • Scroll bubbles — add support for overlay actors to MxScrollBar.

  • Item headers — Added as regular ListItems in the adapter, a list_set_sticky_func API is exposed.

  • Lazy list model — see Alternative list adapter, for an example of how application developers may implement their own model.

  • Flow layout — The number of columns is calculated by dividing the list width by the specified item width. Padding between the resulting columns and rows may be specified using row-spacing and column-spacing properties.

Summary of recommendations

As discussed in the above sections, we recommend:

  • Write a list widget partially based on GtkListBox, which subclasses ApertisWidget.

  • Write a list item widget partially based on GtkListBoxRow which also subclasses ApertisWidget.

  • Add a motion-blur property to MxKineticScrollView.

  • Expose a sticky item callback registration method.

  • Add support for overlay actors to MxScrollBar.

  • Write a Roller widget as a list widget subclass.

  • Ensure new widgets are easily customisable using CSS.

  • Add demo programs for the new widgets.

  • Define unit tests to run manually using the example programs to check the widgets work correctly.

Appendix

Existing roller design

The results of the search are