Cells/cells/ui/src/net/okennedy/cells/sheet/Table.scala

537 lines
17 KiB
Scala

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
import net.okennedy.cells.TableOp
import net.okennedy.cells.SetTablePosition
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.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
import net.okennedy.cells.widgets.FormulaEditor
class Table(val id: Identifier, connection: Connection)
{
implicit val tableOwner: Owner =
new OneTimeOwner( () => println(s"Accessing owner of table $id after killed") )
def this(ser: serialized.Table, connection: Connection) =
{
this(
ser.id,
connection
)
new Transaction( _ =>
this.x.set(ser.x)
this.y.set(ser.y)
this.columns.set(ser.columns)
this.rows.set(ser.rows)
)
startEditing(columns.now()(1).id, rows.now()(1).id)
}
def process(op: TableOp): Unit =
{
op match {
case SetTablePosition(_, newX, newY) =>
new Transaction( _ =>
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()))
}
}
val columns =
Var[Seq[serialized.ColSpec]](initial = Seq())
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())
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[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)
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 positionAndHeightOfRow(rowKey: Identifier): (Int, Int) =
{
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, height), _) => x
}.right.get
}
def positionAndWidthOfColumn(colKey: Identifier): (Int, Int) =
{
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
}
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 startEditing(colKey: Identifier, rowKey: Identifier): Unit =
{
val (cellX, cellWidth) =
positionAndWidthOfColumn(colKey)
val (cellY, cellHeight) =
positionAndHeightOfRow(rowKey)
println(s"Editing $colKey x $rowKey")
FormulaEditor(
cellX+x.now()+Constants.GUTTER_WIDTH+(cellWidth/2),
cellY+y.now()+Constants.GUTTER_HEIGHT+cellHeight
)
}
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) =>
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()
}
},
// 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 <--
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"), "")
)
}
},
// onMouseDown is handled before onClick, so
// avoid the MouseDown event propagating to the
// parent container and triggering a drag
onMouseDown --> {
evt => evt.stopPropagation()
},
onClick --> {
evt => startEditing(colKey, rowKey)
}
)
} }
)
} },
),
// Display the resizing bars - Up<->Down
children <--
rowGutters.map { _.zipWithIndex.map {
case ((rowKey, rowStream), rowIdx) =>
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) =
positionAndHeightOfRow(rowKey)
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(
RequestUpdateRows(
id,
SeqReplace(
rowIdx,
rows.now().find { _.id == rowKey }.get
.copy( height = newHeight )
)
)
)
}
}
evt.stopPropagation()
}
)
} },
// Display the resizing bars - Left<->Right
children <--
columnGutters.map { _.zipWithIndex.map {
case ((colKey, colStream), colIdx) =>
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) =
positionAndWidthOfColumn(colKey)
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(
RequestUpdateColumns(
id,
SeqReplace(
colIdx,
columns.now().find { _.id == colKey }.get
.copy( width = newWidth )
)
)
)
}
}
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()
}
}
)
)
}