├── .gitignore ├── JML.cpp ├── JML.h ├── README.md ├── docs └── attributes-list.md └── src ├── jml_BlockLayoutManager.cpp ├── jml_BlockLayoutManager.h ├── jml_FlexLayoutManager.cpp ├── jml_FlexLayoutManager.h ├── jml_GridLayoutManager.cpp └── jml_GridLayoutManager.h /.gitignore: -------------------------------------------------------------------------------- 1 | # Directories 2 | Builds/ 3 | JuceLibraryCode/ 4 | 5 | #=============================================================================== 6 | # Prerequisites 7 | *.d 8 | 9 | # Compiled Object files 10 | *.slo 11 | *.lo 12 | *.o 13 | *.obj 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Compiled Dynamic libraries 20 | *.so 21 | *.dylib 22 | *.dll 23 | 24 | # Fortran module files 25 | *.mod 26 | *.smod 27 | 28 | # Compiled Static libraries 29 | *.lai 30 | *.la 31 | *.a 32 | *.lib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | -------------------------------------------------------------------------------- /JML.cpp: -------------------------------------------------------------------------------- 1 | #include "JML.h" 2 | 3 | #include "src/jml_BlockLayoutManager.cpp" 4 | #include "src/jml_FlexLayoutManager.cpp" 5 | #include "src/jml_GridLayoutManager.cpp" 6 | 7 | //============================================================================== 8 | JML::JML(const File& file) 9 | { 10 | setJMLFile(file); 11 | } 12 | 13 | //============================================================================== 14 | void JML::setJMLFile(const File& file) 15 | { 16 | jassert(file.existsAsFile()); 17 | jmlFile = file; 18 | 19 | jmlRoot = parseXML(file); 20 | } 21 | 22 | void JML::setJMLRoot(std::unique_ptr root) 23 | { 24 | jassert(root); 25 | 26 | jmlRoot = std::move(root); 27 | } 28 | 29 | void JML::watch(const int updatesPerSecond) 30 | { 31 | startTimer(updatesPerSecond); 32 | } 33 | 34 | //============================================================================== 35 | void JML::timerCallback() 36 | { 37 | jmlRoot = parseXML(jmlFile); 38 | performLayout(); 39 | } 40 | 41 | //============================================================================== 42 | void JML::performLayout() 43 | { 44 | jassert(jmlRoot != nullptr); 45 | performLayoutForElement(jmlRoot.get()); 46 | } 47 | 48 | const int JML::attributeStringToInt(Component* component, 49 | const String& name, String s) 50 | { 51 | if (s.endsWith("%")) 52 | { 53 | auto parent = component->getParentComponent(); 54 | 55 | if (parent != nullptr) 56 | { 57 | auto perc = s.upToLastOccurrenceOf("%", false, true).getFloatValue() / 100.f; 58 | 59 | if (name.contains("x") || name.contains("width")) 60 | return roundToInt(parent->getWidth() * perc); 61 | else if (name.contains("y") || name.contains("height")) 62 | return roundToInt(parent->getHeight() * perc); 63 | else 64 | { 65 | // The attribute name doesn't contain any of the accepted 66 | // specifiers! (See above). 67 | jassertfalse; 68 | return roundToInt(perc * 100.f); 69 | } 70 | } 71 | else 72 | { 73 | // You can't use % on the top-level component! It has no parent! 74 | jassertfalse; 75 | } 76 | } 77 | else 78 | { 79 | return s.getIntValue(); 80 | } 81 | } 82 | 83 | //============================================================================== 84 | void JML::setComponentForTag(const String& tag, Component* component) 85 | { 86 | jassert(component != nullptr); 87 | jassert(tag.isNotEmpty()); 88 | 89 | tagsMap[tag] = component; 90 | } 91 | 92 | //============================================================================== 93 | void JML::performLayoutForElement(XmlElement* element) 94 | { 95 | jassert(element != nullptr); 96 | 97 | auto component = tagsMap[element->getTagName()]; 98 | 99 | jassert(component != nullptr); 100 | 101 | // do Block layout by default 102 | jml::BlockLayoutManager(element, component); 103 | 104 | // perform Grid and Flex layouts 105 | if (element->hasAttribute("display")) 106 | { 107 | auto display = element->getStringAttribute("display").toLowerCase(); 108 | 109 | if (display == "grid") 110 | jml::GridLayoutManager(tagsMap, element, component); 111 | else if (display == "flex") 112 | jml::FlexLayoutManager(tagsMap, element, component); 113 | else 114 | { 115 | jassertfalse; 116 | } 117 | } 118 | 119 | for (int i = 0; i < element->getNumChildElements(); i++) 120 | { 121 | performLayoutForElement(element->getChildElement(i)); 122 | } 123 | } 124 | 125 | //============================================================================== 126 | Component* JML::getComponentForElement(XmlElement* element) 127 | { 128 | jassert(element != nullptr); 129 | return reinterpret_cast(tagsMap[element->getTagName()]); 130 | } 131 | -------------------------------------------------------------------------------- /JML.h: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | BEGIN_JUCE_MODULE_DECLARATION 3 | 4 | ID: jml 5 | vendor: James Johnson 6 | version: 0.2.0 7 | name: JUCE Markup Language 8 | description: JML 9 | dependencies: juce_core, juce_gui_basics 10 | 11 | END_JUCE_MODULE_DECLARATION 12 | *******************************************************************************/ 13 | 14 | #pragma once 15 | #define JML_H_INCLUDED 16 | 17 | // JUCE includes 18 | #include 19 | #include 20 | 21 | // STL includes 22 | #include 23 | 24 | // JML includes 25 | #include "src/jml_BlockLayoutManager.h" 26 | #include "src/jml_FlexLayoutManager.h" 27 | #include "src/jml_GridLayoutManager.h" 28 | 29 | using namespace juce; 30 | 31 | //============================================================================== 32 | class JML : public Timer 33 | { 34 | public: 35 | //========================================================================== 36 | JML() {} 37 | JML(const File& file); 38 | 39 | //========================================================================== 40 | void setJMLFile(const File& file); 41 | void setJMLRoot(std::unique_ptr root); 42 | 43 | /** If called, the JML file will be repeatedly parsed and the layout will be 44 | performed with the new XML tree 45 | */ 46 | void watch(const int updatesPerSecond); 47 | 48 | //========================================================================== 49 | void timerCallback() override; 50 | 51 | //========================================================================== 52 | /** Performs the laying out of the Components. */ 53 | void performLayout(); 54 | 55 | /** Converts an attributes value (as a string) to the correct integer 56 | value. 57 | 58 | This allows for the use of relative values like "50%" instead of 59 | just fixed values. 60 | */ 61 | static const int attributeStringToInt(Component* component, 62 | const String& attributeName, 63 | String attributeValue); 64 | 65 | //========================================================================== 66 | void setComponentForTag(const String& tag, Component* component); 67 | 68 | private: 69 | //========================================================================== 70 | void performLayoutForElement(XmlElement* element); 71 | 72 | void layoutComponent(XmlElement* element); 73 | void performGridLayout(XmlElement* element, XmlElement* parent); 74 | 75 | //========================================================================== 76 | Component* getComponentForElement(XmlElement* element); 77 | 78 | //========================================================================== 79 | File jmlFile; 80 | std::unique_ptr jmlRoot; 81 | std::unordered_map tagsMap; 82 | 83 | // component maps 84 | struct ComponentDefinition 85 | { 86 | Component* component; 87 | Rectangle bounds; 88 | Rectangle minBounds; 89 | Rectangle maxBounds; 90 | BorderSize margin; 91 | }; 92 | 93 | std::unordered_map> componentMap; 94 | 95 | // grid maps 96 | std::unordered_map> gridMap; 97 | std::unordered_map gridJustifyItemsMap; 98 | std::unordered_map gridAlignItemsMap; 99 | std::unordered_map gridJustifyContentMap; 100 | std::unordered_map gridAlignContentMap; 101 | std::unordered_map gridAutoFlowMap; 102 | 103 | std::unordered_map> gridItemMap; 104 | std::unordered_map gridItemJustifySelfMap; 105 | std::unordered_map gridItemAlignSelfMap; 106 | 107 | }; 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JML - JUCE Markup Language 2 | 3 | ## DEPRECATED 4 | 5 | Please see [JIVE](https://github.com/ImJimmi/JIVE) for a new and improved decalrative UI framework for JUCE. 6 | 7 | --- 8 | 9 | JML is an attempt to create a markup language for designing GUI's in [JUCE](https://github.com/WeAreROLI/JUCE). 10 | 11 | This repository is structured as a JUCE module so it can be easily implementing into JUCE projects. The JML module interprets an XML file and uses the list of defined tags and attributes to position components within a JUCE project. 12 | 13 | The convention for JML files is the use the `.jml` extension and to start the file with the `` tag. This is only a convention and isn't actually required - although a root element is required within the chosen file. The syntax for JML files is identical to XML (the module uses JUCE's `XmlElement` class). 14 | 15 | ## Examples 16 | This very basic example positions two buttons in the top-left corner of the parent component: 17 | ```xml 18 | 19 | 20 | 21 | 22 | ``` 23 | 24 | The corresponding C++ code: 25 | ```c++ 26 | MyComponent() 27 | { 28 | JML jml; 29 | jml.setJMLFile(File("sample.jml")); 30 | 31 | jml.setComponentForTag("button1", &button1); 32 | jml.setComponentForTag("button2", &button2); 33 | } 34 | 35 | void resized() 36 | { 37 | jml.perform(); 38 | } 39 | ``` 40 | 41 | The JML file can be altered without ever having to rewrite or even recompile the C++ code. 42 | 43 | --- 44 | 45 | This example uses the grid system to position buttons in a 2x2 grid: 46 | ```xml 47 | 48 | 49 | 50 | 51 | 52 | ``` 53 | 54 | And the corresponding C++ code: 55 | ```c++ 56 | MyComponent() 57 | { 58 | JML jml; 59 | jml.setJMLFile(File("sample.jml")); 60 | jml.setComponentForTag("mainComponent", this); 61 | 62 | for (auto button : buttons) 63 | jml.setComponentForTag(button->getName(), button); 64 | } 65 | 66 | void resized() 67 | { 68 | jml.perform(); 69 | } 70 | ``` 71 | 72 | This is equivalent to the following code when not using JML: 73 | ```c++ 74 | void MainComponent::resized() 75 | { 76 | Grid grid; 77 | grid.templateRows = { Grid::TrackInfo(1_fr), Grid::TrackInfo(1_fr) }; 78 | grid.templateColumns = { Grid::TrackInfo(1_fr), Grid::TrackInfo(1_fr) }; 79 | grid.templateAreas = { "a b", "c d" }; 80 | grid.setGap(10_px); 81 | 82 | grid.items = { GridItem(buttons[0]).withWidth(100.f).withArea("c"), 83 | GridItem(buttons[1]).withRow({ 1, 3 }).withColumn({ 2 }), 84 | GridItem(buttons[2]).withHeight(25.f).withAlignSelf(GridItem::AlignSelf::center) }; 85 | 86 | grid.performLayout(getLocalBounds().reduced(10)); 87 | } 88 | ``` 89 | 90 | In both cases, this is the result: 91 | 92 | ![JML grid example](https://i.imgur.com/9fW4oFj.png) 93 | 94 | 95 | ## Getting Started 96 | 97 | 1. Clone this repository: `git clone https://github.com/ImJimmi/JML.git` 98 | 2. Add the JML module to your JUCE project 99 | 3. In your top-most Component class, create a JML object: `JML jml;` 100 | 4. Set the JML file to use using `jml.setJMLFile(File("myJMLFile.jml"));` 101 | 4. Set the Components to use for each tag in the JML file using `jml.setComponentForTag("tagName", &component);` 102 | 5. Call `jml.perform()` in the `resized()` method 103 | -------------------------------------------------------------------------------- /docs/attributes-list.md: -------------------------------------------------------------------------------- 1 | # JML - Attributes List 2 | ## Components 3 | This table lists the defined attributes that can be used for positioning individual components. Within a JML file they should be used as such: 4 | 5 | ```xml 6 | 7 | ``` 8 | 9 | Attribute Name | Description | Accepted Values | Example Usage 10 | ---|---|---|--- 11 | `x` | The component's x position. | Any integer.
A percentage. | `x="10"`
`x="25%"` 12 | `y` | The component's y position. | Any integer.
A percentage. | `y="5"`
`y="10%"` 13 | `width` | The component's width. | Positive integers.
A percentage. | `width="200"`
`width="100%"` 14 | `height` | The component's height. | Positive integers.
A percentage. | `height="50"`
`height="10%"` 15 | `min-x` | The component's minimum x position. | Any integer.
A percentage. | `min-x="10"`
`min-x="25%"` 16 | `min-y` | The component's minimum y position. | Any integer.
A percentage. | `min-y="5"`
`min-y="10%"` 17 | `min-width` | The component's minimum width. | Positive integers.
A percentage. | `min-width="200"`
`min-width="100%"` 18 | `min-height` | The component's minimum height. | Positive integers.
A percentage. | `min-height="50"`
`min-height="10%"` 19 | `max-x` | The component's maximum x position. | Any integer.
A percentage. | `max-x="10"`
`max-x="25%"` 20 | `max-y` | The component's maximum y position. | Any integer.
A percentage. | `max-y="5"`
`max-y="10%"` 21 | `max-width` | The component's maximum width. | Positive integers.
A percentage. | `max-width="200"`
`max-width="100%"` 22 | `max-height` | The component's maximum height. | Positive integers.
A percentage. | `max-height="50"`
`max-height="10%"` 23 | `margin` | The gap, in pixels, around this component.
Removes the margin from the given size (so a component with width 100 and margin 10 will be drawn as 80 pixels wide | Positive integers.
Single value sets all margins.
Two values set the x and y margins.
Four values set the top, right, bottom, and left margins respectively. | `margin="10"`
`margin="10 20"`
`margin="10 20 50 20"` 24 | 25 | ## Grid 26 | ### Grid 27 | This table lists the defined attributes that can be used with the `` tag. The `` tag should be added as a child to the component that will display as a grid, e.g.: 28 | 29 | ```xml 30 | 31 | 32 | 33 | 34 | 35 | ``` 36 | 37 | Attribute Name | Description | Accepted Values | Example Usage 38 | ---|---|---|--- 39 | `gap` | The gap between cells | Positive integers. | `gap="10"` 40 | `justify-items` | The alignment of content inside the items along the rows. | "start", "end", "center", or "stretch". | `justify-items="center"` 41 | `align-items` | The alignment of content inside the items along the columns. | "start", "end", "center", or "stretch". | `align-items="end` 42 | `justify-content` | The alignment of items along the rows. | "start", "end", "center", "stretch", "spaceAround", "spaceBetween", or "spaceEvenly". | `justify-content="spaceAround"` 43 | `align-content` | The alignment of items along the columns. | "start", "end", "center", "stretch", "spaceAround", "spaceBetween", or "spaceEvenly". | `align-content="spaceBetween"` 44 | `auto-flow` | How the auto-placement algorithm places items. | "row", "column", "row-dense", or "column-dense". | `auto-flow="row"` 45 | `columns` | The number of the columns the grid should have. | Positive integers. | `columns="3"` 46 | `rows` | The number of rows the grid should have. | Positive integers. | `rows="5"` 47 | `column-gap` | The gap between columns. | Positive integers. | `column-gap="15"` 48 | `row-gap` | The gap between rows. | Positive integers. | `row-gap="20"` 49 | `template-rows` | The set of rows the grid should have. | A space-separated list of values.
Each value should either be a positive integer or a fractional value ending in "fr". | `template-rows="100 1fr 3fr"` 50 | `template-columns` | The set of columns the grid should have. | A space-separated list of values.
Each value should either be a positive integer or a fractional value ending in "fr". | `template-columns="50 1fr 1fr 200"` 51 | `template-areas` | The names for each of the cells in the grid. | A space-separated list of quoted, space-separated lists.
use "." for an empty/unnamed cell. | `template-areas="'heading heading' 'left right'"` 52 | `auto-columns` | The track to use for the auto dimension. | Positive integers or a fractional value ending in "fr". | `auto-columns="100"`
`auto-columns="1fr"` 53 | `auto-rows` | The track to use for the auto dimension. | Positive integers or a fractional value ending in "fr". | `auto-rows="100"`
`auto-rows="1fr"` 54 | `margin` | The amount of space to leave around the grid. | 1, 2, or 4 Positive integers. | `margin="10"`
`margin="20 30"`
`margin="10 20 30 20"` 55 | 56 | ### Grid Items 57 | 58 | This table lists the defined attributes that can be used with items *within* the `` tag. 59 | 60 | Attribute Name | Description | Accepted Values | Example Usage 61 | ---|---|---|--- 62 | `area` | The area on the grid this item should be placed in. | The name of an area defined in the grid.
2 or 4 positive integers to define the start (and end, if 4 values provided) of the row and column this item should be placed in. | `area="heading"`
`area="1 3"`
`area="1 1 3 4"` 63 | `order` | The z-order for this item. | Positive integers. 64 | `justify-self` | Where this item should be placed along the row. | "start", "end", "center", or "stretch". | `justify-self="center"` 65 | `align-self` | Where this item should be places along the column. | "start", "end", "center", or "stretch". | `align-self="stretch"` 66 | `column` | The column in which this item should be placed. | Positive integers, greater than 0 (1-indexed). | `column="3"` 67 | `row` | The row in which this item should be placed. | Positive integers, greater than 0 (1-indexed). | `row="2"` 68 | `width` | The width of the item. | Positive decimal numbers. | `width="100"` 69 | `min-width` | The minimum width of the item. | Positive decimal numbers. | `min-width="20"` 70 | `max-width` | The maximum width of the item. | Positive decimal number. | `max-width="200"` 71 | `height` | The height of the item. | Positive decimal numbers. | `height="50"` 72 | `min-height` | The minimum height of the item. | Positive decimal numbers. | `min-hight="25"` 73 | `max-height` | The maximum height of the item. | Positive decimal number. | `max-height="100"` 74 | `margin` | The amount of space to leave around this item. | 1, 2, or 4 positive decimal numbers. | `margin="10"`
`margin="10, 20"`
`margin="20 10 40 10"` 75 | -------------------------------------------------------------------------------- /src/jml_BlockLayoutManager.cpp: -------------------------------------------------------------------------------- 1 | //============================================================================== 2 | namespace jml 3 | { 4 | //========================================================================== 5 | BlockLayoutManager::BlockLayoutManager(XmlElement* ele, 6 | Component* comp, 7 | bool performLayoutNow) 8 | : element(ele), 9 | component(comp) 10 | { 11 | jassert(element != nullptr); 12 | jassert(component != nullptr); 13 | 14 | if (performLayoutNow) 15 | performLayout(); 16 | } 17 | 18 | //========================================================================== 19 | void BlockLayoutManager::performLayout() 20 | { 21 | Rectangle bounds(component->getBounds()); 22 | Rectangle minBounds(0, 0, 0, 0); 23 | Rectangle maxBounds(9999, 9999, 9999, 9999); 24 | BorderSize margin(0); 25 | 26 | auto display = element->getStringAttribute("display").toLowerCase(); 27 | auto shouldHandleMargin = display == "block" || display.isEmpty(); 28 | 29 | for (int i = 0; i < element->getNumAttributes(); i++) 30 | { 31 | auto name = element->getAttributeName(i).toLowerCase(); 32 | auto value = element->getAttributeValue(i).trim(); 33 | 34 | if (name == "margin" && shouldHandleMargin) 35 | { 36 | auto tokens = StringArray::fromTokens(value, false); 37 | 38 | if (tokens.size() == 1) 39 | margin = BorderSize(tokens[0].getIntValue()); 40 | else if (tokens.size() == 2) 41 | { 42 | margin.setTop(tokens[1].getIntValue()); 43 | margin.setRight(tokens[0].getIntValue()); 44 | margin.setBottom(tokens[1].getIntValue()); 45 | margin.setLeft(tokens[0].getIntValue()); 46 | } 47 | else if (tokens.size() == 3) 48 | { 49 | margin.setTop(tokens[0].getIntValue()); 50 | margin.setRight(tokens[1].getIntValue()); 51 | margin.setBottom(tokens[2].getIntValue()); 52 | margin.setLeft(tokens[1].getIntValue()); 53 | } 54 | else if (tokens.size() == 4) 55 | { 56 | margin.setTop(tokens[0].getIntValue()); 57 | margin.setRight(tokens[1].getIntValue()); 58 | margin.setBottom(tokens[2].getIntValue()); 59 | margin.setLeft(tokens[3].getIntValue()); 60 | } 61 | else 62 | { 63 | // Invalid number of values in margin string. 64 | // Should be 1, 2, 3, or 4 65 | jassertfalse; 66 | } 67 | } 68 | else 69 | { 70 | auto numericValue = JML::attributeStringToInt(component, name, value); 71 | 72 | if (name == "x") 73 | bounds.setX(numericValue); 74 | else if (name == "y") 75 | bounds.setY(numericValue); 76 | else if (name == "width") 77 | bounds.setWidth(numericValue); 78 | else if (name == "height") 79 | bounds.setHeight(numericValue); 80 | 81 | else if (name == "min-x") 82 | minBounds.setX(numericValue); 83 | else if (name == "min-y") 84 | minBounds.setY(numericValue); 85 | else if (name == "min-width") 86 | minBounds.setWidth(numericValue); 87 | else if (name == "min-height") 88 | minBounds.setHeight(numericValue); 89 | 90 | else if (name == "max-x") 91 | maxBounds.setX(numericValue); 92 | else if (name == "max-y") 93 | maxBounds.setY(numericValue); 94 | else if (name == "max-width") 95 | maxBounds.setWidth(numericValue); 96 | else if (name == "max-height") 97 | maxBounds.setHeight(numericValue); 98 | } 99 | } 100 | 101 | bounds.setX(jmax(minBounds.getX(), jmin(maxBounds.getX(), bounds.getX()))); 102 | bounds.setY(jmax(minBounds.getY(), jmin(maxBounds.getY(), bounds.getY()))); 103 | bounds.setWidth(jmax(minBounds.getWidth(), jmin(maxBounds.getWidth(), bounds.getWidth()))); 104 | bounds.setHeight(jmax(minBounds.getHeight(), jmin(maxBounds.getHeight(), bounds.getHeight()))); 105 | 106 | margin.subtractFrom(bounds); 107 | 108 | component->setBounds(bounds); 109 | } 110 | } -------------------------------------------------------------------------------- /src/jml_BlockLayoutManager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | //============================================================================== 4 | namespace jml 5 | { 6 | using namespace juce; 7 | 8 | //========================================================================== 9 | class BlockLayoutManager 10 | { 11 | public: 12 | //====================================================================== 13 | BlockLayoutManager(XmlElement* elementForLayout, 14 | Component* componentToPerformLayoutFor, 15 | bool performLayoutNow = true); 16 | 17 | void performLayout(); 18 | 19 | private: 20 | //====================================================================== 21 | XmlElement* element = nullptr; 22 | Component* component = nullptr; 23 | 24 | //====================================================================== 25 | /** Converts an attributes value (as a string) to the correct integer 26 | value. 27 | 28 | This allows for the use of relative values like "50%" instead of 29 | just fixed values. 30 | */ 31 | const int attributeStringToInt(const String& name, String value); 32 | }; 33 | } -------------------------------------------------------------------------------- /src/jml_FlexLayoutManager.cpp: -------------------------------------------------------------------------------- 1 | //============================================================================== 2 | namespace jml 3 | { 4 | //========================================================================== 5 | FlexLayoutManager::FlexLayoutManager(std::unordered_map& tags, 6 | XmlElement* ele, 7 | Component* comp, 8 | bool performLayoutNow) 9 | : tagsMap(tags), 10 | element(ele), 11 | component(comp) 12 | { 13 | jassert(element != nullptr); 14 | jassert(component != nullptr); 15 | 16 | // setup AlignContent mappings 17 | { 18 | alignContentMap["center"] = FlexBox::AlignContent::center; 19 | alignContentMap["end"] = FlexBox::AlignContent::flexEnd; 20 | alignContentMap["start"] = FlexBox::AlignContent::flexStart; 21 | alignContentMap["space-around"] = FlexBox::AlignContent::spaceAround; 22 | alignContentMap["space-between"] = FlexBox::AlignContent::spaceBetween; 23 | alignContentMap["stretch"] = FlexBox::AlignContent::stretch; 24 | } 25 | 26 | // setup AlignItems mappings 27 | { 28 | alignItemsMap["center"] = FlexBox::AlignItems::center; 29 | alignItemsMap["end"] = FlexBox::AlignItems::flexEnd; 30 | alignItemsMap["start"] = FlexBox::AlignItems::flexStart; 31 | alignItemsMap["stretch"] = FlexBox::AlignItems::stretch; 32 | } 33 | 34 | // setup Direction mappings 35 | { 36 | directionMap["column"] = FlexBox::Direction::column; 37 | directionMap["column-reverse"] = FlexBox::Direction::columnReverse; 38 | directionMap["row"] = FlexBox::Direction::row; 39 | directionMap["row-reverse"] = FlexBox::Direction::rowReverse; 40 | } 41 | 42 | // setup Direction mappings 43 | { 44 | justifyContentMap["center"] = FlexBox::JustifyContent::center; 45 | justifyContentMap["end"] = FlexBox::JustifyContent::flexEnd; 46 | justifyContentMap["start"] = FlexBox::JustifyContent::flexStart; 47 | justifyContentMap["space-around"] = FlexBox::JustifyContent::spaceAround; 48 | justifyContentMap["space-between"] = FlexBox::JustifyContent::spaceBetween; 49 | } 50 | 51 | // setup Direction mappings 52 | { 53 | wrapMap["nowrap"] = FlexBox::Wrap::noWrap; 54 | wrapMap["wrap"] = FlexBox::Wrap::wrap; 55 | wrapMap["wrap-reverse"] = FlexBox::Wrap::wrapReverse; 56 | } 57 | 58 | // setup AlignSelf mappings 59 | { 60 | alignSelfMap["auto"] = FlexItem::AlignSelf::autoAlign; 61 | alignSelfMap["center"] = FlexItem::AlignSelf::center; 62 | alignSelfMap["end"] = FlexItem::AlignSelf::flexEnd; 63 | alignSelfMap["start"] = FlexItem::AlignSelf::flexStart; 64 | alignSelfMap["stretch"] = FlexItem::AlignSelf::stretch; 65 | } 66 | 67 | if (performLayoutNow) 68 | performLayout(); 69 | } 70 | 71 | //========================================================================== 72 | void FlexLayoutManager::performLayout() 73 | { 74 | FlexBox flex; 75 | 76 | for (int i = 0; i < element->getNumAttributes(); i++) 77 | { 78 | auto name = element->getAttributeName(i).toLowerCase(); 79 | auto value = element->getAttributeValue(i).trim(); 80 | 81 | if (name == "align-content") 82 | { 83 | flex.alignContent = alignContentMap[value]; 84 | } 85 | else if (name == "align-items") 86 | { 87 | flex.alignItems = alignItemsMap[value]; 88 | } 89 | else if (name == "direction") 90 | { 91 | flex.flexDirection = directionMap[value]; 92 | } 93 | else if (name == "justify-content") 94 | { 95 | flex.justifyContent = justifyContentMap[value]; 96 | } 97 | else if (name == "wrap") 98 | { 99 | flex.flexWrap = wrapMap[value]; 100 | } 101 | } 102 | 103 | for (int i = 0; i < element->getNumChildElements(); i++) 104 | { 105 | auto childElement = element->getChildElement(i); 106 | auto childComponent = reinterpret_cast(tagsMap[childElement->getTagName()]); 107 | 108 | FlexItem item(*childComponent); 109 | 110 | for (int j = 0; j < element->getNumAttributes(); j++) 111 | { 112 | auto name = childElement->getAttributeName(j).toLowerCase(); 113 | auto value = childElement->getAttributeValue(j).trim(); 114 | 115 | if (name == "align-self") 116 | { 117 | item.alignSelf = alignSelfMap[value]; 118 | } 119 | else if (name == "basis") 120 | { 121 | item.flexBasis = value.getFloatValue(); 122 | } 123 | else if (name == "grow") 124 | { 125 | item.flexGrow = value.getFloatValue(); 126 | } 127 | else if (name == "shrink") 128 | { 129 | item.flexShrink = value.getFloatValue(); 130 | } 131 | else if (name == "height") 132 | { 133 | item.height = value.getFloatValue(); 134 | } 135 | else if (name == "margin") 136 | { 137 | auto tokens = StringArray::fromTokens(value, false); 138 | 139 | if (tokens.size() == 1) 140 | { 141 | item.margin.top = tokens[0].getFloatValue(); 142 | item.margin.right = tokens[0].getFloatValue(); 143 | item.margin.bottom = tokens[0].getFloatValue(); 144 | item.margin.left = tokens[0].getFloatValue(); 145 | } 146 | else if (tokens.size() == 2) 147 | { 148 | item.margin.top = tokens[0].getFloatValue(); 149 | item.margin.right = tokens[1].getFloatValue(); 150 | item.margin.bottom = tokens[0].getFloatValue(); 151 | item.margin.left = tokens[1].getFloatValue(); 152 | } 153 | else if (tokens.size() == 3) 154 | { 155 | item.margin.top = tokens[0].getFloatValue(); 156 | item.margin.right = tokens[1].getFloatValue(); 157 | item.margin.bottom = tokens[2].getFloatValue(); 158 | item.margin.left = tokens[1].getFloatValue(); 159 | } 160 | else if (tokens.size() == 4) 161 | { 162 | item.margin.top = tokens[0].getFloatValue(); 163 | item.margin.right = tokens[1].getFloatValue(); 164 | item.margin.bottom = tokens[2].getFloatValue(); 165 | item.margin.left = tokens[3].getFloatValue(); 166 | } 167 | else 168 | { 169 | jassertfalse; 170 | } 171 | } 172 | else if (name == "max-height") 173 | { 174 | item.maxHeight = value.getFloatValue(); 175 | } 176 | else if (name == "max-width") 177 | { 178 | item.maxWidth = value.getFloatValue(); 179 | } 180 | else if (name == "min-height") 181 | { 182 | item.minHeight = value.getFloatValue(); 183 | } 184 | else if (name == "min-width") 185 | { 186 | item.minWidth = value.getFloatValue(); 187 | } 188 | else if (name == "order") 189 | { 190 | item.order = value.getIntValue(); 191 | } 192 | else if (name == "width") 193 | { 194 | item.width = value.getFloatValue(); 195 | } 196 | } 197 | 198 | flex.items.add(item); 199 | } 200 | 201 | flex.performLayout(component->getBounds()); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/jml_FlexLayoutManager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | //============================================================================== 4 | namespace jml 5 | { 6 | using namespace juce; 7 | 8 | //========================================================================== 9 | class FlexLayoutManager 10 | { 11 | public: 12 | //====================================================================== 13 | FlexLayoutManager(std::unordered_map& tags, 14 | XmlElement* elementForLayout, 15 | Component* componentToPerformLayoutFor, 16 | bool performLayoutNow = true); 17 | 18 | void performLayout(); 19 | 20 | private: 21 | //====================================================================== 22 | std::unordered_map& tagsMap; 23 | 24 | XmlElement* element = nullptr; 25 | Component* component = nullptr; 26 | 27 | // FlexBox maps 28 | std::unordered_map alignContentMap; 29 | std::unordered_map alignItemsMap; 30 | std::unordered_map directionMap; 31 | std::unordered_map justifyContentMap; 32 | std::unordered_map wrapMap; 33 | 34 | // FlexItem maps 35 | std::unordered_map alignSelfMap; 36 | }; 37 | } -------------------------------------------------------------------------------- /src/jml_GridLayoutManager.cpp: -------------------------------------------------------------------------------- 1 | //============================================================================== 2 | namespace jml 3 | { 4 | //========================================================================== 5 | GridLayoutManager::GridLayoutManager(std::unordered_map& tags, 6 | XmlElement* ele, 7 | Component* comp, 8 | bool performLayoutNow) 9 | : tagsMap(tags), 10 | element(ele), 11 | component(comp) 12 | { 13 | jassert(element != nullptr); 14 | jassert(component != nullptr); 15 | 16 | // setup the AlignContent mappings 17 | { 18 | alignContentMap["center"] = Grid::AlignContent::center; 19 | alignContentMap["end"] = Grid::AlignContent::end; 20 | alignContentMap["space-around"] = Grid::AlignContent::spaceAround; 21 | alignContentMap["space-between"] = Grid::AlignContent::spaceBetween; 22 | alignContentMap["space-evenly"] = Grid::AlignContent::spaceEvenly; 23 | alignContentMap["start"] = Grid::AlignContent::start; 24 | alignContentMap["stretch"] = Grid::AlignContent::stretch; 25 | } 26 | 27 | // setup the AlignItems mappings 28 | { 29 | alignItemsMap["center"] = Grid::AlignItems::center; 30 | alignItemsMap["end"] = Grid::AlignItems::end; 31 | alignItemsMap["start"] = Grid::AlignItems::start; 32 | alignItemsMap["stretch"] = Grid::AlignItems::stretch; 33 | } 34 | 35 | // setup the AutoFlow mappings 36 | { 37 | autoFlowMap["column"] = Grid::AutoFlow::column; 38 | autoFlowMap["column-dense"] = Grid::AutoFlow::columnDense; 39 | autoFlowMap["row"] = Grid::AutoFlow::row; 40 | autoFlowMap["row-dense"] = Grid::AutoFlow::rowDense; 41 | } 42 | 43 | // setup the JustifyContent mappings 44 | { 45 | justifyContentMap["center"] = Grid::JustifyContent::center; 46 | justifyContentMap["end"] = Grid::JustifyContent::end; 47 | justifyContentMap["space-around"] = Grid::JustifyContent::spaceAround; 48 | justifyContentMap["space-between"] = Grid::JustifyContent::spaceBetween; 49 | justifyContentMap["space-evenly"] = Grid::JustifyContent::spaceEvenly; 50 | justifyContentMap["start"] = Grid::JustifyContent::start; 51 | justifyContentMap["stretch"] = Grid::JustifyContent::stretch; 52 | } 53 | 54 | // setup the JustifyItems mappings 55 | { 56 | justifyItemsMap["center"] = Grid::JustifyItems::center; 57 | justifyItemsMap["end"] = Grid::JustifyItems::end; 58 | justifyItemsMap["start"] = Grid::JustifyItems::start; 59 | justifyItemsMap["stretch"] = Grid::JustifyItems::stretch; 60 | } 61 | 62 | // setup the AlignSelf mappings 63 | { 64 | alignSelfMap["auto"] = GridItem::AlignSelf::autoValue; 65 | alignSelfMap["center"] = GridItem::AlignSelf::center; 66 | alignSelfMap["end"] = GridItem::AlignSelf::end; 67 | alignSelfMap["start"] = GridItem::AlignSelf::start; 68 | alignSelfMap["stretch"] = GridItem::AlignSelf::stretch; 69 | } 70 | 71 | // setup the JustifySelf mappings 72 | { 73 | justifySelfMap["auto"] = GridItem::JustifySelf::autoValue; 74 | justifySelfMap["center"] = GridItem::JustifySelf::center; 75 | justifySelfMap["end"] = GridItem::JustifySelf::end; 76 | justifySelfMap["start"] = GridItem::JustifySelf::start; 77 | justifySelfMap["stretch"] = GridItem::JustifySelf::stretch; 78 | } 79 | 80 | if (performLayoutNow) 81 | performLayout(); 82 | } 83 | 84 | //========================================================================== 85 | void GridLayoutManager::performLayout() 86 | { 87 | // setup the gGrid 88 | Grid grid; 89 | StringArray columnTokens; 90 | StringArray rowTokens; 91 | BorderSize margin; 92 | 93 | for (int i = 0; i < element->getNumAttributes(); i++) 94 | { 95 | auto name = element->getAttributeName(i).toLowerCase(); 96 | auto value = element->getAttributeValue(i).trim(); 97 | 98 | if (name == "align-content") 99 | { 100 | grid.alignContent = alignContentMap[value]; 101 | } 102 | else if (name == "align-items") 103 | { 104 | grid.alignItems = alignItemsMap[value]; 105 | } 106 | else if (name == "auto-columns") 107 | { 108 | if (value.endsWith("fr")) 109 | { 110 | auto fraction = value.upToFirstOccurrenceOf("fr", false, true).getIntValue(); 111 | grid.autoColumns = Grid::TrackInfo(Grid::Fr(fraction)); 112 | } 113 | else 114 | { 115 | auto fraction = value.getIntValue(); 116 | grid.autoColumns = Grid::TrackInfo(Grid::Px(fraction)); 117 | } 118 | } 119 | else if (name == "auto-flow") 120 | { 121 | grid.autoFlow = autoFlowMap[value]; 122 | } 123 | else if (name == "auto-rows") 124 | { 125 | if (value.endsWith("fr")) 126 | { 127 | auto fraction = value.upToFirstOccurrenceOf("fr", false, true).getIntValue(); 128 | grid.autoRows = Grid::TrackInfo(Grid::Fr(fraction)); 129 | } 130 | else 131 | { 132 | auto fraction = value.getIntValue(); 133 | grid.autoRows = Grid::TrackInfo(Grid::Px(fraction)); 134 | } 135 | } 136 | else if (name == "column-gap") 137 | { 138 | grid.columnGap = Grid::Px(value.getIntValue()); 139 | } 140 | else if (name == "columns") 141 | { 142 | auto numColumns = value.getIntValue(); 143 | 144 | columnTokens.clear(); 145 | 146 | for (int i = 0; i < numColumns; i++) 147 | columnTokens.add("1fr"); 148 | } 149 | else if (name == "gap") 150 | { 151 | auto gap = JML::attributeStringToInt(component, name, value); 152 | grid.setGap(Grid::Px(gap)); 153 | } 154 | else if (name == "justify-content") 155 | { 156 | grid.justifyContent = justifyContentMap[value]; 157 | } 158 | else if (name == "justify-items") 159 | { 160 | grid.justifyItems = justifyItemsMap[value]; 161 | } 162 | else if (name == "margin") 163 | { 164 | auto tokens = StringArray::fromTokens(value, false); 165 | 166 | if (tokens.size() == 1) 167 | margin = BorderSize(tokens[0].getIntValue()); 168 | else if (tokens.size() == 2) 169 | { 170 | margin.setTop(tokens[1].getIntValue()); 171 | margin.setRight(tokens[0].getIntValue()); 172 | margin.setBottom(tokens[1].getIntValue()); 173 | margin.setLeft(tokens[0].getIntValue()); 174 | } 175 | else if (tokens.size() == 3) 176 | { 177 | margin.setTop(tokens[0].getIntValue()); 178 | margin.setRight(tokens[1].getIntValue()); 179 | margin.setBottom(tokens[2].getIntValue()); 180 | margin.setLeft(tokens[1].getIntValue()); 181 | } 182 | else if (tokens.size() == 4) 183 | { 184 | margin.setTop(tokens[0].getIntValue()); 185 | margin.setRight(tokens[1].getIntValue()); 186 | margin.setBottom(tokens[2].getIntValue()); 187 | margin.setLeft(tokens[3].getIntValue()); 188 | } 189 | else 190 | { 191 | // Invalid number of values in margin string. 192 | // Should be 1, 2, 3, or 4 193 | jassertfalse; 194 | } 195 | } 196 | else if (name == "row-gap") 197 | { 198 | grid.rowGap = Grid::Px(value.getIntValue()); 199 | } 200 | else if (name == "rows") 201 | { 202 | auto numRows = value.getIntValue(); 203 | 204 | rowTokens.clear(); 205 | 206 | for (int i = 0; i < numRows; i++) 207 | rowTokens.add("1fr"); 208 | } 209 | else if (name == "template-areas") 210 | { 211 | auto tokens = StringArray::fromTokens(value, " ", "'"); 212 | 213 | for (const auto& token : tokens) 214 | grid.templateAreas.add(token.unquoted()); 215 | } 216 | else if (name == "template-columns") 217 | { 218 | columnTokens = StringArray::fromTokens(value, false); 219 | } 220 | else if (name == "template-rows") 221 | { 222 | rowTokens = StringArray::fromTokens(value, false); 223 | } 224 | } 225 | 226 | for (const auto& token : columnTokens) 227 | { 228 | if (token.endsWith("fr")) 229 | { 230 | auto fraction = token.upToFirstOccurrenceOf("fr", false, true).getIntValue(); 231 | grid.templateColumns.add(Grid::Fr(fraction)); 232 | } 233 | else 234 | { 235 | auto fraction = token.getIntValue(); 236 | grid.templateColumns.add(Grid::Px(fraction)); 237 | } 238 | } 239 | 240 | for (const auto& token : rowTokens) 241 | { 242 | if (token.endsWith("fr")) 243 | { 244 | auto fraction = token.upToFirstOccurrenceOf("fr", false, true).getIntValue(); 245 | grid.templateRows.add(Grid::Fr(fraction)); 246 | } 247 | else 248 | { 249 | auto fraction = token.getIntValue(); 250 | grid.templateRows.add(Grid::Px(fraction)); 251 | } 252 | } 253 | 254 | // setup the GridItems 255 | for (int i = 0; i < element->getNumChildElements(); i++) 256 | { 257 | auto childElement = element->getChildElement(i); 258 | auto childComponent = reinterpret_cast(tagsMap[childElement->getTagName()]); 259 | 260 | GridItem item(childComponent); 261 | 262 | for (int j = 0; j < childElement->getNumAttributes(); j++) 263 | { 264 | auto name = childElement->getAttributeName(j).toLowerCase(); 265 | auto value = childElement->getAttributeValue(j).trim(); 266 | 267 | if (name == "align-self") 268 | { 269 | item.alignSelf = alignSelfMap[value]; 270 | } 271 | else if (name == "area") 272 | { 273 | item.area = value; 274 | } 275 | else if (name == "column") 276 | { 277 | auto tokens = StringArray::fromTokens(value, false); 278 | 279 | GridItem::StartAndEndProperty startAndEnd; 280 | 281 | if (tokens.size() == 1) 282 | { 283 | startAndEnd.start = tokens[0].getIntValue(); 284 | startAndEnd.end = tokens[0].getIntValue(); 285 | } 286 | else if (tokens.size() == 2) 287 | { 288 | startAndEnd.start = tokens[0].getIntValue(); 289 | startAndEnd.end = tokens[1].getIntValue(); 290 | } 291 | 292 | item.column = startAndEnd; 293 | } 294 | else if (name == "height") 295 | { 296 | item.height = value.getFloatValue(); 297 | } 298 | else if (name == "justify-self") 299 | { 300 | item.justifySelf = justifySelfMap[value]; 301 | } 302 | else if (name == "margin") 303 | { 304 | auto tokens = StringArray::fromTokens(value, false); 305 | 306 | if (tokens.size() == 1) 307 | { 308 | item.margin.top = tokens[0].getFloatValue(); 309 | item.margin.right = tokens[0].getFloatValue(); 310 | item.margin.bottom = tokens[0].getFloatValue(); 311 | item.margin.left = tokens[0].getFloatValue(); 312 | } 313 | else if (tokens.size() == 2) 314 | { 315 | item.margin.top = tokens[0].getFloatValue(); 316 | item.margin.right = tokens[1].getFloatValue(); 317 | item.margin.bottom = tokens[0].getFloatValue(); 318 | item.margin.left = tokens[1].getFloatValue(); 319 | } 320 | else if (tokens.size() == 3) 321 | { 322 | item.margin.top = tokens[0].getFloatValue(); 323 | item.margin.right = tokens[1].getFloatValue(); 324 | item.margin.bottom = tokens[2].getFloatValue(); 325 | item.margin.left = tokens[1].getFloatValue(); 326 | } 327 | else if (tokens.size() == 4) 328 | { 329 | item.margin.top = tokens[0].getFloatValue(); 330 | item.margin.right = tokens[1].getFloatValue(); 331 | item.margin.bottom = tokens[2].getFloatValue(); 332 | item.margin.left = tokens[3].getFloatValue(); 333 | } 334 | else 335 | { 336 | jassertfalse; 337 | } 338 | } 339 | else if (name == "max-height") 340 | { 341 | item.maxHeight = value.getFloatValue(); 342 | } 343 | else if (name == "max-width") 344 | { 345 | item.maxWidth = value.getFloatValue(); 346 | } 347 | else if (name == "min-height") 348 | { 349 | item.minHeight = value.getFloatValue(); 350 | } 351 | else if (name == "min-width") 352 | { 353 | item.minWidth = value.getFloatValue(); 354 | } 355 | else if (name == "order") 356 | { 357 | item.order = value.getIntValue(); 358 | } 359 | else if (name == "row") 360 | { 361 | auto tokens = StringArray::fromTokens(value, false); 362 | 363 | GridItem::StartAndEndProperty startAndEnd; 364 | 365 | if (tokens.size() == 1) 366 | { 367 | startAndEnd.start = tokens[0].getIntValue(); 368 | startAndEnd.end = tokens[0].getIntValue(); 369 | } 370 | else if (tokens.size() == 2) 371 | { 372 | startAndEnd.start = tokens[0].getIntValue(); 373 | startAndEnd.end = tokens[1].getIntValue(); 374 | } 375 | 376 | item.row = startAndEnd; 377 | } 378 | else if (name == "width") 379 | { 380 | item.width = value.getFloatValue(); 381 | } 382 | } 383 | 384 | grid.items.add(item); 385 | } 386 | 387 | grid.performLayout(margin.subtractedFrom(component->getBounds())); 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /src/jml_GridLayoutManager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | //============================================================================== 4 | namespace jml 5 | { 6 | using namespace juce; 7 | 8 | //========================================================================== 9 | class GridLayoutManager 10 | { 11 | public: 12 | //====================================================================== 13 | GridLayoutManager(std::unordered_map& tags, 14 | XmlElement* elementForLayout, 15 | Component* componentToPerformLayoutFor, 16 | bool performLayoutNow = true); 17 | 18 | void performLayout(); 19 | 20 | private: 21 | //====================================================================== 22 | std::unordered_map& tagsMap; 23 | 24 | XmlElement* element = nullptr; 25 | Component* component = nullptr; 26 | 27 | // Grid maps 28 | std::unordered_map alignContentMap; 29 | std::unordered_map alignItemsMap; 30 | std::unordered_map autoFlowMap; 31 | std::unordered_map justifyContentMap; 32 | std::unordered_map justifyItemsMap; 33 | 34 | // GridItem maps 35 | std::unordered_map alignSelfMap; 36 | std::unordered_map justifySelfMap; 37 | }; 38 | } --------------------------------------------------------------------------------