diff --git a/src/main/scala/ocelot/desktop/ui/UiHandler.scala b/src/main/scala/ocelot/desktop/ui/UiHandler.scala index 5a693cd..c99efb4 100644 --- a/src/main/scala/ocelot/desktop/ui/UiHandler.scala +++ b/src/main/scala/ocelot/desktop/ui/UiHandler.scala @@ -460,7 +460,7 @@ object UiHandler extends Logging { ScrollEvents.events.foreach(dispatchEvent(scrollTarget)) for (event <- MouseEvents.events) { - if (event.state == MouseEvent.State.Press) { + if (event.state == MouseEvent.State.Pressed) { dispatchEvent(mouseTarget)(event) // TODO: this should be done in the event capturing phase in [[Window]] itself. diff --git a/src/main/scala/ocelot/desktop/ui/event/MouseEvent.scala b/src/main/scala/ocelot/desktop/ui/event/MouseEvent.scala index ec6486b..58c4c07 100644 --- a/src/main/scala/ocelot/desktop/ui/event/MouseEvent.scala +++ b/src/main/scala/ocelot/desktop/ui/event/MouseEvent.scala @@ -2,7 +2,7 @@ package ocelot.desktop.ui.event object MouseEvent { object State extends Enumeration { - val Press, Release = Value + val Pressed, Released = Value } object Button extends Enumeration { diff --git a/src/main/scala/ocelot/desktop/ui/event/handlers/MouseHandler.scala b/src/main/scala/ocelot/desktop/ui/event/handlers/MouseHandler.scala index d904971..05c4465 100644 --- a/src/main/scala/ocelot/desktop/ui/event/handlers/MouseHandler.scala +++ b/src/main/scala/ocelot/desktop/ui/event/handlers/MouseHandler.scala @@ -25,10 +25,10 @@ trait MouseHandler extends Widget { protected def allowClickReleaseOutsideThreshold: Boolean = !receiveDragEvents eventHandlers += { - case MouseEvent(MouseEvent.State.Press, button) => + case MouseEvent(MouseEvent.State.Pressed, button) => startPositions += (button -> UiHandler.mousePosition) - case MouseEvent(MouseEvent.State.Release, button) => + case MouseEvent(MouseEvent.State.Released, button) => val mousePos = UiHandler.mousePosition val dragStopped = receiveDragEvents && dragButtons.remove(button) diff --git a/src/main/scala/ocelot/desktop/ui/event/sources/MouseEvents.scala b/src/main/scala/ocelot/desktop/ui/event/sources/MouseEvents.scala index 8b048d3..4f299aa 100644 --- a/src/main/scala/ocelot/desktop/ui/event/sources/MouseEvents.scala +++ b/src/main/scala/ocelot/desktop/ui/event/sources/MouseEvents.scala @@ -25,12 +25,12 @@ object MouseEvents { if (MouseEvent.Button.values.map(_.id).contains(buttonIdx)) { val button = MouseEvent.Button(buttonIdx) - val state = if (Mouse.getEventButtonState) MouseEvent.State.Press else MouseEvent.State.Release + val state = if (Mouse.getEventButtonState) MouseEvent.State.Pressed else MouseEvent.State.Released _events += MouseEvent(state, button) state match { - case MouseEvent.State.Press => + case MouseEvent.State.Pressed => _pressedButtons += button - case MouseEvent.State.Release => + case MouseEvent.State.Released => _pressedButtons -= button } } @@ -49,7 +49,7 @@ object MouseEvents { def releaseButtons(): Unit = { for (button <- pressedButtons) { - _events += MouseEvent(MouseEvent.State.Release, button) + _events += MouseEvent(MouseEvent.State.Released, button) } _pressedButtons.clear() diff --git a/src/main/scala/ocelot/desktop/ui/widget/Button.scala b/src/main/scala/ocelot/desktop/ui/widget/Button.scala index 7e983ac..98807cc 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/Button.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/Button.scala @@ -26,7 +26,7 @@ class Button(tooltip: Option[Tooltip] = None) extends Widget with MouseHandler w override protected def receiveClickEvents: Boolean = true eventHandlers += { - case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) if enabled => + case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) if enabled => clickSoundSource.press.play() case ClickEvent(MouseEvent.Button.Left, _) if enabled => diff --git a/src/main/scala/ocelot/desktop/ui/widget/IconButton.scala b/src/main/scala/ocelot/desktop/ui/widget/IconButton.scala index 77150d3..82dfd3b 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/IconButton.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/IconButton.scala @@ -35,7 +35,7 @@ class IconButton( case HoverEvent(HoverEvent.State.Enter) => onHoverEnter() case HoverEvent(HoverEvent.State.Leave) => onHoverLeave() - case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) => + case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) => mode match { case Mode.Regular => handlePress() case _ => // the other modes are triggered on click. @@ -43,7 +43,7 @@ class IconButton( clickSoundSource.press.play() - case MouseEvent(MouseEvent.State.Release, MouseEvent.Button.Left) => mode match { + case MouseEvent(MouseEvent.State.Released, MouseEvent.Button.Left) => mode match { case Mode.Regular if model.pressed => handleRelease() clickSoundSource.release.play() diff --git a/src/main/scala/ocelot/desktop/ui/widget/Knob.scala b/src/main/scala/ocelot/desktop/ui/widget/Knob.scala index 1dbefc0..74e9b48 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/Knob.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/Knob.scala @@ -28,14 +28,14 @@ abstract class Knob(dyeColor: DyeColor = DyeColor.Red) extends Widget { private var startValue: Int = 0 eventHandlers += { - case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) => + case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) => val mousePos = UiHandler.mousePosition if (bounds.contains(mousePos)) { movePos = Some(mousePos) startValue = input } - case MouseEvent(MouseEvent.State.Release, MouseEvent.Button.Left) => + case MouseEvent(MouseEvent.State.Released, MouseEvent.Button.Left) => movePos = None } diff --git a/src/main/scala/ocelot/desktop/ui/widget/MenuBarButton.scala b/src/main/scala/ocelot/desktop/ui/widget/MenuBarButton.scala index 25235d4..364b300 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/MenuBarButton.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/MenuBarButton.scala @@ -31,7 +31,7 @@ class MenuBarButton(label: String, handler: () => Unit = () => {}) extends Widge def onMouseLeave(): Unit = colorAnimation.goto(ColorScheme("TitleBarBackground")) eventHandlers += { - case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) => + case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) => clickSoundSource.press.play() case ClickEvent(MouseEvent.Button.Left, _) => diff --git a/src/main/scala/ocelot/desktop/ui/widget/ScreenView.scala b/src/main/scala/ocelot/desktop/ui/widget/ScreenView.scala new file mode 100644 index 0000000..2e44338 --- /dev/null +++ b/src/main/scala/ocelot/desktop/ui/widget/ScreenView.scala @@ -0,0 +1,189 @@ +package ocelot.desktop.ui.widget + +import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D} +import ocelot.desktop.graphics.Graphics +import ocelot.desktop.graphics.Texture.MinFilteringMode +import ocelot.desktop.node.nodes.ScreenNode +import ocelot.desktop.node.nodes.ScreenNode.{FontHeight, FontWidth} +import ocelot.desktop.ui.UiHandler +import ocelot.desktop.ui.event.{KeyEvent, MouseEvent, ScrollEvent} +import ocelot.desktop.ui.layout.Layout +import ocelot.desktop.ui.widget.ScreenView.ScaleTag +import ocelot.desktop.util.{Keybind, Persistable, Register} +import ocelot.desktop.{ColorScheme, OcelotDesktop, Settings} +import org.lwjgl.input.Keyboard +import totoro.ocelot.brain.nbt.NBTTagCompound +import totoro.ocelot.brain.util.Tier + +// TODO: use an interface instead of ScreenNode. +abstract class ScreenView(screenNode: ScreenNode) extends Widget with Persistable { + override protected val layout: Layout = new Layout(this) + + private var lastMousePos = Vector2D.Zero + + val scale = Register(1f) + def scaleX: Float = (FontWidth * scale.value).floor / FontWidth + def scaleY: Float = (FontHeight * scale.value).floor / FontHeight + + private def screenWidth: Int = screenNode.screenWidth + private def screenHeight: Int = screenNode.screenHeight + + override def minimumSize: Size2D = Size2D( + screenWidth * FontWidth * scaleX, + screenHeight * FontHeight * scaleY, + ) + + override def maximumSize: Size2D = minimumSize + + private def toBufferCoords(p: Vector2D): Vector2D = { + // no synchronization here (see the note in ScreenNode): the method to change this property is indirect. + if (screenNode.screen.getPrecisionMode) { + Vector2D( + (p.x - position.x) / FontWidth / scaleX, + (p.y - position.y) / FontHeight / scaleY, + ) + } else { + Vector2D( + math.floor((p.x - position.x) / FontWidth / scaleX), + math.floor((p.y - position.y) / FontHeight / scaleY), + ) + } + } + + private def bufferCoordsInBounds(p: Vector2D): Boolean = + new Rect2D(0, 0, screenWidth, screenHeight).contains(p) + + private def mouseCoordsInBounds: Boolean = + bufferCoordsInBounds(toBufferCoords(UiHandler.mousePosition)) + + override def receiveMouseEvents: Boolean = true + + override def receiveScrollEvents: Boolean = true + + protected def isFocused: Boolean + + private def shouldHandleInput: Boolean = isFocused && !root.get.modalDialogPool.isVisible + + // OC doesn't trigger several touch events in a row; the same holds for drop events. + // For the following inputs: + // LMB down, RMB down, RMB up, LMB up + // ...OC only registers LMB down and RMB up, dropping the other two events. + private var pressedButton: Option[MouseEvent.Button.Value] = None + + // NOTE: events are handled before update(). + // if the brain initiates a viewport size change, mouse events could be sent for coordinates outside the new bounds. + // TODO: look into how OpenComputers deals with that, if it does at all. + eventHandlers += { + case event: KeyEvent if shouldHandleInput && event.code != Keyboard.KEY_ESCAPE => + event.state match { + case KeyEvent.State.Press | KeyEvent.State.Repeat => + screenNode.screen.keyDown(event.char, event.code, OcelotDesktop.player) + + // note: in OpenComputers, key_down signal is fired __before__ clipboard signal + if (event.code == Settings.get.keymap(Keybind.Insert)) { + screenNode.screen.clipboard(UiHandler.clipboard, OcelotDesktop.player) + } + + case KeyEvent.State.Release => + screenNode.screen.keyUp(event.char, event.code, OcelotDesktop.player) + } + + event.consume() + + case event: MouseEvent if shouldHandleInput => + val pos = toBufferCoords(UiHandler.mousePosition) + val inBounds = bufferCoordsInBounds(pos) + + if (inBounds) { + lastMousePos = pos + } + + event.state match { + case MouseEvent.State.Pressed if inBounds && screenNode.screen.tier > Tier.One => + if (pressedButton.isEmpty) { + screenNode.screen.mouseDown(pos.x, pos.y, event.button.id, OcelotDesktop.player) + } + + pressedButton = Some(event.button) + event.consume() + + case MouseEvent.State.Released => + if (inBounds && event.button == MouseEvent.Button.Middle) { + screenNode.screen.clipboard(UiHandler.clipboard, OcelotDesktop.player) + event.consume() + } + + if (pressedButton.nonEmpty) { + screenNode.screen.mouseUp(lastMousePos.x, lastMousePos.y, event.button.id, OcelotDesktop.player) + pressedButton = None + + if (inBounds) { + event.consume() + } + } + + case _ => + } + + case event: ScrollEvent if mouseCoordsInBounds && shouldHandleInput && screenNode.screen.tier > Tier.One => + screenNode.screen.mouseScroll(lastMousePos.x, lastMousePos.y, event.offset, OcelotDesktop.player) + event.consume() + } + + override def save(nbt: NBTTagCompound): Unit = { + nbt.setFloat(ScaleTag, scale.value) + super.save(nbt) + } + + override def load(nbt: NBTTagCompound): Unit = { + super.load(nbt) + scale.nextValue = nbt.getFloat(ScaleTag) + } + + private val screenSize = Register.sampling(Size2D(screenWidth, screenHeight)) + + override def update(): Unit = { + super.update() + + // NOTE: the single bar is intentional! both operands have to be evaluated. + if (scale.update() | screenSize.update()) { + recalculateBoundsAndRelayout() + } + + val mousePos = toBufferCoords(UiHandler.mousePosition) + + if (bufferCoordsInBounds(mousePos) && mousePos != lastMousePos) { + lastMousePos = mousePos + + if (isFocused && screenNode.screen.tier > Tier.One) { + for (button <- pressedButton) { + screenNode.screen.mouseDrag(lastMousePos.x, lastMousePos.y, button.id, OcelotDesktop.player) + } + } + } + } + + override def draw(g: Graphics): Unit = { + // no synchronization here (see the note in ScreenNode): the methods to turn the screen on/off are indirect. + if (screenNode.screen.getPowerState) { + screenNode.drawScreenData( + g, + position.x, + position.y, + scaleX, + scaleY, + if (Settings.get.screenWindowMipmap) { + MinFilteringMode.LinearMipmapLinear + } else { + MinFilteringMode.Nearest + }, + ) + } else { + g.rect(bounds, ColorScheme("ScreenOff")) + } + } +} + +object ScreenView { + private val ScaleTag: String = "scale" +} diff --git a/src/main/scala/ocelot/desktop/ui/widget/ScrollView.scala b/src/main/scala/ocelot/desktop/ui/widget/ScrollView.scala index 9b03fd9..ae34c2e 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/ScrollView.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/ScrollView.scala @@ -45,7 +45,7 @@ class ScrollView(val inner: Widget) extends Widget with Logging with HoverHandle } } - case event: MouseEvent if event.state == MouseEvent.State.Press => + case event: MouseEvent if event.state == MouseEvent.State.Pressed => val pos = UiHandler.mousePosition mouseOldPos = pos @@ -59,7 +59,7 @@ class ScrollView(val inner: Widget) extends Widget with Logging with HoverHandle scrollToEnd = false } - case event: MouseEvent if event.state == MouseEvent.State.Release => + case event: MouseEvent if event.state == MouseEvent.State.Released => dragging = 0 } diff --git a/src/main/scala/ocelot/desktop/ui/widget/Slider.scala b/src/main/scala/ocelot/desktop/ui/widget/Slider.scala index 8e30473..41930e6 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/Slider.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/Slider.scala @@ -34,7 +34,7 @@ class Slider(var value: Float, val text: String, val snapPoints: Int = 0) } eventHandlers += { - case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) => + case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) => clickSoundSource.press.play() case ClickEvent(MouseEvent.Button.Left, pos) => diff --git a/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala b/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala index 7153bd0..5b0bc9a 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala @@ -58,7 +58,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w private var prevEnabled = enabled eventHandlers += { - case MouseEvent(MouseEvent.State.Release, MouseEvent.Button.Left) => + case MouseEvent(MouseEvent.State.Released, MouseEvent.Button.Left) => if (isFocused && !clippedBounds.contains(UiHandler.mousePosition)) { unfocus() } diff --git a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuEntry.scala b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuEntry.scala index 459c324..a371ac3 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuEntry.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuEntry.scala @@ -69,7 +69,7 @@ class ContextMenuEntry( override protected def receiveClickEvents: Boolean = true eventHandlers += { - case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) if !contextMenu.isOpening => + case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) if !contextMenu.isOpening => clickSoundSource.press.play() case ClickEvent(MouseEvent.Button.Left, _) if !contextMenu.isOpening => clicked() case HoverEvent(HoverEvent.State.Enter) => enter() diff --git a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenus.scala b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenus.scala index 88ead9d..6c84134 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenus.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenus.scala @@ -27,7 +27,7 @@ class ContextMenus extends Widget { case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_ESCAPE, _) => closeAll() - case MouseEvent(MouseEvent.State.Press, _) => + case MouseEvent(MouseEvent.State.Pressed, _) => if (!menus.map(_.bounds).exists(_.contains(UiHandler.mousePosition))) closeAll() } diff --git a/src/main/scala/ocelot/desktop/ui/widget/verticalmenu/VerticalMenuButton.scala b/src/main/scala/ocelot/desktop/ui/widget/verticalmenu/VerticalMenuButton.scala index c332135..36cc6ed 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/verticalmenu/VerticalMenuButton.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/verticalmenu/VerticalMenuButton.scala @@ -46,7 +46,7 @@ class VerticalMenuButton(icon: IconSource, label: String, handler: VerticalMenuB def onMouseLeave(): Unit = colorAnimation.goto(ColorScheme("VerticalMenuBackground")) eventHandlers += { - case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) => + case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) => clickSoundSource.press.play() case ClickEvent(MouseEvent.Button.Left, _) => diff --git a/src/main/scala/ocelot/desktop/ui/widget/window/BasicWindow.scala b/src/main/scala/ocelot/desktop/ui/widget/window/BasicWindow.scala index 6ac77bf..bf26117 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/window/BasicWindow.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/window/BasicWindow.scala @@ -53,9 +53,11 @@ trait BasicWindow extends Window { } } + protected def borderRenderer: DrawUtils.BorderRenderer = DrawUtils.windowBorder + override def draw(g: Graphics): Unit = { beginDraw(g) - DrawUtils.windowWithShadow(g, position.x, position.y, size.width, size.height, 1f, 0.5f) + DrawUtils.windowWithShadow(g, position.x, position.y, size.width, size.height, 1f, 0.5f, borderRenderer) drawChildren(g) endDraw(g) } diff --git a/src/main/scala/ocelot/desktop/ui/widget/window/PanelWindow.scala b/src/main/scala/ocelot/desktop/ui/widget/window/PanelWindow.scala index 1148190..10a269c 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/window/PanelWindow.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/window/PanelWindow.scala @@ -14,7 +14,11 @@ trait PanelWindow extends BasicWindow { protected def titleMaxLength: Int = 32 - def setInner(inner: Widget, padding: Padding2D = Padding2D(bottom = 13, left = 12, right = 12)): Unit = { + def setInner( + inner: Widget, + padding: Padding2D = Padding2D(bottom = 13, left = 12, right = 12), + titlePadding: Padding2D = Padding2D(top = 8, left = 12, right = 12, bottom = 2), + ): Unit = { children = ArraySeq.empty children :+= new PaddingBox( @@ -22,7 +26,7 @@ trait PanelWindow extends BasicWindow { override def title: String = PanelWindow.this.title override def titleMaxLength: Int = PanelWindow.this.titleMaxLength }, - Padding2D(top = 8, left = 12, right = 12, bottom = 2), + titlePadding, ) children :+= new PaddingBox(inner, padding) diff --git a/src/main/scala/ocelot/desktop/ui/widget/window/Window.scala b/src/main/scala/ocelot/desktop/ui/widget/window/Window.scala index f14f5f1..67d8862 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/window/Window.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/window/Window.scala @@ -30,7 +30,7 @@ trait Window extends Widget with Persistable with MouseHandler { override protected def receiveDragEvents: Boolean = true eventHandlers += { - case MouseEvent(MouseEvent.State.Press, _) => + case MouseEvent(MouseEvent.State.Pressed, _) => focus() case ev @ DragEvent(DragEvent.State.Start, MouseEvent.Button.Left, mousePos) => diff --git a/src/main/scala/ocelot/desktop/util/DrawUtils.scala b/src/main/scala/ocelot/desktop/util/DrawUtils.scala index 50fb29a..2a9a55f 100644 --- a/src/main/scala/ocelot/desktop/util/DrawUtils.scala +++ b/src/main/scala/ocelot/desktop/util/DrawUtils.scala @@ -17,24 +17,24 @@ object DrawUtils { h: Float, color: Color = RGBAColor(255, 255, 255), ): Unit = { - g.sprite("screen/CornerTL", x - 16, y - 20, 16, 20, color) - g.sprite("screen/CornerTR", x + w, y - 20, 16, 20, color) - g.sprite("screen/CornerBL", x - 16, y + h, 16, 16, color) - g.sprite("screen/CornerBR", x + w, y + h, 16, 16, color) + g.sprite("screen/CornerTL", x, y, 16, 20, color) + g.sprite("screen/CornerTR", x + w - 16, y, 16, 20, color) + g.sprite("screen/CornerBL", x, y + h - 16, 16, 16, color) + g.sprite("screen/CornerBR", x + w - 16, y + h - 16, 16, 16, color) - g.sprite("screen/BorderT", x, y - 20, w, 20, color) - g.sprite("screen/BorderB", x, y + h, w, 16, color) + g.sprite("screen/BorderT", x + 16, y, w - 16 - 16, 20, color) + g.sprite("screen/BorderB", x + 16, y + h - 16, w - 16 - 16, 16, color) g.save() - g.translate(x - 16, y) + g.translate(x, y - 16) g.rotate(270.toRadians) - g.sprite("screen/BorderB", -h, 0, h, 16, color) + g.sprite("screen/BorderB", -h, 0, h - 20 - 16, 16, color) g.restore() g.save() - g.translate(x + w, y) + g.translate(x + w - 16, y - 16) g.rotate(270.toRadians) - g.sprite("screen/BorderB", -h, 0, h, 16, color) + g.sprite("screen/BorderB", -h, 0, h - 20 - 16, 16, color) g.restore() } @@ -95,6 +95,8 @@ object DrawUtils { if (alpha < 1f) g.endGroupAlpha(alpha) } + type BorderRenderer = (Graphics, Float, Float, Float, Float, Color) => Unit + def windowWithShadow( g: Graphics, x: Float, @@ -103,9 +105,10 @@ object DrawUtils { h: Float, backgroundAlpha: Float, shadowAlpha: Float, + borderRenderer: BorderRenderer = windowBorder ): Unit = { DrawUtils.shadow(g, x - 8, y - 8, w + 16, h + 20, shadowAlpha) - DrawUtils.windowBorder(g, x, y, w, h, RGBAColorNorm(1, 1, 1, backgroundAlpha)) + borderRenderer(g, x, y, w, h, RGBAColorNorm(1, 1, 1, backgroundAlpha)) } def ring( diff --git a/src/main/scala/ocelot/desktop/util/Register.scala b/src/main/scala/ocelot/desktop/util/Register.scala new file mode 100644 index 0000000..c3840c2 --- /dev/null +++ b/src/main/scala/ocelot/desktop/util/Register.scala @@ -0,0 +1,64 @@ +package ocelot.desktop.util + +import ocelot.desktop.ui.widget.Updatable + +/** + * Stores a value updated by calls to [[update]]. + */ +trait Register[T] { + /** + * The currently stored value. + */ + def value: T + + /** + * Updates the stored value. + * + * @return `true` if the value has changed. + */ + def update(): Boolean +} + +object Register { + class Writeable[T](initialValue: T) extends Register[T] { + private var _value: T = initialValue + override def value: T = _value + + /** + * The value this register will be set to on next update. + */ + var nextValue: T = _value + + override def update(): Boolean = { + val changed = nextValue != _value + _value = nextValue + + changed + } + } + + class Sampling[T](next: () => T) extends Register[T] { + private var _value = next() + override def value: T = _value + + override def update(): Boolean = { + val nextValue = next() + val changed = nextValue != _value + _value = nextValue + + changed + } + } + + /** + * Creates a [[Register.Writeable writeable register]] with the given initial value. + */ + def apply[T](initialValue: T): Writeable[T] = new Writeable(initialValue) + + /** + * Creates a [[Register.Sampling register]] that reevaluates the provided expression on every update. + * + * The expression is evaluated to compute the initial value. + */ + def sampling[T](nextValue: => T): Sampling[T] = new Sampling(() => nextValue) +} diff --git a/src/main/scala/ocelot/desktop/windows/ScreenWindow.scala b/src/main/scala/ocelot/desktop/windows/ScreenWindow.scala index 6c25db3..06c4e93 100644 --- a/src/main/scala/ocelot/desktop/windows/ScreenWindow.scala +++ b/src/main/scala/ocelot/desktop/windows/ScreenWindow.scala @@ -1,122 +1,59 @@ package ocelot.desktop.windows -import ocelot.desktop.audio.SoundSource -import ocelot.desktop.color.RGBAColor -import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D} -import ocelot.desktop.graphics.Graphics -import ocelot.desktop.graphics.Texture.MinFilteringMode +import ocelot.desktop.geometry.{Padding2D, Rect2D, Size2D, Vector2D} import ocelot.desktop.node.nodes.ScreenNode import ocelot.desktop.node.nodes.ScreenNode.{FontHeight, FontWidth} import ocelot.desktop.ui.UiHandler -import ocelot.desktop.ui.event.sources.{KeyEvents, MouseEvents} -import ocelot.desktop.ui.event.{DragEvent, KeyEvent, MouseEvent, ScrollEvent} -import ocelot.desktop.ui.widget.window.BasicWindow -import ocelot.desktop.util.{DrawUtils, Keybind, Logging} +import ocelot.desktop.ui.event.sources.KeyEvents +import ocelot.desktop.ui.event.{DragEvent, MouseEvent} +import ocelot.desktop.ui.widget.ScreenView +import ocelot.desktop.ui.widget.window.PanelWindow +import ocelot.desktop.util.DrawUtils.BorderRenderer +import ocelot.desktop.util.{DrawUtils, Logging} import ocelot.desktop.windows.ScreenWindow._ -import ocelot.desktop.{ColorScheme, OcelotDesktop, Settings} -import org.apache.commons.lang3.StringUtils -import totoro.ocelot.brain.entity.Screen import totoro.ocelot.brain.nbt.NBTTagCompound -import totoro.ocelot.brain.util.Tier -class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging { - private var lastMousePos = Vector2D(0, 0) - private var sentTouchEvent = false +class ScreenWindow(screenNode: ScreenNode) extends PanelWindow with Logging { private var startingWidth = 0f private var scaleDragPoint: Option[Vector2D] = None - private var _scale = 1f - private var scaleX: Float = 1f - private var scaleY: Float = 1f + override protected def title: String = screenNode.label.get - private def scale: Float = _scale - - private def scale_=(value: Float): Any = { - _scale = value - - scaleX = (FontWidth * scale).floor / FontWidth - scaleY = (FontHeight * scale).floor / FontHeight - } - - private def screen: Screen = screenNode.screen + override protected def titleMaxLength: Int = + ((screenWidth * FontWidth * View.scaleX - 15) / FontWidth).toInt private def screenWidth: Int = screenNode.screenWidth private def screenHeight: Int = screenNode.screenHeight - override def minimumSize: Size2D = Size2D( - screenWidth * FontWidth * scaleX + BorderHorizontal, - screenHeight * scaleY * FontHeight + BorderVertical, - ) - override def receiveScrollEvents: Boolean = true + override def maximumSize: Size2D = minimumSize + + private object View extends ScreenView(screenNode) { + override protected def isFocused: Boolean = ScreenWindow.this.isFocused + } + + setInner( + View, + padding = Padding2D( + right = BorderRight, + bottom = BorderBottom, + left = BorderLeft, + ), + titlePadding = Padding2D( + top = 2, + right = BorderRight - 4, + bottom = 2, + left = BorderLeft - 4, + ), + ) + eventHandlers += { - case event: KeyEvent if shouldHandleKeys => - event.state match { - case KeyEvent.State.Press | KeyEvent.State.Repeat => - screen.keyDown(event.char, event.code, OcelotDesktop.player) - - // note: in OpenComputers, key_down signal is fired __before__ clipboard signal - if (event.code == Settings.get.keymap(Keybind.Insert)) - screen.clipboard(UiHandler.clipboard, OcelotDesktop.player) - - case KeyEvent.State.Release => - screen.keyUp(event.char, event.code, OcelotDesktop.player) - } - - event.consume() - - case event: MouseEvent if shouldHandleKeys => - val pos = convertMousePos(UiHandler.mousePosition) - val inside = checkBounds(pos) - - if (inside) - lastMousePos = pos - - event.state match { - case MouseEvent.State.Press => - if (inside && screen.tier > Tier.One) { - screen.mouseDown(pos.x, pos.y, event.button.id, OcelotDesktop.player) - sentTouchEvent = true - event.consume() - } - - if ( - pinButtonBounds.contains(UiHandler.mousePosition) || closeButtonBounds.contains(UiHandler.mousePosition) - ) { - SoundSource.InterfaceClick.press.play() - } - - case MouseEvent.State.Release => - if (event.button == MouseEvent.Button.Middle && inside) { - screen.clipboard(UiHandler.clipboard, OcelotDesktop.player) - event.consume() - } - - if (sentTouchEvent) { - screen.mouseUp(lastMousePos.x, lastMousePos.y, event.button.id, OcelotDesktop.player) - event.consume() - sentTouchEvent = false - } else if (pinButtonBounds.contains(UiHandler.mousePosition)) { - if (isPinned) unpin() else pin() - SoundSource.InterfaceClick.release.play() - } else if (closeButtonBounds.contains(UiHandler.mousePosition)) { - close() - SoundSource.InterfaceClick.release.play() - } - - case _ => - } - - case event: ScrollEvent if shouldHandleKeys && screen.tier > Tier.One => - screen.mouseScroll(lastMousePos.x, lastMousePos.y, event.offset, OcelotDesktop.player) - event.consume() - case event @ DragEvent(DragEvent.State.Start, MouseEvent.Button.Left, _) => if (scaleDragRegion.contains(event.start)) { scaleDragPoint = Some(event.start) - startingWidth = screenWidth * FontWidth * scaleX + startingWidth = screenWidth * FontWidth * View.scaleX } case DragEvent(DragEvent.State.Drag, MouseEvent.Button.Left, mousePos) => @@ -125,7 +62,7 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging { val sy = point.y - mousePos.y val uiScale = UiHandler.scalingFactor - var newScale = scale + var newScale = View.scale.nextValue // TODO: refactor this mess, make it consider both sizes and not have two nearby slightly off "snap points" @@ -134,7 +71,7 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging { val maxWidth = screenWidth * FontWidth var midScale = (newWidth / maxWidth).max(0f) - if (!KeyEvents.isShiftDown && scale <= 1.001) + if (!KeyEvents.isShiftDown && View.scale.nextValue <= 1.001) midScale = midScale.min(1f) val lowScale = (FontWidth * midScale * uiScale).floor / FontWidth / uiScale @@ -146,7 +83,7 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging { val maxHeight = screenHeight * FontHeight var midScale = (newHeight / maxHeight).max(0f) - if (!KeyEvents.isShiftDown && scale <= 1.001) + if (!KeyEvents.isShiftDown && View.scale.nextValue <= 1.001) midScale = midScale.min(1f) val lowScale = (FontHeight * midScale * uiScale).floor / FontHeight / uiScale @@ -155,28 +92,30 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging { newScale = if (midScale - lowScale > highScale - midScale) highScale else lowScale } - if (newScale != scale) - scale = newScale - // enforce minimal screen size - if (scale <= 0.249f) { - scale = 0.25f + if (newScale <= 0.249f) { + newScale = 0.25f } + + View.scale.nextValue = newScale } case DragEvent(DragEvent.State.Stop, MouseEvent.Button.Left, _) => scaleDragPoint = None } - private def shouldHandleKeys: Boolean = isFocused && !root.get.modalDialogPool.isVisible - override def save(nbt: NBTTagCompound): Unit = { - nbt.setFloat("scale", scale) + View.save(nbt) super.save(nbt) } + override def load(nbt: NBTTagCompound): Unit = { + super.load(nbt) + View.load(nbt) + } + override def fitToCenter(): Unit = { - scale = math.min( + View.scale.nextValue = math.min( ((UiHandler.root.width * 0.9f) / (screenWidth * FontWidth + BorderHorizontal)).min(1f).max(0f), ((UiHandler.root.height * 0.9f) / (screenHeight * FontHeight + BorderVertical)).min(1f).max(0f), ) @@ -184,29 +123,6 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging { super.fitToCenter() } - override def load(nbt: NBTTagCompound): Unit = { - scale = nbt.getFloat("scale") - - super.load(nbt) - } - - private def checkBounds(p: Vector2D): Boolean = p.x >= 0 && p.y >= 0 && p.x < screenWidth && p.y < screenHeight - - private def convertMousePos(p: Vector2D): Vector2D = { - // no synchronization here (see the note in ScreenNode): the method to change this property is indirect. - if (screen.getPrecisionMode) { - Vector2D( - (p.x - BorderLeft - position.x) / FontWidth / scaleX, - (p.y - BorderTop - position.y) / FontHeight / scaleY, - ) - } else { - Vector2D( - math.floor((p.x - BorderLeft - position.x) / FontWidth / scaleX), - math.floor((p.y - BorderTop - position.y) / FontHeight / scaleY), - ) - } - } - override protected def dragRegions: Iterator[Rect2D] = Iterator( Rect2D(position.x, position.y, size.width, BorderTop.toFloat), Rect2D(position.x, position.y, BorderLeft.toFloat, size.height), @@ -230,82 +146,9 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging { root.get.statusBar.addMouseEntry("icons/DragLMB", "Scale screen") root.get.statusBar.addKeyMouseEntry("icons/DragLMB", "SHIFT", "Scale screen (magnify)") } - - val currentMousePos = convertMousePos(UiHandler.mousePosition) - if (!checkBounds(currentMousePos) || currentMousePos == lastMousePos) return - - lastMousePos = currentMousePos - - if (isFocused && screen.tier > Tier.One) { - for (button <- MouseEvents.pressedButtons) { - screen.mouseDrag(lastMousePos.x, lastMousePos.y, button.id, OcelotDesktop.player) - } - } } - private def pinButtonBounds: Rect2D = Rect2D( - position.x + screenWidth * FontWidth * scaleX - 13, - position.y + 3, - 14, - 14, - ) - - private def closeButtonBounds: Rect2D = Rect2D( - position.x + screenWidth * FontWidth * scaleX + 2, - position.y + 3, - 15, - 14, - ) - - override def draw(g: Graphics): Unit = { - beginDraw(g) - - val startX = position.x + BorderLeft - val startY = position.y + BorderTop - val windowWidth = screenWidth * FontWidth * scaleX - val windowHeight = screenHeight * FontHeight * scaleY - - DrawUtils.shadow(g, startX - 22, startY - 22, windowWidth + 44, windowHeight + 52, 0.5f) - DrawUtils.screenBorder(g, startX, startY, windowWidth, windowHeight) - - // no synchronization here (see the note in ScreenNode): the methods to turn the screen on/off are indirect. - if (screen.getPowerState) { - screenNode.drawScreenData( - g, - startX, - startY, - scaleX, - scaleY, - if (Settings.get.screenWindowMipmap) { - MinFilteringMode.LinearMipmapLinear - } else { - MinFilteringMode.Nearest - }, - ) - - } else { - g.rect(startX, startY, windowWidth, windowHeight, ColorScheme("ScreenOff")) - } - - g.setSmallFont() - g.background = RGBAColor(0, 0, 0, 0) - g.foreground = RGBAColor(110, 110, 110) - - val freeSpace = ((windowWidth - 15) / 8).toInt - val label = screenNode.label.get - val text = if (label.length <= freeSpace) - label - else - StringUtils.substring(label, 0, (freeSpace - 1).max(0).min(label.length)) + "…" - - g.text(startX - 4, startY - 14, text) - g.setNormalFont() - - g.sprite(if (isPinned) "icons/Unpin" else "icons/Pin", pinButtonBounds) - g.sprite("icons/Close", closeButtonBounds) - - endDraw(g) - } + override protected def borderRenderer: BorderRenderer = DrawUtils.screenBorder } object ScreenWindow {