From 47a1ef160ed21c13cf878ecd6cd995c0fecdb32d Mon Sep 17 00:00:00 2001 From: LeshaInc Date: Sat, 6 Jun 2020 16:32:22 +0300 Subject: [PATCH] Add context menus --- .../scala/ocelot/desktop/color/IntColor.scala | 2 + src/main/scala/ocelot/desktop/node/Node.scala | 32 ++++++- .../ocelot/desktop/node/NodeTypeWidget.scala | 1 + .../desktop/node/nodes/ComputerNode.scala | 34 ++++++- .../desktop/node/nodes/ScreenNode.scala | 32 ++++++- .../scala/ocelot/desktop/ui/UiHandler.scala | 3 + .../desktop/ui/layout/LinearLayout.scala | 1 + .../ocelot/desktop/ui/widget/IconButton.scala | 1 + .../ocelot/desktop/ui/widget/Label.scala | 3 +- .../ocelot/desktop/ui/widget/RootWidget.scala | 11 ++- .../ocelot/desktop/ui/widget/SlotWidget.scala | 1 + .../ocelot/desktop/ui/widget/Widget.scala | 11 ++- .../desktop/ui/widget/WorkspaceView.scala | 3 + .../ui/widget/contextmenu/ContextMenu.scala | 93 +++++++++++++++++++ .../widget/contextmenu/ContextMenuEntry.scala | 75 +++++++++++++++ .../contextmenu/ContextMenuSubmenu.scala | 49 ++++++++++ .../ui/widget/contextmenu/ContextMenus.scala | 59 ++++++++++++ .../ui/widget/contextmenu/Separator.scala | 16 ++++ .../scala/ocelot/desktop/util/DrawUtils.scala | 4 +- .../util/animation/ValueAnimation.scala | 2 +- 20 files changed, 419 insertions(+), 14 deletions(-) create mode 100644 src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenu.scala create mode 100644 src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuEntry.scala create mode 100644 src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuSubmenu.scala create mode 100644 src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenus.scala create mode 100644 src/main/scala/ocelot/desktop/ui/widget/contextmenu/Separator.scala diff --git a/src/main/scala/ocelot/desktop/color/IntColor.scala b/src/main/scala/ocelot/desktop/color/IntColor.scala index 60e260f..afed1f4 100644 --- a/src/main/scala/ocelot/desktop/color/IntColor.scala +++ b/src/main/scala/ocelot/desktop/color/IntColor.scala @@ -14,4 +14,6 @@ case class IntColor(color: Int) extends Color { override def toRGBANorm: RGBAColorNorm = toRGBA.toRGBANorm override def toHSVA: HSVAColor = toRGBANorm.toHSVA + + def withAlpha(a: Float): RGBAColorNorm = toRGBANorm.withAlpha(a) } diff --git a/src/main/scala/ocelot/desktop/node/Node.scala b/src/main/scala/ocelot/desktop/node/Node.scala index e48eb3b..d13347c 100644 --- a/src/main/scala/ocelot/desktop/node/Node.scala +++ b/src/main/scala/ocelot/desktop/node/Node.scala @@ -5,6 +5,7 @@ import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D} import ocelot.desktop.graphics.Graphics import ocelot.desktop.ui.event.handlers.{ClickHandler, DragHandler, HoverHandler} import ocelot.desktop.ui.event.{ClickEvent, DragEvent, HoverEvent, MouseEvent} +import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry} import ocelot.desktop.ui.widget.window.Window import ocelot.desktop.ui.widget.{Widget, WorkspaceView} import ocelot.desktop.util.DrawUtils @@ -37,6 +38,11 @@ trait Node extends Widget with DragHandler with ClickHandler with HoverHandler { case ClickEvent(MouseEvent.Button.Left, _) => window.foreach(window => workspaceView.windowPool.openWindow(window)) + case ClickEvent(MouseEvent.Button.Right, _) => + val menu = new ContextMenu + setupContextMenu(menu) + workspaceView.rootWidget.contextMenus.open(menu) + case DragEvent(DragEvent.State.Start, MouseEvent.Button.Left, pos) => startMoving(pos) @@ -65,6 +71,22 @@ trait Node extends Widget with DragHandler with ClickHandler with HoverHandler { highlight.goto(NoHighlight) } + def setupContextMenu(menu: ContextMenu): Unit = { + menu.addEntry(new ContextMenuEntry("Cut")) + menu.addEntry(new ContextMenuEntry("Copy")) + menu.addEntry(new ContextMenuEntry("Paste")) + menu.addSeparator() + + menu.addEntry(new ContextMenuEntry("Disconnect", () => { + disconnectFromAll() + })) + + menu.addEntry(new ContextMenuEntry("Delete", () => { + disconnectFromAll() + workspaceView.nodes -= (this) + })) + } + def environment: Environment def icon: String = "icons/NA" @@ -91,6 +113,12 @@ trait Node extends Widget with DragHandler with ClickHandler with HoverHandler { node.onConnectionRemoved(portB, this, portA) } + def disconnectFromAll(): Unit = { + for ((a, node, b) <- connections) { + disconnect(a, node, b) + } + } + def isConnected(portA: NodePort, node: Node, portB: NodePort): Boolean = { _connections.contains((portA, node, portB)) } @@ -139,6 +167,8 @@ trait Node extends Widget with DragHandler with ClickHandler with HoverHandler { override def minimumSize: Size2D = Size2D(68, 68) + override def maximumSize: Size2D = minimumSize + private def startMoving(pos: Vector2D): Unit = { highlight.goto(MovingHighlight) isMoving = true @@ -195,5 +225,5 @@ trait Node extends Widget with DragHandler with ClickHandler with HoverHandler { } } - lazy val window: Option[Window] = None + def window: Option[Window] = None } diff --git a/src/main/scala/ocelot/desktop/node/NodeTypeWidget.scala b/src/main/scala/ocelot/desktop/node/NodeTypeWidget.scala index e20463a..5419a5b 100644 --- a/src/main/scala/ocelot/desktop/node/NodeTypeWidget.scala +++ b/src/main/scala/ocelot/desktop/node/NodeTypeWidget.scala @@ -9,6 +9,7 @@ import ocelot.desktop.util.TierColor class NodeTypeWidget(val nodeType: NodeType) extends Widget with ClickHandler { override def minimumSize: Size2D = Size2D(68, 68) + override def maximumSize: Size2D = minimumSize size = maximumSize diff --git a/src/main/scala/ocelot/desktop/node/nodes/ComputerNode.scala b/src/main/scala/ocelot/desktop/node/nodes/ComputerNode.scala index 9e8e541..79b3179 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/ComputerNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/ComputerNode.scala @@ -4,6 +4,7 @@ import ocelot.desktop.OcelotDesktop import ocelot.desktop.color.Color import ocelot.desktop.graphics.Graphics import ocelot.desktop.node.Node +import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry, ContextMenuSubmenu} import ocelot.desktop.util.TierColor import totoro.ocelot.brain.entity.traits.Computer import totoro.ocelot.brain.entity.{CPU, Case, EEPROM, GraphicsCard, HDDManaged, InternetCard, Memory, NetworkCard} @@ -28,7 +29,29 @@ class ComputerNode(val computer: Case) extends Node { override def environment: Computer = computer override val icon: String = "nodes/Computer" - override val iconColor: Color = TierColor.get(computer.tier) + override def iconColor: Color = TierColor.get(computer.tier) + + override def setupContextMenu(menu: ContextMenu): Unit = { + if (computer.machine.isRunning) + menu.addEntry(new ContextMenuEntry("Turn off", () => computer.turnOff())) + else + menu.addEntry(new ContextMenuEntry("Turn on", () => computer.turnOn())) + + menu.addEntry(new ContextMenuSubmenu("Set tier") { + addEntry(new ContextMenuEntry("Tier 1", () => changeTier(Tier.One))) + addEntry(new ContextMenuEntry("Tier 2", () => changeTier(Tier.Two))) + addEntry(new ContextMenuEntry("Tier 3", () => changeTier(Tier.Three))) + addEntry(new ContextMenuEntry("Tier 4 (Creative)", () => changeTier(Tier.Four))) + }) + + menu.addSeparator() + super.setupContextMenu(menu) + } + + private def changeTier(n: Int): Unit = { + computer.tier = n + currentWindow = new ComputerWindow(computer) + } override def draw(g: Graphics): Unit = { super.draw(g) @@ -46,5 +69,12 @@ class ComputerNode(val computer: Case) extends Node { g.sprite("nodes/ComputerActivityOverlay", position.x + 2, position.y + 2, size.width - 4, size.height - 4) } - override lazy val window: Option[ComputerWindow] = Some(new ComputerWindow(computer)) + private var currentWindow: ComputerWindow = _ + + override def window: Option[ComputerWindow] = { + if (currentWindow == null) { + currentWindow = new ComputerWindow(computer) + Some(currentWindow) + } else Some(currentWindow) + } } diff --git a/src/main/scala/ocelot/desktop/node/nodes/ScreenNode.scala b/src/main/scala/ocelot/desktop/node/nodes/ScreenNode.scala index 1f6f00f..cbed653 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/ScreenNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/ScreenNode.scala @@ -4,9 +4,11 @@ import ocelot.desktop.OcelotDesktop import ocelot.desktop.color.Color import ocelot.desktop.graphics.Graphics import ocelot.desktop.node.Node +import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry, ContextMenuSubmenu} import ocelot.desktop.util.TierColor import totoro.ocelot.brain.entity.traits.Environment import totoro.ocelot.brain.entity.{Keyboard, Screen} +import totoro.ocelot.brain.util.Tier class ScreenNode(val screen: Screen) extends Node { OcelotDesktop.workspace.add(screen) @@ -22,6 +24,27 @@ class ScreenNode(val screen: Screen) extends Node { override def iconColor: Color = TierColor.get(screen.tier) + override def setupContextMenu(menu: ContextMenu): Unit = { + if (screen.getPowerState) + menu.addEntry(new ContextMenuEntry("Turn off", () => screen.setPowerState(false))) + else + menu.addEntry(new ContextMenuEntry("Turn on", () => screen.setPowerState(true))) + + menu.addEntry(new ContextMenuSubmenu("Set tier") { + addEntry(new ContextMenuEntry("Tier 1", () => changeTier(Tier.One))) + addEntry(new ContextMenuEntry("Tier 2", () => changeTier(Tier.Two))) + addEntry(new ContextMenuEntry("Tier 3", () => changeTier(Tier.Three))) + }) + + menu.addSeparator() + super.setupContextMenu(menu) + } + + private def changeTier(n: Int): Unit = { + screen.tier = n + currentWindow = new ScreenWindow(screen) + } + override def draw(g: Graphics): Unit = { super.draw(g) @@ -29,5 +52,12 @@ class ScreenNode(val screen: Screen) extends Node { g.sprite("nodes/ScreenOnOverlay", position.x + 2, position.y + 2, size.width - 4, size.height - 4) } - override lazy val window: Option[ScreenWindow] = Some(new ScreenWindow(screen)) + private var currentWindow: ScreenWindow = _ + + override def window: Option[ScreenWindow] = { + if (currentWindow == null) { + currentWindow = new ScreenWindow(screen) + Some(currentWindow) + } else Some(currentWindow) + } } diff --git a/src/main/scala/ocelot/desktop/ui/UiHandler.scala b/src/main/scala/ocelot/desktop/ui/UiHandler.scala index 088ae89..5542243 100644 --- a/src/main/scala/ocelot/desktop/ui/UiHandler.scala +++ b/src/main/scala/ocelot/desktop/ui/UiHandler.scala @@ -203,6 +203,9 @@ object UiHandler extends Logging { for (event <- KeyEvents.events) hierarchy.reverseIterator.foreach(_.handleEvent(event)) + for (event <- MouseEvents.events) + hierarchy.reverseIterator.filter(_.receiveAllMouseEvents).foreach(_.handleEvent(event)) + val scrollTarget = hierarchy.reverseIterator .find(w => w.receiveScrollEvents && w.clippedBounds.contains(mousePos)) diff --git a/src/main/scala/ocelot/desktop/ui/layout/LinearLayout.scala b/src/main/scala/ocelot/desktop/ui/layout/LinearLayout.scala index 7c38354..8928ee2 100644 --- a/src/main/scala/ocelot/desktop/ui/layout/LinearLayout.scala +++ b/src/main/scala/ocelot/desktop/ui/layout/LinearLayout.scala @@ -48,6 +48,7 @@ class LinearLayout(widget: Widget, var usedSpace = 0f for (child <- widget.children) { + child.recalculateBounds() val minSize = child.minimumSize if (stretch) { diff --git a/src/main/scala/ocelot/desktop/ui/widget/IconButton.scala b/src/main/scala/ocelot/desktop/ui/widget/IconButton.scala index ce8c820..b5cd7ef 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/IconButton.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/IconButton.scala @@ -54,6 +54,7 @@ class IconButton(releasedIcon: String, pressedIcon: String, private def pressedIconSize: Size2D = Spritesheet.spriteSize(pressedIcon) * sizeMultiplier override def minimumSize: Size2D = releasedIconSize.max(pressedIconSize) + override def maximumSize: Size2D = minimumSize override def draw(g: Graphics): Unit = { if (alphaAnimation.value < 1f) diff --git a/src/main/scala/ocelot/desktop/ui/widget/Label.scala b/src/main/scala/ocelot/desktop/ui/widget/Label.scala index 0e38a5b..22e2ab8 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/Label.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/Label.scala @@ -14,7 +14,8 @@ class Label extends Widget { private var length = text.length * 8 private def wideLength(g: Graphics): Int = text.map(g.font.charWidth(_)).sum - override def minimumSize: Size2D = Size2D(length, 8) + override def minimumSize: Size2D = Size2D(length, if (isSmall) 8 else 16) + override def maximumSize: Size2D = minimumSize.copy(width = Float.PositiveInfinity) override def draw(g: Graphics): Unit = { if (isSmall) g.setSmallFont() diff --git a/src/main/scala/ocelot/desktop/ui/widget/RootWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/RootWidget.scala index f8e261b..b18ed1b 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/RootWidget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/RootWidget.scala @@ -1,9 +1,14 @@ package ocelot.desktop.ui.widget -import ocelot.desktop.ui.layout.LinearLayout +import ocelot.desktop.ui.layout.CopyLayout +import ocelot.desktop.ui.widget.contextmenu.ContextMenus class RootWidget extends Widget { - override protected val layout = new LinearLayout(this) + override protected val layout = new CopyLayout(this) - children :+= new WorkspaceView + val workspaceView: WorkspaceView = new WorkspaceView + val contextMenus: ContextMenus = new ContextMenus + + children :+= workspaceView + children :+= contextMenus } diff --git a/src/main/scala/ocelot/desktop/ui/widget/SlotWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/SlotWidget.scala index aca0af0..ccfdad8 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/SlotWidget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/SlotWidget.scala @@ -8,6 +8,7 @@ class SlotWidget extends Widget { def tierIcon: String = null override def minimumSize: Size2D = Size2D(36, 36) + override def maximumSize: Size2D = minimumSize override def draw(g: Graphics): Unit = { g.sprite("EmptySlot", bounds) diff --git a/src/main/scala/ocelot/desktop/ui/widget/Widget.scala b/src/main/scala/ocelot/desktop/ui/widget/Widget.scala index 3615c16..4aeb7f0 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/Widget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/Widget.scala @@ -21,7 +21,10 @@ class Widget { final def relayout(): Unit = layout.relayout() - final def recalculateBounds(): Unit = layout.recalculateBounds() + final def recalculateBounds(): Unit = { + layout.recalculateBounds() + size = _size + } final def relayoutParent(): Unit = { shouldRelayoutParent = true @@ -62,8 +65,8 @@ class Widget { val clamped = value.clamped(minimumSize, maximumSize) if (clamped != _size) { - recalculateBounds() - _size = clamped.copy() + layout.recalculateBounds() + _size = value.clamped(minimumSize, maximumSize) relayout() shouldRelayoutParent = true } @@ -128,4 +131,6 @@ class Widget { def receiveScrollEvents: Boolean = false def receiveMouseEvents: Boolean = false + + def receiveAllMouseEvents: Boolean = false } diff --git a/src/main/scala/ocelot/desktop/ui/widget/WorkspaceView.scala b/src/main/scala/ocelot/desktop/ui/widget/WorkspaceView.scala index 9d262b0..a5286e6 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/WorkspaceView.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/WorkspaceView.scala @@ -39,6 +39,8 @@ class WorkspaceView extends Widget with DragHandler with ClickHandler with Hover createDefaultWorkspace() + def rootWidget: RootWidget = parent.get.asInstanceOf[RootWidget] + def addNode(node: Node, pos: Vector2D = newNodePos): Unit = { node.position = pos + cameraOffset resolveCollision(node) @@ -87,6 +89,7 @@ class WorkspaceView extends Widget with DragHandler with ClickHandler with Hover else p - Vector2D(32, 32) + nodeSelector.recalculateBounds() windowPool.openWindow(nodeSelector) newNodePos = pos - cameraOffset diff --git a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenu.scala b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenu.scala new file mode 100644 index 0000000..ed8ce6a --- /dev/null +++ b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenu.scala @@ -0,0 +1,93 @@ +package ocelot.desktop.ui.widget.contextmenu + +import ocelot.desktop.color.IntColor +import ocelot.desktop.geometry.Padding2D +import ocelot.desktop.graphics.Graphics +import ocelot.desktop.ui.layout.LinearLayout +import ocelot.desktop.ui.widget.{PaddingBox, Widget} +import ocelot.desktop.util.animation.ValueAnimation +import ocelot.desktop.util.animation.easing.EaseInOutQuad +import ocelot.desktop.util.{DrawUtils, Orientation} + +class ContextMenu extends Widget { + private[contextmenu] var isClosing = false + private[contextmenu] var isOpening = false + + protected var _contextMenus: ContextMenus = _ + protected val alpha = new ValueAnimation(0f, 8f) + protected var inner: Widget = new Widget { + override protected val layout = new LinearLayout(this, orientation = Orientation.Vertical) + } + + children :+= new PaddingBox(inner, Padding2D(top = 4, bottom = 4)) + + private[contextmenu] def contextMenus: ContextMenus = _contextMenus + + private[contextmenu] def contextMenus_=(value: ContextMenus): Unit = { + entries.foreach(_.contextMenus = value) + _contextMenus = value + } + + def addEntry(entry: ContextMenuEntry): Unit = { + entry.contextMenus = contextMenus + entry.contextMenu = this + inner.children :+= entry + } + + def addSeparator(): Unit = { + inner.children :+= new Separator + } + + private def entries: Array[ContextMenuEntry] = inner.children + .filter(_.isInstanceOf[ContextMenuEntry]) + .map(_.asInstanceOf[ContextMenuEntry]) + + def isClosed: Boolean = { + alpha.value == 0f + } + + def open(): Unit = { + isClosing = false + isOpening = true + alpha.jump(0.001f) + alpha.goto(1f) + + var offset = 0.5f + val step = 0.5f / entries.length.toFloat + for (entry <- entries) { + entry.isGhost = false + entry.textAlpha.jump(offset) + entry.textAlpha.goto(1f) + entry.trans.easing = EaseInOutQuad + entry.trans.jump(-6f + offset * 6f) + entry.trans.goto(0f) + offset -= step + } + } + + def close(): Unit = { + isClosing = true + isOpening = false + alpha.goto(0f) + + for (entry <- entries) { + entry.isGhost = true + entry.textAlpha.goto(1f) + entry.trans.easing = EaseInOutQuad + entry.trans.goto(-8f) + } + } + + override def draw(g: Graphics): Unit = { + alpha.update() + if (alpha.value < 1f) g.beginGroupAlpha() else isOpening = false + + DrawUtils.shadow(g, bounds.x - 8, bounds.y - 8, bounds.w + 16, bounds.h + 20, 0.5f) + g.rect(bounds, IntColor(0x222222).withAlpha(0.8f)) + DrawUtils.ring(g, bounds.x, bounds.y, bounds.w, bounds.h, 1, IntColor(0x444444)) + + drawChildren(g) + + if (alpha.value < 1f) g.endGroupAlpha(alpha.value) + } +} diff --git a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuEntry.scala b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuEntry.scala new file mode 100644 index 0000000..7b4278d --- /dev/null +++ b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuEntry.scala @@ -0,0 +1,75 @@ +package ocelot.desktop.ui.widget.contextmenu + +import ocelot.desktop.color.{Color, IntColor} +import ocelot.desktop.geometry.{Padding2D, Size2D} +import ocelot.desktop.graphics.Graphics +import ocelot.desktop.ui.event.handlers.{ClickHandler, HoverHandler} +import ocelot.desktop.ui.event.{ClickEvent, HoverEvent, MouseEvent} +import ocelot.desktop.ui.widget.{Label, PaddingBox, Widget} +import ocelot.desktop.util.animation.ValueAnimation +import ocelot.desktop.util.animation.easing.{EaseInQuad, EaseOutQuad} + +class ContextMenuEntry(label: String, onClick: () => Unit = () => {}) extends Widget with ClickHandler with HoverHandler { + private[contextmenu] val alpha = new ValueAnimation(0f, 10f) + private[contextmenu] val textAlpha = new ValueAnimation(0f, 5f) + private[contextmenu] val trans = new ValueAnimation(0f, 20f) + private[contextmenu] var contextMenus: ContextMenus = _ + private[contextmenu] var contextMenu: ContextMenu = _ + private[contextmenu] var isGhost: Boolean = false + + children :+= new PaddingBox(new Label { + override def text: String = label + override def color: Color = IntColor(0xB0B0B0) + }, Padding2D(left = 12f, right = 16f, top = 5f, bottom = 5f)) + + override def receiveMouseEvents: Boolean = !isGhost + + eventHandlers += { + case ClickEvent(MouseEvent.Button.Left, _) if !contextMenu.isOpening => clicked() + case HoverEvent(HoverEvent.State.Enter) => enter() + case HoverEvent(HoverEvent.State.Leave) if !isGhost => leave() + } + + override def minimumSize: Size2D = layout.minimumSize.max(Size2D(150, 1)) + + protected def clicked(): Unit = { + onClick() + contextMenus.closeAll() + contextMenus.setGhost(this) + + isGhost = true + alpha.goto(0f) + textAlpha.goto(0f) + alpha.speed = 2.5f + textAlpha.speed = 2.5f + trans.speed = 0f + } + + protected def enter(): Unit = { + alpha.speed = 10f + alpha.goto(1f) + trans.easing = EaseInQuad + trans.goto(2f) + } + + protected def leave(): Unit = { + alpha.speed = 1f + alpha.goto(0f) + trans.easing = EaseOutQuad + trans.goto(0f) + } + + override def draw(g: Graphics): Unit = { + alpha.update() + textAlpha.update() + trans.update() + + g.rect(bounds.mapW(_ - 8).mapX(_ + 4), IntColor(0x333333).withAlpha(alpha.value)) + + g.save() + g.translate(trans.value, 0f) + g.alphaMultiplier = textAlpha.value + drawChildren(g) + g.restore() + } +} diff --git a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuSubmenu.scala b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuSubmenu.scala new file mode 100644 index 0000000..cf60bb9 --- /dev/null +++ b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuSubmenu.scala @@ -0,0 +1,49 @@ +package ocelot.desktop.ui.widget.contextmenu + +import ocelot.desktop.color.{Color, IntColor} +import ocelot.desktop.geometry.Vector2D +import ocelot.desktop.graphics.Graphics +import ocelot.desktop.ui.UiHandler +import ocelot.desktop.ui.event.HoverEvent + +class ContextMenuSubmenu(label: String) extends ContextMenuEntry(label) { + private val parentEntry = this + private val submenu = new ContextMenu { + override def update(): Unit = { + super.update() + if (!isClosing && !bounds.inflate(4).contains(UiHandler.mousePosition) + && !parentEntry.isHovered) close() + } + } + + def addEntry(entry: ContextMenuEntry): Unit = { + submenu.addEntry(entry) + } + + def addSeparator(): Unit = { + submenu.addSeparator() + } + + override def clicked(): Unit = {} + + override def leave(): Unit = { + if (submenu.isClosed) super.leave() + } + + override def update(): Unit = { + if (!isHovered && submenu.isClosed) super.leave() + super.update() + } + + override def draw(g: Graphics): Unit = { + super.draw(g) + g.background = Color.Transparent + g.foreground = IntColor(0xB0B0B0).withAlpha(textAlpha.value) + g.text(position.x + width - 24, position.y + 5, ">") + } + + eventHandlers += { + case HoverEvent(HoverEvent.State.Enter) if !isGhost => + contextMenus.open(submenu, position + Vector2D(width, 0), isSubmenu = true) + } +} diff --git a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenus.scala b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenus.scala new file mode 100644 index 0000000..d86dc0e --- /dev/null +++ b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenus.scala @@ -0,0 +1,59 @@ +package ocelot.desktop.ui.widget.contextmenu + +import ocelot.desktop.geometry.Vector2D +import ocelot.desktop.graphics.Graphics +import ocelot.desktop.ui.UiHandler +import ocelot.desktop.ui.event.{KeyEvent, MouseEvent} +import ocelot.desktop.ui.layout.Layout +import ocelot.desktop.ui.widget.Widget +import org.lwjgl.input.Keyboard + +class ContextMenus extends Widget { + override protected val layout: Layout = new Layout(this) + + private var ghost: Option[ContextMenuEntry] = None + + private def menus: Array[ContextMenu] = children.map(_.asInstanceOf[ContextMenu]) + + private[contextmenu] def setGhost(g: ContextMenuEntry): Unit = { + ghost = Some(g) + } + + override def receiveAllMouseEvents: Boolean = true + + eventHandlers += { + case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_ESCAPE, _) => + closeAll() + + case MouseEvent(MouseEvent.State.Press, _) => + if (!menus.map(_.bounds).exists(_.contains(UiHandler.mousePosition))) closeAll() + } + + def open(menu: ContextMenu, pos: Vector2D = UiHandler.mousePosition + Vector2D(2, 2), isSubmenu: Boolean = false): Unit = { + if (!isSubmenu) for (child <- menus) close(child) + menu.position = pos + menu.size = menu.minimumSize + menu.contextMenus = this + menu.open() + children :+= menu + } + + def close(menu: ContextMenu): Unit = { + menu.close() + } + + def closeAll(): Unit = { + for (child <- menus) close(child) + } + + override def draw(g: Graphics): Unit = { + super.draw(g) + if (ghost.isDefined && ghost.get.alpha == 0f) ghost = None + ghost.foreach(_.draw(g)) + } + + override def update(): Unit = { + super.update() + children = children.filterNot(_.asInstanceOf[ContextMenu].isClosed) + } +} diff --git a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/Separator.scala b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/Separator.scala new file mode 100644 index 0000000..556d35f --- /dev/null +++ b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/Separator.scala @@ -0,0 +1,16 @@ +package ocelot.desktop.ui.widget.contextmenu + +import ocelot.desktop.color.IntColor +import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D} +import ocelot.desktop.graphics.Graphics +import ocelot.desktop.ui.widget.Widget + +class Separator extends Widget { + override def minimumSize: Size2D = Size2D(50, 9) + + override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 9) + + override def draw(g: Graphics): Unit = { + g.rect(Rect2D(position + Vector2D(0, 4), size.copy(height = 1)), IntColor(0x444444)) + } +} diff --git a/src/main/scala/ocelot/desktop/util/DrawUtils.scala b/src/main/scala/ocelot/desktop/util/DrawUtils.scala index 779703f..b46af4f 100644 --- a/src/main/scala/ocelot/desktop/util/DrawUtils.scala +++ b/src/main/scala/ocelot/desktop/util/DrawUtils.scala @@ -69,7 +69,7 @@ object DrawUtils { def windowWithShadow(g: Graphics, x: Float, y: Float, w: Float, h: Float, backgroundAlpha: Float, shadowAlpha: Float): Unit = { - DrawUtils.shadow(g, x - 8, y - 8, w + 12, h + 20, shadowAlpha) + DrawUtils.shadow(g, x - 8, y - 8, w + 16, h + 20, shadowAlpha) DrawUtils.windowBorder(g, x, y, w, h, RGBAColorNorm(1, 1, 1, backgroundAlpha)) } @@ -123,7 +123,7 @@ object DrawUtils { g.sprite("ShadowBorder", 0, 0, h - 48, 24, col) g.restore() - g.rect(x + 24, y + 24, w - 48, h - 49, RGBAColorNorm(0, 0, 0, a)) + g.rect(x + 24, y + 24, w - 48, h - 48, RGBAColorNorm(0, 0, 0, a)) } private def rotSprite(g: Graphics, sprite: String, x: Float, y: Float, w: Float, h: Float, angle: Float, diff --git a/src/main/scala/ocelot/desktop/util/animation/ValueAnimation.scala b/src/main/scala/ocelot/desktop/util/animation/ValueAnimation.scala index c565fff..46a8bf7 100644 --- a/src/main/scala/ocelot/desktop/util/animation/ValueAnimation.scala +++ b/src/main/scala/ocelot/desktop/util/animation/ValueAnimation.scala @@ -3,7 +3,7 @@ package ocelot.desktop.util.animation import ocelot.desktop.ui.UiHandler import ocelot.desktop.util.animation.easing.{Easing, EasingFunction} -class ValueAnimation(init: Float = 0f, speed: Float = 10f) { +class ValueAnimation(init: Float = 0f, var speed: Float = 10f) { private var _value = init private var start = init private var end = init