Another Year With Conrod

Conrod has been getting some painstaking love and attention over the past few months, and considering it has been about a year since the last post I thought it was about time for a quick update!

Imgur a pic of the classic, useless all_widgets.rs example

In summary, the main changes are:

  • Working with stable rust and on crates.io
  • Conrod now supports custom widgets!
  • We landed a widget_ids! macro for automatically generating unique WidgetIds.
  • A massive list of new positioning methods.
  • Tabbed canvasses, draggable canvasses, auto-layout canvas splits (see the canvas example).
  • Themes.
  • Generalised opt-in scrolling (have a widget automatically become scrollable if its children occupy a greater area than its scrollable area).
  • Changed our internal cache datastructure from a Vec to a Graph.
  • Much faster performance (still a lot of room for improvement).

Custom Widgets

Conrod now properly supports custom widgets via the Widget trait. This means it should be easy to build and share third party widgets and have them all work together within the same conrod Ui instance.

All of the widgets that come with Conrod are implemented using this trait so they should make for decent examples. jarret was also kind enough to contribute a heavily annotated example implementing a custom circular button widget (see it here).

widget_ids!

As conrod is an immediate mode GUI framework, it requires that each widget instantiation has its own unique ID. this ID is used to look up cached state within the Ui between updates.

In the past, users have been required to do something like this:

const SLIDER: WidgetId = 0;
const DROP_DOWN_LIST: WidgetId = SLIDER + 1;
const TOGGLE_MATRIX: WidgetId = DROP_DOWN_LIST + 1;
const LABEL: WidgetId = TOGGLE_MATRIX + ROWS * COLS + 1;

which was a pain. Now with the widget_ids! macro the above example would look like this:

widget_ids! {
    SLIDER,
    DROP_DOWN_LIST,
    TOGGLE_MATRIX with ROWS * COLS,
    LABEL,
}

Not only is this much easier and more concise, it is also a lot safer when refactoring or adding/removing widgets.

Positioning Methods

The idea here is to have an extensive enough list so that a user should never need to place a widget using absolute coordinates. Although we still might have a way to go, I think we are getting close!

You can peruse the whole list here.

There are a few different “categories” of positioning methods here:

  • Directional (down, right_from, etc). These describe positioning a widget in some direction either relative to the last set widget or some given specific widget.
  • Placement (middle_of, top_left_of, etc). These are for placing a widget at some place on top of another widget. These are often useful when placing a widget on top of some Canvas widget for example.
  • Alignment (align_top, align_left_of, etc). These allow you to align your widget with either the last set widget or some given specific widget.
  • Absolute (xy, point). Position a widget with absolute coordinates.
  • Relative (relative_xy, relative_to, etc). Set the position of a widget with absolute coordinates relative to some other widget.

Canvasses

These are widgets whose primary purpose revolves around assisting in the positioning of other widgets.

Split is a wrapper around the Canvas widget which automatically calculates their layout by segmenting the window into a tree of splits. This was heavily inspired by eddyb who did something similar with his IDE project.

Tabs is a widget that internally generates a Canvas for each identifier given, and only displays the last that was selected.

Widgets also offer a floating method for becoming “detached” from the gui graph and making them float above other widgets in a draggable manner (useful as a pop-up or alert window). This is particularly useful on the Canvas widget.

Demonstrations of all of these can be found within the canvas.rs example.

canvas.rs

Themes

Writing your own Theme allows you to specify all your default styling and layout behaviour, saving you from calling the same styling methods on widgets again and again. We don’t have any proper examples for writing custom themes yet, but you can see the theme module here.

Scrolling

.scrolling(true)
.horizontal_scrolling(true)
.vertical_scrolling(true)

These methods allow for opt-in scrolling on widgets. For example, if you were to call .vertical_scrolling(true) on a Canvas widget, it would automatically calculate the height occuppied by the Canvas’ children widgets, compare it to the height of the scrollable area and determine whether or not the area should become scrollable in order to fit all child widgets.

These methods are particularly useful to call on Canvas widgets, or any widget that is designed to be a parent widget of many other child widgets.

Performance

We’ve added a .draw_if_changed method which only re-draws all widgets if there has been some visual change to one or more of the widgets (or if needs_redraw has been called manually). This took the all_widgets example from about 25% cpu down to ~7% on my mbp in –release mode. We have some ideas for optimising this further but haven’t had time yet. There are also still a few unnecessary allocations within the Graph’s Element construction that should be easily removed once time permits.

We’ve also included .element and .element_if_changed methods for producing the entire renderable GUI as a single Element. This allows for easy interop with the elmesque graphics layout crate and in general a little more control over the rendering of your GUI.

TODO

Some things that come to mind are:

  • Write a Guide! (issue here)
  • Support setting widgets in either update or render stages, rather than forcing the user to do so in the render stage (issue here).
  • Multi-line text widgets (for both viewing and editing) (issue here, here, here and here).
  • Context Menu (aka Clickdown menu) (issue here).
  • Have custom widgets be able to add child widgets without occupying the public WidgetId space. The main step in this was implementing the new internal graph cache datastructure, now we just need a method.
  • Support for images in general (on buttons, toggles, viewers, etc). The main problem is that I am currently unsure the best way to generically support this.
  • Lots more stuff.

Demo

Here’s a little Synth Editor I made using Conrod and some of the RustAudio crates. Keep in mind it’s a little old, and was around before tabs and generalised scrolling (both of which I would have used had I done it today).

Contribute

We also have a milestone for what we’d like to see in a 1.0 conrod release. If you’re interested in contributing please drop by - any help is more than welcome!

Come and visit conrod!

Piston 0.5 Released

The Piston core is now 0.5 and includes some breaking changes:

  • The event crate is merged with the input crate
  • The event_loop is reexported as a module under piston

If you are using Piston-Window these changes will not have much effects.

To fix your code, replace use piston::event::*; with use piston::input::*;.

The motivation for this change is to make it easier to maintain generic libraries by depending on pistoncore-input only. In many cases you do not need a window abstraction to share code between projects. Such libraries include controllers, AI and UI.

Path Semantics

During the Piston project, I (bvssvni) have developed a mathematical notation for reasoning about API design, which I figured out could be used for theorem proving. I have tried to understand the semantics and how it relates to dependently typed programming languages.

What is path semantics?

In a dependently typed language, you have to encode the “proofs” into the type:

append(vec(X), vec(Y)) -> vec(X+Y)

With path semantics, one can connect two function through another function to express the same relationship:

append(vec, vec) -> vec
len(vec) -> usize

append [len] (usize, usize) -> usize
append [len] [:] (X, Y) -> X+Y

The path [:] treats members of a type as paths from the type to themselves.

0(u32) -> 0
1(u32) -> 1
2(u32) -> 2
...

Formal definition of paths

A “path” is a function with one argument and one return value.

g(x) -> y

When a such function exists, there can possibly exist other functions, that can be inferred logically or experimentally to have the equality:

f [g] (g(x)) = g(f(x))

A such connection might exist partially or probabilistically.

Paths can also be used asymmetrically:

f([g] x, [h] y) -> [i] z

Because paths can be used asymmetrically, they can be computed with as values. This is possible to execute using a form of pattern matching stack machine, where operations are not encoded in a single instruction, but in a sequence of instructions on the stack. Verifying such programs is most likely to be undecidable.

In the pure version of path semantics where types and values emerges from the connections, an axiom of equality can be applied (analog to the univalence axiom):

F0(X0), F1(X1), F0 == F1
------------------------
X0 == X1

This axiom states how equality is to be interpreted, for example [g] x is not equal to x, because the path g changes the identity of the function that takes it as an argument. In a pattern matching state machine, this causes the first match to fail so it looks for other functions in the sub tree.

Semantics of pure paths

In a pure path theory, there are no “functions” in the normal sense, but only “terms” or “atoms”. Each term can have an associated function taking constant arguments and returning a constant. For example:

add [:] (1, 1) -> 2

Which is equal to

add([1] 1, [2] 2) -> [2] 2

To compute on numbers with pure paths, you would need one rule for every possible input. This is not practical in many applications, so a pattern matching and variable binding over multiple inputs can be used:

add [:] (X, Y) -> X + Y

Probabilistic paths

The probabilistic version of a path can be interpreted as “any bit of information about an object can be used to infer some probabilistic knowledge about the object itself and its relations to other objects”. Even if this connection can not be computed exactly, it can be used as a general inference tool.

Alice is a person with red hair, Bob is a person with blue hair:

alice(person) -> alice
bob(person) -> bob

red_hair(person) -> bool
red_hair([alice] alice) -> [true] true
red_hair([bob] bob) -> [false] false

If there is a person with red hair, the probability it is Alice is 50%:

probability(bool) -> f64

is(person, person) -> bool
is([red_hair] [true] true, [alice] alice) -> [probability] [0.5] 0.5

Or, written in short form:

alice: person
bob: person

red_hair(person) -> bool
[:] (alice) -> true
[:] (bob) -> false

probability(bool) -> f64

is(person, person) -> bool
([red_hair] [:] true, [:] alice) -> [probability] [:] 0.5

Notice that there is no way to “type check” this connection without having saying what “probability” means. Path semantics does not tell how to model the world, but it implies there exists possibly a such connection. If there is a such connection, then there are some constraints or internal consistenty to follow relative to what is already said.

The key here is that when the function is returns [probability] [:] 0.5 this changes how the information gets computed later on.

We could define a logical and gate operating on both bool and for probability:

and(bool, bool) -> bool
[:] (true, true) -> true
[:] (_, _) -> false
[probability] [:] (X, Y) -> X * Y

One might use path semantics to model “fuzzy” relationships between objects. The ability to verify such programs is probably an undecidable problem.

Why is this an interesting research project for Piston?

One big problem we have in the Piston project is to design APIs that satisfy some critera. These criteria change over time, and it is easy to forget what the original intentions were. Using a formal language helps guiding the design, and the ability to express “fuzzy” relationships might solve some tricky cases.

Otherwise, I think it is interesting to learn more about the internals of a such language should work. Perhaps some of these ideas might leak into some practical libraries?

I also keep an eye open to the OpenCog project, which seeks to build a human level artificial intelligence. This project uses a knowledge database called “AtomSpace”. It would be interesting if some of the patterns used in AtomSpace could be encoded with path semantics. One project currently going on in OpenCog is to make game characters smarter, which is very interesting. This project has done a lot of testing in this area, and perhaps the Piston project will explore some things in this direction.

This project has also been important for testing Piston-Meta, which might turn out to be very useful for other projects.

Older Newer