View Overview

REBOL/View - View Notes

Please note

Since I wrote this there have been changes that may make some of the statements you find in here just plain wrong. I intend to rectify that situation in due course, but in the meantime, instead of removing this resource I figure it is better to leave it here because most of it could be useful.

Introduction

This document goes into the lower level details of the Graphical User Interface facilities provided in REBOL/View.

I decided to create this document while learning the capabilities of Rebol/View. It resembles a bunch of notes. I figured the best time to record my "ahas" was when they occurred.

You can read this document as HTML on the codeconscious.com website or as a text file or much better than both of those as a Easy* reader document from the codeconscious.com rebsite. The benefit of the rebsite Easy* version is that most code examples within the document are executable - click on them to see them work. If they don't work in the reader they will be copied to the clipboard.

To use the interative Easy* reader version, start REBOL/View, ensure you are connected to the internet by clicking on "local" at bottom left if necessary, click on the "REBOL.com" icon, then on "Sites", then on "Code C.", then finally "Docs".

Panes

Faces can be composite in the sense that they can contain child faces. This is achieved using the PANE field of a Face.

PANE can be one of NONE, a BLOCK!, a Face (OBJECT!) or even a FUNCTION! (described in the section titled "Calculated Panes". Obviously if pane is NONE, there are no children. If pane is a block then the block contains the children. If pane refers to an object then the object is a face and hence there is a single child. If pane refers to a function the pane is dynamically rendered by the function.

It reminds me of design patterns. The composite pattern seems to be implemented when pane is a block. The decoration pattern seems appropriate when pane refers to a face. When pane is a function - well I don't know :).

VID Faces also have a field called PARENT-FACE. While PANEL makes explicit reference to it seems to be ultimately set by the VIEW function to create links from child faces to their parents.

Rebol Technologies has a howto document on making subpanels which covers using PANE.

Specific face fields

SHOW? Is a flag set by the HIDE and SHOW functions. It is true if the face should be drawn.

Size of text

To find the size of text accurately you can use the SIZE-TEXT function. SIZE-TEXT returns the size of the text in a face. Quite useful for dynamic layouts. Alternatively you could create a layout and measure how big that is. Either way you need to create a face first before you can measure the size of text.

Inform, show-popup, hide-popup

INFORM pops up a modal dialog box.

Typing HELP INFORM at the console yields "Display an exclusive focus panel for alerts, dialogs, and requestors."

How to use inform.

inform layout [backdrop blue
    vh1 "Gorim by Graham Chiu"
    vh2 "Rugby by Maarten Koopmans"
    vh2 "Menu by Frank Sievertsen"
    vh2 "Rebol by Rebol Technologies"
    vh2 "Colour text by Cyphre"
    button "Ok" [hide-popup]
]

So it shows a pop up type window.

Making it go away

Well there appear to be two behaviours to describe. The first is when your INFORM pop up has no buttons - in which case just clicking on the window will dismiss.

Here's and example. Click anywhere in the window to dismiss it:

inform layout [
    text "This is a pop up!"
]

But now if you have a button you will need the button to close it for you or you will need to close the window manually as you any other a window.

inform layout [
    text "This is a pop up!"
    button "Close" [hide-popup]
]

In this last example I used the HIDE-POPUP function. The opposite of HIDE-POPUP is SHOW-POPUP which INFORM uses behind the scenes. You should probably always use a HIDE-POPUP if you use SHOW-POPUP because

Strangely enough this little example does not demonstrate any exclusivity as far as I can tell.

Don't inform from inside inform

It is considered a bad idea (and may crash Rebol) to pop up a model box from another so it is better to hide the first and then pop up the next.

Shortcut keys

You don't get any shortcut key functionality when using Inform but you can rectify this if you wish by giving the face a feel of system/view/window-feel.

Calculated Panes

I call it a calculated pane another name might be interated face. The interesting thing is that a pane can be dynamically calculated from a function. To make one you set PANE to a function with a definition like this:

func [face id] []

The FACE argument is straightforward. The ID argument is much more interesting. It serves a dual purpose.

When ID is a PAIR! the function should return an INTEGER!. In this mode the function maps points on screen to a face index. This index is used later when the function is called again this type with ID set to an integer.

When the function receives ID as an INTEGER! it should return the face associated with that index. The face gets rendered, and the function is called again this an ID one greater than the last call. Successive calls are made until the function returns NONE.

Note that the face returned can be used over and over, a new face object does not need to be created for each return - it seems to be used purely for drawing and limited feel functionality (engage may not work). So you can just modify the attributes of the face as required before returning it from the function.

Here's an example of a box with a calculated pane. Try clicking and dragging over the blue faces, and pushing the button:

view layout [
  b: box 200x200 green with [
    iter: make make-face 'box [
      size: 180x20 edge/size: 1x1 color: blue
    ]
    func-pane: func [face id] [
      if pair? id [return 1 + second id / iter/size]
      iter/offset: iter/old-offset: id - 1 * iter/size * 0x1
      if iter/offset/y + iter/size/y > face/size/y [
        return none
      ]
      iter/text: join form ID [#" " now/time/precise]
      iter
    ]
    pane: :func-pane
  ]
  button "Show" [show b]
]

Related VID Styles

For an example to deconstruct look at how the TEXT-LIST style dynamically generates the textual faces. The LIST style also uses a calculated pane. It exists, I guess, as a wrapper around the pane function to make life a easier for a VID programmer.

Handling Events

Have a look at "How to Handle User Interface Events" found at http://www.rebol.com/how-to/feel.html.

Order of events

Using REBOL/View 1.2.1 the order of event based function calls as I see them are as follows:

On calling SHOW for a face:

(1) FACE/PANE  - render the face.
(2) FEEL/REDRAW (action = 'SHOW).
(3) FACE/PANE  - render the face.
(4) FEEL/REDRAW (action of 'DRAW).
(5) FACE/PANE  - render the face.

On an event where DETECT could be called:

(1) FEEL/DETECT.
(2) FACE/PANE  - render the face.
(3) According to the event either:
    (a) FEEL/ENGAGE.
    (b) FEEL/OVER.

Face timer based events bypass DETECT and are directed to ENGAGE.

These events should provide plenty of opportunities for custom styles

Window feels

Recall that the LAYOUT returns a face. The feel of the face returned from LAYOUT is a feel with no feelings :( - joke...

Like this: redraw: none detect: none over: none engage: none

However, one the layout is VIEWed as a window it acquires the feel object system/view/window-feel. This feel object only has the detect function defined to provide shortcut key functionality.

Global Level Event Handlers

Insert-event-func

This function allows setting up global level event handlers. Multiple if desired (you would use multiple handlers when each handler has a different purpose). By global I mean at the Rebol instance level. If there are multiple windows open, they will be all handled by the same handler.

A handler is a function

insert-event-func takes a function or function body block as an argument. If you pass a function ensure that it is declared with these arguments: [ face event ]. Your handler will potentially handle multiple rebol windows if you open them. You can check for specific windows by comparing the face argument with a face of your choice.

Removing the handler

The insert-event-func function returns a reference to the new handler (a function) that was inserted. You use remove-event-func to remove this handler again by calling remove-event-func, using as an arugment the function you recevied back from insert-event-func.

Event processing

Event handlers must pass events on that need to be processed by other handlers. If they are not passed on they are effectively dropped from the system and any behaviours that relate to that event will not occur.

Related to this is the point that the order in which handlers are inserted is important. They appear to form a chain of handlers. This explains why passing an event on by returning it has such a big effect.

See the section on "system/view/screen-face" for information on the mechanics of the event chain.

Alternatives to insert-event-func

The user guides says "insert-event-func function should be used to trap screen-face global events". So there is a clue. It says this as part of the documentation for the DETECT function so you should read up on that too. DETECT may be more appropriate for your needs if you do not need to handle

Demonstration

The example below demonstrates insert-event func.

Note that when you run the example you'll see a message saying the handler is being installed. I've coded the down event to print the text of the title of the face being affected. So you'll see a message when you click on the button on the main face. Indeed you'll see the message any time you push the mouse button down while the cursor is over a face.

This shows how one handler is handling multiple windows. When you close any one of the windows the handler will remove itself from the chain and you will no longer get any more messages when you press the mouse button.

view layout [
    title "Main Face"
    button "Test Now" [
        view/new layout [
            title "Face 1"
            button "Click me"
        ]
        view/new layout [
            title "Face 2" button
            button "Click me"
        ]
    ]
    do [
        print "Installing event function"
        evtfunc: insert-event-func [
            if equal? event/type 'down [
                Print event/face/pane/1/text
            ]
            if equal? event/type 'close [
                print "Removing event function"
                remove-event-func :evtfunc
            ]
            RETURN event
        ]
    ]
]

system/view/screen-face

The event chain

The event chain appears to be located at system/view/screen-face/feel/event-funcs.

This bit of code displays the chain as it is right now.

chain: 'system/view/screen-face/feel/event-funcs
view layout [
    h1 "Event chain"
    label form :chain
    label join "Length of chain: " length? chain
    text as-is mold chain
]

You can see one handler defined.

What processes the event chain?

system/view/screen-face/feel/detect is the function that processes the event chain.

Here is the code that does it:

view layout [
    h1 "Event chain processing function"
    label "system/view/screen-face/feel/detect"
    text as-is mold get in system/view/screen-face/feel 'detect
]

What passes events to system/view/screen-face/feel?

I don't know. The view engine seems like a good answer to me.

system/view/window-feel

system/view/window-feel/detect is the function that finds the face associated with a key and performs that face's action. The function find-key-face is used to find the face associated with a key. It searches through the face object model until it the appropriate face.

Dirty Faces

A dirty flag indicates if the user has made some edits to data in the window. Faces have a dirty? flag. However, you will need to test carefully when this flag changes - it may not be when you are hoping it will change. You can maintain your own dirty flag, which I do here. You can do so by setting a flag using an action block on a field for example. The other way to check for data changes it to compare old values with new values - perhaps when you save or close the window.

Trapping a window closing

With a dirty flag in place it would be nice to ask the user if they want to save their changes when they go to close the window. Closing a window is a window level event, if you only have one window in your application you can use a global event handler to trap the event. If you have more, you'll could code your handler to check for a specific window face.

Here is an example. First there is some supporting script. Then I create a query-on-close function which is the global level event handler that intercepts the close event and pops up a query if there is changed data. If you press cancel, the window will not close. This behaviour arises from the fact that if an event handler does not return an event the event is dropped from the system.

dirty-flag: false

ready-to-continue?: function [/nocancel] [is-ready save-it] [
    either dirty-flag [
        if save-it: either nocancel [
            request/confirm "Save changes now?"
        ][
    request "Save changes before continuing?"
    ] [ alert "Save data now - stub" ]
        is-ready: not none? save-it
   ][ is-ready: true ]
   RETURN is-ready
]

query-on-close: func[face event][
    either equal? event/type 'close [
        if ready-to-continue? [
            remove-event-func :query-on-close
            RETURN event
        ]
    ][event]
]

view layout [
    title "Missing event example"
    field [dirty-flag: true]
    do [ insert-event-func :query-on-close ]
]

Note that in this example query-on-close handler removes itself. This is not necessary if you are just quitting, but (a) it is tidier and (b) I need this line to ensure that the examples work as expected in this documentation viewer (vid-notes.r).

Look at rebodex.r to see another example of a program trapping the close window event.

Are we done?

At this point you would think - cool, I can check that users won't lose any edits when they close the window. But unfortunately that is not correct [this is being written against View 1.2 - verify your own version]. There is something missing.

So far we are relying on the action of a field to set the dirty flag. But this is not enough. There is one case where the dirty flag has to be set but the action field does not fire. This occurs, when there is only one edit made but the edit has not been "confirmed" in the field prior to the window being closed. It depends on your application whether you consider this important.

For example, try this:

1) Run the example above again.

2) Type something into the field but WITHOUT hitting TAB or ENTER, nor clicking anywhere else.

3) Close the window.

See how you are not asked to save changes? Compare with entering, tabbing from the field and then closing the window.

The problem is that the window level event close doesn't go through the View machinery to fire a field action. The solution is to fire the action ourselves. The easiest way to do this is to create another global level event handler. :)

To fire the action, we need to identify which face needs to be checked. system/view/focal-face provides an answer. It appears to be set when fields gain focus. We also need to check that we are not going to fire the action unnecessarily. Happily the dirty? flag I mentioned above solves this. If there has been no edits at all it will be set to none.

So here is the new code. Try the same procedure above with this example. What happens here is that we perform the action that is associated with the face (field in this case) that had focus when the user tried to close the window. In this way the dirty flag is set and thus is trapped when the window is closed.

dirty-flag: false

ready-to-continue?: function [/nocancel] [is-ready save-it] [
    either dirty-flag [
        if save-it: either nocancel [
            request/confirm "Save changes now?"
        ][
    request "Save changes before continuing?"
    ] [ alert "Save data now - stub" ]
        is-ready: not none? save-it
   ][ is-ready: true ]
   RETURN is-ready
]

query-on-close: func[face event][
    either equal? event/type 'close [
        if ready-to-continue? [ RETURN event ]
    ][event]
]

fire-action-on-close: function[face event][focussed-face][
    if all [
        equal? event/type 'close
        focussed-face: system/view/focal-face
        focussed-face/dirty?
        not none? get in focussed-face 'action
    ] [
        focussed-face/action focussed-face focussed-face/data
    ]
    RETURN event
]

view layout [
    title "Missing event generated example"
    field [dirty-flag: true]
    do [
        evtfunc: insert-event-func [
            if equal? event/type 'close [
                remove-event-func :fire-action-on-close
                remove-event-func :query-on-close
                remove-event-func :evtfunc
            ]
            RETURN event
        ]
        insert-event-func :query-on-close
        insert-event-func :fire-action-on-close
    ]
]

There are a few things to note with this example. The first is that I've recoded query-on-close - it no longer removes itself. Instead I create a third handler that is responsible for removing the other handlers and itself. The idea behind this is to say that the query-on-close and fire-action-on-close might be useful in other situations. It is totally over-engineered for this example, but I just wanted to show how the different behaviours could be considered to be seperable and therefore structured for reuse elsewhere. I don't claim it is the best :)

The second point is that I'm probably adding a performance penalty with three handlers all checking the same event ('close), whereas I could have got away with one handler. For some applications or platforms this may be critical, here though my concern is demonstrating concepts.

The third point. As mentioned in the previous section the order in which handlers are inserted is important. So in order of execution, query-on-close must follow fire-action-on-close otherwise my example will fail at trapping the "unconfirmed edit". This shows then that the last handler inserted into the chain is the first to receive an event.

Lastly in this section

In the examples above I did not check for a specific face. This is because I've just assumed that there is only one open window that I have to deal with - and it makes my example code easier.

To check for a specific face I could have added a condition next to equal? event/type? of equal? event/face example-face. This is likely to become important where you are using view/new to open multiple windows at once - see the previous section for an example of this in action.

Resizing

Resizing is about handling the resize event. First you have to trap the event and second perform the resizing of the faces.

If you wanted to perform all your resizing at the window level you could use insert-event-func and the techniques for event handling illustrated in the "Dirty Faces" section. For most people this is likely to be the best way to go, after all resizing does seem to be a window level task.

Here's a quick example. A box stuck in the bottom right corner of the window.

view/options layout [
    size 200x200
    space 0x0 origin 0x0
    bx: box green 50x50
    do [
        bx/offset: 150x150
        evtfunc: insert-event-func [
            if equal? event/type 'resize [
                bx/offset: (bx/parent-face/size - bx/size)
                show bx
            ]
            RETURN event
        ]
    ]
] [resize]

Alternatively you might have a nested hierarchy of faces and decided that you want faces with child face to be responsible for resizing code.

The example below was made by Sterling and modified by Larry Palmiter. It shows how the detect function of feel of a face can be used for resizing.

There are a few things to note here. Creation of window first, then the modification of the feel, then the wait statement to allow the events to be processed. This is because the the view statement creates a feel for the face main-lay which has to be overwritten with a modified version.

!Don't click on this example. The reader does not handle it yet.

main-lay: layout [
    b1: button red "Close" [unview main-lay]
]

view/new/options main-lay [resize]

main-lay/feel: make main-lay/feel [
    detect: func [face event] [
        switch event/type [
            resize [
                b1/color: random 255.255.255
                b1/offset: main-lay/size - (b1/size)
                show b1
            ]
        ]
        event
    ]
]

wait none

Problems with backdrop

[With thanks to Anton Rolls for showing the problem and delivering a solution]

When resizing a layout that has a backdrop, you must resize the backdrop seperately. However the backdrop will not always display the change when you expect.

In this example the gradient is not properly displayed when you click on the text:

view lay: layout [
    origin 0
    bd: backdrop effect [gradient]
    t: text "hello there" [
        append t/text " rebol"
        t/size: t/size + 40x0
        bd/size: lay/size: 4x4 + size-text t
        show lay
    ]
]

This code however demonstrates how to fix the problem (click on the text again):

view lay: layout [
    origin 0
    bd: backdrop effect [gradient]
    t: text "hello there" [
        append t/text " rebol"
        t/size: t/size + 40x0
        t/saved-area: none ; forget the image
        bd/size: lay/size: 4x4 + size-text t
        show lay
    ]
]

Keycodes

Here is yet another use for the event handler technique.

The code below shows the use of an event function to display the last key that was pressed and to hide or show indicators for whether shift or control is being held down.

Events are generated by setting a rate on the backdrop face.

view layout [
    backdrop rate 10
    style box box 50x30
    Text "Try pressing keys, hold and release shift and control"
    across
    shft: box "Shift"
    green ctrl: box "Ctrl"
    yellow keyc: box "key"
    do [
        evtfunc: insert-event-func [
            if equal? event/type 'key [
                keyc/text: mold event/key
                show keyc
            ]
            either event/shift [show shft][hide shft]
            either event/control [show ctrl][hide ctrl]
            if equal? event/type 'close [
                remove-event-func :evtfunc
            ]
            RETURN event
        ]
    ]
]

While this looks quite functional I need to do more investigation. For example not putting the rate on the backdrop means there are no events that will allow me to test shift and control. Changing the rate though seems to have no effect on how the shift and control keys are intercepted. So there must be a missing element here...

Miscellaneous internals

The object ctx-text seems to hold various text handling functionality.

It holds an object called "swipe". Swipe is used as the feel for VID TEXT and 'INFO styles. For example, It provides highlighting of text and copying the selected text to the clipboard.

ctx-text also holds an object called "edit" that is used as the feel for VID fields.

Both of these feel type objects call functions in ctx-text to perform the functionality. For example, the "edit" feel object calls the function ctx-text/edit-text to provide the behaviour of field editing.

Scroll-para

Scroll-para is a function that is delegated the task of scrolling text contents in a face.

Here's a simple example.

view layout [
    do [
        some-text: {}
        for i 1 10 1 [
            append some-text reduce [form system/locale/months newline]
        ]
    ]
    across
    msg-area: area 400x100 copy some-text silver as-is
    sldr: slider 16x100 [scroll-para msg-area sldr]
]

Scrolling Faces

Here an overview of scrolling faces.

Put a big-face inside the visible area of a small-face (in the sense of the Panel How-to). The small face becomes a "window" onto big-face in that you can only now see a part of big-face. The part of big-face you see depends on how much big-face is offset from small-face. The visible area of the small face is reduced by area taken up by edges.

To see the bottom of big-face you would want to offset big-face higher in relation to small face - and at maximum this would be the amount that big-face is taller than the small-face's visible area.

To see the top of big-face you would set the offset to be 0.

The only other thing is the size of the dragger in the slider - it should probably reflect the amount of data that you can see (1 = all, 0.25 = just a quarter).

big-f: layout [
    origin 0x0 space 0x0
    backdrop yellow
    title "bay.jpg"
    image load-thru/binary http://www.rebol.com/view/demos/bay.jpg
]

view layout [
    across
    small-f: box 200x80 green edge [size: 5x5 white]
    do [visible-area: small-f/size - (2 * small-f/edge/size)]
    sldr: slider 16x80 [
        ; Set the vertical offset to a proportion of
        ; the amount we cannot see.
        invisble-amount: subtract big-f/size visible-area
        big-f/offset/2: multiply sldr/data (
            -1 * (max 0 invisble-amount/2 )
        )
        show small-f
    ]
    do [
        small-f/pane: big-f
        big-f/offset: 0x0
        sldr/redrag min 1.0 divide 50 big-f/size/2
    ]
]

Lastly, I've already built some of this functionality in to a script in the rebsite library. There is even a handy vid-style for dealing with scrolling called scrollpanel.

Have a look at standard-guis.r which you can find listed by the library-catalog.r script on the codeconscious.com rebsite "Code C." of the View desktop. [Website users you really should read this stuff using Rebol/View :) ]

Scroll Wheel functionality

If you want to react to mice with scroll wheels you need to trap the scroll-line and scroll-page events - probably in a detect function of a feel.

This bit of code I lifted from an email by Allen kamp shows the sort of thing you need to do, (btw it is not a working example):

!Don't click on this particular example - the reader will not handle it yet. Paste the code into the console instead.

detect: func [face event][
    switch event/type [
        scroll-line [do-myscroll event/offset/y * lineheight]
        scroll-page [do-myscroll event/offset/y * pageheight]
   ]
]

When/if I get my scroll wheel working I'll try to produce a working example.

The CHANGES facet

Every face has a CHANGES field. It is a control or hinting mechanism for the view engine.

Hardware scrolling

On the Rebol list it was mentioned that by setting this field to 'OFFSET and then by calling SHOW, View can employ hardware scrolling.

Activating a window

Setting CHANGES to 'activate and then calling show, brings the window to the front. The following example demonstrates this:

view layout [

    do [
        ui-fce1: layout/offset [
            across
            banner "One" counter1: banner "<init>"
        ] 100x100

        ui-fce2: layout/offset [
            across
            banner "Two" counter2: banner "<init>"
        ] ui-fce1/size + ui-fce1/offset - 30x20

        alt1?: false
        set-activate: does [
            if ui-activate/data [
                alt1?: not alt1?
                act-face: either alt1? [ ui-fce1 ][ ui-fce2 ]
                act-face/changes: 'activate
            ]
        ]

    ]
    across label "Alternately activate windows"
    ui-activate: check false return
    button "Start" [
        view/new ui-fce1
        view/new ui-fce2
        for i 1 10 1 [
            set-activate
            counter1/text: form i show ui-fce1
            counter2/text: form i show ui-fce2
            wait 00:00:01
        ]
        unview ui-fce1
        unview ui-fce2
    ]
]

Other information

win-offset? and screen-offset?

These functions have a bug in that they do not take into account the edge of faces. Romano Paolo Tenca has provided replacements for these functions and I have included these as part of my patches.r script.

Performance

Quote

Don Cox wrote:

"I generally run on 8-bit 1024x768 screens, and that would be what I used for View. The scrolling was much slower than on other programs, such as the Voyager web browser, running on the same screen mode."

Holger replied:

"Probably because the scripts you used did not use hardware scrolling, but rather rerendered all data during every refresh. You can scroll by only changing the offset of a face, then setting face/changes to 'offset and calling 'show. This procedure significantly improves performance, by an order of magnitude."

"There are other tricks that can improve performance, such as using transparency only where necessary, and using non-transparent background colors everywhere else. You won't notice the difference on PCs, but you will on slow machines like the Amiga. Unfortunately most scripts out there are written and tested on PCs and therefore not optimized because the developers would never notice the effect of such optimizations on their machines."

Images

You can manipulate an image directly. Here is an example created by Carl Read

plot: func [image x y color][
    x: to-integer x + .5
    y: to-integer y + .5
    poke image y * image/size/1 + x color
]
pic: to-image 200x200
y: 50
for x 0 199 1 [
    plot pic x y red
    y: y + .5
]
view layout [image pic]