Finishing support for table drag and table resize
parent
076000c934
commit
2478523622
|
@ -2,12 +2,13 @@ package net.okennedy.cells
|
|||
|
||||
import scala.io.Source
|
||||
import play.api.libs.json._
|
||||
import net.okennedy.cells.WebsocketConnection
|
||||
|
||||
object CellsServer extends cask.MainRoutes
|
||||
{
|
||||
|
||||
var layout = new state.Canvas()
|
||||
layout.addTable()
|
||||
layout.addTable(rows = 5, cols = 3)
|
||||
|
||||
def serveResource(path: String, headers: (String,String)*): cask.Response[String] =
|
||||
{
|
||||
|
@ -43,27 +44,8 @@ object CellsServer extends cask.MainRoutes
|
|||
{
|
||||
|
||||
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))
|
||||
}
|
||||
case RequestSetTablePosition(id, x, y) =>
|
||||
send(SetTablePosition(id, x, y))
|
||||
}
|
||||
}
|
||||
val connection = WebsocketConnection.connect(channel)
|
||||
cask.WsActor(connection.handle(_))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
package net.okennedy.cells
|
||||
|
||||
import scala.collection.concurrent
|
||||
import cask.endpoints.WsChannelActor
|
||||
import cask.util.Ws
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import net.okennedy.cells._
|
||||
import play.api.libs.json.Json
|
||||
|
||||
class WebsocketConnection(val id: Int, channel: WsChannelActor)
|
||||
{
|
||||
def handle(event: Ws.Event): Unit =
|
||||
{
|
||||
event match {
|
||||
case Ws.Text(data) =>
|
||||
Json.parse(data).as[WebsocketRequest] match {
|
||||
case WebsocketHello(_) =>
|
||||
for(table <- CellsServer.layout.tables.values)
|
||||
{
|
||||
send(AddTable(table.serialize))
|
||||
}
|
||||
case op: CanvasRequest =>
|
||||
CellsServer.layout.update(op)
|
||||
}
|
||||
|
||||
case Ws.Error(err) =>
|
||||
println(s"Websocket $id Error: ${err.getMessage()}")
|
||||
// WebsocketConnection.allConnect?ions.remove(id)
|
||||
|
||||
case Ws.ChannelClosed() =>
|
||||
/* Close is invoked as well */
|
||||
|
||||
case Ws.Close(code, reason) =>
|
||||
println(s"Websocket $id Closed")
|
||||
WebsocketConnection.allConnections.remove(id)
|
||||
|
||||
case Ws.Ping(value) =>
|
||||
println("ping")
|
||||
channel.send(Ws.Pong(value))
|
||||
}
|
||||
}
|
||||
|
||||
def send(msg: WebsocketResponse): Unit =
|
||||
{
|
||||
// println(s"Sending from $id")
|
||||
channel.send(
|
||||
cask.Ws.Text(
|
||||
Json.toJson(msg).toString
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object WebsocketConnection
|
||||
{
|
||||
var nextId = AtomicInteger(0)
|
||||
val allConnections = concurrent.TrieMap[Int, WebsocketConnection]()
|
||||
|
||||
def connect(channel: WsChannelActor): WebsocketConnection =
|
||||
{
|
||||
val connection = new WebsocketConnection(nextId.addAndGet(1), channel)
|
||||
allConnections.put(connection.id, connection)
|
||||
return connection
|
||||
}
|
||||
|
||||
def broadcast(msg: WebsocketResponse): Unit =
|
||||
{
|
||||
allConnections.values.foreach { _.send(msg) }
|
||||
}
|
||||
}
|
|
@ -3,6 +3,13 @@ package net.okennedy.cells.state
|
|||
import scala.collection.mutable
|
||||
import net.okennedy.cells.Identifier
|
||||
import net.okennedy.cells.serialized
|
||||
import net.okennedy.cells.CanvasOp
|
||||
import net.okennedy.cells.AddTable
|
||||
import net.okennedy.cells.WebsocketConnection
|
||||
import net.okennedy.cells.CanvasRequest
|
||||
import net.okennedy.cells.RequestSetTablePosition
|
||||
import net.okennedy.cells.RequestAddTable
|
||||
import net.okennedy.cells.TableRequest
|
||||
|
||||
class Canvas()
|
||||
{
|
||||
|
@ -18,10 +25,24 @@ class Canvas()
|
|||
return id
|
||||
}
|
||||
|
||||
def addTable(): Unit =
|
||||
def addTable(xy: Option[(Int, Int)] = None, rows: Int = 5, cols: Int = 3): Unit =
|
||||
{
|
||||
val table = new Table(genSafeTableId())
|
||||
xy.foreach { case (x, y) => table.x = x; table.y = y }
|
||||
(0 until cols).foreach { _ => table.addColumn() }
|
||||
(0 until rows).foreach { _ => table.addRow() }
|
||||
tables.put(table.id, table)
|
||||
WebsocketConnection.broadcast(
|
||||
AddTable(table.serialize)
|
||||
)
|
||||
}
|
||||
|
||||
def update(op: CanvasRequest): Unit =
|
||||
{
|
||||
op match {
|
||||
case RequestAddTable(xy) => addTable(xy)
|
||||
case top: TableRequest => tables(top.table).update(top)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,18 +1,121 @@
|
|||
package net.okennedy.cells.state
|
||||
|
||||
import scala.collection.mutable
|
||||
import net.okennedy.cells.Identifier
|
||||
import net.okennedy.cells.serialized
|
||||
import net.okennedy.cells.TableRequest
|
||||
import net.okennedy.cells.RequestSetTablePosition
|
||||
import net.okennedy.cells.RequestAppendColumn
|
||||
import net.okennedy.cells.RequestAppendRow
|
||||
import net.okennedy.cells.RequestUpdateColumn
|
||||
import net.okennedy.cells.RequestUpdateRow
|
||||
import net.okennedy.cells.SetTablePosition
|
||||
import net.okennedy.cells.WebsocketConnection
|
||||
import net.okennedy.cells.serialized.ColSpec
|
||||
import net.okennedy.cells.serialized.RowSpec
|
||||
import net.okennedy.cells.UpdateTableColumns
|
||||
import net.okennedy.cells.UpdateTableRows
|
||||
import net.okennedy.cells.{ SeqOp, SeqInsert, SeqDelete }
|
||||
import net.okennedy.cells.SeqReplace
|
||||
|
||||
class Table(val id: Identifier)
|
||||
{
|
||||
var x = 20
|
||||
var y = 20
|
||||
|
||||
val columns = mutable.ArrayBuffer[ColSpec]()
|
||||
val rows = mutable.ArrayBuffer[RowSpec]()
|
||||
|
||||
def serialize =
|
||||
serialized.Table(
|
||||
id = id,
|
||||
x = x,
|
||||
y = y,
|
||||
rows = rows.toSeq,
|
||||
columns = columns.toSeq,
|
||||
)
|
||||
|
||||
def genSafeColId(depth: Int = 0): Identifier =
|
||||
{
|
||||
val id = java.util.UUID.randomUUID()
|
||||
if(columns exists { _.id == id }){
|
||||
if(depth > 100) { throw new Exception("Too Many Columns!!!") }
|
||||
return genSafeColId(depth + 1)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
def genSafeRowId(depth: Int = 0): Identifier =
|
||||
{
|
||||
val id = java.util.UUID.randomUUID()
|
||||
if(rows exists { _.id == id }){
|
||||
if(depth > 100) { throw new Exception("Too Many Rows!!!") }
|
||||
return genSafeColId(depth + 1)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
def addColumn(width: Int = 100): Unit =
|
||||
{
|
||||
val col = ColSpec(
|
||||
id = genSafeColId(),
|
||||
width = width
|
||||
)
|
||||
columns.append(col)
|
||||
WebsocketConnection.broadcast(
|
||||
UpdateTableColumns(id, SeqInsert(columns.size-1, col))
|
||||
)
|
||||
}
|
||||
|
||||
def addRow(height: Int = 30): Unit =
|
||||
{
|
||||
val row = RowSpec(
|
||||
id = genSafeColId(),
|
||||
height = height
|
||||
)
|
||||
rows.append(row)
|
||||
WebsocketConnection.broadcast(
|
||||
UpdateTableRows(id, SeqInsert(rows.size-1, row))
|
||||
)
|
||||
}
|
||||
|
||||
def updateColumn(spec: ColSpec): Unit =
|
||||
{
|
||||
val idx = columns.indexWhere { _.id == spec.id }
|
||||
if(idx < 0){ return }
|
||||
columns.update(idx, spec)
|
||||
WebsocketConnection.broadcast(
|
||||
UpdateTableColumns(id, SeqReplace(idx, spec))
|
||||
)
|
||||
}
|
||||
|
||||
def updateRow(spec: RowSpec): Unit =
|
||||
{
|
||||
val idx = rows.indexWhere { _.id == spec.id }
|
||||
if(idx < 0){ return }
|
||||
rows.update(idx, spec)
|
||||
WebsocketConnection.broadcast(
|
||||
UpdateTableRows(id, SeqReplace(idx, spec))
|
||||
)
|
||||
}
|
||||
|
||||
def setPosition(x: Int, y: Int): Unit =
|
||||
{
|
||||
this.x = x
|
||||
this.y = y
|
||||
println(s"Position now $x, $y")
|
||||
WebsocketConnection.broadcast(SetTablePosition(id, x, y))
|
||||
}
|
||||
|
||||
def update(op: TableRequest): Unit =
|
||||
{
|
||||
op match {
|
||||
case RequestSetTablePosition(_, x, y) => setPosition(x, y)
|
||||
case RequestAppendColumn(_) => addColumn()
|
||||
case RequestAppendRow(_) => addRow()
|
||||
case RequestUpdateColumn(_, spec) => updateColumn(spec)
|
||||
case RequestUpdateRow(_, spec) => updateRow(spec)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package net.okennedy.cells
|
||||
|
||||
import play.api.libs.json.Json
|
||||
import play.api.libs.json.Format
|
||||
import play.api.libs.json.JsResult
|
||||
import play.api.libs.json.JsValue
|
||||
import play.api.libs.json.JsSuccess
|
||||
import play.api.libs.json.JsObject
|
||||
import play.api.libs.json.JsString
|
||||
|
||||
sealed trait SeqOp[T]
|
||||
{
|
||||
def apply(seq: Seq[T]): Seq[T]
|
||||
def map[B](op: T => B): SeqOp[B]
|
||||
}
|
||||
|
||||
|
||||
case class SeqInsert[T](position: Int, value: T) extends SeqOp[T]
|
||||
{
|
||||
def apply(seq: Seq[T]): Seq[T] =
|
||||
{
|
||||
val (before, after) = seq.splitAt(position)
|
||||
(before :+ value) ++ after
|
||||
}
|
||||
def map[B](op: T => B): SeqOp[B] = SeqInsert[B](position, op(value))
|
||||
}
|
||||
case class SeqDelete[T](position: Int) extends SeqOp[T]
|
||||
{
|
||||
def apply(seq: Seq[T]): Seq[T] =
|
||||
{
|
||||
val (before, after) = seq.splitAt(position)
|
||||
before ++ after.drop(1)
|
||||
}
|
||||
def map[B](op: T => B): SeqOp[B] = SeqDelete(position)
|
||||
}
|
||||
case class SeqReplace[T](position: Int, value: T) extends SeqOp[T]
|
||||
{
|
||||
def apply(seq: Seq[T]): Seq[T] =
|
||||
{
|
||||
val (before, after) = seq.splitAt(position)
|
||||
(before :+ value) ++ after.drop(1)
|
||||
}
|
||||
def map[B](op: T => B): SeqOp[B] = SeqReplace[B](position, op(value))
|
||||
}
|
||||
|
||||
object SeqOp
|
||||
{
|
||||
implicit def SeqInsertFormat[T](implicit format: Format[T]): Format[SeqInsert[T]] = Json.format
|
||||
implicit def SeqDeleteFormat[T](implicit format: Format[T]): Format[SeqDelete[T]] = Json.format
|
||||
implicit def SeqReplaceFormat[T](implicit format: Format[T]): Format[SeqReplace[T]] = Json.format
|
||||
implicit def SeqOpFormat[T](implicit format: Format[T]): Format[SeqOp[T]] =
|
||||
new Format[SeqOp[T]]{
|
||||
def reads(json: JsValue): JsResult[SeqOp[T]] =
|
||||
(json \ "op").as[String] match {
|
||||
case "insert" => JsSuccess(json.as[SeqInsert[T]])
|
||||
case "delete" => JsSuccess(json.as[SeqDelete[T]])
|
||||
case "replace" => JsSuccess(json.as[SeqReplace[T]])
|
||||
}
|
||||
|
||||
def writes(o: SeqOp[T]): JsValue =
|
||||
JsObject(
|
||||
o match {
|
||||
case x:SeqInsert[T] => Json.toJson(x).as[Map[String,JsValue]] ++ Map("op" -> JsString("insert"))
|
||||
case x:SeqDelete[T] => Json.toJson(x).as[Map[String,JsValue]] ++ Map("op" -> JsString("delete"))
|
||||
case x:SeqReplace[T] => Json.toJson(x).as[Map[String,JsValue]] ++ Map("op" -> JsString("replace"))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -8,16 +8,27 @@ case class WebsocketHello(client: String) extends WebsocketRequest
|
|||
|
||||
sealed trait CanvasRequest extends WebsocketRequest
|
||||
|
||||
case class RequestAddTable(xy: Option[(Int, Int)] = None) extends CanvasRequest
|
||||
|
||||
sealed trait TableRequest extends CanvasRequest
|
||||
{
|
||||
val table: Identifier
|
||||
}
|
||||
|
||||
case class RequestSetTablePosition(table: Identifier, x: Int, y: Int) extends TableRequest
|
||||
case class RequestAppendColumn(table: Identifier) extends TableRequest
|
||||
case class RequestUpdateColumn(table: Identifier, spec: serialized.ColSpec) extends TableRequest
|
||||
case class RequestAppendRow(table: Identifier) extends TableRequest
|
||||
case class RequestUpdateRow(table: Identifier, spec: serialized.RowSpec) extends TableRequest
|
||||
|
||||
object WebsocketRequest
|
||||
{
|
||||
implicit val WebsocketHelloFormat: Format[WebsocketHello] = Json.format
|
||||
implicit val RequestAddTableFormat: Format[RequestAddTable] = Json.format
|
||||
implicit val RequestSetTablePositionFormat: Format[RequestSetTablePosition] = Json.format
|
||||
implicit val RequestAppendColumnFormat: Format[RequestAppendColumn] = Json.format
|
||||
implicit val RequestUpdateColumnFormat: Format[RequestUpdateColumn] = Json.format
|
||||
implicit val RequestAppendRowFormat: Format[RequestAppendRow] = Json.format
|
||||
implicit val RequestUpdateRowFormat: Format[RequestUpdateRow] = Json.format
|
||||
implicit val WebsocketRequestFormat: Format[WebsocketRequest] = Json.format
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
package net.okennedy.cells
|
||||
|
||||
import play.api.libs.json._
|
||||
import net.okennedy.cells.serialized.ColSpec
|
||||
import net.okennedy.cells.serialized.RowSpec
|
||||
|
||||
sealed trait WebsocketResponse
|
||||
|
||||
|
@ -12,10 +14,14 @@ sealed trait TableOp extends CanvasOp
|
|||
val table: Identifier
|
||||
}
|
||||
case class SetTablePosition(table: Identifier, x: Int, y: Int) extends TableOp
|
||||
case class UpdateTableColumns(table: Identifier, update: SeqOp[ColSpec]) extends TableOp
|
||||
case class UpdateTableRows(table: Identifier, rows: SeqOp[RowSpec]) extends TableOp
|
||||
|
||||
object WebsocketResponse
|
||||
{
|
||||
implicit val AddTableFormat: Format[AddTable] = Json.format
|
||||
implicit val SetTablePositionResponseFormat: Format[SetTablePosition] = Json.format
|
||||
implicit val UpdateTableColumnsFormat: Format[UpdateTableColumns] = Json.format
|
||||
implicit val UpdateTableRowsFormat: Format[UpdateTableRows] = Json.format
|
||||
implicit val WebsocketResponseFormat: Format[WebsocketResponse] = Json.format
|
||||
}
|
|
@ -1,8 +1,15 @@
|
|||
package net.okennedy.cells.serialized
|
||||
|
||||
import net.okennedy.cells._
|
||||
import play.api.libs.json.Format
|
||||
import play.api.libs.json.Json
|
||||
|
||||
case class ColSpec(
|
||||
id: Identifier,
|
||||
width: Int
|
||||
)
|
||||
)
|
||||
|
||||
object ColSpec
|
||||
{
|
||||
implicit val format: Format[ColSpec] = Json.format
|
||||
}
|
|
@ -1,8 +1,15 @@
|
|||
package net.okennedy.cells.serialized
|
||||
|
||||
import net.okennedy.cells._
|
||||
import play.api.libs.json.Format
|
||||
import play.api.libs.json.Json
|
||||
|
||||
case class RowSpec(
|
||||
id: Identifier,
|
||||
height: Int
|
||||
)
|
||||
)
|
||||
|
||||
object RowSpec
|
||||
{
|
||||
implicit val format: Format[RowSpec] = Json.format
|
||||
}
|
|
@ -7,8 +7,8 @@ case class Table(
|
|||
id: Identifier,
|
||||
x: Int,
|
||||
y: Int,
|
||||
// rows: Seq[RowSpec],
|
||||
// cols: Seq[ColSpec],
|
||||
rows: Seq[RowSpec],
|
||||
columns: Seq[ColSpec],
|
||||
// data: Seq[Seq[Cell]]
|
||||
)
|
||||
|
||||
|
|
|
@ -95,6 +95,28 @@ body
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.columnResizeBar
|
||||
{
|
||||
// background-color: red;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
height: 100%;
|
||||
width: 10px;
|
||||
margin-left: -5px;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.rowResizeBar
|
||||
{
|
||||
// background-color: red;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
margin-top: -5px;
|
||||
cursor: row-resize;
|
||||
}
|
||||
}
|
||||
|
||||
.widgets
|
||||
|
|
|
@ -18,6 +18,10 @@ import com.raquo.airstream.core.Transaction
|
|||
import net.okennedy.cells.CellsUI
|
||||
import net.okennedy.cells.network.Connection
|
||||
import net.okennedy.cells.RequestSetTablePosition
|
||||
import net.okennedy.cells.UpdateTableColumns
|
||||
import net.okennedy.cells.UpdateTableRows
|
||||
import net.okennedy.cells.RequestUpdateRow
|
||||
import net.okennedy.cells.RequestUpdateColumn
|
||||
|
||||
class Table(val id: Identifier, connection: Connection)
|
||||
{
|
||||
|
@ -33,6 +37,8 @@ class Table(val id: Identifier, connection: Connection)
|
|||
new Transaction( _ =>
|
||||
this.x.set(ser.x)
|
||||
this.y.set(ser.y)
|
||||
this.columns.set(ser.columns)
|
||||
this.rows.set(ser.rows)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -43,15 +49,15 @@ class Table(val id: Identifier, connection: Connection)
|
|||
new Transaction( _ =>
|
||||
x.set(newX); y.set(newY)
|
||||
)
|
||||
case UpdateTableColumns(_, update) =>
|
||||
columns.set(update(columns.now()))
|
||||
case UpdateTableRows(_, update) =>
|
||||
rows.set(update(rows.now()))
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
))
|
||||
Var[Seq[serialized.ColSpec]](initial = Seq())
|
||||
|
||||
val columnGutters: Signal[Seq[(Identifier, Signal[ColumnGutter])]] =
|
||||
columns.signal
|
||||
|
@ -64,13 +70,7 @@ class Table(val id: Identifier, connection: Connection)
|
|||
.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),
|
||||
))
|
||||
Var[Seq[serialized.RowSpec]](initial = Seq())
|
||||
|
||||
val rowGutters: Signal[Seq[(Identifier, Signal[RowGutter])]] =
|
||||
rows.signal
|
||||
|
@ -82,15 +82,19 @@ class Table(val id: Identifier, connection: Connection)
|
|||
}
|
||||
.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 cells = Var[Map[(Identifier, Identifier), Cell]](initial =
|
||||
rows.now().zipWithIndex.flatMap { case (r, ridx) =>
|
||||
columns.now().zipWithIndex.map { case (c, cidx) =>
|
||||
(r.id, c.id) -> new Cell("["+cidx+", "+ridx+"]")
|
||||
}
|
||||
}.toMap
|
||||
)
|
||||
|
||||
val cellsByPosition: Signal[Map[(Identifier, Identifier), Signal[Cell]]] =
|
||||
cells.signal.map { _.toSeq }.split( key = _._1 )( project =
|
||||
(cellId, _, cellSignal) => (cellId, cellSignal.map { _._2 })
|
||||
).map { _.toMap }
|
||||
|
||||
val x = Var(initial = 20)
|
||||
val y = Var(initial = 20)
|
||||
|
||||
|
@ -122,17 +126,19 @@ class Table(val id: Identifier, connection: Connection)
|
|||
|
||||
onMouseDown --> {
|
||||
(evt) =>
|
||||
DragFrame.start(
|
||||
x.now(), y.now(),
|
||||
width.now()+Constants.GUTTER_WIDTH,
|
||||
height.now()+Constants.GUTTER_HEIGHT,
|
||||
evt
|
||||
) { (x, y) =>
|
||||
connection.send(
|
||||
RequestSetTablePosition(id, x, y)
|
||||
)
|
||||
if(evt.button == 0){
|
||||
DragFrame.position(
|
||||
x.now(), y.now(),
|
||||
width.now()+Constants.GUTTER_WIDTH,
|
||||
height.now()+Constants.GUTTER_HEIGHT,
|
||||
evt
|
||||
) { (x, y) =>
|
||||
connection.send(
|
||||
RequestSetTablePosition(id, x, y)
|
||||
)
|
||||
}
|
||||
evt.stopPropagation()
|
||||
}
|
||||
evt.stopPropagation()
|
||||
},
|
||||
|
||||
// Display column gutters
|
||||
|
@ -187,37 +193,132 @@ class Table(val id: Identifier, connection: Connection)
|
|||
).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 }
|
||||
rowGutters.map { _.map { case (rowKey, rowStream) =>
|
||||
div(
|
||||
className("row"),
|
||||
styleAttr <--
|
||||
rowStream.map { row =>
|
||||
s"height: ${row.spec.height}px; top: ${row.position}px"
|
||||
},
|
||||
children <--
|
||||
columnGutters.map { _.map { case (colKey, colStream) =>
|
||||
div(
|
||||
className("cell"),
|
||||
styleAttr <--
|
||||
colStream.map { col =>
|
||||
s"width: ${col.spec.width}px; left: ${col.position}px"
|
||||
},
|
||||
child <--
|
||||
cellsByPosition.flatMap {
|
||||
_.get( (rowKey, colKey) )
|
||||
.map { _.map { _.tag } }
|
||||
.getOrElse {
|
||||
Val(
|
||||
div(className("cellBody empty"), "")
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
} }
|
||||
)
|
||||
} },
|
||||
),
|
||||
|
||||
// Display the resizing bars - Up<->Down
|
||||
|
||||
children <--
|
||||
rowGutters.map { _.map {
|
||||
case (rowKey, rowStream) =>
|
||||
div(
|
||||
className("rowResizeBar"),
|
||||
styleAttr <--
|
||||
rowStream.map { col =>
|
||||
s"top: ${Constants.GUTTER_HEIGHT + col.position + col.spec.height}px"
|
||||
},
|
||||
onMouseDown --> {
|
||||
(evt) =>
|
||||
if(evt.button == 0){
|
||||
|
||||
// We can't peek into colStream without violating ownership, so just
|
||||
// recompute the position
|
||||
val (pos, height) =
|
||||
rows.now().foldLeft[Either[Int, (Int, Int)]]( Left(0) ) {
|
||||
case (Left(x), row) if row.id == rowKey =>
|
||||
Right(x -> row.height)
|
||||
case (Left(x), row) =>
|
||||
Left(x + row.height)
|
||||
case (x@Right(pos, width), _) => x
|
||||
}.right.get
|
||||
|
||||
DragFrame.height(
|
||||
x.now(),
|
||||
y.now()+pos+Constants.GUTTER_HEIGHT,
|
||||
width.now()+Constants.GUTTER_WIDTH,
|
||||
height,
|
||||
evt
|
||||
) { newHeight =>
|
||||
println(s"Drag $rowKey @ ($pos, ${pos+height}) -> $newHeight")
|
||||
connection.send(
|
||||
RequestUpdateRow(
|
||||
id,
|
||||
rows.now().find { _.id == rowKey }.get
|
||||
.copy( height = newHeight )
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
)
|
||||
evt.stopPropagation()
|
||||
}
|
||||
)
|
||||
} },
|
||||
|
||||
// Display the resizing bars - Left<->Right
|
||||
|
||||
children <--
|
||||
columnGutters.map { _.map {
|
||||
case (colKey, colStream) =>
|
||||
div(
|
||||
className("columnResizeBar"),
|
||||
styleAttr <--
|
||||
colStream.map { col =>
|
||||
s"left: ${Constants.GUTTER_WIDTH + col.position + col.spec.width}px"
|
||||
},
|
||||
onMouseDown --> {
|
||||
(evt) =>
|
||||
if(evt.button == 0){
|
||||
|
||||
// We can't peek into rowStream without violating ownership, so just
|
||||
// recompute the position
|
||||
val (pos, width) =
|
||||
columns.now().foldLeft[Either[Int, (Int, Int)]]( Left(0) ) {
|
||||
case (Left(x), col) if col.id == colKey =>
|
||||
Right(x -> col.width)
|
||||
case (Left(x), col) =>
|
||||
Left(x + col.width)
|
||||
case (x@Right(pos, width), _) => x
|
||||
}.right.get
|
||||
|
||||
DragFrame.width(
|
||||
x.now()+pos+Constants.GUTTER_WIDTH,
|
||||
y.now(),
|
||||
width,
|
||||
height.now()+Constants.GUTTER_HEIGHT,
|
||||
evt
|
||||
) { newWidth =>
|
||||
println(s"Drag $colKey @ ($pos, ${pos+width}) -> $newWidth")
|
||||
connection.send(
|
||||
RequestUpdateColumn(
|
||||
id,
|
||||
columns.now().find { _.id == colKey }.get
|
||||
.copy( width = newWidth )
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
evt.stopPropagation()
|
||||
}
|
||||
)
|
||||
} }
|
||||
)
|
||||
}
|
|
@ -3,42 +3,99 @@ 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 =
|
||||
def position(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)
|
||||
val frame = new Position(x, y, width, height, evt.pageX, evt.pageY, callback)
|
||||
Widgets.register(frame)
|
||||
}
|
||||
|
||||
def width(x: Int, y: Int, width: Int, height: Int, evt: org.scalajs.dom.MouseEvent)(callback: (Int) => Unit) =
|
||||
{
|
||||
val frame = new Width(x, y, width, height, evt.pageX, evt.pageY, callback)
|
||||
Widgets.register(frame)
|
||||
}
|
||||
|
||||
def height(x: Int, y: Int, width: Int, height: Int, evt: org.scalajs.dom.MouseEvent)(callback: (Int) => Unit) =
|
||||
{
|
||||
val frame = new Height(x, y, width, height, evt.pageX, evt.pageY, callback)
|
||||
Widgets.register(frame)
|
||||
}
|
||||
|
||||
class Position(initX: Int, initY: Int, width: Int, height: Int, cursorX: Double, cursorY: Double, callback: (Int, Int) => Unit)
|
||||
extends Widget
|
||||
{
|
||||
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
|
||||
) )
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
class Width(x: Int, y: Int, width: Int, height: Int, cursorX: Double, cursorY: Double, callback: (Int) => Unit)
|
||||
extends Widget
|
||||
{
|
||||
val curr = Var[Int](initial = width)
|
||||
|
||||
val root = div(
|
||||
className("dragFrame"),
|
||||
styleAttr <--
|
||||
curr.signal.map { case width =>
|
||||
s"left: ${x-2}px; top: ${y-2}px; height: ${height-4}px; width: ${width-4}px"
|
||||
},
|
||||
documentEvents.onMouseUp --> {
|
||||
(evt) =>
|
||||
val width = curr.now()
|
||||
callback( width )
|
||||
Widgets.remove(this)
|
||||
},
|
||||
documentEvents.onMouseMove --> {
|
||||
(evt) =>
|
||||
curr.set( (width) + (evt.pageX - cursorX).toInt )
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
class Height(x: Int, y: Int, width: Int, height: Int, cursorX: Double, cursorY: Double, callback: (Int) => Unit)
|
||||
extends Widget
|
||||
{
|
||||
val curr = Var[Int](initial = height)
|
||||
|
||||
val root = div(
|
||||
className("dragFrame"),
|
||||
styleAttr <--
|
||||
curr.signal.map { case height =>
|
||||
s"left: ${x-2}px; top: ${y-2}px; height: ${height-4}px; width: ${width-4}px"
|
||||
},
|
||||
documentEvents.onMouseUp --> {
|
||||
(evt) =>
|
||||
val height = curr.now()
|
||||
callback( height )
|
||||
Widgets.remove(this)
|
||||
},
|
||||
documentEvents.onMouseMove --> {
|
||||
(evt) =>
|
||||
curr.set( (height) + (evt.pageY - cursorY).toInt )
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue