Closed ruckustboom closed 8 years ago
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.
Just an idea: should there be a DSL for adding CSS? Either to a node or to the View.