Just an idea: should there be a DSL for adding CSS? Either to a node or to the View.
For a node it wouldn't be particularly useful as, in the node's context, all the properties are already accessible.
But it could be nice to have some sort of CSS DSL for the view. Like Kara Framework or something. Just a thought.
That's an interesting idea. It would certainly be possible, even to include every possible property, since it's just CSS 2.1 after all. We could also create some beautiful builders for gradients etc :)
I'm unsure of wether users would prefer this over editing a normal CSS file though. After all, IDE's have pretty spectacular CSS support these days.
If you want to play with this, maybe Kara is a good place to look for inspiration?
I'll look into it more after we finish all the new builders we have in queue.
I agree that CSS files are a much better option for most applications. This would probably only really be used for some quick styling in the new SingleViewApp class. I'm just not sure if that is a use case that would merit such a feature.
I agree it would be very cool :) Other, more important things first though :)
You should mark this as an enhancement, and we can get to it later.
Here is the kara CSS DSL for future reference.
Hmmz then why not add SASS?
Do you mean make the DSL look more like SASS or create a SASS parser?
Make it look like SASS
@ronsmits I remember something about that from the Kara Framework. Since the DSL is written in Kotlin, we can do even more powerful things than SCSS out of the box, so there is no need to mimic a potentially inferior syntax :) That might sound cocky, but just think of the possibilities when you can call actual Kotlin functions within your stylesheet. Now that's power :)
That might actually be the one solid argument for doing this - a lot more expressive syntax for JavaFX CSS. This might be a killer feature if done right!
I agree, go for it
Does anyone have any experience with the Kara framework? I do not, but it would be nice to hear if there is anything about the Kara CSS DSL that should be changed/added/removed.
For example, the overridden render function seems strange to me. Personally I think it would suit TornadoFX better to have the function in the constructor. So instead of (their example):
class DefaultStyles() : Stylesheet() {
override fun render() {
s("body") {
backgroundColor = c("#f0f0f0")
}
s("#main") {
width = 85.percent
backgroundColor = c("#fff")
margin = box(0.px, auto)
padding = box(1.em)
border = "1px solid #ccc"
borderRadius = 5.px
}
s("input[type=text], textarea") {
padding = box(4.px)
width = 300.px
}
s("textarea") {
height = 80.px
}
s("table.fields") {
s("td") {
padding = box(6.px, 3.px)
}
s("td.label") {
textAlign = TextAlign.right
}
s("td.label.top") {
verticalAlign = VerticalAlign.top
}
}
}
}
it could just be
val styles = Stylesheet().apply {
s("body") {
backgroundColor = c("#f0f0f0")
}
s("#main") {
width = 85.percent
backgroundColor = c("#fff")
margin = box(0.px, auto)
padding = box(1.em)
border = "1px solid #ccc"
borderRadius = 5.px
}
s("input[type=text], textarea") {
padding = box(4.px)
width = 300.px
}
s("textarea") {
height = 80.px
}
s("table.fields") {
s("td") {
padding = box(6.px, 3.px)
}
s("td.label") {
textAlign = TextAlign.right
}
s("td.label.top") {
verticalAlign = VerticalAlign.top
}
}
}
and in a View have a css method
init {
css {
s("body") {
backgroundColor = c("#f0f0f0")
}
s("#main") {
width = 85.percent
backgroundColor = c("#fff")
margin = box(0.px, auto)
padding = box(1.em)
border = "1px solid #ccc"
borderRadius = 5.px
}
s("input[type=text], textarea") {
padding = box(4.px)
width = 300.px
}
s("textarea") {
height = 80.px
}
s("table.fields") {
s("td") {
padding = box(6.px, 3.px)
}
s("td.label") {
textAlign = TextAlign.right
}
s("td.label.top") {
verticalAlign = VerticalAlign.top
}
}
}
// TODO: Other stuff
}
I've been messing around trying to get a very small proof of concept, but I've run into a problem. It seems JavaFX does not allow you to add raw CSS as a stylesheet, but requires URLs. There is a way around it, but as far as I can tell, it isn't very flexible (e.g. you can only ever add a single stylesheet, and it has to be a string).
Ah, I remember that, I've seen a couple of different workarounds. I'll mess with this a little myself and report back. I also have a couple of ideas about what we can do to make this even more valuable, but I need to think about it a little more before I write something down :)
Do you want me to send you what I have (just a shell for a CSS DSL), or would you prefer a clean approach?
Nah, hang on - I don't really need anything other than a string to play with this for now :)
I think I found a good way to implement a custom URL handler for our stylesheets, without using any hacks, just plain Java APIs :) I'll need a couple of minutes, will post back soon.
OK, so I added a very minimal Stylesheet class that has the render method. We can change this as per your example above, I just did the minimal amount of work to test this now :)
abstract class Stylesheet {
abstract fun render(): String
}
Create a subclass of stylesheet and return something from render():
class MyTestStylesheet : Stylesheet() {
// No builder support, faking it for now :)
override fun render() = ".root { -fx-background: blue }"
}
Now you can import this stylesheet and even make it reload every time your app gains focus:
class CSSTest : SingleViewApp("CSS Test") {
override val root = HBox(Label("Hello CSS"))
override fun start(stage: Stage) {
importStylesheet(MyTestStylesheet::class)
stage.reloadStylesheetsOnFocus()
super.start(stage)
}
}
Now try changing the background to red, recompile and switch back to your app.
Will this do? :)
The handler is very simple, and it is automatically registered for the new and shiny css:// protocol :)
Give it an url to a fully qualified class name that implements the Stylesheet class, and you're good to go:
val css = URL("css://com.mycompany.css.MainStylesheet")
This can be added to a stage manually as well:
scene.stylesheets.add(css.toExternalForm())
The implementation is basically this:
override fun getInputStream(): InputStream {
val stylesheet = Class.forName(url.host).newInstance() as Stylesheet
return stylesheet.render().byteInputStream(StandardCharsets.UTF_8)
}
I've commited it now so you can play with it :)
Here is a small screencast of the concept in action:
That's pretty slick :)
I don't think it can be integrated with a DSL very well though, as you wouldn't have a fully qualified class path in that case. Or am I missing something?
That's no problem at all really :) Can you show me some of your code and I'll make it work :)
Nevermind :) I think I figured it out.
Nice. If you run into problems getting a class out of it, we could always just render the stylesheet to a basic64 encoded string and included it in the url itself. It would be trivial to change the urlhandler to handle that. Let me know, and I'll make the change :) Can you work within the CSS.kt that I added?
Would be cool if we could include preliminary CSS support in the next release!
When you do:
init {
css {
...
}
}
in a View... where do you want this stylesheet applied?
I've got a _very_ basic prototype working. I'll see if I can flesh it out a bit more by the end of the day.
Current issues:
s(".test-class") { prop("font-size", 18.px) })s(".test-class") { fontSize = 18.px }s("a") { s("b") { s("c") {} } } returns a {} a b {} b c {} instead of a {} a b {} a b c {}As far as where to apply the stylesheet, I'm not sure. For now I'm just doing it in the overridden start(Stage) method.
I've now got the s(".test-class") { fontSize = 18.px } to work properly (thanks to this answer.) I just have to go add all the possible properties.
Demo so far:
class CssTest : SingleViewApp("CSS Test") {
override val root = VBox()
init {
with(root) {
prefWidth = 400.0
prefHeight = 400.0
label("test") {
styleClass += "fred"
}
hbox {
styleClass += "box"
padding = Insets(5.0)
label("test 2")
}
piechart("Imported Fruits") {
data("Grapefruit", 12.0)
data("Oranges", 25.0)
data("Plums", 10.0)
data("Pears", 22.0)
data("Apples", 30.0)
}
}
}
override fun start(stage: Stage) {
class MyTestStylesheet : Stylesheet() {
init {
s(".label") {
fontSize = 18.px
textFill = "orange"
}
s(".label.fred") {
textFill = "blue"
}
s(".box") {
backgroundColor = "green"
s(".label") {
backgroundColor = "red"
textFill = "white"
}
}
}
}
importStylesheet(MyTestStylesheet::class)
println(MyTestStylesheet())
super.start(stage)
}
}

