Skip to content
This repository has been archived by the owner on Jun 26, 2021. It is now read-only.

Element styling / CSS / overriding default styles #22

Open
fschutt opened this issue Oct 15, 2017 · 24 comments
Open

Element styling / CSS / overriding default styles #22

fschutt opened this issue Oct 15, 2017 · 24 comments

Comments

@fschutt
Copy link
Contributor

fschutt commented Oct 15, 2017

Issue for discussing a possible set of CSS styling rules in order to override the default styling on limn widgets + possible implementation design.

I want to style my application to look like MS Office. Some other guy maybe wants to style his icons to look more like MacOS. A third guy doesn't care about styling and just wants to have a basic GUI.

Since I personally think that limn will win the fight for desktop GUIs in the Rust world, it's necessary to talk about styling, since it'll appear at some point.

What the design should allow (my opinion):

  • Padding, colors, gradients, border-radius, border-color and drop-shadow should be stylable / overridable from the default for all widgets, as well as hover and clicked effect modifiers. I think these are the minimum things that need to be stylable. Currently this can already be done, but you have to copy the whole src/widgets/button.rs file and basically make your own button.
  • There should be some way of turning all styling off in limn (to start styling the application from scratch)
  • Usually, you'd style everything consistently, i.e. buttons have a certain default size and padding, headlines and text have a consistent style.
  • Some sort of conditional styling (maybe) to have a fluid layout to hide certain elements if the application window gets smaller?

Now for questions:

  • Should the API be "stringified", like in CSS? This makes for easier changes / faster iteration because you don't have to recompile the binary every time you want to tweak a color. On the other hand, it decreases reliablity (things may go wrong) and decreases type safety.
  • Should the values be compiled in the binary or be overridden when the application starts up?
  • Should there be an ID-and-class system, like in CSS? Should styles be applied globally or rather local to the current component or both, with different weights?
  • Is there a way to use cassowary-rs to solve for the different properties (which one takes precedence)?
  • Should there be hard-coded classes, like in Bootstrap (primary, secondary, success, danger, warning, info, light, dark)?
@christolliday
Copy link
Owner

So, originally I was going to do some refactoring of widget state in general, because there are some things about it that can be made more general, and some of which interact with styling, specifically "properties", but actually I think it might be better to just deal with styling now and try to refactor that later.

A lot of similar mechanisms used to style draw state right now can be expanded to apply to widgets, which along with some mechanism for selecting widgets to style, gets us most of the features you're describing. Also I do think it will be closer to CSS than what I might have suggested before.

Some sort of conditional styling (maybe) to have a fluid layout to hide certain elements if the application window gets smaller?

Hmm, maybe after the refactoring of properties I'm thinking about, you could define your own property for "is above a certain size" and provide a selector on that property. Until then, the simpler solution which is less flexible is just to add a flag to your widget that can be styled that will hide something specific.

There should be some way of turning all styling off in limn (to start styling the application from scratch)

Yes if you don't specify any style sheet, no styling means using the defaults for everything, which will be supplied by Default impls of style types.

Should the API be "stringified", like in CSS? This makes for easier changes / faster iteration because you don't have to recompile the binary every time you want to tweak a color. On the other hand, it decreases reliablity (things may go wrong) and decreases type safety.

Eventually you should be able to load styles from a file, but this will be after everything is made possible in Rust first. It should validate the whole style sheet against types you have registered in your app when the style sheet file is loaded, so it should be as reliable as inline style declarations as long as you make sure you validate before deploying.

Should the values be compiled in the binary or be overridden when the application starts up?

Not totally sure what you mean, but the style sheet, whether it's inline, or from a file, will have to be initialized when the application starts up.

Should there be an ID-and-class system, like in CSS? Should styles be applied globally or rather local to the current component or both, with different weights?

Yes, there should be some ID and class like system for selecting widgets, both globally and locally.

Is there a way to use cassowary-rs to solve for the different properties (which one takes precedence)?

Not sure I understand. A different thing related to cassowary I should mention that it's not possible right now to declare relative constraints between widgets without the widgets actually existing, which I think will be needed to style layouts the way they need to be. Because of this, I'm going to make everything styleable except for layout, then make the needed changes to limn-layout, then make that part stylable after.

Should there be hard-coded classes, like in Bootstrap (primary, secondary, success, danger, warning, info, light, dark)?

There should be pre-defined classes, but they shouldn't be generic to any widget I don't think. So for instance you would have danger_button, which you can apply to a button, but no generic danger class. Not 100% sure how this works in Bootstrap.

Just a heads up, I'm starting to work on this now.

@christolliday
Copy link
Owner

So the basic plan is this, create style definitions for widgets, eg. ButtonStyle, similar to RectStyle and TextStyle. So that a buttons style can be defined by a Vec<ButtonStyle>
For now, the style will be applied when into() -> WidgetBuilder is called.

Styles will be selected based on this priority:

  1. Styles directly applied, so button.style(vec![])
  2. Styles matching a style id, so button.style_id("warning_button") will look up "warning_button" in the global style sheet.
  3. Styles applied to that widget style type, so button will internally set button.style_class(TypeId::of::<ButtonStyle>()), and it will look up the setting for ButtonStyle in the global style sheet.
  4. If there is no match and no definition in the global style sheet, fall back to values defined by impl Default for ButtonStyle
  5. Sub widgets with no style set by the parent will follow the same process

@fschutt
Copy link
Contributor Author

fschutt commented Oct 26, 2017

Yes, this is what I thought to use cassowary for: Assign different weights to the 1 - 5 priorities, then let cassowary solve for the best one. Just an idea, though.

I don't have that much time to work on limn, mostly only weekends. I'll try to finish #29 in order to get the customization going.

@christolliday
Copy link
Owner

Hmm yeah I'm not sure if cassowary can be very useful for weighting priorities, I don't think it makes sense to put styles inside the cassowary solver for example.
With the priorities described here, it should be very straightforward to just use the more specific style, with no weighting, or complex rules for selecting one over the other, however these rules might not ultimately be the only ones. It may be necessary to have multiple styles at level 2 and 3 for example, in which case there will need to be some system for resolution, but my hope is it would be something more intuitive than numbered weights. In general I think it's better to tell the user their style is ambiguous and have them resolve it, than re-introduce all the complexity of CSS and having some reasonable solution to any input.

Btw, not sure if you are still working on #29 but if so I'd say focus on the font loading part and on my comments on there, let me know if you are doing any work that's style related. The work I've done on this will probably overwrite any of your changes to button for example.

What I'm working on actually replaces the custom WidgetBuilder pattern entirely, now instead of creating an object that converts into a widget builder, you create an object that is applied to and modifies a WidgetBuilder, sort of emulating multiple inheritance. These are the objects that will expose attributes that can be styled, and they can be nested. Still haven't settled on the naming scheme, but right now they are called "Components".

@fschutt
Copy link
Contributor Author

fschutt commented Oct 31, 2017

Well, I'd wait for your stuff then and try to merge #29 with your components. I currently have a lot of stuff to do in the coming weeks, so I won't have the time to work on limn much.

@christolliday
Copy link
Owner

I've created a PR that lays the foundation for most of the items you have listed here, this is copied from the new style module documentation:

Styleable components are arbitrary structs that can inherit values from a theme by defining a style struct that can be converted into the original struct by implementing the trait ComponentStyle.

Currently this can be used for structs that implement Draw and WidgetModifier and so can be used to style both drawable types, and complex widgets.

The macro component_style! can be used to simplify defining styleable types:

component_style!{pub struct Button<name="button", style=ButtonStyle> {
    rect: RectStyle = RectStyle::default(),
    text: Option<TextStyle> = None,
}}

This declares two structs:

struct Button {
    rect: RectStyle,
    text: Option<TextStyle>,
}
struct ButtonStyle {
    rect: Option<RectStyle>,
    text: Option<Option<TextStyle>>,
}

and implements various traits on them, but most importantly:

impl ComponentStyle<Component = Button> for ButtonStyle

If you then implement WidgetModifier for Button, you can define how to initialize a button using the rect and text fields.
Then by passing a ButtonStyle in place of a Button to initialize a widget, you can specify only the fields you want to be specific to that widget, with the remaining fields inherited from the theme.

The theme will prioritize which values to use by (currently) simple specificity rules:

-The specific style passed to a widget
-Styles registered in the theme for named style classes that can be applied to widgets, eg. "alert_button"
-The base style for the type, ie. ButtonStyle, registered in the theme
-In values are found no where else, the default values specified in component_style!, in this example, RectStyle::default() and None

What's missing is still:

  • The ability to allow individual fields like padding to be reused across component styles
  • Many of the actual fields you mention, which need to be added to the relevant components, eg. padding, gradient, drop-shadow etc.
  • More base component types, Draw and WidgetModifier don't encapsulate all the things you will want to style.
  • The actual declaring of styles, see default_styles() in examples/util/mod.rs, could be made much cleaner, by cleaning up the API, or using macros, or even runtime loadable style sheets.

I think the foundation is there though so I'm going to leave it as is to focus on other things for the time being, @fschutt let me know if you have any feedback on the current design.

@fschutt
Copy link
Contributor Author

fschutt commented Nov 26, 2017

I guess its OK, but the font is still hard-coded to /assets/fonts/[name].ttf. For me right now it's not usable because of this. You could add an extra field to the TextStyle that would allow to simply have an embedded fallback font (so that the thing in the font field is only used when that font is not present or fails to load).

@fschutt
Copy link
Contributor Author

fschutt commented Nov 26, 2017

I also wanted to say: If you compile the examples in release mode, you get a segmentation fault when trying to run them. I don't know where this is coming from, I'll try to find the source.

UPDATE: The segfault seems to be a problem of nightly 25-11

@christolliday
Copy link
Owner

@fschutt sure, this is not intended to solve font loading, there is no change to how fonts are named or loaded. I don't think there should be any overlap in the logic of font inheritance, handled by the style module, and font fallback, handled by the font loader.
In terms of specifying fallback fonts in TextStyle I think it would be preferable for widgets to only specify font-family, font-variant etc. then you can register fallback fonts in the font loader, and optionally associate them with certain font selectors, if you need to register more than one fallback font.

@christolliday
Copy link
Owner

Thanks for the heads up on the seg fault, just updated nightly, seems to be fixed in 26-11

@jaroslaw-weber
Copy link

component_style!{pub struct Button<name="button", style=ButtonStyle> {
    rect: RectStyle = RectStyle::default(),
    text: Option<TextStyle> = None,
}}

i think you could make the macro cleaner:

component_style!{
  struct_name: Button,
  name: "button",
  style: ButtonStyle
  {
    rect: RectStyle = RectStyle::default(),
    text: Option<TextStyle> = None,
  }
}

or split it to

style!{
 name: ButtonStyle,
 rect: RectStyle = RectStyle::default(),
 text: Option<TextStyle> = None,
};
component!{
 struct_name: Button,
 name: "button",
 style: ButtonStyle
};

not sure if its possible but I think current macro looks little messy.

@christolliday
Copy link
Owner

christolliday commented Nov 30, 2017

@jaroslaw-weber hmm yeah it is a little busy, unfortunately I don't think it can be split without requiring everything to be duplicated in style and component.

I agree there is a bit of rightward drift and it's a little funny how it specifies name where generics would go, but I still think the first is better, if name could be moved out and have Button<ButtonStyle> it might be better but I can't think of a better place to put the name. It might be possible to remove it later though, then you could just have:

component_style!{pub struct Button<ButtonStyle> {
    rect: RectStyle = RectStyle::default(),
    text: Option<TextStyle> = None,
}}

or:

component_style!{
    pub struct Button<ButtonStyle> {
        rect: RectStyle = RectStyle::default(),
        text: Option<TextStyle> = None,
    }
}

@jaroslaw-weber
Copy link

@christolliday
well I am not familiar with the code yet so not sure what is going on inside a macro, but I think current solution needs a little refactoring. I wouldnt put it inside the <> brackets, looks too html-ish to me 👯

@jaroslaw-weber
Copy link

loadin styling from file is nice, but what about performance overhead? with some code generation we could try to make styling embedded, we could load styles faster. for hot reload we could use overriding with serialized stylesheet

about generic styling, like bootstap, i would provide it as outside library. make core library dirty and simple.

@christolliday
Copy link
Owner

Hmm the idea was to have it resemble an associated type hence the <>, it's actually a trait on ButtonStyle that has Button as an associated type, but it makes more sense to declare pub struct Button than the reverse for various reasons. I'm surprised it reminds you of html when Rust is so full of <>! anyway I'm not gonna spend more time on it right now since it could always change down the line and there are much worse APIs in limn 😉

I'm not too worried about the overhead of loading styles from a file, dealing with the overhead of actually applying (and reapplying) the styles is going to take more attention. Maybe eventually embedding styles could be useful but probably not for a while.

As for providing styles in an outside library, yep, right now styles are actually declared in the example code. Eventually it will make sense to chop up limn into sub crates too but I don't think it's needed yet.

@fschutt
Copy link
Contributor Author

fschutt commented Nov 30, 2017

@christolliday I tried to use limn again to style something. So far I've encountered numbers of bugs:

  • Borders don't show if the corner_radius set to None. The corner radius should not affect the visibility of the border.
  • Borders don't work in maximized mode (already reported in Rendering error on X11 - webrender doesn't render bottom of rectangle correctly if window is maximized #40)
  • Available constraints are not documented (i.e. what constraints are available), all I have are the examples
  • Padding can only set for all sides, not padding_left, padding_right, padding_top, padding_bottom
  • An application window somehow gets initialized to the min_dimensions, not to the dimensions. So if I set both .min_dimensions(200, 100) and .dimensions(600, 800), the window will open with the (200, 100) width and height
  • No way to limit the height or width of buttons / rectangles. For example, I want buttons to be exactly 20px high. Not possible. The only thing I can adjust is the font size and padding, that's it.
  • register_class_prop_style("button_rect", ACTIVATED.clone(), MyRectStyle) flat out doesn't work. I passed in a blue color to the MyRectStyle, limn still uses the default gray.
  • padding(5.0) on a ButtonStyle does absolutely nothing! It doesn't actually affect the padding in any way, it's always hard-coded / stuck at 10px padding.
  • Fonts are incorrectly sized. I set the font to 10px, I get a font that's 6px large. Not sure why.

Not sure if I should make seperate issue for these otherwise I'd just be spamming issues. I don't want to sound too negative, but the absolute most I can do right now is to change the border color (for some reason, this only works for the base state, not ACTIVATED or INACTIVE or other states) and the font size only somewhat. I'll try to fix some of these issues in PRs, but currently I can only report that the styling in limn is mediocre.

fschutt added a commit to fschutt/wexplorer that referenced this issue Nov 30, 2017
@wendivoid
Copy link
Contributor

I'm dont know much about the styling code but the constraints are documented here, I agree it's hard to find. As for padding it can be set individually per side like so...

widget.layout().add(constraints![
    bound_left(&parent_widget).padding(10.0),
    bound_right(&parent_widget).padding(5.0),
    bound_bottom(&parent_widget).padding(20.0),
    bound_top(&parent_widget).padding(30.0),
]);

I haven't had much time to play with limn since #34, but i was planning on playing around with the new style code later on this week.

