Added MusicSetVolume and SoundSetVolume.

Also fixed:
 MusicPlay
 Fade Music in ScreenFade
 GameSound Play

--HG--
extra : convert_revision : svn%3Ae934d3cd-0d57-f34a-94d0-9e8d31ae1c29/trunk%4083

Strict

Import mojo
Import functions

'Device width and height
Global DEVICE_WIDTH%
Global DEVICE_HEIGHT%

' Screen width and height
Global SCREEN_WIDTH#
Global SCREEN_HEIGHT#

' Half of SCREEN_WIDTH and HEIGHT
Global SCREEN_WIDTH2#
Global SCREEN_HEIGHT2#

' Used for Virtual Res
Global SCREENX_RATIO# = 1
Global SCREENY_RATIO# = 1

' THE GAME!!
Global game:DiddyApp

' Used for delta timing movement
Global dt:DeltaTimer

Class DiddyApp Extends App

	Field debugOn:Bool = False
	Field drawFPSOn:Bool = False

	Field virtualResOn:Bool = True

	Field FPS% = 60

	' current Screen
	Field currentScreen:Screen
	' next Screen
	Field nextScreen:Screen
	' exit Screen
	Field exitScreen:ExitScreen = new ExitScreen()
	' used for fading
	Field screenFade:ScreenFade = New ScreenFade

	' mouse
	Field mouseX:Int, mouseY:Int
	Field mouseHit:Int

	' Store the images here
	Global images:ImageBank = New ImageBank
	' Store the sounds here
	Global sounds:SoundBank = New SoundBank
	' volume control
	Field soundVolume:Int = 100
	Field musicVolume:Int = 100
	Field musicOkay:Int

	Method OnCreate:Int()
		' Store the device width and height
		DEVICE_WIDTH = DeviceWidth()
		DEVICE_HEIGHT = DeviceHeight()

		SetScreenSize(DEVICE_WIDTH, DEVICE_HEIGHT)

		' set the mouse x,y
		mouseX = MouseX() / SCREENX_RATIO
		mouseY = MouseY() / SCREENY_RATIO

		' Set the Random seed
		Seed = RealMillisecs()
		' Create the delta timer
		dt = New DeltaTimer(FPS)
		SetUpdateRate FPS

		'create all the particles
		Particle.Cache()

		Return 0
	End

	Method SetScreenSize:Void(w:Float, h:Float)
		SCREEN_WIDTH = w
		SCREEN_HEIGHT = h
		SCREEN_WIDTH2 = SCREEN_WIDTH / 2
		SCREEN_HEIGHT2 = SCREEN_HEIGHT / 2

		SCREENX_RATIO = DEVICE_WIDTH/SCREEN_WIDTH
		SCREENY_RATIO = DEVICE_HEIGHT/SCREEN_HEIGHT

		If SCREENX_RATIO <> 1 Or SCREENY_RATIO <> 1
			virtualResOn = True
		End
	End

	Method OnRender:Int()
		FPSCounter.Update()
		If virtualResOn
			PushMatrix
			Scale SCREENX_RATIO, SCREENY_RATIO
		End
			currentScreen.Render()
			If screenFade.active then screenFade.Render()
		if virtualResOn
			PopMatrix
		End
		currentScreen.ExtraRender()
		If debugOn
			DrawDebug()
		End
		If drawFPSOn
			DrawFPS()
		End
		Return 0
	End

	Method OnUpdate:Int()
		dt.UpdateDelta()
		ScreenLogic()

		Return 0
	End

	Method ScreenLogic:Int()
		mouseX = MouseX() / SCREENX_RATIO
		mouseY = MouseY() / SCREENY_RATIO
		mouseHit = MouseHit()

		If screenFade.active then screenFade.Update()
		currentScreen.Update()
	End

	Method DrawDebug:Void()
		SetColor 255, 255, 255
		FPSCounter.Draw(0,0)
		DrawText "Screen         = "+currentScreen.name, 0, 10
		DrawText "Delta          = "+FormatNumber(dt.delta, 2) , 0, 20
		DrawText "Screen Width   = "+SCREEN_WIDTH, 0, 30
		DrawText "Screen Height  = "+SCREEN_HEIGHT, 0, 40
		DrawText "VMouseX        = "+Self.mouseX, 0, 50
		DrawText "VMouseY        = "+Self.mouseY, 0, 60
		DrawText "MouseX         = "+MouseX(), 0, 70
		DrawText "MouseY         = "+MouseY(), 0, 80
		DrawText "MusicOkay      = "+musicOkay, 0, 90
		DrawText "Music State    = "+MusicState(), 0, 100
		DrawText "Music Volume   = "+Self.musicVolume, 0, 110
		DrawText "Sound Volume   = "+Self.soundVolume, 0, 120
		DrawText "Sound Channel  = "+SoundPlayer.channel, 0, 130
	End

	Method DrawFPS:Void()
		DrawText FPSCounter.totalFPS, 0, 0
	End

	Method MusicPlay:Void(file:String, flags:Int=1)
		SetMusicVolume(musicVolume/100.0)
		musicOkay = PlayMusic("music/"+file, flags)
	End

	Method MusicSetVolume:Void(volume:Int)
		If volume < 0 Then volume = 0
		If volume > 100 Then volume = 100
		Self.musicVolume = volume
		SetMusicVolume(musicVolume/100.0)
	End

	Method SoundSetVolume:Void(volume:Int)
		If volume < 0 Then volume = 0
		If volume > 100 Then volume = 100
		Self.soundVolume = volume
		For Local i% = 0 To SoundPlayer.MAX_CHANNELS
			SetChannelVolume(i, game.soundVolume / 100.0)
		Next
	End

End

Class ScreenFade
	Field fadeTime:Float
	Field fadeOut:Bool
	Field ratio:Float = 0
	Field active:Bool
	Field counter:Float
	Field fadeMusic:Bool
	Field fadeSound:Bool

	Method Start:Void(fadeTime:Float, fadeOut:Bool, fadeSound:Bool = False, fadeMusic:Bool = False)
		If active Then Return
		active = True
		Self.fadeTime = fadeTime
		Self.fadeOut = fadeOut
		Self.fadeMusic = fadeMusic
		Self.fadeSound = fadeSound
		If fadeOut Then
			ratio = 1
		Else
			ratio = 0
		End
		counter = 0
	End

	Method Update:Void()
		If Not active Return
		counter += dt.delta
		CalcRatio()
		If fadeSound Then
			For Local i% = 0 To SoundPlayer.MAX_CHANNELS
				SetChannelVolume(i, (ratio) * (game.soundVolume / 100.0))
			Next
		End
		If fadeMusic Then
			SetMusicVolume((ratio) * (game.musicVolume / 100.0))
		End
		if counter > fadeTime
			active = false
			if fadeOut
				game.currentScreen.PostFadeOut()
			else
				game.currentScreen.PostFadeIn()
			End
		End
	End

	Method CalcRatio:Void()
		ratio = counter/fadeTime
		If ratio < 0 Then ratio = 0
		If ratio > 1 Then ratio = 1
		If fadeOut Then
			ratio = 1 - ratio
		End
	End

	Method Render:Void()
		If Not active Return

		SetAlpha 1 - ratio
		SetColor 0, 0, 0
		DrawRect 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT
		SetAlpha 1
		SetColor 255, 255, 255
	End

End

Class ExitScreen Extends Screen
	Method New()
		name = "exit"
	End

	Method Start:Void()
		ExitApp()
	End

	Method Render:Void()
	End

	Method Update:Void()
	End
End

Class Screen Abstract
	Field name$ = ""

	Method PreStart:Void()
		game.currentScreen = self
		Start()
	End

	Method Start:Void() Abstract

	Method Render:Void() Abstract

	Method Update:Void() Abstract

	Method PostFadeOut:Void()
		game.nextScreen.PreStart()
	End

	Method PostFadeIn:Void()
	End

	Method ExtraRender:Void()
	End

End Class

