diff options
-rw-r--r-- | src/main/kotlin/pl/treksoft/kvision/core/Css.kt | 10 | ||||
-rw-r--r-- | src/main/kotlin/pl/treksoft/kvision/core/Style.kt | 34 | ||||
-rw-r--r-- | src/main/kotlin/pl/treksoft/kvision/core/StyledComponent.kt | 362 | ||||
-rw-r--r-- | src/main/kotlin/pl/treksoft/kvision/core/Widget.kt | 34 | ||||
-rw-r--r-- | src/main/kotlin/pl/treksoft/kvision/utils/Cache.kt | 198 |
5 files changed, 465 insertions, 173 deletions
diff --git a/src/main/kotlin/pl/treksoft/kvision/core/Css.kt b/src/main/kotlin/pl/treksoft/kvision/core/Css.kt index 9fc50097..8a468fe5 100644 --- a/src/main/kotlin/pl/treksoft/kvision/core/Css.kt +++ b/src/main/kotlin/pl/treksoft/kvision/core/Css.kt @@ -552,6 +552,8 @@ class Border private constructor( val w = width?.asString() return w.orEmpty() + " " + (style?.borderStyle).orEmpty() + " " + color.orEmpty() } + + override fun toString() = asString() } /** @@ -573,6 +575,8 @@ class Color private constructor(private val color: String? = null) { fun asString(): String { return color.orEmpty() } + + override fun toString() = asString() } /** @@ -676,6 +680,8 @@ class Background private constructor( } + " " + (repeat?.repeat).orEmpty() + " " + (origin?.origin).orEmpty() + " " + (clip?.clip).orEmpty() + " " + (attachment?.attachment).orEmpty() } + + override fun toString() = asString() } /** @@ -717,6 +723,8 @@ class TextDecoration private constructor( (style?.textDecorationStyle).orEmpty() + " " + color.orEmpty() } + + override fun toString() = asString() } /** @@ -764,4 +772,6 @@ class TextShadow private constructor( (blurRadius?.asString()).orEmpty() + " " + color.orEmpty() } + + override fun toString() = asString() } diff --git a/src/main/kotlin/pl/treksoft/kvision/core/Style.kt b/src/main/kotlin/pl/treksoft/kvision/core/Style.kt index 549aa44c..282d2e7e 100644 --- a/src/main/kotlin/pl/treksoft/kvision/core/Style.kt +++ b/src/main/kotlin/pl/treksoft/kvision/core/Style.kt @@ -26,6 +26,7 @@ import com.github.snabbdom.h import org.w3c.dom.Node import pl.treksoft.jquery.JQuery import pl.treksoft.kvision.panel.Root +import kotlin.reflect.KProperty /** * CSS style object. @@ -38,6 +39,7 @@ import pl.treksoft.kvision.panel.Root @Suppress("TooManyFunctions") open class Style(className: String? = null, parentStyle: Style? = null, init: (Style.() -> Unit)? = null) : StyledComponent() { + private val propertyValues: MutableMap<String, Any?> = mutableMapOf() override var parent: Container? = Root.getFirstRoot() @@ -133,6 +135,38 @@ open class Style(className: String? = null, parentStyle: Style? = null, init: (S styles.remove(this) } + protected fun <T> refreshOnUpdate(refreshFunction: ((T) -> Unit) = { this.refresh() }) = + RefreshDelegateProvider<T>(null, refreshFunction) + + protected fun <T> refreshOnUpdate(initialValue: T, refreshFunction: ((T) -> Unit) = { this.refresh() }) = + RefreshDelegateProvider(initialValue, refreshFunction) + + protected inner class RefreshDelegateProvider<T>( + private val initialValue: T?, private val refreshFunction: (T) -> Unit + ) { + operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): RefreshDelegate<T> { + if (initialValue != null) propertyValues[prop.name] = initialValue + return RefreshDelegate(refreshFunction) + } + } + + protected inner class RefreshDelegate<T>(private val refreshFunction: ((T) -> Unit)) { + @Suppress("UNCHECKED_CAST") + operator fun getValue(thisRef: StyledComponent, property: KProperty<*>): T { + val value = propertyValues[property.name] + return if (value != null) { + value as T + } else { + null as T + } + } + + operator fun setValue(thisRef: StyledComponent, property: KProperty<*>, value: T) { + propertyValues[property.name] = value + refreshFunction(value) + } + } + companion object { internal var counter = 0 internal var styles = mutableListOf<Style>() diff --git a/src/main/kotlin/pl/treksoft/kvision/core/StyledComponent.kt b/src/main/kotlin/pl/treksoft/kvision/core/StyledComponent.kt index 428728b7..1207ba5e 100644 --- a/src/main/kotlin/pl/treksoft/kvision/core/StyledComponent.kt +++ b/src/main/kotlin/pl/treksoft/kvision/core/StyledComponent.kt @@ -21,6 +21,7 @@ */ package pl.treksoft.kvision.core +import pl.treksoft.kvision.utils.Cache import pl.treksoft.kvision.utils.asString import kotlin.reflect.KProperty @@ -311,185 +312,196 @@ abstract class StyledComponent : Component { */ @Suppress("ComplexMethod", "LongMethod") protected open fun getSnStyle(): List<StringPair> { - val snstyle = mutableListOf<StringPair>() - width?.let { - snstyle.add("width" to it.asString()) - } - minWidth?.let { - snstyle.add("min-width" to it.asString()) - } - maxWidth?.let { - snstyle.add("max-width" to it.asString()) - } - height?.let { - snstyle.add("height" to it.asString()) - } - minHeight?.let { - snstyle.add("min-height" to it.asString()) - } - maxHeight?.let { - snstyle.add("max-height" to it.asString()) - } - display?.let { - snstyle.add("display" to it.display) - } - position?.let { - snstyle.add("position" to it.position) - } - top?.let { - snstyle.add("top" to it.asString()) - } - left?.let { - snstyle.add("left" to it.asString()) - } - right?.let { - snstyle.add("right" to it.asString()) - } - bottom?.let { - snstyle.add("bottom" to it.asString()) - } - zIndex?.let { - snstyle.add("z-index" to it.toString()) - } - overflow?.let { - snstyle.add("overflow" to it.overflow) - } - overflowWrap?.let { - snstyle.add("overflow-wrap" to it.overflowWrap) - } - resize?.let { - snstyle.add("resize" to it.resize) - } - border?.let { - snstyle.add("border" to it.asString()) - } - borderTop?.let { - snstyle.add("border-top" to it.asString()) - } - borderRight?.let { - snstyle.add("border-right" to it.asString()) - } - borderBottom?.let { - snstyle.add("border-bottom" to it.asString()) - } - borderLeft?.let { - snstyle.add("border-left" to it.asString()) - } - margin?.let { - snstyle.add("margin" to it.asString()) - } - marginTop?.let { - snstyle.add("margin-top" to it.asString()) - } - marginRight?.let { - snstyle.add("margin-right" to it.asString()) - } - marginBottom?.let { - snstyle.add("margin-bottom" to it.asString()) - } - marginLeft?.let { - snstyle.add("margin-left" to it.asString()) - } - padding?.let { - snstyle.add("padding" to it.asString()) - } - paddingTop?.let { - snstyle.add("padding-top" to it.asString()) - } - paddingRight?.let { - snstyle.add("padding-right" to it.asString()) - } - paddingBottom?.let { - snstyle.add("padding-bottom" to it.asString()) - } - paddingLeft?.let { - snstyle.add("padding-left" to it.asString()) - } - color?.let { - snstyle.add("color" to it.asString()) - } - opacity?.let { - snstyle.add("opacity" to it.toString()) - } - background?.let { - snstyle.add("background" to it.asString()) - } - textDirection?.let { - snstyle.add("direction" to it.direction) - } - letterSpacing?.let { - snstyle.add("letter-spacing" to it.asString()) - } - lineHeight?.let { - snstyle.add("line-height" to it.asString()) - } - textAlign?.let { - snstyle.add("text-align" to it.textAlign) - } - textDecoration?.let { - snstyle.add("text-decoration" to it.asString()) - } - textIndent?.let { - snstyle.add("text-indent" to it.asString()) - } - textShadow?.let { - snstyle.add("text-shadow" to it.asString()) - } - textTransform?.let { - snstyle.add("text-transform" to it.textTransform) - } - textOverflow?.let { - snstyle.add("text-overflow" to it.textOverflow) - } - unicodeBidi?.let { - snstyle.add("unicode-bidi" to it.unicodeBidi) - } - verticalAlign?.let { - snstyle.add("vartical-align" to it.verticalAlign) - } - whiteSpace?.let { - snstyle.add("white-space" to it.whiteSpace) - } - wordSpacing?.let { - snstyle.add("word-spacing" to it.asString()) - } - fontFamily?.let { - snstyle.add("font-family" to it) - } - fontSize?.let { - snstyle.add("font-size" to it.asString()) - } - fontStyle?.let { - snstyle.add("font-style" to it.fontStyle) - } - fontWeight?.let { - snstyle.add("font-weight" to it.fontWeight) - } - fontVariant?.let { - snstyle.add("font-variant" to it.fontVariant) - } - float?.let { - snstyle.add("float" to it.posFloat) - } - clear?.let { - snstyle.add("clear" to it.clear) - } - wordBreak?.let { - snstyle.add("word-break" to it.wordBreak) - } - lineBreak?.let { - snstyle.add("line-break" to it.lineBreak) + val cacheKey = getCacheKey() + return globalStyleCache[cacheKey] ?: run { + val snstyle = mutableListOf<StringPair>() + width?.let { + snstyle.add("width" to it.asString()) + } + minWidth?.let { + snstyle.add("min-width" to it.asString()) + } + maxWidth?.let { + snstyle.add("max-width" to it.asString()) + } + height?.let { + snstyle.add("height" to it.asString()) + } + minHeight?.let { + snstyle.add("min-height" to it.asString()) + } + maxHeight?.let { + snstyle.add("max-height" to it.asString()) + } + display?.let { + snstyle.add("display" to it.display) + } + position?.let { + snstyle.add("position" to it.position) + } + top?.let { + snstyle.add("top" to it.asString()) + } + left?.let { + snstyle.add("left" to it.asString()) + } + right?.let { + snstyle.add("right" to it.asString()) + } + bottom?.let { + snstyle.add("bottom" to it.asString()) + } + zIndex?.let { + snstyle.add("z-index" to it.toString()) + } + overflow?.let { + snstyle.add("overflow" to it.overflow) + } + overflowWrap?.let { + snstyle.add("overflow-wrap" to it.overflowWrap) + } + resize?.let { + snstyle.add("resize" to it.resize) + } + border?.let { + snstyle.add("border" to it.asString()) + } + borderTop?.let { + snstyle.add("border-top" to it.asString()) + } + borderRight?.let { + snstyle.add("border-right" to it.asString()) + } + borderBottom?.let { + snstyle.add("border-bottom" to it.asString()) + } + borderLeft?.let { + snstyle.add("border-left" to it.asString()) + } + margin?.let { + snstyle.add("margin" to it.asString()) + } + marginTop?.let { + snstyle.add("margin-top" to it.asString()) + } + marginRight?.let { + snstyle.add("margin-right" to it.asString()) + } + marginBottom?.let { + snstyle.add("margin-bottom" to it.asString()) + } + marginLeft?.let { + snstyle.add("margin-left" to it.asString()) + } + padding?.let { + snstyle.add("padding" to it.asString()) + } + paddingTop?.let { + snstyle.add("padding-top" to it.asString()) + } + paddingRight?.let { + snstyle.add("padding-right" to it.asString()) + } + paddingBottom?.let { + snstyle.add("padding-bottom" to it.asString()) + } + paddingLeft?.let { + snstyle.add("padding-left" to it.asString()) + } + color?.let { + snstyle.add("color" to it.asString()) + } + opacity?.let { + snstyle.add("opacity" to it.toString()) + } + background?.let { + snstyle.add("background" to it.asString()) + } + textDirection?.let { + snstyle.add("direction" to it.direction) + } + letterSpacing?.let { + snstyle.add("letter-spacing" to it.asString()) + } + lineHeight?.let { + snstyle.add("line-height" to it.asString()) + } + textAlign?.let { + snstyle.add("text-align" to it.textAlign) + } + textDecoration?.let { + snstyle.add("text-decoration" to it.asString()) + } + textIndent?.let { + snstyle.add("text-indent" to it.asString()) + } + textShadow?.let { + snstyle.add("text-shadow" to it.asString()) + } + textTransform?.let { + snstyle.add("text-transform" to it.textTransform) + } + textOverflow?.let { + snstyle.add("text-overflow" to it.textOverflow) + } + unicodeBidi?.let { + snstyle.add("unicode-bidi" to it.unicodeBidi) + } + verticalAlign?.let { + snstyle.add("vartical-align" to it.verticalAlign) + } + whiteSpace?.let { + snstyle.add("white-space" to it.whiteSpace) + } + wordSpacing?.let { + snstyle.add("word-spacing" to it.asString()) + } + fontFamily?.let { + snstyle.add("font-family" to it) + } + fontSize?.let { + snstyle.add("font-size" to it.asString()) + } + fontStyle?.let { + snstyle.add("font-style" to it.fontStyle) + } + fontWeight?.let { + snstyle.add("font-weight" to it.fontWeight) + } + fontVariant?.let { + snstyle.add("font-variant" to it.fontVariant) + } + float?.let { + snstyle.add("float" to it.posFloat) + } + clear?.let { + snstyle.add("clear" to it.clear) + } + wordBreak?.let { + snstyle.add("word-break" to it.wordBreak) + } + lineBreak?.let { + snstyle.add("line-break" to it.lineBreak) + } + globalStyleCache[cacheKey] = snstyle + return snstyle } - return snstyle } - protected fun <T> refreshOnUpdate(refreshFunction: ((T) -> Unit) = { this.refresh() }) = + protected open fun getCacheKey(): String { + val SEP = "###KvSep###" + return propertyValues.map { + it.toString() + }.joinToString(SEP) + } + + private fun <T> refreshOnUpdate(refreshFunction: ((T) -> Unit) = { this.refresh() }) = RefreshDelegateProvider<T>(null, refreshFunction) - protected fun <T> refreshOnUpdate(initialValue: T, refreshFunction: ((T) -> Unit) = { this.refresh() }) = + private fun <T> refreshOnUpdate(initialValue: T, refreshFunction: ((T) -> Unit) = { this.refresh() }) = RefreshDelegateProvider(initialValue, refreshFunction) - protected inner class RefreshDelegateProvider<T>( + private inner class RefreshDelegateProvider<T>( private val initialValue: T?, private val refreshFunction: (T) -> Unit ) { operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): RefreshDelegate<T> { @@ -498,7 +510,7 @@ abstract class StyledComponent : Component { } } - protected inner class RefreshDelegate<T>(private val refreshFunction: ((T) -> Unit)) { + private inner class RefreshDelegate<T>(private val refreshFunction: ((T) -> Unit)) { @Suppress("UNCHECKED_CAST") operator fun getValue(thisRef: StyledComponent, property: KProperty<*>): T { val value = propertyValues[property.name] @@ -514,4 +526,8 @@ abstract class StyledComponent : Component { refreshFunction(value) } } + + companion object { + internal val globalStyleCache = Cache<String, List<StringPair>>() + } } diff --git a/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt b/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt index 9bf84209..f0be38cb 100644 --- a/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt +++ b/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt @@ -42,6 +42,7 @@ import pl.treksoft.kvision.utils.snAttrs import pl.treksoft.kvision.utils.snClasses import pl.treksoft.kvision.utils.snOpt import pl.treksoft.kvision.utils.snStyle +import kotlin.reflect.KProperty /** * Base widget class. The parent of all component classes. @@ -53,6 +54,7 @@ import pl.treksoft.kvision.utils.snStyle */ @Suppress("TooManyFunctions", "LargeClass") open class Widget(classes: Set<String> = setOf()) : StyledComponent() { + private val propertyValues: MutableMap<String, Any?> = mutableMapOf() internal val classes = classes.toMutableSet() internal val surroundingClasses: MutableSet<String> = mutableSetOf() @@ -765,6 +767,38 @@ open class Widget(classes: Set<String> = setOf()) : StyledComponent() { override fun dispose() { } + protected fun <T> refreshOnUpdate(refreshFunction: ((T) -> Unit) = { this.refresh() }) = + RefreshDelegateProvider<T>(null, refreshFunction) + + protected fun <T> refreshOnUpdate(initialValue: T, refreshFunction: ((T) -> Unit) = { this.refresh() }) = + RefreshDelegateProvider(initialValue, refreshFunction) + + protected inner class RefreshDelegateProvider<T>( + private val initialValue: T?, private val refreshFunction: (T) -> Unit + ) { + operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): RefreshDelegate<T> { + if (initialValue != null) propertyValues[prop.name] = initialValue + return RefreshDelegate(refreshFunction) + } + } + + protected inner class RefreshDelegate<T>(private val refreshFunction: ((T) -> Unit)) { + @Suppress("UNCHECKED_CAST") + operator fun getValue(thisRef: StyledComponent, property: KProperty<*>): T { + val value = propertyValues[property.name] + return if (value != null) { + value as T + } else { + null as T + } + } + + operator fun setValue(thisRef: StyledComponent, property: KProperty<*>, value: T) { + propertyValues[property.name] = value + refreshFunction(value) + } + } + companion object { /** * DSL builder extension function. diff --git a/src/main/kotlin/pl/treksoft/kvision/utils/Cache.kt b/src/main/kotlin/pl/treksoft/kvision/utils/Cache.kt new file mode 100644 index 00000000..6a473661 --- /dev/null +++ b/src/main/kotlin/pl/treksoft/kvision/utils/Cache.kt @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package pl.treksoft.kvision.utils + +/* + * Copyright 2018 Nazmul Idris All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This is a LRU cache that has no performance impact for cache insertions + * once the capacity of the cache has been reached. For cache hit, + * performance is O(1) and for cache eviction, it is O(1). + */ +class Cache<K, V>(private val capacity: Int = 50) { + private val cache = HashMap<K, V>() + private val insertionOrder = LinkedList<K>() + + /** + * [HashMap] put and remove is O(1). + * More info: https://stackoverflow.com/a/4578039/2085356 + */ + operator fun set(key: K, value: V): K? { + var evictedKey: K? = null + if (cache.size >= capacity) { + evictedKey = getKeyToEvict() + cache.remove(evictedKey) + } + cache[key] = value + insertionOrder.append(key) + return evictedKey + } + + /** + * [HashMap] get is O(1). + * More info: https://stackoverflow.com/a/4578039/2085356 + */ + operator fun get(key: K): V? = cache[key] + + /** + * The head of the [insertionOrder] is removed, which is O(1), since this + * is a linked list, and it's inexpensive to remove an item from head. + * More info: https://stackoverflow.com/a/42849573/2085356 + */ + private fun getKeyToEvict(): K? = insertionOrder.removeAtIndex(0) + + override fun toString() = cache.toString() +} + +/** + * Created by gazollajunior on 07/04/16. + */ +class Node<T>(var value: T) { + var next: Node<T>? = null + var previous: Node<T>? = null +} + +class LinkedList<T> { + + private var head: Node<T>? = null + + var isEmpty: Boolean = head == null + + fun first(): Node<T>? = head + + fun last(): Node<T>? { + var node = head + if (node != null) { + while (node?.next != null) { + node = node.next + } + return node + } else { + return null + } + } + + fun count(): Int { + var node = head + if (node != null) { + var counter = 1 + while (node?.next != null) { + node = node.next + counter += 1 + } + return counter + } else { + return 0 + } + } + + fun nodeAtIndex(index: Int): Node<T>? { + if (index >= 0) { + var node = head + var i = index + while (node != null) { + if (i == 0) return node + i -= 1 + node = node.next + } + } + return null + } + + fun append(value: T) { + val newNode = Node(value) + + val lastNode = this.last() + if (lastNode != null) { + newNode.previous = lastNode + lastNode.next = newNode + } else { + head = newNode + } + } + + fun removeAll() { + head = null + } + + fun removeNode(node: Node<T>): T { + val prev = node.previous + val next = node.next + + if (prev != null) { + prev.next = next + } else { + head = next + } + next?.previous = prev + + node.previous = null + node.next = null + + return node.value + } + + fun removeLast(): T? { + val last = this.last() + if (last != null) { + return removeNode(last) + } else { + return null + } + } + + fun removeAtIndex(index: Int): T? { + val node = nodeAtIndex(index) + if (node != null) { + return removeNode(node) + } else { + return null + } + } + + override fun toString(): String { + var s = "[" + var node = head + while (node != null) { + s += "${node.value}" + node = node.next + if (node != null) { + s += ", " + } + } + return s + "]" + } +} |