Skip to content

Commit

Permalink
Add sections on state management and control flow.
Browse files Browse the repository at this point in the history
  • Loading branch information
tmadden committed Aug 28, 2017
1 parent 9de19b3 commit 57ab7d8
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 1 deletion.
50 changes: 50 additions & 0 deletions docs/control-flow.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
Control Flow
============

Control Flow Macros
-------------------

In order for its data management facilities to function properly, alia needs to know about any place where you have widgets inside if statements or loops. You mark these places by using alia macros in place of the normal control flow keywords. We've already seen a few of these, but the complete list is here.

- ``alia_if``
- ``alia_else_if``
- ``alia_else``
- ``alia_switch``
- ``alia_case``
- ``alia_for``
- ``alia_while``

All control blocks must be terminated with ``alia_end``. (As you've already seen, an ``if/else_if/else`` block only needs one ``alia_end`` to terminate the entire block.)

Note that you must not use ``goto`` in your UI code because it circumvents these mechanisms. You should also avoid using ``return`` in the middle of a function, as this will skip over part of the function without alia knowing. ``break`` and ``continue``, however, are OK.

Also note that when we check the return value of ``do_button``, we use a normal if statement. This is because that if statement represents an event handlers, not a conditional block of widgets.

Context Naming
--------------

All of the above macros assume that your UI context variable is named 'ctx'. If that's not the case, each has a corresponding form with an underscore appended to the end of the name where you can manually specify the name of the context variable. For example, ::

alia_if_ (my_context, editing)
{
// ...
}
alia_end

Loops with Item Reordering
--------------------------

You may be wondering why we can add items to our contact list but not remove them. It's because removing them would confuse the alia_for macro. alia_for and alia_while associate state with the widgets in the loop body based on iteration count (the first iteration always gets the same data, the second iteration always gets the same data, etc.). When iterating over a list, reordering items in the list will cause them to end up associated with the wrong state.

In cases like this, you have to provide alia with a way to identify items in the list that will remain constant even as the items move around. If list items always remain at the same address (this is the case with STL lists), then you can simply use the address of an item as its ID. In other cases, you can use any info that's unique to an item (e.g., you might have assigned a numeric ID). Here's how our contact list loop looks using IDs. ::

naming_context nc(ctx);
for (std::list<contact>::iterator i = contacts.begin(); i != contacts.end(); ++i)
{
named_block nb(ctx, make_id(&*i)); // using contact address as block ID
do_contact_ui(ctx, *i);
}

Creating a named_block at the top of a scope creates a new data context for the scope. All widgets invoked inside that scope (or in any function called in that scope) will get their data from the block associated with the named_block's ID. Note that since the named_block is already taking care of the data management for the loop body, we can use a normal for loop.

The first line in the example defines a new naming context for the loop. A ``naming_context`` provides a context for ``named_block`` IDs. IDs used within one naming context can be reused within another without conflict. Since we only have one loop in this example, this isn't strictly necessary. However, in a real application, where you might have other loops over the same data, this is a good habit.
4 changes: 3 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ Welcome to alia's documentation!
:maxdepth: 2
:caption: Contents:

intro
introduction
state-management
control-flow

Indices and tables
==================
Expand Down
File renamed without changes.
77 changes: 77 additions & 0 deletions docs/state-management.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
State Management
================

Associating State
-----------------

Imagine that we want to modify the contact UI from the last section so that it has two modes: viewing and editing. Contacts are read-only in viewing mode, but the editing UI has the same editing controls as before. We could do that like this... ::

struct contact
{
contact() : editing(false) {}
bool editing;
std::string name;
std::string phone;
};

void do_contact_ui(context& ctx, contact& c)
{
alia_if (c.editing)
{
do_text_control(ctx, inout(&c.name));
do_text_control(ctx, inout(&c.phone));
if (do_button(ctx, "OK"))
c.editing = false;
}
alia_else
{
do_text(ctx, c.name);
do_text(ctx, c.phone);
if (do_link(ctx, "edit"))
c.editing = true;
}
alia_end
do_separator(ctx);
}

The above code behaves as desired, but now we've introduced UI-specific state into our contact structure. Ideally, we'd like our model data to be independent of any UI considerations, so we'd like to store the "editing" flag somewhere UI-specific. Fortunately, alia has a facility for this called get_state. ::

struct contact
{
std::string name;
std::string phone;
};

void do_contact_ui(context& ctx, contact& c)
{
state_accessor<bool> editing = get_state(ctx, false);
alia_if (get(editing))
{
do_text_control(ctx, inout(&c.name));
do_text_control(ctx, inout(&c.phone));
if (do_button(ctx, "OK"))
set(editing, false);
}
alia_else
{
do_text(ctx, c.name);
do_text(ctx, c.phone);
if (do_link(ctx, "edit"))
set(editing, true);
}
alia_end
do_separator(ctx);
}

get_state retrieves persistent state that's associated with the part of the UI that's currently being specified. It's a templated function, so it can retrieve any type of state. The state persists as long as the part of the UI that it's associated with.
get_state comes in two forms. In the form used above, the second argument is the default value of the state. When the state is created, it's initialized with that value. The second form is used as follows. ::

state_accessor<bool> editing;
if (get_state(ctx, &editing))
set(editing, false);

With this form, the first time get_state is called for a particular piece of UI, it returns true, indicating that the state it retrieved was just created (default constructed). The calling function can use this to initialize the state.
Using get_state, you can associate arbitrary UI-specific state with the portion of the UI that needs it, without anyone outside the function ever knowing. In fact, all along, some of the widgets we've been using have been calling get_state without us knowing. For example, this is how the text controls are able to track their cursor position, text selection, etc.

0 comments on commit 57ab7d8

Please sign in to comment.