Declarative UI Framework Proposal

Made some progress on the framework, including a rudimentary first attempt at UI Editor that uses reflection to browse all classes to edit them ‘live’.

Here’s a brief video demo:

Implementing UI Editor in itself is a great test case and acid test. It is surely at the complex end of the spectrum, so if it eventually feels sound and round, the framework might be sufficiently mature to support major projects like a DAW or similar :wink:

2 Likes

Very nice. A couple of comments: Why the use of raw pointers over smart pointers? Also I think the use of std::vector is generally preferred over juce::Array, but I guess that’s a matter of personal preference…

Just converted the project to Juce module format: Two modules ans_extensions and ans_ui. Also moved all code into a namespace. This will make it easier to play around with them. UIEditor also supports namespaces now (did I say how I love reflection?)

The cgn_extensions module may be useful outside the UI framework, especially Metaclass and Symbol. Maybe it’s just me and my OO background, but I can’t live without class-side inheritance and reflection anymore.

@adamski Thanks for your hints. The reason I use Juce containers that much is they have a simple and well documented API (albeit a somewhat inconsistent and incomplete one). std::vector is certainly worth a closer look.

As for the raw pointers, some objects merely need a link back to their parent, which is initialised on construction and never changes (in cases where it’s impossible to delete the parent w/o deleting all children). In other cases I use WeakReference, ReferenceCountedPointer or ScopedPointer. If I missed some cases, let me know.

Those are usually best sorted using references & instead of pointers… Since they are owned by the parent, they are safe and can never be nullptr.

1 Like

When it comes to containers, juce::Array is pretty good I’m sure Jules has mentioned on here he tried to convert JUCE to use std::vector internally and hit performance issues!

As for pointers you could still use std::unique_ptr and transfer ownership of the pointers rather than relying on raw pointers, I think that kind of thing can make intent clearer, however maybe not everybody is as familiar with it? In general I think it’s recommended to use standard library pointer types rather than what is in the JUCE library.

1 Like

Not all objects always have a parent, so nullptr is actually a needed edge case here.

Not all objects always have a parent, so nullptr is actually a needed edge case here.

This sounds suspicious. Is it because the objects don’t have an owner or because they outlive their owner?

They are root elements (no parent).

I agree - I would prefer to see std::unique_ptr in place of any raw pointer (or even ScopedPointer), using std::make_unique at the point of instantiation. This makes it super clear when the object is created, and who currently owns it. And as @Anthony_Nicholls noted it is now the recommended way - ScopedPointer was useful before C++11 was commonplace.

I agree. But maybe I’m missing something. How could a unique_ptr work for a link to a parent when the child doesn’t own the parent?

Or another example in Metaclass: A link from subclass to superclass, or links in a superclass to its subclasses? None of these links establish ownership. They are merely cross-references between objects that never change (no JIT in C++), or get deleted as a whole structure.

The thing is in your video there are some calls to new that are assigned to raw pointers and ultimately it’s those that should go.

Is it that parents own children? if so then you could you create a unique_ptr then move the unique_ptr into the parent?

As for storing parents in children I think a raw pointer is fine here the key is there should be no calls to new or delete. If the ownership is more complicated than that it might be possible you need to use shared_ptr and weak_ptr in some circumstances.

Looking back at one of your earlier posts I would say…

  1. get rid of any calls to new/delete
  2. replace ScopedPointer with unique_ptr
  3. replace ReferenceCountedPointer with shared_ptr
  4. replace WeakReference with weak_ptr
2 Likes

Thanks for the handy summary of how the older Juce pointers relate to their std equivalents. I will have a look into how ‘moving’ a unique_ptr actually works.

You probably meant this auto-generated code?


I don’t like the pointers here either, but couldn’t figure out how to do this more elegantly. The specs need to be configured before they are passed to addComponent() which ultimately adds them to an OwnedArray. So the raw pointer is really only a temporary placeholder in a narrow scope.

If I didn’t use pointers here, the specs would need to be copyable (passed as const&), which they aren’t. I understand and agree with the dogma, but am somewhat reluctant to creating a new object that is destined to be owned by another, only to delete it immediately after it was copied for no gain (except the cool optics of seeing no pointers).

auto part1 = std::make_unique<CompositeSpec> ("part1");
part1->setLayout (...);
{
    auto comments = std::make_unique<TextSpec> ("comments", Transcript);
    ...
    part1->addComponent (comments.release()); // will work with your current implementation 
    // or
    part1->addComponent (std::move (comments)); // if you switch to move semantics
}

The advantage of using move semantics is it forces the user to create a smart pointer which is safer from the point of view of exception safety and reducing the possibility of leaks. However you could use the .release() method with your current code.

To accept the move semantics you will have to accept the type like so…

void ComponentType::addComponent (ComponentType&& componentToMove)
2 Likes

I think this should be addComponent (unique_ptr<ComponentType> componentToMove). (by value because of this)

1 Like

You’re absolutely right I wasn’t sure if I should get into the discussion of passing by move semantics or value. However I completely missed the unique_ptr anyway!

2 Likes

Thanks! Will have to play around with smart pointers a while before I feel firm enough to make the switch. It’s a sweeping change that adds a lot of verbosity! Interestingly, doing it right often looks more convoluted in C++ than doing it wrong :wink:

Juce however seems just not yet prepared to deal with smart pointers:

  • TopLevelWindow has no obvious owner, so there’s no destination for a unique__ptr to go if I just want TopLevelWindowManager to take control.

  • The juce::Component API expects raw pointers across the board.

  • I still need WeakReference for non-pointer objects (e.g. tree & list models).

So the best place to use smart pointers for now is with structures you fully control yourself, which is still very useful, of course.

It begins to dawn on me that I vastly underestimated the effort of making this workable (kudos to Jules and ROLI for their achievements). It’s a time consuming black hole. I spend 80% of my time thinking about how to technically store and represent objects (I’m used to 2%).

The shallow level of abstraction achievable with C++ is also disappointing. For instance, lacking reflection, there’s no way for UIEditor to present to the user a model’s member functions that technically (by method signature) match a particular component and purpose (combo box, or auto-complete).

I mean it’s still fun, but at this time I have a hard time imagining how this could eventually meet the productivity requirements of large projects.

1 Like

Yeah, it’s too bad reflection didn’t make it into C++20.
Some people hate Qt because it has its MOC but at least they have reflection…

I find it quite encouraging that Apple just introduced Swift UI, a declarative UI based on native code that doesn’t rely on XML, JSON or other external files. The basic idea is to build the declarative data structure with native code, which allows for data bindings to be established right away and checked at compile time. Pretty much what this framework is also aiming at.

The prospect of having a background process that compiles your code and updates a mockup UI whenever possible is compelling. Could spare the effort of writing a UIEditor altogether, but TBH, this is way beyond what a single developer could do on the side.

1 Like