@christolliday
Copy link
Owner

@fschutt thanks for the report, to be honest I would say calling it mediocre at this stage is generous. On top of the missing features, there are going to be a few bugs and I think there might be some undocumented assumptions about the order some styling methods need to be run, thanks for reminding me of that in particular I'll try and look into it soon. Actually there are some other things that aren't documented about how these APIs need to be used I'm just remembering now, I'll write those up too, may or may not be related to your issues. I'll look into the rest of your other issues as well but my focus is going to be on working out the fundamental APIs and less on fixing bugs that aren't architectural, so PRs would be appreciated! Issues as well.
Frankly if you are trying to build something reliable or polished with limn at this stage it will be necessary to fix many bugs yourself and be familiar with the inner workings of limn.

The other thing is, the styling work done so far is purely to create a foundation for declaring and applying styles, and is only used for the low hanging fruit so far, there also needs to be work done in creating the appropriate styleable components and using them in more places. (for example, a component that specifies the layout of a child relative to its parent could be made and applied to a child). The work here should simplify declaring layouts in general, so that you should only rarely need the API that specifies relationships between any arbitrary set of widgets (currently the only API).

@fschutt
Copy link
Contributor Author

fschutt commented Feb 1, 2018

@christolliday Just wanted to tell you: I've used your constraint code here. I am working on that library now, which uses CSS instead of constraints (mostly because it's more flexible). I'll credit you as an author, I hope that's OK.

I can't really use limn-layout because it's heavily tied into the model of limn, so I copied the code.

@christolliday
Copy link
Owner

@fschutt of course that's ok and thanks for the heads up, disappointed that you decided against contributing to limn, but more experimentation in this area is always good! I do have some opinions on the scalability of your approach though if you are interested.

Ultimately limn will likely support CSS but I can't blame you for thinking otherwise because I haven't made it at all clear what the path is between where it is and where it is planned to be is, I'm more focused on implementing specific features, and supporting CSS files directly is not yet my first priority.

As for limn-layout, you might think the model of limn-layout is heavily tied into the model of limn but it's as much if not more so based on the model of cassowary, which influences limn. Not all of limn-layout, but the bulk of it.

I had a quick look at how you are using cassowary, I could be wrong but it seems to me you are recreating the entire layout for every change without caching any of it? If that is the case I think you will probably run into major performance issues, and if you try to optimize it and still use cassowary you will end up recreating something similar to limn-layout.

@fschutt
Copy link
Contributor Author

fschutt commented Feb 2, 2018

@christolliday I've thought about performance, but currently it's not too bad. Currently, if I have 500 dom elements I have a frame time of roughly 2 - 4ms + 1ms for rendering and my target is 16ms (60 fps), so I still have a lot of headroom to spare even if cassowary should get expensive. Of course, I don't redraw every frame, only if it's necessary, so the CPU usage is rather low. Yes, you can probably do it faster and there are lots of things that I have missed in terms of performance optimization, but I can fix those later. There are lots of "shortcuts" and caching strategies available, at least for the CSS. For example, if only the background color of something changes (on a hover event, which is very common), you don't have to re-layout the whole thing.

The idea was to rebuild the entire DOM on every redraw so you don't have a button.setText() method which you have to synchronize between your app state and the UI, you only have your app model that "serializes" itself into a UI at 60fps (i.e. relayout on every frame), so the UI is always in sync with your data model. And yeah, this isn't the most optimal way to do it, but it is very convenient for the guy writing the GUI (conditional elements, complex application logic, etc.) and makes the UI "testable" (no side-effects).

Basically, the only time a re-layout is necessary is:

  • The DOM changes (elements are different from the last frame) - currently I can't cache this at all, but could be done with "immutable DOM sub-trees" or similar. Or via "DOM IDs" / hashing and comparison. Since the DOM is only recreated if an element is hit (or an animation is running), I can also use that for caching.
  • The CSS changes (I currently do caching via CssRule - you can only add and remove rules via built-in methods, which sets a dirty flag on the Css). Plus, the change has to be significant to force a re-layout (i.e. not just a color change)

