Constructing a Hex-mapped app

A hex-mapped app is constructed as a set of pages in an interactive comic-book
Each page shows the user some formatted information
and provides controls for selection and data-input.
Most of these controls are common across pages
so after an initial learning curve, the user gets familiar with them.
When using a new control, the user needs to be informed on how to use it.
This is part of the information presented in the page
Each "UI Mode", discussed earlier, is a page in the book
For example, the code presented earlier has 15 pages in its book
The first 13 pages are introductory.
They calibrate the user's system
and teach the user how to use the controls in the main drawing app on pages 14 and 15.
The introductory pages can be are skipped when the app is run a second time

Re-use

It's also very easy to pull out pages from other apps or comic books
and use them in a new comic book
due to the simple nature of the UI mode.
Over time, as we and hex-map developers develop different approaches
to information presentation, selection and data-input
we will have a library of pages, booklets or small comic books, and controls
that can be used for different purposes.
Just like all the different controls available on WIMP based GUI design SDKs
This will make the task of putting together a hex-mapped UI simpler with time
Hex-mapped controls are a lot simpler to implement than the "controls" used in WIMP systems
which are complex due to the presense of the mouse and windowing system and overdesign.

Example: Constructing the page for Hex-Draw

Hex-draw is an app for drafting on a hex grid.
It enables the user to create connected line segments
edit the locations of their anchors and delete anchors

Screen-space selection (Grid navigation)

Screen-space selection which is effectively selecting points on a hex-grid
is done using hex-map's base mechanism
1
via the "trap" which is a map of the alphanumeric keys centered on the 'H' key
It has a trapezoidal shape with a narrower base in the Z../ row
and and a wider top in the numerics row.
Hence we call it a trap.
The trap needs to be shifted both vertically and horizontally
to cover all the points on the screen.
The trap provides for coarse screen-space selection.
2
via sub-trap resolution refinement, which is moving the selection in grid units
along any of the 3 axes of the hex grid
For this, we use the Tab, CapsLock and Shift key at the left edge of the keyboard
and outside of the trap
and the '\', Enter and Shift keys on the right edge of the keyboard.
These 6 keys provide for refinement along each of the 3 hex axes along either direction.
They are the hex equivalent of the arrow keys on a keyboard

Creating anchors and lines

To create an anchor, you navigate to a point where you want to create an anchor
Then, we use the key to right of the Space-Bar which be call RBAR1 to create an anchor
After an anchor has been created, to create a line (or link) starting from that anchor
you double tap the RBar1 key
then you navigate to the end-point of the link and press Spacebar to complete the process

Editing anchors

Anchors often lie at sub-trap grid locations
To select them the coarse to fine navigation approach could be used
Hex-draw provides a quicker way.
To select an anchor, you navigate to its closest trap point
and then you press the LBOT key at the left bottom corner of the kybd
This key is a multi-function key, a yinyang key or magic key.
In this context, it navigates to the anchor closest to the trap point.
Once an anchor has been navigated to
it is selected for editing by pressing the execute key which is the space-bar
Thereafter the user may use the navigation mechanisms to edit its location
When they are done editing they press execute again to commit the edit
They may also delete the anchor using the LBAR1 key

Designing the UIMode for Hex-draw

While this is a rather simplistic drawing app
let's design the UIMode for it
Hex-mapped apps never do anything on their own
such as notifying the user about an email that has arrived
They only react to the user's inputs
Therefore, they must always track the user's intent
what the user is trying to achieve in the current interaction.
An intent is like a instantaneous UIMode.
It has a keymap section associated with it and a user action response section
It only does not have an intro section because it represents the UIMode only instantaneously
A UIMode is designed by compiling all the possible intents a user could have
while interacting with that UIMode
then implementing the functionality for each of those intents

Hex-draw's intents

Let's enumerate the intent's for hex-draw.
navigation
user is navigating the grid using the trap
I_TRAP,
user is shifting the keymap in the x direction
I_KEYMAP_SHIFTX,
user is shifting the keymap in the y direction
I_KEYMAP_SHIFTY,
user is navigating the grid using the sub-trap hexagonal arrow keys
I_SUBTRAP
anchor and link creation
user is creating an anchor
I_CRANCHOR,
user is creating a link
I_CRLINK,
user is moving the end-point of the link using the trap
I_TRAP_LINK,
user is moving the end-point of the link using the sub-trap hex-arrows
I_SUBTRAP_LINK,
commit to a change
user is commiting the end point of a link
or the user is commiting edit changes
I_EXECUTE,
sub-trap auto-select
the user is selecting an anchor for editing at a sub-trap location on the grid
I_SELECT,
editing
the user is editing the location of an anchor using the trap
I_TRAP_EDIT,
the user is editing the location of an anchor using the subtrap hex-arrows
I_SUBTRAP_EDIT,
the user is deleting an anchor
I_DELETE

Hex-draw's kybd_map and blit_uar

In the kybd_map function of Hex-draw's UI mode
the key-press to intent associations are encoded in the kybd_map
Also, at this point functions are defined to implement each of the user intents
Each of these functions is called once by kybd_map to initiate the processing
When they are ready to draw to the screen
they initiate a refresh
the user action response function of the UIMode or blit_uar processes this refresh
it determines the current intent and calls the function that initiated the refresh based on this
So the intent implementation functions are called twice
Once by the kybd_map to do the processing required an issue the refresh
and once by the blit_uar to do the final painting of the screen
In other words, they have 2 ohases of operation
The kybd_map calls them in PH_NOTIFY phase while blit_uar calls them in PH_EXEC phase

The remaining construction of Hex-draw

The remaining construction of the UIMode involves implementing the intent implementation functions
Let's look at one of these as an example
The trap( phase, delta, pCanvas, DC ) function implements I_TRAP
which is when the user is navigating using the trap.
The kybd_map function of hex-draw's UIMode determines the delta in hex coordinates
between the current cross-hair location (which is the current point of navigation)
and the pressed trap key's location
It passes this info to trap alongwith the pCanvas which is used for the refresh.
It also passes it a dummy DC which is unused in this phase of calling trap.
The blit_uar function of hex-draw's UIMode calls trap with the DC.
The delta and pCanvas parameters are ignored in PH_EXEC phase execution of trap.
During PH_NOTIFY phase, trap updates the cross-hair location
and issues a refresh at both old and new cross-hair locations
During PH_EXEC phase,
it re-draws all the underlying layers at the old cross-hair location minus the cross-hair
and draws all the underlying layers and the cross-hair at the new location
Note: all rectangles are stored relative to the center of the screen
and not it's top left corner.
This is more natural for a hexagonal grid based graphics subsystem


void trap( int phase, wxPoint delta, MyCanvas *pCanvas, wxDC& DC )
{
  static wxRect rect, rectOld;
  static wxRect rectC, rectOldC;
  static int kybdYShiftOld = 0;
  
  if( phase == PH_NOTIFY )
  {
    // update crosshair location
    g_CHOffsetOld.x = g_CHOffset.x;
    g_CHOffsetOld.y = g_CHOffset.y;
    g_CHOffset.x = delta.x * g_hexsPerKey + g_kybdXShift * g_hexsPerKey * 2;
    g_CHOffset.y = delta.y * g_hexsPerKey + g_kybdYShift * g_hexsPerKey;
    rect.x = - g_CHW/2 + g_CHOffset.x * 7;
    rect.y = - g_CHH/2 + g_CHOffset.y * 12;
    rect.width = g_CHW; rect.height = g_CHH;
    rectOld.x = - g_CHW/2 + g_CHOffsetOld.x * 7;
    rectOld.y = - g_CHH/2 + g_CHOffsetOld.y * 12;
    rectOld.width = g_CHW; rectOld.height = g_CHH;
    wxRect rectScrn;
    rectScrn.x = -g_scrnW/2; rectScrn.y = -g_scrnH/2; rectScrn.width = g_scrnW; rectScrn.height = g_scrnH;
    rectC = rect * rectScrn ;
    rectOldC = rectOld * rectScrn ;
    refresh( rectC, pCanvas );
    refresh( rectOldC, pCanvas );
    if( g_bDemoMode ){ show_pressed_key( phase, pCanvas, DC ); }
  }
  else // execute
  {
    int x=0,y=0;
    
    if( rectC.x > rect.x )
    { 
      x = rectC.x-rect.x; 
    }
    if( rectC.y > rect.y )
    { 
      y = rectC.y-rect.y; 
    }
    blit_grid( rectC, DC );
    blit_grid( rectOldC, DC );
    blit_kybd( rectC, DC );
    wxRect rectKybd;
    rectKybd.x = - g_kybdW/2 + g_sCalData.oskShift + g_kybdXShift*g_hexsPerKey*14;
    rectKybd.y = - g_kybdH/2 + g_kybdYShift*g_hexsPerKey*12;
    rectKybd.width = g_kybdW; rectKybd.height = g_kybdH;
    if( g_kybdYShift == kybdYShiftOld || rectKybd.Intersects( rectOldC ) )
    {
      blit_kybd( rectOldC, DC );
    }
    blit_anchors( rectC, DC );
    blit_anchors( rectOldC, DC );
    DC.Blit( rectC.x+g_scrnW/2, rectC.y+g_scrnH/2, rectC.width, rectC.height,
            g_DCCH,
            x, y,
            wxCOPY, true, wxDefaultCoord, wxDefaultCoord );
    g_CHOffsetOld.x = g_CHOffset.x;
    g_CHOffsetOld.y = g_CHOffset.y;
    kybdYShiftOld = g_kybdYShift;
    if( g_bDemoMode ){ show_pressed_key( phase, pCanvas, DC ); }
  }
  return;
}