IVD/README.md

501 lines
19 KiB
Markdown
Raw Normal View History

# IVD - Interactive Visual Design Language
IVD is a declarative GUI programming language and framework implementation. The goal is to make it so that you can describe *how* a interface should work and leave the rest to the computer, facilitating highly dynamic GUI programming, whilst maintaining maintainability.
# Why?
The decision to create IVD was not reached lightly. I knew it would be a huge undertaking (Although I underestimated how huge...), and spent considerable time trying to talk myself out of doing it.
Aside from looking at all the GUI toolkits I could find before I broke ground, I had experienced developing user interfaces in the classical way (Qt/GTK), the "modern" way (HTML/CSS/JavaScript), and played around a bit with QML (which inspired some aspects of this project). All of these had their pros and cons, but none of them seemed to represent a true advancement in the problem space, and I'm arrogant enough that I thought I could maybe move the needle myself.
# A Quick(ish) and Mostly Incomplete Rundown of IVD
## IVD is Not Ready
IVD is still in heavy development and as such this readme should be treated as a writeup of the project, rather than as a guide on using it. It's okay if you don't get the syntax completely, it's not written so that you are supposed to; just read on and the high level ought to stand out at least.
## IVD is *Not* CSS++
Before we get started, it's important to acknowledge that although the syntax is superficially similar, the theory of IVD is *very* different from the theory of CSS. Elements in IVD aren't bound to models by default, and they don't inherit attributes from their "parents" (Because they don't have static parents). About the only concepts that carry over from CSS are some attribute names (Although I've renamed some that are otherwise equivalent, just to piss you off), and the box model for styling. Most everything else is either taken more from traditional GUI toolkits or is novel.
IVD allows for visual elements to be free and not bound to the model, so that your data model isn't corrupted with irrelevant noise which only exists for the presentation, as is always the case with `<DIV>` tags in any reasonably complex webpage. As such, it allows your data to be truly semantic.
CSS is for *styling* models, and requires something like HTML and JavaScript to create a GUI with it. IVD on the other hand is for *defining* complete user interfaces with [*style*](https://i.redd.it/vq2q5dqh16qy.png).
## Events Kinda Suck, State System to The Rescue
One great problem I think is the low level nature of typical GUI events. Take for example the case of an element needing to style itself differently when hovered in a scroll area. A naive approach involves monitoring "mouse motion in" and "mouse motion out" events, and setting your internal hover state accordingly. This is all fine and good until you have the cursor hovering over an element, and without moving the mouse, the user scrolls, suddenly throwing the hover out of sync.
It's easy enough to fix said issue, but even easier to break again because the solution isn't intuitive. It's fun to watch applications gain and lose this bug as they go through updates. The idea is to leave these tricky edge cases to the GUI framework, instead of fixing and breaking (solved) problems like this all the time while in the middle of trying to fix something completely unrelated.
The problem with individual widgets processing events is that they have no context (At least at the level that your typical GUI events exist, it is certainly possible to have higher level events, but IVD as a language has other benefits as well). In IVD, the environment is responsible for determining whether an item is hovered or not (Which was in part inspired by CSS's pseudo-classes):
#element-name
{
color: red;
state this.IVD-Item-Hovered:
color: blue;
state ::.IVD-Mouse-Clicked:
color: green;
state ::.IVD-Mouse-Clicked & this.IVD-Item-Hovered:
color: purple;
}
Unlike CSS's pseudo-classes, IVD has a very powerful state system and allows boolean comparisons on states:
state this.aState & (anotherState | !stateeee):
"Overlapping" attributes, where two active states define a different value for a given attribute, override in descending order:
# //anonymous element because you shouldn't be forced to name things you don't want to
{
attr: one;
state sky-is-blue:
attr: two;
state grass-is-green:
attr: three; //"three" is chosen by the runtime
}
Elements can manipulate states, even in other elements:
#an-element
{
state coordinatedState:
color: blue;
}
#
{
state aState:
induce-state: an-element.coordinatedState;
//Can also toggle, unset, etc
toggle-state: state;
unset-state: state;
}
Event-like behavior is handled by "trigger-states". These are states that are guaranteed by the runtime to last only a single frame. This allows you to set processes external to IVD in motion using the `trigger` attribute:
#
{
state this.clicked:
trigger: IVD-Core-Quit;
}
Say you have a group of states, and only one can be active at any given time. It could be tabs, or a radio-box selection, etc. IVD can manage the exclusivity for you:
#
{
radio-state: one, two, three;
state one:
state two:
state three:
}
And then only the last state to be set will be active in that element at any given time.
## Positioning
Widget hierarchies tend to be very ridged. In traditional GUI toolkits, this is because each widget "owns" it's children, and reparenting is an arduous task. HTML isn't much better, where even if you use JavaScript to restructure the DOM, it still has a specific default defined in a very different way from how your dynamic structure is defined.
This makes it difficult to create GUIs that are easy to re-arrange.
The two points that make IVD different here are that elements are completely symmetric, and the fact that the visual element's structure isn't tightly bound to a model. When in use, a model's hierarchy only applies locally, and is largely optional (The element hierarchy doesn't necessarily have to map 1:1 to the model). [More on models below](#models).
IVD's structure being symmetric just means that positioning is always conditional. An element must choose to position itself within another element. It always starts out flat:
#
{
state some-condition:
position-within: window;
state some-other-condition:
position-within: another-element;
}
## Layouts/Materials
One place where traditional toolkits beat HTML/CSS I think unequivically, is in their layout system. Floats... Are unnecessarily complicated to reason about, and enough tears have been shed over that system that I feel I need not ever speak of it again.
IVD follows a typical nested layout system. Built in layouts include hbox and vbox, for horizontal and vertical rows respectively, and the stack layout for layering.
Given the following example:
#window
{
position-within: Environment; //Give us a window
layout: hbox;
}
#
{
position-within: window;
text: "Aye";
}
#
{
position-within: window;
text: "Bye";
}
#
{
position-within: window;
text: "Cye";
}
The following is produced (please excuse the crudity of this model, I didn't have time to build it to scale or paint it):
[AyeByeCye]
That's great, but then the order is almost arbitrary (It's actually based on the order of element declaration but bear with me). To reserve specific "slots", we have a feature called "named-cells":
#window
{
position-within: Environment; //Give us a window
layout: hbox;
named-cells: first, middle, last;
}
#
{
position-within: window.last;
text: "Cye";
}
#
{
position-within: window.first;
text: "Aye";
}
#
{
position-within: window.middle;
text: "Bye";
}
Which produces the same output:
[AyeByeCye]
This makes it painless to slip a column in between two elements later in the development cycle, without touching anything that already works. Empty cells are simply ignored, so they're great to declare where a notification or context popout should appear when it feels like it.
Of course, it also makes it trivial to rearrange items:
#window
{
named-cells: first, middle, last;
state a-state:
named-cells: middle, first, last; //Please use better names tho
}
Which would produce:
[ByeAyeCye]
The built-in layouts should cover 95% (totally legit stat) of use cases simply enough. But for special cases you can extend IVD with custom materials.
## Models
A model may define a hierarchy, but elements bound to specific model items don't necessarily have to reflect it directly, and are free to position items in a root element or separate window, for example.
The IVD philosophy is that a model shouldn't *ridgedly* define the presentation. Contrast with HTML where absolutely everything is a part of the DOM in a very specific order and hierarchy, even unrelated models must be encoded in an arbitrary order.
There are two types of elements in IVD. "Free elements", which are not bound to a model and there is exactly one instance, and "enumerated elements", which are bound to a model instance, and there can be zero or more of them (one for each instance).
In the previous examples, you've seen elements declared as such:
#name-optional {}
This instantiates a single element.
Suppose you have a model uncreatively named `my-model`:
#an-enumerated-element -> my-model {}
This creates a single instance of `an-enumerated-element` for every item in `my-model`. A "model" simply declares a list of model items. The process of binding elements to model items is known in IVD parlance as "enumeration", as elements are *enumerated* by the model.
A model item can define strings, integers, trigger slots and states. All of which may be used by elements in IVD:
# -> model-name
{
text: model.the-text;
state model.a-state:
width: model.width-for-a-state;
state this.clicked:
trigger: model.react-to-click;
}
References to the model within an element are prefixed with the keyword `model` and not the model's identifier because it allows you to easily rename the model, use generic models in classes, and because I felt like it.
Values from a model item that are used by IVD are always kept in sync. If the value changes in the model, the change is reflected in the IVD runtime.
Model states can be manipulated directly by IVD code as well:
# -> arbitrarily-named-model-42
{
state x:
induce-state: model.a-model-state;
}
Models themselves can contain child items, allowing for complex nested data structures.
You can't position a free element within an enumerated element, you can however, position an enumerated element within a free element, or position an enumerated element within another enumerated element which share a model in common:
#Nietzsche -> model-name;
# -> model-name
{
position-within: Nietzsche;
}
The runtime will find the correct instance of `Nietzsche` to position the anonymous element within.
This actually works with any common ancestor, suppose the following:
#an-elephant -> elephants
{
layout: vbox;
}
#leg -> elephants::legs
{
position-within: an-elephant;
}
This enumerates an element for each instance of `elephant` and each instance of `elephant::legs`. Notice that before the `position-within` attribute is set, the item `leg` has no visual relationship to `an-elephant`. Common model deduction is what allows you to position elements enumerated across a model, within elements enumerated by *that model's ancestor*.
Suppose you want ordered items (As one often does). Perhaps the model has triggers defined for sorting the model according to different criteria. The order of elements in IVD can be bound to the model order:
#an-elephant -> elephants
{
layout: vbox;
model-order: enable; //Sort child elements according to model
}
#leg -> elephants::legs
{
position-within: an-elephant;
}
The plan is to be able to rearrange items in IVD, and then have the new order backpropogated to the model as well.
## Equation Solver
IVD allows you to define scalar constraints as equations which are kept up-to-date automatically:
#element1
{
width: other-element.height * 2;
}
The trouble with the above, is that it isn't flexible. What if you just really wanted for `element1.width == 200` to be true, but without violating the constraint?
#element2
{
state I-want-to-set-something:
set: element1 = 200; //error because it doesn't know what to do
}
Wouldn't it be nice if you could get IVD to figure out what `other-element.height` needs to be in order for `element1.width == 200` to be true?
#element1
{
width: [other-element.height] * 2; //Declare which value in the expression is weak
}
#element2
{
state I-want-to-set-something:
set: element1 = 200; //Works, because now it knows to solve for other-element.height
}
What happens in the above is that the expression in `element1.width` is solved for `other-element.height`, and the result is backpropogated to `other-element.height` (Which may be defined as a variable or an expression with a weak value, it can be turtles all the way down). Once that is updated, the expression in `element1.width` is reevaluated.
It's important to think of this as more of a suggestion than an absolute order. `other-element.height` might have a min/max constraint which rounds off the value being propogated, and then ***that*** value is what is observed when `element1.width` is reevaluated. Nothing is ever left in an inconsistent state.
Scalars declared by a model can be back-propogated to as well:
#element1 -> my-model
{
width: [model.a-val] * 2;
}
Everything always obeys the constraints as defined, but you also have the power to update the observed values arbitrarily, giving you the best of both worlds.
## Classes
Classes are very simple. They're really just templates from which attributes are copied:
.class-name
{
color: blue;
}
# : class-name
{
//color is blue
}
Shorthand version if you don't need to declare anything in the body of the deriving element:
# : class-name;
An element can derive from an arbitrary number of classes, and the attributes are overriden in the order that the classes are declared:
.another-class
{
color: yellow;
state ::.IVD-Mouse-Motion:
color: green;
}
# : another-class, class-name
{
//color is blue
state ::.IVD-Mouse-Motion:
//color is green
}
# : class-name, another-class //Class list order different
{
//color is yellow this time
state ::.IVD-Mouse-Motion:
//color is still green because it's not in conflict
}
## Remorial Classes for Code Reuse
And last but *certainly* not least.
Suppose you have the following construct:
#contextual-dialog
{
layout: vbox;
named-cells: message-cell, input-cell, confirmation-cell;
}
#message-area
{
position-within: contextual-dialog.message-cell;
layout: hbox;
named-cells: image-cell, text-cell;
}
#message-image
{
position-within: message-area.image-cell;
image: "image-uri";
}
#message-text
{
position-within: message-area.text-cell;
text: "Enter info below";
}
//etc just use your imagination for the confirmation cell
Everything is fine and good until... You need to reuse that. You can't simply use a class because a class only helps with a single element, and the above can only work with several elements.
Remorial classes are a way of defining a "composite" element. You define the class as normal, and then attach "remoras" (get it?), which are just a special kind of element to it. Whenever an element derives from this class, a copy of each remora is also spun up as well, facilitating reuse of complex elements.
And the syntax is quite simple:
.contextual-dialog
{
layout: vbox;
named-cells: message-cell, input-cell, confirmation-cell;
}
@contextual-dialog.message-area
{
position-within: @.message-cell;
layout: hbox;
named-cells: image-cell, text-cell;
}
@conceptual-dialog.message-image
{
position-within: @::message-area.image-cell;
image: "image-uri";
}
@conceptual-dialog.message-text
{
position-within: @::message-area.text-cell;
text: "Enter info below";
}
//etc
# : contextual-dialog;
This is equivalent to the previous example, except that it is reusable, of course.
Where the remora operator (`@`) is used within an element, it is substituted by the actual instance name given to the element which derives from the remorial class (Which is auto-generated in the above example as it's an anonymous element).
One special feature of remora substitution is that you can address any remora in the "school" (get it???) from any other element in the school using the `@::element-name` syntax, which is substituted with the name generated for it by the compiler. This allows for a remorial class to export values from a child remora (see /src/tests/valid/remoravaluekeysubstitution.ivd) which are all otherwise unaddressable, but the implications of this are outside the scope of this little hoe-down.
The remora example above is obviously incomplete. The biggest failing is that it really should be bound to a model in order to have a place to actually send input data and triggers for buttons.
Remoras work with nested models, and common parent deduction and all that good stuff as well. They're just an additional type of template which tag alongside otherwise normal classes.
Again, remoras are just syntactic sugar, they are expanded by the compiler. The resulting elements are exactly the same as if they had been defined manually. A little bit of witchcraft and maybe some symbol substitution makes it all come together quite nicely~
## But That's Not All!
This is by no means a complete overview of the features developed or in development for IVD. We haven't even mentioned the (working!) animation system or the ability to declare expressions (which is to allow for complex widget interactions such as scrollbars or sliders affecting viewports, all defined within IVD), or the C (see /src/user_include/IVD_c.h) and C++ (see /src/user_include/IVD_cpp.h) bindings... It is simply meant to give you a taste for the project.
# What's Working?
In no particular order:
- The compiler, which produces friendly error messages.
- Remorial class instantiation.
- Basic element styling.
- States and state expressions.
- The layout/material system.
- Text layouts.
- Models, enumeration, common ancestor deduction, etc.
- Animations of arbitrary scalar attributes.
- The equation solver (Not tested thoroughly enough for my tastes though).
- And a bunch of other stuff, probably.
# Contributing
\*sounds of deranged cackling echo in the distance\*
please help
# Credits
Created and developed by Tracy Rust (tracy@enesda.com).
# License
IVD is licensed under the terms of the LGPL-3.0-only