Pull ScreenView out of ScreenWindow

This commit is contained in:
Fingercomp 2025-07-31 18:29:53 +03:00
parent 6babdcf6d8
commit 182d42a843
No known key found for this signature in database
GPG Key ID: BBC71CEE45D86E37
21 changed files with 346 additions and 241 deletions

View File

@ -460,7 +460,7 @@ object UiHandler extends Logging {
ScrollEvents.events.foreach(dispatchEvent(scrollTarget)) ScrollEvents.events.foreach(dispatchEvent(scrollTarget))
for (event <- MouseEvents.events) { for (event <- MouseEvents.events) {
if (event.state == MouseEvent.State.Press) { if (event.state == MouseEvent.State.Pressed) {
dispatchEvent(mouseTarget)(event) dispatchEvent(mouseTarget)(event)
// TODO: this should be done in the event capturing phase in [[Window]] itself. // TODO: this should be done in the event capturing phase in [[Window]] itself.

View File

@ -2,7 +2,7 @@ package ocelot.desktop.ui.event
object MouseEvent { object MouseEvent {
object State extends Enumeration { object State extends Enumeration {
val Press, Release = Value val Pressed, Released = Value
} }
object Button extends Enumeration { object Button extends Enumeration {

View File

@ -25,10 +25,10 @@ trait MouseHandler extends Widget {
protected def allowClickReleaseOutsideThreshold: Boolean = !receiveDragEvents protected def allowClickReleaseOutsideThreshold: Boolean = !receiveDragEvents
eventHandlers += { eventHandlers += {
case MouseEvent(MouseEvent.State.Press, button) => case MouseEvent(MouseEvent.State.Pressed, button) =>
startPositions += (button -> UiHandler.mousePosition) startPositions += (button -> UiHandler.mousePosition)
case MouseEvent(MouseEvent.State.Release, button) => case MouseEvent(MouseEvent.State.Released, button) =>
val mousePos = UiHandler.mousePosition val mousePos = UiHandler.mousePosition
val dragStopped = receiveDragEvents && dragButtons.remove(button) val dragStopped = receiveDragEvents && dragButtons.remove(button)

View File

@ -25,12 +25,12 @@ object MouseEvents {
if (MouseEvent.Button.values.map(_.id).contains(buttonIdx)) { if (MouseEvent.Button.values.map(_.id).contains(buttonIdx)) {
val button = MouseEvent.Button(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) _events += MouseEvent(state, button)
state match { state match {
case MouseEvent.State.Press => case MouseEvent.State.Pressed =>
_pressedButtons += button _pressedButtons += button
case MouseEvent.State.Release => case MouseEvent.State.Released =>
_pressedButtons -= button _pressedButtons -= button
} }
} }
@ -49,7 +49,7 @@ object MouseEvents {
def releaseButtons(): Unit = { def releaseButtons(): Unit = {
for (button <- pressedButtons) { for (button <- pressedButtons) {
_events += MouseEvent(MouseEvent.State.Release, button) _events += MouseEvent(MouseEvent.State.Released, button)
} }
_pressedButtons.clear() _pressedButtons.clear()

View File

@ -26,7 +26,7 @@ class Button(tooltip: Option[Tooltip] = None) extends Widget with MouseHandler w
override protected def receiveClickEvents: Boolean = true override protected def receiveClickEvents: Boolean = true
eventHandlers += { eventHandlers += {
case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) if enabled => case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) if enabled =>
clickSoundSource.press.play() clickSoundSource.press.play()
case ClickEvent(MouseEvent.Button.Left, _) if enabled => case ClickEvent(MouseEvent.Button.Left, _) if enabled =>

View File

@ -35,7 +35,7 @@ class IconButton(
case HoverEvent(HoverEvent.State.Enter) => onHoverEnter() case HoverEvent(HoverEvent.State.Enter) => onHoverEnter()
case HoverEvent(HoverEvent.State.Leave) => onHoverLeave() case HoverEvent(HoverEvent.State.Leave) => onHoverLeave()
case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) => case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) =>
mode match { mode match {
case Mode.Regular => handlePress() case Mode.Regular => handlePress()
case _ => // the other modes are triggered on click. case _ => // the other modes are triggered on click.
@ -43,7 +43,7 @@ class IconButton(
clickSoundSource.press.play() 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 => case Mode.Regular if model.pressed =>
handleRelease() handleRelease()
clickSoundSource.release.play() clickSoundSource.release.play()

View File

@ -28,14 +28,14 @@ abstract class Knob(dyeColor: DyeColor = DyeColor.Red) extends Widget {
private var startValue: Int = 0 private var startValue: Int = 0
eventHandlers += { eventHandlers += {
case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) => case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) =>
val mousePos = UiHandler.mousePosition val mousePos = UiHandler.mousePosition
if (bounds.contains(mousePos)) { if (bounds.contains(mousePos)) {
movePos = Some(mousePos) movePos = Some(mousePos)
startValue = input startValue = input
} }
case MouseEvent(MouseEvent.State.Release, MouseEvent.Button.Left) => case MouseEvent(MouseEvent.State.Released, MouseEvent.Button.Left) =>
movePos = None movePos = None
} }

View File

@ -31,7 +31,7 @@ class MenuBarButton(label: String, handler: () => Unit = () => {}) extends Widge
def onMouseLeave(): Unit = colorAnimation.goto(ColorScheme("TitleBarBackground")) def onMouseLeave(): Unit = colorAnimation.goto(ColorScheme("TitleBarBackground"))
eventHandlers += { eventHandlers += {
case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) => case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) =>
clickSoundSource.press.play() clickSoundSource.press.play()
case ClickEvent(MouseEvent.Button.Left, _) => case ClickEvent(MouseEvent.Button.Left, _) =>

View File

@ -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"
}

View File

@ -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 val pos = UiHandler.mousePosition
mouseOldPos = pos mouseOldPos = pos
@ -59,7 +59,7 @@ class ScrollView(val inner: Widget) extends Widget with Logging with HoverHandle
scrollToEnd = false scrollToEnd = false
} }
case event: MouseEvent if event.state == MouseEvent.State.Release => case event: MouseEvent if event.state == MouseEvent.State.Released =>
dragging = 0 dragging = 0
} }

View File

@ -34,7 +34,7 @@ class Slider(var value: Float, val text: String, val snapPoints: Int = 0)
} }
eventHandlers += { eventHandlers += {
case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) => case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) =>
clickSoundSource.press.play() clickSoundSource.press.play()
case ClickEvent(MouseEvent.Button.Left, pos) => case ClickEvent(MouseEvent.Button.Left, pos) =>

View File

@ -58,7 +58,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
private var prevEnabled = enabled private var prevEnabled = enabled
eventHandlers += { eventHandlers += {
case MouseEvent(MouseEvent.State.Release, MouseEvent.Button.Left) => case MouseEvent(MouseEvent.State.Released, MouseEvent.Button.Left) =>
if (isFocused && !clippedBounds.contains(UiHandler.mousePosition)) { if (isFocused && !clippedBounds.contains(UiHandler.mousePosition)) {
unfocus() unfocus()
} }

View File

@ -69,7 +69,7 @@ class ContextMenuEntry(
override protected def receiveClickEvents: Boolean = true override protected def receiveClickEvents: Boolean = true
eventHandlers += { 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() clickSoundSource.press.play()
case ClickEvent(MouseEvent.Button.Left, _) if !contextMenu.isOpening => clicked() case ClickEvent(MouseEvent.Button.Left, _) if !contextMenu.isOpening => clicked()
case HoverEvent(HoverEvent.State.Enter) => enter() case HoverEvent(HoverEvent.State.Enter) => enter()

View File

@ -27,7 +27,7 @@ class ContextMenus extends Widget {
case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_ESCAPE, _) => case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_ESCAPE, _) =>
closeAll() closeAll()
case MouseEvent(MouseEvent.State.Press, _) => case MouseEvent(MouseEvent.State.Pressed, _) =>
if (!menus.map(_.bounds).exists(_.contains(UiHandler.mousePosition))) closeAll() if (!menus.map(_.bounds).exists(_.contains(UiHandler.mousePosition))) closeAll()
} }

View File

@ -46,7 +46,7 @@ class VerticalMenuButton(icon: IconSource, label: String, handler: VerticalMenuB
def onMouseLeave(): Unit = colorAnimation.goto(ColorScheme("VerticalMenuBackground")) def onMouseLeave(): Unit = colorAnimation.goto(ColorScheme("VerticalMenuBackground"))
eventHandlers += { eventHandlers += {
case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) => case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) =>
clickSoundSource.press.play() clickSoundSource.press.play()
case ClickEvent(MouseEvent.Button.Left, _) => case ClickEvent(MouseEvent.Button.Left, _) =>

View File

@ -53,9 +53,11 @@ trait BasicWindow extends Window {
} }
} }
protected def borderRenderer: DrawUtils.BorderRenderer = DrawUtils.windowBorder
override def draw(g: Graphics): Unit = { override def draw(g: Graphics): Unit = {
beginDraw(g) 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) drawChildren(g)
endDraw(g) endDraw(g)
} }

View File

@ -14,7 +14,11 @@ trait PanelWindow extends BasicWindow {
protected def titleMaxLength: Int = 32 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 = ArraySeq.empty
children :+= new PaddingBox( children :+= new PaddingBox(
@ -22,7 +26,7 @@ trait PanelWindow extends BasicWindow {
override def title: String = PanelWindow.this.title override def title: String = PanelWindow.this.title
override def titleMaxLength: Int = PanelWindow.this.titleMaxLength override def titleMaxLength: Int = PanelWindow.this.titleMaxLength
}, },
Padding2D(top = 8, left = 12, right = 12, bottom = 2), titlePadding,
) )
children :+= new PaddingBox(inner, padding) children :+= new PaddingBox(inner, padding)

View File

@ -30,7 +30,7 @@ trait Window extends Widget with Persistable with MouseHandler {
override protected def receiveDragEvents: Boolean = true override protected def receiveDragEvents: Boolean = true
eventHandlers += { eventHandlers += {
case MouseEvent(MouseEvent.State.Press, _) => case MouseEvent(MouseEvent.State.Pressed, _) =>
focus() focus()
case ev @ DragEvent(DragEvent.State.Start, MouseEvent.Button.Left, mousePos) => case ev @ DragEvent(DragEvent.State.Start, MouseEvent.Button.Left, mousePos) =>

View File

@ -17,24 +17,24 @@ object DrawUtils {
h: Float, h: Float,
color: Color = RGBAColor(255, 255, 255), color: Color = RGBAColor(255, 255, 255),
): Unit = { ): Unit = {
g.sprite("screen/CornerTL", x - 16, y - 20, 16, 20, color) g.sprite("screen/CornerTL", x, y, 16, 20, color)
g.sprite("screen/CornerTR", x + w, y - 20, 16, 20, color) g.sprite("screen/CornerTR", x + w - 16, y, 16, 20, color)
g.sprite("screen/CornerBL", x - 16, y + h, 16, 16, color) g.sprite("screen/CornerBL", x, y + h - 16, 16, 16, color)
g.sprite("screen/CornerBR", x + w, y + h, 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/BorderT", x + 16, y, w - 16 - 16, 20, color)
g.sprite("screen/BorderB", x, y + h, w, 16, color) g.sprite("screen/BorderB", x + 16, y + h - 16, w - 16 - 16, 16, color)
g.save() g.save()
g.translate(x - 16, y) g.translate(x, y - 16)
g.rotate(270.toRadians) 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.restore()
g.save() g.save()
g.translate(x + w, y) g.translate(x + w - 16, y - 16)
g.rotate(270.toRadians) 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.restore()
} }
@ -95,6 +95,8 @@ object DrawUtils {
if (alpha < 1f) g.endGroupAlpha(alpha) if (alpha < 1f) g.endGroupAlpha(alpha)
} }
type BorderRenderer = (Graphics, Float, Float, Float, Float, Color) => Unit
def windowWithShadow( def windowWithShadow(
g: Graphics, g: Graphics,
x: Float, x: Float,
@ -103,9 +105,10 @@ object DrawUtils {
h: Float, h: Float,
backgroundAlpha: Float, backgroundAlpha: Float,
shadowAlpha: Float, shadowAlpha: Float,
borderRenderer: BorderRenderer = windowBorder
): Unit = { ): Unit = {
DrawUtils.shadow(g, x - 8, y - 8, w + 16, 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)) borderRenderer(g, x, y, w, h, RGBAColorNorm(1, 1, 1, backgroundAlpha))
} }
def ring( def ring(

View File

@ -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)
}

View File

@ -1,122 +1,59 @@
package ocelot.desktop.windows package ocelot.desktop.windows
import ocelot.desktop.audio.SoundSource import ocelot.desktop.geometry.{Padding2D, Rect2D, Size2D, Vector2D}
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.node.nodes.ScreenNode import ocelot.desktop.node.nodes.ScreenNode
import ocelot.desktop.node.nodes.ScreenNode.{FontHeight, FontWidth} import ocelot.desktop.node.nodes.ScreenNode.{FontHeight, FontWidth}
import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.sources.{KeyEvents, MouseEvents} import ocelot.desktop.ui.event.sources.KeyEvents
import ocelot.desktop.ui.event.{DragEvent, KeyEvent, MouseEvent, ScrollEvent} import ocelot.desktop.ui.event.{DragEvent, MouseEvent}
import ocelot.desktop.ui.widget.window.BasicWindow import ocelot.desktop.ui.widget.ScreenView
import ocelot.desktop.util.{DrawUtils, Keybind, Logging} 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.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.nbt.NBTTagCompound
import totoro.ocelot.brain.util.Tier
class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging { class ScreenWindow(screenNode: ScreenNode) extends PanelWindow with Logging {
private var lastMousePos = Vector2D(0, 0)
private var sentTouchEvent = false
private var startingWidth = 0f private var startingWidth = 0f
private var scaleDragPoint: Option[Vector2D] = None private var scaleDragPoint: Option[Vector2D] = None
private var _scale = 1f override protected def title: String = screenNode.label.get
private var scaleX: Float = 1f
private var scaleY: Float = 1f
private def scale: Float = _scale override protected def titleMaxLength: Int =
((screenWidth * FontWidth * View.scaleX - 15) / FontWidth).toInt
private def scale_=(value: Float): Any = {
_scale = value
scaleX = (FontWidth * scale).floor / FontWidth
scaleY = (FontHeight * scale).floor / FontHeight
}
private def screen: Screen = screenNode.screen
private def screenWidth: Int = screenNode.screenWidth private def screenWidth: Int = screenNode.screenWidth
private def screenHeight: Int = screenNode.screenHeight 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 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 += { 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, _) => case event @ DragEvent(DragEvent.State.Start, MouseEvent.Button.Left, _) =>
if (scaleDragRegion.contains(event.start)) { if (scaleDragRegion.contains(event.start)) {
scaleDragPoint = Some(event.start) scaleDragPoint = Some(event.start)
startingWidth = screenWidth * FontWidth * scaleX startingWidth = screenWidth * FontWidth * View.scaleX
} }
case DragEvent(DragEvent.State.Drag, MouseEvent.Button.Left, mousePos) => 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 sy = point.y - mousePos.y
val uiScale = UiHandler.scalingFactor 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" // 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 val maxWidth = screenWidth * FontWidth
var midScale = (newWidth / maxWidth).max(0f) var midScale = (newWidth / maxWidth).max(0f)
if (!KeyEvents.isShiftDown && scale <= 1.001) if (!KeyEvents.isShiftDown && View.scale.nextValue <= 1.001)
midScale = midScale.min(1f) midScale = midScale.min(1f)
val lowScale = (FontWidth * midScale * uiScale).floor / FontWidth / uiScale val lowScale = (FontWidth * midScale * uiScale).floor / FontWidth / uiScale
@ -146,7 +83,7 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging {
val maxHeight = screenHeight * FontHeight val maxHeight = screenHeight * FontHeight
var midScale = (newHeight / maxHeight).max(0f) var midScale = (newHeight / maxHeight).max(0f)
if (!KeyEvents.isShiftDown && scale <= 1.001) if (!KeyEvents.isShiftDown && View.scale.nextValue <= 1.001)
midScale = midScale.min(1f) midScale = midScale.min(1f)
val lowScale = (FontHeight * midScale * uiScale).floor / FontHeight / uiScale 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 newScale = if (midScale - lowScale > highScale - midScale) highScale else lowScale
} }
if (newScale != scale)
scale = newScale
// enforce minimal screen size // enforce minimal screen size
if (scale <= 0.249f) { if (newScale <= 0.249f) {
scale = 0.25f newScale = 0.25f
} }
View.scale.nextValue = newScale
} }
case DragEvent(DragEvent.State.Stop, MouseEvent.Button.Left, _) => case DragEvent(DragEvent.State.Stop, MouseEvent.Button.Left, _) =>
scaleDragPoint = None scaleDragPoint = None
} }
private def shouldHandleKeys: Boolean = isFocused && !root.get.modalDialogPool.isVisible
override def save(nbt: NBTTagCompound): Unit = { override def save(nbt: NBTTagCompound): Unit = {
nbt.setFloat("scale", scale) View.save(nbt)
super.save(nbt) super.save(nbt)
} }
override def load(nbt: NBTTagCompound): Unit = {
super.load(nbt)
View.load(nbt)
}
override def fitToCenter(): Unit = { 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.width * 0.9f) / (screenWidth * FontWidth + BorderHorizontal)).min(1f).max(0f),
((UiHandler.root.height * 0.9f) / (screenHeight * FontHeight + BorderVertical)).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() 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( override protected def dragRegions: Iterator[Rect2D] = Iterator(
Rect2D(position.x, position.y, size.width, BorderTop.toFloat), Rect2D(position.x, position.y, size.width, BorderTop.toFloat),
Rect2D(position.x, position.y, BorderLeft.toFloat, size.height), 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.addMouseEntry("icons/DragLMB", "Scale screen")
root.get.statusBar.addKeyMouseEntry("icons/DragLMB", "SHIFT", "Scale screen (magnify)") 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( override protected def borderRenderer: BorderRenderer = DrawUtils.screenBorder
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)
}
} }
object ScreenWindow { object ScreenWindow {