Generated CSS:
.label {
-fx-font-size: 18px;
-fx-text-fill: orange;
}
.label.fred {
-fx-text-fill: blue;
}
.box {
-fx-background-color: green;
}
.box .label {
-fx-background-color: red;
-fx-text-fill: white;
}
Fantastic work!! I have some ideas, will post when I get to a computer!
Any ideas are appreciated.
Demo with Mixin (dumb but gets the point across):
class CssTest : SingleViewApp("CSS Test") {
override val root = VBox()
init {
with(root) {
prefWidth = 400.0
prefHeight = 400.0
label("test") {
styleClass += "fred"
}
hbox {
styleClass += "box"
label("test 2")
}
piechart("Imported Fruits") {
data("Grapefruit", 12.0)
data("Oranges", 25.0)
data("Plums", 10.0)
data("Pears", 22.0)
data("Apples", 30.0)
}
}
}
override fun start(stage: Stage) {
class MyTestStylesheet : Stylesheet() {
init {
val pad = mixin {
prop("padding", 5.px)
s(".label") {
backgroundColor = "red"
textFill = "white"
}
}
s(".label") {
+pad
fontSize = 18.px
textFill = "orange"
}
s(".label.fred") {
textFill = "blue"
}
s(".box") {
+pad
backgroundColor = "green"
}
}
}
importStylesheet(MyTestStylesheet::class)
println(MyTestStylesheet())
super.start(stage)
}
}

.label {
-fx-padding: 5px;
-fx-font-size: 18px;
-fx-text-fill: orange;
}
.label .label {
-fx-background-color: red;
-fx-text-fill: white;
}
.label.fred {
-fx-text-fill: blue;
}
.box {
-fx-padding: 5px;
-fx-background-color: green;
}
.box .label {
-fx-background-color: red;
-fx-text-fill: white;
}
Arbitrarily nested selectors now work
The mixin feature is fantastic. OK, here goes my idea. Look at the Java FX 8 CSS reference for the Labeled class:
https://docs.oracle.com/javase/8/javafx/api/javafx/scene/doc-files/cssref.html#labeled
It basically tells us every possible property that can be specified for .label. It would be fantastic if it was possible to "tell" a style declaration that it will only be applied to a Labeled, so that it would only accept the style properties defined for a labeled:
s("#heading", Labeled::class) {
underline = true // This is OK
orientation = Orientation.HORIZONTAL // Compile error, orientation is not defined for Labeled
}
This might be a pipe dream, but I thought it was worth mentioning it. When you have defined all properties in the Labeled, defining the others are much simpler. For example, ButtonBase would extend Labeled and only add the armed pseudo class.
When you don't specify a class, the default would have to be a class that includes all the subclasses, so that these two declarations would mean the same:
s("#heading") {
// Anything goes
}
s("#heading", AllClasses::class) {
// Anything goes here too :)
}
Again, I suspect this is not doable, and might not even be worth it, but I wanted to throw it out there if you want to play with it or at least thing about the implications that would have on the model :)
The color properties could also accept javafx.scene.paint.Color instances, to increase type safety, don't you think? All these three could be possible:
s("#heading") {
backgroundColor = Color.BLACK
backgroundColor = Color(0.0, 0.0, 0.0)
backgroundColor = "black"
backgroundColor = "#000"
}
One potential downside with the limiting (assuming it's possible) could be scope confusion. For example, if the s("#heading", Labeled::class) { ... } was inside something that did have an orientation property, your above example would compile fine, but apply to the outside selection instead of the #heading.
I'll have to look into supporting colors with better type safety. Currently, properties are stored in a MutableMap<String, Any>. I'm not sure how to add good type safety with that.
An example property: var fontSize: Any by map(properties, "-fx-font-size")
I think the scoping issue is possible to avoid, I think Kara solved that with the way they created the class hierarchy (I might be mistaken, but I think I read that somewhere). The reason we have that problem in TornadoFX is that we use extension functions. This can be avoided for a "evergreen" DSL like this one.
Could I have a look at what you have so far? Easier to give informed advice that way :)
Sure thing. Here is the link to the working branch. I try to keep it up to date as I go.
It's pretty bare bones (just testing out ideas) and the names could be better :)
I haven't done CSS too much with JavaFX, but I really like where this is going. I am biased towards writing all things in code so I'll probably be a huge user of this.
I'm not married to what I've done so far, so if throwing it out and starting from scratch would give a better result, I'm all for it.
OK, I'll play with a little and see how it feels and report back :)
I'm afraid I'm just too tired to do any more today :) Will try to look more at it tomorrow!
Is the current selector syntax good:
s(".bad") {
+mixin
textFill = "red"
s(".label") {
backgroundColor = "black"
}
}
or would an infix be better:
".bad" style {
+mixin
textFill = "red"
".label" style {
backgroundColor = "black"
}
}
The current syntax is good IMHO :)
I have an idea about how to add type safety. First, change the map function to this:
inline fun <reified V> PropertyChunk.map(key: String): ReadWriteProperty<PropertyChunk, V> {
return object : ReadWriteProperty<PropertyChunk, V> {
override fun getValue(thisRef: PropertyChunk, property: KProperty<*>) = properties[key] as V
override fun setValue(thisRef: PropertyChunk, property: KProperty<*>, value: V) {
properties[key] = value as Any
}
}
}
Now the call site gets prettier, _and_ it supports types:
var fontSize: Any by map("-fx-font-size")
If you want to specify type, do:
var backgroundColor: Color by map("-fx-background-color")
Now, setting a Color from a string gets a bit more verbose, so introduce a c() function in PropertyChunk that creates a color from a string, like Kara does.
The next issue that arises from this change is that the buildString function will now do color.toString() which I suspect will not give us the result we want. To fix this, simply check the type of $value in the buildString method, and construct the right kind of string based on the type. This should be a small subset of types to check for.
Btw, I think we should rename the map function to something more specific to avoid confusion/collision. It could also be defined in the PropertyChunk class to reduce its exposure. Maybe cssprop or something
What do you think?
That sounds great. I'll try it out and see how it goes.
As far as renaming goes, the StyleChunk and PropertyChunk names are just placeholders I used because I didn't feel like thinking of better names at the time. Any suggestions for better names.
So far is seems to work fantastically!
Just a heads up that I'm rendering Colors as rgba(...) instead of #... as JavaFX Colors have opacity. I'll have to look into what I can do with other types (including enums).
Cool :) This is turning out really great :)
Obligatory demo:
class CssTest : SingleViewApp("CSS Test") {
override val root = VBox()
init {
with(root) {
prefWidth = 400.0
prefHeight = 400.0
hbox {
styleClass += "box"
label("Alice")
label("Bob")
}
}
}
override fun start(stage: Stage) {
class MyTestStylesheet : Stylesheet() {
init {
val pad = mixin {
padding = 5.px
}
s(".box") {
+pad
backgroundColor = Color.BLUE
spacing = 5.px
s(".label") {
+pad
fontSize = 18
textFill = Color.WHITE
backgroundColor = Color.GREEN
}
}
}
}
importStylesheet(MyTestStylesheet::class)
println(MyTestStylesheet())
super.start(stage)
}
}

.box {
-fx-padding: 5px;
-fx-background-color: rgba(0, 0, 255, 1.0);
-fx-spacing: 5px;
}
.box .label {
-fx-padding: 5px;
-fx-font-size: 18;
-fx-text-fill: rgba(255, 255, 255, 1.0);
-fx-background-color: rgba(0, 128, 0, 1.0);
}
If there is an error parsing the color string (with the c(String) function), what should I default to?
Options:
If you enter an invalid property value in a javafx css stylesheet, the directive is ignored and an error is logged. Therefore I think we should use the same approach.
Oh, and super nice work, I really love the way this is shaping up!
Sounds good
We should probably also have special support for padding/insets, like the box() function in Kara.
I've made dimensions type safe (like in Kara), but it has one downside. All dimensions require a unit. You can no longer use fontSize = 14, you would have to do something like fontSize = 14.pt. Is that acceptable, or should I allow Any for linear dimensions?
(Current accepted units: px, mm, cm, inches, pt, pc, em, ex, percent)
Perfect! That is totally acceptable :)
I've now got most of the properties added (with enums for the ones with limited choices). The only ones left to do are ones that I need to spend some more time figuring out how to do. Feel free to check out what I have so far. In the meantime, I'm going to bed :).
Also, anything with a fill can accept any Paint, not just Colors.
There are also linear dimensions (see above), box dimensions (basically just 4 linear dimensions (this is what the box() function uses (for padding, margin, border radius, etc))), angular dimensions (deg, rad, grad, turn), and temporal dimensions (s, ms).
Fantastic work! I really like the approach you have taken here. It seems to work really well. Please let me know if there are certain areas you need to discuss or need feedback on. I will start playing with this to see how it feels in the mean time.
By the way @t-boom - would you like commit access to the repo? You might as well integrate this stuff directly instead of going via a PR. We could start integrating the stuff and work together on it as well. I've put some stuff in CSS.kt so you would have to merge with that.
Let me know if it's OK with you, and I'll add you as a contributor on the project.
Sure, that would probably make it easier for both of us.
I've merged in your changes (and pushed it up to my repo for now)
Hereby granted :) I'm thinking we could do another release as soon as this DSL is done - it's a major feature! Feel free to delegate work to me after you have checked in what you have so far :)
Alright. I've pushed what I've got so far to the feature/css-dsl branch. I'll let you know what I need help with.
Cool! I just added a feature to SingleViewApp that lets you inline a stylesheet like this:
class CSSApp: SingleViewApp("CSS Test") {
override val root = HBox()
init {
css {
s(".box") {
backgroundColor = Color.BLUE
}
}
with (root) {
addClass("box")
}
}
}
I will try to integrate this into the feature/css-dsl branch to avoid a merge conflict later.
The change described above is now in the feature/css-dsl branch :)
I have a question about the use of extension functions. Take a look at this one for example:
fun <T> SelectionBlock.box(all: T) = CssBox(all, all, all, all)
Shouldn't this just be a member function of SelectionBlock? Same goes for some of the other extension functions - it seems like they could just as well be member functions. What do you think?
Yes, they should. They were originally, but I pulled them out when I was running some tests and forgot to put them back in. I'll fix that.
OK! I also just made Stylesheet an open class like it was in your original design. I mistakenly changed it to abstract when I was playing around. It is nice to be able to instantiate a Stylesheet and then operate on it (see SingleViewApp for an example). Sorry about that :)
I saw you used com.sun.xml.internal.fastinfoset.util.StringArray in a couple of places. This class will be unavailable in JDK9. Did you use it to be able to check the type in toCSS?
If so, say the word and I'll try to implement a workaround :)
That was another Accident :) That should have just been Array
Sweet! Let me know if you have stuff you want to delegate to me :)
For properties that accept a URI, should I just accept in a String?
Also, there are a number of properties that accept a set of values _plus_ some user defined thing. The cursor is a good example as it can accept from a predefined list or a uri to an image. What should I do with those?
String for URI sounds reasonable to me.
I'll think about the other issue for a bit. Wonder if Kara faced/solved similar issues...
I haven't looked. I figured I'd get back to it later once I get the more tedious parts out of the way, but if you want to look into it, that would be awesome.
One possibility:
cursor: EnumValue
cursorUri: String
Haven't thought it through or tested how it feels, just throwing it out there :)
OK, I will have a look tonight and report back!
That would work for the cursor, but there are others that are more complicated. -fx-background-position in Region is pretty bad.
The type should probably be javafx.scene.layout.BackgroundPosition and then we could add convenience functions later. What do you think?
That's a good idea. Many of the others can probably be solved similarly.
Yes, it seems that way, including Cursor.
cursor = Cursor.CROSSHAIR
cursor = Cursor.cursor("http://coolcursors.com/cursor1.png")
To get the cursor name you would have to do something like:
val cursorName = if (cursor is ImageCursor) {
cursor.image.javaClass.getDeclaredField("url").let {
it.isAccessible = true
it.get(cursor.image)
}
} else {
cursor.toString()
}
A bit ugly, but it works :)
We'll go with that.
Three more questions:
is Enum work for enums defined with enum class ...?java.lang.Enum::class.java.isAssignableFrom(x.javaClass)All properties have now been added, but not all render properly (I still need to finish font, effect and borderStyle). There are a few that still need validated better, and several that can probably be replaced with builtin classes / enums.
As far as the skin property goes, the application will crash if you provide a skin name that doesn't exist. Should I require a skin class? I'm not sure how the skin property works.
Great! Looking for enums that can be avoided now. Yeah, we can require a skin class, I can't see how that should be a problem. Either it's on the classpath or it's not. In a pinch, if all the stylesheet author has is a String, a class could be generated with Class.forName("skinClass").kotlin.
Btw, just found a bug in SingleViewApp that i fixed in the master branch. Basically, both the JavaFX Application Launcher and the TornadoFX di framework created an instance of the App class. I'll apply the fix in this branch as well, just in case it bites us while working on this :)
Is that why the CSS is always created twice?
Yes :) Can you verify that it is only created once if you pull my latest change?
Just tried it. It seems to working great.
Phew :)
There are several enums that can probably be replaced. Two examples:
FXContentDisplay -> ContentDisplay
FXTextOverrun -> OverrunStyle
It seems the enums can be used by converting the name to lowercase and switching underscore with dash.
If I understand this correctly, all we would have to do is check if the value is an enum in toCss and do tolowercase and replace _ with -.
Should I go ahead and try that for a couple of the enums?
That was my thought too (thus my question above about checking is Enum). Go for it.
Another (ugly) progress demo:
class CssTest : SingleViewApp("CSS Test") {
override val root = VBox()
init {
css {
val pad = mixin {
padding = box(1.em)
borderColor = box(LinearGradient(0.0, 0.0, 10.0, 10.0, false, CycleMethod.REFLECT, Stop(0.0, Color.RED), Stop(1.0, c(0.0, 1.0, 0.0))))
borderWidth = box(5.px)
}
s(".box") {
+pad
backgroundColor = RadialGradient(90.0, 0.5, 0.5, 0.5, 0.25, true, CycleMethod.REFLECT, Stop(0.0, Color.WHITE), Stop(1.0, Color.BLACK))
spacing = 5.px
s(".label") {
+pad
fontSize = 14.pt
textFill = c("white")
backgroundColor = c("blue", 0.75)
rotate = .95.turn
translateX = .5.inches
minHeight = 6.em
scaleX = 2
scaleY = .75
backgroundRadius = box(25.px)
borderRadius = box(25.px)
}
}
}
with(root) {
prefWidth = 400.0
prefHeight = 400.0
hbox {
styleClass += "box"
label("Alice")
label("Bob")
}
}
}
}

.box {
-fx-padding: 1em 1em 1em 1em;
-fx-border-color: linear-gradient(from 0.0px 0.0px to 10.0px 10.0px, reflect, rgba(255, 0, 0, 1) 0.0%, rgba(0, 255, 0, 1) 100.0%) linear-gradient(from 0.0px 0.0px to 10.0px 10.0px, reflect, rgba(255, 0, 0, 1) 0.0%, rgba(0, 255, 0, 1) 100.0%) linear-gradient(from 0.0px 0.0px to 10.0px 10.0px, reflect, rgba(255, 0, 0, 1) 0.0%, rgba(0, 255, 0, 1) 100.0%) linear-gradient(from 0.0px 0.0px to 10.0px 10.0px, reflect, rgba(255, 0, 0, 1) 0.0%, rgba(0, 255, 0, 1) 100.0%);
-fx-border-width: 5px 5px 5px 5px;
-fx-background-color: radial-gradient(focus-angle 90.0deg, focus-distance 50.0% , center 50.0% 50.0%, radius 25.0%, reflect, rgba(255, 255, 255, 1) 0.0%, rgba(0, 0, 0, 1) 100.0%);
-fx-spacing: 5px;
}
.box .label {
-fx-padding: 1em 1em 1em 1em;
-fx-border-color: linear-gradient(from 0.0px 0.0px to 10.0px 10.0px, reflect, rgba(255, 0, 0, 1) 0.0%, rgba(0, 255, 0, 1) 100.0%) linear-gradient(from 0.0px 0.0px to 10.0px 10.0px, reflect, rgba(255, 0, 0, 1) 0.0%, rgba(0, 255, 0, 1) 100.0%) linear-gradient(from 0.0px 0.0px to 10.0px 10.0px, reflect, rgba(255, 0, 0, 1) 0.0%, rgba(0, 255, 0, 1) 100.0%) linear-gradient(from 0.0px 0.0px to 10.0px 10.0px, reflect, rgba(255, 0, 0, 1) 0.0%, rgba(0, 255, 0, 1) 100.0%);
-fx-border-width: 5px 5px 5px 5px;
-fx-font-size: 14pt;
-fx-text-fill: rgba(255, 255, 255, 1);
-fx-background-color: rgba(0, 0, 255, 0.74902);
-fx-rotate: 0.95turn;
-fx-translate-x: 0.5in;
-fx-min-height: 6em;
-fx-scale-x: 2;
-fx-scale-y: 0.75;
-fx-background-radius: 25px 25px 25px 25px;
-fx-border-radius: 25px 25px 25px 25px;
}
Fantastic :)
I'm running into some problems with checking if value: T is an enum in toCss. Either I have to make the function inline so that T can be reified, or I have to change the bounds of T to T : Any.
The first makes it impossible to recurse into toCss (needed for Array/Pair etc), and the other makes it necessary to perform some casts to Any when calling toCss from a class with type parameters, like CssBox.
Don't love any of those approaches. Will try some more.
I'm not sure if I understand. Why won't
is Enum<*> -> return value.toString().toLowerCase().replace("_", "-")
work?
Haha, that works, I think I'm too tired, midnight here already :) I'll revert that crazyness and commit the removed enums etc.
What about this one?
null -> return "" // This should only happen in a container TODO: Is there a better way to handle this?
Do you need this check at all? All passed in types are non-null.
Yes, because anything inside a box, pair, array, etc. that is null somehow still makes it into that function without causing an error.
borderColor = box(Color.Green, null)
would create
-fx-border-color: rgba(0, 255, 0, 1) null rgba(0, 255, 0, 1) null;
Ah :) Just committed, I'm going to bed before I do any more harm :)
Anything that accepts color has to accept Paint?. It was the only way to handle trying to create invalid colors (JavaFX throws an error, so I return a null and have the property setter ignore it; unfortunately the property setter can't look inside containers, so I had to put something there, and a blank string seems to do the least damage in CSS).
Sorry for being dense here, but I don't quite understand (even after a little sleep, hehe :)
var backgroundColor: Paint?
Why does it help to declare this as nullable? I don't understand the part about creating invalid colors :) Can you give me an example?
When you use JavaFX's methods to create a color, it throws an exception if you give bad values, so I catch the exceptions and return null. If you can think of a good way to avoid nulls, that would be awesome, as we could then make cssprop type <reified T : Any>
Ah, OK, I will investigate and get back to you :)
There's probably a better way to do it. I'm still rather new to Kotlin, so I haven't learned many of the languages nuances.
Well, you certainly learn fast!! Could we just log an error and return Color.WHITE in those cases? Or even Color.RED? :)
We could do that. I'd prefer Color.MAGENTA (it's a standard error color in 3D animation, which is where I have some experience). But we also have to worry about things like errors creating Gradients and ImagePatterns (both are types of Paints). Should we do the same there? Log errors and return a default color?
Allright, MAGENTA it is. Let's go with this approach across the board for now. We won't necessarily need to break the API to change how we handle this later on.
Another (ugly) demo so far:
class CssTest : SingleViewApp("CSS Test") {
override val root = VBox()
init {
css {
val hover = mixin {
+s(":hover") {
backgroundColor = RadialGradient(90.0, 0.5, 0.5, 0.5, 0.25, true, CycleMethod.REPEAT, Stop(0.0, Color.WHITE), Stop(0.5, c("error")), Stop(1.0, Color.BLACK))
}
}
val wrap = mixin {
padding = box(1.em)
borderColor = box(LinearGradient(0.0, 0.0, 10.0, 10.0, false, CycleMethod.REFLECT, Stop(0.0, Color.RED), Stop(1.0, c(0.0, 1.0, 0.0))))
borderWidth = box(5.px)
backgroundRadius = box(25.px)
borderRadius = box(25.px)
+hover
}
s(".box") {
+wrap
backgroundColor = RadialGradient(90.0, 0.5, 0.5, 0.5, 0.25, true, CycleMethod.REPEAT, Stop(0.0, Color.WHITE), Stop(1.0, Color.BLACK))
spacing = 5.px
s(".label") {
+wrap
font = Font.font(14.0)
fontWeight = FontWeight.BOLD
textFill = c("white")
rotate = .95.turn
translateX = .5.inches
minHeight = 6.em
scaleX = 2
scaleY = .75
}
}
}
with(root) {
prefWidth = 400.0
prefHeight = 400.0
hbox {
addClass("box")
label("Alice")
label("Bob")
}
}
}
}
Without hover:

With hover:

CSS:
.box {
-fx-padding: 1em 1em 1em 1em;
-fx-border-color: linear-gradient(from 0.0px 0.0px to 10.0px 10.0px, reflect, rgba(255, 0, 0, 1) 0.0%, rgba(0, 255, 0, 1) 100.0%) linear-gradient(from 0.0px 0.0px to 10.0px 10.0px, reflect, rgba(255, 0, 0, 1) 0.0%, rgba(0, 255, 0, 1) 100.0%) linear-gradient(from 0.0px 0.0px to 10.0px 10.0px, reflect, rgba(255, 0, 0, 1) 0.0%, rgba(0, 255, 0, 1) 100.0%) linear-gradient(from 0.0px 0.0px to 10.0px 10.0px, reflect, rgba(255, 0, 0, 1) 0.0%, rgba(0, 255, 0, 1) 100.0%);
-fx-border-width: 5px 5px 5px 5px;
-fx-background-radius: 25px 25px 25px 25px;
-fx-border-radius: 25px 25px 25px 25px;
-fx-background-color: radial-gradient(focus-angle 90.0deg, focus-distance 50.0% , center 50.0% 50.0%, radius 25.0%, repeat, rgba(255, 255, 255, 1) 0.0%, rgba(0, 0, 0, 1) 100.0%);
-fx-spacing: 5px;
}
.box:hover {
-fx-background-color: radial-gradient(focus-angle 90.0deg, focus-distance 50.0% , center 50.0% 50.0%, radius 25.0%, repeat, rgba(255, 255, 255, 1) 0.0%, rgba(255, 0, 255, 1) 50.0%, rgba(0, 0, 0, 1) 100.0%);
}
.box .label {
-fx-padding: 1em 1em 1em 1em;
-fx-border-color: linear-gradient(from 0.0px 0.0px to 10.0px 10.0px, reflect, rgba(255, 0, 0, 1) 0.0%, rgba(0, 255, 0, 1) 100.0%) linear-gradient(from 0.0px 0.0px to 10.0px 10.0px, reflect, rgba(255, 0, 0, 1) 0.0%, rgba(0, 255, 0, 1) 100.0%) linear-gradient(from 0.0px 0.0px to 10.0px 10.0px, reflect, rgba(255, 0, 0, 1) 0.0%, rgba(0, 255, 0, 1) 100.0%) linear-gradient(from 0.0px 0.0px to 10.0px 10.0px, reflect, rgba(255, 0, 0, 1) 0.0%, rgba(0, 255, 0, 1) 100.0%);
-fx-border-width: 5px 5px 5px 5px;
-fx-background-radius: 25px 25px 25px 25px;
-fx-border-radius: 25px 25px 25px 25px;
-fx-font: normal 14.0pt "System";
-fx-font-weight: 700;
-fx-text-fill: rgba(255, 255, 255, 1);
-fx-rotate: 0.95turn;
-fx-translate-x: 0.5in;
-fx-min-height: 6em;
-fx-scale-x: 2;
-fx-scale-y: 0.75;
}
.box .label:hover {
-fx-background-color: radial-gradient(focus-angle 90.0deg, focus-distance 50.0% , center 50.0% 50.0%, radius 25.0%, repeat, rgba(255, 255, 255, 1) 0.0%, rgba(255, 0, 255, 1) 50.0%, rgba(0, 0, 0, 1) 100.0%);
}
Things to notice:
+, it is set as a modifier selection instead of a nested selection (similar to Sass's & operator)c("error") call now returns Color.MAGENTA instead of nullI've also been able to remove all but two of the custom enums.
Oh man, really good call on the + for modifier, I love it!
Feel free to assign stuff to me of what's left if anything :)
I've just got to get the BorderStrokeStyle rendering to CSS properly, and then I think all the properties should be done. After that, we may want to divvy up testing them. There are a lot.
I have found an interesting bug that I believe is a JavaFX issue. If you set a node's blendMode in css to certain values, you get a strange error. For example, in the above example, if I add blendMode = BlendMode.ADD to the .label selector above, it renders correctly (-fx-blend-mode: add;), but I get this:
WARNING: Caught 'java.lang.ClassCastException: javafx.scene.paint.Color cannot be cast to java.lang.String' while converting value for '-fx-blend-mode' from rule '*.box *.label' in stylesheet <styleseet url>
This happens with the colors (RED, BLUE, GREEN) as well, but at least in that case the warning seems to be relevent (though still a bug). I have no Idea what the color to string conversion for ADD could be about.
There are some ways that this DSL can be improved. Right now, selections are just strings. It would be nice if we parsed the selector string into some form of container. Then, if we were to create a selection like:
s(".label, .text") {
fontWeight = FontWeight.BOLD
+s(":hover:) {
backgroundColor = c("blue", 0.25)
}
}
it would render as:
.label, .text {
-fx-font-weight: 700;
}
.label:hover, .text:hover {
-fx-background-color: rgba(0, 0, 255, 0.25);
}
instead of the current:
.label, .text {
-fx-font-weight: 700;
}
.label, .text:hover {
-fx-background-color: rgba(0, 0, 255, 0.25);
}
Another bug: using borderStyle: BorderStrokeStyle.DOTTED doesn't work because it defines the segments as {0, 2}, and JavaFX refuses any input where all segment on lengths are 0. Does the internal BorderStrokeStyle object use a different array order than the css version? I can't find any documentation about it.
I have a work-around for the BorderStyle bug, but it would still be nice to know if the orders are different, as that will change what the user gets.
I've pushed up what I have so far. This will need a lot of testing, and I'm not sure if the borderStyle property is working properly. I left a FIXME in there for it.
There is definitely a discrepancy between how the border strokes are handled in code and in CSS. Here is a demo:
class CssDemo : SingleViewApp("CSS Demo") {
override val root = VBox()
init {
val borderTest = BorderStrokeStyle(StrokeType.INSIDE, StrokeLineJoin.MITER, StrokeLineCap.BUTT, 10.0, 0.0, listOf(25.0, 5.0))
with(root) {
addClass("box")
label("Alice") { border = Border(BorderStroke(Color.ORANGE, borderTest, null, BorderWidths(5.0))) }
label("Bob") { addClass("bob") }
}
css {
s(".box") {
padding = box(10.px)
spacing = 10.px
}
s(".label") {
fontSize = 56.px
padding = box(5.px, 10.px)
maxWidth = infinity
+s(".bob") {
borderColor = box(Color.ORANGE)
borderStyle = borderTest
borderWidth = box(5.px)
}
}
}
}
}
As you can see I use the same settings for both. However, this is the result:

Here is the generated CSS:
.box {
-fx-padding: 10px 10px 10px 10px;
-fx-spacing: 10px;
}
.label {
-fx-font-size: 56px;
-fx-padding: 5px 10px 5px 10px;
-fx-max-width: infinity;
}
.label.bob {
-fx-border-color: rgba(255, 165, 0, 1) rgba(255, 165, 0, 1) rgba(255, 165, 0, 1) rgba(255, 165, 0, 1);
-fx-border-style: segments(25.0 5.0) inside line-join miter 10.0 line-cap butt;
-fx-border-width: 5px 5px 5px 5px;
}
I don't know if I'm getting the CSS wrong of if there is a problem with JavaFX. Perhaps someone else should look into it.
Great, I will start digging in tonight. Busy week at work, but I'll be able to get some hours in on this each night.
I'm very sorry @t-boom - today is shot as well. Will get back to you tomorrow for sure!
No problem. My day's been quite busy as well.
About the selector string issue, maybe we can cheat a little and just split the string and append the new selector off of each segment?
class Selection(val selector: String) : SelectionBlock() {
var modifier = false
fun render(current: String = ""): String {
return buildString {
val currentSelector = expand(current, selector)
append("$currentSelector {\n")
for ((name, value) in properties) {
append(" $name: ${toCss(value)};\n")
}
append("}\n")
for (selection in selections) {
append(selection.render(if (selection.modifier) currentSelector else "$currentSelector "))
}
}
}
private fun expand(current: String, selector: String) =
current.split(",").map { it + selector }.joinToString(", ").replace(" ", " ")
override fun toString() = render()
}
The only thing changed here is the addition of the expand function, and the call to it (val currentSelector = expand(current, selector)).
Would that work, or is it too simplistic?
That will work fine for now, but there are still a lot of other situations to look out for. For example:
// Notice the space after "test"
s(".test ") {
+s(":hover") { ... }
}
will still result in the :hover selector being nested instead of appended because of the space. The same would happen with spaces between selectors and commas s(".box , .label") and possibly other places. This could of course be handled with some quick string stripping, I just think it would be better future proofing to create some sort of properly handled container.
Another thing that would be nice (I'm not sure how feasible, but definitely more so with proper selection handling) would be the ability to do something like this:
.action-bar .button .contents .text {
...
}
.action-bar .button:hover .contents .text {
...
}
in a simple selection.
What we have now (including the expand functionality which I have now added) is probably good for a phase 1, but we may want to look into enhancing the selection process for future versions.
I agree, and absolutely see your point. After we've used this a little I suspect we might even find better ways to structure it. As long as the API looks good, we're free too change the implementation later on without causing waves.
To solve the postfix-space issue, we could do some sanitation in the constructor of Selection. For example like this:
class Selection(selector: String) : SelectionBlock() {
val selector: String
var modifier = false
init {
// Sanitize selector input
this.selector = selector.trim()
}
}
What do you think?
I like the idea you presented in your last post, and there are multiple routes to get there. Maybe we should look to SCSS and friends to see how they have solved it (if they have?).
I will post an updated version of Selection that also takes care of the s(".box , .label") issue shortly :)
I also introduced a companion object so we can precompile the regex, and also reuse it to clean up the faulty selectors :)
class Selection(selector: String) : SelectionBlock() {
val selector: String
var modifier = false
init {
// Sanitize selector input
this.selector = selector.trim().replace(SelectorSeparator, ", ")
}
fun render(current: String = ""): String {
return buildString {
val currentSelector = expand(current, selector)
append("$currentSelector {\n")
for ((name, value) in properties) {
append(" $name: ${toCss(value)};\n")
}
append("}\n")
for (selection in selections) {
append(selection.render(if (selection.modifier) currentSelector else "$currentSelector "))
}
}
}
private fun expand(current: String, selector: String) =
current.split(SelectorSeparator).map { it + selector }.joinToString().replace(" ", " ")
override fun toString() = render()
companion object {
private val SelectorSeparator = Regex("\\s*,\\s*")
}
}
That seems to work great :)
I agree with what you said about better structure. Maybe that can be mixed in with #57.
We may also eventually want to add some optimizations. Here's a quick example:
s(".graph-node") {
s(".label") {
padding = box(.25.em)
}
+s(".task") {
backgroundColor = c("green", 0.2)
+s(":hover") {
backgroundColor = c("green", 0.3)
}
}
+s(".project") {
backgroundColor = c("red", 0.2)
+s(":hover") {
backgroundColor = c("red", 0.3)
}
}
}
This will render
.graph-node {
}
.graph-node .label {
-fx-padding: 0.25em 0.25em 0.25em 0.25em;
}
.graph-node.task {
-fx-background-color: rgba(0, 128, 0, 0.2);
}
.graph-node.task:hover {
-fx-background-color: rgba(0, 128, 0, 0.30196);
}
.graph-node.project {
-fx-background-color: rgba(255, 0, 0, 0.2);
}
.graph-node.project:hover {
-fx-background-color: rgba(255, 0, 0, 0.30196);
}
The .graph-node selector is empty, and doesn't have to be rendered. This should be an easy one to do, but there are some others that would require a huge restructuring of the code. For example, we could make it so including mixins would render in their own, comma separated section (not sure if this would be beneficial or not, it just popped into my head).
I've commited a fix to not render empty selectors, that's a nice optimization :)
You've taken this pretty far already, so I think we should focus on getting it out there so we can get feedback from users. That will probably spawn some good ideas.
I'll see if I can make sense of that border stroke issue :)
I also commited a fix to support comma separated modifier selections.
Consider:
s(".label, .text") {
+s(":hover, :armed") {
backgroundColor = c("blue", 0.25)
}
}
This would render as:
.label:hover, :armed, .text:hover, :armed {
-fx-background-color: rgba(0, 0, 255, 0,25098);
}
With my latest commit, it will render as:
.label:hover, .text:hover, .label:armed, .text:armed {
-fx-background-color: rgba(0, 0, 255, 0,25098);
}
The solution is a recursive call to expand, the implementation is a little ugly right now, will probably check in a better version soon.
Just noticed there are locale issues here as well, I have Norwegian locale as default, which uses comma instead of dot as the decimal separator :)
Fix for comma separator commited, the above will now print like this independent of the current locale:
.label:hover, .text:hover, .label:armed, .text:armed {
-fx-background-color: rgba(0, 0, 255, 0.25098);
}
There is also an easy optimization available for color rendering. Right now you have this conversion to css color for Paint:
is Paint -> return value.toString().replace(Regex("0x[0-9a-f]{8}")) { Color.web(it.groupValues[0]).css }
This will also run for Color, and that causes decimal numbers to expand, for example 0.25 will be 0.25098 and 0.3 becomes 0.30196. It's also an unnecessary round trip.
I added:
is Color -> return value.css
So now the above renders like this:
.label:hover, .text:hover, .label:armed, .text:armed {
-fx-background-color: rgba(0, 0, 255, 0.25);
}
Awesome updates :)
I agree with getting it out there to get feedback. I'm sure I can come with ideas all day, but they'll probably get worse as I go :) It would be nice to get input from people that use it for actual projects.
Cool! OK, let's get it out there for people to play with! Do you know of any outstanding bugs or issues that needs to be adressed before releasing? The dotted border issue seems so small that we probably can keep the comment and ignore it for now. We could create some tests to verify that the correct CSS is produced, I'll add some simple tests right now :)
Sounds good. It probably needs a fair bit of testing. I found a misnamed property reference (-fx-paint instead of -fx-stroke) that was giving me grief last night, and there may be others. I haven't had a chance to test them all yet.
Perfect, let's start testing :) I added a StylesheetTests.kt with some convenience stuff, so we can write tests like this:
@Test
fun nestedModifier_1() {
stylesheet {
s(".label, .text") {
+s(":hover, :armed") {
backgroundColor = c("blue", 0.25)
}
}
} shouldEqual {
"""
.label:hover, .text:hover, .label:armed, .text:armed {
-fx-background-color: rgba(0, 0, 255, 0.25);
}
"""
}
}
Whitespace is stripped and massaged so it doesn't matter how you indent or line break the expected result.
Sorry, wrong button :P
Let's just start with some random tests, we can use the examples that we have determined to be correct from this thread and a couple more, and then we could add more tests as we go along. I can also also add tests as I start using it :)
I also added a page to the Wiki for documentation. This will be a killer feature, and it would be great if someone with better english than mine wrote it :) Do you feel up for the task? If not, I can always flesh it out and ask @thomasnield to proof it :)
It would basically just introduce the feature, show some examples with the expected text output and an image (like you've done here), and maybe some explanations about the different ways to create properties like color, and others that has special syntax.
What do you think? If you don't feel comfortable with it, I'll do it myself of course :)
For sure! Give me the next few days to study and document it since I haven't used it yet, and Ill add it to the Wiki as well as the guide.
Ah, that's perfect @thomasnield ! OK, we'll get it integrated into the main branch in the mean time, we'll just add some tests first.
@thomasnield Thanks. I'm not much of a writer, but if you need any help or clarification or anything, let me know.
Thanks @t-boom will do : )
I just added another test. Should we merge the feature/css-dsl branch into master and do a release while tagging this as experimental for now?
I think that makes sense since the feature is pretty isolated.
I've started adding some placeholders in the wiki page for the CSS documentation. @t-boom - could you add some more placeholders, you know best what's in there? That will make it easier for Thomas to fill in the blanks :)
Thomas, feel free to delete any or all of my text, I just put something there to get started :)
The examples I used are probably bad as well :)
Ok no problem, I'll start hammering through this tomorrow.
I just had an idea and ended up creating Type Safe Selectors as well :) This is an extremely optional feature, but it enables some very interesting possibilities. Using this, your IDE will tell you if you have unused classes, and you can very easily find the CSS selectors that applies to a certain node. The penalty is that you have to declare all your css classes in your stylesheet, but I'm sure some people will find the peace of mind this provides refreshing.
Will explain more later, here is a working example:
class CssDemo : SingleViewApp("Type Safe Selectors") {
override val root = VBox()
// Declare all the styles you will use
companion object {
val box by cssclass()
val label by cssclass()
val bob by cssclass()
val alice by cssclass()
}
init {
with(root) {
// Look ma, no strings! Add classes to nodes in a type safe manner
addClass(box)
label("Alice") {
addClass(alice)
}
label("Bob") {
addClass(bob)
}
}
css {
// Selectors now supports type safe assignments
s(box) {
padding = box(10.px)
spacing = 10.px
}
s(label) {
fontSize = 56.px
padding = box(5.px, 10.px)
maxWidth = infinity
// Add .label.bob, .label.alice with a nicer syntax
+s(bob, alice) {
borderColor = box(c("orange"))
borderStyle = BorderStrokeStyle(StrokeType.INSIDE, StrokeLineJoin.MITER, StrokeLineCap.BUTT, 10.0, 0.0, listOf(25.0, 5.0))
borderWidth = box(5.px)
}
}
}
}
}
There are some rough edges, will iron them out later :)
That is quite interesting. I like the concept.
One quick question (as I am still learning Kotlin), I noticed you used base.substring(0, 1). Does that have a performance benefit over base[0]? Or just safer in case of an empty string?
Haha, no I'm still learning Kotlin as well :) Didn't think of the get operator function :) Will change it.
Added support for pseudo classes as well:
companion object {
val box by cssclass()
val label by cssclass("mylabel")
val hover by csspseudoclass()
}
...
s(box, label) {
+s(hover) {
backgroundColor = Color.RED
}
}
Result:
.box:hover, .mylabel:hover {
-fx-background-color: rgba(255, 0, 0, 1);
}
I found a way to improve the syntax and reduce memory footprint, so I updated the examples above. Now , works like in a normal stylesheet, and label + alice will render .label.alice. This last one I'm not completely sure about: To render .label .alice you would write label containing alice. Does that make sense or should it be expressed differently?
That's a hard one. You could overload another operator (/ or %?), but that isn't quite as clear. I think contains is probably okay as they could just nest them instead (s(label) { s(alice) { ... } }), but that is a bit verbose too.
I really like the new style though
Yeah that helps :) containing is really hard to write and too long :)
Sorry for editing these last posts multiple times, I've updated the examples to show the current syntax :)
Important to notice: The cssclass delegate returns a new instance every time it is accessed, so that another mention of the same css class would not just augment the first instance :)
I really love that with this approach, your IDE can tell you where a CSS class is defined, and also where it is applied to nodes. This helps tremendously with removing dead code and to see where/if stuff is used. There is also a cssid delegate to create #ids.
I moved the c() and box() functions outside of the SelectionBlock so that they can be used to create constants in the companion object of the stylesheet as well.
It is quite nice. Should we change the selection code to use this new format exclusively, or should we still support string selectors?
Hmm... Interesting.. I think we need to keep it, because we don't know if everybody will be comfortable with the x containing y syntax, and there are actually selectors that cannot be expressed with the type safe selectors yet. We need ".box > .label" for example. That's direct children or first child isn't it? Hmm.. Will check and add it right away!
Forget the color rendering issue, I brain farted :)
Fair point
Added box direct label for direct child. Feel free to suggest better wording :)
Sounds good to me :)
Cool! I'm really happy with how this turned out. Should we merge this branch and let Thomas play with it and create docs?
I'm all for it
Cool, done! Should we add default csspseudoclass entries for the defined pseudoclasses in JavaFX to the companion object of Stylesheet maybe? No need for everybody to define hover etc.
That's a good idea.
What about adding classes for all the JavaFX builtin classes (.label, .button, .check-box, etc.)?
Ah, perfect! I added all the pseudo classes and all 67 style classes from the JavaFX 8 css reference. I added them as internal val so that they can be used in styleclass subclasses, but there should be no need to access them from outside, since there is no need to add these classes to components. I will close this ticket now, let's continue any discussion on the mailing list to see how that works :)
Hey guys, I'm doing some edits to the Wiki right now and playing with this feature. Why am I getting this compile error for this one example...
class CssDemo : SingleViewApp("CSS Demo") {
override val root = VBox()
companion object {
val wrapper by cssclass()
val alice by cssclass()
val bob by cssclass()
}
init {
with(root) {
addClass(wrapper)
label("Alice") {
addClass(alice)
}
label("Bob") {
addClass(bob)
}
}
css {
s(wrapper) {
padding = box(10.px)
spacing = 10.px
}
s(this.label) {
fontSize = 56.px
padding = box(5.px, 10.px)
maxWidth = infinity
+s(bob, alice) {
borderColor = box(Color.ORANGE)
borderStyle = BorderStrokeStyle(StrokeType.INSIDE, StrokeLineJoin.MITER, StrokeLineCap.BUTT, 10.0, 0.0, listOf(25.0, 5.0))
borderWidth = box(5.px)
}
}
}
}
}

Here is the error IDEA is giving me. Is this conflicting with the builders?

I think this has something to do with the question I have about internal attributes. I'm still a bit confused about the whole thing.
I feel like I might have a related issue that might indicate a pattern to the problem. Sometimes when I use the top(), bottom(), left(), and right() function builders for BorderPane, IDEA gets confused and thinks I'm referring to the properties and is wondering why I'm putting a block next to them. My workaround usually entails typing it out again and then it correctly concludes I'm using the extension functions.
Yeah, unfortunately SingleViewApp does not extend Stylesheet, so you cannot access these attributes. It only works when you create a standalone Stylesheet. Let me have a look and see if we can remedy this. Sorry I didn't catch it sooner. For now, just create your stylesheet in a class below the singleviewapp with class Styles : Stylesheet() { ... } and import it using importStylesheet(Styles::class). This has the advantage that it will also support reload on focus.
Okay, that is exactly what I was going to do...
I tried putting the constants in a StylesheetConstants interface and then let Stylesheet and SingleViewApp implement that interface. Then it works, but then you also need to import them, which gives a much poorer experience. Trying some other tricks now.
Having them protected requires @JvmStatic, and that's not available in an interface.. :(
If I make them public, they will be available to SingleViewApp, but they would need an import. They will not need an import for use in Stylesheet though.
I see two options, there might be more, but maybe we can live with either of these:
css { } clause in SingleViewApp and declare stylesheets externally. This has the added benefit of making sure all SingleViewApps also supports reloading of stylesheets.css { }, but you need to import the constants to use them there (but not in Stylesheet):import tornadofx.Stylesheet.Companion.label
class Single : SingleViewApp("Hello CSS") {
override val root = HBox()
init {
css {
s(label) {
}
}
}
}
Looking for more options now.
The IDEA issue you described is not this, by the way Thomas. Hmm.. I can't see any other options. How do you guys feel about the options I outlined above?
Let me think about this for a moment. Some people may abuse SingleViewApp beyond its intended purpose and want to use it for production applications, and not just for portable example code. They may find it limiting to not have the functionality built in either....
I'm actually leaning towards just removing the css thing. That way, there is only one way to create and use stylesheets, and people won't get confused about that they some times need to import constants, and other times not.
This will also remove confusion about why reloadStylesheetsOnFocus only works "some times".
I just realized that internal was the completely wrong keyword to use. It will make sure this only works in the package tornadofx, so it's basically useless in the upcoming 1.4.3 release. I'll have to fix this and do a new release ASAP.
Okay, but this only applies to the SingleViewApp correct?
Uh, no :) hehe.. But it seems I was able to change the artifacts before they landed in maven central. Verifying it now.
Phew, the version that landed in maven central is OK (public fields for the style constants).
Just to clarify: SingleViewApp can use the css { } clause, but it needs imports if style constants are used. We need to decide if we should just not mention the css { } clause and remove it in the next release.
I think that might be a good idea if its functionality is cumbersome. I'm still playing with this and getting comfortable with it. Feel free to document and I'll follow behind with copyediting. The real documentation is probably going to come in the guide...
Should we move this to the Google Group thread? This is becoming quite large. Anyway, so is this the typical pattern? Is the companion object and delegated properties what bridges everything together? Am I missing any other patterns?
class CssDemo : App() {
override val primaryView = MyView::class
}
class MyView: View() {
override val root = VBox()
companion object {
// Define our styles
val wrapper by cssclass()
val bob by cssclass()
val alice by cssclass()
}
init {
with(root) {
addClass(wrapper)
label("Alice") {
addClass(alice)
}
label("Bob") {
addClass(bob)
}
}
importStylesheet(Styles::class)
}
}
class Styles : Stylesheet() {
companion object {
// Define our styles
val wrapper by cssclass()
val bob by cssclass()
val alice by cssclass()
// Define our colors
val dangerColor = c("#a94442")
val hoverColor = c("#d49942")
}
init {
s(wrapper) {
padding = box(10.px)
spacing = 10.px
}
s(label) {
fontSize = 56.px
padding = box(5.px, 10.px)
maxWidth = infinity
+s(bob, alice) {
borderColor = box(dangerColor)
borderStyle = BorderStrokeStyle(StrokeType.INSIDE, StrokeLineJoin.MITER, StrokeLineCap.BUTT, 10.0, 0.0, listOf(25.0, 5.0))
borderWidth = box(5.px)
+s(hover) {
backgroundColor = hoverColor
}
}
}
}
}
Good idea Thomas, I posted my reply on the forum :)
As discussed on the Google Group thread, a Kotlin Slack channel has been created for #tornadofx.