Skip to content

Latest commit

 

History

History
157 lines (102 loc) · 7.13 KB

ARCHITECTURE.md

File metadata and controls

157 lines (102 loc) · 7.13 KB

Architecture

This document describes the high-level architecture of Panoramix.

  • Note: This document had its last update around March 14, 2021; if this message is still here by, let's say, Sept 15 2021, feel free to open an issue or ping me.

First off, let's make things clear:

  • This is an internal documentation, not intended for end users. If you want to use Panoramix as a dependency, look at the crate documentation.
  • This is an informal documentation, loosely following the guidelines described in matklad's post. The idea is to make this easy to update every few months, not exhaustively document every piece of code.
  • This documentation assumes you have some familiarity with Panoramix and have at least compiled a few examples.
  • This documentation is a little sparse and vague, by design. I strongly recommend reading it side by side with an open text editor, so you can read the code at the same time.

That being said, if you're looking to contribute to Panoramix and you don't know where to begin, you're in the right place!

Design principles

Before we talk about code structure, here are the general design principles of panoramix.

Panoramix is functional and declarative

For those familiar with React, this means we want function components, not class components. Writing a piece of GUI in Panoramix looks like this pseudocode:

fn my_component(input, other_input) -> OutputToDisplay {
    return ElementGroup(
        Label("some text"),
        Button("button text")
            .on_press(do_stuff),
        OtherComponent(props),
    );
}

As opposed to this:

struct MyComponent{
    state: SomeState,
    state2: SomeMoreState,
}

impl ComponentLifecycle {
    fn create() { ... }
    fn update() { ... }
    fn display() { ... }
}

This is a central choice of the framework. I believe it makes writing component easier, and more intuitive.

The abstraction that backs this choice is called a "Virtual DOM". See this document for details.

Panoramix is not clever

The common wisdom is that Rust isn't a great language for writing a GUI framework. GUIs require lots of complicated patterns to mutate data in inconvenient ways. Widgets in traditional frameworks usually hold a reference to their parent, and can run callbacks at arbitrary points in the code, that can mutate data that other widgets access, also at arbitrary points in the code.

To make sense of that logic in Rust, one might be tempted to use some of Rust's more arcane features: unsafe code, RefCells, Arcs, mutexes, and so on. To be able to reason about widget hierarchy, one might be tempted to define them with DSL macros, and this is indeed what other Rust GUI frameworks do.

Not here. We do everything the hard way. If you can't get a mutable reference to some data in safe code, you can't mutate that data and that's final.

The good news is, making a GUI that works in Rust without using these patterns is surprisingly doable. You should read the VirtualDom trait and its implementations for details, but the general pattern is:

  • The framework holds a tree of states, which are application-defined PODs.
  • The application-defined components each borrow a given state (as well as props passed from other components) as input, and generate a hierarchy of elements as output; that hiearchy includes event callbacks.
  • A GUI is built from the element tree.
  • The GUI is shown to the user.
  • Depending on user-interaction, some event callbacks are called with a mutable reference to their component state.
  • If the callbacks have changed the state value, we call the components again to regenerate the GUI, and so on.

This is possible because once we get to event processing, the application-defined components are no longer borrowing their state.

Panoramix uses static types

This is less important than the above two, but still part of the original design.

Many virtual DOM frameworks (especially in JS) will have dynamically-typed representation of their virtual DOM. So the output of a component might look like:

VirtualDomNode("list")
- VirtualDomNode("button", data = ...)
- VirtualDomNode("label", data = "sometext")
- VirtualDomNode("row")
 - VirtualDomNode("label")
 - VirtualDomNode("label")

Conceptually, the output of a Panoramix component is more like:

ListNode
- ButtonNode(data = ...)
- LabelNode(data = "sometext")
- RowNode
 - LabelNode
 - LabelNode

Being statically-typed means that Panoramix gets to skip some redundant checks during reconciliation (eg if it previously had a "Label" node, it doesn't need to check that the new node is still a label, though it still needs to compare their text).

Project structure

The important traits are:

  • Element: What every user-facing function returns.
  • VirtualDom: Same thing, but one step removed. VirtualDom items are built from Elements and their types define methods for building the widget tree, reconciliation and event handling. This is where most of the magic happens.
  • WidgetSequence: wrapper for iterators of FlexWidgets. Most elements (eg Label, Button, TextBox, Flex containers) are sequences of a single FlexWidget. Non-Flex containers (Tuple, ElementList, Option, Either) are sequences of 0-to-many widgets. (for instance Option is 0-to-1)
  • FlexWidget: wrapper for a druid::WidgetPod plus some flex data. Object-safe.

Essentially, every element the user can instantiate has to bind to all four of these traits. You can think of Element as the front-end and druid as the backend.

src/elements/

Where Element and VirtualDom implementations are defined.

src/widgets/

Where WidgetSequence and FlexWidget implementations are defined.

src/root_handler.rs

Defines the functions that users call in their main() function to start the app.

src/test_harness.rs

Harness used for unit tests.

This crate uses a lot of unit tests, including some that take the form of "create a mock druid window, initialize $ELEMENT in it, check that the right widgets are created", and sometimes even "have a fake user interact with the window (eg press a button), check that state is changed accordingly".

Harness provides the framework for those tests.

Tutorials

Besides regular inline doc comments, the crate also has a tutorials/ folder, with markdown documents.

The crate directly imports these documents using #[doc = include_str!], so these tutorials are meant to be seen on doc.rs, not in a markdown reader.

The main advantage of this is that code in these tutorials is guaranteed to run and pass doctests.