alloy
alloy copied to clipboard
A new user interface protocol and toolkit implementation
About Alloy
Alloy is a user interface toolkit. It is defined through a set of protocols that allow for a clear interface, as well as a standardised way to integrate Alloy into a target backend.
"The project is currently still in its design phase and large parts of it may change. Please wait warmly."(bold, size 20pt, red)
; TODO: I'd really like an index to be generated by Markless for this. Will need to think about that. Probably a custom instruction in cl-markless.
Examples
A set of simple examples for Alloy can be found in the examples
directory of the source tree.
Helping Out
If you are looking for tasks to help Alloy along, please see the various todo comments in the code base, open "issue tickets"(https://github.com/shirakumo/alloy/issues) on GitHub, and the "TODO file"(link TODO.mess).
Alloy Protocols
Alloy is structured as a family of protocols. This allows it to be very flexible, and allows you, the user, to put together the system in a way that fits your needs. The Alloy project (henceforth "the project"), consists of the following set of protocols:
- Core
- "Component"(link #Component)
- "Data"(link #Data)
- "Elements and Containers"(link #Elements and Containers)
- "Events"(link #Events)
- "Focus"(link #Focus)
- "Geometry"(link #Geometry)
- "Layout"(link #Layout)
- "Observables"(link #Observables)
- "Renderer"(link #Renderer)
- "UI"(link #UI)
- "Units"(link #Units)
- "OpenGL"(link alloy-opengl/index.html#protocol)
- "Simple"(link alloy-simple/index.html#protocol)
- "Presentations"(link alloy-simple-presentations/index.html#protocol)
- "Shapes"(link alloy-simple/index.html#shapes)
- "Transforms"(link alloy-simple/index.html#transforms)
- "Windowing"(link alloy-windowing/index.html#protocol)
The documentation in this document will focus only on the Core protocol. The Core protocol defines the fundamentals of Alloy, while the other protocols focus on extensions built around it. Note that the project also contains implementations of these protocols, not just the protocol definitions themselves.
The order of the protocols as follows is intended to give a clear understanding of each, only introducing further protocols if the ones they depend on have already been explained. If you need to jump to a specific section, please use the index above.
Units
Whenever we deal with real-world measurements we need to talk about units. In the case of a UI toolkit we are concerned with distances. Alloy offers a unit
type that encapsulates a numeric value and allows us to reason about various measurements. In the base protocol there are two absolute units and five relative units. Absolute in this context means that the unit can be translated to device units (typically "pixels") no matter the context it is used in. Relative in turn means that the device unit size is dependent on the context in which the unit is used in. The available units are as follows:
-
px
A direct representation of a number of device pixels. Note that this may still be subject to reinterpretation by the underlying rendering backend. However, as this is the base unit in Alloy, all other units will be subject to the same backend scaling factor in the end. -
cm
A representation of real-world centimetres. This should allow measuring things that correspond to an actual real-world extent. However, this translation depends on user-supplied data, seedots-per-cm
. -
un
The standard unit in Alloy.un
s are scaled relative to the user interface's target resolution and current, actual resolution. This allows the interface to scale up and down dynamically depending on the current resolution and preserve the layout. It is recommended that you useun
s wherever possible. For the scaling factors involved in computing pixels from aun
, seebase-scale
andresolution-scale
. -
vw
&vh
A fraction of the total view width or height. The view is the total visible area in which the renderer can operate, which typically either corresponds to the virtual screen size, the monitor resolution, or a single window. -
pw
&ph
A fraction of the parent width or height. The "parent" is a dynamically determined layout element with a logical extent. See "Layout"(link #Layout).
You can convert between units by simply passing the unit to the constructor of another, or compute directly with units by using one of the many math functions defined for units: u+
u*
u-
u/
umax
umin
u=
u/=
u<
u>
u<=
u>=
.
Often when needing to compute with units, the unit needs to be converted into some numerical value. To do so, use to-px
, which will return an absolute pixel representation of the unit. Beware however that this is subject to the current parent, and conversion without an active parent will signal an error. The unit parent should be bound dynamically with with-unit-parent
.
Units are immutable and are cached or constructed at compile time wherever possible. It is safe to dump them to FASLs, too. While two units of the same type and with the same value may be eq
, this is not guaranteed. To ensure unit size equality, use u=
.
Geometry
While units give us the tool to denote measurements, the geometry protocol gives access to a set of tools to describe and operate on geometric data. All of the geometry in Alloy is based on a two dimensional Cartesian coordinate system. All of the measurements in the geometrical structures are expressed in terms of unit
instances. Specifically, the following structures are available:
-
point
A singular position in space, denoted byx
andy
. -
size
The span of a construct in space, denoted byw
andh
. -
extent
A delimited extent in space, denoted byx
,y
,w
andh
. -
margins
The offset from the borders of a surrounding extent, denoted byl
,u
,r
, andb
. Positive measurements decrease the extent, negative measurements increase the extent.
The default constructors of these structures take unit instances, or a real number that is interpreted as a un
unit. There are alternate convenience constructors that create the structures from device pixel units. These constructors are simply prefixed by px-
(px-extent
, etc). In the other direction, convenience accessors for device pixels of all the coordinates are also available with a px
prefix (pxx
, etc).
Just like units, geometrical constructs are immutable and may be constructed at compile time, or emitted into FASLs. To compare them, you should use the respective type's comparison function (extent=
, etc).
A couple of extra functions exist for convenience purposes, such as destructure-margins
and destructure-extent
to easily deal with all of the fields, contained-p
to check for inclusion, overlapping-p
to check for intersection, extent-intersection
to compute the intersection, and ensure-extent
to coerce any structure into an extent
.
Elements and Containers
In order to abstract away a number of traversal operations, Alloy offers the basic element
and container
classes. A container contains a number of elements and acts similar to a sequence. Whenever a hierarchy is composed in Alloy, it is made up of elements and containers. The following operations are defined on containers:
-
enter
Enters a new element into the container. Where and how the element is inserted is up to the container. A container may specify additional keyword arguments that influence the element's positioning or other metadata the container might have. -
leave
Removes the element from the container. -
update
Changes the metadata of the element and possibly its position within the container. A container may specify keyword arguments that let the user change the element's positioning or other metadata. -
element-count
Returns the number of elements currently contained in the container. -
elements
Returns a sequence of all the elements contained in the container. You may not modify this sequence. -
element-index
Returns the current index of the element within the container. This index may change if elements are entered, left, or updated within the container. Note that the index is not required to be numeric. -
call-with-elements
/do-elements
Repeatedly calls the supplied function with successive elements. The start and end indices may influence the region of iterated elements. A container may ignore the start and end indices if they are not applicable. -
clear
Leaves all elements from the container. This requiresleave
to be called on every element in the container, though the order is unspecified.
Renderer
Alloy is a graphical user interface, and so rendering of the interface plays an important role. In the Core protocol, rendering is extremely simplified, in order to allow the backend the greatest amount of flexibility and control. In fact, it is so simple that components (See "Components"(link #components)) could be just representations of widgets or controls in another UI toolkit.
The renderer protocol is based around a set of generic functions and two classes:
-
renderer
Any rendering backend must provide a subclass of this that is responsible for visually presenting elements in some way. -
renderable
Any element that should be drawn must be a subclass of this. It is illegal to attempt to render objects that are notrenderable
s unless backend explicitly allows it.
The protocol is split into two sections, the first dealing with resource allocation, and the second with the control of visualisations.
Rendering Resource Management
Before a renderable
can be visualised with a renderer
, the register
function must be called to inform the renderer of the renderable. The user must add methods to this function for both renderables and renderers, as appropriate. Specifically, any renderable that contains child elements that should be renderable, must also call register
on its child elements when it itself is registered. The register function may be called at any point.
Before any visualisation at all can be done, the allocate
function must be called with the renderer. Calling this function multiple times should have no further effect. The renderer is encouraged to defer allocation of resources that pop up during register
calls until this point, unless the renderer has already been allocated before. The user is encouraged to call allocate
at a strategic point where it is permissible for loading pauses to occur. A renderer may signal an error of type allocation-failed
if it is currently impossible for the renderer to perform rendering actions for whatever reason.
When deallocate
is called the renderer should free all resources it can. This returns the renderer to the state before allocate
was called for the first time, but does not influence the elements known to the renderer via register
. A renderer may deallocate itself in case a critical failure occurs that prevents it from operating further.
; TODO: Deregistering / deallocating of resources
Renderer Visualisation Control
Visualisation of elements is done via render
. When render
is called, the renderer should perform whatever steps necessary to render the given renderable. The behaviour is undefined if allocate
was not successfully called prior to this, or the renderer was not notified of the renderable via register
. The user is allowed to provide non-primary methods to further customise the rendering behaviour. The user is not allowed to provide primary methods unless the renderer protocol used specifically permits it.
During rendering the renderer must only visualise things if the region to visualise is within the visible-bounds
of the renderer. These bounds can be dynamically constrained via call-with-constrained-visibility
/with-constrained-visibility
. Whether an extent is visible or not can be checked via extent-visible-p
. Specifically, if an extent is partially visible, the renderer must only render the part of the extent that is fully within the visible bounds.
If the renderer supports partial updates, the user is encouraged to call into the rendering machinery via maybe-render
instead. Unlike render
, maybe-render
will silently traverse the hierarchy and only invoke render
on an element if the element was previously marked with mark-for-render
. After render
has been called on a renderable, render-needed-p
will always be NIL
. The user should always call mark-for-render
if any property of a renderable was changed that would change its visual representation.
Focus
In alloy there is a notion of a "focus tree" -- a hierarchy of elements that designates how the focus flows between elements. Focus in this case refers to how important an element currently is. A focus-element
in alloy can have three states; NIL
for no focus at all, :strong
for when it is fully focused, and :weak
for when it should be considered for strong focus.
Within a focus-tree
there must always be exactly one element with strong focus, but there may be many elements with weak or no focus. Every element in a focus tree has exactly one parent element. For the element at the root of the focus tree, this is the element itself. Focus may flow inwards and outwards, meaning that a strongly focused element may pass the strong focus to a child element (activate
), or it may pass the focus to its parent (exit
).
An element may also be strongly focused directly, referred to as "focus stealing".
Within a focus-chain
-- an element that can contain child elements -- only one of its direct children may have strong or weak focus. This is used when the focus-chain is activated, to determine which of the child elements to give strong focus to. Each child also has a direct successor and predecessor. There may be additional ties between child elements that offer more intuitive navigation, but this basic connection is always present.
Note that there is no necessary visual correspondence to the way focus moves between elements. This is important as elements may have a certain visual grouping, but the ideal way focus travels between the elements may not be directly encoded in this grouping.
An element may only be contained in one focus chain at a time. Attempting to enter
an element into multiply focus chain before leave
ing it will signal an error.
More specifically, within a focus-tree
the following invariants must be upheld at any time:
- There must always be exactly one strongly focused element
- All predecessors of the strongly focused element must be weakly focused
- An element may only be weakly focused if:
- Its immediate predecessor is strongly focused
- Or one of its successors is strongly focused
- If an element is an immediate successor of another, the other element must be its
focus-parent
.
Events
Alloy is a retained mode toolkit where you construct an interface, which then reacts to changes in the environment. These changes are communicated via events. When an element handle
s an event, it can either decide to handle it and perform whatever action necessary to do so, or call decline
in order to allow the event to propagate to an element that might want to handle it instead. The behaviour of this propagation is distinguished between the following two types of events:
-
direct-event
Direct events are events without geometric information and are handled by being directed to the element that currently has strong focus, and then bubble outwards in the focus hierarchy if the handling is declined. -
pointer-event
Pointer events are events that have a specific associated location. They are first directed to the element with strong focus similar to direct-events, but if declined will bubble inwards from the root element until the last element that geometrically contains the point is found.
Alloy contains a variety of event classes to describe general user interface changes. These events are loosely grouped into either being specific or descriptive. Specific means that the event describes a particular hardware action directly, such as a key press. Descriptive events on the other hand may be translated from a variety of hardware actions and are used to describe a particular action in the interface, such as focusing the next element.
Descriptive events allow you to write the user interface interaction in a more action-oriented way, which allows the end-user to decide how to map physical buttons and gestures to the interactions they want. This is important for accessibility, internationalisation, and customisation.
; TODO: Translation mechanism, more descriptive events
Layout
Layouting in alloy refers to the decisions made to determine where elements are positioned in space and how large they are. In other words, it's the mechanism to determine the extent
of each element that should be rendered. Similar to focus trees, there are "layout trees" in Alloy -- hierarchies of elements that govern the layouting decisions.
Every element in a layout tree has exactly one parent, with the element at the root having itself as its own parent. Every element also has a bounds
that determines its axis aligned bounding box. When rendered, the visual representation of the element should not exceed this extent.
Layout decisions are primarily made by layout
instances, for which Alloy specifies a protocol to communicate an agreeable layouting between elements. The actual layout decisions are left up completely to the layout implementation.
When an element's bounds change, it must call notice-bounds
in order to notify its parent of the updated bounds. If an element decides that it needs more space, it should call notice-bounds
to ensure a consistent layout. Typically this will result in a standard layout update being run, same as when the layout instance itself changes bounds.
When a layout's bounds change it may recompute the bounds of its direct children. It must do so in the following steps for each of its child elements:
- Compute the new bounds for one element.
- Call
suggest-bounds
with the new bounds for the element. The element must then extend, contract, or otherwise change the suggested event and return a new extent that it finds more agreeable than the last. - Adjust the layouting decisions to account for the element's preferred extent.
- Force the final computed extent on the element by calling
(setf bounds)
.
A layout may perform steps 2 and 3 multiple times before settling on a final extent, though it must guarantee to reach step 4 eventually. suggest-bounds
is primarily used to handle the case of nested layouts or other kinds of elements that may need to shrink or expand to fit their contents. Whether the element's preferred extent is used at all or not however is still up to the layout.
A layout //must// deal in px
units. The extent supplied to suggest-bounds
and (setf bounds)
must only contain px
units. An element is however allowed to use other units for the extent returned from suggest-bounds
. However, be aware that the absolute size of units depends on the currently bound unit parent (See "Units"(link #units)). The layout //must// set this parent to itself when resolving units.
Some layouts may temporarily hide elements or regions from view. In order to force a region to be visible, the function ensure-visible
can be used. This function will traverse upwards to ensure that every layout along the way makes the desired region visible as best possible.
An element may only be contained in one layout at a time. Attempting to enter
an element into multiply layouts before leave
ing it will signal an error.
UI
Since layout trees and focus trees are disjoint, there needs to be a way to tie them together, including any other global information necessary. For this, Alloy has the UI
object, the main entry point once the interface has been constructed. It has a layout-tree
and focus-tree
, as well as provides access to the global unit scaling factors, dots-per-cm
, target-resolution
, resolution-scale
, and base-scale
.
Once you've constructed a UI instance, you should be able to add elements to its focus tree and layout tree, set the desired "native" resolution, and finally render
it, handle
events, or change the effective resolution with suggest-bounds
.
If you would like to switch out the layout or focus hierarchies on the fly, you can set the root
of either tree instance.
; TODO: It might be better to have the slots be different and inherit from focus-tree/layout-tree to remove the indirection.
Observables
In order to allow parts of the system to react to changes that happen elsewhere, Alloy implements an observation protocol. Any object that can be observed for changes must be an observable
. Observations happen based on functions to observe. When an observable function is called with an observable instance, a set of functions that observe this combination is called with the same arguments as the original function call.
An observable may either have observations fired automatically on generic functions that have been defined with define-observable
or made observable with make-observable
, or it may manually fire observations with notify-observers
.
New observers can be added with on
or observe
, and managed with remove-observers
and list-observers
.
; TODO: Might be better to have observables as a separate library, with more of the basic data types reimplemented.
Data
Alloy is created around the idea that the data you present in your interface should be decoupled from the elements that present it. However, to provide standardised interfaces to the data, and to express the requirements for data structure and metadata an element might have, Alloy provides a Data protocol.
The base protocol is very light, though it is expected that elements add further constraints to the protocol in order to express their needs. The basis involves a data
class, from which any data representation should inherit. Every data representation object is observable, to allow the interface to respond to changes.
In order to obtain the most appropriate data representation instance for a place, use place-data
. The user is encouraged to provide additional methods on expand-place-data
and especially exand-compound-place-data
if they add new data representation types.
Note that as long as an object is observable, and the generic functions and observable places as required by the element's data protocol are implemented for the object, the object may be used as a data representation object directly.
Component
Representing user interactions happens through Components. Components are "leaf elements" and should not contain any further elements. Instead, if something should be made up of different interactions, it should be modelled as a combination of layouts, focus chains, and components. Every component is tied to a data
instance that provides the data to visualise and the metadata to determine the interaction constraints.
Being an interactable leaf element, a component is both a layout-element
, a focus-element
, a renderable
, and an observable
. Particularly, it is possible to observe any component's focus and size changes, and react to them remotely. Specific components may offer additional interactions, though typically it is more apt to observe the changes on its data object instead.
Multiple components may share the same data instance and changes between them will update automatically. This allows representing the same information in multiple places, potentially in different ways simultaneously.
Components are typically created for a place or data instance through represent
and represent-with
. Alloy can try to pick the component type for a data type automatically by using T
for the component type. In this case the actual component type to use is resolved via component-class-for-object
.
Standard Implementations
Aside from the protocols, Core provides a set of standard implementations of the protocols that should fill a lot of the needs for an interface.
- Components
- Button
- Combo
- Icon
- Text Input
- Label
- Plot
- Progress
- Radio
- Scroll
- Slider
- Switch
- Data Representations
- Place Data
- Slot Data
- Aref Data
- Computed Data
- Focus Chains
- Focus List
- Focus Grid
- Layouts
- Border Layout
- Clip View
- Fixed Layout
- Grid Layout
- Linear Layouts
- Observables
- Observable Object
- Observable Table
- Structures
- Query
- Scroll View
- Tab View
- Window
Note that Core does //not// provide any standard implementations for renderers. Rendering is a very involved and complex process, and as such is left up to secondary systems and protocol extensions.
Observables
Data Representations
Focus Chains
Layouts
Components
Button
Combo
Icon
Text Input
Label
Plot
Progress
Radio
Scroll
Slider
Switch
Structures
Query
Scroll View
Tab View
Window
Project Systems
Aside from this Core, the project also includes several other systems that fill or extend parts of Alloy.
- "Constraint Layout"(link alloy-constraint/index.html)
- "GLFW"(link alloy-glfw/index.html)
- "OpenGL"(link alloy-opengl/index.html)
- "SVG"(link alloy-svg/index.html)
- "Simple"(link alloy-simple/index.html)
- "Simple Presentations"(link alloy-simple-presentations/index.html)
- "Windowing"(link alloy-windowing/index.html)