Sunday, August 18, 2013

Text Morph and Morphic Tutorial

During the last week, I implemented a text area morph in Amber-Athens. The basic functionality is almost finished, and after three iterations, I can say that I am more or less satisfied with the design. It is not perfect and the implementation is still quite slow, but I know a couple of methods that could be optimized in order to boost the performance.

The text area box supports the following features.

  • Displaying and editing text without formatting

  • Multi-line strings

  • Automatic line break if a line is longer than the width of the text area box

  • Vertical scroll bar

  • Cursor and multi-character selections

  • Navigation with arrow keys

Here's a short overview of the characteristic of my implementation.

  • The content of the text area box contains of several lines. In addition, more lines might be generated if the content is changed, if a line is longer than the width of the text area box. We call all such lines ("real" and "newly-created lines") virtual lines. "AthensVirtualTextAreaLine" represents a virtual line and is responsible for handling horizontal pixel coordinates (offsets inside a line). The text are box ("AthensEditableTextMorph") is responsible for handling vertical pixel coordinates (lines). For example, given a pixel offset, "AthensEditableTextMorph" retrieves the line and "AthensVirtualTextAreaLine" retrieves the position of the cursor in that line.

  • When editing text, the new string is currently set via "text:". This re-generates all virtual lines. Actually, this is not necessary in most cases. Consider, for instance, that we added a single character to a (virtual) line. We might have to move some characters to other lines (below) even in the lines below the insertion, but it is a more or less local change. Lines before the insertion are not affected at all. In a future version, I will change the algorithm to handle small text editings in the virtual line class and use the same algorithm (recursively) for the next line, if a change is necessary (in most cases this will not be the case).

  • The text is rendered in the morph "AthensEditableTextMorph". We can specify a width for this morph, but not a height. It is always as high as the text content requires it to be. "AthensTextAreaMorph" is a subclass of "AthensScrollAreaMorph" and a decorator for "AthensEditableTextMorph". It adds the scrolling functionality to the text area box and delegates all method calls to it.

  • Text selections are represented with a "selectionStart" and a "selectionLength". If the selection length is zero, nothing is selected and we show only the cursor. The length can also be negative. The selection start is the offset of character preceding (in front of) the cursor. I.e., "selectionStart == 1" means that the first cursor is in front of the first character.

With the new text box, I could build a tutorial for the Morphic functionality (works in Chrome/Chromium only so far, I will fix that soon) that runs entirely on the Morphic stuff itself. There are only four steps at the moment, but I will add more steps in the future in order to cover the basic features.

As you can see, the tutorial is now a full-screen application, i.e. the Canvas and the world morph cover the whole page content.

Friday, August 9, 2013

Morphic Update: Scroll Bar, Scroll Area and List Box

In the last week, I implemented the AthensListBoxMorph. A list box is a rectangular container that has several list items as submorphs. A list item can be selected or unselected. The list box clips its content if it contains more list items than it can display. It provides scroll bars instead.

The image shows the demo application (Athens Tutorial) at step 39. Don't forget to execute step 33 before to create the surface and the world.

I implemented the list box by subclassing AthensScrollableArea. This is a morph that contains two scroll bars (horizontal and vertical) and a container morph called "outerContainer". The outer container contains another container called "innerContainer". The inner container's size is the bounding box of all morphs that are added to the scrollable area. We can scroll the content by moving the inner container inside the outer container. We need the outer container only for minor display reasons: the scroll area has a small rectangle in the bottom right corner that should be blank. If we had no outer container that clips the content, we might see content from the submorphs at this position. Furthermore, we do not have bother with z indices: scroll bars are always visible because they can never be overlayed by submorphs.

List items are instances of AthensListItemMorph. We can add arbitrary objects to a list box. They are automatically wrapped inside an AthensListItemMorph. The list item displays the item's string representation ("item asString").

There are only vertical scroll bars. We can generate horizontal scroll bars by rotating them by 90 degrees. Scroll bars have three important properties:

  • value: the position of the scroll bar slider (between 0 and 1)

  • buttonStepSize: defines how the value changes when clicking the up/down buttons. For example, a button step size of 0.1 increases the value by 0.1 when clicking the down button once.

  • sliderRange: the size of the slider

Thursday, August 1, 2013

First UI Morphs implemented

Here is a short status update for the Morphic implementation. So far, I implemented AthensButtonMorph, AthensRadioButtonMorph, AthensCheckBoxMorph, AthensWindowMorph, AthensTextMorph, and AthensIconMorph. The icon morph is simply a text morph that uses Font Awesome to display good-looking icons.

I made one more change to the event handling functionality. Every time we click on a morph, the according mouse event is triggered, no matter whether we clicked the morph itself or one of its submorphs. In my opinion, this is what programmers want most of the time (just one example: click a button that contains an additional image). In the event handler, we can check whether the event was triggered for the top-most morph. Therefore, we can easily check if we clicked the morph itself directly.

If you want to try the implementation yourself, open the tutorial, execute step 33 and then step 37 and/or step 38 (I will eventually change this, such that you don't have to click that often).

The following image shows step 37. It is a simple example that increases or decreases a number. The tiled background is the world morph that contains all other morphs.

This is the source code for step 37.
|window descText counter optIncrement optDecrement button|
"Step 37: [Morphic Demo] Using basic Morphs."

window := AthensWindowMorph new.
window title: 'Counter Example'.

descText := AthensTextMorph new.
descText text: 'Current value: '.
descText translateByX: 25 Y: 40.
window addMorph: descText.

counter := AthensTextMorph new.
counter text: '0'.
counter translateByX: 150 Y: 40.
window addMorph: counter.

optIncrement := AthensRadioButtonMorph new.
optIncrement text: 'Increment number'.
optIncrement translateByX: 25 Y: 70.
window addMorph: optIncrement.

optDecrement := AthensRadioButtonMorph new.
optDecrement text: 'Decrement number'.
optDecrement translateByX: 25 Y: 90.
window addMorph: optDecrement.

optIncrement onChange: [:val | optDecrement checked: val not].
optDecrement onChange: [:val | optIncrement checked: val not].
optIncrement checked: true.

button := AthensButtonMorph new.
button text: 'Do it'.
button translateByX: 25 Y: 120.
button width: 150.
button onMouseClick: [:evt | |val|
val := counter text asNumber.
optIncrement isChecked
ifTrue: [val := val + 1]
ifFalse: [val := val - 1].
counter text: val asString].

window addMorph: button.
surface world addMorph: window.

The next image shows step 38. As I mentioned in a previous post, every morph has its own transformation matrix. This allows us to rotate, scale, translate, and perform any other matrix operation on the morph. The example only contains buttons for rotation and scaling of the x axis.

Performance Issues

You will probably notice that moving windows and especially resizing windows is quite slow. So far, I did not implement any optimizations to the drawing code. Even the smallest change causes the complete world to be redrawn. I plan to optimize this by redrawing only the parts that actually changed.

More importantly, some operations like resizing a window, trigger multiple changes at once, e.g. changing the window's width, its height, and sending a layout change notification to all submorphs (which might again result in changing things). Most operations (changing height, width, transformation, colors, ...) result in a redraw. Therefore, we actually redraw the world multiple time if we just resize a window once. That's why the performance is currently so bad. I plan to optimize this by implementing a world state recorder that draws the world as fast as possible if there are changes. In the example that I just explained, the world state recorder is notified several times that something changes but only redraws the world after the resizing (and all other changes that the resizing triggers) is completed (remember: JavaScript is asyncronous). Therefore, we only redraw the world once even if the width/height of several morphs changes.