|
| 1 | +--- |
| 2 | +--- |
| 3 | +# About |
| 4 | + |
| 5 | +## History |
| 6 | + |
| 7 | +<div class = "img-align-right" style="width:35%"> |
| 8 | + <img src="assets/images/hdpi.png" alt="HDPI Image"> |
| 9 | + <p>HDPI Matters. Zoom in and notice the standard DPI icons at the top |
| 10 | + (e.g. the Power Button from the VST3 Plug-in Test Host), vs. Elements’ |
| 11 | + vector dials.</p> |
| 12 | +</div> |
| 13 | + |
| 14 | +Sometime in 2014, I started searching for a GUI library I can use for some of |
| 15 | +the projects I am developing. The most important requirements to me were: |
| 16 | + |
| 17 | +1. It should be open source with a liberal, non-viral license. |
| 18 | +2. It should be usable in any application and should play well with other GUI |
| 19 | + libraries and frameworks. |
| 20 | +3. Corollary to the second requirement is that it can also be used |
| 21 | + to develop plugins (e.g. it should not own the event loop and can co-exist |
| 22 | + with elements within a plugin host such as VST and AU. |
| 23 | +4. It should be resolution independent and allows for HDPI displays. |
| 24 | + |
| 25 | +I tried hard to find something that satisfies these requirements. I failed. |
| 26 | +I did not find any. JUCE comes close, but it did not satisfy the first requirement. |
| 27 | +iPlug is usable. I actually prototyped some plugins using it, |
| 28 | +but it did not satisfy the 4th requirement. |
| 29 | +I’m also unsure if it satisfies the 2nd requirement. |
| 30 | + |
| 31 | +There are other requirements, such as not relying on a “visual” GUI editor or |
| 32 | +code generator. IMO, the GUI should be declared in the code. Obviously, |
| 33 | +good design and architecture is also another requirement. Most GUI C++ libraries |
| 34 | +are just too 90s for me to consider. None of that crappy OOP bleh! But, hey, |
| 35 | +I am digressing! The truth is, I am even willing to use a library with |
| 36 | +a pure C interface, such as GLFW, as long as it is well designed |
| 37 | +(GLFW is simple and clean) and can be wrapped in C++ anyway. I also tried to use |
| 38 | +NanoVG — not really a GUI library, but it makes it easier to write one (NanoVG |
| 39 | +inspired Elements’ Cairo based vector graphics canvas). |
| 40 | + |
| 41 | +I know that perfect is the enemy of the good, but I just can’t resist it. |
| 42 | +I couldn’t stand it anymore. So at around 2016, I decided to write my own. |
| 43 | +Actually, I did not start from scratch. I had a head start: |
| 44 | +I’ve been writing GUI libraries since the 90s. One of the main projects I got |
| 45 | +involved with when I was working in Japan in the 90s was a lightweight |
| 46 | +GUI library named Pica. So I went ahead, dusted off the old code and rewrote it |
| 47 | +from the ground up using modern C++. |
| 48 | + |
| 49 | +## Flyweight |
| 50 | + |
| 51 | +Elements, is very lightweight… and extremely modular. You compose very fine-grained, |
| 52 | +flyweight “elements” to form deep element hierarchies using a declarative |
| 53 | +interface with heavy emphasis on reuse. A specific example should make it clear. |
| 54 | +Here’s the standard message box (included in elements’ gallery a collection of |
| 55 | +reusable element compositions): |
| 56 | + |
| 57 | +```cpp |
| 58 | +inline auto message_box1( |
| 59 | + char const* message |
| 60 | +, std::uint32_t icon_id |
| 61 | +, char const* ok_text = "OK" |
| 62 | +, size size_ = get_theme().message_box_size |
| 63 | +, color ok_color = get_theme().indicator_color |
| 64 | +) |
| 65 | +{ |
| 66 | + auto textbox = static_text_box{ message }; |
| 67 | + auto ok_button = share(button(ok_text, 1.0, ok_color)); |
| 68 | + auto popup = share( |
| 69 | + key_intercept(align_center_middle( |
| 70 | + fixed_size(size_, |
| 71 | + layer( |
| 72 | + margin({ 20, 20, 20, 20 }, |
| 73 | + vtile( |
| 74 | + htile( |
| 75 | + align_top(icon{ icon_id, 2.5 }), |
| 76 | + left_margin(20, std::move(textbox)) |
| 77 | + ), |
| 78 | + align_right(hsize(100, hold(ok_button))) |
| 79 | + ) |
| 80 | + ), |
| 81 | + panel{} |
| 82 | + ))))); |
| 83 | + popup->on_key = |
| 84 | + [ok_ = get(ok_button)](auto k) |
| 85 | + { |
| 86 | + if (k.key == key_code::enter) |
| 87 | + { |
| 88 | + if (auto ok = ok_.lock()) |
| 89 | + ok->value(true); |
| 90 | + return true; |
| 91 | + } |
| 92 | + return false; |
| 93 | + }; |
| 94 | + return std::pair{ ok_button, popup }; |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +The client uses the gallery code above like this: |
| 99 | + |
| 100 | +```cpp |
| 101 | +void quantum_bs(view& view_) |
| 102 | +{ |
| 103 | + char const* alert_text = |
| 104 | + "We are being called to explore the cosmos itself as an " |
| 105 | + "interface between will and energy. It is a sign of things " |
| 106 | + "to come. The dreamtime is approaching a tipping point." |
| 107 | + ; |
| 108 | + auto [ok_button, popup] = message_box1(alert_text, icons::attention); |
| 109 | + view_.add(popup); |
| 110 | + ok_button->on_click = |
| 111 | + [](bool state) |
| 112 | + { |
| 113 | + // Do something here when the button is clicked |
| 114 | + }; |
| 115 | +} |
| 116 | +``` |
| 117 | +
|
| 118 | + |
| 119 | +
|
| 120 | +Some quick highlights, beyond the obvious: |
| 121 | +
|
| 122 | +- `share`: This element is supposed to be shared (using std::shared_ptr). |
| 123 | + E.g. when returned from functions like this one. Typically you’d want to share |
| 124 | + elements that you need to have access to elsewhere in your code. |
| 125 | +- `hold`: Hold a shared element somewhere in a view hierarchy. |
| 126 | +- `key_intercept`: A delegate element that intercepts key-presses. |
| 127 | +- `fixed_size`: An element that fixes the size of its contained element |
| 128 | + (Elements are extremely lightweight, and typically, do not even have sizes |
| 129 | + nor know their positions in the view hierarchy). |
| 130 | +- `margin`, `left_margin`: Two of the many margins, including `right_margin`, |
| 131 | + `top_margin`, etc., adds some padding, `margin` in this case 20 pixels all |
| 132 | + around the contained element, while `left_margin` adds a padding of 20 to |
| 133 | + separate the icon and the text box. |
| 134 | +- `layer`: Element composites that place elements in multiple layers. |
| 135 | +- `panel`: A simple window-like panel box. |
| 136 | +- `vtile`, `htile`: Fluid vertical and horizontal layout elements that allocate |
| 137 | + enough space for their contained elements allowing for ‘stretchiness’ |
| 138 | + (the ability of elements to stretch within a defined minimum and maximum size |
| 139 | + limits; elements can have infinite sizes) as well as fixed sizing and vertical |
| 140 | + and horizontal alignment (0% to 100%). Vertical and horizontal tiles are used |
| 141 | + all over the place to place elements in a grid. |
| 142 | +- `align_top`, `align_right`, `align_center_middle`: Aligns its element to the |
| 143 | + top, right, and center (horizontally) middle (vertically), respectively. |
| 144 | + Like the margins, we have a lot of these align elements. |
| 145 | +- `on_key`: Attaches a call-back (lambda) function to the key_intercept element |
| 146 | + (strategically placed in the outermost level), to send a value of true to the |
| 147 | + ok_button, essentially programmatically clicking it when the user presses |
| 148 | + the enter key. |
| 149 | +- `get`: To avoid `shared_ptr` cycles, we use a `weak_pointer`, via the get |
| 150 | + function. We’ll need to “lock” this to get the actual shared pointer later |
| 151 | + in the callback. |
| 152 | +
|
| 153 | +Modularity and reuse are two of the most important design aspects. For example, |
| 154 | +the button element is actually composed of even smaller elements. Here’s part of |
| 155 | +the code responsible for making the buttons above (also in the gallery): |
| 156 | +
|
| 157 | +```cpp |
| 158 | +auto constexpr button_margin = rect{ 10, 5, 10, 5 }; |
| 159 | +template <typename Button> |
| 160 | +inline Button make_button( |
| 161 | + std::string const& text |
| 162 | +, float size = 1.0 |
| 163 | +, color body_color = get_theme().default_button_color |
| 164 | +) |
| 165 | +{ |
| 166 | + return make_button<Button>( |
| 167 | + margin( |
| 168 | + button_margin, |
| 169 | + align_center(label(text, size)) |
| 170 | + ), |
| 171 | + body_color |
| 172 | + ); |
| 173 | +} |
| 174 | +``` |
| 175 | + |
| 176 | +After a while, code reuse, using a palette of fine-grained elements, becomes |
| 177 | +very familiar and intuitive, much like using HTML. The declarative C++ code |
| 178 | +tells you what rather than how (imperative). |
| 179 | + |
| 180 | +And, as promised, the elements are very fine grained. Here’s the actual button |
| 181 | +class we are using here: |
| 182 | + |
| 183 | +```cpp |
| 184 | +class layered_button : public array_composite<2, deck_element> |
| 185 | +{ |
| 186 | +public: |
| 187 | + using base_type = array_composite<2, deck_element>; |
| 188 | + using button_function = std::function<void(bool)>; |
| 189 | + using base_type::value; |
| 190 | + template <typename W1, typename W2> |
| 191 | + layered_button(W1&& off, W2&& on); |
| 192 | + virtual element* hit_test(context const& ctx, point p) override; |
| 193 | + virtual element* click(context const& ctx, mouse_button btn) override; |
| 194 | + virtual void drag(context const& ctx, mouse_button btn) override; |
| 195 | + virtual bool is_control() const override; |
| 196 | + virtual void value(int new_state) override; |
| 197 | + virtual void value(bool new_state) override; |
| 198 | + bool value() const; |
| 199 | + button_function on_click; |
| 200 | +protected: |
| 201 | + bool state(bool new_state); |
| 202 | +private: |
| 203 | + bool _state; |
| 204 | +}; |
| 205 | +``` |
| 206 | +
|
| 207 | +The layered button is a type of button that basically has two states: |
| 208 | +pushed and un-pushed. It does not know how to draw the two states. Rather, |
| 209 | +it delegates the states to two elements, composed as a 2-layer composite element, |
| 210 | +using the deck element. The class itself has nothing more than a single(!) |
| 211 | +member variable _state. That’s it! And talking about flexibility, |
| 212 | +the deck is generic and may contain any kind of element, or even your own custom |
| 213 | +drawable element. Here’s an example of a custom element that draws an infinitely |
| 214 | +resizable filled round-rectangle: |
| 215 | +
|
| 216 | +```cpp |
| 217 | +auto box = min_size({ 5, 5 }, |
| 218 | + basic( |
| 219 | + [](context const& ctx) |
| 220 | + { |
| 221 | + auto& c = ctx.canvas; |
| 222 | + c.begin_path(); |
| 223 | + c.round_rect(ctx.bounds, 4); |
| 224 | + c.fill_style(colors::gold.opacity(0.8)); |
| 225 | + c.fill(); |
| 226 | + } |
| 227 | + ) |
| 228 | +); |
| 229 | +``` |
| 230 | + |
| 231 | +Elements has its own HTML5 inspired canvas drawing engine using Cairo underneath. |
| 232 | + |
| 233 | +## For now... |
| 234 | + |
| 235 | +There’s obviously still a lot to cover, but for now, this quick tour of Elements |
| 236 | +should suffice. Is it ready yet? Is it usable? Well, I am already using it, |
| 237 | +at least for the MacOS, which is my preferred development system. I am using it |
| 238 | +in the Ascend project. But, I have to be honest. While it is usable, and based |
| 239 | +on very solid architecture and design, there is still a lot of work to do. |
| 240 | + |
| 241 | +And so that being said, if anyone out there, familiar with modern C++ |
| 242 | +(esp. C++14 and C++17), finds this brief introduction compelling enough, |
| 243 | +I would very much welcome some help. Hey, this is Open Source. The license will |
| 244 | +remain permissive and liberal (currently MIT). |
| 245 | +Send me an email! joel-at-cycfi-dot-com. |
0 commit comments