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

Superscript for JFX via tspan and baseline-shift. Add superscript demo to DemoModelA.kt #945

Merged
merged 2 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Superscript for JFX via tspan and baseline-shift. Add superscript dem…
…o to DemoModelA.kt
  • Loading branch information
IKupriyanov-HORIS committed Nov 24, 2023
commit cbba4e866cdb6441b38c9dc521f0d3f5be8f9216
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

package demo.plot.jfx.component

import javafx.application.Platform.runLater
import demo.plot.shared.model.component.TooltipBoxDemo
import demo.common.jfx.demoUtils.SvgViewerDemoWindowJfx
import demo.plot.shared.model.component.TooltipBoxDemo
import javafx.application.Platform.runLater
import org.jetbrains.letsPlot.jfx.util.runOnFxThread
import java.awt.EventQueue.invokeLater

Expand All @@ -23,7 +23,7 @@ fun main() {
runOnFxThread {
invokeLater {
runLater {
models.forEach { it.second() }
models.forEach { runLater { it.second() } }
}
}
}
Expand Down
22 changes: 18 additions & 4 deletions demo/svg/src/commonMain/kotlin/demo/svgMapping/model/DemoModelA.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import org.jetbrains.letsPlot.commons.geometry.DoubleVector
import org.jetbrains.letsPlot.commons.values.Color
import org.jetbrains.letsPlot.commons.values.FontFace
import org.jetbrains.letsPlot.commons.values.FontFamily
import org.jetbrains.letsPlot.datamodel.svg.style.StyleSheet
import org.jetbrains.letsPlot.datamodel.svg.style.TextStyle
import org.jetbrains.letsPlot.datamodel.svg.dom.*
import org.jetbrains.letsPlot.datamodel.svg.dom.SvgConstants.SVG_STROKE_DASHARRAY_ATTRIBUTE
import org.jetbrains.letsPlot.datamodel.svg.dom.slim.SvgSlimElements
import org.jetbrains.letsPlot.datamodel.svg.dom.*
import org.jetbrains.letsPlot.datamodel.svg.style.StyleSheet
import org.jetbrains.letsPlot.datamodel.svg.style.TextStyle

object DemoModelA {

Expand All @@ -34,10 +34,24 @@ object DemoModelA {
face = FontFace.BOLD,
size = 20.0,
color = Color.RED
)
),
"EMC2" to TextStyle(FontFamily.HELVETICA.name, face = FontFace.BOLD, size = 22.0, color = Color.BLUE),

)
svgRoot.children().add(createStyleElement(textStyles))

svgRoot.children().add(
SvgTextElement().apply {
addClass("EMC2")
transform().set(SvgTransformBuilder().translate(300.0, 150.0).build())
addTSpan(SvgTSpanElement("E=mc"))
addTSpan(SvgTSpanElement("2").apply {
setAttribute("baseline-shift", "super")
setAttribute("font-size", "75%")
})
}
)

var text = SvgTextElement(30.0, 85.0, "Slim elements")
text.addClass("TEXT1")
SvgUtils.transformRotate(text, -45.0, 20.0, 100.0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ package org.jetbrains.letsPlot.jfx.mapping.svg
import javafx.scene.Group
import javafx.scene.Node
import javafx.scene.image.ImageView
import javafx.scene.text.Text
import org.jetbrains.letsPlot.awt.util.RGBEncoderAwt
import org.jetbrains.letsPlot.datamodel.mapping.framework.Mapper
import org.jetbrains.letsPlot.datamodel.mapping.framework.MapperFactory
import org.jetbrains.letsPlot.datamodel.svg.dom.*
import org.jetbrains.letsPlot.awt.util.RGBEncoderAwt

internal class SvgNodeMapperFactory(private val peer: SvgJfxPeer) : MapperFactory<SvgNode, Node> {

Expand All @@ -39,7 +38,7 @@ internal class SvgNodeMapperFactory(private val peer: SvgJfxPeer) : MapperFactor
is SvgStyleElement -> SvgStyleElementMapper(src, target as Group, peer)
is SvgGElement -> SvgGElementMapper(src, target as Group, peer)
is SvgSvgElement -> SvgSvgElementMapper(src, peer)
is SvgTextElement -> SvgTextElementMapper(src, target as Text, peer)
is SvgTextElement -> SvgTextElementMapper(src, target as TextLine, peer)
// is SvgTextNode -> result = SvgTextNodeMapper(src, target as Text, myDoc, peer)
is SvgImageElement -> SvgImageElementMapper(src, target as ImageView, peer)
is SvgElement -> SvgElementMapper(src, target, peer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,27 @@

package org.jetbrains.letsPlot.jfx.mapping.svg

import javafx.beans.value.ChangeListener
import javafx.beans.value.ObservableValue
import javafx.geometry.Bounds
import javafx.scene.paint.Color
import javafx.scene.text.Font
import javafx.scene.text.FontPosture
import javafx.scene.text.FontWeight
import javafx.scene.text.Text
import org.jetbrains.letsPlot.commons.intern.observable.collections.ObservableCollection
import org.jetbrains.letsPlot.commons.intern.observable.property.ReadableProperty
import org.jetbrains.letsPlot.commons.intern.observable.property.SimpleCollectionProperty
import org.jetbrains.letsPlot.commons.intern.observable.property.WritableProperty
import org.jetbrains.letsPlot.datamodel.mapping.framework.Synchronizers
import org.jetbrains.letsPlot.datamodel.svg.dom.SvgNode
import org.jetbrains.letsPlot.datamodel.svg.dom.SvgTSpanElement
import org.jetbrains.letsPlot.datamodel.svg.dom.SvgTextElement
import org.jetbrains.letsPlot.datamodel.svg.dom.SvgTextNode
import org.jetbrains.letsPlot.datamodel.svg.style.StyleSheet
import org.jetbrains.letsPlot.datamodel.svg.style.TextStyle
import org.jetbrains.letsPlot.datamodel.svg.dom.SvgConstants.SVG_STYLE_ATTRIBUTE
import org.jetbrains.letsPlot.jfx.mapping.svg.attr.SvgTextElementAttrMapping
import org.jetbrains.letsPlot.datamodel.svg.dom.*


internal class SvgTextElementMapper(
source: SvgTextElement,
target: Text,
target: TextLine,
peer: SvgJfxPeer
) : SvgElementMapper<SvgTextElement, Text>(source, target, peer) {

private val myTextAttrSupport = TextAttributesSupport(target)

override fun setTargetAttribute(name: String, value: Any?) {
// println("text -> $name = $value")
// println("text -> ${target.font}")
// val def = Font.getDefault()
// println("text -> ${def.family}")
// val font = Font.font(def.family, FontPosture.ITALIC, def.size)
// val font = Font.font("Times New Roman", FontPosture.ITALIC, def.size)
// target.font = font
// println("text -> ${target.font}")
// Font.getFamilies().forEach { println(it) }
// Font.getFontNames().forEach { println(it) }
// throw RuntimeException("The End")


myTextAttrSupport.setAttribute(name, value)
}
) : SvgElementMapper<SvgTextElement, TextLine>(source, target, peer) {

override fun applyStyle() {
setFontProperties(target, peer.styleSheet)
Expand All @@ -56,93 +35,96 @@ internal class SvgTextElementMapper(
super.registerSynchronizers(conf)

// Sync TextNodes, TextSpans
val sourceTextProperty = sourceTextProperty(source.children())
// sourceTextProperty.addHandler(object : EventHandler<PropertyChangeEvent<out String>> {
// override fun onEvent(event: PropertyChangeEvent<out String>) {
// println("new text: ${event.newValue}")
// }
// })
val sourceTextRunProperty = sourceTextRunProperty(source.children())
val targetTextRunProperty = targetTextRunProperty(target)
conf.add(
Synchronizers.forPropsOneWay(
sourceTextProperty,
targetTextProperty(target)
sourceTextRunProperty,
targetTextRunProperty
)
)
}

private fun setFontProperties(target: Text, styleSheet: StyleSheet?) {
private fun setFontProperties(target: TextLine, styleSheet: StyleSheet?) {
if (styleSheet == null) {
return
}

// Note: in SVG style and CSS have higher priority than attributes.
// We don't support this behavior because right now we never set
// font and fill properties via attributes and style at the same time.
val className = source.fullClass()
if (className.isNotEmpty()) {
val style = styleSheet.getTextStyle(className)
target.font = style.createFont()
myTextAttrSupport.setAttribute(SVG_STYLE_ATTRIBUTE, "fill:${style.color.toHexColor()};")

val posture = if (style.face.italic) FontPosture.ITALIC else null
val weight = if (style.face.bold) FontWeight.BOLD else null
val familyList = style.family.split(",").map { it.trim(' ', '\"') }
val family = familyList
.map { Font.font(it, weight, posture, style.size) }
.firstOrNull {
(if (style.face.italic) it.style.contains("italic", ignoreCase = true) else true) &&
(if (style.face.bold) it.style.contains("bold", ignoreCase = true) else true)
}?.family
?: familyList.firstOrNull()

target.fontFamily = family
target.fontWeight = weight
target.fontPosture = posture
target.fontSize = style.size
target.fill = Color.web(style.color.toHexColor())
}
}

companion object {
private fun sourceTextProperty(nodes: ObservableCollection<SvgNode>): ReadableProperty<String> {
return object : SimpleCollectionProperty<SvgNode, String>(nodes, joinToString(nodes)) {
override val propExpr = "joinToString($collection)"
override fun doGet() = joinToString(collection)
}
}

private fun joinToString(nodes: ObservableCollection<SvgNode>): String {
return nodes.asSequence()
.flatMap{((it as? SvgTSpanElement)?.children() ?: listOf(it as SvgTextNode)).asSequence()}
.joinToString ("\n") { (it as SvgTextNode).textContent().get() }
}

private fun targetTextProperty(target: Text): WritableProperty<String?> {
return object : WritableProperty<String?> {
override fun set(value: String?) {
target.text = value ?: "n/a"
private fun sourceTextRunProperty(nodes: ObservableCollection<SvgNode>): ReadableProperty<List<TextLine.TextRun>> {
fun textRuns(nodes: ObservableCollection<SvgNode>): List<TextLine.TextRun> {
return nodes.flatMap { node ->
val nodeTextRuns = when (node) {
is SvgTextNode -> listOf(TextLine.TextRun(node.textContent().get()))
is SvgTSpanElement -> node.children().map { child ->
require(child is SvgTextNode)
val fontScale = node.getAttribute("font-size").get()?.let {
require(it is String) { "font-size: only string value is supported" }
require(it.endsWith("%")) { "font-size: only percent value is supported" }
it.removeSuffix("%").toFloat() / 100.0
}

// TODO: replace with Specs from LP
val baselineShift = node.getAttribute("baseline-shift").get()?.let {
when (it) {
"sub" -> TextLine.BaselineShift.SUB
"super" -> TextLine.BaselineShift.SUPER
else -> error("Unexpected baseline-shift value: $it")
}
}

TextLine.TextRun(
text = child.textContent().get(),
baselineShift = baselineShift,
fontScale = fontScale
)
}

else -> error("Unexpected node type: ${node::class.simpleName}")
}

nodeTextRuns
}
}
}

private fun TextStyle.createFont(): Font {
val posture = if (face.italic) FontPosture.ITALIC else null
val weight = if (face.bold) FontWeight.BOLD else null
// todo Need to choose an available font:
// 'TextStyle.family' string may contain a comma-separated list of families
val familyList = family.toString().split(",").map { it.trim(' ', '\"') }
return familyList
.map { Font.font(it, weight, posture, size) }
.firstOrNull {
// todo choose a font with a supported style ('bold' or/and 'italic')
(if (face.italic) it.style.contains("italic", ignoreCase = true) else true) &&
(if (face.bold) it.style.contains("bold", ignoreCase = true) else true)
}
?: Font.font(
familyList.firstOrNull(),
weight,
posture,
size
)
return object : SimpleCollectionProperty<SvgNode, List<TextLine.TextRun>>(nodes, textRuns(nodes)) {
override val propExpr = "textRuns($collection)"
override fun doGet() = textRuns(collection)
}
}
}

private class TextAttributesSupport(val target: Text) {
private var mySvgTextAnchor: String? = null

init {
@Suppress("ObjectLiteralToLambda")
target.boundsInLocalProperty().addListener(object : ChangeListener<Bounds> {
override fun changed(observable: ObservableValue<out Bounds>?, oldValue: Bounds?, newValue: Bounds?) {
SvgTextElementAttrMapping.revalidatePositionAttributes(mySvgTextAnchor, target)
private fun targetTextRunProperty(target: TextLine): WritableProperty<List<TextLine.TextRun>?> {
return object : WritableProperty<List<TextLine.TextRun>?> {
override fun set(value: List<TextLine.TextRun>?) {
target.content = value ?: emptyList()
}
})
}

fun setAttribute(name: String, value: Any?) {
if (name == SvgTextContent.TEXT_ANCHOR.name) {
mySvgTextAnchor = value as String?
}
SvgTextElementAttrMapping.setAttribute(target, name, value)
}
}
}
}
Loading