mirror of
https://gitlab.com/cc-ru/ocelot/ocelot-desktop.git
synced 2025-12-20 02:59:19 +01:00
234 lines
6.9 KiB
Scala
234 lines
6.9 KiB
Scala
package ocelot.desktop.ui.widget
|
|
|
|
import ocelot.desktop.ColorScheme
|
|
import ocelot.desktop.color.RGBAColorNorm
|
|
import ocelot.desktop.geometry.{Padding2D, Rect2D, Size2D, Vector2D}
|
|
import ocelot.desktop.graphics.{Graphics, IconSource}
|
|
import ocelot.desktop.ui.UiHandler
|
|
import ocelot.desktop.ui.event.{ClickEvent, MouseEvent}
|
|
import ocelot.desktop.ui.event.handlers.MouseHandler
|
|
import ocelot.desktop.ui.layout.{Layout, LinearLayout}
|
|
import ocelot.desktop.ui.widget.LogWidget.{BorderThickness, EntryMargin, EntryPadding, LogEntry}
|
|
import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
|
|
import ocelot.desktop.util.{DrawUtils, Orientation}
|
|
|
|
import scala.annotation.tailrec
|
|
import scala.collection.mutable
|
|
|
|
abstract class LogWidget extends Widget {
|
|
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
|
|
|
|
private object MessageListWidget extends Widget with MouseHandler { messageList =>
|
|
override protected val layout: Layout = new Layout(this)
|
|
|
|
private val entries = mutable.ArrayDeque.empty[Entry]
|
|
|
|
val dummyEntry: Entry = new RxEntry("", nextMessageY)
|
|
|
|
override def receiveClickEvents: Boolean = true
|
|
|
|
eventHandlers += {
|
|
case ClickEvent(MouseEvent.Button.Right, mouse) =>
|
|
visibleEntries.find(_.absoluteBounds.contains(mouse)).foreach {
|
|
case entry: MessageEntry =>
|
|
val menu = new ContextMenu
|
|
menu.addEntry(ContextMenuEntry("Copy", IconSource.Icons.Copy) {
|
|
UiHandler.clipboard = entry.message
|
|
})
|
|
root.get.contextMenus.open(menu)
|
|
}
|
|
}
|
|
|
|
private def removedOffset: Float = entries.headOption.map(_.y - EntryMargin).getOrElse(0)
|
|
|
|
private def applyRemovedOffset(): Unit = {
|
|
val offset = removedOffset
|
|
|
|
for (entry <- entries) {
|
|
entry.y -= offset
|
|
}
|
|
}
|
|
|
|
override def minimumSize: Size2D = Size2D(
|
|
dummyEntry.minimumSize.width + 2 * EntryMargin,
|
|
nextMessageY - removedOffset,
|
|
)
|
|
|
|
private def nextMessageY: Float = {
|
|
entries.lastOption.map(_.maxY).getOrElse(0f) + EntryMargin
|
|
}
|
|
|
|
def addEntry(entry: LogEntry): Unit = {
|
|
entries += (entry match {
|
|
case LogEntry.Rx(message) => new RxEntry(message, nextMessageY)
|
|
case LogEntry.Tx(message) => new TxEntry(message, nextMessageY)
|
|
})
|
|
|
|
parent.get.recalculateBoundsAndRelayout()
|
|
}
|
|
|
|
def removeFirst(count: Int): Unit = {
|
|
if (count >= entries.length) {
|
|
entries.clear()
|
|
} else {
|
|
entries.dropInPlace(count)
|
|
}
|
|
|
|
parent.get.recalculateBoundsAndRelayout()
|
|
}
|
|
|
|
@tailrec
|
|
private def firstVisibleIdxSearch(y: Float, from: Int = 0, to: Int = entries.length): Int = {
|
|
// search the range [from, to)
|
|
if (to <= from) to
|
|
else {
|
|
val idx = from + (to - from - 1) / 2
|
|
val entryY = entries(idx).maxY
|
|
|
|
if (entryY <= y) firstVisibleIdxSearch(y, idx + 1, to)
|
|
else firstVisibleIdxSearch(y, from, idx)
|
|
}
|
|
}
|
|
|
|
private def visibleEntries: Iterator[Entry] = {
|
|
val firstVisibleIdx = firstVisibleIdxSearch(parent.get.asInstanceOf[ScrollView].offset.y)
|
|
entries.iterator.drop(firstVisibleIdx).takeWhile(_.absoluteBounds.y <= clippedBounds.max.y)
|
|
}
|
|
|
|
override def draw(g: Graphics): Unit = {
|
|
applyRemovedOffset()
|
|
for (entry <- visibleEntries) {
|
|
entry.draw(g)
|
|
}
|
|
super.draw(g)
|
|
}
|
|
|
|
trait Entry {
|
|
var y: Float
|
|
|
|
def maxY: Float
|
|
|
|
def draw(g: Graphics): Unit
|
|
|
|
def minimumSize: Size2D
|
|
|
|
def size: Size2D
|
|
|
|
private def position: Vector2D = Vector2D(EntryMargin, y)
|
|
|
|
final def absoluteBounds: Rect2D = Rect2D(position + messageList.position, size)
|
|
}
|
|
|
|
private abstract class MessageEntry(val message: String, override var y: Float) extends Entry {
|
|
private val lines = Text.wrap(message, Some(textWidth))
|
|
|
|
def minimumSize: Size2D = Size2D(
|
|
textWidth * 8 +
|
|
EntryPadding * 2 +
|
|
BorderThickness * 2,
|
|
Text.CharHeight * (lines.length max 1) +
|
|
EntryPadding * 2 +
|
|
BorderThickness * 2,
|
|
)
|
|
|
|
def size: Size2D = Size2D(
|
|
minimumSize.width max (messageList.size.width - 2 * EntryMargin),
|
|
minimumSize.height,
|
|
)
|
|
|
|
override def maxY: Float = y + minimumSize.height
|
|
|
|
def background: RGBAColorNorm
|
|
def foreground: RGBAColorNorm
|
|
def border: RGBAColorNorm
|
|
|
|
override def draw(g: Graphics): Unit = {
|
|
val innerBounds = absoluteBounds.inflated(-BorderThickness)
|
|
g.rect(innerBounds, background)
|
|
DrawUtils.ring(
|
|
g,
|
|
absoluteBounds.x,
|
|
absoluteBounds.y,
|
|
absoluteBounds.w,
|
|
absoluteBounds.h,
|
|
BorderThickness,
|
|
border,
|
|
)
|
|
|
|
g.foreground = foreground
|
|
DrawUtils.drawTextLines(
|
|
g,
|
|
absoluteBounds.x + BorderThickness + EntryPadding,
|
|
absoluteBounds.y + BorderThickness + EntryPadding,
|
|
lines,
|
|
)
|
|
}
|
|
}
|
|
|
|
private class RxEntry(message: String, _y: Float) extends MessageEntry(message, _y) {
|
|
override def background: RGBAColorNorm = ColorScheme("LogEntryBackground")
|
|
override def foreground: RGBAColorNorm = ColorScheme("Label")
|
|
override def border: RGBAColorNorm = ColorScheme("LogEntryBorder")
|
|
}
|
|
|
|
private class TxEntry(message: String, _y: Float) extends MessageEntry(message, _y) {
|
|
override def background: RGBAColorNorm = ColorScheme("LogEntryTxBackground")
|
|
override def foreground: RGBAColorNorm = ColorScheme("LogEntryTxForeground")
|
|
override def border: RGBAColorNorm = ColorScheme("LogEntryTxBorder")
|
|
}
|
|
}
|
|
|
|
private val scrollView = new ScrollView(MessageListWidget)
|
|
|
|
children :+= new PaddingBox(scrollView, Padding2D.equal(BorderThickness))
|
|
|
|
def scrollToEnd: Boolean = scrollView.scrollToEnd
|
|
|
|
def scrollToEnd_=(value: Boolean): Unit = {
|
|
scrollView.scrollToEnd = value
|
|
}
|
|
|
|
protected def textWidth: Int
|
|
|
|
override def minimumSize: Size2D = Size2D(
|
|
MessageListWidget.minimumSize.width + 2 * BorderThickness,
|
|
MessageListWidget.dummyEntry.maxY +
|
|
EntryMargin +
|
|
BorderThickness * 2, // outer border (top + bottom)
|
|
)
|
|
|
|
def addEntries(entries: Iterable[LogEntry]): Unit = {
|
|
for (entry <- entries) {
|
|
MessageListWidget.addEntry(entry)
|
|
}
|
|
}
|
|
|
|
def removeFirstEntries(count: Int): Unit = {
|
|
MessageListWidget.removeFirst(count)
|
|
}
|
|
|
|
override def draw(g: Graphics): Unit = {
|
|
g.rect(bounds, ColorScheme("LogBackground"))
|
|
DrawUtils.ring(g, bounds.x, bounds.y, bounds.w, bounds.h, BorderThickness, ColorScheme("LogBorder"))
|
|
|
|
super.draw(g)
|
|
}
|
|
}
|
|
|
|
object LogWidget {
|
|
val BorderThickness = 2
|
|
val EntryPadding = 2
|
|
val EntryMargin = 2
|
|
|
|
sealed trait LogEntry
|
|
|
|
object LogEntry {
|
|
|
|
/** A message received from a log source. */
|
|
case class Rx(message: String) extends LogEntry
|
|
|
|
/** A message sent to a log source. */
|
|
case class Tx(message: String) extends LogEntry
|
|
}
|
|
}
|