Adding column/row insert/delete

main
Oliver Kennedy 2023-01-14 13:53:50 -05:00
parent 2478523622
commit fc8b106200
Signed by: okennedy
GPG Key ID: 3E5F9B3ABD3FDB60
19 changed files with 3025 additions and 75 deletions

View File

@ -91,6 +91,19 @@ object cells extends Module {
.map { PathRef(_) }
}
def css =
T.sources {
os.walk(millSourcePath / "css")
.filter { _.ext == "css" }
.map { PathRef(_) }
}
def fonts =
T.sources {
os.walk(millSourcePath / "fonts")
.map { PathRef(_) }
}
}
def resources =
@ -110,13 +123,15 @@ object cells extends Module {
)
os.write(
target / "app" / "cells.css",
target / "app" / "css" / "cells.css",
ui.compiledSass(),
createFolders = true
)
val assets =
ui.html().map { x => (x.path -> os.rel / x.path.last) }
ui.html().map { x => (x.path -> os.rel / x.path.last) }++
ui.css().map { x => (x.path -> os.rel / "css" / x.path.last) }++
ui.fonts().map { x => (x.path -> os.rel / "fonts" / x.path.last) }
for((asset, assetTarget) <- assets){
os.copy.over(

View File

@ -7,6 +7,8 @@ import net.okennedy.cells.WebsocketConnection
object CellsServer extends cask.MainRoutes
{
override def port: Int = 4444
var layout = new state.Canvas()
layout.addTable(rows = 5, cols = 3)
@ -31,10 +33,6 @@ object CellsServer extends cask.MainRoutes
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")
@ -49,8 +47,15 @@ object CellsServer extends cask.MainRoutes
}
}
@cask.staticResources("/app")
def staticResourceRoutes() = "app"
@cask.staticResources("/css",
headers = Seq("Content-Type" -> "text/css")
)
def cssResourceRoutes() = "app/css"
@cask.staticResources("/fonts",
// headers = Seq("Content-Type" -> "text/css")
)
def fontResourceRoutes() = "app/fonts"
initialize()
}

View File

@ -13,6 +13,8 @@ class WebsocketConnection(val id: Int, channel: WsChannelActor)
{
event match {
case Ws.Text(data) =>
println(s"ZZZ: $data")
Json.parse(data).as[WebsocketRequest] match {
case WebsocketHello(_) =>
for(table <- CellsServer.layout.tables.values)

View File

@ -5,10 +5,8 @@ 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.RequestUpdateColumns
import net.okennedy.cells.RequestUpdateRows
import net.okennedy.cells.SetTablePosition
import net.okennedy.cells.WebsocketConnection
import net.okennedy.cells.serialized.ColSpec
@ -55,48 +53,48 @@ class Table(val id: Identifier)
return id
}
def updateColumns(op: SeqOp[ColSpec]): Unit =
{
val opWithValidId =
op match {
case SeqInsert(_, _) => op.map { _.copy(id = genSafeColId()) }
case SeqReplace(pos, _) => op.map { _.copy(id = columns(pos).id) }
case SeqDelete(_) => op
}
println(s"Update: $op")
opWithValidId.bufferApply(columns)
WebsocketConnection.broadcast(
UpdateTableColumns(id, opWithValidId)
)
}
def addColumn(width: Int = 100): Unit =
{
val col = ColSpec(
id = genSafeColId(),
width = width
)
columns.append(col)
updateColumns(SeqInsert(columns.length,
ColSpec(id = null, width = width)
))
}
def updateRows(op: SeqOp[RowSpec]): Unit =
{
val opWithValidId =
op match {
case SeqInsert(_, _) => op.map { _.copy(id = genSafeRowId()) }
case SeqReplace(pos, _) => op.map { _.copy(id = rows(pos).id) }
case SeqDelete(_) => op
}
opWithValidId.bufferApply(rows)
WebsocketConnection.broadcast(
UpdateTableColumns(id, SeqInsert(columns.size-1, col))
UpdateTableRows(id, opWithValidId)
)
}
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))
)
updateRows(SeqInsert(rows.length,
RowSpec(id = null, height = height)
))
}
def setPosition(x: Int, y: Int): Unit =
@ -109,12 +107,11 @@ class Table(val id: Identifier)
def update(op: TableRequest): Unit =
{
println(s"Table update: $op")
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)
case RequestUpdateColumns(_, cop) => updateColumns(cop)
case RequestUpdateRows(_, rop) => updateRows(rop)
}
}

View File

@ -1,5 +1,6 @@
package net.okennedy.cells
import scala.collection.mutable
import play.api.libs.json.Json
import play.api.libs.json.Format
import play.api.libs.json.JsResult
@ -12,6 +13,7 @@ sealed trait SeqOp[T]
{
def apply(seq: Seq[T]): Seq[T]
def map[B](op: T => B): SeqOp[B]
def bufferApply(seq: mutable.Buffer[T]): Unit
}
@ -23,6 +25,8 @@ case class SeqInsert[T](position: Int, value: T) extends SeqOp[T]
(before :+ value) ++ after
}
def map[B](op: T => B): SeqOp[B] = SeqInsert[B](position, op(value))
def bufferApply(seq: mutable.Buffer[T]): Unit =
seq.insert(position, value)
}
case class SeqDelete[T](position: Int) extends SeqOp[T]
{
@ -32,6 +36,8 @@ case class SeqDelete[T](position: Int) extends SeqOp[T]
before ++ after.drop(1)
}
def map[B](op: T => B): SeqOp[B] = SeqDelete(position)
def bufferApply(seq: mutable.Buffer[T]): Unit =
seq.remove(position)
}
case class SeqReplace[T](position: Int, value: T) extends SeqOp[T]
{
@ -41,6 +47,8 @@ case class SeqReplace[T](position: Int, value: T) extends SeqOp[T]
(before :+ value) ++ after.drop(1)
}
def map[B](op: T => B): SeqOp[B] = SeqReplace[B](position, op(value))
def bufferApply(seq: mutable.Buffer[T]): Unit =
seq(position) = value
}
object SeqOp

View File

@ -16,19 +16,15 @@ sealed trait TableRequest extends CanvasRequest
}
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
case class RequestUpdateColumns(table: Identifier, op: SeqOp[serialized.ColSpec]) extends TableRequest
case class RequestUpdateRows(table: Identifier, op: SeqOp[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 RequestUpdateColumnsFormat: Format[RequestUpdateColumns] = Json.format
implicit val RequestUpdateRowsFormat: Format[RequestUpdateRows] = Json.format
implicit val WebsocketRequestFormat: Format[WebsocketRequest] = Json.format
}

View File

@ -117,6 +117,44 @@ body
margin-top: -5px;
cursor: row-resize;
}
.addRemoveColumnWidget
{
$addRemoveColumnWidgetOffset: 16px;
position: absolute;
right: -1px-$addRemoveColumnWidgetOffset;
top: 0px;
width: $addRemoveColumnWidgetOffset;
height: 20px;
background-color: #eee;
text-align: center;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
cursor: ew-resize;
visibility: hidden;
}
.addRemoveRowWidget
{
$addRemoveRowWidgetOffset: 16px;
position: absolute;
left: 0px;
bottom: -1px-$addRemoveRowWidgetOffset;
height: $addRemoveRowWidgetOffset;
width: 20px;
background-color: #eee;
text-align: center;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
cursor: ns-resize;
visibility: hidden;
}
&:hover
{
.addRemoveColumnWidget { visibility: visible; }
.addRemoveRowWidget { visibility: visible; }
}
}
.widgets

4
cells/ui/css/font-awesome.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -4,7 +4,8 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cells</title>
<link rel="stylesheet" type="text/css" href="cells.css">
<link rel="stylesheet" type="text/css" href="css/cells.css">
<link rel="stylesheet" type="text/css" href="css/font-awesome.min.css"/>
</head>
<body>
<script type="text/javascript" src="cells.js"></script>

View File

@ -14,7 +14,7 @@ object CellsUI
@JSExport("run")
def run(): Unit =
{
val conn = new network.Connection("ws://localhost:8080/ws")
val conn = new network.Connection("ws://localhost:4444/ws")
dom.window.onload = { (evt: dom.Event) =>
val container: dom.Element =

View File

@ -5,4 +5,10 @@ object Constants
val GUTTER_HEIGHT = 20
val GUTTER_WIDTH = 30
val BORDER = 1
val DEFAULT_CELL_WIDTH = 100
val DEFAULT_CELL_HEIGHT = 30
val NULL_UUID =
java.util.UUID(0, 0)
}

View File

@ -20,8 +20,14 @@ 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
import net.okennedy.cells.RequestUpdateRows
import net.okennedy.cells.RequestUpdateColumns
import net.okennedy.cells.widgets.Icon
import scala.collection.Searching.Found
import scala.collection.Searching.InsertionPoint
import net.okennedy.cells.SeqReplace
import net.okennedy.cells.SeqInsert
import net.okennedy.cells.SeqDelete
class Table(val id: Identifier, connection: Connection)
{
@ -50,8 +56,10 @@ class Table(val id: Identifier, connection: Connection)
x.set(newX); y.set(newY)
)
case UpdateTableColumns(_, update) =>
println(s"ColUpdate??? : $update")
columns.set(update(columns.now()))
case UpdateTableRows(_, update) =>
println(s"RowUpdate??? : $update")
rows.set(update(rows.now()))
}
}
@ -106,6 +114,43 @@ class Table(val id: Identifier, connection: Connection)
rows.signal.map { _.foldLeft(0)( (h, row) => h + row.height) }
.observe
def snapIndex(snaps: Seq[Int], excessStep: Int, value: Int): Int =
{
// println(s"Snap $value")
if(snaps.size > 0 && value < snaps.last){
snaps.search(value) match {
case Found(foundIndex) =>
// println(s"Found: $foundIndex");
foundIndex
case InsertionPoint(insertionPoint) =>
if(insertionPoint == 0) {
// println(s"Insert @ start");
0
} else if(insertionPoint >= snaps.length) {
// println(s"Insert @ end");
snaps.length - 1
} else {
val lowDiff = value - snaps(insertionPoint - 1)
val highDiff = snaps(insertionPoint) - value
// println(s"Insert @ [${insertionPoint-1}, $insertionPoint] : $lowDiff <-> $highDiff");
if(lowDiff > highDiff){ insertionPoint }
else { insertionPoint-1 }
}
}
} else {
val excess = value - snaps.lastOption.getOrElse(0) + (excessStep / 2)
// println(s"Over with excess: $excess")
(snaps.length - 1) + (excess / excessStep).toInt
}
}
def snapTo(snaps: Seq[Int], excessStep: Int, value: Int): Int =
{
val idx = snapIndex(snaps, excessStep, value)
if(idx < snaps.length){ snaps(idx) }
else { snaps.lastOption.getOrElse(0) + excessStep * (idx - (snaps.length -1)) }
}
def root = div(
className("dataTable"),
@ -228,8 +273,8 @@ class Table(val id: Identifier, connection: Connection)
// Display the resizing bars - Up<->Down
children <--
rowGutters.map { _.map {
case (rowKey, rowStream) =>
rowGutters.map { _.zipWithIndex.map {
case ((rowKey, rowStream), rowIdx) =>
div(
className("rowResizeBar"),
styleAttr <--
@ -260,10 +305,13 @@ class Table(val id: Identifier, connection: Connection)
) { newHeight =>
println(s"Drag $rowKey @ ($pos, ${pos+height}) -> $newHeight")
connection.send(
RequestUpdateRow(
RequestUpdateRows(
id,
rows.now().find { _.id == rowKey }.get
.copy( height = newHeight )
SeqReplace(
rowIdx,
rows.now().find { _.id == rowKey }.get
.copy( height = newHeight )
)
)
)
}
@ -276,8 +324,8 @@ class Table(val id: Identifier, connection: Connection)
// Display the resizing bars - Left<->Right
children <--
columnGutters.map { _.map {
case (colKey, colStream) =>
columnGutters.map { _.zipWithIndex.map {
case ((colKey, colStream), colIdx) =>
div(
className("columnResizeBar"),
styleAttr <--
@ -308,10 +356,13 @@ class Table(val id: Identifier, connection: Connection)
) { newWidth =>
println(s"Drag $colKey @ ($pos, ${pos+width}) -> $newWidth")
connection.send(
RequestUpdateColumn(
RequestUpdateColumns(
id,
columns.now().find { _.id == colKey }.get
.copy( width = newWidth )
SeqReplace(
colIdx,
columns.now().find { _.id == colKey }.get
.copy( width = newWidth )
)
)
)
}
@ -319,6 +370,132 @@ class Table(val id: Identifier, connection: Connection)
evt.stopPropagation()
}
)
} }
} },
// Display the add/remove column widget
div(
className("addRemoveColumnWidget"),
Icon("caret-right"),
onMouseDown --> {
(evt) =>
if(evt.button == 0){
val snaps =
columns.now()
.foldLeft( Seq[Int]() ){
(accum, col) =>
accum :+ (accum.lastOption.getOrElse(0) + col.width )
}
// println(s"Snaps: ${snaps.mkString(", ")}")
DragFrame.snapWidth(
x.now()+Constants.GUTTER_WIDTH,
y.now(),
width.now(),
height.now()+Constants.GUTTER_HEIGHT,
evt
)(snapTo(snaps, Constants.DEFAULT_CELL_WIDTH, _)) { snap =>
val oldColCount = columns.now().length
val newColCount = snapIndex(snaps, Constants.DEFAULT_CELL_WIDTH, snap) + 1
var i = 0
if(oldColCount < newColCount)
{
for(i <- oldColCount until newColCount)
{
println(s"Adding column $i")
connection.send(
RequestUpdateColumns(
id,
SeqInsert(
i,
serialized.ColSpec(
id = Constants.NULL_UUID,
width = Constants.DEFAULT_CELL_WIDTH
)
)
)
)
}
} else if(oldColCount > newColCount)
{
for(i <- (oldColCount-1) to newColCount by -1)
{
println(s"Removing column $i")
connection.send(
RequestUpdateColumns(
id,
SeqDelete(i)
)
)
}
}
}
evt.stopPropagation()
}
}
),
div(
className("addRemoveRowWidget"),
Icon("caret-down"),
onMouseDown --> {
(evt) =>
if(evt.button == 0){
val snaps =
rows.now()
.foldLeft( Seq[Int]() ){
(accum, row) =>
accum :+ (accum.lastOption.getOrElse(0) + row.height )
}
// println(s"Snaps: ${snaps.mkString(", ")}")
DragFrame.snapHeight(
x.now(),
y.now()+Constants.GUTTER_HEIGHT,
width.now()+Constants.GUTTER_WIDTH,
height.now(),
evt
)(snapTo(snaps, Constants.DEFAULT_CELL_HEIGHT, _)) { snap =>
val oldRowCount = rows.now().length
val newRowCount = snapIndex(snaps, Constants.DEFAULT_CELL_HEIGHT, snap) + 1
var i = 0
if(oldRowCount < newRowCount)
{
for(i <- oldRowCount until newRowCount)
{
println(s"Adding row $i")
connection.send(
RequestUpdateRows(
id,
SeqInsert(
i,
serialized.RowSpec(
id = Constants.NULL_UUID,
height = Constants.DEFAULT_CELL_HEIGHT
)
)
)
)
}
} else if(oldRowCount > newRowCount)
{
for(i <- (oldRowCount-1) to newRowCount by -1)
{
println(s"Removing row $i")
connection.send(
RequestUpdateRows(
id,
SeqDelete(i)
)
)
}
}
}
evt.stopPropagation()
}
}
)
)
}

View File

@ -17,12 +17,24 @@ object DragFrame
Widgets.register(frame)
}
def snapWidth(x: Int, y: Int, width: Int, height: Int, evt: org.scalajs.dom.MouseEvent)(doSnap: Int => Int)(callback: (Int) => Unit) =
{
val frame = new Width(x, y, width, height, evt.pageX, evt.pageY, callback) { override def snap(x: Int) = doSnap(x+width)-width }
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)
}
def snapHeight(x: Int, y: Int, width: Int, height: Int, evt: org.scalajs.dom.MouseEvent)(doSnap: Int => Int)(callback: (Int) => Unit) =
{
val frame = new Height(x, y, width, height, evt.pageX, evt.pageY, callback) { override def snap(x: Int) = doSnap(x+height)-height }
Widgets.register(frame)
}
class Position(initX: Int, initY: Int, width: Int, height: Int, cursorX: Double, cursorY: Double, callback: (Int, Int) => Unit)
extends Widget
{
@ -55,6 +67,8 @@ object DragFrame
{
val curr = Var[Int](initial = width)
def snap(v: Int): Int = v
val root = div(
className("dragFrame"),
styleAttr <--
@ -69,7 +83,7 @@ object DragFrame
},
documentEvents.onMouseMove --> {
(evt) =>
curr.set( (width) + (evt.pageX - cursorX).toInt )
curr.set( (width) + snap( (evt.pageX - cursorX).toInt ) )
}
)
}
@ -79,6 +93,8 @@ object DragFrame
{
val curr = Var[Int](initial = height)
def snap(v: Int): Int = v
val root = div(
className("dragFrame"),
styleAttr <--
@ -93,7 +109,7 @@ object DragFrame
},
documentEvents.onMouseMove --> {
(evt) =>
curr.set( (height) + (evt.pageY - cursorY).toInt )
curr.set( (height) + snap( (evt.pageY - cursorY).toInt ) )
}
)
}

View File

@ -0,0 +1,14 @@
package net.okennedy.cells.widgets
import org.scalajs.dom
import com.raquo.laminar.api.L._
import com.raquo.laminar.nodes.ReactiveHtmlElement
object Icon
{
def apply(name: String): ReactiveHtmlElement[dom.html.Element]=
i(
className(s"fa fa-${name}"),
aria.hidden(true)
)
}