commit 74afa40620371eba6c128c316c98784a74e381c6 Author: Oliver Date: Wed Jan 4 11:07:04 2023 -0500 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..99051be --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/out +/.bloop +/.metals \ No newline at end of file diff --git a/build.sc b/build.sc new file mode 100644 index 0000000..38731d7 --- /dev/null +++ b/build.sc @@ -0,0 +1,133 @@ +import $ivy.`com.lihaoyi::mill-contrib-bloop:$MILL_VERSION` +import $ivy.`io.bit3:jsass:5.10.4` +import mill._ +import mill.scalalib._ +import mill.scalalib.publish._ +import mill.scalajslib._ +import coursier.maven.{ MavenRepository } +import mill.util.Ctx +import mill.api.{ Result, PathRef } +import io.bit3.jsass.{ Compiler => SassCompiler, Options => SassOptions, OutputStyle => SassOutputStyle } + + +/************************************************* + *** Overarching Target + *************************************************/ +object cells extends Module { + + val scalaVersion = "3.2.1" + + def run() = server.run() + + object shared extends Module + + object server extends ScalaModule + { + def scalaVersion = cells.scalaVersion + + def mainClass = Some("net.okennedy.cells.CellsServer") + + def ivyDeps = Agg( + ivy"com.lihaoyi::cask:0.8.3", + ivy"com.typesafe.play::play-json::2.10.0-RC7", + ) + + def sources = T.sources( + millSourcePath, + shared.millSourcePath + ) + + def resources = + T.sources( + millSourcePath / "shared" / "resources", + cells.resources() + ) + } + + object ui extends ScalaJSModule + { + def scalaVersion = cells.scalaVersion + def scalaJSVersion = "1.12.0" + + def ivyDeps = Agg( + ivy"org.scala-js::scalajs-dom::2.3.0", + ivy"com.raquo::laminar::0.14.5", + ivy"com.typesafe.play::play-json::2.10.0-RC7", + ) + + def sources = T.sources( + millSourcePath, + shared.millSourcePath + ) + + def sass = + T.sources { + os.walk(millSourcePath / "css") + .filter { _.ext == "scss" } + .map { PathRef(_) } + } + + def compiledSass = + T { + val compiler = new SassCompiler() + val options = new SassOptions() + val target = T.dest + options.setOutputStyle(SassOutputStyle.COMPRESSED) + + val src = sass().filter { _.path.last == "cells.scss" }.head + val out = target / "cells.css" + println(s"IGNORE THE FOLLOWING DEPRECATION WARNING: https://gitlab.com/jsass/jsass/-/issues/95") + val output = compiler.compileFile( + new java.net.URI((src.path.toString).toString), + new java.net.URI(out.toString), + options + ) + output.getCss + } + + def html = + T.sources { + os.walk(millSourcePath / "html") + .map { PathRef(_) } + } + + } + + def resources = + T { + val target = T.dest + + // Cells UI binary + os.copy.over( + ui.fastOpt().path, + target / "app" / "cells.js", + createFolders = true + ) + os.copy.over( + ui.fastOpt().path / os.up / (ui.fastOpt().path.last+".map"), + target / "app" / (ui.fastOpt().path.last+".map"), + createFolders = true + ) + + os.write( + target / "app" / "cells.css", + ui.compiledSass(), + createFolders = true + ) + + val assets = + ui.html().map { x => (x.path -> os.rel / x.path.last) } + + for((asset, assetTarget) <- assets){ + os.copy.over( + asset, + target / "app" / assetTarget, + createFolders = true + ) + } + + println(s"Generated UI resource dir: $target") + target + } +} + diff --git a/cells/server/src/net/okennedy/cells/CellsServer.scala b/cells/server/src/net/okennedy/cells/CellsServer.scala new file mode 100644 index 0000000..ef675dc --- /dev/null +++ b/cells/server/src/net/okennedy/cells/CellsServer.scala @@ -0,0 +1,72 @@ +package net.okennedy.cells + +import scala.io.Source +import play.api.libs.json._ + +object CellsServer extends cask.MainRoutes +{ + + var layout = new state.Canvas() + layout.addTable() + + def serveResource(path: String, headers: (String,String)*): cask.Response[String] = + { + val resource = + Source.fromInputStream( + this.getClass().getClassLoader().getResourceAsStream(path) + ).getLines.mkString + + cask.Response.apply( + resource, + headers = headers + ) + } + + @cask.get("/") + def index(request: cask.Request) = + serveResource("app/index.html", "Content-Type" -> "text/html") + + @cask.get("/cells.js") + def app(request: cask.Request) = + serveResource("app/cells.js", "Content-Type" -> "application/javascript") + + @cask.get("/cells.css") + def css(request: cask.Request) = + serveResource("app/cells.css", "Content-Type" -> "text/css") + + @cask.get("/out.js.map") + def appMap(request: cask.Request) = + serveResource("app/out.js.map", "Content-Type" -> "application/javascript") + + @cask.websocket("/ws") + def websocket(): cask.WebsocketResult = + { + + cask.WsHandler { channel => + def send(msg: WebsocketResponse): Unit = + { + channel.send( + cask.Ws.Text( + Json.toJson(msg).toString + ) + ) + + } + cask.WsActor { + case cask.Ws.Text(data) => + Json.parse(data).as[WebsocketRequest] match { + case WebsocketHello(_) => + for(table <- layout.tables.values) + { + send(AddTable(table.serialize)) + } + } + } + } + } + + @cask.staticResources("/app") + def staticResourceRoutes() = "app" + + initialize() +} \ No newline at end of file diff --git a/cells/server/src/net/okennedy/cells/state/Canvas.scala b/cells/server/src/net/okennedy/cells/state/Canvas.scala new file mode 100644 index 0000000..eb051be --- /dev/null +++ b/cells/server/src/net/okennedy/cells/state/Canvas.scala @@ -0,0 +1,27 @@ +package net.okennedy.cells.state + +import scala.collection.mutable +import net.okennedy.cells.Identifier +import net.okennedy.cells.serialized + +class Canvas() +{ + val tables = mutable.Map[Identifier, Table]() + + def genSafeTableId(depth: Int = 0): Identifier = + { + val id = java.util.UUID.randomUUID() + if(tables contains id){ + if(depth > 100) { throw new Exception("Too Many Tables!!!") } + return genSafeTableId(depth + 1) + } + return id + } + + def addTable(): Unit = + { + val table = new Table(genSafeTableId()) + tables.put(table.id, table) + } + +} \ No newline at end of file diff --git a/cells/server/src/net/okennedy/cells/state/Table.scala b/cells/server/src/net/okennedy/cells/state/Table.scala new file mode 100644 index 0000000..b8cd27b --- /dev/null +++ b/cells/server/src/net/okennedy/cells/state/Table.scala @@ -0,0 +1,14 @@ +package net.okennedy.cells.state + +import net.okennedy.cells.Identifier +import net.okennedy.cells.serialized + +class Table(var id: Identifier) +{ + + def serialize = + serialized.Table( + id + ) + +} \ No newline at end of file diff --git a/cells/shared/src/net/okennedy/cells/WebsocketRequest.scala b/cells/shared/src/net/okennedy/cells/WebsocketRequest.scala new file mode 100644 index 0000000..c12fb56 --- /dev/null +++ b/cells/shared/src/net/okennedy/cells/WebsocketRequest.scala @@ -0,0 +1,13 @@ +package net.okennedy.cells + +import play.api.libs.json._ + +sealed trait WebsocketRequest + +case class WebsocketHello(client: String) extends WebsocketRequest + +object WebsocketRequest +{ + implicit val WebsocketHelloFormat: Format[WebsocketHello] = Json.format + implicit val WebsocketRequestFormat: Format[WebsocketRequest] = Json.format +} \ No newline at end of file diff --git a/cells/shared/src/net/okennedy/cells/WebsocketResponse.scala b/cells/shared/src/net/okennedy/cells/WebsocketResponse.scala new file mode 100644 index 0000000..f229739 --- /dev/null +++ b/cells/shared/src/net/okennedy/cells/WebsocketResponse.scala @@ -0,0 +1,17 @@ +package net.okennedy.cells + +import play.api.libs.json._ + +sealed trait WebsocketResponse + +sealed trait CanvasOp extends WebsocketResponse +case class AddTable(table: serialized.Table) extends CanvasOp + +sealed trait TableOp extends CanvasOp + + +object WebsocketResponse +{ + implicit val AddTableFormat: Format[AddTable] = Json.format + implicit val WebsocketResponseFormat: Format[WebsocketResponse] = Json.format +} \ No newline at end of file diff --git a/cells/shared/src/net/okennedy/cells/serialized/Cell.scala b/cells/shared/src/net/okennedy/cells/serialized/Cell.scala new file mode 100644 index 0000000..ba6fb65 --- /dev/null +++ b/cells/shared/src/net/okennedy/cells/serialized/Cell.scala @@ -0,0 +1,5 @@ +package net.okennedy.cells.serialized + +import play.api.libs.json._ + +case class Cell(spec: String, value: JsValue) \ No newline at end of file diff --git a/cells/shared/src/net/okennedy/cells/serialized/ColSpec.scala b/cells/shared/src/net/okennedy/cells/serialized/ColSpec.scala new file mode 100644 index 0000000..8c30bab --- /dev/null +++ b/cells/shared/src/net/okennedy/cells/serialized/ColSpec.scala @@ -0,0 +1,8 @@ +package net.okennedy.cells.serialized + +import net.okennedy.cells._ + +case class ColSpec( + id: Identifier, + width: Int +) \ No newline at end of file diff --git a/cells/shared/src/net/okennedy/cells/serialized/RowSpec.scala b/cells/shared/src/net/okennedy/cells/serialized/RowSpec.scala new file mode 100644 index 0000000..9240bd5 --- /dev/null +++ b/cells/shared/src/net/okennedy/cells/serialized/RowSpec.scala @@ -0,0 +1,8 @@ +package net.okennedy.cells.serialized + +import net.okennedy.cells._ + +case class RowSpec( + id: Identifier, + height: Int +) \ No newline at end of file diff --git a/cells/shared/src/net/okennedy/cells/serialized/Table.scala b/cells/shared/src/net/okennedy/cells/serialized/Table.scala new file mode 100644 index 0000000..9548484 --- /dev/null +++ b/cells/shared/src/net/okennedy/cells/serialized/Table.scala @@ -0,0 +1,18 @@ +package net.okennedy.cells.serialized + +import play.api.libs.json._ +import net.okennedy.cells._ + +case class Table( + id: Identifier, + // x: Int, + // y: Int, + // rows: Seq[RowSpec], + // cols: Seq[ColSpec], + // data: Seq[Seq[Cell]] +) + +object Table +{ + implicit val tableFormat: Format[Table] = Json.format +} \ No newline at end of file diff --git a/cells/shared/src/net/okennedy/cells/types.scala b/cells/shared/src/net/okennedy/cells/types.scala new file mode 100644 index 0000000..56c1df6 --- /dev/null +++ b/cells/shared/src/net/okennedy/cells/types.scala @@ -0,0 +1,3 @@ +package net.okennedy.cells + +type Identifier = java.util.UUID \ No newline at end of file diff --git a/cells/ui/css/cells.scss b/cells/ui/css/cells.scss new file mode 100644 index 0000000..5cfe767 --- /dev/null +++ b/cells/ui/css/cells.scss @@ -0,0 +1,110 @@ +.debug-red +{ + background-color: red; +} + +body +{ + width: 100vw; + height: 100vh; + margin: 0px; + padding: 0px; +} + +.spreadsheetCanvas +{ + width: 100%; + height: 100%; + margin: 0px; + padding: 0px; + background-color: #eeeeee; +} + +.dataTable +{ + position: absolute; + background-color: #cccccc; + border-top-left-radius: 4px; + user-select: none; + + .columnGutters + { + position: absolute; + + .columnGutter + { + position: absolute; + border: 1px solid darkgrey; + text-align: center; + border-top-left-radius: 2px; + border-top-right-radius: 2px; + } + } + + .rowGutters + { + position: absolute; + + .rowGutter + { + position: absolute; + border: 1px solid darkgrey; + text-align: center; + border-top-left-radius: 2px; + border-top-right-radius: 2px; + + .valignHack + { + position: absolute; + width: calc(100% - 4px); + text-align: right; + margin-top: -0.5em; + height: 1em; + top: 50%; + padding-right: 4px; + } + } + } + + .content + { + background-color: #fff; + position: absolute; + + .row + { + position: absolute; + width: 100%; + + .cell + { + position: absolute; + height: 100%; + border-right: solid 1px lightgrey; + border-bottom: solid 1px lightgrey; + + .cellBody + { + position: absolute; + height: 1em; + top: 50%; + margin-top: -0.5em; + min-width: 100%; + text-align: center; + } + } + } + } +} + +.widgets +{ + .dragFrame + { + position: absolute; + background-color: rgba(127, 127, 127, .3); + margin-left: 2px; + margin-top: 2px; + border: dashed lightgrey 2px; + } +} \ No newline at end of file diff --git a/cells/ui/html/index.html b/cells/ui/html/index.html new file mode 100644 index 0000000..3a5159e --- /dev/null +++ b/cells/ui/html/index.html @@ -0,0 +1,15 @@ + + + + + + Cells + + + + + + + \ No newline at end of file diff --git a/cells/ui/src/net/okennedy/cells/CellsUI.scala b/cells/ui/src/net/okennedy/cells/CellsUI.scala new file mode 100644 index 0000000..52ce758 --- /dev/null +++ b/cells/ui/src/net/okennedy/cells/CellsUI.scala @@ -0,0 +1,37 @@ +package net.okennedy.cells + +import org.scalajs.dom +import scala.scalajs.js.annotation._ +import com.raquo.laminar.api.L._ + + +@JSExportTopLevel("Cells") +object CellsUI +{ + + // val dataTables = Var[Map[String, sheet.DataTable]](initial = Map.empty) + + @JSExport("run") + def run(): Unit = + { + val conn = new network.Connection("ws://localhost:8080/ws") + + dom.window.onload = { (evt: dom.Event) => + val container: dom.Element = + dom.document.querySelector("body") + + // val tableStream = + // dataTables.signal + // .map { _.values.toSeq } + // .split { _.id } + + render( + container, + div( + conn.canvas.root, + widgets.Widgets.root + ) + ) + } + } +} \ No newline at end of file diff --git a/cells/ui/src/net/okennedy/cells/network/Connection.scala b/cells/ui/src/net/okennedy/cells/network/Connection.scala new file mode 100644 index 0000000..32561e9 --- /dev/null +++ b/cells/ui/src/net/okennedy/cells/network/Connection.scala @@ -0,0 +1,57 @@ +package net.okennedy.cells.network + +import org.scalajs.dom +import play.api.libs.json._ +import net.okennedy.cells.WebsocketRequest +import net.okennedy.cells.WebsocketResponse +import net.okennedy.cells.WebsocketHello +import net.okennedy.cells.CanvasOp +import net.okennedy.cells.sheet + +class Connection(url: String) +{ + val canvas = new sheet.Canvas() + + var socket = getSocket() + + protected[network] def getSocket(): dom.WebSocket = + { + val s = new dom.WebSocket(url) + s.onopen = onConnected(_) + s.onclose = onClosed(_) + s.onerror = onError(_) + s.onmessage = onMessage(_) + // keepaliveTimer = setInterval(20000) { keepalive(s) } + s + } + + def send(msg: WebsocketRequest): Unit = + { + socket.send(Json.toJson(msg).toString) + } + + + def onConnected(event: dom.Event): Unit = + { + println("Connected") + send(WebsocketHello("itsame")) + } + + def onClosed(event: dom.Event): Unit = + { + println("Closed") + } + def onError(event: dom.Event): Unit = + { + println("Error!") + } + + def onMessage(message: dom.MessageEvent): Unit = + { + // println(s"Received: ${message.data.toString.take(200)}") + val request = Json.parse(message.data.toString).as[WebsocketResponse] + request match { + case x:CanvasOp => canvas.process(x) + } + } +} \ No newline at end of file diff --git a/cells/ui/src/net/okennedy/cells/sheet/Canvas.scala b/cells/ui/src/net/okennedy/cells/sheet/Canvas.scala new file mode 100644 index 0000000..d60e0da --- /dev/null +++ b/cells/ui/src/net/okennedy/cells/sheet/Canvas.scala @@ -0,0 +1,28 @@ +package net.okennedy.cells.sheet + +import scala.collection.mutable +import com.raquo.airstream.state.Var +import net.okennedy.cells._ +import com.raquo.laminar.api.L._ + +class Canvas() +{ + val tables = Var[Map[Identifier, Table]](initial = Map.empty) + + def process(op: CanvasOp): Unit = + { + op match { + case AddTable(table) => + tables.set(tables.now() ++ Map(table.id -> new Table(table))) + } + } + + val tableNodes = + tables.signal.map { _.values.toSeq } + .split( key = _.id )( (k, tabl, sig) => tabl.root ) + + val root = div( + className("spreadsheetCanvas"), + children <-- tableNodes + ) +} \ No newline at end of file diff --git a/cells/ui/src/net/okennedy/cells/sheet/Cell.scala b/cells/ui/src/net/okennedy/cells/sheet/Cell.scala new file mode 100644 index 0000000..0a0c95a --- /dev/null +++ b/cells/ui/src/net/okennedy/cells/sheet/Cell.scala @@ -0,0 +1,12 @@ +package net.okennedy.cells.sheet + +import com.raquo.laminar.api.L._ + +class Cell(val data: String) +{ + val tag = + div( + className("cellBody text"), + data + ) +} \ No newline at end of file diff --git a/cells/ui/src/net/okennedy/cells/sheet/ColumnGutter.scala b/cells/ui/src/net/okennedy/cells/sheet/ColumnGutter.scala new file mode 100644 index 0000000..84bc79e --- /dev/null +++ b/cells/ui/src/net/okennedy/cells/sheet/ColumnGutter.scala @@ -0,0 +1,19 @@ +package net.okennedy.cells.sheet + +import net.okennedy.cells.Identifier +import net.okennedy.cells.serialized + +class ColumnGutter(val spec: serialized.ColSpec, val position: Int, val idx: Int) +{ + def label: String = + { + var i = idx + var l = "" + while(true) { + l += ('A'.toByte + (i % 26)).toChar.toString + if(i < 26) { return l } + i /= 26 + } + return "???" + } +} \ No newline at end of file diff --git a/cells/ui/src/net/okennedy/cells/sheet/Constants.scala b/cells/ui/src/net/okennedy/cells/sheet/Constants.scala new file mode 100644 index 0000000..e7bedfe --- /dev/null +++ b/cells/ui/src/net/okennedy/cells/sheet/Constants.scala @@ -0,0 +1,8 @@ +package net.okennedy.cells.sheet + +object Constants +{ + val GUTTER_HEIGHT = 20 + val GUTTER_WIDTH = 30 + val BORDER = 1 +} \ No newline at end of file diff --git a/cells/ui/src/net/okennedy/cells/sheet/RowGutter.scala b/cells/ui/src/net/okennedy/cells/sheet/RowGutter.scala new file mode 100644 index 0000000..4566420 --- /dev/null +++ b/cells/ui/src/net/okennedy/cells/sheet/RowGutter.scala @@ -0,0 +1,9 @@ +package net.okennedy.cells.sheet + +import net.okennedy.cells.Identifier +import net.okennedy.cells.serialized + +class RowGutter(val spec: serialized.RowSpec, val position: Int, val idx: Int) +{ + def label = (idx+1).toString +} \ No newline at end of file diff --git a/cells/ui/src/net/okennedy/cells/sheet/Table.scala b/cells/ui/src/net/okennedy/cells/sheet/Table.scala new file mode 100644 index 0000000..6c47cda --- /dev/null +++ b/cells/ui/src/net/okennedy/cells/sheet/Table.scala @@ -0,0 +1,198 @@ +package net.okennedy.cells.sheet + +import com.raquo.laminar.api.L._ +import net.okennedy.cells.Identifier +import net.okennedy.cells.serialized +import com.raquo.airstream.core.Signal +import com.raquo.airstream.combine.MergeEventStream +import play.api.libs.json._ +import com.raquo.airstream.core.EventStream +import com.raquo.airstream.flatten.ConcurrentEventStream +import com.raquo.airstream.flatten.FlattenStrategy.ConcurrentStreamStrategy +import com.raquo.airstream.flatten.FlattenStrategy +import net.okennedy.cells.widgets.DragFrame +import com.raquo.airstream.ownership.OneTimeOwner + +class Table(val id: Identifier) +{ + implicit val tableOwner: Owner = + new OneTimeOwner( () => println(s"Accessing owner of table $id after killed") ) + + def this(ser: serialized.Table) = + { + this( + ser.id + ) + } + + val columns = + Var[Seq[serialized.ColSpec]](initial = Seq( + serialized.ColSpec(java.util.UUID.randomUUID(), width = 100), + serialized.ColSpec(java.util.UUID.randomUUID(), width = 100), + serialized.ColSpec(java.util.UUID.randomUUID(), width = 100), + )) + + val columnGutters: Signal[Seq[(Identifier, Signal[ColumnGutter])]] = + columns.signal + .map { + _.foldLeft( (0, 0, Seq[ColumnGutter]()) ) { + case ((pos, idx, accum), col) => + (pos+col.width, idx+1, accum :+ new ColumnGutter(col, pos, idx)) + }._3 + } + .split(_.spec.id)( project = (key, _, signal) => key -> signal) + + val rows = + Var[Seq[serialized.RowSpec]](initial = Seq( + serialized.RowSpec(java.util.UUID.randomUUID(), height = 30), + serialized.RowSpec(java.util.UUID.randomUUID(), height = 30), + serialized.RowSpec(java.util.UUID.randomUUID(), height = 30), + serialized.RowSpec(java.util.UUID.randomUUID(), height = 30), + serialized.RowSpec(java.util.UUID.randomUUID(), height = 30), + )) + + val rowGutters: Signal[Seq[(Identifier, Signal[RowGutter])]] = + rows.signal + .map { + _.foldLeft( (0, 0, Seq[RowGutter]()) ) { + case ((pos, idx, accum), col) => + (pos+col.height, idx+1, accum :+ new RowGutter(col, pos, idx)) + }._3 + } + .split(_.spec.id)( project = (key, _, signal) => key -> signal) + + val cells = Var[Seq[(Identifier, Seq[(Identifier, Cell)])]](initial = + rows.now().zipWithIndex.map { case (r, ridx) => + r.id -> + columns.now().zipWithIndex.map { case (c, cidx) => + c.id -> new Cell("["+cidx+", "+ridx+"]") + } + } + ) + + val x = Var(initial = 20) + val y = Var(initial = 20) + + val width = + columns.signal.map { _.foldLeft(0)( (w, col) => w + col.width) } + .observe + + val height = + rows.signal.map { _.foldLeft(0)( (h, row) => h + row.height) } + .observe + + def root = div( + className("dataTable"), + + // Table position and size information (reactive) + + styleAttr <-- + Signal.combine(x, y, width, height) + .map { case (x, y, w, h) => + Seq( + s"left: ${x}px;", + s"top: ${y}px;", + s"width: ${w+Constants.GUTTER_WIDTH}px;", + s"height: ${h+Constants.GUTTER_HEIGHT}px;" + ).mkString(" ") + }, + + // Initiate a Frame Drag session on click + + onMouseDown --> { + (evt) => + DragFrame.start( + x.now(), y.now(), + width.now()+Constants.GUTTER_WIDTH, + height.now()+Constants.GUTTER_HEIGHT, + evt + ) { (x, y) => println(s"Move to : $x, $y")} + evt.stopPropagation() + }, + + // Display column gutters + + div( + className("columnGutters"), + styleAttr(s"left: ${Constants.GUTTER_WIDTH}px"), + children <-- + columnGutters.map { _.map { case (_, signal) => + div( + className("columnGutter"), + styleAttr <-- signal.map { case col => + s"width: ${col.spec.width-Constants.BORDER}px; height: ${Constants.GUTTER_HEIGHT-2*Constants.BORDER}px; left: ${col.position}px" + }, + child <-- signal.map { _.label } + ) + }}, + ), + + // Display row gutters + + div( + className("rowGutters"), + styleAttr(s"top: ${Constants.GUTTER_HEIGHT}px"), + children <-- + rowGutters.map { _.map { case (_, signal) => + div( + className("rowGutter"), + styleAttr <-- signal.map { case row => + s"width: ${Constants.GUTTER_WIDTH-2*Constants.BORDER}px; height: ${row.spec.height-Constants.BORDER}px; top: ${row.position}px" + }, + div( + className("valignHack"), + child <-- signal.map { _.label } + ) + ) + }}, + ), + + // Display the actual content + + div( + className("content"), + styleAttr <-- + Signal.combine(width, height) + .map { case (w, h) => + Seq( + s"width: ${w}px;", + s"height: ${h}px;", + s"left: ${Constants.GUTTER_WIDTH}px;", + s"top: ${Constants.GUTTER_HEIGHT}px", + ).mkString(" ") + }, + children <-- + cells.signal.split( key = _._1 ){ + (rowKey, _, rowCellStream) => + val rowGutterStream: Signal[RowGutter] = + rowGutters.flatMap { + _.find(_._1 == rowKey).get._2:Signal[RowGutter] + } + div( + className("row"), + styleAttr <-- + rowGutterStream.map { row => + s"height: ${row.spec.height}px; top: ${row.position}px" + }, + children <-- + rowCellStream.map { _._2 }.split( key = _._1 ) { + (colKey, _, cellStream) => + val cellGutterStream: Signal[ColumnGutter] = + columnGutters.flatMap { + _.find(_._1 == colKey).get._2:Signal[ColumnGutter] + } + div( + className("cell"), + styleAttr <-- + cellGutterStream.map { col => + s"width: ${col.spec.width}px; left: ${col.position}px" + }, + child <-- + cellStream.map { _._2.tag } + ) + } + ) + }, + ) + ) +} \ No newline at end of file diff --git a/cells/ui/src/net/okennedy/cells/widgets/DragFrame.scala b/cells/ui/src/net/okennedy/cells/widgets/DragFrame.scala new file mode 100644 index 0000000..143c831 --- /dev/null +++ b/cells/ui/src/net/okennedy/cells/widgets/DragFrame.scala @@ -0,0 +1,44 @@ +package net.okennedy.cells.widgets + +import com.raquo.laminar.api.L._ +import com.raquo.airstream.ownership.OneTimeOwner + +class DragFrame(initX: Int, initY: Int, width: Int, height: Int, cursorX: Double, cursorY: Double, callback: (Int, Int) => Unit) + extends Widget +{ + implicit val tableOwner: Owner = + new OneTimeOwner( () => println(s"Accessing owner of DragFrame after killed") ) + + val curr = Var[(Int, Int)](initial = (initX, initY)) + + val root = div( + className("dragFrame"), + styleAttr <-- + curr.signal.map { case (x, y) => + s"left: ${x-2}px; top: ${y-2}px; height: ${height-4}px; width: ${width-4}px" + }, + documentEvents.onMouseUp --> { + (evt) => + val (x, y) = curr.now() + callback( x, y ) + Widgets.remove(this) + }, + documentEvents.onMouseMove --> { + (evt) => + curr.set( ( + initX + (evt.pageX - cursorX).toInt, + initY + (evt.pageY - cursorY).toInt + ) ) + } + ) + +} + +object DragFrame +{ + def start(x: Int, y: Int, width: Int, height: Int, evt: org.scalajs.dom.MouseEvent)(callback: (Int, Int) => Unit): Unit = + { + val frame = new DragFrame(x, y, width, height, evt.pageX, evt.pageY, callback) + Widgets.register(frame) + } +} \ No newline at end of file diff --git a/cells/ui/src/net/okennedy/cells/widgets/Widgets.scala b/cells/ui/src/net/okennedy/cells/widgets/Widgets.scala new file mode 100644 index 0000000..73c02cc --- /dev/null +++ b/cells/ui/src/net/okennedy/cells/widgets/Widgets.scala @@ -0,0 +1,30 @@ +package net.okennedy.cells.widgets + +import com.raquo.laminar.api.L._ + +trait Widget +{ + val root: com.raquo.laminar.nodes.ReactiveHtmlElement[org.scalajs.dom.html.Element] +} + +object Widgets +{ + val widgets = Var[List[Widget]](initial = Nil) + + val root = div( + className("widgets"), + children <-- widgets.signal.map { _.map { _.root } } + ) + + def register(widget: Widget): Unit = + { + widgets.set(widget :: widgets.now()) + } + + def remove(widget: Widget): Unit = + { + + println(s"Removing: $widget from ${widgets.now()}") + widgets.set(widgets.now().filter { _ ne widget }) + } +} \ No newline at end of file