Class FPSCounter Abstract
	Global fpsCount:Int
	Global startTime:Int
	Global totalFPS:Int

	Function Update:Void()
		If Millisecs() - startTime >= 1000
			totalFPS = fpsCount
			fpsCount = 0
			startTime = Millisecs()
		Else
			fpsCount+=1
		End
	End

	Function Draw:Void(x% = 0, y% = 0, ax# = 0, ay# = 0)
		DrawText("FPS: " + totalFPS, x, y, ax, ay)
	End
End

' From James Boyd
Class DeltaTimer
	Field targetfps:Float = 60
	Field currentticks:Float
	Field lastticks:Float
	Field frametime:Float
	Field delta:Float

	Method New (fps:Float)
		targetfps = fps
		lastticks = Millisecs()
	End

	Method UpdateDelta:Void()
		currentticks = Millisecs()
		frametime = currentticks - lastticks
		delta = frametime / (1000.0 / targetfps)
		lastticks = currentticks
	End
End

Class ImageBank Extends StringMap<GameImage>

	Field path$ = "graphics/"

	Method Load:Void(name:String, nameoverride:String = "", midhandle:bool=true)
		Local i:GameImage = New GameImage
		i.Load(path + name, midhandle)

		If nameoverride <> "" Then i.name = nameoverride.ToUpper()
		Self.Set(i.name , i)
	End

	Method LoadAnim:Void(name:String, w%, h%, total%, tmpImage:Image, midhandle:Bool=true)
		Local i:GameImage = New GameImage
		i.LoadAnim(path + name, w, h, total, tmpImage, midhandle)
		Self.Set(i.name, i)
	End

	Method Find:GameImage(name:String)
		name = name.ToUpper()

		' debug: print all keys in the map
	'	For Local key:String = EachIn self.Keys()
	'		Print key + " is stored in the map."
	'	Next

		Local i:GameImage =  self.Get(name)
		AssertNotNull(i, "Image '" + name + "' not found in the ImageBank")
		Return i
	End

	Method PreCache:Void()
		Local gi:GameImage
		For Local key:String = EachIn self.Keys()
			gi = Self.Get(key)
			gi.PreCache()
		Next
	End

End

Class GameImage
	Field name$
	Field image:Image
	Field w%
	Field h%
	Field w2#
	Field h2#
	Field midhandled%=0

	Field leftMargin%=0
	Field rightMargin%=0
	Field topMargin%=0
	Field bottomMargin%=0

	Method Load:Void(file$, midhandle:bool=true)
		name = StripAll(file.ToUpper())
		image = LoadBitmap(file)
		CalcSize()
		MidHandle(midhandle)
	End

	Method LoadAnim:Void(file$, w%, h%, total%, tmpImage:Image, midhandle:Bool=true)
		name = StripAll(file.ToUpper())
		image = LoadAnimBitmap(file, w, h, total, tmpImage)
		CalcSize()
		MidHandle(midhandle)
	End

	Method CalcSize:Void()
		If image <> Null Then
			w = image.Width()
			h = image.Height()
			w2 = w/2
			h2 = h/2
		End
	End

	Method MidHandle:Void(On:Bool)
		If On Then
			image.SetHandle(w2, h2)
			midhandled=1
		Else
			image.SetHandle(0, 0)
			midhandled=0
		End
	End

	Method Draw:Void(x#, y#, rotation# = 0, scaleX# = 1, scaleY# = 1, frame% = 0)
		DrawImage(self.image, x, y, rotation, scaleX, scaleY, frame)
	End

	Method DrawTiled:Void(x#, y#, w#, h#, scaleX# = 1, scaleY# = 1, frame% = 0)
	End

	Method DrawStretched:Void(x#, y#, rw#, rh#, frame% = 0)
	End

	Method DrawGrid:Void(x#, y#, rw#, rh#, frame% = 0)
		' draw top left corner
		DrawImageRect(self.image, x, y, 0, 0, leftMargin, topMargin, frame)
		' draw top right corner
		DrawImageRect(self.image, x+rw-rightMargin, y, w-rightMargin, 0, rightMargin, topMargin, frame)
		' draw bottom left corner
		DrawImageRect(self.image, x, y+rh-bottomMargin, 0, h-bottomMargin, leftMargin, bottomMargin, frame)
		' draw bottom right corner
		DrawImageRect(self.image, x+rw-rightMargin, y+rh-bottomMargin, w-rightMargin, h-bottomMargin, rightMargin, bottomMargin, frame)

		' work out how many horizontal and vertical tiles
		Local tileWidth% = (w-leftMargin-rightMargin)
		Local tileHeight% = (h-topMargin-bottomMargin)
		Local tileXCount% = (rw-leftMargin-rightMargin) / tileWidth
		Local tileYCount% = (rh-topMargin-bottomMargin) / tileHeight
		Local tileXOverflow% = (rw-leftMargin-rightMargin) Mod tileWidth
		Local tileYOverflow% = (rh-topMargin-bottomMargin) Mod tileHeight

		' tile top and bottom edges
		For Local i% = 0 Until tileXCount
			DrawImageRect(self.image, leftMargin+i*tileWidth,0,leftMargin,0,tileWidth,topMargin,frame)
			DrawImageRect(self.image, leftMargin+i*tileWidth,rh-bottomMargin,leftMargin,h-bottomMargin,tileWidth,bottomMargin,frame)
		Next
		If tileXOverflow > 0 Then
			DrawImageRect(self.image, leftMargin+tileXCount*tileWidth,0,leftMargin,0,tileXOverflow,topMargin,frame)
			DrawImageRect(self.image, leftMargin+tileXCount*tileWidth,rh-bottomMargin,leftMargin,h-bottomMargin,tileXOverflow,bottomMargin,frame)
		End

		' tile left and right edges
		For Local i% = 0 Until tileYCount
			DrawImageRect(self.image, 0, topMargin+i*tileHeight,0,topMargin,leftMargin,tileHeight,frame)
			DrawImageRect(self.image, rw-rightMargin,topMargin+i*tileHeight,w-rightMargin,topMargin,rightMargin,tileHeight,frame)
		Next
		If tileYOverflow > 0 Then
			DrawImageRect(self.image, 0, topMargin+tileYCount*tileHeight,0,topMargin,leftMargin,tileYOverflow,frame)
			DrawImageRect(self.image, rw-rightMargin,topMargin+tileYCount*tileHeight,w-rightMargin,topMargin,rightMargin,tileYOverflow,frame)
		End

		' tile centre
		' TODO
	End

	Method PreCache:Void()
		DrawImage self.image, -self.w-50, -self.h-50
	End
End

Class SoundBank Extends StringMap<GameSound>

	Global path$ = "sounds/"

	Method Load:Void(name:String, nameoverride:String = "")
		Local i:GameSound = New GameSound
		i.Load(name)

		If nameoverride <> "" Then i.name = nameoverride.ToUpper()
		Self.Set(i.name , i)
	End

	Method Find:GameSound(name:String)
		name = name.ToUpper()

		' debug: print all keys in the map
	'	For Local key:String = EachIn self.Keys()
	'		Print key + " is stored in the map."
	'	Next

		Local i:GameSound =  Self.Get(name)
		AssertNotNull(i, "Sound '" + name + "' not found in the SoundBank")
		Return i
	End
End

Class GameSound
	Field name$
	Field sound:Sound
	Field rate# = 1
	Field pan# = 0
	Field volume# = 1
	Field loop% = 0

	Method Load:Void(file$)

		If file.Contains(".wav") Or file.Contains(".ogg") Or file.Contains(".mp3") Then
			sound = LoadSoundSample(SoundBank.path + file)
		Else
			#if TARGET="flash"
				sound = LoadSoundSample(SoundBank.path + file +".mp3")
			#else If TARGET="android"
				sound = LoadSoundSample(SoundBank.path + file +".ogg")
			#else
				sound = LoadSoundSample(SoundBank.path + file +".wav")
			#endif
		End

		name = StripAll(file.ToUpper())
	End

	Method Play:Void()
		SoundPlayer.PlayFx(sound, pan, rate, volume * (game.soundVolume / 100.0), loop)
	End
End

Class SoundPlayer
	Global channel:Int
	Const MAX_CHANNELS:Int = 31

	Function PlayFx:Void(s:Sound, pan#=0, rate#=1, volume#=1, loop% = 0)
		channel += 1
		If (channel > MAX_CHANNELS) Then channel = 0

		StopChannel(channel)
		PlaySound(s, channel, loop)
		SetChannelPan(channel, pan)
		SetChannelRate(channel, rate)
		SetChannelVolume(channel, volume)
	End
End

Class Sprite
	Field name$
	Field visible?
	Field x#, y#
	Field ox#, oy#
	Field dx#, dy#
	Field speedX#, speedY#, speed#
	Field maxXSpeed#, maxYSpeed#
	Field image:GameImage
	Field scaleX# = 1, scaleY# = 1

	Field red% = 255, green% = 255, blue% = 255, alpha# = 1
	Field hitBoxX:Int = 0
	Field hitBoxY:Int = 0
	Field hitBoxWidth:Int
	Field hitBoxHeight:Int

	' Animation
	Field frame:Int
	Field frameTimer:Int
	Field frameStart:Int
	Field frameEnd:Int
	Field frameSpeed:Int = 0
	Field reverse:Bool = false
	Field pingPong:Bool = false
	Field loop:Bool = true
	Field ping%

	' Scale
	Field scaleCounter#=0
	Field scaleXSpeed# = 0.1
	Field scaleYSpeed# = 0.1
	Field ygravity#
	Field maxFrame:Int

	' Rotation
	Field rotationCounter#=0
	Field rotationLength%=1000
	Field rotationLoop% = 0
	Field rotation#, rotationSpeed# = 1

	Method New(img:GameImage,x#, y#)
		Self.image = img
		Self.x = x
		Self.y = y
		self.alpha = 1
		self.SetHitBox(-img.w2, -img.h2, img.w, img.h)
		Self.visible = True
	End

	Method SetImage:Void(gi:GameImage)
		image = gi
		GetSize()
	End

	Method GetSize:Void()
		'Sets w and h based on the current image.
		If image <> Null Then
			image.w = image.image.Width()
			image.h = image.image.Height()
			image.w2 = image.w/2
			image.h2 = image.h/2
		End
	End

	Method Precache:Void()
		DrawImage image.image, -image.w-50, -image.h-50
	End

	Method SetRGB:Void(r%,g%,b%)
		Self.red = r
		Self.green = g
		Self.blue = b
	End

	Method SetScaleXY:Void(sx#, sy#)
		self.scaleX = sx
		self.scaleY = sy
	End

	Method Move:Void()
		Self.x+=Self.dx * dt.delta
		Self.y+=Self.dy * dt.delta

		If ygravity > 0 Then dy += ygravity * dt.delta
	End

	Method ManageScale:Void()
		If scaleCounter>0 Then
			scaleCounter-=1*dt.delta
			scaleX+=scaleXSpeed*dt.delta
			scaleY+=scaleYSpeed*dt.delta
		End
	End

	Method ManageRotation:Void()
		If rotationLoop Then
			rotation+=rotationSpeed*dt.delta
			If rotation >= 360 Then rotation-=360
			If rotation <0 Then rotation+=360
		Else
			If rotationCounter>0 Then
				rotationCounter-=1 * dt.delta
				rotation+=rotationSpeed * dt.delta
			End
		End
	End

	Method MoveForward:Void()
		dx = -Sin(rotation) * speed
        dy = -Cos(rotation) * speed

		move()
	End

	Method SetFrame:Void(startFrame:Int=0, endFrame:Int=0, speed:Int=125, pingPong:Bool = false, loop:Bool = true)
		frame = startFrame
		frameStart = startFrame
		frameEnd = endFrame
		if startFrame > endFrame
			reverse = true
		else
			reverse = false
		End
		self.pingPong = pingPong
		self.loop = loop
		frameSpeed = speed
		frameTimer = Millisecs()
		ping = 0
	End

	Method UpdateAnimation:Void()
		if frameSpeed > 0
			If Millisecs() > frameTimer + frameSpeed
				if not reverse
					frame+=1
					If frame > frameEnd
						ResetAnim()
					End
				else
					frame-=1
					If frame < frameEnd
						ResetAnim()
					End
				End
				frameTimer = Millisecs()
			End
		end
	End

	Method ResetAnim:Void()
		if loop then
			if pingPong
				reverse = Not reverse
				frame = frameEnd
				local ts% = frameStart
				frameStart = frameEnd
				frameEnd = ts
			else
				frame = frameStart
			End
		else
			if pingPong and ping <1
				reverse = Not reverse
				frame = frameEnd
				local ts% = frameStart
				frameStart = frameEnd
				frameEnd = ts
				ping+=1
			else
				frame = frameEnd
			End
		End
	End

	Method Draw:Void()
		Draw(0,0)
	End

	Method Draw:Void(offsetx#=0, offsety#=0)
		If x - offsetx + image.w < 0 Or x - offsetx - image.w >= SCREEN_WIDTH Or y - offsety + image.h < 0 Or y - offsety - image.h >= SCREEN_HEIGHT Then Return
		If Self.alpha > 1 Then Self.alpha = 1
		If Self.alpha < 0 Then Self.alpha = 0
		SetAlpha Self.alpha
		SetColor red, green, blue ' doesnt work with images!?!??!
		DrawImage(image.image, x - offsetx, y - offsety, rotation, scaleX, scaleY, frame)
		SetColor 255, 255, 255
		SetAlpha 1

	'	if debug
	'		drawRectOutline(x + hitBoxX, y + hitBoxY, hitBoxWidth, hitBoxHeight)
	'	End
	End

	Method SetupRotation:Void(rotationSpeed:Float, length:Int, loop:Bool = False, rndPosition:Bool = False)
		Self.rotationSpeed = rotationSpeed
		If loop Then
			rotationLoop = 1
			If rndPosition Then rotation = Rnd(0,360)
		Else
			rotationLength = length
			rotationCounter = rotationLength
		End
	End

	Method Collide:Int(sprite:Sprite)
		Return RectsOverlap(x + hitBoxX, y + hitBoxY, hitBoxWidth, hitBoxHeight,
							sprite.x + sprite.hitBoxX, sprite.y + sprite.hitBoxY, sprite.hitBoxWidth, sprite.hitBoxHeight)
	End

	Method SetHitBox:Void(hitX:Int, hitY:Int, hitWidth:Int, hitHeight:Int)
		hitBoxX = hitX
		hitBoxY = hitY
		hitBoxWidth = hitWidth
		hitBoxHeight = hitHeight
	End
End

Class Particle Extends Sprite
	Global MAX_PARTICLES% = 800
	Global particles:Particle[MAX_PARTICLES]
	Global lastDeath% = 0
	Global maxIndex% = -1
	Global minIndex% = -1
	Global particleCount% = 0
	Field lifeCounter# = 0
	Field fadeIn# = 0
	Field fadeCounter#
	Field fadeInLength# = 0
	Field fadeLength#=0
	Field active% = 0

	Function Cache:Void()
		For Local i% = 0 to MAX_PARTICLES - 1
			particles[i] = New Particle()
		Next
	End

	Function Create:Particle(gi:GameImage, x#, y#, dx#=0, dy#=0, gravity#=0, fadeLength#=0, lifeCounter%=0)
		Local i%=lastDeath
		Repeat
			If particles[i] = Null Then particles[i] = New Particle()
			If Not particles[i].active Then
				particles[i].SetImage(gi)
				particles[i].x = x
				particles[i].y = y
				particles[i].dx = dx
				particles[i].dy = dy

				particles[i].ygravity = gravity
				particles[i].fadeLength = fadeLength / 10
				particles[i].fadeCounter = particles[i].fadeLength
				If lifeCounter>0 Then particles[i].lifeCounter = lifeCounter / 10
				particles[i].active = 1
				If maxIndex < 0 Or i > maxIndex Then maxIndex = i
				If minIndex < 0 Or i < minIndex Then minIndex = i
				particleCount += 1
				Return particles[i]
			End
			i += 1
			If i >= MAX_PARTICLES Then i = 0
		Until i = lastDeath
		Return Null
	End

	Function Clear:Void()
		For Local i% = 0 to MAX_PARTICLES - 1
			particles[i].alpha = 0
			particles[i].active = False
		Next
		minIndex = -1
		maxIndex = -1
		particleCount = 0
		lastDeath = 0
	End

	Function DrawAll:Void()
		If minIndex < 0 Or maxIndex < 0 Then Return
		For Local i% = minIndex to maxIndex
			if particles[i] <> null And particles[i].image <> null
				If particles[i].fadeCounter > 0 and particles[i].active Then
					If particles[i].fadeIn Then
						particles[i].alpha = particles[i].fadeCounter/particles[i].fadeInLength
					Else
						particles[i].alpha = particles[i].fadeCounter/particles[i].fadeLength
					End
				End
				particles[i].Draw()
			End
		Next
	End

	Function UpdateAll:Void()
		If minIndex < 0 Or maxIndex < 0 Then Return
		Local newMinIndex% = -1
		Local newMaxIndex% = -1
		For Local i% = minIndex to maxIndex
			if particles[i] <> null And particles[i].image <> null
				If particles[i].active
					particles[i].Update()
					If particles[i].active Then
						If newMinIndex < 0 Then newMinIndex = i
						newMaxIndex = i
					Else
						lastDeath = i
						particleCount -= 1
					End
				End
			End
		Next
		minIndex = newMinIndex
		maxIndex = newMaxIndex
	End

	Method Update:Void()
		Super.Move()

		If fadeIn Then
			fadeCounter+=dt.delta

			If fadeCounter >= fadeInLength Then
				fadeCounter = fadeLength
				fadeIn = 0
				alpha = 1
			End
		ElseIf fadeCounter>0 Then
			fadeCounter-=dt.delta
			If fadeCounter <= 0 Then
				alpha = 0
				active = 0
			End
		End
	End

End

Strict

Import mojo
Import functions

'Device width and height
Global DEVICE_WIDTH%
Global DEVICE_HEIGHT%

' Screen width and height
Global SCREEN_WIDTH#
Global SCREEN_HEIGHT#

' Half of SCREEN_WIDTH and HEIGHT
Global SCREEN_WIDTH2#
Global SCREEN_HEIGHT2#

' Used for Virtual Res
Global SCREENX_RATIO# = 1
Global SCREENY_RATIO# = 1

' THE GAME!!
Global game:DiddyApp

' Used for delta timing movement
Global dt:DeltaTimer

Class DiddyApp Extends App

	Field debugOn:Bool = False
	Field drawFPSOn:Bool = False

	Field virtualResOn:Bool = True

	Field FPS% = 60

	' current Screen
	Field currentScreen:Screen
	' next Screen
	Field nextScreen:Screen
	' exit Screen
	Field exitScreen:ExitScreen = new ExitScreen()
	' used for fading
	Field screenFade:ScreenFade = New ScreenFade

	' mouse
	Field mouseX:Int, mouseY:Int
	Field mouseHit:Int

	' Store the images here
	Global images:ImageBank = New ImageBank
	' Store the sounds here
	Global sounds:SoundBank = New SoundBank
	' volume control
	Field soundVolume:Int = 100
	Field musicVolume:Int = 100
	Field musicOkay:Int

	Method OnCreate:Int()
		' Store the device width and height
		DEVICE_WIDTH = DeviceWidth()
		DEVICE_HEIGHT = DeviceHeight()

		SetScreenSize(DEVICE_WIDTH, DEVICE_HEIGHT)

		' set the mouse x,y
		mouseX = MouseX() / SCREENX_RATIO
		mouseY = MouseY() / SCREENY_RATIO

		' Set the Random seed
		Seed = RealMillisecs()
		' Create the delta timer
		dt = New DeltaTimer(FPS)
		SetUpdateRate FPS

		'create all the particles
		Particle.Cache()

		Return 0
	End

	Method SetScreenSize:Void(w:Float, h:Float)
		SCREEN_WIDTH = w
		SCREEN_HEIGHT = h
		SCREEN_WIDTH2 = SCREEN_WIDTH / 2
		SCREEN_HEIGHT2 = SCREEN_HEIGHT / 2

		SCREENX_RATIO = DEVICE_WIDTH/SCREEN_WIDTH
		SCREENY_RATIO = DEVICE_HEIGHT/SCREEN_HEIGHT

		If SCREENX_RATIO <> 1 Or SCREENY_RATIO <> 1
			virtualResOn = True
		End
	End

	Method OnRender:Int()
		FPSCounter.Update()
		If virtualResOn
			PushMatrix
			Scale SCREENX_RATIO, SCREENY_RATIO
		End
			currentScreen.Render()
			If screenFade.active then screenFade.Render()
		if virtualResOn
			PopMatrix
		End
		currentScreen.ExtraRender()
		If debugOn
			DrawDebug()
		End
		If drawFPSOn
			DrawFPS()
		End
		Return 0
	End

	Method OnUpdate:Int()
		dt.UpdateDelta()
		ScreenLogic()

		Return 0
	End

	Method ScreenLogic:Int()
		mouseX = MouseX() / SCREENX_RATIO
		mouseY = MouseY() / SCREENY_RATIO
		mouseHit = MouseHit()

		If screenFade.active then screenFade.Update()
		currentScreen.Update()
	End

	Method DrawDebug:Void()
		SetColor 255, 255, 255
		FPSCounter.Draw(0,0)
		DrawText "Screen         = "+currentScreen.name, 0, 10
		DrawText "Delta          = "+FormatNumber(dt.delta, 2) , 0, 20
		DrawText "Screen Width   = "+SCREEN_WIDTH, 0, 30
		DrawText "Screen Height  = "+SCREEN_HEIGHT, 0, 40
		DrawText "VMouseX        = "+Self.mouseX, 0, 50
		DrawText "VMouseY        = "+Self.mouseY, 0, 60
		DrawText "MouseX         = "+MouseX(), 0, 70
		DrawText "MouseY         = "+MouseY(), 0, 80
		DrawText "MusicOkay      = "+musicOkay, 0, 90
		DrawText "Music State    = "+MusicState(), 0, 100
		DrawText "Music Volume   = "+Self.musicVolume, 0, 110
		DrawText "Sound Volume   = "+Self.soundVolume, 0, 120
	End

	Method DrawFPS:Void()
		DrawText FPSCounter.totalFPS, 0, 0
	End

	Method MusicPlay:Void(file:String, flags:Int=1)
		SetMusicVolume(musicVolume/100)
		musicOkay = PlayMusic("music/"+file, flags)
	End
End

Class ScreenFade
	Field fadeTime:Float
	Field fadeOut:Bool
	Field ratio:Float = 0
	Field active:Bool
	Field counter:Float
	Field fadeMusic:Bool
	Field fadeSound:Bool

	Method Start:Void(fadeTime:Float, fadeOut:Bool, fadeSound:Bool = False, fadeMusic:Bool = False)
		If active Then Return
		active = True
		Self.fadeTime = fadeTime
		Self.fadeOut = fadeOut
		Self.fadeMusic = fadeMusic
		Self.fadeSound = fadeSound
		If fadeOut Then
			ratio = 1
		Else
			ratio = 0
		End
		counter = 0
	End

	Method Update:Void()
		If Not active Return
		counter += dt.delta
		CalcRatio()
		If fadeSound Then
			For Local i% = 0 To SoundPlayer.MAX_CHANNELS
				SetChannelVolume(i, (ratio) * (game.soundVolume / 100))
			Next
		End
		If fadeMusic Then
			SetMusicVolume((ratio) * (game.musicVolume / 100))
		End
		if counter > fadeTime
			active = false
			if fadeOut
				game.currentScreen.PostFadeOut()
			else
				game.currentScreen.PostFadeIn()
			End
		End
	End

	Method CalcRatio:Void()
		ratio = counter/fadeTime
		If ratio < 0 Then ratio = 0
		If ratio > 1 Then ratio = 1
		If fadeOut Then
			ratio = 1 - ratio
		End
	End

	Method Render:Void()
		If Not active Return

		SetAlpha 1 - ratio
		SetColor 0, 0, 0
		DrawRect 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT
		SetAlpha 1
		SetColor 255, 255, 255
	End

End

Class ExitScreen Extends Screen
	Method New()
		name = "exit"
	End

	Method Start:Void()
		ExitApp()
	End

	Method Render:Void()
	End

	Method Update:Void()
	End
End

Class Screen Abstract
	Field name$ = ""

	Method PreStart:Void()
		game.currentScreen = self
		Start()
	End

	Method Start:Void() Abstract

	Method Render:Void() Abstract

	Method Update:Void() Abstract

	Method PostFadeOut:Void()
		game.nextScreen.PreStart()
	End

	Method PostFadeIn:Void()
	End

	Method ExtraRender:Void()
	End

End Class

Class FPSCounter Abstract
	Global fpsCount:Int
	Global startTime:Int
	Global totalFPS:Int

	Function Update:Void()
		If Millisecs() - startTime >= 1000
			totalFPS = fpsCount
			fpsCount = 0
			startTime = Millisecs()
		Else
			fpsCount+=1
		End
	End

	Function Draw:Void(x% = 0, y% = 0, ax# = 0, ay# = 0)
		DrawText("FPS: " + totalFPS, x, y, ax, ay)
	End
End

' From James Boyd
Class DeltaTimer
	Field targetfps:Float = 60
	Field currentticks:Float
	Field lastticks:Float
	Field frametime:Float
	Field delta:Float

	Method New (fps:Float)
		targetfps = fps
		lastticks = Millisecs()
	End

	Method UpdateDelta:Void()
		currentticks = Millisecs()
		frametime = currentticks - lastticks
		delta = frametime / (1000.0 / targetfps)
		lastticks = currentticks
	End
End

Class ImageBank Extends StringMap<GameImage>

	Field path$ = "graphics/"

	Method Load:Void(name:String, nameoverride:String = "", midhandle:bool=true)
		Local i:GameImage = New GameImage
		i.Load(path + name, midhandle)

		If nameoverride <> "" Then i.name = nameoverride.ToUpper()
		Self.Set(i.name , i)
	End

	Method LoadAnim:Void(name:String, w%, h%, total%, tmpImage:Image, midhandle:Bool=true)
		Local i:GameImage = New GameImage
		i.LoadAnim(path + name, w, h, total, tmpImage, midhandle)
		Self.Set(i.name, i)
	End

	Method Find:GameImage(name:String)
		name = name.ToUpper()

		' debug: print all keys in the map
	'	For Local key:String = EachIn self.Keys()
	'		Print key + " is stored in the map."
	'	Next

		Local i:GameImage =  self.Get(name)
		AssertNotNull(i, "Image '" + name + "' not found in the ImageBank")
		Return i
	End

	Method PreCache:Void()
		Local gi:GameImage
		For Local key:String = EachIn self.Keys()
			gi = Self.Get(key)
			gi.PreCache()
		Next
	End

End

Class GameImage
	Field name$
	Field image:Image
	Field w%
	Field h%
	Field w2#
	Field h2#
	Field midhandled%=0

	Field leftMargin%=0
	Field rightMargin%=0
	Field topMargin%=0
	Field bottomMargin%=0

	Method Load:Void(file$, midhandle:bool=true)
		name = StripAll(file.ToUpper())
		image = LoadBitmap(file)
		CalcSize()
		MidHandle(midhandle)
	End

	Method LoadAnim:Void(file$, w%, h%, total%, tmpImage:Image, midhandle:Bool=true)
		name = StripAll(file.ToUpper())
		image = LoadAnimBitmap(file, w, h, total, tmpImage)
		CalcSize()
		MidHandle(midhandle)
	End

	Method CalcSize:Void()
		If image <> Null Then
			w = image.Width()
			h = image.Height()
			w2 = w/2
			h2 = h/2
		End
	End

	Method MidHandle:Void(On:Bool)
		If On Then
			image.SetHandle(w2, h2)
			midhandled=1
		Else
			image.SetHandle(0, 0)
			midhandled=0
		End
	End

	Method Draw:Void(x#, y#, rotation# = 0, scaleX# = 1, scaleY# = 1, frame% = 0)
		DrawImage(self.image, x, y, rotation, scaleX, scaleY, frame)
	End

	Method DrawTiled:Void(x#, y#, w#, h#, scaleX# = 1, scaleY# = 1, frame% = 0)
	End

	Method DrawStretched:Void(x#, y#, rw#, rh#, frame% = 0)
	End

	Method DrawGrid:Void(x#, y#, rw#, rh#, frame% = 0)
		' draw top left corner
		DrawImageRect(self.image, x, y, 0, 0, leftMargin, topMargin, frame)
		' draw top right corner
		DrawImageRect(self.image, x+rw-rightMargin, y, w-rightMargin, 0, rightMargin, topMargin, frame)
		' draw bottom left corner
		DrawImageRect(self.image, x, y+rh-bottomMargin, 0, h-bottomMargin, leftMargin, bottomMargin, frame)
		' draw bottom right corner
		DrawImageRect(self.image, x+rw-rightMargin, y+rh-bottomMargin, w-rightMargin, h-bottomMargin, rightMargin, bottomMargin, frame)

		' work out how many horizontal and vertical tiles
		Local tileWidth% = (w-leftMargin-rightMargin)
		Local tileHeight% = (h-topMargin-bottomMargin)
		Local tileXCount% = (rw-leftMargin-rightMargin) / tileWidth
		Local tileYCount% = (rh-topMargin-bottomMargin) / tileHeight
		Local tileXOverflow% = (rw-leftMargin-rightMargin) Mod tileWidth
		Local tileYOverflow% = (rh-topMargin-bottomMargin) Mod tileHeight

		' tile top and bottom edges
		For Local i% = 0 Until tileXCount
			DrawImageRect(self.image, leftMargin+i*tileWidth,0,leftMargin,0,tileWidth,topMargin,frame)
			DrawImageRect(self.image, leftMargin+i*tileWidth,rh-bottomMargin,leftMargin,h-bottomMargin,tileWidth,bottomMargin,frame)
		Next
		If tileXOverflow > 0 Then
			DrawImageRect(self.image, leftMargin+tileXCount*tileWidth,0,leftMargin,0,tileXOverflow,topMargin,frame)
			DrawImageRect(self.image, leftMargin+tileXCount*tileWidth,rh-bottomMargin,leftMargin,h-bottomMargin,tileXOverflow,bottomMargin,frame)
		End

		' tile left and right edges
		For Local i% = 0 Until tileYCount
			DrawImageRect(self.image, 0, topMargin+i*tileHeight,0,topMargin,leftMargin,tileHeight,frame)
			DrawImageRect(self.image, rw-rightMargin,topMargin+i*tileHeight,w-rightMargin,topMargin,rightMargin,tileHeight,frame)
		Next
		If tileYOverflow > 0 Then
			DrawImageRect(self.image, 0, topMargin+tileYCount*tileHeight,0,topMargin,leftMargin,tileYOverflow,frame)
			DrawImageRect(self.image, rw-rightMargin,topMargin+tileYCount*tileHeight,w-rightMargin,topMargin,rightMargin,tileYOverflow,frame)
		End

		' tile centre
		' TODO
	End

	Method PreCache:Void()
		DrawImage self.image, -self.w-50, -self.h-50
	End
End

Class SoundBank Extends StringMap<GameSound>

	Global path$ = "sounds/"

	Method Load:Void(name:String, nameoverride:String = "")
		Local i:GameSound = New GameSound
		i.Load(name)

		If nameoverride <> "" Then i.name = nameoverride.ToUpper()
		Self.Set(i.name , i)
	End

	Method Find:GameSound(name:String)
		name = name.ToUpper()

		' debug: print all keys in the map
	'	For Local key:String = EachIn self.Keys()
	'		Print key + " is stored in the map."
	'	Next

		Local i:GameSound =  Self.Get(name)
		AssertNotNull(i, "Sound '" + name + "' not found in the SoundBank")
		Return i
	End
End

Class GameSound
	Field name$
	Field sound:Sound
	Field rate# = 1
	Field pan# = 0
	Field volume# = 1
	Field loop% = 0

	Method Load:Void(file$)

		If file.Contains(".wav") Or file.Contains(".ogg") Or file.Contains(".mp3") Then
			sound = LoadSoundSample(SoundBank.path + file)
		Else
			#if TARGET="flash"
				sound = LoadSoundSample(SoundBank.path + file +".mp3")
			#else If TARGET="android"
				sound = LoadSoundSample(SoundBank.path + file +".ogg")
			#else
				sound = LoadSoundSample(SoundBank.path + file +".wav")
			#endif
		End

		name = StripAll(file.ToUpper())
	End

	Method Play:Void()
		SoundPlayer.PlayFx(sound, pan, rate, volume * (game.soundVolume / 100), loop)
	End
End

Class SoundPlayer
	Global channel:Int
	Const MAX_CHANNELS:Int = 31

	Function PlayFx:Void(s:Sound, pan#=0, rate#=1, volume#=1, loop% = 0)
		channel += 1
		If (channel > MAX_CHANNELS) Then channel = 0

		StopChannel(channel)
		PlaySound(s, channel, loop)
		SetChannelPan(channel, pan)
		SetChannelRate(channel, rate)
		SetChannelVolume(channel, volume)
	End
End

Class Sprite
	Field name$
	Field visible?
	Field x#, y#
	Field ox#, oy#
	Field dx#, dy#
	Field speedX#, speedY#, speed#
	Field maxXSpeed#, maxYSpeed#
	Field image:GameImage
	Field scaleX# = 1, scaleY# = 1

	Field red% = 255, green% = 255, blue% = 255, alpha# = 1
	Field hitBoxX:Int = 0
	Field hitBoxY:Int = 0
	Field hitBoxWidth:Int
	Field hitBoxHeight:Int

	' Animation
	Field frame:Int
	Field frameTimer:Int
	Field frameStart:Int
	Field frameEnd:Int
	Field frameSpeed:Int = 0
	Field reverse:Bool = false
	Field pingPong:Bool = false
	Field loop:Bool = true
	Field ping%

	' Scale
	Field scaleCounter#=0
	Field scaleXSpeed# = 0.1
	Field scaleYSpeed# = 0.1
	Field ygravity#
	Field maxFrame:Int

	' Rotation
	Field rotationCounter#=0
	Field rotationLength%=1000
	Field rotationLoop% = 0
	Field rotation#, rotationSpeed# = 1

	Method New(img:GameImage,x#, y#)
		Self.image = img
		Self.x = x
		Self.y = y
		self.alpha = 1
		self.SetHitBox(-img.w2, -img.h2, img.w, img.h)
		Self.visible = True
	End

	Method SetImage:Void(gi:GameImage)
		image = gi
		GetSize()
	End

	Method GetSize:Void()
		'Sets w and h based on the current image.
		If image <> Null Then
			image.w = image.image.Width()
			image.h = image.image.Height()
			image.w2 = image.w/2
			image.h2 = image.h/2
		End
	End

	Method Precache:Void()
		DrawImage image.image, -image.w-50, -image.h-50
	End

	Method SetRGB:Void(r%,g%,b%)
		Self.red = r
		Self.green = g
		Self.blue = b
	End

	Method SetScaleXY:Void(sx#, sy#)
		self.scaleX = sx
		self.scaleY = sy
	End

	Method Move:Void()
		Self.x+=Self.dx * dt.delta
		Self.y+=Self.dy * dt.delta

		If ygravity > 0 Then dy += ygravity * dt.delta
	End

	Method ManageScale:Void()
		If scaleCounter>0 Then
			scaleCounter-=1*dt.delta
			scaleX+=scaleXSpeed*dt.delta
			scaleY+=scaleYSpeed*dt.delta
		End
	End

	Method ManageRotation:Void()
		If rotationLoop Then
			rotation+=rotationSpeed*dt.delta
			If rotation >= 360 Then rotation-=360
			If rotation <0 Then rotation+=360
		Else
			If rotationCounter>0 Then
				rotationCounter-=1 * dt.delta
				rotation+=rotationSpeed * dt.delta
			End
		End
	End

	Method MoveForward:Void()
		dx = -Sin(rotation) * speed
        dy = -Cos(rotation) * speed

		move()
	End

	Method SetFrame:Void(startFrame:Int=0, endFrame:Int=0, speed:Int=125, pingPong:Bool = false, loop:Bool = true)
		frame = startFrame
		frameStart = startFrame
		frameEnd = endFrame
		if startFrame > endFrame
			reverse = true
		else
			reverse = false
		End
		self.pingPong = pingPong
		self.loop = loop
		frameSpeed = speed
		frameTimer = Millisecs()
		ping = 0
	End

	Method UpdateAnimation:Void()
		if frameSpeed > 0
			If Millisecs() > frameTimer + frameSpeed
				if not reverse
					frame+=1
					If frame > frameEnd
						ResetAnim()
					End
				else
					frame-=1
					If frame < frameEnd
						ResetAnim()
					End
				End
				frameTimer = Millisecs()
			End
		end
	End

	Method ResetAnim:Void()
		if loop then
			if pingPong
				reverse = Not reverse
				frame = frameEnd
				local ts% = frameStart
				frameStart = frameEnd
				frameEnd = ts
			else
				frame = frameStart
			End
		else
			if pingPong and ping <1
				reverse = Not reverse
				frame = frameEnd
				local ts% = frameStart
				frameStart = frameEnd
				frameEnd = ts
				ping+=1
			else
				frame = frameEnd
			End
		End
	End

	Method Draw:Void()
		Draw(0,0)
	End

	Method Draw:Void(offsetx#=0, offsety#=0)
		If x - offsetx + image.w < 0 Or x - offsetx - image.w >= SCREEN_WIDTH Or y - offsety + image.h < 0 Or y - offsety - image.h >= SCREEN_HEIGHT Then Return
		If Self.alpha > 1 Then Self.alpha = 1
		If Self.alpha < 0 Then Self.alpha = 0
		SetAlpha Self.alpha
		SetColor red, green, blue ' doesnt work with images!?!??!
		DrawImage(image.image, x - offsetx, y - offsety, rotation, scaleX, scaleY, frame)
		SetColor 255, 255, 255
		SetAlpha 1

	'	if debug
	'		drawRectOutline(x + hitBoxX, y + hitBoxY, hitBoxWidth, hitBoxHeight)
	'	End
	End

	Method SetupRotation:Void(rotationSpeed:Float, length:Int, loop:Bool = False, rndPosition:Bool = False)
		Self.rotationSpeed = rotationSpeed
		If loop Then
			rotationLoop = 1
			If rndPosition Then rotation = Rnd(0,360)
		Else
			rotationLength = length
			rotationCounter = rotationLength
		End
	End

	Method Collide:Int(sprite:Sprite)
		Return RectsOverlap(x + hitBoxX, y + hitBoxY, hitBoxWidth, hitBoxHeight,
							sprite.x + sprite.hitBoxX, sprite.y + sprite.hitBoxY, sprite.hitBoxWidth, sprite.hitBoxHeight)
	End

	Method SetHitBox:Void(hitX:Int, hitY:Int, hitWidth:Int, hitHeight:Int)
		hitBoxX = hitX
		hitBoxY = hitY
		hitBoxWidth = hitWidth
		hitBoxHeight = hitHeight
	End
End

Class Particle Extends Sprite
	Global MAX_PARTICLES% = 800
	Global particles:Particle[MAX_PARTICLES]
	Global lastDeath% = 0
	Global maxIndex% = -1
	Global minIndex% = -1
	Global particleCount% = 0
	Field lifeCounter# = 0
	Field fadeIn# = 0
	Field fadeCounter#
	Field fadeInLength# = 0
	Field fadeLength#=0
	Field active% = 0

	Function Cache:Void()
		For Local i% = 0 to MAX_PARTICLES - 1
			particles[i] = New Particle()
		Next
	End

	Function Create:Particle(gi:GameImage, x#, y#, dx#=0, dy#=0, gravity#=0, fadeLength#=0, lifeCounter%=0)
		Local i%=lastDeath
		Repeat
			If particles[i] = Null Then particles[i] = New Particle()
			If Not particles[i].active Then
				particles[i].SetImage(gi)
				particles[i].x = x
				particles[i].y = y
				particles[i].dx = dx
				particles[i].dy = dy

				particles[i].ygravity = gravity
				particles[i].fadeLength = fadeLength / 10
				particles[i].fadeCounter = particles[i].fadeLength
				If lifeCounter>0 Then particles[i].lifeCounter = lifeCounter / 10
				particles[i].active = 1
				If maxIndex < 0 Or i > maxIndex Then maxIndex = i
				If minIndex < 0 Or i < minIndex Then minIndex = i
				particleCount += 1
				Return particles[i]
			End
			i += 1
			If i >= MAX_PARTICLES Then i = 0
		Until i = lastDeath
		Return Null
	End

	Function Clear:Void()
		For Local i% = 0 to MAX_PARTICLES - 1
			particles[i].alpha = 0
			particles[i].active = False
		Next
		minIndex = -1
		maxIndex = -1
		particleCount = 0
		lastDeath = 0
	End

	Function DrawAll:Void()
		If minIndex < 0 Or maxIndex < 0 Then Return
		For Local i% = minIndex to maxIndex
			if particles[i] <> null And particles[i].image <> null
				If particles[i].fadeCounter > 0 and particles[i].active Then
					If particles[i].fadeIn Then
						particles[i].alpha = particles[i].fadeCounter/particles[i].fadeInLength
					Else
						particles[i].alpha = particles[i].fadeCounter/particles[i].fadeLength
					End
				End
				particles[i].Draw()
			End
		Next
	End

	Function UpdateAll:Void()
		If minIndex < 0 Or maxIndex < 0 Then Return
		Local newMinIndex% = -1
		Local newMaxIndex% = -1
		For Local i% = minIndex to maxIndex
			if particles[i] <> null And particles[i].image <> null
				If particles[i].active
					particles[i].Update()
					If particles[i].active Then
						If newMinIndex < 0 Then newMinIndex = i
						newMaxIndex = i
					Else
						lastDeath = i
						particleCount -= 1
					End
				End
			End
		Next
		minIndex = newMinIndex
		maxIndex = newMaxIndex
	End

	Method Update:Void()
		Super.Move()

		If fadeIn Then
			fadeCounter+=dt.delta

			If fadeCounter >= fadeInLength Then
				fadeCounter = fadeLength
				fadeIn = 0
				alpha = 1
			End
		ElseIf fadeCounter>0 Then
			fadeCounter-=dt.delta
			If fadeCounter <= 0 Then
				alpha = 0
				active = 0
			End
		End
	End

End


Absolute mouse position is now passed into mouse listeners as extra parameters (mandatory, so update your code). Fixed the window drag "rubber band" effect. Added string-based style naming (for GUI layout files).

Import mojo
Import diddy

Const ACTION_CLICKED$ = "clicked"
Const ACTION_VALUE_CHANGED$ = "changed"

Class Rectangle
	Field x#, y#, w#, h#
	Field empty? = False

	Method New(x#, y#, w#, h#)
		Set(x, y, w, h)
	End

	Method Set:Void(x#, y#, w#, h#)
		Self.x = x
		Self.y = y
		Self.w = w
		Self.h = h
		Self.empty = Self.w <= 0 Or Self.h <= 0
	End

	Method Set:Void(srcRect:Rectangle)
		Self.x = srcRect.x
		Self.y = srcRect.y
		Self.w = srcRect.w
		Self.h = srcRect.h
		Self.empty = Self.w <= 0 Or Self.h <= 0
	End

	Method Clear:Void()
		Self.w = -1
		Self.h = -1
		Self.empty = True
	End

	Method Intersect:Void(x#, y#, w#, h#)
		If x >= Self.x + Self.w Or y >= Self.y + Self.h Or Self.x >= x + w Or Self.y >= y + h Then
			Clear()
			Return
		End

		Local r% = Self.x + Self.w
		Local b% = Self.y + Self.h
		If Self.x < x Then Self.x = x
		If Self.y < y Then Self.y = y
		If r > x + w Then r = x + w
		If b > y + h Then b = y + h
		Self.w = r - Self.x
		Self.h = b - Self.y
	End
End

Class GUI
	Field desktop:Desktop
	Field scissors:Rectangle[] = New Rectangle[128]
	Field scissorDepth:Int = 0

	Field mouseDown:Bool[3]
	Field mouseDownX:Float[3]
	Field mouseDownY:Float[3]
	Field mouseDownComponent:Component[3]

	Field mouseLastX:Float
	Field mouseLastY:Float
	Field mouseLastComponent:Component
	Field mouseThisX:Float
	Field mouseThisY:Float
	Field mouseThisComponent:Component

	Method New()
		desktop = New Desktop(Self)
		desktop.SetBounds(0, 0, DeviceWidth(), DeviceHeight())
		For Local i% = 0 Until scissors.Length
			scissors[i] = New Rectangle
		Next
	End

	Method PushScissor:Void(x#, y#, w#, h#)
		' don't use assert, for speed on android (one less method call)
		If scissorDepth >= scissors.Length Then Error("GUI.PushScissor: Out of space for scissors.")
		If scissorDepth = 0 Then
			scissors[0].Set(x, y, w, h)
		Else
			scissors[scissorDepth].Set(scissors[scissorDepth-1])
			scissors[scissorDepth].Intersect(x, y, w, h)
		End
		scissorDepth += 1
		UpdateScissor()
	End

	Method PopScissor:Void()
		If scissorDepth > 0 Then
			scissorDepth -= 1
			scissors[scissorDepth].Clear()
		End
		UpdateScissor()
	End

	Method UpdateScissor:Void()
		If scissorDepth > 0 Then
			If Not EmptyScissor() Then
				SetScissor(scissors[scissorDepth-1].x, scissors[scissorDepth-1].y, scissors[scissorDepth-1].w, scissors[scissorDepth-1].h)
			End
		Else
			SetScissor(0, 0, DeviceWidth(), DeviceHeight())
		End
	End

	Method EmptyScissor:Bool()
		If scissorDepth <= 0 Then Return False
		Return scissors[scissorDepth-1].empty
	End

	Method Draw()
		desktop.Draw(Self)
		scissorDepth = 0
		SetScissor(0, 0, DeviceWidth(), DeviceHeight())
	End

	Method ComponentAtPoint:Component(x#, y#, parent:Component=Null)
		' if no parent, it's the desktop
		If parent = Null Then parent = desktop
		' if the mouse is outside the component, return null
		If x<0 Or y<0 Or x >= parent.w Or y >= parent.h Then Return Null
		' check if it's inside a child
		Local rv:Component = Null
		For Local i% = 0 Until parent.children.Size
			Local c:Component = parent.children.Get(i)
			rv = ComponentAtPoint(x-c.x, y-c.y, c)
			If rv <> Null Then Return rv
		Next
		' not inside a child, so it's this one
		Return parent
	End

	Method GetAbsoluteX:Float(comp:Component)
		Local rv# = comp.x
		While comp.parent <> Null
			comp = comp.parent
			rv += comp.x
		Wend
		Return rv
	End

	Method GetAbsoluteY:Float(comp:Component)
		Local rv# = comp.y
		While comp.parent <> Null
			comp = comp.parent
			rv += comp.y
		Wend
		Return rv
	End

	Method Update:Void()
		mouseLastX = mouseThisX
		mouseLastY = mouseThisY
		mouseLastComponent = mouseThisComponent
		mouseThisX = game.mouseX
		mouseThisY = game.mouseY
		mouseThisComponent = ComponentAtPoint(mouseThisX, mouseThisY)
		DoMouse(MOUSE_LEFT)
		DoMouse(MOUSE_MIDDLE)
		DoMouse(MOUSE_RIGHT)
	End

	Method DoMouse:Void(button%)
		Local absX#, absY#
		If MouseHit(button) Then
			mouseDown[button] = True
			mouseDownX[button] = mouseThisX
			mouseDownY[button] = mouseThisY
			mouseDownComponent[button] = mouseThisComponent

			' fire pressed on mouseThisComponent
			If mouseThisComponent.mouseAdapter <> Null Then
				absX = GetAbsoluteX(mouseThisComponent)
				absY = GetAbsoluteY(mouseThisComponent)
				mouseThisComponent.mouseAdapter.MousePressed(mouseThisX-absX, mouseThisY-absY, button, mouseThisX, mouseThisY)
			End

			' set mouseDown
			mouseThisComponent.mouseDown = True

		ElseIf mouseDown[button] Then
			' if we released the button
			If Not MouseDown(button) Then
				mouseDown[button] = False
				Local comp:Component = mouseDownComponent[button]
				mouseDownComponent[button] = Null

				' fire mouse released on comp
				If comp.mouseAdapter <> Null Then
					absX = GetAbsoluteX(comp)
					absY = GetAbsoluteY(comp)
					comp.mouseAdapter.MouseReleased(mouseThisX-absX, mouseThisY-absY, button, mouseThisX, mouseThisY)
				End

				' clear mouseDown
				comp.mouseDown = False

				' if we released on the same component, fire mouse clicked
				If mouseThisComponent = comp Then
					If comp.mouseAdapter <> Null Then comp.mouseAdapter.MouseClicked(mouseThisX-absX, mouseThisY-absY, button, mouseThisX, mouseThisY)
				End

			ElseIf mouseLastX <> mouseThisX Or mouseLastY <> mouseThisY Then
				' fire mouse dragged on mouseDownComponent
				If mouseDownComponent[button].mouseMotionAdapter <> Null Then
					absX = GetAbsoluteX(mouseDownComponent[button])
					absY = GetAbsoluteY(mouseDownComponent[button])
					mouseDownComponent[button].mouseMotionAdapter.MouseDragged(mouseThisX-absX, mouseThisY-absY, button, mouseThisX, mouseThisY)
				End

				' if the component changed, fire exit/enter
				If mouseThisComponent <> mouseLastComponent Then
					' fire mouse exited on mouseLastComponent
					If mouseLastComponent.mouseAdapter <> Null Then
						absX = GetAbsoluteX(mouseLastComponent)
						absY = GetAbsoluteY(mouseLastComponent)
						mouseLastComponent.mouseAdapter.MouseExited(mouseThisX-absX, mouseThisY-absY, mouseThisComponent, mouseThisX, mouseThisY)
					End

					' clear mouseHover
					mouseLastComponent.mouseHover = False

					' fire mouse entered on mouseThisComponent
					If mouseThisComponent.mouseAdapter <> Null Then
						absX = GetAbsoluteX(mouseThisComponent)
						absY = GetAbsoluteY(mouseThisComponent)
						mouseThisComponent.mouseAdapter.MouseEntered(mouseThisX-absX, mouseThisY-absY, mouseLastComponent, mouseThisX, mouseThisY)
					End

					' set the STATE_HOVER bit
					mouseThisComponent.mouseHover = True
				End
			End
		Else
			If mouseLastX <> mouseThisX Or mouseLastY <> mouseThisY Then
				' fire mouse moved on mouseThisComponent
				If mouseThisComponent <> Null And mouseThisComponent.mouseMotionAdapter <> Null Then
					absX = GetAbsoluteX(mouseThisComponent)
					absY = GetAbsoluteY(mouseThisComponent)
					mouseThisComponent.mouseMotionAdapter.MouseMoved(mouseThisX-absX, mouseThisY-absY, mouseThisX, absY)
				End

				' if the component changed, fire exit/enter
				If mouseThisComponent <> mouseLastComponent Then
					' fire mouse exited on mouseLastComponent
					If mouseLastComponent <> Null Then
						If mouseLastComponent.mouseAdapter <> Null Then
							absX = GetAbsoluteX(mouseLastComponent)
							absY = GetAbsoluteY(mouseLastComponent)
							mouseLastComponent.mouseAdapter.MouseExited(mouseThisX-absX, mouseThisY-absY, mouseThisComponent, mouseThisX, mouseThisY)
						End

						' clear mouseHover
						mouseLastComponent.mouseHover = False
					End

					' fire mouse entered on mouseThisComponent
					If mouseThisComponent <> Null Then
						If mouseThisComponent.mouseAdapter <> Null Then
							absX = GetAbsoluteX(mouseThisComponent)
							absY = GetAbsoluteY(mouseThisComponent)
							mouseThisComponent.mouseAdapter.MouseEntered(mouseThisX-absX, mouseThisY-absY, mouseLastComponent, mouseThisX, mouseThisY)
						End
						' set mouseHover
						mouseThisComponent.mouseHover = True
					End
				End
			End
		End
	End

	Method ActionPerformed:Void(source:Component, action:String)
	End
End

Class AbstractMouseAdapter Abstract
	Method MousePressed:Void(x#, y#, button%, absoluteX#, absoluteY#)
	End
	Method MouseReleased:Void(x#, y#, button%, absoluteX#, absoluteY#)
	End
	Method MouseClicked:Void(x#, y#, button%, absoluteX#, absoluteY#)
	End
	Method MouseEntered:Void(x#, y#, exitedComp:Component, absoluteX#, absoluteY#)
	End
	Method MouseExited:Void(x#, y#, enteredComp:Component, absoluteX#, absoluteY#)
	End
End

Class AbstractMouseMotionAdapter Abstract
	Method MouseMoved:Void(x#, y#, absoluteX#, absoluteY#)
	End
	Method MouseDragged:Void(x#, y#, button%, absoluteX#, absoluteY#)
	End
End

Class Desktop Extends Component
	Field restrictWindows:Bool = True
	Field parentGUI:GUI
	Method New(parentGUI:GUI)
		Super.New(Null)
		Self.parentGUI = parentGUI
	End

	Method ActionPerformed:Void(source:Component, action:String)
		parentGUI.ActionPerformed(source, action)
	End
End

Class Component
Private
	Field alpha# = 1
	Field children:ArrayList<Component> = New ArrayList<Component>
	Field mouseAdapter:AbstractMouseAdapter
	Field mouseMotionAdapter:AbstractMouseMotionAdapter
	Field forwardAction:Component = Null
	Field mouseHover:Bool = False
	Field mouseDown:Bool = False

	Field styleNormal:ComponentStyle = Null

Public
	Field parent:Component

	Field x#, y#
	Field w#, h#
	Field visible:Bool = True

	Method MouseAdapter:Void(mouseAdapter:AbstractMouseAdapter) Property
		Self.mouseAdapter = mouseAdapter
	End
	Method MouseAdapter:AbstractMouseAdapter() Property
		Return Self.mouseAdapter
	End
	Method MouseMotionAdapter:Void(mouseMotionAdapter:AbstractMouseMotionAdapter) Property
		Self.mouseMotionAdapter = mouseMotionAdapter
	End
	Method MouseMotionAdapter:AbstractMouseMotionAdapter() Property
		Return Self.mouseMotionAdapter
	End

	Method StyleNormal:ComponentStyle() Property
		If styleNormal = Null Then styleNormal = New ComponentStyle
		Return styleNormal
	End
	Method StyleNormal:Void(style:ComponentStyle) Property
		AssertNotNull(style, "StyleNormal may not be null.")
		styleNormal = style
	End

	Method GetStyle:ComponentStyle(name$)
		If name = "normal" Then Return styleNormal
		Return Null
	End

	Method SetStyle:Void(name$, style:ComponentStyle)
		If name = "normal" Then styleNormal = style
	End

	Method New(parent:Component)
		styleNormal = New ComponentStyle
		' if this isn't the desktop, it must have a parent
		If Not Desktop(Self) Then
			AssertNotNull(parent, "Components must have a parent.")
			Self.parent = parent
			parent.children.Add(Self)
		Else
			styleNormal.drawBackground = False
		End
		AddNotify()
	End

	Method Alpha#() Property
		Return alpha
	End

	Method Alpha:Void(alpha#) Property
		Self.alpha = alpha
		If Self.alpha < 0 Then Self.alpha = 0
		If Self.alpha > 1 Then Self.alpha = 1
	End

	Method SetBackground:Void(red#, green#, blue#)
		If red < 0 Then red = 0
		If red > 255 Then red = 255
		If green < 0 Then green = 0
		If green > 255 Then green = 255
		If blue < 0 Then blue = 0
		If blue > 255 Then blue = 255
		styleNormal.red = red
		styleNormal.green = green
		styleNormal.blue = blue
		styleNormal.drawBackground = True
	End

	Method SetBackground:Void(drawBackground:Bool)
		styleNormal.drawBackground = drawBackground
	End

	Method SetLocation:Void(x#, y#)
		Self.x = x
		Self.y = y
		Layout()
	End

	Method SetSize:Void(w#, h#)
		Self.w = w
		Self.h = h
		Layout()
	End

	Method SetBounds:Void(x#, y#, w#, h#)
		Self.x = x
		Self.y = y
		Self.w = w
		Self.h = h
		Layout()
	End

	Method Draw:Void(parentGui:GUI, alpha# = 1, absx# = 0, absy# = 0)
		If visible Then
			parentGui.PushScissor(absx, absy, w, h)
			If Not parentGui.EmptyScissor() Then
				SetAlpha(alpha)
				DrawComponent()
				DrawChildren(parentGui, alpha, absx, absy)
			End
			parentGui.PopScissor()
		End
	End

	Method DrawChildren:Void(parentGui:GUI, alpha# = 1, absx#, absy#)
		For Local c:Component = EachIn children
			PushMatrix()
			Translate(c.x, c.y)
			c.Draw(parentGui, c.alpha * alpha, absx + c.x, absy + c.y)
			PopMatrix()
		Next
	End

	Method DrawComponent:Void()
		Local style:ComponentStyle = GetCurrentStyle()
		If style <> Null Then
			' background colour first
			If style.drawBackground Then
				SetColor(style.red, style.green, style.blue)
				SetAlpha(alpha)
				DrawRect(0, 0, w, h)
			End

			' image next, priority = down, hover, normal
			If mouseDown And style.downImage <> Null Then
				SetColor(255,255,255)
				SetAlpha(alpha)
				If style.downImageMode = ComponentStyle.IMAGE_GRID Then
					style.downImage.DrawGrid(0, 0, w, h, style.downImageFrame)
				ElseIf style.downImageMode = ComponentStyle.IMAGE_NORMAL Then
					style.downImage.Draw(0, 0, 0, 1, 1, style.downImageFrame)
				End
			ElseIf mouseHover And style.hoverImage <> Null Then
				SetColor(255,255,255)
				SetAlpha(alpha)
				If style.hoverImageMode = ComponentStyle.IMAGE_GRID Then
					style.hoverImage.DrawGrid(0, 0, w, h, style.hoverImageFrame)
				ElseIf style.hoverImageMode = ComponentStyle.IMAGE_NORMAL Then
					style.hoverImage.Draw(0, 0, 0, 1, 1, style.hoverImageFrame)
				End
			ElseIf style.image <> Null Then
				SetColor(255,255,255)
				SetAlpha(alpha)
				If style.imageMode = ComponentStyle.IMAGE_GRID Then
					style.image.DrawGrid(0, 0, w, h, style.imageFrame)
				ElseIf style.imageMode = ComponentStyle.IMAGE_NORMAL Then
					style.image.Draw(0, 0, 0, 1, 1, style.imageFrame)
				End
			End

			' reset stuff
			SetColor(255,255,255)
			SetAlpha(1)
		End
	End

	Method GetCurrentStyle:ComponentStyle()
		Return styleNormal
	End

	Method Dispose:Void(recursing:Bool = False)
		' we do an empty check first to save creating an enumerator object
		If Not children.IsEmpty() Then
			Local enum:AbstractEnumerator<Component> = children.Enumerator()
			While enum.HasNext()
				Local c:Component = enum.NextObject()
				c.Dispose(True)
				enum.Remove()
			End
		End
		DisposeNotify()
		Local p:Component = Self.parent
		Self.parent = Null
		If Not recursing Then p.children.Remove(Self)
	End

	Method AddNotify:Void()
	End

	Method DisposeNotify:Void()
	End

	Method Layout:Void()
	End

	Method ActionPerformed:Void(source:Component, action:String)
		If forwardAction <> Null Then
			forwardAction.ActionPerformed(source, action)
		End
	End

	Method FindActionTarget:Component()
		' if this is a window or desktop, return itself
		If Window(Self) <> Null Or Desktop(Self) <> Null Then Return Self
		' traverse up the hierarchy and find the closest Window or Desktop to receive the actions
		Local comp:Component = Self
		While Window(Self) = Null And Desktop(Self) = Null And comp.parent <> Null
			comp = comp.parent
		End
		Return comp
	End
End

Class ComponentStyle
	Global IMAGE_NORMAL% = 0
	Global IMAGE_TILE% = 1
	Global IMAGE_STRETCH% = 2
	Global IMAGE_GRID% = 3

	Field drawBackground:Bool = True
	Field red:Int = 255
	Field green:Int = 255
	Field blue:Int = 255
	Field image:GameImage = Null
	Field imageFrame:Int = 0
	Field imageMode:Int = IMAGE_NORMAL
	Field imageAlignX:Float = 0
	Field imageAlignY:Float = 0
	Field hoverImage:GameImage = Null
	Field hoverImageFrame:Int = 0
	Field hoverImageMode:Int = IMAGE_NORMAL
	Field hoverImageAlignX:Float = 0
	Field hoverImageAlignY:Float = 0
	Field downImage:GameImage = Null
	Field downImageFrame:Int = 0
	Field downImageMode:Int = IMAGE_NORMAL
	Field downImageAlignX:Float = 0
	Field downImageAlignY:Float = 0
End

Class Panel Extends Component
	Method New(parent:Component)
		Super.New(parent)
	End
End

Class Window Extends Component
Private
	Field contentPane:Panel
	Field titlePane:Panel
	Field buttonPane:Panel

	Field closeButton:WindowButtonPanelButton

	Field titleHeight:Int = 22
	Field buttonWidth:Int = 15

	' note: a window can be all three of these states at once!
	' priority is: minimised, maximised, shaded
	Field maximised:Bool = False
	Field minimised:Bool = False
	Field shaded:Bool = False
	Field dragX#, dragY#, originalX#, originalY#
	Field dragging:Bool = False

	Field normalX#, normalY#, normalWidth#, normalHeight#

	Method CreateButtonPane:Void()
		buttonPane = New Panel(Self)
		buttonPane.w = buttonWidth*3
		buttonPane.StyleNormal.drawBackground = False
		'closeButton = New WindowButtonPanelButton(buttonPane, WindowButtonPanelButton.CLOSE_BUTTON)
		'closeButton.StyleNormal.red = 255
		'closeButton.StyleNormal.green = 255
		'closeButton.StyleNormal.blue = 255
		'closeButton.SetBounds(0, 0, buttonWidth, buttonWidth)
	End

	Method CreateContentPane:Void()
		contentPane = New Panel(Self)
		contentPane.StyleNormal.drawBackground = False
	End

	Method CreateTitlePane:Void()
		titlePane = New Panel(Self)
		titlePane.StyleNormal.drawBackground = False
		titlePane.mouseAdapter = New WindowTitlePaneMouseAdapter(Self)
		titlePane.mouseMotionAdapter = New WindowTitlePaneMouseMotionAdapter(Self)
	End
Public
	Method ContentPane:Panel() Property
		Return contentPane
	End

	Method ContentPane:Void(contentPane:Panel) Property
		If self.contentPane <> Null Then self.contentPane.Dispose()
		Self.contentPane = contentPane
	End

	Method TitlePane:Panel() Property
		Return titlePane
	End

	Method ButtonPane:Panel() Property
		Return buttonPane
	End

	Method Maximised:Void(maximised:Bool) Property
		StoreWindowSize()
		Self.maximised = maximised
		UpdateWindowSize()
	End

	Method Maximised:Bool() Property
		Return maximised
	End

	Method Minimised:Void(minimised:Bool) Property
		StoreWindowSize()
		Self.minimised = minimised
		UpdateWindowSize()
	End

	Method Minimised:Bool() Property
		Return minimised
	End

	Method Shaded:Void(shaded:Bool) Property
		StoreWindowSize()
		Self.shaded = shaded
		UpdateWindowSize()
	End

	Method Shaded:Bool() Property
		Return shaded
	End

	Method StoreWindowSize:Void()
		If maximised Or minimised Then Return
		normalX = x
		normalY = y
		If Not shaded Then
			normalWidth = w
			normalHeight = h
		End
	End

	Method UpdateWindowSize:Void()
		If Not maximised And Not minimised And Not shaded Then
			SetBounds(normalX, normalY, normalWidth, normalHeight)
		ElseIf minimised Then
			SetBounds(0, 0, 50, titleHeight)
		ElseIf maximised Then
			SetBounds(0, 0, parent.w, parent.h)
		ElseIf shaded Then
			SetBounds(normalX, normalY, normalWidth, titleHeight)
		End
	End

	Method New(parent:Component)
		Super.New(parent)
		CreateButtonPane()
		CreateContentPane()
		CreateTitlePane()
	End

	Method Layout:Void()
		If minimised Or shaded Then
			If contentPane <> Null Then contentPane.visible = False
		Else
			If contentPane <> Null Then
				contentPane.visible = True
				contentPane.SetBounds(4, titleHeight, w-8, h-titleHeight-4)
			End
		End
		buttonPane.SetBounds(w-buttonPane.w-4, 0, buttonPane.w, titleHeight)
		titlePane.SetBounds(4, 0, buttonPane.x-4, titleHeight)
	End

End

Class WindowTitlePaneMouseAdapter Extends AbstractMouseAdapter
	Field window:Window
	Method New(window:Window)
		Self.window = window
	End
	Method MousePressed:Void(x#, y#, button%, absoluteX#, absoluteY#)
		If window.dragging Then Return
		If window.maximised Or window.minimised Then Return
		window.dragging = True
		window.dragX = absoluteX
		window.dragY = absoluteY
		window.originalX = window.x
		window.originalY = window.y
	End
	Method MouseReleased:Void(x#, y#, button%, absoluteX#, absoluteY#)
		window.dragging = False
	End
End

Class WindowTitlePaneMouseMotionAdapter Extends AbstractMouseMotionAdapter
	Field window:Window
	Method New(window:Window)
		Self.window = window
	End
	Method MouseDragged:Void(x#, y#, button%, absoluteX#, absoluteY#)
		If window.maximised Or window.minimised Then window.dragging = False
		If Not window.dragging Then Return
		Local dx# = absoluteX-window.dragX, dy# = absoluteY-window.dragY
		Local newX# = window.originalX + dx, newY# = window.originalY + dy

		If Desktop(window.parent) <> Null And Desktop(window.parent).restrictWindows Then
			If newX + window.w > window.parent.w Then newX = window.parent.w - window.w
			If newY + window.h > window.parent.h Then newY = window.parent.h - window.h
			If newX < 0 Then newX = 0
			If newY < 0 Then newY = 0
		End
		window.SetLocation(newX, newY)
	End
End

Class WindowButtonPanelButton Extends Button
	Const CLOSE_BUTTON% = 0
	Const MAXIMISE_BUTTON% = 1
	Const MINIMISE_BUTTON% = 2
	Const SHADE_BUTTON% = 3
	Field buttonType%

	Method New(parent:Component, buttonType%)
		Super.New(parent)
		Self.buttonType = buttonType
	End

	Method ButtonClicked:Void()
		Local window:Window = Window(parent)
		AssertNotNull(window, "WindowButtonPanelButton.ButtonClicked: window was null")
		Select buttonType
			Case CLOSE_BUTTON
				window.Dispose()
		End
	End
End

Class Label Extends Component
	Field text:String
	Field textRed%, textGreen%, textBlue%
	Field textXOffset# = 0
	Field textYOffset# = 0
	Field textXAlign# = 0
	Field textYAlign# = 0

	Method New(parent:Component)
		Super.New(parent)
	End

	Method DrawComponent:Void()
		Super.DrawComponent()
		' TODO: draw text
	End
End

Class Button Extends Label
Private
	Field styleSelected:ComponentStyle = Null

Public
	Field selected:Bool
	Field toggle:Bool
	Field radioGroup:RadioGroup
	Field radioValue$

	Method New(parent:Component)
		Super.New(parent)
		Self.forwardAction = FindActionTarget()
		mouseAdapter = New ButtonMouseAdapter(Self)
	End

	Method New(parent:Component, forwardAction:Component)
		Super.New(parent)
		Self.forwardAction = forwardAction
		mouseAdapter = New ButtonMouseAdapter(Self)
	End

	Method StyleSelected:ComponentStyle() Property
		If styleSelected = Null Then styleSelected = New ComponentStyle
		Return styleSelected
	End
	Method StyleSelected:Void(style:ComponentStyle) Property
		styleSelected = style
	End

	Method GetStyle:ComponentStyle(name$)
		If name = "selected" Then Return styleSelected
		Return Super.GetStyle(name)
	End

	Method SetStyle:Void(name$, style:ComponentStyle)
		If name = "selected" Then
			styleSelected = style
		Else
			Super.GetStyle(name)
		End
	End

	Method GetCurrentStyle:ComponentStyle()
		If selected And styleSelected <> Null Then Return styleSelected
		Return Super.GetCurrentStyle()
	End
End

Class ButtonMouseAdapter Extends AbstractMouseAdapter
	Field button:Button
	Method New(button:Button)
		Self.button = button
	End
	Method MouseClicked:Void(x#, y#, button%, absoluteX#, absoluteY#)
		' is it a radio button?
		If Self.button.radioGroup <> Null Then
			Self.button.radioGroup.SelectButton(Self.button)
		' is it a toggle button?
		ElseIf Self.button.toggle Then
			Self.button.selected = Not Self.button.selected
		End
		Self.button.ActionPerformed(Self.button, ACTION_CLICKED)
	End
End

Class RadioGroup
	Field buttons:ArrayList<Button> = New ArrayList<Button>
	Field currentValue$

	Method SelectButton:String(button:Button)
		For Local b:Button = EachIn buttons
			b.selected = (b = button)
			If b.selected Then
				currentValue = b.radioValue
			End
		Next
		Return currentValue
	End

	Method SelectValue:Button(value$)
		Local rv:Button = Null
		For Local b:Button = EachIn buttons
			b.selected = (b.radioValue = value)
			If b.selected Then
				currentValue = value
				rv = b
			End
		Next
		Return rv
	End

	Method AddButton:Void(button:Button, value$)
		button.radioValue = value
		button.radioGroup = Self
		buttons.Add(button)
	End

	Method RemoveButton:Void(button:Button)
		button.radioValue = ""
		button.radioGroup = Null
		buttons.Remove(button)
	End
End

Class Slider Extends Component
	Const SLIDER_HORIZONTAL% = 0
	Const SLIDER_VERTICAL% = 1
	Const SLIDER_DIRECTION_TL_TO_BR% = 0 ' min is top or left, max is bottom or right
	Const SLIDER_DIRECTION_BR_TO_TL% = 1 ' min is bottom or right, max is top or left
Private
	Field buttonUpLeft:Button ' the button used for up and left
	Field buttonDownRight:Button ' the button used for down and right
	Field handle:Label
	Field showButtons:Bool
	Field orientation:Int = SLIDER_HORIZONTAL
	Field direction:Int = SLIDER_DIRECTION_TL_TO_BR

	Field dragX%, dragY%, originalX%, originalY%
	Field dragging:Bool = False

	Field styleLeftButton:ComponentStyle
	Field styleRightButton:ComponentStyle
	Field styleTopButton:ComponentStyle
	Field styleBottomButton:ComponentStyle
	Field styleHorizontalHandle:ComponentStyle
	Field styleVerticalHandle:ComponentStyle

Public
	Field minValue% = 0
	Field maxValue% = 100
	Field value% = 50
	Field tickInterval% = 10
	Field handleMargin% = 10
	Field handleSize% = 10
	Field buttonSize% = 15
	Field snapToTicks:Bool = True

	Method New(parent:Component)
		Super.New(parent)
		buttonUpLeft = New Button(Self, Self)
		buttonDownRight = New Button(Self, Self)
		handle = New Label(Self)
		handle.mouseAdapter = New SliderHandleMouseAdapter(Self)
		handle.mouseMotionAdapter = New SliderHandleMouseMotionAdapter(Self)
		buttonUpLeft.visible = False
		buttonDownRight.visible = False
	End

	Method GetStyle:ComponentStyle(name$)
		If name = "leftButton" Then
			Return styleLeftButton
		ElseIf name = "rightButton" Then
			Return styleRightButton
		ElseIf name = "topButton" Then
			Return styleTopButton
		ElseIf name = "bottomButton" Then
			Return styleBottomButton
		ElseIf name = "horizontalHandle" Then
			Return styleHorizontalHandle
		ElseIf name = "verticalHandle" Then
			Return styleVerticalHandle
		End
		Return Super.GetStyle(name)
	End

	Method SetStyle:Void(name$, style:ComponentStyle)
		If name = "leftButton" Then
			styleLeftButton = style
		ElseIf name = "rightButton" Then
			styleRightButton = style
		ElseIf name = "topButton" Then
			styleTopButton = style
		ElseIf name = "bottomButton" Then
			styleBottomButton = style
		ElseIf name = "horizontalHandle" Then
			styleHorizontalHandle = style
		ElseIf name = "verticalHandle" Then
			styleVerticalHandle = style
		Else
			Super.SetStyle(name, style)
		End
		UpdateStyles()
	End

	Method ShowButtons:Bool() Property
		Return showButtons
	End

	Method ShowButtons:Void(showButtons:Bool) Property
		If showButtons <> Self.showButtons Then
			Self.showButtons = showButtons
			Layout()
		End
		Self.showButtons = showButtons
	End

	Method Orientation:Int() Property
		Return orientation
	End

	Method Orientation:Void(orientation:Int) Property
		If Self.orientation <> orientation Then
			Self.orientation = orientation
			UpdateStyles()
			Layout()
		End
		Self.orientation = orientation
	End

	Method Direction:Int() Property
		Return direction
	End

	Method Direction:Void(direction:Int) Property
		If Self.direction <> direction Then
			Self.direction = direction
			Layout()
		End
		Self.direction = direction
	End

	Method DrawComponent:Void()
		Super.DrawComponent()
		' TODO: render groove and ticks, etc.
	End

	Method UpdateStyles:Void()
		If orientation = SLIDER_HORIZONTAL Then
			buttonUpLeft.StyleNormal = styleLeftButton
			buttonDownRight.StyleNormal = styleRightButton
			handle.StyleNormal = styleHorizontalHandle
		Else
			buttonUpLeft.StyleNormal = styleTopButton
			buttonDownRight.StyleNormal = styleBottomButton
			handle.StyleNormal = styleVerticalHandle
		End
	End

	' TODO: adjust layout using xml offsets rather than hardcoded
	Method Layout:Void()
		If showButtons Then
			If orientation = SLIDER_HORIZONTAL Then
				buttonUpLeft.SetBounds(0, 0, buttonSize, Self.h)
				buttonDownRight.SetBounds(Self.w - buttonSize, 0, buttonSize, Self.h)
			Else
				buttonUpLeft.SetBounds(0, 0, Self.w, buttonSize)
				buttonDownRight.SetBounds(0, Self.h - buttonSize, Self.w, buttonSize)
			End
			buttonUpLeft.visible = True
			buttonDownRight.visible = True
		Else
			buttonUpLeft.visible = False
			buttonDownRight.visible = False
		End
		Local startVal% = buttonSize+handleMargin
		Local endVal% = -buttonSize-handleMargin
		Local fraction# = Float(value-minValue)/Float(maxValue-minValue)
		Local currentVal%
		If orientation = SLIDER_HORIZONTAL Then
			endVal += Self.w
			currentVal = startVal + (endVal - startVal) * fraction
			handle.SetBounds(currentVal-handleSize/2, 0, handleSize, Self.h)
		Else
			endVal += Self.h
			currentVal = startVal + (endVal - startVal) * fraction
			handle.SetBounds(0, currentVal-handleSize/2, Self.w, handleSize)
		End
	End

	Method HandleDrag:Int(mx%, my%)
		Local pos%, topLeft% = handleMargin, bottomRight% = -handleMargin
		If showButtons Then
			topLeft += buttonSize
			bottomRight -= buttonSize
		End
		If orientation = SLIDER_HORIZONTAL Then
			bottomRight += w
			pos = Min(Max(topLeft, mx), bottomRight)
		Else
			bottomRight += h
			pos = Min(Max(topLeft, my), bottomRight)
		End
		Local fraction# = Float(pos-topLeft) / Float(bottomRight-topLeft)
		If direction = SLIDER_DIRECTION_BR_TO_TL Then fraction = 1-fraction

		Local oldValue% = value
		value = SnapToValue(minValue + (maxValue - minValue)*fraction)

		' if it changed, update the layout and fire an event
		If value <> oldValue Then
			Local target:Component = forwardAction
			If target = Null Then target = FindActionTarget()
			If target <> Null Then
				Layout()
				target.ActionPerformed(Self, ACTION_VALUE_CHANGED)
			EndIf
		End
	End

	Method ActionPerformed:Void(source:Component, action:String)
		If source = buttonUpLeft And action = ACTION_CLICKED Then
			If direction = SLIDER_DIRECTION_TL_TO_BR Then
				AdjustValue(-1)
			Else
				AdjustValue(1)
			End
		ElseIf source = buttonDownRight And action = ACTION_CLICKED Then
			If direction = SLIDER_DIRECTION_TL_TO_BR Then
				AdjustValue(1)
			Else
				AdjustValue(-1)
			End
		End
	End

	Method SnapToValue:Int(val%)
		If val < minValue Then
			val = minValue
			Return val
		End
		If val > maxValue Then
			val = maxValue
			Return val
		End
		If val Mod tickInterval = 0 Then Return val
		If val Mod tickInterval < tickInterval / 2 Then
			val -= val Mod tickInterval
		Else
			val -= val Mod tickInterval
			val += tickInterval
		End
		Return val
	End

	Method AdjustValue:Bool(amount%)
		If amount = 0 Then Return
		Local oldValue% = value

		' snap if we must
		If value Mod tickInterval > 0 Then
			If amount < 0 Then
				value += tickInterval - (value Mod tickInterval)
			Else
				value -= value Mod tickInterval
			End
		End

		' adjust it
		value += amount * tickInterval

		' check that it's in range
		If value < minValue Then value = minValue
		If value > maxValue Then value = maxValue

		' if it changed, update the layout and fire an event
		If value <> oldValue Then
			Local target:Component = forwardAction
			If target = Null Then target = FindActionTarget()
			If target <> Null Then
				Layout()
				target.ActionPerformed(Self, ACTION_VALUE_CHANGED)
			EndIf
		End
		Return value <> oldValue
	End
End

Class SliderHandleMouseAdapter Extends AbstractMouseAdapter
	Field slider:Slider
	Method New(slider:Slider)
		Self.slider = slider
	End
	Method MousePressed:Void(x#, y#, button%, absoluteX#, absoluteY#)
		If slider.dragging Then Return
		slider.dragging = True
		slider.HandleDrag(slider.handle.x + x, slider.handle.y + y)
	End
	Method MouseReleased:Void(x#, y#, button%, absoluteX#, absoluteY#)
		slider.dragging = False
		slider.HandleDrag(slider.handle.x + x, slider.handle.y + y)
	End
End

Class SliderHandleMouseMotionAdapter Extends AbstractMouseMotionAdapter
	Field slider:Slider
	Method New(slider:Slider)
		Self.slider = slider
	End
	Method MouseDragged:Void(x#, y#, button%, absoluteX#, absoluteY#)
		If Not slider.dragging Then Return
		slider.HandleDrag(slider.handle.x + x, slider.handle.y + y)
	End
End

Import mojo
Import diddy

Const ACTION_CLICKED$ = "clicked"
Const ACTION_VALUE_CHANGED$ = "changed"

Class Rectangle
	Field x#, y#, w#, h#
	Field empty? = False

	Method New(x#, y#, w#, h#)
		Set(x, y, w, h)
	End

	Method Set:Void(x#, y#, w#, h#)
		Self.x = x
		Self.y = y
		Self.w = w
		Self.h = h
		Self.empty = Self.w <= 0 Or Self.h <= 0
	End

	Method Set:Void(srcRect:Rectangle)
		Self.x = srcRect.x
		Self.y = srcRect.y
		Self.w = srcRect.w
		Self.h = srcRect.h
		Self.empty = Self.w <= 0 Or Self.h <= 0
	End

	Method Clear:Void()
		Self.w = -1
		Self.h = -1
		Self.empty = True
	End

	Method Intersect:Void(x#, y#, w#, h#)
		If x >= Self.x + Self.w Or y >= Self.y + Self.h Or Self.x >= x + w Or Self.y >= y + h Then
			Clear()
			Return
		End

		Local r% = Self.x + Self.w
		Local b% = Self.y + Self.h
		If Self.x < x Then Self.x = x
		If Self.y < y Then Self.y = y
		If r > x + w Then r = x + w
		If b > y + h Then b = y + h
		Self.w = r - Self.x
		Self.h = b - Self.y
	End
End

Class GUI
	Field desktop:Desktop
	Field scissors:Rectangle[] = New Rectangle[128]
	Field scissorDepth:Int = 0

	Field mouseDown:Bool[3]
	Field mouseDownX:Float[3]
	Field mouseDownY:Float[3]
	Field mouseDownComponent:Component[3]

	Field mouseLastX:Float
	Field mouseLastY:Float
	Field mouseLastComponent:Component
	Field mouseThisX:Float
	Field mouseThisY:Float
	Field mouseThisComponent:Component

	Method New()
		desktop = New Desktop(Self)
		desktop.SetBounds(0, 0, DeviceWidth(), DeviceHeight())
		For Local i% = 0 Until scissors.Length
			scissors[i] = New Rectangle
		Next
	End

	Method PushScissor:Void(x#, y#, w#, h#)
		' don't use assert, for speed on android (one less method call)
		If scissorDepth >= scissors.Length Then Error("GUI.PushScissor: Out of space for scissors.")
		If scissorDepth = 0 Then
			scissors[0].Set(x, y, w, h)
		Else
			scissors[scissorDepth].Set(scissors[scissorDepth-1])
			scissors[scissorDepth].Intersect(x, y, w, h)
		End
		scissorDepth += 1
		UpdateScissor()
	End

	Method PopScissor:Void()
		If scissorDepth > 0 Then
			scissorDepth -= 1
			scissors[scissorDepth].Clear()
		End
		UpdateScissor()
	End

	Method UpdateScissor:Void()
		If scissorDepth > 0 Then
			If Not EmptyScissor() Then
				SetScissor(scissors[scissorDepth-1].x, scissors[scissorDepth-1].y, scissors[scissorDepth-1].w, scissors[scissorDepth-1].h)
			End
		Else
			SetScissor(0, 0, DeviceWidth(), DeviceHeight())
		End
	End

	Method EmptyScissor:Bool()
		If scissorDepth <= 0 Then Return False
		Return scissors[scissorDepth-1].empty
	End

	Method Draw()
		desktop.Draw(Self)
		scissorDepth = 0
		SetScissor(0, 0, DeviceWidth(), DeviceHeight())
	End

	Method ComponentAtPoint:Component(x#, y#, parent:Component=Null)
		' if no parent, it's the desktop
		If parent = Null Then parent = desktop
		' if the mouse is outside the component, return null
		If x<0 Or y<0 Or x >= parent.w Or y >= parent.h Then Return Null
		' check if it's inside a child
		Local rv:Component = Null
		For Local i% = 0 Until parent.children.Size
			Local c:Component = parent.children.Get(i)
			rv = ComponentAtPoint(x-c.x, y-c.y, c)
			If rv <> Null Then Return rv
		Next
		' not inside a child, so it's this one
		Return parent
	End

	Method GetAbsoluteX:Float(comp:Component)
		Local rv# = comp.x
		While comp.parent <> Null
			comp = comp.parent
			rv += comp.x
		Wend
		Return rv
	End

	Method GetAbsoluteY:Float(comp:Component)
		Local rv# = comp.y
		While comp.parent <> Null
			comp = comp.parent
			rv += comp.y
		Wend
		Return rv
	End

	Method Update:Void()
		mouseLastX = mouseThisX
		mouseLastY = mouseThisY
		mouseLastComponent = mouseThisComponent
		mouseThisX = game.mouseX
		mouseThisY = game.mouseY
		mouseThisComponent = ComponentAtPoint(mouseThisX, mouseThisY)
		DoMouse(MOUSE_LEFT)
		DoMouse(MOUSE_MIDDLE)
		DoMouse(MOUSE_RIGHT)
	End

	Method DoMouse:Void(button%)
		Local absX#, absY#
		If MouseHit(button) Then
			mouseDown[button] = True
			mouseDownX[button] = mouseThisX
			mouseDownY[button] = mouseThisY
			mouseDownComponent[button] = mouseThisComponent

			' fire pressed on mouseThisComponent
			If mouseThisComponent.mouseAdapter <> Null Then
				absX = GetAbsoluteX(mouseThisComponent)
				absY = GetAbsoluteY(mouseThisComponent)
				mouseThisComponent.mouseAdapter.MousePressed(mouseThisX-absX, mouseThisY-absY, button)
			End

			' set mouseDown
			mouseThisComponent.mouseDown = True

		ElseIf mouseDown[button] Then
			' if we released the button
			If Not MouseDown(button) Then
				mouseDown[button] = False
				Local comp:Component = mouseDownComponent[button]
				mouseDownComponent[button] = Null

				' fire mouse released on comp
				If comp.mouseAdapter <> Null Then
					absX = GetAbsoluteX(comp)
					absY = GetAbsoluteY(comp)
					comp.mouseAdapter.MouseReleased(mouseThisX-absX, mouseThisY-absY, button)
				End

				' clear mouseDown
				comp.mouseDown = False

				' if we released on the same component, fire mouse clicked
				If mouseThisComponent = comp Then
					If comp.mouseAdapter <> Null Then comp.mouseAdapter.MouseClicked(mouseThisX-absX, mouseThisY-absY, button)
				End

			ElseIf mouseLastX <> mouseThisX Or mouseLastY <> mouseThisY Then
				' fire mouse dragged on mouseDownComponent
				If mouseDownComponent[button].mouseMotionAdapter <> Null Then
					absX = GetAbsoluteX(mouseDownComponent[button])
					absY = GetAbsoluteY(mouseDownComponent[button])
					mouseDownComponent[button].mouseMotionAdapter.MouseDragged(mouseThisX-absX, mouseThisY-absY, button)
				End

				' if the component changed, fire exit/enter
				If mouseThisComponent <> mouseLastComponent Then
					' fire mouse exited on mouseLastComponent
					If mouseLastComponent.mouseAdapter <> Null Then
						absX = GetAbsoluteX(mouseLastComponent)
						absY = GetAbsoluteY(mouseLastComponent)
						mouseLastComponent.mouseAdapter.MouseExited(mouseThisX-absX, mouseThisY-absY, mouseThisComponent)
					End

					' clear mouseHover
					mouseLastComponent.mouseHover = False

					' fire mouse entered on mouseThisComponent
					If mouseThisComponent.mouseAdapter <> Null Then
						absX = GetAbsoluteX(mouseThisComponent)
						absY = GetAbsoluteY(mouseThisComponent)
						mouseThisComponent.mouseAdapter.MouseEntered(mouseThisX-absX, mouseThisY-absY, mouseLastComponent)
					End

					' set the STATE_HOVER bit
					mouseThisComponent.mouseHover = True
				End
			End
		Else
			If mouseLastX <> mouseThisX Or mouseLastY <> mouseThisY Then
				' fire mouse moved on mouseThisComponent
				If mouseThisComponent <> Null And mouseThisComponent.mouseMotionAdapter <> Null Then
					absX = GetAbsoluteX(mouseThisComponent)
					absY = GetAbsoluteY(mouseThisComponent)
					mouseThisComponent.mouseMotionAdapter.MouseMoved(mouseThisX-absX, mouseThisY-absY)
				End

				' if the component changed, fire exit/enter
				If mouseThisComponent <> mouseLastComponent Then
					' fire mouse exited on mouseLastComponent
					If mouseLastComponent <> Null Then
						If mouseLastComponent.mouseAdapter <> Null Then
							absX = GetAbsoluteX(mouseLastComponent)
							absY = GetAbsoluteY(mouseLastComponent)
							mouseLastComponent.mouseAdapter.MouseExited(mouseThisX-absX, mouseThisY-absY, mouseThisComponent)
						End

						' clear mouseHover
						mouseLastComponent.mouseHover = False
					End

					' fire mouse entered on mouseThisComponent
					If mouseThisComponent <> Null Then
						If mouseThisComponent.mouseAdapter <> Null Then
							absX = GetAbsoluteX(mouseThisComponent)
							absY = GetAbsoluteY(mouseThisComponent)
							mouseThisComponent.mouseAdapter.MouseEntered(mouseThisX-absX, mouseThisY-absY, mouseLastComponent)
						End
						' set mouseHover
						mouseThisComponent.mouseHover = True
					End
				End
			End
		End
	End

	Method ActionPerformed:Void(source:Component, action:String)
	End
End

Class AbstractMouseAdapter Abstract
	Method MousePressed:Void(x#, y#, button%)
	End
	Method MouseReleased:Void(x#, y#, button%)
	End
	Method MouseClicked:Void(x#, y#, button%)
	End
	Method MouseEntered:Void(x#, y#, exitedComp:Component)
	End
	Method MouseExited:Void(x#, y#, enteredComp:Component)
	End
End

Class AbstractMouseMotionAdapter Abstract
	Method MouseMoved:Void(x#, y#)
	End
	Method MouseDragged:Void(x#, y#, button%)
	End
End

Class Desktop Extends Component
	Field restrictWindows:Bool = True
	Field parentGUI:GUI
	Method New(parentGUI:GUI)
		Super.New(Null)
		Self.parentGUI = parentGUI
	End

	Method ActionPerformed:Void(source:Component, action:String)
		parentGUI.ActionPerformed(source, action)
	End
End

Class Component
Private
	Field alpha# = 1
	Field children:ArrayList<Component> = New ArrayList<Component>
	Field mouseAdapter:AbstractMouseAdapter
	Field mouseMotionAdapter:AbstractMouseMotionAdapter
	Field forwardAction:Component = Null
	Field mouseHover:Bool = False
	Field mouseDown:Bool = False

	Field styleNormal:ComponentStyle = Null

Public
	Field parent:Component

	Field x#, y#
	Field w#, h#
	Field visible:Bool = True

	Method MouseAdapter:Void(mouseAdapter:AbstractMouseAdapter) Property
		Self.mouseAdapter = mouseAdapter
	End
	Method MouseAdapter:AbstractMouseAdapter() Property
		Return Self.mouseAdapter
	End
	Method MouseMotionAdapter:Void(mouseMotionAdapter:AbstractMouseMotionAdapter) Property
		Self.mouseMotionAdapter = mouseMotionAdapter
	End
	Method MouseMotionAdapter:AbstractMouseMotionAdapter() Property
		Return Self.mouseMotionAdapter
	End

	Method StyleNormal:ComponentStyle() Property
		If styleNormal = Null Then styleNormal = New ComponentStyle
		Return styleNormal
	End
	Method StyleNormal:Void(style:ComponentStyle) Property
		AssertNotNull(style, "StyleNormal may not be null.")
		styleNormal = style
	End

	Method New(parent:Component)
		styleNormal = New ComponentStyle
		' if this isn't the desktop, it must have a parent
		If Not Desktop(Self) Then
			AssertNotNull(parent, "Components must have a parent.")
			Self.parent = parent
			parent.children.Add(Self)
		Else
			styleNormal.drawBackground = False
		End
		AddNotify()
	End

	Method Alpha#() Property
		Return alpha
	End

	Method Alpha:Void(alpha#) Property
		Self.alpha = alpha
		If Self.alpha < 0 Then Self.alpha = 0
		If Self.alpha > 1 Then Self.alpha = 1
	End

	Method SetBackground:Void(red#, green#, blue#)
		If red < 0 Then red = 0
		If red > 255 Then red = 255
		If green < 0 Then green = 0
		If green > 255 Then green = 255
		If blue < 0 Then blue = 0
		If blue > 255 Then blue = 255
		styleNormal.red = red
		styleNormal.green = green
		styleNormal.blue = blue
		styleNormal.drawBackground = True
	End

	Method SetBackground:Void(drawBackground:Bool)
		styleNormal.drawBackground = drawBackground
	End

	Method SetLocation:Void(x#, y#)
		Self.x = x
		Self.y = y
		Layout()
	End

	Method SetSize:Void(w#, h#)
		Self.w = w
		Self.h = h
		Layout()
	End

	Method SetBounds:Void(x#, y#, w#, h#)
		Self.x = x
		Self.y = y
		Self.w = w
		Self.h = h
		Layout()
	End

	Method Draw:Void(parentGui:GUI, alpha# = 1, absx# = 0, absy# = 0)
		If visible Then
			parentGui.PushScissor(absx, absy, w, h)
			If Not parentGui.EmptyScissor() Then
				SetAlpha(alpha)
				DrawComponent()
				DrawChildren(parentGui, alpha, absx, absy)
			End
			parentGui.PopScissor()
		End
	End

	Method DrawChildren:Void(parentGui:GUI, alpha# = 1, absx#, absy#)
		For Local c:Component = EachIn children
			PushMatrix()
			Translate(c.x, c.y)
			c.Draw(parentGui, c.alpha * alpha, absx + c.x, absy + c.y)
			PopMatrix()
		Next
	End

	Method DrawComponent:Void()
		Local style:ComponentStyle = GetCurrentStyle()
		If style <> Null Then
			' background colour first
			If style.drawBackground Then
				SetColor(style.red, style.green, style.blue)
				SetAlpha(alpha)
				DrawRect(0, 0, w, h)
			End

			' image next, priority = down, hover, normal
			If mouseDown And style.downImage <> Null Then
				SetColor(255,255,255)
				SetAlpha(alpha)
				If style.downImageMode = ComponentStyle.IMAGE_GRID Then
					style.downImage.DrawGrid(0, 0, w, h, style.downImageFrame)
				ElseIf style.downImageMode = ComponentStyle.IMAGE_NORMAL Then
					style.downImage.Draw(0, 0, 0, 1, 1, style.downImageFrame)
				End
			ElseIf mouseHover And style.hoverImage <> Null Then
				SetColor(255,255,255)
				SetAlpha(alpha)
				If style.hoverImageMode = ComponentStyle.IMAGE_GRID Then
					style.hoverImage.DrawGrid(0, 0, w, h, style.hoverImageFrame)
				ElseIf style.hoverImageMode = ComponentStyle.IMAGE_NORMAL Then
					style.hoverImage.Draw(0, 0, 0, 1, 1, style.hoverImageFrame)
				End
			ElseIf style.image <> Null Then
				SetColor(255,255,255)
				SetAlpha(alpha)
				If style.imageMode = ComponentStyle.IMAGE_GRID Then
					style.image.DrawGrid(0, 0, w, h, style.imageFrame)
				ElseIf style.imageMode = ComponentStyle.IMAGE_NORMAL Then
					style.image.Draw(0, 0, 0, 1, 1, style.imageFrame)
				End
			End

			' reset stuff
			SetColor(255,255,255)
			SetAlpha(1)
		End
	End

	Method GetCurrentStyle:ComponentStyle()
		Return styleNormal
	End

	Method Dispose:Void(recursing:Bool = False)
		' we do an empty check first to save creating an enumerator object
		If Not children.IsEmpty() Then
			Local enum:AbstractEnumerator<Component> = children.Enumerator()
			While enum.HasNext()
				Local c:Component = enum.NextObject()
				c.Dispose(True)
				enum.Remove()
			End
		End
		DisposeNotify()
		Local p:Component = Self.parent
		Self.parent = Null
		If Not recursing Then p.children.Remove(Self)
	End

	Method AddNotify:Void()
	End

	Method DisposeNotify:Void()
	End

	Method Layout:Void()
	End

	Method ActionPerformed:Void(source:Component, action:String)
		If forwardAction <> Null Then
			forwardAction.ActionPerformed(source, action)
		End
	End

	Method FindActionTarget:Component()
		' if this is a window or desktop, return itself
		If Window(Self) <> Null Or Desktop(Self) <> Null Then Return Self
		' traverse up the hierarchy and find the closest Window or Desktop to receive the actions
		Local comp:Component = Self
		While Window(Self) = Null And Desktop(Self) = Null And comp.parent <> Null
			comp = comp.parent
		End
		Return comp
	End
End

Class ComponentStyle
	Global IMAGE_NORMAL% = 0
	Global IMAGE_TILE% = 1
	Global IMAGE_STRETCH% = 2
	Global IMAGE_GRID% = 3

	Field drawBackground:Bool = True
	Field red:Int = 255
	Field green:Int = 255
	Field blue:Int = 255
	Field image:GameImage = Null
	Field imageFrame:Int = 0
	Field imageMode:Int = IMAGE_NORMAL
	Field imageAlignX:Float = 0
	Field imageAlignY:Float = 0
	Field hoverImage:GameImage = Null
	Field hoverImageFrame:Int = 0
	Field hoverImageMode:Int = IMAGE_NORMAL
	Field hoverImageAlignX:Float = 0
	Field hoverImageAlignY:Float = 0
	Field downImage:GameImage = Null
	Field downImageFrame:Int = 0
	Field downImageMode:Int = IMAGE_NORMAL
	Field downImageAlignX:Float = 0
	Field downImageAlignY:Float = 0
End

Class Panel Extends Component
	Method New(parent:Component)
		Super.New(parent)
	End
End

Class Window Extends Component
Private
	Field contentPane:Panel
	Field titlePane:Panel
	Field buttonPane:Panel

	Field closeButton:WindowButtonPanelButton

	Field titleHeight:Int = 22
	Field buttonWidth:Int = 15

	' note: a window can be all three of these states at once!
	' priority is: minimised, maximised, shaded
	Field maximised:Bool = False
	Field minimised:Bool = False
	Field shaded:Bool = False
	Field dragX#, dragY#, originalX#, originalY#
	Field dragging:Bool = False

	Field normalX#, normalY#, normalWidth#, normalHeight#

	Method CreateButtonPane:Void()
		buttonPane = New Panel(Self)
		buttonPane.w = buttonWidth*3
		buttonPane.StyleNormal.drawBackground = False
		'closeButton = New WindowButtonPanelButton(buttonPane, WindowButtonPanelButton.CLOSE_BUTTON)
		'closeButton.StyleNormal.red = 255
		'closeButton.StyleNormal.green = 255
		'closeButton.StyleNormal.blue = 255
		'closeButton.SetBounds(0, 0, buttonWidth, buttonWidth)
	End

	Method CreateContentPane:Void()
		contentPane = New Panel(Self)
		contentPane.StyleNormal.drawBackground = False
	End

	Method CreateTitlePane:Void()
		titlePane = New Panel(Self)
		titlePane.StyleNormal.drawBackground = False
		titlePane.mouseAdapter = New WindowTitlePaneMouseAdapter(Self)
		titlePane.mouseMotionAdapter = New WindowTitlePaneMouseMotionAdapter(Self)
	End
Public
	Method ContentPane:Panel() Property
		Return contentPane
	End

	Method ContentPane:Void(contentPane:Panel) Property
		If self.contentPane <> Null Then self.contentPane.Dispose()
		Self.contentPane = contentPane
	End

	Method TitlePane:Panel() Property
		Return titlePane
	End

	Method ButtonPane:Panel() Property
		Return buttonPane
	End

	Method Maximised:Void(maximised:Bool) Property
		StoreWindowSize()
		Self.maximised = maximised
		UpdateWindowSize()
	End

	Method Maximised:Bool() Property
		Return maximised
	End

	Method Minimised:Void(minimised:Bool) Property
		StoreWindowSize()
		Self.minimised = minimised
		UpdateWindowSize()
	End

	Method Minimised:Bool() Property
		Return minimised
	End

	Method Shaded:Void(shaded:Bool) Property
		StoreWindowSize()
		Self.shaded = shaded
		UpdateWindowSize()
	End

	Method Shaded:Bool() Property
		Return shaded
	End

	Method StoreWindowSize:Void()
		If maximised Or minimised Then Return
		normalX = x
		normalY = y
		If Not shaded Then
			normalWidth = w
			normalHeight = h
		End
	End

	Method UpdateWindowSize:Void()
		If Not maximised And Not minimised And Not shaded Then
			SetBounds(normalX, normalY, normalWidth, normalHeight)
		ElseIf minimised Then
			SetBounds(0, 0, 50, titleHeight)
		ElseIf maximised Then
			SetBounds(0, 0, parent.w, parent.h)
		ElseIf shaded Then
			SetBounds(normalX, normalY, normalWidth, titleHeight)
		End
	End

	Method New(parent:Component)
		Super.New(parent)
		CreateButtonPane()
		CreateContentPane()
		CreateTitlePane()
	End

	Method Layout:Void()
		If minimised Or shaded Then
			If contentPane <> Null Then contentPane.visible = False
		Else
			If contentPane <> Null Then
				contentPane.visible = True
				contentPane.SetBounds(4, titleHeight, w-8, h-titleHeight-4)
			End
		End
		buttonPane.SetBounds(w-buttonPane.w-4, 0, buttonPane.w, titleHeight)
		titlePane.SetBounds(4, 0, buttonPane.x-4, titleHeight)
	End

End

Class WindowTitlePaneMouseAdapter Extends AbstractMouseAdapter
	Field window:Window
	Method New(window:Window)
		Self.window = window
	End
	Method MousePressed:Void(x#, y#, button%)
		If window.dragging Then Return
		If window.maximised Or window.minimised Then Return
		window.dragging = True
		window.dragX = x
		window.dragY = y
		window.originalX = window.x
		window.originalY = window.y
	End
	Method MouseReleased:Void(x#, y#, button%)
		window.dragging = False
	End
End

Class WindowTitlePaneMouseMotionAdapter Extends AbstractMouseMotionAdapter
	Field window:Window
	Method New(window:Window)
		Self.window = window
	End
	Method MouseDragged:Void(x#, y#, button%)
		If window.maximised Or window.minimised Then window.dragging = False
		If Not window.dragging Then Return
		Local dx# = x-window.dragX, dy# = y-window.dragY
		Local newX# = window.originalX + dx, newY# = window.originalY + dy

		If Desktop(window.parent) <> Null And Desktop(window.parent).restrictWindows Then
			If newX + window.w > window.parent.w Then newX = window.parent.w - window.w
			If newY + window.h > window.parent.h Then newY = window.parent.h - window.h
			If newX < 0 Then newX = 0
			If newY < 0 Then newY = 0
		End
		window.SetLocation(newX, newY)
	End
End

Class WindowButtonPanelButton Extends Button
	Const CLOSE_BUTTON% = 0
	Const MAXIMISE_BUTTON% = 1
	Const MINIMISE_BUTTON% = 2
	Const SHADE_BUTTON% = 3
	Field buttonType%

	Method New(parent:Component, buttonType%)
		Super.New(parent)
		Self.buttonType = buttonType
	End

	Method ButtonClicked:Void()
		Local window:Window = Window(parent)
		AssertNotNull(window, "WindowButtonPanelButton.ButtonClicked: window was null")
		Select buttonType
			Case CLOSE_BUTTON
				window.Dispose()
		End
	End
End

Class Label Extends Component
	Field text:String
	Field textRed%, textGreen%, textBlue%
	Field textXOffset# = 0
	Field textYOffset# = 0
	Field textXAlign# = 0
	Field textYAlign# = 0

	Method New(parent:Component)
		Super.New(parent)
	End

	Method DrawComponent:Void()
		Super.DrawComponent()
		' TODO: draw text
	End
End

Class Button Extends Label
Private
	Field styleSelected:ComponentStyle = Null

Public
	Field selected:Bool
	Field toggle:Bool
	Field radioGroup:RadioGroup
	Field radioValue$

	Method New(parent:Component)
		Super.New(parent)
		Self.forwardAction = FindActionTarget()
		mouseAdapter = New ButtonMouseAdapter(Self)
	End

	Method New(parent:Component, forwardAction:Component)
		Super.New(parent)
		Self.forwardAction = forwardAction
		mouseAdapter = New ButtonMouseAdapter(Self)
	End

	Method StyleSelected:ComponentStyle() Property
		If styleSelected = Null Then styleSelected = New ComponentStyle
		Return styleSelected
	End
	Method StyleSelected:Void(style:ComponentStyle) Property
		styleSelected = style
	End

	Method GetCurrentStyle:ComponentStyle()
		If selected And styleSelected <> Null Then Return styleSelected
		Return Super.GetCurrentStyle()
	End
End

Class ButtonMouseAdapter Extends AbstractMouseAdapter
	Field button:Button
	Method New(button:Button)
		Self.button = button
	End
	Method MouseClicked:Void(x#, y#, button%)
		' is it a radio button?
		If Self.button.radioGroup <> Null Then
			Self.button.radioGroup.SelectButton(Self.button)
		' is it a toggle button?
		ElseIf Self.button.toggle Then
			Self.button.selected = Not Self.button.selected
		End
		Self.button.ActionPerformed(Self.button, ACTION_CLICKED)
	End
End

Class RadioGroup
	Field buttons:ArrayList<Button> = New ArrayList<Button>
	Field currentValue$

	Method SelectButton:String(button:Button)
		For Local b:Button = EachIn buttons
			b.selected = (b = button)
			If b.selected Then
				currentValue = b.radioValue
			End
		Next
		Return currentValue
	End

	Method SelectValue:Button(value$)
		Local rv:Button = Null
		For Local b:Button = EachIn buttons
			b.selected = (b.radioValue = value)
			If b.selected Then
				currentValue = value
				rv = b
			End
		Next
		Return rv
	End

	Method AddButton:Void(button:Button, value$)
		button.radioValue = value
		button.radioGroup = Self
		buttons.Add(button)
	End

	Method RemoveButton:Void(button:Button)
		button.radioValue = ""
		button.radioGroup = Null
		buttons.Remove(button)
	End
End

Class Slider Extends Component
	Const SLIDER_HORIZONTAL% = 0
	Const SLIDER_VERTICAL% = 1
	Const SLIDER_DIRECTION_TL_TO_BR% = 0 ' min is top or left, max is bottom or right
	Const SLIDER_DIRECTION_BR_TO_TL% = 1 ' min is bottom or right, max is top or left
Private
	Field buttonUpLeft:Button ' the button used for up and left
	Field buttonDownRight:Button ' the button used for down and right
	Field handle:Label
	Field showButtons:Bool
	Field orientation:Int = SLIDER_HORIZONTAL
	Field direction:Int = SLIDER_DIRECTION_TL_TO_BR

	Field dragX%, dragY%, originalX%, originalY%
	Field dragging:Bool = False

Public
	Field minValue% = 0
	Field maxValue% = 100
	Field value% = 50
	Field tickInterval% = 10
	Field handleMargin% = 10
	Field handleSize% = 10
	Field buttonSize% = 15
	Field snapToTicks:Bool = True

	Method New(parent:Component)
		Super.New(parent)
		buttonUpLeft = New Button(Self, Self)
		buttonDownRight = New Button(Self, Self)
		handle = New Label(Self)
		handle.mouseAdapter = New SliderHandleMouseAdapter(Self)
		handle.mouseMotionAdapter = New SliderHandleMouseMotionAdapter(Self)
		buttonUpLeft.visible = False
		buttonDownRight.visible = False
	End

	Method ShowButtons:Bool() Property
		Return showButtons
	End

	Method ShowButtons:Void(showButtons:Bool) Property
		If showButtons <> Self.showButtons Then
			Self.showButtons = showButtons
			Layout()
		End
		Self.showButtons = showButtons
	End

	Method Orientation:Int() Property
		Return orientation
	End

	Method Orientation:Void(orientation:Int) Property
		If Self.orientation <> orientation Then
			Self.orientation = orientation
			Layout()
		End
		Self.orientation = orientation
	End

	Method Direction:Int() Property
		Return direction
	End

	Method Direction:Void(direction:Int) Property
		If Self.direction <> direction Then
			Self.direction = direction
			Layout()
		End
		Self.direction = direction
	End

	Method DrawComponent:Void()
		Super.DrawComponent()
		' TODO: render groove and ticks, etc.
	End

	' TODO: adjust layout using xml offsets rather than hardcoded
	Method Layout:Void()
		If showButtons Then
			If orientation = SLIDER_HORIZONTAL Then
				buttonUpLeft.SetBounds(0, 0, buttonSize, Self.h)
				buttonDownRight.SetBounds(Self.w - buttonSize, 0, buttonSize, Self.h)
			Else
				buttonUpLeft.SetBounds(0, 0, Self.w, buttonSize)
				buttonDownRight.SetBounds(0, Self.h - buttonSize, Self.w, buttonSize)
			End
			buttonUpLeft.visible = True
			buttonDownRight.visible = True
		Else
			buttonUpLeft.visible = False
			buttonDownRight.visible = False
		End
		Local startVal% = buttonSize+handleMargin
		Local endVal% = -buttonSize-handleMargin
		Local fraction# = Float(value-minValue)/Float(maxValue-minValue)
		Local currentVal%
		If orientation = SLIDER_HORIZONTAL Then
			endVal += Self.w
			currentVal = startVal + (endVal - startVal) * fraction
			handle.SetBounds(currentVal-handleSize/2, 0, handleSize, Self.h)
		Else
			endVal += Self.h
			currentVal = startVal + (endVal - startVal) * fraction
			handle.SetBounds(0, currentVal-handleSize/2, Self.w, handleSize)
		End
	End

	Method HandleDrag:Int(mx%, my%)
		Local pos%, topLeft% = handleMargin, bottomRight% = -handleMargin
		If showButtons Then
			topLeft += buttonSize
			bottomRight -= buttonSize
		End
		If orientation = SLIDER_HORIZONTAL Then
			bottomRight += w
			pos = Min(Max(topLeft, mx), bottomRight)
		Else
			bottomRight += h
			pos = Min(Max(topLeft, my), bottomRight)
		End
		Local fraction# = Float(pos-topLeft) / Float(bottomRight-topLeft)
		If direction = SLIDER_DIRECTION_BR_TO_TL Then fraction = 1-fraction

		Local oldValue% = value
		value = SnapToValue(minValue + (maxValue - minValue)*fraction)

		' if it changed, update the layout and fire an event
		If value <> oldValue Then
			Local target:Component = forwardAction
			If target = Null Then target = FindActionTarget()
			If target <> Null Then
				Layout()
				target.ActionPerformed(Self, ACTION_VALUE_CHANGED)
			EndIf
		End
	End

	Method ActionPerformed:Void(source:Component, action:String)
		If source = buttonUpLeft And action = ACTION_CLICKED Then
			If direction = SLIDER_DIRECTION_TL_TO_BR Then
				AdjustValue(-1)
			Else
				AdjustValue(1)
			End
		ElseIf source = buttonDownRight And action = ACTION_CLICKED Then
			If direction = SLIDER_DIRECTION_TL_TO_BR Then
				AdjustValue(1)
			Else
				AdjustValue(-1)
			End
		End
	End

	Method SnapToValue:Int(val%)
		If val < minValue Then
			val = minValue
			Return val
		End
		If val > maxValue Then
			val = maxValue
			Return val
		End
		If val Mod tickInterval = 0 Then Return val
		If val Mod tickInterval < tickInterval / 2 Then
			val -= val Mod tickInterval
		Else
			val -= val Mod tickInterval
			val += tickInterval
		End
		Return val
	End

	Method AdjustValue:Bool(amount%)
		If amount = 0 Then Return
		Local oldValue% = value

		' snap if we must
		If value Mod tickInterval > 0 Then
			If amount < 0 Then
				value += tickInterval - (value Mod tickInterval)
			Else
				value -= value Mod tickInterval
			End
		End

		' adjust it
		value += amount * tickInterval

		' check that it's in range
		If value < minValue Then value = minValue
		If value > maxValue Then value = maxValue

		' if it changed, update the layout and fire an event
		If value <> oldValue Then
			Local target:Component = forwardAction
			If target = Null Then target = FindActionTarget()
			If target <> Null Then
				Layout()
				target.ActionPerformed(Self, ACTION_VALUE_CHANGED)
			EndIf
		End
		Return value <> oldValue
	End
End

Class SliderHandleMouseAdapter Extends AbstractMouseAdapter
	Field slider:Slider
	Method New(slider:Slider)
		Self.slider = slider
	End
	Method MousePressed:Void(x#, y#, button%)
		If slider.dragging Then Return
		slider.dragging = True
		slider.HandleDrag(slider.handle.x + x, slider.handle.y + y)
	End
	Method MouseReleased:Void(x#, y#, button%)
		slider.dragging = False
		slider.HandleDrag(slider.handle.x + x, slider.handle.y + y)
	End
End

Class SliderHandleMouseMotionAdapter Extends AbstractMouseMotionAdapter
	Field slider:Slider
	Method New(slider:Slider)
		Self.slider = slider
	End
	Method MouseDragged:Void(x#, y#, button%)
		If Not slider.dragging Then Return
		slider.HandleDrag(slider.handle.x + x, slider.handle.y + y)
	End
End