Finishing support for table drag and table resize

main
Oliver Kennedy 2023-01-06 12:09:34 -05:00
parent 076000c934
commit 2478523622
Signed by: okennedy
GPG Key ID: 3E5F9B3ABD3FDB60
13 changed files with 576 additions and 119 deletions

View File

@ -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(_))
}
}

View File

@ -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) }
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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"))
}
)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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]]
)

View File

@ -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

View File

@ -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()
}
)
} }
)
}

View File

@ -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 )
}
)
}
}