diff --git a/Readme.md b/Readme.md index 6a483b66..bf3371d0 100644 --- a/Readme.md +++ b/Readme.md @@ -147,28 +147,26 @@ the state in the client scripts. # Mutability -terminal21 ui components are mutable. This is a decision choice (for now) because of how much more simple code is this way. I.e. -changing the text of a paragraph on an event handler is as simple as : +terminal21 ui components are immutable from v0.20. Use `component.withX(...).renderChanges()` to modify a component +and render it. Note that the original `component` is not changed. -```scala - p.text = "new text" -``` - -The equivalent immutable code would be (at least) -```scala - p.copy(text= "new text") -``` - -Also by default some component values (like input boxes) are changed by the user. These changes are reflected in the component graph, something that -would be a lot harder if the graph was immutable. - -If there is a reasonable way to refactor to have immutability without compromising simplicity, it will be done. +Also when getting a value of i.e. an Input, use `myInput.current.value`. `current` makes sure we read the component with +all changes that may have occurred at the browser and all the changes we did on our script. # Need help? Please use the [discussions](https://github.com/kostaskougios/terminal21-restapi/discussions) of the project to post any questions, comments or ideas. # Changelog +## Version 0.20 + +- immutable components +- option to render only changed components + +## Version 0.12 + +- ability to render only 1 element and it's children +- refactoring internal way of storing elements on lib - server - ui ## Version 0.11 diff --git a/build.sbt b/build.sbt index f9bf9122..03eee7fb 100644 --- a/build.sbt +++ b/build.sbt @@ -1,9 +1,11 @@ +import sbt.librarymanagement.ModuleFilter + /** This build has different sections for each integration. I.e. an http4s section and a kafka section. These sections are not related to each other, please * examine the section you're interested in. */ val scala3Version = "3.3.1" -ThisBuild / version := "0.11" +ThisBuild / version := "0.20" ThisBuild / organization := "io.github.kostaskougios" name := "rest-api" ThisBuild / scalaVersion := scala3Version @@ -25,8 +27,11 @@ val FunctionsHelidonClient = "io.github.kostaskougios" %% "helidon-client" val FunctionsHelidonWsClient = "io.github.kostaskougios" %% "helidon-ws-client" % FunctionsVersion val FunctionsFibers = "io.github.kostaskougios" %% "fibers" % FunctionsVersion -val ScalaTest = "org.scalatest" %% "scalatest" % "3.2.15" % Test -val Mockito = "org.mockito" % "mockito-all" % "2.0.2-beta" % Test +val ScalaTest = "org.scalatest" %% "scalatest" % "3.2.15" % Test +val Mockito = "org.mockito" % "mockito-all" % "2.0.2-beta" % Test +val Scala3Tasty = "org.scala-lang" %% "scala3-tasty-inspector" % scala3Version +val CommonsText = "org.apache.commons" % "commons-text" % "1.10.0" +val CommonsIO = "commons-io" % "commons-io" % "2.11.0" val CirceVersion = "0.14.6" val Circe = Seq( @@ -94,13 +99,13 @@ lazy val `terminal21-server` = project LogBack ) ++ Circe ) - .dependsOn(`terminal21-ui-std-exports`, `terminal21-server-client-common`) + .dependsOn(`terminal21-ui-std-exports` % "compile->compile;test->test", `terminal21-server-client-common`) .enablePlugins(FunctionsRemotePlugin) lazy val `terminal21-ui-std-exports` = project .settings( commonSettings, - libraryDependencies ++= Seq(ScalaTest), + libraryDependencies ++= Seq(ScalaTest) ++ Circe, // make sure exportedArtifact points to the full artifact name of the receiver. buildInfoKeys := Seq[BuildInfoKey](organization, name, version, scalaVersion, "exportedArtifact" -> "none"), buildInfoPackage := "org.terminal21.ui.std" @@ -176,3 +181,14 @@ lazy val `terminal21-mathjax` = project ) ) .dependsOn(`terminal21-ui-std` % "compile->compile;test->test") + +lazy val `terminal21-code-generation`: Project = project + .settings( + commonSettings, + libraryDependencies ++= Seq( + ScalaTest, + Scala3Tasty, + CommonsText, + CommonsIO + ) + ) diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala index 5940e29e..6bdebe53 100644 --- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala @@ -2,7 +2,8 @@ package tests import org.terminal21.client.* import org.terminal21.client.components.chakra.* -import org.terminal21.client.components.{Paragraph, render} +import org.terminal21.client.components.render +import org.terminal21.client.components.std.Paragraph import tests.chakra.* import java.util.concurrent.CountDownLatch diff --git a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala index c34fe57d..731e437a 100644 --- a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala @@ -19,4 +19,4 @@ import org.terminal21.client.components.mathjax.* style = Map("backgroundColor" -> "gray") ) ).render() - session.waitTillUserClosesSession() + session.leaveSessionOpenAfterExiting() diff --git a/end-to-end-tests/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala index 7f633f7b..67c870ea 100644 --- a/end-to-end-tests/src/main/scala/tests/StdComponents.scala +++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala @@ -2,16 +2,16 @@ package tests import org.terminal21.client.* import org.terminal21.client.components.* +import org.terminal21.client.components.std.* @main def stdComponents(): Unit = Sessions.withNewSession("std-components", "Std Components"): session => given ConnectedSession = session - val input = Input(defaultValue = "Please enter your name") + val input = Input(defaultValue = Some("Please enter your name")) val output = Paragraph(text = "This will reflect what you type in the input") input.onChange: newValue => - output.text = newValue - session.render() + output.withText(newValue).renderChanges() Seq( Header1(text = "Welcome to the std components demo/test"), diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala b/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala index 8c112cea..7e3674e1 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala @@ -1,8 +1,8 @@ package tests.chakra import org.terminal21.client.ConnectedSession -import org.terminal21.client.components.UiElement -import org.terminal21.client.components.chakra.{Box, Button} +import org.terminal21.client.components.* +import org.terminal21.client.components.chakra.* import tests.chakra.Common.* import java.util.concurrent.CountDownLatch @@ -14,10 +14,10 @@ object Buttons: Seq( box1, exitButton.onClick: () => - box1.text = "Exit Clicked!" - exitButton.text = "Stopping..." - exitButton.colorScheme = Some("green") - session.render() + Seq( + box1.withText("Exit Clicked!"), + exitButton.withText("Stopping...").withColorScheme(Some("green")) + ).renderChanges() Thread.sleep(1000) latch.countDown() ) diff --git a/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala b/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala index 4265da5a..320e493d 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala @@ -12,6 +12,15 @@ object DataDisplay: Th(text = "into"), Th(text = "multiply by", isNumeric = true) ) + val quickTable1 = QuickTable() + .headers("id", "name") + .caption("Quick Table Caption") + .rows( + Seq( + Seq(1, "Kostas"), + Seq(2, "Andreas") + ) + ) Seq( commonBox(text = "Badges"), HStack().withChildren( @@ -23,6 +32,8 @@ object DataDisplay: Button(text = "test") ) ), + commonBox(text = "Quick Tables"), + quickTable1, commonBox(text = "Tables"), TableContainer().withChildren( Table(variant = "striped", colorScheme = Some("teal"), size = "lg").withChildren( diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala index c9dd0a5a..505f5edf 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala @@ -15,16 +15,14 @@ object Editables: ) editable1.onChange: newValue => - status.text = s"editable1 newValue = $newValue, verify editable1.value = ${editable1.value}" - session.render() + status.withText(s"editable1 newValue = $newValue, verify editable1.value = ${editable1.current.value}").renderChanges() val editable2 = Editable(defaultValue = "For longer maybe-editable texts\nUse an EditableTextarea\nIt uses a textarea control.").withChildren( EditablePreview(), EditableTextarea() ) editable2.onChange: newValue => - status.text = s"editable2 newValue = $newValue, verify editable2.value = ${editable2.value}" - session.render() + status.withText(s"editable2 newValue = $newValue, verify editable2.value = ${editable2.current.value}").renderChanges() Seq( commonBox(text = "Editables"), diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala index 21e6ce13..f693b29c 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala @@ -1,7 +1,7 @@ package tests.chakra import org.terminal21.client.ConnectedSession -import org.terminal21.client.components.UiElement +import org.terminal21.client.components.* import org.terminal21.client.components.chakra.* import tests.chakra.Common.* @@ -15,14 +15,14 @@ object Forms: val email = Input(`type` = "email", value = "my@email.com") email.onChange: newValue => - status.text = s"email input new value = $newValue, verify email.value = ${email.value}" - if newValue.contains("@") then emailRightAddOn.children = Seq(okIcon) else emailRightAddOn.children = Seq(notOkIcon) - session.render() + Seq( + status.withText(s"email input new value = $newValue, verify email.value = ${email.current.value}"), + if newValue.contains("@") then emailRightAddOn.withChildren(okIcon) else emailRightAddOn.withChildren(notOkIcon) + ).renderChanges() val description = Textarea(placeholder = "Please enter a few things about you") description.onChange: newValue => - status.text = s"description input new value = $newValue, verify description.value = ${description.value}" - session.render() + status.withText(s"description input new value = $newValue, verify description.value = ${description.current.value}").renderChanges() val select1 = Select(placeholder = "Please choose").withChildren( Option_(text = "Male", value = "male"), @@ -30,8 +30,7 @@ object Forms: ) select1.onChange: newValue => - status.text = s"select1 input new value = $newValue, verify select1.value = ${select1.value}" - session.render() + status.withText(s"select1 input new value = $newValue, verify select1.value = ${select1.current.value}").renderChanges() val select2 = Select(value = "1", bg = Some("tomato"), color = Some("black"), borderColor = Some("yellow")).withChildren( Option_(text = "First", value = "1"), @@ -41,33 +40,32 @@ object Forms: val password = Input(`type` = "password", value = "mysecret") val dob = Input(`type` = "datetime-local") dob.onChange: newValue => - status.text = s"dob = $newValue , verify dob.value = ${dob.value}" - session.render() + status.withText(s"dob = $newValue , verify dob.value = ${dob.current.value}").renderChanges() val color = Input(`type` = "color") color.onChange: newValue => - status.text = s"color = $newValue , verify color.value = ${color.value}" - session.render() + status.withText(s"color = $newValue , verify color.value = ${color.current.value}").renderChanges() val checkbox2 = Checkbox(text = "Check 2", defaultChecked = true) checkbox2.onChange: newValue => - status.text = s"checkbox2 checked is $newValue , verify checkbox2.checked = ${checkbox2.checked}" - session.render() + status.withText(s"checkbox2 checked is $newValue , verify checkbox2.checked = ${checkbox2.current.checked}").renderChanges() val checkbox1 = Checkbox(text = "Check 1") checkbox1.onChange: newValue => - checkbox2.isDisabled = newValue - status.text = s"checkbox1 checked is $newValue , verify checkbox1.checked = ${checkbox1.checked}" - session.render() + Seq( + status.withText(s"checkbox1 checked is $newValue , verify checkbox1.checked = ${checkbox1.current.checked}"), + checkbox2.withIsDisabled(newValue) + ).renderChanges() val switch1 = Switch(text = "Switch 1") - val switch2 = Switch(text = "Switch 2") + val switch2 = Switch(text = "Switch 2", defaultChecked = true) switch1.onChange: newValue => - switch2.isDisabled = newValue - status.text = s"switch1 checked is $newValue , verify switch1.checked = ${switch1.checked}" - session.render() + Seq( + status.withText(s"switch1 checked is $newValue , verify switch1.checked = ${switch1.current.checked}"), + switch2.withIsDisabled(newValue) + ).renderChanges() val radioGroup = RadioGroup(defaultValue = "2").withChildren( HStack().withChildren( @@ -78,8 +76,7 @@ object Forms: ) radioGroup.onChange: newValue => - status.text = s"radioGroup newValue=$newValue , verify radioGroup.value=${radioGroup.value}" - session.render() + status.withText(s"radioGroup newValue=$newValue , verify radioGroup.value=${radioGroup.current.value}").renderChanges() Seq( commonBox(text = "Forms"), @@ -137,14 +134,14 @@ object Forms: ButtonGroup(variant = Some("outline"), spacing = Some("24")).withChildren( Button(text = "Save", colorScheme = Some("red")) .onClick: () => - status.text = - s"Saved clicked. Email = ${email.value}, password = ${password.value}, dob = ${dob.value}, check1 = ${checkbox1.checked}, check2 = ${checkbox2.checked}, radio = ${radioGroup.value}" - session.render() - , + status + .withText( + s"Saved clicked. Email = ${email.current.value}, password = ${password.current.value}, dob = ${dob.current.value}, check1 = ${checkbox1.current.checked}, check2 = ${checkbox2.current.checked}, radio = ${radioGroup.current.value}, switch1 = ${switch1.current.checked}, switch2 = ${switch2.current.checked}" + ) + .renderChanges(), Button(text = "Cancel") .onClick: () => - status.text = "Cancel clicked" - session.render() + status.withText("Cancel clicked").renderChanges() ), radioGroup, status diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala index 75e672cb..bed13a35 100644 --- a/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala +++ b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala @@ -18,21 +18,14 @@ object Overlay: MenuList().withChildren( MenuItem(text = "Download menu-download") .onClick: () => - box1.text = "'Download' clicked" - session.render() - , + box1.withText("'Download' clicked").renderChanges(), MenuItem(text = "Copy").onClick: () => - box1.text = "'Copy' clicked" - session.render() - , + box1.withText("'Copy' clicked").renderChanges(), MenuItem(text = "Paste").onClick: () => - box1.text = "'Paste' clicked" - session.render() - , + box1.withText("'Paste' clicked").renderChanges(), MenuDivider(), MenuItem(text = "Exit").onClick: () => - box1.text = "'Exit' clicked" - session.render() + box1.withText("'Exit' clicked").renderChanges() ) ), box1 diff --git a/example-scripts/bouncing-ball.sc b/example-scripts/bouncing-ball.sc index 108e3364..abd85ce1 100755 --- a/example-scripts/bouncing-ball.sc +++ b/example-scripts/bouncing-ball.sc @@ -20,13 +20,10 @@ Sessions.withNewSession("bouncing-ball", "C64 bouncing ball"): session => // Files under ~/.terminal21/web will be served under /web . Please place a ball.png file under ~/.terminal21/web/images on the box where the server runs. val ball = Image(src = "/web/images/ball.png") - Seq( - ball - ).render() + ball.render() @tailrec def animateBall(x: Int, y: Int, dx: Int, dy: Int): Unit = - ball.style = Map("position" -> "fixed", "left" -> (x + "px"), "top" -> (y + "px")) - session.render() + ball.withStyle("position" -> "fixed", "left" -> (x + "px"), "top" -> (y + "px")).renderChanges() Thread.sleep(1000 / 120) val newDx = if x < 0 || x > 600 then -dx else dx val newDy = if y < 0 || y > 500 then -dy else dy diff --git a/example-scripts/csv-editor.sc b/example-scripts/csv-editor.sc index 7e7c3deb..79a01e0e 100755 --- a/example-scripts/csv-editor.sc +++ b/example-scripts/csv-editor.sc @@ -6,6 +6,8 @@ // always import these import org.terminal21.client.* + +import java.util.concurrent.atomic.AtomicBoolean // std components, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala import org.terminal21.client.components.* // use the chakra components for menus, forms etc, https://chakra-ui.com/docs/components @@ -14,7 +16,6 @@ import org.terminal21.client.components.chakra.* import org.apache.commons.io.FileUtils import java.io.File -import java.util.concurrent.CountDownLatch import scala.collection.concurrent.TrieMap if args.length != 1 then @@ -55,7 +56,7 @@ def saveCsvMap() = FileUtils.writeStringToFile(file, s, "UTF-8") // this will be countDown to 0 when we have to exit -val exitLatch = new CountDownLatch(1) +val exitFlag = new AtomicBoolean(false) Sessions.withNewSession(s"csv-editor-$fileName", s"CsvEdit: $fileName"): session => given ConnectedSession = session @@ -64,13 +65,12 @@ Sessions.withNewSession(s"csv-editor-$fileName", s"CsvEdit: $fileName"): session val saveAndExit = Button(text = "Save & Exit") .onClick: () => saveCsvMap() - session.add(Paragraph(text = "Csv file saved, exiting.")) - session.render() - exitLatch.countDown() + status.withText("Csv file saved, exiting.").renderChanges() + exitFlag.set(true) val exit = Button(text = "Exit Without Saving") .onClick: () => - exitLatch.countDown() + exitFlag.set(true) def newEditable(x: Int, y: Int, value: String) = Editable(defaultValue = value) @@ -80,8 +80,7 @@ Sessions.withNewSession(s"csv-editor-$fileName", s"CsvEdit: $fileName"): session ) .onChange: newValue => csvMap((x, y)) = newValue - status.text = s"($x,$y) value changed to $newValue" - session.render() + status.withText(s"($x,$y) value changed to $newValue").renderChanges() Seq( TableContainer().withChildren( @@ -107,4 +106,5 @@ Sessions.withNewSession(s"csv-editor-$fileName", s"CsvEdit: $fileName"): session println(s"Now open ${session.uiUrl} to view the UI") // wait for one of the save/exit buttons to be pressed. - exitLatch.await() + session.waitTillUserClosesSessionOr(exitFlag.get()) + diff --git a/example-scripts/csv-viewer.sc b/example-scripts/csv-viewer.sc index 2ecd5a2d..85fbcfcc 100755 --- a/example-scripts/csv-viewer.sc +++ b/example-scripts/csv-viewer.sc @@ -23,7 +23,7 @@ if args.length != 1 then ) val fileName = args(0) -val file = new File(fileName) +val file = new File(fileName) val contents = FileUtils.readFileToString(file, "UTF-8") val csv = contents.split("\n").map(_.split(",")) @@ -31,12 +31,11 @@ val csv = contents.split("\n").map(_.split(",")) Sessions.withNewSession(s"csv-viewer-$fileName", s"CsvView: $fileName"): session => given ConnectedSession = session - Seq( - TableContainer().withChildren( + TableContainer() + .withChildren( Table(variant = "striped", colorScheme = Some("teal"), size = "mg") .withChildren( TableCaption(text = "Csv file contents"), - Thead(), Tbody( children = csv.map: row => Tr( @@ -45,7 +44,7 @@ Sessions.withNewSession(s"csv-viewer-$fileName", s"CsvView: $fileName"): session ) ) ) - ) - ).render() + ).render() println(s"Now open ${session.uiUrl} to view the UI.") - session.waitTillUserClosesSession() + // since this is a read-only UI, we can exit the app but leave the session open on the UI for the user to examine the data. + session.leaveSessionOpenAfterExiting() diff --git a/example-scripts/hello-world.sc b/example-scripts/hello-world.sc index b6b7aa34..e314267f 100755 --- a/example-scripts/hello-world.sc +++ b/example-scripts/hello-world.sc @@ -7,11 +7,10 @@ import org.terminal21.client.* // std components, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala import org.terminal21.client.components.* +import org.terminal21.client.components.std.* Sessions.withNewSession("hello-world", "Hello World Example"): session => given ConnectedSession = session - Seq( - Paragraph(text = "Hello World!") - ).render() - session.waitTillUserClosesSession() + Paragraph(text = "Hello World!").render() + session.leaveSessionOpenAfterExiting() diff --git a/example-scripts/mathjax.sc b/example-scripts/mathjax.sc index 902ff2e2..30c2ea72 100755 --- a/example-scripts/mathjax.sc +++ b/example-scripts/mathjax.sc @@ -23,4 +23,4 @@ Sessions.withNewSession("mathjax", "MathJax Example", MathJaxLib /* note we need |""".stripMargin ) ).render() - session.waitTillUserClosesSession() + session.leaveSessionOpenAfterExiting() diff --git a/example-scripts/nivo-bar-chart.sc b/example-scripts/nivo-bar-chart.sc index 47ee7837..546d1205 100755 --- a/example-scripts/nivo-bar-chart.sc +++ b/example-scripts/nivo-bar-chart.sc @@ -3,6 +3,7 @@ import org.terminal21.client.* import org.terminal21.client.fiberExecutor import org.terminal21.client.components.* +import org.terminal21.client.components.std.* import org.terminal21.client.components.nivo.* import scala.util.Random @@ -39,15 +40,14 @@ Sessions.withNewSession("nivo-bar-chart", "Nivo Bar Chart", NivoLib /* note we n ) Seq( - Paragraph(text = "Means of transportation for various countries", style = Map("margin" -> 20)), + Paragraph(text = "Various foods.", style = Map("margin" -> 20)), chart ).render() fiberExecutor.submit: while !session.isClosed do Thread.sleep(2000) - chart.data = createRandomData - session.render() + chart.withData(createRandomData).renderChanges() session.waitTillUserClosesSession() diff --git a/example-scripts/nivo-line-chart.sc b/example-scripts/nivo-line-chart.sc index 664f5698..6f5843ad 100755 --- a/example-scripts/nivo-line-chart.sc +++ b/example-scripts/nivo-line-chart.sc @@ -3,6 +3,7 @@ import org.terminal21.client.* import org.terminal21.client.fiberExecutor import org.terminal21.client.components.* +import org.terminal21.client.components.std.* import org.terminal21.client.components.nivo.* import scala.util.Random @@ -27,8 +28,7 @@ Sessions.withNewSession("nivo-line-chart", "Nivo Line Chart", NivoLib /* note we fiberExecutor.submit: while !session.isClosed do Thread.sleep(2000) - chart.data = createRandomData - session.render() + chart.withData(createRandomData).renderChanges() session.waitTillUserClosesSession() diff --git a/example-scripts/postit.sc b/example-scripts/postit.sc index cca81ef6..b64c48a6 100755 --- a/example-scripts/postit.sc +++ b/example-scripts/postit.sc @@ -7,6 +7,7 @@ import org.terminal21.client.* // std components, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala import org.terminal21.client.components.* +import org.terminal21.client.components.std.* // use the chakra components for menus, forms etc, https://chakra-ui.com/docs/components // The scala case classes : https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala import org.terminal21.client.components.chakra.* @@ -14,22 +15,23 @@ import org.terminal21.client.components.chakra.* Sessions.withNewSession("postit", "Post-It"): session => given ConnectedSession = session - val editor = Textarea(placeholder = "Please post your note by clicking here and editing the content") + val editor = Textarea(placeholder = "Please post your note by clicking here and editing the content") val messages = VStack(align = Some("stretch")) - val add = Button(text = "Post It.").onClick: () => + val add = Button(text = "Post It.").onClick: () => // add the new msg. // note: editor.value is automatically updated by terminal-ui - messages.addChildren( - HStack().withChildren( - Image( - src = "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Apple_Notes_icon.svg/2048px-Apple_Notes_icon.svg.png", - boxSize = Some("32px") - ), - Box(text = editor.value) + val currentMessages = messages.current + currentMessages + .addChildren( + HStack().withChildren( + Image( + src = "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Apple_Notes_icon.svg/2048px-Apple_Notes_icon.svg.png", + boxSize = Some("32px") + ), + Box(text = editor.current.value) + ) ) - ) - // always render after adding/modifying something - session.render() + .renderChanges() Seq( Paragraph(text = "Please type your note below and click 'Post It' to post it so that everyone can view it."), diff --git a/example-scripts/project.scala b/example-scripts/project.scala index dec52579..320278ba 100644 --- a/example-scripts/project.scala +++ b/example-scripts/project.scala @@ -1,7 +1,8 @@ //> using jvm "21" //> using scala 3 -//> using dep io.github.kostaskougios::terminal21-ui-std:0.11 -//> using dep io.github.kostaskougios::terminal21-nivo:0.11 -//> using dep io.github.kostaskougios::terminal21-mathjax:0.11 +//> using dep io.github.kostaskougios::terminal21-ui-std:0.20 +//> using dep io.github.kostaskougios::terminal21-nivo:0.20 +//> using dep io.github.kostaskougios::terminal21-mathjax:0.20 + //> using dep commons-io:commons-io:2.15.1 diff --git a/example-scripts/server.sc b/example-scripts/server.sc index b93acb6e..be0e534a 100755 --- a/example-scripts/server.sc +++ b/example-scripts/server.sc @@ -2,7 +2,7 @@ //> using jvm "21" //> using scala 3 -//> using dep io.github.kostaskougios::terminal21-server:0.11 +//> using dep io.github.kostaskougios::terminal21-server:0.20 import org.terminal21.server.Terminal21Server diff --git a/example-scripts/textedit.sc b/example-scripts/textedit.sc index dc894124..e0561f3b 100755 --- a/example-scripts/textedit.sc +++ b/example-scripts/textedit.sc @@ -12,6 +12,7 @@ import java.io.File import org.terminal21.client.* // std components, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala import org.terminal21.client.components.* +import org.terminal21.client.components.std.* // use the chakra components for menus, forms etc, https://chakra-ui.com/docs/components // The scala case classes : https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala import org.terminal21.client.components.chakra.* @@ -24,7 +25,7 @@ if args.length != 1 then ) val fileName = args(0) -val file = new File(fileName) +val file = new File(fileName) val contents = if file.exists() then FileUtils.readFileToString(file, "UTF-8") else "" @@ -33,18 +34,17 @@ def saveFile(content: String) = FileUtils.writeStringToFile(file, content, "UTF- Sessions.withNewSession(s"textedit-$fileName", s"Edit: $fileName"): session => given ConnectedSession = session // we will wait till the user clicks the "Exit" menu, this latch makes sure the main thread of the app waits. - val exitLatch = new CountDownLatch(1) + val exitLatch = new CountDownLatch(1) // the main editor area. - val editor = Textarea(value = contents) + val editor = Textarea(value = contents) // This will display a "saved" badge for a second when the user saves the file - val status = Badge() + val status = Badge() // This will display an asterisk when the contents of the file are changed in the editor - val modified = Badge(colorScheme = Some("red")) + val modified = Badge(colorScheme = Some("red")) // when the user changes the textarea, we get the new text and we can compare it with the loaded value. editor.onChange: newValue => - modified.text = if newValue != contents then "*" else "" - session.render() + modified.withText(if newValue != contents then "*" else "").renderChanges() Seq( HStack().withChildren( @@ -53,15 +53,15 @@ Sessions.withNewSession(s"textedit-$fileName", s"Edit: $fileName"): session => MenuList().withChildren( MenuItem(text = "Save") .onClick: () => - saveFile(editor.value) + saveFile(editor.current.value) // we'll display a "Saved" badge for 1 second. - status.text = "Saved" - modified.text = "" - session.render() - // each event handler runs on a new fibler, it is ok to sleep here + Seq( + status.withText("Saved"), + modified.withText("") + ).renderChanges() + // each event handler runs on a new fiber, it is ok to sleep here Thread.sleep(1000) - status.text = "" - session.render() + status.withText("").renderChanges() , MenuItem(text = "Exit") .onClick: () => diff --git a/example-spark/project.scala b/example-spark/project.scala index f921305a..a3ec4846 100644 --- a/example-spark/project.scala +++ b/example-spark/project.scala @@ -8,10 +8,10 @@ //> using javaOpt -Dlogback.configurationFile=file:etc/logback.xml // terminal21 dependencies -//> using dep io.github.kostaskougios::terminal21-ui-std:0.11 -//> using dep io.github.kostaskougios::terminal21-spark:0.11 -//> using dep io.github.kostaskougios::terminal21-nivo:0.11 -//> using dep io.github.kostaskougios::terminal21-mathjax:0.11 +//> using dep io.github.kostaskougios::terminal21-ui-std:0.20 +//> using dep io.github.kostaskougios::terminal21-spark:0.20 +//> using dep io.github.kostaskougios::terminal21-nivo:0.20 +//> using dep io.github.kostaskougios::terminal21-mathjax:0.20 //> using dep ch.qos.logback:logback-classic:1.4.14 diff --git a/example-spark/spark-notebook.sc b/example-spark/spark-notebook.sc index c0cce5e2..02a6a39c 100755 --- a/example-spark/spark-notebook.sc +++ b/example-spark/spark-notebook.sc @@ -54,12 +54,12 @@ SparkSessions.newTerminal21WithSparkSession(SparkSessions.newSparkSession(/* con val oldestPeopleChartCalc = peopleDS .orderBy($"age".desc) .visualize("Oldest people", oldestPeopleChart): data => - oldestPeopleChart.data = Seq( + oldestPeopleChart.withData(Seq( Serie( "Person", data = data.take(5).map(person => Datum(person.name, person.age)) ) - ) + )) Seq( // just make it look a bit more like a proper notebook by adding some fake maths diff --git a/project/build.properties b/project/build.properties index e8a1e246..abbbce5d 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.7 +sbt.version=1.9.8 diff --git a/terminal21-code-generation/src/main/scala/functions/tastyextractor/BetterErrors.scala b/terminal21-code-generation/src/main/scala/functions/tastyextractor/BetterErrors.scala new file mode 100644 index 00000000..2696d725 --- /dev/null +++ b/terminal21-code-generation/src/main/scala/functions/tastyextractor/BetterErrors.scala @@ -0,0 +1,6 @@ +package functions.tastyextractor + +object BetterErrors: + def betterError[R](name: String)(f: => R): R = + try f + catch case t: Throwable => throw new IllegalStateException(name, t) diff --git a/terminal21-code-generation/src/main/scala/functions/tastyextractor/StructureExtractor.scala b/terminal21-code-generation/src/main/scala/functions/tastyextractor/StructureExtractor.scala new file mode 100644 index 00000000..7cbef960 --- /dev/null +++ b/terminal21-code-generation/src/main/scala/functions/tastyextractor/StructureExtractor.scala @@ -0,0 +1,114 @@ +package functions.tastyextractor + +import dotty.tools.dotc.ast.untpd.ImportSelector +import dotty.tools.dotc.core.Symbols.ClassSymbol +import functions.tastyextractor.model.* + +import scala.collection.mutable +import scala.quoted.* +import scala.tasty.inspector.* +import functions.tastyextractor.model.{EImport, EMethod, EPackage, EParam, EType} + +private class StructureExtractorInspector extends Inspector: + val packages = mutable.ListBuffer.empty[EPackage] + + override def inspect(using Quotes)(tastys: List[Tasty[quotes.type]]): Unit = + import quotes.reflect.* + + def eTypeOf(tpe: TypeRepr): EType = tpe match + case t: TypeRef => + val name = t.name + val code = t.show + EType.code(name, code) + case at: AppliedType => + val args = at.args.map(eTypeOf) + val tycon = at.tycon + EType(tycon.typeSymbol.name, tpe.show, args, Nil, None, Nil) + case c => + // not sure if this works: + EType(c.typeSymbol.name, tpe.show, Nil, Nil, None, Nil) + + object MethodTraverser extends TreeAccumulator[List[EMethod]]: + def foldTree(existing: List[EMethod], tree: Tree)(owner: Symbol): List[EMethod] = + def paramsCode(param: Any) = + param match + case v: ValDef @unchecked => + val tpe = eTypeOf(v.tpt.tpe) + EParam(v.name, tpe, s"${v.name} : ${v.tpt.show}") + + val r = tree match + case d: DefDef if !d.name.contains("$") && d.name != "" => + BetterErrors.betterError(s"Error while parsing. Owner: $owner method: ${d.show}"): + val m = EMethod(d.name, d.paramss.map(pc => pc.params.map(paramsCode)), eTypeOf(d.returnTpt.tpe), d.symbol.docstring) + List(m) + case _ => + Nil + foldOverTree(existing ++ r, tree)(owner) + end MethodTraverser + + object ValTraverser extends TreeAccumulator[List[EParam]]: + def foldTree(existing: List[EParam], tree: Tree)(owner: Symbol): List[EParam] = + val r = tree match + case v: ValDef => + val tpe = eTypeOf(v.tpt.tpe) + owner match + case cc: ClassSymbol => + List(EParam(v.name, tpe, s"${v.name} : ${v.tpt.show}")) + case _ => Nil + case _ => + Nil + foldOverTree(existing ++ r, tree)(owner) + end ValTraverser + + object TypeTraverser extends TreeAccumulator[List[EType]]: + + def foldTree(existing: List[EType], tree: Tree)(owner: Symbol): List[EType] = + val r = tree match + case c: ClassDef => + val methods = MethodTraverser.foldTree(Nil, c)(owner) + val vals = ValTraverser.foldTree(Nil, c)(owner) + val t = EType(c.name, c.name, Nil, vals, c.symbol.docstring, methods) + List(t) + case _ => + Nil + foldOverTree(existing ++ r, tree)(owner) + end TypeTraverser + + object ImportTraverser extends TreeAccumulator[List[EImport]]: + def foldTree(existing: List[EImport], tree: Tree)(owner: Symbol): List[EImport] = + val r = tree match + case Import(module, paths) => + for case ImportSelector(ident, _, _) <- paths + yield EImport(module.show + "." + ident.name.toString) + + case _ => Nil + foldOverTree(existing ++ r, tree)(owner) + end ImportTraverser + + object PackageTraverser extends TreeAccumulator[List[EPackage]]: + def foldTree(existing: List[EPackage], tree: Tree)(owner: Symbol): List[EPackage] = + val r = tree match + case p: PackageClause => + val types = TypeTraverser.foldTree(Nil, p)(owner) + val imports = ImportTraverser.foldTree(Nil, p)(owner) + val t = EPackage(p.pid.show, imports, types) + List(t) + case _ => + Nil + foldOverTree(existing ++ r, tree)(owner) + end PackageTraverser + + for tasty <- tastys do + val tree = tasty.ast + packages ++= PackageTraverser.foldTree(Nil, tree)(tree.symbol) + +/** Converts tasty files to an easier to digest domain model + */ +class StructureExtractor: + def fromFiles(files: List[String], dependencyCp: List[String]): Seq[EPackage] = + val inspector = new StructureExtractorInspector + TastyInspector.inspectAllTastyFiles(files, Nil, dependencyCp)(inspector) + inspector.packages.toSeq + +object StructureExtractor: + def apply() = new StructureExtractor diff --git a/terminal21-code-generation/src/main/scala/functions/tastyextractor/model/EImport.scala b/terminal21-code-generation/src/main/scala/functions/tastyextractor/model/EImport.scala new file mode 100644 index 00000000..c0913ba8 --- /dev/null +++ b/terminal21-code-generation/src/main/scala/functions/tastyextractor/model/EImport.scala @@ -0,0 +1,3 @@ +package functions.tastyextractor.model + +case class EImport(fullName: String) diff --git a/terminal21-code-generation/src/main/scala/functions/tastyextractor/model/EMethod.scala b/terminal21-code-generation/src/main/scala/functions/tastyextractor/model/EMethod.scala new file mode 100644 index 00000000..6a9cf310 --- /dev/null +++ b/terminal21-code-generation/src/main/scala/functions/tastyextractor/model/EMethod.scala @@ -0,0 +1,3 @@ +package functions.tastyextractor.model + +case class EMethod(name: String, paramss: List[List[EParam]], returnType: EType, scalaDocs: Option[String]) diff --git a/terminal21-code-generation/src/main/scala/functions/tastyextractor/model/EPackage.scala b/terminal21-code-generation/src/main/scala/functions/tastyextractor/model/EPackage.scala new file mode 100644 index 00000000..0f280962 --- /dev/null +++ b/terminal21-code-generation/src/main/scala/functions/tastyextractor/model/EPackage.scala @@ -0,0 +1,4 @@ +package functions.tastyextractor.model + +case class EPackage(name: String, imports: Seq[EImport], types: Seq[EType]): + def toPath: String = name.replace('.', '/') diff --git a/terminal21-code-generation/src/main/scala/functions/tastyextractor/model/EParam.scala b/terminal21-code-generation/src/main/scala/functions/tastyextractor/model/EParam.scala new file mode 100644 index 00000000..49f7be5e --- /dev/null +++ b/terminal21-code-generation/src/main/scala/functions/tastyextractor/model/EParam.scala @@ -0,0 +1,3 @@ +package functions.tastyextractor.model + +case class EParam(name: String, `type`: EType, code: String) diff --git a/terminal21-code-generation/src/main/scala/functions/tastyextractor/model/EType.scala b/terminal21-code-generation/src/main/scala/functions/tastyextractor/model/EType.scala new file mode 100644 index 00000000..9b820658 --- /dev/null +++ b/terminal21-code-generation/src/main/scala/functions/tastyextractor/model/EType.scala @@ -0,0 +1,11 @@ +package functions.tastyextractor.model + +import org.apache.commons.lang3.StringUtils + +case class EType(name: String, code: String, typeArgs: Seq[EType], vals: List[EParam], scalaDocs: Option[String], methods: Seq[EMethod]): + def isUnit: Boolean = code == "scala.Unit" + + def simplifiedCode: String = if typeArgs.isEmpty then name else s"$name[${typeArgs.map(_.simplifiedCode).mkString(", ")}]" + +object EType: + def code(name: String, code: String) = EType(name, code, Nil, Nil, None, Nil) diff --git a/terminal21-code-generation/src/main/scala/org/terminal21/codegen/Code.scala b/terminal21-code-generation/src/main/scala/org/terminal21/codegen/Code.scala new file mode 100644 index 00000000..c8b0b5ef --- /dev/null +++ b/terminal21-code-generation/src/main/scala/org/terminal21/codegen/Code.scala @@ -0,0 +1,10 @@ +package org.terminal21.codegen + +import org.apache.commons.io.FileUtils + +import java.io.File + +case class Code(file: String, code: String): + def writeTo(srcRootFolder: String): Unit = + val f = new File(srcRootFolder, file) + FileUtils.writeStringToFile(f, code, "UTF-8") diff --git a/terminal21-code-generation/src/main/scala/org/terminal21/codegen/PropertiesExtensionGenerator.scala b/terminal21-code-generation/src/main/scala/org/terminal21/codegen/PropertiesExtensionGenerator.scala new file mode 100644 index 00000000..0f2b6dbf --- /dev/null +++ b/terminal21-code-generation/src/main/scala/org/terminal21/codegen/PropertiesExtensionGenerator.scala @@ -0,0 +1,68 @@ +package org.terminal21.codegen + +import functions.tastyextractor.StructureExtractor +import functions.tastyextractor.model.EType +import org.terminal21.codegen.PropertiesExtensionGenerator.{extract, generate} + +import java.io.File +object PropertiesExtensionGenerator: + private val e = StructureExtractor() + + def extract(tastys: List[String]): Code = + val mainCp = detectClasspath(new File("../terminal21-ui-std")) + val packages = e.fromFiles(tastys, List(mainCp.getAbsolutePath)) + val ext = packages.map: p => + val extCode = p.types + .filterNot(_.name.contains("$")) + .filterNot(_.vals.isEmpty) + .map: t => + createExtension(t) + + extCode.mkString("\n") + + val p = packages.head + Code( + s"${p.name.replace('.', '/')}/extensions.scala", + s""" + |package ${p.name} + | + |// GENERATED WITH PropertiesExtensionGenerator, DON'T EDIT + | + |${p.imports.map(_.fullName).mkString("import ", "\nimport ", "")} + |${ext.mkString("\n")} + |""".stripMargin + ) + + def fix(n: String) = n match + case "type" => "`type`" + case _ => n + + def createExtension(t: EType): String = + val skipVals = Set("children", "rendered", "style") + val methods = t.vals + .filterNot(v => skipVals(v.name)) + .map: vl => + s"def with${vl.name.capitalize}(v: ${vl.`type`.simplifiedCode}) = copy(${fix(vl.name)} = v)" + + s""" + |extension (e: ${t.name}) + | ${methods.mkString("\n ")} + |""".stripMargin + + def detectClasspath(moduleDir: File) = + val targetDir = new File(moduleDir, "target") + val scala3Dir = targetDir.listFiles().find(_.getName.startsWith("scala-3")).get + val classesDir = new File(scala3Dir, "classes") + classesDir + + def generate(moduleDir: File, pckg: String): Unit = + println(s"Generating for $pckg") + val classesRootDir = detectClasspath(moduleDir) + val classesDir = new File(classesRootDir, pckg.replace('.', '/')) + val code = extract(classesDir.listFiles().filter(_.getName.endsWith(".tasty")).filterNot(_.getName.contains("$")).map(_.getAbsolutePath).toList) + code.writeTo(s"${moduleDir.getAbsolutePath}/src/main/ui-generated") + +@main def propertiesExtensionGeneratorApp(): Unit = + generate(new File("../terminal21-ui-std"), "org.terminal21.client.components.std") + generate(new File("../terminal21-ui-std"), "org.terminal21.client.components.chakra") +// generate(new File("../terminal21-nivo"), "org.terminal21.client.components.nivo") diff --git a/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala b/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala index 10fad50f..4deef557 100644 --- a/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala +++ b/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala @@ -9,9 +9,11 @@ sealed trait MathJaxElement extends UiElement */ case class MathJax( key: String = Keys.nextKey, - /** expression should be like """ text \( asciimath \) text""", i.e. """When \(a \ne 0\), there are two solutions to \(ax^2 + bx + c = 0\)""" - */ - @volatile var expression: String = """fill in the expression as per https://asciimath.org/""", - @volatile var style: Map[String, Any] = Map.empty // Note: some of the styles are ignored by mathjax lib + // expression should be like """ text \( asciimath \) text""", i.e. """When \(a \ne 0\), there are two solutions to \(ax^2 + bx + c = 0\)""" + expression: String = """fill in the expression as per https://asciimath.org/""", + style: Map[String, Any] = Map.empty // Note: some of the styles are ignored by mathjax lib ) extends MathJaxElement - with HasStyle + with HasStyle[MathJax]: + override def withStyle(v: Map[String, Any]): MathJax = copy(style = v) + def withKey(k: String) = copy(key = k) + def withExpression(e: String) = copy(expression = e) diff --git a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoLib.scala b/terminal21-nivo/src/main/scala/org/terminal21/client/components/NivoLib.scala similarity index 66% rename from terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoLib.scala rename to terminal21-nivo/src/main/scala/org/terminal21/client/components/NivoLib.scala index d4030b54..4a68b032 100644 --- a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoLib.scala +++ b/terminal21-nivo/src/main/scala/org/terminal21/client/components/NivoLib.scala @@ -1,12 +1,12 @@ -package org.terminal21.client.components.nivo +package org.terminal21.client.components -import io.circe.{Encoder, Json} -import org.terminal21.client.components.{ComponentLib, UiElement} -import io.circe.* import io.circe.generic.auto.* import io.circe.syntax.* +import io.circe.* +import org.terminal21.client.components.nivo.NEJson +import org.terminal21.client.components.{ComponentLib, UiElement} object NivoLib extends ComponentLib: import org.terminal21.client.components.StdElementEncoding.given override def toJson(using Encoder[UiElement]): PartialFunction[UiElement, Json] = - case n: NivoElement => n.asJson.mapObject(o => o.add("type", "Nivo".asJson)) + case n: NEJson => n.asJson.mapObject(o => o.add("type", "Nivo".asJson)) diff --git a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala index 7200fc36..8816f500 100644 --- a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala +++ b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala @@ -3,7 +3,8 @@ package org.terminal21.client.components.nivo import org.terminal21.client.components.UiElement.HasStyle import org.terminal21.client.components.{Keys, UiElement} -sealed trait NivoElement extends UiElement +sealed trait NEJson extends UiElement +sealed trait NivoElement[A <: UiElement] extends NEJson with HasStyle[A] /** https://nivo.rocks/line/ */ @@ -11,25 +12,27 @@ case class ResponsiveLine( key: String = Keys.nextKey, // to give width and height, we wrap the component in a wrapper element. Height must be provided // for nivo components to be visible - @volatile var style: Map[String, Any] = Map("height" -> "400px"), - @volatile var data: Seq[Serie] = Nil, - @volatile var margin: Margin = Margin(right = 110), - @volatile var xScale: Scale = Scale.Point, - @volatile var yScale: Scale = Scale(), - @volatile var yFormat: String = " >-.2f", - @volatile var axisTop: Option[Axis] = None, - @volatile var axisRight: Option[Axis] = None, - @volatile var axisBottom: Option[Axis] = Some(Axis(legend = "y", legendOffset = 36)), - @volatile var axisLeft: Option[Axis] = Some(Axis(legend = "x", legendOffset = -40)), - @volatile var pointSize: Int = 10, - @volatile var pointColor: Map[String, String] = Map("theme" -> "background"), - @volatile var pointBorderWidth: Int = 2, - @volatile var pointBorderColor: Map[String, String] = Map("from" -> "serieColor"), - @volatile var pointLabelYOffset: Int = -12, - @volatile var useMesh: Boolean = true, - @volatile var legends: Seq[Legend] = Nil -) extends NivoElement - with HasStyle + style: Map[String, Any] = Map("height" -> "400px"), + data: Seq[Serie] = Nil, + margin: Margin = Margin(right = 110), + xScale: Scale = Scale.Point, + yScale: Scale = Scale(), + yFormat: String = " >-.2f", + axisTop: Option[Axis] = None, + axisRight: Option[Axis] = None, + axisBottom: Option[Axis] = Some(Axis(legend = "y", legendOffset = 36)), + axisLeft: Option[Axis] = Some(Axis(legend = "x", legendOffset = -40)), + pointSize: Int = 10, + pointColor: Map[String, String] = Map("theme" -> "background"), + pointBorderWidth: Int = 2, + pointBorderColor: Map[String, String] = Map("from" -> "serieColor"), + pointLabelYOffset: Int = -12, + useMesh: Boolean = true, + legends: Seq[Legend] = Nil +) extends NivoElement[ResponsiveLine]: + override def withStyle(v: Map[String, Any]): ResponsiveLine = copy(style = v) + def withKey(v: String) = copy(key = v) + def withData(data: Seq[Serie]) = copy(data = data) /** https://nivo.rocks/bar/ */ @@ -37,22 +40,24 @@ case class ResponsiveBar( key: String = Keys.nextKey, // to give width and height, we wrap the component in a wrapper element. Height must be provided // for nivo components to be visible - @volatile var style: Map[String, Any] = Map("height" -> "400px"), - @volatile var data: Seq[Seq[BarDatum]] = Nil, - @volatile var keys: Seq[String] = Nil, - @volatile var indexBy: String = "", - @volatile var margin: Margin = Margin(right = 110), - @volatile var padding: Float = 0, - @volatile var valueScale: Scale = Scale(), - @volatile var indexScale: Scale = Scale(), - @volatile var colors: Map[String, String] = Map("scheme" -> "nivo"), - @volatile var defs: Seq[Defs] = Nil, - @volatile var fill: Seq[Fill] = Nil, - @volatile var axisTop: Option[Axis] = None, - @volatile var axisRight: Option[Axis] = None, - @volatile var axisBottom: Option[Axis] = Some(Axis(legend = "y", legendOffset = 36)), - @volatile var axisLeft: Option[Axis] = Some(Axis(legend = "x", legendOffset = -40)), - @volatile var legends: Seq[Legend] = Nil, - @volatile var ariaLabel: String = "Chart Label" -) extends NivoElement - with HasStyle + style: Map[String, Any] = Map("height" -> "400px"), + data: Seq[Seq[BarDatum]] = Nil, + keys: Seq[String] = Nil, + indexBy: String = "", + margin: Margin = Margin(right = 110), + padding: Float = 0, + valueScale: Scale = Scale(), + indexScale: Scale = Scale(), + colors: Map[String, String] = Map("scheme" -> "nivo"), + defs: Seq[Defs] = Nil, + fill: Seq[Fill] = Nil, + axisTop: Option[Axis] = None, + axisRight: Option[Axis] = None, + axisBottom: Option[Axis] = Some(Axis(legend = "y", legendOffset = 36)), + axisLeft: Option[Axis] = Some(Axis(legend = "x", legendOffset = -40)), + legends: Seq[Legend] = Nil, + ariaLabel: String = "Chart Label" +) extends NivoElement[ResponsiveBar]: + override def withStyle(v: Map[String, Any]): ResponsiveBar = copy(style = v) + def withKey(v: String) = copy(key = v) + def withData(data: Seq[Seq[BarDatum]]) = copy(data = data) diff --git a/terminal21-server/src/main/scala/org/terminal21/server/json/WsRequest.scala b/terminal21-server/src/main/scala/org/terminal21/server/json/WsRequest.scala index 35f0406d..96164b3d 100644 --- a/terminal21-server/src/main/scala/org/terminal21/server/json/WsRequest.scala +++ b/terminal21-server/src/main/scala/org/terminal21/server/json/WsRequest.scala @@ -8,6 +8,8 @@ case class WsRequest(operation: String, body: Option[Body]) sealed trait Body +case class SessionFullRefresh(sessionId: String) extends Body + sealed trait UiEvent extends Body: def sessionId: String diff --git a/terminal21-server/src/main/scala/org/terminal21/server/json/WsResponse.scala b/terminal21-server/src/main/scala/org/terminal21/server/json/WsResponse.scala index c10797c1..e6be4591 100644 --- a/terminal21-server/src/main/scala/org/terminal21/server/json/WsResponse.scala +++ b/terminal21-server/src/main/scala/org/terminal21/server/json/WsResponse.scala @@ -1,10 +1,12 @@ package org.terminal21.server.json -import io.circe.Json import org.terminal21.model.Session +import org.terminal21.ui.std.ServerJson sealed trait WsResponse case class SessionsWsResponse(sessions: Seq[Session]) extends WsResponse -case class StateWsResponse(session: Session, sessionState: Json) extends WsResponse +case class StateWsResponse(session: Session, sessionState: ServerJson) extends WsResponse + +case class StateChangeWsResponse(session: Session, sessionStateChange: ServerJson) extends WsResponse diff --git a/terminal21-server/src/main/scala/org/terminal21/server/model/SessionState.scala b/terminal21-server/src/main/scala/org/terminal21/server/model/SessionState.scala index ac308494..2189ed89 100644 --- a/terminal21-server/src/main/scala/org/terminal21/server/model/SessionState.scala +++ b/terminal21-server/src/main/scala/org/terminal21/server/model/SessionState.scala @@ -2,10 +2,11 @@ package org.terminal21.server.model import org.terminal21.model.CommandEvent import org.terminal21.server.utils.NotificationRegistry +import org.terminal21.ui.std.ServerJson case class SessionState( - json: String, + serverJson: ServerJson, eventsNotificationRegistry: NotificationRegistry[CommandEvent] ): - def withNewState(newJson: String): SessionState = copy(json = newJson) - def close: SessionState = copy(eventsNotificationRegistry = new NotificationRegistry[CommandEvent]) + def withNewState(newJson: ServerJson): SessionState = copy(serverJson = newJson) + def close: SessionState = copy(eventsNotificationRegistry = new NotificationRegistry[CommandEvent]) diff --git a/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala b/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala index f3ae206a..d8749662 100644 --- a/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala +++ b/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala @@ -5,7 +5,7 @@ import org.terminal21.model.* import org.terminal21.server.json.UiEvent import org.terminal21.server.model.SessionState import org.terminal21.server.utils.{ListenerFunction, NotificationRegistry} -import org.terminal21.ui.std.SessionsService +import org.terminal21.ui.std.{ServerJson, SessionsService} import java.util.UUID @@ -15,11 +15,13 @@ class ServerSessionsService extends SessionsService: private val sessions = collection.concurrent.TrieMap.empty[Session, SessionState] private val sessionChangeNotificationRegistry = new NotificationRegistry[Seq[Session]] - private val sessionStateChangeNotificationRegistry = new NotificationRegistry[(Session, SessionState)] + private val sessionStateChangeNotificationRegistry = new NotificationRegistry[(Session, SessionState, Option[ServerJson])] def sessionById(sessionId: String): Session = sessions.keys.find(_.id == sessionId).getOrElse(throw new IllegalArgumentException(s"Invalid session id = $sessionId")) + def sessionStateOf(session: Session): SessionState = sessions(session) + def notifyMeWhenSessionsChange(listener: ListenerFunction[Seq[Session]]): Unit = sessionChangeNotificationRegistry.addAndNotify(allSessions)(listener) @@ -38,25 +40,32 @@ class ServerSessionsService extends SessionsService: val s = Session(id, name, UUID.randomUUID().toString, true) logger.info(s"Creating session $s") sessions.keys.toList.foreach(s => if s.id == id then sessions.remove(s)) - val state = SessionState("""{ "elements" : [] }""", new NotificationRegistry) + val state = SessionState(ServerJson.Empty, new NotificationRegistry) sessions += s -> state sessionChangeNotificationRegistry.notifyAll(allSessions) s def allSessions: Seq[Session] = sessions.keySet.toList - def notifyMeWhenSessionChanges(f: ListenerFunction[(Session, SessionState)]): Unit = + def notifyMeWhenSessionChanges(f: ListenerFunction[(Session, SessionState, Option[ServerJson])]): Unit = sessionStateChangeNotificationRegistry.add(f) - for (session, state) <- sessions do f(session, state) + for (session, state) <- sessions do f(session, state, None) - override def setSessionJsonState(session: Session, newStateJson: String): Unit = + override def setSessionJsonState(session: Session, newStateJson: ServerJson): Unit = val oldV = sessions(session) val newV = oldV.withNewState(newStateJson) sessions += session -> newV - sessionStateChangeNotificationRegistry.notifyAll((session, newV)) + sessionStateChangeNotificationRegistry.notifyAll((session, newV, None)) logger.info(s"Session $session new state $newStateJson") - def addEvent(event: UiEvent): Unit = + override def changeSessionJsonState(session: Session, change: ServerJson): Unit = + val oldV = sessions(session) + val newV = oldV.withNewState(oldV.serverJson.include(change)) + sessions += session -> newV + sessionStateChangeNotificationRegistry.notifyAll((session, newV, Some(change))) + logger.info(s"Session $session change $change") + + def triggerUiEvent(event: UiEvent): Unit = val e = event match case org.terminal21.server.json.OnClick(_, key) => OnClick(key) case org.terminal21.server.json.OnChange(_, key, value) => OnChange(key, value) diff --git a/terminal21-server/src/main/scala/org/terminal21/server/ui/SessionsWebSocket.scala b/terminal21-server/src/main/scala/org/terminal21/server/ui/SessionsWebSocket.scala index 60cc5e93..e78a3544 100644 --- a/terminal21-server/src/main/scala/org/terminal21/server/ui/SessionsWebSocket.scala +++ b/terminal21-server/src/main/scala/org/terminal21/server/ui/SessionsWebSocket.scala @@ -9,6 +9,7 @@ import org.terminal21.model.Session import org.terminal21.server.json.* import org.terminal21.server.model.SessionState import org.terminal21.server.service.ServerSessionsService +import org.terminal21.ui.std.ServerJson import org.terminal21.utils.ErrorLogger // websocket: https://helidon.io/docs/v4/#/se/websocket @@ -21,15 +22,22 @@ class SessionsWebSocket(sessionsService: ServerSessionsService) extends WsListen WsSessionOps.returnTrueIfSessionIsNotClosed: sendSessions(wsSession, allSessions) - sessionsService.notifyMeWhenSessionChanges: (session, sessionState) => + sessionsService.notifyMeWhenSessionChanges: (session, sessionState, changeOpt) => WsSessionOps.returnTrueIfSessionIsNotClosed: - sendSessionState(wsSession, session, sessionState) + changeOpt match + case None => sendSessionState(wsSession, session, sessionState) + case Some(change) => sendSessionStateChange(wsSession, session, change) private def sendSessionState(wsSession: WsSession, session: Session, sessionState: SessionState): Unit = - val response = StateWsResponse(session.hideSecret, sessionState.json.asJson).asJson.noSpaces + val response = StateWsResponse(session.hideSecret, sessionState.serverJson).asJson.noSpaces logger.info(s"$wsSession: Sending session state response $response") wsSession.send(response, true) + private def sendSessionStateChange(wsSession: WsSession, session: Session, change: ServerJson): Unit = + val response = StateChangeWsResponse(session.hideSecret, change).asJson.noSpaces + logger.info(s"$wsSession: Sending session change state response $response") + wsSession.send(response, true) + private def sendSessions(wsSession: WsSession, allSessions: Seq[Session]): Unit = val sessions = allSessions.map(_.hideSecret).sortBy(_.name) val json = SessionsWsResponse(sessions).asJson.noSpaces @@ -40,21 +48,26 @@ class SessionsWebSocket(sessionsService: ServerSessionsService) extends WsListen logger.info(s"$wsSession: Received json: $text , last = $last") errorLogger.logErrors: WsRequest.decoder(text) match - case Right(WsRequest("sessions", None)) => + case Right(WsRequest("sessions", None)) => continuouslyRespond(wsSession) logger.info(s"$wsSession: sessions processed successfully") - case Right(WsRequest(eventName, Some(event: UiEvent))) => + case Right(WsRequest("session-full-refresh", Some(SessionFullRefresh(sessionId)))) => + logger.info(s"$wsSession: session-full-refresh requested, sending full session data for $sessionId") + val session = sessionsService.sessionById(sessionId) + val sessionState = sessionsService.sessionStateOf(session) + sendSessionState(wsSession, session, sessionState) + case Right(WsRequest(eventName, Some(event: UiEvent))) => logger.info(s"$wsSession: Received event $eventName = $event") - sessionsService.addEvent(event) - case Right(WsRequest("ping", None)) => + sessionsService.triggerUiEvent(event) + case Right(WsRequest("ping", None)) => logger.info(s"$wsSession: ping received") - case Right(WsRequest("close-session", Some(CloseSession(sessionId)))) => + case Right(WsRequest("close-session", Some(CloseSession(sessionId)))) => val session = sessionsService.sessionById(sessionId) sessionsService.terminateSession(session) - case Right(WsRequest("remove-session", Some(RemoveSession(sessionId)))) => + case Right(WsRequest("remove-session", Some(RemoveSession(sessionId)))) => val session = sessionsService.sessionById(sessionId) sessionsService.removeSession(session) - case x => + case x => logger.error(s"Invalid request : $x") override def onOpen(wsSession: WsSession): Unit = diff --git a/terminal21-server/src/main/scala/org/terminal21/server/utils/NotificationRegistry.scala b/terminal21-server/src/main/scala/org/terminal21/server/utils/NotificationRegistry.scala index e95c5c69..1b8c2c36 100644 --- a/terminal21-server/src/main/scala/org/terminal21/server/utils/NotificationRegistry.scala +++ b/terminal21-server/src/main/scala/org/terminal21/server/utils/NotificationRegistry.scala @@ -1,12 +1,15 @@ package org.terminal21.server.utils +import org.slf4j.LoggerFactory + import scala.util.Try // make sure this doesn't throw any exceptions type ListenerFunction[A] = A => Boolean class NotificationRegistry[A]: - private var ns = List.empty[ListenerFunction[A]] + private val logger = LoggerFactory.getLogger(getClass) + private var ns = List.empty[ListenerFunction[A]] def add(listener: ListenerFunction[A]): Unit = synchronized: @@ -17,5 +20,10 @@ class NotificationRegistry[A]: def notifyAll(a: A): Int = synchronized: - ns = ns.filter(f => Try(f(a)).getOrElse(false)) + ns = ns.filter: f => + Try(f(a)) + .recover: e => + logger.error("an error occurred during a notification", e) + false + .get ns.size diff --git a/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala b/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala new file mode 100644 index 00000000..ddd96db5 --- /dev/null +++ b/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala @@ -0,0 +1,150 @@ +package org.terminal21.server.service + +import io.circe.Json +import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatest.matchers.should.Matchers.* +import org.terminal21.model.{OnChange, OnClick, SessionClosed} +import org.terminal21.server.json +import org.terminal21.ui.std.StdExportsBuilders.serverJson + +class ServerSessionsServiceTest extends AnyFunSuiteLike: + test("sessionById"): + new App: + val session = createSession + serverSessionsService.setSessionJsonState(session, serverJson()) + serverSessionsService.sessionById(session.id) should be(session) + + test("sessionStateOf"): + new App: + val session = createSession + val sj = serverJson() + serverSessionsService.setSessionJsonState(session, sj) + serverSessionsService.sessionStateOf(session).serverJson should be(sj) + + test("removeSession"): + new App: + val session = createSession + serverSessionsService.setSessionJsonState(session, serverJson()) + serverSessionsService.removeSession(session) + an[IllegalArgumentException] should be thrownBy: + serverSessionsService.sessionById(session.id) + + test("removeSession notifies listeners"): + new App: + val session = createSession + serverSessionsService.setSessionJsonState(session, serverJson()) + var listenerCalled = 0 + serverSessionsService.notifyMeWhenSessionsChange: sessions => + listenerCalled match + case 0 => sessions should be(Seq(session)) + case 1 => sessions should be(Nil) + listenerCalled += 1 + true + serverSessionsService.removeSession(session) + listenerCalled should be(2) + + test("terminateSession marks session as closed"): + new App: + val session = createSession + serverSessionsService.setSessionJsonState(session, serverJson()) + serverSessionsService.terminateSession(session) + serverSessionsService.sessionById(session.id).isOpen should be(false) + + test("terminateSession notifies session listeners"): + new App: + val session = createSession + serverSessionsService.setSessionJsonState(session, serverJson()) + var eventCalled = false + serverSessionsService.notifyMeOnSessionEvents(session): event => + event should be(SessionClosed("-")) + eventCalled = true + true + serverSessionsService.terminateSession(session) + eventCalled should be(true) + + test("terminateSession notifies sessions listeners"): + new App: + val session = createSession + serverSessionsService.setSessionJsonState(session, serverJson()) + var listenerCalled = 0 + serverSessionsService.notifyMeWhenSessionsChange: sessions => + listenerCalled match + case 0 => sessions should be(Seq(session)) + case 1 => sessions should be(Seq(session.close)) + listenerCalled += 1 + true + serverSessionsService.terminateSession(session) + listenerCalled should be(2) + + test("createSession notifies listeners"): + new App: + var listenerCalled = 0 + serverSessionsService.notifyMeWhenSessionsChange: sessions => + listenerCalled match + case 0 => sessions should be(Nil) + case 1 => sessions.size should be(1) + listenerCalled += 1 + true + + createSession + listenerCalled should be(2) + + test("changeSessionJsonState changes session's state"): + new App: + val session = createSession + val sj1 = serverJson(elements = Map("e1" -> Json.fromString("e1v"))) + serverSessionsService.setSessionJsonState(session, sj1) + val sj2 = serverJson(elements = Map("e2" -> Json.fromString("e2v"))) + serverSessionsService.changeSessionJsonState(session, sj2) + serverSessionsService.sessionStateOf(session).serverJson should be( + sj1.include(sj2) + ) + + test("changeSessionJsonState notifies listeners"): + new App: + val session = createSession + val sj1 = serverJson(elements = Map("e1" -> Json.fromString("e1v"))) + serverSessionsService.setSessionJsonState(session, sj1) + val sj2 = serverJson(elements = Map("e2" -> Json.fromString("e2v"))) + var called = 0 + serverSessionsService.notifyMeWhenSessionChanges: (s, sessionState, sjOption) => + called match + case 0 => + s should be(session) + sjOption should be(None) + case 1 => + s should be(session) + sjOption should be(Some(sj2)) + + called += 1 + true + serverSessionsService.changeSessionJsonState(session, sj2) + called should be(2) + + test("triggerUiEvent notifies listeners for clicks"): + new App: + val session = createSession + var called = false + serverSessionsService.notifyMeOnSessionEvents(session): e => + called = true + e should be(OnClick("key1")) + true + + serverSessionsService.triggerUiEvent(json.OnClick(session.id, "key1")) + called should be(true) + + test("triggerUiEvent notifies listeners for change"): + new App: + val session = createSession + var called = false + serverSessionsService.notifyMeOnSessionEvents(session): e => + called = true + e should be(OnChange("key1", "newvalue")) + true + + serverSessionsService.triggerUiEvent(json.OnChange(session.id, "key1", "newvalue")) + called should be(true) + + class App: + val serverSessionsService = new ServerSessionsService + def createSession = serverSessionsService.createSession("test", "Test") diff --git a/terminal21-server/src/test/scala/scripts/ExamineJson.scala b/terminal21-server/src/test/scala/scripts/ExamineJson.scala deleted file mode 100644 index bf91d08b..00000000 --- a/terminal21-server/src/test/scala/scripts/ExamineJson.scala +++ /dev/null @@ -1,9 +0,0 @@ -//package scripts -// -//import io.circe.* -//import io.circe.generic.auto.* -//import io.circe.syntax.* -//import org.terminal21.server.json.{OnClick, WsRequest} -// -//@main def examineJson(): Unit = -// println(WsRequest("init", Some(OnClick("123"))).asJson.noSpaces) diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala index 55e63694..2ee3e933 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala @@ -8,8 +8,8 @@ import org.terminal21.client.components.{Keys, UiElement} import org.terminal21.sparklib.calculations.{ReadWriter, StdUiSparkCalculation} extension [OUT: ReadWriter](ds: OUT) - def visualize(name: String, dataUi: UiElement with HasStyle)( - toUi: OUT => Unit + def visualize(name: String, dataUi: UiElement with HasStyle[_])( + toUi: OUT => UiElement & HasStyle[_] )(using session: ConnectedSession, executor: FiberExecutor, @@ -17,7 +17,7 @@ extension [OUT: ReadWriter](ds: OUT) ) = val ui = new StdUiSparkCalculation[OUT](Keys.nextKey, name, dataUi): override protected def whenResultsReady(results: OUT): Unit = - try toUi(results) + try updateUi(toUi(results)) catch case t: Throwable => t.printStackTrace() super.whenResultsReady(results) override def nonCachedCalculation: OUT = ds diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala index 10b20321..fc7ed440 100644 --- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala +++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala @@ -48,7 +48,7 @@ trait SparkCalculation[OUT: ReadWriter](name: String)(using executor: FiberExecu abstract class StdUiSparkCalculation[OUT: ReadWriter]( val key: String, name: String, - dataUi: UiElement with HasStyle + dataUi: UiElement with HasStyle[_] )(using session: ConnectedSession, executor: FiberExecutor, spark: SparkSession) extends SparkCalculation[OUT](name) with StdUiCalculation[OUT](name, dataUi) diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala index 1b391b16..ec6d1a2a 100644 --- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala +++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala @@ -57,8 +57,7 @@ import org.terminal21.sparklib.endtoend.model.CodeFile.scanSourceFiles val sourceFileChart = sortedSourceFilesDS.visualize("Biggest Code Files", chart): results => val data = results.take(10).map(cf => Datum(StringUtils.substringBeforeLast(cf.name, ".scala"), cf.numOfLines)).toList - chart.data = Seq(Serie("Scala", data = data)) - session.render() + chart.withData(Seq(Serie("Scala", data = data))) Seq( codeFilesCalculation, diff --git a/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala b/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala new file mode 100644 index 00000000..5ff4371b --- /dev/null +++ b/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala @@ -0,0 +1,18 @@ +package org.terminal21.ui.std + +import io.circe.Json + +case class ServerJson( + rootKeys: Seq[String], + elements: Map[String, Json], + keyTree: Map[String, Seq[String]] +): + def include(j: ServerJson): ServerJson = + ServerJson( + rootKeys, + elements ++ j.elements, + keyTree ++ j.keyTree + ) + +object ServerJson: + val Empty = ServerJson(Nil, Map.empty, Map.empty) diff --git a/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/SessionsService.scala b/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/SessionsService.scala index c724ddfc..8809d7da 100644 --- a/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/SessionsService.scala +++ b/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/SessionsService.scala @@ -8,4 +8,5 @@ trait SessionsService: def createSession(id: String, name: String): Session def terminateSession(session: Session): Unit - def setSessionJsonState(session: Session, state: String): Unit + def setSessionJsonState(session: Session, state: ServerJson): Unit + def changeSessionJsonState(session: Session, state: ServerJson): Unit diff --git a/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/ServerJsonTest.scala b/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/ServerJsonTest.scala new file mode 100644 index 00000000..4a2f7dbf --- /dev/null +++ b/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/ServerJsonTest.scala @@ -0,0 +1,17 @@ +package org.terminal21.ui.std + +import io.circe.Json +import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatest.matchers.should.Matchers.* + +class ServerJsonTest extends AnyFunSuiteLike: + test("include"): + val j1 = ServerJson(Seq("k1"), Map("k1" -> Json.fromInt(1), "k2" -> Json.fromInt(2)), Map("k1" -> Seq("k2", "k3"))) + val j2 = ServerJson(Nil, Map("k2" -> Json.fromInt(3)), Map("k2" -> Seq("k4"))) + j1.include(j2) should be( + ServerJson( + Seq("k1"), + Map("k1" -> Json.fromInt(1), "k2" -> Json.fromInt(3)), + Map("k1" -> Seq("k2", "k3"), "k2" -> Seq("k4")) + ) + ) diff --git a/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/StdExportsBuilders.scala b/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/StdExportsBuilders.scala new file mode 100644 index 00000000..68f693e6 --- /dev/null +++ b/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/StdExportsBuilders.scala @@ -0,0 +1,14 @@ +package org.terminal21.ui.std + +import io.circe.Json + +object StdExportsBuilders: + def serverJson( + rootKeys: Seq[String] = Nil, + elements: Map[String, Json] = Map.empty, + keyTree: Map[String, Seq[String]] = Map.empty + ) = ServerJson( + rootKeys, + elements, + keyTree + ) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ClientEventsWsListener.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ClientEventsWsListener.scala index ae26915c..3753d9f2 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ClientEventsWsListener.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ClientEventsWsListener.scala @@ -34,7 +34,8 @@ class ClientEventsWsListener(wsClient: WsClient, session: ConnectedSession, exec logger.error(s"An invalid json was received as an event. error = $e") case Right(event) => executor.submit: - session.fireEvent(event) + try session.fireEvent(event) + catch case t: Throwable => logger.error("An error occurred while an event was fired", t) def close(): Unit = eventsListener.close() diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala index b5a73373..dbeb26d0 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala @@ -2,44 +2,29 @@ package org.terminal21.client import io.circe.* import io.circe.generic.auto.* -import io.circe.syntax.* import org.slf4j.LoggerFactory -import org.terminal21.client.components.UiElement -import org.terminal21.client.components.UiElement.{HasEventHandler, allDeep} -import org.terminal21.client.components.UiElementEncoding +import org.terminal21.client.components.UiElement.{HasChildren, HasEventHandler, allDeep} +import org.terminal21.client.components.{UiComponent, UiElement, UiElementEncoding} +import org.terminal21.client.internal.EventHandlers import org.terminal21.model.* -import org.terminal21.ui.std.SessionsService +import org.terminal21.ui.std.{ServerJson, SessionsService} import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.{CountDownLatch, TimeUnit} import scala.annotation.tailrec +import scala.collection.concurrent.TrieMap class ConnectedSession(val session: Session, encoding: UiElementEncoding, val serverUrl: String, sessionsService: SessionsService, onCloseHandler: () => Unit): private val logger = LoggerFactory.getLogger(getClass) - private var elements = List.empty[UiElement] + private val handlers = new EventHandlers(this) def uiUrl: String = serverUrl + "/ui" - def clear(): Unit = synchronized: - elements = Nil + def clear(): Unit = + render() + handlers.clear() + modifiedElements.clear() - def add(es: UiElement*): Unit = - val withEvents = allDeep(es).collect: - case h: HasEventHandler => h - - for e <- withEvents do addEventHandlerAtTheTop(e.key, e.defaultEventHandler) - - synchronized: - elements = elements ::: es.toList - - private val eventHandlers = collection.concurrent.TrieMap.empty[String, List[EventHandler]] - - private def addEventHandlerAtTheTop(key: String, handler: EventHandler): Unit = - val handlers = eventHandlers.getOrElse(key, Nil) - eventHandlers += key -> (handler :: handlers) - - def addEventHandler(key: String, handler: EventHandler): Unit = - val handlers = eventHandlers.getOrElse(key, Nil) - eventHandlers += key -> (handlers :+ handler) + def addEventHandler(key: String, handler: EventHandler): Unit = handlers.addEventHandler(key, handler) private val exitLatch = new CountDownLatch(1) @@ -79,7 +64,7 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se exitLatch.countDown() onCloseHandler() case _ => - eventHandlers.get(event.key) match + handlers.getEventHandler(event.key) match case Some(handlers) => for handler <- handlers do (event, handler) match @@ -90,16 +75,43 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se case None => logger.warn(s"There is no event handler for event $event") - def render(): Unit = - val j = toJson - sessionsService.setSessionJsonState(session, j.toJson.noSpaces) - - def allElements: Seq[UiElement] = synchronized(elements) - - private def toJson: JsonObject = - import encoding.given - val elementsCopy = allElements - val json = - for e <- elementsCopy - yield e.asJson.deepDropNullValues - JsonObject(("elements", json.asJson)) + def render(es: UiElement*): Unit = + handlers.registerEventHandlers(es) + val j = toJson(es) + sessionsService.setSessionJsonState(session, j) + + def renderChanges(es: UiElement*): Unit = + for e <- es do modified(e) + val j = toJson(es) + sessionsService.changeSessionJsonState(session, j) + + private def toJson(elements: Seq[UiElement]): ServerJson = + val flat = elements.flatMap(_.flat) + val sj = ServerJson( + elements.map(_.key), + flat + .map: el => + ( + el.key, + el match + case e: UiComponent => encoding.uiElementEncoder(e).deepDropNullValues + case e: HasChildren[_] => encoding.uiElementEncoder(e.withChildren()).deepDropNullValues + case e => encoding.uiElementEncoder(e).deepDropNullValues + ) + .toMap, + flat + .map: e => + ( + e.key, + e match + case e: UiComponent => e.rendered.map(_.key) + case e: HasChildren[_] => e.children.map(_.key) + case _ => Nil + ) + .toMap + ) + sj + private val modifiedElements = TrieMap.empty[String, UiElement] + def modified(e: UiElement): Unit = + modifiedElements += e.key -> e + def currentState[A <: UiElement](e: A): A = modifiedElements.getOrElse(e.key, e).asInstanceOf[A] diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala deleted file mode 100644 index 1c4d316b..00000000 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala +++ /dev/null @@ -1,34 +0,0 @@ -package org.terminal21.client.components - -import org.terminal21.client.components.UiElement.{HasChildren, HasEventHandler, HasStyle} -import org.terminal21.client.{ConnectedSession, OnChangeEventHandler} - -sealed trait StdElement extends UiElement with HasStyle - -case class Span(key: String = Keys.nextKey, @volatile var text: String, @volatile var style: Map[String, Any] = Map.empty) extends StdElement -case class NewLine(key: String = Keys.nextKey, @volatile var style: Map[String, Any] = Map.empty) extends StdElement -case class Em(key: String = Keys.nextKey, @volatile var text: String, @volatile var style: Map[String, Any] = Map.empty) extends StdElement - -case class Header1(key: String = Keys.nextKey, @volatile var text: String, @volatile var style: Map[String, Any] = Map.empty) extends StdElement - -case class Paragraph( - key: String = Keys.nextKey, - @volatile var text: String = "", - @volatile var style: Map[String, Any] = Map.empty, - @volatile var children: Seq[UiElement] = Nil -) extends StdElement - with HasChildren[Paragraph] - -case class Input( - key: String = Keys.nextKey, - `type`: String = "text", - defaultValue: String = "", - @volatile var style: Map[String, Any] = Map.empty, - @volatile var value: String = "" -) extends StdElement - with HasEventHandler: - override def defaultEventHandler: OnChangeEventHandler = newValue => value = newValue - - def onChange(h: OnChangeEventHandler)(using session: ConnectedSession): Input = - session.addEventHandler(key, h) - this diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala index c46bec4e..44c7b279 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala @@ -5,7 +5,7 @@ import org.terminal21.client.ConnectedSession import org.terminal21.client.components.UiElement.HasStyle import org.terminal21.client.components.chakra.* -import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} /** Creates a standard UI for a calculation which may take time. While the calculation runs, the UI is grayed out, including the dataUi component. When the * calculation completes, it allows for updating the dataUi component. @@ -14,48 +14,52 @@ import java.util.concurrent.atomic.AtomicBoolean */ trait StdUiCalculation[OUT]( name: String, - dataUi: UiElement with HasStyle + dataUi: UiElement with HasStyle[_] )(using session: ConnectedSession, executor: FiberExecutor) extends Calculation[OUT] with UiComponent: - val badge = Badge() - private val running = new AtomicBoolean(false) - val recalc = Button(text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())).onClick: () => + private val running = new AtomicBoolean(false) + private val currentUi = new AtomicReference(dataUi) + + protected def updateUi(dataUi: UiElement & HasStyle[_]) = currentUi.set(dataUi) + + lazy val badge = Badge() + lazy val recalc = Button(text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())).onClick: () => if running.compareAndSet(false, true) then try reCalculate() finally running.set(false) - val header = Box(bg = "green", p = 4).withChildren( - HStack().withChildren( - Text(text = name), - badge, - recalc + override lazy val rendered: Seq[UiElement] = + val header = Box( + bg = "green", + p = 4, + children = Seq( + HStack(children = Seq(Text(text = name), badge, recalc)) + ) ) - ) - @volatile var children: Seq[UiElement] = Seq( - header, - dataUi - ) + Seq(header, dataUi) override def onError(t: Throwable): Unit = - badge.text = s"Error: ${t.getMessage}" - badge.colorScheme = Some("red") - recalc.isDisabled = None - session.render() + session.renderChanges( + badge.withText(s"Error: ${t.getMessage}").withColorScheme(Some("red")), + dataUi, + recalc.withIsDisabled(None) + ) super.onError(t) override protected def whenResultsNotReady(): Unit = - badge.text = "Calculating" - badge.colorScheme = Some("purple") - recalc.isDisabled = Some(true) - dataUi.style = dataUi.style + ("filter" -> "grayscale(100%)") - session.render() + session.renderChanges( + badge.withText("Calculating").withColorScheme(Some("purple")), + currentUi.get().withStyle(dataUi.style + ("filter" -> "grayscale(100%)")), + recalc.withIsDisabled(Some(true)) + ) super.whenResultsNotReady() override protected def whenResultsReady(results: OUT): Unit = - badge.text = "Ready" - badge.colorScheme = None - recalc.isDisabled = Some(false) - dataUi.style = dataUi.style - "filter" - session.render() + val newDataUi = currentUi.get().withStyle(dataUi.style - "filter") + session.renderChanges( + badge.withText("Ready").withColorScheme(None), + newDataUi, + recalc.withIsDisabled(Some(false)) + ) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiComponent.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiComponent.scala index bf121453..c1213662 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiComponent.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiComponent.scala @@ -4,4 +4,8 @@ import org.terminal21.client.components.UiElement.HasChildren /** A UiComponent is a UI element that is composed of a seq of other ui elements */ -trait UiComponent extends UiElement with HasChildren[UiComponent] +trait UiComponent extends UiElement: + // Note: impl as a lazy val to avoid UiElements getting a random key and try to fix the + // keys of any sub-elements the component has. + def rendered: Seq[UiElement] + override def flat = Seq(this) ++ rendered.flatMap(_.flat) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala index eca8cb17..049acba6 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala @@ -4,10 +4,15 @@ import org.terminal21.client.{ConnectedSession, EventHandler} trait UiElement: def key: String + def flat: Seq[UiElement] = Seq(this) def render()(using session: ConnectedSession): Unit = - session.add(this) - session.render() + session.render(this) + + /** Renders any changes for this element and it's children (if any). The element must previously have been added to the session. + */ + def renderChanges()(using session: ConnectedSession): Unit = + session.renderChanges(this) object UiElement: def allDeep(elements: Seq[UiElement]): Seq[UiElement] = @@ -16,20 +21,21 @@ object UiElement: case hc: HasChildren[_] => allDeep(hc.children) .flatten - trait HasChildren[A]: - this: A => - var children: Seq[UiElement] - - def withChildren(cn: UiElement*): A = - children = cn - this + trait Current[A <: UiElement]: + this: UiElement => + def current(using session: ConnectedSession): A = session.currentState(this.asInstanceOf[A]) - def addChildren(e: UiElement*): A = - children = children ++ e - this + trait HasChildren[A <: UiElement]: + this: A => + def children: Seq[UiElement] + override def flat: Seq[UiElement] = Seq(this) ++ children.flatMap(_.flat) + def withChildren(cn: UiElement*): A + def addChildren(cn: UiElement*): A = withChildren(children ++ cn: _*) trait HasEventHandler: - def defaultEventHandler: EventHandler + def defaultEventHandler(session: ConnectedSession): EventHandler - trait HasStyle: - var style: Map[String, Any] + trait HasStyle[A <: UiElement]: + def style: Map[String, Any] + def withStyle(v: Map[String, Any]): A + def withStyle(vs: (String, Any)*): A = withStyle(vs.toMap) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala index 1d312f19..04993b10 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala @@ -3,7 +3,8 @@ package org.terminal21.client.components import io.circe.* import io.circe.generic.auto.* import io.circe.syntax.* -import org.terminal21.client.components.chakra.{Box, ChakraElement} +import org.terminal21.client.components.chakra.{Box, CEJson, ChakraElement} +import org.terminal21.client.components.std.{StdEJson, StdElement} class UiElementEncoding(libs: Seq[ComponentLib]): given uiElementEncoder: Encoder[UiElement] = @@ -29,8 +30,8 @@ object StdElementEncoding extends ComponentLib: Json.obj(vs: _*) override def toJson(using Encoder[UiElement]): PartialFunction[UiElement, Json] = - case std: StdElement => std.asJson.mapObject(o => o.add("type", "Std".asJson)) - case c: ChakraElement => c.asJson.mapObject(o => o.add("type", "Chakra".asJson)) - case c: UiComponent => - val b: ChakraElement = Box(children = c.children) + case std: StdEJson => std.asJson.mapObject(o => o.add("type", "Std".asJson)) + case c: CEJson => c.asJson.mapObject(o => o.add("type", "Chakra".asJson)) + case c: UiComponent => + val b: ChakraElement[Box] = Box(key = c.key, text = "") b.asJson.mapObject(o => o.add("type", "Chakra".asJson)) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala index 54be2d48..128deac6 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala @@ -1,143 +1,220 @@ package org.terminal21.client.components.chakra -import org.terminal21.client.components.UiElement.{HasChildren, HasEventHandler, HasStyle} +import org.terminal21.client.components.UiElement.{Current, HasChildren, HasEventHandler, HasStyle} import org.terminal21.client.components.{Keys, UiElement} -import org.terminal21.client.{OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler} +import org.terminal21.client.{ConnectedSession, OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler} + +sealed trait CEJson extends UiElement /** The chakra-react based components, for a complete (though bit rough) example please see * https://github.com/kostaskougios/terminal21-restapi/blob/main/examples/src/main/scala/tests/ChakraComponents.scala and it's related scala files under * https://github.com/kostaskougios/terminal21-restapi/tree/main/examples/src/main/scala/tests/chakra */ -sealed trait ChakraElement extends UiElement with HasStyle +sealed trait ChakraElement[A <: ChakraElement[A]] extends CEJson with HasStyle[A] with Current[A] /** https://chakra-ui.com/docs/components/button */ case class Button( key: String = Keys.nextKey, - @volatile var text: String = "Ok", - @volatile var size: Option[String] = None, - @volatile var variant: Option[String] = None, - @volatile var colorScheme: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty, - @volatile var leftIcon: Option[UiElement] = None, - @volatile var rightIcon: Option[UiElement] = None, - @volatile var isActive: Option[Boolean] = None, - @volatile var isDisabled: Option[Boolean] = None, - @volatile var isLoading: Option[Boolean] = None, - @volatile var isAttached: Option[Boolean] = None, - @volatile var spacing: Option[String] = None -) extends ChakraElement - with OnClickEventHandler.CanHandleOnClickEvent[Button] + text: String = "Ok", + size: Option[String] = None, + variant: Option[String] = None, + colorScheme: Option[String] = None, + style: Map[String, Any] = Map.empty, + leftIcon: Option[UiElement] = None, + rightIcon: Option[UiElement] = None, + isActive: Option[Boolean] = None, + isDisabled: Option[Boolean] = None, + isLoading: Option[Boolean] = None, + isAttached: Option[Boolean] = None, + spacing: Option[String] = None +) extends ChakraElement[Button] + with OnClickEventHandler.CanHandleOnClickEvent[Button]: + override def withStyle(v: Map[String, Any]): Button = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + def withSize(v: Option[String]) = copy(size = v) + def withVariant(v: Option[String]) = copy(variant = v) + def withColorScheme(v: Option[String]) = copy(colorScheme = v) + def withLeftIcon(v: Option[UiElement]) = copy(leftIcon = v) + def withRightIcon(v: Option[UiElement]) = copy(rightIcon = v) + def withIsActive(v: Option[Boolean]) = copy(isActive = v) + def withIsDisabled(v: Option[Boolean]) = copy(isDisabled = v) + def withIsLoading(v: Option[Boolean]) = copy(isLoading = v) + def withIsAttached(v: Option[Boolean]) = copy(isAttached = v) + def withSpacing(v: Option[String]) = copy(spacing = v) /** https://chakra-ui.com/docs/components/button */ case class ButtonGroup( key: String = Keys.nextKey, - @volatile var variant: Option[String] = None, - @volatile var spacing: Option[String] = None, - @volatile var size: Option[String] = None, - @volatile var width: Option[String] = None, - @volatile var height: Option[String] = None, - @volatile var border: Option[String] = None, - @volatile var borderColor: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty, - @volatile var children: Seq[UiElement] = Nil -) extends ChakraElement - with HasChildren[ButtonGroup] + variant: Option[String] = None, + spacing: Option[String] = None, + size: Option[String] = None, + width: Option[String] = None, + height: Option[String] = None, + border: Option[String] = None, + borderColor: Option[String] = None, + style: Map[String, Any] = Map.empty, + children: Seq[UiElement] = Nil +) extends ChakraElement[ButtonGroup] + with HasChildren[ButtonGroup]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]): ButtonGroup = copy(style = v) + def withKey(v: String) = copy(key = v) + def withVariant(v: Option[String]) = copy(variant = v) + def withSpacing(v: Option[String]) = copy(spacing = v) + def withSize(v: Option[String]) = copy(size = v) + def withWidth(v: Option[String]) = copy(width = v) + def withHeight(v: Option[String]) = copy(height = v) + def withBorder(v: Option[String]) = copy(border = v) + def withBorderColor(v: Option[String]) = copy(borderColor = v) /** https://chakra-ui.com/docs/components/box */ case class Box( key: String = Keys.nextKey, - @volatile var text: String = "", - @volatile var bg: String = "", - @volatile var w: String = "", - @volatile var p: Int = 0, - @volatile var color: String = "", - @volatile var style: Map[String, Any] = Map.empty, - @volatile var as: Option[String] = None, - @volatile var children: Seq[UiElement] = Nil -) extends ChakraElement - with HasChildren[Box] + text: String = "", + bg: String = "", + w: String = "", + p: Int = 0, + color: String = "", + style: Map[String, Any] = Map.empty, + as: Option[String] = None, + children: Seq[UiElement] = Nil +) extends ChakraElement[Box] + with HasChildren[Box]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + def withBg(v: String) = copy(bg = v) + def withW(v: String) = copy(w = v) + def withP(v: Int) = copy(p = v) + def withColor(v: String) = copy(color = v) + def withAs(v: Option[String]) = copy(as = v) /** https://chakra-ui.com/docs/components/stack */ case class HStack( key: String = Keys.nextKey, - @volatile var spacing: Option[String] = None, - @volatile var align: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty, - @volatile var children: Seq[UiElement] = Nil -) extends ChakraElement - with HasChildren[HStack] + spacing: Option[String] = None, + align: Option[String] = None, + style: Map[String, Any] = Map.empty, + children: Seq[UiElement] = Nil +) extends ChakraElement[HStack] + with HasChildren[HStack]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withSpacing(v: Option[String]) = copy(spacing = v) + def withAlign(v: Option[String]) = copy(align = v) + case class VStack( key: String = Keys.nextKey, - @volatile var spacing: Option[String] = None, - @volatile var align: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty, - @volatile var children: Seq[UiElement] = Nil -) extends ChakraElement - with HasChildren[VStack] + spacing: Option[String] = None, + align: Option[String] = None, + style: Map[String, Any] = Map.empty, + children: Seq[UiElement] = Nil +) extends ChakraElement[VStack] + with HasChildren[VStack]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withSpacing(v: Option[String]) = copy(spacing = v) + def withAlign(v: Option[String]) = copy(align = v) case class SimpleGrid( key: String = Keys.nextKey, - @volatile var spacing: Option[String] = None, - @volatile var spacingX: Option[String] = None, - @volatile var spacingY: Option[String] = None, - @volatile var columns: Int = 2, - @volatile var children: Seq[UiElement] = Nil, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement - with HasChildren[SimpleGrid] + spacing: Option[String] = None, + spacingX: Option[String] = None, + spacingY: Option[String] = None, + columns: Int = 2, + children: Seq[UiElement] = Nil, + style: Map[String, Any] = Map.empty +) extends ChakraElement[SimpleGrid] + with HasChildren[SimpleGrid]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withSpacing(v: Option[String]) = copy(spacing = v) + def withSpacingX(v: Option[String]) = copy(spacingX = v) + def withSpacingY(v: Option[String]) = copy(spacingY = v) + def withColumns(v: Int) = copy(columns = v) /** https://chakra-ui.com/docs/components/editable */ case class Editable( key: String = Keys.nextKey, defaultValue: String = "", - @volatile var value: String = "", - @volatile var style: Map[String, Any] = Map.empty, - @volatile var children: Seq[UiElement] = Nil -) extends ChakraElement + value: String = "", + style: Map[String, Any] = Map.empty, + children: Seq[UiElement] = Nil +) extends ChakraElement[Editable] with HasEventHandler with HasChildren[Editable] with OnChangeEventHandler.CanHandleOnChangeEvent[Editable]: - if value == "" then value = defaultValue - override def defaultEventHandler: OnChangeEventHandler = newValue => value = newValue - -case class EditablePreview(key: String = Keys.nextKey, @volatile var style: Map[String, Any] = Map.empty) extends ChakraElement -case class EditableInput(key: String = Keys.nextKey, @volatile var style: Map[String, Any] = Map.empty) extends ChakraElement -case class EditableTextarea(key: String = Keys.nextKey, @volatile var style: Map[String, Any] = Map.empty) extends ChakraElement + override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = + newValue => session.modified(copy(value = newValue)) + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withDefaultValue(v: String) = copy(defaultValue = v) + def withValue(v: String) = copy(value = v) + +case class EditablePreview(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement[EditablePreview]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + +case class EditableInput(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement[EditableInput]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + +case class EditableTextarea(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement[EditableTextarea]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) /** https://chakra-ui.com/docs/components/form-control */ case class FormControl( key: String = Keys.nextKey, as: String = "", - @volatile var style: Map[String, Any] = Map.empty, - @volatile var children: Seq[UiElement] = Nil -) extends ChakraElement - with HasChildren[FormControl] + style: Map[String, Any] = Map.empty, + children: Seq[UiElement] = Nil +) extends ChakraElement[FormControl] + with HasChildren[FormControl]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withAs(v: String) = copy(as = v) /** https://chakra-ui.com/docs/components/form-control */ case class FormLabel( key: String = Keys.nextKey, - @volatile var text: String, - @volatile var style: Map[String, Any] = Map.empty, - @volatile var children: Seq[UiElement] = Nil -) extends ChakraElement - with HasChildren[FormLabel] + text: String, + style: Map[String, Any] = Map.empty, + children: Seq[UiElement] = Nil +) extends ChakraElement[FormLabel] + with HasChildren[FormLabel]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) /** https://chakra-ui.com/docs/components/form-control */ case class FormHelperText( key: String = Keys.nextKey, - @volatile var text: String, - @volatile var style: Map[String, Any] = Map.empty, - @volatile var children: Seq[UiElement] = Nil -) extends ChakraElement - with HasChildren[FormHelperText] + text: String, + style: Map[String, Any] = Map.empty, + children: Seq[UiElement] = Nil +) extends ChakraElement[FormHelperText] + with HasChildren[FormHelperText]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) /** https://chakra-ui.com/docs/components/input */ @@ -145,750 +222,1155 @@ case class Input( key: String = Keys.nextKey, `type`: String = "text", placeholder: String = "", - @volatile var size: String = "md", - @volatile var variant: Option[String] = None, - @volatile var value: String = "", - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + size: String = "md", + variant: Option[String] = None, + value: String = "", + style: Map[String, Any] = Map.empty +) extends ChakraElement[Input] with HasEventHandler with OnChangeEventHandler.CanHandleOnChangeEvent[Input]: - override def defaultEventHandler: OnChangeEventHandler = newValue => value = newValue + override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(value = newValue)) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withType(v: String) = copy(`type` = v) + def withPlaceholder(v: String) = copy(placeholder = v) + def withSize(v: String) = copy(size = v) + def withVariant(v: Option[String]) = copy(variant = v) + def withValue(v: String) = copy(value = v) case class InputGroup( key: String = Keys.nextKey, - @volatile var size: String = "md", - @volatile var style: Map[String, Any] = Map.empty, - @volatile var children: Seq[UiElement] = Nil -) extends ChakraElement - with HasChildren[InputGroup] + size: String = "md", + style: Map[String, Any] = Map.empty, + children: Seq[UiElement] = Nil +) extends ChakraElement[InputGroup] + with HasChildren[InputGroup]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withSize(v: String) = copy(size = v) case class InputLeftAddon( key: String = Keys.nextKey, - @volatile var text: String = "", - @volatile var style: Map[String, Any] = Map.empty, - @volatile var children: Seq[UiElement] = Nil -) extends ChakraElement - with HasChildren[InputLeftAddon] + text: String = "", + style: Map[String, Any] = Map.empty, + children: Seq[UiElement] = Nil +) extends ChakraElement[InputLeftAddon] + with HasChildren[InputLeftAddon]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) case class InputRightAddon( key: String = Keys.nextKey, - @volatile var text: String = "", - @volatile var style: Map[String, Any] = Map.empty, - @volatile var children: Seq[UiElement] = Nil -) extends ChakraElement - with HasChildren[InputRightAddon] + text: String = "", + style: Map[String, Any] = Map.empty, + children: Seq[UiElement] = Nil +) extends ChakraElement[InputRightAddon] + with HasChildren[InputRightAddon]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) /** https://chakra-ui.com/docs/components/checkbox */ case class Checkbox( key: String = Keys.nextKey, - @volatile var text: String = "", + text: String = "", defaultChecked: Boolean = false, - @volatile var isDisabled: Boolean = false, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + isDisabled: Boolean = false, + style: Map[String, Any] = Map.empty, + checkedV: Option[Boolean] = None +) extends ChakraElement[Checkbox] with HasEventHandler with OnChangeBooleanEventHandler.CanHandleOnChangeEvent[Checkbox]: - @volatile private var checkedV: Option[Boolean] = None - def checked: Boolean = checkedV.getOrElse(defaultChecked) - override def defaultEventHandler: OnChangeEventHandler = newValue => checkedV = Some(newValue.toBoolean) + def checked: Boolean = checkedV.getOrElse(defaultChecked) + override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(checkedV = Some(newValue.toBoolean))) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + def withDefaultChecked(v: Boolean) = copy(defaultChecked = v) + def withIsDisabled(v: Boolean) = copy(isDisabled = v) /** https://chakra-ui.com/docs/components/radio */ case class Radio( key: String = Keys.nextKey, value: String, - @volatile var text: String = "", - @volatile var colorScheme: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + text: String = "", + colorScheme: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[Radio]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withValue(v: String) = copy(value = v) + def withText(v: String) = copy(text = v) + def withColorScheme(v: Option[String]) = copy(colorScheme = v) + case class RadioGroup( key: String = Keys.nextKey, defaultValue: String = "", - @volatile var value: String = "", - @volatile var style: Map[String, Any] = Map.empty, - @volatile var children: Seq[UiElement] = Nil -) extends ChakraElement + valueV: Option[String] = None, + style: Map[String, Any] = Map.empty, + children: Seq[UiElement] = Nil +) extends ChakraElement[RadioGroup] with HasEventHandler with HasChildren[RadioGroup] with OnChangeEventHandler.CanHandleOnChangeEvent[RadioGroup]: - if value == "" then value = defaultValue - - override def defaultEventHandler: OnChangeEventHandler = newValue => value = newValue + override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(valueV = Some(newValue))) + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def value: String = valueV.getOrElse(defaultValue) + def withKey(v: String) = copy(key = v) + def withDefaultValue(v: String) = copy(defaultValue = v) case class Center( key: String = Keys.nextKey, - @volatile var text: String = "", - @volatile var children: Seq[UiElement] = Nil, - @volatile var bg: Option[String] = None, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement - with HasChildren[Center] + text: String = "", + children: Seq[UiElement] = Nil, + bg: Option[String] = None, + w: Option[String] = None, + h: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[Center] + with HasChildren[Center]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + def withBg(v: Option[String]) = copy(bg = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withColor(v: Option[String]) = copy(color = v) case class Circle( key: String = Keys.nextKey, - @volatile var text: String = "", - @volatile var children: Seq[UiElement] = Nil, - @volatile var bg: Option[String] = None, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement - with HasChildren[Circle] + text: String = "", + children: Seq[UiElement] = Nil, + bg: Option[String] = None, + w: Option[String] = None, + h: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[Circle] + with HasChildren[Circle]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + def withBg(v: Option[String]) = copy(bg = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withColor(v: Option[String]) = copy(color = v) case class Square( key: String = Keys.nextKey, - @volatile var text: String = "", - @volatile var children: Seq[UiElement] = Nil, - @volatile var bg: Option[String] = None, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement - with HasChildren[Square] + text: String = "", + children: Seq[UiElement] = Nil, + bg: Option[String] = None, + w: Option[String] = None, + h: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[Square] + with HasChildren[Square]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + def withBg(v: Option[String]) = copy(bg = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class AddIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[AddIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class ArrowBackIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[ArrowBackIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class ArrowDownIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[ArrowDownIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class ArrowForwardIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[ArrowForwardIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class ArrowLeftIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[ArrowLeftIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class ArrowRightIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[ArrowRightIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class ArrowUpIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[ArrowUpIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class ArrowUpDownIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[ArrowUpDownIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class AtSignIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[AtSignIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class AttachmentIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[AttachmentIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class BellIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[BellIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class CalendarIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[CalendarIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class ChatIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[ChatIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class CheckIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[CheckIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class CheckCircleIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[CheckCircleIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class ChevronDownIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[ChevronDownIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class ChevronLeftIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[ChevronLeftIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class ChevronRightIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[ChevronRightIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class ChevronUpIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[ChevronUpIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class CloseIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[CloseIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class CopyIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[CopyIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class DeleteIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[DeleteIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class DownloadIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[DownloadIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class DragHandleIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[DragHandleIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class EditIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[EditIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class EmailIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[EmailIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class ExternalLinkIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[ExternalLinkIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class HamburgerIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[HamburgerIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class InfoIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[InfoIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class InfoOutlineIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[InfoOutlineIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class LinkIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[LinkIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class LockIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[LockIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class MinusIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[MinusIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class MoonIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[MoonIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class NotAllowedIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[NotAllowedIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class PhoneIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[PhoneIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class PlusSquareIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[PlusSquareIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class QuestionIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[QuestionIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class QuestionOutlineIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[QuestionOutlineIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class RepeatIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[RepeatIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class RepeatClockIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[RepeatClockIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class SearchIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[SearchIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class Search2Icon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[Search2Icon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class SettingsIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[SettingsIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class SmallAddIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[SmallAddIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class SmallCloseIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[SmallCloseIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class SpinnerIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[SpinnerIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class StarIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[StarIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class SunIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[SunIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class TimeIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[TimeIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class TriangleDownIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[TriangleDownIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class TriangleUpIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[TriangleUpIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class UnlockIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[UnlockIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class UpDownIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[UpDownIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class ViewIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[ViewIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class ViewOffIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[ViewOffIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class WarningIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[WarningIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon */ case class WarningTwoIcon( key: String = Keys.nextKey, - @volatile var w: Option[String] = None, - @volatile var h: Option[String] = None, - @volatile var boxSize: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + w: Option[String] = None, + h: Option[String] = None, + boxSize: Option[String] = None, + color: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[WarningTwoIcon]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withW(v: Option[String]) = copy(w = v) + def withH(v: Option[String]) = copy(h = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withColor(v: Option[String]) = copy(color = v) /** https://chakra-ui.com/docs/components/textarea */ @@ -896,59 +1378,85 @@ case class Textarea( key: String = Keys.nextKey, `type`: String = "text", placeholder: String = "", - @volatile var size: String = "md", - @volatile var variant: Option[String] = None, - @volatile var value: String = "", - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + size: String = "md", + variant: Option[String] = None, + value: String = "", + style: Map[String, Any] = Map.empty +) extends ChakraElement[Textarea] with HasEventHandler with OnChangeEventHandler.CanHandleOnChangeEvent[Textarea]: - override def defaultEventHandler: OnChangeEventHandler = newValue => value = newValue + override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(value = newValue)) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withType(v: String) = copy(`type` = v) + def withPlaceholder(v: String) = copy(placeholder = v) + def withSize(v: String) = copy(size = v) + def withVariant(v: Option[String]) = copy(variant = v) + def withValue(v: String) = copy(value = v) /** https://chakra-ui.com/docs/components/switch */ case class Switch( key: String = Keys.nextKey, - @volatile var text: String = "", + text: String = "", defaultChecked: Boolean = false, - @volatile var isDisabled: Boolean = false, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + isDisabled: Boolean = false, + style: Map[String, Any] = Map.empty, + checkedV: Option[Boolean] = None +) extends ChakraElement[Switch] with HasEventHandler with OnChangeBooleanEventHandler.CanHandleOnChangeEvent[Switch]: - @volatile private var checkedV: Option[Boolean] = None - def checked: Boolean = checkedV.getOrElse(defaultChecked) - override def defaultEventHandler: OnChangeEventHandler = newValue => checkedV = Some(newValue.toBoolean) + def checked: Boolean = checkedV.getOrElse(defaultChecked) + override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(checkedV = Some(newValue.toBoolean))) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + def withDefaultChecked(v: Boolean) = copy(defaultChecked = v) + def withIsDisabled(v: Boolean) = copy(isDisabled = v) /** https://chakra-ui.com/docs/components/select */ case class Select( key: String = Keys.nextKey, placeholder: String = "", - @volatile var value: String = "", - @volatile var bg: Option[String] = None, - @volatile var color: Option[String] = None, - @volatile var borderColor: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty, - @volatile var children: Seq[UiElement] = Nil -) extends ChakraElement + value: String = "", + bg: Option[String] = None, + color: Option[String] = None, + borderColor: Option[String] = None, + style: Map[String, Any] = Map.empty, + children: Seq[UiElement] = Nil +) extends ChakraElement[Select] with HasEventHandler with HasChildren[Select] with OnChangeEventHandler.CanHandleOnChangeEvent[Select]: - override def defaultEventHandler: OnChangeEventHandler = newValue => value = newValue + override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(value = newValue)) + override def withStyle(v: Map[String, Any]) = copy(style = v) + override def withChildren(cn: UiElement*) = copy(children = cn) + def withKey(v: String) = copy(key = v) + def withPlaceholder(v: String) = copy(placeholder = v) + def withValue(v: String) = copy(value = v) + def withBg(v: Option[String]) = copy(bg = v) + def withColor(v: Option[String]) = copy(color = v) + def withBorderColor(v: Option[String]) = copy(borderColor = v) case class Option_( key: String = Keys.nextKey, value: String, - @volatile var text: String = "", - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + text: String = "", + style: Map[String, Any] = Map.empty +) extends ChakraElement[Option_]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withValue(v: String) = copy(value = v) + def withText(v: String) = copy(text = v) /** https://chakra-ui.com/docs/components/table/usage */ -case class TableContainer(key: String = Keys.nextKey, @volatile var children: Seq[UiElement] = Nil, @volatile var style: Map[String, Any] = Map.empty) - extends ChakraElement +case class TableContainer(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) + extends ChakraElement[TableContainer] with HasChildren[TableContainer]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) def withRowStringData(data: Seq[Seq[String]]): TableContainer = withRowData(data.map(_.map(c => Text(text = c)))) def withRowData(data: Seq[Seq[UiElement]]): TableContainer = val tableBodies = children @@ -965,86 +1473,154 @@ case class TableContainer(key: String = Keys.nextKey, @volatile var children: Se for b <- tableBodies do b.withChildren(newTrs: _*) this + override def withChildren(cn: UiElement*) = copy(children = cn) + case class Table( key: String = Keys.nextKey, - @volatile var variant: String = "simple", - @volatile var size: String = "md", - @volatile var colorScheme: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty, - @volatile var children: Seq[UiElement] = Nil -) extends ChakraElement - with HasChildren[Table] -case class TableCaption(key: String = Keys.nextKey, @volatile var text: String = "", @volatile var style: Map[String, Any] = Map.empty) extends ChakraElement -case class Thead(key: String = Keys.nextKey, @volatile var children: Seq[UiElement] = Nil, @volatile var style: Map[String, Any] = Map.empty) - extends ChakraElement - with HasChildren[Thead] -case class Tbody(key: String = Keys.nextKey, @volatile var children: Seq[UiElement] = Nil, @volatile var style: Map[String, Any] = Map.empty) - extends ChakraElement - with HasChildren[Tbody] -case class Tfoot(key: String = Keys.nextKey, @volatile var children: Seq[UiElement] = Nil, @volatile var style: Map[String, Any] = Map.empty) - extends ChakraElement - with HasChildren[Tfoot] + variant: String = "simple", + size: String = "md", + colorScheme: Option[String] = None, + style: Map[String, Any] = Map.empty, + children: Seq[UiElement] = Nil +) extends ChakraElement[Table] + with HasChildren[Table]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withVariant(v: String) = copy(variant = v) + def withSize(v: String) = copy(size = v) + def withColorScheme(v: Option[String]) = copy(colorScheme = v) + +case class TableCaption(key: String = Keys.nextKey, text: String = "", style: Map[String, Any] = Map.empty) extends ChakraElement[TableCaption]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + +case class Thead(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) + extends ChakraElement[Thead] + with HasChildren[Thead]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + +case class Tbody(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) + extends ChakraElement[Tbody] + with HasChildren[Tbody]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + +case class Tfoot(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty) + extends ChakraElement[Tfoot] + with HasChildren[Tfoot]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + case class Tr( key: String = Keys.nextKey, - @volatile var children: Seq[UiElement] = Nil, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement - with HasChildren[Tr] + children: Seq[UiElement] = Nil, + style: Map[String, Any] = Map.empty +) extends ChakraElement[Tr] + with HasChildren[Tr]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + case class Th( key: String = Keys.nextKey, - @volatile var text: String = "", + text: String = "", isNumeric: Boolean = false, - @volatile var children: Seq[UiElement] = Nil, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement - with HasChildren[Th] + children: Seq[UiElement] = Nil, + style: Map[String, Any] = Map.empty +) extends ChakraElement[Th] + with HasChildren[Th]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + def withIsNumeric(v: Boolean) = copy(isNumeric = v) + case class Td( key: String = Keys.nextKey, - @volatile var text: String = "", + text: String = "", isNumeric: Boolean = false, - @volatile var style: Map[String, Any] = Map.empty, - @volatile var children: Seq[UiElement] = Nil -) extends ChakraElement - with HasChildren[Td] + style: Map[String, Any] = Map.empty, + children: Seq[UiElement] = Nil +) extends ChakraElement[Td] + with HasChildren[Td]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + def withIsNumeric(v: Boolean) = copy(isNumeric = v) /** https://chakra-ui.com/docs/components/menu/usage */ -case class Menu(key: String = Keys.nextKey, @volatile var style: Map[String, Any] = Map.empty, @volatile var children: Seq[UiElement] = Nil) - extends ChakraElement - with HasChildren[Menu] +case class Menu(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil) + extends ChakraElement[Menu] + with HasChildren[Menu]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + case class MenuButton( key: String = Keys.nextKey, - @volatile var text: String = "", - @volatile var size: Option[String] = None, - @volatile var colorScheme: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty, - @volatile var children: Seq[UiElement] = Nil -) extends ChakraElement - with HasChildren[MenuButton] -case class MenuList(key: String = Keys.nextKey, @volatile var style: Map[String, Any] = Map.empty, @volatile var children: Seq[UiElement] = Nil) - extends ChakraElement - with HasChildren[MenuList] + text: String = "", + size: Option[String] = None, + colorScheme: Option[String] = None, + style: Map[String, Any] = Map.empty, + children: Seq[UiElement] = Nil +) extends ChakraElement[MenuButton] + with HasChildren[MenuButton]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + def withSize(v: Option[String]) = copy(size = v) + def withColorScheme(v: Option[String]) = copy(colorScheme = v) + +case class MenuList(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil) + extends ChakraElement[MenuList] + with HasChildren[MenuList]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + case class MenuItem( key: String = Keys.nextKey, - @volatile var style: Map[String, Any] = Map.empty, - @volatile var text: String = "", - @volatile var children: Seq[UiElement] = Nil -) extends ChakraElement + style: Map[String, Any] = Map.empty, + text: String = "", + children: Seq[UiElement] = Nil +) extends ChakraElement[MenuItem] with HasChildren[MenuItem] - with OnClickEventHandler.CanHandleOnClickEvent[MenuItem] + with OnClickEventHandler.CanHandleOnClickEvent[MenuItem]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) -case class MenuDivider(key: String = Keys.nextKey, @volatile var style: Map[String, Any] = Map.empty) extends ChakraElement +case class MenuDivider(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement[MenuDivider]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) case class Badge( key: String = Keys.nextKey, - @volatile var text: String = "", - @volatile var colorScheme: Option[String] = None, - @volatile var variant: Option[String] = None, - @volatile var size: String = "md", - @volatile var children: Seq[UiElement] = Nil, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement - with HasChildren[Badge] + text: String = "", + colorScheme: Option[String] = None, + variant: Option[String] = None, + size: String = "md", + children: Seq[UiElement] = Nil, + style: Map[String, Any] = Map.empty +) extends ChakraElement[Badge] + with HasChildren[Badge]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + def withColorScheme(v: Option[String]) = copy(colorScheme = v) + def withVariant(v: Option[String]) = copy(variant = v) + def withSize(v: String) = copy(size = v) /** https://chakra-ui.com/docs/components/image/usage * @@ -1054,24 +1630,40 @@ case class Badge( */ case class Image( key: String = Keys.nextKey, - @volatile var src: String = "", - @volatile var alt: String = "", - @volatile var boxSize: Option[String] = None, - @volatile var borderRadius: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + src: String = "", + alt: String = "", + boxSize: Option[String] = None, + borderRadius: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[Image]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withSrc(v: String) = copy(src = v) + def withAlt(v: String) = copy(alt = v) + def withBoxSize(v: Option[String]) = copy(boxSize = v) + def withBorderRadius(v: Option[String]) = copy(borderRadius = v) /** https://chakra-ui.com/docs/components/text */ case class Text( key: String = Keys.nextKey, - @volatile var text: String = "text.text is empty. Did you accidentally assigned the text to the `key` field?", - @volatile var fontSize: Option[String] = None, - @volatile var noOfLines: Option[Int] = None, - @volatile var color: Option[String] = None, - @volatile var as: Option[String] = None, - @volatile var align: Option[String] = None, - @volatile var casing: Option[String] = None, - @volatile var decoration: Option[String] = None, - @volatile var style: Map[String, Any] = Map.empty -) extends ChakraElement + text: String = "text.text is empty. Did you accidentally assigned the text to the `key` field?", + fontSize: Option[String] = None, + noOfLines: Option[Int] = None, + color: Option[String] = None, + as: Option[String] = None, + align: Option[String] = None, + casing: Option[String] = None, + decoration: Option[String] = None, + style: Map[String, Any] = Map.empty +) extends ChakraElement[Text]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + def withFontSize(v: Option[String]) = copy(fontSize = v) + def withNoOfLines(v: Option[Int]) = copy(noOfLines = v) + def withColor(v: Option[String]) = copy(color = v) + def withAs(v: Option[String]) = copy(as = v) + def withAlign(v: Option[String]) = copy(align = v) + def withCasing(v: Option[String]) = copy(casing = v) + def withDecoration(v: Option[String]) = copy(decoration = v) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala index fb7dd571..a3394b89 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala @@ -7,35 +7,42 @@ case class QuickTable( key: String = Keys.nextKey, variant: String = "striped", colorScheme: String = "teal", - size: String = "mg" + size: String = "mg", + style: Map[String, Any] = Map.empty, + caption: Option[String] = None, + headers: Seq[UiElement] = Nil, + rows: Seq[Seq[UiElement]] = Nil ) extends UiComponent - with HasStyle: - val head = Thead() - val body = Tbody() - - val table = Table(variant = variant, colorScheme = Some(colorScheme), size = size) - .withChildren( - head, - body + with HasStyle[QuickTable]: + def withKey(v: String) = copy(key = v) + def withVariant(v: String) = copy(variant = v) + def withColorScheme(v: String) = copy(colorScheme = v) + def withSize(v: String) = copy(size = v) + def withCaption(v: Option[String]) = copy(caption = v) + def withHeaders(v: Seq[UiElement]) = copy(headers = v) + def withRows(v: Seq[Seq[UiElement]]) = copy(rows = v) + + override lazy val rendered: Seq[UiElement] = + val head = Thead(key = key + "-th", children = Seq(Tr(children = headers.map(h => Th(children = Seq(h)))))) + val body = Tbody( + key = key + "-tb", + children = rows.map: row => + Tr(children = row.map(c => Td(children = Seq(c)))) ) - @volatile var tableContainer = TableContainer().withChildren(table) - @volatile var children: Seq[UiElement] = Seq(tableContainer) - - def headers(headers: String*): QuickTable = headersElements(headers.map(h => Text(text = h)): _*) - def headersElements(headers: UiElement*): QuickTable = - head.children = headers.map(h => Th(children = Seq(h))) - this - - def rows(data: Seq[Seq[Any]]): QuickTable = rowsElements(data.map(_.map(c => Text(text = c.toString)))) - - def rowsElements(data: Seq[Seq[UiElement]]): QuickTable = - body.children = data.map: row => - Tr(children = row.map(c => Td().withChildren(c))) - this + val table = Table( + key = key + "-t", + variant = variant, + colorScheme = Some(colorScheme), + size = size, + children = caption.map(text => TableCaption(text = text)).toSeq ++ Seq(head, body) + ) + val tableContainer = TableContainer(key = key + "-tc", style = style, children = Seq(table)) + Seq(tableContainer) - def style: Map[String, Any] = tableContainer.style - def style_=(s: Map[String, Any]): Unit = tableContainer.style = s + def headers(headers: String*): QuickTable = copy(headers = headers.map(h => Text(text = h))) + def headersElements(headers: UiElement*): QuickTable = copy(headers = headers) + def rows(data: Seq[Seq[Any]]): QuickTable = copy(rows = data.map(_.map(c => Text(text = c.toString)))) + def rowsElements(data: Seq[Seq[UiElement]]): QuickTable = copy(rows = data) - def caption(text: String): QuickTable = - table.addChildren(TableCaption(text = text)) - this + def caption(text: String): QuickTable = copy(caption = Some(text)) + override def withStyle(v: Map[String, Any]): QuickTable = copy(style = v) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/extensions.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/extensions.scala index 72585946..c0159356 100644 --- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/extensions.scala +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/extensions.scala @@ -4,5 +4,7 @@ import org.terminal21.client.ConnectedSession extension (s: Seq[UiElement]) def render()(using session: ConnectedSession): Unit = - session.add(s: _*) - session.render() + session.render(s: _*) + + def renderChanges()(using session: ConnectedSession): Unit = + session.renderChanges(s: _*) diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala new file mode 100644 index 00000000..b1384c30 --- /dev/null +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala @@ -0,0 +1,58 @@ +package org.terminal21.client.components.std + +import org.terminal21.client.components.UiElement.{Current, HasChildren, HasEventHandler, HasStyle} +import org.terminal21.client.components.{Keys, UiElement} +import org.terminal21.client.{ConnectedSession, OnChangeEventHandler} + +sealed trait StdEJson extends UiElement +sealed trait StdElement[A <: UiElement] extends StdEJson with HasStyle[A] with Current[A] + +case class Span(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Span]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + +case class NewLine(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends StdElement[NewLine]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + +case class Em(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Em]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + +case class Header1(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Header1]: + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + +case class Paragraph( + key: String = Keys.nextKey, + text: String = "", + style: Map[String, Any] = Map.empty, + children: Seq[UiElement] = Nil +) extends StdElement[Paragraph] + with HasChildren[Paragraph]: + override def withChildren(cn: UiElement*) = copy(children = cn) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withText(v: String) = copy(text = v) + +case class Input( + key: String = Keys.nextKey, + `type`: String = "text", + defaultValue: Option[String] = None, + style: Map[String, Any] = Map.empty, + value: Option[String] = None +) extends StdElement[Input] + with HasEventHandler: + override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(value = Some(newValue))) + override def withStyle(v: Map[String, Any]) = copy(style = v) + def withKey(v: String) = copy(key = v) + def withType(v: String) = copy(`type` = v) + def withDefaultValue(v: Option[String]) = copy(defaultValue = v) + def withValue(v: Option[String]) = copy(value = v) + + def onChange(h: OnChangeEventHandler)(using session: ConnectedSession): Input = + session.addEventHandler(key, h) + this diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/internal/EventHandlers.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/internal/EventHandlers.scala new file mode 100644 index 00000000..39ba715e --- /dev/null +++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/internal/EventHandlers.scala @@ -0,0 +1,28 @@ +package org.terminal21.client.internal + +import org.terminal21.client.{ConnectedSession, EventHandler} +import org.terminal21.client.components.UiElement +import org.terminal21.client.components.UiElement.{HasEventHandler, allDeep} + +class EventHandlers(session: ConnectedSession): + private val eventHandlers = collection.concurrent.TrieMap.empty[String, List[EventHandler]] + + def registerEventHandlers(es: Seq[UiElement]): Unit = synchronized: + val all = allDeep(es) + val withEvents = all.collect: + case h: HasEventHandler => h + + for e <- withEvents do addEventHandlerAtTheTop(e.key, e.defaultEventHandler(session)) + + def addEventHandler(key: String, handler: EventHandler): Unit = + val handlers = eventHandlers.getOrElse(key, Nil) + eventHandlers += key -> (handlers :+ handler) + + def getEventHandler(key: String): Option[List[EventHandler]] = eventHandlers.get(key) + + private def addEventHandlerAtTheTop(key: String, handler: EventHandler): Unit = + val handlers = eventHandlers.getOrElse(key, Nil) + eventHandlers += key -> (handler :: handlers) + + def clear(): Unit = synchronized: + eventHandlers.clear() diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionMock.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionMock.scala index ca667af7..6323b0c7 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionMock.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionMock.scala @@ -6,6 +6,11 @@ import org.terminal21.model.CommonModelBuilders.session import org.terminal21.ui.std.SessionsService object ConnectedSessionMock: - def newConnectedSessionMock: ConnectedSession = + val encoding = new UiElementEncoding(Seq(StdElementEncoding)) + val encoder = ConnectedSessionMock.encoding.uiElementEncoder + + def newConnectedSessionAndSessionServiceMock: (SessionsService, ConnectedSession) = val sessionsService = mock(classOf[SessionsService]) - new ConnectedSession(session(), new UiElementEncoding(Seq(StdElementEncoding)), "test", sessionsService, () => ()) + (sessionsService, new ConnectedSession(session(), encoding, "test", sessionsService, () => ())) + + def newConnectedSessionMock: ConnectedSession = newConnectedSessionAndSessionServiceMock._2 diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala index 42706244..5b26595c 100644 --- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala +++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala @@ -1,16 +1,63 @@ package org.terminal21.client +import org.mockito.Mockito +import org.mockito.Mockito.verify import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.matchers.should.Matchers.* +import org.terminal21.client.ConnectedSessionMock.encoder import org.terminal21.client.components.chakra.Editable +import org.terminal21.client.components.std.{Paragraph, Span} import org.terminal21.model.OnChange +import org.terminal21.ui.std.ServerJson class ConnectedSessionTest extends AnyFunSuiteLike: + test("default event handlers are invoked before user handlers"): given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock val editable = Editable() editable.onChange: newValue => - editable.value should be(newValue) + editable.current.value should be(newValue) - connectedSession.add(editable) + connectedSession.render(editable) connectedSession.fireEvent(OnChange(editable.key, "new value")) + + test("to server json"): + val (sessionService, connectedSession) = ConnectedSessionMock.newConnectedSessionAndSessionServiceMock + + val p1 = Paragraph(text = "p1") + val span1 = Span(text = "span1") + connectedSession.render(p1.withChildren(span1)) + connectedSession.render() + verify(sessionService).setSessionJsonState( + connectedSession.session, + ServerJson( + Seq(p1.key), + Map(p1.key -> encoder(p1.withChildren()), span1.key -> encoder(span1)), + Map(p1.key -> Seq(span1.key), span1.key -> Nil) + ) + ) + + test("renderChanges changes state on server"): + val (sessionService, connectedSession) = ConnectedSessionMock.newConnectedSessionAndSessionServiceMock + + val p1 = Paragraph(text = "p1") + val span1 = Span(text = "span1") + connectedSession.render(p1) + connectedSession.renderChanges(p1.withChildren(span1)) + verify(sessionService).changeSessionJsonState( + connectedSession.session, + ServerJson( + Seq(p1.key), + Map(p1.key -> encoder(p1.withChildren()), span1.key -> encoder(span1)), + Map(p1.key -> Seq(span1.key), span1.key -> Nil) + ) + ) + + test("renderChanges updates current version of component"): + given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock + + val p1 = Paragraph(text = "p1") + val span1 = Span(text = "span1") + connectedSession.render(p1) + connectedSession.renderChanges(p1.withChildren(span1)) + p1.current.children should be(Seq(span1))