gitea | include_toc |
---|---|
none |
true |
Will help you understand Spaces on deeper level to be able to alter how things work.
Key takeaways:
- Spaces are organized as a tree: each parent has links to it's children in it's
/map
facet (e.g.map: [child [offset 10x10 size 50x30]]
), which is generated by/draw
function (possibly from/content
and/items
facets). There's no/pane
facet, just/map
. /map
is expressed in parent's coordinates, so generally one should not assumespace/size = map/:space/size
(even more, some spaces can be infinite, but have finite size in their parent's map)/parent
,/size
and/map
facets gets set during render and are only valid for the duration of the last frame.- A path is used to specify the hierarchical relation of each space (e.g.
base/list/button/text
). It may originate from/parent
or/map
, depending on the event. - Styles and event handlers look up specific sequence in the path to find a match (e.g.
list/button
will match the above path).
Path has 2 formats:
- Block of space! objects. Used by all non-pointer events: keyboard, timer, focus, etc.
- thus,
space = path/1
is the space object for which the event handler was defined path/-1
is the parent objectpath/2
is a child object (possible e.g. if child space is focusable but did not process the key)path/3
is an inner child object, etc.- since it's a block, one can write something like:
set [parent: space: child: sub-child:] back path
- thus,
- Block of object + pair tuples. Used by pointer events:
over wheel up mid-up alt-up aux-up down mid-down alt-down aux-down click dbl-click
space = path/1
is still truepath/2
is the pointer coordinate inside this space's coordinate systempath/-2
is the parent objectpath/-1
is the pointer coordinate inside parent's coordinate systempath/3
is the child object,path/4
- pointer coordinate in the child, and so on...- example:
[list-view 210x392 hscroll 210x8 thumb 196x8]
(objects are shown as their names for illustration)
Format 1 describes the hierarchy, and is often written in this document using path (not block) notation, i.e. separated by slashes.
Tree paths can be discovered for any given layout face using the dump-tree
function.
>> view/no-wait [
face: host [
vlist [
text "Hello, space!"
button "OK" [quit]
]
]
]
>> dump-tree/from face
base:87x72
base:87x72/list:87x72
base:87x72/list:87x72/text:67x16
base:87x72/list:87x72/button:67x26
base:87x72/list:87x72/button:67x26/text:16x16
By default, spaces styles are minimalistic and adhere to the theme of user's OS. However if you're making software for yourself only, or if you're certain users won't curse you for enforcing your taste on them, you can fully customize the look of any space.
Spaces are designed in such a way that their core logic is separated from UI/UX.
Two styling mechanisms are used:
- VID/S styles allow for some quick customization of features on per-widget basis.
- template styles (explained below) allow one to define a style for whole templates, globally. Such style is capable to fully redefine the look.
Default styles are loaded from the styles.red
file.
New styles are currently created by set-style
function which accepts single style value:
>> ? set-style
USAGE:
SET-STYLE name style
DESCRIPTION:
Define a named style.
SET-STYLE is a function! value.
ARGUMENTS:
name [word! path!]
style [block! function!]
Or with define-styles
which is a simple dialect for stylesheet definition:
>> ? define-styles
USAGE:
DEFINE-STYLES styles
DESCRIPTION:
Define one or multiple styles using Styling dialect.
DEFINE-STYLES is a function! value.
ARGUMENTS:
styles [block!] "Stylesheet."
REFINEMENTS:
/unique => Warn about duplicates.
For style to have effect, style name should be either:
- a
word!
which should coincide with the space's type (which usually equals it's template name) - a
path!
of such valid space names that will be matched against the tree path
Examples:
set-style 'paragraph ..style-descriptor..
set-style 'list/item ..style-descriptor..
define-styles [
paragraph: ..style-descriptor..
list/item: menu/item: ..style-descriptor..
]
When looking up a style for each particular space, full hierarchical path in the space tree is scanned for this word or path. When found, style applies. E.g. list/item
only styles item
s that have list
as their parent space, and has bigger priority than just item
style.
How style lookup works...
Lookup order is "specific-to-generic". E.g. for path = base/list-view/list/item/paragraph
the order will be:
base/list-view/list/item/paragraph
list-view/list/item/paragraph
list/item/paragraph
item/paragraph
paragraph
I.e. if you define both list/item/paragraph
and paragraph
then the former (more specific) takes precedence over latter (generic).
Thus styles apply widely by default (like paragraph
), but can be specialized (like paragraph
within item
within list
). As opposed to assigning a style to each object separately by default and then making effort to cover more.
base
is the root style that allows one to define style common for all spaces. E.g. line-width, fill-pen, font. The other words are the names of the spaces.
Style descriptor carries the style body. It can be either a block or a function.
Is just a block bound to space and evaluated. It minimizes the amount of boilerplate code and makes styles more readable.
Block style may contain special below:
and above:
blocks with Draw commands which will be inserted before and after the Draw block returned by the space's /draw
function:
below
is great for drawing frame and background after/size
gets setabove
is great for drawing focus overlay or tinting the space (like button in it's "pushed" state)
Examples:
set-style 'list/item [
below: [pen cyan] ;) changes pen used by /draw
]
set-style 'paragraph [
font: serif-12 ;) modifies /font facet before /draw call
below: [pen blue]
]
define-styles [
tube: list: box: [ ;) allows color override for containers
;) `select self 'color` ensures that `color: none` value works same as absence of `color:` facet
below: when select self 'color [
;) note usage of unqualified `size` here (compose gets called after /draw so it's valid)
push (make-box size 0 'off color)
]
]
hscroll/thumb: vscroll/thumb: [ ;) box that indicates focus presence in a scrollable
above: when focused?/above 2 ( ;) test if focus is 2 levels above thumb (scrollable, grid-view, list-view)
make-box/margin size 1 checkered-pen none 4x3
)
]
]
Tip: when
function tests a condition and if true, returns the block after it or evaluates a paren. If false, returns an empty block. It's a very useful helper for compose
.
Animated style example: drunken scrollbars used in some of the tests
define-styles [
back-arrow: [below: [rotate (angle) (size / 2)]]
forth-arrow: [below: [rotate (angle) (size / 2)]]
thumb: [below: [translate (size * 0x1 / 2) skew (angle / -2) translate (size * 0x-1 / 2)]]
]
Where angle
is updated 3 times per sec as:
angle: pick [0 -13 -20 -13 0 13 20 13] (counter: counter + 1) % 8 + 1
Such power can be held in just a few lines! :D
How block styles are applied...
There are 3 steps:
-
Block is bound to the space object and evaluated before the
/draw
call.This allows one to set various space facets before drawing it. Like the above
/font
facet is set before space is drawn.
Draw is quite limited: e.g. you can't "ask" what current pen color is, to modify it, and you can't set font for rich-text using "font" command, and so on. Evaluation is aimed to empower styles while still keeping them short.
Evaluation result is unused except forbelow:
andabove:
fields.\ -
Space's
/draw
function is called to get a list of commands to render it./draw
sets the/size
and/map
facets, so they will be valid when accessed frombelow
/above
blocks during their composition. -
Values of
below:
andabove:
are composed (usingcompose/deep
) and inserted around/draw
result:[(below) (drawn) (above)]
.If any of these values are absent or
none
, they're ignored.
Note: set-words except below:
and above:
and those bound to space object are local, and will raise an error if used after style is evaluated. E.g. this style is bugged, because evaluation (1) leaves a code
reference that during composition (3) will have no context:
code: [..produces a block..]
below: [(do code)]
Function syntax gives more control over styling, e.g.:
- it can ignore
/draw
function completely - it gets access to the
canvas
andxy1
/xy2
arguments - it's faster because it's not being bound to the space at runtime
Sometimes it's more readable. Plus there's little risk of accidentally leaking words.
Style function:
- receives space as it's mandatory argument and should use path syntax to access it's facets
- should call
/draw
manually, passing/on canvas fill-x fill-y
and/window xy1 xy2
arguments to/draw
if it supports those - should return a block of Draw commands, which will be used to draw the space without any further modifications
Example for grid/cell
that draws full cell background regardless of how small/big the cell content happens to be:
set-style 'grid/cell function [cell /on canvas fill-x fill-y] [
#assert [canvas] ;-- grid should provide finite canvas
drawn: cell/draw/on canvas fill-x fill-y ;-- passes canvas arguments
;; when cell content is not compressible, cell/size may be bigger than canvas, but we draw up to allowed size only
canvas: min canvas cell/size
color: any [ ;-- allow cell color override and highlight pinned cells by default
select cell 'color
if grid-ctx/pinned? [mix 'panel opaque 'text 15%]
]
bgnd: make-box canvas 0 'off color ;-- always fill canvas, even if cell is constrained
reduce ['push bgnd drawn] ;-- compose result of /draw and background
]
Example for switch
that doesn't use /draw
at all, and sets the /size
itself:
set-style 'switch function [self] [
cross?: when self/state [line 3x3 13x13 line 13x3 3x13]
frame: make-box self/size: 16x16 1 none none
reduce [frame cross?]
]
Note: style function should not call the render
function on it's own space, because the main difference between /draw
and render
is that render
looks up and applies styles (and will deadlock if style calls it back).
For spaces that adapt their size automatically, their styling function should accept /on canvas [pair!] fill-x [logic!] fill-y [logic!]
refinement and pass it on to it's draw
function. One or both canvas
dimensions may equal those of infxinf
, which should be treated as unlimited.
When it makes sense to draw only a portion of a space (e.g. it's big or infinite), styling function should accept /window xy1 [none! pair!] xy2 [none! pair!]
refinement and pass it on to it's draw
function. Value of window
should not be accounted for, only values of xy1
and xy2
matter. none
means "unspecified" and implies rendering of the whole space area.
focused?
is a function that returnstrue
inside a style that has focus. As in the example above, it can be used to indicate focused state.
focused?/above n
checks for focus of n-th level parent instead- spaces may have their own flags, e.g.
/pushed?
flag of a button
This chapter describes general event handlers that are applied to whole templates. VID/S actors can be used to tune each single space separately.
Why write an event handler:
- non-standard or extended behavior for standard templates
- behavior of custom templates
Core logic of each space is (and should be) implemented in it's source code. Standard templates are currently presented in the templates.red
file.
However the behavior is defined in the standard-handlers.red
file. Event handlers are meant to be easy to tune manually.
Example behavior definition using event handler description DSL:
define-handlers [ ;) `define-handlers` is used to define events
inf-scrollable: extends 'scrollable [ ;) `extends` copies event handlers from another space
on-down [space path event] [ ;) event names follow those of View
space/slide ;) `slide` moves the window inside an infinite space
]
on-key-down [space path event] [ ;) these handlers do not override those of `scrollable`!
space/slide ;) they are called after the inherited ones
]
slide-timer: [ ;) slide-timer is a timer space owned by each inf-scrollable
on-time [space path event delay] [ ;) it has it's own event handler
scrollable: path/-1 ;) path/-1 is the outer space (inf-scrollable)
scrollable/slide
]
]
]
]
Note that the above example defines events both for inf-scrollable
template and inf-scrollable/slide-timer
specialized timer.
Event handler description DSL quick reference:
Example | Syntax | Description |
---|---|---|
template-name: [...] |
set-word! block! or set-word! 'extends lit-word! block! |
Defines events for spaces with the given name, optional extends modifier inherits handlers from another space. Can define handlers for spaces belonging to other spaces (like slide-timer: above) |
on-event [space path event] [...] |
word! block! block! |
Defines handler for a specific event. Internally uses function constructor, so inner set-words are collected |
Handler spec almost always takes 3 arguments: space path event
. Only on-time
event accepts an additional delay
argument.
It's possible to provide typesets: space [object!] path [block!] event [event! none!] delay [percent!]
usual name | accepted types | description |
---|---|---|
space |
object! |
Space object that receives the event. Convenience shortcut that equals path/1 |
path |
block! |
Path in the tree of faces, at the index of current space. Can be of 2 formats described above. |
event |
event! or none! |
View event that triggered the handler. Can be none for focus /unfocus events, because they do not come from View, but are generated internally (and it's impossible to create an event value from Red). |
delay |
percent! |
0% is the ideal value. But timers do not get called at a precise time. They can be called early (delay < 0% ), but usually they are late (delay > 0% ). delay = 100% means it's late by one timer period. This value can be used to produce smoother animations. |
Access to full tree path gives handlers ability to access their parent objects, like the slide-timer
above calls a function slide
from it's parent to affect it.
Path received by the handler is relative to the space that defined it. E.g. for screen/window/base/list-view/thumb
, if list-view
receives the event then path
is skip [screen window base list-view thumb] 3
Parent handler can also know that interaction is made with one of it's children. E.g. scrollable
space's handlers know if interaction is made with a scroller's thumb or one of the arrows...
Snippet from scrollable
that uses item
and subitem
to refer to it's children targeted by the pointer:
scrollable: [
on-down [space path event] [
set [_: _: item: _: subitem:] path ;) offsets (even parts) are not used
case [
find [hscroll vscroll] select item 'type [ ;) move or start dragging
move-by: :scrollable-space/move-by
axis: item/axis
switch select subitem 'type [
forth-arrow [move-by space 'line 'forth axis]
back-arrow [move-by space 'line 'back axis]
forth-page [move-by space 'page 'forth axis]
back-page [move-by space 'page 'back axis]
]
start-drag path
]
item = space/content [
start-drag path ;) start dragging (e.g. grid-view within grid-view)
pass ;) but let content handle it (it may override dragging path)
]
]
]
]
Spaces that define /rate
facet (to a positive integer, float or time value), and on-time:
handler can receive timer events.
Timer event handlers have an extra argument: delay [percent!]
, meaning delay from the expected time [-100% .. can be big]
. It can be used to make animations more smooth.
E.g. if animation moves a sprite by 50px every 50ms:
delay = 100%
means it should move by 50+50=100px, because timer skipped an event and coming latedelay = -30%
means is should move by 50-15=35px, because timer fired too early
Timer handlers should be prepared to handle huge delays, possible when device lagged or just woke from sleep (e.g. by ignoring them).
Delay bias is accumulated internally and event system automatically makes delayed timers fire more often until bias is zeroed (possible up to 50-55 fps, after which Windows' native timers can't keep up anymore). This is done to best achieve a desired framerate, regardless of whether handler handles the delay
in any way.
Event handlers are divided into 3 stacks (called in this order obviously):
- previewers (e.g. to focus a space on clicks, and still process the click)
- normal handlers (described above)
- finalizers (e.g. to catch Tab key if it wasn't processed and move focus)
Previewers and finalizers help modularize the event system. E.g. tabbing, hovering, single click event emulation are separate files that are added on top of the core event system and are not required for it's operation.
register-previewer
and register-finalizer
functions add global handlers...
>> ? register-previewer
USAGE:
REGISTER-PREVIEWER types handler
DESCRIPTION:
Register a previewer in the event chain; remove previous instances.
REGISTER-PREVIEWER is a function! value.
ARGUMENTS:
types [block!] {List of event/type words that this HANDLER supports.}
handler [function!] "func [space path event]."
>> ? register-finalizer
USAGE:
REGISTER-FINALIZER types handler
DESCRIPTION:
Register a finalizer in the event chain; remove previous instances.
REGISTER-FINALIZER is a function! value.
ARGUMENTS:
types [block!] {List of event/type words that this HANDLER supports.}
handler [function!] "func [space path event]."
>> ? delist-previewer
USAGE:
DELIST-PREVIEWER handler
DESCRIPTION:
Unregister a previewer from the event chain.
DELIST-PREVIEWER is a function! value.
ARGUMENTS:
handler [function!] "Previously registered."
>> ? delist-finalizer
USAGE:
DELIST-FINALIZER handler
DESCRIPTION:
Unregister a finalizer from the event chain.
DELIST-FINALIZER is a function! value.
ARGUMENTS:
handler [function!] "Previously registered."
Spec of global handlers follows that of normal event handlers.
For examples see tabbing.red
, single-click.red
, focus.red
A set of commands is available to each event handler, implementing the idea of REP 80. Compared to View actors, there's no risk of accidentally returning something we didn't want to and wreaking havoc upon the whole program and then making it freeze.
command | returned value | description |
---|---|---|
pass | N/A | tells that event should be propagated to the next handler (in case current handler does not want to process this event) |
stop | N/A | tells the opposite: event is processed and should not be passed to other handlers |
stop? | logic! | true if event was not processed yet |
stop?
pipeline deserves special mention
stop?: false
is set before calling previewers. Previewers can usestop
command to stop the event from reaching normal event handlersstop?: true
is set before entering every normal event handler, which may callpass
to pass it further. If it does not, event won't be passed to other normal handlers- finalizers may inspect
stop?
state to only react to events (e.g. keys - tabbing module only reacts to Tab presses not processed in other handlers) - previewers and finalizers are all called regardless of this flag's state (cannot be blocked)
Order is two-dimensional: outer before inner, then specific before generic...
E.g. if hittest returns [list-view 210x392 hscroll 210x8 thumb 196x8]
, and we reduce that to list-view/hscroll/thumb
then the order would be:
list-view
list-view/hscroll
hscroll
list-view/hscroll/thumb
hscroll/thumb
thumb
I.e. list-view
(outermost) gets an upper hand and it can stop the event from reaching it's children. Then hscroll
then finally thumb
(innermost). But event handlers written for list-view/hscroll/thumb
(specific) get precedence over thumb
(generic) when present.
In this example, if list-view/hscroll
handler calls pass
command, the event gets into hscroll
handler. Thus specific handlers may extend generic handlers and only care about their own aspects, passing evaluation further.
Path along the spaces tree comes:
- for pointer events from
hittest
function (called internally byhost
) - for keyboard events and focus/unfocus - from
spaces/keyboard/focus
value (but only space types listed inspaces/keyboard/focusable
can receive keyboard events) - for timer events - from the internal tree built up by
render
Define events for paths to ensure hierarchy.
E.g. if event is defined for list-view/hscroll/thumb
, thumb
space that receives it will be able to access hscroll
as path/-1
and list-view
as path/-2
and never worry that it might have been used inside another space. Another way to do that is define events for list-view
and inspect if path/2 = 'hscroll
and path/3 = 'thumb
. The choice is a matter of convenience.
Timer events handlers cannot block each other with stop
: they always get triggered (because hierarchy doesn't make sense for timers).
Previewers however can use the stop
command to stop the event from reaching all normal timer event handlers.
Current API (not yet mature enough) - start-drag
, stop-drag
, dragging?
, drag-offset
, drag-parameter
, drag-path
>> ? events/start-drag
USAGE:
EVENTS/START-DRAG path
DESCRIPTION:
Start dragging marking the initial state by PATH.
EVENTS/START-DRAG is a function! value.
ARGUMENTS:
path [path! block!]
REFINEMENTS:
/with =>
param [any-type!] "Attach any data to the dragging state."
>> ? events/stop-drag
USAGE:
EVENTS/STOP-DRAG
DESCRIPTION:
Stop dragging; return truthy if stopped, none otherwise.
EVENTS/STOP-DRAG is a function! value.
>> ? events/dragging?
USAGE:
EVENTS/DRAGGING?
DESCRIPTION:
EVENTS/DRAGGING? is a function! value.
>> ? events/drag-path
USAGE:
EVENTS/DRAG-PATH
DESCRIPTION:
Return path that started dragging (or none).
EVENTS/DRAG-PATH is a function! value.
>> ? events/drag-parameter
USAGE:
EVENTS/DRAG-PARAMETER
DESCRIPTION:
Fetch the user data attached to the dragging state.
EVENTS/DRAG-PARAMETER is a function! value.
>> ? events/drag-offset
USAGE:
EVENTS/DRAG-OFFSET path
DESCRIPTION:
Get current dragging offset (or none if not dragging).
EVENTS/DRAG-OFFSET is a function! value.
ARGUMENTS:
path [path! block!] {index of PATH controls the space to which offset will be relative to.}
events/
prefix is not needed inside event handlers.
An event handler is supposed to call start-drag
to focus subsequent pointer events on that same space, until this or another handler calls stop-drag
. An optional parameter can be passed with /with
, while drag-offset
is computed automatically.