-
-
Notifications
You must be signed in to change notification settings - Fork 12
A new user interface protocol and toolkit implementation
License
Shirakumo/alloy
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
# 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, see ``dots-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 use ``un``s wherever possible. For the scaling factors involved in computing pixels from a ``un``, see ``base-scale`` and ``resolution-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 by ``x`` and ``y``. - ``size`` The span of a construct in space, denoted by ``w`` and ``h``. - ``extent`` A delimited extent in space, denoted by ``x``, ``y``, ``w`` and ``h``. - ``margins`` The offset from the borders of a surrounding extent, denoted by ``l``, ``u``, ``r``, and ``b``. 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 requires ``leave`` 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 not ``renderable``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. 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. ### Layout Nodes Layout decisions are primarily made by nodes in the layout tree that are ``layout`` instances. For such objects, Alloy specifies a protocol to communicate an agreeable layouting between layouts and contained elements. While elements contained in a layout can report a preferred size, the actual layout decisions are left up completely to the respective layout implementation. When a layout node has to recompute the bounds of its direct children, for example because its own bounds have changed, the following steps must be performed for each child element of the layout: 1. The layout computes a suggested new size for the element according to the bounds of the layout node and its associated layouting strategy. 2. The layout calls ``suggest-size`` with the new size for the element. The element must then extend, contract, or otherwise change the suggested size as follows: 1. The element retrieves its sizing strategy object by calling ``sizing-strategy`` (See ''Sizing Strategies''(#sizing-strategies)). 2. The element calls ``compute-ideal-size`` with the sizing strategy object, the element and the suggested size to compute preferred or at least more agreeable size based o the suggested size. 3. The element returns the result of the above call as its response to the size suggestion. 3. The layout possibly adjust the layouting decisions to account for the element's preferred size. 4. The layout installs the final computed extent on the element by calling ``(setf bounds)``. A layout implementation may perform steps 2 and 3 multiple times before settling on a final extent, though it must guarantee to reach step 4 eventually. ``suggest-size`` 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 size is used at all or not however is still up to the layout. A layout //must// deal in ``px`` units. The size supplied to ``suggest-size`` and the extent supplied to ``(setf bounds)`` must only contain ``px`` units. An element is however allowed to use other units for the size returned from ``suggest-size``. 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. ### Element Nodes When an element's space requirements change, it must call ``notice-size`` in order to notify its parent of the change. For example, if an element decides that it needs more space, it should call ``notice-size`` 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. For some elements, the preferred size depends on the visual representation of the respective element. For example, a button that is represented as a label text surrounded by a border needs a total amount of space that consists of space for the text at the configured font style and space for the border with the chosen thickness and margins. The visual representation of elements in turn depends on the selected renderer since each renderer freely chooses representations for components. Furthermore, the layout module does not (by design) know anything about renderers and visual representations. These requirements are the reason for equipping elements with a (mutable) sizing strategy slot: Typically, the renderer installs a sizing strategy that works in conjunction with the visual representation into the slot when it realizes the visual representation of a given component. This way, the renderer can supply an appropriate strategy and layouts can query elements for the preferred size without undue coupling between the two. The sizing strategy of an element node can be read using ``sizing-strategy`` and written using ``(setf sizing-strategy)``. The sizing strategy object must be an instance of a subclass of ``sizing-strategy``. ### Sizing Strategies ! label sizing-strategies Sizing strategies are subclasses of ``sizing-strategy`` for which a specialized method on the ``compute-ideal-size`` generic function is defined. Sizing strategies will often be specific to a particular renderer. For example, for the presentations-based renderer, the size of a layout element may be derived from a single or multiple shapes. Specific strategies should be defined in the module of the respective renderer and installed via ``(setf (sizing-strategy layout-element) strategy)`` when the renderer "realizes" the component. The following basic sizing strategies and strategy combinators are built-in: - ``fixed-size`` Returns a user-supplied fixed sized, ignoring the size suggested by the layout parent. The fixed size must be specified using the ``:fixed-size`` initarg and can be read using the ``fixed-size`` reader. - ``dynamic-size`` Calls a user-supplied function to perform the size computation. The function must be specified using the ``:size-function`` initarg and can be read using the ``size-function`` reader. - ``dont-care`` Just accept and use the size suggested by the parent element. - ``at-least`` If the size suggested by the layout parent is smaller than a user-supplied minimum, enlarge the suggested size to the minimum size. The supplied minimum can be either a ``size`` or a ``sizing-strategy``. The minimum size must be specified using the ``:minimum-size`` initarg and can be read using the ``minimum-size`` reader. - ``fit-to-content`` This is a superclass for strategies that compute the size of the layout element based on the displayed content that the renderer uses to realize the component. Specialized methods on ``compute-ideal-size`` must be defined for subclasses since this class does not (and cannot) provide any default behavior. ## 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-size``. 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 - Grid Bag 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) # Support If you'd like to support the continued development of Alloy, please consider becoming a backer on Patreon: [ image https://filebox.tymoon.eu//file/TWpjeU9RPT0=, link https://patreon.com/shinmera ]
About
A new user interface protocol and toolkit implementation
Topics
Resources
License
Stars
Watchers
Forks
Releases
No releases published
Packages 0
No packages published