So yes, I don't currently cache much (except for the CSS), but I don't think I'll have extreme performance problems, even though my model isn't that CPU-friendly. And yes, that doesn't fit very well with cassowary. I exploit a bit the fact that Rust is already pretty fast. Right now it's in the early stages of development, though, so I can't really predict if the performance will be sufficient.

As for limn, it's a cool project, but I just didn't see any updates for months and I thought you had given up. My goal with CSS is basically to force the application programmer to embed a CSS file in the application (via include_str!) or use the "platform-native" CSS, because I noticed that writing CSS is just a lot easier and more flexible than managing styles / layout in the limn way. So the application has a slightly slower startup, but now you can use a web-browser for rapid prototyping, before you convert it into a desktop app and you can do hot-reloading of styles from files, etc. My end goal is to do an "html-to-rust" compiler where you can use regular HTML and CSS to layout the app, but then use the compiler to generate the necessary Rust code so you get the best of both worlds.

If you want to use CSS, you can take a look at the css-parser file, it has lots of utility functions that you might want.

@christolliday
Copy link
Owner

@fschutt to be clear the benefits of having your UI state be a function of application state are undeniable and is what I'm aiming for as well but in a different way. It seems like you are creating a better Conrod, which is what I am trying to avoid with limn.

I think the only reason this approach works efficiently for javascript frameworks is because they are not constructing the UI for every change, they are only constructing a representation (virtual DOM) that can be diffed against a representation of the existing state.

My plan with Limn is to first create a mutable tree (DOM) and it's representation as a foundation for a diffable representation (virtual DOM). Both so that the mutable tree API can be used on a per component basis to avoid the cost of diffing, but also to avoid overreach, no point in a virtual DOM if the DOM is not reliable.

You might want to skip the part of having a mutable API to fallback on, but unless you use something analogous to a virtual DOM, I think the upper bound on performance will be below that of a web app, which seems to me at least to defeat the purpose of using Rust for a GUI.

It's good that you only update the UI in response to model changes, but optimizing for the case where the model changes once per frame is important too. When you say ~3-5ms per render, does that include adding and calculating constraints for 500+ elements?

@fschutt
Copy link
Contributor Author

fschutt commented Feb 3, 2018

@christolliday good catch. I've tested this with 500 constraints, if I add and remove all constraints every frame, it needs roughly 8ms (for 500 constraints), otherwise only 0.3 ms (for calculating the layout). Now, I can currently use the slow way because even then I'll stay below 16ms. But eventually, I'll need to do it correctly and cache the components.

I've already implemented the css caching (i.e. it only re-layouts now if you push a css rule and only if that css rule actually affects the layout). Now to cache the DOM I'll use hashing. Each DOM node can currently be hashed and then get compared to the hash of the previous state (or be directly compared, without hashing). This way I am still trading in a bit of performance, but it hashing / calculating the checksum of 500 nodes should be faster than removing / adding constraints. Thanks for the heads up.

@christolliday
Copy link
Owner

@fschutt ok yeah, you can probably get good enough performance through some caching scheme like that, just thought I'd say something having thought about this a lot from looking at Conrod, thinking it's caching scheme is pretty complex and deciding on the mutable tree/virtual DOM approach. But, it's not going to happen overnight for limn and you'll probably get what you need faster the way you are going especially if performance is not critical.

Also it might be more trouble than it's worth but if you do cache the layout you might consider using limn-layout and LimnSolver as a layout cache, maintaining a map of DOM node hashes to LayoutIds (analagous to WidgetId in limn), or replacing LayoutIds with hashes, then you can replace constraints on a per node basis. It's not well documented and it might be a poor match for your CSS based system but if you are thinking about it let me know and I'll clean it up/document it better.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants