Proposal improve of look and feel of Juce's applications


#43

This is certainly a key part of the trick to it. If your users only ever do the layout via tooling (i.e. where all inputs are validated), then it’s reasonable that can be achieved. But if you’re building a CSS-like system where developers are able to write the css rules themselves, then it becomes critically important to have tooling to help with sorting out where the rules have gone wrong. If you’ve done web development for a number of years, you’ll certainly know the value of the tooling that browsers have for this these days.


#44

Oh absolutely, and you can tell, that I didn’t. The full features of CSS is way beyond me.
But how often is that needed in this environment?

Anyway, I just wanted to share my ideas, in case somebody wants to team up. I don’t claim to deliver a solution to solve all worlds problems… :wink:


#45

Ok, so there is a risk that I write some non-sense in this post so please be patient and correct me if necessary…

By reading the comments, most of us wouldn’t be necessarily opposed to replace the ColourId system by a setStyle(String styleDefinition) system where “styleDefinition” is in the CSS or a similar designer-readable format. This could optionally incorporate later other tags than just colors to configure the Component in other ways but without forcing anybody to do so.

But this should be backwards compatible and it shouldn’t add a significant run-time overhead.

So let’s see if this is possible…

Current system

I don’t understand everything well, specially the getColourPropertyID bit but when a colour is set or retrieved the colourId is transformed to an Identifier(String) with this rule:

    static Identifier getColourPropertyID (int colourID)
    {
    char buffer[32];
    auto* end = buffer + numElementsInArray (buffer) - 1;
    auto* t = end;
    *t = 0;

    for (auto v = (uint32) colourID;;)
    {
        *--t = "0123456789abcdef" [v & 15];
        v >>= 4;

        if (v == 0)
            break;
    }

    for (int i = (int) sizeof (colourPropertyPrefix) - 1; --i >= 0;)
        *--t = colourPropertyPrefix[i];

    return t;
}

Then it looks the name-value up in a NamedValueSet which is at the end an array of String-var pairs.

By looking at this it seems to me that the values are set then in run-time and that the algorithm to set and get the values is O(N) because it involves a search in an array and not a simple access (which would be O(1)).

Alternative 1 (feasible)

Why don’t we use for the new system a std::map with garanteed O(log N) access, search, insertion and deletion?

Cheat sheet for data structure operations: http://bigocheatsheet.com/

Actually it seems to me that this would even reduce the run-time overhead.

Alternative 2 (future?)

Some people are already experimenting with compile time parsers thanks to the introduction of C++17.

https://blog.therocode.net/2018/09/compile-time-string-parsing

That would be fantastic (the format strings would be translated to values in compile time) but to be honest this looks like a bumpy ride. Maybe in a few years? In my opinion though, it means that if we take this path, we will eventually be able to move everything to the compiler if we keep the syntax simple enough. I wouldn’t be surprised if somebody came up with a compile time CSS parser as one of the first examples of these new capabilities, so this would be another reason to adopt CSS, to be able to use work from other areas where there is more investment.


#46

I think the resolving text and traversing the DOM is not really the big deal, that’s what I said before. There are even classes in JUCE like RelativeRectangle, that allow proportions to be specified in text form.

I understand, that there are CSS rules, that are rather code than just single instructions or references, which is when it starts to become tricky, like @Graeme pointed out.

And again, I don’t think the performance to resolve the CSS with a proper caching is the problem to worry about, how often is the layout code evaluated per second anyway, compared with your DSP code?

IMHO the challenge is to be compliant with the existing classes and to cover all future use cases. Not like the ComponentBuilder, that just stopped to grow at some point, which I wanted to use for a dynamic system back then…


#47

Is there really a need to implement those rules? The intention of my first post in this thread was to suggest the use primitive CSS to define only format parameters. Defining anything that specifies the interaction between different components would open a Pandora’s box and most of us have already coded this in C++. I actually prefer it this way and I don’t really think a designer will ever use anything like QML, so I’m personally not motivated to go beyond that.

Maybe we could implement this in a separate “Style” class and derive the new Components from it?


#48

Whatever is done, it should probably be in a bundle. Something like JUCE_Css.

I hadn’t thought about this, but holy cow are you right. Debugging CSS is already pretty ugly, but this would be a nightmare.

A pretty easy solution would be to extend AudioPluginHost and have it set up a local server which returns a non-interactive raw html/css version of the current state of the plugin.

You could debug ui like so:

  1. Navigate your plugin to the broken view
  2. Refresh your browser at (localhost::[AUDIO_PLUGIN_HOST_SERVER_PORT])
  3. The plugin renders a non interactive dom and appends all plugin css
    • Basically just get the components to return a DOM of <div class="myClassNames" id="myId">
  4. Then figure out which css is the problem, update it, recompile and start over
    • (Bonus points) the plugin watches the css for updates and does it without recompiling

Practically no overheard, and the only assumption is that the developer has a browser (with dev tools).


#49

So, maybe we should get started. Is anyone else interested in finding an open source CSS/Dom rendering library and creating a

#import "SomeOpenSourceCssLibrary.h"

class CssComponent
  : public JUCE::Component,
    public SomeOpenCssLib::DomElement
{
}

I’d really love to work on this project, but not alone.

I’m thinking the first step is to find a solid FOSS Css/Dom library. Anyone got a lead on that?

EDIT: for anyone who’d rather not use CSS/Dom, that’s fine. But I’d still like to work on it, with others. As long as they’re modules, JUCE can have more than one rendering style.

EDIT: I’m currently in contact with the slack channel for https://ultralig.ht/. It’s not a perfectly FOSS solution, but ASFAICT it’s the best mostly open source css/dom rendering library out there.


#50

@daniel Fwiw, I was just making a general comment about these kinds of systems, nothing specific to your implementation. :slight_smile:


#51

Alright. After doing some research, it ends up there is no open source bare-bones css/dom rendering code base out there.

LibRocket was close, but it’s 2 years dead.

Ultralight has all the right functionality, but after talking to the dev it’s become apparent that it’s behind a closed source wall. Another perfectly good project made useless by greed.

From there, the best I could find is the WebCore section of webkit. A solid starting point would be Node.h (https://trac.webkit.org/browser/webkit/branches/buildbot-0.9/Source/WebCore/dom/Node.h). It doesn’t look too bloated in terms of what it does. Things like shadowRoot and documentFragment functionality are totally unnecessary, but the rest of it seems essential.

I also found this old but still accurate blog on using WebCore: https://webkit.org/blog/114/webcore-rendering-i-the-basics/


#52

Also, regarding layout, a widely used library is https://yogalayout.com (used in React Native)

From my experience using React Native, a simple declarative XML to represent the component tree, along with stylesheets (not cascading), works really well. Basically, each component has a set of “style” attributes which it can respond to; basic ones such as backgroundColour, width etc as well as component-specific attributes.

With a system like that it becomes very quick to re-style components. The current JUCE LookAndFeel system can feel very cumbersome in comparison.

My opinion: I don’t see a need to recreate CSS here, I often found CSS overly complex and confusing to use in my web development days. I think JSON or dictionary objects to represent styles would be sufficient.

Something like this? (a simplified example from React Native translated into C++)

typedef std::map<String,std::map<String,var>> StyleSheet;

struct StyledComponent : public Component
{
	StyleSheet styles =
	{
		{
			"container",
			{
				{"borderRadius", 4},
				{"borderWidth", 0.5f},
				{"borderColor", "#d6d7da"}
			}
		},
		{

			"title",
			{
				{"fontSize", 19},
				{"fontWeight", "bold"}
			}
		}
	};

	String layoutXml = R"(
	<ContainerComponent styles="container">
      <Label styles="title"/>
    </ContainerComponent>
	)";
};

If preferred, the layout could be imported from an XML file, and the stylesheet from e.g. a JSON/TOML file.


#53

+1

But I still suggest the StyleSheets syntax to be able to use an existing standard, with an optional “style” tag to define/override parameters directly in the XML.

    String layoutXml = R"(
	<ContainerComponent id="container">
      <Label id="title" style="font-family: 'Times New Roman', Times, serif;"/>
    </ContainerComponent>
	)";
}

I agree with the data structure, I would only add two methods: one to import from JSON and one from CSS (without the cascading) which is a bit less verbose and great for quick inline definitions.

typedef std::map<String,var> Style;
typedef std::map<String,Style> StyleSheet;

struct Layout {
    StyleSheet styleSheet;
    void setLayout(String xml);
    Style &getStyle(String id);
};

struct StyledComponent : public Component{
    Style style;
    void setStyleJSON(String json);
    void setStyleCSS(String css);
    var getStyleProperty(String propertyName);
};

In terms of propertyName, there is room for implementing them progressively (actually not all of them make sense in every Component) but we could use as a reference those marked in green as a W3C Recommendation (REC) in the CSS standard:
https://www.w3.org/Style/CSS/all-properties.en.html
(There are 148 properties in total in green, and I think that those which relate to speech, video or audio can be safely ignored, leaving 116 that are relevant)


#54

Isn’t it ironic, that this page has layout problems? :rofl:


#55

Well you clearly don’t remember or didn’t have the chance to enjoy the great days of Mozilla and Internet Explorer, not to mention the wonders of Flash. Websites would even show up blank if you were using the wrong browser or operating system. Actually at that time, designers wouldn’t even know CSS, all they knew was Flash so all I’m saying is let’s build on that progress…


#56

That was no criticism on the achievements of CSS and web technology. I do remember this non-standard enhancements, the battle of political influence over things like round corners and other behaviour. The fights between IE and Netscape Navigator, in fact, I wrote my first website in 1998.

But I avoid web design for exactly that reason, because you are constantly fixing other peoples bugs or working around their “features”. I don’t miss it, and my hair stands up, when I have to touch that.

But I follow the argument, that it would be good to use technology, that people are already familiar with, that’s why I agree a layout and styling system should adapt as much as possible the CSS/DOM approach.


#57

So you went for plugins :wink:


#58

Haha, indeed… not much different in some regards :wink:


#59

I agree with the extra methods you suggest. However, I still disagree about using CSS. CSS is a web standard; it needs to be a standard for web browsers to be able to render HTML layouts in a consistent way. I see no reason to follow that standard to the tee when writing a styling engine for JUCE.

e.g. font-family: 'Times New Roman', Times, serif; - there is a lot to implement just in that small piece of inline styling. And it is not clear what it does, unless you already know CSS. Why not just give a font name? And why use dash-case when everything else in JUCE is camelCase? I have to say, it’s one of the things I really like about React Native: that they did not re-invent CSS, but took the idea, and simplified it to a level that is easy and intuitive to use, e.g.

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  title: {
    fontSize: 25,
    fontWeight: 'bold',
  }
}); 

As @daniel also mentioned, QML is another nice example of styling for native applications.

On that note, a very cool project to check out is http://log.fundamental-code.com/2018/06/16/mruby-zest.html - he used embedded Ruby (mruby) and an adaptation of QML to build a plugin UI.


#60

I get your point, but those are all arguments from a coder’s perspective and I insist once again that the only reason that I have to propose CSS is because I don’t find a reason to teach the designers another set of tags, write a reference for that or even more challenging: making them read it, so I find it irrelevant that JUCE uses camelCase. PS. Do you think it is possible to make a designer respect the camelCase convention? :face_with_raised_eyebrow:

CSS is terrible (https://stackoverflow.com/questions/2501723/css-color-vs-font-color/2501741) but can we really change it now? I mean if you can convince me that there is another way to effectively communicate with graphic artists I’m all for it.

The font-family line might be a PITA to implement but it says: I’d like the ‘Times New Roman’ font but if you don’t have it your system, use ‘Times’ and if you don’t have this one either, just use one in the serif family. It solves a very real situation where you don’t want or you can’t embed the font that you want in your plugin.

Still, there is not need to fully implement CSS, we can definitely agree on a small subset of properties with single values.


#61

Ok, I guess we have different use cases; the only designers I know tend to avoid any form of code, or maybe know a little HTML/CSS, but generally, I’m the one implementing their designs from PSD/SVG files. I don’t expect designers to touch my styling code; my aim is to reduce the friction in styling JUCE components, and make it quick and painless to try out and adjust styles and layouts.


#63

Ok, here’s another proposal. And if we allow for multiple sets of property names to coexist by agreeing on a minimal set of “standard JUCE tags”?

I came out with the following code where, after declaring the property names that you deem to be better suited to your use case, you can change:

#define StyleSyntax CSS

and let’s say set it to your QML list of properties:

#define StyleSyntax QML

The syntax is defined in compile time, the properties accessed in O(log(N)) and the getters and setters for each property are defined in the same place with lambdas. It also allows you to declare your own non-compatible properties in your custom Components. I welcome your suggestions to improve the code.

enum StyleIds {
  backgroundColourId,
  borderBottomColourId,
  borderBottomWidthId,
  borderLeftColourId,
  borderLeftWidthId,
  borderRightColourId,
  borderRightWidthId,
  borderTopColourId,
  borderTopWidthId,
  outlineColourId,
  outlineWidthId,
  caretColourId,
  cursorId,
  directionId,
  fontFamilyId,
  fontKerningId,
  fontSizeId,
  fontStyleId,
  textColourId,
  colourId,
  fontWeightId,
  xId,
  yId,
  heightId,
  widthId,
  marginBottomId,
  marginLeftId,
  marginRightId,
  marginTopId,
  opacityId,
  paddingBottomId,
  paddingLeftId,
  paddingRightId,
  paddingTopId,
  horizontalAlignId,
  verticalAlignId,
  zIndexId,
  numStyleIds
};

const String CSS[numStyleIds] = {
  "background-color", //backgroundColourId,
  "border-bottom-color", //borderBottomColourId,
  "border-bottom-width", //borderBottomWidthId,
  "border-left-color", //borderLeftColourId,
  "border-left-width", //borderLeftWidthId,
  "border-right-color", //borderRightColourId,
  "border-right-width", //borderRightWidthId,
  "border-top-color", //borderTopColourId,
  "border-top-width", //borderToptWidthId,
  "outline-color", //outlineColourId,
  "outline-width", //outlineWidthId,
  "caret-color", //caretColourId,
  "cursor", //cursorId,
  "direction", //directionId,
  "font-family", //fontFamilyId,
  "font-kerning", //fontKerningId,
  "font-size", //fontSizeId,
  "font-style", //fontStyleId,
  "color", //textColourId,
  "color", //colourId,
  "font-weight", //fontWeightId,
  "left", //xId,
  "top", //yId,
  "height", //heightId,
  "width", //widthId,
  "margin-bottom", //marginBottomId,
  "margin-left", //marginLeftId,
  "margin-right", //marginRightId,
  "margin-top", //marginTopId,
  "opacity", //opacityId,
  "padding-bottom", //paddingBottomId,
  "padding-left", //paddingLeftId,
  "padding-right", //paddingRightId,
  "padding-top", //paddingTopId,
  "text-align", //horizontalAlignId,
  "vertical-algin", //verticalAlignId,
  "z-index" //zIndexId
};

//change this line to compile with a different syntax
#define StyleSyntax CSS  

#if (JUCE_DEBUG)
  #define logError(errorString){ Logger::writeToLog(errorString); }
#else
  #define logError(errorString){}
#endif

//list of fonts available in the system
const StringArray SYSTEM_TYPEFACE_NAMES=Font::findAllTypefaceNames();

static Colour parseColour(const String &str){
  String colourStr(str.fromFirstOccurrenceOf("#",false,false));
  if (colourStr.isEmpty())
    colourStr=str;
  if (colourStr.length()==6) //alpha missing
    colourStr="FF"+colourStr;
  return Colour(colourStr.getHexValue32());
}

static String removeInvisibleChars(const String &str) {
  int start=0;
  while (start<str.length() && (str[start]==' ' || str[start]=='\n' || str[start]=='\r' || str[start]=='\t' )) start++;
  int end=str.length()-1;
  while (end>0 && (str[start]==' ' || str[start]=='\n' || str[start]=='\r' || str[start]=='\t' )) end--;
  return str.substring(start,end+1);
}

static bool isStringOnlyNumeric(const String &str )
  {
    if (str.length() == String(str.getFloatValue()).length())
      return true;

    return false;
  }

class Styled {    
public:
  typedef std::function<void(const var&)> Setter;
  typedef std::function<var()> Getter;
  typedef std::pair<Setter,Getter> PropertyMethods;
  typedef std::map<String, PropertyMethods> Style;  

  Styled(Style &style,const String &name=String()): name(name),style(style){}
  virtual ~Styled(){}   

  bool hasProperty(const String &propertyName) {
    return (style.find(propertyName)!=style.end());
  }

  String getSupportedProperties() {
    String keys="";
    for (auto it : style) {
      keys+=it.first+",";
    }
    return keys.substring(0,keys.length()-1);
  }

  void setProperty(const String &property, const var &value) {
    auto it=style.find(property);
    if (it!=style.end()){
      it->second.first(value);
    } else {
      logError("setProperty error: '"+String(property)+ "' is not a supported property in component '"+name+"'")
    } 
  }

  void setProperty(StyleIds propertyId,const var &value) {
    setProperty(StyleSyntax[propertyId],value);
  }

  var getProperty(const String &property) {
    auto it=style.find(property);
    if (it!=style.end()){
      return it->second.second();
    } else {
      logError("getProperty error: '"+String(property)+ "' is not a supported property in component '"+name+"'")
      return var::null;
    }
  }

  var getProperty(StyleIds propertyId) {
    return getProperty(StyleSyntax[propertyId]);
  }

  virtual void styleChanged(const String &styleStr){}

  bool setStyle(const String &styleStr, bool callStyleChanged=true) {
    bool error=false;
    StringArray attributeList;
    attributeList.addTokens(styleStr,";","");
    for (auto attribute : attributeList) {
      if (removeInvisibleChars(attribute).isNotEmpty()){
        StringArray fields;
        fields.addTokens(attribute,":","\"'");
        if (fields.size()==2){
          setProperty(removeInvisibleChars(fields[0]),removeInvisibleChars(fields[1]));
        } else {
          logError("style error: wrong format '"+attribute+"' in Component '"+name+"'")
          error=true;
        }
      }
    }
    if (callStyleChanged)
      styleChanged(styleStr);
    return error;
  }
private:
  const String name;
  Style &style;
};

class StyledLabel :  public Label,
                     public Styled{  

  Style style = {
    {StyleSyntax[StyleIds::fontFamilyId], PropertyMethods(
      [this](const var &value){
        //sets the font to the first typeface available in this system
        StringArray typefaceList;
        String listOfFamilies=value.toString();
        typefaceList.addTokens(listOfFamilies,",","\"'");
        for (auto typeface:typefaceList){
          if (SYSTEM_TYPEFACE_NAMES.contains(removeInvisibleChars(typeface),true)){
            //get the lower and upper cases right
            fontFamily=SYSTEM_TYPEFACE_NAMES[SYSTEM_TYPEFACE_NAMES.indexOf(removeInvisibleChars(typeface),true)];
            break;
          }
        }
        //check if the fontStyle is valid with the chosen font family
        if (fontStyle.isNotEmpty()) {
          StringArray systemStyles=juce::Font::findAllTypefaceStyles(fontFamily);          
          if (systemStyles.contains(fontStyle,true)){
            //get the lower and upper cases right
            fontStyle=systemStyles[systemStyles.indexOf(fontStyle,true)];
          }
          else {
            logError("Label '"+getName()+"' with font-family "+fontFamily+" has not the following style available: "+fontStyle);
            fontStyle="";
          }
        }
        updateFont();
      },
      [this](){
        return var(fontFamily);
      })
    },
    {StyleSyntax[StyleIds::fontSizeId], PropertyMethods(
      [this](const var &value){
        String sizeStr=value.toString();

        if (sizeStr.endsWith("pt")) {
          sizeStr=sizeStr.upToLastOccurrenceOf("pt",false,true);
          if (isStringOnlyNumeric(sizeStr)) {
            fontSize=sizeStr.getFloatValue();
            fontSizeIsInPt=true;
          } else {
            logError("Label '"+getName()+"' has a font-size with wrong format: "+sizeStr);
          }

        } else if (sizeStr.endsWith("px")) {
          sizeStr=sizeStr.upToLastOccurrenceOf("px",false,true);
          if (isStringOnlyNumeric(sizeStr)) {
            fontSize=sizeStr.getFloatValue();
            fontSizeIsInPt=false;
          }  else {
            logError("Label '"+getName()+"' has a font-size with wrong format: "+sizeStr);
          }

        } else {
          if (isStringOnlyNumeric(sizeStr)) {
            fontSize=sizeStr.getFloatValue();
            fontSizeIsInPt=true;
          } else {
            logError("Label '"+getName()+"' has a font-size with wrong format: "+sizeStr);
          }
        }
        updateFont();
      },
      [this](){
        return var(String(fontSize)+String((fontSizeIsInPt)? "pt":"px"));
      })
    },
    {StyleSyntax[StyleIds::fontStyleId], PropertyMethods(
      [this](const var &value){                    
        /* this is not the implementation of the CSS font-style
           because in CSS font-style only indicates italics 
           (font-weight is used for bold)
           but this implements what both JUCE and most graphic tools mean by style
        */
        fontStyle=value.toString();

        if (fontFamily.isNotEmpty()) {
          //if the typefaceName is already set, check if it is a valid style

          StringArray systemStyles=juce::Font::findAllTypefaceStyles(fontFamily);
          if (systemStyles.contains(fontStyle,true)){
            //get the lower and upper cases right
            fontStyle=systemStyles[systemStyles.indexOf(fontStyle,true)];
          }
          else {
            logError("Label '"+getName()+"' with font-family "+fontFamily+" has not the following style available: "+fontStyle); 
            fontStyle="";
          }
        }
        updateFont();
      },
      [this](){
        return var(fontStyle);
      })
    },
    {StyleSyntax[StyleIds::textColourId], PropertyMethods(
      [this](const var &value){
        fontColor=parseColour(value.toString());
      },
      [this](){
        return var("#"+String::toHexString((int)fontColor.getARGB()));
      })
    },
    {StyleSyntax[StyleIds::horizontalAlignId], PropertyMethods(
      [this](const var &value){
        String str=value.toString().toLowerCase();
        if (str == "center") {
          justification=Justification::centredTop;
        } else if (str == "left") {
          justification=Justification::topLeft;
        } else if (str == "right") {
          justification=Justification::topRight;
        } else if (str == "justify") {
          //A label only displays 1 line of text, so it can't really justify anything
          justification=Justification::bottomLeft;
        }
      },
      [this](){
        if (justification==Justification::centredTop) {
          return var("center");
        } else if (justification==Justification::topLeft) {
          return var("left");
        } else if (justification==Justification::topRight) {
          return var("right");
        }
        return var("center");
      })
    }
  };

  void updateFont() {
    if (fontFamily.isNotEmpty()) font.setTypefaceName(fontFamily);
    if (fontStyle.isNotEmpty()) font.setTypefaceStyle(fontStyle);

    if (fontSizeIsInPt)
      font=font.withPointHeight(fontSize);
    else
      font=font.withHeight(fontSize);      
    setFont(font);
  }

  ...
  
  StyledLabel (const String& name, const String& labelText)
    : Label(name,labelText), Styled (style){
  }

private:
    Font font;
    Colour fontColor=Colours::black;    
    String fontFamily="";
    String fontStyle="";
    float fontSize=12;
    bool fontSizeIsInPt=true;
    Justification justification;
  
}

Examples:

StyledLabel label;

// backwards compatibility
label->setProperty(textColourId,"#FFFFFF");

/* using Strings to refer to the properties 
   with property names that can be set to match the syntax in your style files
  (for faster parsing) */
label->setProperty("color","#FFFFFF"); 

// or if you prefer to set multiple properties at once
label->setStyle("font-family:Arial;color:#FFFFFF"); 

EDIT: std::map could be replaced by std::unordered_map which is a hash table that should give you O(1) access with a good hash function, but since the list of properties is rather short it is probably not worth it.