Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for global styles without relying on css classname literals #451

Closed
reubenfirmin opened this issue Nov 14, 2022 · 3 comments
Closed

Comments

@reubenfirmin
Copy link

reubenfirmin commented Nov 14, 2022

One way to avoid recalculating styles on the fly (and yet keep styling in code) is to have a global styles class (or series of classes/objects) which is initialized.

For example:

class GlobalStyles {

    val cardStyle = Style(".card") {
            // stuff here
        }
    }
}

This gets "imported" as follows:

val styles = GlobalStyles()

...and now styles don't need to be regenerated each time components are rerendered. However, it's a bit messy, since it relies on the css classname to "link" the style to the component; i.e. to use it, of course, I have to have a widget that does:

class CardView(): Div() {
   init {
      addCssClass("card")
   }
}

I can't even break out "card" fully to a constant, because the css rule wants a dot in front of it, so I'd need to prepend with a dot when initializing the style (which is...ok, but still smelly.)

Can you come up with a cleaner way of "linking" the component to the global style? (This may be the same solution as #450, but I thought I would file separately in case there are two separate ways of tackling this.)

One possibility, though I don't know if inline/reifying/etc will work with the js compiler:

class GlobalStyles {

    val cardStyle = Style.for<CardView>() {
        // stuff here
    }
}

Of course, people would eventually want to replicate CSS selectors in code, so you could imagine craziness like:

class GlobalStyles {

    val cardStyle = Style.for<Heading1>().in<CardView> {
        // stuff here
    }
}

...but maybe this isn't so bad, as it's more readable than css selectors for somebody who isn't a browser developer.

Another simpler possibility:

class CardView {

    staticStyle {
         // stuff here
    }
}

@reubenfirmin
Copy link
Author

reubenfirmin commented Nov 14, 2022

Here's an example class that I use to both define and turn on and off css classes. It'd be awesome if this could be made cleaner (without the strings).

package com.example.model

import com.example.*
import com.example.App.Companion.debug
import com.example.view.canvas.CardView
import io.kvision.core.*
import io.kvision.core.Color.Companion.hex
import io.kvision.core.UNIT.*
import io.kvision.html.H1
import io.kvision.html.H2
import io.kvision.utils.em
import io.kvision.utils.px

const val CLASS_CARD = "card"
const val CLASS_ROUNDED = "rounded"
const val CLASS_OUTLINE_BLUE = "outline_blue" // TODO semantic name
const val CLASS_OUTLINE_GREEN = "outline_green"
const val CLASS_DECORATED = "decorated"
const val CLASS_HOVER = "hover"
const val CLASS_SELECTED = "selected"
const val CLASS_HEADING = "heading"
const val CLASS_INFO = "info"
const val CLASS_ALLDONE = "alldone"
const val CLASS_STRIKETHROUGH = "strikethrough"
const val CLASS_TASK = "task"

class CardStyle(val theme: Theme) {

    private val foundationCardStyle = Style() {
        color = hex(theme.cardFontColor)
        position = Position.RELATIVE
        whiteSpace = WhiteSpace.NORMAL
    }

    private val baseCardStyle =  inheritingStyle(parentStyle = foundationCardStyle) {
        margin = 10.px
        fontSize = 16.px
        lineHeight = 1.5.em
        opacity = 100.0
        padding = 10.px
        background = Background(hex(theme.cardBackground))
    }

    private val imageCardStyle = inheritingStyle(parentStyle = foundationCardStyle) {
        paddingRight = 15 to px
        paddingLeft = 15 to px
        paddingTop = 10 to px
        paddingBottom = 0 to px
        margin = 0 to px
    }

    private val roundedCardStyle = inheritingStyle(selector = ".$CLASS_CARD.$CLASS_ROUNDED", parentStyle = baseCardStyle) {
        borderRadius = 10.px
        boxShadow = BoxShadow(0.px, 8.px, 14.px, 0.px, Color.rgba(0,0,0,45))
    }

    private val linkCardStyle = inheritingStyle(parentStyle = roundedCardStyle) {
        textAlign = TextAlign.CENTER
        fontSize = 14 to px
        cursor = Cursor.POINTER
    }

    private val outlineCardStyleBlue = inheritingStyle(selector = ".$CLASS_CARD.$CLASS_OUTLINE_BLUE", parentStyle = linkCardStyle) {
        border = Border(2 to px, BorderStyle.SOLID, hex(theme.cardStripeFocused))
    }

    private val outlineCardStyleGreen = inheritingStyle(selector = ".$CLASS_CARD.$CLASS_OUTLINE_GREEN", parentStyle = linkCardStyle) {
        border = Border(2 to px, BorderStyle.SOLID, hex(theme.cardStripeDone))
    }

    private val decoratedCardStyle = inheritingStyle(selector = ".$CLASS_CARD.$CLASS_DECORATED", parentStyle = roundedCardStyle) {
        borderLeft = Border(10 to px, BorderStyle.SOLID, hex(theme.cardStripe))
    }

    private val decoratedCardStyleHover = inheritingStyle(selector = ".$CLASS_CARD.$CLASS_DECORATED.$CLASS_HOVER", parentStyle = decoratedCardStyle, pClass = PClass.HOVER) {
        background = Background(hex(theme.cardFocusedBackground))
        borderLeft = Border(10 to px, BorderStyle.SOLID, hex(theme.cardStripeFocused))
    }

    private val headingCard = Style(".$CLASS_CARD.$CLASS_HEADING") {
        margin = 10 to px
        borderLeft = Border(0 to px)
        background = Background(hex(theme.appBackground))
    }

    private val decoratedCardAllDoneStyle = inheritingStyle(selector = ".$CLASS_CARD.$CLASS_ALLDONE", parentStyle = decoratedCardStyle) {
        borderLeft = Border(10 to px, BorderStyle.SOLID, hex(theme.cardStripeDone))
    }

    private val selectedStyle = Style(".$CLASS_CARD.selected") {
        background = Background(hex(theme.cardFocusedBackground))
    }

    private val decoratedSelectedStyle = inheritingStyle(selector = ".$CLASS_CARD.$CLASS_DECORATED.$CLASS_SELECTED", parentStyle = selectedStyle) {
        borderLeft = Border(10 to UNIT.px, BorderStyle.SOLID, hex(theme.cardStripeFocused))
    }

    private val strikeThrough = Style(".$CLASS_STRIKETHROUGH") {
        textDecoration = TextDecoration(TextDecorationLine.LINETHROUGH)
        color = hex(theme.cardHeadingDone)
    }

    fun styleCard(card: Card, view: CardView, model: Model) {
        view.addCssClass(CLASS_CARD)
        if (view.selected) {
            view.addCssClass(CLASS_SELECTED)
        } else {
            view.removeCssClass(CLASS_SELECTED)
        }
        when {
            card.headingOnly() -> {
                view.addCssClass(CLASS_HEADING)
            }
            card.infoOnly() -> {
                view.addCssClass(CLASS_ROUNDED)
                view.addCssClass(CLASS_INFO)
            }
            else -> {
                if (card.done()) {
                    view.addCssClass(CLASS_ALLDONE)
                    // TODO this would be better as a ".class > h1,h2"
                    view.getChildren().filter { it is H1 || it is H2 }.forEach { child ->
                        child.addCssClass(CLASS_STRIKETHROUGH)
                    }
                } else {
                    view.addCssClass(CLASS_DECORATED)
                    view.addCssClass(CLASS_HOVER)
                }
                view.addCssClass(CLASS_TASK)
            }
        }

        if (card.isCanvasLink()) {
            applyCanvasLinkStyle(model, view)
        } else if (card.isImageLink()) {
            view.addCssStyle(imageCardStyle)
        }
    }

    fun applyCanvasLinkStyle(model: Model, view: CardView) {
        if (model.exists(view.card.linkedCanvas()!!)) {
            view.removeCssClass(CLASS_OUTLINE_GREEN)
            view.addCssClass(CLASS_OUTLINE_BLUE)
        } else {
            view.removeCssClass(CLASS_OUTLINE_BLUE)
            view.addCssClass(CLASS_OUTLINE_GREEN)
        }
    }
}

@rjaros
Copy link
Owner

rjaros commented Nov 14, 2022

But you don't have to rely on class selectors, because they are generally auto-generated. So instead of:

val cardStyle = Style(".card") {
    fontSize = 2.em
}

div {
    addCssClass("card")
    +"Some large text"
}

you should do:

val cardStyle = Style {
    fontSize = 2.em
}

div {
    addCssStyle(cardStyle)
    +"Some large text"
}

or even:

div {
    style {
        fontSize = 2.em
   }
    +"Some large text"
}

@reubenfirmin
Copy link
Author

reubenfirmin commented Nov 15, 2022

Yes I think that's right, though these styles need to be declared outside of the class, otherwise they get a new classname per instance of the class.

Check this out:

2022-11-15_04-36

Left arrow is me taking a hit by loading some global styles.

Middle three arrows is the app on initialization creating three help dialogs (which are initially hidden). They each contain the same boilerplate UI; however, as you can see below, each of them gets their own version of the Style for the X button, because it's created on class initialization.

2022-11-15_04-43
2022-11-15_04-42

I also had this problem on initialization of some buttons, shown at the right of first screenshot in this comment. Each of the buttons has their own style class too.

The above is following the pattern:

abstract class HelpTemplate(...) {
    val someWidgetStyle = Style {

    }

/// ...
     widget.addCssStyle(someWidgetStyle)
}

If I instead change to:

class HelpTemplateStyles() {
    val someWidgetStyle = Style {

    }
}

abstract class HelpTemplate(val styles: HelpTemplateStyles) {

/// ...
     widget.addCssStyle(styles.someWidgetStyle)
}

... then initialization instead looks like this:

2022-11-15_04-58

The three arrows on the right are the three different help templates, now using all of the common style objects. The arrow at the left is the initialization of the global styles for HelpTemplates. So, doing just this cut down rendering time by around 40ms. Incidentally I can't put these into a companion object, because I need to initialize them with the selected theme; and tangentially when the user changes theme, I force a full reload of the app, to reinitialize all of the global styles.

@rjaros rjaros closed this as completed Jul 3, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants