2023-03-28 01:07:36 +03:00

355 lines
11 KiB
Scala

package ocelot.desktop.ui
import buildinfo.BuildInfo
import ocelot.desktop.audio.Audio
import ocelot.desktop.geometry.{Size2D, Vector2D}
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.ui.event.MouseEvent
import ocelot.desktop.ui.event.handlers.HoverHandler
import ocelot.desktop.ui.event.sources.{KeyEvents, MouseEvents, ScrollEvents}
import ocelot.desktop.ui.widget.{RootWidget, Widget}
import ocelot.desktop.util._
import ocelot.desktop.{OcelotDesktop, Settings}
import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.SystemUtils
import org.lwjgl.BufferUtils
import org.lwjgl.input.Mouse
import org.lwjgl.opengl._
import java.awt.Toolkit
import java.awt.datatransfer.{DataFlavor, StringSelection, UnsupportedFlavorException}
import java.io.{File, FileOutputStream}
import java.nio.ByteBuffer
import java.nio.channels.Channels
import java.nio.file.{Files, Paths}
import javax.imageio.ImageIO
import scala.collection.mutable
object UiHandler extends Logging {
var root: RootWidget = _
var graphics: Graphics = _
private val hierarchy = mutable.ArrayBuffer(root: Widget)
private var shouldUpdateHierarchy = true
private val fpsCalculator = new FPSCalculator
private val ticker = new Ticker
ticker.tickInterval = 1000f / 60f
def getHierarchy: Array[Widget] = hierarchy.toArray
def updateHierarchy(): Unit = {
shouldUpdateHierarchy = true
}
private def _updateHierarchy(): Unit = {
val stack = mutable.Stack(root: Widget)
hierarchy.clear()
while (stack.nonEmpty) {
val widget = stack.pop()
hierarchy += widget
for (child <- widget.hierarchy.reverseIterator) {
stack.push(child)
}
}
shouldUpdateHierarchy = false
}
def fps: Float = fpsCalculator.fps
def dt: Float = fpsCalculator.dt
def mousePosition: Vector2D = {
Vector2D(Mouse.getX, Display.getHeight - Mouse.getY)
}
private val _clipboard = Toolkit.getDefaultToolkit.getSystemClipboard
def clipboard: String = try {
_clipboard.getData(DataFlavor.stringFlavor).toString
} catch {
case _: UnsupportedFlavorException =>
logger.debug("Trying to paste non-Unicode content from clipboard! Replaced with empty string.")
""
}
def clipboard_=(value: String): Unit = {
val data = new StringSelection(value)
_clipboard.setContents(data, data)
}
def isFullScreen: Boolean = Display.isFullscreen
def isFullScreen_=(value: Boolean): Any = {
if (value) {
Display.setDisplayModeAndFullscreen(Display.getDesktopDisplayMode)
}
else {
// Updating size from settings
val settingsSize = Settings.get.windowSize
root.size = if (settingsSize.isSet) Size2D(settingsSize.x, settingsSize.y) else Size2D(800, 600)
// Setting normal display mode (non-fullscreen by default)
Display.setDisplayMode(new DisplayMode(root.size.width.toInt, root.size.height.toInt))
// Updating position from settings
if (Settings.get.windowPosition.isSet) {
if (Settings.get.windowValidatePosition) {
// the configuration can have incorrect values for coordinates
// and that can cause the window to "disappear" from the screen
// TODO: after upgrading to LWJGL 3 consider the multi-monitor setups
val desktop = Display.getDesktopDisplayMode
if (Settings.get.windowPosition.x < -Settings.get.windowSize.x)
Settings.get.windowPosition.x = 0
if (Settings.get.windowPosition.x > desktop.getWidth)
Settings.get.windowPosition.x = desktop.getWidth - Settings.get.windowSize.x
if (Settings.get.windowPosition.y < -Settings.get.windowSize.y)
Settings.get.windowPosition.y = 0
if (Settings.get.windowPosition.y > desktop.getHeight)
Settings.get.windowPosition.y = desktop.getHeight - Settings.get.windowSize.y
}
Display.setLocation(Settings.get.windowPosition.x, Settings.get.windowPosition.y)
}
// Window should be resizable after exit from fullscreen mode
// But it seems like there's lwjgl bug with internal resizable value caching
// How to reproduce:
// 1) Enter fullscreen mode via setDisplayModeAndFullscreen(...) or setFullScreen(true)
// 2) Set window resizable via setResizable(true)
// 3) Exit from fullscreen mode
// 4) Set window resizable again
// Result:
// Window is not resizable and can't be resized with mouse
// Expected behaviour:
// Window should be resizable
Display.setResizable(false)
Display.setResizable(true)
}
Settings.get.windowFullscreen = value
}
def init(root: RootWidget): Unit = {
this.root = root
root.relayout()
loadLibraries()
isFullScreen = Settings.get.windowFullscreen
windowTitle = "Ocelot Desktop v" + BuildInfo.version
loadIcons()
Display.setVSyncEnabled(true)
Display.create()
KeyEvents.init()
MouseEvents.init()
logger.info(s"Created window with ${root.size}")
logger.info(s"OpenGL vendor: ${GL11.glGetString(GL11.GL_VENDOR)}")
logger.info(s"OpenGL renderer: ${GL11.glGetString(GL11.GL_RENDERER)}")
Spritesheet.load()
graphics = new Graphics
Audio.init()
}
private var nativeLibrariesDir: String = _
private def loadLibraries(): Unit = {
// we cannot remove DLL files on Windows after they were loaded by Ocelot
// therefore we will create them in local directory and keep for future
nativeLibrariesDir = if (SystemUtils.IS_OS_WINDOWS)
Paths.get(this.getClass.getProtectionDomain.getCodeSource.getLocation.toURI.resolve( "natives")).toString
else Files.createTempDirectory("ocelot-desktop").toString
val arch = System.getProperty("os.arch")
val is64bit = arch.startsWith("amd64")
val libs = if (SystemUtils.IS_OS_WINDOWS)
if (is64bit)
Array("jinput-dx8_64.dll", "jinput-raw_64.dll", "jinput-wintab.dll", "lwjgl64.dll", "OpenAL64.dll")
else
Array("jinput-dx8.dll", "jinput-raw.dll", "jinput-wintab.dll", "lwjgl.dll", "OpenAL32.dll")
else if (SystemUtils.IS_OS_MAC_OSX)
Array("liblwjgl.dylib")
else if (SystemUtils.IS_OS_LINUX)
if (is64bit)
Array("libjinput-linux64.so", "liblwjgl64.so", "libopenal64.so")
else
Array("libjinput-linux.so", "liblwjgl.so", "libopenal.so")
else
throw new Exception("Unsupported OS")
logger.debug("Unpacking native libraries to: " + nativeLibrariesDir)
for (lib <- libs) {
val dest = new File(Paths.get(nativeLibrariesDir, lib).toString)
if (!dest.exists()) {
val source = getClass.getResourceAsStream("/" + lib)
if (!dest.getParentFile.exists())
dest.getParentFile.mkdirs()
val output = new FileOutputStream(dest)
output.getChannel.transferFrom(Channels.newChannel(source), 0, Long.MaxValue)
output.flush()
output.close()
source.close()
}
}
System.setProperty("org.lwjgl.librarypath", nativeLibrariesDir)
}
private def loadIcons(): Unit = {
val sizes = Array(256, 128, 64, 32, 16)
val list = new Array[ByteBuffer](sizes.length)
for ((size, i) <- sizes.zipWithIndex) {
val imageURL = getClass.getResource(s"/ocelot/desktop/images/icon$size.png")
val image = ImageIO.read(imageURL)
val pixels = new Array[Int](image.getWidth * image.getHeight)
image.getRGB(0, 0, image.getWidth, image.getHeight, pixels, 0, image.getWidth)
val buf = BufferUtils.createByteBuffer(image.getWidth * image.getHeight * 4)
for (y <- 0 until image.getHeight) {
for (x <- 0 until image.getWidth) {
val pixel = pixels(y * image.getWidth + x)
buf.put(((pixel >> 16) & 0xFF).toByte)
buf.put(((pixel >> 8) & 0xFF).toByte)
buf.put((pixel & 0xFF).toByte)
buf.put(((pixel >> 24) & 0xFF).toByte)
}
}
buf.flip()
list(i) = buf
}
Display.setIcon(list)
logger.info(s"Loaded window icons of sizes ${sizes.mkString(", ")}")
}
//noinspection ScalaWeakerAccess
def windowTitle: String = Display.getTitle
//noinspection ScalaWeakerAccess
def windowTitle_=(title: String): Unit = Display.setTitle(title)
private var exitRequested = false
def exit(): Unit = {
exitRequested = true
}
def start(): Unit = {
while (!exitRequested) {
if (Display.isCloseRequested)
OcelotDesktop.exit()
OcelotDesktop.withTickLockAcquired {
updateWindowSizeAndPosition()
Audio.update()
KeyEvents.update()
MouseEvents.update()
Profiler.startTimeMeasurement("000_update")
update()
Profiler.endTimeMeasurement("000_update")
Profiler.startTimeMeasurement("001_draw")
draw()
Profiler.endTimeMeasurement("001_draw")
}
Profiler.startTimeMeasurement("002_sleep")
ticker.waitNext()
Display.update()
fpsCalculator.tick()
Profiler.endTimeMeasurement("002_sleep")
}
}
def terminate(): Unit = {
root.workspaceView.dispose()
KeyEvents.destroy()
MouseEvents.destroy()
Display.destroy()
Audio.destroy()
if (!SystemUtils.IS_OS_WINDOWS)
FileUtils.deleteDirectory(new File(nativeLibrariesDir))
}
private def update(): Unit = {
if (shouldUpdateHierarchy) _updateHierarchy()
val mousePos = mousePosition
if (mousePos.x < 0 || mousePos.y < 0 || mousePos.x > root.width || mousePos.y > root.height) {
KeyEvents.releaseKeys()
MouseEvents.releaseButtons()
}
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))
val mouseTarget = hierarchy.reverseIterator
.find(w => w.receiveMouseEvents && w.clippedBounds.contains(mousePos))
for (event <- ScrollEvents.events)
scrollTarget.foreach(_.handleEvent(event))
for (event <- MouseEvents.events)
if (event.state == MouseEvent.State.Press) {
mouseTarget.foreach(_.handleEvent(event))
} else
hierarchy.reverseIterator.foreach(_.handleEvent(event))
hierarchy.reverseIterator.foreach {
case handler: HoverHandler if !mouseTarget.contains(handler) => handler.setHovered(false)
case _ =>
}
mouseTarget.foreach {
case handler: HoverHandler => handler.setHovered(true)
case _ =>
}
root.update()
}
private def updateWindowSizeAndPosition(): Unit = {
val width = Display.getWidth
val height = Display.getHeight
graphics.resize(width, height)
root.size = Size2D(width, height)
// Settings fields should be updated only in non-fullscreen mode
if (isFullScreen)
return
Settings.get.windowSize.set(width, height)
Settings.get.windowPosition.set(Display.getX, Display.getY)
}
private def draw(): Unit = {
graphics.setViewport(root.width.asInstanceOf[Int], root.height.asInstanceOf[Int])
graphics.clear()
root.draw(graphics)
graphics.flush()
graphics.update()
}
}