浏览代码

Initial checkin

Brian Fiete 6 年之前
父节点
当前提交
078564ac9e
共有 100 个文件被更改,包括 27937 次插入0 次删除
  1. 411 0
      Beef.sln
  2. 93 0
      BeefBoot/BeefBoot.cpp
  3. 180 0
      BeefBoot/BeefBoot.h
  4. 177 0
      BeefBoot/BeefBoot.vcxproj
  5. 33 0
      BeefBoot/BeefBoot.vcxproj.filters
  6. 841 0
      BeefBoot/BootApp.cpp
  7. 80 0
      BeefBoot/BootApp.h
  8. 122 0
      BeefBoot/CMakeLists.txt
  9. 34 0
      BeefBuild/BeefProj.toml
  10. 92 0
      BeefBuild/BeefSpace.toml
  11. 250 0
      BeefBuild/src/BuildApp.bf
  12. 147 0
      BeefBuild/src/Program.bf
  13. 36 0
      BeefLibs/Beefy2D/BeefProj.toml
  14. 787 0
      BeefLibs/Beefy2D/src/BFApp.bf
  15. 772 0
      BeefLibs/Beefy2D/src/BFWindow.bf
  16. 32 0
      BeefLibs/Beefy2D/src/Rand.bf
  17. 447 0
      BeefLibs/Beefy2D/src/Utils.bf
  18. 14 0
      BeefLibs/Beefy2D/src/events/DialogEvent.bf
  19. 15 0
      BeefLibs/Beefy2D/src/events/DragEvent.bf
  20. 11 0
      BeefLibs/Beefy2D/src/events/EditEvent.bf
  21. 12 0
      BeefLibs/Beefy2D/src/events/Event.bf
  22. 38 0
      BeefLibs/Beefy2D/src/events/KeyboardEvent.bf
  23. 22 0
      BeefLibs/Beefy2D/src/events/MouseEvent.bf
  24. 13 0
      BeefLibs/Beefy2D/src/events/ScrollEvent.bf
  25. 9 0
      BeefLibs/Beefy2D/src/events/WidgetParentEvent.bf
  26. 493 0
      BeefLibs/Beefy2D/src/geom/Matrix4.bf
  27. 18 0
      BeefLibs/Beefy2D/src/geom/Point.bf
  28. 775 0
      BeefLibs/Beefy2D/src/geom/Quaternion.bf
  29. 197 0
      BeefLibs/Beefy2D/src/geom/Rect.bf
  30. 87 0
      BeefLibs/Beefy2D/src/geom/Vector2.bf
  31. 247 0
      BeefLibs/Beefy2D/src/geom/Vector3.bf
  32. 158 0
      BeefLibs/Beefy2D/src/gfx/Color.bf
  33. 103 0
      BeefLibs/Beefy2D/src/gfx/ConstantDataDefinition.bf
  34. 19 0
      BeefLibs/Beefy2D/src/gfx/ConstantDataMemberAttribute.bf
  35. 25 0
      BeefLibs/Beefy2D/src/gfx/DefaultVertex.bf
  36. 88 0
      BeefLibs/Beefy2D/src/gfx/DrawLayer.bf
  37. 881 0
      BeefLibs/Beefy2D/src/gfx/Font.bf
  38. 1223 0
      BeefLibs/Beefy2D/src/gfx/Graphics.bf
  39. 11 0
      BeefLibs/Beefy2D/src/gfx/IDrawable.bf
  40. 283 0
      BeefLibs/Beefy2D/src/gfx/Image.bf
  41. 205 0
      BeefLibs/Beefy2D/src/gfx/Matrix.bf
  42. 391 0
      BeefLibs/Beefy2D/src/gfx/Model.bf
  43. 14 0
      BeefLibs/Beefy2D/src/gfx/PixelSnapping.bf
  44. 23 0
      BeefLibs/Beefy2D/src/gfx/RenderCmd.bf
  45. 175 0
      BeefLibs/Beefy2D/src/gfx/RenderState.bf
  46. 83 0
      BeefLibs/Beefy2D/src/gfx/Shader.bf
  47. 21 0
      BeefLibs/Beefy2D/src/gfx/TexCoords.bf
  48. 29 0
      BeefLibs/Beefy2D/src/gfx/Vertex3D.bf
  49. 166 0
      BeefLibs/Beefy2D/src/gfx/VertexDefinition.bf
  50. 25 0
      BeefLibs/Beefy2D/src/gfx/VertexElementUsage.bf
  51. 11 0
      BeefLibs/Beefy2D/src/gfx/VertexLayout.bf
  52. 20 0
      BeefLibs/Beefy2D/src/gfx/VertexMemberAttribute.bf
  53. 206 0
      BeefLibs/Beefy2D/src/res/PSDReader.bf
  54. 184 0
      BeefLibs/Beefy2D/src/res/ResourceManager.bf
  55. 22 0
      BeefLibs/Beefy2D/src/res/SoundBank.bf
  56. 24 0
      BeefLibs/Beefy2D/src/res/SoundEvent.bf
  57. 11 0
      BeefLibs/Beefy2D/src/res/SoundGameObject.bf
  58. 21 0
      BeefLibs/Beefy2D/src/res/SoundParameter.bf
  59. 11 0
      BeefLibs/Beefy2D/src/sys/SysBitmap.bf
  60. 150 0
      BeefLibs/Beefy2D/src/sys/SysMenu.bf
  61. 99 0
      BeefLibs/Beefy2D/src/theme/ThemeFactory.bf
  62. 114 0
      BeefLibs/Beefy2D/src/theme/dark/DarkButton.bf
  63. 176 0
      BeefLibs/Beefy2D/src/theme/dark/DarkCheckBox.bf
  64. 375 0
      BeefLibs/Beefy2D/src/theme/dark/DarkComboBox.bf
  65. 125 0
      BeefLibs/Beefy2D/src/theme/dark/DarkDialog.bf
  66. 137 0
      BeefLibs/Beefy2D/src/theme/dark/DarkDockingFrame.bf
  67. 922 0
      BeefLibs/Beefy2D/src/theme/dark/DarkEditWidget.bf
  68. 21 0
      BeefLibs/Beefy2D/src/theme/dark/DarkIconButton.bf
  69. 160 0
      BeefLibs/Beefy2D/src/theme/dark/DarkInfiniteScrollbar.bf
  70. 1132 0
      BeefLibs/Beefy2D/src/theme/dark/DarkListView.bf
  71. 408 0
      BeefLibs/Beefy2D/src/theme/dark/DarkMenu.bf
  72. 192 0
      BeefLibs/Beefy2D/src/theme/dark/DarkScrollbar.bf
  73. 194 0
      BeefLibs/Beefy2D/src/theme/dark/DarkSmartEdit.bf
  74. 755 0
      BeefLibs/Beefy2D/src/theme/dark/DarkTabbedView.bf
  75. 530 0
      BeefLibs/Beefy2D/src/theme/dark/DarkTheme.bf
  76. 398 0
      BeefLibs/Beefy2D/src/theme/dark/DarkTooltip.bf
  77. 161 0
      BeefLibs/Beefy2D/src/theme/dark/DarkVirtualListViewItem.bf
  78. 105 0
      BeefLibs/Beefy2D/src/utils/BeefPerf.bf
  79. 28 0
      BeefLibs/Beefy2D/src/utils/DisposeProxy.bf
  80. 12 0
      BeefLibs/Beefy2D/src/utils/ISerializable.bf
  81. 899 0
      BeefLibs/Beefy2D/src/utils/IdSpan.bf
  82. 17 0
      BeefLibs/Beefy2D/src/utils/ManualBreak.bf
  83. 79 0
      BeefLibs/Beefy2D/src/utils/PerfTimer.bf
  84. 74 0
      BeefLibs/Beefy2D/src/utils/SmoothValue.bf
  85. 2552 0
      BeefLibs/Beefy2D/src/utils/StructuredData.bf
  86. 161 0
      BeefLibs/Beefy2D/src/utils/TextSearcher.bf
  87. 311 0
      BeefLibs/Beefy2D/src/utils/UndoManager.bf
  88. 41 0
      BeefLibs/Beefy2D/src/widgets/ButtonWidget.bf
  89. 49 0
      BeefLibs/Beefy2D/src/widgets/Checkbox.bf
  90. 11 0
      BeefLibs/Beefy2D/src/widgets/ComboBox.bf
  91. 1160 0
      BeefLibs/Beefy2D/src/widgets/Composition.bf
  92. 15 0
      BeefLibs/Beefy2D/src/widgets/DesignEditableAttribute.bf
  93. 383 0
      BeefLibs/Beefy2D/src/widgets/Dialog.bf
  94. 185 0
      BeefLibs/Beefy2D/src/widgets/DockedWidget.bf
  95. 989 0
      BeefLibs/Beefy2D/src/widgets/DockingFrame.bf
  96. 185 0
      BeefLibs/Beefy2D/src/widgets/DragHelper.bf
  97. 3425 0
      BeefLibs/Beefy2D/src/widgets/EditWidget.bf
  98. 19 0
      BeefLibs/Beefy2D/src/widgets/IMenu.bf
  99. 24 0
      BeefLibs/Beefy2D/src/widgets/ImageWidget.bf
  100. 246 0
      BeefLibs/Beefy2D/src/widgets/InfiniteScrollbar.bf

+ 411 - 0
Beef.sln

@@ -0,0 +1,411 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.27205.2004
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BeefySysLib", "BeefySysLib\BeefySysLib.vcxproj", "{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7340A6F8-D716-4DD6-93AC-F5056D850D06}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "IDEHelper", "IDEHelper\IDEHelper.vcxproj", "{F8D29C38-D37C-4AF2-8540-2F6E543264F1}"
+	ProjectSection(ProjectDependencies) = postProject
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D} = {ECEAB68D-2F15-495F-A29C-5EA9548AA23D}
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B} = {53609BB3-D874-465C-AF7B-DF626DB0D89B}
+	EndProjectSection
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BeefySysLib_static", "BeefySysLib\BeefySysLib_static.vcxproj", "{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BeefRT", "BeefRT\BeefRT.vcxproj", "{5BE0B958-C13B-4986-A588-97A8F707E42E}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libhunspell", "extern\hunspell\src\win_api\libhunspell.vcxproj", "{53609BB3-D874-465C-AF7B-DF626DB0D89B}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Debugger32", "Debugger32\Debugger32.vcxproj", "{29025CF2-07CC-4C25-A672-6324C95AA28E}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Debugger64", "Debugger64\Debugger64.vcxproj", "{29025CF2-07CC-4C25-A672-6324C95AA28F}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BeefLink", "BeefLink\BeefLink.vcxproj", "{78BE2825-EF6A-4C57-B22D-FF69EB925359}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BeefBoot", "BeefBoot\BeefBoot.vcxproj", "{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BeefDbg", "BeefRT\BeefDbg\BeefDbg.vcxproj", "{8F529049-889A-4FC6-9F4C-FAAFC42C9519}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug Static CStatic|x64 = Debug Static CStatic|x64
+		Debug Static CStatic|x86 = Debug Static CStatic|x86
+		Debug Static|x64 = Debug Static|x64
+		Debug Static|x86 = Debug Static|x86
+		Debug_dll|x64 = Debug_dll|x64
+		Debug_dll|x86 = Debug_dll|x86
+		Debug|x64 = Debug|x64
+		Debug|x86 = Debug|x86
+		Release Static CStatic|x64 = Release Static CStatic|x64
+		Release Static CStatic|x86 = Release Static CStatic|x86
+		Release Static|x64 = Release Static|x64
+		Release Static|x86 = Release Static|x86
+		Release_dll|x64 = Release_dll|x64
+		Release_dll|x86 = Release_dll|x86
+		Release_NoDbgInfo|x64 = Release_NoDbgInfo|x64
+		Release_NoDbgInfo|x86 = Release_NoDbgInfo|x86
+		Release|x64 = Release|x64
+		Release|x86 = Release|x86
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Debug Static CStatic|x64.ActiveCfg = Debug Static|x64
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Debug Static CStatic|x64.Build.0 = Debug Static|x64
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Debug Static CStatic|x86.ActiveCfg = Debug Static|Win32
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Debug Static CStatic|x86.Build.0 = Debug Static|Win32
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Debug Static|x64.ActiveCfg = Debug Static|x64
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Debug Static|x64.Build.0 = Debug Static|x64
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Debug Static|x86.ActiveCfg = Debug Static|Win32
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Debug Static|x86.Build.0 = Debug Static|Win32
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Debug_dll|x64.ActiveCfg = Debug|x64
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Debug_dll|x64.Build.0 = Debug|x64
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Debug_dll|x86.ActiveCfg = Debug|Win32
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Debug_dll|x86.Build.0 = Debug|Win32
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Debug|x64.ActiveCfg = Debug|x64
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Debug|x64.Build.0 = Debug|x64
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Debug|x86.ActiveCfg = Debug|Win32
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Debug|x86.Build.0 = Debug|Win32
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Release Static CStatic|x64.ActiveCfg = Release Static|x64
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Release Static CStatic|x64.Build.0 = Release Static|x64
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Release Static CStatic|x86.ActiveCfg = Release Static|Win32
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Release Static CStatic|x86.Build.0 = Release Static|Win32
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Release Static|x64.ActiveCfg = Release Static|x64
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Release Static|x64.Build.0 = Release Static|x64
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Release Static|x86.ActiveCfg = Release Static|Win32
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Release Static|x86.Build.0 = Release Static|Win32
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Release_dll|x64.ActiveCfg = Release|x64
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Release_dll|x64.Build.0 = Release|x64
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Release_dll|x86.ActiveCfg = Release|Win32
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Release_dll|x86.Build.0 = Release|Win32
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Release_NoDbgInfo|x64.ActiveCfg = Release|x64
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Release_NoDbgInfo|x64.Build.0 = Release|x64
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Release_NoDbgInfo|x86.ActiveCfg = Release|Win32
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Release|x64.ActiveCfg = Release|x64
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Release|x64.Build.0 = Release|x64
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Release|x86.ActiveCfg = Release|Win32
+		{E958BC7A-F2D8-4765-AFD6-85BBF54CD921}.Release|x86.Build.0 = Release|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Debug Static CStatic|x64.ActiveCfg = Debug|x64
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Debug Static CStatic|x64.Build.0 = Debug|x64
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Debug Static CStatic|x86.ActiveCfg = Debug|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Debug Static CStatic|x86.Build.0 = Debug|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Debug Static|x64.ActiveCfg = Debug|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Debug Static|x86.ActiveCfg = Debug|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Debug Static|x86.Build.0 = Debug|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Debug_dll|x64.ActiveCfg = Release|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Debug_dll|x64.Build.0 = Release|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Debug_dll|x86.ActiveCfg = Debug|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Debug_dll|x86.Build.0 = Debug|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Debug|x64.ActiveCfg = Debug|x64
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Debug|x64.Build.0 = Debug|x64
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Debug|x86.ActiveCfg = Debug|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Debug|x86.Build.0 = Debug|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Release Static CStatic|x64.ActiveCfg = Release_NoDbgInfo|x64
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Release Static CStatic|x64.Build.0 = Release_NoDbgInfo|x64
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Release Static CStatic|x86.ActiveCfg = Release_NoDbgInfo|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Release Static CStatic|x86.Build.0 = Release_NoDbgInfo|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Release Static|x64.ActiveCfg = Release|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Release Static|x86.ActiveCfg = Release|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Release Static|x86.Build.0 = Release|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Release_dll|x64.ActiveCfg = Release|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Release_dll|x64.Build.0 = Release|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Release_dll|x86.ActiveCfg = Release|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Release_dll|x86.Build.0 = Release|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Release_NoDbgInfo|x64.ActiveCfg = Release|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Release_NoDbgInfo|x86.ActiveCfg = Release|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Release_NoDbgInfo|x86.Build.0 = Release|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Release|x64.ActiveCfg = Release|x64
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Release|x64.Build.0 = Release|x64
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Release|x86.ActiveCfg = Release|Win32
+		{F8D29C38-D37C-4AF2-8540-2F6E543264F1}.Release|x86.Build.0 = Release|Win32
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Debug Static CStatic|x64.ActiveCfg = Debug|x64
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Debug Static CStatic|x64.Build.0 = Debug|x64
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Debug Static CStatic|x86.ActiveCfg = Debug|Win32
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Debug Static CStatic|x86.Build.0 = Debug|Win32
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Debug Static|x64.ActiveCfg = Debug|x64
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Debug Static|x64.Build.0 = Debug|x64
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Debug Static|x86.ActiveCfg = Debug|Win32
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Debug Static|x86.Build.0 = Debug|Win32
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Debug_dll|x64.ActiveCfg = Debug|x64
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Debug_dll|x64.Build.0 = Debug|x64
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Debug_dll|x86.ActiveCfg = Debug|Win32
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Debug_dll|x86.Build.0 = Debug|Win32
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Debug|x64.ActiveCfg = Debug|x64
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Debug|x64.Build.0 = Debug|x64
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Debug|x86.ActiveCfg = Debug|Win32
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Debug|x86.Build.0 = Debug|Win32
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Release Static CStatic|x64.ActiveCfg = Release|x64
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Release Static CStatic|x64.Build.0 = Release|x64
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Release Static CStatic|x86.ActiveCfg = Release|Win32
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Release Static CStatic|x86.Build.0 = Release|Win32
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Release Static|x64.ActiveCfg = Release|x64
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Release Static|x64.Build.0 = Release|x64
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Release Static|x86.ActiveCfg = Release|Win32
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Release Static|x86.Build.0 = Release|Win32
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Release_dll|x64.ActiveCfg = Release|x64
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Release_dll|x64.Build.0 = Release|x64
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Release_dll|x86.ActiveCfg = Release|Win32
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Release_dll|x86.Build.0 = Release|Win32
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Release_NoDbgInfo|x64.ActiveCfg = Release|x64
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Release_NoDbgInfo|x64.Build.0 = Release|x64
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Release_NoDbgInfo|x86.ActiveCfg = Release|Win32
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Release_NoDbgInfo|x86.Build.0 = Release|Win32
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Release|x64.ActiveCfg = Release|x64
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Release|x64.Build.0 = Release|x64
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Release|x86.ActiveCfg = Release|Win32
+		{ECEAB68D-2F15-495F-A29C-5EA9548AA23D}.Release|x86.Build.0 = Release|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Debug Static CStatic|x64.ActiveCfg = Debug Static CStatic|x64
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Debug Static CStatic|x64.Build.0 = Debug Static CStatic|x64
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Debug Static CStatic|x86.ActiveCfg = Debug Static CStatic|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Debug Static CStatic|x86.Build.0 = Debug Static CStatic|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Debug Static|x64.ActiveCfg = Debug|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Debug Static|x86.ActiveCfg = Debug|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Debug Static|x86.Build.0 = Debug|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Debug_dll|x64.ActiveCfg = Release|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Debug_dll|x64.Build.0 = Release|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Debug_dll|x86.ActiveCfg = Debug|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Debug_dll|x86.Build.0 = Debug|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Debug|x64.ActiveCfg = Debug|x64
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Debug|x64.Build.0 = Debug|x64
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Debug|x86.ActiveCfg = Debug|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Debug|x86.Build.0 = Debug|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Release Static CStatic|x64.ActiveCfg = Release Static CStatic|x64
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Release Static CStatic|x64.Build.0 = Release Static CStatic|x64
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Release Static CStatic|x86.ActiveCfg = Release Static CStatic|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Release Static CStatic|x86.Build.0 = Release Static CStatic|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Release Static|x64.ActiveCfg = Release|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Release Static|x86.ActiveCfg = Release|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Release Static|x86.Build.0 = Release|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Release_dll|x64.ActiveCfg = Release|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Release_dll|x64.Build.0 = Release|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Release_dll|x86.ActiveCfg = Release|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Release_dll|x86.Build.0 = Release|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Release_NoDbgInfo|x64.ActiveCfg = Release|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Release_NoDbgInfo|x86.ActiveCfg = Release|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Release_NoDbgInfo|x86.Build.0 = Release|Win32
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Release|x64.ActiveCfg = Release|x64
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Release|x64.Build.0 = Release|x64
+		{5BE0B958-C13B-4986-A588-97A8F707E42E}.Release|x86.ActiveCfg = Release|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Debug Static CStatic|x64.ActiveCfg = Debug_dll|x64
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Debug Static CStatic|x64.Build.0 = Debug_dll|x64
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Debug Static CStatic|x86.ActiveCfg = Debug_dll|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Debug Static CStatic|x86.Build.0 = Debug_dll|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Debug Static|x64.ActiveCfg = Release|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Debug Static|x64.Build.0 = Release|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Debug Static|x86.ActiveCfg = Debug|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Debug Static|x86.Build.0 = Debug|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Debug_dll|x64.ActiveCfg = Debug_dll|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Debug_dll|x86.ActiveCfg = Debug_dll|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Debug_dll|x86.Build.0 = Debug_dll|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Debug|x64.ActiveCfg = Debug|x64
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Debug|x64.Build.0 = Debug|x64
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Debug|x86.ActiveCfg = Debug|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Debug|x86.Build.0 = Debug|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Release Static CStatic|x64.ActiveCfg = Release_dll|x64
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Release Static CStatic|x64.Build.0 = Release_dll|x64
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Release Static CStatic|x86.ActiveCfg = Release_dll|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Release Static CStatic|x86.Build.0 = Release_dll|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Release Static|x64.ActiveCfg = Release|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Release Static|x64.Build.0 = Release|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Release Static|x86.ActiveCfg = Release|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Release Static|x86.Build.0 = Release|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Release_dll|x64.ActiveCfg = Release_dll|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Release_dll|x86.ActiveCfg = Release_dll|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Release_dll|x86.Build.0 = Release_dll|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Release_NoDbgInfo|x64.ActiveCfg = Release|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Release_NoDbgInfo|x86.ActiveCfg = Release|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Release_NoDbgInfo|x86.Build.0 = Release|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Release|x64.ActiveCfg = Release|x64
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Release|x64.Build.0 = Release|x64
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Release|x86.ActiveCfg = Release|Win32
+		{53609BB3-D874-465C-AF7B-DF626DB0D89B}.Release|x86.Build.0 = Release|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Debug Static CStatic|x64.ActiveCfg = Debug|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Debug Static CStatic|x64.Build.0 = Debug|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Debug Static CStatic|x86.ActiveCfg = Debug|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Debug Static CStatic|x86.Build.0 = Debug|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Debug Static|x64.ActiveCfg = Debug|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Debug Static|x64.Build.0 = Debug|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Debug Static|x86.ActiveCfg = Debug|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Debug Static|x86.Build.0 = Debug|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Debug_dll|x64.ActiveCfg = Debug|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Debug_dll|x64.Build.0 = Debug|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Debug_dll|x86.ActiveCfg = Debug|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Debug_dll|x86.Build.0 = Debug|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Debug|x64.ActiveCfg = Debug|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Debug|x64.Build.0 = Debug|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Debug|x86.ActiveCfg = Debug|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Debug|x86.Build.0 = Debug|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Release Static CStatic|x64.ActiveCfg = Release|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Release Static CStatic|x64.Build.0 = Release|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Release Static CStatic|x86.ActiveCfg = Release|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Release Static CStatic|x86.Build.0 = Release|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Release Static|x64.ActiveCfg = Release|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Release Static|x64.Build.0 = Release|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Release Static|x86.ActiveCfg = Release|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Release Static|x86.Build.0 = Release|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Release_dll|x64.ActiveCfg = Release|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Release_dll|x64.Build.0 = Release|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Release_dll|x86.ActiveCfg = Release|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Release_dll|x86.Build.0 = Release|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Release_NoDbgInfo|x64.ActiveCfg = Release|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Release_NoDbgInfo|x64.Build.0 = Release|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Release_NoDbgInfo|x86.ActiveCfg = Release|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Release_NoDbgInfo|x86.Build.0 = Release|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Release|x64.ActiveCfg = Release|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Release|x64.Build.0 = Release|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Release|x86.ActiveCfg = Release|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28E}.Release|x86.Build.0 = Release|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Debug Static CStatic|x64.ActiveCfg = Debug|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Debug Static CStatic|x64.Build.0 = Debug|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Debug Static CStatic|x86.ActiveCfg = Debug|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Debug Static CStatic|x86.Build.0 = Debug|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Debug Static|x64.ActiveCfg = Debug|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Debug Static|x64.Build.0 = Debug|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Debug Static|x86.ActiveCfg = Debug|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Debug Static|x86.Build.0 = Debug|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Debug_dll|x64.ActiveCfg = Debug|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Debug_dll|x64.Build.0 = Debug|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Debug_dll|x86.ActiveCfg = Debug|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Debug_dll|x86.Build.0 = Debug|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Debug|x64.ActiveCfg = Debug|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Debug|x64.Build.0 = Debug|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Debug|x86.ActiveCfg = Debug|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Debug|x86.Build.0 = Debug|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Release Static CStatic|x64.ActiveCfg = Release|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Release Static CStatic|x64.Build.0 = Release|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Release Static CStatic|x86.ActiveCfg = Release|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Release Static CStatic|x86.Build.0 = Release|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Release Static|x64.ActiveCfg = Release|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Release Static|x64.Build.0 = Release|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Release Static|x86.ActiveCfg = Release|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Release Static|x86.Build.0 = Release|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Release_dll|x64.ActiveCfg = Release|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Release_dll|x64.Build.0 = Release|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Release_dll|x86.ActiveCfg = Release|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Release_dll|x86.Build.0 = Release|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Release_NoDbgInfo|x64.ActiveCfg = Release|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Release_NoDbgInfo|x64.Build.0 = Release|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Release_NoDbgInfo|x86.ActiveCfg = Release|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Release_NoDbgInfo|x86.Build.0 = Release|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Release|x64.ActiveCfg = Release|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Release|x64.Build.0 = Release|x64
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Release|x86.ActiveCfg = Release|Win32
+		{29025CF2-07CC-4C25-A672-6324C95AA28F}.Release|x86.Build.0 = Release|Win32
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Debug Static CStatic|x64.ActiveCfg = Debug|x64
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Debug Static CStatic|x64.Build.0 = Debug|x64
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Debug Static CStatic|x86.ActiveCfg = Debug|Win32
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Debug Static CStatic|x86.Build.0 = Debug|Win32
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Debug Static|x64.ActiveCfg = Debug|x64
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Debug Static|x64.Build.0 = Debug|x64
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Debug Static|x86.ActiveCfg = Debug|Win32
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Debug Static|x86.Build.0 = Debug|Win32
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Debug_dll|x64.ActiveCfg = Debug|x64
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Debug_dll|x64.Build.0 = Debug|x64
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Debug_dll|x86.ActiveCfg = Debug|Win32
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Debug_dll|x86.Build.0 = Debug|Win32
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Debug|x64.ActiveCfg = Debug|x64
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Debug|x64.Build.0 = Debug|x64
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Debug|x86.ActiveCfg = Debug|Win32
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Debug|x86.Build.0 = Debug|Win32
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Release Static CStatic|x64.ActiveCfg = Release|x64
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Release Static CStatic|x64.Build.0 = Release|x64
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Release Static CStatic|x86.ActiveCfg = Release|Win32
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Release Static CStatic|x86.Build.0 = Release|Win32
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Release Static|x64.ActiveCfg = Release|x64
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Release Static|x64.Build.0 = Release|x64
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Release Static|x86.ActiveCfg = Release|Win32
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Release Static|x86.Build.0 = Release|Win32
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Release_dll|x64.ActiveCfg = Release|x64
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Release_dll|x64.Build.0 = Release|x64
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Release_dll|x86.ActiveCfg = Release|Win32
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Release_dll|x86.Build.0 = Release|Win32
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Release_NoDbgInfo|x64.ActiveCfg = Release|x64
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Release_NoDbgInfo|x64.Build.0 = Release|x64
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Release_NoDbgInfo|x86.ActiveCfg = Release|Win32
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Release_NoDbgInfo|x86.Build.0 = Release|Win32
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Release|x64.ActiveCfg = Release|x64
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Release|x64.Build.0 = Release|x64
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Release|x86.ActiveCfg = Release|Win32
+		{78BE2825-EF6A-4C57-B22D-FF69EB925359}.Release|x86.Build.0 = Release|Win32
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Debug Static CStatic|x64.ActiveCfg = Debug|x64
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Debug Static CStatic|x64.Build.0 = Debug|x64
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Debug Static CStatic|x86.ActiveCfg = Debug|Win32
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Debug Static CStatic|x86.Build.0 = Debug|Win32
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Debug Static|x64.ActiveCfg = Debug|x64
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Debug Static|x64.Build.0 = Debug|x64
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Debug Static|x86.ActiveCfg = Debug|Win32
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Debug Static|x86.Build.0 = Debug|Win32
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Debug_dll|x64.ActiveCfg = Debug|x64
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Debug_dll|x64.Build.0 = Debug|x64
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Debug_dll|x86.ActiveCfg = Debug|Win32
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Debug_dll|x86.Build.0 = Debug|Win32
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Debug|x64.ActiveCfg = Debug|x64
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Debug|x64.Build.0 = Debug|x64
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Debug|x86.ActiveCfg = Debug|Win32
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Debug|x86.Build.0 = Debug|Win32
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Release Static CStatic|x64.ActiveCfg = Release|x64
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Release Static CStatic|x64.Build.0 = Release|x64
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Release Static CStatic|x86.ActiveCfg = Release|Win32
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Release Static CStatic|x86.Build.0 = Release|Win32
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Release Static|x64.ActiveCfg = Release|x64
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Release Static|x64.Build.0 = Release|x64
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Release Static|x86.ActiveCfg = Release|Win32
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Release Static|x86.Build.0 = Release|Win32
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Release_dll|x64.ActiveCfg = Release|x64
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Release_dll|x64.Build.0 = Release|x64
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Release_dll|x86.ActiveCfg = Release|Win32
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Release_dll|x86.Build.0 = Release|Win32
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Release_NoDbgInfo|x64.ActiveCfg = Release|x64
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Release_NoDbgInfo|x64.Build.0 = Release|x64
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Release_NoDbgInfo|x86.ActiveCfg = Release|Win32
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Release_NoDbgInfo|x86.Build.0 = Release|Win32
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Release|x64.ActiveCfg = Release|x64
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Release|x64.Build.0 = Release|x64
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Release|x86.ActiveCfg = Release|Win32
+		{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}.Release|x86.Build.0 = Release|Win32
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Debug Static CStatic|x64.ActiveCfg = Debug|x64
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Debug Static CStatic|x64.Build.0 = Debug|x64
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Debug Static CStatic|x86.ActiveCfg = Debug|Win32
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Debug Static CStatic|x86.Build.0 = Debug|Win32
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Debug Static|x64.ActiveCfg = Debug|x64
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Debug Static|x64.Build.0 = Debug|x64
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Debug Static|x86.ActiveCfg = Debug|Win32
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Debug Static|x86.Build.0 = Debug|Win32
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Debug_dll|x64.ActiveCfg = Debug|x64
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Debug_dll|x64.Build.0 = Debug|x64
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Debug_dll|x86.ActiveCfg = Debug|Win32
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Debug_dll|x86.Build.0 = Debug|Win32
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Debug|x64.ActiveCfg = Debug|x64
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Debug|x64.Build.0 = Debug|x64
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Debug|x86.ActiveCfg = Debug|Win32
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Debug|x86.Build.0 = Debug|Win32
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Release Static CStatic|x64.ActiveCfg = Release|x64
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Release Static CStatic|x64.Build.0 = Release|x64
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Release Static CStatic|x86.ActiveCfg = Release|Win32
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Release Static CStatic|x86.Build.0 = Release|Win32
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Release Static|x64.ActiveCfg = Release|x64
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Release Static|x64.Build.0 = Release|x64
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Release Static|x86.ActiveCfg = Release|Win32
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Release Static|x86.Build.0 = Release|Win32
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Release_dll|x64.ActiveCfg = Release|x64
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Release_dll|x64.Build.0 = Release|x64
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Release_dll|x86.ActiveCfg = Release|Win32
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Release_dll|x86.Build.0 = Release|Win32
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Release_NoDbgInfo|x64.ActiveCfg = Release|x64
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Release_NoDbgInfo|x64.Build.0 = Release|x64
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Release_NoDbgInfo|x86.ActiveCfg = Release|Win32
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Release_NoDbgInfo|x86.Build.0 = Release|Win32
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Release|x64.ActiveCfg = Release|x64
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Release|x64.Build.0 = Release|x64
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Release|x86.ActiveCfg = Release|Win32
+		{8F529049-889A-4FC6-9F4C-FAAFC42C9519}.Release|x86.Build.0 = Release|Win32
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		VD_Solution = <?xml version="1.0" encoding="utf-16"?>|§r|§n<DocumentsSolution xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">|§r|§n  <m_persistedPinnedDocumentGroupLabels>|§r|§n    <string>X86</string>|§r|§n  </m_persistedPinnedDocumentGroupLabels>|§r|§n</DocumentsSolution>
+		SolutionGuid = {460361DC-9F40-4AE7-B647-82D5D69B70DC}
+	EndGlobalSection
+EndGlobal

+ 93 - 0
BeefBoot/BeefBoot.cpp

@@ -0,0 +1,93 @@
+//#define USE_OLD_BEEFBUILD
+
+#pragma warning(disable:4996)
+
+#include <iostream>
+#include "BeefySysLib/Common.h"
+#include "BeefySysLib/util/Array.h"
+#include "BeefySysLib/util/SizedArray.h"
+#include "BeefySysLib/util/Dictionary.h"
+#include "BeefySysLib/util/CabUtil.h"
+#include "BeefySysLib/util/BeefPerf.h"
+#include "BeefySysLib/util/Deque.h"
+#include "BeefySysLib/util/HashSet.h"
+#include "BeefySysLib/util/MultiHashSet.h"
+
+//#include <mmsystem.h>
+//#include <shellapi.h>
+//#include <Objbase.h>
+
+#define BF_DBG_64
+#include "IDEHelper/StrHashMap.h"
+
+using namespace Beefy;
+
+#include "BootApp.h"
+
+BF_IMPORT void BF_CALLTYPE Debugger_ProgramDone();
+
+int main(int argc, char* argv[])
+{	
+	BfpSystem_SetCommandLine(argc, argv);
+
+	BfpThread_SetName(NULL, "MainThread", NULL);
+
+    BfpSystem_Init(BFP_VERSION, BfpSystemInitFlag_InstallCrashCatcher);
+	
+	gApp = new BootApp();
+
+	bool success = true;
+	for (int i = 1; i < argc; i++)
+	{			
+		std::string arg = argv[i];
+				
+		if (arg[0] == '"')
+		{
+			arg.erase(0, 1);
+			if ((arg.length() > 1) && (arg[arg.length() - 1] == '"'))
+				arg.erase(arg.length() - 1);
+			success &= gApp->HandleCmdLine(arg, "");
+			continue;
+		}
+		
+		int eqPos = (int)arg.find('=');
+		if (eqPos == -1)
+		{
+			success &= gApp->HandleCmdLine(arg, "");
+			continue;
+		}
+
+		std::string cmd = arg.substr(0, eqPos);
+		std::string param = arg.substr(eqPos + 1);
+		if ((param.length() > 1) && (param[0] == '"'))
+		{
+			param.erase(0, 1);
+			if ((param.length() > 1) && (param[param.length() - 1] == '"'))
+				param.erase(param.length() - 1);
+		}
+		success &= gApp->HandleCmdLine(cmd, param);		
+	}
+	
+	if (!gApp->mShowedHelp)
+	{
+		if (success)
+			success = gApp->Init();
+		if (success)
+			success = gApp->Compile();
+
+		if (success)
+			gApp->OutputLine("SUCCESS", OutputPri_Critical);
+		else
+			gApp->OutputLine("FAILED", OutputPri_Critical);
+	}
+
+	delete gApp;		
+
+	Debugger_ProgramDone();
+
+    BfpSystem_Shutdown();
+
+	BP_SHUTDOWN();
+
+	return success ? 0 : 1;
+}

+ 180 - 0
BeefBoot/BeefBoot.h

@@ -0,0 +1,180 @@
+#pragma once
+
+#include "BeefySysLib/Common.h"
+#include "BeefySysLib/util/Array.h"
+#include "BeefySysLib/util/String.h"
+#include <set>
+//#include <direct.h>
+
+#pragma warning(disable:4996)
+
+NS_BF_BEGIN
+
+#define APPEND2(VAL1, VAL2) VAL1##VAL2
+#define APPEND(VAL1, VAL2) APPEND2(VAL1, VAL2)
+#define ENUM_VAL_GENERATE(ENUM_ENTRY) APPEND(ENUM_TYPE, _##ENUM_ENTRY),
+#define ENUM_NAME_GENERATE(ENUM_ENTRY) #ENUM_ENTRY,
+#define ENUM_CREATE_DO2(EnumName) \
+	static const char* EnumName##_Names[] = { ENUM_DECLARE(ENUM_NAME_GENERATE) }; \
+	enum EnumName { ENUM_DECLARE(ENUM_VAL_GENERATE) }; \
+	static bool EnumParse(const String& name, EnumName& result) \
+	{ \
+		for (int i = 0; i < sizeof(EnumName##_Names)/sizeof(const char*); i++) \
+			if (name == EnumName##_Names[i]) { result = (EnumName)i; return true; } \
+		return false; \
+	} \
+	static const char* EnumToString(EnumName enumVal) \
+	{ \
+		return EnumName##_Names[(int)enumVal]; \
+	}
+#define ENUM_CREATE_DO(EnumType) ENUM_CREATE_DO2(EnumType)
+#define ENUM_CREATE ENUM_CREATE_DO(ENUM_TYPE)
+
+class IDEUtils
+{
+public:
+	static bool FixFilePath(String& filePath)
+	{
+		if (filePath.length() == 0)
+			return false;
+
+		if (filePath[0] == '<')
+			return false;
+
+		if ((filePath.length() > 1) && (filePath[1] == ':'))
+			filePath[0] = tolower(filePath[0]);
+
+		bool prevWasSlash = false;
+		for (int i = 0; i < filePath.length(); i++)
+		{
+			//if ((filePath[i] == '/') && (filePath[i - 1])
+
+			if (filePath[i] == DIR_SEP_CHAR_ALT)
+				filePath[i] = DIR_SEP_CHAR;
+
+			if (filePath[i] == DIR_SEP_CHAR)
+			{
+				if ((prevWasSlash) && (i > 1))
+				{
+					filePath.Remove(i, 1);
+					i--;
+					continue;
+				}
+
+				prevWasSlash = true;
+			}
+			else
+				prevWasSlash = false;
+
+			if ((i >= 4) && (filePath[i - 3] == DIR_SEP_CHAR) && (filePath[i - 2] == '.') && (filePath[i - 1] == '.') && (filePath[i] == DIR_SEP_CHAR))
+			{
+				int prevSlash = (int)filePath.LastIndexOf(DIR_SEP_CHAR, i - 4);
+				if (prevSlash != -1)
+				{
+					filePath.Remove(prevSlash, i - prevSlash);
+					i = prevSlash;
+				}
+			}
+		}
+		return true;
+	}
+	
+	static void GetDirWithSlash(String& dirName)
+	{
+		if (dirName.length() == 0)
+			return;
+		char lastC = dirName[dirName.length() - 1];
+		if ((lastC != '\\') && (lastC != '/'))
+			dirName += DIR_SEP_CHAR;
+	}
+
+	static FILE* CreateFileWithDir(const String& fileName, const char* options)
+	{
+		FILE* fp = fopen(fileName.c_str(), options);
+		if (fp == NULL)
+		{
+			String fileDir = GetFileDir(fileName);
+			if (!fileDir.empty())
+			{
+				RecursiveCreateDirectory(fileDir);
+				fp = fopen(fileName.c_str(), "w");
+			}
+		}
+		return fp;
+	}
+
+	static bool WriteAllText(const String& fileName, const String& data)
+	{
+		FILE* fp = CreateFileWithDir(fileName, "w");
+		if (fp == NULL)
+			return false;
+		fwrite(data.c_str(), 1, data.length(), fp);
+		fclose(fp);
+		return true;
+	}
+
+	static void GetFileNameWithoutExtension(const String& filePath, String& outFileName)
+	{
+		outFileName += GetFileName(filePath);
+		int dot = (int)outFileName.LastIndexOf('.');
+		if (dot != -1)
+			outFileName.RemoveToEnd(dot);
+	}
+
+	static void GetExtension(const String& filePath, String& ext)
+	{
+		int idx = (int)filePath.LastIndexOf('.');
+		if (idx != -1)
+			ext = filePath.Substring(idx);
+	}
+
+	static void AppendWithOptionalQuotes(String& targetStr, const String& srcFileName)
+	{
+		if ((int)srcFileName.IndexOf(' ') == -1)
+			targetStr += srcFileName;
+		else
+			targetStr += "\"" + srcFileName + "\"";
+	}
+};
+
+class ArgBuilder
+{
+public:
+	String* mTarget;
+	bool mDoLongBreak;
+	int mLastBreak;
+	std::multiset<String> mLinkPaths;
+
+public:
+	ArgBuilder(String& target, bool doLongBreak)
+	{
+		mTarget = &target;
+		mDoLongBreak = doLongBreak;
+		if (mDoLongBreak)
+			mLastBreak = (int)mTarget->LastIndexOf('\n');
+		else
+			mLastBreak = 0;
+	}
+
+	void AddSep()
+	{
+		if (mDoLongBreak)
+		{
+			if (mTarget->length() - mLastBreak > 0x1F000)
+			{
+				mLastBreak = (int)mTarget->length();
+				mTarget->Append('\n');
+				return;
+			}
+		}
+		mTarget->Append(' ');
+	}
+
+	void AddFileName(const String& filePath)
+	{
+		IDEUtils::AppendWithOptionalQuotes(*mTarget, filePath);
+	}
+};
+
+NS_BF_END
+

+ 177 - 0
BeefBoot/BeefBoot.vcxproj

@@ -0,0 +1,177 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup Label="ProjectConfigurations">
+    <ProjectConfiguration Include="Debug|Win32">
+      <Configuration>Debug</Configuration>
+      <Platform>Win32</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Release|Win32">
+      <Configuration>Release</Configuration>
+      <Platform>Win32</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Debug|x64">
+      <Configuration>Debug</Configuration>
+      <Platform>x64</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Release|x64">
+      <Configuration>Release</Configuration>
+      <Platform>x64</Platform>
+    </ProjectConfiguration>
+  </ItemGroup>
+  <PropertyGroup Label="Globals">
+    <ProjectGuid>{755663F3-7C3F-4321-ABFF-CB036C0F2C9F}</ProjectGuid>
+    <Keyword>Win32Proj</Keyword>
+    <RootNamespace>BeefBoot</RootNamespace>
+    <WindowsTargetPlatformVersion>10.0.16299.0</WindowsTargetPlatformVersion>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
+    <ConfigurationType>Application</ConfigurationType>
+    <UseDebugLibraries>true</UseDebugLibraries>
+    <PlatformToolset>v141</PlatformToolset>
+    <CharacterSet>Unicode</CharacterSet>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
+    <ConfigurationType>Application</ConfigurationType>
+    <UseDebugLibraries>false</UseDebugLibraries>
+    <PlatformToolset>v141</PlatformToolset>
+    <WholeProgramOptimization>true</WholeProgramOptimization>
+    <CharacterSet>Unicode</CharacterSet>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
+    <ConfigurationType>Application</ConfigurationType>
+    <UseDebugLibraries>true</UseDebugLibraries>
+    <PlatformToolset>v141</PlatformToolset>
+    <CharacterSet>Unicode</CharacterSet>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
+    <ConfigurationType>Application</ConfigurationType>
+    <UseDebugLibraries>false</UseDebugLibraries>
+    <PlatformToolset>v141</PlatformToolset>
+    <WholeProgramOptimization>true</WholeProgramOptimization>
+    <CharacterSet>Unicode</CharacterSet>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
+  <ImportGroup Label="ExtensionSettings">
+  </ImportGroup>
+  <ImportGroup Label="Shared">
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+  </ImportGroup>
+  <PropertyGroup Label="UserMacros" />
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <LinkIncremental>true</LinkIncremental>
+    <OutDir>$(SolutionDir)\ide\dist\</OutDir>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <LinkIncremental>true</LinkIncremental>
+    <TargetName>$(ProjectName)_d</TargetName>
+    <OutDir>$(SolutionDir)\ide\dist\</OutDir>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <LinkIncremental>false</LinkIncremental>
+    <OutDir>$(SolutionDir)\ide\dist\</OutDir>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <LinkIncremental>false</LinkIncremental>
+    <OutDir>$(SolutionDir)\ide\dist\</OutDir>
+  </PropertyGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <ClCompile>
+      <PrecompiledHeader>
+      </PrecompiledHeader>
+      <WarningLevel>Level3</WarningLevel>
+      <Optimization>Disabled</Optimization>
+      <PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+    </ClCompile>
+    <Link>
+      <SubSystem>Console</SubSystem>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <ClCompile>
+      <PrecompiledHeader>
+      </PrecompiledHeader>
+      <WarningLevel>Level3</WarningLevel>
+      <Optimization>Disabled</Optimization>
+      <PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <AdditionalIncludeDirectories>../;../IDEHelper;../BeefySysLib/platform/win;../BeefySysLib/third_party;..\extern\llvm-project_8_0_0\llvm\include;..\extern\llvm_win64_8_0_0\include;..\extern\llvm-project_8_0_0\llvm\lib\Target;..\extern\llvm_win64_8_0_0\lib\Target\X86;..\extern\llvm-project_8_0_0\llvm\tools\clang\include</AdditionalIncludeDirectories>
+      <RuntimeTypeInfo>true</RuntimeTypeInfo>
+      <SupportJustMyCode>false</SupportJustMyCode>
+    </ClCompile>
+    <Link>
+      <SubSystem>Console</SubSystem>
+      <GenerateDebugInformation>DebugFull</GenerateDebugInformation>
+      <OutputFile>$(SolutionDir)\ide\dist\$(TargetName).exe</OutputFile>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <ClCompile>
+      <WarningLevel>Level3</WarningLevel>
+      <PrecompiledHeader>
+      </PrecompiledHeader>
+      <Optimization>MaxSpeed</Optimization>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+      <IntrinsicFunctions>true</IntrinsicFunctions>
+      <PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+    </ClCompile>
+    <Link>
+      <SubSystem>Console</SubSystem>
+      <EnableCOMDATFolding>true</EnableCOMDATFolding>
+      <OptimizeReferences>true</OptimizeReferences>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <ClCompile>
+      <WarningLevel>Level3</WarningLevel>
+      <PrecompiledHeader>
+      </PrecompiledHeader>
+      <Optimization>MaxSpeed</Optimization>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+      <IntrinsicFunctions>true</IntrinsicFunctions>
+      <PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <RuntimeTypeInfo>true</RuntimeTypeInfo>
+      <AdditionalIncludeDirectories>../;../IDEHelper;../BeefySysLib/platform/win;../BeefySysLib/third_party;..\extern\llvm-project_8_0_0\llvm\include;..\extern\llvm_win64_8_0_0\include;..\extern\llvm-project_8_0_0\llvm\lib\Target;..\extern\llvm_win64_8_0_0\lib\Target\X86;..\extern\llvm\tools\clang\include</AdditionalIncludeDirectories>
+    </ClCompile>
+    <Link>
+      <SubSystem>Console</SubSystem>
+      <EnableCOMDATFolding>true</EnableCOMDATFolding>
+      <OptimizeReferences>true</OptimizeReferences>
+      <GenerateDebugInformation>DebugFull</GenerateDebugInformation>
+      <OutputFile>$(SolutionDir)\ide\dist\$(TargetName).exe</OutputFile>
+      <LinkTimeCodeGeneration>Default</LinkTimeCodeGeneration>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemGroup>
+    <ClCompile Include="BeefBoot.cpp" />
+    <ClCompile Include="BootApp.cpp" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\BeefySysLib\BeefySysLib_static.vcxproj">
+      <Project>{eceab68d-2f15-495f-a29c-5ea9548aa23d}</Project>
+    </ProjectReference>
+    <ProjectReference Include="..\IDEHelper\IDEHelper.vcxproj">
+      <Project>{f8d29c38-d37c-4af2-8540-2f6e543264f1}</Project>
+      <Private>false</Private>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <ClInclude Include="BeefBoot.h" />
+    <ClInclude Include="BootApp.h" />
+  </ItemGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+  <ImportGroup Label="ExtensionTargets">
+  </ImportGroup>
+</Project>

+ 33 - 0
BeefBoot/BeefBoot.vcxproj.filters

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup>
+    <Filter Include="Source Files">
+      <UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
+      <Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
+    </Filter>
+    <Filter Include="Header Files">
+      <UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
+      <Extensions>h;hh;hpp;hxx;hm;inl;inc;xsd</Extensions>
+    </Filter>
+    <Filter Include="Resource Files">
+      <UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
+      <Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
+    </Filter>
+  </ItemGroup>
+  <ItemGroup>
+    <ClCompile Include="BeefBoot.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="BootApp.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+  </ItemGroup>
+  <ItemGroup>
+    <ClInclude Include="BootApp.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="BeefBoot.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+  </ItemGroup>
+</Project>

+ 841 - 0
BeefBoot/BootApp.cpp

@@ -0,0 +1,841 @@
+#pragma warning(disable:4996)
+
+//#define BFBUILD_MAIN_THREAD_COMPILE
+
+#include "BootApp.h"
+#include <iostream>
+#include "BeefySysLib/util/String.h"
+#include "BeefySysLib/util/FileEnumerator.h"
+#include "BeefySysLib/util/WorkThread.h"
+#include "BeefySysLib/platform/PlatformHelper.h"
+#include "Compiler/BfSystem.h"
+
+#ifdef BF_PLATFORM_WINDOWS
+#include <direct.h>
+#endif
+
+BF_IMPORT void BF_CALLTYPE Targets_Create();
+BF_IMPORT void BF_CALLTYPE Targets_Delete();
+
+BF_IMPORT void BF_CALLTYPE BfSystem_ReportMemory(void* bfSystem);
+BF_EXPORT void BF_CALLTYPE BfCompiler_ProgramDone();
+
+BF_IMPORT void BF_CALLTYPE Debugger_FullReportMemory();
+
+//////////////////////////////////////////////////////////////////////////
+
+enum BfCompilerOptionFlags
+{
+	BfCompilerOptionFlag_None = 0,
+	BfCompilerOptionFlag_EmitDebugInfo = 1,
+	BfCompilerOptionFlag_EmitLineInfo = 2,
+	BfCompilerOptionFlag_WriteIR = 4,
+	BfCompilerOptionFlag_GenerateOBJ = 8,
+	BfCompilerOptionFlag_NoFramePointerElim = 0x10,
+	BfCompilerOptionFlag_ClearLocalVars = 0x20,
+	BfCompilerOptionFlag_ArrayBoundsCheck = 0x40,
+	BfCompilerOptionFlag_EmitDynamicCastCheck = 0x80,
+	BfCompilerOptionFlag_EnableObjectDebugFlags = 0x100,
+	BfCompilerOptionFlag_EmitObjectAccessCheck = 0x200,
+	BfCompilerOptionFlag_EnableCustodian = 0x400,
+	BfCompilerOptionFlag_EnableRealtimeLeakCheck = 0x800,
+	BfCompilerOptionFlag_EnableSideStack = 0x1000,
+	BfCompilerOptionFlag_EnableHotSwapping = 0x2000
+};
+
+BF_IMPORT void BF_CALLTYPE BfCompiler_Delete(void* bfCompiler);
+BF_EXPORT void BF_CALLTYPE BfCompiler_SetOptions(void* bfCompiler, void* hotProject, int hotIdx,
+	int machineType, int toolsetType, int simdSetting, int allocStackCount, int maxWorkerThreads,
+	BfCompilerOptionFlags optionFlags, const char* mallocLinkName, const char* freeLinkName);
+BF_IMPORT void BF_CALLTYPE BfCompiler_ClearBuildCache(void* bfCompiler);
+BF_IMPORT bool BF_CALLTYPE BfCompiler_Compile(void* bfCompiler, void* bfPassInstance, const char* outputPath);
+BF_IMPORT float BF_CALLTYPE BfCompiler_GetCompletionPercentage(void* bfCompiler);
+BF_IMPORT const char* BF_CALLTYPE BfCompiler_GetOutputFileNames(void* bfCompiler, void* bfProject, bool* hadOutputChanges);
+BF_IMPORT const char* BF_CALLTYPE BfCompiler_GetUsedOutputFileNames(void* bfCompiler, void* bfProject, bool flushQueuedHotFiles, bool* hadOutputChanges);
+
+BF_IMPORT void* BF_CALLTYPE BfSystem_CreateParser(void* bfSystem, void* bfProject);
+BF_IMPORT void BF_CALLTYPE BfParser_SetSource(void* bfParser, const char* data, int length, const char* fileName);
+BF_IMPORT void BF_CALLTYPE BfParser_SetCharIdData(void* bfParser, uint8* data, int length);
+BF_IMPORT bool BF_CALLTYPE BfParser_Parse(void* bfParser, void* bfPassInstance, bool compatMode);
+BF_IMPORT bool BF_CALLTYPE BfParser_Reduce(void* bfParser, void* bfPassInstance);
+BF_IMPORT bool BF_CALLTYPE BfParser_BuildDefs(void* bfParser, void* bfPassInstance, void* resolvePassData, bool fullRefresh);
+
+//////////////////////////////////////////////////////////////////////////
+
+BF_IMPORT void* BF_CALLTYPE BfSystem_Create();
+BF_IMPORT void BF_CALLTYPE BfSystem_ReportMemory(void* bfSystem);
+BF_IMPORT void BF_CALLTYPE BfSystem_Delete(void* bfSystem);
+BF_IMPORT void* BF_CALLTYPE BfSystem_CreatePassInstance(void* bfSystem);
+BF_IMPORT void* BF_CALLTYPE BfSystem_CreateCompiler(void* bfSystem, bool isResolveOnly);
+BF_IMPORT void* BF_CALLTYPE BfSystem_CreateProject(void* bfSystem, const char* projectName);
+BF_IMPORT void BF_CALLTYPE BfParser_Delete(void* bfParser);
+BF_IMPORT void BF_CALLTYPE BfSystem_AddTypeOptions(void* bfSystem, const char* filter, int32 simdSetting, int32 optimizationLevel, int32 emitDebugInfo, int32 arrayBoundsCheck,
+	int32 initLocalVariables, int32 emitDynamicCastCheck, int32 emitObjectAccessCheck, int32 allocStackTraceDepth);
+
+//////////////////////////////////////////////////////////////////////////
+
+BF_IMPORT void BF_CALLTYPE BfProject_SetDisabled(void* bfProject, bool disabled);
+BF_IMPORT void BF_CALLTYPE BfProject_SetOptions(void* bfProject, int targetType, const char* startupObject, const char* preprocessorMacros,
+	int optLevel, int ltoType, bool mergeFunctions, bool combineLoads, bool vectorizeLoops, bool vectorizeSLP);
+BF_IMPORT void BF_CALLTYPE BfProject_ClearDependencies(void* bfProject);
+BF_IMPORT void BF_CALLTYPE BfProject_AddDependency(void* bfProject, void* depProject);
+
+//////////////////////////////////////////////////////////////////////////
+
+BF_IMPORT const char* BF_CALLTYPE BfPassInstance_PopOutString(void* bfPassInstance);
+BF_IMPORT void BF_CALLTYPE BfPassInstance_Delete(void* bfPassInstance);
+
+//////////////////////////////////////////////////////////////////////////
+
+BF_IMPORT const char* BF_CALLTYPE VSSupport_Find();
+
+//////////////////////////////////////////////////////////////////////////
+
+USING_NS_BF;
+
+BootApp* Beefy::gApp = NULL;
+uint32 gConsoleFGColor = 0;
+uint32 gConsoleBGColor = 0;
+
+static bool GetConsoleColor(uint32& fgColor, uint32& bgColor)
+{
+#ifdef _WIN32
+	static uint32 consoleColors[16] = { 0xff000000, 0xff000080, 0xff008000, 0xff008080, 0xff800000, 0xff800080, 0xff808000, 0xffc0c0c0,
+		0xff808080, 0xff0000ff, 0xff00ff00, 0xff00ffff, 0xffff0000, 0xffff00ff, 0xffffff00, 0xffffffff };
+
+	CONSOLE_SCREEN_BUFFER_INFO screenBuffInfo = { 0 };	
+	GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &screenBuffInfo);
+	fgColor = consoleColors[screenBuffInfo.wAttributes & 0xF];
+	bgColor = consoleColors[(screenBuffInfo.wAttributes >> 4) & 0xF];	
+	return true;
+#else
+	fgColor = 0xFF808080;
+	bgColor = 0xFF000000;
+	return false;
+#endif	
+}
+
+static WORD GetColorCode(uint32 color)
+{
+	WORD code = 0;
+#ifdef _WIN32	
+	if (((color >> 0) & 0xFF) > 0x40)
+		code |= FOREGROUND_BLUE;
+	if (((color >> 8) & 0xFF) > 0x40)
+		code |= FOREGROUND_GREEN;
+	if (((color >> 16) & 0xFF) > 0x40)
+		code |= FOREGROUND_RED;
+	if ((((color >> 0) & 0xFF) > 0xC0) ||
+		(((color >> 8) & 0xFF) > 0xC0) ||
+		(((color >> 16) & 0xFF) > 0xC0))
+		code |= FOREGROUND_INTENSITY;
+#endif
+	return code;
+}
+
+static bool SetConsoleColor(uint32 fgColor, uint32 bgColor)
+{
+#ifdef _WIN32
+	WORD attr = GetColorCode(fgColor) | (GetColorCode(bgColor) << 4);
+	SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), attr);
+	SetConsoleTextAttribute(GetStdHandle(STD_ERROR_HANDLE), attr);
+	return true;
+#else
+	return false;
+#endif
+}
+
+
+BootApp::BootApp()
+{	
+	Targets_Create();
+	
+	mVerbosity = Verbosity_Normal;
+	mTargetType = BfTargetType_BeefConsoleApplication;
+
+	//char str[MAX_PATH];
+	//GetModuleFileNameA(NULL, str, MAX_PATH);
+	//mInstallDir = GetFileDir(str) + "/";
+
+	//getcwd(str, MAX_PATH);
+	//mStartupDir = str;
+	//mStartupDir += "/";
+
+	//mDoClean = false;
+	mHadCmdLine = false;
+	mShowedHelp = false;
+	mHadErrors = false;
+
+	mSystem = NULL;
+	mCompiler = NULL;
+	mProject = NULL;	
+
+#ifdef BF_PLATFORM_WINDOWS
+	mOptLevel = BfOptLevel_OgPlus;
+	mToolset = BfToolsetType_Microsoft;
+#else
+	mOptLevel = BfOptLevel_O0;
+	mToolset = BfToolsetType_GNU;
+#endif
+	mEmitIR = false;
+	
+	GetConsoleColor(gConsoleFGColor, gConsoleBGColor);
+}
+
+BootApp::~BootApp()
+{
+	Targets_Delete();
+}
+
+void BootApp::OutputLine(const String& text, OutputPri outputPri)
+{
+	if (mLogFile.IsOpen())
+	{
+		mLogFile.WriteSNZ(text);
+		mLogFile.WriteSNZ("\n");
+	}
+
+	if (outputPri == OutputPri_Error)
+		mHadErrors = true;
+
+	switch (outputPri)
+	{
+	case OutputPri_Low:
+		if (mVerbosity < Verbosity_Detailed)
+			return;
+		break;
+	case OutputPri_Normal:
+		if (mVerbosity < Verbosity_Normal)
+			return;
+		break;
+	case OutputPri_High:
+	case OutputPri_Warning:
+	case OutputPri_Error:
+		if (mVerbosity < Verbosity_Minimal)
+			return;
+		break;
+	}
+
+	if (outputPri == OutputPri_Warning)
+	{
+		SetConsoleColor(0xFFFFFF00, gConsoleBGColor);
+		std::cerr << text.c_str() << std::endl;
+		SetConsoleColor(gConsoleFGColor, gConsoleBGColor);
+	}
+	else if (outputPri == OutputPri_Error)
+	{
+		SetConsoleColor(0xFFFF0000, gConsoleBGColor);
+		std::cerr << text.c_str() << std::endl;
+		SetConsoleColor(gConsoleFGColor, gConsoleBGColor);
+	}
+	else
+		std::cout << text.c_str() << std::endl;
+}
+
+void BootApp::Fail(const String& error)
+{
+	if (mLogFile.IsOpen())
+		mLogFile.WriteSNZ("FAIL: " + error + "\n");
+	std::cerr << "FAIL: " << error.c_str() << std::endl;
+	mHadErrors = true;
+}
+
+bool BootApp::HandleCmdLine(const String &cmd, const String& param)
+{
+	mHadCmdLine = true;
+
+	bool wantedParam = false;
+
+	if ((cmd == "--help") || (cmd == "-h") || (cmd == "/?"))
+	{
+		mShowedHelp = true;
+		std::cout << "BeefBoot - Beef bootstrapping tool" << std::endl;
+		
+		return false;
+	}
+	else if (cmd == "--src")
+	{
+		mRequestedSrc.Add(param);
+		wantedParam = true;
+	}	
+	else if (cmd == "--verbosity")
+	{
+		if (param == "quiet")
+			mVerbosity = Verbosity_Quiet;
+		else if (param == "minimal")
+			mVerbosity = Verbosity_Minimal;
+		else if (param == "normal")
+			mVerbosity = Verbosity_Normal;
+		else if (param == "detailed")
+			mVerbosity = Verbosity_Detailed;
+		else if (param == "diagnostic")
+			mVerbosity = Verbosity_Diagnostic;
+		else
+		{
+			Fail(StrFormat("Invalid verbosity level: '%s'", param.c_str()));
+			return false;
+		}
+		wantedParam = true;
+	}
+	else if (cmd == "--define")
+	{
+		if (!mDefines.IsEmpty())
+			mDefines += "\n";
+		mDefines += param;
+		wantedParam = true;
+	}
+	else if (cmd == "--startup")
+	{
+		mStartupObject = param;
+		wantedParam = true;
+	}
+	else if (cmd == "--out")
+	{
+		mTargetPath = param;
+		wantedParam = true;
+	}
+	else if (cmd == "--linkparams")
+	{
+		mLinkParams = param;
+		wantedParam = true;
+	}
+	else if (cmd == "-Og+")
+	{
+		mOptLevel = BfOptLevel_OgPlus;
+	}
+	else if (cmd == "-O0")
+	{
+		mOptLevel = BfOptLevel_O0;
+	}
+	else if (cmd == "-O1")
+	{
+		mOptLevel = BfOptLevel_O1;
+	}
+	else if (cmd == "-O2")
+	{
+		mOptLevel = BfOptLevel_O2;
+	}
+	else if (cmd == "-O3")
+	{
+		mOptLevel = BfOptLevel_O3;
+	}
+	else if (cmd == "-gnu")
+	{
+		mToolset = BfToolsetType_GNU;
+	}
+	else if (cmd == "-emitir")
+	{
+		mEmitIR = true;
+	}
+	else
+	{
+		Fail("Unknown option: " + cmd);
+		return false;
+	}
+
+	if ((wantedParam) && (param.empty()))
+	{
+		Fail(StrFormat("Parameter expected for '%s'", cmd.c_str()));
+		return false;
+	}
+	else if ((!wantedParam) && (!param.empty()))
+	{
+		Fail(StrFormat("No parameter expected for '%s'", cmd.c_str()));
+		return false;
+	}
+
+	return true;
+}
+
+bool BootApp::Init()
+{    
+	char* cwdPtr = getcwd(NULL, 0);
+	mWorkingDir = cwdPtr;
+	free(cwdPtr);
+
+	if (mTargetPath.IsEmpty())
+	{
+		Fail("'Out' path not specified");
+	}
+
+	if (mRequestedSrc.IsEmpty())
+	{
+		Fail("No source specified");
+	}
+
+	return !mHadErrors;
+}
+
+void BootApp::QueueFile(const StringImpl& path)
+{
+	String ext;
+	ext = GetFileExtension(path);
+    if ((ext.Equals(".bf", StringImpl::CompareKind_OrdinalIgnoreCase)) ||
+        (ext.Equals(".cs", StringImpl::CompareKind_OrdinalIgnoreCase)))
+	{		
+		int len;
+		const char* data = LoadTextData(path, &len);
+		if (data == NULL)
+		{
+			Fail(StrFormat("Unable to load file '%s'", path.c_str()));
+			return;
+		}
+		
+		bool worked = true;
+		void* bfParser = BfSystem_CreateParser(mSystem, mProject);
+		BfParser_SetSource(bfParser, data, len, path.c_str());
+		//bfParser.SetCharIdData(charIdData);
+		worked &= BfParser_Parse(bfParser, mPassInstance, false);
+		worked &= BfParser_Reduce(bfParser, mPassInstance);
+		worked &= BfParser_BuildDefs(bfParser, mPassInstance, NULL, false);
+		
+		delete data;
+	}
+}
+
+void BootApp::QueuePath(const StringImpl& path)
+{
+	if (DirectoryExists(path))
+	{
+		for (auto& fileEntry : FileEnumerator(path, FileEnumerator::Flags_Files))
+		{
+			String filePath = fileEntry.GetFilePath();
+			
+			String fileName;
+			fileName = GetFileName(filePath);
+
+			QueueFile(filePath);			
+		}
+
+		for (auto& fileEntry : FileEnumerator(path, FileEnumerator::Flags_Directories))
+		{
+			String childPath = fileEntry.GetFilePath();
+			String dirName;
+			dirName = GetFileName(childPath);
+
+			if (dirName == "build")
+				continue;
+
+			QueuePath(childPath);			
+		}
+	}
+	else
+	{
+		QueueFile(path);		
+	}
+}
+
+static void CompileThread(void* param)
+{
+	BfpThread_SetName(NULL, "CompileThread", NULL);
+
+	BootApp* app = (BootApp*)param;
+	BfCompiler_ClearBuildCache(app->mCompiler);
+	
+	if (!BfCompiler_Compile(app->mCompiler, app->mPassInstance, app->mBuildDir.c_str()))
+		app->mHadErrors = true;
+}
+
+void BootApp::DoCompile()
+{
+#ifdef BFBUILD_MAIN_THREAD_COMPILE
+	mOutputDirectory = outputDirectory;
+	CompileThread(this);
+#else	
+
+	WorkThreadFunc workThread;
+	workThread.Start(CompileThread, this);
+
+	int lastProgressTicks = 0;
+
+	bool showProgress = mVerbosity >= Verbosity_Normal;
+
+	int progressSize = 30;
+	if (showProgress)
+	{
+		std::cout << "[";
+		for (int i = 0; i < progressSize; i++)
+			std::cout << " ";
+		std::cout << "]";
+		for (int i = 0; i < progressSize + 1; i++)
+			std::cout << "\b";
+		std::cout.flush();
+	}
+
+	while (true)
+	{
+		bool isDone = workThread.WaitForFinish(100);
+
+		float pct = BfCompiler_GetCompletionPercentage(mCompiler);
+		if (isDone)
+			pct = 1.0;
+		int progressTicks = (int)(pct * progressSize + 0.5f);
+
+		while (progressTicks > lastProgressTicks)
+		{
+			if (showProgress)
+			{
+				std::cout << "*";
+				std::cout.flush();
+			}
+			lastProgressTicks++;
+		}
+
+		if (isDone)
+			break;
+	}
+
+	if (showProgress)
+		std::cout << std::endl;
+#endif
+}
+
+struct OutputContext
+{
+	bool mIsError;
+	BfpFile* mFile;
+};
+
+static void OutputThread(void* param)
+{
+	OutputContext* context = (OutputContext*)param;
+
+	String queuedStr;
+	
+	while (true)
+	{
+		char data[1024];
+		
+		BfpFileResult result;
+		int bytesRead = (int)BfpFile_Read(context->mFile, data, 1023, -1, &result);
+		if ((result != BfpFileResult_Ok) && (result != BfpFileResult_PartialData))
+			return;
+
+		data[bytesRead] = 0;
+		if (context->mIsError)
+		{
+			std::cerr << data;
+			std::cerr.flush();
+		}
+		else
+		{
+			std::cout << data;
+			std::cout.flush();
+		}
+
+		if (gApp->mLogFile.IsOpen())
+		{
+			// This is to ensure that error and output lines are not merged together, though they may interleave
+			queuedStr.Append(data, bytesRead);
+			while (true)
+			{
+				int crPos = (int)queuedStr.IndexOf('\n');
+				if (crPos == -1)
+					break;
+
+				AutoCrit autoCrit(gApp->mLogCritSect);
+				if (context->mIsError)
+					gApp->mLogFile.WriteSNZ("err> ");
+				else
+					gApp->mLogFile.WriteSNZ("out> ");
+
+				int endPos = crPos;
+				if ((endPos > 0) && (queuedStr[endPos - 1] == '\r'))
+					endPos--;
+				gApp->mLogFile.Write((void*)queuedStr.c_str(), endPos);
+				gApp->mLogFile.WriteSNZ("\n");
+				queuedStr.Remove(0, crPos + 1);
+			}
+		}
+	}
+}
+
+bool BootApp::QueueRun(const String& fileName, const String& args, const String& workingDir, BfpSpawnFlags extraFlags)
+{
+	OutputLine(StrFormat("EXECUTING: %s %s", fileName.c_str(), args.c_str()), OutputPri_Low);
+
+    BfpSpawnFlags spawnFlags = (BfpSpawnFlags)(BfpSpawnFlag_NoWindow | BfpSpawnFlag_RedirectStdOutput | BfpSpawnFlag_RedirectStdError | extraFlags);    
+    BfpSpawn* spawn = BfpSpawn_Create(fileName.c_str(), args.c_str(), workingDir.c_str(), NULL, spawnFlags, NULL);
+    if (spawn == NULL)
+    {
+        Fail(StrFormat("Failed to execute '%s'", fileName.c_str()));
+        return false;
+    }
+    int exitCode = 0;
+
+	OutputContext outputContext;;
+	outputContext.mIsError = false;
+	OutputContext errorContext;	
+	errorContext.mIsError = false;
+	BfpSpawn_GetStdHandles(spawn, NULL, &outputContext.mFile, &errorContext.mFile);
+
+	BfpThread* outputThread = BfpThread_Create(OutputThread, (void*)&outputContext);
+	BfpThread* errorThread = BfpThread_Create(OutputThread, (void*)&errorContext);
+	
+    BfpSpawn_WaitFor(spawn, -1, &exitCode, NULL);	
+
+	if (outputContext.mFile != NULL)
+		BfpFile_Close(outputContext.mFile, NULL);
+	if (errorContext.mFile != NULL)
+		BfpFile_Close(errorContext.mFile, NULL);
+
+	BfpThread_WaitFor(outputThread, -1);
+	BfpThread_WaitFor(errorThread, -1);	
+
+	BfpThread_Release(outputThread);
+	BfpThread_Release(errorThread);
+    BfpSpawn_Release(spawn);
+
+    if (exitCode != 0)
+    {
+        Fail(StrFormat("Exit code returned: %d", exitCode));
+        return false;
+    }
+    return true;
+}
+
+#ifdef BF_PLATFORM_WINDOWS
+void BootApp::DoLinkMS()
+{
+	String vsStr = VSSupport_Find();
+		
+	int toolIdx = (int)vsStr.IndexOf("TOOL64\t");
+	int toolCrIdx = (int)vsStr.IndexOf('\n', toolIdx + 1);
+	if ((toolIdx == -1) || (toolCrIdx == -1))
+	{
+		Fail("Failed to detect Visual Studio configuration. Is Visual Studio 2015 or later installed?");
+		return;
+	}
+	
+	String linkerPath = vsStr.Substring(toolIdx + 7, toolCrIdx - toolIdx - 7);
+	linkerPath.Append("\\link.exe");
+
+	String linkLine;
+
+	String targetPath = mTargetPath;
+
+	bool hadOutputChanges;
+	const char* result = BfCompiler_GetUsedOutputFileNames(mCompiler, mProject, true, &hadOutputChanges);
+	if (result == NULL)
+		return;
+	std::string fileNamesStr;
+	fileNamesStr += result;
+	if (fileNamesStr.length() == 0)
+		return;
+	int curIdx = -1;
+	while (curIdx < (int)fileNamesStr.length())
+	{
+		int nextBr = (int)fileNamesStr.find('\n', curIdx + 1);
+		if (nextBr == -1)
+			nextBr = (int)fileNamesStr.length();
+		linkLine.Append(fileNamesStr.substr(curIdx + 1, nextBr - curIdx - 1));
+		linkLine.Append(" ");
+		curIdx = nextBr;
+	}
+
+	linkLine.Append("-out:");
+	IDEUtils::AppendWithOptionalQuotes(linkLine, targetPath);
+	linkLine.Append(" ");
+
+	if (mTargetType == BfTargetType_BeefConsoleApplication)
+		linkLine.Append("-subsystem:console ");
+	else
+		linkLine.Append("-subsystem:windows ");
+
+	linkLine.Append("-defaultlib:libcmtd ");
+	linkLine.Append("-nologo ");
+
+	linkLine.Append("-pdb:");
+	int lastDotPos = (int)targetPath.LastIndexOf('.');
+    if (lastDotPos == -1)
+        lastDotPos = (int)targetPath.length();
+	auto pdbName = String(targetPath, 0, lastDotPos);
+	pdbName.Append(".pdb");
+	IDEUtils::AppendWithOptionalQuotes(linkLine, pdbName);
+	linkLine.Append(" ");
+
+	linkLine.Append("-debug ");
+
+	int checkIdx = 0;
+	while (true)
+	{
+		int libIdx = (int)vsStr.IndexOf("LIB64\t", checkIdx);
+		if (libIdx == -1)
+			break;
+		int libCrIdx = (int)vsStr.IndexOf('\n', libIdx + 1);
+		if (libCrIdx == -1)
+			break;
+
+		String libPath = vsStr.Substring(libIdx + 6, libCrIdx - libIdx - 6);
+		linkLine.Append("-libpath:\"");
+		linkLine.Append(libPath);
+		linkLine.Append("\" ");
+		checkIdx = libCrIdx + 1;
+	}
+
+	linkLine.Append(mLinkParams);	
+
+	BfpSpawnFlags flags = BfpSpawnFlag_None;
+	if (true)
+	{
+		//if (linkLine.HasMultibyteChars())
+		if (true)
+			flags = (BfpSpawnFlags)(BfpSpawnFlag_UseArgsFile | BfpSpawnFlag_UseArgsFile_Native | BfpSpawnFlag_UseArgsFile_BOM);
+		else
+			flags = (BfpSpawnFlags)(BfpSpawnFlag_UseArgsFile);
+	}
+
+	auto runCmd = QueueRun(linkerPath, linkLine, mWorkingDir, flags);
+}
+#endif
+
+void BootApp::DoLinkGNU()
+{
+    String linkerPath = "/usr/bin/c++";
+
+    String linkLine;
+
+    String targetPath = mTargetPath;
+
+    bool hadOutputChanges;
+    const char* result = BfCompiler_GetUsedOutputFileNames(mCompiler, mProject, true, &hadOutputChanges);
+    if (result == NULL)
+        return;
+    std::string fileNamesStr;
+    fileNamesStr += result;
+    if (fileNamesStr.length() == 0)
+        return;
+    int curIdx = -1;
+    while (curIdx < (int)fileNamesStr.length())
+    {
+        int nextBr = (int)fileNamesStr.find('\n', curIdx + 1);
+        if (nextBr == -1)
+            nextBr = (int)fileNamesStr.length();
+        linkLine.Append(fileNamesStr.substr(curIdx + 1, nextBr - curIdx - 1));
+        linkLine.Append(" ");
+        curIdx = nextBr;
+    }
+
+    linkLine.Append("-o ");
+    IDEUtils::AppendWithOptionalQuotes(linkLine, targetPath);
+    linkLine.Append(" ");
+    linkLine.Append("-g ");
+
+    linkLine.Append("-debug -no-pie ");
+    linkLine.Append(mLinkParams);
+
+    auto runCmd = QueueRun(linkerPath, linkLine, mWorkingDir, true ? BfpSpawnFlag_UseArgsFile : BfpSpawnFlag_None);
+}
+
+bool BootApp::Compile()
+{
+	DWORD startTick = BFTickCount();
+
+	mSystem = BfSystem_Create();
+
+	mCompiler = BfSystem_CreateCompiler(mSystem, false);
+
+	String projectName = GetFileName(mTargetPath);
+	int dotPos = (int)projectName.IndexOf('.');
+	if (dotPos != -1)
+		projectName.RemoveToEnd(dotPos);
+
+	mProject = BfSystem_CreateProject(mSystem, projectName.c_str());
+	
+	int ltoType = 0;
+    BfProject_SetOptions(mProject, mTargetType, mStartupObject.c_str(), mDefines.c_str(), mOptLevel, ltoType, false, false, false, false);
+	
+	mPassInstance = BfSystem_CreatePassInstance(mSystem);
+
+	Beefy::String exePath;
+	BfpGetStrHelper(exePath, [](char* outStr, int* inOutStrSize, BfpResult* result)
+		{
+			BfpSystem_GetExecutablePath(outStr, inOutStrSize, (BfpSystemResult*)result);
+		});
+	mBuildDir = GetFileDir(exePath) + "/build";
+	
+	RecursiveCreateDirectory(mBuildDir + "/" + projectName);
+
+	BfCompilerOptionFlags optionFlags = (BfCompilerOptionFlags)(BfCompilerOptionFlag_EmitDebugInfo | BfCompilerOptionFlag_EmitLineInfo | BfCompilerOptionFlag_GenerateOBJ);
+	if (mEmitIR)
+		optionFlags = (BfCompilerOptionFlags)(optionFlags | BfCompilerOptionFlag_WriteIR);
+
+
+	int maxWorkerThreads = BfpSystem_GetNumLogicalCPUs(NULL);
+	if (maxWorkerThreads <= 1)
+		maxWorkerThreads = 6;
+
+    BfCompiler_SetOptions(mCompiler, NULL, 0, BfMachineType_x64, mToolset, BfSIMDSetting_SSE2, 1, maxWorkerThreads, optionFlags, "malloc", "free");
+	    
+	for (auto& srcName : mRequestedSrc)
+	{
+		String absPath = GetAbsPath(srcName, mWorkingDir);
+		QueuePath(absPath);
+	}
+
+	if (!mHadErrors)
+	{
+		DoCompile();
+		OutputLine(StrFormat("TIMING: Beef compiling: %0.1fs", (BFTickCount() - startTick) / 1000.0), OutputPri_Normal);
+	}
+
+	while (true)
+	{
+		const char* msg = BfPassInstance_PopOutString(mPassInstance);
+		if (msg == NULL)
+			break;
+
+		if ((strncmp(msg, ":warn ", 6) == 0))
+		{
+			OutputLine(msg + 6, OutputPri_Warning);
+		}
+		else if ((strncmp(msg, ":error ", 7) == 0))
+		{
+			OutputLine(msg + 7, OutputPri_Error);
+		}
+		else if ((strncmp(msg, ":med ", 5) == 0))
+		{
+			OutputLine(msg + 5, OutputPri_Normal);
+		}
+		else if ((strncmp(msg, ":low ", 5) == 0))
+		{
+			OutputLine(msg + 5, OutputPri_Low);
+		}
+		else if ((strncmp(msg, "ERROR(", 6) == 0) || (strncmp(msg, "ERROR:", 6) == 0))
+		{
+			OutputLine(msg, OutputPri_Error);
+		}
+		else if ((strncmp(msg, "WARNING(", 8) == 0) || (strncmp(msg, "WARNING:", 8) == 0))
+		{
+			OutputLine(msg, OutputPri_Warning);
+		}
+		else
+			OutputLine(msg);
+	}
+		
+	if (!mHadErrors)
+    {
+		if (mVerbosity == Verbosity_Normal)
+		{
+			std::cout << "Linking " << mTargetPath.c_str() << "...";
+			std::cout.flush();
+		}
+
+#ifdef BF_PLATFORM_WINDOWS
+        DoLinkMS();
+#else
+        DoLinkGNU();
+#endif
+
+		if (mVerbosity == Verbosity_Normal)
+			std::cout << std::endl;
+    }
+
+	BfPassInstance_Delete(mPassInstance);
+	BfCompiler_Delete(mCompiler);
+
+	BfSystem_Delete(mSystem);
+
+	return !mHadErrors;
+}
+

+ 80 - 0
BeefBoot/BootApp.h

@@ -0,0 +1,80 @@
+#pragma once
+
+#include "BeefBoot.h"
+#include "BeefySysLib/FileStream.h"
+#include "BeefySysLib/util/CritSect.h"
+#include "BeefySysLib/util/String.h"
+#include "BeefySysLib/util/Array.h"
+#include "Compiler/BfSystem.h"
+
+NS_BF_BEGIN
+
+enum OutputPri
+{
+	OutputPri_Low,
+	OutputPri_Normal,
+	OutputPri_High,
+	OutputPri_Warning,
+	OutputPri_Error,
+	OutputPri_Critical
+};
+
+enum Verbosity
+{
+	Verbosity_Quiet,
+	Verbosity_Minimal,
+	Verbosity_Normal,
+	Verbosity_Detailed,
+	Verbosity_Diagnostic,
+};
+
+class BootApp
+{
+public:
+	CritSect mLogCritSect;
+	FileStream mLogFile;
+	Verbosity mVerbosity;
+	BfTargetType mTargetType;
+	bool mHadCmdLine;
+	bool mShowedHelp;
+	bool mHadErrors;
+	Array<String> mRequestedSrc;
+	BfOptLevel mOptLevel;
+	BfToolsetType mToolset;	
+	bool mEmitIR;
+	String mBuildDir;
+	String mWorkingDir;
+	String mDefines;
+	String mStartupObject;
+	String mTargetPath;
+	String mLinkParams;
+
+	void* mSystem;
+	void* mCompiler;	
+	void* mProject;
+	void* mPassInstance;
+
+public:
+	void Fail(const String & error);
+	void OutputLine(const String& text, OutputPri outputPri = OutputPri_Normal);
+	bool QueueRun(const String& fileName, const String& args, const String& workingDir, BfpSpawnFlags extraFlags);
+
+	void QueueFile(const StringImpl& path);
+	void QueuePath(const StringImpl& path);
+	void DoCompile();
+    void DoLinkMS();
+    void DoLinkGNU();
+
+public:
+	BootApp();
+	~BootApp();	
+
+	bool HandleCmdLine(const String& cmd, const String& param);
+
+	bool Init();
+	bool Compile();
+};
+
+extern BootApp* gApp;
+
+NS_BF_END

+ 122 - 0
BeefBoot/CMakeLists.txt

@@ -0,0 +1,122 @@
+cmake_minimum_required(VERSION 3.0.0 FATAL_ERROR)
+
+################### Variables. ####################
+# Change if you want modify path or other values. #
+###################################################
+
+set(PROJECT_NAME BeefBoot)
+# Output Variables
+set(OUTPUT_DEBUG Debug/bin)
+set(OUTPUT_RELEASE Release/bin)
+
+############## CMake Project ################
+#        The main options of project        #
+#############################################
+
+project(${PROJECT_NAME} CXX C)
+
+# Define Release by default.
+if(NOT CMAKE_BUILD_TYPE)
+  set(CMAKE_BUILD_TYPE "Debug")
+  message(STATUS "Build type not specified: Use Debug by default.")
+endif(NOT CMAKE_BUILD_TYPE)
+
+# Definition of Macros
+add_definitions(   
+   -DIDEHELPER_EXPORTS 
+   -DBFSYSLIB_DYNAMIC 
+   -DUNICODE
+   -D_UNICODE
+   -DBF_NO_FBX
+   -DFT2_BUILD_LIBRARY
+   -DBFSYSLIB_DYNAMIC
+)
+
+include_directories(
+  .
+  ../BeefySysLib/
+  ../BeefySysLib/platform/linux
+  ../BeefySysLib/third_party
+  ../BeefySysLib/third_party/freetype/include
+  ../  
+  ../extern/llvm-project_8_0_0/llvm/include  
+  ../extern/llvm-project_8_0_0/llvm/lib/Target
+  ../IDEHelper
+)
+
+############## Artefacts Output #################
+# Defines outputs , depending Debug or Release. #
+#################################################
+
+if(CMAKE_BUILD_TYPE STREQUAL "Debug")  
+  add_definitions(
+    -D_DEBUG
+  )
+  include_directories(
+    ../extern/llvm_linux_8_0_0/include
+    ../extern/llvm_linux_8_0_0/lib/Target/X86
+  )
+  set(EXECUTABLE_OUTPUT_PATH  "${CMAKE_BINARY_DIR}/${OUTPUT_DEBUG}")
+  set(LLVM_LIB "${CMAKE_CURRENT_SOURCE_DIR}/../extern/llvm_linux_8_0_0/lib")
+else()
+  include_directories(
+    ../extern/llvm_linux_rel_8_0_0/include
+    ../extern/llvm_linux_rel_8_0_0/lib/Target/X86
+  )
+  set(EXECUTABLE_OUTPUT_PATH  "${CMAKE_BINARY_DIR}/${OUTPUT_RELEASE}")
+  set(LLVM_LIB "${CMAKE_CURRENT_SOURCE_DIR}/../extern/llvm_linux_rel_8_0_0/lib")    
+endif()
+
+################### Dependencies ##################
+# Add Dependencies to project.                    #
+###################################################
+
+option(BUILD_DEPENDS 
+   "Build other CMake project." 
+   ON 
+)
+
+# Dependencies : disable BUILD_DEPENDS to link with lib already build.
+if(BUILD_DEPENDS)
+   
+else()
+   
+endif()
+
+################# Flags ################
+# Defines Flags for Windows and Linux. #
+########################################
+
+if(MSVC)
+   set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /W3 /MD /MDd /Od /EHsc")
+   set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /W3 /GL /Od /Oi /Gy /EHsc")
+endif(MSVC)
+if(NOT MSVC)
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wno-multichar -Wno-invalid-offsetof")  
+  if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
+    set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++")
+  endif()
+endif(NOT MSVC)
+
+################ Files ################
+#   --   Add files to project.   --   #
+#######################################
+
+file(GLOB SRC_FILES
+    BeefBoot.cpp
+    BootApp.cpp
+)
+
+# Add executable to build.
+add_executable(${PROJECT_NAME}
+   ${SRC_FILES}
+)
+
+# Link with other dependencies.
+if(MSVC)
+   target_link_libraries(${PROJECT_NAME} BeefySysLib IDEHelper kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib)
+else()
+    target_link_libraries(${PROJECT_NAME} BeefySysLib IDEHelper tinfo
+        #${LLVM_LIB}/libLLVMMC.a
+        )    
+endif()

文件差异内容过多而无法显示
+ 34 - 0
BeefBuild/BeefProj.toml


+ 92 - 0
BeefBuild/BeefSpace.toml

@@ -0,0 +1,92 @@
+FileVersion = 1
+Projects = {BeefBuild = {Path = "."}, corlib = {Path = "../IDE\\corlib"}, Beefy2D = {Path = "../BeefLibs/Beefy2D"}, IDEHelper = {Path = "../IDEHelper"}, Debugger64 = {Path = "../Debugger64"}, BeefySysLib = {Path = "../BeefySysLib"}}
+
+[Workspace]
+StartupProject = "BeefBuild"
+
+[Configs.Debug.Win32]
+Toolset = "GNU"
+BfOptimizationLevel = "O0"
+InitLocalVariables = true
+EmitObjectAccessCheck = false
+EnableCustodian = false
+EnableRealtimeLeakCheck = false
+AllocStackTraceDepth = 0
+ConfigSelections = {IDEHelper = {Enabled = false}}
+
+[Configs.Debug.Win64]
+EnableSideStack = true
+ConfigSelections = {IDEHelper = {Enabled = false}, Debugger64 = {Enabled = false}, BeefySysLib = {Enabled = false}}
+
+[Configs.Release.Win32]
+Toolset = "GNU"
+BfOptimizationLevel = "OgPlus"
+EmitDebugInfo = "No"
+AllocStackTraceDepth = 0
+ConfigSelections = {IDEHelper = {Enabled = false}}
+
+[Configs.Release.Win64]
+
+[Configs.Release.Win64z]
+EnableObjectDebugFlags = true
+EmitObjectAccessCheck = true
+EnableCustodian = true
+EnableRealtimeLeakCheck = true
+AllocStackTraceDepth = 0
+
+[Configs.Debug-IDE.Win32]
+Toolset = "GNU"
+BfOptimizationLevel = "O0"
+EmitDebugInfo = "No"
+ArrayBoundsCheck = false
+EmitDynamicCastCheck = false
+EnableObjectDebugFlags = false
+EmitObjectAccessCheck = false
+EnableCustodian = false
+EnableRealtimeLeakCheck = false
+AllocStackTraceDepth = 0
+ConfigSelections = {corlib = {Config = "Debug"}, Beefy2D = {Config = "Debug"}, IDEHelper = {Config = "Debug"}, Debugger64 = {Config = "Debug"}, BeefySysLib = {Config = "Debug"}}
+
+[Configs.Debug-IDE.Win64]
+Toolset = "GNU"
+BfOptimizationLevel = "O0"
+EmitDebugInfo = "No"
+ArrayBoundsCheck = false
+EmitDynamicCastCheck = false
+EnableObjectDebugFlags = false
+EmitObjectAccessCheck = false
+EnableCustodian = false
+EnableRealtimeLeakCheck = false
+ConfigSelections = {corlib = {Config = "Debug"}, Beefy2D = {Config = "Debug"}, IDEHelper = {Config = "Debug"}, Debugger64 = {Config = "Debug"}, BeefySysLib = {Config = "Debug"}}
+
+[Configs.Debug2.Win32]
+Toolset = "GNU"
+BfOptimizationLevel = "O0"
+EmitDebugInfo = "No"
+ArrayBoundsCheck = false
+EmitDynamicCastCheck = false
+EnableObjectDebugFlags = false
+EmitObjectAccessCheck = false
+EnableCustodian = false
+EnableRealtimeLeakCheck = false
+AllocStackTraceDepth = 0
+ConfigSelections = {corlib = {Config = "Debug"}, Beefy2D = {Config = "Debug"}, IDEHelper = {Config = "Debug"}, Debugger64 = {Config = "Debug"}, BeefySysLib = {Config = "Debug"}}
+
+[Configs.Debug2.Win64]
+PreprocessorMacros = ["NEWFONT"]
+IntermediateType = "ObjectAndIRCode"
+ConfigSelections = {corlib = {Config = "Debug"}, Beefy2D = {Config = "Debug"}, IDEHelper = {Config = "Debug"}, Debugger64 = {Config = "Debug"}, BeefySysLib = {Config = "Debug"}}
+
+[Configs.Paranoid.Win32]
+Toolset = "GNU"
+COptimizationLevel = "O2"
+
+[Configs.Paranoid.Win64]
+Toolset = "GNU"
+COptimizationLevel = "O2"
+
+[Configs.Test.Win32]
+COptimizationLevel = "O2"
+
+[Configs.Test.Win64]
+COptimizationLevel = "O2"

+ 250 - 0
BeefBuild/src/BuildApp.bf

@@ -0,0 +1,250 @@
+using IDE;
+using System;
+using System.IO;
+using System.Threading;
+using System.Diagnostics;
+using System.Collections.Generic;
+using IDE.Util;
+
+namespace BeefBuild
+{
+	class BuildApp : IDEApp
+	{
+		const int cProgressSize = 30;
+		int mProgressIdx = 0;		
+		public bool mIsTest;
+		public bool mIsFailTest;
+
+		/*void Test()
+		{
+			/*CURL.Easy easy = new CURL.Easy();
+			easy.SetOpt(.URL, "http://raw.githubusercontent.com/alexcrichton/curl-rust/master/Cargo.toml");
+			easy.SetOpt(.Verbose, true);
+			easy.Perform();*/
+			Transfer transfer = new Transfer();
+			//transfer.Setup("https://curl.haxx.se/download/curl-7.57.0.zip");
+			transfer.Setup("https://secure-appldnld.apple.com/itunes12/091-56359-20171213-EDF2198A-E039-11E7-9A9F-D21A1E4B8CED/iTunes64Setup.exe");
+			transfer.PerformBackground();
+
+			while (transfer.IsRunning)
+			{
+				Thread.Sleep(100);
+				Console.WriteLine("{0}/{1} @{2}Kps", transfer.BytesReceived, transfer.TotalBytes, transfer.BytesPerSecond / 1024);
+			}
+
+#unwarn
+			let result = transfer.GetResult();
+		}*/
+
+		public this()
+		{
+			//mConfigName.Clear();
+			//mPlatformName.Clear();
+			mVerbosity = .Normal;
+
+			//Test();
+		}
+
+		public override void Init()
+		{
+			if (mConfigName.IsEmpty)
+			{
+				mConfigName.Set(mIsTest ? "Test" : "Debug");
+			}
+
+			if (mPlatformName.IsEmpty)
+			{
+				mPlatformName.Set(sPlatform64Name);
+			}
+
+			mMainThread = Thread.CurrentThread;
+
+			if (mConfigName.IsEmpty)
+				Fail("Config not specified");
+			if (mPlatformName.IsEmpty)
+				Fail("Platform not specified");
+
+			base.Init();
+
+			mInitialized = true;
+			CreateBfSystems();
+			if (mWantsClean)
+			{
+				mBfBuildCompiler.ClearBuildCache();
+				mWantsClean = false;
+			}
+
+			if (mWorkspace.mDir == null)
+			{
+				mWorkspace.mDir = new String();
+				Directory.GetCurrentDirectory(mWorkspace.mDir);
+			}
+
+			if (mWorkspace.mDir != null)
+			{
+			    mWorkspace.mName = new String();
+			    Path.GetFileName(mWorkspace.mDir, mWorkspace.mName);
+			    LoadWorkspace();                
+			}
+			else
+				Fail("Workspace not specified");
+
+			if (mFailed)
+				return;
+
+			WorkspaceLoaded();
+
+			if (mIsTest)
+			{
+				RunTests(false);
+			}
+			else
+				Compile(.Normal, null);
+		}
+
+		public override bool HandleCommandLineParam(String key, String value)
+		{
+			if (value == null)
+			{
+				switch (key)
+				{
+				case "-test":
+					mIsTest = true;
+					return true;
+				case "-testfail":
+					mIsFailTest = true;
+					return true;
+				case "-clean":
+					mWantsClean = true;
+					return true;
+				case "-noir":
+					mConfig_NoIR = true;
+					return true;
+				}
+			}
+			else
+			{
+				if ((key == "-proddir") || (key == "-workspace"))
+				{
+				    var relDir = scope String(value);
+					if ((relDir.EndsWith("\\")) || relDir.EndsWith("\""))
+						relDir.RemoveToEnd(relDir.Length - 1); //...
+				    IDEUtils.FixFilePath(relDir);
+
+					String fullDir = new String();
+					Path.GetFullPath(relDir, fullDir);
+
+					mWorkspace.mDir = fullDir;
+					return true;
+				}
+
+				switch (key)
+				{
+				case "-config":
+					mConfigName.Set(value);
+					return true;
+				case "-platform":
+					mPlatformName.Set(value);
+					return true;
+				case "-verbosity":
+				    if (value == "quiet")
+				        mVerbosity = .Quiet;
+				    else if (value == "minimal")
+				        mVerbosity = .Minimal;
+				    else if (value == "normal")
+				        mVerbosity = .Normal;
+				    else if (value == "detailed")
+				        mVerbosity = .Detailed;
+				    //else if (value == "diagnostic")
+				        //mVerbosity = .Diagnostic;
+				    return true;
+				}
+			}
+
+#if BF_PLATFORM_WINDOWS
+			if (key == "-wait")
+			{
+				Windows.MessageBoxA((Windows.HWnd)0, "Wait2", "Wait", 0);
+				return true;
+			}
+#endif //BF_PLATFORM_WINDOWS
+			
+			return false;
+		}
+
+		protected override void BeefCompileStarted()
+		{
+			base.BeefCompileStarted();
+
+			if (mVerbosity >= .Normal)
+			{
+				if (cProgressSize > 0)
+				{
+					String str = scope String();
+					str.Append("[");
+					str.Append(' ', cProgressSize);
+					str.Append("]");
+					str.Append('\b', cProgressSize + 1);
+					Console.Write(str);
+				}
+			}
+		}
+
+		void WriteProgress(float pct)
+		{
+			if (mVerbosity >= .Normal)
+			{
+				int progressIdx = (int)Math.Round(pct * cProgressSize);
+				while (progressIdx > mProgressIdx)
+				{
+					mProgressIdx++;
+					Console.Write("*");
+				}
+			}
+		}
+
+		protected override void BeefCompileDone()
+		{
+			base.BeefCompileDone();
+			WriteProgress(1.0f);
+			Console.WriteLine("");
+		}
+
+		public override void LoadFailed()
+		{
+			mFailed = true;
+		}
+
+		public override void TestFailed()
+		{
+			mFailed = true;
+		}
+
+		protected override void CompileFailed()
+		{
+			base.CompileFailed();
+			mFailed = true;
+		}
+
+		protected override void CompileDone(bool succeeded)
+		{
+			if (!succeeded)
+				mFailed = true;
+		}
+
+		public override void Update(bool batchStart)
+		{
+			base.Update(batchStart);
+
+			if (mCompilingBeef)
+			{
+				WriteProgress(mBfBuildCompiler.GetCompletionPercentage());
+			}
+
+			if ((!IsCompiling) && (!AreTestsRunning()))
+			{
+				Stop();
+			}
+		}
+	}
+}

+ 147 - 0
BeefBuild/src/Program.bf

@@ -0,0 +1,147 @@
+using System;
+using IDE.Util;
+using System.Diagnostics;
+
+namespace BeefBuild
+{
+	class Program
+	{
+		static void TestZip()
+		{
+			String str = "Good morning Dr. Chandra. This is Hal. I am ready for my first lesson.Good morning Dr. Chandra. This is Hal. I am ready for my first lesson.Good morning Dr. Chandra. This is Hal. I am ready for my first lesson.Good morning Dr. Chandra. This is Hal. I am ready for my first lesson.Good morning Dr. Chandra. This is Hal. I am ready for my first lesson.Good morning Dr. Chandra. This is Hal. I am ready for my first lesson.Good morning Dr. Chandra. This is Hal. I am ready for my first lesson.";
+			int src_len = str.Length;
+			int uncomp_len = (uint32)src_len;
+			int cmp_len = MiniZ.CompressBound(src_len);
+
+			var pCmp = new uint8[cmp_len]*;
+#unwarn
+			var pUncomp = new uint8[src_len]*;
+
+#unwarn
+			//var cmp_status = MiniZ.compress(pCmp, ref cmp_len, (uint8*)(char8*)str, src_len);
+
+			var cmp_status = MiniZ.Compress(pCmp, ref cmp_len, (uint8*)(char8*)str, src_len, .BEST_COMPRESSION);
+
+			cmp_status = MiniZ.Uncompress(pUncomp, ref uncomp_len, pCmp, cmp_len);
+		}
+
+		static void TestZip2()
+		{
+			MiniZ.ZipArchive zipArchive = default;
+
+			if (!MiniZ.ZipReaderInitFile(&zipArchive, "c:\\temp\\zip\\test.zip", default))
+				return;
+
+			for (int32 i = 0; i < (int)MiniZ.ZipReaderGetNumFiles(&zipArchive); i++)
+			{
+				MiniZ.ZipArchiveFileStat file_stat;
+				if (!MiniZ.ZipReaderFileStat(&zipArchive, i, &file_stat))
+				{			
+					MiniZ.ZipReaderEnd(&zipArchive);
+					return;
+				}
+
+				//printf("Filename: \"%s\", Comment: \"%s\", Uncompressed size: %u, Compressed size: %u, Is Dir: %u\n", file_stat.m_filename, file_stat.m_comment, 
+					//(uint)file_stat.m_uncomp_size, (uint)file_stat.m_comp_size, mz_zip_reader_is_file_a_directory(&zipArchive, i));
+
+				/*if (!strcmp(file_stat.m_filename, "directory/"))
+				{
+					if (!mz_zip_reader_is_file_a_directory(&zipArchive, i))
+					{
+						printf("mz_zip_reader_is_file_a_directory() didn't return the expected results!\n");
+						mz_zip_reader_end(&zipArchive);
+					}
+				}*/
+
+				var str = scope String();
+				str.AppendF("c:\\temp\\file.{0}", i);
+				MiniZ.ZipReaderExtractToFile(&zipArchive, i, str, default);
+			}
+
+			// Close the archive, freeing any resources it was using
+			MiniZ.ZipReaderEnd(&zipArchive);
+		}
+
+		/*[StdCall]
+		static int32 fetch_progress(Git.git_transfer_progress* stats, void* payload)
+		{
+			return 0;
+		}
+
+		[StdCall]
+		static void checkout_progress(char8* path, int cur, int tot, void* payload)
+		{
+			
+		}
+
+		static void TestGit()
+		{
+			Git.git_libgit2_init();
+			Git.git_repository* repo = null;
+
+
+			Git.git_clone_options cloneOptions = default;
+			cloneOptions.version = 1;
+			cloneOptions.checkout_opts.version = 1;
+			cloneOptions.checkout_opts.checkout_strategy = 1;
+			//cloneOptions.checkout_opts.perfdata_cb = => checkout_progress;
+			cloneOptions.checkout_opts.progress_cb = => checkout_progress;
+			cloneOptions.fetch_opts.version = 1;
+			cloneOptions.fetch_opts.callbacks.version = 1;
+			cloneOptions.fetch_opts.update_fetchhead = 1;
+			cloneOptions.fetch_opts.proxy_opts.version = 1;
+			cloneOptions.fetch_opts.callbacks.transfer_progress = => fetch_progress;
+			//cloneOptions.
+
+			var result = Git.git_clone(&repo, "https://github.com/ponylang/pony-stable.git", "c:/temp/pony-stable", &cloneOptions);
+			Git.git_repository_free(repo);
+			Git.git_libgit2_shutdown();
+		}*/
+
+		public static int32 Main(String[] args)		
+		{
+			//TestGit();
+
+			for (let arg in args)
+			{
+				if (arg != "-help")
+					continue;
+				Console.WriteLine(
+					"""
+					BeefBuild [args]
+					  If no arguments are specified, a build will occur using current working directory as the workspace.
+					    -config=<config>        Sets the config (defaults to Debug)
+					    -minidump=<path>        Opens windows minidup file
+					    -new                    Creates a new workspace and project
+					    -platform=<platform>    Sets the platform (defaults to system platform)
+					    -test=<path>            Executes test script
+					    -verbosity=<verbosity>  Set verbosity level to: quiet/minimal/normal/detailed/diagnostics
+					    -workspace=<path>       Sets workspace path (defaults to current working directory)
+					""");
+				return 0;
+			}
+
+			//TestZip2();
+			String commandLine = scope String();
+			commandLine.Join(" ", params args);
+
+			BuildApp mApp = new BuildApp();	
+			mApp.ParseCommandLine(commandLine);
+			if (mApp.mFailed)
+			{
+				Console.Error.WriteLine("  Run with \"-help\" for a list of command-line arguments");
+			}
+			else
+			{
+				mApp.Init();
+				mApp.Run();
+			}
+			mApp.Shutdown();
+			int32 result = mApp.mFailed ? 1 : 0;
+
+			delete mApp;
+
+			return result;
+		}
+	}
+}

+ 36 - 0
BeefLibs/Beefy2D/BeefProj.toml

@@ -0,0 +1,36 @@
+FileVersion = 1
+
+[Project]
+Name = "Beefy2D"
+TargetType = "BeefLib"
+DefaultNamespace = ""
+
+[Configs.Debug.Win32]
+OtherLinkFlags = ""
+PreprocessorMacros = ["DEBUG", "BF32"]
+OptimizationLevel = "O0"
+
+[Configs.Debug.Win64]
+CLibType = "Static"
+BeefLibType = "Static"
+
+[Configs.Release.Win32]
+OtherLinkFlags = ""
+PreprocessorMacros = ["RELEASE", "BF32"]
+OptimizationLevel = "O0"
+
+[Configs.Paranoid.Win32]
+CLibType = "Static"
+BeefLibType = "Static"
+
+[Configs.Paranoid.Win64]
+CLibType = "Static"
+BeefLibType = "Static"
+
+[Configs.Test.Win32]
+CLibType = "Static"
+BeefLibType = "Static"
+
+[Configs.Test.Win64]
+CLibType = "Static"
+BeefLibType = "Static"

+ 787 - 0
BeefLibs/Beefy2D/src/BFApp.bf

@@ -0,0 +1,787 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Runtime.InteropServices;
+using System.Diagnostics;
+using System.IO;
+using Beefy.gfx;
+using Beefy.widgets;
+using Beefy.theme;
+using Beefy.theme.dark;
+using Beefy.utils;
+using Beefy.res;
+using Beefy.geom;
+using System.Threading;
+#if MONOTOUCH
+using MonoTouch;
+#endif
+
+#if STUDIO_CLIENT || STUDIO_HOST
+using Beefy.ipc;
+#endif
+
+namespace Beefy
+{   
+    public enum Cursor
+    {
+	    Pointer,
+	    Hand,
+	    Dragging,
+	    Text,
+	    CircleSlash,
+	    Sizeall,
+	    SizeNESW,
+	    SizeNS,
+	    SizeNWSE,
+	    SizeWE,	
+	    Wait,
+	    None,	
+	    COUNT
+    }
+
+    public class BFApp
+#if STUDIO_CLIENT
+        : IStudioClient
+#endif
+    {
+        public delegate void UpdateDelegate(bool batchStart);
+        public delegate void DrawDelegate();
+
+        public static BFApp sApp;
+        public int32 mUpdateCnt;
+        public bool mIsUpdateBatchStart;
+		public bool mAutoDirty = true;
+        public Graphics mGraphics ~ delete _;
+        int32 mRefreshRate = 60;
+        public String mInstallDir ~ delete _;
+        public String mUserDataDir ~ delete _;
+        public List<BFWindow> mWindows = new List<BFWindow>() ~ delete _;
+		List<Object> mDeferredDeletes = new List<Object>() ~ delete _;
+		public bool mShuttingDown = false;
+		public bool mStopping;
+		public bool mStarted;
+
+        public ResourceManager mResourceManager ~ delete _;
+
+        public int32 mFPSDrawCount;
+        public int32 mFPSUpdateCount;
+        public int32 mLastFPS;
+        public uint32 mLastFPSUpdateCnt;
+
+		public Matrix4? mColorMatrix;
+		public ConstantDataDefinition mColorMatrixDataDef = new ConstantDataDefinition(16, new ConstantDataDefinition.DataType[] { ConstantDataDefinition.DataType.Matrix | ConstantDataDefinition.DataType.PixelShaderUsage }) ~ delete _;
+
+		[StdCall, CLink]
+		static extern void Lib_Startup(int32 argc, char8** argv, void* startupCallback);
+
+        [StdCall, CLink]
+        static extern void BFApp_GetDesktopResolution(out int32 width, out int32 height);
+
+        [StdCall, CLink]
+        static extern void BFApp_GetWorkspaceRect(out int32 x, out int32 y, out int32 width, out int32 height);
+
+        [StdCall, CLink]
+        static extern void BFApp_Create();
+
+        [StdCall, CLink]
+        static extern void BFApp_Delete();
+
+        [StdCall, CLink]
+        static extern void BFApp_SetRefreshRate(int32 rate);
+
+        [StdCall, CLink]
+        public static extern void BFApp_SetDrawEnabled(int32 enabled);
+
+        [StdCall, CLink]
+        static extern void BFApp_Init();
+
+        [StdCall, CLink]
+        static extern void BFApp_Run();
+
+        [StdCall, CLink]
+        static extern void BFApp_Shutdown();
+
+        [StdCall, CLink]
+        static extern void BFApp_SetCallbacks(void* updateDelegate, void* drawDelegate);
+
+        [StdCall, CLink]
+        static extern char8* BFApp_GetInstallDir();
+
+        [StdCall, CLink]
+        static extern void BFApp_SetCursor(int32 cursor);
+
+        [StdCall, CLink]
+        static extern void* BFApp_GetClipboardData(char8* format, out int32 size);
+
+        [StdCall, CLink]
+        static extern void BFApp_ReleaseClipboardData(void* ptr);
+
+        [StdCall, CLink]
+        static extern void BFApp_SetClipboardData(char8* format, void* ptr, int32 size, int32 resetClipboard);
+
+		[StdCall, CLink]
+		public static extern void BFApp_CheckMemory();
+
+		[StdCall, CLink]
+		public static extern void BFApp_RehupMouse();
+
+        UpdateDelegate mUpdateDelegate ~ delete _;
+        DrawDelegate mDrawDelegate ~ delete _;
+		
+#if STUDIO_CLIENT
+        public bool mTrackingDraw = false;
+        public IPCClientManager mIPCClientManager;
+        public IPCProxy<IStudioHost> mStudioHost;
+        public static IStudioHost StudioHostProxy
+        {
+            get
+            {
+                return sApp.mStudioHost.Proxy;
+            }            
+        }    
+#endif
+
+#if STUDIO_CLIENT        
+        public IPCEndpoint<IStudioClient> mStudioClient;         
+#endif
+
+#if MONOTOUCH
+		[MonoPInvokeCallback(typeof(DrawDelegate))]
+		static void Static_Draw()
+		{
+			sApp.Draw();
+		}
+		[MonoPInvokeCallback(typeof(UpdateDelegate))]
+		static void Static_Update()
+		{
+			sApp.Update();
+		}
+#endif
+        
+        static void Static_Draw()
+        {
+            sApp.Draw();
+        }
+        
+        static void Static_Update(bool batchStart)
+        {
+            sApp.Update(batchStart);
+        }
+
+        float mLastUpdateDelta; // In seconds
+
+        public this()
+        {
+            Utils.GetTickCountMicro();
+
+			//mColorMatrix = Matrix4.Identity;
+			/*mColorMatrix = Matrix4(
+	            1, 1, 1, 1,
+	            1, 1, 1, 1,
+	            1, 1, 1, 1,
+	            1, 1, 1, 1);*/
+
+            sApp = this;
+            BFApp_Create();
+#if STUDIO_CLIENT
+            BFApp_SetRefreshRate(0); // Let studio dictate updating/drawing
+            mUpdateDelegate = LocalUpdate;
+            mDrawDelegate = LocalDraw;
+#else
+            BFApp_SetRefreshRate(mRefreshRate);
+			
+			mUpdateDelegate = new => Static_Update;
+			mDrawDelegate = new => Static_Draw;			
+#endif
+            BFApp_SetCallbacks(mUpdateDelegate.GetFuncPtr(), mDrawDelegate.GetFuncPtr());
+        }
+
+#if STUDIO_CLIENT
+        int mStudioUpdateCnt = 0;
+        int mCount = 0;
+        uint mLocalUpdateTick;
+        public virtual void LocalUpdate()
+        {
+            mLocalUpdateTick = Utils.GetTickCount();
+
+            if (mHostIsPerfRecording)
+            {
+                PerfTimer.Log("HostPerfRecording Done at: {0}", Utils.GetTickCount());
+
+                PerfTimer.StopRecording();
+                PerfTimer.DbgPrint();
+                mHostIsPerfRecording = false;
+            }
+
+            IPCClientManager.sIPCClientManager.Update();
+
+            if (mHostIsPerfRecording)            
+                PerfTimer.Log("Update Done at: {0}", Utils.GetTickCount());
+
+            IPCClientManager.sIPCClientManager.RemoteSyncToRead();            
+
+            if (mHostIsPerfRecording)
+                PerfTimer.Log("RemoteSyncToRead Done at: {0}", Utils.GetTickCount());
+
+            mStudioUpdateCnt++;
+            mStudioHost.Proxy.ClientUpdated(mStudioUpdateCnt);
+
+            if (mHostIsPerfRecording)
+                PerfTimer.Log("ClientUpdated Done at: {0}", Utils.GetTickCount());
+
+            mCount++;            
+        }
+
+        public virtual void LocalDraw()
+        {
+        }
+
+        public void TrackDraw()
+        {
+            if (!mTrackingDraw)
+                return;            
+
+            StackTrace st = new StackTrace(true);
+            StringBuilder sb = new StringBuilder();
+            var frames = st.GetFrames();
+            for (int frameNum = 0; frameNum < frames.Length; frameNum++)            
+            {
+                var frame = frames[frameNum];
+                sb.Append(frame.GetMethod().DeclaringType.ToString());
+                sb.Append(".");
+                sb.Append(frame.GetMethod().Name);
+                sb.Append("(");
+                int paramIdx = 0;
+                for (var param in frame.GetMethod().GetParameters())
+                {
+                    if (paramIdx > 0)
+                        sb.Append(", ");
+                    sb.Append(param.ParameterType.ToString());
+                    sb.Append(" ");
+                    sb.Append(param.Name);
+                    paramIdx++;
+                }
+                sb.Append(")");
+                sb.Append("|");
+                sb.Append(frame.GetFileName());
+                sb.Append("|");
+                sb.Append(frame.GetFileLineNumber());
+                sb.AppendLine();
+            }
+        }
+#endif
+
+        public int32 RefreshRate
+        {
+            get
+            {
+                return mRefreshRate;
+            }
+
+            set
+            {
+                mRefreshRate = value;
+                BFApp_SetRefreshRate(mRefreshRate);
+            }
+        }        
+
+        // Simulation seconds since last update
+        public float UpdateDelta
+        {
+            get
+            {
+                if (mRefreshRate != 0)
+                    return 1.0f / mRefreshRate;
+                return mLastUpdateDelta;
+            }
+        }
+
+		public static void Startup(String[] args, Action startupCallback)
+		{
+			/*string[] newArgs = new string[args.Length + 1];
+			Array.Copy(args, 0, newArgs, 1, args.Length);
+			newArgs[0] = System.Reflection.Assembly.GetEntryAssembly().Location;
+			Lib_Startup(newArgs.Length, newArgs, startupCallback);*/
+			
+			char8*[] char8PtrArr = scope char8*[args.Count];
+			for (int32 i = 0; i < args.Count; i++)
+				char8PtrArr[i] = args[i];
+			Lib_Startup((int32)args.Count, char8PtrArr.CArray(), startupCallback.GetFuncPtr());
+		}
+
+        public virtual bool HandleCommandLineParam(String key, String value)
+        {
+			return false;
+        }
+
+		public virtual void UnhandledCommandLine(String key, String value)
+		{
+
+		}
+
+		public virtual void ParseCommandLine(String[] args)
+		{
+			for (var str in args)
+			{
+				int eqPos = str.IndexOf('=');
+				if (eqPos == -1)
+				{
+					if (!HandleCommandLineParam(str, null))
+						UnhandledCommandLine(str, null);
+				}	
+				else
+				{
+					var cmd = scope String(str, 0, eqPos);
+					var param = scope String(str, eqPos + 1);
+					if (!HandleCommandLineParam(cmd, param))
+						UnhandledCommandLine(cmd, param);
+				}
+			}
+		}
+
+        public virtual void ParseCommandLine(String theString)
+        {
+            List<String> stringList = scope List<String>();
+
+            String curString = null;
+            bool hadSpace = false;
+            bool inQuote = false;
+            for (int32 i = 0; i < theString.Length; i++)
+            {
+                char8 c = theString[i];
+
+                if ((theString[i] == ' ') && (!inQuote))
+                {
+                    hadSpace = true;
+                }
+                else
+                {
+                    if (hadSpace)
+                    {
+                        if (!inQuote)
+                        {
+                            if (c == '=')
+                            {
+                            }
+                            else
+                            {
+                                if (curString != null)
+                                {
+                                    stringList.Add(curString);
+                                    curString = null;
+                                }
+                            }
+                        }
+                        hadSpace = false;
+                    }
+
+                    if (curString == null)
+                        curString = scope:: String();
+
+                    if (c == '"')
+                    {
+                        inQuote = !inQuote;
+                    }
+                    else
+                    {
+                        curString.Append(theString[i]);
+                    }
+                }
+                
+            }
+
+            if (curString != null)
+                stringList.Add(curString);
+
+            for (String param in stringList)
+            {
+                int32 eqPos = (int32)param.IndexOf('=');
+                if (eqPos != -1)
+				{
+					String key = scope String(param, 0, eqPos);
+					String value = scope String(param, eqPos + 1);
+
+                    if (!HandleCommandLineParam(key, value))
+						UnhandledCommandLine(key, value);
+				}
+                else
+                    if (!HandleCommandLineParam(param, null))
+						UnhandledCommandLine(param, null);
+            }
+        }
+
+        public void GetDesktopResolution(out int width, out int height)
+        {
+			int32 widthOut;
+			int32 heightOut;
+            BFApp_GetDesktopResolution(out widthOut, out heightOut);
+			width = widthOut;
+			height = heightOut;
+        }
+
+        public void GetWorkspaceRect(out int x, out int y, out int width, out int height)
+        {
+			int32 xOut;
+			int32 yOut;
+			int32 widthOut;
+			int32 heightOut;
+            BFApp_GetWorkspaceRect(out xOut, out yOut, out widthOut, out heightOut);
+			x = xOut;
+			y = yOut;
+			width = widthOut;
+			height = heightOut;
+        }
+
+        public bool HasModalDialogs()
+        {
+            for (var window in mWindows)
+            {
+                if (window.mWindowFlags.HasFlag(BFWindowBase.Flags.Modal))
+                    return true;
+            }
+            return false;
+        }
+
+        public bool HasPopupMenus()
+        {
+            for (var window in mWindows)
+            {
+                var widgetWindow = window as WidgetWindow;
+                if ((widgetWindow != null) && (widgetWindow.mRootWidget is MenuContainer))
+                    return true;
+            }
+            return false;
+        }
+
+        public virtual void Init()
+        {
+			scope AutoBeefPerf("BFApp.Init");
+
+#if STUDIO_CLIENT
+            mIPCClientManager = IPCClientManager.sIPCClientManager = new IPCClientManager();
+            mIPCClientManager.Init();
+            
+            mStudioClient = IPCEndpoint<IStudioClient>.CreateNamed(this);
+            mStudioHost = IPCProxy<IStudioHost>.CreateNamed();
+            mIPCClientManager.mRemoteProcessId = mStudioHost.Proxy.ClientConnected(mStudioClient.ObjId, Process.GetCurrentProcess().Id);
+            mIPCClientManager.mConnecting = false;
+#endif
+            BFApp_Init();
+
+            Interlocked.Fence();
+       
+            mInstallDir = new String(BFApp_GetInstallDir());
+            mUserDataDir = new String(mInstallDir);
+
+            mResourceManager = new ResourceManager();
+            String resFileName = scope String();
+            resFileName.Append(mInstallDir, "Resources.json");
+            if (File.Exists(resFileName))
+            {
+                StructuredData structuredData = scope StructuredData();
+                structuredData.Load(resFileName);
+                mResourceManager.ParseConfigData(structuredData);
+            }            
+        }
+
+        public void InitGraphics()
+        {
+			DefaultVertex.Init();
+			ModelDef.VertexDef.Init();
+
+            mGraphics = new Graphics();
+
+			String filePath = scope String();
+			filePath.Append(mInstallDir, "shaders/Std");
+
+			if (mColorMatrix != null)
+				filePath.Append("_hue");
+
+            Shader shader = Shader.CreateFromFile(filePath, DefaultVertex.sVertexDefinition);
+            //shader.SetTechnique("Standard");            
+            mGraphics.mDefaultShader = shader;
+            mGraphics.mDefaultRenderState = RenderState.Create();
+            mGraphics.mDefaultRenderState.mIsFromDefaultRenderState = true;
+            mGraphics.mDefaultRenderState.Shader = shader;
+
+			filePath.Append("_font");
+			Shader textShader = Shader.CreateFromFile(filePath, DefaultVertex.sVertexDefinition);
+			mGraphics.mTextShader = textShader;
+			//shader.SetTechnique("Standard");            
+			//mGraphics.mDefaultShader = textShader;
+			//mGraphics.mDefaultRenderState = RenderState.Create();
+			//mGraphics.mDefaultRenderState.mIsFromDefaultRenderState = true;
+			//mGraphics.mDefaultRenderState.Shader = shader;
+
+			//mGraphics.mDefaultShader = textShader;
+			//mGraphics.mDefaultRenderState.Shader = textShader;
+        }
+
+        public ~this()
+        {
+#if STUDIO_HOST || STUDIO_CLIENT
+            if (IPCManager.sIPCManager != null)
+                IPCManager.sIPCManager.Dispose();
+#endif
+			Debug.Assert(mShuttingDown, "Shutdown must be called before deleting the app");
+
+			ProcessDeferredDeletes();
+
+			BFApp_Delete();
+
+			ShutdownCompleted();
+        }
+
+		public void DeferDelete(Object obj)
+		{
+			//Slow, paranoid check.
+			//Debug.Assert(!mDeferredDeletes.Contains(obj));
+
+			mDeferredDeletes.Add(obj);
+		}
+
+		public void ProcessDeferredDeletes()
+		{
+			for (int32 i = 0; i < mDeferredDeletes.Count; i++)
+				delete mDeferredDeletes[i];
+			mDeferredDeletes.Clear();
+		}
+
+		public virtual void ShutdownCompleted()
+		{
+
+		}
+
+        public virtual void Shutdown()
+        {
+			Debug.Assert(!mShuttingDown, "Shutdown can only be called once");
+			/*if (!mStarted)
+				Debug.Assert(mStopping, "Shutdown can only be called after the app is stopped");*/
+
+			mShuttingDown = true;
+            //Dispose();
+        }
+
+        public virtual void Run()
+        {
+			if (mStopping)
+				return;
+            BFApp_Run();
+        }
+
+		public virtual void Stop()
+		{
+			mStopping = true;
+			while (mWindows.Count > 0)
+				mWindows[0].Close();
+
+			BFApp_Shutdown();
+		}
+
+        uint32 mPrevUpdateTickCount;
+        uint32 mUpdateTickCount;
+        uint32 mLastEndedTickCount;
+
+        public virtual void Update(bool batchStart)
+        {
+            //Utils.BFRT_CPP("gBFGC.MutatorSectionEntry()");
+
+            mIsUpdateBatchStart = batchStart;
+            mUpdateCnt++;
+
+            mPrevUpdateTickCount = mUpdateTickCount;
+            mUpdateTickCount = Utils.GetTickCount();
+            if (mPrevUpdateTickCount != 0)
+                mLastUpdateDelta = (mUpdateTickCount - mPrevUpdateTickCount) / 1000.0f;
+
+            using (PerfTimer.ZoneStart("BFApp.Update"))
+            {
+                mFPSUpdateCount++;
+
+                if (mRefreshRate == 0)
+                {
+                    int32 elapsed = (int32)(mUpdateTickCount - mLastFPSUpdateCnt);
+                    if (elapsed > 1000)
+                    {
+                        mLastFPS = mFPSDrawCount;
+                        mLastFPSUpdateCnt = mUpdateTickCount;
+
+                        mFPSUpdateCount = 0;
+                        mFPSDrawCount = 0;                        
+                    }
+                }
+                else
+                {
+                    if (mFPSUpdateCount == mRefreshRate)
+                    {
+                        mLastFPS = mFPSDrawCount;
+                        //Debug.WriteLine("FPS: " + mLastFPS);
+
+                        mFPSUpdateCount = 0;
+                        mFPSDrawCount = 0;
+                    }
+                }                
+
+                //GC.Collect();
+                using (PerfTimer.ZoneStart("BFApp.Update mWindows updates"))
+                {                    
+                    for (int32 windowIdx = 0; windowIdx < mWindows.Count; windowIdx++)
+                    {
+                        mWindows[windowIdx].Update();
+                    }
+                }
+            }
+
+            mLastEndedTickCount = Utils.GetTickCount();
+			ProcessDeferredDeletes();
+
+            //Utils.BFRT_CPP("gBFGC.MutatorSectionExit()");
+        }        
+
+        public virtual void DoDraw()
+        {
+        }
+
+#if STUDIO_CLIENT
+        bool mHostIsPerfRecording;
+
+        public virtual void HostIsPerfRecording(bool isPerfRecording, int drawCount)
+        {
+            mHostIsPerfRecording = isPerfRecording;
+            if (mHostIsPerfRecording)
+            {
+                PerfTimer.StartRecording();
+                PerfTimer.Log("HostIsPerfRecording {0}", drawCount);
+                PerfTimer.Log("PrevUpdateTickCount: {0}", mPrevUpdateTickCount);
+                PerfTimer.Log("UpdateTickCount: {0}", mUpdateTickCount);
+                PerfTimer.Log("LocalUpdateTick: {0}", mLocalUpdateTick);
+                
+                //using (PerfTimer.ZoneStart("HostIsPerfRecording"))
+                    //PerfTimer.Log("Got HostIsPerfRecording");
+            }
+        }
+#endif
+        
+        public virtual void Draw()
+        {
+#if STUDIO_CLIENT            
+            
+#endif            
+
+            PerfTimer.ZoneStart("BFApp.Draw");
+            PerfTimer.Message("Client Draw Start");
+
+            mFPSDrawCount++;
+
+#if STUDIO_CLIENT
+            mStudioHost.Proxy.FrameStarted(mUpdateCnt);
+#endif
+            if (mGraphics == null)
+                InitGraphics();
+
+            mGraphics.StartDraw();
+
+            for (BFWindow window in mWindows)
+            {
+                if ((window.mVisible) && ((window.mIsDirty) || (mAutoDirty)))
+                {
+                    window.PreDraw(mGraphics);
+					if (mColorMatrix != null)
+					{
+#if DEBUG
+						mColorMatrix = Matrix4(
+							0.90f,  0.10f,  0.00f, 0,
+							0.00f,  0.90f,  0.10f, 0,
+							0.10f,  0.00f,  1.05f, 0,
+							    0,      0,      0, 1);
+#else
+						mColorMatrix = Matrix4(
+							0.90f,  0.10f,  0.00f, 0,
+							0.00f,  1.05f,  0.10f, 0,
+							0.10f,  0.00f,  0.90f, 0,
+							    0,      0,      0, 1);
+#endif
+						mGraphics.SetShaderConstantData(0, &mColorMatrix.ValueRef, mColorMatrixDataDef);
+					}
+                    window.Draw(mGraphics);
+                    window.PostDraw(mGraphics);
+					window.mIsDirty = false;
+                }
+            }
+            mGraphics.EndDraw();
+
+#if STUDIO_CLIENT            
+            PerfTimer.Log("Sending FrameDone");
+            mStudioHost.Proxy.FrameDone();
+            //mFrameDonesSent++;
+#endif
+
+            PerfTimer.ZoneEnd();                        
+        }
+
+		public void MarkDirty()
+		{
+			for (var window in mWindows)
+			{
+				if (var widgetWindow = window as WidgetWindow)
+				{
+					widgetWindow.mIsDirty = true;
+				}
+			}	
+		}
+
+        Cursor curCursor;
+        public virtual void SetCursor(Cursor cursor)
+        {
+            if (curCursor == cursor)
+                return;            
+            curCursor = cursor;            
+            BFApp_SetCursor((int32)cursor);
+        }
+
+        public virtual void* GetClipboardData(String format, out int32 size)
+        {
+            return BFApp_GetClipboardData(format, out size);
+        }
+
+        public virtual void ReleaseClipboardData(void* ptr)
+        {
+            BFApp_ReleaseClipboardData(ptr);
+        }
+
+        public virtual bool GetClipboardText(String outStr)
+        {
+			int32 aSize;
+            void* clipboardData = GetClipboardData("text", out aSize);
+            if (clipboardData == null)
+                return false;
+
+            outStr.Append((char8*)clipboardData);
+			ReleaseClipboardData(clipboardData);
+			return true;
+        }
+
+        public virtual void SetClipboardData(String format, void* ptr, int32 size, bool resetClipboard)
+        {
+            BFApp_SetClipboardData(format, ptr, size, resetClipboard ? 1 : 0);
+        }
+
+        public virtual void SetClipboardText(String text)
+        {
+            //IntPtr aPtr = Marshal.StringToCoTaskMemUni(text);
+            SetClipboardData("text", text.CStr(), (int32)text.Length + 1, true);
+        }
+
+#if STUDIO_CLIENT
+        public int GetProcessId()
+        {
+            return Process.GetCurrentProcess().Id;
+        }
+#endif	 
+		
+		public void RehupMouse()
+		{
+			BFApp_RehupMouse();
+		}
+    }
+}

+ 772 - 0
BeefLibs/Beefy2D/src/BFWindow.bf

@@ -0,0 +1,772 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Runtime.InteropServices;
+using System.Diagnostics;
+using Beefy.gfx;
+using Beefy.sys;
+using System.IO;
+
+#if MONOTOUCH
+using MonoTouch;
+#endif
+
+#if STUDIO_CLIENT
+using Beefy.ipc;
+#endif
+
+namespace Beefy
+{
+    public class BFWindowBase
+    {
+        public enum Flags
+        {
+            Border = 0x000001,
+            ThickFrame = 0x000002,
+            Resizable = 0x000004,
+            SysMenu = 0x000008,
+            Caption = 0x000010,
+            Minimize = 0x000020,
+            Maximize = 0x000040,
+            ClientSized = 0x000080,
+            QuitOnClose = 0x000100,
+            VSync = 0x000200,
+            PopupPosition = 0x000400,
+            DestAlpha = 0x000800,
+            AlphaMask = 0x001000,
+            Child = 0x002000,
+            TopMost = 0x004000,
+            ToolWindow = 0x008000,
+            NoActivate = 0x01'0000,
+            NoMouseActivate = 0x02'0000,
+            Menu = 0x04'0000,
+            Modal = 0x08'0000,
+            ScaleContent = 0x10'0000,
+            UseParentMenu = 0x20'0000,
+			CaptureMediaKeys = 0x40'0000,
+			Fullscreen = 0x80'0000,
+			FakeFocus = 0x0100'0000
+        };
+
+        public enum HitTestResult
+        {
+            NotHandled = -3,
+
+            Border = 18,
+            Bottom = 15,
+            BottomLeft = 16,
+            BottomRight = 17,
+            Caption = 2,
+            Client = 1,
+            Close = 20,
+            Error = -2,
+            GrowBox = 4,
+            Help = 21,
+            HScroll = 6,
+            Left = 10,
+            Menu = 5,
+            MaxButton = 9,
+            MinButton = 8,
+            NoWhere = 0,
+            Reduce = 8,
+            Right = 11,
+            Size = 4,
+            SysMenu = 3,
+            Top = 12,
+            TopLeft = 13,
+            TopRight = 14,
+            Transparent = -1,
+            VScroll = 7,
+            Zoom = 9            
+        }
+
+        public SysMenu mSysMenu ~ delete _;
+        public Dictionary<int, SysMenu> mSysMenuMap = new Dictionary<int, SysMenu>() ~ delete _;
+        public DrawLayer mDefaultDrawLayer ~ delete _;
+
+        public virtual void Draw(Graphics g)
+        {
+            
+        }
+
+        public virtual void PreDraw(Graphics g)
+        {
+            g.PushDrawLayer(mDefaultDrawLayer);            
+        }
+
+        public virtual void PostDraw(Graphics g)
+        {
+            g.PopDrawLayer();
+        }
+    }
+#if !STUDIO_CLIENT
+    public class BFWindow : BFWindowBase, INativeWindow
+    {
+        delegate void NativeMovedDelegate(void* window);
+        delegate int32 NativeCloseQueryDelegate(void* window);
+        delegate void NativeClosedDelegate(void* window);
+        delegate void NativeGotFocusDelegate(void* window);
+        delegate void NativeLostFocusDelegate(void* window);
+        delegate void NativeKeyCharDelegate(void* window, char32 theChar);
+		delegate bool NativeKeyDownDelegate(void* window, int32 keyCode, int32 isRepeat);
+        delegate void NativeKeyUpDelegate(void* window, int32 keyCode);
+        delegate int32 NativeHitTestDelegate(void* window, int32 x, int32 y);
+        delegate void NativeMouseMoveDelegate(void* window, int32 x, int32 y);
+        delegate void NativeMouseProxyMoveDelegate(void* window, int32 x, int32 y);
+        delegate void NativeMouseDownDelegate(void* window, int32 x, int32 y, int32 btn, int32 btnCount);
+        delegate void NativeMouseUpDelegate(void* window, int32 x, int32 y, int32 btn);
+        delegate void NativeMouseWheelDelegate(void* window, int32 x, int32 y, int32 delta);
+        delegate void NativeMouseLeaveDelegate(void* window);
+        delegate void NativeMenuItemSelectedDelegate(void* window, void* menu);        
+
+        public void* mNativeWindow;
+		public bool mNativeWindowClosed;
+
+		static int32 sId;
+		protected int32 mId = ++sId;
+
+		public String mName ~ delete _;
+        public String mTitle ~ delete _;
+        public int32 mX;
+        public int32 mY;
+        public int32 mWindowWidth;
+        public int32 mWindowHeight;
+        public int32 mClientX;
+        public int32 mClientY;
+        public int32 mClientWidth;
+        public int32 mClientHeight;
+        public float mAlpha = 1.0f;
+        public Flags mWindowFlags;
+        public bool mVisible = true;
+        private bool mMouseVisible;
+        public bool mHasFocus = false;        
+        public bool mHasClosed;
+		public bool mIsDirty = true;
+        public BFWindow mParent;
+        public List<BFWindow> mChildWindows = new List<BFWindow>() ~ delete _;
+
+        static protected Dictionary<int, BFWindow> sWindowDictionary = new Dictionary<int, BFWindow>() ~ delete _;
+
+        static NativeMovedDelegate sNativeMovedDelegate ~ delete _;
+        static NativeCloseQueryDelegate sNativeCloseQueryDelegate ~ delete _;
+        static NativeClosedDelegate sNativeClosedDelegate ~ delete _;
+        static NativeGotFocusDelegate sNativeGotFocusDelegate ~ delete _;
+        static NativeLostFocusDelegate sNativeLostFocusDelegate ~ delete _;
+        static NativeKeyCharDelegate sNativeKeyCharDelegate ~ delete _;
+		static NativeKeyDownDelegate sNativeKeyDownDelegate ~ delete _;
+        static NativeKeyUpDelegate sNativeKeyUpDelegate ~ delete _;
+        static NativeHitTestDelegate sNativeHitTestDelegate ~ delete _;
+        static NativeMouseMoveDelegate sNativeMouseMoveDelegate ~ delete _;
+        static NativeMouseProxyMoveDelegate sNativeMouseProxyMoveDelegate ~ delete _;
+        static NativeMouseDownDelegate sNativeMouseDownDelegate ~ delete _;
+        static NativeMouseUpDelegate sNativeMouseUpDelegate ~ delete _;
+        static NativeMouseWheelDelegate sNativeMouseWheelDelegate ~ delete _;
+        static NativeMouseLeaveDelegate sNativeMouseLeaveDelegate ~ delete _;
+        static NativeMenuItemSelectedDelegate sNativeMenuItemSelectedDelegate ~ delete _;
+
+        [StdCall, CLink]
+        static extern void* BFApp_CreateWindow(void* parent, char8* title, int32 x, int32 y, int32 width, int32 height, int32 windowFlags);
+
+		[StdCall, CLink]
+		static extern void* BFWindow_GetNativeUnderlying(void* window);
+
+        [StdCall, CLink]
+        static extern void BFWindow_SetCallbacks(void* window, void* movedDelegate, void* closeQueryDelegate, void* closedDelegate, 
+            void* gotFocusDelegate, void* lostFocusDelegate,
+			void* keyCharDelegate, void* keyDownDelegate, void* keyUpDelegate, void* hitTestDelegate,
+            void* mouseMoveDelegate, void* mouseProxyMoveDelegate, void* mouseDownDelegate, void* mouseUpDelegate, void* mouseWheelDelegate, void* mouseLeaveDelegate,
+            void* menuItemSelectedDelegate);
+
+		[StdCall, CLink]
+		static extern void BFWindow_SetTitle(void* window, char8* title);
+
+        [StdCall, CLink]
+        static extern void BFWindow_SetMinimumSize(void* window, int32 minWidth, int32 minHeight, bool clientSized);
+
+        [StdCall, CLink]
+        static extern void BFWindow_GetPosition(void* window, out int32 x, out int32 y, out int32 width, out int32 height, out int32 clientX, out int32 clientY, out int32 clientWidth, out int32 clientHeight);
+
+        [StdCall, CLink]
+        static extern void BFWindow_Resize(void* window, int32 x, int32 y, int32 width, int32 height);
+
+        [StdCall, CLink]
+        static extern void BFWindow_Close(void* window, int32 force);
+
+        [StdCall, CLink]
+        static extern void BFWindow_SetForeground(void* window);
+
+        [StdCall, CLink]
+        static extern void BFWindow_LostFocus(void* window, void* newFocus);
+
+        [StdCall, CLink]
+        static extern void BFWindow_SetNonExclusiveMouseCapture(void* window);
+
+        [StdCall, CLink]
+        static extern void BFWindow_SetClientPosition(void* window, int32 x, int32 y);
+
+        [StdCall, CLink]
+        static extern void BFWindow_SetAlpha(void* window, float alpha, uint32 destAlphaSrcMask, int32 mouseVisible);
+
+        [StdCall, CLink]
+        static extern void BFWindow_SetMouseVisible(void* window, bool mouseVisible);
+
+		[StdCall, CLink]
+		static extern void BFWindow_CaptureMouse(void* window);
+
+		[StdCall, CLink]
+		static extern bool BFWindow_IsMouseCaptured(void* window);
+
+        [StdCall, CLink]
+        static extern void* BFWindow_AddMenuItem(void* window, void* parent, int32 insertIdx, char8* text, char8* hotKey, void* bitmap, int32 enabled, int32 checkState, int32 radioCheck);
+
+        [StdCall, CLink]
+        static extern void* BFWindow_ModifyMenuItem(void* window, void* item, char8* text, char8* hotKey, void* bitmap, int32 enabled, int32 checkState, int32 radioCheck);
+
+        [StdCall, CLink]
+        static extern void* BFWindow_DeleteMenuItem(void* window, void* item);
+
+        public static BFWindow GetBFWindow(void* window)
+        {
+            return sWindowDictionary[(int)window];
+        }
+
+		#if MONOTOUCH
+		[MonoPInvokeCallback(typeof(NativeMovedDelegate))]
+		static void Static_NativeMovedDelegate(void* window) { GetBFWindow(window).Moved(); }
+		[MonoPInvokeCallback(typeof(NativeClosedDelegate))]
+		static void Static_NativeClosedDelegate(void* window) { GetBFWindow(window).Closed(); }
+		[MonoPInvokeCallback(typeof(NativeCloseQueryDelegate))]
+		static int Static_NativeCloseQueryDelegate(void* window) { return GetBFWindow(window).CloseQuery(); }
+		[MonoPInvokeCallback(typeof(NativeGotFocusDelegate))]
+		static void Static_NativeGotFocusDelegate(void* window) { GetBFWindow(window).GotFocus(); }
+		[MonoPInvokeCallback(typeof(NativeLostFocusDelegate))]
+		static void Static_NativeLostFocusDelegate(void* window) { GetBFWindow(window).LostFocus(); }
+		[MonoPInvokeCallback(typeof(NativeKeyCharDelegate))]
+		static void Static_NativeKeyCharDelegate(void* window, char8 c) { GetBFWindow(window).KeyChar(c); }
+		[MonoPInvokeCallback(typeof(NativeKeyDownDelegate))]
+		static bool Static_NativeKeyDownDelegate(void* window, int key, int isRepeat) { return GetBFWindow(window).KeyDown(key, isRepeat); }
+		[MonoPInvokeCallback(typeof(NativeKeyUpDelegate))]
+		static void Static_NativeKeyUpDelegate(void* window, int key) { GetBFWindow(window).KeyUp(key); }
+		[MonoPInvokeCallback(typeof(NativeMouseMoveDelegate))]
+		static void Static_NativeMouseMoveDelegate(void* window, int mouseX, int mouseY) { GetBFWindow(window).MouseMove(mouseX, mouseY); }
+		[MonoPInvokeCallback(typeof(NativeMouseProxyMoveDelegate))]
+		static void Static_NativeMouseProxyMoveDelegate(void* window, int mouseX, int mouseY) { GetBFWindow(window).MouseProxyMove(mouseX, mouseY); }
+		[MonoPInvokeCallback(typeof(NativeMouseDownDelegate))]
+		static void Static_NativeMouseDownDelegate(void* window, int mouseX, int mouseY, int btnNum, int btnCount) { GetBFWindow(window).MouseDown(mouseX, mouseY, btnNum, btnCount); }
+		[MonoPInvokeCallback(typeof(NativeMouseUpDelegate))]
+		static void Static_NativeMouseUpDelegate(void* window, int mouseX, int mouseY, int btnNum) { GetBFWindow(window).MouseUp(mouseX, mouseY, btnNum); }
+		[MonoPInvokeCallback(typeof(NativeMouseWheelDelegate))]
+		static void Static_NativeMouseWheelDelegate(void* window, int mouseX, int mouseY, int delta) { GetBFWindow(window).MouseWheel(mouseX, mouseY, delta); }
+		[MonoPInvokeCallback(typeof(NativeMouseLeaveDelegate))]
+		static void Static_NativeMouseLeaveDelegate(void* window) { GetBFWindow(window).MouseLeave(); }
+		[MonoPInvokeCallback(typeof(NativeMenuItemSelectedDelegate))]
+		static void Static_NativeMenuItemSelectedDelegate(void* window, void* item) { GetBFWindow(window).NativeMenuItemSelected(item); }
+		#else
+		static void Static_NativeMovedDelegate(void* window) { GetBFWindow(window).Moved(); }
+		static void Static_NativeClosedDelegate(void* window) { GetBFWindow(window).Closed(); }
+		static int32 Static_NativeCloseQueryDelegate(void* window) { return GetBFWindow(window).CloseQuery(); }
+		static void Static_NativeGotFocusDelegate(void* window) { GetBFWindow(window).GotFocus(); }
+		static void Static_NativeLostFocusDelegate(void* window) { GetBFWindow(window).LostFocus(null); }
+		static void Static_NativeKeyCharDelegate(void* window, char32 c) { GetBFWindow(window).KeyChar(c); }
+		static bool Static_NativeKeyDownDelegate(void* window, int32 key, int32 isRepeat) { return GetBFWindow(window).KeyDown(key, isRepeat); }
+		static void Static_NativeKeyUpDelegate(void* window, int32 key) { GetBFWindow(window).KeyUp(key); }
+        static int32 Static_NativeHitTestDelegate(void* window, int32 x, int32 y) { return (int32)GetBFWindow(window).HitTest(x, y); }        
+        static void Static_NativeMouseMoveDelegate(void* window, int32 mouseX, int32 mouseY) { GetBFWindow(window).MouseMove(mouseX, mouseY); }
+		static void Static_NativeMouseProxyMoveDelegate(void* window, int32 mouseX, int32 mouseY) { GetBFWindow(window).MouseProxyMove(mouseX, mouseY); }
+		static void Static_NativeMouseDownDelegate(void* window, int32 mouseX, int32 mouseY, int32 btnNum, int32 btnCount) { GetBFWindow(window).MouseDown(mouseX, mouseY, btnNum, btnCount); }
+		static void Static_NativeMouseUpDelegate(void* window, int32 mouseX, int32 mouseY, int32 btnNum) { GetBFWindow(window).MouseUp(mouseX, mouseY, btnNum); }
+		static void Static_NativeMouseWheelDelegate(void* window, int32 mouseX, int32 mouseY, int32 delta) { GetBFWindow(window).MouseWheel(mouseX, mouseY, delta); }
+		static void Static_NativeMouseLeaveDelegate(void* window) { GetBFWindow(window).MouseLeave(); }
+		static void Static_NativeMenuItemSelectedDelegate(void* window, void* item) { GetBFWindow(window).NativeMenuItemSelected(item); }
+		#endif
+
+		public this()
+		{
+		}
+
+        public this(BFWindow parent, String title, int x, int y, int width, int height, BFWindow.Flags windowFlags)
+		{
+			Init(parent, title, x, y, width, height, windowFlags);
+		}
+
+		public ~this()
+		{
+			bool worked = sWindowDictionary.Remove((int)mNativeWindow);
+			Debug.Assert(worked);
+		}
+
+		void Init(BFWindow parent, String title, int x, int y, int width, int height, BFWindow.Flags windowFlags)
+        {
+            mTitle = new String(title);
+            mParent = parent;
+            if (mParent != null)
+                mParent.mChildWindows.Add(this);
+
+			var useFlags = windowFlags;
+			/*if (useFlags.HasFlag(.FakeFocus))
+			{
+				useFlags |= .NoActivate;
+				useFlags |= .NoMouseActivate;
+			}*/
+
+            mNativeWindow = BFApp_CreateWindow((parent != null) ? (parent.mNativeWindow) : null, title, (int32)x, (int32)y, (int32)width, (int32)height, (int32)useFlags);
+            sWindowDictionary[(int)mNativeWindow] = this;
+
+            if (sNativeMovedDelegate == null)
+            {
+				sNativeMovedDelegate = new => Static_NativeMovedDelegate;
+				sNativeClosedDelegate = new => Static_NativeClosedDelegate;
+				sNativeCloseQueryDelegate = new => Static_NativeCloseQueryDelegate;
+				sNativeGotFocusDelegate = new => Static_NativeGotFocusDelegate;
+				sNativeLostFocusDelegate = new => Static_NativeLostFocusDelegate;
+				sNativeKeyCharDelegate = new => Static_NativeKeyCharDelegate;
+				sNativeKeyDownDelegate = new => Static_NativeKeyDownDelegate;
+				sNativeKeyUpDelegate = new => Static_NativeKeyUpDelegate;
+                sNativeHitTestDelegate = new => Static_NativeHitTestDelegate;
+				sNativeMouseMoveDelegate = new => Static_NativeMouseMoveDelegate;
+				sNativeMouseProxyMoveDelegate = new => Static_NativeMouseProxyMoveDelegate;
+				sNativeMouseDownDelegate = new => Static_NativeMouseDownDelegate;
+				sNativeMouseUpDelegate = new => Static_NativeMouseUpDelegate;
+				sNativeMouseWheelDelegate = new => Static_NativeMouseWheelDelegate;
+				sNativeMouseLeaveDelegate = new => Static_NativeMouseLeaveDelegate;
+				sNativeMenuItemSelectedDelegate = new => Static_NativeMenuItemSelectedDelegate;
+            }
+
+            BFWindow_SetCallbacks(mNativeWindow, sNativeMovedDelegate.GetFuncPtr(), sNativeCloseQueryDelegate.GetFuncPtr(), sNativeClosedDelegate.GetFuncPtr(), sNativeGotFocusDelegate.GetFuncPtr(), sNativeLostFocusDelegate.GetFuncPtr(),
+                sNativeKeyCharDelegate.GetFuncPtr(), sNativeKeyDownDelegate.GetFuncPtr(), sNativeKeyUpDelegate.GetFuncPtr(), sNativeHitTestDelegate.GetFuncPtr(),
+                sNativeMouseMoveDelegate.GetFuncPtr(), sNativeMouseProxyMoveDelegate.GetFuncPtr(), sNativeMouseDownDelegate.GetFuncPtr(), sNativeMouseUpDelegate.GetFuncPtr(), sNativeMouseWheelDelegate.GetFuncPtr(), sNativeMouseLeaveDelegate.GetFuncPtr(),
+                sNativeMenuItemSelectedDelegate.GetFuncPtr());            
+            BFApp.sApp.mWindows.Add(this);
+
+            mDefaultDrawLayer = new DrawLayer(this);
+
+            if (windowFlags.HasFlag(Flags.Menu))
+            {
+                mSysMenu = new SysMenu();
+                mSysMenu.mWindow = this;                
+            }
+            mWindowFlags = windowFlags;
+
+            if ((parent != null) && (!mWindowFlags.HasFlag(Flags.NoActivate)))
+                parent.LostFocus(this);
+
+            if ((parent != null) && (mWindowFlags.HasFlag(Flags.Modal)))
+                parent.PreModalChild();
+
+			if ((mWindowFlags.HasFlag(.FakeFocus)) && (!mWindowFlags.HasFlag(.NoActivate)))
+				GotFocus();
+
+			BFApp.sApp.RehupMouse();
+        }
+
+		public void SetTitle(String title)
+		{
+			mTitle.Set(title);
+			BFWindow_SetTitle(mNativeWindow, mTitle);
+		}
+
+		public int Handle
+		{
+			get
+			{
+				return (int)BFWindow_GetNativeUnderlying(mNativeWindow);
+			}
+		}
+
+#if BF_PLATFORM_WINDOWS
+		public Windows.HWnd HWND
+		{
+			get
+			{
+				return (.)(int)BFWindow_GetNativeUnderlying(mNativeWindow);
+			}
+		}
+#endif
+
+        public void PreModalChild()
+        {
+            //MouseLeave();
+        }
+
+        public virtual void Dispose()
+        {            
+            Close(true);            
+        }
+
+        public virtual int32 CloseQuery()
+        {
+            return 1;
+        }
+
+        public virtual void Close(bool force = false)
+        {
+			// This doesn't play properly with CloseQuery.  We may do a force close in CloseQuery, but that fails
+			//  if we do this following logic:
+			/*if (mNativeWindowClosed)
+				return;
+			mNativeWindowClosed = true;*/
+
+			while (mChildWindows.Count > 0)	
+				mChildWindows[mChildWindows.Count - 1].Close(force);
+
+			//for (var childWindow in mChildWindows)
+				//childWindow.Close(force);
+				//mChildWindows[mChildWindows.Count - 1].Close(force);
+
+            if (mNativeWindow != null)
+            {
+                BFWindow_Close(mNativeWindow, force ? 1 : 0);
+            }
+			else
+			{
+				Closed();
+			}
+        }
+
+        public virtual void Resize(int x, int y, int width, int height)
+        {
+            Debug.Assert(mNativeWindow != null);
+            BFWindow_Resize(mNativeWindow, (int32)x, (int32)y, (int32)width, (int32)height);
+        }
+
+        public void SetForeground()
+        {
+            BFWindow_SetForeground(mNativeWindow);
+            GotFocus();
+        }
+
+        public void SetNonExclusiveMouseCapture()
+        {
+            // Does checking of mouse coords against all window even when this window has mouse capture,
+            //  helps some dragging scenarios.  Gets turned off automatically on mouse up.
+            BFWindow_SetNonExclusiveMouseCapture(mNativeWindow);            
+        }
+
+        public virtual void Closed()
+        {
+			if (mHasClosed)
+				return;
+			mHasClosed = true;
+
+            bool hadFocus = mHasFocus;
+
+            BFApp.sApp.mWindows.Remove(this);
+
+            if (mWindowFlags.HasFlag(Flags.QuitOnClose))
+                BFApp.sApp.Stop();
+            
+            if (mParent != null)
+            {
+                mParent.mChildWindows.Remove(this);
+                if ((hadFocus) && (mWindowFlags.HasFlag(Flags.Modal)))
+                    mParent.GotFocus();
+            }
+
+			DeleteAndNullify!(mDefaultDrawLayer);
+			BFApp.sApp.DeferDelete(this);
+        }
+
+        public void SetMinimumSize(int32 minWidth, int32 minHeight, bool clientSized = false)
+        {
+            BFWindow_SetMinimumSize(mNativeWindow, minWidth, minHeight, clientSized);
+        }
+
+        public virtual void Moved()
+        {            
+            BFWindow_GetPosition(mNativeWindow, out mX, out mY, out mWindowWidth, out mWindowHeight, out mClientX, out mClientY, out mClientWidth, out mClientHeight);
+			mIsDirty = true;
+        }
+
+        public virtual void SetClientPosition(float x, float y)
+        {
+            mClientX = (int32)x;
+            mClientY = (int32)y;
+            BFWindow_SetClientPosition(mNativeWindow, (int32)x, (int32)y);
+        }
+
+        public virtual void SetAlpha(float alpha, uint32 destAlphaSrcMask, bool mouseVisible)
+        {
+            mAlpha = alpha;
+            BFWindow_SetAlpha(mNativeWindow, alpha, destAlphaSrcMask, mouseVisible ? 1 : 0);
+        }
+
+        public virtual void SetMouseVisible(bool mouseVisible)
+        {
+            mMouseVisible = mouseVisible;
+            BFWindow_SetMouseVisible(mNativeWindow, mouseVisible);
+        }
+
+		public virtual void CaptureMouse()
+		{
+			BFWindow_CaptureMouse(mNativeWindow);
+		}
+
+		public bool IsMouseCaptured()
+		{
+			return BFWindow_IsMouseCaptured(mNativeWindow);
+		}
+
+        public virtual void* AddMenuItem(void* parent, int insertIdx, String text, String hotKey, void* bitmap, bool enabled, int checkState, bool radioCheck)
+        {
+            return BFWindow_AddMenuItem(mNativeWindow, parent, (int32)insertIdx, text, hotKey, bitmap, enabled ? 1 : 0, (int32)checkState, radioCheck ? 1 : 0);
+        }
+
+        public virtual void ModifyMenuItem(void* item, String text, String hotKey, void* bitmap, bool enabled, int checkState, bool radioCheck)
+        {
+            BFWindow_ModifyMenuItem(mNativeWindow, item, text, hotKey, bitmap, enabled ? 1 : 0, (int32)checkState, radioCheck ? 1 : 0);
+        }
+
+        public virtual void DeleteMenuItem(void* menuItem)
+        {
+            BFWindow_DeleteMenuItem(mNativeWindow, menuItem);
+        }
+
+        public virtual void MenuItemSelected(SysMenu sysMenu)
+        {
+            sysMenu.Selected();
+        }
+
+        public virtual void NativeMenuItemSelected(void* menu)
+        {
+            SysMenu aSysMenu = mSysMenuMap[(int)menu];
+            MenuItemSelected(aSysMenu);                      
+        }
+
+        public virtual SysBitmap LoadSysBitmap(String path)
+        {
+            return null;
+        }
+
+        public virtual void GotFocus()
+        {
+            if (mHasFocus)
+                return;
+            mHasFocus = true;
+            //Console.WriteLine("GotFocus {0}", mTitle);
+        }
+
+        public virtual void LostFocus(BFWindow newFocus)
+        {			
+            if (!mHasFocus)
+                return;
+			mHasFocus = false;
+            if (mNativeWindow != null)
+                BFWindow_LostFocus(mNativeWindow, (newFocus != null) ? newFocus.mNativeWindow : null);            
+
+			//TODO: REMOVE
+            //Debug.WriteLine("LostFocus {0}", mTitle);
+        }
+        
+        public virtual void KeyChar(char32 theChar)
+        {
+        }
+
+        public virtual bool KeyDown(int32 keyCode, int32 isRepeat)
+        {
+            return false;
+        }
+
+        public virtual void KeyUp(int32 keyCode)
+        {
+			
+        }
+
+        public virtual HitTestResult HitTest(int32 x, int32 y)
+        {
+            return HitTestResult.NotHandled;
+        }
+
+        public virtual void MouseMove(int32 x, int32 y)
+        {
+        }
+
+        public virtual void MouseProxyMove(int32 x, int32 y)
+        {
+        }
+
+        public virtual void MouseDown(int32 x, int32 y, int32 btn, int32 btnCount)
+        {
+        }
+
+        public virtual void MouseUp(int32 x, int32 y, int32 btn)
+        {
+        }
+
+        public virtual void MouseWheel(int32 x, int32 y, int32 delta)
+        {
+        }
+
+        public virtual void MouseLeave()
+        {
+        }
+
+        public override void Draw(Graphics g)
+        {
+        }
+
+        public virtual void Update()
+        {            
+        }
+    }
+#else
+    public class BFWindow : BFWindowBase, IStudioClientWindow
+    {        
+        //IStudioWidgetWindow mProxy;
+        public int mClientWidth;
+        public int mClientHeight;
+        public int mClientX;
+        public int mClientY;
+        public float mAlpha = 1.0f;
+        public bool mVisible = true;
+        public Flags mWindowFlags;
+        
+        public IPCProxy<IStudioHostWindow> mRemoteWindow;
+        public IPCEndpoint<IStudioClientWindow> mStudioClientWindow;
+
+        public BFWindow(BFWindow parent, string title, int x, int y, int width, int height, BFWindow.Flags windowFlags)
+        {
+            mStudioClientWindow = IPCEndpoint<IStudioClientWindow>.Create(this);
+
+            IStudioHost studioInstance = BFApp.sApp.mStudioHost.Proxy;
+            IPCObjectId remoteWindowId = studioInstance.CreateWindow(mStudioClientWindow.ObjId, 0, title, x, y, width, height, (int)windowFlags);
+            mRemoteWindow = IPCProxy<IStudioHostWindow>.Create(remoteWindowId);
+
+            mDefaultDrawLayer = new DrawLayer(this);
+
+            BFApp.sApp.mWindows.Add(this);
+
+            mClientX = 0;
+            mClientY = 0;
+            mClientWidth = width;
+            mClientHeight = height;
+
+            mWindowFlags = windowFlags;
+        }
+
+        public void Dispose()
+        {
+            Close();
+        }
+
+        public virtual int CloseQuery()
+        {
+            return 1;
+        }
+
+        public virtual void Close(bool force = false)
+        {
+            //if (mNativeWindow != null)
+                //BFWindow_Close(mNativeWindow, force ? 1 : 0);
+        }
+
+        public virtual void Closed()
+        {
+            //mNativeWindow = null;
+            BFApp.sApp.mWindows.Remove(this);
+        }
+
+        public virtual void Moved()
+        {
+            //BFWindow_GetPosition(mNativeWindow, out mX, out mY, out mWindowWidth, out mWindowHeight, out mClientX, out mClientY, out mClientWidth, out mClientHeight);
+        }
+
+        public virtual void SetClientPosition(float x, float y)
+        {
+            //BFWindow_SetClientPosition(mNativeWindow, (int)x, (int)y);
+        }
+
+        public virtual void SetAlpha(float alpha, uint destAlphaSrcMask, bool mouseVisible)
+        {
+            //mAlpha = alpha;
+            //BFWindow_SetAlpha(mNativeWindow, alpha, destAlphaSrcMask, mouseVisible ? 1 : 0);
+        }
+
+        public virtual void* AddMenuItem(void* parent, string text, string hotKey, void* bitmap, bool enabled, int checkState, bool radioCheck)
+        {
+            //return BFWindow_AddMenuItem(mNativeWindow, parent, text, hotKey, bitmap, enabled ? 1 : 0, checkState, radioCheck ? 1 : 0);
+            return null;
+        }
+
+        public virtual void MenuItemSelected(SysMenu sysMenu)
+        {
+            sysMenu.Selected();
+        }
+
+        public virtual void NativeMenuItemSelected(void* menu)
+        {
+            //SysMenu aSysMenu = mSysMenuMap[menu];
+            //MenuItemSelected(aSysMenu);
+        }
+
+        public virtual SysBitmap LoadSysBitmap(string path)
+        {
+            return null;
+        }
+
+        public virtual void GotFocus()
+        {
+        }
+
+        public virtual void LostFocus()
+        {
+        }
+
+        public virtual void KeyChar(char32 theChar)
+        {
+        }        
+
+        public virtual void KeyDown(int keyCode, int isRepeat)
+        {
+        }
+
+        public virtual void KeyUp(int keyCode)
+        {
+        }
+
+        public virtual void MouseMove(int x, int y)
+        {
+        }
+
+        public virtual void MouseProxyMove(int x, int y)
+        {
+        }
+
+        public virtual void MouseDown(int x, int y, int btn, int btnCount)
+        {
+        }
+
+        public virtual void MouseUp(int x, int y, int btn)
+        {
+        }
+
+        public virtual void MouseWheel(int x, int y, int delta)
+        {
+        }
+
+        public virtual void MouseLeave()
+        {
+        }
+                
+        public virtual void Update()
+        {
+        }
+
+        public override void PreDraw(Graphics g)
+        {                        
+            base.PreDraw(g);
+            mRemoteWindow.Proxy.RemoteDrawingStarted();
+        }
+
+        public override void PostDraw(Graphics g)
+        {
+            base.PostDraw(g);
+            mRemoteWindow.Proxy.RemoteDrawingDone();
+        }
+
+        public void SetIsVisible(bool visible)
+        {
+            mVisible = visible;
+        }
+    }
+#endif
+}

+ 32 - 0
BeefLibs/Beefy2D/src/Rand.bf

@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Beefy
+{
+    public class Rand
+    {
+        static Random sRand = new Random() ~ delete _;
+
+        public static int32 Int()
+        {
+            return sRand.NextI32();
+        }
+
+        public static int32 SInt()
+        {
+            return sRand.Next(int32.MinValue, int32.MaxValue);
+        }
+
+        public static float Float()
+        {
+            return (float)sRand.NextDouble();
+        }
+
+        public static float SFloat()
+        {
+            return (float)(sRand.NextDouble() * 2) - 1.0f;
+        }
+    }
+}

+ 447 - 0
BeefLibs/Beefy2D/src/Utils.bf

@@ -0,0 +1,447 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Runtime.InteropServices;
+using System.Reflection;
+using System.Threading;
+using System.IO;
+using System.Diagnostics;
+using System.Security.Cryptography;
+
+namespace Beefy
+{
+    public static class Utils
+    {
+        static Random mRandom = new Random() ~ delete _;
+
+        [StdCall, CLink]
+        static extern int32 BF_TickCount();
+
+        [StdCall, CLink]
+        static extern int32 BF_TickCountMicroFast();
+
+        public static float Deg2Rad = Math.PI_f / 180.0f;
+
+        
+
+        public static int32 Rand()
+        {
+            return mRandom.NextI32();
+        }
+
+		public static float RandFloat()
+		{
+			return (Rand() & 0xFFFFFF) / (float)0xFFFFFF;
+		}
+
+		public static float Interpolate(float left, float right, float pct)
+		{
+			return left + (right - left) * pct;
+		}
+
+        public static float Clamp(float val, float min, float max)
+        {
+            return Math.Max(min, Math.Min(max, val));
+        }
+
+        public static float Lerp(float val1, float val2, float pct)
+        {
+            return val1 + (val2 - val1) * pct;
+        }
+
+        public static float EaseInAndOut(float pct)
+        {
+            return ((-Math.Cos(pct * Math.PI_f) + 1.0f) / 2.0f);
+        }
+
+        public static float Distance(float dX, float dY)
+        {
+            return Math.Sqrt(dX * dX + dY * dY);
+        }
+
+        public static char8 CtrlKeyChar(char32 theChar)
+        {
+            char8 aChar = (char8)(theChar + (int)'A' - (char8)1);
+            if (aChar < (char8)0)
+                return (char8)0;
+            return aChar;
+        }
+
+        public static uint32 GetTickCount()
+        {
+            return (uint32)BF_TickCount();
+        }
+
+        public static uint64 GetTickCountMicro()
+        {
+            return (uint32)BF_TickCountMicroFast();
+        }
+
+        public static Object DefaultConstruct(Type theType)
+        {
+			ThrowUnimplemented();
+
+            /*ConstructorInfo constructor = theType.GetConstructors()[0];
+
+            ParameterInfo[] paramInfos = constructor.GetParameters();
+            object[] aParams = new object[paramInfos.Length];
+
+            for (int paramIdx = 0; paramIdx < aParams.Length; paramIdx++)
+                aParams[paramIdx] = paramInfos[paramIdx].DefaultValue;
+
+            object newObject = constructor.Invoke(aParams);
+            return newObject;*/
+        }
+
+        /*public static int StrToInt(string theString)
+        {
+            if (theString.StartsWith("0X", StringComparison.OrdinalIgnoreCase))
+                return Convert.ToInt32(theString.Substring(2), 16);
+            return Convert.ToInt32(theString);
+        }
+
+        // WaitForEvent differs from theEvent.WaitOne in that it doesn't pump the Windows
+        //  message loop under STAThread
+        public static bool WaitForEvent(EventWaitHandle theEvent, int timeMS)
+        {
+            return WaitForSingleObject(theEvent.SafeWaitHandle, timeMS) == 0;
+        }*/
+
+        
+        public static void GetDirWithSlash(String dir)
+        {
+			if (dir.IsEmpty)
+				return;
+            char8 endChar = dir[dir.Length - 1];
+            if ((endChar != Path.DirectorySeparatorChar) && (endChar != Path.AltDirectorySeparatorChar))
+                dir.Append(Path.DirectorySeparatorChar);
+        }
+
+        public static Result<void> DelTree(String path, Predicate<String> fileFilter = null)
+        {
+			if (path.Length <= 2)
+				return .Err;
+			if ((path[0] != '/') && (path[0] != '\\'))
+			{
+				if (path[1] == ':')
+				{
+					if (path.Length < 3)
+						return .Err;
+				}
+				else
+					return .Err;
+			}
+
+            for (var fileEntry in Directory.EnumerateDirectories(path))
+            {
+				let fileName = scope String();
+				fileEntry.GetFilePath(fileName);
+                Try!(DelTree(fileName, fileFilter));
+            }
+
+            for (var fileEntry in Directory.EnumerateFiles(path))
+            {
+				let fileName = scope String();
+				fileEntry.GetFilePath(fileName);
+
+				if (fileFilter != null)
+					if (!fileFilter(fileName))
+						continue;
+
+                Try!(File.SetAttributes(fileName, FileAttributes.Archive));
+                Try!(File.Delete(fileName));
+            }
+
+			// Allow failure for the directory, this can often be locked for various reasons
+			//  but we only consider a file failure to be an "actual" failure
+            Directory.Delete(path).IgnoreError();
+			return .Ok;
+        }
+
+        public static Result<void, FileError> LoadTextFile(String fileName, String outBuffer, bool autoRetry = true, delegate void() onPreFilter = null)
+        {
+			// Retry for a while if the other side is still writing out the file
+			for (int i = 0; i < 100; i++)
+			{
+				if (File.ReadAllText(fileName, outBuffer, true) case .Err(let err))
+				{
+					bool retry = false;
+					if ((autoRetry) && (err case .FileOpenError(let fileOpenErr)))
+					{
+						if (fileOpenErr == .SharingViolation)
+							retry = true;
+					}
+					if (!retry)
+                    	return .Err(err);
+				}
+				else
+					break;
+				Thread.Sleep(20);
+			}
+
+			if (onPreFilter != null)
+				onPreFilter();
+
+			/*if (hashPtr != null)
+				*hashPtr = MD5.Hash(Span<uint8>((uint8*)outBuffer.Ptr, outBuffer.Length));*/
+
+			bool isAscii = false;
+
+			int outIdx = 0;
+            for (int32 i = 0; i < outBuffer.Length; i++)
+            {
+                char8 c = outBuffer[i];
+				if (c >= '\x80')
+				{
+					switch (UTF8.TryDecode(outBuffer.Ptr + i, outBuffer.Length - i))
+					{
+					case .Ok((?, let len)):
+						if (len > 1)
+						{
+							for (int cnt < len)
+							{
+								char8 cPart = outBuffer[i++];
+								outBuffer[outIdx++] = cPart;
+							}
+							i--;
+							continue;
+						}
+					case .Err: isAscii = true;
+					}
+				}
+
+                if (c != '\r')
+                {
+					if (outIdx == i)
+					{
+						outIdx++;
+                        continue;
+					}
+					outBuffer[outIdx++] = c;
+				}
+            }
+			outBuffer.RemoveToEnd(outIdx);
+
+			if (isAscii)
+			{
+				String prevBuffer = scope String();
+				outBuffer.MoveTo(prevBuffer);
+
+				for (var c in prevBuffer.RawChars)
+				{
+					outBuffer.Append((char32)c, 1);
+				}
+			}
+
+            return .Ok;
+        }
+
+        public static bool FileTextEquals(String textA, String textB)
+        {
+            int32 posA = 0;
+            int32 posB = 0;
+
+            while (true)
+            {
+                char8 char8A = (char8)0;
+                char8 char8B = (char8)0;
+
+                while (posA < textA.Length)
+                {
+                    char8A = textA[posA++];
+                    if (char8A != '\r')
+                        break;
+                    char8A = (char8)0;
+                }
+
+                while (posB < textB.Length)
+                {
+                    char8B = textB[posB++];
+                    if (char8B != '\r')
+                        break;
+                    char8B = (char8)0;
+                }
+
+                if ((char8A == (char8)0) && (char8B == (char8)0))
+                    return true;
+                if (char8A != char8B)                
+                    return false;                
+            }
+        }
+
+        public static Result<void> WriteTextFile(StringView path, StringView text)
+        {
+			var stream = scope FileStream();
+			if (stream.Create(path) case .Err)
+			{
+				return .Err;
+			}
+            if (stream.WriteStrUnsized(text) case .Err)
+				return .Err;
+            
+            return .Ok;
+        } 
+
+        public static int LevenshteinDistance(String s, String t)
+        {
+            int n = s.Length;
+            int m = t.Length;
+            int32[,] d = new int32[n + 1, m + 1];
+			defer delete d;
+            if (n == 0)
+            {
+                return m;
+            }
+            if (m == 0)
+            {
+                return n;
+            }
+            for (int32 i = 0; i <= n; d[i, 0] = i++)
+                {}
+            for (int32 j = 0; j <= m; d[0, j] = j++)
+				{}
+            for (int32 i = 1; i <= n; i++)
+            {
+                for (int32 j = 1; j <= m; j++)
+                {
+                    int32 cost = (t[j - 1] == s[i - 1]) ? 0 : 1;
+                    d[i, j] = Math.Min(
+                        Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1),
+                        d[i - 1, j - 1] + cost);
+                }
+            }
+            return d[n, m];
+        }
+
+        /*public static List<TSource> ToSortedList<TSource>(this IEnumerable<TSource> source)
+        {
+            var list = source.ToList();
+            list.Sort();
+            return list;
+        }*/
+
+		public static int64 DecodeInt64(ref uint8* ptr)
+		{
+			int64 value = 0;
+			int32 shift = 0;
+			int64 curByte;
+			repeat
+			{
+				curByte = *(ptr++);
+				value |= ((curByte & 0x7f) << shift);
+				shift += 7;
+			
+			} while (curByte >= 128);
+			// Sign extend negative numbers.
+			if (((curByte & 0x40) != 0) && (shift < 64))
+				value |= -1 << shift;
+			return value;
+		}
+
+        public static int32 DecodeInt(uint8[] buf, ref int idx)
+        {
+            int32 value = 0;
+            int32 Shift = 0;
+            int32 curByte;
+
+            repeat
+            {
+                curByte = buf[idx++];
+                value |= ((curByte & 0x7f) << Shift);
+                Shift += 7;
+
+            } while (curByte >= 128);
+            // Sign extend negative numbers.
+            if ((curByte & 0x40) != 0)
+                value |= -1 << Shift;
+            return value;
+        }
+
+        public static void EncodeInt(uint8[] buf, ref int idx, int value)
+        {
+			int curValue = value;
+
+            bool hasMore;
+            repeat
+            {
+                uint8 curByte = (uint8)(curValue & 0x7f);    
+                curValue >>= 7;
+                hasMore = !((((curValue == 0) && ((curByte & 0x40) == 0)) ||
+                    ((curValue == -1) && ((curByte & 0x40) != 0))));
+                if (hasMore)
+                    curByte |= 0x80;
+                buf[idx++] = curByte;                
+            }
+            while (hasMore);        
+        }
+
+		public static bool Contains<T>(IEnumerator<T> itr, T value)
+		{
+			for (var check in itr)
+				if (check == value)
+					return true;
+			return false;
+		}
+
+		public static void QuoteString(String str, String strOut)
+		{
+			strOut.Append('"');
+			for (int i < str.Length)
+			{
+				char8 c = str[i];
+				strOut.Append(c);
+			}
+			strOut.Append('"');
+		}
+
+		public static void ParseSpaceSep(String str, ref int idx, String subStr)
+		{
+			while (idx < str.Length)
+			{
+				char8 c = str[idx];
+				if (c != ' ')
+					break;
+				idx++;
+			}
+
+			if (idx >= str.Length)
+				return;
+
+			// Quoted
+			if (str[idx] == '"')
+			{
+				idx++;
+				while (idx < str.Length)
+				{
+					char8 c = str[idx++];
+					if (c == '"')
+						break;
+					subStr.Append(c);
+				}
+				return;
+			}
+
+			// Unquoted
+			while (idx < str.Length)
+			{
+				char8 c = str[idx++];
+				if (c == ' ')
+					break;
+				subStr.Append(c);
+			}
+		}
+
+		public static void SnapScale(ref float val, float scale)
+		{
+			val *= scale;
+			float frac = val - (int)val;
+			if ((frac <= 0.001f) || (frac >= 0.999f))
+				val = (float)Math.Round(val);
+		}
+
+		public static void RoundScale(ref float val, float scale)
+		{
+			val = (float)Math.Round(val * scale);
+		}
+    }        
+}

+ 14 - 0
BeefLibs/Beefy2D/src/events/DialogEvent.bf

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.widgets;
+
+namespace Beefy.events
+{
+    public class DialogEvent : Event
+    {
+        public bool mCloseDialog;
+        public ButtonWidget mButton;
+        public String mResult;
+    }
+}

+ 15 - 0
BeefLibs/Beefy2D/src/events/DragEvent.bf

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Beefy.events
+{
+    public class DragEvent : Event
+    {
+        public float mX;
+        public float mY;
+        public Object mDragTarget;
+        public int32 mDragTargetDir;
+        public bool mDragAllowed = true;
+    }
+}

+ 11 - 0
BeefLibs/Beefy2D/src/events/EditEvent.bf

@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Beefy.events
+{
+    public class EditEvent : Event
+    {
+        //string mText;
+    }
+}

+ 12 - 0
BeefLibs/Beefy2D/src/events/Event.bf

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Beefy.events
+{
+    public class Event
+    {
+        public Object mSender;
+        public bool mHandled;
+    }
+}

+ 38 - 0
BeefLibs/Beefy2D/src/events/KeyboardEvent.bf

@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.widgets;
+
+namespace Beefy.widgets
+{
+    public enum KeyFlags
+    {
+		None = 0,
+        Alt = 1,
+        Ctrl = 2,
+        Shift = 4
+    }
+}
+
+namespace Beefy.events
+{
+    /*[Flags]
+    public enum KeyFlags
+    {
+        Alt = 1,
+        Ctrl = 2,
+        Shift = 4
+    }*/
+
+    public class KeyDownEvent : Event
+    {
+        public KeyFlags mKeyFlags;
+        public KeyCode mKeyCode;
+		public bool mIsRepeat;
+    }
+
+	public class KeyCharEvent : Event
+	{
+		public char32 mChar;
+	}
+}

+ 22 - 0
BeefLibs/Beefy2D/src/events/MouseEvent.bf

@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.widgets;
+
+namespace Beefy.events
+{
+    public class MouseEvent : Event
+    {                
+        public float mX;
+        public float mY;
+        public int32 mBtn;
+        public int32 mBtnCount;
+        public int32 mWheelDelta;
+
+        public void GetRootCoords(out float x, out float y)
+        {
+            Widget widget = (Widget)mSender;
+            widget.SelfToRootTranslate(mX, mY, out x, out y);
+        }
+    }
+}

+ 13 - 0
BeefLibs/Beefy2D/src/events/ScrollEvent.bf

@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Beefy.events
+{
+    public class ScrollEvent : Event
+    {
+        public double mOldPos;
+        public double mNewPos;
+		public bool mIsFromUser;
+    }
+}

+ 9 - 0
BeefLibs/Beefy2D/src/events/WidgetParentEvent.bf

@@ -0,0 +1,9 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.widgets;
+
+namespace Beefy.events
+{
+
+}

+ 493 - 0
BeefLibs/Beefy2D/src/geom/Matrix4.bf

@@ -0,0 +1,493 @@
+using System;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Beefy.geom;
+
+namespace Beefy.geom
+{
+    public struct Matrix4
+    {
+        public float m00;
+        public float m01;
+        public float m02;
+        public float m03;
+        public float m10;
+        public float m11;
+        public float m12;
+        public float m13;
+        public float m20;
+        public float m21;
+        public float m22;
+        public float m23;
+        public float m30;
+        public float m31;
+        public float m32;
+        public float m33;
+
+        public static readonly Matrix4 Identity = Matrix4(1f, 0f, 0f, 0f,
+            0f, 1f, 0f, 0f,
+            0f, 0f, 1f, 0f,
+            0f, 0f, 0f, 1f);
+
+        public this(
+		    float m00, float m01, float m02, float m03,
+		    float m10, float m11, float m12, float m13,
+		    float m20, float m21, float m22, float m23,
+		    float m30, float m31, float m32, float m33)
+	    {
+		    this.m00 = m00;
+		    this.m01 = m01;
+		    this.m02 = m02;
+		    this.m03 = m03;
+		    this.m10 = m10;
+		    this.m11 = m11;
+		    this.m12 = m12;
+		    this.m13 = m13;
+		    this.m20 = m20;
+		    this.m21 = m21;
+		    this.m22 = m22;
+		    this.m23 = m23;
+		    this.m30 = m30;
+		    this.m31 = m31;
+		    this.m32 = m32;
+		    this.m33 = m33;
+	    }
+
+        public static Matrix4 CreatePerspective(float width, float height, float nearPlaneDistance, float farPlaneDistance)
+        {
+            Matrix4 matrix;
+            if (nearPlaneDistance <= 0f)
+            {
+                Runtime.FatalError("nearPlaneDistance <= 0");
+            }
+            if (farPlaneDistance <= 0f)
+            {
+                Runtime.FatalError("farPlaneDistance <= 0");
+            }
+            if (nearPlaneDistance >= farPlaneDistance)
+            {
+                Runtime.FatalError("nearPlaneDistance >= farPlaneDistance");
+            }
+            /*matrix.M11 = (2f * nearPlaneDistance) / width;
+            matrix.M12 = matrix.M13 = matrix.M14 = 0f;
+            matrix.M22 = (2f * nearPlaneDistance) / height;
+            matrix.M21 = matrix.M23 = matrix.M24 = 0f;
+            matrix.M33 = farPlaneDistance / (nearPlaneDistance - farPlaneDistance);
+            matrix.M31 = matrix.M32 = 0f;
+            matrix.M34 = -1f;
+            matrix.M41 = matrix.M42 = matrix.M44 = 0f;
+            matrix.M43 = (nearPlaneDistance * farPlaneDistance) / (nearPlaneDistance - farPlaneDistance);*/
+
+            /*matrix.m00 = (2f * nearPlaneDistance) / width;
+            matrix.m01 = 0f;                        
+            matrix.m02 = 0f;
+            matrix.m03 = 0f;
+
+            matrix.m10 = 0f;
+            matrix.m11 = (2f * nearPlaneDistance) / height;
+            matrix.m12 = 0f;
+            matrix.m13 = 0f;
+
+            matrix.m20 = 0f;
+            matrix.m21 = 0f;            
+            matrix.m22 = farPlaneDistance / (nearPlaneDistance - farPlaneDistance);
+            matrix.m23 = (nearPlaneDistance * farPlaneDistance) / (nearPlaneDistance - farPlaneDistance);
+
+            matrix.m30 = 0f;
+            matrix.m31 = 0f;
+            matrix.m32 = -1f;
+            matrix.m33 = 0f;            */
+
+            matrix.m00 = (2f * nearPlaneDistance) / width;
+            matrix.m10 = matrix.m20 = matrix.m30 = 0f;
+            matrix.m11 = (2f * nearPlaneDistance) / height;
+            matrix.m01 = matrix.m21 = matrix.m31 = 0f;
+            matrix.m22 = farPlaneDistance / (nearPlaneDistance - farPlaneDistance);
+            matrix.m02 = matrix.m12 = 0f;
+            matrix.m32 = -1f;
+            matrix.m03 = matrix.m13 = matrix.m33 = 0f;
+            matrix.m23 = (nearPlaneDistance * farPlaneDistance) / (nearPlaneDistance - farPlaneDistance);
+
+            return matrix;
+        }
+
+        public static Matrix4 CreatePerspectiveFieldOfView(float fieldOfView, float aspectRatio, float nearPlaneDistance, float farPlaneDistance)
+        {
+            Matrix4 result;
+            CreatePerspectiveFieldOfView(fieldOfView, aspectRatio, nearPlaneDistance, farPlaneDistance, out result);
+            return result;
+        }
+
+
+        public static void CreatePerspectiveFieldOfView(float fieldOfView, float aspectRatio, float nearPlaneDistance, float farPlaneDistance, out Matrix4 result)
+        {
+            if ((fieldOfView <= 0f) || (fieldOfView >= 3.141593f))
+            {
+                Runtime.FatalError("fieldOfView <= 0 or >= PI");
+            }
+            if (nearPlaneDistance <= 0f)
+            {
+                Runtime.FatalError("nearPlaneDistance <= 0");
+            }
+            if (farPlaneDistance <= 0f)
+            {
+                Runtime.FatalError("farPlaneDistance <= 0");
+            }
+            if (nearPlaneDistance >= farPlaneDistance)
+            {
+                Runtime.FatalError("nearPlaneDistance >= farPlaneDistance");
+            }
+            float num = 1f / ((float)Math.Tan((double)(fieldOfView * 0.5f)));
+            float num9 = num / aspectRatio;
+            result.m00 = num9;
+            result.m10 = result.m20 = result.m30 = 0;
+            result.m11 = num;
+            result.m01 = result.m21 = result.m31 = 0;
+            result.m02 = result.m12 = 0f;
+            result.m22 = farPlaneDistance / (nearPlaneDistance - farPlaneDistance);
+            result.m32 = -1;
+            result.m03 = result.m13 = result.m33 = 0;
+            result.m23 = (nearPlaneDistance * farPlaneDistance) / (nearPlaneDistance - farPlaneDistance);
+        }
+
+
+        public static Matrix4 CreatePerspectiveOffCenter(float left, float right, float bottom, float top, float nearPlaneDistance, float farPlaneDistance)
+        {
+            Matrix4 result;
+            CreatePerspectiveOffCenter(left, right, bottom, top, nearPlaneDistance, farPlaneDistance, out result);
+            return result;
+        }
+
+
+        public static void CreatePerspectiveOffCenter(float left, float right, float bottom, float top, float nearPlaneDistance, float farPlaneDistance, out Matrix4 result)
+        {
+            if (nearPlaneDistance <= 0f)
+            {
+                Runtime.FatalError("nearPlaneDistance <= 0");
+            }
+            if (farPlaneDistance <= 0f)
+            {
+                Runtime.FatalError("farPlaneDistance <= 0");
+            }
+            if (nearPlaneDistance >= farPlaneDistance)
+            {
+                Runtime.FatalError("nearPlaneDistance >= farPlaneDistance");
+            }
+            result.m00 = (2f * nearPlaneDistance) / (right - left);
+            result.m10 = result.m20 = result.m30 = 0;
+            result.m11 = (2f * nearPlaneDistance) / (top - bottom);
+            result.m01 = result.m21 = result.m31 = 0;
+            result.m02 = (left + right) / (right - left);
+            result.m12 = (top + bottom) / (top - bottom);
+            result.m22 = farPlaneDistance / (nearPlaneDistance - farPlaneDistance);
+            result.m32 = -1;
+            result.m23 = (nearPlaneDistance * farPlaneDistance) / (nearPlaneDistance - farPlaneDistance);
+            result.m03 = result.m13 = result.m33 = 0;
+        }
+
+        public static Matrix4 Multiply(Matrix4 m1, Matrix4 m2)
+	    {
+		    Matrix4 r;
+		    r.m00 = m1.m00 * m2.m00 + m1.m01 * m2.m10 + m1.m02 * m2.m20 + m1.m03 * m2.m30;
+		    r.m01 = m1.m00 * m2.m01 + m1.m01 * m2.m11 + m1.m02 * m2.m21 + m1.m03 * m2.m31;
+		    r.m02 = m1.m00 * m2.m02 + m1.m01 * m2.m12 + m1.m02 * m2.m22 + m1.m03 * m2.m32;
+		    r.m03 = m1.m00 * m2.m03 + m1.m01 * m2.m13 + m1.m02 * m2.m23 + m1.m03 * m2.m33;
+
+		    r.m10 = m1.m10 * m2.m00 + m1.m11 * m2.m10 + m1.m12 * m2.m20 + m1.m13 * m2.m30;
+		    r.m11 = m1.m10 * m2.m01 + m1.m11 * m2.m11 + m1.m12 * m2.m21 + m1.m13 * m2.m31;
+		    r.m12 = m1.m10 * m2.m02 + m1.m11 * m2.m12 + m1.m12 * m2.m22 + m1.m13 * m2.m32;
+		    r.m13 = m1.m10 * m2.m03 + m1.m11 * m2.m13 + m1.m12 * m2.m23 + m1.m13 * m2.m33;
+
+		    r.m20 = m1.m20 * m2.m00 + m1.m21 * m2.m10 + m1.m22 * m2.m20 + m1.m23 * m2.m30;
+		    r.m21 = m1.m20 * m2.m01 + m1.m21 * m2.m11 + m1.m22 * m2.m21 + m1.m23 * m2.m31;
+		    r.m22 = m1.m20 * m2.m02 + m1.m21 * m2.m12 + m1.m22 * m2.m22 + m1.m23 * m2.m32;
+		    r.m23 = m1.m20 * m2.m03 + m1.m21 * m2.m13 + m1.m22 * m2.m23 + m1.m23 * m2.m33;
+
+		    r.m30 = m1.m30 * m2.m00 + m1.m31 * m2.m10 + m1.m32 * m2.m20 + m1.m33 * m2.m30;
+		    r.m31 = m1.m30 * m2.m01 + m1.m31 * m2.m11 + m1.m32 * m2.m21 + m1.m33 * m2.m31;
+		    r.m32 = m1.m30 * m2.m02 + m1.m31 * m2.m12 + m1.m32 * m2.m22 + m1.m33 * m2.m32;
+		    r.m33 = m1.m30 * m2.m03 + m1.m31 * m2.m13 + m1.m32 * m2.m23 + m1.m33 * m2.m33;
+
+		    return r;
+	    }
+
+	    public static Matrix4 Transpose(Matrix4 m)
+	    {
+		    return Matrix4(
+			    m.m00, m.m10, m.m20, m.m30,
+			    m.m01, m.m11, m.m21, m.m31,
+			    m.m02, m.m12, m.m22, m.m32,
+			    m.m03, m.m13, m.m23, m.m33);
+	    }
+
+        public static Matrix4 CreateTranslation(float x, float y, float z)
+	    {
+		    return Matrix4(
+			    1, 0, 0, x,
+			    0, 1, 0, y,
+			    0, 0, 1, z,
+			    0, 0, 0, 1);
+	    }
+
+        public static Matrix4 CreateTransform(Vector3 position, Vector3 scale, Quaternion orientation)
+        {
+            // Ordering:
+	        //    1. Scale
+	        //    2. Rotate
+	        //    3. Translate
+
+	        Matrix4 rot = orientation.ToMatrix();
+	        return Matrix4(
+		        scale.mX * rot.m00, scale.mY * rot.m01, scale.mZ * rot.m02, position.mX,
+		        scale.mX * rot.m10, scale.mY * rot.m11, scale.mZ * rot.m12, position.mY,
+		        scale.mX * rot.m20, scale.mY * rot.m21, scale.mZ * rot.m22, position.mZ,	
+		        0, 0, 0, 1);
+        }
+
+        public static Matrix4 CreateRotationX(float radians)
+        {
+            Matrix4 result = Matrix4.Identity;
+
+            var val1 = (float)Math.Cos(radians);
+            var val2 = (float)Math.Sin(radians);
+
+            result.m11 = val1;
+            result.m21 = val2;
+            result.m12 = -val2;
+            result.m22 = val1;
+
+            return result;
+        }
+
+        public static Matrix4 CreateRotationY(float radians)
+        {
+            Matrix4 returnMatrix = Matrix4.Identity;
+
+            var val1 = (float)Math.Cos(radians);
+            var val2 = (float)Math.Sin(radians);
+
+            returnMatrix.m00 = val1;
+            returnMatrix.m20 = -val2;
+            returnMatrix.m02 = val2;
+            returnMatrix.m22 = val1;
+
+            return returnMatrix;
+        }
+
+        public static Matrix4 CreateRotationZ(float radians)
+        {
+            Matrix4 returnMatrix = Matrix4.Identity;
+
+            var val1 = (float)Math.Cos(radians);
+            var val2 = (float)Math.Sin(radians);
+
+            returnMatrix.m00 = val1;
+            returnMatrix.m10 = val2;
+            returnMatrix.m01 = -val2;
+            returnMatrix.m11 = val1;
+
+            return returnMatrix;
+        }
+
+        public static Matrix4 CreateScale(float scale)
+        {
+            Matrix4 result;
+            result.m00 = scale;
+            result.m10 = 0;
+            result.m20 = 0;
+            result.m30 = 0;
+            result.m01 = 0;
+            result.m11 = scale;
+            result.m21 = 0;
+            result.m31 = 0;
+            result.m02 = 0;
+            result.m12 = 0;
+            result.m22 = scale;
+            result.m32 = 0;
+            result.m03 = 0;
+            result.m13 = 0;
+            result.m23 = 0;
+            result.m33 = 1;
+            return result;
+        }
+
+        public static Matrix4 CreateScale(float xScale, float yScale, float zScale)
+        {
+            Matrix4 result;
+            result.m00 = xScale;
+            result.m10 = 0;
+            result.m20 = 0;
+            result.m30 = 0;
+            result.m01 = 0;
+            result.m11 = yScale;
+            result.m21 = 0;
+            result.m31 = 0;
+            result.m02 = 0;
+            result.m12 = 0;
+            result.m22 = zScale;
+            result.m32 = 0;
+            result.m03 = 0;
+            result.m13 = 0;
+            result.m23 = 0;
+            result.m33 = 1;
+            return result;
+        }
+
+        public static Matrix4 CreateScale(Vector3 scales)
+        {
+            Matrix4 result;
+            result.m00 = scales.mX;
+            result.m10 = 0;
+            result.m20 = 0;
+            result.m30 = 0;
+            result.m01 = 0;
+            result.m11 = scales.mY;
+            result.m21 = 0;
+            result.m31 = 0;
+            result.m02 = 0;
+            result.m12 = 0;
+            result.m22 = scales.mZ;
+            result.m32 = 0;
+            result.m03 = 0;
+            result.m13 = 0;
+            result.m23 = 0;
+            result.m33 = 1;
+            return result;
+        }
+
+        public static Matrix4 CreateTranslation(Vector3 position)
+        {
+            Matrix4 result;
+            result.m00 = 1;
+            result.m10 = 0;
+            result.m20 = 0;
+            result.m30 = 0;
+            result.m01 = 0;
+            result.m11 = 1;
+            result.m21 = 0;
+            result.m31 = 0;
+            result.m02 = 0;
+            result.m12 = 0;
+            result.m22 = 1;
+            result.m32 = 0;
+            result.m03 = position.mX;
+            result.m13 = position.mY;
+            result.m23 = position.mZ;
+            result.m33 = 1;
+            return result;
+        }
+
+        /*public static Matrix4 Inverse()
+        {
+            Real m00 = m[0][0], m01 = m[0][1], m02 = m[0][2], m03 = m[0][3];
+            Real m10 = m[1][0], m11 = m[1][1], m12 = m[1][2], m13 = m[1][3];
+            Real m20 = m[2][0], m21 = m[2][1], m22 = m[2][2], m23 = m[2][3];
+            Real m30 = m[3][0], m31 = m[3][1], m32 = m[3][2], m33 = m[3][3];
+
+            Real v0 = m20 * m31 - m21 * m30;
+            Real v1 = m20 * m32 - m22 * m30;
+            Real v2 = m20 * m33 - m23 * m30;
+            Real v3 = m21 * m32 - m22 * m31;
+            Real v4 = m21 * m33 - m23 * m31;
+            Real v5 = m22 * m33 - m23 * m32;
+
+            Real t00 = + (v5 * m11 - v4 * m12 + v3 * m13);
+            Real t10 = - (v5 * m10 - v2 * m12 + v1 * m13);
+            Real t20 = + (v4 * m10 - v2 * m11 + v0 * m13);
+            Real t30 = - (v3 * m10 - v1 * m11 + v0 * m12);
+
+            Real invDet = 1 / (t00 * m00 + t10 * m01 + t20 * m02 + t30 * m03);
+
+            Real d00 = t00 * invDet;
+            Real d10 = t10 * invDet;
+            Real d20 = t20 * invDet;
+            Real d30 = t30 * invDet;
+
+            Real d01 = - (v5 * m01 - v4 * m02 + v3 * m03) * invDet;
+            Real d11 = + (v5 * m00 - v2 * m02 + v1 * m03) * invDet;
+            Real d21 = - (v4 * m00 - v2 * m01 + v0 * m03) * invDet;
+            Real d31 = + (v3 * m00 - v1 * m01 + v0 * m02) * invDet;
+
+            v0 = m10 * m31 - m11 * m30;
+            v1 = m10 * m32 - m12 * m30;
+            v2 = m10 * m33 - m13 * m30;
+            v3 = m11 * m32 - m12 * m31;
+            v4 = m11 * m33 - m13 * m31;
+            v5 = m12 * m33 - m13 * m32;
+
+            Real d02 = + (v5 * m01 - v4 * m02 + v3 * m03) * invDet;
+            Real d12 = - (v5 * m00 - v2 * m02 + v1 * m03) * invDet;
+            Real d22 = + (v4 * m00 - v2 * m01 + v0 * m03) * invDet;
+            Real d32 = - (v3 * m00 - v1 * m01 + v0 * m02) * invDet;
+
+            v0 = m21 * m10 - m20 * m11;
+            v1 = m22 * m10 - m20 * m12;
+            v2 = m23 * m10 - m20 * m13;
+            v3 = m22 * m11 - m21 * m12;
+            v4 = m23 * m11 - m21 * m13;
+            v5 = m23 * m12 - m22 * m13;
+
+            Real d03 = - (v5 * m01 - v4 * m02 + v3 * m03) * invDet;
+            Real d13 = + (v5 * m00 - v2 * m02 + v1 * m03) * invDet;
+            Real d23 = - (v4 * m00 - v2 * m01 + v0 * m03) * invDet;
+            Real d33 = + (v3 * m00 - v1 * m01 + v0 * m02) * invDet;
+
+            return Matrix4(
+                d00, d01, d02, d03,
+                d10, d11, d12, d13,
+                d20, d21, d22, d23,
+                d30, d31, d32, d33);
+        }*/
+    
+        bool IsAffine()
+        {
+            return m30 == 0 && m31 == 0 && m32 == 0 && m33 == 1;
+        }
+
+        public static Matrix4 InverseAffine(Matrix4 mtx)
+        {
+            Debug.Assert(mtx.IsAffine());
+
+            float m10 = mtx.m10, m11 = mtx.m11, m12 = mtx.m12;
+            float m20 = mtx.m20, m21 = mtx.m21, m22 = mtx.m22;
+
+            float t00 = m22 * m11 - m21 * m12;
+            float t10 = m20 * m12 - m22 * m10;
+            float t20 = m21 * m10 - m20 * m11;
+
+            float m00 = mtx.m00, m01 = mtx.m01, m02 = mtx.m02;
+
+            float invDet = 1 / (m00 * t00 + m01 * t10 + m02 * t20);
+
+            t00 *= invDet; t10 *= invDet; t20 *= invDet;
+
+            m00 *= invDet; m01 *= invDet; m02 *= invDet;
+
+            float r00 = t00;
+            float r01 = m02 * m21 - m01 * m22;
+            float r02 = m01 * m12 - m02 * m11;
+
+            float r10 = t10;
+            float r11 = m00 * m22 - m02 * m20;
+            float r12 = m02 * m10 - m00 * m12;
+
+            float r20 = t20;
+            float r21 = m01 * m20 - m00 * m21;
+            float r22 = m00 * m11 - m01 * m10;
+
+            float m03 = mtx.m03, m13 = mtx.m13, m23 = mtx.m23;
+
+            float r03 = -(r00 * m03 + r01 * m13 + r02 * m23);
+            float r13 = -(r10 * m03 + r11 * m13 + r12 * m23);
+            float r23 = -(r20 * m03 + r21 * m13 + r22 * m23);
+
+            return Matrix4(
+                r00, r01, r02, r03,
+                r10, r11, r12, r13,
+                r20, r21, r22, r23,
+                  0, 0, 0, 1);
+        }
+    }
+}

+ 18 - 0
BeefLibs/Beefy2D/src/geom/Point.bf

@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Beefy.geom
+{
+    public struct Point
+    {
+        public float x;
+        public float y;
+
+        public this(float x, float y)
+        {
+            this.x = x;
+            this.y = y;
+        }
+    }
+}

+ 775 - 0
BeefLibs/Beefy2D/src/geom/Quaternion.bf

@@ -0,0 +1,775 @@
+using System;
+using Beefy.gfx;
+
+namespace Beefy.geom
+{
+    public struct Quaternion : IHashable, IEquatable<Quaternion>
+    {
+        public float mX;
+        public float mY;
+        public float mZ;
+        public float mW;
+        public static readonly Quaternion Identity = Quaternion(0, 0, 0, 1);
+        
+        public this(float x, float y, float z, float w)
+        {
+            mX = x;
+            mY = y;
+            mZ = z;
+            mW = w;
+        }
+        
+        public this(Vector3 vectorPart, float scalarPart)
+        {
+            mX = vectorPart.mX;
+            mY = vectorPart.mY;
+            mZ = vectorPart.mZ;
+            mW = scalarPart;
+        }
+
+        public static Quaternion Add(Quaternion quaternion1, Quaternion quaternion2)
+        {            
+            Quaternion quaternion;
+            quaternion.mX = quaternion1.mX + quaternion2.mX;
+            quaternion.mY = quaternion1.mY + quaternion2.mY;
+            quaternion.mZ = quaternion1.mZ + quaternion2.mZ;
+            quaternion.mW = quaternion1.mW + quaternion2.mW;
+            return quaternion;
+        }
+
+
+        public static void Add(ref Quaternion quaternion1, ref Quaternion quaternion2, out Quaternion result)
+        {            
+            result.mX = quaternion1.mX + quaternion2.mX;
+            result.mY = quaternion1.mY + quaternion2.mY;
+            result.mZ = quaternion1.mZ + quaternion2.mZ;
+            result.mW = quaternion1.mW + quaternion2.mW;
+        }
+
+        public static Quaternion Concatenate(Quaternion value1, Quaternion value2)
+        {
+            Quaternion quaternion;
+            float x = value2.mX;
+            float y = value2.mY;
+            float z = value2.mZ;
+            float w = value2.mW;
+            float num4 = value1.mX;
+            float num3 = value1.mY;
+            float num2 = value1.mZ;
+            float num = value1.mW;
+            float num12 = (y * num2) - (z * num3);
+            float num11 = (z * num4) - (x * num2);
+            float num10 = (x * num3) - (y * num4);
+            float num9 = ((x * num4) + (y * num3)) + (z * num2);
+            quaternion.mX = ((x * num) + (num4 * w)) + num12;
+            quaternion.mY = ((y * num) + (num3 * w)) + num11;
+            quaternion.mZ = ((z * num) + (num2 * w)) + num10;
+            quaternion.mW = (w * num) - num9;
+            return quaternion;
+        }
+
+        public static void Concatenate(ref Quaternion value1, ref Quaternion value2, out Quaternion result)
+        {
+            float x = value2.mX;
+            float y = value2.mY;
+            float z = value2.mZ;
+            float w = value2.mW;
+            float num4 = value1.mX;
+            float num3 = value1.mY;
+            float num2 = value1.mZ;
+            float num = value1.mW;
+            float num12 = (y * num2) - (z * num3);
+            float num11 = (z * num4) - (x * num2);
+            float num10 = (x * num3) - (y * num4);
+            float num9 = ((x * num4) + (y * num3)) + (z * num2);
+            result.mX = ((x * num) + (num4 * w)) + num12;
+            result.mY = ((y * num) + (num3 * w)) + num11;
+            result.mZ = ((z * num) + (num2 * w)) + num10;
+            result.mW = (w * num) - num9;
+        }
+                
+        public void Conjugate() mut
+        {
+            mX = -mX;
+            mY = -mY;
+            mZ = -mZ;
+        }
+                
+        public static Quaternion Conjugate(Quaternion value)
+        {
+            Quaternion quaternion;
+            quaternion.mX = -value.mX;
+            quaternion.mY = -value.mY;
+            quaternion.mZ = -value.mZ;
+            quaternion.mW = value.mW;
+            return quaternion;
+        }
+
+        public static void Conjugate(ref Quaternion value, out Quaternion result)
+        {
+            result.mX = -value.mX;
+            result.mY = -value.mY;
+            result.mZ = -value.mZ;
+            result.mW = value.mW;
+        }
+
+        public static Quaternion CreateFromAxisAngle(Vector3 axis, float angle)
+        {
+            Quaternion quaternion;
+            float num2 = angle * 0.5f;
+            float num = (float)Math.Sin((double)num2);
+            float num3 = (float)Math.Cos((double)num2);
+            quaternion.mX = axis.mX * num;
+            quaternion.mY = axis.mY * num;
+            quaternion.mZ = axis.mZ * num;
+            quaternion.mW = num3;
+            return quaternion;
+        }
+
+        public static void CreateFromAxisAngle(ref Vector3 axis, float angle, out Quaternion result)
+        {
+            float num2 = angle * 0.5f;
+            float num = (float)Math.Sin((double)num2);
+            float num3 = (float)Math.Cos((double)num2);
+            result.mX = axis.mX * num;
+            result.mY = axis.mY * num;
+            result.mZ = axis.mZ * num;
+            result.mW = num3;
+        }
+
+        public static Quaternion CreateFromRotationMatrix(Matrix4 matrix)
+        {
+            float num8 = (matrix.m11 + matrix.m22) + matrix.m33;
+            Quaternion quaternion = Quaternion();
+            if (num8 > 0f)
+            {
+                float num = (float)Math.Sqrt((double)(num8 + 1f));
+                quaternion.mW = num * 0.5f;
+                num = 0.5f / num;
+                quaternion.mX = (matrix.m23 - matrix.m32) * num;
+                quaternion.mY = (matrix.m31 - matrix.m13) * num;
+                quaternion.mZ = (matrix.m12 - matrix.m21) * num;
+                return quaternion;
+            }
+            if ((matrix.m11 >= matrix.m22) && (matrix.m11 >= matrix.m33))
+            {
+                float num7 = (float)Math.Sqrt((double)(((1f + matrix.m11) - matrix.m22) - matrix.m33));
+                float num4 = 0.5f / num7;
+                quaternion.mX = 0.5f * num7;
+                quaternion.mY = (matrix.m12 + matrix.m21) * num4;
+                quaternion.mZ = (matrix.m13 + matrix.m31) * num4;
+                quaternion.mW = (matrix.m23 - matrix.m32) * num4;
+                return quaternion;
+            }
+            if (matrix.m22 > matrix.m33)
+            {
+                float num6 = (float)Math.Sqrt((double)(((1f + matrix.m22) - matrix.m11) - matrix.m33));
+                float num3 = 0.5f / num6;
+                quaternion.mX = (matrix.m21 + matrix.m12) * num3;
+                quaternion.mY = 0.5f * num6;
+                quaternion.mZ = (matrix.m32 + matrix.m23) * num3;
+                quaternion.mW = (matrix.m31 - matrix.m13) * num3;
+                return quaternion;
+            }
+            float num5 = (float)Math.Sqrt((double)(((1f + matrix.m33) - matrix.m11) - matrix.m22));
+            float num2 = 0.5f / num5;
+            quaternion.mX = (matrix.m31 + matrix.m13) * num2;
+            quaternion.mY = (matrix.m32 + matrix.m23) * num2;
+            quaternion.mZ = 0.5f * num5;
+            quaternion.mW = (matrix.m12 - matrix.m21) * num2;
+
+            return quaternion;
+
+        }
+
+        public static void CreateFromRotationMatrix(ref Matrix4 matrix, out Quaternion result)
+        {
+            float num8 = (matrix.m11 + matrix.m22) + matrix.m33;
+            if (num8 > 0f)
+            {
+                float num = (float)Math.Sqrt((double)(num8 + 1f));
+                result.mW = num * 0.5f;
+                num = 0.5f / num;
+                result.mX = (matrix.m23 - matrix.m32) * num;
+                result.mY = (matrix.m31 - matrix.m13) * num;
+                result.mZ = (matrix.m12 - matrix.m21) * num;
+            }
+            else if ((matrix.m11 >= matrix.m22) && (matrix.m11 >= matrix.m33))
+            {
+                float num7 = (float)Math.Sqrt((double)(((1f + matrix.m11) - matrix.m22) - matrix.m33));
+                float num4 = 0.5f / num7;
+                result.mX = 0.5f * num7;
+                result.mY = (matrix.m12 + matrix.m21) * num4;
+                result.mZ = (matrix.m13 + matrix.m31) * num4;
+                result.mW = (matrix.m23 - matrix.m32) * num4;
+            }
+            else if (matrix.m22 > matrix.m33)
+            {
+                float num6 = (float)Math.Sqrt((double)(((1f + matrix.m22) - matrix.m11) - matrix.m33));
+                float num3 = 0.5f / num6;
+                result.mX = (matrix.m21 + matrix.m12) * num3;
+                result.mY = 0.5f * num6;
+                result.mZ = (matrix.m32 + matrix.m23) * num3;
+                result.mW = (matrix.m31 - matrix.m13) * num3;
+            }
+            else
+            {
+                float num5 = (float)Math.Sqrt((double)(((1f + matrix.m33) - matrix.m11) - matrix.m22));
+                float num2 = 0.5f / num5;
+                result.mX = (matrix.m31 + matrix.m13) * num2;
+                result.mY = (matrix.m32 + matrix.m23) * num2;
+                result.mZ = 0.5f * num5;
+                result.mW = (matrix.m12 - matrix.m21) * num2;
+            }
+        }
+
+        public static Quaternion CreateFromYawPitchRoll(float yaw, float pitch, float roll)
+        {
+            Quaternion quaternion;
+            float num9 = roll * 0.5f;
+            float num6 = (float)Math.Sin((double)num9);
+            float num5 = (float)Math.Cos((double)num9);
+            float num8 = pitch * 0.5f;
+            float num4 = (float)Math.Sin((double)num8);
+            float num3 = (float)Math.Cos((double)num8);
+            float num7 = yaw * 0.5f;
+            float num2 = (float)Math.Sin((double)num7);
+            float num = (float)Math.Cos((double)num7);
+            quaternion.mX = ((num * num4) * num5) + ((num2 * num3) * num6);
+            quaternion.mY = ((num2 * num3) * num5) - ((num * num4) * num6);
+            quaternion.mZ = ((num * num3) * num6) - ((num2 * num4) * num5);
+            quaternion.mW = ((num * num3) * num5) + ((num2 * num4) * num6);
+            return quaternion;
+        }
+
+        public static void CreateFromYawPitchRoll(float yaw, float pitch, float roll, out Quaternion result)
+        {
+            float num9 = roll * 0.5f;
+            float num6 = (float)Math.Sin((double)num9);
+            float num5 = (float)Math.Cos((double)num9);
+            float num8 = pitch * 0.5f;
+            float num4 = (float)Math.Sin((double)num8);
+            float num3 = (float)Math.Cos((double)num8);
+            float num7 = yaw * 0.5f;
+            float num2 = (float)Math.Sin((double)num7);
+            float num = (float)Math.Cos((double)num7);
+            result.mX = ((num * num4) * num5) + ((num2 * num3) * num6);
+            result.mY = ((num2 * num3) * num5) - ((num * num4) * num6);
+            result.mZ = ((num * num3) * num6) - ((num2 * num4) * num5);
+            result.mW = ((num * num3) * num5) + ((num2 * num4) * num6);
+        }
+
+        public static Quaternion Divide(Quaternion quaternion1, Quaternion quaternion2)
+        {
+            Quaternion quaternion;
+            float x = quaternion1.mX;
+            float y = quaternion1.mY;
+            float z = quaternion1.mZ;
+            float w = quaternion1.mW;
+            float num14 = (((quaternion2.mX * quaternion2.mX) + (quaternion2.mY * quaternion2.mY)) + (quaternion2.mZ * quaternion2.mZ)) + (quaternion2.mW * quaternion2.mW);
+            float num5 = 1f / num14;
+            float num4 = -quaternion2.mX * num5;
+            float num3 = -quaternion2.mY * num5;
+            float num2 = -quaternion2.mZ * num5;
+            float num = quaternion2.mW * num5;
+            float num13 = (y * num2) - (z * num3);
+            float num12 = (z * num4) - (x * num2);
+            float num11 = (x * num3) - (y * num4);
+            float num10 = ((x * num4) + (y * num3)) + (z * num2);
+            quaternion.mX = ((x * num) + (num4 * w)) + num13;
+            quaternion.mY = ((y * num) + (num3 * w)) + num12;
+            quaternion.mZ = ((z * num) + (num2 * w)) + num11;
+            quaternion.mW = (w * num) - num10;
+            return quaternion;
+        }
+
+        public static void Divide(ref Quaternion quaternion1, ref Quaternion quaternion2, out Quaternion result)
+        {
+            float x = quaternion1.mX;
+            float y = quaternion1.mY;
+            float z = quaternion1.mZ;
+            float w = quaternion1.mW;
+            float num14 = (((quaternion2.mX * quaternion2.mX) + (quaternion2.mY * quaternion2.mY)) + (quaternion2.mZ * quaternion2.mZ)) + (quaternion2.mW * quaternion2.mW);
+            float num5 = 1f / num14;
+            float num4 = -quaternion2.mX * num5;
+            float num3 = -quaternion2.mY * num5;
+            float num2 = -quaternion2.mZ * num5;
+            float num = quaternion2.mW * num5;
+            float num13 = (y * num2) - (z * num3);
+            float num12 = (z * num4) - (x * num2);
+            float num11 = (x * num3) - (y * num4);
+            float num10 = ((x * num4) + (y * num3)) + (z * num2);
+            result.mX = ((x * num) + (num4 * w)) + num13;
+            result.mY = ((y * num) + (num3 * w)) + num12;
+            result.mZ = ((z * num) + (num2 * w)) + num11;
+            result.mW = (w * num) - num10;
+        }
+
+        public static float Dot(Quaternion quaternion1, Quaternion quaternion2)
+        {
+            return ((((quaternion1.mX * quaternion2.mX) + (quaternion1.mY * quaternion2.mY)) + (quaternion1.mZ * quaternion2.mZ)) + (quaternion1.mW * quaternion2.mW));
+        }
+
+        public static void Dot(ref Quaternion quaternion1, ref Quaternion quaternion2, out float result)
+        {
+            result = (((quaternion1.mX * quaternion2.mX) + (quaternion1.mY * quaternion2.mY)) + (quaternion1.mZ * quaternion2.mZ)) + (quaternion1.mW * quaternion2.mW);
+        }
+
+        public bool Equals(Quaternion other)
+        {
+            return (mX == other.mX) && (mY == other.mY) && (mZ == other.mZ) && (mW == other.mW);
+        }
+
+        public int GetHashCode()
+        {
+            //return ((mX.GetHashCode() + mY.GetHashCode()) + mZ.GetHashCode()) + mW.GetHashCode();
+			ThrowUnimplemented();
+        }
+
+        public static Quaternion Inverse(Quaternion quaternion)
+        {
+            Quaternion quaternion2;
+            float num2 = (((quaternion.mX * quaternion.mX) + (quaternion.mY * quaternion.mY)) + (quaternion.mZ * quaternion.mZ)) + (quaternion.mW * quaternion.mW);
+            float num = 1f / num2;
+            quaternion2.mX = -quaternion.mX * num;
+            quaternion2.mY = -quaternion.mY * num;
+            quaternion2.mZ = -quaternion.mZ * num;
+            quaternion2.mW = quaternion.mW * num;
+            return quaternion2;
+        }
+
+        public static void Inverse(ref Quaternion quaternion, out Quaternion result)
+        {
+            float num2 = (((quaternion.mX * quaternion.mX) + (quaternion.mY * quaternion.mY)) + (quaternion.mZ * quaternion.mZ)) + (quaternion.mW * quaternion.mW);
+            float num = 1f / num2;
+            result.mX = -quaternion.mX * num;
+            result.mY = -quaternion.mY * num;
+            result.mZ = -quaternion.mZ * num;
+            result.mW = quaternion.mW * num;
+        }
+
+        public float Length()
+        {
+            float num = (((mX * mX) + (mY * mY)) + (mZ * mZ)) + (mW * mW);
+            return (float)Math.Sqrt((double)num);
+        }
+
+        public float LengthSquared()
+        {
+            return ((((mX * mX) + (mY * mY)) + (mZ * mZ)) + (mW * mW));
+        }
+
+        public static Quaternion Lerp(Quaternion quaternion1, Quaternion quaternion2, float amount)
+        {
+            float num = amount;
+            float num2 = 1f - num;
+            Quaternion quaternion = Quaternion();
+            float num5 = (((quaternion1.mX * quaternion2.mX) + (quaternion1.mY * quaternion2.mY)) + (quaternion1.mZ * quaternion2.mZ)) + (quaternion1.mW * quaternion2.mW);
+            if (num5 >= 0f)
+            {
+                quaternion.mX = (num2 * quaternion1.mX) + (num * quaternion2.mX);
+                quaternion.mY = (num2 * quaternion1.mY) + (num * quaternion2.mY);
+                quaternion.mZ = (num2 * quaternion1.mZ) + (num * quaternion2.mZ);
+                quaternion.mW = (num2 * quaternion1.mW) + (num * quaternion2.mW);
+            }
+            else
+            {
+                quaternion.mX = (num2 * quaternion1.mX) - (num * quaternion2.mX);
+                quaternion.mY = (num2 * quaternion1.mY) - (num * quaternion2.mY);
+                quaternion.mZ = (num2 * quaternion1.mZ) - (num * quaternion2.mZ);
+                quaternion.mW = (num2 * quaternion1.mW) - (num * quaternion2.mW);
+            }
+            float num4 = (((quaternion.mX * quaternion.mX) + (quaternion.mY * quaternion.mY)) + (quaternion.mZ * quaternion.mZ)) + (quaternion.mW * quaternion.mW);
+            float num3 = 1f / ((float)Math.Sqrt((double)num4));
+            quaternion.mX *= num3;
+            quaternion.mY *= num3;
+            quaternion.mZ *= num3;
+            quaternion.mW *= num3;
+            return quaternion;
+        }
+
+        public static void Lerp(ref Quaternion quaternion1, ref Quaternion quaternion2, float amount, out Quaternion result)
+        {
+            float num = amount;
+            float num2 = 1f - num;
+            float num5 = (((quaternion1.mX * quaternion2.mX) + (quaternion1.mY * quaternion2.mY)) + (quaternion1.mZ * quaternion2.mZ)) + (quaternion1.mW * quaternion2.mW);
+            if (num5 >= 0f)
+            {
+                result.mX = (num2 * quaternion1.mX) + (num * quaternion2.mX);
+                result.mY = (num2 * quaternion1.mY) + (num * quaternion2.mY);
+                result.mZ = (num2 * quaternion1.mZ) + (num * quaternion2.mZ);
+                result.mW = (num2 * quaternion1.mW) + (num * quaternion2.mW);
+            }
+            else
+            {
+                result.mX = (num2 * quaternion1.mX) - (num * quaternion2.mX);
+                result.mY = (num2 * quaternion1.mY) - (num * quaternion2.mY);
+                result.mZ = (num2 * quaternion1.mZ) - (num * quaternion2.mZ);
+                result.mW = (num2 * quaternion1.mW) - (num * quaternion2.mW);
+            }
+            float num4 = (((result.mX * result.mX) + (result.mY * result.mY)) + (result.mZ * result.mZ)) + (result.mW * result.mW);
+            float num3 = 1f / ((float)Math.Sqrt((double)num4));
+            result.mX *= num3;
+            result.mY *= num3;
+            result.mZ *= num3;
+            result.mW *= num3;
+        }
+
+        public static Quaternion Slerp(Quaternion quaternion1, Quaternion quaternion2, float amount)
+        {
+            float num2;
+            float num3;
+            Quaternion quaternion;
+            float num = amount;
+            float num4 = (((quaternion1.mX * quaternion2.mX) + (quaternion1.mY * quaternion2.mY)) + (quaternion1.mZ * quaternion2.mZ)) + (quaternion1.mW * quaternion2.mW);
+            bool flag = false;
+            if (num4 < 0f)
+            {
+                flag = true;
+                num4 = -num4;
+            }
+            if (num4 > 0.999999f)
+            {
+                num3 = 1f - num;
+                num2 = flag ? -num : num;
+            }
+            else
+            {
+                float num5 = (float)Math.Acos((double)num4);
+                float num6 = (float)(1.0 / Math.Sin((double)num5));
+                num3 = ((float)Math.Sin((double)((1f - num) * num5))) * num6;
+                num2 = flag ? (((float)(-Math.Sin((double)(num * num5))) * num6)) : (((float)Math.Sin((double)(num * num5))) * num6);
+            }
+            quaternion.mX = (num3 * quaternion1.mX) + (num2 * quaternion2.mX);
+            quaternion.mY = (num3 * quaternion1.mY) + (num2 * quaternion2.mY);
+            quaternion.mZ = (num3 * quaternion1.mZ) + (num2 * quaternion2.mZ);
+            quaternion.mW = (num3 * quaternion1.mW) + (num2 * quaternion2.mW);
+            return quaternion;
+        }
+        
+        public static void Slerp(ref Quaternion quaternion1, ref Quaternion quaternion2, float amount, out Quaternion result)
+        {
+            float num2;
+            float num3;
+            float num = amount;
+            float num4 = (((quaternion1.mX * quaternion2.mX) + (quaternion1.mY * quaternion2.mY)) + (quaternion1.mZ * quaternion2.mZ)) + (quaternion1.mW * quaternion2.mW);
+            bool flag = false;
+            if (num4 < 0f)
+            {
+                flag = true;
+                num4 = -num4;
+            }
+            if (num4 > 0.999999f)
+            {
+                num3 = 1f - num;
+                num2 = flag ? -num : num;
+            }
+            else
+            {
+                float num5 = (float)Math.Acos((double)num4);
+                float num6 = (float)(1.0 / Math.Sin((double)num5));
+                num3 = ((float)Math.Sin((double)((1f - num) * num5))) * num6;
+                num2 = flag ? (((float)(-Math.Sin((double)(num * num5))) * num6)) : (((float)Math.Sin((double)(num * num5))) * num6);
+            }
+            result.mX = (num3 * quaternion1.mX) + (num2 * quaternion2.mX);
+            result.mY = (num3 * quaternion1.mY) + (num2 * quaternion2.mY);
+            result.mZ = (num3 * quaternion1.mZ) + (num2 * quaternion2.mZ);
+            result.mW = (num3 * quaternion1.mW) + (num2 * quaternion2.mW);
+        }
+
+
+        public static Quaternion Subtract(Quaternion quaternion1, Quaternion quaternion2)
+        {
+            Quaternion quaternion;
+            quaternion.mX = quaternion1.mX - quaternion2.mX;
+            quaternion.mY = quaternion1.mY - quaternion2.mY;
+            quaternion.mZ = quaternion1.mZ - quaternion2.mZ;
+            quaternion.mW = quaternion1.mW - quaternion2.mW;
+            return quaternion;
+        }
+        
+        public static void Subtract(ref Quaternion quaternion1, ref Quaternion quaternion2, out Quaternion result)
+        {
+            result.mX = quaternion1.mX - quaternion2.mX;
+            result.mY = quaternion1.mY - quaternion2.mY;
+            result.mZ = quaternion1.mZ - quaternion2.mZ;
+            result.mW = quaternion1.mW - quaternion2.mW;
+        }
+        
+        public static Quaternion Multiply(Quaternion quaternion1, Quaternion quaternion2)
+        {
+            Quaternion quaternion;
+            float x = quaternion1.mX;
+            float y = quaternion1.mY;
+            float z = quaternion1.mZ;
+            float w = quaternion1.mW;
+            float num4 = quaternion2.mX;
+            float num3 = quaternion2.mY;
+            float num2 = quaternion2.mZ;
+            float num = quaternion2.mW;
+            float num12 = (y * num2) - (z * num3);
+            float num11 = (z * num4) - (x * num2);
+            float num10 = (x * num3) - (y * num4);
+            float num9 = ((x * num4) + (y * num3)) + (z * num2);
+            quaternion.mX = ((x * num) + (num4 * w)) + num12;
+            quaternion.mY = ((y * num) + (num3 * w)) + num11;
+            quaternion.mZ = ((z * num) + (num2 * w)) + num10;
+            quaternion.mW = (w * num) - num9;
+            return quaternion;
+        }
+        
+        public static Quaternion Multiply(Quaternion quaternion1, float scaleFactor)
+        {
+            Quaternion quaternion;
+            quaternion.mX = quaternion1.mX * scaleFactor;
+            quaternion.mY = quaternion1.mY * scaleFactor;
+            quaternion.mZ = quaternion1.mZ * scaleFactor;
+            quaternion.mW = quaternion1.mW * scaleFactor;
+            return quaternion;
+        }
+        
+        public static void Multiply(ref Quaternion quaternion1, float scaleFactor, out Quaternion result)
+        {
+            result.mX = quaternion1.mX * scaleFactor;
+            result.mY = quaternion1.mY * scaleFactor;
+            result.mZ = quaternion1.mZ * scaleFactor;
+            result.mW = quaternion1.mW * scaleFactor;
+        }
+        
+        public static void Multiply(ref Quaternion quaternion1, ref Quaternion quaternion2, out Quaternion result)
+        {
+            float x = quaternion1.mX;
+            float y = quaternion1.mY;
+            float z = quaternion1.mZ;
+            float w = quaternion1.mW;
+            float num4 = quaternion2.mX;
+            float num3 = quaternion2.mY;
+            float num2 = quaternion2.mZ;
+            float num = quaternion2.mW;
+            float num12 = (y * num2) - (z * num3);
+            float num11 = (z * num4) - (x * num2);
+            float num10 = (x * num3) - (y * num4);
+            float num9 = ((x * num4) + (y * num3)) + (z * num2);
+            result.mX = ((x * num) + (num4 * w)) + num12;
+            result.mY = ((y * num) + (num3 * w)) + num11;
+            result.mZ = ((z * num) + (num2 * w)) + num10;
+            result.mW = (w * num) - num9;
+        }
+        
+        public static Quaternion Negate(Quaternion quaternion)
+        {
+            Quaternion quaternion2;
+            quaternion2.mX = -quaternion.mX;
+            quaternion2.mY = -quaternion.mY;
+            quaternion2.mZ = -quaternion.mZ;
+            quaternion2.mW = -quaternion.mW;
+            return quaternion2;
+        }
+        
+        public static void Negate(ref Quaternion quaternion, out Quaternion result)
+        {
+            result.mX = -quaternion.mX;
+            result.mY = -quaternion.mY;
+            result.mZ = -quaternion.mZ;
+            result.mW = -quaternion.mW;
+        }
+        
+        public void Normalize() mut
+        {
+            float num2 = (((mX * mX) + (mY * mY)) + (mZ * mZ)) + (mW * mW);
+            float num = 1f / ((float)Math.Sqrt((double)num2));
+            mX *= num;
+            mY *= num;
+            mZ *= num;
+            mW *= num;
+        }
+        
+        public static Quaternion Normalize(Quaternion quaternion)
+        {
+            Quaternion quaternion2;
+            float num2 = (((quaternion.mX * quaternion.mX) + (quaternion.mY * quaternion.mY)) + (quaternion.mZ * quaternion.mZ)) + (quaternion.mW * quaternion.mW);
+            float num = 1f / ((float)Math.Sqrt((double)num2));
+            quaternion2.mX = quaternion.mX * num;
+            quaternion2.mY = quaternion.mY * num;
+            quaternion2.mZ = quaternion.mZ * num;
+            quaternion2.mW = quaternion.mW * num;
+            return quaternion2;
+        }
+        
+        public static void Normalize(ref Quaternion quaternion, out Quaternion result)
+        {
+            float num2 = (((quaternion.mX * quaternion.mX) + (quaternion.mY * quaternion.mY)) + (quaternion.mZ * quaternion.mZ)) + (quaternion.mW * quaternion.mW);
+            float num = 1f / ((float)Math.Sqrt((double)num2));
+            result.mX = quaternion.mX * num;
+            result.mY = quaternion.mY * num;
+            result.mZ = quaternion.mZ * num;
+            result.mW = quaternion.mW * num;
+        }
+        
+        public static Quaternion operator +(Quaternion quaternion1, Quaternion quaternion2)
+        {
+            Quaternion quaternion;
+            quaternion.mX = quaternion1.mX + quaternion2.mX;
+            quaternion.mY = quaternion1.mY + quaternion2.mY;
+            quaternion.mZ = quaternion1.mZ + quaternion2.mZ;
+            quaternion.mW = quaternion1.mW + quaternion2.mW;
+            return quaternion;
+        }
+        
+        public static Quaternion operator /(Quaternion quaternion1, Quaternion quaternion2)
+        {
+            Quaternion quaternion;
+            float x = quaternion1.mX;
+            float y = quaternion1.mY;
+            float z = quaternion1.mZ;
+            float w = quaternion1.mW;
+            float num14 = (((quaternion2.mX * quaternion2.mX) + (quaternion2.mY * quaternion2.mY)) + (quaternion2.mZ * quaternion2.mZ)) + (quaternion2.mW * quaternion2.mW);
+            float num5 = 1f / num14;
+            float num4 = -quaternion2.mX * num5;
+            float num3 = -quaternion2.mY * num5;
+            float num2 = -quaternion2.mZ * num5;
+            float num = quaternion2.mW * num5;
+            float num13 = (y * num2) - (z * num3);
+            float num12 = (z * num4) - (x * num2);
+            float num11 = (x * num3) - (y * num4);
+            float num10 = ((x * num4) + (y * num3)) + (z * num2);
+            quaternion.mX = ((x * num) + (num4 * w)) + num13;
+            quaternion.mY = ((y * num) + (num3 * w)) + num12;
+            quaternion.mZ = ((z * num) + (num2 * w)) + num11;
+            quaternion.mW = (w * num) - num10;
+            return quaternion;
+        }
+        
+        public static bool operator ==(Quaternion quaternion1, Quaternion quaternion2)
+        {
+            return ((((quaternion1.mX == quaternion2.mX) && (quaternion1.mY == quaternion2.mY)) && (quaternion1.mZ == quaternion2.mZ)) && (quaternion1.mW == quaternion2.mW));
+        }
+        
+        public static bool operator !=(Quaternion quaternion1, Quaternion quaternion2)
+        {
+            if (((quaternion1.mX == quaternion2.mX) && (quaternion1.mY == quaternion2.mY)) && (quaternion1.mZ == quaternion2.mZ))            
+                return (quaternion1.mW != quaternion2.mW);            
+            return true;
+        }
+        
+        public static Quaternion operator *(Quaternion quaternion1, Quaternion quaternion2)
+        {
+            Quaternion quaternion;
+            float x = quaternion1.mX;
+            float y = quaternion1.mY;
+            float z = quaternion1.mZ;
+            float w = quaternion1.mW;
+            float num4 = quaternion2.mX;
+            float num3 = quaternion2.mY;
+            float num2 = quaternion2.mZ;
+            float num = quaternion2.mW;
+            float num12 = (y * num2) - (z * num3);
+            float num11 = (z * num4) - (x * num2);
+            float num10 = (x * num3) - (y * num4);
+            float num9 = ((x * num4) + (y * num3)) + (z * num2);
+            quaternion.mX = ((x * num) + (num4 * w)) + num12;
+            quaternion.mY = ((y * num) + (num3 * w)) + num11;
+            quaternion.mZ = ((z * num) + (num2 * w)) + num10;
+            quaternion.mW = (w * num) - num9;
+            return quaternion;
+        }
+        
+        public static Quaternion operator *(Quaternion quaternion1, float scaleFactor)
+        {
+            Quaternion quaternion;
+            quaternion.mX = quaternion1.mX * scaleFactor;
+            quaternion.mY = quaternion1.mY * scaleFactor;
+            quaternion.mZ = quaternion1.mZ * scaleFactor;
+            quaternion.mW = quaternion1.mW * scaleFactor;
+            return quaternion;
+        }
+        
+        public static Quaternion operator -(Quaternion quaternion1, Quaternion quaternion2)
+        {
+            Quaternion quaternion;
+            quaternion.mX = quaternion1.mX - quaternion2.mX;
+            quaternion.mY = quaternion1.mY - quaternion2.mY;
+            quaternion.mZ = quaternion1.mZ - quaternion2.mZ;
+            quaternion.mW = quaternion1.mW - quaternion2.mW;
+            return quaternion;
+        }
+        
+        public static Quaternion operator -(Quaternion quaternion)
+        {
+            Quaternion quaternion2;
+            quaternion2.mX = -quaternion.mX;
+            quaternion2.mY = -quaternion.mY;
+            quaternion2.mZ = -quaternion.mZ;
+            quaternion2.mW = -quaternion.mW;
+            return quaternion2;
+        }
+        
+        public override void ToString(String outStr)
+        {
+            ThrowUnimplemented();
+        }
+
+        internal Matrix4 ToMatrix()
+        {
+            Matrix4 matrix = Matrix4.Identity;
+            ToMatrix(out matrix);
+            return matrix;
+        }
+
+        /*internal void ToMatrix(out Matrix4 matrix)
+        {
+            Quaternion.ToMatrix(this, out matrix);
+        }*/
+
+        public void ToMatrix(out Matrix4 matrix)
+        {            
+            float fTx = mX + mX;
+            float fTy = mY + mY;
+            float fTz = mZ + mZ;
+            float fTwx = fTx * mW;
+            float fTwy = fTy * mW;
+            float fTwz = fTz * mW;
+            float fTxx = fTx * mX;
+            float fTxy = fTy * mX;
+            float fTxz = fTz * mX;
+            float fTyy = fTy * mY;
+            float fTyz = fTz * mY;
+            float fTzz = fTz * mZ;
+
+            matrix.m00 = 1.0f - (fTyy + fTzz);
+            matrix.m01 = fTxy - fTwz;
+            matrix.m02 = fTxz + fTwy;
+            matrix.m03 = 0;
+
+            matrix.m10 = fTxy + fTwz;
+            matrix.m11 = 1.0f - (fTxx + fTzz);
+            matrix.m12 = fTyz - fTwx;
+            matrix.m13 = 0;
+
+            matrix.m20 = fTxz - fTwy;
+            matrix.m21 = fTyz + fTwx;
+            matrix.m22 = 1.0f - (fTxx + fTyy);
+            matrix.m23 = 0;
+
+            matrix.m30 = 0;
+            matrix.m31 = 0;
+            matrix.m32 = 0;
+            matrix.m33 = 1.0f;
+        }
+
+        internal Vector3 XYZ
+        {
+            get
+            {
+                return Vector3(mX, mY, mZ);
+            }
+
+            set mut
+            {
+                mX = value.mX;
+                mY = value.mY;
+                mZ = value.mZ;
+            }
+        }
+    }
+}

+ 197 - 0
BeefLibs/Beefy2D/src/geom/Rect.bf

@@ -0,0 +1,197 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.gfx;
+
+namespace Beefy.geom
+{
+    public struct Rect
+    {
+        public float mX;
+        public float mY;
+        public float mWidth;
+        public float mHeight;
+
+		public float Left
+		{
+			get
+			{
+				return mX;
+			}
+		}
+
+		public float Top
+		{
+			get
+			{
+				return mY;
+			}
+		}
+
+		public float Right
+		{
+			get
+			{
+				return mX + mWidth;
+			}
+		}
+
+		public float Bottom
+		{
+			get
+			{
+				return mY + mHeight;
+			}
+		}
+
+		public float Width
+		{
+			get
+			{
+				return mWidth;
+			}
+		}
+
+		public float Height
+		{
+			get
+			{
+				return mHeight;
+			}
+		}
+
+        public this(float x = 0, float y = 0, float width = 0, float height = 0)
+        {
+            mX = x;
+            mY = y;
+            mWidth = width;
+            mHeight = height;
+        }
+
+        public void Set(float x = 0, float y = 0, float width = 0, float height = 0) mut
+        {
+            mX = x;
+            mY = y;
+            mWidth = width;
+            mHeight = height;
+        }
+
+        public bool Intersects(Rect rect)
+        {
+            return !((rect.mX + rect.mWidth <= mX) ||
+                    (rect.mY + rect.mHeight <= mY) ||
+                    (rect.mX >= mX + mWidth) ||
+                    (rect.mY >= mY + mHeight));
+        }
+
+        public void SetIntersectionOf(Rect rect1, Rect rect2) mut
+        {            
+            float x1 = Math.Max(rect1.mX, rect2.mX);
+            float x2 = Math.Min(rect1.mX + rect1.mWidth, rect2.mX + rect2.mWidth);
+            float y1 = Math.Max(rect1.mY, rect2.mY);
+            float y2 = Math.Min(rect1.mY + rect1.mHeight, rect2.mY + rect2.mHeight);
+            if (((x2 - x1) < 0) || ((y2 - y1) < 0))
+            {
+                mX = 0;
+                mY = 0;
+                mWidth = 0;
+                mHeight = 0;
+            }
+            else
+            {
+                mX = x1;
+                mY = y1;
+                mWidth = x2 - x1;
+                mHeight = y2 - y1;
+            }
+        }
+
+        public void SetIntersectionOf(Rect rect1, float x, float y, float width, float height) mut
+        {
+            float x1 = Math.Max(rect1.mX, x);
+            float x2 = Math.Min(rect1.mX + rect1.mWidth, x + width);
+            float y1 = Math.Max(rect1.mY, y);
+            float y2 = Math.Min(rect1.mY + rect1.mHeight, y + height);
+            if (((x2 - x1) < 0) || ((y2 - y1) < 0))
+            {
+                mX = 0;
+                mY = 0;
+                mWidth = 0;
+                mHeight = 0;
+            }
+            else
+            {
+                mX = x1;
+                mY = y1;
+                mWidth = x2 - x1;
+                mHeight = y2 - y1;
+            }
+        }
+
+        public Rect Intersection(Rect rect)
+        {
+            float x1 = Math.Max(mX, rect.mX);
+            float x2 = Math.Min(mX + mWidth, rect.mX + rect.mWidth);
+            float y1 = Math.Max(mY, rect.mY);
+            float y2 = Math.Min(mY + mHeight, rect.mY + rect.mHeight);
+            if (((x2 - x1) < 0) || ((y2 - y1) < 0))
+                return Rect(0, 0, 0, 0);
+            else
+                return Rect(x1, y1, x2 - x1, y2 - y1);
+        }
+
+        public Rect Union(Rect rect)
+        {
+            float x1 = Math.Min(mX, rect.mX);
+            float x2 = Math.Max(mX + mWidth, rect.mX + rect.mWidth);
+            float y1 = Math.Min(mY, rect.mY);
+            float y2 = Math.Max(mY + mHeight, rect.mY + rect.mHeight);
+            return Rect(x1, y1, x2 - x1, y2 - y1);
+        }
+
+        public bool Contains(float x, float y)
+        {
+            return ((x >= mX) && (x < mX + mWidth) &&
+                    (y >= mY) && (y < mY + mHeight));
+        }
+
+        public bool Contains(Point pt)
+        {
+            return Contains(pt.x, pt.y);
+        }
+
+        public bool Contains(Rect rect)
+        {
+            return Contains(rect.mX, rect.mY) && Contains(rect.mX + rect.mWidth, rect.mY + rect.mHeight);
+        }
+
+        public void Offset(float x, float y) mut
+        {
+            mX += x;
+            mY += y;
+        }
+
+        public void Inflate(float x, float y) mut
+        {
+            mX -= x;
+            mWidth += x * 2;
+            mY -= y;
+            mHeight += y * 2;
+        }
+
+        public void Scale(float scaleX, float scaleY) mut
+        {
+            mX *= scaleX;
+            mY *= scaleY;
+            mWidth *= scaleX;
+            mHeight *= scaleY;
+        }
+
+        public void ScaleFrom(float scaleX, float scaleY, float centerX, float centerY) mut
+        {
+            Offset(-centerX, -centerY);
+            Scale(scaleX, scaleY);
+            Offset(centerX, centerY);
+        }
+    }
+}

+ 87 - 0
BeefLibs/Beefy2D/src/geom/Vector2.bf

@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Beefy.geom
+{
+    public struct Vector2
+    {
+        public float mX;
+        public float mY;
+
+        public float Length
+        {
+            get
+            {
+                return (float)Math.Sqrt(mX * mX + mY * mY);
+            }
+        }
+
+        public float LengthSquared
+        {
+            get
+            {
+                return mX * mX + mY * mY;
+            }
+        }
+
+        public this(float x, float y)
+        {
+            mX = x;
+            mY = y;
+        }
+
+        public static void DistanceSquared(Vector2 value1, Vector2 value2, out float result)
+        {
+            result = (value1.mX - value2.mX) * (value1.mX - value2.mX) +
+                     (value1.mY - value2.mY) * (value1.mY - value2.mY);
+        }
+
+        public static float Distance(Vector2 vector1, Vector2 vector2)
+        {
+            float result;
+            DistanceSquared(vector1, vector2, out result);
+            return (float)Math.Sqrt(result);
+        }
+
+        public static Vector2 Add(Vector2 vec1, Vector2 vec2)
+        {
+            return Vector2(vec1.mX + vec2.mX, vec1.mY + vec2.mY);
+        }
+
+        public static Vector2 Subtract(Vector2 vec1, Vector2 vec2)
+        {
+            return Vector2(vec1.mX - vec2.mX, vec1.mY - vec2.mY);
+        }
+
+        public static float Dot(Vector2 vec1, Vector2 vec2)
+        {
+            return vec1.mX * vec2.mX + vec1.mY * vec2.mY;            
+        }
+
+        public static Vector2 FromAngle(float angle, float length = 1.0f)
+        {
+            return Vector2((float)Math.Cos(angle) * length, (float)Math.Sin(angle) * length);
+        }
+
+        public static Vector2 operator +(Vector2 vec1, Vector2 vec2)
+        {
+            return Vector2(vec1.mX + vec2.mX, vec1.mY + vec2.mY);
+        }
+
+        public static Vector2 operator -(Vector2 vec1, Vector2 vec2)
+        {
+            return  Vector2(vec1.mX - vec2.mX, vec1.mY - vec2.mY);
+        }
+
+        public static Vector2 operator *(Vector2 vec1, float factor)
+        {
+            return Vector2(vec1.mX * factor, vec1.mY * factor);
+        }
+
+		public static Vector2 operator /(Vector2 vec1, float factor)
+		{
+		    return Vector2(vec1.mX / factor, vec1.mY / factor);
+		}
+    }
+}

+ 247 - 0
BeefLibs/Beefy2D/src/geom/Vector3.bf

@@ -0,0 +1,247 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Beefy.gfx;
+
+namespace Beefy.geom
+{
+    public struct Vector3 : IHashable, IEquatable<Vector3>
+    {
+		[Reflect]
+        public float mX;
+		[Reflect]
+        public float mY;
+		[Reflect]
+        public float mZ;
+
+        private static Vector3 sZero = Vector3(0f, 0f, 0f);
+        private static Vector3 sOne = Vector3(1f, 1f, 1f);
+        private static Vector3 sUnitX = Vector3(1f, 0f, 0f);
+        private static Vector3 sUnitY = Vector3(0f, 1f, 0f);
+        private static Vector3 sUnitZ = Vector3(0f, 0f, 1f);
+        private static Vector3 sUp = Vector3(0f, 1f, 0f);
+        private static Vector3 sDown = Vector3(0f, -1f, 0f);
+        private static Vector3 sRight = Vector3(1f, 0f, 0f);
+        private static Vector3 sLeft = Vector3(-1f, 0f, 0f);
+        private static Vector3 sForward = Vector3(0f, 0f, -1f);
+        private static Vector3 sBackward = Vector3(0f, 0f, 1f);
+
+        public static Vector3 Zero
+        {
+            get { return sZero; }
+        }
+
+        public static Vector3 One
+        {
+            get { return sOne; }
+        }
+
+        public static Vector3 UnitX
+        {
+            get { return sUnitX; }
+        }
+
+        public static Vector3 UnitY
+        {
+            get { return sUnitY; }
+        }
+
+        public static Vector3 UnitZ
+        {
+            get { return sUnitZ; }
+        }
+
+        public static Vector3 Up
+        {
+            get { return sUp; }
+        }
+
+        public static Vector3 Down
+        {
+            get { return sDown; }
+        }
+
+        public static Vector3 Right
+        {
+            get { return sRight; }
+        }
+
+        public static Vector3 Left
+        {
+            get { return sLeft; }
+        }
+
+        public static Vector3 Forward
+        {
+            get { return sForward; }
+        }
+
+        public static Vector3 Backward
+        {
+            get { return sBackward; }
+        }
+
+        public float Length
+        {
+            get
+            {
+                return (float)Math.Sqrt(mX * mX + mY * mY + mZ * mZ);
+            }
+        }
+
+        public float LengthSquared
+        {
+            get
+            {
+                return mX * mX + mY * mY + mZ * mZ;
+            }
+        }
+
+        public this(float x, float y, float z)
+        {
+            mX = x;
+            mY = y;
+            mZ = z;
+        }
+
+        public bool Equals(Vector3 other)
+        {
+            return this == other;
+        }
+
+        public int GetHashCode()
+        {
+            return (int)(this.mX + this.mY + this.mZ);
+        }
+
+
+        /*public static Vector2D Add(Vector2D vec1, Vector2D vec2)
+        {
+            return new Vector2D(vec1.mX + vec2.mX, vec1.mY + vec2.mY);
+        }
+
+        public static Vector2D Subtract(Vector2D vec1, Vector2D vec2)
+        {
+            return new Vector2D(vec1.mX - vec2.mX, vec1.mY - vec2.mY);
+        }*/
+        
+
+        public static Vector3 Normalize(Vector3 vector)
+        {
+			Vector3 newVec;
+            Normalize(vector, out newVec);
+            return vector;
+        }
+
+        public static void Normalize(Vector3 value, out Vector3 result)
+        {
+            float factor= Distance(value, sZero);
+            factor = 1f / factor;
+            result.mX = value.mX * factor;
+            result.mY = value.mY * factor;
+            result.mZ = value.mZ * factor;
+        }
+
+        public static float Dot(Vector3 vec1, Vector3 vec2)
+        {
+            return vec1.mX * vec2.mX + vec1.mY * vec2.mY + vec1.mZ * vec2.mZ;
+        }
+
+        public static Vector3 Cross(Vector3 vector1, Vector3 vector2)
+        {
+            return Vector3(vector1.mY * vector2.mZ - vector2.mY * vector1.mZ,
+                                 -(vector1.mX * vector2.mZ - vector2.mX * vector1.mZ),
+                                 vector1.mX * vector2.mY - vector2.mX * vector1.mY);
+        }
+
+        public static float DistanceSquared(Vector3 value1, Vector3 value2)
+        {
+            return (value1.mX - value2.mX) * (value1.mX - value2.mX) +
+                     (value1.mY - value2.mY) * (value1.mY - value2.mY) +
+                     (value1.mZ - value2.mZ) * (value1.mZ - value2.mZ);
+        }
+
+        public static float Distance(Vector3 vector1, Vector3 vector2)
+        {
+            float result = DistanceSquared(vector1, vector2);
+            return (float)Math.Sqrt(result);
+        }
+
+        /*public static Vector2D FromAngle(float angle, float length = 1.0f)
+        {
+            return new Vector2D((float)Math.Cos(angle) * length, (float)Math.Sin(angle) * length);
+        }*/
+
+        public static Vector3 Transform(Vector3 vec, Matrix4 matrix)
+        {
+			Vector3 result;
+            float fInvW = 1.0f / (matrix.m30 * vec.mX + matrix.m31 * vec.mY + matrix.m32 * vec.mZ + matrix.m33);
+
+            result.mX = (matrix.m00 * vec.mX + matrix.m01 * vec.mY + matrix.m02 * vec.mZ + matrix.m03) * fInvW;
+            result.mY = (matrix.m10 * vec.mX + matrix.m11 * vec.mY + matrix.m12 * vec.mZ + matrix.m13) * fInvW;
+            result.mZ = (matrix.m20 * vec.mX + matrix.m21 * vec.mY + matrix.m22 * vec.mZ + matrix.m23) * fInvW;
+			return result;
+        }
+
+        /*public static void Transform(Vector3[] sourceArray, ref Matrix4 matrix, Vector3[] destinationArray)
+        {
+            //Debug.Assert(destinationArray.Length >= sourceArray.Length, "The destination array is smaller than the source array.");
+            
+            for (var i = 0; i < sourceArray.Length; i++)
+            {
+                var position = sourceArray[i];
+                destinationArray[i] =
+                    new Vector3(
+                        (position.mX * matrix.m11) + (position.mY * matrix.m21) + (position.mZ * matrix.m31) + matrix.m41,
+                        (position.mX * matrix.m12) + (position.mY * matrix.m22) + (position.mZ * matrix.m32) + matrix.m42,
+                        (position.mX * matrix.m13) + (position.mY * matrix.m23) + (position.mZ * matrix.m33) + matrix.m43);
+            }
+        }*/
+
+        public static Vector3 Transform(Vector3 vec, Quaternion quat)
+        {        
+            Matrix4 matrix = quat.ToMatrix();
+            return Transform(vec, matrix);
+        }
+
+        public static Vector3 TransformNormal(Vector3 normal, Matrix4 matrix)
+        {
+            return Vector3((normal.mX * matrix.m11) + (normal.mY * matrix.m21) + (normal.mZ * matrix.m31),
+                                 (normal.mX * matrix.m12) + (normal.mY * matrix.m22) + (normal.mZ * matrix.m32),
+                                 (normal.mX * matrix.m13) + (normal.mY * matrix.m23) + (normal.mZ * matrix.m33));
+        }
+
+        public static bool operator ==(Vector3 value1, Vector3 value2)
+        {
+            return (value1.mX == value2.mX) &&
+                (value1.mY == value2.mY) &&
+                (value1.mZ == value2.mZ);
+        }
+
+        public static bool operator !=(Vector3 value1, Vector3 value2)
+        {
+            return !(value1 == value2);
+        }
+
+        public static Vector3 operator +(Vector3 vec1, Vector3 vec2)
+        {
+            return Vector3(vec1.mX + vec2.mX, vec1.mY + vec2.mY, vec1.mZ + vec2.mZ);
+        }        
+
+        public static Vector3 operator -(Vector3 vec1, Vector3 vec2)
+        {
+            return Vector3(vec1.mX - vec2.mX, vec1.mY - vec2.mY, vec1.mZ - vec2.mZ);
+        }
+
+        public static Vector3 operator *(Vector3 vec, float scale)
+        {
+            return Vector3(vec.mX * scale, vec.mY * scale, vec.mZ * scale);
+        }
+
+        public override void ToString(String str)
+        {
+            str.AppendF("{0:0.0#}, {1:0.0#}, {2:0.0#}", mX, mY, mZ);
+        }
+    }
+}

+ 158 - 0
BeefLibs/Beefy2D/src/gfx/Color.bf

@@ -0,0 +1,158 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Beefy.gfx
+{
+    public struct Color : uint32
+    {
+        public const Color White = 0xFFFFFFFF;
+        public const Color Black = 0xFF000000;
+        public const Color Red = 0xFFFF0000;
+        public const Color Green = 0xFF00FF00;
+        public const Color Blue = 0xFF0000FF;
+        public const Color Yellow = 0xFFFFFF00;
+
+		public static implicit operator uint32(Color color);
+		public static implicit operator Color(uint32 color);
+
+		public this(int32 r, int32 g, int32 b)
+		{
+			this = 0xFF000000 | (uint32)((r << 16) | (g << 8) | (b));
+		}
+
+		public this(int32 r, int32 g, int32 b, int32 a)
+		{
+			this = (uint32)((a << 24) | (r << 16) | (g << 8) | (b));
+		}
+
+        public static Color Get(float a)
+        {
+            return 0x00FFFFFF | (((uint32)(255.0f * a)) << 24);
+        }
+
+        public static Color Get(uint32 rgb, float a)
+        {
+            return (uint32)(((uint32)(a * 255) << 24) | (rgb & 0xffffff));
+        }
+
+        public static Color Get(int32 r, int32 g, int32 b)
+        {
+            return 0xFF000000 | (uint32)((r << 16) | (g << 8) | (b));
+        }
+
+        public static Color Get(int32 r, int32 g, int32 b, int32 a)
+        {
+            return (uint32)((a << 24) | (r << 16) | (g << 8) | (b));
+        }
+
+        public static float GetAlpha(uint32 color)
+        {
+            return ((float)(0xff & (color >> 24))) / 255.0f;
+        }
+        
+        public static Color Mult(Color color, Color colorMult)
+        {
+            if (color == 0xFFFFFFFF)
+                return colorMult;
+            if (colorMult == 0xFFFFFFFF)
+                return color;
+
+            uint32 result =
+              (((((color >> 24) & 0xFF) * ((colorMult >> 24) & 0xFF)) / 255) << 24) |
+                (((((color >> 16) & 0xFF) * ((colorMult >> 16) & 0xFF)) / 255) << 16) |
+                (((((color >> 8) & 0xFF) * ((colorMult >> 8) & 0xFF)) / 255) << 8) |
+                (((color & 0xFF) * (colorMult & 0xFF)) / 255);
+            return result;
+        }
+
+        public static Color Lerp(Color color1, Color color2, float pct)
+        {
+            if (color1 == color2)
+                return color1;
+
+            uint32 a = (uint32)(pct * 256.0f);
+            uint32 oma = 256 - a;
+            uint32 aColor =
+                (((((color1 & 0x000000FF) * oma) + ((color2 & 0x000000FF) * a)) >> 8) & 0x000000FF) |
+                (((((color1 & 0x0000FF00) * oma) + ((color2 & 0x0000FF00) * a)) >> 8) & 0x0000FF00) |
+                (((((color1 & 0x00FF0000) * oma) + ((color2 & 0x00FF0000) * a)) >> 8) & 0x00FF0000) |
+                (((((color1 >> 24) & 0xFF) * oma) + (((color2 >> 24) & 0xFF) * a) & 0x0000FF00) << 16);
+
+            return aColor;
+        }
+
+        public static void ToHSV(uint32 color, out float h, out float s, out float v)
+        {
+            float r = ((color >> 16) & 0xFF) / 255.0f;
+            float g = ((color >> 8) & 0xFF) / 255.0f;
+            float b = ((color >> 0) & 0xFF) / 255.0f;
+
+            float min, max, delta;
+            min = Math.Min(r, Math.Min(g, b));
+            max = Math.Max(r, Math.Max(g, b));
+            v = max;               // v
+            delta = max - min;
+            if (max != 0)
+                s = delta / max;       // s
+            else
+            {
+                // r = g = b = 0		// s = 0, v is undefined
+                s = 0;
+                h = -1;
+                return;
+            }
+            if (r == max)
+                h = (g - b) / delta;       // between yellow & magenta
+            else if (g == max)
+                h = 2 + (b - r) / delta;   // between cyan & yellow
+            else
+                h = 4 + (r - g) / delta;   // between magenta & cyan
+            h /= 6;                        // degrees
+            if (h < 0)
+                h += 1.0f;
+        }
+
+        public static Color FromHSV(float h, float s, float v, int32 a)
+        {            
+            float r, g, b;
+		    if (s == 0.0f)
+            {
+			    r = g = b = v;
+            }
+            else
+            {
+                float useH = h * 6.0f;
+		        int32 i = (int32)useH;
+		        float f = useH - i;
+		        if ((i & 1) == 0)
+			        f = 1 - f;
+		        float m = v * (1 - s);
+		        float n = v * (1 - s*f);
+		        switch(i)
+		        {
+		        case 0: fallthrough; 
+                case 6: r = v; g = n; b = m; break;
+		        case 1: r = n; g = v; b = m; break;
+		        case 2: r = m; g = v; b = n; break;
+		        case 3: r = m; g = n; b = v; break;
+		        case 4: r = n; g = m; b = v; break;
+		        case 5: r = v; g = m; b = n; break;
+		        default: r = 0; g = 0; b = 0; break;
+                }
+		    }
+        
+            return Get((int32)(r * 255.0f), (int32)(g * 255.0f), (int32)(b * 255.0f), a);
+        }
+
+        public static Color FromHSV(float h, float s, float v)
+        {
+            return FromHSV(h, s, v, 0xFF);
+        }
+
+		public static uint32 ToNative(Color color)
+		{
+			return (color & 0xFF00FF00) | ((color & 0x00FF0000) >> 16) | ((color & 0x000000FF) << 16);
+		}
+    }
+}

+ 103 - 0
BeefLibs/Beefy2D/src/gfx/ConstantDataDefinition.bf

@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using System.Runtime.InteropServices;
+using System.Reflection;
+using System.Diagnostics;
+
+namespace Beefy.gfx
+{
+    public class ConstantDataDefinition
+    {
+        public enum DataType
+        {
+            Float,
+            Vector2,
+            Vector3,
+            Vector4,
+            Matrix,
+
+			VertexShaderUsage = 0x100,
+			PixelShaderUsage = 0x200
+        }
+
+        public int32 mDataSize;
+        public DataType[] mDataTypes ~ delete _;
+
+		public this(int32 dataSize, DataType[] dataTypes)
+		{
+			mDataSize = dataSize;
+			mDataTypes = dataTypes;
+		}
+
+        public this(Type type)
+        {
+			ThrowUnimplemented();
+
+            /*var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);            
+            int memberCount = fields.Length;
+            mDataTypes = new int[memberCount];
+
+            mDataSize = Marshal.SizeOf(type);
+            
+            List<Type> primitives = new List<Type>();
+
+            int fieldIdx = 0;
+            foreach (var field in fields)                
+            {
+                var memberAttribute = field.GetCustomAttribute<VertexMemberAttribute>();
+                
+                primitives.Clear();
+                VertexDefinition.FindPrimitives(field.FieldType, primitives);
+
+                int floats = 0;
+                int shorts = 0;
+                int colors = 0;
+
+                foreach (var prim in primitives)
+                {
+                    if (prim == typeof(float))
+                        floats++;
+                    else if (prim == typeof(ushort))
+                        shorts++;
+                    else if (prim == typeof(uint))
+                        colors++;
+                }
+
+                DataType dataType = DataType.Single;
+                int usageType = 1;
+
+                if (floats != 0)
+                {
+                    Debug.Assert(floats == primitives.Count);
+                    if (floats == 16)
+                        dataType = DataType.Matrix;
+                    else
+                    {
+                        Debug.Assert(floats <= 4);
+                        dataType = DataType.Single + floats - 1;
+                    }
+                }
+                else if (shorts != 0)
+                {
+                    /*if (shorts == 2)
+                        dataType = DataType.Short2;
+                    else if (shorts == 4)
+                        dataType = DataType.Short4;
+                    else*/
+                        Debug.Fail("Invalid short count");
+                }
+                else if (colors != 0)
+                {
+                    /*if (colors == 1)
+                        vertexDefData.mFormat = VertexElementFormat.Color;
+                    else*/
+                        Debug.Fail("Invalid color count");
+                }
+
+                mDataTypes[fieldIdx++] = (int)dataType | (usageType << 8);
+            } */
+        }
+    }
+}

+ 19 - 0
BeefLibs/Beefy2D/src/gfx/ConstantDataMemberAttribute.bf

@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Beefy.gfx
+{
+    public struct ConstantDataMemberAttribute : Attribute
+    {
+        public bool mVertexShaderWants;
+        public bool mPixelShaderWants;
+
+        public this(bool vertexShaderWants, bool pixelShaderWants)
+        {
+            mVertexShaderWants = vertexShaderWants;
+            mPixelShaderWants = pixelShaderWants;
+        }
+    }
+}

+ 25 - 0
BeefLibs/Beefy2D/src/gfx/DefaultVertex.bf

@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Beefy.geom;
+
+namespace Beefy.gfx
+{
+    public struct DefaultVertex
+    {
+        [VertexMember(VertexElementUsage.Position2D)]
+        public Vector3 mPos;
+        [VertexMember(VertexElementUsage.TextureCoordinate)]
+        public TexCoords mTexCoords;
+        [VertexMember(VertexElementUsage.Color)]
+        public uint32 mColor;
+        
+        public static VertexDefinition sVertexDefinition ~ delete _;
+
+		public static void Init()
+		{
+			sVertexDefinition = new VertexDefinition(typeof(DefaultVertex));
+		}
+    }
+}

+ 88 - 0
BeefLibs/Beefy2D/src/gfx/DrawLayer.bf

@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Text;
+using Beefy.widgets;
+using Beefy;
+
+#if STUDIO_CLIENT
+using Beefy.ipc;
+#endif
+
+namespace Beefy.gfx
+{
+#if !STUDIO_CLIENT
+    public class DrawLayer
+    {
+        [StdCall, CLink]
+        static extern void* DrawLayer_Create(void* window);
+
+        [StdCall, CLink]
+        static extern void DrawLayer_Delete(void* drawLayer);
+
+        [StdCall, CLink]
+        static extern void DrawLayer_Clear(void* drawLayer);
+
+        [StdCall, CLink]
+        static extern void DrawLayer_Activate(void* drawLayer);
+
+        [StdCall, CLink]
+        static extern void DrawLayer_DrawToRenderTarget(void* drawLayer, void* texture);
+
+        public void* mNativeDrawLayer;
+
+        public this(BFWindow window)
+        {
+            mNativeDrawLayer = DrawLayer_Create((window != null) ? (window.mNativeWindow) : null);
+        }
+
+        public ~this()
+        {
+            DrawLayer_Delete(mNativeDrawLayer);
+        }
+
+        public void Activate()
+        {
+            DrawLayer_Activate(mNativeDrawLayer);
+        }
+
+        public void Clear()
+        {
+            DrawLayer_Clear(mNativeDrawLayer);
+        }
+
+        public void DrawToRenderTarget(Image texture)
+        {
+            DrawLayer_DrawToRenderTarget(mNativeDrawLayer, texture.mNativeTextureSegment);
+        }
+    }
+#else
+    public class DrawLayer
+    {
+        IPCProxy<IStudioDrawLayer> mDrawLayer;
+
+        public DrawLayer(BFWindow window)
+        {
+            IPCObjectId drawLayerObjId = BFApp.sApp.mStudioHost.Proxy.CreateDrawLayer((window != null) ? window.mRemoteWindow.ObjId : IPCObjectId.Null);
+            mDrawLayer = IPCProxy<IStudioDrawLayer>.Create(drawLayerObjId);
+        }
+
+        public void Dispose()
+        {            
+        }
+
+        public void Activate()
+        {
+            mDrawLayer.Proxy.Activate();
+        }
+
+        public void Clear()
+        {            
+        }
+
+        public void DrawToRenderTarget(Image texture)
+        {
+        }
+    }
+#endif
+}

+ 881 - 0
BeefLibs/Beefy2D/src/gfx/Font.bf

@@ -0,0 +1,881 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Collections.Generic;
+using System.Text;
+using System.IO;
+using Beefy.geom;
+using Beefy.utils;
+using System.Diagnostics;
+using System.Threading;
+
+namespace Beefy.gfx
+{
+    public enum FontOverflowMode
+    {            
+        Overflow,
+        Clip,
+        Truncate,
+        Wrap,
+        Ellipsis
+    }
+																													  
+    public enum FontAlign
+    {
+        Left = -1,
+        Centered,
+        Right
+    }
+
+    public struct FontMetrics
+    {
+        public int32 mLineCount;
+        public float mMinX;
+        public float mMinY;
+        public float mMaxX;
+        public float mMaxY;
+
+        public float mMaxWidth;
+    }
+
+    public class Font
+    {
+		[StdCall, CLink]
+		static extern FTFont* FTFont_Load(char8* fileName, float pointSize);
+
+		[StdCall, CLink]
+		static extern void FTFont_Delete(FTFont* ftFont, bool cacheRetain);
+
+		[StdCall, CLink]
+		static extern void FTFont_ClearCache();
+
+		[StdCall, CLink]
+		static extern FTGlyph* FTFont_AllocGlyph(FTFont* ftFont, int32 char8Code, bool allowDefault);
+
+		[StdCall, CLink]
+		static extern int32 FTFont_GetKerning(FTFont* font, int32 char8CodeA, int32 char8CodeB);
+
+		static Dictionary<String, String> sFontNameMap ~ DeleteDictionyAndKeysAndItems!(_);
+		static Monitor sMonitor = new .() ~ delete _;
+
+		struct FTFont
+		{
+			public int32 mHeight;
+			public int32 mAscent;
+			public int32 mDescent;
+			public int32 mMaxAdvance;
+		}
+
+		struct FTGlyph
+		{
+			public void* mPage;
+			public void* mTextureSegment;
+			public int32 mX;
+			public int32 mY;
+			public int32 mWidth;
+			public int32 mHeight;
+			public int32 mXOffset;
+			public int32 mYOffset;
+			public int32 mXAdvance;
+		}
+
+        public class CharData
+        {
+            public Image mImageSegment ~ delete _;
+            public int32 mX;
+            public int32 mY;
+            public int32 mWidth;
+            public int32 mHeight;
+            public int32 mXOffset;
+            public int32 mYOffset;
+            public int32 mXAdvance;
+			public bool mIsCombiningMark;
+        }
+
+        public class Page
+        {
+            public Image mImage ~ delete _;
+            public bool mIsCorrectedImage = true;
+        }
+
+		public struct AltFont
+		{
+			public Font mFont;
+			public bool mOwned;
+		}
+
+		class MarkRefData
+		{
+			public float mTop;
+			public float mBottom;
+		}
+
+		enum MarkPosition
+		{
+			AboveC, // Center
+			AboveR, // Left edge of mark aligned on center of char8
+			AboveE, // Center of mark aligned on right edge of char8
+			BelowC,
+			BelowR,
+			OverC,
+			OverE,
+			TopR, // Center of edge aligned to top of char8
+		}
+
+        const int32 LOW_CHAR_COUNT = 128;
+
+        Dictionary<char32, CharData> mCharData;
+        CharData[] mLowCharData;
+		FTFont* mFTFont;
+		String mPath;
+		List<AltFont> mAlternates;
+		MarkRefData mMarkRefData ~ delete _;
+		float[] mLoKerningTable;
+
+        public this()
+        {
+        }
+
+		public ~this()
+		{
+			Dispose();
+		}
+
+		public static ~this()
+		{
+			FTFont_ClearCache();
+		}
+
+		static void BuildFontNameCache()
+		{
+#if BF_PLATFORM_WINDOWS
+			using (sMonitor.Enter())
+			{
+				sFontNameMap = new .();
+
+				Windows.HKey hkey;
+				if (Windows.RegOpenKeyExA(Windows.HKEY_LOCAL_MACHINE, @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts", 0,
+					Windows.KEY_QUERY_VALUE | Windows.KEY_WOW64_32KEY | Windows.KEY_ENUMERATE_SUB_KEYS, out hkey) == Windows.S_OK)
+				{
+					defer Windows.RegCloseKey(hkey);
+
+					for (int32 i = 0; true; i++)
+					{
+						char16[256] fontNameArr;
+						uint32 nameLen = 255;
+						uint32 valType = 0;
+
+						char16[256] data;
+						uint32 dataLen = 256 * 2;
+						int32 result = Windows.RegEnumValueW(hkey, i, &fontNameArr, &nameLen, null, &valType, &data, &dataLen);
+						if (result == 0)
+						{
+							if (valType == 1)
+							{
+								String fontName = new String(&fontNameArr);
+								int parenPos = fontName.IndexOf(" (");
+								if (parenPos != -1)
+									fontName.RemoveToEnd(parenPos);
+								fontName.ToUpper();
+								String fontPath = new String(&data);
+								if ((!fontPath.EndsWith(".TTF", .OrdinalIgnoreCase)) || (!sFontNameMap.TryAdd(fontName, fontPath)))
+								{
+									delete fontName;
+									delete fontPath;
+								}
+							}
+						}
+						else
+						{
+							if (result == Windows.ERROR_MORE_DATA)
+								continue;
+
+							break;
+						}
+					}
+				}
+			}
+#endif
+		}
+
+		public static void ClearFontNameCache()
+		{
+			using (sMonitor.Enter())
+			{
+				DeleteDictionyAndKeysAndItems!(sFontNameMap);
+				sFontNameMap = null;
+			}
+		}
+
+		public void Dispose(bool cacheRetain)
+		{
+			if (mFTFont != null)
+			{
+				FTFont_Delete(mFTFont, cacheRetain);
+				mFTFont = null;
+			}
+
+			if (mLowCharData != null)
+			{
+				for (var charData in mLowCharData)
+					delete charData;
+				DeleteAndNullify!(mLowCharData);
+			}
+
+			if (mCharData != null)
+			{
+				for (var charData in mCharData.Values)
+					delete charData;
+				DeleteAndNullify!(mCharData);
+			}
+
+			if (mAlternates != null)
+			{
+				for (var altFont in mAlternates)
+					if (altFont.mOwned)
+						delete altFont.mFont;
+				DeleteAndNullify!(mAlternates);
+			}
+
+			DeleteAndNullify!(mPath);
+			DeleteAndNullify!(mLoKerningTable);
+		}
+
+		public void Dispose()
+		{
+			Dispose(false);
+		}
+
+		public static void ClearCache()
+		{
+			FTFont_ClearCache();
+		}
+
+		public void AddAlternate(Font altFont)
+		{
+			AltFont altFontEntry;
+			altFontEntry.mFont = altFont;
+			altFontEntry.mOwned = false;
+			mAlternates.Add(altFontEntry);
+		}
+
+		public Result<void> AddAlternate(String path, float pointSize = -1)
+		{
+			Font altFont = Try!(LoadFromFile(path, pointSize));
+			AltFont altFontEntry;
+			altFontEntry.mFont = altFont;
+			altFontEntry.mOwned = true;
+			mAlternates.Add(altFontEntry);
+			return .Ok;
+		}
+		
+        public bool GetVal(String outVal, List<String> cmds, String find)
+        {            
+            for (String cmd in cmds)
+            {
+                if (cmd.StartsWith(find) && (cmd[find.Length] == '='))
+				{
+                    outVal.Append(cmd, find.Length + 1);
+					return true;
+				}
+            }
+
+            return false;
+        }
+
+        public int32 GetValInt(List<String> cmds, String find)
+        {
+			String strVal = scope String();
+			if (!GetVal(strVal, cmds, find))
+				return 0;
+            return int32.Parse(strVal);
+        }
+
+		public void CalcKerning()
+		{
+			for (char8 c0 = ' '; c0 < '\x80'; c0++)
+			{
+				for (char8 c1 = ' '; c1 < '\x80'; c1++)
+				{
+					float kernVal = FTFont_GetKerning(mFTFont, (int32)c0, (int32)c1);
+					if (kernVal != 0)
+					{
+						if (mLoKerningTable == null)
+							mLoKerningTable = new float[128*128];
+						mLoKerningTable[(int32)c0 + ((int32)c1)*128] = kernVal;
+					}
+				}
+			}
+		}
+
+		public float GetKerning(char32 c0, char32 c1)
+		{
+			if (mLoKerningTable == null)
+				return 0;
+
+			if ((c0 < '\x80') && (c1 < '\x80'))
+			{
+				return mLoKerningTable[(int32)c0 + ((int32)c1)*128];
+			}
+
+			return FTFont_GetKerning(mFTFont, (int32)c0, (int32)c1);
+		}
+
+		void GetFontPath(StringView fontName, String path)
+		{
+			if (fontName.Contains('.'))
+			{
+				Path.GetAbsolutePath(fontName, BFApp.sApp.mInstallDir, path);
+			}
+			else
+			{
+				using (sMonitor.Enter())
+				{
+					if (sFontNameMap == null)
+						BuildFontNameCache();
+#if BF_PLATFORM_WINDOWS
+					let lookupStr = scope String(fontName)..ToUpper();
+					String pathStr;
+					if (sFontNameMap.TryGetValue(lookupStr, out pathStr))
+					{
+						char8[256] windowsDir;
+						Windows.GetWindowsDirectoryA(&windowsDir, 256);
+						path.Append(&windowsDir);
+						path.Append(@"\Fonts\");
+						path.Append(pathStr);
+					}
+#endif
+				}
+			}
+		}
+
+        internal bool Load(StringView fontName, float pointSize = -1)
+        {
+			Dispose();
+
+			mCharData = new Dictionary<char32, CharData>();
+			mLowCharData = new CharData[LOW_CHAR_COUNT];
+			mAlternates = new List<AltFont>();
+
+			float usePointSize = pointSize;
+			mPath = new String();
+			GetFontPath(fontName, mPath);
+
+			String fontPath = scope String(mPath);
+			if (pointSize == -1)
+			{
+				fontPath.Set(BFApp.sApp.mInstallDir);
+				fontPath.Append("fonts/SourceCodePro-Regular.ttf");
+				usePointSize = 9;
+			}
+			mFTFont = FTFont_Load(fontPath, usePointSize);
+			if (mFTFont == null)
+				return false;
+			
+			CalcKerning();
+            return true;
+        }
+
+        public static Result<Font> LoadFromFile(String path, float pointSize = -1)
+        {
+			scope AutoBeefPerf("Font.LoadFromFile");
+
+            Font font = new Font();
+            if (!font.Load(path, pointSize))
+                return .Err; //TODO: Make proper error
+            return font;
+        }
+
+        public bool HasChar(char32 theChar)
+        {
+			return true;
+            /*if ((theChar >= (char8)0) && (theChar < (char8)LOW_CHAR_COUNT))
+                return mLowCharData[(int)theChar] != null;
+            return mCharData.ContainsKey(theChar);*/
+        }
+
+        public float GetWidth(StringView theString)
+        {
+            float curX = 0;
+
+			int len = theString.Length;
+			if (len == 0)
+			{
+				return 0;
+			}
+
+            char32 prevChar = (char8)0;
+			for (var c in theString.DecodedChars)
+			{
+				int idx = @c.NextIndex;
+				if (c == (char32)'\x01')
+				{
+					@c.NextIndex = idx + 4;
+					continue;
+				}
+				else if (c == (char32)'\x02')
+				{
+					continue;
+				}
+                CharData charData = GetCharData((char32)c);
+				if ((charData != null) && (!charData.mIsCombiningMark))
+				{
+	                curX += charData.mXAdvance;
+	                if (prevChar != (char8)0)
+	                {
+	                    float kernAmount = GetKerning(prevChar, c);
+	                    curX += kernAmount;
+	                }
+				}
+
+                prevChar = c;
+
+				if (idx >= len)
+					break;
+            }
+
+            return curX;
+        }
+
+		static MarkPosition[] sMarkPositionsLow = new MarkPosition[0x70]
+		//       0        1        2        3            4        5        6        7             8        9        A        B             C        D        E        F
+       /*0*/{.AboveC, .AboveC, .AboveC, .AboveC, /**/ .AboveC, .AboveC, .AboveC, .AboveC, /**/ .AboveC, .AboveC, .AboveC, .AboveC, /**/ .AboveC, .AboveC, .AboveC, .AboveC,
+	   /*1*/ .AboveC, .AboveC, .AboveC, .AboveC, /**/ .AboveC, .AboveE, .AboveC, .AboveC, /**/ .BelowC, .BelowC, .AboveE, .TopR,   /**/ .BelowC, .BelowC, .BelowC, .BelowC,
+	   /*2*/ .BelowC, .BelowC, .BelowC, .BelowC, /**/ .BelowC, .BelowC, .BelowC, .BelowC, /**/ .BelowC, .BelowC, .BelowC, .BelowC, /**/ .BelowC, .BelowC, .BelowC, .BelowC,
+       /*3*/ .BelowC, .BelowC, .BelowC, .BelowC, /**/ .OverC , .OverC , .OverC , .OverC , /**/ .OverC , .BelowC, .BelowC, .BelowC, /**/ .AboveC, .AboveC, .AboveC, .AboveC,
+       /*4*/ .AboveC, .AboveC, .AboveC, .AboveC, /**/ .AboveC, .BelowC, .AboveC, .BelowC, /**/ .BelowC, .BelowC, .AboveC, .AboveC, /**/ .AboveC, .BelowC, .BelowC, .OverC,
+       /*5*/ .AboveC, .AboveC, .AboveC, .BelowC, /**/ .BelowC, .BelowC, .BelowC, .AboveC, /**/ .AboveE, .BelowC, .AboveC, .AboveC, /**/ .BelowR, .AboveR, .AboveR, .BelowR,
+       /*6*/ .AboveR, .AboveR, .BelowR, .AboveC, /**/ .AboveC, .AboveC, .AboveC, .AboveC, /**/ .AboveC, .AboveC, .AboveC, .AboveC, /**/ .AboveC, .AboveC, .AboveC, .AboveC,
+			} ~ delete _;
+
+		MarkPosition GetMarkPosition(char32 checkChar)
+		{
+			if ((checkChar >= '\u{0300}') && (checkChar <= '\u{036F}'))
+			{
+				return sMarkPositionsLow[(int)(checkChar - '\u{0300}')];
+			}
+			return .OverC;
+		}
+
+        CharData GetCharData(char32 checkChar)
+        {
+            CharData charData;
+            if ((checkChar >= (char8)0) && (checkChar < (char32)LOW_CHAR_COUNT))
+                charData = mLowCharData[(int)checkChar];
+            else
+                mCharData.TryGetValue(checkChar, out charData);
+            if (charData == null)
+			{
+				for (int fontIdx = -1; fontIdx < mAlternates.Count; fontIdx++)
+				{
+					FTFont* ftFont;
+					if (fontIdx == -1)
+						ftFont = mFTFont;
+					else
+						ftFont = mAlternates[fontIdx].mFont.mFTFont;
+					if (ftFont == null)
+						continue;
+
+					var ftGlyph = FTFont_AllocGlyph(ftFont, (int32)checkChar, fontIdx == mAlternates.Count - 1);
+					if (ftGlyph == null)
+						continue;
+
+					charData = new CharData();
+					charData.mX = ftGlyph.mX;
+					charData.mY = ftGlyph.mY;
+					charData.mWidth = ftGlyph.mWidth;
+					charData.mHeight = ftGlyph.mHeight;
+					charData.mXAdvance = ftGlyph.mXAdvance;
+					charData.mXOffset = ftGlyph.mXOffset;
+					charData.mYOffset = ftGlyph.mYOffset;
+					charData.mImageSegment = new Image();
+					charData.mImageSegment.mNativeTextureSegment = ftGlyph.mTextureSegment;
+					charData.mImageSegment.mX = ftGlyph.mX;
+					charData.mImageSegment.mY = ftGlyph.mY;
+					charData.mImageSegment.mWidth = ftGlyph.mWidth;
+					charData.mImageSegment.mHeight = ftGlyph.mHeight;
+					charData.mImageSegment.mSrcWidth = ftGlyph.mWidth;
+					charData.mImageSegment.mSrcHeight = ftGlyph.mHeight;
+					charData.mIsCombiningMark = ((checkChar >= '\u{0300}') && (checkChar <= '\u{036F}')) || ((checkChar >= '\u{1DC0}') && (checkChar <= '\u{1DFF}'));
+					if (charData.mIsCombiningMark)
+						charData.mXAdvance = 0;
+					if ((checkChar >= (char32)0) && (checkChar < (char32)LOW_CHAR_COUNT))
+						mLowCharData[(int)checkChar] = charData;
+					else
+						mCharData[checkChar] = charData;
+	                return charData;
+				}
+				
+				if (checkChar == (char32)'?')
+					return null;
+				return GetCharData((char32)'?');
+			}
+            return charData;
+        }
+
+		MarkRefData GetMarkRefData()
+		{
+			if (mMarkRefData == null)
+			{
+				mMarkRefData = new MarkRefData();
+				var charData = GetCharData('o');
+				mMarkRefData.mTop = charData.mYOffset;
+				mMarkRefData.mBottom = charData.mYOffset + charData.mHeight;
+			}
+			return mMarkRefData;
+		}
+
+        public float GetWidth(char32 theChar)
+        {
+            CharData charData = GetCharData(theChar);
+            return charData.mXAdvance;
+        }
+
+        public int GetCharCountToLength(StringView theString, float maxLength, bool* hitTabStop = null)
+        {
+            float curX = 0;
+
+			int startIdx = 0;
+            char32 prevChar = (char8)0;
+            for (var c in theString.DecodedChars)
+            {
+				int idx = @c.NextIndex;
+				if (c == (char32)'\x01')
+				{
+					@c.NextIndex = idx + 4;
+					//startIdx = idx;
+					continue;
+				}
+				else if (c == (char32)'\x02')
+				{
+					//startIdx = idx;
+					continue;
+				}
+				else if (c == (char32)'\t')
+				{
+					if (hitTabStop != null)
+					{
+						*hitTabStop = true;
+						return startIdx;
+					}
+				}
+
+                CharData charData = GetCharData(c);
+                curX += charData.mXAdvance;
+                if (prevChar != (char32)0)
+                {
+                    float kernAmount = GetKerning(prevChar, c);
+					curX += kernAmount;
+                }
+
+                if (curX > maxLength)
+                    return startIdx;
+
+                prevChar = c;
+				startIdx = idx;
+            }
+
+            return (int32)theString.Length;
+        }
+
+        public float GetLineSpacing()
+        {
+			if (mFTFont == null)
+				return 0;
+            return mFTFont.mHeight;
+        }
+
+		public float GetHeight()
+		{
+			if (mFTFont == null)
+				return 0;
+		    return mFTFont.mHeight;
+		}
+
+		public float GetAscent()
+		{
+			if (mFTFont == null)
+				return 0;
+		    return mFTFont.mAscent;
+		}
+
+		public float GetDescent()
+		{
+			if (mFTFont == null)
+				return 0;
+		    return mFTFont.mDescent;
+		}
+
+        public float GetWrapHeight(StringView theString, float width)
+        {
+            return Draw(null, theString, 0, 0, -1, width, FontOverflowMode.Wrap);
+        }
+
+		public static void StrEncodeColor(uint32 color, String outString)
+		{
+			uint32 colorVal = (color >> 1) & 0x7F7F7F7F;
+			outString.Append('\x01');
+			outString.Append((char8*)&colorVal, 4);
+		}
+
+		public static void StrEncodePopColor(String outString)
+		{
+			outString.Append('\x02');
+		}
+
+		public static char8[5] EncodeColor(uint32 color)
+		{
+			char8[5] val;
+			val[0] = '\x01';
+			*((uint32*)&val[1]) = (color >> 1) & 0x7F7F7F7F;
+			return val;
+		}
+
+		public static char8 EncodePopColor()
+		{
+			return '\x02';
+		}
+
+        public void Draw(Graphics g, StringView theString, FontMetrics* fontMetrics = null)
+        {
+			if (mFTFont == null)
+				return;
+
+            float curX = 0;
+            float curY = 0;
+
+            Matrix newMatrix = Matrix();
+			bool hasClipRect = g.mClipRect.HasValue;
+            Rect clipRect = g.mClipRect.GetValueOrDefault();
+
+			uint32 color = g.mColor;
+
+			g.PushTextRenderState();
+
+#unwarn
+			float markScale = mFTFont.mHeight / 8.0f;
+			float markTopOfs = 0;
+			float markBotOfs = 0;
+
+			CharData lastCharData = null;
+			float lastCharX = 0;
+            char32 prevChar = (char32)0;			
+            
+			for (var c in theString.DecodedChars)
+            {
+				int idx = @c.NextIndex;
+
+				if (c == (char32)'\x01') // Set new color
+				{
+					if (idx <= theString.Length - 4)
+					{
+						uint32 newColor = *(uint32*)(theString.Ptr + idx) << 1;
+						color = Color.Mult(newColor, g.mColor);
+						@c.NextIndex = idx + 4;
+						continue;
+					}
+				}
+				else if (c == (char32)'\x02') // Restore color
+				{
+					color = g.mColor;
+					continue;
+				}
+
+                CharData charData = GetCharData(c);
+				float drawX = curX + charData.mXOffset;
+				float drawY = curY + charData.mYOffset;
+				
+				if ((charData.mIsCombiningMark) && (lastCharData != null))
+				{
+					var markRefData = GetMarkRefData();
+					var markPos = GetMarkPosition(c);
+
+					if (markPos == .TopR)
+					{
+						drawX = lastCharX + lastCharData.mXOffset + lastCharData.mWidth - charData.mWidth / 2;
+						drawY = curY + lastCharData.mYOffset - charData.mHeight / 2;
+					}
+					else if ((markPos == .AboveE) || (markPos == .OverE))
+					{
+						drawX = lastCharX + lastCharData.mXOffset + lastCharData.mWidth - charData.mWidth / 2;
+					}
+					else
+					{
+						drawX = lastCharX + lastCharData.mXOffset + (lastCharData.mWidth / 2);
+						if ((markPos == .AboveC) || (markPos == .BelowC) || (markPos == .OverC))
+							drawX -= charData.mWidth / 2;
+					}
+
+					if ((markPos == .AboveC) || (markPos == .AboveR))
+					{
+                        drawY += lastCharData.mYOffset - markRefData.mTop - markTopOfs;
+						markTopOfs += charData.mHeight;
+					}
+					else if ((markPos == .BelowC) || (markPos == .BelowR))
+					{
+						drawY += (lastCharData.mYOffset + lastCharData.mHeight) - markRefData.mBottom + markBotOfs;
+						markBotOfs += charData.mHeight;
+					}
+				}
+				else
+				{
+					lastCharX = curX;
+					markTopOfs = 0;
+					markBotOfs = 0;
+
+					if (prevChar != (char8)0)
+					{
+					    float kernAmount = GetKerning(prevChar, c);
+						curX += kernAmount;
+						drawX += kernAmount;
+					}
+					lastCharData = charData;
+				}
+
+                newMatrix.SetMultiplied(drawX, drawY, ref g.mMatrix);
+				bool isFullyClipped = false;
+				if (hasClipRect)
+				{
+					if (newMatrix.tx < clipRect.mX -charData.mImageSegment.mWidth)
+					{
+						isFullyClipped = true;
+					}
+					else if (newMatrix.tx > clipRect.mX + clipRect.mWidth)
+					{
+						isFullyClipped = true;
+						if ((newMatrix.a > 0) && (fontMetrics == null)) // Forward? If so, all future char8s will clip
+							break;
+					}
+				}
+
+				if (!isFullyClipped)
+                    charData.mImageSegment.Draw(newMatrix, g.ZDepth, color);
+
+                curX += charData.mXAdvance;
+
+                prevChar = c;
+            }
+
+			g.PopRenderState();
+        }
+
+        public float Draw(Graphics g, StringView theString, float x, float y, int32 justification = -1, float width = 0, FontOverflowMode stringEndMode = FontOverflowMode.Overflow, FontMetrics* fontMetrics = null)
+        {
+            float drawHeight = 0;
+			float useX = x;
+			float useY = y;
+
+			StringView workingStr = theString;
+			String tempStr = null;
+
+			void PopulateTempStr()
+			{
+				if (tempStr.Length != workingStr.Length)
+				{
+					tempStr.Clear();
+					tempStr.Append(workingStr);
+				}	
+			}
+
+            while (true)
+            {
+                int32 crPos = (int32)workingStr.IndexOf('\n');
+                if (crPos == -1)
+                    break;
+				
+                float sectionHeight = Draw(g, StringView(workingStr, 0, crPos), useX, useY, justification, width, stringEndMode, fontMetrics);
+                drawHeight += sectionHeight;
+                useY += sectionHeight;
+				workingStr = .(workingStr, crPos + 1, workingStr.Length - (crPos + 1));
+            }
+
+            if ((justification != -1) || (stringEndMode != FontOverflowMode.Overflow))
+            {
+                float aWidth;
+                while (true)
+                {
+                    aWidth = GetWidth(workingStr);
+                    if (aWidth <= width)
+                        break;
+
+                    if (stringEndMode == FontOverflowMode.Ellipsis)
+                    {
+                        float ellipsisLen = GetWidth("...");
+                        if (width < ellipsisLen)
+                            return 0; // Don't draw at all if we don't even have enough space for the ellipsis
+                        
+						int strLen = GetCharCountToLength(workingStr, width - ellipsisLen);
+						tempStr = scope:: String(Math.Min(strLen, 128));
+						tempStr.Append(theString, 0, strLen);
+						tempStr.Append("...");
+						workingStr = tempStr;
+                        aWidth = GetWidth(workingStr);
+                        break;
+                    }
+                    else if (stringEndMode == FontOverflowMode.Truncate)
+                    {
+						int strLen = GetCharCountToLength(workingStr, width);
+						tempStr = scope:: String(Math.Min(strLen, 128));
+						tempStr.Append(theString, 0, strLen);
+						workingStr = tempStr;
+						aWidth = GetWidth(workingStr);
+                        break;
+                    }
+                    else if (stringEndMode == FontOverflowMode.Wrap)
+                    {
+                        int32 maxChars = (int32)GetCharCountToLength(workingStr, width);
+
+                        int32 checkIdx = maxChars;
+						if (checkIdx < workingStr.Length)
+						{
+	                        while ((checkIdx > 0) && (!workingStr[checkIdx].IsWhiteSpace))
+	                            checkIdx--;
+						}
+
+                        if (checkIdx == 0)
+                        {
+                            // Can't break on whitespace
+                            checkIdx = maxChars;
+                        }
+
+                        if (checkIdx > 0)
+                        {
+                            if (fontMetrics != null)
+                                fontMetrics.mLineCount++;
+
+                            drawHeight += Draw(g, StringView(workingStr, 0, checkIdx), useX, useY, justification, width, stringEndMode, fontMetrics);
+                            useY += GetLineSpacing();                            
+                            workingStr.Adjust(checkIdx);
+                        }
+                    }
+                    else
+                        break;
+                }
+
+                if (fontMetrics != null)
+                    fontMetrics.mMaxWidth = Math.Max(fontMetrics.mMaxWidth, aWidth);
+
+                if (justification == 0)
+                    useX += (width - aWidth) / 2;
+                else if (justification == 1)
+                    useX += width - aWidth;
+            }
+
+            if (g != null)
+            {
+                using (g.PushTranslate(useX, useY))
+                    Draw(g, workingStr, fontMetrics);
+            }
+            else
+            {
+                if (fontMetrics != null)
+                    fontMetrics.mMaxWidth = Math.Max(fontMetrics.mMaxWidth, GetWidth(workingStr));
+            }
+            drawHeight += GetLineSpacing();
+
+            if (fontMetrics != null)
+                fontMetrics.mLineCount++;
+
+            return drawHeight;
+        }
+    }
+}

+ 1223 - 0
BeefLibs/Beefy2D/src/gfx/Graphics.bf

@@ -0,0 +1,1223 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Runtime.InteropServices;
+using Beefy.utils;
+using Beefy.geom;
+using System.Diagnostics;
+
+namespace Beefy.gfx
+{    
+    public abstract class GraphicsBase
+    {
+        struct StateStack<T>
+        {
+            public T[] mStack;
+            public int32 mIdx;
+            DisposeProxy mDisposeProxy;
+
+            public this(int32 size, T defaultValue, DisposeProxyDelegate disposeProxy)
+            {
+                mStack = new T[size];
+                mStack[0] = defaultValue;
+                mIdx = 0;
+                mDisposeProxy = new DisposeProxy();
+                mDisposeProxy.mDisposeProxyDelegate = disposeProxy;
+            }
+
+			public void Dispose()
+			{
+				delete mStack;
+				delete mDisposeProxy;
+			}
+
+            public IDisposable Push(T newValue) mut
+            {
+                mStack[++mIdx] = newValue;
+                return mDisposeProxy;
+            }
+
+            public T Pop() mut
+            {
+                return mStack[--mIdx];
+            }
+        }
+
+        public Shader mDefaultShader ~ delete _;
+		public Shader mTextShader ~ delete _;
+        public RenderState mDefaultRenderState ~ delete _;
+        public Font mFont;
+        public Image mWhiteDot ~ delete _;
+        public float ZDepth { get; set; }
+        
+        protected DisposeProxy mMatrixDisposeProxy ~ delete _;
+        const int32 MATIX_STACK_SIZE = 256;
+        public Matrix[] mMatrixStack = new Matrix[MATIX_STACK_SIZE] ~ delete _;
+        public int32 mMatrixStackIdx = 0;
+        public Matrix mMatrix;
+
+        protected DisposeProxy mDrawLayerDisposeProxy ~ delete _;
+        const int32 DRAW_LAYER_SIZE = 256;
+        public DrawLayer[] mDrawLayerStack = new DrawLayer[DRAW_LAYER_SIZE] ~ delete _;
+        public int32 mDrawLayerStackIdx = 0;
+        public DrawLayer mDrawLayer;
+
+        protected DisposeProxy mColorDisposeProxy ~ delete _;
+        const int32 COLOR_STACK_SIZE = 256;
+        public Color[] mColorStack = new Color[COLOR_STACK_SIZE] ~ delete _;
+        public int32 mColorStackIdx = 0;
+        public Color mColor = Color.White;
+
+        /*protected DisposeProxy mZDepthDisposeProxy;
+        const int COLOR_STACK_SIZE = 256;
+        public uint[] mColorStack = new uint[COLOR_STACK_SIZE];
+        public int mColorStackIdx = 0;
+        public uint mColor = Color.White;*/
+
+        StateStack<float> mZDepthStack ~ _.Dispose();
+
+        protected DisposeProxy mClipDisposeProxy ~ delete _;
+        const int32 CLIP_STACK_SIZE = 256;
+        public Rect?[] mClipStack = new Rect?[CLIP_STACK_SIZE] ~ delete _;
+		
+        public int32 mClipStackIdx = 0;
+        public Rect? mClipRect = null;
+
+        protected DisposeProxy mRenderStateDisposeProxy ~ delete _;
+        const int32 RENDERSTATE_STACK_SIZE = 256;
+        public RenderState[] mRenderStateStack = new RenderState[RENDERSTATE_STACK_SIZE] ~ delete _;
+        public int32 mRenderStateStackIdx = 0;
+        
+        public List<RenderState> mRenderStatePool = new List<RenderState>() ~ DeleteContainerAndItems!(_);
+        public int32 mClipPoolIdx = 0;
+        public List<int32> mClipPoolStarts = new List<int32>() ~ delete _;
+
+        public int32 mDrawNestingDepth;
+
+        internal this()
+        {
+            mZDepthStack = StateStack<float>(256, 0.0f, new => PopZDepth);
+
+            mMatrixDisposeProxy = new DisposeProxy();
+            mMatrixDisposeProxy.mDisposeProxyDelegate = new => PopMatrix;
+            mDrawLayerDisposeProxy = new DisposeProxy();
+            mDrawLayerDisposeProxy.mDisposeProxyDelegate = new => PopDrawLayer;
+            mColorDisposeProxy = new DisposeProxy();
+            mColorDisposeProxy.mDisposeProxyDelegate = new => PopColor;
+            mClipDisposeProxy = new DisposeProxy();
+            mClipDisposeProxy.mDisposeProxyDelegate = new => PopClip;
+            mRenderStateDisposeProxy = new DisposeProxy();
+
+			String filePath = scope String();
+			filePath.Append(BFApp.sApp.mInstallDir, "images/whiteDot.tga");
+            mWhiteDot = Image.LoadFromFile(filePath);
+
+            for (int32 i = 0; i < MATIX_STACK_SIZE; i++)
+                mMatrixStack[i] = Matrix.IdentityMatrix;
+            mMatrix = mMatrixStack[0];
+            
+            mColorStack[0] = Color.White;            
+        }
+
+        public ~this()
+        {
+            
+        }
+
+        public void StartDraw()
+        {
+            if (mDrawNestingDepth == 0)
+            {
+                if (mDefaultRenderState != null)                
+                    mRenderStateStack[0] = mDefaultRenderState;
+                
+                Debug.Assert(mMatrixStackIdx == 0);
+                Debug.Assert(mDrawLayerStackIdx == 0);
+                Debug.Assert(mRenderStateStackIdx == 0);
+                Debug.Assert(mColorStackIdx == 0);
+                Debug.Assert(mClipStackIdx == 0);
+            }
+            mDrawNestingDepth++;
+            mClipPoolStarts.Add(mClipPoolIdx);
+
+            PushMatrixOverride(Matrix.IdentityMatrix);
+            PushRenderState(mDefaultRenderState);
+            PushClipDisable();
+            PushColorOverride(Color.White);
+            PushZDepth(0.0f);
+        }
+
+        public void EndDraw()
+        {            
+            mDrawNestingDepth--;
+            
+            PopMatrix();
+            mRenderStateStack[0] = mDefaultRenderState; // So the 'pop' gets the correct render state
+            PopRenderState();
+            PopClip();
+            PopColor();
+            PopZDepth();
+
+            if (mDrawNestingDepth == 0)
+            {
+                Debug.Assert(mZDepthStack.mIdx == 0);
+                Debug.Assert(mMatrixStackIdx == 0);
+                Debug.Assert(mDrawLayerStackIdx == 0);
+                Debug.Assert(mRenderStateStackIdx == 0);
+                Debug.Assert(mColorStackIdx == 0);
+                Debug.Assert(mClipStackIdx == 0);
+            }
+
+            mClipPoolIdx = mClipPoolStarts[mClipPoolStarts.Count - 1];
+            mClipPoolStarts.RemoveAt(mClipPoolStarts.Count - 1);
+        }
+
+        public IDisposable PushTranslate(float x, float y)
+        {
+            mMatrixStackIdx++;
+            mMatrixStack[mMatrixStackIdx].SetMultiplied(x, y, ref mMatrix);
+            mMatrix = mMatrixStack[mMatrixStackIdx];
+
+            return mMatrixDisposeProxy;
+        }
+
+        public IDisposable PushScale(float scaleX, float scaleY)
+        {
+            Matrix m = Matrix.IdentityMatrix;
+            m.Identity();
+            m.Scale(scaleX, scaleY);
+            return PushMatrix(m);
+        }
+
+        public IDisposable PushScale(float scaleX, float scaleY, float x, float y)
+        {
+            Matrix m = Matrix.IdentityMatrix;
+            m.Identity();
+            m.Translate(-x, -y);
+            m.Scale(scaleX, scaleY);
+            m.Translate(x, y);
+            return PushMatrix(m);
+        }
+
+        public IDisposable PushRotate(float rot)
+        {
+            Matrix m = Matrix.IdentityMatrix;
+            m.Identity();
+            m.Rotate(rot);    
+            return PushMatrix(m);
+        }
+
+		public IDisposable PushRotate(float rot, float x, float y)
+		{
+			Matrix m = Matrix.IdentityMatrix;
+			m.Identity();
+			m.Translate(-x, -y);
+			m.Rotate(rot);
+			m.Translate(x, y);
+			return PushMatrix(m);
+		}
+
+        public IDisposable PushMatrix(Matrix matrix)
+        {
+            mMatrixStackIdx++;
+            mMatrixStack[mMatrixStackIdx].SetMultiplied(matrix, mMatrix);
+            mMatrix = mMatrixStack[mMatrixStackIdx];
+
+            return mMatrixDisposeProxy;
+        }
+
+        public IDisposable PushMatrixOverride(Matrix matrix)
+        {
+            mMatrixStackIdx++;
+            mMatrixStack[mMatrixStackIdx].Set(matrix);
+            mMatrix = mMatrixStack[mMatrixStackIdx];
+
+            return mMatrixDisposeProxy;
+        }
+
+        public void PopMatrix()
+        {
+            mMatrix = mMatrixStack[--mMatrixStackIdx];
+        }
+
+        public IDisposable PushDrawLayer(DrawLayer drawLayer)
+        {
+            mDrawLayerStackIdx++;
+            mDrawLayerStack[mDrawLayerStackIdx] = drawLayer;
+            mDrawLayer = drawLayer;
+            mDrawLayer.Activate();
+
+            return mDrawLayerDisposeProxy;
+        }
+
+        public void PopDrawLayer()
+        {
+            mDrawLayer = mDrawLayerStack[--mDrawLayerStackIdx];
+            if (mDrawLayer != null)
+                mDrawLayer.Activate();
+        }
+
+        public IDisposable PushZDepth(float zDepth)
+        {
+            ZDepth = zDepth;
+            return mZDepthStack.Push(ZDepth);
+        }
+
+        public void PopZDepth()
+        {
+            ZDepth = mZDepthStack.Pop();
+        }
+
+        public IDisposable PushColor(Color color)
+        {
+            Color physColor = color;
+
+            mColorStackIdx++;
+            mColor = mColorStack[mColorStackIdx] = Color.Mult(mColor, physColor);
+
+            return mColorDisposeProxy;
+        }
+
+        public IDisposable PushColorOverride(Color color)
+        {
+            Color physColor = color;
+
+            mColorStackIdx++;
+            mColor = mColorStack[mColorStackIdx] = physColor;
+
+            return mColorDisposeProxy;
+        }
+
+        public void SetColor(Color color)
+        {
+            Color physColor = (color & 0xFF00FF00) | ((color & 0x00FF0000) >> 16) | ((color & 0x000000FF) << 16);
+
+            if (mColorStackIdx > 0)
+                mColor = mColorStack[mColorStackIdx] = Color.Mult(mColorStack[mColorStackIdx - 1], physColor);
+            else
+                mColor = mColorStack[mColorStackIdx] = physColor;
+        }
+
+        public void PopColor()
+        {
+            mColor = mColorStack[--mColorStackIdx];
+        }
+
+        public void SetFont(Font font)
+        {
+            mFont = font;
+        }
+
+        public abstract IDisposable PushRenderState(RenderState renderState);
+        public abstract void PopRenderState();
+        
+        public IDisposable PushClip(float x, float y, float width, float height)
+        {            
+            Matrix m = Matrix();
+            m.SetMultiplied(x, y, width, height, ref mMatrix);
+
+            mClipStackIdx++;
+            if (!mClipRect.HasValue)
+                mClipStack[mClipStackIdx] = Rect(m.tx, m.ty, m.a, m.d);
+            else
+                mClipStack[mClipStackIdx] = mClipRect.Value.Intersection(Rect(m.tx, m.ty, m.a, m.d));
+
+            //mClipStack[mClipStackIdx] = new Rect(m.tx, m.ty, m.a, m.d);
+
+            mClipRect = mClipStack[mClipStackIdx];             
+
+            /*RenderState clipRenderState = null;
+            var curRenderState = mRenderStateStack[mRenderStateStackIdx];
+            if (curRenderState.mIsFromDefaultRenderState)
+            {
+                if (mRenderStatePool.Count <= mClipPoolIdx)
+                {
+                    clipRenderState = RenderState.Create(mDefaultRenderState);
+                    clipRenderState.mIsFromDefaultRenderState = true;
+                    mRenderStatePool.Add(clipRenderState);
+                }
+                clipRenderState = mRenderStatePool[mClipPoolIdx++];
+            }
+            else
+                clipRenderState = RenderState.Create(curRenderState);*/
+			
+            Rect rectThing = mClipRect.Value;
+            mClipRect = rectThing;
+
+			var clipRenderState = AllocRenderState(mDefaultShader, mClipRect);
+
+            //clipRenderState.ClipRect = mClipRect;
+            PushRenderState(clipRenderState);            
+
+            return mClipDisposeProxy;
+        }
+
+		RenderState AllocRenderState(Shader shader, Rect? clipRect)
+		{
+			RenderState renderState = null;
+			var curRenderState = mRenderStateStack[mRenderStateStackIdx];
+			if (curRenderState.mIsFromDefaultRenderState)
+			{
+			    if (mRenderStatePool.Count <= mClipPoolIdx)
+			    {                    
+			        renderState = RenderState.Create(mDefaultRenderState);
+			        renderState.mIsFromDefaultRenderState = true;
+			        mRenderStatePool.Add(renderState);
+			    }
+			    renderState = mRenderStatePool[mClipPoolIdx++];
+			}
+			else
+			    renderState = RenderState.Create(curRenderState);
+			renderState.Shader = shader;
+			renderState.ClipRect = clipRect;
+			return renderState;
+		}
+
+        public IDisposable PushClipDisable()
+        {
+			mClipStackIdx++;
+			mClipStack[mClipStackIdx] = null;
+			mClipRect = null;
+			var clipRenderState = AllocRenderState(mDefaultShader, null);
+            //clipRenderState.ClipRect = null;
+            PushRenderState(clipRenderState);
+
+            return mClipDisposeProxy;
+        }
+
+		public void PushTextRenderState()
+		{
+			var textRenderState = AllocRenderState(mTextShader, mClipRect);
+			//textRenderState.ClipRect = mClipRect;
+			//textRenderState.Shader = mTextShader;
+			PushRenderState(textRenderState);
+		}
+
+        public void PopClip()
+        {            
+            mClipRect = mClipStack[--mClipStackIdx];                        
+            PopRenderState();
+        }        
+    }
+
+#if !STUDIO_CLIENT
+    public class Graphics : GraphicsBase
+    {               
+        [StdCall, CLink]	
+        static extern void Gfx_SetRenderState(void* renderState);
+
+        [StdCall, CLink]
+        static extern void Gfx_AllocTris(void* textureSegment, int32 vtxCount);
+
+        [StdCall, CLink]
+        static extern void Gfx_SetDrawVertex(int32 idx, float x, float y, float z, float u, float v, uint32 color);
+
+        [StdCall, CLink]
+        static extern void Gfx_CopyDrawVertex(int32 destIdx, int32 srcIdx);
+
+        //[StdCall, CLink]
+        //static unsafe extern void Gfx_DrawIndexedVertices2D(void* vtxData, int vtxCount, int* idxData, int idxCount, float a, float b, float c, float d, float tx, float ty, float z);
+
+        [StdCall, CLink]
+        static extern void Gfx_DrawIndexedVertices2D(int32 vertexSize, void* vtxData, int32 vtxCount, uint16* idxData, int32 idxCount, float a, float b, float c, float d, float tx, float ty, float z);
+
+        [StdCall, CLink]
+        static extern void Gfx_SetShaderConstantData(int32 slotIdx, void* data, int32 size);
+
+        [StdCall, CLink]
+        static extern void Gfx_SetShaderConstantDataTyped(int32 slotIdx, void* data, int32 size, int32* typeData, int32 typeCount);
+
+        [StdCall, CLink]
+        static extern void Gfx_DrawQuads(void* textureSegment, Vertex3D* vertices, int32 vtxCount);
+
+        [StdCall, CLink]
+        extern static void* Gfx_QueueRenderCmd(void* nativeRenderCmd);
+
+        [StdCall, CLink]
+        extern static void Gfx_SetTexture_TextureSegment(int32 textureIdx, void* textureSegment);
+        
+        internal this()
+        {
+            mRenderStateDisposeProxy.mDisposeProxyDelegate = new => PopRenderState;
+        }
+
+        public void SetTexture(int32 textureIdx, Image image)
+        {
+            Debug.Assert(image.mSrcTexture == null);
+            Gfx_SetTexture_TextureSegment(textureIdx, image.mNativeTextureSegment);
+        }
+
+        /*public void StartDraw()
+        {
+            Debug.Assert(mRenderStateStackIdx == 0);
+            Debug.Assert(mColorStackIdx == 0);
+            Debug.Assert(mClipStackIdx == 0);
+
+            if ((mDefaultRenderState != null) && (mRenderStateStack[0] == null))
+            {
+                mRenderStateStack[0] = mDefaultRenderState;
+                Gfx_SetRenderState(mRenderStateStack[0].mNativeRenderState);
+            }            
+            mMatrix.Identity();
+            mClipPoolIdx = 0;            
+        }
+
+        public void EndDraw()
+        {
+            Debug.Assert(mMatrixStackIdx == 0);
+            Debug.Assert(mColorStackIdx == 0);
+        }*/
+
+        public void SetRenderState(RenderState renderState)
+        {
+            mRenderStateStack[mRenderStateStackIdx] = renderState;
+            Gfx_SetRenderState(renderState.mNativeRenderState);
+        }
+
+        public override IDisposable PushRenderState(RenderState renderState)
+        {
+            Gfx_SetRenderState(renderState.mNativeRenderState);
+            mRenderStateStack[++mRenderStateStackIdx] = renderState;
+            return mRenderStateDisposeProxy;
+        }
+
+        public override void PopRenderState()
+        {
+            Gfx_SetRenderState(mRenderStateStack[--mRenderStateStackIdx].mNativeRenderState);            
+        }
+
+        public void Draw(RenderCmd renderCmd)
+        {
+            Gfx_QueueRenderCmd(renderCmd.mNativeRenderCmd);
+        }
+        
+        public void Draw(IDrawable drawable, float x = 0, float y = 0)
+        {
+            Matrix newMatrix = Matrix();
+            newMatrix.SetMultiplied(x, y, ref mMatrix);
+            
+            //newMatrix.tx *= g.mScale;
+            //newMatrix.ty *= g.mScale;
+            
+            drawable.Draw(newMatrix, ZDepth, mColor);
+
+            /*SexyExport.MatrixDrawImageInst(cSGraphics.mGraphics, mImageInstInfos[cel], newMatrix.a, newMatrix.b, newMatrix.c, newMatrix.d, newMatrix.tx, newMatrix.ty,
+                (int)mPixelSnapping, mSmoothing ? 1 : 0, mAdditive ? 1 : 0, (int)g.mColor);*/
+        }
+
+        public void DrawButton(Image image, float x, float y, float width)
+        {
+            Matrix m = Matrix();
+            m.SetMultiplied(x, y, ref mMatrix);
+
+            Gfx_AllocTris(image.mNativeTextureSegment, 6 * 3);
+
+			float useWidth = width;
+            if (SnapMatrix(ref m, image.mPixelSnapping))
+            {
+                useWidth = (int32)useWidth;
+            }
+
+            float segmentW = Math.Min(image.mWidth / 2, useWidth / 2); 
+
+            float a;
+            float b;
+            float c = m.c * image.mHeight;
+            float d = m.d * image.mHeight;
+
+            float u1 = 0;
+            float u2 = segmentW / (float)image.mWidth;
+            int32 vtx = 0;
+
+            for (int32 col = 0; col < 3; col++)
+            {
+                if ((col == 0) || (col == 2))
+                {
+                    a = m.a * segmentW;
+                    b = m.b * segmentW;
+                    if (col == 2)
+                    {
+                        u1 = 1.0f - u1;
+                        u2 = 1.0f;
+                    }
+                }
+                else if (useWidth > image.mWidth)
+                {
+                    a = m.a * (useWidth - segmentW * 2);
+                    b = m.b * (useWidth - segmentW * 2);
+                    u1 = u2;
+                }
+                else
+                {
+                    a = 0;
+                    b = 0;
+                    u1 = u2;
+                }
+
+                Gfx_SetDrawVertex(vtx + 0, m.tx, m.ty, 0, u1, 0, mColor);
+                Gfx_SetDrawVertex(vtx + 1, m.tx + a, m.ty + b, 0, u2, 0, mColor);
+                Gfx_SetDrawVertex(vtx + 2, m.tx + c, m.ty + d, 0, u1, 1, mColor);
+                Gfx_CopyDrawVertex(vtx + 3, vtx + 2);
+                Gfx_CopyDrawVertex(vtx + 4, vtx + 1);
+                Gfx_SetDrawVertex(vtx + 5, m.tx + (a + c), m.ty + (b + d), 0, u2, 1, mColor);
+
+                m.tx += a;
+                m.ty += b;                
+                vtx += 6;
+            }
+        }
+
+        public bool SnapMatrix(ref Matrix m, PixelSnapping pixelSnapping)
+        {
+            bool wantSnap = pixelSnapping == PixelSnapping.Always;
+            if (pixelSnapping == PixelSnapping.Auto)
+            {
+                if ((m.b == 0) && (m.c == 0))
+                    wantSnap = true;
+            }
+            if (wantSnap)
+            {
+                m.tx = (int32)m.tx;
+                m.ty = (int32)m.ty;
+            }
+            return wantSnap;
+        }
+
+        public void DrawButtonVert(Image image, float x, float y, float height)
+        {
+            Matrix m = Matrix();
+            m.SetMultiplied(x, y, ref mMatrix);
+
+            Gfx_AllocTris(image.mNativeTextureSegment, 6 * 3);
+
+			float useHeight = height;
+            if (SnapMatrix(ref m, image.mPixelSnapping))
+                useHeight = (int32)useHeight;
+
+            float segmentH = Math.Min(image.mHeight / 2, useHeight / 2);
+
+            float a = m.a * image.mWidth;
+            float b = m.b * image.mWidth;
+            float c = 0;
+            float d = 0;
+
+            float v1 = 0;
+            float v2 = segmentH / (float)image.mHeight;
+            int32 vtx = 0;
+
+            /*for (int col = 0; col < 3; col++)
+            {
+                if ((col == 0) || (col == 2))
+                {
+                    a = m.a * segmentW;
+                    b = m.b * segmentW;
+                    if (col == 2)
+                    {
+                        u1 = 1.0f - u1;
+                        u2 = 1.0f;
+                    }
+                }
+                else
+                {
+                    a = m.a * (width - segmentW * 2);
+                    b = m.b * (width - segmentW * 2);
+                    u1 = u2;
+                }
+
+                Gfx_SetDrawVertex(vtx + 0, m.tx, m.ty, 0, u1, 0, mColor);
+                Gfx_SetDrawVertex(vtx + 1, m.tx + a, m.ty + b, 0, u2, 0, mColor);
+                Gfx_SetDrawVertex(vtx + 2, m.tx + c, m.ty + d, 0, u1, 1, mColor);
+                Gfx_CopyDrawVertex(vtx + 3, vtx + 2);
+                Gfx_CopyDrawVertex(vtx + 4, vtx + 1);
+                Gfx_SetDrawVertex(vtx + 5, m.tx + (a + c), m.ty + (b + d), 0, u2, 1, mColor);
+
+                m.tx += a;
+                m.ty += b;
+                vtx += 6;
+            }*/
+
+            for (int32 row = 0; row < 3; row++)
+            {
+                if ((row == 0) || (row == 2))
+                {
+                    c = m.c * segmentH;
+                    d = m.d * segmentH;
+                    if (row == 2)
+                    {
+                        v1 = 1.0f - v1;
+                        v2 = 1.0f;
+                    }
+                }
+                else if (useHeight > image.mHeight)
+                {
+                    c = m.c * (useHeight - image.mHeight);
+                    d = m.d * (useHeight - image.mHeight);
+                    v1 = v2;
+                }
+                else
+                {
+                    c = 0;
+                    d = 0;
+                    v1 = v2;
+                }
+
+                Gfx_SetDrawVertex(vtx + 0, m.tx, m.ty, 0, 0, v1, mColor);
+                Gfx_SetDrawVertex(vtx + 1, m.tx + a, m.ty + b, 0, 1, v1, mColor);
+                Gfx_SetDrawVertex(vtx + 2, m.tx + c, m.ty + d, 0, 0, v2, mColor);
+                Gfx_CopyDrawVertex(vtx + 3, vtx + 2);
+                Gfx_CopyDrawVertex(vtx + 4, vtx + 1);
+                Gfx_SetDrawVertex(vtx + 5, m.tx + (a + c), m.ty + (b + d), 0, 1, v2, mColor);
+
+                m.tx += c;
+                m.ty += d;
+                vtx += 6;
+            }
+        }
+
+        public void DrawBox(Image image, float x, float y, float width, float height)
+        {
+            Matrix m = Matrix.IdentityMatrix;
+            m.SetMultiplied(x, y, ref mMatrix);
+            
+			float useWidth = width;
+			float useHeight = height;
+            if (SnapMatrix(ref m, image.mPixelSnapping))
+            {
+                useWidth = (int32)useWidth;
+                useHeight = (int32)useHeight;
+            }
+
+			float heightDiff = useHeight - image.mHeight;
+			if (heightDiff >= 0)
+				Gfx_AllocTris(image.mNativeTextureSegment, 6 * 3 * 3);
+			else
+				Gfx_AllocTris(image.mNativeTextureSegment, 6 * 3 * 2);
+
+            float a;
+            float b;
+            float c;
+            float d;
+            
+            int32 vtx = 0;
+
+            //float prevX = m.tx;
+
+            float segmentW = Math.Min(image.mWidth / 2, useWidth / 2);
+
+            float v1 = 0;
+            float v2 = segmentW / (float) image.mWidth;
+
+            for (int32 row = 0; row < 3; row++)
+            {
+                if ((row == 0) || (row == 2))
+                {
+					float halfHeight = Math.Min(image.mHeight / 2, useHeight/2);					
+                    c = m.c * halfHeight;
+                    d = m.d * halfHeight;
+
+                    if (row == 2)
+                    {
+                        v1 = 1.0f - v1;
+                        v2 = 1.0f;
+                    }
+                }
+                else
+                {
+					v1 = v2;
+					if (heightDiff < 0)
+						continue;
+                    c = m.c * heightDiff;
+                    d = m.d * heightDiff;                    
+                }
+
+                float u1 = 0;
+                float u2 = 0.5f;
+
+                float prevTX = m.tx;
+                float prevTY = m.ty;
+
+                for (int32 col = 0; col < 3; col++)
+                {                    
+                    if ((col == 0) || (col == 2))
+                    {
+                        a = m.a * image.mWidth / 2;
+                        b = m.b * image.mWidth / 2;
+                        if (col == 2)
+                            u2 = 1.0f;
+                    }
+                    else
+                    {
+                        a = m.a * (useWidth - image.mWidth);
+                        b = m.b * (useWidth - image.mWidth);
+                        u1 = u2;
+                    }
+
+                    Gfx_SetDrawVertex(vtx + 0, m.tx, m.ty, 0, u1, v1, mColor);
+                    Gfx_SetDrawVertex(vtx + 1, m.tx + a, m.ty + b, 0, u2, v1, mColor);
+                    Gfx_SetDrawVertex(vtx + 2, m.tx + c, m.ty + d, 0, u1, v2, mColor);
+                    Gfx_CopyDrawVertex(vtx + 3, vtx + 2);
+                    Gfx_CopyDrawVertex(vtx + 4, vtx + 1);
+                    Gfx_SetDrawVertex(vtx + 5, m.tx + (a + c), m.ty + (b + d), 0, u2, v2, mColor);
+
+                    m.tx += a;
+                    m.ty += b;
+                    vtx += 6;                    
+                }
+
+                m.tx = prevTX + c;
+                m.ty = prevTY + d;
+            }
+        }
+
+        public void DrawIndexedVertices(VertexDefinition vertexDef, void* vertices, int vtxCount, uint16[] indices)
+        {
+            Gfx_DrawIndexedVertices2D(vertexDef.mVertexSize, vertices, (int32)vtxCount, indices.CArray(), (int32)indices.Count,
+                mMatrix.a, mMatrix.b, mMatrix.c, mMatrix.d, mMatrix.tx, mMatrix.ty, ZDepth);
+        }
+
+        public void DrawIndexedVertices(VertexDefinition vertexDef, void* vertices, int vtxCount, uint16* indices, int idxCount)
+        {
+            Gfx_DrawIndexedVertices2D(vertexDef.mVertexSize, vertices, (int32)vtxCount, indices, (int32)idxCount,
+                mMatrix.a, mMatrix.b, mMatrix.c, mMatrix.d, mMatrix.tx, mMatrix.ty, ZDepth);
+        }
+
+        public void SetShaderConstantData(int slotIdx, void* data, int size)
+        {
+            Gfx_SetShaderConstantData((int32)slotIdx, data, (int32)size);
+        }
+
+        public void SetShaderConstantData(int32 slotIdx, void* data, ConstantDataDefinition constantDataDefinition)
+        {
+            int32* dataTypesPtr = (int32*)constantDataDefinition.mDataTypes.CArray();
+            Gfx_SetShaderConstantDataTyped(slotIdx, data, constantDataDefinition.mDataSize, dataTypesPtr, (int32)constantDataDefinition.mDataTypes.Count);
+        }
+
+        public void SetShaderConstantData(int32 slotIdx, Matrix4 matrix)
+        {
+			var mtx = matrix;
+            Gfx_SetShaderConstantData(slotIdx, &mtx, (int32)sizeof(Matrix4));
+        }        
+
+        public float DrawString(StringView theString, float x, float y, FontAlign alignment = FontAlign.Left, float width = 0, FontOverflowMode overflowMode = FontOverflowMode.Overflow, FontMetrics* fontMetrics = null)
+        {
+            return mFont.Draw(this, theString, x, y, (int32)alignment, width, overflowMode, fontMetrics);
+        }
+
+        public void DrawQuad(Image image, float x, float y, float u1, float v1, float width, float height, float u2, float v2)
+        {
+            Matrix m = Matrix();
+            m.SetMultiplied(x, y, ref mMatrix);
+
+            float a = m.a * width;
+            float b = m.b * width;
+            float c = m.c * height;
+            float d = m.d * height;
+
+            Gfx_AllocTris(image.mNativeTextureSegment, 6);
+            Gfx_SetDrawVertex(0, m.tx, m.ty, 0, u1, 0, mColor);
+            Gfx_SetDrawVertex(1, m.tx + a, m.ty + b, 0, u2, 0, mColor);
+            Gfx_SetDrawVertex(2, m.tx + c, m.ty + d, 0, u1, 1, mColor);
+            Gfx_CopyDrawVertex(3, 2);
+            Gfx_CopyDrawVertex(4, 1);
+            Gfx_SetDrawVertex(5, m.tx + (a + c), m.ty + (b + d), 0, u2, 1, mColor);
+        }
+
+        // Untranslated
+        public void DrawQuads(Image img, Vertex3D[] vertices, int32 vtxCount)
+        {
+            Vertex3D* vtxPtr = vertices.CArray();
+            Gfx_DrawQuads(img.mNativeTextureSegment, vtxPtr, vtxCount);
+        }        
+
+        /*public TextMetrics GetTextMetrics(string theString, int justification, float width, StringEndMode stringEndMode)
+        {
+            
+        }*/
+        
+        public void FillRect(float x, float y, float width, float height)
+        {
+            Matrix newMatrix = Matrix.IdentityMatrix;
+            newMatrix.SetMultiplied(x, y, width, height, ref mMatrix);
+
+            mWhiteDot.Draw(newMatrix, ZDepth, mColor);
+        }
+
+        public void OutlineRect(float x, float y, float width, float height, float thickness = 1)
+        {
+            FillRect(x, y, width, thickness); // Top
+            FillRect(x, y + thickness, thickness, height - thickness * 2); // Left            
+            FillRect(x + width - thickness, y + thickness, thickness, height - thickness * 2); // Right
+            FillRect(x, y + height - thickness, width, thickness);
+        }
+
+        public void FillRectGradient(float x, float y, float width, float height,
+            Color colorTopLeft, Color colorTopRight, Color colorBotLeft, Color colorBotRight)
+        {
+            Matrix m = Matrix.IdentityMatrix;
+            m.SetMultiplied(x, y, width, height, ref mMatrix);
+
+            //TODO: Multiply color
+
+            Gfx_AllocTris(mWhiteDot.mNativeTextureSegment, 6);
+            
+            Gfx_SetDrawVertex(0, m.tx, m.ty, 0, 0, 0, Color.Mult(mColor, colorTopLeft));
+            Gfx_SetDrawVertex(1, m.tx + m.a, m.ty + m.b, 0, 0, 0, Color.Mult(mColor, colorTopRight));
+            Gfx_SetDrawVertex(2, m.tx + m.c, m.ty + m.d, 0, 0, 0, Color.Mult(mColor, colorBotLeft));
+
+            Gfx_CopyDrawVertex(3, 2);
+            Gfx_CopyDrawVertex(4, 1);
+            Gfx_SetDrawVertex(5, m.tx + (m.a + m.c), m.ty + (m.b + m.d), 0, 0, 0, Color.Mult(mColor, colorBotRight));
+        }
+        
+        public void PolyStart(Image image, int32 vertices)
+        {
+            Gfx_AllocTris(image.mNativeTextureSegment, vertices);            
+        }
+
+        public void PolyVertex(int32 idx, float x, float y, float u, float v, Color color = Color.White)
+        {
+            Matrix m = mMatrix;
+            float aX = m.tx + m.a * x + m.c * y;
+            float aY = m.ty + m.b * x + m.d * y;
+            Gfx_SetDrawVertex(idx, aX, aY, 0, u, v, color);
+        }
+
+        public void PolyVertexCopy(int32 idx, int32 srcIdx)
+        {
+            Gfx_CopyDrawVertex(idx, srcIdx);
+        }
+    }
+#else
+    public class Graphics : GraphicsBase
+    {        
+        internal Graphics()
+        {            
+            mRenderStateDisposeProxy.mDisposeProxyDelegate = PopRenderState;
+        }
+
+        public void SetTexture(int textureIdx, Image image)
+        {
+            Debug.Assert(image.mSrcTexture == null);
+            BFApp.StudioHostProxy.Gfx_SetTexture(textureIdx, image.mStudioImage);            
+        }
+
+        /*public void StartDraw()
+        {            
+            Debug.Assert(mRenderStateStackIdx == 0);
+            Debug.Assert(mColorStackIdx == 0);
+            Debug.Assert(mClipStackIdx == 0);
+
+            if (mDefaultRenderState != null)
+            {
+                mRenderStateStack[0] = mDefaultRenderState;
+                //Gfx_SetRenderState(mRenderStateStack[0].mNativeRenderState);
+            }
+            mMatrix.Identity();
+            mClipPoolIdx = 0;
+        }
+
+        public void EndDraw()
+        {
+            Debug.Assert(mMatrixStackIdx == 0);
+            Debug.Assert(mColorStackIdx == 0);
+        }*/
+
+        public override IDisposable PushRenderState(RenderState renderState)
+        {
+            BFApp.sApp.mStudioHost.Proxy.Gfx_SetRenderState(renderState.mStudioRenderState);
+            mRenderStateStack[++mRenderStateStackIdx] = renderState;
+            return mRenderStateDisposeProxy;            
+        }
+
+        public override void PopRenderState()
+        {
+            BFApp.sApp.mStudioHost.Proxy.Gfx_SetRenderState(mRenderStateStack[--mRenderStateStackIdx].mStudioRenderState);
+        }
+
+        public void Draw(IDrawable drawable, float x = 0, float y = 0, int cel = 0)
+        {
+#if STUDIO_CLIENT
+            BFApp.sApp.TrackDraw();
+#endif
+
+            Matrix newMatrix = new Matrix();
+            newMatrix.SetMultiplied(x, y, ref mMatrix);
+
+            //newMatrix.tx *= g.mScale;
+            //newMatrix.ty *= g.mScale;
+
+            drawable.Draw(newMatrix, ZDepth, mColor, cel);
+
+            /*SexyExport.MatrixDrawImageInst(cSGraphics.mGraphics, mImageInstInfos[cel], newMatrix.a, newMatrix.b, newMatrix.c, newMatrix.d, newMatrix.tx, newMatrix.ty,
+                (int)mPixelSnapping, mSmoothing ? 1 : 0, mAdditive ? 1 : 0, (int)g.mColor);*/
+        }
+
+        public void DrawButton(Image image, float x, float y, float width)
+        {
+            Matrix m = new Matrix();
+            m.SetMultiplied(x, y, ref mMatrix);
+
+            //Gfx_AllocDrawVertices(image.mNativeTextureSegment, 6 * 3);
+
+            //TODO: Snap?            
+
+            float segmentW = Math.Min(image.mSrcWidth / 2, width / 2);
+
+            float a;
+            float b;
+            float c = m.c * image.mSrcHeight;
+            float d = m.d * image.mSrcHeight;
+
+            float u1 = 0;
+            float u2 = segmentW / (float)image.mSrcWidth;
+            int vtx = 0;
+
+            for (int col = 0; col < 3; col++)
+            {
+                if ((col == 0) || (col == 2))
+                {
+                    a = m.a * segmentW;
+                    b = m.b * segmentW;
+                    if (col == 2)
+                    {
+                        u1 = 1.0f - u1;
+                        u2 = 1.0f;
+                    }
+                }
+                else if (width > image.mSrcWidth)
+                {
+                    a = m.a * (width - segmentW * 2);
+                    b = m.b * (width - segmentW * 2);
+                    u1 = u2;
+                }
+                else
+                {
+                    a = 0;
+                    b = 0;
+                    u1 = u2;
+                }
+
+                /*Gfx_SetDrawVertex(vtx + 0, m.tx, m.ty, 0, u1, 0, mColor);
+                Gfx_SetDrawVertex(vtx + 1, m.tx + a, m.ty + b, 0, u2, 0, mColor);
+                Gfx_SetDrawVertex(vtx + 2, m.tx + c, m.ty + d, 0, u1, 1, mColor);
+                Gfx_CopyDrawVertex(vtx + 3, vtx + 2);
+                Gfx_CopyDrawVertex(vtx + 4, vtx + 1);
+                Gfx_SetDrawVertex(vtx + 5, m.tx + (a + c), m.ty + (b + d), 0, u2, 1, mColor);*/
+
+                m.tx += a;
+                m.ty += b;
+                vtx += 6;
+            }
+        }
+
+        public void DrawButtonVert(Image image, float x, float y, float height)
+        {
+            Matrix m = new Matrix();
+            m.SetMultiplied(x, y, ref mMatrix);
+
+            //Gfx_AllocDrawVertices(image.mNativeTextureSegment, 6 * 3);
+
+            //TODO: Snap?            
+
+            float segmentH = Math.Min(image.mSrcHeight / 2, height / 2);
+
+            float a = m.a * image.mSrcWidth;
+            float b = m.b * image.mSrcWidth;
+            float c = 0;
+            float d = 0;
+
+            float v1 = 0;
+            float v2 = segmentH / (float)image.mSrcHeight;
+            int vtx = 0;
+
+            for (int row = 0; row < 3; row++)
+            {
+                if ((row == 0) || (row == 2))
+                {
+                    c = m.c * segmentH;
+                    d = m.d * segmentH;
+                    if (row == 2)
+                    {
+                        v1 = 1.0f - v1;
+                        v2 = 1.0f;
+                    }
+                }
+                else if (height > image.mSrcHeight)
+                {
+                    c = m.c * (height - image.mSrcHeight);
+                    d = m.d * (height - image.mSrcHeight);
+                    v1 = v2;
+                }
+                else
+                {
+                    c = 0;
+                    d = 0;
+                    v1 = v2;
+                }
+
+                /*Gfx_SetDrawVertex(vtx + 0, m.tx, m.ty, 0, 0, v1, mColor);
+                Gfx_SetDrawVertex(vtx + 1, m.tx + a, m.ty + b, 0, 1, v1, mColor);
+                Gfx_SetDrawVertex(vtx + 2, m.tx + c, m.ty + d, 0, 0, v2, mColor);
+                Gfx_CopyDrawVertex(vtx + 3, vtx + 2);
+                Gfx_CopyDrawVertex(vtx + 4, vtx + 1);
+                Gfx_SetDrawVertex(vtx + 5, m.tx + (a + c), m.ty + (b + d), 0, 1, v2, mColor);*/
+
+                m.tx += c;
+                m.ty += d;
+                vtx += 6;
+            }
+        }
+
+        public void DrawBox(Image image, float x, float y, float width, float height)
+        {
+            Matrix m = new Matrix();
+            m.SetMultiplied(x, y, ref mMatrix);
+
+            //Gfx_AllocDrawVertices(image.mNativeTextureSegment, 6 * 3 * 3);
+
+            //TODO: Snap?            
+
+            float a;
+            float b;
+            float c;
+            float d;
+
+
+            int vtx = 0;
+
+            float prevX = m.tx;
+
+            float segmentW = Math.Min(image.mSrcWidth / 2, width / 2);
+
+            float v1 = 0;
+            float v2 = segmentW / (float)image.mSrcWidth;
+
+            for (int row = 0; row < 3; row++)
+            {
+                if ((row == 0) || (row == 2))
+                {
+                    c = m.c * image.mSrcHeight / 2;
+                    d = m.d * image.mSrcHeight / 2;
+                    if (row == 2)
+                    {
+                        v1 = 1.0f - v1;
+                        v2 = 1.0f;
+                    }
+                }
+                else
+                {
+                    c = m.c * (height - image.mSrcHeight);
+                    d = m.d * (height - image.mSrcHeight);
+                    v1 = v2;
+                }
+
+                float u1 = 0;
+                float u2 = 0.5f;
+
+                float prevTX = m.tx;
+                float prevTY = m.ty;
+
+                for (int col = 0; col < 3; col++)
+                {
+                    if ((col == 0) || (col == 2))
+                    {
+                        a = m.a * image.mSrcWidth / 2;
+                        b = m.b * image.mSrcWidth / 2;
+                        if (col == 2)
+                            u2 = 1.0f;
+                    }
+                    else
+                    {
+                        a = m.a * (width - image.mSrcWidth);
+                        b = m.b * (width - image.mSrcWidth);
+                        u1 = u2;
+                    }
+
+                    /*Gfx_SetDrawVertex(vtx + 0, m.tx, m.ty, 0, u1, v1, mColor);
+                    Gfx_SetDrawVertex(vtx + 1, m.tx + a, m.ty + b, 0, u2, v1, mColor);
+                    Gfx_SetDrawVertex(vtx + 2, m.tx + c, m.ty + d, 0, u1, v2, mColor);
+                    Gfx_CopyDrawVertex(vtx + 3, vtx + 2);
+                    Gfx_CopyDrawVertex(vtx + 4, vtx + 1);
+                    Gfx_SetDrawVertex(vtx + 5, m.tx + (a + c), m.ty + (b + d), 0, u2, v2, mColor);*/
+
+                    m.tx += a;
+                    m.ty += b;
+                    vtx += 6;
+                }
+
+                m.tx = prevTX + c;
+                m.ty = prevTY + d;
+            }
+        }
+
+        public unsafe void DrawIndexedVertices(VertexDefinition vertexDef, void* vertices, int vtxCount, ushort[] indices)
+        {
+            fixed (ushort* indicesPtr = indices)
+                BFApp.sApp.mStudioHost.Proxy.Gfx_DrawIndexedVertices(vertexDef.mStudioVertexDefinition, vertexDef.mVertexSize * vtxCount, vertices, vtxCount,
+                    indices.Length * sizeof(ushort), indicesPtr, indices.Length);
+        }
+
+        public unsafe void SetShaderConstantData(int slotIdx, Matrix4 matrix)
+        {            
+            BFApp.StudioHostProxy.Gfx_SetShaderConstantData(slotIdx, Marshal.SizeOf(matrix), &matrix);
+        }
+
+        public unsafe void Draw(RenderCmd renderCmd)
+        {
+            BFApp.StudioHostProxy.Gfx_DrawRenderCmd(renderCmd.mStudioRenderCmd);
+        }
+
+        public float DrawString(string theString, float x, float y, FontAlign alignment = FontAlign.Left, float width = 0, FontOverflowMode overflowMode = FontOverflowMode.Overflow, FontMetrics fontMetrics = null)
+        {
+            return mFont.Draw(this, theString, x, y, (int)alignment, width, overflowMode, fontMetrics);
+        }
+
+        /*public TextMetrics GetTextMetrics(string theString, int justification, float width, StringEndMode stringEndMode)
+        {
+            
+        }*/
+
+        public void FillRect(float x, float y, float width, float height)
+        {
+            Matrix newMatrix = new Matrix();
+            newMatrix.SetMultiplied(x, y, width, height, ref mMatrix);
+
+            mWhiteDot.Draw(newMatrix, ZDepth, mColor, 0);
+        }
+
+        public void FillRectGradient(float x, float y, float width, float height,
+            uint colorTopLeft, uint colorTopRight, uint colorBotLeft, uint colorBotRight)
+        {
+            Matrix m = new Matrix();
+            m.SetMultiplied(x, y, width, height, ref mMatrix);
+
+            //TODO: Multiply color
+
+            /*Gfx_AllocDrawVertices(mWhiteDot.mNativeTextureSegment, 6);
+
+            Gfx_SetDrawVertex(0, m.tx, m.ty, 0, 0, 0, Color.Mult(mColor, colorTopLeft));
+            Gfx_SetDrawVertex(1, m.tx + m.a, m.ty + m.b, 0, 0, 0, Color.Mult(mColor, colorTopRight));
+            Gfx_SetDrawVertex(2, m.tx + m.c, m.ty + m.d, 0, 0, 0, Color.Mult(mColor, colorBotLeft));
+
+            Gfx_CopyDrawVertex(3, 2);
+            Gfx_CopyDrawVertex(4, 1);
+            Gfx_SetDrawVertex(5, m.tx + (m.a + m.c), m.ty + (m.b + m.d), 0, 0, 0, Color.Mult(mColor, colorBotRight));*/
+        }
+
+        public void PolyStart(Image image, int vertices)
+        {
+            //TODO:Implement
+        }
+
+        public void PolyVertex(int idx, float x, float y, float u, float v, uint color = Color.White)
+        {
+            //TODO:Implement
+        }
+
+        public void PolyVertexCopy(int idx, int srcIdx)
+        {
+            //TODO:Implement
+        }
+    }
+#endif
+}

+ 11 - 0
BeefLibs/Beefy2D/src/gfx/IDrawable.bf

@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Beefy.gfx
+{
+    public interface IDrawable
+    {
+        void Draw(Matrix matrix, float z, uint32 color);
+    }
+}

+ 283 - 0
BeefLibs/Beefy2D/src/gfx/Image.bf

@@ -0,0 +1,283 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Runtime.InteropServices;
+using Beefy.utils;
+using System.Diagnostics;
+
+#if STUDIO_CLIENT
+using Beefy.ipc;
+#endif
+
+namespace Beefy.gfx
+{
+#if !STUDIO_CLIENT
+    public class Image : IDrawable
+    {        
+        public Image mSrcTexture;
+        public int32 mSrcX;
+        public int32 mSrcY;
+        public int32 mSrcWidth;
+        public int32 mSrcHeight;
+        public float mWidth;
+        public float mHeight;
+        public float mX;
+        public float mY;
+        public void* mNativeTextureSegment;
+        public PixelSnapping mPixelSnapping = PixelSnapping.Auto;
+        
+        [StdCall, CLink]
+        public static extern void Gfx_DrawTextureSegment(void* textureSegment, float a, float b, float c, float d, float tx, float ty, float z, uint32 color, int32 pixelSnapping);
+
+		[StdCall, CLink]
+		static extern void* Gfx_LoadTexture(char8* fileName, int32 additive);
+
+        [StdCall, CLink]
+        static extern void* Gfx_CreateDynTexture(int32 width, int32 height);
+
+        [StdCall, CLink]
+        static extern void* Gfx_CreateRenderTarget(int32 width, int32 height, int32 destAlpha);
+
+        [StdCall, CLink]
+        static extern void* Gfx_ModifyTextureSegment(void* destSegment, void* srcTextureSegment, int32 srcX, int32 srcY, int32 srcWidth, int32 srcHeight);
+
+		[StdCall, CLink]
+		static extern void* Gfx_CreateTextureSegment(void* textureSegment, int32 srcX, int32 srcY, int32 srcWidth, int32 srcHeight);
+
+		[StdCall, CLink]
+		public static extern void Gfx_SetDrawSize(void* textureSegment, int32 width, int32 height);
+
+		[StdCall, CLink]
+		static extern void Gfx_Texture_SetBits(void* textureSegment, int32 destX, int32 destY, int32 destWidth, int32 destHeight, int32 srcPitch, uint32* bits);
+
+        [StdCall, CLink]
+        static extern void Gfx_Texture_Delete(void* textureSegment);
+
+        [StdCall, CLink]
+        static extern int32 Gfx_Texture_GetWidth(void* textureSegment);
+
+        [StdCall, CLink]
+        static extern int32 Gfx_Texture_GetHeight(void* textureSegment);
+
+        internal this()
+        {            
+        }
+
+		public ~this()
+		{
+			Gfx_Texture_Delete(mNativeTextureSegment);
+		}
+
+        public void Draw(Matrix matrix, float z, uint32 color)
+        {            
+            Image.Gfx_DrawTextureSegment(mNativeTextureSegment, matrix.a, matrix.b, matrix.c, matrix.d, matrix.tx, matrix.ty, z,
+                color, (int32)mPixelSnapping);            
+        }
+
+        public static Image CreateRenderTarget(int32 width, int32 height, bool destAlpha = false)
+        {
+            void* aNativeTextureSegment = Gfx_CreateRenderTarget(width, height, destAlpha ? 1 : 0);
+            if (aNativeTextureSegment == null)
+                return null;
+
+            return CreateFromNativeTextureSegment(aNativeTextureSegment);
+        }
+
+        public static Image LoadFromFile(String fileName, bool additive = false)
+        {
+			scope AutoBeefPerf("Image.LoadFromFile");
+
+            void* aNativeTextureSegment = Gfx_LoadTexture(fileName, additive ? 1 : 0);
+            if (aNativeTextureSegment == null)
+                return null;
+
+            return CreateFromNativeTextureSegment(aNativeTextureSegment);
+        }
+
+		public static Image CreateDynamic(int width, int height)
+		{
+			void* nativeTextureSegment = Gfx_CreateDynTexture((.)width, (.)height);
+			if (nativeTextureSegment == null)
+				return null;
+
+			return CreateFromNativeTextureSegment(nativeTextureSegment);
+		}
+
+        public static Image CreateFromNativeTextureSegment(void* nativeTextureSegment)
+        {
+            Image texture = new Image();
+            texture.mNativeTextureSegment = nativeTextureSegment;
+            texture.mSrcWidth = Gfx_Texture_GetWidth(nativeTextureSegment);
+            texture.mSrcHeight = Gfx_Texture_GetHeight(nativeTextureSegment);
+            texture.mWidth = texture.mSrcWidth;
+            texture.mHeight = texture.mSrcHeight;
+            return texture;
+        }
+
+        public Image CreateImageSegment(int srcX, int srcY, int srcWidth, int srcHeight)
+        {
+            Image textureSegment = new Image();
+            textureSegment.mSrcTexture = this;
+            textureSegment.mSrcX = (int32)srcX;
+            textureSegment.mSrcY = (int32)srcY;
+            textureSegment.mSrcWidth = (int32)srcWidth;
+            textureSegment.mSrcHeight = (int32)srcHeight;
+            textureSegment.mWidth = Math.Abs(srcWidth);
+            textureSegment.mHeight = Math.Abs(srcHeight);
+            textureSegment.mNativeTextureSegment = Gfx_CreateTextureSegment(mNativeTextureSegment, (int32)srcX, (int32)srcY, (int32)srcWidth, (int32)srcHeight);
+            return textureSegment;
+        }
+
+		public void CreateImageSegment(Image srcImage, int srcX, int srcY, int srcWidth, int srcHeight)
+		{
+			if (mNativeTextureSegment != null)
+			{
+				Gfx_Texture_Delete(mNativeTextureSegment);
+			}
+		    
+		    mSrcTexture = srcImage;
+		    mSrcX = (int32)srcX;
+		    mSrcY = (int32)srcY;
+		    mSrcWidth = (int32)srcWidth;
+		    mSrcHeight = (int32)srcHeight;
+		    mWidth = Math.Abs(srcWidth);
+		    mHeight = Math.Abs(srcHeight);
+		    mNativeTextureSegment = Gfx_CreateTextureSegment(srcImage.mNativeTextureSegment, (int32)srcX, (int32)srcY, (int32)srcWidth, (int32)srcHeight);
+		}
+
+		public void SetDrawSize(int width, int height)
+		{
+			mWidth = width;
+			mHeight = height;
+			Gfx_SetDrawSize(mNativeTextureSegment, (int32)width, (int32)height);
+		}
+
+		public void Scale(float scale)
+		{
+			mWidth = (int32)(mSrcWidth * scale);
+			mHeight = (int32)(mSrcHeight * scale);
+			Gfx_SetDrawSize(mNativeTextureSegment, (int32)mWidth, (int32)mHeight);
+		}
+
+		public void Modify(Image srcImage, int srcX, int srcY, int srcWidth, int srcHeight)
+		{
+			mSrcTexture = srcImage;
+			mSrcX = (int32)srcX;
+			mSrcY = (int32)srcY;
+			mSrcWidth = (int32)srcWidth;
+			mSrcHeight = (int32)srcHeight;
+			mWidth = Math.Abs(srcWidth);
+			mHeight = Math.Abs(srcHeight);
+			Gfx_ModifyTextureSegment(mNativeTextureSegment, srcImage.mNativeTextureSegment, (int32)srcX, (int32)srcY, (int32)srcWidth, (int32)srcHeight);
+		}
+
+		public void SetBits(int destX, int destY, int destWidth, int destHeight, int srcPitch, uint32* bits)
+		{
+			Gfx_Texture_SetBits(mNativeTextureSegment, (.)destX, (.)destY, (.)destWidth, (.)destHeight, (.)srcPitch, bits);
+		}
+
+        public void CreateImageCels(Image[,] celImages)
+        {
+			int32 rows = (int32)celImages.GetLength(0);
+			int32 cols = (int32)celImages.GetLength(1);
+
+            int32 celW = mSrcWidth / cols;
+            int32 celH = mSrcHeight / rows;
+
+            Debug.Assert(celW * cols == mSrcWidth);
+            Debug.Assert(celH * rows == mSrcHeight);
+            
+            for (int32 row = 0; row < rows; row++)
+            {
+                for (int32 col = 0; col < cols; col++)
+                {
+                    celImages[row, col] = CreateImageSegment(col * celW, row * celH, celW, celH);
+                }
+            }
+        }
+    }
+#else    
+    public class Image : IDrawable
+    {
+        public Image mSrcTexture;
+        public int mSrcX;
+        public int mSrcY;
+        public int mSrcWidth;
+        public int mSrcHeight;
+        public float mWidth;
+        public float mHeight;
+        public float mX;
+        public float mY;        
+        public IPCProxy<IStudioImage> mStudioImage;
+        public Image[] mCelImages;
+
+        PixelSnapping mPixelSnapping = PixelSnapping.Auto;        
+
+        public void Draw(Matrix matrix, float z, uint color, int cel)
+        {
+            //Image.Gfx_DrawTextureSegment(mNativeTextureSegment, matrix.a, matrix.b, matrix.c, matrix.d, matrix.tx, matrix.ty,
+                //color, mAdditive ? 1 : 0, (int)mPixelSnapping);
+            mStudioImage.Proxy.DrawTextureSegment(matrix.a, matrix.b, matrix.c, matrix.d, matrix.tx, matrix.ty, z,
+                color, (int)mPixelSnapping);
+        }
+
+        public static Image LoadFromFile(string fileName, bool additive = false)
+        {
+            //void* aNativeTextureSegment = Gfx_LoadTexture(fileName);
+
+            IPCObjectId aNativeTextureSegmentId = BFApp.sApp.mStudioHost.Proxy.LoadImage(fileName, additive);
+            if (aNativeTextureSegmentId.IsNull())
+                return null;
+
+            return CreateFromNativeTextureSegment(aNativeTextureSegmentId);
+        }
+
+        public static Image CreateFromNativeTextureSegment(IPCObjectId nativeTextureSegmentId)
+        {
+            Image texture = new Image();
+            texture.mStudioImage = IPCProxy<IStudioImage>.Create(nativeTextureSegmentId);
+            texture.mSrcWidth = texture.mStudioImage.Proxy.GetSrcWidth();
+            texture.mSrcHeight = texture.mStudioImage.Proxy.GetSrcHeight();
+            texture.mWidth = texture.mSrcWidth;
+            texture.mHeight = texture.mSrcHeight;
+            return texture;
+        }
+
+        internal Image()
+        {
+        }
+
+        public Image CreateImageSegment(int srcX, int srcY, int srcWidth, int srcHeight)
+        {
+            Image aTextureSegment = new Image();
+            aTextureSegment.mStudioImage = IPCProxy<IStudioImage>.Create(mStudioImage.Proxy.CreateImageSegment(srcX, srcY, srcWidth, srcHeight));
+            aTextureSegment.mSrcTexture = this;
+            aTextureSegment.mSrcX = srcX;
+            aTextureSegment.mSrcY = srcY;
+            aTextureSegment.mSrcWidth = srcWidth;
+            aTextureSegment.mSrcHeight = srcHeight;
+            aTextureSegment.mWidth = Math.Abs(srcWidth);
+            aTextureSegment.mHeight = Math.Abs(srcHeight);   
+            return aTextureSegment;
+        }
+
+        public void CreateImageCels(int cols, int rows)
+        {
+            int celW = mSrcWidth / cols;
+            int celH = mSrcHeight / rows;
+
+            Debug.Assert(celW * cols == mSrcWidth);
+            Debug.Assert(celH * rows == mSrcHeight);
+
+            mCelImages = new Image[cols * rows];
+            for (int row = 0; row < rows; row++)
+            {
+                for (int col = 0; col < cols; col++)
+                {
+                    mCelImages[col + row * cols] = CreateImageSegment(col * celW, row * celH, celW, celH);
+                }
+            }
+        }
+    }
+#endif
+}

+ 205 - 0
BeefLibs/Beefy2D/src/gfx/Matrix.bf

@@ -0,0 +1,205 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.geom;
+
+namespace Beefy.gfx
+{
+    public struct Matrix
+    {
+        public float a;
+        public float b;
+        public float c;
+        public float d;
+        public float tx;
+        public float ty;
+
+		public this()
+		{
+			a = 1;
+			b = 0;
+			c = 0;
+			d = 1;
+			tx = 0;
+			ty = 0;
+		}
+
+        public this(float _a, float _b, float _c, float _d, float _tx, float _ty)
+        {
+            a = _a;
+            b = _b;
+            c = _c;
+            d = _d;
+            tx = _tx;
+            ty = _ty;
+        }
+        
+        public static Matrix IdentityMatrix = Matrix(1, 0, 0, 1, 0, 0);       
+
+        public Matrix Duplicate()
+        {
+            return Matrix(a, b, c, d, tx, ty);
+        }
+
+        public void Identity() mut
+        {
+            tx = 0;
+            ty = 0;
+            a = 1;
+            b = 0;
+            c = 0;
+            d = 1;
+        }
+
+        public void Translate(float x, float y) mut
+        {
+            tx += x;
+            ty += y;
+        }
+
+        public void Scale(float scaleX, float scaleY) mut
+        {
+            a *= scaleX;
+            b *= scaleY;
+            c *= scaleX;
+            d *= scaleY;
+            tx *= scaleX;
+            ty *= scaleY;
+        }
+
+        public void Rotate(float angle) mut
+        {
+            float _a = a;
+            float _b = b;
+            float _c = c;
+            float _d = d;
+            float _tx = tx;
+            float _ty = ty;
+
+            float sin = (float)Math.Sin(angle);
+            float cos = (float)Math.Cos(angle);
+
+            a = _a * cos - _b * sin;
+            b = _a * sin + _b * cos;
+            c = _c * cos - _d * sin;
+            d = _c * sin + _d * cos;
+            tx = _tx * cos - _ty * sin;
+            ty = _tx * sin + _ty * cos;
+        }
+
+        public void Multiply(Matrix mat2) mut
+        {
+            float _a = a;
+            float _b = b;
+            float _c = c;
+            float _d = d;
+            float _tx = tx;
+            float _ty = ty;
+
+            a = _a * mat2.a + _b * mat2.c;
+            b = _a * mat2.b + _b * mat2.d;
+            c = _c * mat2.a + _d * mat2.c;
+            d = _c * mat2.b + _d * mat2.d;
+
+            tx = _tx * mat2.a + _ty * mat2.c + mat2.tx;
+            ty = _tx * mat2.b + _ty * mat2.d + mat2.ty;
+        }
+
+        public void Set(Matrix mat2) mut
+        {
+            a = mat2.a;
+            b = mat2.b;
+            c = mat2.c;
+            d = mat2.d;
+
+            tx = mat2.tx;
+            ty = mat2.ty;
+        }
+
+        public void SetMultiplied(float x, float y, ref Matrix mat2) mut
+        {            
+            a = mat2.a;
+            b = mat2.b;
+            c = mat2.c;
+            d = mat2.d;
+
+            tx = x * mat2.a + y * mat2.c + mat2.tx;
+            ty = x * mat2.b + y * mat2.d + mat2.ty;
+        }
+         
+        public void SetMultiplied(float x, float y, float width, float height, ref Matrix mat2) mut
+        {
+            a = mat2.a * width;
+            b = mat2.b * width;
+            c = mat2.c * height;
+            d = mat2.d * height;
+
+            tx = x * mat2.a + y * mat2.c + mat2.tx;
+            ty = x * mat2.b + y * mat2.d + mat2.ty;
+        }
+
+        public void SetMultiplied(Matrix mat1, Matrix mat2) mut
+        {
+            float a1 = mat1.a;
+            float b1 = mat1.b;
+            float c1 = mat1.c;
+            float d1 = mat1.d;
+            float tx1 = mat1.tx;
+            float ty1 = mat1.ty;
+
+            float a2 = mat2.a;
+            float b2 = mat2.b;
+            float c2 = mat2.c;
+            float d2 = mat2.d;
+            float tx2 = mat2.tx;
+            float ty2 = mat2.ty;
+
+            a = a1 * a2 + b1 * c2;
+            b = a1 * b2 + b1 * d2;
+            c = c1 * a2 + d1 * c2;
+            d = c1 * b2 + d1 * d2;
+
+            tx = tx1 * a2 + ty1 * c2 + tx2;
+            ty = tx1 * b2 + ty1 * d2 + ty2;
+        }
+
+        public Point Multiply(Point point)
+        {
+            return Point(tx + a * point.x + c * point.y, ty + b * point.x + d * point.y);
+        }
+
+        public void Invert() mut
+        {
+            float _a = a;
+            float _b = b;
+            float _c = c;
+            float _d = d;
+            float _tx = tx;
+            float _ty = ty;
+
+            float den = a * d - b * c;
+
+            a = _d / den;
+            b = -_b / den;
+            c = -_c / den;
+            d = _a / den;
+            tx = (_c * _ty - _d * _tx) / den;
+            ty = -(_a * _ty - _b * _tx) / den;
+        }
+
+        public static Matrix Lerp(Matrix mat1, Matrix mat2, float pct)
+        {
+            float omp = 1.0f - pct;
+
+            Matrix matrix = Matrix();
+            matrix.a = (mat1.a * omp) + (mat2.a * pct);
+            matrix.b = (mat1.b * omp) + (mat2.b * pct);
+            matrix.c = (mat1.c * omp) + (mat2.c * pct);
+            matrix.d = (mat1.d * omp) + (mat2.d * pct);
+            matrix.tx = (mat1.tx * omp) + (mat2.tx * pct);
+            matrix.ty = (mat1.ty * omp) + (mat2.ty * pct);
+
+            return matrix;
+        }
+    }
+}

+ 391 - 0
BeefLibs/Beefy2D/src/gfx/Model.bf

@@ -0,0 +1,391 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Beefy;
+using Beefy.utils;
+using Beefy.geom;
+
+#if STUDIO_CLIENT
+using Beefy.ipc;
+#endif
+
+namespace Beefy.gfx
+{
+    public class ModelDef
+    {
+        public struct JointTranslation
+        {
+            public Quaternion mQuat;
+            public Vector3 mScale;
+            public Vector3 mTrans;
+        }
+
+        //[StructLayout(LayoutKind.Sequential)]
+        public struct VertexDef
+        {
+            [VertexMember(VertexElementUsage.Position3D)]
+            public Vector3 mPosition;
+            [VertexMember(VertexElementUsage.Color)]
+            public uint32 mColor;
+            [VertexMember(VertexElementUsage.TextureCoordinate)]
+            public TexCoords mTexCoords;
+            [VertexMember(VertexElementUsage.Normal)]
+            public Vector3 mNormal;
+            [VertexMember(VertexElementUsage.TextureCoordinate, 1)]
+            public TexCoords mBumpTexCoords;
+            [VertexMember(VertexElementUsage.Tangent)]
+            public Vector3 mTangent;
+
+            public static VertexDefinition sVertexDefinition ~ delete _;
+
+			public static void Init()
+			{
+				sVertexDefinition = new VertexDefinition(typeof(VertexDef));
+			}
+        }
+    }
+
+    #if !STUDIO_CLIENT
+    extension ModelDef
+    {        
+        public class Animation
+        {            
+            public void* mNativeModelDefAnimation;
+            public int32 mFrameCount;
+            public String mName;
+            public int32 mAnimIdx;
+
+            [StdCall, CLink]
+            extern static void ModelDefAnimation_GetJointTranslation(void* nativeAnimation, int32 jointIdx, float frame, out JointTranslation jointTranslation);
+
+            [StdCall, CLink]
+            extern static int32 ModelDefAnimation_GetFrameCount(void* nativeAnimation);
+
+            [StdCall, CLink]
+            extern static char8* ModelDefAnimation_GetName(void* nativeAnimation);
+
+            [StdCall, CLink]
+            extern static void ModelDefAnimation_Clip(void* nativeAnimation, int32 startFrame, int32 numFrames);
+
+            internal this(void* nativeModelDefAnimation)
+            {
+                mNativeModelDefAnimation = nativeModelDefAnimation;
+                mFrameCount = ModelDefAnimation_GetFrameCount(mNativeModelDefAnimation);
+                mName = new String(ModelDefAnimation_GetName(mNativeModelDefAnimation));
+            }
+
+            public void GetJointTranslation(int32 jointIdx, float frame, out JointTranslation jointTranslation)
+            {
+                ModelDefAnimation_GetJointTranslation(mNativeModelDefAnimation, jointIdx, frame, out jointTranslation);
+            }
+
+            public void Clip(int32 startFrame, int32 numFrames)
+            {
+                ModelDefAnimation_Clip(mNativeModelDefAnimation, startFrame, numFrames);
+                mFrameCount = ModelDefAnimation_GetFrameCount(mNativeModelDefAnimation);
+            }
+        }        
+
+        public void* mNativeModelDef;        
+        public float mFrameRate;
+        public int32 mJointCount;
+        public Animation[] mAnims;
+        public Dictionary<String, Animation> mAnimMap = new Dictionary<String, Animation>();
+
+        [StdCall, CLink]
+        extern static void* Res_OpenFBX(String fileName, void* nativeVertexDef);
+
+        [StdCall, CLink]
+        extern static void* ModelDef_CreateModelInstance(void* nativeModel);
+
+        [StdCall, CLink]
+        extern static float ModelDef_GetFrameRate(void* nativeModel);
+
+        [StdCall, CLink]
+        extern static int32 ModelDef_GetJointCount(void* nativeModel);
+
+        [StdCall, CLink]
+        extern static int32 ModelDef_GetAnimCount(void* nativeModel);
+
+        [StdCall, CLink]
+        extern static void* ModelDef_GetAnimation(void* nativeModel, int32 animIdx);
+
+        this(void* nativeModelDef)
+        {
+            mNativeModelDef = nativeModelDef;
+
+            mFrameRate = ModelDef_GetFrameRate(mNativeModelDef);
+            mJointCount = ModelDef_GetJointCount(mNativeModelDef);
+            int32 animCount = ModelDef_GetAnimCount(mNativeModelDef);
+            mAnims = new Animation[animCount];
+
+            for (int32 animIdx = 0; animIdx < animCount; animIdx++)
+            {
+                var anim = new Animation(ModelDef_GetAnimation(mNativeModelDef, animIdx));
+                anim.mAnimIdx = animIdx;                
+                mAnims[animIdx] = anim;
+                mAnimMap[anim.mName] = anim;
+            }
+        }
+
+        public static ModelDef LoadModel(String fileName)
+        {
+            void* nativeModelDef = Res_OpenFBX(fileName, VertexDef.sVertexDefinition.mNativeVertexDefinition);
+            if (nativeModelDef == null)
+                return null;
+            return new ModelDef(nativeModelDef);            
+        }
+
+        public ModelInstance CreateInstance()
+        {
+            void* nativeModelInstance = ModelDef_CreateModelInstance(mNativeModelDef);
+            if (nativeModelInstance == null)
+                return null;
+            var modelInstance = new ModelInstance(nativeModelInstance, this);            
+            return modelInstance;
+        }
+
+        public Animation GetAnimation(String name)
+        {
+            return mAnimMap[name];
+        }
+    }
+
+    public class ModelInstance : RenderCmd
+    {
+        [StdCall, CLink]
+        extern static void ModelInstance_SetJointTranslation(void* nativeModelInstance, int32 jointIdx, ref ModelDef.JointTranslation jointTranslation);
+
+        [StdCall, CLink]
+        extern static void ModelInstance_SetMeshVisibility(void* nativeModelInstance, int32 jointIdx, int32 visibility);
+
+        public ModelDef mModelDef;
+        public ModelDef.Animation mAnim;
+        public float mFrame;
+        public float mAnimSpeed = 1.0f;
+        public bool mLoop;
+
+        internal this(void* nativeModelInstance, ModelDef modelDef)
+        {
+            mNativeRenderCmd = nativeModelInstance;
+            mModelDef = modelDef;
+        }
+
+        public void RehupAnimState()
+        {
+            for (int32 jointIdx = 0; jointIdx < mModelDef.mJointCount; jointIdx++)
+            {
+                ModelDef.JointTranslation jointTranslation;
+                mAnim.GetJointTranslation(jointIdx, mFrame, out jointTranslation);
+                SetJointTranslation(jointIdx, ref jointTranslation);
+            }
+        }
+
+        public void Update()
+        {
+            if (mAnim == null)
+                return;
+
+            mFrame += mModelDef.mFrameRate * BFApp.sApp.UpdateDelta * mAnimSpeed;
+
+            /*if ((mFrame >= 35.0f) || (mFrame < 1.0f))
+                mFrame = 34.0f;*/
+
+            if (mAnim.mFrameCount > 1)
+            {
+                float endFrameNum = mAnim.mFrameCount - 1.0f;
+                while (mFrame >= endFrameNum)
+                {
+                    if (mLoop)
+                        mFrame -= endFrameNum;
+                    else
+                        mFrame = endFrameNum - 0.00001f;
+                }
+                while (mFrame < 0)
+                    mFrame += endFrameNum;
+            }
+
+            RehupAnimState();
+        }
+
+        public void Play(ModelDef.Animation anim, bool loop = false)
+        {
+            mLoop = loop;
+            mAnim = anim;
+            mFrame = 0;
+            RehupAnimState();            
+        }
+
+        public void Play(String name, bool loop = false)
+        {            
+            Play(mModelDef.GetAnimation(name), loop);
+        }
+
+        public void Play(bool loop = false)
+        {
+            Play(mModelDef.mAnims[0], loop);
+        }
+
+        public void SetJointTranslation(int32 jointIdx, ref ModelDef.JointTranslation jointTranslation)
+        {
+            ModelInstance_SetJointTranslation(mNativeRenderCmd, jointIdx, ref jointTranslation);
+        }
+
+        public void SetMeshVisibility(int32 meshIdx, bool visible)
+        {
+            ModelInstance_SetMeshVisibility(mNativeRenderCmd, meshIdx, visible ? 1 : 0);
+        }
+    }
+#else
+    extension ModelDef
+    {        
+        public class Animation
+        {
+            public void* mNativeModelDefAnimation;
+            public int mFrameCount;
+            public string mName;
+            public int mAnimIdx;
+            public IPCProxy<IStudioModelDefAnimation> mStudioModelDefAnimation;
+
+            internal Animation(IPCProxy<IStudioModelDefAnimation> studioModelDefAnimation)
+            {
+                mStudioModelDefAnimation = studioModelDefAnimation;
+                mFrameCount = mStudioModelDefAnimation.Proxy.GetFrameCount();
+                mName = mStudioModelDefAnimation.Proxy.GetName();
+            }
+            
+            public void Clip(int startFrame, int numFrames)
+            {
+                //ModelDefAnimation_Clip(mNativeModelDefAnimation, startFrame, numFrames);
+                //mFrameCount = ModelDefAnimation_GetFrameCount(mNativeModelDefAnimation);
+                mStudioModelDefAnimation.Proxy.Clip(startFrame, numFrames);
+                mFrameCount = mStudioModelDefAnimation.Proxy.GetFrameCount();
+            }
+        }
+
+        public IPCProxy<IStudioModelDef> mStudioModelDef;        
+        public float mFrameRate;
+        public int mJointCount;
+        public Animation[] mAnims;
+        public Dictionary<string, Animation> mAnimMap = new Dictionary<string, Animation>();
+
+        ModelDef(IPCProxy<IStudioModelDef> studioModelDef)
+        {
+            mStudioModelDef = studioModelDef;
+
+            mFrameRate = mStudioModelDef.Proxy.GetFrameRate();// ModelDef_GetFrameRate(mNativeModelDef);
+            mJointCount = mStudioModelDef.Proxy.GetJointCount();
+            int animCount = mStudioModelDef.Proxy.GetAnimCount();
+            mAnims = new Animation[animCount];
+
+            for (int animIdx = 0; animIdx < animCount; animIdx++)
+            {
+                var objId = mStudioModelDef.Proxy.GetAnimation(animIdx);
+                var anim = new Animation(IPCProxy<IStudioModelDefAnimation>.Create(objId));
+                anim.mAnimIdx = animIdx;
+                mAnims[animIdx] = anim;
+                mAnimMap[anim.mName] = anim;
+            }
+        }
+
+        public static ModelDef LoadModel(string fileName)
+        {
+            var objId = BFApp.StudioHostProxy.Res_OpenFBX(fileName, VertexDef.sVertexDefinition.mStudioVertexDefinition);
+            IPCProxy<IStudioModelDef> studioModelDef = IPCProxy<IStudioModelDef>.Create(objId);
+            if (studioModelDef == null)
+                return null;
+            return new ModelDef(studioModelDef);
+        }
+
+        public ModelInstance CreateInstance()
+        {
+            var objId = mStudioModelDef.Proxy.CreateModelInstance();
+            IPCProxy<IStudioModelInstance> studioModelInstance = IPCProxy<IStudioModelInstance>.Create(objId);
+            if (studioModelInstance == null)
+                return null;
+            var modelInstance = new ModelInstance(studioModelInstance, this);
+            return modelInstance;
+        }
+
+        public Animation GetAnimation(string name)
+        {
+            return mAnimMap[name];
+        }
+    }
+
+    public class ModelInstance : RenderCmd
+    {        
+        public ModelDef mModelDef;
+        public ModelDef.Animation mAnim;
+        public float mFrame;
+        public float mAnimSpeed = 1.0f;
+        public bool mLoop;
+        public IPCProxy<IStudioModelInstance> mStudioModelInstance;
+
+        internal ModelInstance(IPCProxy<IStudioModelInstance> studioModelInstance, ModelDef modelDef)
+        {
+            mStudioModelInstance = studioModelInstance;
+            var objId = studioModelInstance.Proxy.GetAsRenderCmd();
+            mStudioRenderCmd = IPCProxy<IStudioRenderCmd>.Create(objId);
+            mModelDef = modelDef;
+        }
+
+        void RehupAnimState()
+        {
+            mStudioModelInstance.Proxy.RehupAnimState(mAnim.mAnimIdx, mFrame);            
+        }
+
+        public void Update()
+        {
+            if (mAnim == null)
+                return;
+
+            mFrame += mModelDef.mFrameRate * BFApp.sApp.UpdateDelta * mAnimSpeed;
+
+            /*if ((mFrame >= 35.0f) || (mFrame < 1.0f))
+                mFrame = 34.0f;*/
+
+            if (mAnim.mFrameCount > 1)
+            {
+                float endFrameNum = mAnim.mFrameCount - 1.0f;
+                while (mFrame >= endFrameNum)
+                {
+                    if (mLoop)
+                        mFrame -= endFrameNum;
+                    else
+                        mFrame = endFrameNum - 0.00001f;
+                }
+                while (mFrame < 0)
+                    mFrame += endFrameNum;
+            }
+
+            RehupAnimState();
+        }
+
+        public void Play(ModelDef.Animation anim, bool loop = false)
+        {
+            mLoop = loop;
+            mAnim = anim;
+            mFrame = 0;
+            RehupAnimState();
+        }
+
+        public void Play(string name, bool loop = false)
+        {
+            Play(mModelDef.GetAnimation(name), loop);
+        }
+
+        public void Play(bool loop = false)
+        {
+            Play(mModelDef.mAnims[0], loop);
+        }
+        
+        public void SetMeshVisibility(int meshIdx, bool visible)
+        {
+            mStudioModelInstance.Proxy.SetMeshVisibility(meshIdx, visible);
+        }
+    }
+#endif
+}

+ 14 - 0
BeefLibs/Beefy2D/src/gfx/PixelSnapping.bf

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Beefy.gfx
+{
+    public enum PixelSnapping
+    {
+        Never,
+        Always,
+        Auto,
+        Default
+    }
+}

+ 23 - 0
BeefLibs/Beefy2D/src/gfx/RenderCmd.bf

@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+#if STUDIO_CLIENT
+using Beefy.ipc;
+#endif
+
+namespace Beefy.gfx
+{
+#if !STUDIO_CLIENT
+    public class RenderCmd
+    {
+        public void* mNativeRenderCmd;
+    }
+#else
+    public class RenderCmd
+    {
+        public IPCProxy<IStudioRenderCmd> mStudioRenderCmd;
+    }
+#endif
+}

+ 175 - 0
BeefLibs/Beefy2D/src/gfx/RenderState.bf

@@ -0,0 +1,175 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+using Beefy;
+using Beefy.geom;
+
+namespace Beefy.gfx
+{
+    public enum DepthFunc
+    {
+        Never,
+        Less,
+        LessEqual,
+        Equal,
+        Greater,
+        NotEqual,
+        GreaterEqual,
+        Always
+    }
+
+#if !STUDIO_CLIENT
+    public class RenderState
+    {        
+        [StdCall, CLink]
+        static extern void* Gfx_CreateRenderState(void* srcNativeRenderState);
+
+        [StdCall, CLink]
+        static extern void RenderState_Delete(void* renderState);
+
+        [StdCall, CLink]
+        static extern void RenderState_SetClip(void* renderState, float x, float y, float width, float height);
+
+        [StdCall, CLink]
+        static extern void RenderState_DisableClip(void* renderState);
+
+        [StdCall, CLink]
+        static extern void RenderState_SetShader(void* nativeRenderState, void* nativeShader);
+
+        [StdCall, CLink]
+        static extern void RenderState_SetDepthFunc(void* nativeRenderState, int32 depthFunc);
+
+        [StdCall, CLink]
+        static extern void RenderState_SetDepthWrite(void* nativeRenderState, int32 depthWrite);
+
+        public void* mNativeRenderState;
+        public bool mIsFromDefaultRenderState;
+
+        internal this()
+        {
+
+        }
+
+		internal ~this()
+		{
+			RenderState_Delete(mNativeRenderState);
+		}
+
+        public static RenderState Create(RenderState srcRenderState = null)
+        {
+            void* nativeRenderState = Gfx_CreateRenderState((srcRenderState != null) ? srcRenderState.mNativeRenderState : null);
+            if (nativeRenderState == null)
+                return null;
+
+            RenderState renderState = new RenderState();
+            renderState.mNativeRenderState = nativeRenderState;
+            return renderState;
+        }
+
+        public Shader Shader
+        {            
+            set
+            {
+                RenderState_SetShader(mNativeRenderState, value.mNativeShader);
+            }
+        }
+
+        public DepthFunc DepthFunc
+        {
+            set
+            {
+                RenderState_SetDepthFunc(mNativeRenderState, (int32)value);
+            }
+        }
+
+        public bool DepthWrite
+        {
+            set
+            {
+                RenderState_SetDepthWrite(mNativeRenderState, value ? 1 : 0);
+            }
+        }
+
+        public Rect? ClipRect
+        {
+            set
+            {
+                if (value.HasValue)
+                {
+                    Rect rect = value.Value;
+                    RenderState_SetClip(mNativeRenderState, rect.mX, rect.mY, rect.mWidth, rect.mHeight);
+                }
+                else
+                    RenderState_DisableClip(mNativeRenderState);
+            }
+        }        
+    }
+#else
+    public class RenderState
+    {
+        public IPCProxy<IStudioRenderState> mStudioRenderState;
+        public bool mIsFromDefaultRenderState;
+ 
+        internal RenderState()
+        {
+
+        }
+
+        public static RenderState Create(RenderState srcRenderState = null)
+        {
+            /*if (nativeRenderState == IntPtr.Zero)
+                return null;*/
+
+            RenderState renderState = new RenderState();
+            var renderStateRef = new IPCStudioObjectRef<IStudioRenderState>();
+            if (srcRenderState != null)
+                renderStateRef = srcRenderState.mStudioRenderState;
+            var objId = BFApp.sApp.mStudioHost.Proxy.CreateRenderState(renderStateRef);
+            renderState.mStudioRenderState = IPCProxy<IStudioRenderState>.Create(objId);
+            return renderState;
+        }
+
+        public Shader Shader
+        {
+            set
+            {
+                mStudioRenderState.Proxy.SetShader(value.mStudioShader);                
+            }
+        }
+
+        public DepthFunc DepthFunc
+        {
+            set
+            {
+                mStudioRenderState.Proxy.SetDepthFunc((int)value); ;                
+            }
+        }
+
+        public bool DepthWrite
+        {
+            set
+            {
+                mStudioRenderState.Proxy.SetDepthWrite(value);                
+            }
+        }
+
+        public Rect? ClipRect
+        {
+            set
+            {
+                if (value.HasValue)
+                {
+                    Rect rect = value.Value;
+                    mStudioRenderState.Proxy.SetClipRect(rect.mX, rect.mY, rect.mWidth, rect.mHeight);                    
+                }
+                else
+                {
+                    mStudioRenderState.Proxy.DisableClipRect();                    
+                }
+            }
+        }
+    }
+#endif
+}

+ 83 - 0
BeefLibs/Beefy2D/src/gfx/Shader.bf

@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Runtime.InteropServices;
+
+namespace Beefy.gfx
+{
+    public class ShaderParam
+    {        
+        public void* mNativeShaderParam;
+
+        internal this(void* shaderParam)
+        {
+        }
+    }
+
+#if !STUDIO_CLIENT
+    public class Shader
+    {
+        public void* mNativeShader;
+        public Dictionary<String, ShaderParam> mShaderParamMap;
+
+        [StdCall, CLink]
+        static extern void* Gfx_LoadShader(char8* fileName, void* vertexDefinition);
+
+        [StdCall, CLink]
+        static extern void* Gfx_Shader_Delete(void* shader);
+
+        [StdCall, CLink]
+        static extern void* Gfx_GetShaderParam(void* shader, String paramName);
+
+        public static Shader CreateFromFile(String fileName, VertexDefinition vertexDefinition)
+        {
+            void* aNativeShader = Gfx_LoadShader(fileName, vertexDefinition.mNativeVertexDefinition);
+            if (aNativeShader == null)
+                return null;
+
+            Shader aShader = new Shader(aNativeShader);
+            return aShader;
+        }        
+
+        internal this(void* nativeShader)
+        {
+            mNativeShader = nativeShader;        
+        }
+
+        public ~this()
+        {
+            Gfx_Shader_Delete(mNativeShader);
+        }
+
+        ShaderParam GetParam(String paramName)
+        {
+            ShaderParam aShaderParam = null;
+            if (!mShaderParamMap.TryGetValue(paramName, out aShaderParam))
+            {
+                void* nativeShaderParam = Gfx_GetShaderParam(mNativeShader, paramName);
+                if (nativeShaderParam != null)
+                    aShaderParam = new ShaderParam(nativeShaderParam);
+                mShaderParamMap[paramName] = aShaderParam;
+            }
+            return aShaderParam;
+        }
+    }
+#else
+    public class Shader : IStudioShader
+    {
+        public IPCProxy<IStudioShader> mStudioShader;
+
+        public static Shader CreateFromFile(string fileName, VertexDefinition vertexDefinition)
+        {
+            Shader shader = new Shader();
+            IPCObjectId objId = BFApp.StudioHostProxy.CreateShaderFromFile(fileName, vertexDefinition.mStudioVertexDefinition);
+            shader.mStudioShader = IPCProxy<IStudioShader>.Create(objId);
+            return shader;
+        }
+
+        internal Shader()
+        {            
+        }
+    }
+#endif
+}

+ 21 - 0
BeefLibs/Beefy2D/src/gfx/TexCoords.bf

@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Beefy.gfx
+{
+    public struct TexCoords
+    {
+		[Reflect]
+        public float mU;
+		[Reflect]
+        public float mV;
+
+        public this(float u, float v)
+        {
+            mU = u;
+            mV = v;
+        }
+    }
+}

+ 29 - 0
BeefLibs/Beefy2D/src/gfx/Vertex3D.bf

@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using System.Runtime.InteropServices;
+
+namespace Beefy.gfx
+{
+    //[StructLayout(LayoutKind.Sequential)]
+    public struct Vertex3D
+    {
+        public float mX;
+        public float mY;
+        public float mZ;
+        public float mU;
+        public float mV;
+        public uint32 mColor;
+
+        public this(float x, float y, float z, float u, float v, uint32 color)
+        {
+            mX = x;
+            mY = y;
+            mZ = z;
+            mU = u;
+            mV = v;
+            mColor = color;
+        }
+    }
+}

+ 166 - 0
BeefLibs/Beefy2D/src/gfx/VertexDefinition.bf

@@ -0,0 +1,166 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using System.Runtime.InteropServices;
+using System.Reflection;
+using System.Diagnostics;
+
+namespace Beefy.gfx
+{
+    public class VertexDefinition
+    {
+        enum VertexElementFormat
+        {
+            Float,
+            Vector2,
+            Vector3,
+            Vector4,
+            Color,
+            Byte4,
+            Short2,
+            Short4,
+            NormalizedShort2,
+            NormalizedShort4,
+            HalfVector2,
+            HalfVector4
+        }
+
+		[CRepr]
+        struct VertexDefData
+        {
+            public VertexElementUsage mUsage;
+            public int32 mUsageIndex;
+            public VertexElementFormat mFormat;            
+        }
+
+#if !STUDIO_CLIENT
+        [StdCall, CLink]
+        static extern void* Gfx_CreateVertexDefinition(VertexDefData* elementData, int32 numElements);
+
+        [StdCall, CLink]
+        static extern void Gfx_VertexDefinition_Delete(void* nativeVertexDefinition);
+
+        public void* mNativeVertexDefinition;
+#else
+        public IPCProxy<IStudioVertexDefinition> mStudioVertexDefinition;
+#endif
+
+        public int32 mVertexSize;
+        public int32 mPosition2DOffset = -1;
+
+        public static void FindPrimitives(Type type, List<Type> primitives)
+        {
+			if ((type.IsPrimitive) /*|| (field.FieldType.IsArray)*/)
+			{
+			    primitives.Add(type);
+				return;
+			}
+
+			var typeInst = type as TypeInstance;
+			if (typeInst == null)
+				return;
+			
+            for (var field in typeInst.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
+            {
+                FindPrimitives(field.FieldType, primitives);
+            }
+        }
+
+#if !STUDIO_CLIENT
+        public static VertexDefinition CreateFromData(void* elementData, int32 numElements, int32 vertexSize)
+        {
+            var vertexDefinition = new VertexDefinition();
+            vertexDefinition.mNativeVertexDefinition = Gfx_CreateVertexDefinition((VertexDefData*)elementData, numElements);
+            vertexDefinition.mVertexSize = vertexSize;
+            return vertexDefinition;
+        }
+#endif
+
+        internal this()
+        {
+
+        }
+
+		public ~this()
+		{
+		    Gfx_VertexDefinition_Delete(mNativeVertexDefinition);
+		}
+
+        public this(Type type)
+        {
+			var typeInst = type as TypeInstance;
+            var vertexDefDataArray = scope VertexDefData[typeInst.FieldCount];
+
+            mVertexSize = typeInst.InstanceSize;
+            
+            List<Type> primitives = scope List<Type>(3);
+
+            int32 fieldIdx = 0;
+            for (var field in typeInst.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
+            {
+                var memberAttribute = field.GetCustomAttribute<VertexMemberAttribute>().Get();
+				
+                var vertexDefData = VertexDefData();
+                
+                vertexDefData.mUsage = memberAttribute.mElementUsage;
+                vertexDefData.mUsageIndex = memberAttribute.mUsageIndex;
+                
+                primitives.Clear();
+                FindPrimitives(field.FieldType, primitives);
+
+                int32 floats = 0;
+                int32 shorts = 0;
+                int32 colors = 0;
+
+                for (var prim in primitives)
+                {
+                    if (prim == typeof(float))
+                        floats++;
+                    else if (prim == typeof(uint16))
+                        shorts++;
+                    else if (prim == typeof(uint32))
+                        colors++;
+                }
+
+                if (memberAttribute.mElementUsage == VertexElementUsage.Position2D)
+                    mPosition2DOffset = field.MemberOffset;
+
+                if (floats != 0)
+                {
+                    Debug.Assert(floats == primitives.Count);
+                    Debug.Assert(floats <= 4);
+                    vertexDefData.mFormat = VertexElementFormat.Float + floats - 1;
+                }
+                else if (shorts != 0)
+                {
+                    if (shorts == 2)
+                        vertexDefData.mFormat = VertexElementFormat.Short2;
+                    else if (shorts == 4)
+                        vertexDefData.mFormat = VertexElementFormat.Short4;
+                    else
+                        Runtime.FatalError("Invalid short count");
+                }
+                else if (colors != 0)
+                {
+                    if (colors == 1)
+                        vertexDefData.mFormat = VertexElementFormat.Color;
+                    else
+                        Runtime.FatalError("Invalid color count");
+                }                
+
+                vertexDefDataArray[fieldIdx++] = vertexDefData;
+            }
+
+#if !STUDIO_CLIENT
+            mNativeVertexDefinition = Gfx_CreateVertexDefinition(vertexDefDataArray.CArray(), fieldIdx);
+#else
+            fixed (VertexDefData* vertexDefData = vertexDefDataArray)
+            {
+                IPCObjectId objId = BFApp.sApp.mStudioHost.Proxy.CreateVertexDefinition(Marshal.SizeOf(typeof(VertexDefData)) * vertexDefDataArray.Length, vertexDefData, vertexDefDataArray.Length, mVertexSize);
+                mStudioVertexDefinition = IPCProxy<IStudioVertexDefinition>.Create(objId);
+            }
+#endif
+        }
+    }
+}

+ 25 - 0
BeefLibs/Beefy2D/src/gfx/VertexElementUsage.bf

@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Beefy.gfx
+{
+    public enum VertexElementUsage
+    {
+        Position2D,
+        Position3D,
+        Color,
+        TextureCoordinate,
+        Normal,
+        Binormal,
+        Tangent,
+        BlendIndices,
+        BlendWeight,
+        Depth,
+        Fog,
+        PointSize,
+        Sample,
+        TessellateFactor
+    }
+}

+ 11 - 0
BeefLibs/Beefy2D/src/gfx/VertexLayout.bf

@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Beefy.gfx
+{
+    public class VertexLayout
+    {
+    }
+}

+ 20 - 0
BeefLibs/Beefy2D/src/gfx/VertexMemberAttribute.bf

@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Beefy.gfx
+{
+	[AttributeUsage(.Field, .ReflectAttribute, ReflectUser=.All)]
+    public struct VertexMemberAttribute : Attribute
+    {        
+        public VertexElementUsage mElementUsage;
+        public int32 mUsageIndex;
+
+        public this(VertexElementUsage elementUsage, int32 usageIndex = 0)
+        {
+            mElementUsage = elementUsage;
+            mUsageIndex = usageIndex;
+        }
+    }
+}

+ 206 - 0
BeefLibs/Beefy2D/src/res/PSDReader.bf

@@ -0,0 +1,206 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Runtime.InteropServices;
+using Beefy.gfx;
+
+namespace Beefy.res
+{
+#if !STUDIO_CLIENT
+    public class PSDLayer
+    {
+        [StdCall, CLink]
+        extern static void Res_PSDLayer_GetSize(void* layerInfo, out int32 x, out int32 y, out int32 width, out int32 height);
+        [StdCall, CLink]
+        extern static int32 Res_PSDLayer_GetLayerId(void* layerInfo);
+        [StdCall, CLink]
+        extern static char8* Res_PSDLayer_GetName(void* layerInfo);
+     	[StdCall, CLink]
+        extern static int32 Res_PSDLayer_IsVisible(void* layerInfo);
+
+        public void* mNativeLayerInfo;
+        public int32 mIdx;
+
+        public this(void* nativeLayerInfo, int32 idx)
+        {
+            mNativeLayerInfo = nativeLayerInfo;
+            mIdx = idx;
+        }
+
+        public void GetName(String str)
+        {
+            str.Append(Res_PSDLayer_GetName(mNativeLayerInfo));
+        }
+
+        public int32 GetLayerId()
+        {
+            return Res_PSDLayer_GetLayerId(mNativeLayerInfo);
+        }
+
+        public void GetSize(out int32 x, out int32 y, out int32 width, out int32 height)
+        {
+            Res_PSDLayer_GetSize(mNativeLayerInfo, out x, out y, out width, out height);
+        }
+
+        public bool IsVisible()
+        {
+            return Res_PSDLayer_IsVisible(mNativeLayerInfo) != 0;
+        }
+    }
+
+    public class PSDReader
+    {
+        [StdCall, CLink]
+        extern static void* Res_OpenPSD(String fileName);
+
+        [StdCall, CLink]
+        extern static void Res_DeletePSDReader(void* pSDReader);
+
+        [StdCall, CLink]
+        extern static int32 Res_PSD_GetLayerCount(void* pSDReader);
+
+        [StdCall, CLink]
+        extern static void* Res_PSD_GetLayerTexture(void* pSDReader, int32 layerIdx, out int32 xOfs, out int32 yOfs);
+
+        [StdCall, CLink]
+        extern static void* Res_PSD_GetMergedLayerTexture(void* pSDReader, void* layerIndices, int32 count, out int32 xOfs, out int32 yOfs);
+
+        [StdCall, CLink]
+        extern static void* Res_PSD_GetLayerInfo(void* pSDReader, int32 layerIdx);
+        
+        void* mNativePSDReader;
+
+        protected this()
+        {            
+        }
+
+        public int32 GetLayerCount()
+        {
+            return Res_PSD_GetLayerCount(mNativePSDReader);
+        }
+
+        public PSDLayer GetLayer(int32 layerIdx)
+        {
+            return new PSDLayer(Res_PSD_GetLayerInfo(mNativePSDReader, layerIdx), layerIdx);
+        }
+
+        public static PSDReader OpenFile(String fileName)
+        {
+            void* nativePSDReader = Res_OpenPSD(fileName);
+            if (nativePSDReader == null)
+                return null;
+            PSDReader aPSDReader = new PSDReader();
+            aPSDReader.mNativePSDReader = nativePSDReader;
+            return aPSDReader;
+        }
+
+        public Image LoadLayerImage(int32 layerIdx)
+        {
+            int32 aXOfs = 0;
+            int32 aYOfs = 0;
+            void* texture = Res_PSD_GetLayerTexture(mNativePSDReader, layerIdx, out aXOfs, out aYOfs);
+            if (texture == null)
+                return null;
+            Image image = Image.CreateFromNativeTextureSegment(texture);
+            image.mX = aXOfs;
+            image.mY = aYOfs;
+            return image;
+        }
+
+        public Image LoadMergedLayerImage(int32 [] layerIndices)
+        {
+            int32 aXOfs = 0;
+            int32 aYOfs = 0;
+            void* texture = Res_PSD_GetMergedLayerTexture(mNativePSDReader, layerIndices.CArray(), (int32)layerIndices.Count, out aXOfs, out aYOfs);
+
+            if (texture == null)
+                return null;
+            Image image = Image.CreateFromNativeTextureSegment(texture);
+            image.mX = aXOfs;
+            image.mY = aYOfs;            
+            return image;
+        }
+
+        public void Dipose()
+        {
+            if (mNativePSDReader != null)
+            {
+                Res_DeletePSDReader(mNativePSDReader);
+                mNativePSDReader = null;
+            }
+        }
+    }
+#else
+    public class PSDLayer
+    {        
+        public int mIdx;
+
+        public PSDLayer(int nativeLayerInfo, int idx)
+        {
+            //mNativeLayerInfo = nativeLayerInfo;
+            mIdx = idx;
+        }
+
+        public string GetName()
+        {
+            return null;
+        }
+
+        public int GetLayerId()
+        {
+            return 0;
+        }
+
+        public void GetSize(out int x, out int y, out int width, out int height)
+        {
+            x = 0;
+            y = 0;
+            width = 0;
+            height = 0;
+            return;
+        }
+
+        public bool IsVisible()
+        {
+            return false;
+        }
+    }
+
+    public class PSDReader
+    {        
+        protected PSDReader()
+        {            
+        }
+
+        public int GetLayerCount()
+        {
+            return 0;
+        }
+
+        public PSDLayer GetLayer(int layerIdx)
+        {
+            return null;
+        }
+
+        public static PSDReader OpenFile(string fileName)
+        {
+            return null;
+        }
+
+        public Image LoadLayerImage(int layerIdx)
+        {            
+            return null;
+        }
+
+        public Image LoadMergedLayerImage(int [] layerIndices)
+        {
+            return null;
+        }
+
+        public void Dipose()
+        {
+            //TODO: Dispose
+        }
+    }
+#endif
+}

+ 184 - 0
BeefLibs/Beefy2D/src/res/ResourceManager.bf

@@ -0,0 +1,184 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Reflection;
+using Beefy.utils;
+using Beefy.gfx;
+using System.Runtime.InteropServices;
+
+namespace Beefy.res
+{    
+    public class ResInfo
+    {        
+        public String mName ~ delete _;
+        public String mFilePath ~ delete _;
+        public Guid mResId;
+
+        public Object mLoadedObject;
+    }
+    
+    public class ResImageInfo : ResInfo
+    {
+
+    }
+
+    public class ResGroup
+    {
+        public List<ResInfo> mResInfoList = new List<ResInfo>() ~ DeleteContainerAndItems!(_);
+    }
+
+    public class ResourceManager
+    {
+        /*[StdCall, CLink]
+        static extern void Wwise_Shutdown();*/
+
+        public Dictionary<String, ResGroup> mResGroupMap = new Dictionary<String, ResGroup>() ~ delete _;
+        public Dictionary<Guid, ResInfo> mIdToResInfoMap = new Dictionary<Guid, ResInfo>() ~ delete _;
+
+        public ~this()
+        {
+            //Wwise_Shutdown();
+        }
+
+        public void ParseConfigData(StructuredData data)
+        {
+			ThrowUnimplemented();
+
+            /*int fileResVer = data.GetInt("ResVer");
+            using (data.Open("Groups"))
+            {                
+                for (int groupIdx = 0; groupIdx < data.Count; groupIdx++)
+                {
+                    using (data.Open(groupIdx))
+                    {
+                        ResGroup resGroup = new ResGroup();
+                        mResGroupMap[data.GetString("Name")] = resGroup;
+
+                        using (data.Open("Resources"))
+                        {
+                            for (int resIdx = 0; resIdx < data.Count; resIdx++)
+                            {
+                                using (data.Open(resIdx))
+                                {
+                                    ResInfo resInfo = null;
+
+                                    string aType = data.GetString("Type");
+
+                                    if (aType == "Image")
+                                    {
+                                        resInfo = new ResImageInfo();
+                                    }
+
+                                    resInfo.mName = data.GetString("Name");
+                                    resInfo.mResId = Guid.Parse(data.GetString("Id"));
+                                    resInfo.mFilePath = data.GetString("Path");
+                                    resGroup.mResInfoList.Add(resInfo);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            Type appType = BFApp.sApp.GetType();
+            Type resourcesType = appType.Assembly.GetType(appType.Namespace + ".Res");
+			if (resourcesType == null)
+				return;
+            Type soundBanksTypes = resourcesType.GetNestedType("SoundBanks");
+			if (soundBanksTypes != null)
+			{			
+				using (data.Open("SoundBanks"))
+				{
+					for (int soundBanksIdx = 0; soundBanksIdx < data.Count; soundBanksIdx++)
+					{
+						string name = data.Keys[soundBanksIdx];
+						uint wwiseId = data.GetUInt(soundBanksIdx);
+
+						SoundBank soundBank = new SoundBank();
+						soundBank.mName = name;
+						soundBank.mWwiseSoundId = wwiseId;
+
+						FieldInfo fieldInfo = soundBanksTypes.GetField(name);
+						fieldInfo.SetValue(null, soundBank);
+					}
+				}
+			}
+
+            Type soundEventsTypes = resourcesType.GetNestedType("SoundEvents");
+			if (soundEventsTypes != null)
+			{
+				using (data.Open("SoundEvents"))
+				{
+					for (int soundBanksIdx = 0; soundBanksIdx < data.Count; soundBanksIdx++)
+					{
+						string name = data.Keys[soundBanksIdx];
+						uint wwiseId = data.GetUInt(soundBanksIdx);
+
+						SoundEvent soundRes = new SoundEvent();
+						soundRes.mWwiseEventId = wwiseId;
+
+						FieldInfo fieldInfo = soundEventsTypes.GetField(name);
+						fieldInfo.SetValue(null, soundRes);
+					}
+				}
+			}
+
+            Type soundParametersTypes = resourcesType.GetNestedType("SoundParameters");
+			if (soundParametersTypes != null)
+			{
+				using (data.Open("SoundParameters"))
+				{
+					for (int soundBanksIdx = 0; soundBanksIdx < data.Count; soundBanksIdx++)
+					{
+						string name = data.Keys[soundBanksIdx];
+						uint wwiseId = data.GetUInt(soundBanksIdx);
+
+						SoundParameter soundParameter = new SoundParameter();
+						soundParameter.mWwiseParamId = wwiseId;
+
+						FieldInfo fieldInfo = soundParametersTypes.GetField(name);
+						fieldInfo.SetValue(null, soundParameter);
+					}
+				}
+			}*/
+        }
+
+        public bool LoadResGroup(String groupName)
+        {
+			ThrowUnimplemented();
+
+            /*Type appType = BFApp.sApp.GetType();
+            Type resourcesType = appType.Assembly.GetType(appType.Namespace + ".Res");
+            Type imagesType = resourcesType.GetNestedType("Images");
+
+            ResGroup resGroup = mResGroupMap[groupName];
+            foreach (ResInfo resInfo in resGroup.mResInfoList)
+            {
+                if (resInfo is ResImageInfo)
+                {
+                    string fileName = resInfo.mFilePath;
+                    if (!fileName.Contains(':'))
+                        fileName = BFApp.sApp.mInstallDir + resInfo.mFilePath;
+                    Image image = Image.LoadFromFile(fileName, false);
+                    FieldInfo fieldInfo = imagesType.GetField(resInfo.mName);
+                    fieldInfo.SetValue(null, image);
+                }
+            }
+
+            return true;*/
+        }
+
+        public Object GetResourceById(Guid id, bool allowLoad = false)
+        {
+			ThrowUnimplemented();
+
+            /*ResInfo resInfo = mIdToResInfoMap[id];
+            if ((resInfo.mLoadedObject == null) && (allowLoad))
+            {
+                if (resInfo is ResImageInfo)
+                    resInfo.mLoadedObject = Image.LoadFromFile(resInfo.mFilePath, false);
+            }            
+            return resInfo.mLoadedObject;*/
+        }        
+    }
+}

+ 22 - 0
BeefLibs/Beefy2D/src/res/SoundBank.bf

@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Runtime.InteropServices;
+using Beefy;
+
+namespace Beefy.res
+{
+    /*public class SoundBank
+    {
+        [StdCall, CLink]
+        static extern void Wwise_LoadBankByName(String name);
+
+        public String mName;
+        public uint32 mWwiseSoundId;
+
+        public void Load()
+        {
+            Wwise_LoadBankByName(mName);
+        }
+    }*/
+}

+ 24 - 0
BeefLibs/Beefy2D/src/res/SoundEvent.bf

@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Runtime.InteropServices;
+using Beefy;
+
+namespace Beefy.res
+{
+    /*public class SoundEvent
+    {
+        [StdCall, CLink]
+        static extern void Wwise_SendEvent(uint32 eventId, void* objectId);
+
+        public uint32 mWwiseEventId;
+
+        public void Post(SoundGameObject soundGameObject = null)
+        {
+            if (soundGameObject == null)
+                Wwise_SendEvent(mWwiseEventId, null);
+            else if (soundGameObject == null)
+                Wwise_SendEvent(mWwiseEventId, soundGameObject.mWwiseObject);
+        }
+    }*/
+}

+ 11 - 0
BeefLibs/Beefy2D/src/res/SoundGameObject.bf

@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Beefy.res
+{
+    public class SoundGameObject
+    {
+        public void* mWwiseObject;
+    }
+}

+ 21 - 0
BeefLibs/Beefy2D/src/res/SoundParameter.bf

@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Runtime.InteropServices;
+using Beefy;
+
+namespace Beefy.res
+{
+    /*public class SoundParameter
+    {
+        [StdCall, CLink]
+        static extern int32 Wwise_SetRTPCValue(uint32 paramId, float value, void* soundGameObject);
+
+        public uint32 mWwiseParamId;
+
+        public bool Set(float value, SoundGameObject soundGameObject = null)
+        {
+            return Wwise_SetRTPCValue(mWwiseParamId, value, (soundGameObject == null) ? null : soundGameObject.mWwiseObject) != 0;
+        }
+    }*/
+}

+ 11 - 0
BeefLibs/Beefy2D/src/sys/SysBitmap.bf

@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Beefy.sys
+{
+    public class SysBitmap
+    {
+        public void* mNativeBFBitmap;
+    }
+}

+ 150 - 0
BeefLibs/Beefy2D/src/sys/SysMenu.bf

@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.widgets;
+using Beefy;
+using System.Runtime.InteropServices;
+
+namespace Beefy.sys
+{    
+    public class SysMenu : IMenu
+    {        
+        public String mText ~ delete _;
+		public String mHotKey ~ delete _;
+		public SysBitmap mBitmap;
+		public bool mEnabled;
+		public int32 mCheckState;
+		public bool mRadioCheck;
+
+        public SysMenu mParent;
+        public BFWindow mWindow;
+        public void* mNativeBFMenu;
+        public Event<MenuItemSelectedHandler> mOnMenuItemSelected ~ _.Dispose();
+        public Event<MenuItemUpdateHandler> mOnMenuItemUpdate ~ _.Dispose();
+        public List<SysMenu> mChildren ~ DeleteContainerAndItems!(_);
+
+        internal this()
+        {
+        }
+
+		public int ChildCount
+		{
+			get
+			{
+				return (mChildren != null) ? mChildren.Count : 0;
+			}
+		}
+
+        public virtual SysMenu AddMenuItem(String text, String hotKey = null, MenuItemSelectedHandler menuItemSelectedHandler = null, MenuItemUpdateHandler menuItemUpdateHandler = null,
+            SysBitmap bitmap = null, bool enabled = true, int32 checkState = -1, bool radioCheck = false)
+        {            
+            if (mChildren == null)
+                mChildren = new List<SysMenu>();
+
+            SysMenu sysMenu = new SysMenu();
+			if (text != null)
+            	sysMenu.mText = new String(text);
+			if (hotKey != null)
+				sysMenu.mHotKey = new String(hotKey);
+			sysMenu.mBitmap = bitmap;
+			sysMenu.mEnabled = enabled;
+			sysMenu.mCheckState = checkState;
+			sysMenu.mRadioCheck = radioCheck;
+			if (menuItemSelectedHandler != null)
+            	sysMenu.mOnMenuItemSelected.Add(menuItemSelectedHandler);
+			if (menuItemUpdateHandler != null)
+            	sysMenu.mOnMenuItemUpdate.Add(menuItemUpdateHandler);
+            sysMenu.mNativeBFMenu = mWindow.AddMenuItem(mNativeBFMenu, mChildren.Count, text, hotKey, (bitmap != null) ? bitmap.mNativeBFBitmap : null, enabled, checkState, radioCheck);
+            sysMenu.mParent = this;
+            sysMenu.mWindow = mWindow;
+
+            mWindow.mSysMenuMap[(int)sysMenu.mNativeBFMenu ] = sysMenu;
+            mChildren.Add(sysMenu);
+
+            return sysMenu;
+        }
+
+		public void SetDisabled(bool disabled)
+		{
+			mEnabled = !disabled;
+			Modify(mText, mHotKey, mBitmap, mEnabled, mCheckState, mRadioCheck);
+		}
+
+		public void SetHotKey(StringView hotKey)
+		{
+			if (hotKey.IsNull)
+			{
+				DeleteAndNullify!(mHotKey);
+			}
+			else
+			{
+				if (mHotKey == null)
+					mHotKey = new String(hotKey);
+				else
+					mHotKey.Set(hotKey);
+			}
+			UpdateChanges();
+		}
+
+		public void UpdateChanges()
+		{
+			mWindow.ModifyMenuItem(mNativeBFMenu, mText, mHotKey, (mBitmap != null) ? mBitmap.mNativeBFBitmap : null, mEnabled, mCheckState, mRadioCheck);
+		}
+
+        public virtual void Modify(String text, String hotKey = null, SysBitmap bitmap = null, bool enabled = true, int32 checkState = -1, bool radioCheck = false)
+        {
+			if ((Object)mText != text)
+			{
+				if (text != null)
+				{
+					if (mText == null)
+						mText = new String(text);
+					else
+						mText.Set(text);
+				}
+				else
+					DeleteAndNullify!(mText);
+			}
+
+			if ((Object)mHotKey != hotKey)
+			{
+				if (hotKey != null)
+				{
+					if (mHotKey == null)
+						mHotKey = new String(hotKey);
+					else
+						mHotKey.Set(hotKey);
+				}
+				else
+					DeleteAndNullify!(mHotKey);
+			}
+			mBitmap = bitmap;
+			mEnabled = enabled;
+			mCheckState = checkState;
+			mRadioCheck = radioCheck;
+
+            UpdateChanges();
+        }
+
+        public virtual void Dispose()
+        {
+            mWindow.mSysMenuMap.Remove((int)mNativeBFMenu);
+            mWindow.DeleteMenuItem(mNativeBFMenu);
+        }
+
+		public void UpdateChildItems()
+		{
+			if (mChildren != null)
+			{
+				for (SysMenu child in mChildren)
+					child.mOnMenuItemUpdate(child);
+			}
+		}
+
+        public virtual void Selected()
+        {
+            mOnMenuItemSelected(this);
+            UpdateChildItems();
+        }
+    }
+}

+ 99 - 0
BeefLibs/Beefy2D/src/theme/ThemeFactory.bf

@@ -0,0 +1,99 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.widgets;
+using Beefy.gfx;
+
+namespace Beefy.theme
+{
+    public class DesignToolboxEntry
+    {
+        public String mGroupName;
+        public String mName;
+        public String mSortName;
+        public Image mIcon;
+        public Type mType;
+
+        public this(String name, Type theType, Image icon = null)
+        {
+            int32 index = (int32)name.IndexOf('@');
+            if (index != -1)
+            {
+                mName = new String(index);
+                mName.Append(name, 0, index);
+				mSortName = new String();
+                mSortName.Append(name, index + 1);
+            }
+            else
+            {
+                mName = new String(name);
+                mSortName = new String(name);
+            }
+            mType = theType;
+            mIcon = icon;
+        }
+    }
+
+    public class ThemeFactory
+    {
+        public static ThemeFactory mDefault;
+
+        public virtual void Init()
+        {
+        }
+
+		public virtual void Update()
+		{
+		}
+
+        public virtual ButtonWidget CreateButton(Widget parent, String caption, float x, float y, float width, float height)
+        {
+            return null;
+        }
+
+        public virtual CheckBox CreateCheckbox(Widget parent, float x = 0, float y = 0, float width = 0, float height = 0)
+        {
+            return null;
+        }
+
+        public virtual EditWidget CreateEditWidget(Widget parent, float x = 0, float y = 0, float width = 0, float height = 0)
+        {
+            return null;
+        }
+
+        public virtual TabbedView CreateTabbedView(TabbedView.SharedData sharedData, Widget parent = null, float x = 0, float y = 0, float width = 0, float height = 0)
+        {
+            return null;
+        }
+
+        public virtual DockingFrame CreateDockingFrame(DockingFrame parent = null)
+        {
+            return null;
+        }
+
+        public virtual ListView CreateListView()
+        {
+            return null;
+        }
+
+        public virtual Scrollbar CreateScrollbar(Scrollbar.Orientation orientation)
+        {
+            return null;
+        }
+       
+        public virtual InfiniteScrollbar CreateInfiniteScrollbar()
+        {
+            return null;
+        }
+
+        public virtual MenuWidget CreateMenuWidget(Menu menu)
+        {
+            return null;
+        }
+
+        public virtual Dialog CreateDialog(String title = null, String text = null, Image icon = null)
+        {
+            return null;
+        }
+    }
+}

+ 114 - 0
BeefLibs/Beefy2D/src/theme/dark/DarkButton.bf

@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.widgets;
+using Beefy.gfx;
+using Beefy.utils;
+
+namespace Beefy.theme.dark
+{
+    public class DarkButton : ButtonWidget, IHotKeyHandler
+    {
+        public String mLabel ~ delete _;
+		public float mDrawDownPct;
+		public float mLabelYOfs;
+
+        [DesignEditable(DefaultEditString=true)]
+		public String Label
+		{
+			get
+			{
+				return mLabel;
+			}
+
+			set
+			{
+				String.NewOrSet!(mLabel, value);
+			}
+		}
+
+		bool IHotKeyHandler.Handle(KeyCode keyCode)
+		{
+			if (mDisabled)
+				return false;
+
+			if (DarkTheme.CheckUnderlineKeyCode(mLabel, keyCode))
+			{
+				mDrawDownPct = 1.0f;
+				MouseClicked(0, 0, 3);
+				return true;
+			}
+			return false;
+		}
+
+        public override void DefaultDesignInit()
+        {
+            base.DefaultDesignInit();
+            mIdStr = "Button";
+            Label = "Button";
+            mWidth = GS!(80);
+            mHeight = GS!(20);
+        }
+
+        public override void Draw(Graphics g)
+        {
+            base.Draw(g);
+
+			bool drawDown = ((mMouseDown && mMouseOver) || (mMouseFlags.HasFlag(MouseFlag.Kbd)));
+			if (mDrawDownPct > 0)
+				drawDown = true;
+
+            Image texture = drawDown ? DarkTheme.sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.BtnDown] :
+                mMouseOver ? DarkTheme.sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.BtnOver] :
+                DarkTheme.sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.BtnUp];
+
+            if (mDisabled)
+                texture = DarkTheme.sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.BtnUp];
+
+            g.DrawBox(texture, 0, 0, mWidth, mHeight);
+
+            if ((mHasFocus) && (!mDisabled))
+            {
+                using (g.PushColor(DarkTheme.COLOR_SELECTED_OUTLINE))
+                    g.DrawBox(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.Outline), 0, 0, mWidth, mHeight);
+            }
+
+            g.SetFont(DarkTheme.sDarkTheme.mSmallFont);
+            if (mLabel != null)
+            {
+                using (g.PushColor(mDisabled ? 0x80FFFFFF : Color.White))
+                {
+					DarkTheme.DrawUnderlined(g, mLabel, GS!(2), (mHeight - GS!(20)) / 2 + mLabelYOfs, .Centered, mWidth - GS!(4), .Truncate);
+                }
+            }
+        }
+
+		public override void Update()
+		{
+			base.Update();
+			if (mDrawDownPct > 0)
+			{
+				mDrawDownPct = Math.Max(mDrawDownPct - 0.25f, 0);
+				MarkDirty();
+			}
+		}
+
+        public override void Serialize(StructuredData data)
+        {
+            base.Serialize(data);
+            data.Add("Label", mLabel);
+        }
+
+        public override bool Deserialize(StructuredData data)
+        {
+            base.Deserialize(data);
+            data.GetString(mLabel, "Label");
+            return true;
+        }
+
+		public override void GotFocus()
+		{
+			base.GotFocus();
+		}
+    }
+}

+ 176 - 0
BeefLibs/Beefy2D/src/theme/dark/DarkCheckBox.bf

@@ -0,0 +1,176 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.widgets;
+using Beefy.gfx;
+
+namespace Beefy.theme.dark
+{
+    public class DarkCheckBox : CheckBox, IHotKeyHandler
+    {
+        public bool mLargeFormat;
+        public Image mCheckIcon;
+		public Image mIndeterminateIcon;
+        public String mLabel ~ delete _;
+        public Font mFont;
+		public bool mDisabled;
+		public State mState;
+
+		public override bool Checked
+		{
+			get
+			{
+				return mState != .Unchecked;
+			}
+
+			set
+			{
+				mState = value ? .Checked : .Unchecked;
+			}
+		}
+
+		public override State State
+		{
+			get
+			{
+				return mState;
+			}
+
+			set
+			{
+				mState = value;
+			}
+		}
+
+		public String Label
+		{
+			get
+			{
+				return mLabel;
+			}
+
+			set
+			{
+				String.NewOrSet!(mLabel, value);
+			}
+		}
+
+        public this()
+        {
+            mMouseInsets = new Insets(2, 2, 3, 3);
+            mCheckIcon = DarkTheme.sDarkTheme.GetImage(.Check);
+			mIndeterminateIcon = DarkTheme.sDarkTheme.GetImage(.CheckIndeterminate);
+            mFont = DarkTheme.sDarkTheme.mSmallFont;
+        }
+
+		bool IHotKeyHandler.Handle(KeyCode keyCode)
+		{
+			if (mDisabled)
+				return false;
+
+			if (DarkTheme.CheckUnderlineKeyCode(mLabel, keyCode))
+			{
+				//SetFocus();
+				Checked = !Checked;
+				return true;
+			}
+			return false;
+		}
+
+        public override void DefaultDesignInit()
+        {
+            base.DefaultDesignInit();
+            mIdStr = "CheckBox";
+            mWidth = 20;
+            mHeight = 20;
+        }
+
+        public override void Draw(Graphics g)
+        {
+            base.Draw(g);
+
+            if (mLargeFormat)
+            {
+                g.Draw(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.CheckboxLarge));
+            }
+            else
+            {
+                if (mMouseOver)
+                {
+                    if (mMouseDown)
+                        g.Draw(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.CheckboxDown));
+                    else
+                        g.Draw(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.CheckboxOver));
+                }
+                else
+                    g.Draw(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.Checkbox));
+            }
+
+            if (mState == .Checked)
+                g.Draw(mCheckIcon);
+			else if (mState == .Indeterminate)
+				g.Draw(mIndeterminateIcon);
+
+			if (mHasFocus)
+			{
+			    using (g.PushColor(DarkTheme.COLOR_SELECTED_OUTLINE))
+			        g.DrawButton(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.Outline), 0, 0, mWidth);
+			}
+
+            if (mLabel != null)
+            {
+				g.SetFont(mFont);
+
+				DarkTheme.DrawUnderlined(g, mLabel, GS!(22), 0);
+
+				/*int underlinePos = mLabel.IndexOf('&');
+				if ((underlinePos != -1) && (underlinePos < mLabel.Length - 1))
+				{
+					String label = scope String();
+					label.Append(mLabel, 0, underlinePos);
+					float underlineX = mFont.GetWidth(label);
+
+					char32 underlineC = mLabel.GetChar32(underlinePos + 1).0;
+					float underlineWidth = mFont.GetWidth(underlineC);
+
+					label.Append(mLabel, underlinePos + 1);
+					g.DrawString(label, GS!(22), 0);
+
+					g.FillRect(GS!(22) + underlineX, mFont.GetAscent() + GS!(1), underlineWidth, (int)GS!(1.2f));
+				}
+				else
+				{
+	                g.DrawString(mLabel, GS!(22), 0);
+				}*/
+            }
+        }
+
+		public override void DrawAll(Graphics g)
+		{
+			if (mDisabled)
+			{
+				using (g.PushColor(0x80FFFFFF))
+					base.DrawAll(g);
+			}
+			else
+				base.DrawAll(g);
+		}
+
+        public float CalcWidth()
+        {
+            if (mLabel == null)
+                return 20;
+            return mFont.GetWidth(mLabel) + GS!(22);
+        }
+
+		public override void KeyChar(char32 theChar)
+		{
+			if (mDisabled)
+				return;
+
+			base.KeyChar(theChar);
+			if (theChar == ' ')
+				Checked = !Checked;
+		}
+    }
+}

+ 375 - 0
BeefLibs/Beefy2D/src/theme/dark/DarkComboBox.bf

@@ -0,0 +1,375 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.widgets;
+using Beefy.gfx;
+using Beefy.events;
+
+namespace Beefy.theme.dark
+{
+    public class DarkComboBox : ComboBox
+    {
+		public class CBMenuWidget : DarkMenuWidget
+		{
+			public this(Menu menu) :
+			    base(menu)
+			{
+			}
+
+			public override float GetReverseAdjust()
+			{
+				return GS!(-10);
+			}
+		}
+
+        /*public string Label
+        {
+            get
+            {
+                if (mEditWidget == null)
+                    return mLabel;
+                else
+                    return mEditWidget.Text;
+            }
+
+            set
+            {
+                if (mEditWidget == null)
+                    mLabel = value;
+                else
+                    mEditWidget.Text = value;
+            }
+        }*/
+
+        String mLabel ~ delete _;
+        public float mLabelX = GS!(8);
+        public FontAlign mLabelAlign = FontAlign.Centered;
+        public bool mFrameless = false;
+
+        public Event<Action<Menu>> mPopulateMenuAction ~ _.Dispose();
+        public CBMenuWidget mCurMenuWidget;
+        bool mJustClosed;
+        public uint32 mBkgColor;
+        public DarkEditWidget mEditWidget;
+		bool mAllowReverseDropdown; // Allow popdown to "popup" if there isn't enough space
+		public Widget mPrevFocusWidget;
+		public bool mFocusDropdown = true;
+		
+		virtual public StringView Label 
+        { 
+            get
+			{
+				if (mEditWidget != null)
+				{
+					if (mLabel == null)
+						mLabel = new String();
+					mLabel.Clear();
+					mEditWidget.GetText(mLabel);
+				}
+				return mLabel;
+			}
+
+            set
+			{
+				String.NewOrSet!(mLabel, value);
+				if (mEditWidget != null)
+					mEditWidget.SetText(mLabel);
+			} 
+        }
+
+        public override void Draw(Graphics g)
+        {
+            base.Draw(g);
+            
+            float yOfs = 0;
+            if (mEditWidget != null)
+            {
+                g.DrawBox(DarkTheme.sDarkTheme.GetImage(.EditBox), 0, 0, mWidth, mHeight);
+                g.Draw(DarkTheme.sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.ComboEnd], mWidth - GS!(25), (mHeight - GS!(22)) / 2 + yOfs);
+
+                if ((mHasFocus) || (mEditWidget.mHasFocus))
+                {
+                    using (g.PushColor(DarkTheme.COLOR_SELECTED_OUTLINE))
+                        g.DrawBox(DarkTheme.sDarkTheme.GetImage(.Outline), 0, 0, mWidth, mHeight);
+                }
+
+                return;
+            }
+
+            if (!mFrameless)
+            {
+                Image texture = DarkTheme.sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.ComboBox];
+                g.DrawBox(texture, 0, -2, mWidth, mHeight);
+            }
+            else 
+            {
+                if (mBkgColor != 0)
+                {
+                    using (g.PushColor(mBkgColor))
+                        g.FillRect(0, 0, mWidth, mHeight);
+                }
+                yOfs = 2.0f;
+            }
+
+            if (mEditWidget == null)
+            {
+                using (g.PushColor(mDisabled ? 0x80FFFFFF : Color.White))
+                {
+                    g.Draw(DarkTheme.sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.ComboEnd], mWidth - GS!(25), (mHeight - GS!(24)) / 2 + yOfs);
+
+                    g.SetFont(DarkTheme.sDarkTheme.mSmallFont);
+					String label = scope String();
+					GetLabel(label);
+                    if (label != null)
+                    {
+						float fontHeight = g.mFont.GetHeight();
+
+                        //g.DrawString(label, mLabelX, (mHeight - GS!(24)) / 2, mLabelAlign, mWidth - mLabelX - GS!(24), FontOverflowMode.Ellipsis);
+						g.DrawString(label, mLabelX, (mHeight - fontHeight) / 2 - GS!(2) - 1, mLabelAlign, mWidth - mLabelX - GS!(24), FontOverflowMode.Ellipsis);
+                    }
+                }
+
+				if (mHasFocus)
+				{
+				    using (g.PushColor(DarkTheme.COLOR_SELECTED_OUTLINE))
+				        g.DrawBox(DarkTheme.sDarkTheme.GetImage(.Outline), GS!(2), 0, mWidth - GS!(4), mHeight - GS!(4));
+				}
+            }            
+        }
+
+		public void GetLabel(String label)
+		{
+			if (mEditWidget == null)
+			{
+				if (mLabel != null)
+                	label.Append(mLabel);
+			}
+			else
+			    mEditWidget.GetText(label);
+		}
+
+        void HandleKeyDown(KeyDownEvent evt)
+        {
+            if (evt.mKeyCode == KeyCode.Escape)
+            {
+                evt.mHandled = true;
+                mCurMenuWidget.Close();
+            }
+        }
+
+        public virtual void MenuClosed()
+        {
+			if (mPrevFocusWidget != null)
+			{
+				mPrevFocusWidget.SetFocus();
+			}
+        }
+
+        void HandleClose(Menu menu, Menu selectedItem)
+        {
+            mCurMenuWidget = null;
+            mJustClosed = true;
+            //mWidgetWindow.mWindowKeyDownDelegate -= HandleKeyDown;
+            MenuClosed();
+        }
+
+        public virtual MenuWidget ShowDropdown()
+        {
+			mPrevFocusWidget = mWidgetWindow.mFocusWidget;
+
+            float popupXOfs = GS!(5);
+            float popupYOfs = GS!(-2);
+            if (mEditWidget != null)
+            {
+                popupXOfs = GS!(2);
+                popupYOfs = GS!(2);
+            }
+            else if (mFrameless)
+            {
+                popupXOfs = GS!(2);
+                popupYOfs = GS!(2);
+            }
+
+			//if (mCurMenuWidget != null)
+				//mCurMenuWidget.Close();
+
+            mAutoFocus = false;
+
+            Menu aMenu = new Menu();
+            mPopulateMenuAction(aMenu);
+
+            WidgetWindow menuWindow = null;
+            if (mCurMenuWidget != null)
+                menuWindow = mCurMenuWidget.mWidgetWindow;
+
+            let menuWidget = new CBMenuWidget(aMenu);
+            //menuWidget.SetShowPct(0.0f);
+            //menuWidget.mWindowFlags &= ~(BFWindow.Flags.PopupPosition);
+            mCurMenuWidget = menuWidget;
+            aMenu.mOnMenuClosed.Add(new => HandleClose);
+            menuWidget.mMinContainerWidth = mWidth + GS!(8);
+            menuWidget.mMaxContainerWidth = Math.Max(menuWidget.mMinContainerWidth, 1536);
+			menuWidget.mWindowFlags = .ClientSized | .DestAlpha | .FakeFocus;
+			if (!mFocusDropdown)
+				menuWidget.mWindowFlags |= .NoActivate;
+			menuWidget.CalcSize();
+
+			float popupY = mHeight + popupYOfs;
+			//TODO: Autocomplete didn't work on this: BFApp.sApp.GetW...
+            menuWidget.Init(this, popupXOfs, popupY, true, menuWindow);
+			// Why were we capturing?
+			mWidgetWindow.TransferMouse(menuWidget.mWidgetWindow);
+            if (menuWindow == null)
+                menuWidget.SetShowPct(0.0f);
+			return menuWidget;
+        }
+
+        public override void MouseDown(float x, float y, int32 btn, int32 btnCount)
+        {
+            base.MouseDown(x, y, btn, btnCount);
+
+            if (mDisabled)
+                return;
+
+            if ((mCurMenuWidget == null) && (!mJustClosed))
+            {
+				if (mEditWidget != null)
+					SetFocus();
+                ShowDropdown();
+
+				//mCurMenuWidget.SetFocus();
+
+                //mWidgetWindow.mWindowKeyDownDelegate += HandleKeyDown;
+            }
+        }
+
+        public override void SetFocus()
+        {
+            if (mEditWidget != null)
+            {
+                mEditWidget.SetFocus();
+                return;
+            }
+            base.SetFocus();
+        }
+
+        public override void Update()
+        {
+            base.Update();
+            mJustClosed = false;
+
+			bool hasFocus = mHasFocus;
+			if ((mEditWidget != null) && (mEditWidget.mHasFocus))
+				hasFocus = true;
+			if ((!hasFocus) && (mCurMenuWidget != null))
+				if (mCurMenuWidget.mWidgetWindow.mHasFocus)
+					hasFocus = true;
+			if ((!hasFocus) && (mCurMenuWidget != null))
+			{
+				mCurMenuWidget.SubmitSelection();
+				if (mCurMenuWidget != null)
+					mCurMenuWidget.Close();
+			}
+        }
+
+        public override void Resize(float x, float y, float width, float height)
+        {
+            base.Resize(x, y, width, height);
+            ResizeComponents();
+        }
+
+        private void ResizeComponents()
+        {
+            if (mEditWidget != null)
+            {
+                mEditWidget.Resize(0, 0, Math.Max(mWidth - GS!(24), 0), mHeight);                
+            }
+        }
+
+		public virtual bool WantsKeyHandling()
+		{
+			return true;
+		}	
+
+		void EditKeyDownHandler(KeyDownEvent evt)
+		{
+			if (!WantsKeyHandling())
+				return;
+
+			if ((evt.mKeyCode == .Up) || (evt.mKeyCode == .Down) || 
+			    (evt.mKeyCode == .PageUp) || (evt.mKeyCode == .PageDown) ||
+			    (evt.mKeyCode == .Home) || (evt.mKeyCode == .End))
+			{
+			    if (mCurMenuWidget != null)
+			        mCurMenuWidget.KeyDown(evt.mKeyCode, false);
+			}
+
+			if ((evt.mKeyCode == .Down) && (mCurMenuWidget == null))
+			{
+				ShowDropdown();
+
+				var label = Label;
+				for (let itemWidget in mCurMenuWidget.mItemWidgets)
+				{
+					if (itemWidget.mMenuItem.mLabel == label)
+						mCurMenuWidget.SetSelection(@itemWidget.Index);
+				}
+			}
+
+			if ((evt.mKeyCode == .Escape) && (mCurMenuWidget != null) && (mEditWidget != null))
+			{
+				mCurMenuWidget.Close();
+				mEditWidget.SetFocus();
+				evt.mHandled = true;
+			}
+
+			if ((evt.mKeyCode == .Return) && (mCurMenuWidget != null))
+			{
+				mCurMenuWidget.SubmitSelection();
+				evt.mHandled = true; 	
+			}
+		}
+
+		public override void KeyDown(KeyCode keyCode, bool isRepeat)
+		{
+			if (mCurMenuWidget != null)
+				mCurMenuWidget.KeyDown(keyCode, isRepeat);
+		}
+
+        public void MakeEditable(DarkEditWidget editWidget = null)
+        {
+            if (mEditWidget == null)
+            {
+				mEditWidget = editWidget;
+				if (mEditWidget == null)
+                	mEditWidget = new DarkEditWidget();
+                mEditWidget.mDrawBox = false;
+                mEditWidget.mOnSubmit.Add(new => EditSubmit);
+				mEditWidget.mOnKeyDown.Add(new => EditKeyDownHandler);
+				var ewc = (DarkEditWidgetContent)mEditWidget.mEditWidgetContent;
+				ewc.mScrollToStartOnLostFocus = true;
+                AddWidget(mEditWidget);
+                ResizeComponents();
+            }
+        }
+
+        private void EditSubmit(EditEvent theEvent)
+        {
+            if (mCurMenuWidget != null)
+            {
+                if (mCurMenuWidget.mSelectIdx != -1)
+                {
+                    mCurMenuWidget.SubmitSelection();
+                    if (mCurMenuWidget != null)
+                        mCurMenuWidget.Close();
+                }
+            }
+        }
+
+		public override void KeyDown(KeyDownEvent keyEvent)
+		{
+			base.KeyDown(keyEvent);
+			EditKeyDownHandler(keyEvent);
+		}
+    }
+}

+ 125 - 0
BeefLibs/Beefy2D/src/theme/dark/DarkDialog.bf

@@ -0,0 +1,125 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.gfx;
+using Beefy.widgets;
+
+namespace Beefy.theme.dark
+{
+    public class DarkDialog : Dialog
+    {
+        public Font mFont;
+        public Insets mTextInsets = new Insets() ~ delete _;
+        public float mMinWidth;
+        public float mButtonBottomMargin = GS!(12);
+        public float mButtonRightMargin = GS!(16);
+
+        public this(String title = null, String text = null, Image icon = null) :
+            base(title, text, icon)
+        {
+            mFont = DarkTheme.sDarkTheme.mSmallFont;
+
+            mTextInsets.Set(GS!(20), GS!(20), GS!(20), GS!(20));
+
+            if (icon != null)
+                mTextInsets.mLeft += icon.mWidth + GS!(20);
+        }
+
+        public override ButtonWidget CreateButton(String label)
+        {
+            DarkButton button = new DarkButton();
+            button.Label = label;
+            button.mAutoFocus = true;
+            return button;
+        }
+
+        public override EditWidget CreateEditWidget()
+        {
+            return new DarkEditWidget();
+        }
+
+        public override void CalcSize()
+        {
+            base.CalcSize();
+
+            // Size buttons with a zero width first so we can determine a minimium size this dialog needs to be
+            mWidth = mMinWidth;
+            mHeight = 0;
+            ResizeComponents();
+
+			String str = scope String();
+            for (var strView in mText.Split('\n'))
+            {
+				str.Clear();
+				strView.ToString(str);
+                mWidth = Math.Max(mWidth, mFont.GetWidth(str) + mTextInsets.Horz);
+			}
+
+            mWidth = Math.Max(mWidth, GS!(240));
+
+			float maxWidth = GS!(900);
+            if (mWidth >= maxWidth)
+            {
+				// We don't want to barely size the dialog large enough, otherwise the wrapping would leave just a short chunk
+				if (mWidth < maxWidth * 1.4f)
+					mWidth = maxWidth * 0.7f;
+				else
+					mWidth = maxWidth;
+			}
+            mWidth = Math.Max(-mButtons[0].mX + mTextInsets.mLeft, mWidth);
+            mHeight = GS!(80);
+            for (var strView in mText.Split('\n'))
+			{
+				str.Clear();
+				strView.ToString(str);
+                mHeight += mFont.GetWrapHeight(str, mWidth - mTextInsets.Horz);
+			}
+
+            if (mDialogEditWidget != null)
+                mHeight += GS!(16);
+        }
+
+        public override void ResizeComponents()
+        {
+            base.ResizeComponents();
+
+            float maxTextLen = 0;
+
+            for (DarkButton button in mButtons)
+                maxTextLen = Math.Max(maxTextLen, mFont.GetWidth(button.mLabel));
+
+            float buttonBaseSize = GS!(30) + maxTextLen;
+            float spacing = GS!(8);
+
+            float curY = mHeight - GS!(20) - mButtonBottomMargin;
+            float curX = mWidth - (mButtons.Count * buttonBaseSize) - ((mButtons.Count - 1) * spacing) - mButtonRightMargin;
+
+            if (mDialogEditWidget != null)
+            {                
+                mDialogEditWidget.Resize(GS!(16), curY - GS!(36), Math.Max(mWidth - GS!(16) * 2, 0), GS!(24));
+            }
+
+            for (DarkButton button in mButtons)
+            {
+                float aSize = buttonBaseSize;
+                button.Resize(curX, curY, aSize, GS!(22));
+                curX += aSize + spacing;
+            }
+        }              
+
+        public override void Draw(Graphics g)
+        {
+            base.Draw(g);
+
+            using (g.PushColor(DarkTheme.COLOR_WINDOW))
+                g.FillRect(0, 0, mWidth, mHeight);            
+
+            if (mIcon != null)
+                g.Draw(mIcon, GS!(20), (mHeight - GS!(8) - mIcon.mHeight) / 2);
+
+            g.SetFont(mFont);
+            if (mText != null)
+                g.DrawString(mText, mTextInsets.mLeft, mTextInsets.mTop, FontAlign.Left, mWidth - mTextInsets.Horz, FontOverflowMode.Wrap);
+        }
+    }
+}

+ 137 - 0
BeefLibs/Beefy2D/src/theme/dark/DarkDockingFrame.bf

@@ -0,0 +1,137 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.widgets;
+using Beefy.gfx;
+
+namespace Beefy.theme.dark
+{
+    public class DarkDockingFrame : DockingFrame
+    {
+        IDockable mCurDragTarget;
+		public bool mDrawBkg = true;
+
+        public this()
+        {
+            mMinWindowSize = GS!(100);
+
+			mMinWindowSize = GS!(32);
+			mDragMarginSize = GS!(64);
+			mDragWindowMarginSize = GS!(10);
+
+			mWindowMargin = 0;
+			mSplitterSize = GS!(6.0f);
+			mWindowSpacing = GS!(2.0f);
+        }
+
+		public ~this()
+		{
+		}
+
+		public override void RemovedFromParent(Widget previousParent, WidgetWindow window)
+		{
+			base.RemovedFromParent(previousParent, window);
+		}
+
+        public override void Draw(Graphics g)
+        {
+			if (mDrawBkg)
+			{
+	            using (g.PushColor(DarkTheme.COLOR_BKG))
+	                g.FillRect(0, 0, mWidth, mHeight);
+			}
+            base.Draw(g);            
+        }
+
+        public override void DrawAll(Graphics g)
+        {            
+            base.DrawAll(g);           
+
+            if ((mCurDragTarget != null) && (mWidgetWindow.mAlpha == 1.0f))
+            {
+                using (g.PushTranslate(mWidgetWindow.mMouseX, mWidgetWindow.mMouseY))
+                {
+                    mCurDragTarget.DrawDockPreview(g);
+                }
+            }
+        }
+        
+        public override void DrawDraggingDock(Graphics g)
+        {            
+            if (mDraggingCustomDock != null)
+            {
+                mDraggingCustomDock.Draw(g);
+            }
+            else
+            {
+                if (mDraggingAlign == WidgetAlign.Inside)
+                {
+                    using (g.PushColor(0x30FFFFFF))
+                        g.DrawBox(DarkTheme.sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.WhiteCircle], mDraggingRef.mX, mDraggingRef.mY, mDraggingRef.mWidth, mDraggingRef.mHeight);
+                }
+                else
+                {
+                    DockedWidget widgetRef = mDraggingRef ?? this;
+
+                    Matrix matrix = Matrix.IdentityMatrix;
+                    float dist = 0;
+
+                    if (mDraggingAlign == WidgetAlign.Left)
+                    {
+                        matrix.Rotate(Math.PI_f);
+                        matrix.Translate(widgetRef.mX, widgetRef.mY + widgetRef.mHeight);
+                        dist = widgetRef.mHeight;
+                    }
+                    else if (mDraggingAlign == WidgetAlign.Right)
+                    {
+                        matrix.Rotate(0);
+                        matrix.Translate(widgetRef.mX + widgetRef.mWidth, widgetRef.mY);
+                        dist = widgetRef.mHeight;
+                    }
+                    else if (mDraggingAlign == WidgetAlign.Top)
+                    {
+                        matrix.Rotate(-Math.PI_f / 2);
+                        matrix.Translate(widgetRef.mX, widgetRef.mY);
+                        dist = widgetRef.mWidth;
+                    }
+                    else if (mDraggingAlign == WidgetAlign.Bottom)
+                    {
+                        matrix.Rotate(Math.PI_f / 2);
+                        matrix.Translate(widgetRef.mX + widgetRef.mWidth, widgetRef.mY + widgetRef.mHeight);
+                        dist = widgetRef.mWidth;
+                    }
+
+                    using (g.PushMatrix(matrix))
+                    {
+                        if (mDraggingRef != null)
+                            g.DrawBox(DarkTheme.sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.GlowDot], GS!(-9), GS!(-8), GS!(20), dist + GS!(8) * 2);
+                        else
+                            g.DrawBox(DarkTheme.sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.WhiteCircle], GS!(-13), GS!(-8), GS!(20), dist + GS!(8) * 2);
+
+                        int32 arrowCount = 3;
+                        if (dist < 80)
+                            arrowCount = 1;
+                        else if (dist < 120)
+                            arrowCount = 2;
+                        float arrowSep = dist / (arrowCount + 1);
+                        for (int32 arrowNum = 0; arrowNum < arrowCount; arrowNum++)
+                            g.Draw(DarkTheme.sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.ArrowRight], GS!(-22), GS!(-9) + (arrowNum + 1) * arrowSep);
+                    }
+                }
+                
+            }
+        }
+
+        public override void ShowDragTarget(IDockable draggingItem)
+        {
+            base.ShowDragTarget(draggingItem);
+            mCurDragTarget = draggingItem;
+        }
+
+        public override void HideDragTarget(IDockable draggingItem, bool executeDrag = false)
+        {
+            base.HideDragTarget(draggingItem, executeDrag);
+            mCurDragTarget = null;
+        }
+    }
+}

+ 922 - 0
BeefLibs/Beefy2D/src/theme/dark/DarkEditWidget.bf

@@ -0,0 +1,922 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+using Beefy.widgets;
+using Beefy.gfx;
+using Beefy.utils;
+
+namespace Beefy.theme.dark
+{
+    public class DarkEditWidgetContent : EditWidgetContent
+    {
+        public Font mFont;                
+        public uint32[] mTextColors = sDefaultColors;
+        public uint32 mHiliteColor = 0xFF2f5c88;
+        public uint32 mUnfocusedHiliteColor = 0x00000000;
+        public int32 mRecalcSizeLineNum = -1;
+        public float mRecalcSizeCurMaxWidth = 0;
+		public bool mHasQueuedRecalcSize;
+		public int32 mTopCharId = -1;
+		public double mTopCharIdVertPos = -1;
+		public bool mWantsCheckScrollPosition;
+		public uint32 mViewWhiteSpaceColor;
+		public bool mScrollToStartOnLostFocus;
+
+		protected static uint32[] sDefaultColors = new uint32[] { Color.White } ~ delete _;
+
+        public this(EditWidgetContent refContent = null) : base(refContent)
+        {
+            //mTextInsets.Set(-3, 2, 0, 2);
+			//mTextInsets.Set(GS!(-3), GS!(2), 0, GS!(2));
+
+			mTextInsets.Set(GS!(0), GS!(2), 0, GS!(2));
+
+            mWidth = GS!(100);
+            mHeight = GS!(24);
+            mHorzJumpSize = GS!(40);
+            mFont = DarkTheme.sDarkTheme.mSmallFont;            
+        }
+
+        public override void GetTextData()
+        {
+            // Generate text flags if we need to...
+            if ((mData.mTextFlags == null) && (mWordWrap))
+            {
+				scope AutoBeefPerf("DEWC.GetTextData");
+
+                mData.mTextFlags = new uint8[mData.mTextLength + 1];
+
+                int32 lineIdx = 0;
+                int32 lineStartIdx = 0;
+				String lineCheck = scope String();
+                for (int32 i = 0; i < mData.mTextLength; i++)
+                {
+                    char8 c = (char8)mData.mText[i].mChar;
+                    
+                    lineCheck.Clear();
+                    if (c == '\n')                    
+                        ExtractString(lineStartIdx, i - lineStartIdx, lineCheck);
+                    else if (i == mData.mTextLength - 1)
+                        ExtractString(lineStartIdx, i - lineStartIdx + 1, lineCheck);
+
+					if (lineCheck.Length > 0)
+					{
+						String lineCheckLeft = scope String();
+						lineCheckLeft.Reference(lineCheck);
+						while (true)
+						{
+						    int32 maxChars = GetTabbedCharCountToLength(lineCheckLeft, mEditWidget.mScrollContentContainer.mWidth - mTextInsets.mLeft - mTextInsets.mRight);
+						    if (maxChars == 0)
+						        maxChars = 1;
+						    if (maxChars >= lineCheckLeft.Length)
+						        break;
+						    
+						    int32 checkIdx = maxChars;
+						    while ((checkIdx > 0) && (!lineCheckLeft[checkIdx].IsWhiteSpace))
+						        checkIdx--;
+	
+						    if (checkIdx == 0)
+						        checkIdx = maxChars - 1;
+	
+						    mData.mTextFlags[lineStartIdx + checkIdx + 1] |= (int32)TextFlags.Wrap;
+						    lineStartIdx += checkIdx + 1;
+						    
+							//lineCheck.Remove(0, checkIdx + 1);
+							lineCheckLeft.AdjustPtr(checkIdx + 1);
+						}
+					}
+
+                    if (c == '\n')
+                    {                        
+                        lineStartIdx = i + 1;
+                        lineIdx++;
+                    }
+                }
+            }
+
+            base.GetTextData();
+        }
+
+		protected override void AdjustCursorsAfterExternalEdit(int index, int ofs)
+ 		{
+			 base.AdjustCursorsAfterExternalEdit(index, ofs);
+			 mWantsCheckScrollPosition = true;
+		}
+
+        public float GetTabbedPos(float startX)
+        {
+            float spaceWidth = mFont.GetWidth((char32)' ');
+            if (mTabSize == 0)
+                return startX + spaceWidth;
+            return (float)Math.Truncate((startX + spaceWidth) / mTabSize + 0.999f) * mTabSize;
+        }
+
+		static mixin GetTabSection(var origString, var stringLeft, var subStr)
+		{
+			int32 tabIdx = (int32)stringLeft.IndexOf('\t');
+			if (tabIdx == -1)
+			    break;
+
+			if (subStr == null)
+			{
+				subStr = scope:: String(stringLeft, 0, tabIdx);
+				stringLeft = scope:: String(origString, tabIdx + 1);
+			}
+			else
+			{
+				subStr.Clear();
+				subStr.Append(stringLeft, 0, tabIdx);
+				stringLeft.Remove(0, tabIdx + 1);
+			}
+
+			tabIdx
+		}
+
+        public float DoDrawText(Graphics g, String origString, float x, float y)
+        {
+            String stringLeft = origString;
+            float aX = x;
+            float aY = y;
+
+			void DrawString(String str, float x, float y)
+			{
+				if (str.Length == 0)
+					return;
+
+				g.DrawString(str, x, y);
+
+				if (mViewWhiteSpaceColor != 0)
+				{
+					let prevColor = g.mColor;
+					g.PopColor();
+					g.PushColor(mViewWhiteSpaceColor);
+
+					float curX = x;
+					int lastNonSpace = 0;
+					for (int i < str.Length)
+					{
+						char8 c = str[i];
+						if (c == ' ')
+						{
+							// Flush length
+							if (lastNonSpace < i)
+							{
+								var contentStr = scope String();
+								contentStr.Reference(str.Ptr + lastNonSpace, i - lastNonSpace);
+								curX += mFont.GetWidth(contentStr);
+							}
+
+							g.DrawString("·", curX, y);
+							curX += mFont.GetWidth(' ');
+							lastNonSpace = i + 1;
+						}
+					}
+
+					g.PopColor();
+					g.PushColorOverride(prevColor);
+				}
+			}
+
+			String subStr = null;
+            while (true)
+            {
+                GetTabSection!(origString, stringLeft, subStr);
+
+                if (g != null)
+                    DrawString(subStr, aX, aY);
+                
+                aX += mFont.GetWidth(subStr);
+
+				if ((mViewWhiteSpaceColor != 0) && (g != null))
+				{
+					let prevColor = g.mColor;
+					g.PopColor();
+					g.PushColor(mViewWhiteSpaceColor);
+					g.DrawString("→", aX, y);
+					g.PopColor();
+					g.PushColorOverride(prevColor);
+				}
+
+                aX = GetTabbedPos(aX);
+            }
+            
+            if (g != null)
+				DrawString(stringLeft, aX, aY);
+
+			//TODO: This is just an "emergency dropout", remove when we optimize more?
+			/*if ((mX + x >= 0) && (stringLeft.Length > 1000))
+			{
+				return aX + 10000;
+			}*/
+
+            aX += mFont.GetWidth(stringLeft);
+            return aX;
+        }        
+
+        /*public int GetTabbedCharCountToLength(String origString, float len)
+        {
+            String stringLeft = origString;
+            float aX = 0;
+            int idx = 0;
+
+			String subStr = null;
+            while (true)
+            {
+                int tabIdx = GetTabSection!(origString, stringLeft, subStr);
+                
+                int char8Count = mFont.GetCharCountToLength(subStr, len - aX);
+                if (char8Count < subStr.Length)
+                    return idx + char8Count;
+
+                idx += tabIdx + 1;
+                aX += mFont.GetWidth(subStr);
+                float prevX = aX;
+
+                aX = GetTabbedPos(aX);
+
+                if (len < aX)
+                    return idx - 1;
+            }
+
+            return idx + mFont.GetCharCountToLength(stringLeft, len - aX);
+        }*/
+
+		public int32 GetTabbedCharCountToLength(String origString, float len)
+		{
+		    float aX = 0;
+		    int32 idx = 0;
+
+			String subStr = scope String();
+			subStr.Reference(origString);
+		    while (true)
+		    {
+				bool hitTabStop = false;
+				int32 char8Count = (int32)mFont.GetCharCountToLength(subStr, len - aX, &hitTabStop);
+				if (!hitTabStop)
+					return idx + char8Count;
+
+		        aX += mFont.GetWidth(StringView(subStr, 0, char8Count));
+		        aX = GetTabbedPos(aX);
+				if (aX > len + 0.001f)
+					return idx + char8Count;
+				idx += char8Count + 1;
+				subStr.AdjustPtr(char8Count + 1);
+		    }
+		}
+
+        public virtual void DrawSectionFlagsOver(Graphics g, float x, float y, float width, uint8 flags)
+        {
+
+        }
+
+        public float GetTabbedWidth(String origString, float x, bool forceAccurate = false)
+        {
+            String stringLeft = origString;
+            float aX = x;
+
+			String subStr = null;
+            while (true)
+            {
+#unwarn
+                int32 tabIdx = GetTabSection!(origString, stringLeft, subStr);
+
+                aX += mFont.GetWidth(subStr);
+                aX = GetTabbedPos(aX);
+            }
+
+			//TODO: This is just an "emergency dropout", remove when we optimize more?
+			/*if ((!forceAccurate) && (mX + x >= 0) && (stringLeft.Length > 1000))
+			{
+				return aX + 10000;
+			}*/
+
+            return aX + mFont.GetWidth(stringLeft);
+        }
+
+        public void SetFont(Font font, bool isMonospace, bool virtualCursor)
+        {
+            mFont = font;
+            if (isMonospace)
+            {
+                mCharWidth = mFont.GetWidth((char32)' ');
+                //Debug.Assert(mFont.GetWidth((char32)'W') == mCharWidth);
+				if (mTabSize == 0)
+                	mTabSize = mTabLength * mCharWidth;
+				else
+					mTabSize = (float)Math.Round(mTabSize / mCharWidth) * mCharWidth;
+            }
+            else
+                mCharWidth = -1;
+            if (virtualCursor)
+                Debug.Assert(isMonospace);
+            mAllowVirtualCursor = virtualCursor;
+        }
+
+		public override void RehupScale(float oldScale, float newScale)
+		{
+			base.RehupScale(oldScale, newScale);
+			Utils.RoundScale(ref mTabSize, newScale / oldScale);
+			SetFont(mFont, mCharWidth != -1, mAllowVirtualCursor);
+			mContentChanged = true; // Defer calling of RecalcSize
+		}
+
+        public virtual float DrawText(Graphics g, String str, float x, float y, uint16 typeIdAndFlags)
+        {
+            using (g.PushColor(mTextColors[typeIdAndFlags & 0xFF]))
+                return DoDrawText(g, str, x, y);
+        }
+
+		public virtual uint32 GetSelectionColor(uint8 flags)
+		{
+		    return mEditWidget.mHasFocus ? mHiliteColor : mUnfocusedHiliteColor;
+		}
+
+        public override void Draw(Graphics g)
+        {            
+            base.Draw(g);
+            
+#unwarn
+            int lineCount = GetLineCount();
+            float lineSpacing = GetLineHeight(0);
+
+            g.SetFont(mFont);
+
+			float offsetY = mTextInsets.mTop;
+			if (mHeight < lineSpacing)
+				offsetY = (mHeight - lineSpacing) * 0.75f;
+
+            g.PushTranslate(mTextInsets.mLeft, offsetY);
+
+            int selStartLine = -1;
+            int selStartCharIdx = -1;
+            int selEndLine = -1;
+            int selEndCharIdx = -1;
+
+            int selStartIdx = -1;
+            int selEndIdx = -1;
+
+            if (mSelection != null)
+            {                
+                mSelection.Value.GetAsForwardSelect(out selStartIdx, out selEndIdx);
+                GetLineCharAtIdx(selStartIdx, out selStartLine, out selStartCharIdx);
+                GetLineCharAtIdx(selEndIdx, out selEndLine, out selEndCharIdx);
+            }
+
+            int firstLine;
+            int firstCharIdx;
+            float overflowX;
+            GetLineCharAtCoord(0, -mY, out firstLine, out firstCharIdx, out overflowX);
+
+            int lastLine;
+            int lastCharIdx;
+            float lastOverflowX;
+            GetLineCharAtCoord(0, -mY + mEditWidget.mScrollContentContainer.mHeight, out lastLine, out lastCharIdx, out lastOverflowX);
+
+            bool drewCursor = false;
+			String sectionText = scope String(256);
+            for (int lineIdx = firstLine; lineIdx <= lastLine; lineIdx++)
+            {
+                //string lineText = GetLineText(lineIdx);
+                int lineStart;
+                int lineEnd;
+                GetLinePosition(lineIdx, out lineStart, out lineEnd);
+
+                int lineDrawStart = lineStart;
+                float curX = 0;
+                float curY = lineIdx * lineSpacing;
+                while (true)
+                {
+                    int lineDrawEnd = lineDrawStart;
+                    uint16 curTypeIdAndFlags = *(uint16*)&mData.mText[lineDrawStart].mDisplayTypeId;                    
+
+                    // Check for transition of curTypeIdAndFlags - colors ignore whitespace, but if flags are set then we need 
+                    //  to be exact
+                    /*while ((lineDrawEnd < lineEnd) && ((*(uint16*)&mData.mText[lineDrawEnd].mDisplayTypeId == curTypeIdAndFlags) ||
+                        ((curTypeIdAndFlags < 0x100) && (((char8)mData.mText[lineDrawEnd].mChar).IsWhiteSpace))))
+                        lineDrawEnd++;*/
+
+					while (true)
+					{
+						var checkEnd = ref mData.mText[lineDrawEnd];
+						if ((lineDrawEnd < lineEnd) && ((*(uint16*)&checkEnd.mDisplayTypeId == curTypeIdAndFlags) ||
+							((curTypeIdAndFlags < 0x100) && (checkEnd.mChar.IsWhiteSpace) && (checkEnd.mDisplayFlags == 0))))
+							lineDrawEnd++;
+						else
+							break;
+					}
+
+					sectionText.Clear();
+                    ExtractString(lineDrawStart, lineDrawEnd - lineDrawStart, sectionText);
+					
+                    int selStart = Math.Max(0, selStartIdx - lineDrawStart);
+                    int selEnd = Math.Min(lineDrawEnd - lineDrawStart, selEndIdx - lineDrawStart);
+
+                    uint8 flags = (uint8)(curTypeIdAndFlags >> 8);
+                    if ((lineDrawStart >= selStartIdx) && (lineDrawEnd < selEndIdx) && (lineDrawEnd == lineDrawStart))
+                    {
+                        // Blank line selected
+                        using (g.PushColor(GetSelectionColor(flags)))
+                            g.FillRect(curX, curY, 4, lineSpacing);
+                    }
+
+                    if (selEnd > selStart)
+                    {
+						String selPrevString = scope String(selStart);
+						selPrevString.Append(sectionText, 0, selStart);
+						String selIncludeString = scope String(selEnd);
+						selIncludeString.Append(sectionText, 0, selEnd);
+
+                        float selStartX = GetTabbedWidth(selPrevString, curX);
+                        float selEndX = GetTabbedWidth(selIncludeString, curX);
+
+                        if (lineIdx != selEndLine)
+                            selEndX += mFont.GetWidth((char32)' ');
+
+                        using (g.PushColor(GetSelectionColor(flags)))
+                            g.FillRect(selStartX, curY, selEndX - selStartX, lineSpacing);
+                    }
+
+                    float nextX = curX;
+                    nextX = DrawText(g, sectionText, curX, curY, curTypeIdAndFlags);                                        
+                    DrawSectionFlagsOver(g, curX, curY, nextX - curX, flags);
+
+                    //int32 lineDrawStartColumn = lineDrawStart - lineStart;
+                    //int32 lineDrawEndColumn = lineDrawEnd - lineStart;
+                    if ((mEditWidget.mHasFocus) && (!drewCursor))
+                    {
+                        float aX = -1;
+                        if (mVirtualCursorPos != null)
+                        {
+                            if ((lineIdx == mVirtualCursorPos.Value.mLine) && (lineDrawEnd == lineEnd))
+                            {
+                                aX = mVirtualCursorPos.Value.mColumn * mCharWidth;
+                            }
+                        }
+                        else if (mCursorTextPos >= lineDrawStart)
+                        {
+                            bool isInside = mCursorTextPos < lineDrawEnd;
+                            if ((mCursorTextPos == lineDrawEnd) && (lineDrawEnd == lineEnd))
+                            {                                
+                                if (lineDrawEnd == mData.mTextLength)
+                                    isInside = true;
+                                if (mWordWrap)
+                                {
+                                    if ((mShowCursorAtLineEnd) || (lineEnd >= mData.mTextFlags.Count) || (mData.mTextFlags[lineEnd] & (int32)TextFlags.Wrap) == 0)
+                                        isInside = true;
+                                }
+                                else
+                                    isInside = true;
+                            }
+
+                            if (isInside)
+                            {
+								String subText = scope String(mCursorTextPos - lineDrawStart);
+								subText.Append(sectionText, 0, mCursorTextPos - lineDrawStart);
+                                aX = GetTabbedWidth(subText, curX);
+                            }
+                        }                        
+
+                        if (aX != -1)
+                        {                            
+                            float brightness = (float)Math.Cos(Math.Max(0.0f, mCursorBlinkTicks - 20) / 9.0f);                            
+                            brightness = Math.Max(0, Math.Min(1.0f, brightness * 2.0f + 1.6f));
+                            if (mEditWidget.mVertPos.IsMoving)
+                                brightness = 0; // When we animate a pgup or pgdn, it's weird seeing the cursor scrolling around
+
+                            if (mOverTypeMode)
+                            {
+                                if (mCharWidth <= 2)
+                                {
+                                    using (g.PushColor(Color.Get(brightness * 0.75f)))
+                                        g.FillRect(aX, curY, GS!(2), lineSpacing);
+                                }
+                                else
+                                {
+                                    using (g.PushColor(Color.Get(brightness * 0.30f)))
+                                        g.FillRect(aX, curY, mCharWidth, lineSpacing);
+                                }
+                            }
+                            else
+                            {
+                                using (g.PushColor(Color.Get(brightness)))
+                                    g.FillRect(aX, curY, Math.Max(1.0f, GS!(1)), lineSpacing);
+                            }
+                            drewCursor = true;
+                        }
+                    }
+
+                    lineDrawStart = lineDrawEnd;
+                    curX = nextX;
+
+                    if (lineDrawStart >= lineEnd)
+                        break;
+                }   
+            }
+
+            g.PopMatrix();
+
+			/*using (g.PushColor(0x4000FF00))
+				g.FillRect(-8, -8, mWidth + 16, mHeight + 16);*/
+
+			/*if (mDbgX != -1)
+				g.FillRect(mDbgX - 1, mDbgY - 1, 3, 3);*/
+        }
+
+        public override void AddWidget(Widget widget)
+        {
+            base.AddWidget(widget);
+        }
+
+        public override bool AllowChar(char32 theChar)
+        {
+			if ((int)theChar < 32)
+            	return (theChar == '\n') || (mIsMultiline && (theChar == '\t'));
+            return mFont.HasChar(theChar);
+        }
+
+        public override void InsertAtCursor(String theString, InsertFlags insertFlags)
+        {
+			scope AutoBeefPerf("DarkEditWidgetContent.InsertAtCursor");
+
+            base.InsertAtCursor(theString, insertFlags);            
+        }
+
+        public override void GetTextCoordAtLineChar(int line, int lineChar, out float x, out float y)
+        {
+            String lineText = scope String(256);
+            GetLineText(line, lineText);
+            if (lineChar > lineText.Length)
+                x = GetTabbedWidth(lineText, 0) + (mFont.GetWidth((char32)' ') * (lineChar - (int32)lineText.Length)) + mTextInsets.mLeft;
+            else
+			{
+				String subText = scope String(Math.Min(lineChar, 256));
+				subText.Append(lineText, 0, lineChar);
+                x = GetTabbedWidth(subText, 0, true) + mTextInsets.mLeft;
+			}
+            y = mTextInsets.mTop + line * mFont.GetLineSpacing();                        
+        }
+
+        public override void GetTextCoordAtLineAndColumn(int line, int column, out float x, out float y)
+        {
+            Debug.Assert((mCharWidth != -1) || (column == 0));
+            String lineText = scope String(256);
+            GetLineText(line, lineText);
+            x = mTextInsets.mLeft + column * mCharWidth;
+            y = mTextInsets.mTop + line * mFont.GetLineSpacing();                        
+        }
+
+        public override bool GetLineCharAtCoord(float x, float y, out int line, out int char8Idx, out float overflowX)
+        {
+            line = (int) ((y - mTextInsets.mTop) / mFont.GetLineSpacing() + 0.001f);
+            int lineCount = GetLineCount();
+
+            if (line < 0)
+                line = 0;
+            if (line >= lineCount)
+                line = lineCount - 1;
+
+            String lineText = scope String(256);
+            GetLineText(line, lineText);
+            int32 char8Count = GetTabbedCharCountToLength(lineText, x - mTextInsets.mLeft);
+            char8Idx = char8Count;
+
+            if (char8Count < lineText.Length)
+            {
+				String subString = scope String(char8Count);
+				subString.Append(lineText, 0, char8Count);
+                float subWidth = GetTabbedWidth(subString, 0);
+
+				var utf8enumerator = lineText.DecodedChars(char8Count);
+				if (utf8enumerator.MoveNext())
+				{
+					char32 c = utf8enumerator.Current;
+	                float checkCharWidth = 0;
+	                if (c == '\t')
+	                    checkCharWidth = mTabSize * 0.5f;
+	                else
+					{
+						checkCharWidth = mFont.GetWidth(c) * 0.5f;
+					}
+
+	                if (x >= subWidth + mTextInsets.mLeft + checkCharWidth)
+	                    char8Idx = (int32)utf8enumerator.NextIndex;
+				}
+            }
+            else
+            {
+                overflowX = (x - mTextInsets.mLeft) - (GetTabbedWidth(lineText, 0) + 0.001f);
+                return overflowX <= 0;                
+            }
+
+            overflowX = 0;
+            return true;
+        }
+
+        public override bool GetLineAndColumnAtCoord(float x, float y, out int line, out int column)
+        {
+            line = (int32)((y - mTextInsets.mTop) / mFont.GetLineSpacing() + 0.001f);
+            if (line >= GetLineCount())
+                line = GetLineCount() - 1;
+            line = Math.Max(0, line);
+            column = Math.Max(0, (int32)((x - mTextInsets.mLeft + 1) / mCharWidth + 0.6f));
+            return mCharWidth != -1;
+        }
+
+        void RecalcSize(int32 startLineNum, int32 endLineNum, bool forceAccurate = false)
+        {
+			scope AutoBeefPerf("DEWC.RecalcSize");
+
+			String line = scope String();
+            for (int32 lineIdx = startLineNum; lineIdx < endLineNum; lineIdx++)
+            {
+				line.Clear();
+                GetLineText(lineIdx, line);
+                mRecalcSizeCurMaxWidth = Math.Max(mRecalcSizeCurMaxWidth, GetTabbedWidth(line, 0, forceAccurate) + mHorzJumpSize);
+                Debug.Assert(!mRecalcSizeCurMaxWidth.IsNaN);
+            }
+        }
+
+		public override void CursorToLineEnd()
+		{
+			//int32 line;
+			//int32 lineChar;
+			//GetCursorLineChar(out line, out lineChar);
+			/*RecalcSize(line, line + 1, true);
+			if (mRecalcSizeCurMaxWidth > mWidth)
+			{
+				mRecalcSizeLineNum = -1;
+
+			}*/
+			mRecalcSizeLineNum = -1;
+			RecalcSize(true);
+			base.CursorToLineEnd();
+		}
+
+		public void RecalcSize(bool forceAccurate = false)
+		{
+			mMaximalScrollAddedHeight = 0;
+			if (mRecalcSizeLineNum == -1)
+			{
+				mRecalcSizeCurMaxWidth = 0;
+				mHasQueuedRecalcSize = false;
+			}
+			else // We need to recalc again after our current pass
+				mHasQueuedRecalcSize = true; 
+
+			if (!mIsReadOnly)
+			{
+			    float cursorX;
+			    float cursorY;
+			    GetTextCoordAtCursor(out cursorX, out cursorY);
+			    mRecalcSizeCurMaxWidth = Math.Max(mRecalcSizeCurMaxWidth, cursorX + mHorzJumpSize);
+			}
+
+			if (mUpdateCnt == 0)
+			{                
+			    RecalcSize(0, GetLineCount());
+			    mWidth = mRecalcSizeCurMaxWidth + mTextInsets.mLeft + mTextInsets.mRight;
+			    Debug.Assert(!mWidth.IsNaN);
+			}
+			else if (mRecalcSizeLineNum == -1)
+			{
+			   	mRecalcSizeLineNum = 0;
+
+			    // The actual recalculation will take 16 ticks so just make sure we have enough width for 
+			    //  the current line for now
+			    var lineAndCol = CursorLineAndColumn;
+			    RecalcSize(lineAndCol.mLine, lineAndCol.mLine + 1, forceAccurate);
+			    mWidth = Math.Max(mWidth, mRecalcSizeCurMaxWidth + mTextInsets.mLeft + mTextInsets.mRight);
+			    Debug.Assert(!mWidth.IsNaN);
+			}
+
+			mHeight = GetLineCount() * mFont.GetLineSpacing() + mTextInsets.mTop + mTextInsets.mBottom;
+			UpdateMaximalScroll();
+			base.RecalcSize();
+		}
+
+        public override void RecalcSize()
+        {
+            RecalcSize(false);
+        }
+
+		public override void ContentChanged()
+		{
+			base.ContentChanged();
+			mRecalcSizeLineNum = -1;
+		}
+
+		public override void TextAppended(String str)
+		{
+			if ((mData.mLineStarts != null) && (mIsReadOnly))
+			{
+				int32 recalcSizeLineNum = Math.Max((int32)mData.mLineStarts.Count - 2, 0);
+				if ((mRecalcSizeLineNum == -1) || (recalcSizeLineNum < mRecalcSizeLineNum))
+					mRecalcSizeLineNum = recalcSizeLineNum;
+			}
+			base.TextAppended(str);
+		}
+
+        void UpdateMaximalScroll()
+        {            
+            if (mAllowMaximalScroll)
+            {
+				let prevHeight = mHeight;
+
+                mHeight -= mMaximalScrollAddedHeight;
+                mMaximalScrollAddedHeight = mEditWidget.mScrollContentContainer.mHeight - mFont.GetLineSpacing();
+                mHeight += mMaximalScrollAddedHeight;
+
+				if (mHeight != prevHeight)
+					mEditWidget.UpdateScrollbars();
+            }
+        }
+
+        public override void Resize(float x, float y, float width, float height)
+        {
+            base.Resize(x, y, width, height);
+            UpdateMaximalScroll();
+        }
+
+        public override float GetLineHeight(int line)
+        {
+            return mFont.GetLineSpacing();
+        }
+
+        public override float GetPageScrollTextHeight()
+        {
+            float numLinesVisible = mEditWidget.mScrollContentContainer.mHeight / mFont.GetLineSpacing();
+            if (numLinesVisible - (int32)numLinesVisible < 0.90f)
+                numLinesVisible = (int32) numLinesVisible;
+
+            float val = numLinesVisible * mFont.GetLineSpacing();
+            if (val <= 0)
+                return base.GetPageScrollTextHeight();
+            return val;
+        }
+
+		public void CheckRecordScrollTop()
+		{
+			if (mWantsCheckScrollPosition)
+			{
+				if (mTopCharId != -1)
+				{
+					int textIdx = mData.mTextIdData.GetPrepared().GetIndexFromId(mTopCharId);
+					if (textIdx != -1)
+					{
+						int line;
+						int lineChar;
+						GetLineCharAtIdx(textIdx, out line, out lineChar);
+
+						var vertPos = mEditWidget.mVertPos.mDest;
+						var offset = vertPos % mFont.GetLineSpacing();
+						mEditWidget.mVertScrollbar.ScrollTo(line * mFont.GetLineSpacing() + offset);
+					}
+					else
+					{
+						mTopCharId = -1;
+					}
+				}
+				mWantsCheckScrollPosition = false;
+			}
+
+			if (mEditWidget.mHasFocus)
+			{
+				mTopCharId = -1;
+			}
+			else
+			{
+				var vertPos = mEditWidget.mVertPos.mDest;
+				if ((mTopCharId == -1) || (mTopCharIdVertPos != vertPos))
+				{
+					float lineNum = (float)(vertPos / mFont.GetLineSpacing());
+					int lineStart;
+					int lineEnd;
+					GetLinePosition((int32)lineNum, out lineStart, out lineEnd);
+					int idAtStart = mData.mTextIdData.GetIdAtIndex((int32)lineStart);
+					if (idAtStart == -1)
+						idAtStart = 0;
+					mTopCharId = (int32)idAtStart;
+					mTopCharIdVertPos = vertPos;
+				}
+			}
+		}
+
+        public override void Update()
+        {
+            base.Update();
+
+            if ((mRecalcSizeLineNum != -1) && (BFApp.sApp.mIsUpdateBatchStart))
+            {
+                int32 lineCount = GetLineCount();
+                int32 toLine = Math.Min(lineCount, mRecalcSizeLineNum + Math.Max(1, lineCount / 16) + 80);
+                RecalcSize(mRecalcSizeLineNum, toLine);
+                if (toLine == lineCount)
+                {
+                    mRecalcSizeLineNum = -1;
+                    mWidth = mRecalcSizeCurMaxWidth + mTextInsets.mLeft + mTextInsets.mRight;
+                    base.RecalcSize();
+                }
+                else
+                    mRecalcSizeLineNum = toLine;
+            }
+
+			if ((mRecalcSizeLineNum == -1) && (mHasQueuedRecalcSize))
+				RecalcSize();
+
+			CheckRecordScrollTop();
+        }
+    }
+
+    public class DarkEditWidget : EditWidget
+    {
+        public bool mDrawBox = true;     
+
+        public this(DarkEditWidgetContent content = null)
+        {
+            mEditWidgetContent = content;
+            if (mEditWidgetContent == null)
+                mEditWidgetContent = new DarkEditWidgetContent();
+            mEditWidgetContent.mEditWidget = this;
+            mScrollContent = mEditWidgetContent;
+            mScrollContentContainer.AddWidget(mEditWidgetContent);
+
+            SetupInsets();
+            //mScrollbarInsets.Set(18, 1, 0, 0);
+
+            mHorzPos.mSpeed = 0.2f;
+            mVertPos.mSpeed = 0.2f;
+            mScrollbarBaseContentSizeOffset = GS!(3);
+        }
+
+		protected virtual void SetupInsets()
+		{
+			mScrollContentInsets.Set(GS!(3), GS!(3), GS!(3), GS!(3));
+		}
+
+		protected override void HandleWindowMouseDown(Beefy.events.MouseEvent event)
+		{
+			base.HandleWindowMouseDown(event);
+
+			// If we got closed as part of this click, don't propagate the click through
+			if (mParent == null)
+			{
+				event.mHandled = true;
+			}
+		}
+
+		public override void RehupScale(float oldScale, float newScale)
+		{
+			base.RehupScale(oldScale, newScale);
+			SetupInsets();
+		}
+
+        public override void DefaultDesignInit()
+        {
+            base.DefaultDesignInit();
+            mWidth = GS!(80);
+            mHeight = GS!(20);
+            SetText("Edit Text");
+        }
+
+        public override void InitScrollbars(bool wantHorz, bool wantVert)
+        {
+            SetupInsets();
+
+            base.InitScrollbars(wantHorz, wantVert);
+
+            float scrollIncrement = ((DarkEditWidgetContent) mEditWidgetContent).mFont.GetLineSpacing() * GS!(3);
+            if (mHorzScrollbar != null)
+                mHorzScrollbar.mScrollIncrement = scrollIncrement;
+            if (mVertScrollbar != null)
+                mVertScrollbar.mScrollIncrement = scrollIncrement;
+        }
+
+        public override void Draw(Graphics g)
+        {
+            base.Draw(g);
+
+            if (mDrawBox)
+            {
+                g.DrawBox(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.EditBox), 0, 0, mWidth, mHeight);
+                if (mHasFocus)
+                {
+                    using (g.PushColor(DarkTheme.COLOR_SELECTED_OUTLINE))
+                        g.DrawBox(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.Outline), 0, 0, mWidth, mHeight);
+                }
+            }
+
+			/*using (g.PushColor(0x40FF0000))
+				g.FillRect(0, 0, mWidth, mHeight);*/
+        }
+
+		public override void LostFocus()
+		{
+			base.LostFocus();
+			var darkEditWidgetContent = (DarkEditWidgetContent)mEditWidgetContent;
+			darkEditWidgetContent.CheckRecordScrollTop();
+			if (darkEditWidgetContent.mScrollToStartOnLostFocus)
+				HorzScrollTo(0);
+		}
+    }
+}

+ 21 - 0
BeefLibs/Beefy2D/src/theme/dark/DarkIconButton.bf

@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.widgets;
+using Beefy.gfx;
+
+namespace Beefy.theme.dark
+{
+    public class DarkIconButton : ButtonWidget
+    {
+        public Image mIcon;
+        public float mIconOfsX;
+        public float mIconOfsY;
+
+        public override void Draw(Graphics g)
+        {
+            base.Draw(g);
+            g.Draw(mIcon, mIconOfsX, mIconOfsY);
+        }
+    }
+}

+ 160 - 0
BeefLibs/Beefy2D/src/theme/dark/DarkInfiniteScrollbar.bf

@@ -0,0 +1,160 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.gfx;
+using Beefy.widgets;
+using Beefy.geom;
+
+namespace Beefy.theme.dark
+{
+    public class DarkInfiniteScrollbar : InfiniteScrollbar
+    {
+        /*public class DarkThumb : InfiniteScrollbar.Thumb
+        {               
+            public override void Draw(Graphics g)
+            {
+                base.Draw(g);
+                DarkInfiniteScrollbar scrollbar = (DarkInfiniteScrollbar)mScrollbar;
+                g.DrawButtonVert(scrollbar.GetImage((mMouseOver || mMouseDown) ? DarkTheme.ImageIdx.ScrollbarThumb : DarkTheme.ImageIdx.ScrollbarThumbOver), 0, 0, mHeight);
+            }
+        }*/
+
+        public class DarkArrow : InfiniteScrollbar.Arrow
+        {            
+            public this(bool isBack)
+            {
+                mBack = isBack;
+            }
+
+            public override void Draw(Graphics g)
+            {
+                base.Draw(g);
+
+                DarkInfiniteScrollbar scrollbar = (DarkInfiniteScrollbar)mScrollbar;
+
+                if (mMouseOver && mMouseDown)
+                    g.Draw(scrollbar.GetImage(DarkTheme.ImageIdx.ShortButtonDown));
+                else if (mMouseOver)
+                    g.Draw(scrollbar.GetImage(DarkTheme.ImageIdx.ShortButton));
+
+                if (mBack)
+                {
+                    g.PushScale(1, -1);
+                    g.Draw(scrollbar.GetImage(DarkTheme.ImageIdx.ScrollbarArrow), 0, -DarkTheme.sUnitSize);
+                    g.PopMatrix();
+                }
+                else
+                {
+                    g.Draw(scrollbar.GetImage(DarkTheme.ImageIdx.ScrollbarArrow));
+                }
+            }
+        }
+
+		public override void RehupScale(float oldScale, float newScale)
+		{
+			mBaseSize = DarkTheme.sUnitSize - 1;
+			base.RehupScale(oldScale, newScale);
+		}
+
+        public this()
+        {
+            mBaseSize = DarkTheme.sUnitSize - 1;
+            mDualBarSizeOffset = -2;
+
+            /*mThumb = new DarkThumb();
+            mThumb.mScrollbar = this;
+            AddWidget(mThumb);*/
+            
+            mStartArrow = new DarkArrow(true);
+            mStartArrow.mScrollbar = this;
+            AddWidget(mStartArrow);
+            
+            mEndArrow = new DarkArrow(false);
+            mEndArrow.mScrollbar = this;
+            AddWidget(mEndArrow);
+        }
+
+        public Image GetImage(DarkTheme.ImageIdx image)
+        {
+            return DarkTheme.sDarkTheme.GetImage((DarkTheme.ImageIdx)((int32)image + (int32)DarkTheme.ImageIdx.VertScrollbar - (int32)DarkTheme.ImageIdx.Scrollbar));
+        }
+
+		public override Beefy.geom.Rect GetThumbPos()
+		{
+			float btnMargin = GS!(18);
+			float sizeLeft = (mHeight - btnMargin * 2);
+
+			float pagePct = 0.125f;
+			float thumbSize = Math.Min(sizeLeft, Math.Max(GS!(16), sizeLeft * pagePct));
+
+			bool wasNeg = mScrollThumbFrac < 0;
+			float trackPct = (float)Math.Pow(Math.Abs(mScrollThumbFrac) * 1000, 0.5f) / 200;
+
+			if (wasNeg)
+				trackPct = 0.5f - trackPct;
+			else
+				trackPct = 0.5f + trackPct;
+
+			float thumbPos = Math.Clamp(btnMargin + trackPct*sizeLeft - thumbSize/2, btnMargin, mHeight - btnMargin - thumbSize);
+
+			return Rect(0, thumbPos, mWidth, thumbSize);
+		}
+
+        public override void Draw(Graphics g)
+        {
+            base.Draw(g);
+
+            g.DrawButtonVert(GetImage(DarkTheme.ImageIdx.Scrollbar), 0, 0, mHeight);
+
+			let thumbPos = GetThumbPos();
+			bool isOver = mMouseOver && thumbPos.Contains(mLastMouseX, mLastMouseY);
+
+			using (g.PushColor(0x40000000))
+			{
+				float y0 = mHeight / 2;
+				float y1 = thumbPos.mY + thumbPos.mHeight / 2;
+				g.FillRect(GS!(6), Math.Min(y0, y1), mWidth - GS!(12), Math.Abs(y0 - y1));
+			}
+
+			g.DrawButtonVert(GetImage(isOver ? DarkTheme.ImageIdx.ScrollbarThumb : DarkTheme.ImageIdx.ScrollbarThumbOver), 0, thumbPos.mY, thumbPos.mHeight);
+
+			//mThumb.mVisible = true;
+			//mThumb.Resize(0, btnMargin + thumbPos - (int)(thumbSize*0.5f), mBaseSize, thumbSize);
+        }
+
+        public override float GetAccelFracAt(float dx, float dy)
+        {
+            float btnMargin = GS!(18);
+            float sizeLeft = (mHeight - btnMargin * 2);
+            
+            float trackSize = sizeLeft - GS!(20);
+            return dy / trackSize * 2.0f;
+        }
+
+        public override void ResizeContent()
+        {
+            mStartArrow.Resize(0, 0, mBaseSize, mBaseSize);
+            mEndArrow.Resize(0, mHeight - mBaseSize, mBaseSize, mBaseSize);
+
+            //float btnMargin = GS!(18);
+            //float sizeLeft = (mHeight - btnMargin * 2);
+
+            /*float pagePct = 0.125f;
+            float thumbSize = Math.Min(sizeLeft, Math.Max(GS!(12), sizeLeft * pagePct));
+            float trackSize = sizeLeft + 1; // sizeLeft - ((thumbSize) - (sizeLeft * pagePct)) + 1;
+            float trackPct = ((float)mScrollThumbFrac + 1.0f) * 0.5f;*/
+
+            //float thumbPos = Utils.Lerp(thumbSize * 0.5f, trackSize - (thumbSize * 0.5f), trackPct);
+            
+            //mThumb.mVisible = true;
+            //mThumb.Resize(0, btnMargin + thumbPos - (int)(thumbSize*0.5f), mBaseSize, thumbSize);
+        }
+
+        public override void Resize(float x, float y, float width, float height)
+        {
+            base.Resize(x, y, width, height);
+
+            ResizeContent();
+        }
+    }
+}

+ 1132 - 0
BeefLibs/Beefy2D/src/theme/dark/DarkListView.bf

@@ -0,0 +1,1132 @@
+///
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.gfx;
+using Beefy.theme;
+using Beefy.widgets;
+using Beefy.events;
+using System.Diagnostics;
+using Beefy.geom;
+
+namespace Beefy.theme.dark
+{
+    public class DarkTreeOpenButton : ButtonWidget
+    {
+		public DarkListViewItem mItem;
+        public float mRot = 0;
+        public bool mIsOpen;
+        public bool mAllowOpen = true;
+		public bool mIsReversed;
+        
+        public override void Draw(Graphics g)
+        {
+            base.Draw(g);
+
+            Matrix matrix = Matrix.IdentityMatrix;
+            matrix.Translate(-DarkTheme.sUnitSize/2, -DarkTheme.sUnitSize/2);
+			if (mIsReversed)
+            	matrix.Rotate(-mRot);
+			else
+				matrix.Rotate(mRot);
+            matrix.Translate(DarkTheme.sUnitSize/2, DarkTheme.sUnitSize/2);
+
+            using (g.PushMatrix(matrix))
+                g.Draw(DarkTheme.sDarkTheme.mTreeArrow);
+        }
+        
+        public override void MouseDown(float x, float y, int32 btn, int32 btnCount)
+        {
+            bool wasOpen = mIsOpen;
+            base.MouseDown(x, y, btn, btnCount);
+            if (wasOpen == mIsOpen)
+                Open(!mIsOpen, false);
+        }        
+
+        public void Open(bool open, bool immediate)
+        {
+            if ((open) && (!mAllowOpen))
+                return;
+            mIsOpen = open;
+            if (immediate)
+                mRot = mIsOpen ? (Math.PI_f / 2) : 0;
+        }
+
+        public override void Update()
+        {
+            base.Update();
+
+            int childCount = mItem.mChildItems.Count;
+
+            float rotSpeed = 0.12f + (1.0f / (childCount + 1));
+
+            if ((mIsOpen) && (mRot < Math.PI_f / 2))
+            {
+                mRot = Math.Min(Math.PI_f / 2, mRot + rotSpeed);
+                mItem.mListView.mListSizeDirty = true;
+				MarkDirty();
+            }
+            else if ((!mIsOpen) && (mRot > 0))
+            {
+                mRot = (float)Math.Max(0, mRot - rotSpeed);
+                mItem.mListView.mListSizeDirty = true;
+				MarkDirty();
+            }
+
+            float x;
+            float y;
+            SelfToOtherTranslate(mItem.mListView, 0, 0, out x, out y);
+            if (mItem.mListView.mColumns.Count > 0)
+                mVisible = x + 8 < mItem.mListView.mColumns[0].mWidth;
+            else
+                mVisible = true;
+        }
+
+        public override void MouseMove(float x, float y)
+        {
+            base.MouseMove(x, y);
+
+        }
+    }    
+
+    public class DarkListViewItem : ListViewItem, IDragInterface
+    {        
+        public float mChildIndent;
+        public DarkTreeOpenButton mOpenButton;
+        public uint32 mTextColor = Color.White;
+        public uint32 mFocusColor = DarkTheme.COLOR_MENU_FOCUSED;
+        public uint32 mSelectColor = DarkTheme.COLOR_MENU_SELECTED;
+        public float mTextAreaLengthOffset;        
+
+        public DragEvent mCurDragEvent ~ delete _;
+        public DragHelper mDragHelper ~ delete _;
+        public DarkListViewItem mDragTarget;
+        public int32 mDragTargetInsertDir;
+        public bool mOpenOnDoubleClick = true;
+        public bool mIsBold;
+
+		public bool AllowDragging
+		{
+			get
+			{
+				return mDragHelper != null;
+			}
+
+			set
+			{
+				if (value)
+				{
+					if (mDragHelper == null)
+					{
+						mDragHelper = new DragHelper(this, this);
+						mDragHelper.mMinDownTicks = 15;
+						mDragHelper.mTriggerDist = 2;
+					}
+				}
+				else
+				{
+					DeleteAndNullify!(mDragHelper);
+				}
+			}
+		}
+
+        public override float LabelX 
+        { 
+            get 
+            {
+                float x;
+
+                if (mColumnIdx == 0)
+                    x = ((DarkListView)mListView).mLabelX;
+                else
+                    x = 6;
+                float absX;
+                float absY;
+                SelfToOtherTranslate(mListView, x, 0, out absX, out absY);
+                return absX;
+            } 
+        }
+
+        public override float LabelWidth
+        {
+            get
+            {
+                DarkListView listView = (DarkListView)mListView;
+                float labelX = (mColumnIdx == 0) ? ((DarkListView)mListView).mLabelX : 6;
+                float calcWidth = 0;
+                for (int32 i = mColumnIdx; i < mListView.mColumns.Count; i++)
+                {
+                    if ((mSubItems != null) && (i > mColumnIdx) && (i < mSubItems.Count) && (mSubItems[i] != null))                    
+                        break;
+                    calcWidth += mListView.mColumns[i].mWidth;
+                }
+                if (mColumnIdx == 0)
+                    calcWidth -= (mDepth - 1) * listView.mChildIndent;
+                return calcWidth - labelX;
+            }
+        }
+
+        public this()
+        {
+            
+        }
+
+		public override void Init(ListView listView)
+		{
+			var darkListView = (DarkListView)listView;
+			if (darkListView.mFont != null)
+				mSelfHeight = darkListView.mFont.GetLineSpacing();
+		}
+
+		public override void RehupScale(float oldScale, float newScale)
+		{
+			if (mOpenButton != null)
+			{
+				mOpenButton.Resize(((DarkListView)mListView).mOpenButtonX, 0, DarkTheme.sUnitSize, DarkTheme.sUnitSize);
+			}
+
+			base.RehupScale(oldScale, newScale);
+			var listView = (DarkListView)mListView;
+			if (mX != 0)
+				mX = listView.mChildIndent;
+			if ((listView.mFont != null) && (mSelfHeight != 0))
+				mSelfHeight = listView.mFont.GetLineSpacing();
+			Utils.RoundScale(ref mBottomPadding, newScale / oldScale);
+
+			if (mChildItems != null)
+			{
+				for (var child in mChildItems)
+					child.RehupScale(oldScale, newScale);
+			}
+		}
+
+        protected virtual float GetLabelOffset()
+        {
+            return 0;
+        }
+
+		public virtual bool WantsTooltip(float mouseX, float mouseY)
+		{
+		    // The default tooltip behavior is to show a tooltip if the text is obscured-
+		    //  meaning either offscreen or in a column that's too small
+
+		    if (mLabel == null)
+		        return false;
+
+		    var ideListView = (DarkListView)mListView;
+
+		    float x = 8;
+		    if (mColumnIdx == 0)
+		        x = LabelX + 8;
+		    float textWidth = ideListView.mFont.GetWidth(mLabel);
+		    bool isObscured = false;
+		    if (mColumnIdx < ideListView.mColumns.Count - 1)
+		    {
+		        float maxWidth = ideListView.mColumns[mColumnIdx].mWidth;
+		        isObscured |= (x + textWidth + 8 >= maxWidth);
+		    }
+
+		    float parentEndX;
+		    float parentEndY;
+		    SelfToOtherTranslate(ideListView.mScrollContentContainer, x + textWidth, 0, out parentEndX, out parentEndY);
+		    isObscured |= (parentEndX > ideListView.mScrollContentContainer.mWidth);
+
+		    return isObscured;
+		}
+
+		public virtual void ShowTooltip(float mouseX, float mouseY)
+		{
+		    float x = GS!(8);
+		    if (mColumnIdx == 0)
+		        x = LabelX + GS!(8);
+		    DarkTooltipManager.ShowTooltip(mLabel, this, x, mHeight);
+		}
+
+        public override void Draw(Graphics g)
+        {
+            base.Draw(g);
+
+            /*if (mDepth != 0)
+            {
+                using (g.PushColor(0x400000FF))
+                    g.FillRect(1, 1, mWidth - 2, mHeight - 2);
+            }*/
+
+            DarkListView listView = (DarkListView)mListView;
+            float labelOfs = GetLabelOffset();
+            float labelX = labelOfs + ((mColumnIdx == 0) ? ((DarkListView)mListView).mLabelX : GS!(6));
+            DarkListView darkListView = (DarkListView)listView;
+            g.SetFont(mIsBold ? darkListView.mBoldFont : darkListView.mFont);
+
+            int32 nextContentColumn = -1;
+            float calcWidth = 0;
+            for (int32 i = mColumnIdx; i < mListView.mColumns.Count; i++)
+            {
+                if ((mSubItems != null) && (i > mColumnIdx) && (i < mSubItems.Count) && (mSubItems[i] != null))
+                {
+                    nextContentColumn = i;
+                    break;
+                }
+                calcWidth += mListView.mColumns[i].mWidth;
+            }
+
+            if (Selected)
+            {
+                /*float lastStrWidth = labelX + g.mFont.GetWidth(mLabel);
+
+                for (int i = 1; i < mListView.mColumns.Count; i++)
+                {
+                    if ((mSubItems != null) && (i > mColumnIdx) && (i < mSubItems.Count) && (mSubItems[i] != null))
+                    {
+                        DarkListViewItem subItem = (DarkListViewItem) mSubItems[i];
+                        if (subItem.mLabel != null)
+                            lastStrWidth = subItem.mX - mX + g.mFont.GetWidth(subItem.mLabel) - 6;
+                        //g.FillRect(subItem.mX - mX, subItem.mY - mY, 3, 3);
+                    }
+                }
+                
+                using (g.PushColor(mSelectColor))
+                    g.DrawButton(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.MenuSelect), 4 + listView.mHiliteOffset, 0, Math.Max(lastStrWidth + 16, mWidth - 4 * 2 - listView.mHiliteOffset));*/
+            }
+            
+            if (mIconImage != null)
+            {
+                IDisposable colorScope = null;
+                if (mIconImageColor != 0)
+                    colorScope = g.PushColor(mIconImageColor);
+                g.Draw(mIconImage, listView.mIconX + labelOfs, 0);
+                if (colorScope != null)
+                    colorScope.Dispose();
+            }
+
+            if (mLabel != null)
+            {
+                float wantWidth = calcWidth - labelX;
+                if (mColumnIdx == 0)
+                    wantWidth -= (mDepth - 1) * listView.mChildIndent;
+                wantWidth += GS!(mTextAreaLengthOffset);
+
+				if ((listView.mEndInEllipsis) && (nextContentColumn == -1) && (listView.mVertScrollbar == null))
+				{
+					wantWidth = mListView.mWidth - labelX - mX;
+					if (listView.mInsets != null)
+						wantWidth -= listView.mInsets.mRight;
+				}							 
+
+                using (g.PushColor(mTextColor))
+                    g.DrawString(mLabel, labelX, 0, .Left, wantWidth, ((nextContentColumn != -1) || (listView.mEndInEllipsis)) ? .Ellipsis : .Overflow);
+            }
+        }
+
+        public override void MouseDown(float x, float y, int32 btn, int32 btnCount)
+        {
+            base.MouseDown(x, y, btn, btnCount);
+
+            if ((btn == 0) && (btnCount > 1) && (mOpenOnDoubleClick) && (mOpenButton != null))
+                mOpenButton.Open(!mOpenButton.mIsOpen, false);
+        }
+
+        public override void MakeParent()
+        {
+            if ((mChildItems == null) && (mDepth > 0))
+            {
+                DarkTreeOpenButton treeOpenButton = new DarkTreeOpenButton();
+                treeOpenButton.mItem = this;
+                AddWidget(treeOpenButton);
+                treeOpenButton.Resize(((DarkListView)mListView).mOpenButtonX, 0, DarkTheme.sUnitSize, DarkTheme.sUnitSize);
+                mOpenButton = treeOpenButton;
+            }
+
+            base.MakeParent();
+        }
+
+		public override void TryUnmakeParent()
+		{
+			base.TryUnmakeParent();
+			if ((mChildItems == null) && (mOpenButton != null))
+			{
+				mOpenButton.RemoveSelf();
+				DeleteAndNullify!(mOpenButton);
+			}
+		}
+
+        public override ListViewItem CreateChildItem()
+        {
+            MakeParent();
+            DarkListViewItem child = (DarkListViewItem)base.CreateChildItem();            
+            if (mDepth != 0)
+                child.mX = ((DarkListView)mListView).mChildIndent;
+            return child;
+        }
+
+        public override void AddChildAtIndex(int index, ListViewItem item)
+        {
+            base.AddChildAtIndex(index, item);
+            if (mDepth != 0)
+                item.mX = ((DarkListView)mListView).mChildIndent;
+            else
+                item.mX = 0;
+        }        
+
+        void UpdateShowChildPct()
+        {
+            // Fast to slow
+            if (mOpenButton.mIsOpen)            
+                mShowChildPct = Math.Min((float)Math.Sin(mOpenButton.mRot) + 0.001f, 1.0f);            
+            else            
+                mShowChildPct = Math.Max(1.0f - Math.Sin(Math.PI_f / 2 - mOpenButton.mRot) - 0.001f, 0.0f);            
+        }
+
+        public override float CalculatedDesiredHeight()
+        {            
+            mShowChildPct = 1.0f;
+            
+            if (mOpenButton != null)
+            {
+                UpdateShowChildPct();
+                if (mShowChildPct >= 0.999f)
+                    mShowChildPct = 1.0f;
+            }
+
+			mChildAreaHeight = 0;
+			if ((mChildItems != null) && (mShowChildPct > 0))
+			{
+			    for (ListViewItem listViewItem in mChildItems)
+			        mChildAreaHeight += listViewItem.CalculatedDesiredHeight();
+				mChildAreaHeight *= mShowChildPct;
+			}
+
+			return mChildAreaHeight + mSelfHeight + mBottomPadding;
+
+            //return base.CalculatedDesiredHeight();
+        }
+
+        public override ListViewItem CreateChildItemAtIndex(int idx)
+        {
+            var child = base.CreateChildItemAtIndex(idx);
+            if (mOpenButton != null)
+                UpdateShowChildPct();
+            child.mVisible = mShowChildPct > 0.0f;
+            return child;
+        }
+
+        public override Widget FindWidgetByCoords(float x, float y)
+        {
+            if ((mShowChildPct < 1.0f) && (y > mHeight))
+                return null;
+
+            let result = base.FindWidgetByCoords(x, y);
+
+			if (mChildItems != null)
+			{
+				if ((y >= 0) && (y < mChildAreaHeight + mSelfHeight + mBottomPadding))
+				{
+					int itemStart = Math.Max(0, FindItemAtY(y) - 1);
+					int itemEnd = Math.Min(itemStart + 1, mChildItems.Count);
+
+					for (int childIdx = itemStart; childIdx < itemEnd; childIdx++)
+					{
+						let child = mChildItems[childIdx];
+					    child.ParentToSelfTranslate(x, y, var childX, var childY);
+					    Widget foundWidget = child.FindWidgetByCoords(childX, childY);
+					    if (foundWidget != null)
+					        return foundWidget;
+					}
+				}
+			}
+
+			return result;
+        }
+
+		public bool IsVisible(Graphics g)
+		{
+			float checkY = g.mMatrix.ty;            
+
+			float minY = g.mClipRect.Value.mY;
+			float maxY = minY + g.mClipRect.Value.mHeight;
+
+			if ((checkY + Math.Max(mHeight, mChildAreaHeight) < minY) || (checkY > maxY))
+			    return false;
+			return true;
+		}
+
+        void DrawLinesGrid(Graphics g)
+        {
+            var listView = (DarkListView)mListView;
+            float originX;
+            float originY;
+            SelfToOtherTranslate(listView, 0, 0, out originX, out originY);
+
+            for (int32 i = 0; i < mChildItems.Count; i++)
+            {
+                var childItem = mChildItems[i];
+				if (childItem.mHeight == 0)
+					continue;
+                using (g.PushColor(listView.mGridLinesColor))
+                {
+                    float linePos = childItem.mY + childItem.mHeight;
+                    if (linePos <= mChildAreaHeight + mSelfHeight)
+                        g.FillRect(-originX, linePos, listView.mWidth, 1);
+                }
+            }
+        }
+
+        public virtual void DrawSelect(Graphics g)
+        {
+            var listView = (DarkListView)mListView;
+            float labelOfs = GetLabelOffset();
+            float labelX = labelOfs + ((mColumnIdx == 0) ? ((DarkListView)mListView).mLabelX : 6);
+            float lastStrWidth = labelX + g.mFont.GetWidth(mLabel);
+
+            for (int32 i = 1; i < mListView.mColumns.Count; i++)
+            {
+                if ((mSubItems != null) && (i > mColumnIdx) && (i < mSubItems.Count) && (mSubItems[i] != null))
+                {
+                    DarkListViewItem subItem = (DarkListViewItem)mSubItems[i];
+                    if (subItem.mLabel != null)
+                        lastStrWidth = subItem.mX - mX + g.mFont.GetWidth(subItem.mLabel) - GS!(6);
+                }
+            }
+
+			float hiliteX = GS!(4) + listView.mHiliteOffset;
+			
+
+			if (mListView.mColumns.Count > 0)
+			{
+				float adjust = LabelX - mListView.mColumns[0].mWidth;
+
+				if (adjust > 0)
+				{
+					hiliteX -= adjust;
+				}
+			}
+
+            uint32 color = Focused ? mFocusColor : mSelectColor;
+            using (g.PushColor(color))
+                g.DrawButton(DarkTheme.sDarkTheme.GetImage(Focused ? DarkTheme.ImageIdx.MenuSelect : DarkTheme.ImageIdx.MenuNonFocusSelect),
+                    hiliteX, 0, Math.Max(lastStrWidth + GS!(16), mWidth - GS!(4) - hiliteX));
+        }
+
+		int FindItemAtY(float y)
+		{
+			int lo = 0;
+		    int hi = mChildItems.Count - 1;
+			
+			while (lo <= hi)
+		    {
+		        int i = (lo + hi) / 2;
+				let midVal = mChildItems[i];
+				float c = midVal.mY - y;
+
+		        if (c == 0) return i;
+		        if (c < 0)
+		            lo = i + 1;                    
+		        else
+		            hi = i - 1;
+			}
+			return lo;
+		}
+
+		protected virtual void DrawChildren(Graphics g, int itemStart, int itemEnd)
+		{
+			for (int childIdx = itemStart; childIdx < itemEnd; childIdx++)
+			{
+				if (childIdx >= mChildItems.Count)
+					break;
+				let child = mChildItems[childIdx];
+			    g.PushTranslate(child.mX, child.mY);
+			    child.DrawAll(g);
+			    g.PopMatrix();
+			}
+		}
+
+		void DrawChildren(Graphics g)
+		{
+			base.DrawAll(g);
+
+			if (mChildItems != null)
+			{
+				//mChildItems.BinarySearch()
+				float drawStartY = g.mClipRect.mValue.mY - g.mMatrix.ty;
+				float drawEndY = g.mClipRect.mValue.Bottom - g.mMatrix.ty;
+				int itemStart = 0;
+				if (drawStartY > 0)
+					itemStart = Math.Max(0, FindItemAtY(drawStartY) - 1);
+				int itemEnd = FindItemAtY(drawEndY);
+
+				if (itemStart < itemEnd)
+					DrawChildren(g, itemStart, itemEnd);
+			}
+		}
+
+        public override void DrawAll(Graphics g)
+        {
+            if ((!mVisible) || (mHeight <= 0))
+                return;
+
+            var listView = (DarkListView)mListView;
+
+            if (Selected)
+            {
+                DrawSelect(g);
+            }
+            
+            if ((mShowChildPct < 1.0f) && (mShowChildPct > 0.0f))            
+            {
+                using (g.PushClip(0, 0, mWidgetWindow.mClientWidth, mSelfHeight + mChildAreaHeight))
+                {
+                    if ((listView.mShowGridLines) && (mShowChildPct > 0) && (mChildItems != null))
+                        DrawLinesGrid(g);
+                    DrawChildren(g);
+                }
+                return;
+            }
+
+            if ((listView.mShowGridLines) && (mShowChildPct > 0) && (mChildItems != null))
+                DrawLinesGrid(g);
+
+			DrawChildren(g);
+            
+            if (mDragTarget != null)
+            {
+                float targetX;
+                float targetY;
+                mDragTarget.SelfToOtherTranslate(mListView, 0, 0, out targetX, out targetY);
+
+                float curX;
+                float curY;
+                SelfToOtherTranslate(mListView, 0, 0, out curX, out curY);
+
+                bool wasTargetBelowBottom = false;
+
+                using (g.PushTranslate(-curX, -curY))
+                {
+                    /*if ((mUpdateCnt % 60) == 0)
+                        Debug.WriteLine(String.Format("{0} indent {1}", mDragTarget.mLabel, mDragTarget.mDepth));*/
+
+                    if (mDragTargetInsertDir >= 0) // Inside or after
+                        targetY += mDragTarget.mSelfHeight;
+                    
+                    if (mDragTargetInsertDir > 0) // After
+                        targetY += mDragTarget.mChildAreaHeight + mDragTarget.mBottomPadding;
+
+                    if (mDragTargetInsertDir == 0) // Inside
+                        targetX += ((DarkListView)mListView).mChildIndent + mDragTarget.mChildIndent;
+
+                    if (-curY + targetY > mHeight)                    
+                        wasTargetBelowBottom = true;                    
+
+                    using (g.PushColor(0xFF95A68F))
+                        g.FillRect(targetX + 4, targetY, mListView.mWidth - targetX - 28, 2);
+                }
+
+                if (wasTargetBelowBottom)
+                {
+                    /*Image img = DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.MoveDownArrow);
+                    g.Draw(img, mWidth / 2 - img.mWidth / 2, mHeight - img.mHeight);*/
+                }
+            }
+        }
+
+        public override bool Open(bool open, bool immediate = false)
+        {
+            if ((mOpenButton != null) && (mOpenButton.mIsOpen != open))
+            {
+                mOpenButton.Open(open, immediate);
+                return true;
+            }
+            return false;
+        }
+
+        public void DragStart()
+        {            
+        }
+
+        public void DragEnd()
+        {            
+            DarkListView listView = (DarkListView)mListView;
+
+            if (mCurDragEvent != null)
+            {
+                DragEvent e = mCurDragEvent;
+                e.mSender = GetMainItem();
+                e.mDragTarget = mDragTarget;                
+                if (mDragHelper.mAborted)
+                    e.mDragAllowed = false;
+                if (listView.mOnDragEnd.HasListeners)
+                    listView.mOnDragEnd(e);
+            }
+            mDragTarget = null;
+        }
+
+        public void MouseDrag(float x, float y, float dX, float dY)
+        {
+            float aX = x;
+            float aY = y;
+
+            ListViewItem head = this;
+            if (mColumnIdx != 0)
+                head = head.GetSubItem(0);
+
+            if (!head.Selected)
+                return;
+
+            DarkListView listView = (DarkListView)mListView;
+
+            if (listView.mOnDragUpdate.HasListeners)
+            {                
+                SelfToRootTranslate(x, y, out aX, out aY);
+                Widget foundWidget = mWidgetWindow.mRootWidget.FindWidgetByCoords(aX, aY);
+
+                if (foundWidget == null)
+                {                                        
+                    return;
+                }
+
+                var root = mListView.GetRoot();
+                if ((foundWidget is ListView) || (foundWidget == root))
+                {                    
+                    var lastItem = root.mChildItems[root.mChildItems.Count - 1];
+                    float lastWindowX;
+                    float lastWindowY;
+                    lastItem.SelfToRootTranslate(lastItem.mSelfHeight + lastItem.mChildAreaHeight + lastItem.mBottomPadding, 0, out lastWindowX, out lastWindowY);
+                    if (aY > lastWindowY)
+                    {
+                        // After last item
+                        foundWidget = lastItem;
+                    }
+                }
+
+                var listViewItem = foundWidget as DarkListViewItem;
+                if (listViewItem == null)
+                    listViewItem = foundWidget.mParent as DarkListViewItem;
+                if (listViewItem == null)
+                    return;
+
+                if (listViewItem.mColumnIdx != 0)
+                    listViewItem = (DarkListViewItem)listViewItem.GetSubItem(0);
+                foundWidget = listViewItem;                
+
+                float childX;
+                float childY;
+                foundWidget.SelfToRootTranslate(0, 0, out childX, out childY);
+                
+                float yOfs = aY - childY;
+                if (yOfs < mHeight / 2)
+                    mDragTargetInsertDir = -1;
+                else
+                {
+                    mDragTargetInsertDir = 1;
+                    if ((listViewItem.mOpenButton != null) && (listViewItem.mOpenButton.mIsOpen))
+                        mDragTargetInsertDir = 0;
+                }
+
+				delete mCurDragEvent;
+                mCurDragEvent = new DragEvent();
+                mCurDragEvent.mX = x;
+                mCurDragEvent.mY = y;
+                mCurDragEvent.mSender = head;
+                mCurDragEvent.mDragTarget = foundWidget;
+                mCurDragEvent.mDragTargetDir = mDragTargetInsertDir;
+                listView.mOnDragUpdate(mCurDragEvent);                
+                mDragTargetInsertDir = mCurDragEvent.mDragTargetDir;
+
+                mDragTarget = mCurDragEvent.mDragTarget as DarkListViewItem;
+                if (!mCurDragEvent.mDragAllowed)
+                    mDragTarget = null;
+
+                if (mDragTarget != null)
+                {
+                    if (mDragTarget == mListView.GetRoot())
+                    {
+                        mDragTarget = null;
+                    }
+                    else
+                    {
+                        if (mDragTarget.mColumnIdx != 0)
+                            mDragTarget = (DarkListViewItem)mDragTarget.GetSubItem(0);      
+                    }
+                }
+            }
+        }
+
+        public override void Update()
+        {
+            base.Update();
+            mDragHelper?.Update();
+
+			if ((DarkTooltipManager.sTooltip != null) && (DarkTooltipManager.sTooltip.mRelWidget == this))
+			{
+			    Point mousePoint;
+			    DarkTooltipManager.CheckMouseover(this, -1, out mousePoint);
+			    if (!WantsTooltip(mousePoint.x, mousePoint.y))
+			        DarkTooltipManager.CloseTooltip();
+			}
+
+			if (DarkTooltipManager.sLastMouseWidget == this)
+			{
+			    Point mousePoint;
+			    if (DarkTooltipManager.CheckMouseover(this, 20, out mousePoint))
+			    {
+			        if (WantsTooltip(mousePoint.x, mousePoint.y))                    
+			            ShowTooltip(mousePoint.x, mousePoint.y);
+			        else
+			            DarkTooltipManager.CloseTooltip();
+			    }
+			}
+        }
+
+		public override void UpdateAll()
+		{
+			if (mVisible)
+			{
+				base.UpdateAll();
+
+				if (mChildItems != null)
+				{
+					for (int32 anIdx = 0; anIdx < mChildItems.Count; anIdx++)
+					{
+					    Widget child = mChildItems[anIdx];
+					    Debug.Assert(child.mParent == this);
+					    Debug.Assert(child.mWidgetWindow == mWidgetWindow);
+					    child.UpdateAll();
+					}
+				}
+			}
+		}
+    }
+
+    public class DarkListView : ListView
+    {
+		public struct SortType
+		{
+			public int mColumn;
+			public bool mReverse;
+
+			public this()
+			{
+				mColumn = -1;
+				mReverse = false;
+			}
+
+			public this(int column, bool reverse)
+			{
+				mColumn = column;
+				mReverse = reverse;
+			}
+		}
+
+        public float mHeaderSplitIdx = -1;        
+        public float mDragOffset = 0;
+        public bool mShowColumnGrid;
+        public bool mShowGridLines;
+		public Color mGridLinesColor = 0x0CFFFFFF;
+        public bool mShowHeader = true;
+		public bool mEndInEllipsis;
+        public float mLabelX = DarkTheme.sUnitSize;        
+        public float mChildIndent = DarkTheme.sUnitSize;
+        public float mOpenButtonX = 0;
+        public float mIconX = GS!(4);
+        public float mHiliteOffset;        
+        public Font mFont;
+        public Font mBoldFont;
+        public DarkTheme.ImageIdx mHeaderImageIdx = DarkTheme.ImageIdx.ListViewHeader;
+        public float mHeaderLabelYOfs = 0;
+		public SortType mSortType = SortType() ~ { mSortType.mColumn = -1; };
+		public Insets mInsets ~ delete _;
+
+        public Event<Action<DragEvent>> mOnDragUpdate ~ _.Dispose();
+        public Event<Action<DragEvent>> mOnDragEnd ~ _.Dispose();
+
+        public this()
+        {
+            mFont = DarkTheme.sDarkTheme.mSmallFont;
+            mBoldFont = DarkTheme.sDarkTheme.mSmallBoldFont;
+            SetShowHeader(true);
+			
+        }
+
+        public void SetShowHeader(bool showHeader)
+        {
+            mShowHeader = showHeader;
+            SetScaleData();
+        }
+
+		protected virtual void SetScaleData()
+		{
+			mHeaderHeight = mShowHeader ? DarkTheme.sUnitSize : 0;
+			mScrollbarInsets.Set(mShowHeader ? GS!(18) : 0, 0, 0, 0);
+			mScrollContentInsets.Set(GS!(2), 0, GS!(-1), GS!(-1));
+			mLabelX = DarkTheme.sUnitSize;
+			mChildIndent = DarkTheme.sUnitSize;
+			mListSizeDirty = true;
+		}
+
+        public override void InitScrollbars(bool wantHorz, bool wantVert)
+        {
+            if (!wantHorz)
+                mScrollContentInsets.mBottom += GS!(2);
+
+            base.InitScrollbars(wantHorz, wantVert);
+        }
+
+        protected override ListViewItem CreateListViewItem()
+        {
+            DarkListViewItem anItem = new DarkListViewItem();            
+            return anItem;
+        }
+
+        float GetMaxContentWidth(float xOfs, DarkListViewItem listViewItem)
+        {            
+            float maxWidth = 0;
+            if ((listViewItem.mSubItems != null) && (listViewItem.mSubItems.Count > 1))
+            {
+                var subItem = listViewItem.mSubItems[listViewItem.mSubItems.Count - 1];
+                if (subItem.mLabel != null)
+                    maxWidth = mFont.GetWidth(subItem.mLabel) + xOfs + listViewItem.mX + subItem.mX + 6;
+            }
+            else if (listViewItem.mLabel != null)
+            {
+                maxWidth = mFont.GetWidth(listViewItem.mLabel) + xOfs + listViewItem.mX + mLabelX;
+            }
+
+            if ((listViewItem.mChildItems != null) && (listViewItem.mChildAreaHeight > 0))
+            {
+                for (var child in listViewItem.mChildItems)
+                    maxWidth = Math.Max(maxWidth, GetMaxContentWidth(listViewItem.mX + xOfs, (DarkListViewItem)child));
+            }
+
+            return maxWidth;
+        }
+
+        public override float GetListWidth()
+        {
+            float listWidth = base.GetListWidth();
+            float maxContentWidth = GetMaxContentWidth(0, (DarkListViewItem)mRoot) + 6;
+            return Math.Max(listWidth, maxContentWidth);
+        }
+
+        protected virtual void DrawColumnGridColumn(Graphics g, float x, float y, float height, uint32 color)
+        {
+            using (g.PushColor(color))
+                g.FillRect(x, y, 1, height);
+        }
+
+		public virtual void DrawColumn(Graphics g, int32 columnIdx)
+		{
+			var column = mColumns[columnIdx];
+			float drawXOfs = GS!(6);
+			float drawWidth = column.mWidth - drawXOfs - GS!(6);
+			g.DrawString(column.mLabel, drawXOfs, mHeaderLabelYOfs + GS!(2), FontAlign.Left, drawWidth, (columnIdx < mColumns.Count - 1) ? FontOverflowMode.Ellipsis : FontOverflowMode.Overflow);
+			
+			if (columnIdx != 0)
+			{
+			    g.Draw(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.Seperator), GS!(-2), mHeaderLabelYOfs);
+			}
+
+			if ((mShowColumnGrid) && (columnIdx < mColumns.Count - 1))
+			{
+			    DrawColumnGridColumn(g, column.mWidth, DarkTheme.sUnitSize, mHeight - DarkTheme.sUnitSize - 1, 0xFF707070);
+			}
+
+			float sortArrowX = g.mFont.GetWidth(column.mLabel) + DarkTheme.sUnitSize/2;
+			if (columnIdx == mSortType.mColumn)
+			{
+				if (!mSortType.mReverse)
+				{
+					using (g.PushScale(1.0f, -1.0f, column.mWidth - DarkTheme.sUnitSize, DarkTheme.sUnitSize/2))
+						g.Draw(DarkTheme.sDarkTheme.GetImage(.ListViewSortArrow), sortArrowX, 0);
+				}
+				else
+			        g.Draw(DarkTheme.sDarkTheme.GetImage(.ListViewSortArrow), sortArrowX, 0);
+			}
+		}
+
+        public override void Draw(Graphics g)
+        {
+            base.Draw(g);
+
+			if (mWidth <= GS!(3))
+				return;
+
+            if (mShowHeader)
+            {
+                if (mHeaderImageIdx < DarkTheme.ImageIdx.COUNT)
+                    g.DrawButton(DarkTheme.sDarkTheme.GetImage(mHeaderImageIdx), 0, 0, mWidth);
+            
+                using (g.PushClip(1, 0, mWidth - GS!(2), mHeight))
+                {
+                    using (g.PushTranslate(mScrollContent.mX, 0))
+                    {
+                        g.SetFont(DarkTheme.sDarkTheme.mHeaderFont);
+
+                        float curX = 0;
+                        for (int32 columnIdx = 0; columnIdx < mColumns.Count; columnIdx++)
+                        {
+                            ListViewColumn column = mColumns[columnIdx];
+							using (g.PushTranslate(curX, 0))
+								DrawColumn(g, columnIdx);
+							curX += column.mWidth;
+                        }
+                    }
+                }
+            }
+            else if (mShowColumnGrid)
+            {                
+                using (g.PushClip(1, 0, mWidth - GS!(2), mHeight))
+                {
+                    using (g.PushTranslate(mScrollContent.mX, 0))
+                    {                        
+                        float curX = 0;
+                        for (int32 columnIdx = 0; columnIdx < mColumns.Count; columnIdx++)
+                        {
+                            ListViewColumn column = mColumns[columnIdx];                            
+                            curX += column.mWidth;                            
+
+                            if ((mShowColumnGrid) && (columnIdx < mColumns.Count - 1))
+                            {
+                                DrawColumnGridColumn(g, curX, GS!(4), mHeight - GS!(8), 0xFF888888);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        public override void MouseMove(float x, float y)
+        {
+            base.MouseMove(x, y);
+
+			float useX = x - mScrollContent.mX;
+			float useY = y;
+
+            if (mMouseDown)
+            {
+                float curX = 0;
+                for (int32 columnIdx = 0; columnIdx < mColumns.Count - 1; columnIdx++)
+                {
+                    ListViewColumn column = mColumns[columnIdx];
+                    
+                    if (columnIdx == mHeaderSplitIdx)
+                    {
+                        mColumns[columnIdx].mWidth = useX - curX - mDragOffset;
+                        if (mColumns[columnIdx].mMinWidth != 0)
+                            mColumns[columnIdx].mWidth = (float) Math.Max(mColumns[columnIdx].mMinWidth, mColumns[columnIdx].mWidth);
+                        if (mColumns[columnIdx].mMaxWidth != 0)
+                            mColumns[columnIdx].mWidth = (float)Math.Min(mColumns[columnIdx].mMaxWidth, mColumns[columnIdx].mWidth);
+                        ColumnResized(mColumns[columnIdx]);
+                        mListSizeDirty = true;
+                    }
+
+                    curX += column.mWidth;
+                }
+            }
+            else
+            {
+                mHeaderSplitIdx = -1;
+
+                if ((useY >= 0) && ((useY < mHeaderHeight) || (mShowColumnGrid)))
+                {
+                    float curX = 0;
+                    for (int32 columnIdx = 0; columnIdx < mColumns.Count - 1; columnIdx++)
+                    {
+                        ListViewColumn column = mColumns[columnIdx];
+                        curX += column.mWidth;
+
+                        if (Math.Abs(useX - curX + 1) <= GS!(3))
+                        {
+                            mHeaderSplitIdx = columnIdx;
+                            mDragOffset = useX - curX;
+                        }
+                    }
+                }
+
+                if (mHeaderSplitIdx != -1)
+                    BFApp.sApp.SetCursor(Cursor.SizeWE);
+                else
+                    BFApp.sApp.SetCursor(Cursor.Pointer);
+            }
+        }
+
+		public (int columnIdx, bool isSplitter) GetColumnAt(float x, float y)
+		{
+			float useX = x - mScrollContent.mX;
+			float useY = y;
+
+			if ((useY >= 0) && (useY < mHeaderHeight))
+			{
+			    float curX = 0;
+			    for (int32 columnIdx = 0; columnIdx < mColumns.Count; columnIdx++)
+			    {
+			        ListViewColumn column = mColumns[columnIdx];
+			        curX += column.mWidth;
+
+			        if (Math.Abs(useX - curX + 1) <= 3)
+			        {
+			            return (columnIdx, true);
+			        }
+
+					if (useX <= curX)
+						return (columnIdx, false);
+			    }
+			}
+
+			return (-1, false);
+		}
+
+        public override void MouseDown(float x, float y, int32 btn, int32 btnCount)
+        {
+            base.MouseDown(x, y, btn, btnCount);
+
+            let (column, isSplitter) = GetColumnAt(x, y);
+			if ((column != -1) && (!isSplitter))
+			{
+				var newSortType = mSortType;
+				if (newSortType.mColumn == column)
+				{
+					newSortType.mReverse = !newSortType.mReverse;
+				}
+				else
+				{
+					newSortType.mColumn = column;
+				}
+				ChangeSort(newSortType);
+			}
+        }
+
+		public virtual void ChangeSort(SortType sortType)
+		{
+
+		}
+
+        public override void MouseUp(float x, float y, int32 btn)
+        {
+            base.MouseUp(x, y, btn);
+            MouseMove(x, y);
+        }
+
+        public override void MouseLeave()
+        {
+            base.MouseLeave();
+            BFApp.sApp.SetCursor(Cursor.Pointer);
+        }
+
+		public override void RehupScale(float oldScale, float newScale)
+		{
+			float valScale = newScale / oldScale;
+			for (var column in mColumns)
+			{
+				Utils.RoundScale(ref column.mWidth, valScale);
+				Utils.RoundScale(ref column.mMinWidth, valScale);
+				Utils.RoundScale(ref column.mMaxWidth, valScale);
+			}
+
+			mListSizeDirty = true;
+			mIconX *= valScale;
+			mScrollbarInsets.Scale(valScale);
+			SetScaleData();
+
+			base.RehupScale(oldScale, newScale);
+		}
+    }
+}

+ 408 - 0
BeefLibs/Beefy2D/src/theme/dark/DarkMenu.bf

@@ -0,0 +1,408 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.widgets;
+using Beefy.gfx;
+using System.Diagnostics;
+
+namespace Beefy.theme.dark
+{
+    public class DarkMenuItem : MenuItemWidget
+    {                
+        int32 mSelectedTicks;
+        int32 mDeselectedTicks;
+
+        public this(Menu menuItem) : base(menuItem)
+        {
+            if (mMenuItem.mDisabled)
+                mMouseVisible = false;
+        }
+
+        public override void Draw(Graphics g)
+        {
+            base.Draw(g);
+
+            if (mMenuItem.mLabel == null)
+                g.DrawButton(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.MenuSepHorz), GS!(28), 0, mWidth - GS!(32));
+            else
+            {
+				let darkMenuWidget = (DarkMenuWidget)mMenuWidget;
+				g.SetFont(mMenuItem.mBold ? darkMenuWidget.mBoldFont : darkMenuWidget.mFont);
+
+                if (mMenuItem.mDisabled)
+                {
+                    using (g.PushColor(0xFFA8A8A8))
+                        g.DrawString(mMenuItem.mLabel, GS!(36), 0);
+                }
+                else
+                    g.DrawString(mMenuItem.mLabel, GS!(36), 0);
+
+                if (mMenuItem.mIconImage != null)
+                    g.Draw(mMenuItem.mIconImage, GS!(4), 0);
+            }
+
+            if (mMenuItem.mItems.Count > 0)
+            {
+                g.Draw(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.RightArrow), mWidth - GS!(16), 0);
+            }
+        }
+
+        public void CloseSubMenu()
+        {
+            mSubMenu.Close();
+        }
+
+        void SubMenuClosed(Menu menu, Menu selectedItem)
+        {            
+            mSubMenu.mMenu.mOnMenuClosed.Remove(scope => SubMenuClosed, true);
+            mSubMenu = null;
+            if ((!mMouseOver) && (mMenuWidget.mSelectIdx == mIndex))
+                mMenuWidget.SetSelection(-1);
+            if (selectedItem != null)
+            {
+                mMenuWidget.mItemSelected = (Menu)selectedItem;
+                mMenuWidget.Close();
+            }            
+        }
+
+        public void OpenSubMenu(bool setFocus)
+        {
+			if (mWidgetWindow.mHasClosed)
+				return;
+
+			mMenuWidget.mOpeningSubMenu = true;
+            mSubMenu = new DarkMenuWidget(mMenuItem);
+			if (setFocus)
+				mSubMenu.mSelectIdx = 0;
+			else
+				mSubMenu.mWindowFlags = .ClientSized | .NoActivate | .DestAlpha | .FakeFocus;
+            mSubMenu.Init(mWidgetWindow.mRootWidget, mX + mWidth + GS!(10), mY);
+            mSubMenu.mMenu.mOnMenuClosed.Add(new => SubMenuClosed);
+            mSubMenu.mParentMenuItemWidget = this;
+			mMenuWidget.mOpeningSubMenu = false;
+        }
+
+        public override void MouseDown(float x, float y, int32 btn, int32 btnCount)
+        {
+            base.MouseDown(x, y, btn, btnCount);
+
+            if ((mMenuItem.mItems.Count > 0) && (mSubMenu == null))
+            {
+                OpenSubMenu(true);
+            }
+        }
+
+        public override void Update()
+        {
+            base.Update();
+            if (mMenuItem.mItems.Count > 0)
+            {
+                if (mIndex == mMenuWidget.mSelectIdx)
+                {
+                    mSelectedTicks++;
+                    mDeselectedTicks = 0;
+                    if ((mSelectedTicks == 10) && (mSubMenu == null))
+                    {
+                        for (DarkMenuItem item in mMenuWidget.mItemWidgets)
+                        {
+                            if ((item != this) && (item.mSubMenu != null))
+                                item.CloseSubMenu();
+                        }
+                        OpenSubMenu(false);
+                    }
+                }
+                else
+                {
+                    mDeselectedTicks++;
+                    mSelectedTicks = 0;
+                    if ((mDeselectedTicks > 20) && (mSubMenu != null))
+                        CloseSubMenu();
+                }
+            }
+        }
+
+        public override void Submit()
+        {
+            mMenuWidget.mItemSelected = mMenuItem;
+			mMenuWidget.Close();
+            if (mMenuItem.mOnMenuItemSelected.HasListeners)
+                mMenuItem.mOnMenuItemSelected(mMenuItem);
+        }
+
+        public override void MouseUp(float x, float y, int32 btn)
+        {
+            if (mMenuItem.mItems.Count > 0)
+                return;
+
+            if ((btn == 0) && (mMouseOver))
+            {
+                Submit();
+            }
+            
+            base.MouseUp(x, y, btn);
+            
+        }        
+    }
+
+    public class DarkMenuContainer : MenuContainer
+    {
+        public float mShowPct = 1.0f;
+        public float mDrawHeight;
+		public float mDrawY;
+
+        public void DrawSelf(Graphics g)
+        {
+            base.Draw(g);
+
+            using (g.PushColor(0x80000000))
+                g.DrawBox(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.DropShadow), GS!(2), mDrawY + GS!(2), mWidth - GS!(2), mDrawHeight - GS!(2));
+
+            base.Draw(g);
+            using (g.PushColor(0xFFFFFFFF))
+                g.DrawBox(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.Menu), 0, mDrawY, mWidth - GS!(8), mDrawHeight - GS!(8));
+        }
+
+        public override void DrawAll(Graphics g)
+        {
+            float toDeg = Math.PI_f * 0.4f;
+            float toVal = Math.Sin(toDeg);
+            float curvedOpenPct = Math.Min(1.0f, (float)Math.Sin(mShowPct * toDeg) / toVal + 0.01f);
+			
+            mDrawHeight = Math.Min(curvedOpenPct * mHeight + GS!(20), mHeight);
+			if (mReverse)
+			{
+				mDrawY = mHeight - mDrawHeight;
+			}
+			else
+			{
+				mDrawY = 0;
+			}
+
+            using (g.PushClip(0, mDrawY, mWidth, mDrawHeight))
+            {
+                DrawSelf(g);
+            }
+
+            using (g.PushClip(0, mDrawY, mWidth, mDrawHeight - GS!(10)))
+            {
+                //using (g.PushColor(Color.Get(Math.Min(1.0f, curvedOpenPct * 1.5f))))
+                base.DrawAll(g);
+            }
+        }
+
+        public override void Update()
+        {
+            base.Update();
+
+            var darkMenuWidget = (DarkMenuWidget)mScrollContent;
+
+            if (mShowPct < 1.0f)
+            {
+                float openSpeed = 0.08f + (0.15f / (darkMenuWidget.mItemWidgets.Count + 1));
+                mShowPct = Math.Min(1.0f, mShowPct + openSpeed);
+				MarkDirty();
+            }
+        }        
+    }
+
+    public class DarkMenuWidget : MenuWidget
+    {
+        public float mItemSpacing;
+        public Font mFont;
+		public Font mBoldFont;
+        public bool mHasDrawn;        
+
+        public this(Menu menu) :
+            base(menu)
+        {
+            mFont = DarkTheme.sDarkTheme.mSmallFont;
+			mBoldFont = DarkTheme.sDarkTheme.mSmallBoldFont;
+            mItemSpacing = mFont.GetLineSpacing();
+            mWindowFlags |= BFWindow.Flags.DestAlpha;
+
+            mPopupInsets.Set(GS!(2), GS!(2), GS!(10), GS!(10));
+        }
+
+        public override MenuContainer CreateMenuContainer()
+        {
+            DarkMenuContainer menuContainer = new DarkMenuContainer();
+            menuContainer.mScrollbarInsets.Set(GS!(2), 0, GS!(10), GS!(10));
+            //menuContainer.mScrollContentInsets = new Insets(0, 0, 0, 0);
+            return menuContainer;
+        }
+
+        public override MenuItemWidget CreateMenuItemWidget(Menu menuItem)
+        {
+            return new DarkMenuItem(menuItem);
+        }
+
+		public override float GetReverseAdjust()
+		{
+			return GS!(10);
+		}
+
+		ScrollableWidget GetScrollableParent()
+		{
+			ScrollableWidget scrollableWidget = mParent as ScrollableWidget;
+			if (scrollableWidget != null)
+				return scrollableWidget;
+
+			if (mParent != null)
+			{
+				scrollableWidget = mParent.mParent as ScrollableWidget;
+				if (scrollableWidget != null)
+					return scrollableWidget;
+			}
+
+			return null;
+		}
+
+        public override void EnsureItemVisible(int itemIdx)
+        {
+            base.EnsureItemVisible(itemIdx);
+
+            if (itemIdx == -1)
+                return;
+
+            var item = mItemWidgets[itemIdx];
+
+            float aX;
+            float aY;
+            item.SelfToOtherTranslate(this, 0, 0, out aX, out aY);
+
+            float topInsets = 0;
+            float lineHeight = item.mHeight;
+
+            ScrollableWidget scrollableWidget = GetScrollableParent();
+            if (scrollableWidget == null)
+                return;
+
+            float bottomInset = GS!(8);
+
+            if (aY < scrollableWidget.mVertPos.mDest + topInsets)
+            {
+                float scrollPos = aY - topInsets;                
+                scrollableWidget.VertScrollTo(scrollPos);
+            }
+            else if (aY + lineHeight + bottomInset >= scrollableWidget.mVertPos.mDest + scrollableWidget.mScrollContentContainer.mHeight)
+            {
+                float scrollPos = aY + lineHeight + bottomInset - scrollableWidget.mScrollContentContainer.mHeight;                
+                scrollableWidget.VertScrollTo(scrollPos);
+            }            
+        }
+
+        public override void Draw(Graphics g)
+        {
+            /*using (g.PushColor(0xFFFF0000))
+                g.FillRect(0, 0, mWidth, mHeight);*/
+
+            float mDrawHeight = mHeight;
+            g.DrawButtonVert(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.MenuSepVert), GS!(18), GS!(2), mDrawHeight - GS!(4));
+
+            g.SetFont(mFont);
+            for (int32 itemIdx = 0; itemIdx < mItemWidgets.Count; itemIdx++)
+            {
+#unwarn
+                MenuItemWidget item = (MenuItemWidget)mItemWidgets[itemIdx];
+                
+                float curY = GS!(2) + mItemSpacing * itemIdx;
+
+                if (itemIdx == mSelectIdx)
+                {
+                    using (g.PushColor(DarkTheme.COLOR_MENU_FOCUSED))
+                        g.DrawButton(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.MenuSelect), GS!(4), curY, mWidth - GS!(6));
+                }
+
+                //if (item.mMenu.mLabel != null)
+                //g.Draw(DarkTheme.mDarkTheme.GetImage(DarkTheme.ImageIdx.Check), 6, curY + 1);                
+            }            
+        }
+
+        public void SetShowPct(float showPct)
+        {
+            DarkMenuContainer darkMenuContainer = (DarkMenuContainer)mParent.mParent;
+            darkMenuContainer.mShowPct = showPct;
+        }
+
+        public override void MouseLeave()
+        {
+            base.MouseLeave();
+            mSelectIdx = -1;
+        }
+
+        public override void ResizeComponents()
+        {
+            float maxWidth = 0;
+            float curY = GS!(2);
+            for (MenuItemWidget item in mItemWidgets)
+            {
+                if (item.mMenuItem.mLabel != null)
+                    maxWidth = Math.Max(maxWidth, mFont.GetWidth(item.mMenuItem.mLabel));
+                
+                item.Resize(0, curY, mWidth - GS!(8), mItemSpacing);
+                item.mMouseInsets = new Insets(0, GS!(6), 0, 0);
+                curY += mItemSpacing;
+            }
+        }
+
+        public override void CalcSize()
+        {   
+            float maxWidth = 0;
+            for (MenuItemWidget item in mItemWidgets)
+            {
+                if (item.mMenuItem.mLabel != null)
+                    maxWidth = Math.Max(maxWidth, mFont.GetWidth(item.mMenuItem.mLabel));
+            }
+            
+            mWidth = Math.Max(mWidth, GS!(25) + maxWidth + GS!(40));
+            mHeight = mMenu.mItems.Count * mItemSpacing + GS!(6);
+        }
+
+        public override void Update()
+        {
+            base.Update();
+            
+        }
+
+		public override void KeyDown(KeyCode keyCode, bool isRepeat)
+		{
+			base.KeyDown(keyCode, isRepeat);
+
+			switch (keyCode)
+			{
+			case .Right:
+				if (mSelectIdx != -1)
+				{
+					var darkMenuItem = (DarkMenuItem)mItemWidgets[mSelectIdx];
+					if (darkMenuItem.mSubMenu == null)
+					{
+						if (darkMenuItem.mMenuItem.mItems.Count > 0)
+							darkMenuItem.OpenSubMenu(true);
+					}
+					else
+					{
+						mOpeningSubMenu = true;
+						darkMenuItem.mSubMenu.SetFocus();
+						if (darkMenuItem.mSubMenu.mSelectIdx == -1)
+						{
+							darkMenuItem.mSubMenu.mSelectIdx = 0;
+							darkMenuItem.mSubMenu.MarkDirty();
+						}
+						darkMenuItem.mSubMenu.mWidgetWindow.SetForeground();
+						mOpeningSubMenu = false;
+					}
+				}
+			case .Left:
+				
+				if (mParentMenuItemWidget != null)
+				{
+					var darkMenuItem = (DarkMenuItem)mParentMenuItemWidget;
+					int32 selectIdx = darkMenuItem.mMenuWidget.mSelectIdx;
+					darkMenuItem.CloseSubMenu();
+					darkMenuItem.mMenuWidget.mSelectIdx = selectIdx;
+				}
+			default:
+			}
+		}
+    }
+}

+ 192 - 0
BeefLibs/Beefy2D/src/theme/dark/DarkScrollbar.bf

@@ -0,0 +1,192 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.gfx;
+using Beefy.widgets;
+
+namespace Beefy.theme.dark
+{
+    public class DarkScrollbar : Scrollbar
+    {
+        public class DarkThumb : Scrollbar.Thumb
+        {               
+            public override void Draw(Graphics g)
+            {
+                base.Draw(g);
+                DarkScrollbar scrollbar = (DarkScrollbar)mScrollbar;
+                if (mScrollbar.mOrientation == Scrollbar.Orientation.Horz)
+                    g.DrawButton(scrollbar.GetImage((mMouseOver || mMouseDown) ? DarkTheme.ImageIdx.ScrollbarThumb : DarkTheme.ImageIdx.ScrollbarThumbOver), 0, 0, mWidth);
+                else
+                    g.DrawButtonVert(scrollbar.GetImage((mMouseOver || mMouseDown) ? DarkTheme.ImageIdx.ScrollbarThumb : DarkTheme.ImageIdx.ScrollbarThumbOver), 0, 0, mHeight);
+            }
+        }
+
+        public class DarkArrow : Scrollbar.Arrow
+        {            
+            public this(bool isBack)
+            {
+                mBack = isBack;
+            }
+
+            public override void Draw(Graphics g)
+            {				
+                base.Draw(g);
+
+                DarkScrollbar scrollbar = (DarkScrollbar)mScrollbar;
+
+                if (mMouseOver && mMouseDown)
+                    g.Draw(scrollbar.GetImage(DarkTheme.ImageIdx.ShortButtonDown));
+                else if (mMouseOver)
+                    g.Draw(scrollbar.GetImage(DarkTheme.ImageIdx.ShortButton));
+
+                if (mBack)
+                {
+                    if (mScrollbar.mOrientation == Scrollbar.Orientation.Horz)
+                    {
+                        g.PushScale(-1.0f, 1);
+                        g.Draw(scrollbar.GetImage(DarkTheme.ImageIdx.ScrollbarArrow), -DarkTheme.sUnitSize, 0);
+                        g.PopMatrix();
+                    }
+                    else
+                    {
+                        g.PushScale(1, -1);
+                        g.Draw(scrollbar.GetImage(DarkTheme.ImageIdx.ScrollbarArrow), 0, -DarkTheme.sUnitSize);
+                        g.PopMatrix();
+                    }
+                }
+                else
+                {
+                    g.Draw(scrollbar.GetImage(DarkTheme.ImageIdx.ScrollbarArrow));
+                }
+            }
+        }
+
+        public this()
+        {
+            mBaseSize = DarkTheme.sUnitSize - 1;
+            mDualBarSizeOffset = -2;
+
+            mThumb = new DarkThumb();
+            mThumb.mScrollbar = this;
+            AddWidget(mThumb);
+            
+            mStartArrow = new DarkArrow(true);
+            mStartArrow.mScrollbar = this;
+            AddWidget(mStartArrow);
+            
+            mEndArrow = new DarkArrow(false);
+            mEndArrow.mScrollbar = this;
+            AddWidget(mEndArrow);
+        }
+
+		public override void RehupScale(float oldScale, float newScale)
+		{
+			mBaseSize = DarkTheme.sUnitSize - 1;
+			base.RehupScale(oldScale, newScale);
+		}
+
+        public Image GetImage(DarkTheme.ImageIdx image)
+        {
+            if (mOrientation == Orientation.Horz)
+                return DarkTheme.sDarkTheme.GetImage(image);
+            else
+                return DarkTheme.sDarkTheme.GetImage((DarkTheme.ImageIdx)((int32)image + (int32)DarkTheme.ImageIdx.VertScrollbar - (int32)DarkTheme.ImageIdx.Scrollbar));
+        }
+        
+        public override void Draw(Graphics g)
+        {
+            base.Draw(g);
+
+            if (mOrientation == Orientation.Horz)
+            {                
+                g.DrawButton(GetImage(DarkTheme.ImageIdx.Scrollbar), 0, 0, mWidth);
+            }
+            else
+            {                
+                g.DrawButtonVert(GetImage(DarkTheme.ImageIdx.Scrollbar), 0, 0, mHeight);
+            }
+        }
+
+		public override void DrawAll(Graphics g)
+		{
+			if ((mWidth <= 0) || (mHeight <= 0))
+				return;
+
+			base.DrawAll(g);
+		}
+
+        public override double GetContentPosAt(float x, float y)
+        {
+            float btnMargin = GS!(18);
+            float sizeLeft = (mOrientation == Orientation.Horz) ? (mWidth - btnMargin * 2) : (mHeight - btnMargin * 2);
+
+            if (mOrientation == Orientation.Horz)
+            {                
+                float trackSize = sizeLeft - mThumb.mWidth;
+                float trackPct = (x - btnMargin) / trackSize;
+                double contentPos = (mContentSize - mPageSize) * trackPct;
+                return contentPos;
+            }
+            else
+            {
+                float trackSize = sizeLeft - mThumb.mHeight;
+                float trackPct = (y - btnMargin) / trackSize;
+                double contentPos = (mContentSize - mPageSize) * trackPct;
+                return contentPos;
+            }
+        }
+
+        public override void ResizeContent()
+        {
+            mStartArrow.Resize(0, 0, mBaseSize, mBaseSize);
+            if (mOrientation == Orientation.Horz)
+                mEndArrow.Resize(mWidth - mBaseSize, 0, mBaseSize, mBaseSize);
+            else
+                mEndArrow.Resize(0, mHeight - mBaseSize, mBaseSize, mBaseSize);
+
+            float btnMargin = GS!(18);
+
+            float sizeLeft = (mOrientation == Orientation.Horz) ? (mWidth - btnMargin * 2) : (mHeight - btnMargin * 2);
+
+            if ((mPageSize < mContentSize) && (mContentSize > 0))
+            {
+                double pagePct = mPageSize / mContentSize;
+                mThumb.mVisible = true;
+				
+				if (sizeLeft >= GS!(12))
+				{
+					mThumb.mVisible = true;
+					mStartArrow.mVisible = true;
+					mEndArrow.mVisible = true;
+				}
+				else
+				{						       
+     				mThumb.mVisible = false;
+					mStartArrow.mVisible = false;
+					mEndArrow.mVisible = false;
+	                /*sizeLeft = 0;
+					sizeLeft = (mOrientation == Orientation.Horz) ? (mWidth) : (mHeight);
+					btnMargin = 0;*/
+				}
+
+				double thumbSize = Math.Max(Math.Round(Math.Min(sizeLeft, Math.Max(GS!(12), sizeLeft * pagePct))), 0);
+				double trackSize = Math.Max(sizeLeft - ((thumbSize) - (sizeLeft * pagePct)) + 1, 0);
+				if (mOrientation == Orientation.Horz)
+				    mThumb.Resize((float)(btnMargin + (float)Math.Round(mContentPos / mContentSize * trackSize)), 0, (float)thumbSize, mBaseSize);
+				else
+				    mThumb.Resize(0, (float)(btnMargin + (float)Math.Round(mContentPos / mContentSize * trackSize)), mBaseSize, (float)thumbSize);
+            }
+            else
+            {
+                mThumb.mVisible = false;
+            }
+        }
+
+        public override void Resize(float x, float y, float width, float height)
+        {
+            base.Resize(x, y, width, height);
+
+            ResizeContent();
+        }
+    }
+}

+ 194 - 0
BeefLibs/Beefy2D/src/theme/dark/DarkSmartEdit.bf

@@ -0,0 +1,194 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy;
+using Beefy.widgets;
+using Beefy.events;
+using Beefy.gfx;
+
+namespace Beefy.theme.dark
+{
+    public class DarkSmartEdit : Widget
+    {
+        public String mText;
+        public bool mAllowEdit = true;        
+        public Object mValue;
+        public List<Widget> mMoveWidgets;
+        public Action<DarkSmartEdit> mValueChangedAction;
+
+        bool mCancelingEdit;
+        float mMouseDownX;
+        float mMouseDownY;
+        Object mMouseDownValue;
+        bool mDidDragEdit;        
+        EditWidget mCurEditWidget;        
+
+        public void SetValue(float value)
+        {
+            mValue = value;
+            mText.AppendF("{0:0.0}", value);
+            UpdateWidth();
+        }
+
+        public void SetValue(String value)
+        {
+            mValue = value;
+            mText = value;
+            UpdateWidth();
+        }
+
+        public void SetDisplay(String value)
+        {
+            mText = value;
+            UpdateWidth();
+        }
+
+        public void NudgeMoveWidgets(float offset)
+        {
+            if (mMoveWidgets != null)
+            {
+                for (Widget moveWidget in mMoveWidgets)
+                    moveWidget.mX += offset;
+            }
+        }
+
+        void UpdateWidth()
+        {
+            float newWidth;
+            if (mCurEditWidget != null)
+                newWidth = mCurEditWidget.mWidth + 2;
+            else
+                newWidth = DarkTheme.sDarkTheme.mSmallFont.GetWidth(mText);
+
+            float adjust = newWidth - mWidth;
+            NudgeMoveWidgets(adjust);
+            mWidth = newWidth;            
+        }
+
+        public override void Draw(Graphics g)
+        {
+            base.Draw(g);
+
+            if (mText == null)
+                return;
+
+            g.SetFont(DarkTheme.sDarkTheme.mSmallFont);
+
+            if ((mMouseDown) && (mMouseDownValue is float))
+            {
+                g.Draw(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.LeftArrowDisabled), -mX - 11, 0);
+
+                float endX = mWidth;
+                if (mMoveWidgets != null)
+                    endX = mMoveWidgets[mMoveWidgets.Count - 1].mX + mMoveWidgets[mMoveWidgets.Count - 1].mWidth - mX;
+
+                g.Draw(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.RightArrowDisabled), endX - 5, 0);
+            }
+
+            using (g.PushColor(0xFFCC9600))
+                g.DrawString(mText, 0, 0);
+        }
+
+        void HandleEditLostFocus(Widget widget)
+        {
+            EditWidget editWidget = (EditWidget)widget;
+            editWidget.mOnLostFocus.Remove(scope => HandleEditLostFocus, true);
+            editWidget.mOnSubmit.Remove(scope => HandleEditSubmit, true);
+            editWidget.mOnCancel.Remove(scope => HandleEditCancel, true);
+
+            if (!mCancelingEdit)
+            {
+				mText.Clear();
+				editWidget.GetText(mText);
+                if (mValue is float)
+                {
+                    float aValue = 0;
+                    aValue = float.Parse(mText);
+                    SetValue(aValue);
+                    if (mValueChangedAction != null)
+                        mValueChangedAction(this);
+                }
+            }
+
+            editWidget.RemoveSelf();
+            mCurEditWidget = null;
+            UpdateWidth();
+        }
+
+        void HandleEditCancel(EditEvent theEvent)
+        {
+            mCancelingEdit = true;
+            HandleEditLostFocus((EditWidget)theEvent.mSender);
+        }
+
+        void HandleEditSubmit(EditEvent theEvent)
+        {
+            HandleEditLostFocus((EditWidget)theEvent.mSender);
+        }
+
+        public override void MouseDown(float x, float y, int32 btn, int32 btnCount)
+        {
+            base.MouseDown(x, y, btn, btnCount);
+
+            mMouseDownX = x;
+            mMouseDownY = y;
+            mMouseDownValue = mValue;
+            mDidDragEdit = false;
+        }
+
+        public override void MouseMove(float x, float y)
+        {
+            base.MouseMove(x, y);
+
+            if (mMouseDown)
+            {
+                if (mMouseDownValue is float)
+                {
+                    float delta = (float)Math.Round(x - mMouseDownX + y - mMouseDownY);                    
+                    // First drag must be at least 5 pixels to avoid accidentally dragging when trying to click
+                    if ((mDidDragEdit) || (Math.Abs(delta) >= 4))
+                    {
+                        mDidDragEdit = true;
+                        float aValue = (float)mMouseDownValue + delta;
+                        SetValue(aValue);
+                        if (mValueChangedAction != null)
+                            mValueChangedAction(this);
+                    }
+                }
+            }
+        }
+
+        public override void MouseUp(float x, float y, int32 btn)
+        {
+            base.MouseUp(x, y, btn);
+
+            if ((mMouseOver) && (!mDidDragEdit) && (mValue != null))
+            {
+                mCancelingEdit = false;
+
+                EditWidget editWidget = new DarkEditWidget();
+				String numString = scope String();
+				numString.AppendF("{0}", mValue);
+                editWidget.SetText(numString);
+                editWidget.Content.SelectAll();
+
+                float aX;
+                float aY;                
+                SelfToParentTranslate(0, 0, out aX, out aY);
+
+                float width = mWidth + 8;
+
+                editWidget.Resize(aX, aY, width, 20);
+                mParent.AddWidget(editWidget);
+
+                editWidget.mOnLostFocus.Add(new => HandleEditLostFocus);
+                editWidget.mOnSubmit.Add(new => HandleEditSubmit);
+                editWidget.mOnCancel.Add(new => HandleEditCancel);
+                editWidget.SetFocus();
+
+                mCurEditWidget = editWidget;
+                UpdateWidth();
+            }
+        }
+    }
+}

+ 755 - 0
BeefLibs/Beefy2D/src/theme/dark/DarkTabbedView.bf

@@ -0,0 +1,755 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.widgets;
+using Beefy.gfx;
+using Beefy.events;
+using System.Diagnostics;
+
+namespace Beefy.theme.dark
+{
+    public class DarkTabbedView : TabbedView
+    {        
+        public DarkTabEnd mTabEnd;
+        public DarkTabButton mRightTab; // Shows between tabs and tabEnd
+        public float mLeftObscure;
+        float mAllowRightSpace;
+        float cMinTabSize = 5.0f;
+
+        public class DarkTabButtonClose : ButtonWidget
+        {
+            public override void Draw(Graphics g)
+            {
+                base.Draw(g);
+                if (mMouseOver)
+                {
+                    using (g.PushColor(mMouseDown ? 0xFFFF0000 : Color.White))
+                        g.Draw(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.CloseOver), GS!(-4), GS!(-4));
+                }
+                else
+                    g.Draw(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.Close), GS!(-4), GS!(-4));
+            }
+
+            public override void MouseClicked(float x, float y, int32 btn)
+            {
+                base.MouseClicked(x, y, btn);
+
+                var tabButton = (DarkTabButton)mParent;
+                if (tabButton.mCloseClickedEvent.HasListeners)
+                    tabButton.mCloseClickedEvent();
+            }
+        }
+
+        public class DarkTabButton : TabbedView.TabButton
+        {
+            public Action mForceCloseEvent;
+            public DarkTabButtonClose mCloseButton;
+            public bool mIsEndTab;
+            public bool mIsRightTab;
+            public float mObscuredDir; // <0 = left, >0 = right
+            public uint32 mTextColor = Color.White;
+			public float mTabWidthOffset = 30;
+
+            public this(bool isEnd = false)
+            {
+                mContentInsets.Set(0, GS!(1), GS!(1), GS!(1));
+
+                mIsEndTab = isEnd;
+                if (!mIsEndTab)
+                {
+                    mCloseButton = new DarkTabButtonClose();
+                    AddWidget(mCloseButton);
+                }
+
+                mDragHelper.mMinDownTicks = 15;
+                mDragHelper.mTriggerDist = GS!(2);
+            }
+
+			public override void RehupScale(float oldScale, float newScale)
+			{
+				base.RehupScale(oldScale, newScale);
+				mContentInsets.Set(0, GS!(1), GS!(1), GS!(1));
+				mDragHelper.mTriggerDist = GS!(2);
+				if (mLabel != null)
+					mWantWidth = DarkTheme.sDarkTheme.mSmallFont.GetWidth(mLabel) + GS!(mTabWidthOffset);
+				//mHeight = DarkTheme.sUnitSize;
+			}
+
+            public override void MouseDown(float x, float y, int32 btn, int32 btnCount)
+            {
+                base.MouseDown(x, y, btn, btnCount);
+                mDragHelper.mAllowDrag = mObscuredDir == 0;                
+            }
+
+            public override void Resize(float x, float y, float width, float height)
+            {
+                base.Resize(x, y, width, height);
+                if (mCloseButton != null)
+                    mCloseButton.Resize(mWidth - GS!(13), GS!(4), GS!(12), GS!(12));
+            }
+
+            public override void Draw(Graphics g)
+            {
+                base.Draw(g);
+
+                if (mIsEndTab)
+                    return;
+
+                /*float drawWidth = mDrawWidth;
+                if (drawWidth == 0)
+                    drawWidth = mWidth;*/
+                float drawWidth = mWidth;
+
+                Image image = null;
+                if (mIsActive)
+                    image = (mMouseOver || mMouseDown) ? DarkTheme.sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.TabActiveOver] : DarkTheme.sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.TabActive];
+                else
+                    image = ((!mIsEndTab) && (mMouseOver || mMouseDown)) ? DarkTheme.sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.TabInactiveOver] : DarkTheme.sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.TabInactive];
+                g.DrawButton(image, 0, 0, drawWidth + 1);
+
+				if ((mIsActive) && (DarkTheme.sScale != 1.0f))
+				{
+					// When scaling, we can end up with a subpixel we don't want
+					//using (g.PushColor(0xFFFF0000))
+						g.DrawButton(DarkTheme.sDarkTheme.mWindowTopImage, GS!(2), (float)Math.Ceiling(DarkTheme.sScale * (20)) - 1, drawWidth - GS!(4));
+				}
+
+                g.SetFont(DarkTheme.sDarkTheme.mSmallFont);
+                if ((mLabel != null) && (drawWidth > GS!(16)))
+                {
+                    //using (g.PushColor(((DarkTabbedView)mParent).mTextColor))
+                    using (g.PushColor(mTextColor))
+                    {
+						float textWidth = g.mFont.GetWidth(mLabel);
+						float useWidth = mWidth - GS!(12)*2;
+
+						if (textWidth < useWidth)
+                        	g.DrawString(mLabel, GS!(9) + (useWidth - textWidth)/2, 0, .Left, useWidth, .Truncate);
+						else
+							g.DrawString(mLabel, GS!(12), 0, .Left, useWidth, .Truncate);
+                    }
+                }
+            }
+
+            public override void DrawDockPreview(Graphics g)
+            {
+                using (g.PushTranslate(-mX - mDragHelper.mMouseDownX, -mY - mDragHelper.mMouseDownY))
+                {
+                    if (IsTotalWindowContent())
+                        ((DarkTabbedView)mTabbedView).DrawDockPreview(g);
+                    else
+                        ((DarkTabbedView)mTabbedView).DrawDockPreview(g, this);
+                }
+            }            
+        }
+
+        public class DarkTabDock : ICustomDock
+        {
+            public TabbedView mTabbedView;
+            public bool mAlreadyContains;
+            
+            public this(TabbedView tabbedView, bool alreadyContains)
+            {
+                mTabbedView = tabbedView;
+                mAlreadyContains = alreadyContains;
+            }
+
+            public void Draw(Graphics g)
+            {
+                if (!mAlreadyContains)
+                {
+                    using (g.PushColor(0x60FFFFFF))
+                        g.DrawBox(DarkTheme.sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.WhiteCircle], mTabbedView.mX - GS!(6), mTabbedView.mY - GS!(6), mTabbedView.mWidth + GS!(6) * 2, GS!(32));
+                }
+            }
+
+            public void Dock(IDockable dockable)
+            {
+                dockable.Dock(mTabbedView.mParentDockingFrame, mTabbedView, DockingFrame.WidgetAlign.Inside);
+            }
+        }
+
+        public class DarkTabMenuButton : ButtonWidget
+        {
+            public override void Draw(Graphics g)
+            {
+                base.Draw(g);
+                if (mMouseOver || mMouseDown)
+                {
+                    using (g.PushColor(0xFFF7A900))
+                        g.Draw(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.DropMenuButton), GS!(-4), GS!(-4));                    
+                }
+                else
+                    g.Draw(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.DropMenuButton), GS!(-4), GS!(-4));
+            }
+        }
+
+        public class DarkTabEnd : DarkTabButton
+        {
+            public DarkTabMenuButton mMenuButton;
+            public int32 mMenuClosedTick;            
+
+            public this()
+                : base(true)
+            {
+                mMenuButton = new DarkTabMenuButton();
+                AddWidget(mMenuButton);
+                mMenuButton.mOnMouseDown.Add(new => MenuClicked);
+            }
+
+            void ShowMenu(float x, float y)
+            {
+                Menu menu = new Menu();
+                /*menu.AddItem("Item 1");
+                menu.AddItem("Item 2");
+                menu.AddItem();
+                menu.AddItem("Item 3");*/
+
+				var menuItem = menu.AddItem("Frame Type");
+
+				var subItem = menuItem.AddItem("Static");
+				subItem.mOnMenuItemSelected.Add(new (evt) =>
+					{
+						mTabbedView.mIsFillWidget = false;
+						mTabbedView.mSizePriority = 0;
+						mTabbedView.mRequestedWidth = mTabbedView.mWidth;
+						mTabbedView.GetRootDockingFrame().Rehup();
+						mTabbedView.GetRootDockingFrame().ResizeContent();
+					});
+				if (!mTabbedView.mIsFillWidget)
+					subItem.mIconImage = DarkTheme.sDarkTheme.GetImage(.Check);
+
+				subItem = menuItem.AddItem("Documents");
+				subItem.mOnMenuItemSelected.Add(new (evt) =>
+					{
+						/*for (var tabSibling in mTabbedView.mParentDockingFrame.mDockedWidgets)
+						{
+							tabSibling.mSizePriority = 0;
+							if (mTabbedView.mParentDockingFrame.mSplitType == .Horz)
+								tabSibling.mRequestedWidth = tabSibling.mWidth;
+							else
+								tabSibling.mRequestedHeight = tabSibling.mHeight;
+						}*/
+						mTabbedView.mIsFillWidget = true;
+						mTabbedView.GetRootDockingFrame().Rehup();
+						mTabbedView.GetRootDockingFrame().ResizeContent();
+					});
+				if (mTabbedView.mIsFillWidget)
+					subItem.mIconImage = DarkTheme.sDarkTheme.GetImage(.Check);
+
+				menuItem = menu.AddItem("Permanent");
+				if (!mTabbedView.mAutoClose)
+					menuItem.mIconImage = DarkTheme.sDarkTheme.GetImage(.Check);
+				menuItem.mOnMenuItemSelected.Add(new (evt) =>
+					{
+						mTabbedView.mAutoClose = !mTabbedView.mAutoClose;
+					});
+
+				menuItem = menu.AddItem("Close");
+				menuItem.mOnMenuItemSelected.Add(new (evt) =>
+					{
+						let prevAutoClose = mTabbedView.mAutoClose;
+						mTabbedView.mAutoClose = true;
+						var tabs = scope List<TabButton>();
+						for (var tab in mTabbedView.mTabs)
+							tabs.Add(tab);
+
+						if (tabs.IsEmpty)
+						{
+							if (var dockingFrame = mTabbedView.mParent as DockingFrame)
+							{
+								dockingFrame.RemoveDockedWidget(mTabbedView);
+								BFApp.sApp.DeferDelete(mTabbedView);
+							}
+						}
+						else
+						{
+							for (var tab in tabs)
+								tab.mCloseClickedEvent();
+						}
+						mTabbedView.mAutoClose = prevAutoClose;
+					});
+
+				menu.AddItem();
+
+                for (var tab in mTabbedView.mTabs)
+                {
+                    menuItem = menu.AddItem(tab.mLabel);
+                    menuItem.mOnMenuItemSelected.Add(new (selMenuItem) =>
+	                    {
+	                        TabbedView.TabButton activateTab = tab;
+	                        activateTab.Activate();
+	                    });
+                }
+
+                if (mTabbedView.mPopulateMenuEvent != null)
+                    mTabbedView.mPopulateMenuEvent(menu);
+
+                if (menu.mItems.Count > 0)
+                {
+                    MenuWidget menuWidget = DarkTheme.sDarkTheme.CreateMenuWidget(menu);
+
+                    menuWidget.Init(this, x, y, true);
+                    menuWidget.mWidgetWindow.mOnWindowClosed.Add(new => MenuClosed);
+                }
+				else
+					delete menu;
+            }
+
+            void MenuClicked(MouseEvent theEvent)
+            {
+                if (mMenuClosedTick != mUpdateCnt)
+                {
+                    ShowMenu(mMenuButton.mX + GS!(14), mMenuButton.mY + GS!(6));                    
+                }
+            }
+
+            void MenuClosed(BFWindow window)
+            {
+                mMenuClosedTick = mUpdateCnt;
+            }
+
+            public override void MouseDown(float x, float y, int32 btn, int32 btnCount)
+            {
+                base.MouseDown(x, y, btn, btnCount);
+                if (btn == 1)
+                    ShowMenu(x, y);
+            }
+
+            public override void Dock(DockingFrame frame, DockedWidget refWidget, DockingFrame.WidgetAlign align)
+            {
+                mTabbedView.Dock(frame, refWidget, align);
+            }
+
+            public override void Draw(Graphics g)
+            {
+                base.Draw(g);
+                g.Draw(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.Grabber), mWidth - DarkTheme.sUnitSize, 0);
+
+                /*if (mMouseOver)
+                {
+                    using (g.PushColor(0x80FF0000))
+                        g.FillRect(0, 0, mWidth, mHeight);
+                }*/
+            }
+
+            public override void Activate(bool setFocus)
+            {                
+            }
+
+            public override void Resize(float x, float y, float width, float height)
+            {
+                base.Resize(x, y, width, height);
+                if (mMenuButton != null)
+                    mMenuButton.Resize(mWidth - GS!(30), GS!(3), GS!(14), GS!(12));
+            }
+
+            public override bool IsTotalWindowContent()
+            {
+                return (mTabbedView.mParentDockingFrame.mParentDockingFrame == null) &&
+                    (mTabbedView.mParentDockingFrame.GetDockedWindowCount() == 1);
+            }
+        }        
+
+        public this(SharedData sharedData = null) : base(sharedData)
+        {
+            mTabHeight = DarkTheme.sUnitSize;
+            mTabEnd = new DarkTabEnd();
+            mTabEnd.mTabbedView = this;
+            AddWidget(mTabEnd);
+
+			Object obj = this;
+			obj.[Friend]GCMarkMembers();
+        }
+        
+		public ~this()
+		{
+			if (mRightTab != null)
+			{
+				Widget.RemoveAndDelete(mRightTab);
+				mRightTab = null;
+			}
+		}
+
+		public override void RehupScale(float oldScale, float newScale)
+		{
+			base.RehupScale(oldScale, newScale);
+			mTabHeight = DarkTheme.sUnitSize;
+		}
+
+		public override int GetTabCount()
+		{
+			int tabCount = base.GetTabCount();
+			if (mRightTab != null)
+				tabCount++;
+			return tabCount;
+		}
+
+        public override void WithTabs(Action<TabbedView.TabButton> func)
+        {
+            for (var tab in mTabs)
+                func(tab);
+            if (mRightTab != null)
+                func(mRightTab);
+        }
+
+        public override TabButton AddTab(String label, float width, Widget content, bool ownsContent)
+        {
+			float useWidth = width;
+            if (useWidth == 0)
+                useWidth = DarkTheme.sDarkTheme.mSmallFont.GetWidth(label) + GS!(30);
+            return base.AddTab(label, useWidth, content, ownsContent);
+        }
+
+        public override void RemoveTab(TabButton tabButton, bool deleteTab = true)
+        {
+            if (mRightTab == tabButton)
+                SetRightTab(null, deleteTab);
+            else
+                base.RemoveTab(tabButton, deleteTab);            
+        }
+
+        protected override TabButton CreateTabButton()
+        {
+            return new DarkTabButton();
+        }
+
+        public override void Draw(Graphics g)
+        {
+            base.Draw(g);
+            using (g.PushColor(0x80FFFFFF))
+                g.DrawButton(DarkTheme.sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.TabInactive], 0, 0, mWidth);
+            g.DrawBox(DarkTheme.sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.Window], 0, mTabHeight - GS!(2), mWidth, mHeight - mTabHeight + GS!(2));
+        }
+
+        protected override void ResizeTabs(bool immediate)
+        {
+            if (mWidgetWindow == null)
+                return;
+
+			List<Widget> afterTabWidgets = scope List<Widget>(8);
+
+            // Remove tabs, then them back
+            for (int childIdx = mChildWidgets.Count - 1; childIdx >= 0; childIdx--)
+            {
+                var widget = mChildWidgets[childIdx];
+                if ((widget is TabButton) /*&& (widget != mTabEnd) && (widget != mRightTab)*/)
+				{
+                    mChildWidgets.RemoveAt(childIdx);
+				}
+				else
+				{
+					afterTabWidgets.Add(widget);
+					mChildWidgets.RemoveAt(childIdx);
+				}
+            }
+
+            if (mRightTab != null)
+                mRightTab.mWidth = mRightTab.mWantWidth;
+            
+            float maxAreaWidth = mWidth - GS!(36);
+            if (mRightTab != null)
+                maxAreaWidth -= mRightTab.mWidth + GS!(2);
+
+            float leftObscure = mLeftObscure;
+
+            int32 maxStackedDocs = 16;
+
+            float curX = 0;
+            int32 tabIdx = 0;
+            // Before active tab button            
+
+            bool foundActiveTab = false;
+            for (tabIdx = 0; tabIdx < mTabs.Count; tabIdx++)
+            {
+                var tabButton = (DarkTabButton)mTabs[tabIdx];
+
+				float useWidth = tabButton.mWantWidth;
+				if (curX + useWidth > mWidth - GS!(36))
+					useWidth = Math.Max(mWidth - GS!(36), 0);
+				useWidth = (float)Math.Round(useWidth);
+
+                tabButton.Resize(curX, tabButton.mY, useWidth, tabButton.mHeight);
+
+                //float offset = tabIdx - leftObscure;
+                float widthSubtract = Math.Max(0, leftObscure);
+                leftObscure -= tabButton.mWantWidth;
+
+                float showWidth = 0;
+                tabButton.mVisible = leftObscure < 1536.0f;
+                if (tabButton.mVisible)
+                    showWidth = (float)Math.Round(Math.Max(tabButton.mWantWidth - widthSubtract, GS!(cMinTabSize)));
+
+                if (widthSubtract > 0)
+                {
+                    tabButton.mObscuredDir = -widthSubtract;
+                }
+                else
+                    tabButton.mObscuredDir = 0;
+
+				if (mTabs.Count > 1)
+				{
+	                // Truncate drawing so we don't have the right side of the button showing from under the right side of the 
+	                //  next tab (if next tab is shorter and on top of the previous one)
+	                if (tabIdx < mTabs.Count - 1)
+	                {
+	                    tabButton.mWidth = (float)Math.Round(Math.Min(tabButton.mWantWidth, showWidth + GS!(8)));
+	                    tabButton.mCloseButton.mVisible = tabButton.mWidth >= tabButton.mWantWidth;
+	                }
+	                else
+	                {
+	                    tabButton.mWidth = (float)Math.Round(tabButton.mWantWidth);
+	                    tabButton.mCloseButton.mVisible = true;
+	                }
+				}
+
+                mChildWidgets.Add(tabButton);                
+
+                float pixelsOffscreen = curX + tabButton.mWidth + Math.Min(maxStackedDocs, mTabs.Count - tabIdx - 1) * GS!(cMinTabSize) - maxAreaWidth;
+                curX += showWidth;
+
+                if ((pixelsOffscreen > 0) && (leftObscure <= 0))
+                {
+                    if (tabButton.mObscuredDir != 0)
+                    {
+                        tabButton.mObscuredDir = 0;
+                    }
+                    else
+                    {                        
+                        tabButton.mObscuredDir = pixelsOffscreen;                        
+                    }
+                }
+                tabButton.mVisible = true;                
+
+                foundActiveTab |= tabButton.mIsActive;
+                if ((mRightTab != null) && (mRightTab.mIsActive))
+                    foundActiveTab = true;
+
+                if ((foundActiveTab) && (tabButton.mObscuredDir >= -2))
+                {
+                    tabIdx++;
+                    break;
+                }
+            }
+            
+            int32 numRightObscuredButtons = 0;
+            bool foundEnd = false;
+            float stackedSize = 0;
+
+            int selInsertPos = mChildWidgets.Count;
+
+            // After the active button
+            for (; tabIdx < mTabs.Count; tabIdx++)
+            {                
+                var tabButton = (DarkTabButton)mTabs[tabIdx];
+                
+                float showWidth = (float)Math.Round(tabButton.mWantWidth);
+                if (!foundEnd)
+                    stackedSize = Math.Min(maxStackedDocs, mTabs.Count - tabIdx - 1) * GS!(cMinTabSize);
+                float maxX = (float)Math.Round(maxAreaWidth - showWidth - stackedSize);
+
+                tabButton.mWidth = showWidth;
+                tabButton.mVisible = true;
+
+                if (maxX < curX)
+                {
+                    curX = maxX;
+                    tabButton.mObscuredDir = 1;                    
+                    if (numRightObscuredButtons > maxStackedDocs)
+                    {
+                        //int a = 0;
+                        tabButton.mVisible = false;
+                    }
+                    numRightObscuredButtons++;
+                    foundEnd = true;
+                    stackedSize -= GS!(cMinTabSize);
+
+                    var prevButton = (DarkTabbedView.DarkTabButton)mTabs[tabIdx - 1];
+                    if (prevButton.mWidth < prevButton.mWantWidth - 1)
+                    {
+                        // Super-squished, fix for small label, make small enough that the label doesn't draw
+                        prevButton.mWidth = GS!(16);
+                    }
+                }
+                else
+                    tabButton.mObscuredDir = 0;                
+
+                tabButton.mCloseButton.mVisible = tabButton.mObscuredDir == 0;
+
+                mChildWidgets.Insert(0, tabButton);
+
+                tabButton.Resize(curX, tabButton.mY, tabButton.mWidth, tabButton.mHeight);
+                
+                curX += showWidth;                
+            }
+
+            if ((curX < maxAreaWidth) && (mLeftObscure > 0))
+            {
+                var activeTab = (DarkTabButton)GetActiveTab();
+                float pixelsLeft = maxAreaWidth - curX;
+                if ((activeTab != null) && (pixelsLeft > mAllowRightSpace))
+                    activeTab.mObscuredDir = -pixelsLeft;
+            }
+
+            float tabX = 0;
+            if (mTabs.Count > 0)
+                tabX = Math.Min(mWidth - GS!(36), mTabs[mTabs.Count - 1].mX + mTabs[mTabs.Count - 1].mWidth);
+            if (mRightTab != null)
+            {
+                if (mRightTab.mIsActive)
+                    selInsertPos = mChildWidgets.Count;
+
+                //tabX = Math.Min(tabX, maxAreaWidth);
+                //tabX += 2;
+                mRightTab.Resize(maxAreaWidth, 0, mRightTab.mWidth, DarkTheme.sUnitSize);
+                //tabX += mRightTab.mWidth;
+                mChildWidgets.Insert(selInsertPos, mRightTab);
+            }
+
+			for (int afterTabIdx = afterTabWidgets.Count - 1; afterTabIdx >= 0; afterTabIdx--)
+			{
+				mChildWidgets.Add(afterTabWidgets[afterTabIdx]);
+			}
+
+            mTabEnd.Resize(tabX, 0, mWidth - tabX - GS!(1), DarkTheme.sUnitSize);
+            mChildWidgets.Insert(selInsertPos, mTabEnd);
+            
+            mNeedResizeTabs = false;
+
+			if (immediate)
+				UpdateTabView(true);
+        }
+
+        public void SetRightTab(DarkTabButton tabButton, bool deletePrev = true)
+        {
+			bool hadFocus = mWidgetWindow.mFocusWidget != null;
+			bool needsNewFocus = false;
+
+            mNeedResizeTabs = true;
+            if (mRightTab != null)
+            {
+                if (mRightTab.mIsActive)
+                {
+					needsNewFocus = true;
+					mRightTab.Deactivate();
+				}
+                mRightTab.mIsRightTab = false;
+                RemoveWidget(mRightTab);
+                mTabs.Remove(mRightTab);
+                /*if ((GetActiveTab() == null) && (mTabs.Count > 0))
+                    mTabs[0].Activate((hadFocus) && (mWidgetWindow.mFocusWidget == null));*/
+
+                mRightTab.mTabbedView = null;   
+				if (deletePrev)
+             		BFApp.sApp.DeferDelete(mRightTab);
+            }
+
+            mRightTab = tabButton;
+            if (tabButton != null)
+            {
+                tabButton.mIsRightTab = true;
+                tabButton.mTabbedView = this;
+                AddWidgetAtIndex(0, mRightTab);
+
+				if (needsNewFocus)
+					tabButton.Activate((hadFocus) && (mWidgetWindow.mFocusWidget == null));
+            }
+			else
+			{
+				if (mTabs.Count > 0)
+					mTabs[0].Activate((hadFocus) && (mWidgetWindow.mFocusWidget == null));
+			}
+        }
+
+		void UpdateTabView(bool immediate)
+		{
+			var darkTabButton = (DarkTabButton)GetActiveTab();
+			if (darkTabButton != null)
+			{
+			    if (darkTabButton.mObscuredDir != 0)
+			    {
+			        float obscureAdd = darkTabButton.mObscuredDir * 0.2f;
+			        obscureAdd += Math.Sign(obscureAdd) * 1.5f;
+			        
+			        if ((Math.Abs(obscureAdd) > Math.Abs(darkTabButton.mObscuredDir)) || (immediate))
+			        {
+			            obscureAdd = darkTabButton.mObscuredDir;
+			            darkTabButton.mObscuredDir = 0;
+			        }                    
+
+			        mLeftObscure = Math.Max(0, mLeftObscure + obscureAdd);
+			        if (mLeftObscure == 0)
+			            darkTabButton.mObscuredDir = 0;
+
+			        if (obscureAdd > 0)                    
+			            mAllowRightSpace = GS!(cMinTabSize) + 0.1f; // To remove oscillations                    
+			        ResizeTabs(false);
+			        mAllowRightSpace = 0;
+
+					MarkDirty();
+			    }
+			}
+		}
+
+        public override void Update()
+        {
+            base.Update();
+
+			UpdateTabView(false);
+        }
+
+        public override void Resize(float x, float y, float width, float height)
+        {
+            base.Resize(x, y, width, height);
+            ResizeTabs(true);
+        }
+
+        public override ICustomDock GetCustomDock(IDockable dockable, float x, float y)
+        {
+            if (y < mTabHeight)
+            {
+                bool alreadyContains = false;
+
+				var tabButton = dockable as TabButton;
+                if ((tabButton != null) && (mTabs.Contains(tabButton)))
+                {
+                    // Resizing our own tabs...
+                    //  We do this twice so it switches back if our new mouse location doesn't contain ourselves.
+                    //  This avoids rapidly switching position back and foreth on edges of unequally-sized tabs
+                    for (int32 pass = 0; pass < 2; pass++)
+                    {
+                        TabButton foundTab = FindWidgetByCoords(x, y) as TabButton;
+                        if ((foundTab != null) && (foundTab != dockable))
+                        {
+                            int32 foundIndex = mTabs.IndexOf(foundTab);
+                            if (foundIndex != -1)
+                            {
+                                int32 dragIndex = mTabs.IndexOf((TabButton)dockable);
+
+                                mTabs[dragIndex] = mTabs[foundIndex];
+                                mTabs[foundIndex] = (TabButton)dockable;
+                                ResizeTabs(false);
+                            }
+                        }
+                    }
+
+                    alreadyContains = true;
+                }
+                
+                return new DarkTabDock(this, alreadyContains);
+            }
+
+            return null;
+        }
+        
+        public virtual void DrawDockPreview(Graphics g, TabButton tabButton)
+        {
+            using (g.PushColor(0x80FFFFFF))
+            {                    
+                g.DrawBox(DarkTheme.sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.Window], 0, mTabHeight - GS!(2), mWidth, mHeight - mTabHeight + GS!(2));   
+                using (g.PushTranslate(tabButton.mX, tabButton.mY))
+                    tabButton.Draw(g);
+            }            
+        }
+    }
+}

+ 530 - 0
BeefLibs/Beefy2D/src/theme/dark/DarkTheme.bf

@@ -0,0 +1,530 @@
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.widgets;
+using Beefy.gfx;
+
+namespace Beefy.theme.dark
+{    
+    public class DarkTheme : ThemeFactory
+    {
+        public enum ImageIdx
+        {
+            Bkg,
+            Window,
+            Dots,
+            RadioOn,
+            RadioOff,
+            MainBtnUp,
+            MainBtnDown,
+            BtnUp,
+            BtnOver,
+            BtnDown,
+            Seperator,
+            TabActive,
+            TabActiveOver,
+            TabInactive,
+            TabInactiveOver,            
+            EditBox,
+            Checkbox,
+            CheckboxOver,
+            CheckboxDown,
+            Check,
+            Close,
+            CloseOver,
+            DownArrow,
+            GlowDot,
+            ArrowRight,
+            WhiteCircle,
+            DropMenuButton,
+            ListViewHeader,
+            ListViewSortArrow,
+            Outline,
+            Scrollbar,
+            ScrollbarThumbOver,
+            ScrollbarThumb,
+            ScrollbarArrow,
+            ShortButton,
+            ShortButtonDown,
+            VertScrollbar,
+            VertScrollbarThumbOver,
+            VertScrollbarThumb,
+            VertScrollbarArrow,
+            VertShortButton,
+            VertShortButtonDown,
+            Grabber,
+            DropShadow,
+            Menu,
+            MenuSepVert,
+            MenuSepHorz,
+            MenuSelect,
+            TreeArrow,
+
+            UIPointer,
+            UIImage,
+            UIComposition,
+            UILabel,
+            UIButton,
+            UIEdit,
+            UICombobox,
+            UICheckbox,
+            UIRadioButton,
+            UIListView,
+            UITabView,
+
+            EditCorners,
+            EditCircle,
+            EditPathNode,
+            EditPathNodeSelected,
+            EditAnchor,
+            
+            UIBone,
+            UIBoneJoint,
+
+            VisibleIcon,
+            LockIcon,
+            LeftArrow,
+            KeyframeMakeOff,
+            RightArrow,
+            LeftArrowDisabled,
+            KeyframeMakeOn,
+            RightArrowDisabled,
+            TimelineSelector,
+            TimelineBracket,
+            KeyframeOff,
+            KeyframeOn,
+
+            LinkedIcon,
+            CheckboxLarge,
+            ComboBox,
+            ComboEnd,
+            ComboSelectedIcon,
+
+            LinePointer,
+            RedDot,
+            Document,
+            ReturnPointer,
+            RefreshArrows,
+            MoveDownArrow,
+
+            IconObject,
+            IconObjectDeleted,
+            IconObjectAppend,
+            IconObjectStack,
+            IconValue,
+            IconPointer,
+            IconType,
+            IconError,
+            IconBookmark,
+
+            ProjectFolder,
+            Project,
+            ArrowMoveDown,
+            Workspace,
+            MemoryArrowSingle,
+            MemoryArrowDoubleTop,
+            MemoryArrowDoubleBottom,
+            MemoryArrowTripleTop,
+            MemoryArrowTripleMiddle,
+            MemoryArrowTripleBottom,
+            MemoryArrowRainbow,
+            Namespace,
+            ResizeGrabber,
+            AsmArrow,
+            AsmArrowRev,
+            AsmArrowShadow,
+            MenuNonFocusSelect,
+            StepFilter,
+            WaitSegment,
+            FindCaseSensitive,
+            FindWholeWord,
+
+            RedDotUnbound,
+            MoreInfo,
+
+            Interface,
+            Property,
+            Field,
+            Method,
+            Variable,
+            Constant,
+
+            Type_ValueType,
+            Type_Class,
+
+			LinePointer_Prev,
+			LinePointer_Opt,
+			RedDotEx,
+			RedDotExUnbound,
+			RedDotDisabled,
+			RedDotExDisabled,
+			RedDotRunToCursor,
+
+			GotoButton,
+			YesJmp,
+			NoJmp,
+			WhiteBox,
+			UpDownArrows,
+			EventInfo,
+			WaitBar,
+			HiliteOutline,
+			HiliteOutlineThin,
+
+			IconPayloadEnum,
+			StepFilteredDefault,
+
+			ThreadBreakpointMatch,
+			ThreadBreakpointNoMatch,
+			ThreadBreakpointUnbound,
+			Search,
+			CheckIndeterminate,
+
+            COUNT
+        };
+
+        public const uint32 COLOR_WINDOW              = 0xFF595959;
+        public const uint32 COLOR_BKG                 = 0xFF262626;
+        public const uint32 COLOR_SELECTED_OUTLINE    = 0xFFE6A800;
+        public const uint32 COLOR_MENU_FOCUSED        = 0xFFFFA000;
+        public const uint32 COLOR_MENU_SELECTED       = 0xFFD0A070;
+        public const uint32 COLOR_TIMELINE_SEP        = 0xFF202020;
+        public const uint32 COLOR_TIMELINE_RULE       = 0xFF4A4A4A;
+
+		public static float sScale = 1.0f;
+		public static int32 sSrcImgScale = 1;
+		public static int32 sSrcImgUnitSize = 20;
+		public static int32 sUnitSize = 20;
+
+        public static DarkTheme sDarkTheme ~ delete _;
+        Image mThemeImage ~ delete _;
+        public Image[] mImages = new Image[(int32) ImageIdx.COUNT] ~ delete _;
+
+        public Font mHeaderFont ~ delete _;
+        public Font mSmallFont ~ delete _;
+        public Font mSmallBoldFont ~ delete _;
+        public Image mTreeArrow ~ delete _;
+		public Image mWindowTopImage ~ delete _;
+        public Image mIconWarning ~ delete _;
+        public Image mIconError ~ delete _;
+
+        public static DesignToolboxEntry[] GetDesignToolboxEntries()
+        {
+            Get();
+
+            DesignToolboxEntry [] entries = new DesignToolboxEntry [] 
+            {
+                new DesignToolboxEntry("ButtonWidget", typeof(DarkButton), sDarkTheme.mImages[(int32)ImageIdx.UIButton]),
+                new DesignToolboxEntry("LabelWidget", null, sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.UILabel]),                
+                new DesignToolboxEntry("EditWidget", typeof(DarkEditWidget), sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.UIEdit]),
+                new DesignToolboxEntry("ComboBox", null, sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.UICombobox]),
+                new DesignToolboxEntry("CheckBox", typeof(DarkCheckBox), sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.UICheckbox]),
+                new DesignToolboxEntry("RadioButton", null, sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.UIRadioButton]),
+                new DesignToolboxEntry("ListView", typeof(DarkListView), sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.UIListView]),
+                new DesignToolboxEntry("TabView", typeof(DarkTabbedView), sDarkTheme.mImages[(int32)DarkTheme.ImageIdx.UITabView])
+            };
+
+            for (DesignToolboxEntry entry in entries)
+                entry.mGroupName = "DarkTheme";
+            return entries;
+        }
+
+        public static DarkTheme Get()
+        {
+            if (sDarkTheme != null)
+                return sDarkTheme;
+
+            sDarkTheme = new DarkTheme();
+            sDarkTheme.Init();
+            return sDarkTheme;
+        }
+
+		public static int GetScaled(int val)
+		{
+			return (int)(val * sScale);
+		}
+
+		public static float GetScaled(float val)
+		{
+			return (val * sScale);
+		}
+
+		public static void SetScale(float scale)
+		{
+			sScale = scale;
+			sSrcImgScale = (int32)Math.Clamp((int)Math.Ceiling(scale), 1, 4);
+			if (sSrcImgScale == 3)
+				sSrcImgScale = 4;
+			sSrcImgUnitSize = (int32)(20.0f * sSrcImgScale);
+			sUnitSize = (int32)(sScale * 20);
+			if (sDarkTheme != null)
+				sDarkTheme.Rehup();
+		}
+
+		Image LoadSizedImage(StringView baseName)
+		{
+			var fileName = scope String();
+			fileName.Append(BFApp.sApp.mInstallDir, "images/");
+			fileName.Append(baseName);
+			if (sSrcImgScale > 1)
+				fileName.AppendF("_{0}", sSrcImgScale);
+			fileName.Append(".png");
+			var image = Image.LoadFromFile(fileName);
+			image.Scale(GS!(48) / image.mWidth);
+			return image;
+		}
+
+        public override void Init()
+        {
+            sDarkTheme = this;
+
+			//SetScale(2);
+			//String tempStr = scope String();
+
+            /*mIconError = Image.LoadFromFile(StringAppend!(tempStr, BFApp.sApp.mInstallDir, "images/IconError.png"));
+            mIconWarning = Image.LoadFromFile(StringAppend!(tempStr, BFApp.sApp.mInstallDir, "images/IconWarning.png"));*/
+
+			for (int32 i = 0; i < (int32)ImageIdx.COUNT; i++)
+			{
+				mImages[i] = new Image();
+			}
+
+			mHeaderFont = new Font();
+			mSmallFont = new Font();
+			mSmallBoldFont = new Font();
+			//SetScale(2.2f);
+			SetScale(sScale);
+        }
+
+		public void Rehup()
+		{
+			String tempStr = scope String();
+
+			if (mThemeImage != null)
+			{
+				delete mIconError;
+				delete mIconWarning;
+				delete mThemeImage;
+				delete mTreeArrow;
+				delete mWindowTopImage;
+			}
+
+			String uiFileName = null;
+			switch (sSrcImgScale)
+			{
+			case 1:
+				uiFileName = "DarkUI.png";
+			case 2:
+				uiFileName = "DarkUI_2.png";
+			case 4:
+				uiFileName = "DarkUI_4.png";
+			default:
+				Runtime.FatalError("Invalid scale");
+			}
+
+			mIconError = LoadSizedImage("IconError");
+			mIconWarning = LoadSizedImage("IconWarning");
+			mThemeImage = Image.LoadFromFile(scope String..Append(tempStr, BFApp.sApp.mInstallDir, "images/", uiFileName));
+
+			for (int32 i = 0; i < (int32)ImageIdx.COUNT; i++)
+			{
+				var image = mImages[i];
+				image.CreateImageSegment(mThemeImage, (i % 20) * sSrcImgUnitSize, (i / 20) * sSrcImgUnitSize, sSrcImgUnitSize, sSrcImgUnitSize);
+				image.SetDrawSize(sUnitSize, sUnitSize);
+			}
+
+			// Trim off outside pixels
+			mTreeArrow = mImages[(int32) ImageIdx.TreeArrow].CreateImageSegment(1, 1, DarkTheme.sSrcImgUnitSize - 2, DarkTheme.sSrcImgUnitSize - 2);
+
+			mWindowTopImage = mImages[(int32)ImageIdx.Window].CreateImageSegment(2 * DarkTheme.sSrcImgScale, 2 * DarkTheme.sSrcImgScale, 16 * DarkTheme.sSrcImgScale, (int)Math.Ceiling(sScale));
+
+			//mIconError.Scale(sScale);
+			//mIconWarning.Scale(sScale);
+			mTreeArrow.SetDrawSize((int)(18 * sScale), (int)(18 * sScale));
+
+			mHeaderFont.Dispose(true);
+			/*mHeaderFont.Load(StringAppend!(tempStr, BFApp.sApp.mInstallDir, "fonts/segoeui.ttf"), 11.7f * sScale); //8.8
+			mHeaderFont.AddAlternate(scope String(BFApp.sApp.mInstallDir, "fonts/segoeui.ttf"), 11.7f * sScale);
+			mHeaderFont.AddAlternate(scope String(BFApp.sApp.mInstallDir, "fonts/seguisym.ttf"), 11.7f * sScale);
+			mHeaderFont.AddAlternate(scope String(BFApp.sApp.mInstallDir, "fonts/seguihis.ttf"), 11.7f * sScale);*/
+
+			mHeaderFont.Load("Segoe UI", 11.7f * sScale); //8.8
+			mHeaderFont.AddAlternate("Segoe UI Symbol", 11.7f * sScale);
+			mHeaderFont.AddAlternate("Segoe UI Historic", 11.7f * sScale);
+			mHeaderFont.AddAlternate("Segoe UI Emoji", 11.7f * sScale);
+
+			mSmallFont.Dispose(true);
+			mSmallFont.Load("Segoe UI", 12.8f * sScale); // 10.0
+			mSmallFont.AddAlternate("Segoe UI Symbol", 12.8f * sScale);
+			mSmallFont.AddAlternate("Segoe UI Historic", 12.8f * sScale);
+			mSmallFont.AddAlternate("Segoe UI Emoji", 12.8f * sScale);
+
+			mSmallBoldFont.Dispose(true);
+			mSmallBoldFont.Dispose(true);
+			mSmallBoldFont.Load("Segoe UI Bold", 12.8f * sScale); // 10.0
+			mSmallBoldFont.AddAlternate("Segoe UI Symbol", 12.8f * sScale);
+			mSmallBoldFont.AddAlternate("Segoe UI Historic", 12.8f * sScale);
+			mSmallBoldFont.AddAlternate("Segoe UI Emoji", 12.8f * sScale);
+			/*mSmallBoldFont.Load(StringAppend!(tempStr, BFApp.sApp.mInstallDir, "fonts/segoeuib.ttf"), 12.8f * sScale); // 10.0
+			mSmallBoldFont.AddAlternate(scope String(BFApp.sApp.mInstallDir, "fonts/segoeui.ttf"), 12.8f * sScale);
+			mSmallBoldFont.AddAlternate(scope String(BFApp.sApp.mInstallDir, "fonts/seguisym.ttf"), 12.8f * sScale);
+			mSmallBoldFont.AddAlternate(scope String(BFApp.sApp.mInstallDir, "fonts/seguihis.ttf"), 12.8f * sScale);*/
+		}
+
+		public override void Update()
+		{
+			base.Update();
+			DarkTooltipManager.UpdateTooltip();
+			DarkTooltipManager.UpdateMouseover();
+		}
+
+        public ~this()
+        {
+            for (var image in mImages)
+                delete image;
+        }
+
+        public Image GetImage(ImageIdx idx)
+        {
+            return mImages[(int32)idx];
+        }
+
+        public override ButtonWidget CreateButton(Widget parent, String label, float x, float y, float width, float height)
+        {
+            DarkButton button = new DarkButton();
+            button.Resize(x, y, width, height);
+            button.Label = label;
+            if (parent != null)
+                parent.AddWidget(button);
+            return button;
+        }
+
+        public override EditWidget CreateEditWidget(Widget parent, float x = 0, float y = 0, float width = 0, float height = 0)
+        {
+            DarkEditWidget editWidget = new DarkEditWidget();
+            editWidget.Resize(x, y, width, height);            
+            if (parent != null)
+                parent.AddWidget(editWidget);
+            return editWidget;
+        }
+
+        public override TabbedView CreateTabbedView(TabbedView.SharedData sharedData, Widget parent, float x, float y, float width, float height)
+        {
+            DarkTabbedView tabbedView = new DarkTabbedView(sharedData);
+            tabbedView.Resize(x, y, width, height);
+            if (parent != null)
+                parent.AddWidget(tabbedView);
+            return tabbedView;
+        }
+
+        public override DockingFrame CreateDockingFrame(DockingFrame parent)
+        {
+            DarkDockingFrame dockingFrame = new DarkDockingFrame();
+            if (parent == null)
+                dockingFrame.mWindowMargin = 1;
+			else if (var darkParent = parent as DarkDockingFrame)
+			{
+				dockingFrame.mDrawBkg = darkParent.mDrawBkg;
+			}
+            return dockingFrame;
+        }
+
+        public override ListView CreateListView()
+        {
+            return new DarkListView();
+        }
+
+        public override Scrollbar CreateScrollbar(Scrollbar.Orientation orientation)
+        {
+            DarkScrollbar scrollbar = new DarkScrollbar();
+            scrollbar.mOrientation = orientation;
+            return scrollbar;
+        }
+
+        public override InfiniteScrollbar CreateInfiniteScrollbar()
+        {
+            return new DarkInfiniteScrollbar();
+        }
+
+        public override CheckBox CreateCheckbox(Widget parent, float x = 0, float y = 0, float width = 0, float height = 0)
+        {
+            DarkCheckBox checkbox = new DarkCheckBox();
+            checkbox.Resize(x, y, width, height);
+            if (parent != null)
+                parent.AddWidget(checkbox);
+            return checkbox;
+        }
+
+        public override MenuWidget CreateMenuWidget(Menu menu)
+        {
+            return new DarkMenuWidget(menu);
+        }
+
+        public override Dialog CreateDialog(String title = null, String text = null, Image icon = null)
+        {
+            return new DarkDialog(title, text, icon);
+        }
+
+		public static bool CheckUnderlineKeyCode(StringView label, KeyCode keyCode)
+		{
+			int underlinePos = label.IndexOf('&');
+			if (underlinePos == -1)
+				return false;
+			char32 underlineC = label.GetChar32(underlinePos + 1).0;
+			underlineC = underlineC.ToUpper;
+			return ((char32)keyCode == underlineC);
+		}
+
+		public static void DrawUnderlined(Graphics g, StringView str, float x, float y, FontAlign alignment = FontAlign.Left, float width = 0, Beefy.gfx.FontOverflowMode overflowMode = .Overflow)
+		{
+			int underlinePos = str.IndexOf('&');
+			if ((underlinePos != -1) && (underlinePos < str.Length - 1))
+			{
+				String label = scope String();
+				label.Append(str, 0, underlinePos);
+				float underlineX = g.mFont.GetWidth(label);
+
+				char32 underlineC = str.GetChar32(underlinePos + 1).0;
+				float underlineWidth = g.mFont.GetWidth(underlineC);
+
+				FontMetrics fm = .();
+				label.Append(str, underlinePos + 1);
+				g.DrawString(label, x, y, alignment, width, overflowMode, &fm);
+
+				float drawX;
+				switch (alignment)
+				{
+				case .Centered:
+					drawX = x + underlineX + (width - fm.mMaxWidth) / 2;
+				default:
+					drawX = x + underlineX;
+				}
+
+				g.FillRect(drawX, y + g.mFont.GetAscent() + GS!(1), underlineWidth, (int)GS!(1.2f));
+			}
+			else
+			{
+				g.DrawString(str, x, y, alignment, width, overflowMode);
+			}
+		}
+    }
+
+	static
+	{
+		public static mixin GS(int32 val)
+		{
+			(int32)(val * DarkTheme.sScale)
+		}
+
+		public static mixin GS(int64 val)
+		{
+			(int64)(val * DarkTheme.sScale)
+		}
+
+		public static mixin GS(float val)
+		{
+			float fVal = val * DarkTheme.sScale;
+			fVal
+		}
+	}
+}

+ 398 - 0
BeefLibs/Beefy2D/src/theme/dark/DarkTooltip.bf

@@ -0,0 +1,398 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Beefy;
+using Beefy.events;
+using Beefy.gfx;
+using Beefy.theme.dark;
+using Beefy.widgets;
+using System.Diagnostics;
+using Beefy.geom;
+using Beefy.utils;
+
+namespace Beefy.theme.dark
+{
+    public class DarkTooltipContainer : Widget
+    {
+        public DarkTooltip mTooltip;
+
+        public override void Resize(float x, float y, float width, float height)
+        {
+            base.Resize(x, y, width, height);
+            mTooltip.Resize(0, GS!(4), width - GS!(DarkTooltip.cShadowSize), height - GS!(DarkTooltip.cShadowSize) - GS!(4));
+        }
+    }
+
+    public class DarkTooltip : Widget
+    {
+        public Event<Action> mCloseEvent ~ _.Dispose();
+        public Widget mRelWidget;
+        public Font mFont;
+        public String mText ~ delete _;
+        public bool mAllowResize;
+		public bool mHasClosed;
+		public Insets mRelWidgetMouseInsets ~ delete _;
+		public bool mAllowMouseInsideSelf;
+
+        public const float cShadowSize = 8;
+
+        public this(String text, Widget relWidget, float x, float y, float minWidth, float minHeight, bool allowResize, bool mouseVisible)
+        {
+            DarkTooltipContainer container = new DarkTooltipContainer();
+            container.mTooltip = this;
+            container.AddWidget(this);
+
+            Attach(relWidget);
+            mFont = DarkTheme.sDarkTheme.mSmallFont;
+            mText = new String(text);
+            mAllowResize = allowResize;
+
+            FontMetrics fontMetrics = .();
+            float height = mFont.Draw(null, mText, x, y, 0, 0, FontOverflowMode.Overflow, &fontMetrics);
+            mWidth = Math.Max(minWidth, fontMetrics.mMaxWidth + GS!(32));
+            mHeight = Math.Max(minHeight, height + GS!(16));
+            
+            float screenX;
+            float screenY;
+            relWidget.SelfToRootTranslate(x, y, out screenX, out screenY);
+            screenX += relWidget.mWidgetWindow.mClientX;
+            screenY += relWidget.mWidgetWindow.mClientY;
+            //screenX -= 2;
+            //screenY += 14;
+
+            BFWindow.Flags windowFlags = BFWindow.Flags.ClientSized | BFWindow.Flags.PopupPosition | BFWindow.Flags.NoActivate | BFWindow.Flags.DestAlpha;                                        
+            WidgetWindow widgetWindow = new WidgetWindow(relWidget.mWidgetWindow,
+                "Tooltip",
+                (int32)(screenX), (int32)(screenY),
+                (int32)(mWidth +GS!(cShadowSize)), (int32)(mHeight + GS!(cShadowSize)),
+                windowFlags,
+                container);
+            widgetWindow.SetMinimumSize((int32)widgetWindow.mWindowWidth, (int32)widgetWindow.mWindowHeight);
+            if (!mouseVisible)
+                widgetWindow.SetMouseVisible(mouseVisible);
+
+            if (allowResize)
+                widgetWindow.mOnHitTest.Add(new => HitTest);
+            WidgetWindow.sOnMouseDown.Add(new => HandleMouseDown);
+            WidgetWindow.sOnMouseWheel.Add(new => HandleMouseWheel);
+            WidgetWindow.sOnMenuItemSelected.Add(new => HandleSysMenuItemSelected);
+            WidgetWindow.sOnKeyDown.Add(new => HandleKeyDown);
+        }
+
+		public ~this()
+		{
+			Detach();
+		}
+
+		void Attach(Widget widget)
+		{
+			if (mRelWidget != null)
+				Detach();
+
+			mRelWidget = widget;
+			if (mRelWidget != null)
+				mRelWidget.mOnRemovedFromParent.Add(new => WidgetRemoved);
+		}
+
+		void Detach()
+		{
+			if (mRelWidget != null)
+			{
+				mRelWidget.mOnRemovedFromParent.Remove(scope => WidgetRemoved, true);
+				mRelWidget = null;
+			}
+		}
+
+		void WidgetRemoved(Widget widget, Widget prevParent, WidgetWindow widgetWindow)
+		{
+			Detach();
+			Close();
+		}
+
+		public void Reinit(String text, Widget relWidget, float x, float y, float minWidth, float minHeight, bool allowResize, bool mouseVisible)
+		{
+			mRelWidget = relWidget;
+			mFont = DarkTheme.sDarkTheme.mSmallFont;
+			mText.Set(text);
+			mAllowResize = allowResize;
+
+			FontMetrics fontMetrics = .();
+			float height = mFont.Draw(null, mText, x, y, 0, 0, FontOverflowMode.Overflow, &fontMetrics);
+			mWidth = Math.Max(minWidth, fontMetrics.mMaxWidth + GS!(32));
+			mHeight = Math.Max(minHeight, height + GS!(16));
+
+			float screenX;
+			float screenY;
+			relWidget.SelfToRootTranslate(x, y, out screenX, out screenY);
+			screenX += relWidget.mWidgetWindow.mClientX;
+			screenY += relWidget.mWidgetWindow.mClientY;
+
+			mWidgetWindow.Resize((int32)(screenX), (int32)(screenY),
+                (int32)(mWidth + GS!(cShadowSize)), (int32)(mHeight + GS!(cShadowSize)));
+		}
+
+        void HandleKeyDown(KeyDownEvent keyboardEvent)
+        {
+			mOnKeyDown(keyboardEvent);
+
+            if (keyboardEvent.mHandled)
+                return;
+
+            if (keyboardEvent.mKeyCode == KeyCode.Escape)
+            {
+                Close();
+                keyboardEvent.mHandled = true;
+            }
+        }
+
+        BFWindow.HitTestResult HitTest(int32 x, int32 y)
+        {
+            int32 relX = x - mWidgetWindow.mX;
+            int32 relY = y - mWidgetWindow.mY;
+
+            if ((relX >= mWidgetWindow.mWindowWidth - GS!(18)) && (relY >= mWidgetWindow.mWindowHeight - GS!(18)))
+                return BFWindowBase.HitTestResult.BottomRight;
+            return BFWindowBase.HitTestResult.Client;
+        }
+
+        public override void Draw(Graphics g)
+        {
+            base.Draw(g);
+
+            using (g.PushColor(0x80000000))
+                g.DrawBox(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.DropShadow), 0, 0, mWidth + cShadowSize, mHeight + cShadowSize);
+
+            using (g.PushColor(0xFFFFFFFF))
+                g.DrawBox(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.Menu), 0, 0, mWidth, mHeight);
+
+            g.SetFont(mFont);
+            g.DrawString(mText, 0, GS!(5), FontAlign.Centered, mWidth);
+
+            if (mAllowResize)
+                g.Draw(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.ResizeGrabber), mWidth - GS!(22), mHeight - GS!(22));
+        }
+        
+		public override void ParentDeleted()
+		{
+			Close();
+			base.ParentDeleted();
+		}
+
+        public void Close()
+        {   
+			if (mHasClosed)
+				return;
+			mHasClosed = true;
+
+            if (mWidgetWindow != null)
+            {
+                WidgetWindow.sOnMouseDown.Remove(scope => HandleMouseDown, true);
+                WidgetWindow.sOnMouseWheel.Remove(scope => HandleMouseWheel, true);
+                WidgetWindow.sOnMenuItemSelected.Remove(scope => HandleSysMenuItemSelected, true);
+                WidgetWindow.sOnKeyDown.Remove(scope => HandleKeyDown, true);
+
+                mWidgetWindow.Close();
+            }
+            mCloseEvent();
+        }
+
+        void HandleMouseWheel(MouseEvent evt)
+        {            
+            WidgetWindow widgetWindow = (WidgetWindow)evt.mSender;
+            if (widgetWindow == mWidgetWindow)
+                return;
+
+            Close();
+        }
+
+        void HandleMouseDown(MouseEvent evt)
+        {
+            WidgetWindow widgetWindow = (WidgetWindow)evt.mSender;
+            if (widgetWindow == mWidgetWindow)
+                return;
+            //if ((!(widgetWindow.mRootWidget is HoverWatch)) && (!(widgetWindow.mRootWidget is MenuWidget)))
+            Close();
+        }
+
+        void HandleSysMenuItemSelected(IMenu sysMenu)
+        {
+            Close();
+        }
+
+        public override void Update()
+        {
+            base.Update();
+
+            if (mWidgetWindow == null)
+                return;
+
+            /*var lastMouseWidget = IDEApp.sApp.sLastMouseWidget;
+            if ((lastMouseWidget != null) && (lastMouseWidget != mRelWidget) && (lastMouseWidget.mWidgetWindow != mWidgetWindow))
+                Close();*/
+
+			float rootX;
+			float rootY;
+			mRelWidget.SelfToRootTranslate(0, 0, out rootX, out rootY);
+
+			Rect checkRect = Rect(rootX, rootY, mRelWidget.mWidth, mRelWidget.mHeight);
+			mRelWidgetMouseInsets?.ApplyTo(ref checkRect);
+			if ((mRelWidget.mWidgetWindow != null) && (mRelWidget.mWidgetWindow.mHasMouseInside))
+			{
+				//checkRect.Inflate(8, 8);
+				if (checkRect.Contains(mRelWidget.mWidgetWindow.mClientMouseX, mRelWidget.mWidgetWindow.mClientMouseY))
+					return;
+			}
+
+			if ((mWidgetWindow.mHasMouseInside) && (mAllowMouseInsideSelf))
+				return;
+			
+			Close();
+        }
+    }
+
+	static class DarkTooltipManager
+	{
+		public static DarkTooltip sTooltip;
+		public static Widget sLastMouseWidget;
+		public static int32 sMouseStillTicks;
+		public static Point sLastAbsMousePos;
+		public static Point sLastRelMousePos;
+		public static bool sWantsRefireMouseOver;
+
+		public static bool IsTooltipShown(Widget relWidget)
+		{
+		    return (sTooltip != null) && (sTooltip.mRelWidget == relWidget);
+		}
+
+		public static DarkTooltip ShowTooltip(String text, Widget relWidget, float x, float y, float minWidth = 0, float minHeight = 0, bool allowResize = false, bool mouseVisible = false)
+		{
+			scope AutoBeefPerf("DarkTooltipManager.ShowTooltip");
+
+		    if (sTooltip != null)
+		    {
+		        if (relWidget == sTooltip.mRelWidget)
+				{
+					sTooltip.Reinit(text, relWidget, x, y, minWidth, minHeight, allowResize, mouseVisible);
+		            return null; // Only return the tooltip when a new one has been allocated
+				}
+
+		        sTooltip.Close();
+		    }
+
+		    sTooltip = new DarkTooltip(text, relWidget, x, y, minWidth, minHeight, allowResize, mouseVisible);
+		    sTooltip.mCloseEvent.Add(new () => {sTooltip = null; });
+		    return sTooltip;
+		}
+
+		public static void CloseTooltip()
+		{
+		    if (sTooltip != null)
+		        sTooltip.Close();
+		}
+
+		public static void UpdateTooltip()
+		{
+		    if (sTooltip == null)
+		        return;
+		}
+
+		public static bool CheckMouseover(Widget checkWidget, int wantTicks, out Point mousePoint, bool continuous = false)
+		{
+		    mousePoint = Point(Int32.MinValue, Int32.MinValue);
+		    if (checkWidget != sLastMouseWidget) 
+		        return false;
+
+			for (var childWindow in checkWidget.mWidgetWindow.mChildWindows)
+			{
+				var childWidgetWindow = childWindow as WidgetWindow;
+				if (childWidgetWindow == null)
+					continue;
+				if (childWidgetWindow.mRootWidget is MenuContainer)
+					return false;
+			}
+
+		    checkWidget.RootToSelfTranslate(sLastRelMousePos.x, sLastRelMousePos.y, out mousePoint.x, out mousePoint.y);
+			if ((continuous) && (sMouseStillTicks > wantTicks))
+				return true;
+			if (sWantsRefireMouseOver)
+			{
+				sWantsRefireMouseOver = false;
+				return true;
+			}
+		    return sMouseStillTicks == wantTicks;
+		}
+
+		static void LastMouseWidgetDeleted(Widget widget)
+		{
+			if (sLastMouseWidget == widget)
+				sLastMouseWidget = null;
+		}
+
+		public static void RefireMouseOver()
+		{
+			sWantsRefireMouseOver = true;
+		}
+
+		static void SetLastMouseWidget(Widget newWidget)
+		{
+			if (sLastMouseWidget != null)
+				sLastMouseWidget.mOnDeleted.Remove(scope => LastMouseWidgetDeleted, true);
+			sLastMouseWidget = newWidget;
+			if (sLastMouseWidget != null)
+				sLastMouseWidget.mOnDeleted.Add(new => LastMouseWidgetDeleted);
+		}
+
+		public static void UpdateMouseover()
+		{
+			if (sMouseStillTicks != -1)
+		    	sMouseStillTicks++;
+
+		    Widget overWidget = null;
+		    int32 numOverWidgets = 0;
+		    for (var window in BFApp.sApp.mWindows)
+		    {
+		        var widgetWindow = window as WidgetWindow;
+		        
+		        widgetWindow.RehupMouse(false);
+		        var windowOverWidget = widgetWindow.mCaptureWidget ?? widgetWindow.mOverWidget;
+		        if ((windowOverWidget != null) && (widgetWindow.mAlpha == 1.0f) && (widgetWindow.mCaptureWidget == null))
+		        {
+		            overWidget = windowOverWidget;
+		            numOverWidgets++;
+		            if (overWidget != sLastMouseWidget)
+		            {
+						SetLastMouseWidget(overWidget);                        
+		                sMouseStillTicks = -1;
+		            }
+
+					float actualX = widgetWindow.mClientX + widgetWindow.mMouseX;
+					float actualY = widgetWindow.mClientY + widgetWindow.mMouseY;
+		            if ((sLastAbsMousePos.x != actualX) || (sLastAbsMousePos.y != actualY))
+		            {
+		                sMouseStillTicks = 0;
+		                sLastAbsMousePos.x = actualX;
+		                sLastAbsMousePos.y = actualY;
+		            }
+					sLastRelMousePos.x = widgetWindow.mMouseX;
+					sLastRelMousePos.y = widgetWindow.mMouseY;
+		        }
+		    }
+
+		    if (overWidget == null)
+		    {     
+		   		SetLastMouseWidget(null);
+		        sMouseStillTicks = -1;
+		    }
+
+		    if (numOverWidgets > 1)
+		    {
+				//int a = 0;
+				Debug.WriteLine("numOverWidgets > 1");
+		    }
+
+
+		    //Debug.Assert(numOverWidgets <= 1);                        
+		}
+	}
+}

+ 161 - 0
BeefLibs/Beefy2D/src/theme/dark/DarkVirtualListViewItem.bf

@@ -0,0 +1,161 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Beefy.theme.dark;
+using Beefy.widgets;
+using System.Diagnostics;
+
+namespace Beefy.theme.dark
+{
+    public class DarkVirtualListViewItem : DarkListViewItem
+    {
+        public DarkVirtualListViewItem mVirtualHeadItem;
+        public int32 mVirtualCount; // Including head item
+        public int32 mVirtualIdx;
+        public bool mDisabled = false;
+		public bool mUpdating = false;
+
+		public ~this()
+		{
+			Debug.Assert(!mUpdating);
+		}
+		
+        public override void Update()
+        {
+			mUpdating = true;
+			defer { mUpdating = false; }
+
+            base.Update();
+
+            if (mParentItem == null)
+                return;
+
+            var virtualListView = (DarkVirtualListView)mListView;
+
+            if (mParentItem.mChildAreaHeight != 0)
+            {
+                float itemHeight = virtualListView.mFont.GetLineSpacing();
+                if (mVirtualHeadItem == this)
+                {
+                    float ofsX;
+                    float ofsY;
+                    mParent.SelfToOtherTranslate(mListView, 0, 0, out ofsX, out ofsY);
+                    ofsY -= (float)(mListView.mVertPos.mDest + mListView.mScrollContent.mY);
+
+                    int32 curMemberIdx = 0;
+                    DarkVirtualListViewItem prevVirtualListViewItem = null;
+                    DarkVirtualListViewItem nextVirtualListViewItem = (DarkVirtualListViewItem)mParentItem.mChildItems[curMemberIdx];
+                    
+                    int32 showCount = mVirtualCount;
+
+                    float curY = mY;
+                    float prevY = curY;
+                    float lastBottomPadding = 0;
+                    for (int32 idx = 0; idx < showCount; idx++)
+                    {
+                        DarkVirtualListViewItem curVirtualListViewItem = null;
+
+                        if ((nextVirtualListViewItem != null) && (idx == nextVirtualListViewItem.mVirtualIdx))
+                        {
+                            curVirtualListViewItem = nextVirtualListViewItem;
+                            curMemberIdx++;
+                            if (curMemberIdx < mParentItem.mChildItems.Count)
+                            {
+                                nextVirtualListViewItem = (DarkVirtualListViewItem)mParentItem.mChildItems[curMemberIdx];
+                                if (nextVirtualListViewItem.mVirtualHeadItem != this)
+                                    nextVirtualListViewItem = null;
+                                if (nextVirtualListViewItem != null)
+                                    lastBottomPadding = nextVirtualListViewItem.mBottomPadding;
+                            }
+                            else
+                                nextVirtualListViewItem = null;
+                        }
+
+                        bool wantsFillIn = (curY + ofsY + itemHeight >= 0) && (curY + ofsY < mListView.mHeight);
+                        bool wantsDelete = !wantsFillIn;
+
+                        if (mDisabled)
+                        {
+                            wantsFillIn = false;
+                            wantsDelete = false;
+                        }
+
+                        if ((curVirtualListViewItem == null) && (wantsFillIn))
+                        {
+                            prevVirtualListViewItem.mBottomPadding = (curY - prevVirtualListViewItem.mY) - prevVirtualListViewItem.mSelfHeight - prevVirtualListViewItem.mChildAreaHeight;
+                            curVirtualListViewItem = (DarkVirtualListViewItem)mParentItem.CreateChildItemAtIndex(curMemberIdx);
+                            curVirtualListViewItem.mVisible = false;
+                            curVirtualListViewItem.mX = mX;
+                            curVirtualListViewItem.mVirtualHeadItem = this;
+                            curVirtualListViewItem.mVirtualIdx = idx;
+                            virtualListView.PopulateVirtualItem(curVirtualListViewItem);                            
+                            curMemberIdx++;
+                        }
+
+                        if ((wantsDelete) && (idx != 0) && (curVirtualListViewItem != null) && (curVirtualListViewItem.mChildAreaHeight == 0))
+                        {
+                            curMemberIdx--;
+                            mParentItem.RemoveChildItem(curVirtualListViewItem);
+                            curVirtualListViewItem = null;
+                        }
+
+                        if (prevVirtualListViewItem != null)
+                        {
+                            if (mDisabled)
+                                prevVirtualListViewItem.mBottomPadding = 0;
+                            else
+                                prevVirtualListViewItem.mBottomPadding = (curY - prevY) - prevVirtualListViewItem.mSelfHeight - prevVirtualListViewItem.mChildAreaHeight;
+                        }
+
+                        if (curVirtualListViewItem != null)
+                            prevY = curY;
+
+                        curY += itemHeight;
+                        if (curVirtualListViewItem != null)
+                        {
+                            curY += curVirtualListViewItem.mChildAreaHeight;
+                            prevVirtualListViewItem = curVirtualListViewItem;
+                        }
+                    }
+                   
+                    if (prevVirtualListViewItem != null)
+                    {
+                        if (mDisabled)
+                            prevVirtualListViewItem.mBottomPadding = 0;
+                        else
+                            prevVirtualListViewItem.mBottomPadding = (curY - prevY) - prevVirtualListViewItem.mSelfHeight - prevVirtualListViewItem.mChildAreaHeight;
+
+                        if (prevVirtualListViewItem.mBottomPadding != lastBottomPadding)
+                            mListView.mListSizeDirty = true;
+                    }
+
+
+                    while ((curMemberIdx > 0) && (curMemberIdx < mParentItem.mChildItems.Count))
+                    {
+                        var curVirtualListViewItem = (DarkVirtualListViewItem)mParentItem.mChildItems[curMemberIdx];
+                        if (curVirtualListViewItem.mVirtualHeadItem != this)
+                            break;
+                        mParentItem.RemoveChildItem(curVirtualListViewItem);
+                        if (mParentItem == null) // Last item
+                            return;
+                    }
+                }
+            }
+        }
+    }
+
+    public class DarkVirtualListView : DarkListView
+    {
+        protected override ListViewItem CreateListViewItem()
+        {            
+            var anItem = new DarkVirtualListViewItem();
+            return anItem;
+        }
+
+        public virtual void PopulateVirtualItem(DarkVirtualListViewItem item)
+        {
+            
+        }
+    }
+}

+ 105 - 0
BeefLibs/Beefy2D/src/utils/BeefPerf.bf

@@ -0,0 +1,105 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Beefy.utils
+{
+	static class BeefPerf
+	{
+		[StdCall, CLink]
+		extern static void BpInit(char8* severName, char8* sessionName);
+
+		[StdCall, CLink]
+		extern static void BpShutdown();
+
+		[StdCall, CLink]
+		extern static void BpRetryConnect();
+
+		[StdCall, CLink]
+		extern static void BpPause();
+
+		[StdCall, CLink]
+		extern static void BpUnpause();
+
+		[StdCall, CLink]
+		extern static void BpSetThreadName(char8* threadName);
+
+		[StdCall, CLink]
+		extern static void BpEnter(char8* name);        
+
+		[StdCall, CLink]
+		extern static void BpLeave();
+
+		[StdCall, CLink]
+		extern static void BpEvent(char8* name, char8* details);
+
+		[StdCall, CLink]
+		extern static char8* BpDynStr(char8* string);
+
+		public static void Init(StringView serverName, StringView sessionName)
+		{
+			BpInit(serverName.ToScopeCStr!(), sessionName.ToScopeCStr!());
+		}
+
+		public static void RetryConnect()
+		{
+			BpRetryConnect();
+		}
+
+		public static void Shutdown()
+		{
+			BpShutdown();
+		}
+
+		public static void Pause()
+		{
+			BpPause();
+		}
+
+		public static void Unpause()
+		{
+			BpUnpause();
+		}
+
+		public static void SetThreadName(StringView threadName)
+		{
+			BpSetThreadName(threadName.ToScopeCStr!());
+		}
+
+		[Inline]
+		public static void Enter(char8* name)
+		{
+			BpEnter(name);
+		}
+
+		[Inline]
+		public static void Leave()
+		{
+			BpLeave();
+		}
+
+		[Inline]
+		public static void Event(char8* name, char8* details)
+		{
+			BpEvent(name, details);
+		}
+
+		[Inline]
+		public static char8* DynStr(char8* string)
+		{
+			return BpDynStr(string);
+		}
+	}
+
+	class AutoBeefPerf
+	{
+		public this(char8* name)
+		{
+			BeefPerf.Enter(name);
+		}
+
+		public ~this()
+		{
+			BeefPerf.Leave();
+		}
+	}
+}

+ 28 - 0
BeefLibs/Beefy2D/src/utils/DisposeProxy.bf

@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Beefy.utils
+{
+    public delegate void DisposeProxyDelegate();
+
+    public class DisposeProxy : IDisposable
+    {
+        public DisposeProxyDelegate mDisposeProxyDelegate;
+
+        public this(DisposeProxyDelegate theDelegate = null)
+        {
+            mDisposeProxyDelegate = theDelegate;
+        }
+
+		public ~this()
+		{
+			delete mDisposeProxyDelegate;
+		}
+
+        public void Dispose()
+        {
+            mDisposeProxyDelegate();
+        }
+    }
+}

+ 12 - 0
BeefLibs/Beefy2D/src/utils/ISerializable.bf

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Beefy.utils
+{
+    public interface ISerializable
+    {
+        //TODO: void Serialize(StructuredData data);
+        //TODO: void Deserialize(StructuredData data);
+    }
+}

+ 899 - 0
BeefLibs/Beefy2D/src/utils/IdSpan.bf

@@ -0,0 +1,899 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Beefy.utils
+{
+    public struct IdSpan
+    {
+		enum Change
+		{
+			case Insert(int32 index, int32 id, int16 length);
+			case Remove(int32 index, int16 length);
+
+			public int32 GetIndex()
+			{
+				int32 index;
+				switch (this)
+				{
+				case .Insert(out index, ?, ?):
+				case .Remove(out index, ?):
+				}
+				return index;
+			}
+		}
+		enum ChangeKind
+		{
+			case None;
+			case Insert;
+			case Remove;
+		}
+
+        static uint8[] sEmptyData = new uint8[] { 0 } ~ delete _;
+
+        public uint8[] mData;
+        public int32 mLength;
+		List<Change> mChangeList;
+
+		public static int32 sId;
+		public int32 mId = ++sId;
+
+		public bool mAlwaysPrepare = false;
+
+		/*public uint8[] mData
+		{
+			get
+			{
+				Debug.Assert(mChangeList == null);
+				return mData;
+			}
+		}*/
+
+		public this()
+		{
+			mData = sEmptyData;
+			mLength = 0;
+			mChangeList = null;
+		}
+
+        public this(uint8[] data, int32 length)
+        {
+            mData = data;
+            mLength = length;
+			mChangeList = null;
+        }
+
+		bool HasChangeList
+		{
+			get
+			{
+				return mChangeList != null;
+			}
+		}
+
+		public bool IsEmpty
+		{
+			get
+			{
+				return mLength == 0;
+			}
+		}
+
+		struct Span
+		{
+			public int32 mIndex;
+			public int32 mId;
+			public int32 mLength;
+
+			public int32 mNext;
+		}
+
+		public void PrepareReverse() mut
+		{
+			PrepareReverse(0, mChangeList.Count);
+			DeleteAndNullify!(mChangeList);
+		}
+
+		// Decode change lists, move backwards through it applying changes, the reencode it.
+		//  Repeats as necessary if items aren't in reverse order
+		void PrepareReverse(int startChangeIdx, int endChangeIdx) mut
+		{
+			int changeIdx = startChangeIdx;
+
+			List<Span> spans = scope List<Span>();
+
+			while (changeIdx < endChangeIdx)
+			{
+				spans.Clear();
+
+				int encodeIdx = 0;
+				int32 charId = 1;
+				int32 charIdx = 0;
+				while (true)
+				{
+				    int32 cmd = Utils.DecodeInt(mData, ref encodeIdx);
+				    if (cmd > 0)
+				    {
+						charId = cmd;
+					}
+				    else
+				    {
+				        int32 spanSize = -cmd;
+				        
+				        if (cmd == 0)
+				            break;
+
+						Span span;
+						span.mId = charId;
+						span.mIndex = charIdx;
+						span.mLength = spanSize;
+						span.mNext = -1;
+						spans.Add(span);
+
+						charId += spanSize;
+						charIdx += spanSize;
+
+						if (spans.Count > 1)
+							spans[spans.Count - 2].mNext = (int32)spans.Count - 1;
+				    }
+				}
+
+				// Initialize to something so we can pick up the insert
+				if (spans.Count == 0)
+				{
+					Span span;
+					span.mId = 1;
+					span.mIndex = 0;
+					span.mLength = 0;
+					span.mNext = -1;
+					spans.Add(span);
+				}
+
+				
+				int32 index = -1;
+				int32 length = -1;
+				int32 curId = -1;
+				ChangeKind changeKind = .None;
+
+				int32 checkIdx = (int32)spans.Count - 1;
+				int32 headSpanIdx = 0;
+
+				// Reverse find the first span
+				while ((checkIdx >= 0) && (changeIdx < endChangeIdx))
+				{
+					if (changeKind == .None)
+					{
+						switch (mChangeList[changeIdx])
+						{
+						case .Insert(out index, out curId, out length):
+							changeKind = .Insert;
+						case .Remove(out index, out length):
+							changeKind = .Remove;
+						}
+					}
+
+					var checkSpan = ref spans[checkIdx];
+					if ((index >= checkSpan.mIndex) && (index <= checkSpan.mIndex + checkSpan.mLength))
+					{
+						if (changeKind == .Insert)
+						{
+							if (index == checkSpan.mIndex)
+							{
+								if (checkSpan.mLength == 0)
+								{
+									checkSpan.mLength = length;
+									checkSpan.mId = curId;
+								}
+								else
+								{
+									int32 newSpanIdx = (.)spans.Count;
+
+									// Insert before span
+									Span span;
+									span.mIndex = index;
+									span.mId = curId;
+									span.mLength = length;
+									span.mNext = (.)checkIdx;
+									spans.Add(span);
+
+									if (checkIdx > 0)
+									{
+										Debug.Assert(spans[checkIdx - 1].mNext == checkIdx);
+										spans[checkIdx - 1].mNext = newSpanIdx;
+									}
+									else
+										headSpanIdx = newSpanIdx;
+
+									// Since we remapped the previous entries mNext, we are no longer in order and can't be reused this pass
+									checkIdx = checkIdx - 1;
+								}
+							}
+							else
+							{
+								var checkSpan;
+
+								// Split span
+								int32 leftLength = index - checkSpan.mIndex;
+
+								int32 newSpanId = (.)spans.Count;
+								int32 newRightId = (.)spans.Count + 1;
+								@checkSpan.mNext = newSpanId;
+								@checkSpan.mLength = leftLength;
+
+								Span span;
+								span.mIndex = index;
+								span.mId = curId;
+								span.mLength = length;
+								span.mNext = newRightId;
+								spans.Add(span);
+
+								Span rightSpan;
+								rightSpan.mIndex = index + span.mLength;
+								rightSpan.mId = checkSpan.mId + leftLength;
+								rightSpan.mLength = checkSpan.mLength - leftLength;
+								rightSpan.mNext = checkSpan.mNext;
+								spans.Add(rightSpan);
+							}
+						}
+						else // Remove
+						{
+							int removeSpanIdx = checkIdx;
+							if (index == checkSpan.mIndex)
+							{
+								// Removing from front of span.  Handled in loop below.
+							}
+							else if (index + length >= checkSpan.mIndex + checkSpan.mLength)
+							{
+								// Removing up to or past end of span
+								int32 removeCount = Math.Min(length, checkSpan.mLength - (index - checkSpan.mIndex));
+								checkSpan.mLength -= removeCount;
+								length -= removeCount;
+								removeSpanIdx = checkSpan.mNext;
+							}
+							else
+							{
+								var checkSpan;
+
+								int32 splitIdx = index - checkSpan.mIndex;
+								int32 splitOfs = splitIdx + length;
+								int32 newRightId = (.)spans.Count;
+								@checkSpan.mNext = newRightId;
+								@checkSpan.mLength = index - checkSpan.mIndex;
+
+								Span rightSpan;
+								rightSpan.mIndex = checkSpan.mIndex + splitIdx;
+								rightSpan.mId = checkSpan.mId + splitOfs;
+								rightSpan.mLength = checkSpan.mLength - splitOfs;
+								rightSpan.mNext = checkSpan.mNext;
+								spans.Add(rightSpan);
+
+								length = 0;
+
+								if (newRightId == checkIdx + 1)
+									checkIdx = newRightId; // rightSpan index is valid now and it is next in sequence
+							}
+
+							while (length > 0)
+							{
+								var removeSpan = ref spans[removeSpanIdx];
+
+								// Remove from start of span
+								int32 removeCount = Math.Min(removeSpan.mLength, length);
+								removeSpan.mId += removeCount;
+								removeSpan.mLength -= removeCount;
+
+								length -= removeCount;
+								
+								removeSpanIdx = removeSpan.mNext;
+							}
+						}
+
+						changeIdx++;
+						changeKind = .None;
+						continue;
+					}
+
+					checkIdx--;
+				}
+
+				curId = 1;
+				int32 spanIdx = headSpanIdx;
+				int curEncodeIdx = 0;
+				if (mData != sEmptyData)
+					delete mData;
+				mData = new uint8[spans.Count * 8];
+
+				while (spanIdx != -1)
+				{
+					var span = ref spans[spanIdx];
+
+					if (span.mLength == 0)
+					{
+						spanIdx = span.mNext;
+						continue;
+					}	
+
+					if (span.mId != curId)
+					{
+						Utils.EncodeInt(mData, ref curEncodeIdx, span.mId);
+						curId = span.mId;
+					}
+
+					Utils.EncodeInt(mData, ref curEncodeIdx, -span.mLength);
+					curId += span.mLength;
+
+					spanIdx = span.mNext;
+				}
+				Utils.EncodeInt(mData, ref curEncodeIdx, 0);
+				mLength = (int32)curEncodeIdx;
+			}
+		}
+
+		void MaybePrepare() mut
+		{
+			// For sanity - only queue up so many changes
+			if (mChangeList.Count >= 8192)
+			{
+				Prepare();
+			}
+		}
+
+		public void Prepare() mut
+		{
+			if (mChangeList == null)
+				return;
+
+			scope AutoBeefPerf("IdSpan.Prepare");
+
+			int changeIdx = 0;
+
+			while (changeIdx < mChangeList.Count)
+			{
+				// Check to see if we have a reverse-order encoding.  This can occur when undoing forward-ordered changes (for example)
+				bool hasReverse = false;
+				int reverseLastIdx = changeIdx;
+
+				int32 prevIndex = mChangeList[changeIdx].GetIndex();
+				for (int checkIdx = changeIdx + 1; checkIdx < mChangeList.Count; checkIdx++)
+				{
+					int32 nextIndex = mChangeList[checkIdx].GetIndex();
+					if (nextIndex > prevIndex)
+						break;
+					if (nextIndex < prevIndex)
+						hasReverse = true;
+					reverseLastIdx = checkIdx;
+					prevIndex = nextIndex;
+				}
+
+				if (hasReverse)
+				{
+					PrepareReverse(changeIdx, reverseLastIdx + 1);
+					changeIdx = reverseLastIdx + 1;
+					continue;
+				}
+
+				// We need space to encode '-length', the new span ID, 
+				//  reverting back to the original ID, and a split span length
+				uint8[] textIdData = new uint8[mLength + mChangeList.Count*16];
+
+				int prevCharIdx = 0;
+				int prevEncodeIdx = 0;
+				int prevLastSpanLength = 0;
+				int prevLastSpanIdStart = 1;
+
+				int curEncodeIdx = 0;
+				int curSpanIdStart = 1;
+				bool foundSpan = false;
+				int ignoreLength = 0;
+
+				int32 index;
+				int32 length;
+				int32 curId = -1;
+				ChangeKind changeKind;
+
+				
+
+				switch (mChangeList[changeIdx++])
+				{
+				case .Insert(out index, out curId, out length):
+					changeKind = .Insert;
+				case .Remove(out index, out length):
+					changeKind = .Remove;
+				}
+
+				while (prevLastSpanIdStart != -1)
+				{
+				    if (ignoreLength > 0)
+				    {
+				        int handleLength = Math.Min(prevLastSpanLength, ignoreLength);
+				        ignoreLength -= handleLength;
+				        prevLastSpanIdStart += handleLength;
+				        prevLastSpanLength -= handleLength;
+				    }
+
+				    if ((curSpanIdStart != prevLastSpanIdStart) && (prevLastSpanLength > 0) && (ignoreLength == 0))
+				    {
+				        Utils.EncodeInt(textIdData, ref curEncodeIdx, prevLastSpanIdStart);
+				        curSpanIdStart = prevLastSpanIdStart;
+				    }
+
+				    if ((prevCharIdx + prevLastSpanLength >= index) && (!foundSpan) && (ignoreLength == 0))
+				    {
+				        foundSpan = true;
+
+						if (curSpanIdStart != prevLastSpanIdStart)
+						{
+						    Utils.EncodeInt(textIdData, ref curEncodeIdx, prevLastSpanIdStart);
+						    curSpanIdStart = prevLastSpanIdStart;
+						}
+
+						if (changeKind case .Insert)
+						{
+							// Time to insert
+							int leftSplitLen = index - prevCharIdx;
+							if (leftSplitLen > 0)
+							{
+							    Utils.EncodeInt(textIdData, ref curEncodeIdx, -leftSplitLen);
+							    curSpanIdStart += leftSplitLen;
+							    prevLastSpanIdStart += leftSplitLen;
+							    prevCharIdx += leftSplitLen;
+							    prevLastSpanLength -= leftSplitLen;
+							}
+
+							if (curSpanIdStart != curId)
+							{
+							    curSpanIdStart = curId;
+							    Utils.EncodeInt(textIdData, ref curEncodeIdx, curSpanIdStart);
+							}
+							curId += length;
+
+							if (length > 0)
+							{
+							    Utils.EncodeInt(textIdData, ref curEncodeIdx, -length);
+							    curSpanIdStart += length;
+								prevCharIdx += length;
+							}
+						}
+						else
+						{
+					        ignoreLength = length;
+
+					        // Time to insert
+					        int leftSplitLen = index - prevCharIdx;
+					        if (leftSplitLen > 0)
+					        {
+					            Utils.EncodeInt(textIdData, ref curEncodeIdx, -leftSplitLen);
+					            curSpanIdStart += leftSplitLen;
+					            prevLastSpanIdStart += leftSplitLen;
+					            prevCharIdx += leftSplitLen;
+					            prevLastSpanLength -= leftSplitLen;
+					        }
+						}
+
+						if (changeIdx < mChangeList.Count)
+						{
+							switch (mChangeList[changeIdx])
+							{
+							case .Insert(out index, out curId, out length):
+								changeKind = .Insert;
+							case .Remove(out index, out length):
+								changeKind = .Remove;
+							}
+							if (index >= prevCharIdx)
+							{
+								// We are inserting into a just-removed location
+								foundSpan = false;
+								changeIdx++;
+							}
+						}
+
+				        continue;
+				    }
+
+				    int cmd = Utils.DecodeInt(mData, ref prevEncodeIdx);
+				    if (cmd >= 0)
+				    {
+				        if (prevLastSpanLength > 0)
+				        {
+				            Utils.EncodeInt(textIdData, ref curEncodeIdx, -prevLastSpanLength);
+				            curSpanIdStart += prevLastSpanLength;
+				            prevLastSpanIdStart += prevLastSpanLength;
+				            prevCharIdx += prevLastSpanLength;
+				            prevLastSpanLength = 0;
+				        }
+
+				        Debug.Assert(prevLastSpanLength == 0);
+				        prevLastSpanIdStart = cmd;
+
+				        if (cmd == 0)
+				            break;
+				    }
+				    else
+				        prevLastSpanLength += -cmd;
+				}
+
+				Utils.EncodeInt(textIdData, ref curEncodeIdx, 0);
+				mLength = (int32)curEncodeIdx;
+				if (mData != sEmptyData)
+					delete mData;
+				mData = textIdData;
+			}
+			DeleteAndNullify!(mChangeList);
+		}
+
+		public IdSpan GetPrepared() mut
+		{
+			Prepare();
+			return this;
+		}
+
+		public void Dispose() mut
+		{
+			if (mData != sEmptyData)
+				delete mData;
+			delete mChangeList;
+			mData = sEmptyData;
+			mLength = 0;
+		}
+
+		public void DuplicateFrom(ref IdSpan span) mut
+		{
+			Dispose();
+			this = span.Duplicate();
+		}
+
+        public static readonly IdSpan Empty = IdSpan(sEmptyData, 1);
+
+        public void Insert(int index, int length, ref int32 curId) mut
+        {
+			var index;
+			var length;
+
+			if (mChangeList == null)
+				mChangeList = new .();
+			else if (mChangeList.Count > 0)
+			{
+				var change = ref mChangeList.Back;
+				if (change case .Insert(let prevIndex, let prevId, var ref prevLength))
+				{
+					if ((prevIndex + prevLength == index) && (prevId + prevLength == curId))
+					{
+						int16 curLen = (int16)Math.Min(length, 0x7FFF - prevLength);
+						prevLength += curLen;
+
+						curId += curLen;
+						index += curLen;
+						length -= curLen;
+					}
+				}
+			}
+
+			while (length > 0)
+			{
+				int16 curLen = (int16)Math.Min(length, 0x7FFF);
+				mChangeList.Add(.Insert((int32)index, curId, curLen));
+
+				curId += curLen;
+				index += curLen;
+				length -= curLen;
+			}
+
+			if (mAlwaysPrepare)
+				Prepare();
+			else
+				MaybePrepare();
+        }
+
+        public void Remove(int index, int length) mut
+        {
+			var index;
+			var length;
+
+			if (mChangeList == null)
+				mChangeList = new .();
+			else if (mChangeList.Count > 0)
+			{
+				var change = ref mChangeList.Back;
+				if (change case .Remove(let prevIndex, var ref prevLength))
+				{
+					if (prevIndex == index)
+					{
+						int16 curLen = (int16)Math.Min(length, 0x7FFF - prevLength);
+						prevLength += curLen;
+						length -= curLen;
+					}
+				}
+			}
+
+			while (length > 0)
+			{
+				int16 curLen = (int16)Math.Min(length, 0x7FFF);
+				mChangeList.Add(.Remove((int32)index, curLen));
+				length -= curLen;
+			}
+
+			if (mAlwaysPrepare)
+				Prepare();
+			else
+				MaybePrepare();
+        }
+        
+        public int GetIndexFromId(int32 findCharId)
+        {
+			Debug.Assert(!HasChangeList);
+            int encodeIdx = 0;
+            int charId = 1;
+            int charIdx = 0;
+            while (true)
+            {
+                int cmd = Utils.DecodeInt(mData, ref encodeIdx);
+                if (cmd > 0)
+                    charId = cmd;
+                else
+                {
+                    int spanSize = -cmd;
+                    if ((findCharId >= charId) && (findCharId < charId + spanSize))
+                        return charIdx + (findCharId - charId);
+                    charId += spanSize;
+                    charIdx += spanSize;
+
+                    if (cmd == 0)
+                        return -1;
+                }
+            }
+        }
+
+        public int32 GetIdAtIndex(int findIndex)
+        {
+            int encodeIdx = 0;
+            int32 charId = 1;
+            int charIdx = 0;
+            while (true)
+            {
+                int32 cmd = Utils.DecodeInt(mData, ref encodeIdx);
+                if (cmd > 0)
+                    charId = cmd;
+                else
+                {
+                    int32 spanSize = -cmd;
+                    if ((findIndex >= charIdx) && (findIndex < charIdx + spanSize))
+                        return charId + (int32)(findIndex - charIdx);
+                    charId += spanSize;
+                    charIdx += spanSize;
+
+                    if (cmd == 0)
+                        return -1;
+                }
+            }
+        }
+
+        public IdSpan Duplicate()
+        {
+			Debug.Assert(!HasChangeList);
+            IdSpan idSpan = IdSpan();
+            if (mData != null)
+            {
+                idSpan.mData = new uint8[mLength];
+				mData.CopyTo(idSpan.mData, 0, 0, mLength);
+                idSpan.mLength = mLength;
+            }
+            return idSpan;
+        }
+
+        public bool Equals(IdSpan idData2)
+        {
+			Debug.Assert(!HasChangeList);
+			Debug.Assert(!idData2.HasChangeList);
+			
+			if ((mLength == 0) || (idData2.mLength == 0))
+				return (mLength == 0) && (idData2.mLength == 0);
+
+            int encodeIdx1 = 0;
+            int encodeIdx2 = 0;
+
+            int curSpanId1 = 1;
+            int curSpanId2 = 1;
+
+            int spanLen1 = 0;
+            int spanLen2 = 0;
+
+            while (true)
+            {
+                while (spanLen1 == 0)
+                {
+                    int cmd = Utils.DecodeInt(mData, ref encodeIdx1);
+                    if (cmd < 0)
+                    {
+                        spanLen1 = -cmd;
+                    }
+                    else
+                    {
+                        curSpanId1 = cmd;
+                        if (cmd == 0)
+                            break;
+                    }
+                }
+
+                while (spanLen2 == 0)
+                {
+                    int32 cmd = Utils.DecodeInt(idData2.mData, ref encodeIdx2);
+                    if (cmd < 0)
+                    {
+                        spanLen2 = -cmd;
+                    }
+                    else
+                    {
+                        curSpanId2 = cmd;
+                        if (cmd == 0)
+                        {
+                            // They are equal if both spans are at the end
+                            return spanLen1 == 0;
+                        }
+                    }
+                }
+
+                if (curSpanId1 != curSpanId2)
+                    return false;
+                int minLen = Math.Min(spanLen1, spanLen2);
+                curSpanId1 += minLen;
+                spanLen1 -= minLen;
+                curSpanId2 += minLen;
+                spanLen2 -= minLen;
+            }
+        }
+
+        public int GetTotalLength()
+        {
+            int len = 0;
+            int encodeIdx = 0;
+            while (true)
+            {
+                int cmd = Utils.DecodeInt(mData, ref encodeIdx);
+                if (cmd == 0)
+                    return len;
+                if (cmd < 0)
+                    len += -cmd;
+            }
+        }
+
+        static bool FindId(uint8[] idData, ref int encodeIdx, ref int32 curSpanId, ref int32 spanLen, int32 findSpanId)
+        {
+            while (true)
+            {
+                int32 cmd = Utils.DecodeInt(idData, ref encodeIdx);
+                if (cmd < 0)
+                {
+                    spanLen = -cmd;
+                    if ((findSpanId >= curSpanId) && (findSpanId < curSpanId + spanLen))
+                    {
+                        int32 delta = findSpanId - curSpanId;
+                        curSpanId += delta;
+                        spanLen -= delta;
+                        return true;
+                    }
+					curSpanId += spanLen;
+                }
+                else
+                {
+                    curSpanId = cmd;
+                    if (cmd == 0)
+                        return false;
+                }
+            }
+        }
+
+        public bool IsRangeEqual(IdSpan idData2, int32 startCharId, int32 endCharId)
+        {
+            int encodeIdx1 = 0;
+            int encodeIdx2 = 0;
+
+            int32 curSpanId1 = 1;
+            int32 curSpanId2 = 1;
+
+            int32 spanLen1 = 0;
+            int32 spanLen2 = 0;
+
+            if (!FindId(mData, ref encodeIdx1, ref curSpanId1, ref spanLen1, startCharId))
+                return false;
+            if (!FindId(idData2.mData, ref encodeIdx2, ref curSpanId2, ref spanLen2, startCharId))
+                return false;
+
+            while (true)
+            {
+                while (spanLen1 == 0)
+                {
+                    int32 cmd = Utils.DecodeInt(mData, ref encodeIdx1);
+                    if (cmd < 0)
+                    {
+                        spanLen1 = -cmd;
+                    }
+                    else
+                    {
+                        curSpanId1 = cmd;
+                        if (cmd == 0)
+                            break;
+                    }
+                }
+
+                while (spanLen2 == 0)
+                {
+                    int32 cmd = Utils.DecodeInt(idData2.mData, ref encodeIdx2);
+                    if (cmd < 0)
+                    {
+                        spanLen2 = -cmd;
+                    }
+                    else
+                    {
+                        curSpanId2 = cmd;
+                        if (cmd == 0)
+                        {
+                            // They are equal if both spans are at the end
+                            return spanLen1 == 0;
+                        }
+                    }
+                }
+
+                if (curSpanId1 != curSpanId2)
+                    return false;
+                if (curSpanId1 == endCharId)
+                    return true;
+
+                int minLen = Math.Min(spanLen1, spanLen2);
+                if ((endCharId >= curSpanId1) && (endCharId < curSpanId1 + minLen))
+                    minLen = Math.Min(minLen, endCharId - curSpanId1);
+                if ((endCharId >= curSpanId2) && (endCharId < curSpanId2 + minLen))
+                    minLen = Math.Min(minLen, endCharId - curSpanId2);
+
+                curSpanId1 += (int32)minLen;
+                spanLen1 -= (.)minLen;
+                curSpanId2 += (int32)minLen;
+                spanLen2 -= (.)minLen;
+            }
+        }
+
+        public static IdSpan GetDefault(int32 length)
+        {            
+            uint8[] idData = new uint8[8];
+            int encodeIdx = 0;
+            Utils.EncodeInt(idData, ref encodeIdx, (int32)-length);
+            Utils.EncodeInt(idData, ref encodeIdx, 0);
+
+            IdSpan idSpan = IdSpan();
+            idSpan.mData = idData;
+            idSpan.mLength = (int32)encodeIdx;
+            return idSpan;
+        }
+
+		public void Dump() mut
+		{
+			Prepare();
+			Debug.WriteLine("IdSpan Dump:");
+		    int encodeIdx = 0;
+		    int charId = 1;
+		    int charIdx = 0;
+		    while (true)
+		    {
+		        int32 cmd = Utils.DecodeInt(mData, ref encodeIdx);
+		        if (cmd > 0)
+		        {
+					charId = cmd;
+					Debug.WriteLine(" Id: {0}", charId);
+				}
+		        else
+		        {
+		            int32 spanSize = -cmd;
+		            
+		            charId += spanSize;
+		            charIdx += spanSize;
+
+		            if (cmd == 0)
+		                return;
+
+					Debug.WriteLine(" Len: {0}", spanSize);
+		        }
+		    }
+		}
+    }
+}

+ 17 - 0
BeefLibs/Beefy2D/src/utils/ManualBreak.bf

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Runtime;
+using System.Diagnostics;
+
+namespace Beefy.utils
+{
+    public static class ManualBreak
+    {
+        public static void Break()
+        {
+			ThrowUnimplemented();
+            //Debugger.Break();
+        }
+    }
+}

+ 79 - 0
BeefLibs/Beefy2D/src/utils/PerfTimer.bf

@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Beefy.utils
+{
+    public abstract class PerfTimer
+    {
+        [StdCall, CLink]
+        extern static void PerfTimer_ZoneStart(char8* name);
+
+        [StdCall, CLink]
+        extern static void PerfTimer_ZoneEnd();
+
+        [StdCall, CLink]
+        extern static void PerfTimer_Message(char8* theString);
+
+        [StdCall, CLink]
+        extern static int32 PerfTimer_IsRecording();
+
+        [StdCall, CLink]
+        extern static void PerfTimer_StartRecording();
+
+        [StdCall, CLink]
+        extern static void PerfTimer_StopRecording();
+
+        [StdCall, CLink]
+        extern static void PerfTimer_DbgPrint();
+
+        static DisposeProxy mZoneEndDisposeProxy ~ delete _;
+
+        public static DisposeProxy ZoneStart(String name)
+        {
+            if (mZoneEndDisposeProxy == null)
+                mZoneEndDisposeProxy = new DisposeProxy(new => ZoneEnd);
+
+            PerfTimer_ZoneStart(name);
+            return mZoneEndDisposeProxy;
+        }
+
+        public static void ZoneEnd()
+        {
+            PerfTimer_ZoneEnd();
+        }
+
+        public static void Message(String theString)
+        {
+            PerfTimer_Message(theString);
+        }
+
+        public static void Message(String format, params Object[] theParams)
+        {
+			String outStr = scope String();
+			outStr.AppendF(format, params theParams);
+            Message(outStr);
+        }
+
+        public static bool IsRecording()
+        {
+            return PerfTimer_IsRecording() != 0;
+        }
+
+        public static void StartRecording()
+        {
+            PerfTimer_StartRecording();
+        }
+
+        public static void StopRecording()
+        {
+            PerfTimer_StopRecording();
+        }
+
+        public static void DbgPrint()
+        {
+            PerfTimer_DbgPrint();
+        }
+    }
+}

+ 74 - 0
BeefLibs/Beefy2D/src/utils/SmoothValue.bf

@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Beefy.utils
+{
+    public class SmoothValue
+    {
+        public double mSrc;
+        public double mDest;
+        public float mPct;
+        public float mSpeed = 1.0f;
+        public float mSpeedScale = 1.0f;
+
+        public double v
+        {
+            get
+            {
+                if (mPct == 1.0f)
+                    return mDest;
+                return mSrc + (mDest - mSrc) * EasedPct;
+            }
+        }
+
+        public double EasedPct
+        {
+            get
+            {
+                if (mPct == 1.0f)
+                    return 1.0f;
+                return Utils.EaseInAndOut(mPct);
+            }
+        }
+
+        public bool IsMoving
+        {
+            get { return mPct != 1.0f; }
+        }
+
+        public void Update()
+        {
+            mPct = Math.Min(1.0f, mPct + mSpeed * mSpeedScale);
+        }
+
+        public void Set(double val, bool immediate = false)
+        {
+            if ((!immediate) && (val == mDest))
+                return;
+
+            if ((mPct != 1.0f) && (mPct != 0.0f))
+            {                
+                double cur = v;
+
+                if (mPct > 0.80f)
+                    mPct = 0.80f;
+
+                mDest = val;
+                mSrc = -(cur - mDest * EasedPct) / (EasedPct - 1);
+                mSpeedScale = (1.0f - mPct);
+            }
+            else
+            {
+                mSrc = v;
+                mPct = 0.0f;
+                mSpeedScale = 1.0f;
+                mDest = val;
+            }
+
+
+            if (immediate)
+                mPct = 1.0f;
+        }
+    }
+}

+ 2552 - 0
BeefLibs/Beefy2D/src/utils/StructuredData.bf

@@ -0,0 +1,2552 @@
+using System;
+using System.Collections.Generic;
+using System.Collections;
+using System.Text;
+using System.Diagnostics;
+using System.IO;
+
+namespace Beefy.utils
+{
+    public class StructuredData : ILeakIdentifiable
+    {
+		public enum Error
+		{
+			case FileError;
+			case FormatError(int line);
+			case ParseError;
+			case ColonNotExpected;
+			case KeyInArray;
+			case KeyRequiredForVal;
+			case ValueExpected;
+			case PrecedingCommaExpected;
+			case UnexpectedObjectEnd;
+			case ExpectedArrayNameEnd;
+
+			/*public override void ToString(String str)
+			{
+				switch (this)
+				{
+				case FormatError: str.Append("Format error");
+				case ParseError: str.Append("Parse error");
+				case ColonNotExpected: str.Append("Colon not expected");
+				case KeyInArray: str.Append("Cannot add key/val to array");
+				case KeyRequiredForVal:  str.Append("Key required for value");
+				case ValueExpected: str.Append("Value expected");
+				case PrecedingCommaExpected: str.Append("Preceding comma expected");
+				case UnexpectedObjectEnd: str.Append("Unexpected object end");
+				}
+			}*/
+		}
+
+        public struct Enumerator : IEnumerator<StringView>
+        {
+			StructuredData mStructuredData;
+			bool mIsFirst = true;
+
+            public this(StructuredData data)
+            {
+                mStructuredData = data;
+            }
+
+            public StringView Current
+            {
+                get
+                {
+					if (mStructuredData.mCurrent.mLastKey != -1)
+						return mStructuredData.mKeys[mStructuredData.mCurrent.mLastKey];
+					return StringView();
+                }
+            }
+
+            public bool MoveNext() mut
+            {
+				var current = ref mStructuredData.mCurrent;
+
+				if (current.mLastValue == -1)
+					return false;
+
+                if (mIsFirst)
+				{
+					//mStructuredData.mOpenStack.Add(mStructuredData.mCurrent);
+					//mStructuredData.mCurrent = CurrentEntry(mValues);
+
+					mIsFirst = false;
+					return true;
+				}
+
+				if (current.mLastKey != -1)
+					current.mLastKey = mStructuredData.mNextKeys[current.mLastKey];
+				current.mLastValue = mStructuredData.mNextValues[current.mLastValue];
+				return current.mLastValue != -1;
+            }
+
+			public void Dispose()
+			{
+				//if (mIsOpen)
+				{
+					mStructuredData.Close();
+				}
+			}
+
+			public Result<StringView> GetNext() mut
+			{
+				if (!MoveNext())
+					return .Err;
+				return Current;
+			}
+        }
+
+		public class Values
+		{
+			public int32 mValueIdx = -1;
+
+			public virtual bool IsArray
+			{
+				get
+				{
+                    return true;
+				}
+			}
+
+			public virtual bool IsObject
+			{
+				get
+				{
+                    return false;
+				}
+			}
+
+			public virtual bool ForceInline
+			{
+				get
+				{
+					return false;
+				}
+			}
+		}
+
+		public class InlineValues : Values
+		{
+			public override bool ForceInline
+			{
+				get
+				{
+					return true;
+				}
+			}
+		}
+
+		public class NamedValues : Values
+		{
+			public int32 mKeyIdx = -1;
+
+			public override bool IsArray
+			{
+				get
+				{
+                    return false;
+				}
+			}
+
+			public override bool IsObject
+			{
+				get
+				{
+                    return true;
+				}
+			}
+		}
+
+		public class InlineNamedValues : NamedValues
+		{
+			public override bool ForceInline
+			{
+				get
+				{
+					return true;
+				}
+			}
+		}
+
+		struct CurrentEntry
+		{
+			public Values mValues;
+			public int32 mLastValue = -1;
+			public int32 mLastKey = -1;
+
+			public this(Values values)
+			{
+				mValues = values;
+			}
+
+			public bool HasValues
+			{
+				get
+				{
+					return mLastValue != -1;
+				}
+			}
+		}
+
+		static Object sEmptySentinel = new Object() ~ delete _;
+
+		BumpAllocator mBumpAllocator = CreateAllocator() ~ delete _;
+		String mSource;
+		List<CurrentEntry> mOpenStack = new:mBumpAllocator List<CurrentEntry>() ~ delete:mBumpAllocator _;
+
+		Values mHeadValues;
+
+        CurrentEntry mCurrent = .(null);
+		NamedValues mEmptyData;
+        
+        List<Object> mValues ~
+	        {
+				/*for (var item in _)
+					if (item != sEmptySentinel)
+						delete item;*/
+				delete:mBumpAllocator _;
+			};
+
+        List<StringView> mKeys ~ delete:mBumpAllocator _;
+
+		List<int32> mNextValues ~ delete:mBumpAllocator _;
+		List<int32> mNextKeys ~ delete:mBumpAllocator _;
+
+        Dictionary<String, Object> mMap ~ delete:mBumpAllocator _;
+
+        DisposeProxy mStructuredDisposeProxy ~ delete _;
+        
+        bool mCanWrite = true;
+        bool mCanRead = true;
+
+        public this()
+        {
+            
+        }
+
+		public ~this()
+		{
+		}
+
+		protected virtual BumpAllocator CreateAllocator()
+		{
+			var bumpAlloc = new BumpAllocator();
+			bumpAlloc.DestructorHandling = .Ignore;
+			return bumpAlloc;
+		}
+
+        /*public ListRef<String> Keys
+        {
+            get 
+            {
+				if (var namedValues = mCurrent as NamedValues)
+				{
+				 	return ListRef<String>(mKeys, namedValues.mKeyIdx, namedValues.mCount);
+				}
+                return ListRef<String>(null, 0, 0);
+            }
+        }
+
+        public ListRef<Object> Values
+        {
+            get 
+            {
+                return ListRef<Object>(mValues, mCurrent.mValueIdx, mCurrent.mCount);
+            }
+        }
+        
+        public int Count
+        {
+            get { return mCurrent.mCount; }
+        }*/
+
+        public bool IsArray
+        {
+            get { return GetCurrent().GetType() == typeof(Values); }
+        }
+
+        public bool IsObject
+        {
+            get { return GetCurrent().GetType() == typeof(NamedValues); }
+        }
+
+		public int GetValueCount()
+		{
+			int count = 0;
+			int32 checkValue = mCurrent.mValues.mValueIdx;
+			while (checkValue != -1)
+			{
+				if (mValues[checkValue] != sEmptySentinel)
+					count++;
+				checkValue = mNextValues[checkValue];
+			}
+			return count;
+		}
+
+        /*public Values Current
+        {
+            get { return mCurrent; } 
+        }*/
+
+		/*protected override void GCMarkMembers()
+		{
+			// We do this because these containers are held in the bump allocator so they don't get marked
+			GC.Mark(mKeys.[Friend]mItems);
+			GC.Mark(mNextValues.[Friend]mItems);
+			GC.Mark(mNextKeys.[Friend]mItems);
+			if (mMap != null)
+			{
+				GC.Mark(mMap.[Friend]mBuckets);
+				GC.Mark(mMap.[Friend]mEntries);
+			}
+		}*/
+
+		static int32 sIdx;
+		int32 mIdx = sIdx++;
+		public void ToLeakString(String str)
+		{
+			str.AppendF("Idx:{0}", mIdx);
+		}
+
+        public bool TryGet(String name, out Object value)
+        {
+			var current = GetCurrent();
+			if ((current == null) || (current.GetType() != typeof(NamedValues)))
+			{
+				value = null;
+				return false;
+			}
+
+			let namedValues = (NamedValues)current;
+
+            /*if (mMap != null)
+                return mMap.TryGetValue(name, out value);
+
+            if (mKeys.Count > 32)
+            {
+                mMap = new Dictionary<String, Object>();
+
+                // Only create a map for large lists
+                for (int32 i = 0; i < mKeys.Count; i++)
+                    mMap[mKeys[i]] = mValues[i];
+                return mMap.TryGetValue(name, out value);
+            }*/
+
+			int32 checkKey = namedValues.mKeyIdx;
+			int32 checkValue = namedValues.mValueIdx;
+			while (checkKey != -1)
+			{
+				if (mKeys[checkKey] == name)
+				{
+					value = mValues[checkValue];
+					return true;
+				}
+
+				checkValue = mNextValues[checkValue];
+				checkKey = mNextKeys[checkKey];
+			}
+
+			value = null;
+			return false;
+        }
+
+		public bool TryGet(String name, out NamedValues namedValues, out int keyIdx, out int valueIdx)
+		{
+			var current = GetCurrent();
+			if ((current == null) || (current.GetType() != typeof(NamedValues)))
+			{
+				//value = null;
+				namedValues = null;
+				valueIdx = -1;
+				keyIdx = -1;
+				return false;
+			}
+
+			namedValues = (NamedValues)current;
+
+		    /*if (mMap != null)
+		        return mMap.TryGetValue(name, out value);
+
+		    if (mKeys.Count > 32)
+		    {
+		        mMap = new Dictionary<String, Object>();
+
+		        // Only create a map for large lists
+		        for (int32 i = 0; i < mKeys.Count; i++)
+		            mMap[mKeys[i]] = mValues[i];
+		        return mMap.TryGetValue(name, out value);
+		    }*/
+
+			int32 checkKey = namedValues.mKeyIdx;
+			int32 checkValue = namedValues.mValueIdx;
+			while (checkKey != -1)
+			{
+				if (mKeys[checkKey] == name)
+				{
+					valueIdx = checkValue;
+					keyIdx = checkKey;
+					return true;
+				}
+
+				checkValue = mNextValues[checkValue];
+				checkKey = mNextKeys[checkKey];
+			}
+
+			namedValues = null;
+			valueIdx = -1;
+			keyIdx = -1;
+			return false;
+		}
+
+        public bool Contains(String name)
+        {
+            Object value;
+            return TryGet(name, out value);
+        }
+
+        public Object Get(String name)
+        {
+            Object value;
+            TryGet(name, out value);
+            return value;
+        }
+
+		public bool Get(String name, ref bool val)
+		{
+		    Object aVal = Get(name);
+		    if ((aVal == null) || (!(aVal is bool)))
+		        return false;
+		    val = (bool)aVal;
+			return true;
+		}
+
+		public void Get(String name, ref int32 val)
+		{
+			Object obj = Get(name);
+			if (obj == null)
+				return;
+			switch (obj.GetType())
+			{
+			case typeof(Int32): val = (int32)obj;
+			default:
+			}
+		}
+
+		public void Get(String name, ref float val)
+		{
+			Object obj = Get(name);
+			if (obj == null)
+				return;
+			switch (obj.GetType())
+			{
+			case typeof(Int32): val = (int32)obj;
+			case typeof(Float): val = (float)obj;
+			default:
+			}
+		}
+
+		public void Get<T>(String name, ref T val) where T : Enum
+		{
+			Object obj = Get(name);
+			if (obj == null)
+				return;
+			Result<T> result;
+			if (let str = obj as String)
+				result = Enum.Parse<T>(str);
+			else if (obj is StringView)
+				result = Enum.Parse<T>((StringView)obj);
+			else
+				return;
+			if (result case .Ok(var parsedVal))
+				val = parsedVal;
+		}
+
+		public void Get(String name, String outString)
+		{
+		    Object obj = Get(name);
+			if (obj == null)
+				return;
+			outString.Clear();
+		    if (obj is uint64)
+		        obj.ToString(outString);
+			if (obj != null)
+			{
+				var type = obj.GetType();
+				if (type == typeof(StringView))
+					outString.Append((StringView)obj);
+				else if (type == typeof(String))
+		            outString.Append((String)obj);
+			}
+		}
+
+        public Object GetCurrent()
+        {
+			/*int32 checkValue = mCurrent.mValues.mValueIdx;
+			for (int i < idx)
+			{
+				checkValue = mNextValues[checkValue];
+			}
+			return mValues[checkValue];*/
+			if (mCurrent.mLastValue == -1)
+				return null;
+			return mValues[mCurrent.mLastValue];
+        }
+
+        public void GetString(String name, String outString, String theDefault = null)
+        {
+            Object val = Get(name);
+
+			outString.Clear();
+            if (val is uint64)
+                val.ToString(outString);
+
+			if (val != null)
+			{
+				var type = val.GetType();
+				if (type == typeof(StringView))
+				{
+					outString.Append((StringView)val);
+					return;
+				}
+				if (type == typeof(String))
+				{
+                    outString.Append((String)val);
+					return;
+				}
+			}
+            
+			if (theDefault != null)
+            	outString.Append(theDefault);
+			return;
+        }
+
+        public int32 GetInt(String name, int32 theDefault = 0)
+        {
+            Object aVal = Get(name);
+            if ((aVal == null) || (!(aVal is int32)))
+                return theDefault;
+            return (int32)aVal;
+        }
+
+        public int64 GetLong(String name, int64 theDefault = 0)
+        {
+            Object aVal = Get(name);
+
+            if (aVal is int32)
+                return (int64)(int32)aVal;
+
+            if ((aVal == null) || (!(aVal is int64)))
+                return theDefault;
+            return (int64)aVal;
+        }
+
+        public uint64 GetULong(String name, uint64 theDefault = 0)
+        {
+            Object aVal = Get(name);
+
+            if (aVal is int32)
+                return (uint64)(int32)aVal;
+
+            if ((aVal == null) || (!(aVal is uint64)))
+                return theDefault;
+            return (uint64)aVal;
+        }
+
+        public float GetFloat(String name, float theDefault = 0)
+        {
+            Object val = Get(name);
+			if (val == null)
+				return theDefault;
+			switch (val.GetType())
+			{
+			case typeof(Float): return (float)val;
+			case typeof(Int32): return (int32)val;
+			case typeof(Int): return (int)val;
+			default: return theDefault;
+			}
+        }
+
+        public bool GetBool(String name, bool theDefault = false)
+        {
+            Object aVal = Get(name);
+            if ((aVal == null) || (!(aVal is bool)))
+                return theDefault;
+            return (bool)aVal;
+        }
+
+        public T GetEnum<T>(String name, T defaultVal = default(T)) where T : Enum
+        {
+            Object obj = Get(name);
+			if (obj == null)
+				return defaultVal;
+
+			Result<T> result;
+			if (let str = obj as String)
+				result = Enum.Parse<T>(str);
+			else if (obj is StringView)
+				result = Enum.Parse<T>((StringView)obj);
+            else
+				return defaultVal;
+
+			if (result case .Ok(var val))
+				return val;
+			return defaultVal;
+        }
+
+		public bool GetEnum<T>(String name, ref T val) where T : Enum
+		{
+			Object obj = Get(name);
+			if (obj == null)
+				return false;
+
+			Result<T> result;
+			if (let str = obj as String)
+				result = Enum.Parse<T>(str);
+			else if (obj is StringView)
+				result = Enum.Parse<T>((StringView)obj);
+			else
+				return false;
+
+			if (result case .Ok(out val))
+				return true;
+			return false;
+		}
+
+        ///
+        public void GetCurString(String outString, String theDefault = null)
+        {
+            Object val = GetCurrent();
+
+			outString.Clear();
+			if (val is uint64)
+			    val.ToString(outString);
+
+			if (val != null)
+			{
+				var type = val.GetType();
+				if (type == typeof(StringView))
+				{
+					outString.Append((StringView)val);
+					return;
+				}
+				if (type == typeof(String))
+				{
+			        outString.Append((String)val);
+					return;
+				}
+			}
+
+			if (theDefault != null)
+				outString.Append(theDefault);
+			return;
+        }
+
+        public int32 GetCurInt(int32 theDefault = 0)
+        {
+            Object aVal = GetCurrent();
+            if ((aVal == null) || (!(aVal is int32)))
+                return theDefault;
+            return (int32)aVal;
+        }
+
+        public uint32 GetCurUInt(uint32 theDefault = 0)
+        {
+            Object aVal = GetCurrent();
+            if ((aVal == null) || (!(aVal is uint32)))
+                return theDefault;
+            return (uint32)aVal;
+        }
+
+        public float GetCurFloat(float theDefault = 0)
+        {
+            Object aVal = GetCurrent();
+            if ((aVal == null) || (!(aVal is float)))
+                return theDefault;
+            return (float)aVal;
+        }
+
+        public bool GetCurBool(bool theDefault = false)
+        {
+            Object aVal = GetCurrent();
+            if ((aVal == null) || (!(aVal is bool)))
+                return theDefault;
+            return (bool)aVal;
+        }
+
+        ///
+        
+        public void DoAdd(ref CurrentEntry currentEntry, Object obj)
+        {
+            //Temporary type check?
+            Debug.Assert(IsValidWriteObject(obj));
+            Debug.Assert(mCanWrite);
+
+            Debug.Assert(!currentEntry.mValues.IsObject); // Can't be an object
+
+			Debug.Assert((currentEntry.mValues.mValueIdx == -1) == (currentEntry.mLastValue == -1));
+
+			EnsureHasData();
+
+			int valueIdx = mValues.Count;
+			if (currentEntry.mLastValue != -1)
+				mNextValues[currentEntry.mLastValue] = (int32)valueIdx;
+			else
+				currentEntry.mValues.mValueIdx = (int32)valueIdx;
+
+			currentEntry.mLastValue = (int32)valueIdx;
+			mValues.Add(obj);
+			mNextValues.Add(-1);
+        }
+
+		public void Add<T>(T value) where T : struct
+		{
+		    DoAdd(ref mCurrent, new:mBumpAllocator box value);
+		}
+
+		public void Add(String value)
+		{
+			DoAdd(ref mCurrent, (Object)new:mBumpAllocator String(value));
+		}
+
+		public void Add<T>(String name, T value) where T : struct
+		{
+		    DoAdd(ref mCurrent, AllocStringView(name), new:mBumpAllocator box value);
+		}
+
+		public void ConditionalAdd<T>(String name, T value) where T : var
+		{
+			if (value != default(T))
+				Add(name, value);
+		}
+
+		public void ConditionalAdd<T>(String name, T value, T defaultVal) where T : var
+		{
+			if ((value != null) && (value != defaultVal))
+				Add(name, value);
+		}
+
+		void EnsureHasData()
+		{
+			if (mValues == null)
+			{
+				mValues = new:mBumpAllocator ListWithAlloc<Object>(mBumpAllocator);
+				mKeys = new:mBumpAllocator ListWithAlloc<StringView>(mBumpAllocator);
+				mNextKeys = new:mBumpAllocator ListWithAlloc<int32>(mBumpAllocator);
+				mNextValues = new:mBumpAllocator ListWithAlloc<int32>(mBumpAllocator);
+			}
+		}
+
+		void DoAdd(ref CurrentEntry currentEntry, StringView name, Object value)
+		{
+			EnsureHasData();
+
+		    //Temporary type check?
+		    Debug.Assert(IsValidWriteObject(value));
+		    Debug.Assert(mCanWrite);
+
+		    Debug.Assert(!currentEntry.mValues.IsArray); // Can't be an array
+
+			Debug.Assert((currentEntry.mValues.mValueIdx == -1) == (currentEntry.mLastValue == -1));
+
+			var namedValues = currentEntry.mValues as NamedValues;
+
+			int valueIdx = mValues.Count;
+			int keyIdx = mKeys.Count;
+
+			if (currentEntry.mLastValue != -1)
+				mNextValues[currentEntry.mLastValue] = (int32)valueIdx;
+			else
+				currentEntry.mValues.mValueIdx = (int32)valueIdx;
+
+			if (currentEntry.mLastKey != -1)
+				mNextKeys[currentEntry.mLastKey] = (int32)keyIdx;
+			else
+				namedValues.mKeyIdx = (int32)keyIdx;
+
+			currentEntry.mLastValue = (int32)valueIdx;
+			currentEntry.mLastKey = (int32)keyIdx;
+			mKeys.Add(name);
+			mNextKeys.Add(-1);
+			mValues.Add(value);
+			mNextValues.Add(-1);
+		}
+
+		StringView AllocStringView(String str)
+		{
+			int len = str.Length;
+			char8* ptr = (char8*)mBumpAllocator.Alloc(len, 1);
+			Internal.MemCpy(ptr, str.Ptr, len);
+			return StringView(ptr, len);
+		}
+
+		public void Add(String name, String value)
+		{
+			DoAdd(ref mCurrent, AllocStringView(name), (Object)new:mBumpAllocator String(value));
+		}
+
+		public void ConditionalAdd(String name, String value, String defaultVal)
+		{
+			if (value != defaultVal)
+				DoAdd(ref mCurrent, AllocStringView(name), (Object)new:mBumpAllocator String(value));
+		}
+
+		public void ConditionalAdd(String name, String value)
+		{
+			if ((value != null) && (value != ""))
+				DoAdd(ref mCurrent, AllocStringView(name), (Object)new:mBumpAllocator String(value));
+		}
+        
+        ///
+
+        public void Close()
+        {
+            mCurrent = mOpenStack.PopBack();
+        }
+
+		public void RemoveIfEmpty()
+		{
+			bool hasValue = false;
+
+			if (mCurrent.mLastValue != -1)
+			{
+				if (mValues[mCurrent.mLastValue] != sEmptySentinel)
+					hasValue = true;
+				else
+				{
+					// We need a (potentially) full value pass if we have sEmptySentinel
+					int checkValue = mCurrent.mValues.mValueIdx;
+					while (checkValue != -1)
+					{
+						if (mValues[checkValue] != sEmptySentinel)
+						{
+							hasValue = true;
+							break;
+						}
+						checkValue = mNextValues[checkValue];
+					}
+				}
+			}
+
+			if (!hasValue)
+			{
+				var prev = ref mOpenStack.Back;
+				var lastVal = mValues[prev.mLastValue];
+				Debug.Assert(lastVal == mCurrent.mValues);
+
+				if (lastVal == mCurrent.mValues)
+				{
+					mValues[prev.mLastValue] = sEmptySentinel;
+				}
+			}
+		}
+
+        public IDisposable Open(String name)
+        {
+            if (mStructuredDisposeProxy == null)
+            {
+                mStructuredDisposeProxy = new DisposeProxy();
+                mStructuredDisposeProxy.mDisposeProxyDelegate = new => Close;
+            }
+
+			mOpenStack.Add(mCurrent);
+
+            NamedValues namedValues;
+			int keyIdx;
+			int valueIdx;
+            if (TryGet(name, out namedValues, out keyIdx, out valueIdx))
+            {
+                mCurrent = CurrentEntry(namedValues);
+				mCurrent.mLastKey = (int32)keyIdx;
+				mCurrent.mLastValue = (int32)valueIdx;
+            }
+            else
+            {
+				if (mEmptyData == null)
+				{
+					mEmptyData = new:mBumpAllocator NamedValues();
+				}
+                mCurrent = CurrentEntry(mEmptyData);
+            }            
+
+            return mStructuredDisposeProxy;
+        }
+
+		public Enumerator Enumerate(String name)
+		{
+			Enumerator enumerator;
+
+		    mOpenStack.Add(mCurrent);
+
+		    Object val;
+		    if ((TryGet(name, out val)) && (let values = val as Values))
+		    {
+				mCurrent = CurrentEntry(values);
+				if (let namedValues = values as NamedValues)
+					mCurrent.mLastKey = namedValues.mKeyIdx;
+				mCurrent.mLastValue = values.mValueIdx;
+		        enumerator = Enumerator(this);
+		    }
+		    else
+		    {
+				mCurrent = CurrentEntry(null);
+				enumerator = Enumerator(this);
+		    }            
+
+		    return enumerator;
+		}
+
+		public Enumerator Enumerate()
+		{
+			Enumerator enumerator;
+
+		    mOpenStack.Add(mCurrent);
+
+		    Object val = GetCurrent();
+		    if (let values = val as Values)
+		    {
+				mCurrent = CurrentEntry(values);
+				if (let namedValues = values as NamedValues)
+					mCurrent.mLastKey = namedValues.mKeyIdx;
+				mCurrent.mLastValue = values.mValueIdx;
+		        enumerator = Enumerator(this);
+		    }
+		    else
+		    {
+				mCurrent = CurrentEntry(null);
+				enumerator = Enumerator(this);
+		    }            
+
+		    return enumerator;
+		}
+
+        /*IEnumerator IEnumerable.GetEnumerator()
+        {
+            return new Enumerator(this);
+        }*/
+
+        /*public Enumerator GetEnumerator()
+        {
+            return Enumerator(this, mCurrent.mValues);
+        }*/
+
+        /*public IEnumerable<StructuredData> GetValues(String name)
+        {
+            Open(name);
+            return this;
+        }*/
+
+        IDisposable GetDisposeProxy()
+        {
+            if (mStructuredDisposeProxy == null)
+            {
+                mStructuredDisposeProxy = new DisposeProxy();
+                mStructuredDisposeProxy.mDisposeProxyDelegate = new => Close;
+            }
+
+            return mStructuredDisposeProxy;
+        }
+
+        public IDisposable Open(int index)
+        {
+			ThrowUnimplemented();
+
+            /*if (mStructuredDisposeProxy == null)
+			{
+			    mStructuredDisposeProxy = new DisposeProxy();
+			    mStructuredDisposeProxy.mDisposeProxyDelegate = new => Close;
+			}
+
+			mOpenStack.Add(mCurrent);
+
+			Object val = Get(index);
+			if (let values = val as Values)
+			{
+			    mCurrent = CurrentEntry(values);
+			}
+			else
+			{
+				if (mEmptyData == null)
+				{
+					mEmptyData = new NamedValues();
+				}
+			    mCurrent = CurrentEntry(mEmptyData);
+			}            
+
+			return mStructuredDisposeProxy;*/
+        }
+
+		public IDisposable Open(Enumerator enumerator)
+		{
+			ThrowUnimplemented();
+		    /*if (mStructuredDisposeProxy == null)
+			{
+			    mStructuredDisposeProxy = new DisposeProxy();
+			    mStructuredDisposeProxy.mDisposeProxyDelegate = new => Close;
+			}
+
+			mOpenStack.Add(mCurrent);
+
+			Object val = mValues[enumerator.[Friend]mValueIdx];
+			if (let values = val as Values)
+			{
+			    mCurrent = CurrentEntry(values);
+			}
+			else
+			{
+				if (mEmptyData == null)
+				{
+					mEmptyData = new NamedValues();
+				}
+			    mCurrent = CurrentEntry(mEmptyData);
+			}            
+
+			return mStructuredDisposeProxy;*/
+		}
+
+		public void CreateNew()
+		{
+			if (mCurrent.mValues == null)
+			{
+				NamedValues values = new:mBumpAllocator NamedValues();
+				mCurrent = CurrentEntry(values);
+			}
+		}
+
+        public IDisposable CreateArray()
+        {
+            Values values = new:mBumpAllocator Values();
+            DoAdd(ref mCurrent, values);
+			mOpenStack.Add(mCurrent);
+            mCurrent = CurrentEntry(values);
+            return GetDisposeProxy();
+        }
+
+        public IDisposable CreateObject(bool forceInline = false)
+        {
+            //NamedValues values = new:mBumpAllocator NamedValues();
+			NamedValues values;
+			if (forceInline)
+				values = new:mBumpAllocator InlineNamedValues();
+			else
+				values = new:mBumpAllocator NamedValues();
+
+			DoAdd(ref mCurrent, values);
+			mOpenStack.Add(mCurrent);
+			mCurrent = CurrentEntry(values);
+			return GetDisposeProxy();
+        }
+
+        public IDisposable CreateArray(String name, bool forceInline = false)
+        {
+            //Values values = new:mBumpAllocator Values();
+
+			Values values;
+			if (forceInline)
+				values = new:mBumpAllocator InlineValues();
+			else
+				values = new:mBumpAllocator Values();	
+
+			DoAdd(ref mCurrent, AllocStringView(name), values);
+			mOpenStack.Add(mCurrent);
+			mCurrent = CurrentEntry(values);
+			return GetDisposeProxy();
+        }
+
+        public IDisposable CreateObject(String name, bool forceInline = false)
+        {
+            NamedValues values;
+			if (forceInline)
+				values = new:mBumpAllocator InlineNamedValues();
+			else
+				values = new:mBumpAllocator NamedValues();	
+
+			DoAdd(ref mCurrent, AllocStringView(name), values);
+			mOpenStack.Add(mCurrent);
+			mCurrent = CurrentEntry(values);
+			return GetDisposeProxy();
+        }
+
+		public void ForceInline()
+		{
+			//mCurrent.mValues;
+		}
+
+        void StringEncode(String outString, StringView theString)
+        {
+			outString.Append('"');
+
+            for (int32 i = 0; i < theString.Length; i++)
+            {
+                char8 c = theString.Ptr[i];
+                char8 slashC = '\0';
+
+                switch (c)
+                {
+                    case '\b':
+                        slashC = 'b';
+                        break;
+                    case '\f':
+                        slashC = 'f';
+                        break;
+                    case '\n':
+                        slashC = 'n';
+                        break;
+                    case '\r':
+                        slashC = 'r';
+                        break;
+                    case '\t':
+                        slashC = 't';
+                        break;
+                    case '"':
+                        slashC = '"';
+                        break;
+                    case '\\':
+                        slashC = '\\';
+                        break;
+                }
+
+                if (slashC != '\0')
+                {
+                    outString.Append('\\');
+                    outString.Append(slashC);
+                }
+                else
+                    outString.Append(c);
+            }
+
+            outString.Append('"');
+        }
+
+        static void StringDecode(String inString)
+        {
+            bool lastWasSlash = false;
+
+			char8* headPtr = inString.Ptr;
+			char8* strInPtr = headPtr;
+			char8* strOutPtr = headPtr;
+			char8* strInEndPtr = headPtr + inString.Length;
+
+            while (strInPtr < strInEndPtr)
+            {
+                char8 c = *(strInPtr++);
+				
+                if (lastWasSlash)
+                {
+                    switch (c)
+                    {
+                        case 'b':
+                            c = '\b';
+                            break;
+                        case 'f':
+                            c = '\f';
+                            break;
+                        case 'n':
+                            c = '\n';
+                            break;
+                        case 'r':
+                            c = '\r';
+                            break;
+                        case 't':
+                            c = '\t';
+                            break;
+                    }
+
+                    lastWasSlash = false;
+                }
+                else if (c == '\\')
+                {
+                    lastWasSlash = true;
+					continue;
+				}
+
+				*(strOutPtr++) = c;
+            }
+
+			inString.[Friend]mLength = (int32)(strOutPtr - headPtr);
+        }
+
+        bool IsValidWriteObject(Object theObject)
+        {
+			if (theObject == null)
+				return true;
+
+			var type = theObject.GetType();
+			switch (type)
+			{
+			case typeof(Boolean): return true;
+			case typeof(Int32): return true;
+			case typeof(UInt32): return true;
+			case typeof(Int64): return true;
+			case typeof(UInt64): return true;
+			case typeof(Float): return true;
+			case typeof(Double): return true;	
+			case typeof(Int): return true;
+			case typeof(UInt): return true;
+			case typeof(Values): return true;
+			case typeof(InlineValues): return true;
+			case typeof(NamedValues): return true;
+			case typeof(InlineNamedValues): return true;
+			case typeof(StringView): return true;
+			default:
+				if (type.IsEnum)
+					return true;
+				if ((theObject is String) ||
+					(theObject is StructuredData))
+					return true;
+				return false;
+			}
+        }
+
+        Result<void> ObjectToString(String str, Object theObject)
+        {
+			var type = theObject.GetType();
+			
+			Debug.Assert(type != null);
+
+            if (theObject == null)
+                str.Append("null");
+            else if (type == typeof(String))
+                StringEncode(str, StringView((String)theObject));
+			else if (type == typeof(StringView))
+				StringEncode(str, (StringView)theObject);
+            else if (type == typeof(System.Int32))
+                ((int32)theObject).ToString(str);
+            else if (type == typeof(System.UInt32))
+			{
+                ((uint32)theObject).ToString(str);
+                str.Append("U");
+			}
+            else if (type == typeof(System.Int64))
+			{
+                ((uint64)theObject).ToString(str);
+                str.Append("L");
+			}
+            else if (type == typeof(System.UInt64))
+			{
+                ((uint64)theObject).ToString(str);
+                str.Append("UL");
+			}
+			else if (type == typeof(System.Int))
+			{
+			    ((int32)(int)theObject).ToString(str);
+			    //str.Append("L");
+			}
+			else if (type == typeof(System.UInt))
+			{
+			    ((uint32)(uint)theObject).ToString(str);
+			    //str.Append("UL");
+			}
+            else if (type == typeof(System.Float))
+			{
+                str.AppendF("{0:0.0######}", (float)theObject);
+			}
+			else if (type == typeof(System.Double))
+			{
+			    str.AppendF("{0:0.0######}", (float)(double)theObject);
+			}
+            else if (type == typeof(System.Boolean))
+			{
+                str.Append(((bool)theObject) ? "true" : "false");
+			}
+            else if (type.IsEnum)
+			{
+				String enumStr = scope String();
+				theObject.ToString(enumStr);
+                StringEncode(str, StringView(enumStr));
+			}
+			else
+			{
+				theObject.ToString(str);
+			}
+            /*else
+			{
+                //return new Exception("Invalid type: " + theObject.GetType().FullName);
+				String name = scope String();
+				type.GetName(name);
+				Debug.WriteLine("Invalid Type: {0}", name);
+				return new Exception();
+			}*/
+			return .Ok;
+        }        
+
+        void ToJSONHelper(Values values, String outStr, bool humanReadable, String prefix)
+        {
+			String innerPrefix = scope String(prefix.Length + 1);
+			innerPrefix.Append(prefix);
+			if (humanReadable)
+				innerPrefix.Append('\t');
+
+            if (let namedValues = values as NamedValues)
+			{
+				int keyIdx = namedValues.mKeyIdx;
+				int valueIdx = namedValues.mValueIdx;
+
+			    outStr.Append('{');
+			    if (humanReadable)
+			        outStr.Append('\n');
+
+				bool isFirst = true;
+				while (valueIdx != -1)
+				{
+			        StringView key = mKeys[keyIdx];
+			        Object value = mValues[valueIdx];
+					if (value == sEmptySentinel)
+					{
+						valueIdx = mNextValues[valueIdx];
+						keyIdx = mNextKeys[keyIdx];
+						continue;
+					}
+
+					if (isFirst)
+					{
+						isFirst = false;
+					}
+					else
+					{
+						outStr.Append(',');
+						if (humanReadable)
+							outStr.Append('\n');
+					}
+
+			        if (humanReadable)
+			            outStr.Append(innerPrefix);
+
+					StringEncode(outStr, key);
+
+			        if (humanReadable)
+			            outStr.Append(": ");
+			        else
+			            outStr.Append(":");
+
+			        if (let subValues = value as Values)
+			        {
+			            ToJSONHelper(subValues, outStr, humanReadable, innerPrefix);
+			        }
+			        else
+			            ObjectToString(outStr, value);
+
+					valueIdx = mNextValues[valueIdx];
+					keyIdx = mNextKeys[keyIdx];
+				}
+				if (humanReadable)
+					outStr.Append('\n');
+
+			    if (humanReadable)
+			        outStr.Append(prefix);
+			    outStr.Append('}');                
+			}
+			else
+            {
+			    outStr.Append('[');
+                if (humanReadable)
+                    outStr.Append('\n');
+
+				int valueIdx = values.mValueIdx;
+
+				bool isFirst = true;
+                while (valueIdx != -1)
+                {
+                    Object value = mValues[valueIdx];
+					if (value == sEmptySentinel)
+					{
+						valueIdx = mNextValues[valueIdx];
+						continue;
+					}
+
+					if (isFirst)
+					{
+						isFirst = false;
+					}
+					else
+					{
+						outStr.Append(',');
+						if (humanReadable)
+							outStr.Append('\n');
+					}
+
+                    if (humanReadable)
+                        outStr.Append(innerPrefix);
+
+                    if (let subValues = value as Values)
+					{
+					    ToJSONHelper(subValues, outStr, humanReadable, innerPrefix);
+					}
+					else
+					    ObjectToString(outStr, value);
+
+					valueIdx = mNextValues[valueIdx];
+                }
+				if (humanReadable)
+					outStr.Append('\n');
+
+                if (humanReadable)
+                    outStr.Append(prefix);
+                outStr.Append(']');                
+            }
+        }
+
+        public void ToJSON(String str, bool humanReadable = false)
+        {
+            //String aStringBuilder = new String();
+            ToJSONHelper(mCurrent.mValues, str, humanReadable, "");
+            //return aStringBuilder.ToString();
+        }
+
+		public void ToTOML(String outStr)
+		{
+			List<StringView> nameStack = scope List<StringView>();
+			List<Values> valueStack = scope List<Values>();
+
+			void EncodeName(StringView str)
+			{
+				bool NeedsEncoding(StringView str)
+				{
+					if (str.Length == 0)
+						return true;
+
+					if (str[0].IsNumber)
+						return true;
+
+					for (int i < str.Length)
+					{
+						char8 c = str[i];
+						if ((c.IsLetterOrDigit) || (c == '_') || (c == '-'))
+						{
+							// Valid
+						}
+						else
+							return true;
+					}
+
+					return false;
+				}
+
+				if (NeedsEncoding(str))
+					StringEncode(outStr, str);
+				else
+					outStr.Append(str);
+			}
+
+			void EncodeObject(Object obj)
+			{
+				if (let values = obj as Values)
+				{
+					if (let namedValues = values as NamedValues)
+					{
+						int keyIdx = namedValues.mKeyIdx;
+						int valueIdx = namedValues.mValueIdx;
+
+						outStr.Append('{');
+
+						bool wasFirst = true;
+						while (valueIdx != -1)
+						{
+						    StringView key = mKeys[keyIdx];
+						    Object value = mValues[valueIdx];
+							if (value == sEmptySentinel)
+							{
+								valueIdx = mNextValues[valueIdx];
+								keyIdx = mNextKeys[keyIdx];
+								continue;
+							}
+
+							if (!wasFirst)
+								outStr.Append(", ");
+
+							EncodeName(key);
+						    outStr.Append(" = ");
+						    EncodeObject(value);
+
+							valueIdx = mNextValues[valueIdx];
+							keyIdx = mNextKeys[keyIdx];
+
+							wasFirst = false;
+						}
+
+						outStr.Append('}');
+						return;
+					}
+
+					bool wasFirst = true;
+					outStr.Append("[");
+					int valueIdx = values.mValueIdx;
+					while (valueIdx != -1)
+					{
+					    Object value = mValues[valueIdx];
+						if (value == sEmptySentinel)
+						{
+							valueIdx = mNextValues[valueIdx];
+							continue;
+						}
+
+						if (!wasFirst)
+							outStr.Append(", ");
+
+						EncodeObject(value);
+						
+						valueIdx = mNextValues[valueIdx];
+
+					    wasFirst = false;
+					}
+					outStr.Append("]");
+					return;
+				}
+
+				ObjectToString(outStr, obj);
+			}
+
+			void EncodeHeader()
+			{
+				bool isArray = false;
+				if (valueStack.Count > 1)
+					isArray = valueStack[valueStack.Count - 2].IsArray;
+
+				if (!outStr.IsEmpty)
+					outStr.Append("\n");
+				outStr.Append(isArray ? "[[" : "[");
+				for (int nameIdx < nameStack.Count)
+				{
+					if (nameIdx > 0)
+						outStr.Append(".");
+					EncodeName(nameStack[nameIdx]);
+				}
+				outStr.Append(isArray ? "]]\n" : "]\n");
+			}
+
+			void EncodeValues(Values values)
+			{
+				valueStack.Add(values);
+				defer valueStack.PopBack();
+
+				if (let namedValues = values as NamedValues)
+				{
+					bool forceAllInline = namedValues is InlineNamedValues;
+
+					//bool needsHeader = false;
+					if ((!outStr.IsEmpty) || (nameStack.Count > 0))
+					{
+						int valueIdx = namedValues.mValueIdx;
+
+						bool needsHeader = false;
+						while (valueIdx != -1)
+						{
+						    Object value = mValues[valueIdx];
+							if (value == sEmptySentinel)
+							{
+								valueIdx = mNextValues[valueIdx];
+								continue;
+							}
+
+							if ((!value is NamedValues) || (forceAllInline))
+								needsHeader = true;
+
+							valueIdx = mNextValues[valueIdx];
+						}
+
+						if (needsHeader)
+							EncodeHeader();
+					}	
+						
+					for (int pass = 0; pass < 2; pass++)
+					{
+						bool isInlinePass = pass == 0;
+
+						int keyIdx = namedValues.mKeyIdx;
+						int valueIdx = namedValues.mValueIdx;
+
+						while (valueIdx != -1)
+						{
+						    StringView key = mKeys[keyIdx];
+
+						    Object value = mValues[valueIdx];
+							if (value == sEmptySentinel)
+							{
+								valueIdx = mNextValues[valueIdx];
+								keyIdx = mNextKeys[keyIdx];
+								continue;
+							}
+
+							bool doValuesInline = true;
+							if ((!forceAllInline) && (var subValues = value as Values))
+							{
+								if (!subValues.ForceInline)
+								{
+									doValuesInline = false;
+									if (subValues.IsArray)
+									{
+										int subValueIdx = subValues.mValueIdx;
+										while (subValueIdx != -1)
+										{
+											Object subValue = mValues[subValueIdx];
+											if (!subValue is NamedValues)
+											{
+												doValuesInline = true;
+											}
+											subValueIdx = mNextValues[subValueIdx];
+										}
+									}
+								}
+
+								if ((!doValuesInline) && (!isInlinePass))
+								{
+									nameStack.Add(key);
+									EncodeValues(subValues);
+									nameStack.PopBack();
+								}
+							}
+
+							if (doValuesInline && isInlinePass)
+							{
+								EncodeName(key);
+								outStr.Append(" = ");
+								EncodeObject(value);
+								outStr.Append("\n");
+							}
+
+							valueIdx = mNextValues[valueIdx];
+							keyIdx = mNextKeys[keyIdx];
+						}
+					}
+				}
+				else
+				{
+					int valueIdx = values.mValueIdx;
+					while (valueIdx != -1)
+					{
+						bool needsHeader = true;
+
+					    Object value = mValues[valueIdx];
+						if (value == sEmptySentinel)
+						{
+							valueIdx = mNextValues[valueIdx];
+							continue;
+						}
+
+						if (var subValues = value as Values)
+						{
+							EncodeValues(subValues);
+
+							needsHeader = true;
+						}
+						else
+						{
+							ThrowUnimplemented();
+
+							/*if (needsHeader)
+							{
+								outStr.Append("[");
+								for (int nameIdx < nameStack.Count)
+								{
+									if (nameIdx > 0)
+										outStr.Append(".");
+									EncodeName(nameStack[nameIdx]);
+								}
+								outStr.Append("]\n");
+								needsHeader = false;
+							}
+
+							EncodeName(key);
+							outStr.Append(" = ");
+							ObjectToString(outStr, value);
+							outStr.Append("\n");*/
+						}
+
+						valueIdx = mNextValues[valueIdx];
+					}
+				}
+			}
+
+		    //String aStringBuilder = new String();
+			
+		    EncodeValues(mCurrent.mValues);
+		    //return aStringBuilder.ToString();
+		}
+
+        public override void ToString(String str)
+        {
+            ToJSON(str, false);
+        }
+
+        Result<Object, Error> LoadJSONHelper(String string, ref int32 idx, ref int32 lineNum)
+        {
+			CurrentEntry currentEntry = default;
+            Values values = null;
+			NamedValues namedValues = null;
+            Object valueData = null;
+            
+            int32 valueStartIdx = -1;
+            int32 valueEndIdx = -1;
+            
+            int32 keyStartIdx = -1;
+            int32 keyEndIdx = -1;
+
+            bool valueHadWhitespace = false;
+            bool inQuote = false;
+            bool lastWasSlash = false;
+            bool hadSlash = false;
+			bool keyHadSlash = false;
+
+			bool returningValues = false;
+			/*defer
+			{
+				if (!returningValues)
+					delete values;
+			};*/
+
+			int length = string.Length;
+			char8* cPtr = string.Ptr;
+            for (int32 char8Idx = idx; char8Idx < length; char8Idx++)
+            {
+                char8 c = cPtr[char8Idx];
+
+                if (lastWasSlash)
+                {
+                    lastWasSlash = false;
+                    valueEndIdx = char8Idx;
+                    continue;
+                }
+
+                bool doProcess = false;
+                bool atEnd = false;
+
+                if (c == '\n')
+                    lineNum++;
+                else if (c == '\\')
+                {
+                    lastWasSlash = true;
+                    hadSlash = true;
+                }
+                else if (c == '"')
+                {
+                    if (inQuote)
+                    {
+                        valueEndIdx = char8Idx;
+                        inQuote = false;
+                    }
+                    else
+                    {
+                        if ((valueStartIdx != -1) && (valueHadWhitespace))
+							return .Err(.FormatError(lineNum));
+                            //Runtime.FatalError("Preceding comma expected");
+
+                        valueStartIdx = char8Idx;
+                        inQuote = true;
+                    }
+                }
+                else if (inQuote)
+                {
+                    valueEndIdx = char8Idx;
+                }
+                else if (c == '{')
+                {
+                    if (values != null)
+                    {
+						var result = LoadJSONHelper(string, ref char8Idx, ref lineNum);
+						if (result case .Err(var err))
+							return .Err(err);
+                        valueData = result.Get();
+                    }
+                    else
+                    {
+                        values = namedValues = new:mBumpAllocator NamedValues();
+						currentEntry = CurrentEntry(values);
+                    }
+                }
+                else if (c == '}')
+                {
+                    if (namedValues == null)
+                        return .Err(.UnexpectedObjectEnd);
+                    atEnd = true;
+                    if (((valueStartIdx != -1) || (valueData != null)) && (keyStartIdx != -1))
+                        doProcess = true;
+                }
+                else if (c == '[')
+                {
+                    if (values != null)
+                    {
+                        //valueData = ETry!(LoadJSONHelper(theString, ref char8Idx, ref lineNum));
+						var result = LoadJSONHelper(string, ref char8Idx, ref lineNum);
+						if (result case .Err(var err))
+							return .Err(err);
+						valueData = result.Get();
+                    }
+                    else
+                    {
+                        values = new:mBumpAllocator Values();
+						currentEntry = CurrentEntry(values);
+                    }
+                }
+                else if (c == ']')
+                {
+                    if ((values == null) || (namedValues != null))
+						return .Err(Error.UnexpectedObjectEnd);
+                    atEnd = true;
+                    if ((valueStartIdx != -1) || (valueData != null))
+                        doProcess = true;
+                }                
+                else if (c == ',')
+                {
+                    doProcess = true;
+                }
+                else if (c == ':')
+                {
+                    if ((keyStartIdx != -1) || (valueStartIdx == -1))
+						return .Err(Error.ColonNotExpected);
+
+                    keyStartIdx = valueStartIdx;
+                    keyEndIdx = valueEndIdx;
+					keyHadSlash = hadSlash;
+
+                    valueStartIdx = -1;
+                    valueEndIdx = -1;
+                }
+                else if (!c.IsWhiteSpace)
+                {
+                    if (valueStartIdx == -1)
+                    {
+                        valueHadWhitespace = false;
+                        valueStartIdx = char8Idx;
+                    }
+                    else if (valueHadWhitespace)
+						return .Err(Error.PrecedingCommaExpected);
+                    valueEndIdx = char8Idx;
+                }
+                else
+                {
+                    valueHadWhitespace = true;
+                }
+
+                if (doProcess)
+                {                    
+                    Object aValue = null;
+                    if (valueData != null)
+                    {
+                        if (valueStartIdx != -1)
+							return .Err(Error.FormatError(lineNum));
+                        aValue = valueData;
+                        valueData = null;
+
+						//WTF was this?
+                       /*Values values = aValue as StructuredDataa;
+                        if (valueStructuredData != null)
+                            valueStructuredData.mParent = values;*/
+                    }
+                    else
+                    {
+                        if (valueStartIdx == -1)
+							return .Err(Error.ValueExpected);
+
+                        if (cPtr[valueStartIdx] == '"')
+                        {
+							//String str = new:mBumpAllocator String();
+                            //theString.Substring(valueStartIdx + 1, valueEndIdx - valueStartIdx - 1, str);
+
+							if (hadSlash)
+							{
+                                String str = new:mBumpAllocator String(string, valueStartIdx + 1, valueEndIdx - valueStartIdx - 1);
+                                StringDecode(str);
+								aValue = str;
+							}
+							else
+							{
+								aValue = new:mBumpAllocator box StringView(string, valueStartIdx + 1, valueEndIdx - valueStartIdx - 1);
+							}
+                        }
+                        else
+                        {
+                            //String str = scope String(string, valueStartIdx, valueEndIdx - valueStartIdx + 1);
+
+							StringView strView = StringView(string, valueStartIdx, valueEndIdx - valueStartIdx + 1);
+
+                            //string.Substring(valueStartIdx, valueEndIdx - valueStartIdx + 1, str);
+
+							char8 lastC = strView.Ptr[strView.Length - 1];
+							switch (lastC)
+							{
+							case 'l':
+								if (strView.Equals("null"))
+									aValue = null;
+							case 'e':
+								if (strView.Equals("true"))
+								    aValue = new:mBumpAllocator box true;
+								else if (strView.Equals("false"))
+								    aValue = new:mBumpAllocator box false;
+							case 'U':
+								ThrowUnimplemented();
+							case 'L':
+								ThrowUnimplemented();
+							default:
+								if (strView.Contains('.'))
+								{
+								    aValue = new:mBumpAllocator box (float)float.Parse(strView);
+								}
+								else
+								{
+									var parseVal = int32.Parse(strView);
+									if (parseVal case .Ok(var intVal))
+										aValue = new:mBumpAllocator box intVal;
+									else
+									{
+										//TODO: Is temporary
+										/*if (str.StartsWith("Int@"))
+											aValue = new:mBumpAllocator box (int)0;
+										else if (str.StartsWith("Double@"))
+											aValue = new:mBumpAllocator box 0.0f;
+										else*/
+											return .Err(Error.ParseError);
+									}
+								}
+							}
+                        }
+                    }
+
+                    if (namedValues == null)
+                    {
+                        if (keyStartIdx != -1)
+							return .Err(Error.KeyInArray);
+                        //DoAdd(ref currentEntry, aValue);
+
+						int valueIdx = mValues.Count;
+						if (currentEntry.mLastValue != -1)
+							mNextValues[currentEntry.mLastValue] = (int32)valueIdx;
+						else
+							currentEntry.mValues.mValueIdx = (int32)valueIdx;
+
+						currentEntry.mLastValue = (int32)valueIdx;
+						mValues.Add(aValue);
+						mNextValues.Add(-1);
+                    }
+                    else
+                    {
+                        if (keyStartIdx == -1)
+							return .Err(Error.KeyRequiredForVal);
+
+						StringView key;
+						if (keyHadSlash)
+						{
+							String keyStr = new:mBumpAllocator String(string, keyStartIdx + 1, keyEndIdx - keyStartIdx - 1);
+							StringDecode(keyStr);
+							key = StringView(keyStr);
+						}
+						else
+						{
+							key = StringView(string, keyStartIdx + 1, keyEndIdx - keyStartIdx - 1);
+						}
+                        
+                        //DoAdd(ref currentEntry, key, aValue);
+						int valueIdx = mValues.Count;
+						int keyIdx = mKeys.Count;
+
+						if (currentEntry.mLastValue != -1)
+							mNextValues[currentEntry.mLastValue] = (int32)valueIdx;
+						else
+							currentEntry.mValues.mValueIdx = (int32)valueIdx;
+
+						if (currentEntry.mLastKey != -1)
+							mNextKeys[currentEntry.mLastKey] = (int32)keyIdx;
+						else
+							namedValues.mKeyIdx = (int32)keyIdx;
+
+						currentEntry.mLastValue = (int32)valueIdx;
+						currentEntry.mLastKey = (int32)keyIdx;
+						mKeys.Add(key);
+						mNextKeys.Add(-1);
+						mValues.Add(aValue);
+						mNextValues.Add(-1);
+                        
+                        keyStartIdx = -1;
+                        keyEndIdx = -1;
+                    }
+                    
+                    valueStartIdx = -1;
+                    valueEndIdx = -1;
+                    valueHadWhitespace = false;
+                    hadSlash = false;
+					keyHadSlash = false;
+                }
+
+                if (atEnd)
+                {
+                    idx = char8Idx;
+                    //values.mCanWrite = false;
+					returningValues = true;
+                    return values;
+                }
+            }
+
+			//if (values != null)
+            	//values.mCanWrite = false;
+			returningValues = true;
+            return values;
+        }
+
+		class LoadSection
+		{
+			public Dictionary<String, LoadSection> mSectionDict ~
+				{
+					if (_ != null)
+					{
+						for (var val in _.Values)
+							delete val;
+						delete _;
+					}
+				};
+			public List<LoadSection> mSectionList ~
+				{
+					if (_ != null)
+					{
+						for (var val in _)
+							delete val;
+						delete _;
+					}
+				};
+			public CurrentEntry mCurrentEntry;
+
+			public bool IsArray
+			{
+				get
+				{
+					return mSectionList != null;
+				}
+			}
+		}
+
+		Result<void, Error> LoadTOMLHelper(String contentStr, NamedValues values, ref int32 idx, ref int32 lineNum)
+		{
+			LoadSection loadRoot = scope LoadSection();
+			loadRoot.mSectionDict = new Dictionary<String, LoadSection>();
+			loadRoot.mCurrentEntry = CurrentEntry(values);
+			LoadSection loadSection = loadRoot;
+			//CurrentEntry currentEntry = default;
+
+			char8* cPtr = contentStr.CStr();
+
+			char8 NextChar()
+			{
+				char8 c = cPtr[idx];
+				if (c != 0)
+					idx++;
+				return c;
+			}
+
+			char8 PeekNextChar()
+			{
+				return cPtr[idx];
+			}
+
+			void EatWhiteSpace()
+			{
+				while (true)
+				{
+					char8 nextC = PeekNextChar();
+					if ((nextC != ' ') && (nextC != '\t'))
+						return;
+					idx++;
+				}
+			}
+
+			void EatInlineWhiteSpace()
+			{
+				while (true)
+				{
+					char8 nextC = PeekNextChar();
+					switch (nextC)
+					{
+					case ' ': break;
+					case '\t': break;
+					case '\n': break;
+					case '\r': break;
+					default: return;
+					}
+					idx++;
+				}
+			}
+
+			bool ReadToLineEnd()
+			{
+				bool foundContentChars = false;
+				bool inComment = false;
+
+				while (true)
+				{
+					char8 c = NextChar();
+					if (c == 0)
+						return !foundContentChars;
+					if (c == '\r')
+						continue;
+					if (c == '\n')
+					{
+						lineNum++;
+                        return !foundContentChars;
+					}
+					if (inComment)
+						continue;
+					if (c == '#')
+					{
+						if (!foundContentChars)
+							inComment = true;
+						continue;
+					}
+					if ((c != '\t') && (c != ' '))
+						foundContentChars = true;
+				}
+			}
+
+			bool ReadName(String outName)
+			{
+				int startIdx = idx;
+				char8 c = NextChar();
+				if (c == '"')
+				{
+					bool hadSlash = false;
+					bool inSlash = false;
+					while (true)
+					{
+						char8 nextC = NextChar();
+						if (nextC == 0)
+							return false;
+						if (inSlash)
+						{
+							inSlash = false;
+							continue;
+						}
+						if (nextC == '\\')
+						{
+							hadSlash = true;
+                            inSlash = true;
+							continue;
+						}
+						if (nextC == '"')
+							break;
+					}
+					outName.Append(contentStr, startIdx + 1, idx - startIdx - 2);
+					if (hadSlash)
+						StringDecode(outName);
+					return true;
+				}
+				else
+				{
+					while (true)
+					{
+						char8 nextC = NextChar();
+						if ((nextC.IsLetterOrDigit) || (nextC == '_') || (nextC == '-'))
+						{
+							// Valid part of name, keep going
+							continue;
+						}
+						idx--;
+						outName.Append(contentStr, startIdx, idx - startIdx);
+						return true;
+					}
+					
+				}
+			}
+
+			Object ReadObject()
+			{
+				EatWhiteSpace();
+				char8 c = NextChar();
+				if ((c == '"') || (c == '\''))
+				{
+					String valueStr = scope String(256);
+					idx--;
+					if (!ReadName(valueStr))
+						return null;
+
+					return new:mBumpAllocator String(valueStr);
+				}
+
+				if (c == '[')
+				{
+					// Array
+
+					Values values = new:mBumpAllocator Values();
+					CurrentEntry arrayEntry = CurrentEntry(values);
+
+					while (true)
+					{
+						EatInlineWhiteSpace();
+						char8 nextC = NextChar();
+						if (nextC == ']')
+							break;
+
+						if (!arrayEntry.HasValues)
+							idx--;
+						else if (nextC != ',')
+							return null;
+
+						EatInlineWhiteSpace();
+						Object obj = ReadObject();
+						if (obj == null)
+							return null;
+						DoAdd(ref arrayEntry, obj);
+					}
+
+					return values;
+				}
+
+				if (c == '{')
+				{
+					// Inline table
+
+					NamedValues namedValues = new:mBumpAllocator NamedValues();
+					CurrentEntry tableEntry = CurrentEntry(namedValues);
+
+					while (true)
+					{
+						EatInlineWhiteSpace();
+
+						char8 nextC = NextChar();
+						if (nextC == '}')
+							break;
+
+						if (!tableEntry.HasValues)
+							idx--;
+						else if (nextC != ',')
+							return null;
+
+						EatInlineWhiteSpace();
+						String key = scope String(256);
+						if (!ReadName(key))
+							return null;
+
+						EatInlineWhiteSpace();
+						char8 eqChar = NextChar();
+						if (eqChar != '=')
+							return null;
+
+						EatInlineWhiteSpace();
+						Object obj = ReadObject();
+						if (obj == null)
+							return null;
+
+						DoAdd(ref tableEntry, new:mBumpAllocator String(key), obj);
+					}
+
+					return namedValues;
+				}
+
+				bool isNumber = false;
+				if ((c == '-') || (c.IsNumber))
+					isNumber = true;
+
+				bool hasDate = false;
+				bool hasTime = false;
+				bool isFloat = false;
+				bool isCompoundDate = false;
+
+				int startIdx = idx - 1;
+				ValueStrLoop: while (true)
+				{
+					char8 nextC = NextChar();
+					if ((nextC == ' ') && (hasDate))
+					{
+						if (!isCompoundDate)
+						{
+							if (PeekNextChar().IsNumber)
+							{
+								isCompoundDate = true;
+								continue;
+							}
+						}
+					}
+
+					switch (nextC)
+					{
+					case 0: fallthrough;
+					case ' ': fallthrough;
+					case '\t': fallthrough;
+					case '\r': fallthrough;
+					case '\n': fallthrough;
+					case ',': fallthrough;
+					case ']': fallthrough;
+					case '}':
+						idx--;
+						break ValueStrLoop;
+					}
+
+					if (nextC == '-')
+						hasDate = true;
+					else if (nextC == ':')
+						hasTime = true;
+					else if (nextC == 'T')
+						isCompoundDate = true;
+					else if (nextC == '.')
+						isFloat = true;
+				}
+
+				String value = scope String(256);
+				value.Append(contentStr, startIdx, idx - startIdx);
+
+				if ((hasDate) || (hasTime))
+				{
+					return new:mBumpAllocator String(value);
+				}
+
+				if (isNumber)
+				{
+					for (int i < value.Length)
+						if (value[i] == '_')
+							value.Remove(i--);
+
+					if (isFloat)
+					{
+						switch (Float.Parse(value))
+						{
+						case .Err: return null;
+						case .Ok(let num): return new:mBumpAllocator box num;
+						}
+					}
+
+					switch (Int32.Parse(value))
+					{
+					case .Err: return null;
+					case .Ok(let num): return new:mBumpAllocator box num;
+					}
+				}
+
+				if (value == "true")
+					return new:mBumpAllocator box true;
+
+				if (value == "false")
+					return new:mBumpAllocator box false;
+
+				return null;
+			}
+
+			while (true)
+			{
+			    char8 c = NextChar();
+				if (c == 0)
+				{
+					return .Ok;
+				}	
+
+				if (c == '#')
+				{
+					ReadToLineEnd();
+					continue;
+				}
+
+			    if (c == '\n')
+			    {
+                    lineNum++;
+					continue;
+				}
+			    if ((c == ' ') || (c == '\t') || (c == '\r'))
+					continue;
+
+				if (c == '[')
+				{
+					bool outerIsArray = PeekNextChar() == '[';
+					if (outerIsArray)
+						NextChar();
+
+					loadSection = loadRoot;
+
+					// TODO: Allow quoted names
+					while (true)
+					{
+						String entryName = scope String(256);
+						EatWhiteSpace();
+						if (!ReadName(entryName))
+							return .Err(.FormatError(lineNum));
+
+						bool hasMoreParts = false;
+						EatWhiteSpace();
+						char8 nextC = NextChar();
+						hasMoreParts = nextC == '.';
+
+						bool isArray = outerIsArray && !hasMoreParts;
+
+						/*if (loadSection.IsArray)
+						{
+							if (hasMoreParts)
+							{
+								if (loadSection.mSectionList.Count == 0)
+									return .Err(.FormatError);
+								loadSection = loadSection.mSectionList.Back;
+							}
+						}*/
+
+						String* keyPtr;
+						LoadSection* valuePtr;
+						if (loadSection.mSectionDict.TryAdd(entryName, out keyPtr, out valuePtr))
+						{
+							var key = new:mBumpAllocator String(entryName);
+
+							*keyPtr = key;
+							let newSection = new LoadSection();
+							if (isArray)
+							{
+								newSection.mSectionList = new List<LoadSection>();
+                                newSection.mCurrentEntry = CurrentEntry(new:mBumpAllocator Values());
+							}
+							else
+							{
+								newSection.mSectionDict = new Dictionary<String, LoadSection>();
+                                newSection.mCurrentEntry = CurrentEntry(new:mBumpAllocator NamedValues());
+							}
+
+							DoAdd(ref loadSection.mCurrentEntry, key, newSection.mCurrentEntry.mValues);
+
+							loadSection = newSection;
+							*valuePtr = loadSection;
+						}
+						else
+						{
+                            loadSection = *valuePtr;
+
+							if (hasMoreParts)
+							{
+								if (loadSection.mCurrentEntry.mValues.IsArray)
+								{
+									// Use most recent entry
+									if (loadSection.mSectionList.Count == 0)
+										return .Err(.FormatError(lineNum));
+									loadSection = loadSection.mSectionList.Back;
+								}
+							}
+							else
+							{
+								if (loadSection.mCurrentEntry.mValues.IsArray != isArray)
+									return .Err(.FormatError(lineNum));
+							}
+						}
+
+						if (isArray)
+						{
+							var newSection = new LoadSection();
+							newSection.mSectionDict = new Dictionary<String, LoadSection>();
+							newSection.mCurrentEntry = CurrentEntry(new:mBumpAllocator NamedValues());
+							loadSection.mSectionList.Add(newSection);
+							DoAdd(ref loadSection.mCurrentEntry, newSection.mCurrentEntry.mValues);
+							loadSection = newSection;
+							//currentEntry = loadSection.mCurrentEntry;
+						}
+						else
+						{
+							//currentEntry = loadSection.mCurrentEntry;
+						}
+
+						if (hasMoreParts)
+						{
+							// Go deeper!
+							continue;
+						}
+
+						if (nextC == ']')
+						{
+							if ((outerIsArray) && (NextChar() != ']'))
+								return .Err(.ExpectedArrayNameEnd);
+							break;
+						}
+
+						return .Err(.FormatError(lineNum));
+					}
+					continue;
+				}
+
+				// It's a value
+				String key = scope String(256);
+				idx--;
+				if (!ReadName(key))
+					return .Err(.FormatError(lineNum));
+
+				EatWhiteSpace();
+				char8 eqChar = NextChar();
+				if (eqChar != '=')
+					return .Err(.FormatError(lineNum));
+
+				Object obj = ReadObject();
+				if (obj == null)
+					return .Err(.FormatError(lineNum));
+
+				DoAdd(ref loadSection.mCurrentEntry, new:mBumpAllocator String(key), obj);
+
+				if (!ReadToLineEnd())
+					return .Err(.FormatError(lineNum));
+			}
+		}
+
+		protected Result<void, Error> Load()
+		{
+			EnsureHasData();
+			int guessItems = mSource.Length / 32;
+			mValues.Reserve(guessItems);
+			mKeys.Reserve(guessItems);
+			mNextValues.Reserve(guessItems);
+			mNextKeys.Reserve(guessItems);
+
+			bool isJson = false;
+			for (char8 c in mSource.RawChars)
+			{
+				if (c.IsWhiteSpace)
+					continue;
+				if (c == '{')
+					isJson = true;
+				break;
+			}
+
+			int32 aLineNum = 1;
+			int32 anIdx = 0;
+			Object objResult;
+			if (isJson)
+			{
+				let result = LoadJSONHelper(mSource, ref anIdx, ref aLineNum);
+				if (result case .Err(var err))
+					return .Err(err);
+				objResult = result.Get();
+			}
+			else
+			{
+				var values = new:mBumpAllocator NamedValues();
+				let result = LoadTOMLHelper(mSource, values, ref anIdx, ref aLineNum);
+				if (result case .Err(var err))
+					return .Err(err);
+				objResult = values;
+			}
+
+			if (var values = objResult as Values)
+			{
+				//mCurrent = CurrentEntry(values);
+				int valIdx = mValues.Count;
+
+				mHeadValues = new:mBumpAllocator Values();
+				mHeadValues.mValueIdx = (int32)valIdx;
+				mCurrent.mValues = mHeadValues;
+				mCurrent.mLastValue = (int32)valIdx;
+				mValues.Add(values);
+				mNextValues.Add(-1);
+
+				return .Ok;
+			}	
+			else
+			{
+				//delete obj;
+				return .Err(Error.FormatError(aLineNum));
+			}
+		}
+
+		public Result<void, Error> Load(StringView fileName)
+		{
+			mSource = new:mBumpAllocator StringWithAlloc(mBumpAllocator);
+			if (File.ReadAllText(scope String(fileName), mSource, true) case .Err)
+			{
+				return .Err(Error.FileError);
+			}
+
+			return Load();
+		}
+
+		public Result<void, Error> LoadFromString(StringView data)
+		{
+			mSource = new:mBumpAllocator StringWithAlloc(mBumpAllocator);
+			mSource.Reserve(data.Length + 1);
+			mSource.Append(data);
+			return Load();
+		}
+
+    }
+}
+

+ 161 - 0
BeefLibs/Beefy2D/src/utils/TextSearcher.bf

@@ -0,0 +1,161 @@
+using System;
+using System.Collections.Generic;
+
+namespace Beefy.utils
+{
+	class TextSearcher
+	{
+		enum CmdKind
+		{
+			Contains,
+			NotContains,
+			Equals,
+		}
+
+		struct CmdElement
+		{
+			public CmdKind mKind;
+			public String mString;
+		}
+
+		List<CmdElement> mCmds = new List<CmdElement>() ~
+		{
+			for (var cmdElement in mCmds)
+			{
+				delete cmdElement.mString;
+			}
+			delete _;
+		};
+
+		Result<void> EndCmd(String findStr, ref char8 startChar, String outError, OperatorDelegate operatorHandler)
+		{
+			if (findStr.IsEmpty)
+				return .Ok;
+			
+			CmdElement cmdElement;
+
+			if ((findStr.StartsWith(":")) && (operatorHandler != null))
+			{
+				int opIdx = -1;
+				for (int i < findStr.Length)
+				{
+					char8 c = findStr[i];
+					if ((c == '=') || (c == '>') || (c == '<'))
+					{
+						if (opIdx != -1)
+						{
+							outError.Append("Multiple operators cannot be defined in the same text segment");
+							return .Err;
+						}
+						opIdx = i;
+					}
+				}
+
+				if (opIdx != -1)
+				{
+					if (startChar != 0)
+					{
+						outError.Append("Multiple operators cannot be defined in the same text segment");
+						return .Err;
+					}
+
+					if ((operatorHandler == null) || (!operatorHandler(scope String(findStr, 0, opIdx), findStr[opIdx], scope String(findStr, opIdx + 1))))
+					{
+						outError.AppendF("Invalid expression: {0}", findStr);
+						return .Err;
+					}
+					return .Ok;
+				}
+			}
+
+			if (startChar == '=')
+				cmdElement.mKind = .Equals;
+			else if (startChar == '-')
+				cmdElement.mKind = .NotContains;
+			else
+				cmdElement.mKind = .Contains;
+			cmdElement.mString = new String(findStr);
+			mCmds.Add(cmdElement);
+
+			findStr.Clear();
+			startChar = 0;
+
+			return .Ok;
+		}
+
+		delegate bool OperatorDelegate(String lhs, char8 op, String rhs);
+		public Result<void> Init(String searchStr, String outError, OperatorDelegate operatorHandler = null)
+		{
+			bool inQuote = false;
+
+			char8 startChar = 0;
+
+			String findStr = scope String();
+
+			for (int i < searchStr.Length)
+			{
+				var c = searchStr[i];
+				if (c == '\"')
+				{
+					inQuote = !inQuote;
+					continue;
+				}
+				if (c == '\"')
+				{
+					c = searchStr[++i];
+					findStr.Append(c);
+					continue;
+				}
+
+				if ((c == ' ') && (!inQuote))
+				{
+					Try!(EndCmd(findStr, ref startChar, outError, operatorHandler));
+					continue;
+				}
+
+				if ((startChar == 0) && (findStr.IsEmpty) && (!inQuote))
+				{
+					if ((c == '=') || (c == '-'))
+					{
+						startChar = c;
+						continue;
+					}
+				}
+				findStr.Append(c);
+			}
+			Try!(EndCmd(findStr, ref startChar, outError, operatorHandler));
+
+			return .Ok;
+		}
+
+		public bool Matches(String checkStr)
+		{
+			for (var cmd in ref mCmds)
+			{
+				switch (cmd.mKind)
+				{
+				case .Contains:
+					if (checkStr.IndexOf(cmd.mString, true) == -1)
+						return false;
+				case .NotContains:
+					if (checkStr.IndexOf(cmd.mString, true) != -1)
+						return false;
+				case .Equals:
+					if (!String.Equals(checkStr, cmd.mString, .OrdinalIgnoreCase))
+						return false;
+				}
+			}
+
+			return true;
+		}
+		
+		public bool IsEmpty
+		{
+			get
+			{
+				return mCmds.Count == 0;
+			}
+		}
+	}
+}
+

+ 311 - 0
BeefLibs/Beefy2D/src/utils/UndoManager.bf

@@ -0,0 +1,311 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Diagnostics;
+
+namespace Beefy.utils
+{
+    public class UndoAction
+    {        
+        public virtual bool Merge(UndoAction nextAction)
+        {
+            return false;
+        }
+
+        public virtual bool Undo()
+        {
+            return true;
+        }
+
+        public virtual bool Redo()
+        {
+            return true;
+        }
+
+		public virtual int32 GetCost()
+		{
+			return 1;
+		}
+    }
+
+    public interface IUndoBatchStart
+    {
+        IUndoBatchEnd BatchEnd { get; }
+        String Name { get; }
+    }
+
+    public interface IUndoBatchEnd
+    {
+        IUndoBatchStart BatchStart { get; }
+        String Name { get; }
+    }
+
+    public class UndoBatchStart : UndoAction, IUndoBatchStart
+    {
+        public String mName;
+        public UndoBatchEnd mBatchEnd;
+
+		public int32 mBatchInsertIdx;
+		public int32 mBatchSize = 0; // Don't close out until we add the end
+
+        public String Name
+        {
+            get
+            {
+                return mName;
+            }
+        }
+
+        public this(String name)
+        {
+            mName = name;
+            mBatchEnd = new UndoBatchEnd();
+            mBatchEnd.mBatchStart = this;
+        }
+
+        public IUndoBatchEnd BatchEnd 
+        {
+            get
+            {
+                return mBatchEnd;
+            }
+        }
+
+		public override int32 GetCost()
+		{
+			return Math.Min(mBatchSize, 256); // Don't allow a large batch (ie: rename) to cause us to pull too much out of the undo buffer
+		}
+    }
+
+    public class UndoBatchEnd : UndoAction, IUndoBatchEnd
+    {
+        public IUndoBatchStart mBatchStart;
+        public String Name
+        {
+            get
+            {
+                return mBatchStart.Name;
+            }
+        }
+
+        public IUndoBatchStart BatchStart
+        {
+            get
+            {
+                return mBatchStart;
+            }
+        }
+    }
+
+    public class UndoManager
+    {
+        List<UndoAction> mUndoList = new List<UndoAction>() ~ DeleteContainerAndItems!(_);
+        int32 mUndoIdx = 0;
+        int32 mMaxCost = 8192;
+		int32 mCurCost = 0;
+        bool mSkipNextMerge; // Don't merge after we do an undo or redo step
+		int32 mFreezeDeletes;
+
+        public ~this()
+        {
+            Clear();
+        }
+
+        public void WithActions(Action<UndoAction> func)
+        {
+            for (var action in mUndoList)
+                func(action);
+        }
+
+        public void Clear()
+        {
+            while (mUndoList.Count > 0)
+            {
+				var undoAction = mUndoList.PopBack();
+				delete undoAction;
+            }
+            mUndoIdx = 0;
+			mCurCost = 0;
+        }
+
+        public void Add(UndoAction action, bool allowMerge = true)
+        {
+			if (mFreezeDeletes == 0)
+				mCurCost += action.GetCost();
+			if (action is IUndoBatchStart)
+			{
+				mFreezeDeletes++;
+				if (var undoBatchStart = action as UndoBatchStart)
+				{
+					undoBatchStart.mBatchInsertIdx = GetActionCount();
+				}
+			}
+			else if (action is IUndoBatchEnd)
+			{
+				mFreezeDeletes--;
+				if (var undoBatchEnd = action as UndoBatchEnd)
+				{
+					if (var undoBatchStart = undoBatchEnd.mBatchStart as UndoBatchStart)
+					{
+						undoBatchStart.mBatchSize = GetActionCount() - undoBatchStart.mBatchInsertIdx;
+						int32 cost = undoBatchStart.GetCost();
+						Debug.Assert(cost >= 0);
+						mCurCost += cost;
+					}
+				}
+			}
+
+            if (mUndoIdx < mUndoList.Count)
+            {
+				int32 batchDepth = 0;
+				for (int checkIdx = mUndoIdx; checkIdx < mUndoList.Count; checkIdx++)
+				{
+					var checkAction = mUndoList[checkIdx];
+					if (batchDepth == 0)
+						mCurCost -= checkAction.GetCost();
+
+					if (checkAction is IUndoBatchStart)
+						batchDepth++;
+					else if (checkAction is IUndoBatchEnd)
+						batchDepth--;
+
+					delete checkAction;
+				}
+				Debug.Assert(batchDepth == 0);
+
+				mUndoList.RemoveRange(mUndoIdx, mUndoList.Count - mUndoIdx);
+            }
+
+            if ((allowMerge) && (!mSkipNextMerge))
+            {
+                UndoAction prevAction = GetLastUndoAction();
+				if (prevAction is UndoBatchStart)
+				{
+					var undoBatchStart = (UndoBatchStart)prevAction;
+					if (undoBatchStart.mBatchEnd == action)
+					{
+						// It's an empty batch!
+						mUndoList.PopBack();
+						delete prevAction;
+						delete action;
+						mUndoIdx--;
+						return;
+					}
+				}
+
+                if ((prevAction != null) && (prevAction.Merge(action)))
+				{
+					delete action;
+                    return;
+				}
+            }
+
+            mSkipNextMerge = false;
+            mUndoList.Add(action);
+            mUndoIdx++;
+
+			if ((mCurCost > mMaxCost) && (mFreezeDeletes == 0))
+			{
+				int32 wantCost = (int32)(mMaxCost * 0.8f); // Don't just remove one item at a time
+
+				int32 checkIdx = 0;
+				int32 batchDepth = 0;
+				while (((mCurCost > wantCost) || (batchDepth > 0)) &&
+					(checkIdx < mUndoList.Count))
+				{
+					var checkAction = mUndoList[checkIdx];
+					if (batchDepth == 0)
+						mCurCost -= checkAction.GetCost();
+
+					if (checkAction is IUndoBatchStart)
+						batchDepth++;
+					else if (checkAction is IUndoBatchEnd)
+						batchDepth--;
+
+					delete checkAction;
+					checkIdx++;
+				}
+
+				mUndoList.RemoveRange(0, checkIdx);
+				mUndoIdx -= checkIdx;
+			}
+        }
+
+        public UndoAction GetLastUndoAction()
+        {
+            if (mUndoIdx == 0)
+                return null;
+            return mUndoList[mUndoIdx - 1];
+        }
+
+        public bool Undo()
+        {
+            mSkipNextMerge = true;
+
+            if (mUndoIdx == 0)
+                return false;
+
+            var undoAction = mUndoList[mUndoIdx - 1];
+            if (IUndoBatchEnd undoBatchEnd = undoAction as IUndoBatchEnd)
+            {
+                while (true)
+                {
+                    if (!undoAction.Undo())
+                        return false;
+                    if (mUndoIdx == 0)
+                        return false;
+                    mUndoIdx--;
+                    if ((undoAction == undoBatchEnd.BatchStart) || (mUndoIdx == 0))
+                        return true;
+                    undoAction = mUndoList[mUndoIdx - 1];
+                }
+            }
+
+            if (!undoAction.Undo())
+                return false;
+
+            mUndoIdx--;
+            return true;
+        }
+
+        public bool Redo()
+        {
+            mSkipNextMerge = true;
+            
+            if (mUndoIdx >= mUndoList.Count)
+                return false;
+
+            int32 startUndoIdx = mUndoIdx;
+            var undoAction = mUndoList[mUndoIdx];
+            if (undoAction is UndoBatchStart)
+            {
+                UndoBatchStart undoBatchStart = (UndoBatchStart)undoAction;
+                while (true)
+                {
+                    if (!undoAction.Redo())
+                    {
+                        mUndoIdx = startUndoIdx;
+                        return false;
+                    }
+                    if (mUndoIdx >= mUndoList.Count)
+                        return false;
+                    mUndoIdx++;
+                    if (undoAction == undoBatchStart.mBatchEnd)
+                        return true;
+                    undoAction = mUndoList[mUndoIdx];
+                }
+            }
+
+            if (!undoAction.Redo())
+                return false;
+
+            mUndoIdx++;
+            return true;
+        }
+
+		public int32 GetActionCount()
+		{
+			return mUndoIdx;
+		}
+    }
+}

+ 41 - 0
BeefLibs/Beefy2D/src/widgets/ButtonWidget.bf

@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.gfx;
+using System.Runtime.InteropServices;
+
+namespace Beefy.widgets
+{
+    public class ButtonWidget : Widget
+    {
+        public bool mDisabled;
+
+        public override void Draw(Graphics g)
+        {            
+            
+        }
+
+        public override void KeyDown(KeyCode keyCode, bool isRepeat)
+        {
+            base.KeyDown(keyCode, isRepeat);
+
+            if ((keyCode == KeyCode.Return) || (keyCode == KeyCode.Space))
+                MouseDown(0, 0, 3, 1);
+            else
+                mParent.KeyDown(keyCode, isRepeat);            
+        }
+
+        public override void KeyUp(KeyCode keyCode)
+        {
+            base.KeyUp(keyCode);
+
+            if ((keyCode == KeyCode.Return) || (keyCode == KeyCode.Space))
+            {
+                if (mMouseFlags != 0)
+                    MouseUp(0, 0, 3);
+            }
+            else
+                mParent.KeyUp(keyCode);
+        }        
+    }
+}

+ 49 - 0
BeefLibs/Beefy2D/src/widgets/Checkbox.bf

@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Diagnostics;
+
+namespace Beefy.widgets
+{
+    public abstract class CheckBox : Widget
+    {
+		public enum State
+		{
+			Unchecked,
+			Checked,
+			Indeterminate
+		}
+
+        [DesignEditable]
+        public abstract bool Checked { get; set; }
+		public virtual State State
+			{
+				get
+				{
+					return Checked ? .Checked : .Unchecked;
+				}
+
+				set
+				{
+					Debug.Assert(value != .Indeterminate);
+					Checked = value != .Unchecked;
+				}
+			}
+		public Event<Action> mOnValueChanged ~ _.Dispose();
+
+        public override void MouseDown(float x, float y, int32 btn, int32 btnCount)
+        {
+            base.MouseDown(x, y, btn, btnCount);
+        }
+
+		public override void MouseUp(float x, float y, int32 btn)
+		{
+			if (mMouseOver)
+			{
+				Checked = !Checked;
+				mOnValueChanged();
+			}
+			base.MouseUp(x, y, btn);
+		}
+    }
+}

+ 11 - 0
BeefLibs/Beefy2D/src/widgets/ComboBox.bf

@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Beefy.widgets
+{
+    public class ComboBox : Widget
+    {
+        public bool mDisabled;
+    }
+}

+ 1160 - 0
BeefLibs/Beefy2D/src/widgets/Composition.bf

@@ -0,0 +1,1160 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Reflection;
+using System.Diagnostics;
+using Beefy.utils;
+using Beefy.gfx;
+using Beefy.theme.dark;
+using Beefy;
+
+namespace Beefy.widgets
+{
+    public class TimelineData
+    {
+        /*void SetValueObject(float frame, object value, out object oldValue, out bool isNewKeyframe)
+        {
+        }*/
+    }
+
+    public class TimelineData<T> : TimelineData
+    {
+        public delegate T Interpolator(T from, T to, float pct);
+
+        public struct Entry
+        {
+            public float mFrame;
+            public T mValue;
+
+			public static bool operator==(Entry val1, Entry val2)
+			{
+				return ((val1.mFrame == val2.mFrame) && (val1.mValue == val2.mValue));
+			}
+        }
+
+        public class EntryComparer : IComparer<Entry>
+        {
+            public int Compare(Entry ent1, Entry ent2)
+            {
+                return ent1.mFrame.CompareTo(ent2.mFrame);
+            }
+        }
+
+        public List<Entry> mEntries;
+        public Interpolator mInterpolator;
+        public T mConstantValue;
+        static EntryComparer sEntryComparer;
+
+        public void SetValue(float frame, T value, out T oldValue, out bool isNewKeyframe)
+        {
+            oldValue = mConstantValue;
+
+            if (mEntries == null)
+                mEntries = new List<Entry>();
+
+            if (mEntries.Count == 0)            
+                mConstantValue = value;            
+
+            Entry entry;
+            entry.mFrame = frame;
+            entry.mValue = value;
+
+            int32 index = mEntries.BinarySearch(entry, sEntryComparer);
+            if (index >= 0)
+            {
+                isNewKeyframe = false;
+                oldValue = mEntries[index].mValue;
+                mEntries[index] = entry;
+                return;
+            }
+
+            isNewKeyframe = true;   
+
+            index = ~index;
+            mEntries.Insert(index, entry);
+        }
+
+        public void DeleteKeyframe(float frame)
+        {
+            Entry entry;
+            entry.mFrame = frame;
+            entry.mValue = default(T);
+
+            int32 index = mEntries.BinarySearch(entry, sEntryComparer);
+            mEntries.RemoveAt(index);
+        }
+
+        public void SetValue(float frame, T value)
+        {
+            T oldValue;
+            bool isNewKeyframe;
+            SetValue(frame, value, out oldValue, out isNewKeyframe);
+        }
+
+        public this(Interpolator interpolator = null)
+        {
+            mInterpolator = interpolator;
+        }
+
+		public ~this()
+		{
+			delete mInterpolator;
+		}
+
+        public T GetValue(float frame)
+        {
+            if ((mEntries == null) || (mEntries.Count == 0))
+                return mConstantValue;
+
+            if (sEntryComparer == null)
+                sEntryComparer = new EntryComparer();
+            
+            Entry find;
+            find.mValue = default(T);
+            find.mFrame = frame;
+            int32 index = mEntries.BinarySearch(find, sEntryComparer);
+            if (index >= 0)
+                return mEntries[index].mValue;
+
+            index = ~index - 1;
+            if (index >= mEntries.Count - 1)
+                return mEntries[mEntries.Count - 1].mValue;
+            if (index < 0)
+                return mEntries[0].mValue;
+            Entry entry1 = mEntries[index];
+            Entry entry2 = mEntries[index + 1];
+
+            float aPct = (frame - entry1.mFrame) / (entry2.mFrame - entry1.mFrame);
+            return mInterpolator(entry1.mValue, entry2.mValue, aPct);
+        }
+    }
+
+    public struct CompositionPos : IEquatable<CompositionPos>
+    {
+        public float mX;
+        public float mY;
+
+        public this(float x=0, float y=0)
+        {
+            mX = x;
+            mY = y;
+        }
+
+		public bool Equals(CompositionPos other)
+		{
+			return (mX == other.mX) && (mY == other.mY);
+		}
+
+		public static bool operator==(CompositionPos val1, CompositionPos val2)
+		{
+			return (val1.mX == val2.mX) && (val1.mY == val2.mY);
+		}
+
+        static public CompositionPos Interpolator(CompositionPos from, CompositionPos to, float pct)
+        {
+            CompositionPos aPos;
+            aPos.mX = from.mX + (to.mX - from.mX) * pct;
+            aPos.mY = from.mY + (to.mY - from.mY) * pct;
+            return aPos;
+        }
+
+        static public void WriteX(StructuredData data, CompositionPos pos)
+        {
+            data.Add(pos.mX);
+        }
+
+        static public void WriteY(StructuredData data, CompositionPos pos)
+        {
+            data.Add(pos.mY);
+        }
+
+        static public void ReadX(StructuredData data, int32 idx, ref CompositionPos pos)
+        {
+            pos.mX = data.GetCurFloat(idx);
+        }
+
+        static public void ReadY(StructuredData data, int32 idx, ref CompositionPos pos)
+        {
+            pos.mY = data.GetCurFloat(idx);
+        }
+    }
+
+    public struct CompositionScale : IEquatable<CompositionScale>
+    {
+        public float mScaleX;
+        public float mScaleYAspect; // ScaleY = ScaleX * mScaleYAspect
+
+        public this(float scaleX = 1, float scaleYAspect = 1)
+        {
+            mScaleX = scaleX;
+            mScaleYAspect = scaleYAspect;
+        }
+
+		public static bool operator==(CompositionScale val1, CompositionScale val2)
+		{
+			return (val1.mScaleX == val2.mScaleX) && (val1.mScaleYAspect == val2.mScaleYAspect);
+		}
+
+		public bool Equals(CompositionScale other)
+		{
+			return (mScaleX == other.mScaleX) && (mScaleYAspect == other.mScaleYAspect);
+		}
+
+        static public CompositionScale Interpolator(CompositionScale from, CompositionScale to, float pct)
+        {
+            CompositionScale aScale;
+            aScale.mScaleX = from.mScaleX + (to.mScaleX - from.mScaleX) * pct;
+            aScale.mScaleYAspect = from.mScaleYAspect + (to.mScaleYAspect - from.mScaleYAspect) * pct;
+            return aScale;
+        }
+
+        static public void WriteX(StructuredData data, CompositionScale scale)
+        {
+            data.Add(scale.mScaleX);
+        }
+
+        static public void WriteY(StructuredData data, CompositionScale scale)
+        {
+            data.Add(scale.mScaleYAspect);
+        }
+
+        static public void ReadX(StructuredData data, int32 idx, ref CompositionScale scale)
+        {
+            scale.mScaleX = data.GetCurFloat(idx);
+        }
+
+        static public void ReadY(StructuredData data, int32 idx, ref CompositionScale scale)
+        {
+            scale.mScaleYAspect = data.GetCurFloat(idx);
+        }
+    }
+    
+
+    public class CompositionItemDef
+    {
+        public int32 mIdxInParent;
+        public String mName;
+        public StructuredData mData;
+        public List<CompositionItemDef> mChildItemDefs;    
+        public Type mObjectType;
+        public CompositionItemDef mFollowingItemDef;
+        public Guid mBoundResId;
+
+        public TimelineData<CompositionPos> mTimelineAnchor = new TimelineData<CompositionPos>(new => CompositionPos.Interpolator);
+        public TimelineData<CompositionPos> mTimelinePosition = new TimelineData<CompositionPos>(new => CompositionPos.Interpolator);
+        public TimelineData<CompositionScale> mTimelineScale = new TimelineData<CompositionScale>(new => CompositionScale.Interpolator);
+        public TimelineData<float> mTimelineRot = new TimelineData<float>(new => Utils.Lerp);
+        public TimelineData<Color> mTimelineColor = new TimelineData<Color>(new => Color.Lerp);
+
+        public this()
+        {
+            CompositionScale aScale;
+            aScale.mScaleX = 1;
+            aScale.mScaleYAspect = 1;
+            mTimelineScale.mConstantValue = aScale;
+        }
+
+        protected delegate void ComponentReadAction<T>(StructuredData data, int32 pos, ref T refValue);
+
+        protected bool DeserializeTimeline<T>(StructuredData data, TimelineData<T> timelineData, String name,
+            String[] componentNames, ComponentReadAction<T>[] readActions)
+        {
+            /*using (data.Open(name))
+            {                
+                if (data.Count == 0)
+                    return false;
+
+                if (data.IsObject)
+                {
+                    using (data.Open("Times"))
+                    {
+                        timelineData.mEntries = new List<TimelineData<T>.Entry>(data.Count);
+
+                        for (int32 valueIdx = 0; valueIdx < data.Count; valueIdx++)
+                        {
+                            TimelineData<T>.Entry entry;
+                            entry.mFrame = data.GetInt(valueIdx);
+                            entry.mValue = default(T);
+                            timelineData.mEntries.Add(entry);
+                        }
+                    }
+
+                    for (int32 componentIdx = 0; componentIdx < componentNames.Count; componentIdx++)
+                    {
+                        using (data.Open(componentNames[componentIdx]))
+                        {
+                            if (readActions != null)
+                            {
+                                for (int32 valueIdx = 0; valueIdx < data.Count; valueIdx++)
+                                {
+                                    var entry = timelineData.mEntries[valueIdx];
+                                    readActions[componentIdx](data, valueIdx, ref entry.mValue);
+                                    timelineData.mEntries[valueIdx] = entry;
+                                }
+                            }
+                            else
+                            {
+                                for (int32 valueIdx = 0; valueIdx < data.Count; valueIdx++)
+                                {
+                                    var entry = timelineData.mEntries[valueIdx];
+                                    entry.mValue = (T)data.Get(valueIdx);
+                                    timelineData.mEntries[valueIdx] = entry;
+                                }
+                            }
+                        }
+                    }
+                }
+                else
+                {
+                    Debug.Assert(data.Count == componentNames.Count);
+                    for (int32 componentIdx = 0; componentIdx < componentNames.Count; componentIdx++)
+                    {
+                        if (readActions != null)
+                            readActions[componentIdx](data, componentIdx, ref timelineData.mConstantValue);
+                        else
+                            timelineData.mConstantValue = (T)data.Get(componentIdx);
+                    }
+                }
+            }*/
+
+            return true;
+        }
+
+        public void Deserialize(StructuredData data)
+        {
+            /*mData = data.Current;
+
+            String resId = scope String();
+            mData.GetString(resId, "ResId");
+            if (resId != null)
+                mBoundResId = Guid.Parse(resId);
+			mName = new String();
+            mData.GetString(mName, "Name");
+            using (data.Open("Children"))
+            {
+                if (data.Count > 0)
+                    mChildItemDefs = new List<CompositionItemDef>();
+
+                for (int32 childIdx = 0; childIdx < data.Count; childIdx++)
+                {
+                    CompositionItemDef childItemDef = new CompositionItemDef();
+                    childItemDef.mIdxInParent = childIdx;
+                    mChildItemDefs.Add(childItemDef);
+                }
+
+                for (int32 childIdx = 0; childIdx < data.Count; childIdx++)
+                {
+                    using (data.Open(childIdx))
+                    {
+                        String typeName = scope String();
+                        data.GetString(typeName, "Type");
+
+                        Type aType = null;
+						ThrowUnimplemented();
+                        //aType = Type.GetType(typeName);
+
+                        CompositionItemDef childItemDef = mChildItemDefs[childIdx];                        
+                        childItemDef.mObjectType = aType;
+
+                        int32 followIdx = data.GetInt("Follow", -1);
+                        if (followIdx != -1)
+                            childItemDef.mFollowingItemDef = mChildItemDefs[followIdx];
+
+                        childItemDef.Deserialize(data);
+                    }
+                }
+            }
+
+            DeserializeTimeline(data, mTimelineAnchor, "Anchor",
+                scope String[] { "X", "Y" },
+                scope ComponentReadAction<CompositionPos>[] { scope => CompositionPos.ReadX, scope => CompositionPos.ReadY });
+
+            DeserializeTimeline(data, mTimelinePosition, "Position",
+                scope String[] { "X", "Y" },
+                scope ComponentReadAction<CompositionPos>[] { scope => CompositionPos.ReadX, scope => CompositionPos.ReadY });
+
+            DeserializeTimeline(data, mTimelineScale, "Scale",
+                scope String[] { "X", "YAspect" },
+                scope ComponentReadAction<CompositionScale>[] { scope => CompositionScale.ReadX, scope => CompositionScale.ReadY });
+
+            DeserializeTimeline(data, mTimelineRot, "Rotation",
+                scope String[] { "Angle" },
+                null);*/            
+        }
+
+        protected void SerializeTimeline<T>(StructuredData data, TimelineData<T> timelineData, String name,
+            String[] componentNames, Action<StructuredData, T>[] writeActions, T theDefault) where T : IEquatable<T>
+        {                        
+            /*if ((timelineData.mEntries != null) && (timelineData.mEntries.Count > 0))
+            {
+                using (data.CreateObject(name))
+                {
+                    using (data.CreateArray("Times"))
+                        foreach (var entry in timelineData.mEntries)
+                            data.Add((int32)entry.mFrame);
+
+                    for (int32 componentIdx = 0; componentIdx < componentNames.Count; componentIdx++)
+                    {
+                        using (data.CreateArray(componentNames[componentIdx]))
+                        {
+                            if (writeActions != null)
+                            {
+                                foreach (var entry in timelineData.mEntries)
+                                    writeActions[componentIdx](data, entry.mValue);
+                            }
+                            else
+                            {
+                                foreach (var entry in timelineData.mEntries)
+                                    data.DoAdd(entry.mValue);
+                            }
+                        }
+                    }
+                }
+            }
+            else
+            {
+                if (!timelineData.mConstantValue.Equals(theDefault))
+                {
+                    using (data.CreateArray(name))
+                    {                        
+                        for (int32 componentIdx = 0; componentIdx < componentNames.Count; componentIdx++)
+                        {
+                            if (writeActions != null)
+                                writeActions[componentIdx](data, timelineData.mConstantValue);
+                            else
+                                data.DoAdd(timelineData.mConstantValue);
+                        }                        
+                    }
+                }
+            }*/
+        }
+
+        // Serialization always occurs from a CompositionItemInst.Serialize so there's so this isn't a direct
+        //  parallel to Deserialize -- children aren't iterated through, for example
+        public void PartialSerialize(StructuredData data)
+        {
+            if (mBoundResId != Guid.Empty)
+			{
+				var resIdStr = scope String();
+				mBoundResId.ToString(resIdStr);
+                data.Add("ResId", resIdStr);
+			}
+
+            SerializeTimeline(data, mTimelineAnchor, "Anchor",
+                scope String[] { "X", "Y" },
+                scope Action<StructuredData, CompositionPos>[] { scope => CompositionPos.WriteX, scope => CompositionPos.WriteY },
+                CompositionPos(0, 0));
+
+            SerializeTimeline(data, mTimelinePosition, "Position",
+                scope String[] { "X", "Y" },
+                scope Action<StructuredData, CompositionPos>[] { scope => CompositionPos.WriteX, scope => CompositionPos.WriteY },
+                CompositionPos(0, 0));
+
+            SerializeTimeline(data, mTimelineScale, "Scale",
+                scope String[] { "X", "YAspect" },
+                scope Action<StructuredData, CompositionScale>[] { scope => CompositionScale.WriteX, scope => CompositionScale.WriteY },
+                CompositionScale(1, 1));
+
+            SerializeTimeline(data, mTimelineRot, "Rotation",
+                scope String[] { "Angle" },
+                null,
+                0.0f);
+        }
+        
+        public void RebuildChildIndices()
+        {
+            if (mChildItemDefs != null)
+            {
+                for (int32 childItemDefIdx = 0; childItemDefIdx < mChildItemDefs.Count; childItemDefIdx++)
+                {
+                    var childItemDef = mChildItemDefs[childItemDefIdx];
+                    childItemDef.mIdxInParent = childItemDefIdx;
+                }
+
+                /*foreach (CompositionItemDef childItemDef in mChildItemDefs)
+                {
+                }*/
+            }
+        }
+    }    
+
+    public class CompositionDef
+    {
+        public CompositionItemDef mRootItemDef;        
+
+        public float mFrameRate;
+        public int32 mGridX = 8;
+        public int32 mGridY = 8;
+        public bool mGridEnabled = true;        
+
+        public uint32 mBkgColor;
+        public StructuredData mData;        
+
+        public void Serialize(StructuredData data)
+        {            
+            data.Add("FrameRate", mFrameRate);
+            data.Add("BkgColor", (int32) mBkgColor);
+        }
+
+        public void Deserialize(StructuredData data)
+        {
+            /*mData = data.Current;            
+            mFrameRate = data.GetFloat("FrameRate");
+            mBkgColor = (uint32)data.GetInt("BkgColor");            
+            mRootItemDef = new CompositionItemDef();                
+            mRootItemDef.Deserialize(data);*/            
+        }
+    }
+    
+    public class CompositionItemInst
+    {   
+        public enum HitCode
+        {
+            None,
+            Inside,            
+            CornerTopLeft,
+            CornerTopRight,
+            CornerBotLeft,
+            CornerBotRight            
+        };
+
+        public Composition mComposition;
+        public List<CompositionItemInst> mChildItemInsts; 
+        public CompositionItemDef mItemDef;        
+        public CompositionItemInst mParentItemInst;
+        public Object mRuntimeObject;
+        public CompositionItemInst mFollowItemInst;
+
+        public virtual CompositionItemInst FindItemInstAt(float x, float y, out HitCode hitCode, out float distance)
+        {
+            CompositionItemInst found = null;
+            hitCode = HitCode.Inside;
+            distance = 1000000;
+
+            if (mChildItemInsts != null)
+            {
+                for (CompositionItemInst itemInst in mChildItemInsts)
+                {
+                    HitCode curHitCode;
+                    float curDistance;
+                    CompositionItemInst curFound = itemInst.FindItemInstAt(x, y, out curHitCode, out curDistance);
+                    if (curFound != null)
+                    {
+                        if (curDistance <= distance)
+                        {
+                            distance = curDistance;
+                            hitCode = curHitCode;
+                            found = curFound;
+                        }
+                    }
+                }
+            }
+
+            return found;
+        }
+
+        public CompositionItemInst FindItemInstForRuntimeObject(Object runtimeObject)
+        {
+            if (mRuntimeObject == runtimeObject)
+                return this;
+
+            if (mChildItemInsts != null)
+            {
+                for (CompositionItemInst itemInst in mChildItemInsts)
+                {
+                    CompositionItemInst found = itemInst.FindItemInstForRuntimeObject(runtimeObject);
+                    if (found != null)
+                        return found;
+                }
+            }
+
+            return null;
+        }
+
+        public virtual void Serialize(StructuredData data)
+        {
+            data.Add("Name", mItemDef.mName);
+
+            if (mChildItemInsts != null)
+            {
+                using (data.CreateArray("Children"))
+                {
+                    for (var childInst in mChildItemInsts)
+                    {
+                        using (data.CreateObject())
+                        {
+							var typeName = scope String();
+							childInst.mRuntimeObject.GetType().GetFullName(typeName);
+                            data.Add("Type", typeName);
+
+                            if (childInst.mItemDef.mFollowingItemDef != null)
+                            {
+                                int32 followingIdx = mItemDef.mChildItemDefs.IndexOf(childInst.mItemDef.mFollowingItemDef);
+                                data.Add("Follow", followingIdx);
+                            }
+
+                            childInst.mItemDef.PartialSerialize(data);
+                            childInst.Serialize(data);
+                        }
+                    }
+                }
+            }
+        }
+
+        public virtual void Init(Object boundResource)
+        {            
+            if (mItemDef.mChildItemDefs != null)
+            {
+                mChildItemInsts = new List<CompositionItemInst>(mItemDef.mChildItemDefs.Count);
+
+                for (int32 childDefIdx = 0; childDefIdx < mItemDef.mChildItemDefs.Count; childDefIdx++)
+                {
+                    var childDef = mItemDef.mChildItemDefs[childDefIdx];
+                    Object childObject = Utils.DefaultConstruct(childDef.mObjectType);
+
+                    CompositionItemInst childItemInst = null;
+
+                    Widget childWidget = childObject as Widget;
+                    if (childWidget != null)
+                    {
+                        CompositionWidgetInst selfWidgetInst = (CompositionWidgetInst)this;
+
+                        selfWidgetInst.mWidget.AddWidget(childWidget);
+                        childWidget.mIdStr = mItemDef.mName;
+
+                        CompositionWidgetInst childWidgetInst = new CompositionWidgetInst();
+                        childItemInst = childWidgetInst;
+
+                        childWidgetInst.mWidget = childWidget;
+                        childWidgetInst.mRuntimeObject = childWidget;
+                        childWidgetInst.mComposition = mComposition;
+                    }
+                    else
+                    {
+                        //TODO: Create bone stuff
+                    }
+                    
+                    childItemInst.mItemDef = childDef;
+                    childItemInst.mParentItemInst = this;
+                    mChildItemInsts.Add(childItemInst);
+                }
+
+                for (int32 childDefIdx = 0; childDefIdx < mItemDef.mChildItemDefs.Count; childDefIdx++)                
+                {
+                    var childDef = mItemDef.mChildItemDefs[childDefIdx];
+                    CompositionItemInst childItemInst = mChildItemInsts[childDefIdx];
+
+                    if (childDef.mFollowingItemDef != null)
+                        childItemInst.mFollowItemInst = mChildItemInsts[childDef.mFollowingItemDef.mIdxInParent];
+
+                    Object aBoundResource = null;
+                    if (childDef.mBoundResId != Guid.Empty)
+                        aBoundResource = BFApp.sApp.mResourceManager.GetResourceById(childDef.mBoundResId, true);
+
+                    childItemInst.Init(aBoundResource);
+                }
+            }
+        }
+
+        public virtual void RemoveChild(CompositionItemInst inst)
+        {
+            mItemDef.mChildItemDefs.Remove(inst.mItemDef);            
+
+            mChildItemInsts.Remove(inst);
+            inst.mParentItemInst = null;
+        }
+
+        public virtual void AddChildAtIndex(int idx, CompositionItemInst inst)
+        {
+            if (mItemDef.mChildItemDefs == null)
+                mItemDef.mChildItemDefs = new List<CompositionItemDef>();
+            mItemDef.mChildItemDefs.Insert(idx, inst.mItemDef);
+
+            mChildItemInsts.Insert(idx, inst);
+            inst.mParentItemInst = this;
+        }
+
+        public virtual void InsertChild(CompositionItemInst item, CompositionItemInst insertBefore)
+        {
+            int aPos = (insertBefore != null) ? mChildItemInsts.IndexOf(insertBefore) : mChildItemInsts.Count;
+            AddChildAtIndex(aPos, item);
+        }
+
+        public virtual void AddChild(CompositionItemInst item, CompositionItemInst addAfter = null)
+        {
+            int aPos = (addAfter != null) ? (mChildItemInsts.IndexOf(addAfter) + 1) : mChildItemInsts.Count;
+            AddChildAtIndex(aPos, item);
+        }
+
+        public virtual void DesignDraw(Graphics g)
+        {
+            if (mChildItemInsts != null)
+            {
+                for (var childInst in mChildItemInsts)
+                    childInst.DesignDraw(g);
+            }
+        }
+
+        public virtual void DesignInit(float x, float y)
+        {
+        }
+
+        public virtual void DesignSetCreatePosition(float x, float y, bool isFinal)
+        {
+        }
+
+        public virtual void GetCurTransformComponents(out CompositionPos anchorPos, out CompositionPos position, out CompositionScale scale, out float rotation)
+        {
+            float time = mComposition.mCurFrame;            
+            anchorPos = mItemDef.mTimelineAnchor.GetValue(time);
+            position = mItemDef.mTimelinePosition.GetValue(time);
+            scale = mItemDef.mTimelineScale.GetValue(time);
+            rotation = mItemDef.mTimelineRot.GetValue(time);
+            if (mFollowItemInst == null)
+                return;
+
+            if (mFollowItemInst != null)
+            {                                
+                CompositionPos followAnchorPos;
+                CompositionPos followPos;
+                CompositionScale followScale;                
+                float followRotation;
+                mFollowItemInst.GetCurTransformComponents(out followAnchorPos, out followPos, out followScale, out followRotation);
+
+                CompositionPos prevPos = position;
+
+                prevPos.mX -= followAnchorPos.mX;
+                prevPos.mY -= followAnchorPos.mY;
+                
+                prevPos.mX *= followScale.mScaleX;
+                prevPos.mY *= followScale.mScaleX * followScale.mScaleYAspect;
+
+                position.mX = (prevPos.mX * (float)Math.Cos(followRotation) - prevPos.mY * (float)Math.Sin(followRotation));
+                position.mY = (prevPos.mX * (float)Math.Sin(followRotation) + prevPos.mY * (float)Math.Cos(followRotation));
+                
+                position.mX += followPos.mX;
+                position.mY += followPos.mY;
+                rotation += followRotation;
+
+                scale.mScaleX *= followScale.mScaleX;
+                scale.mScaleYAspect *= followScale.mScaleYAspect;
+            }
+        }
+
+        public virtual void GetCurTransformMatrix(ref Matrix matrix, out bool isSimpleTranslate)
+        {
+            float time = mComposition.mCurFrame;
+            CompositionPos anAnchorPos = mItemDef.mTimelineAnchor.GetValue(time);
+            CompositionPos aPosition = mItemDef.mTimelinePosition.GetValue(time);
+            CompositionScale aScale = mItemDef.mTimelineScale.GetValue(time);
+            float aRotation = mItemDef.mTimelineRot.GetValue(time);
+
+            if (mFollowItemInst != null)
+            {                
+                mFollowItemInst.GetCurTransformMatrix(ref matrix, out isSimpleTranslate);
+                isSimpleTranslate = false;
+
+                /*if ((aRotation == 0) && (aScale.mScaleX == 1.0f) && (aScale.mScaleYAspect == 1.0f))
+                {
+                    //m.tx = aPosition.mX - anAnchorPos.mX;
+                    //m.ty = aPosition.mY - anAnchorPos.mY;
+                    matrix.SetMultiplied(aPosition.mX - anAnchorPos.mX, aPosition.mX - anAnchorPos.mY, matrix);
+                }
+                else*/
+                {
+                    Matrix m = Matrix.IdentityMatrix;
+                    m.Translate(-anAnchorPos.mX, -anAnchorPos.mY);
+                    m.Scale(aScale.mScaleX, aScale.mScaleX * aScale.mScaleYAspect);
+                    m.Rotate(aRotation);
+                    m.Translate(aPosition.mX, aPosition.mY);
+
+                    Matrix mat = m;
+                    mat.Multiply(matrix);
+
+                    matrix.SetMultiplied(m, matrix);
+                }
+            }
+            else
+            {
+                matrix.Identity();
+                if ((aRotation == 0) && (aScale.mScaleX == 1.0f) && (aScale.mScaleYAspect == 1.0f))
+                {
+                    isSimpleTranslate = true;
+                    matrix.tx = aPosition.mX - anAnchorPos.mX;
+                    matrix.ty = aPosition.mY - anAnchorPos.mY;                    
+                }
+                else
+                {
+                    isSimpleTranslate = false;
+                    matrix.Translate(-anAnchorPos.mX, -anAnchorPos.mY);
+                    matrix.Scale(aScale.mScaleX, aScale.mScaleX * aScale.mScaleYAspect);
+                    matrix.Rotate(aRotation);
+                    matrix.Translate(aPosition.mX, aPosition.mY);                    
+                }
+            }
+        }
+
+        public virtual void DesignTimelineEdited()
+        {
+        }        
+    }
+
+    public class CompositionBoneJoint : CompositionItemInst
+    {        
+        public float mX;
+        public float mY;
+
+        public override CompositionItemInst FindItemInstAt(float x, float y, out HitCode hitCode, out float distance)
+        {
+            hitCode = HitCode.Inside;
+            distance = Math.Max(0, Utils.Distance(x - mX, y - mY) - 5);
+            return this;
+        }
+    }
+
+    public class CompositionBoneInst : CompositionItemInst
+    {
+        public CompositionBoneJoint mStartJoint;
+        public CompositionBoneJoint mEndJoint;
+        public float mWidth;
+
+        public static Image sBoneTex;
+
+        CompositionBoneJoint CreateJoint()
+        {
+            CompositionBoneJoint joint = new CompositionBoneJoint();
+            joint.mParentItemInst = this;
+            if (mChildItemInsts == null)
+                mChildItemInsts = new List<CompositionItemInst>();
+            mChildItemInsts.Add(joint);
+            //joint.mComposition = this;
+            
+            //mParentItemInst.mChildItemInsts.Add(joint);
+            return joint;
+        }
+
+        public override void DesignInit(float x, float y)
+        {
+            /*mX = mComposition.mWidth / 2;
+            mY = mComposition.mHeight / 2;
+
+            mAngle = (float)Math.Atan2(y - mY, x - mX);
+            mLength = Utils.Distance(y - mY, x - mX);*/
+
+            /*HitCode aHitCode;
+            float aDistance;
+            mComposition.mRootItemInst.FindItemInstAt(x, y, out aHitCode, out aDistance);*/
+
+            if (mStartJoint == null)
+            {
+                mStartJoint = CreateJoint();
+                mStartJoint.mX = x;
+                mStartJoint.mY = y;
+            }
+            mEndJoint = CreateJoint();
+            mEndJoint.mX = x;
+            mEndJoint.mY = y;
+
+            mWidth = 5;
+        }
+
+        public override void DesignSetCreatePosition(float x, float y, bool isFinal)
+        {
+            mEndJoint.mX = x;
+            mEndJoint.mY = y;
+        }
+
+        public override void DesignDraw(Graphics g)
+        {
+            base.DesignDraw(g);
+
+            float startX = mStartJoint.mX;
+            float startY = mStartJoint.mY;
+
+            float endX = mEndJoint.mX;
+            float endY = mEndJoint.mY;
+            
+            Matrix m = Matrix.IdentityMatrix;
+
+            float angle = (float)Math.Atan2(endY - startY, endX - startX);
+            float length = Utils.Distance(endY - startY, endX - startX);
+
+            m.Rotate(angle - Math.PI_f / 2);
+            m.Translate(startX, startY);
+
+            using (g.PushMatrix(m))
+            {
+                for (int32 row = 0; row < 2; row++)
+                {
+                    for (int32 col = 0; col < 2; col++)
+                    {
+                        float yStart = 0;
+
+                        float thicknessW = 1.0f;
+                        float thicknessH = 4;
+
+                        float aWidth = mWidth;
+                        float aHeight = length;
+
+                        float splitPct = Math.Min(0.25f, 16.0f / length);
+
+                        if (col == 1)
+                        {
+                            thicknessW = -thicknessW;
+                            aWidth = -aWidth;
+                        }
+
+                        if (row == 0)
+                        {
+                            aHeight *= splitPct;
+                        }
+                        else
+                        {
+                            yStart += aHeight;
+                            aHeight = -aHeight * (1.0f - splitPct);
+                            thicknessH = -thicknessH;
+                        }
+
+                        g.PolyStart(sBoneTex, 9);
+                        
+                        g.PolyVertex(0, 0, yStart - thicknessH, 0.125f, 0);
+                        g.PolyVertex(1, -aWidth - thicknessW, yStart + aHeight, 0.125f, 0);
+                        g.PolyVertex(2, 0, yStart + thicknessH, 0.625f, 0);
+
+                        g.PolyVertexCopy(3, 1);
+                        g.PolyVertexCopy(4, 2);
+                        g.PolyVertex(5, -aWidth + thicknessW, yStart + aHeight, 0.625f, 0);
+
+                        g.PolyVertexCopy(6, 4);
+                        g.PolyVertexCopy(7, 5);
+                        g.PolyVertex(8, 0, yStart + aHeight, 0.625f, 0);
+                    }
+                }
+            }
+
+			ThrowUnimplemented();
+            //g.Draw(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.UIBoneJoint), startX - 9, startY - 9);
+            //g.Draw(DarkTheme.sDarkTheme.GetImage(DarkTheme.ImageIdx.UIBoneJoint), endX - 9, endY - 9);
+
+            /*using (g.PushColor(Color.Red))
+                g.FillRect(endX, endY, 2, 2);*/
+        }
+    }
+    
+    public class CompositionWidgetInst : CompositionItemInst
+    {
+        public Widget mWidget;
+
+        public override void Init(Object boundResource)
+        {
+            base.Init(boundResource);
+            mWidget.Deserialize(mItemDef.mData);
+
+            if (mWidget is ImageWidget)
+            {
+                ImageWidget imageWidget = (ImageWidget)mWidget;
+                imageWidget.mImage = (Image)boundResource;
+            }
+        }
+
+        public override void RemoveChild(CompositionItemInst inst)
+        {
+            base.RemoveChild(inst);
+            if (inst is CompositionWidgetInst)
+            {
+                CompositionWidgetInst childWidgetInst = (CompositionWidgetInst)inst;
+                mWidget.RemoveWidget(childWidgetInst.mWidget);
+            }
+        }
+
+        public override void AddChildAtIndex(int idx, CompositionItemInst inst)
+        {
+            if (inst is CompositionWidgetInst)
+            {
+                CompositionWidgetInst childWidgetInst = (CompositionWidgetInst)inst;
+                int32 insertIdx = 0;
+
+                for (int32 checkIdx = 0; checkIdx < idx; checkIdx++)
+                {
+                    if (mChildItemInsts[checkIdx] is CompositionWidgetInst)
+                        insertIdx = checkIdx + 1;
+                }
+
+                mWidget.AddWidgetAtIndex(insertIdx, childWidgetInst.mWidget);
+
+            }
+
+            base.AddChildAtIndex(idx, inst);
+        }
+
+        public override CompositionItemInst FindItemInstAt(float x, float y, out HitCode hitCode, out float distance)
+        {
+            /*float transX;
+            float transY;
+            mWidget.SelfToOtherTranslate(mComposition, 0, 0, out transX, out transY);
+
+            float dX = transX - x;
+            if (dX < 0)
+            {
+                dX = x - (transX + mWidget.mWidth);
+                if (dX < 0)
+                    dX = 0;
+            }
+
+            float dY = transY - y;
+            if (dY < 0)
+            {
+                dY = y - (transY + mWidget.mHeight);
+                if (dX < 0)
+                    dX = 0;
+            }*/
+
+            float transX;
+            float transY;
+            mComposition.SelfToRootTranslate(x, y, out transX, out transY);
+            mWidget.RootToSelfTranslate(transX, transY, out transX, out transY);            
+            
+            float dX = -transX;
+            if (dX < 0)
+            {
+                dX = transX - mWidget.mWidth;
+                if (dX < 0)
+                    dX = 0;
+            }
+
+            float dY = -transY;
+            if (dY < 0)
+            {
+                dY = transY - mWidget.mHeight;
+                if (dY < 0)
+                    dY = 0;
+            }
+            
+            distance = Math.Max(dX, dY);
+            hitCode = HitCode.Inside;
+
+            HitCode childHitCode;
+            float childDistance;
+            CompositionItemInst childInst = base.FindItemInstAt(x, y, out childHitCode, out childDistance);
+            if (childDistance <= distance)
+            {
+                hitCode = childHitCode;
+                distance = childDistance;
+                return childInst;
+            }            
+
+            return this;
+        }
+
+        public override void Serialize(StructuredData data)
+        {
+            mWidget.Serialize(data);
+            base.Serialize(data);
+        }
+        
+        public override void DesignTimelineEdited()
+        {            
+            bool isSimpleTranslate = false;
+            if (mWidget.mTransformData != null)
+            {
+                var m = mWidget.Transform;
+                GetCurTransformMatrix(ref m, out isSimpleTranslate);
+                mWidget.Transform = m;
+            }
+            else
+            {
+                // Check the matrix and see if we need a matrix or not
+                Matrix m = Matrix();
+                GetCurTransformMatrix(ref m, out isSimpleTranslate);
+                if (isSimpleTranslate)
+                {
+                    mWidget.mX = m.tx;
+                    mWidget.mY = m.ty;
+                }
+                else
+                {
+                    mWidget.Transform = m;
+                }
+            }
+            
+        }
+    }
+
+    public class Composition : Widget
+    {
+        public CompositionDef mDef;        
+        public CompositionWidgetInst mRootItemInst;
+        public float mCurFrame;
+
+        [DesignEditable(DisplayType="Color")]
+        public uint32 BkgColor { get { return mDef.mBkgColor; } set { mDef.mBkgColor = value; } }
+
+        public void CreateNew(float width, float height, float frameRate = 60)
+        {
+            mDef = new CompositionDef();
+            mDef.mFrameRate = frameRate;
+
+            CompositionItemDef itemDef = new CompositionItemDef();            
+            mDef.mRootItemDef = itemDef;
+            mDef.mRootItemDef.mChildItemDefs = new List<CompositionItemDef>();
+
+            CompositionWidgetInst itemInst = new CompositionWidgetInst();
+            itemInst.mItemDef = itemDef;
+            itemInst.mWidget = this;
+            itemInst.mRuntimeObject = this;
+            itemInst.mComposition = this;
+            itemInst.mChildItemInsts = new List<CompositionItemInst>();
+            mRootItemInst = itemInst;
+
+            mWidth = width;
+            mHeight = height;
+            
+            //itemInst.Init(false);
+        }
+
+        public void CreateFrom(CompositionDef def)
+        {
+            mDef = def;
+
+            mRootItemInst = new CompositionWidgetInst();
+            mRootItemInst.mItemDef = mDef.mRootItemDef;
+            mRootItemInst.mWidget = this;
+            mRootItemInst.mRuntimeObject = this;
+            mRootItemInst.mComposition = this;
+            mRootItemInst.Init(null);            
+
+            Deserialize(mDef.mData);
+            UpdateAllItemDataPositions(mRootItemInst);
+        }
+
+        public override void Draw(Graphics g)
+        {
+            base.Draw(g);
+
+            if (mDef.mBkgColor != 0)
+            {
+                using (g.PushColor(mDef.mBkgColor))
+                    g.FillRect(0, 0, mWidth, mHeight);
+            }
+        }
+
+        public virtual void DesignDraw(Graphics g)
+        {
+            mRootItemInst.DesignDraw(g);
+        }
+
+        void UpdateAllItemDataPositions(CompositionItemInst compositionItem)
+        {
+            if (compositionItem.mChildItemInsts != null)
+            {
+                for (var childItem in compositionItem.mChildItemInsts)
+                    childItem.DesignTimelineEdited();
+            }
+        }
+
+        public void UpdatePositions()
+        {
+            UpdateAllItemDataPositions(mRootItemInst);
+        }
+    }
+}

+ 15 - 0
BeefLibs/Beefy2D/src/widgets/DesignEditableAttribute.bf

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Beefy.widgets
+{
+    public struct DesignEditableAttribute : Attribute
+    {
+        public String DisplayType { get; set; }
+        public String Group { get; set; }
+        public String DisplayName { get; set; }
+        public String SortName { get; set; }
+        public bool DefaultEditString { get; set; }
+    }
+}

+ 383 - 0
BeefLibs/Beefy2D/src/widgets/Dialog.bf

@@ -0,0 +1,383 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+using Beefy.gfx;
+using Beefy.events;
+
+namespace Beefy.widgets
+{
+    public delegate void DialogEventHandler(DialogEvent theEvent);
+
+	interface IHotKeyHandler
+	{
+		bool Handle(KeyCode keyCode);
+	}
+
+    public abstract class Dialog : Widget
+    {
+        public bool mInPopupWindow;
+        public String mTitle ~ delete _;
+        public String mText ~ delete _;
+        public Image mIcon;
+        public BFWindowBase.Flags mWindowFlags = .ClientSized | .TopMost | .Caption | .Border | .SysMenu | .Modal;
+        public List<ButtonWidget> mButtons = new List<ButtonWidget>() ~ delete _;
+        public List<Widget> mTabWidgets = new List<Widget>() ~ delete _;
+        public Dictionary<ButtonWidget, DialogEventHandler> mHandlers = new Dictionary<ButtonWidget, DialogEventHandler>() ~ delete _;
+        public ButtonWidget mDefaultButton;
+        public ButtonWidget mEscButton;
+        public EditWidget mDialogEditWidget;
+        public bool mClosed;
+        public Event<Action> mOnClosed ~ _.Dispose();
+
+        static String STRING_OK = "Ok";
+        static String STRING_CANCEL = "Cancel";
+        static String STRING_YES = "Yes";
+        static String STRING_NO = "No";
+        static String STRING_CLOSE = "Close";
+
+		public String Title
+		{
+			get
+			{
+				return mTitle;
+			}
+
+			set
+			{
+				String.NewOrSet!(mTitle, value);
+				if (mWidgetWindow != null)
+					mWidgetWindow.SetTitle(mTitle);
+			}
+		}
+
+        public this(String title = null, String text = null, Image icon = null)
+        {
+			if (title != null)
+            	mTitle = new String(title);
+			if (text != null)
+            	mText = new String(text);
+            mIcon = icon;
+        }
+
+		public ~this()
+		{			
+            for (var handler in mHandlers.Values)
+				delete handler;
+		}
+
+        public virtual void Close()
+        {
+            mClosed = true;
+			if (mWidgetWindow == null)
+				return;
+
+            if (mInPopupWindow)
+            {                                
+                BFWindow lastChildWindow = null;
+                if (mWidgetWindow.mParent.mChildWindows != null)
+                {                    
+                    for (var checkWindow in mWidgetWindow.mParent.mChildWindows)
+                        if (checkWindow != mWidgetWindow)
+                            lastChildWindow = checkWindow;
+                    if (lastChildWindow != null)
+                        lastChildWindow.SetForeground();                    
+                }
+                if (lastChildWindow == null)
+                {
+                    if (!mWidgetWindow.mParent.mHasClosed)
+                        mWidgetWindow.mParent.SetForeground();
+                }
+
+				mWidgetWindow.mOnWindowKeyDown.Remove(scope => WindowKeyDown, true);
+                mWidgetWindow.Close(true);
+                
+                mInPopupWindow = false;
+                
+                if (mOnClosed.HasListeners)
+                    mOnClosed();
+            }            
+        }
+
+        public virtual ButtonWidget CreateButton(String label)
+        {
+            return null;
+        }
+
+        public virtual EditWidget CreateEditWidget()
+        {
+            return null;
+        }
+        
+        void HandleMouseClick(MouseEvent theEvent)
+        {   
+            ButtonWidget widget = (ButtonWidget) theEvent.mSender;
+
+            DialogEvent dialogEvent = scope DialogEvent();
+            dialogEvent.mButton = widget;
+            dialogEvent.mCloseDialog = true;
+            dialogEvent.mSender = this;
+
+            if (mHandlers[widget] != null)
+                mHandlers[widget](dialogEvent);
+            
+            if (dialogEvent.mCloseDialog)
+                Close();
+        }
+
+        public virtual ButtonWidget AddButton(String label, DialogEventHandler handler = null)
+        {
+			Debug.AssertNotStack(handler);
+
+            ButtonWidget button = CreateButton(label);
+            mHandlers[button] = handler;
+            button.mOnMouseClick.Add(new => HandleMouseClick);
+            mButtons.Add(button);
+            mTabWidgets.Add(button);
+            AddWidget(button);
+            return button;
+        }
+
+        public virtual EditWidget AddEdit(String text, DialogEventHandler handler = null)
+        {
+			Debug.AssertNotStack(handler);
+
+            var editWidget = CreateEditWidget();            
+            AddWidget(editWidget);
+            if (text != null)
+            {
+                editWidget.SetText(text);
+                editWidget.Content.SelectAll();
+            }
+            mTabWidgets.Add(editWidget);
+			if (mDialogEditWidget == null)
+            	mDialogEditWidget = editWidget;
+            editWidget.mOnSubmit.Add(new => EditSubmitHandler);
+            editWidget.mOnCancel.Add(new => EditCancelHandler);
+            return editWidget;
+        }
+
+		public virtual EditWidget AddEdit(EditWidget editWidget, DialogEventHandler handler = null)
+		{
+			Debug.AssertNotStack(handler);
+			if (editWidget.mParent == null)
+				AddWidget(editWidget);
+		    mTabWidgets.Add(editWidget);
+			if (mDialogEditWidget == null)
+		    	mDialogEditWidget = editWidget;
+		    editWidget.mOnSubmit.Add(new => EditSubmitHandler);
+		    editWidget.mOnCancel.Add(new => EditCancelHandler);
+		    return editWidget;
+		}
+
+		public void AddDialogComponent(Widget widget)
+		{
+			mTabWidgets.Add(widget);
+			AddWidget(widget);
+		}
+
+        protected void EditSubmitHandler(EditEvent theEvent)
+        {
+            Submit();
+        }
+
+        protected void EditCancelHandler(EditEvent theEvent)
+        {
+            if (mEscButton != null)
+                mEscButton.MouseClicked(0, 0, 3);
+        }
+
+        public virtual void AddCloseButton(DialogEventHandler closeHandler, int32 theDefault = -1)
+        {
+			Debug.AssertNotStack(closeHandler);
+
+#unwarn
+            ButtonWidget closeButton = AddButton(STRING_CLOSE, closeHandler);
+        }
+
+        public virtual void AddYesNoButtons(DialogEventHandler yesHandler = null, DialogEventHandler noHandler = null, int32 theDefault = -1, int32 escapeBtn = -1)
+        {
+			Debug.AssertNotStack(yesHandler);
+			Debug.AssertNotStack(noHandler);
+
+            ButtonWidget yesButton = AddButton(STRING_YES, yesHandler);
+            ButtonWidget noButton = AddButton(STRING_NO, noHandler);
+
+            mDefaultButton = (theDefault == 0) ? yesButton : (theDefault == 1) ? noButton : null;
+            mEscButton = (escapeBtn == 0) ? yesButton : (escapeBtn == 1) ? noButton : null;
+        }
+
+        public virtual void AddOkCancelButtons(DialogEventHandler okHandler = null, DialogEventHandler cancelHandler = null, int32 theDefault = -1, int32 escapeBtn = -1)
+        {
+			Debug.AssertNotStack(okHandler);
+			Debug.AssertNotStack(cancelHandler);
+
+            ButtonWidget okButton = AddButton(STRING_OK, okHandler);
+            ButtonWidget cancelButton = AddButton(STRING_CANCEL, cancelHandler);
+
+            mDefaultButton = (theDefault == 0) ? okButton : (theDefault == 1) ? cancelButton : null;
+            mEscButton = (escapeBtn == 0) ? okButton : (escapeBtn == 1) ? cancelButton : null;
+        }
+
+        public virtual void PreShow()
+        {
+        }
+
+        public virtual void CalcSize()
+        {
+        }
+
+        public virtual void ResizeComponents()
+        {
+            
+        }
+
+		public override void Resize(float x, float y, float width, float height)
+		{
+			base.Resize(x, y, width, height);
+			ResizeComponents();
+		}
+
+        public override void AddedToParent()
+        {
+            if (mDialogEditWidget != null)
+                mDialogEditWidget.SetFocus();
+            else if (mDefaultButton != null)
+                mDefaultButton.SetFocus();
+            else
+                SetFocus();
+        }  
+
+        public virtual void PopupWindow(WidgetWindow parentWindow, float offsetX = 0, float offsetY = 0)
+        {
+            if (mClosed)
+                return;
+
+            mInPopupWindow = true;
+
+            CalcSize();
+            ResizeComponents();
+
+			int desktopWidth;
+			int desktopHeight;
+			BFApp.sApp.GetDesktopResolution(out desktopWidth, out desktopHeight);
+
+            //TODO: Clip to desktop width / height
+            mWidth = Math.Min(desktopWidth - 32, mWidth);
+            mHeight = Math.Min(desktopHeight - 32, mHeight);
+
+            float aX = parentWindow.mClientX + (parentWindow.mClientWidth - mWidth) / 2 + offsetX;
+            float aY = parentWindow.mClientY + (parentWindow.mClientHeight - mHeight) / 2 + offsetY;
+
+            for (int32 i = 0; i < parentWindow.mChildWindows.Count; i++)
+            {
+                var otherChild = parentWindow.mChildWindows[i];
+
+                if (otherChild.mWindowFlags.HasFlag(BFWindowBase.Flags.Modal))
+                {
+                    aX = Math.Max(aX, otherChild.mClientX + 16);
+                    aY = Math.Max(aY, otherChild.mClientY + 16);
+                }                
+            }
+
+            WidgetWindow widgetWindow = new WidgetWindow(parentWindow,
+                mTitle ?? "Dialog",
+                (int32)(aX), (int32)(aY),
+                (int32)mWidth, (int32)mHeight,
+                mWindowFlags,
+                this);
+
+			WindowCloseQueryHandler windowCloseHandler = new (evt) =>
+                {
+                    Close();
+                    return false;
+                };
+
+            widgetWindow.mOnWindowCloseQuery.Add(windowCloseHandler);
+            widgetWindow.mOnWindowKeyDown.Add(new => WindowKeyDown);
+        }
+
+        public virtual bool HandleTab(int dir)
+        {
+            return Widget.HandleTab(dir, mTabWidgets);
+        }
+
+		public virtual void Escape()
+		{
+			mEscButton.MouseClicked(0, 0, 3);
+		}
+
+		public virtual void Submit()
+		{
+			if ((mDefaultButton != null) && (!mDefaultButton.mDisabled))
+				mDefaultButton.MouseClicked(0, 0, 3);
+		}
+
+        void WindowKeyDown(KeyDownEvent evt)
+        {
+			if ((evt.mKeyCode != .Alt) && (mWidgetWindow.IsKeyDown(.Alt)) && (!mWidgetWindow.IsKeyDown(.Control)))
+			{
+				if (mChildWidgets != null)
+				{
+					for (var widget in mChildWidgets)
+					{
+						if (let hotKeyHandler = widget as IHotKeyHandler)
+						{
+							if (hotKeyHandler.Handle(evt.mKeyCode))
+							{
+								evt.mHandled = true;
+								return;
+							}
+						}
+					}
+				}
+			}
+
+			if (evt.mKeyCode == .Escape)
+			{
+				if (mWidgetWindow.mFocusWidget != null)
+				{
+					let widgetWindow = mWidgetWindow;
+
+					let focusWidget = widgetWindow.mFocusWidget;
+					widgetWindow.mFocusWidget.KeyDown(evt);
+					if (focusWidget != widgetWindow.mFocusWidget)
+						evt.mHandled = true; // Infers we handled the event
+					if (evt.mHandled)
+						return;
+					evt.mHandled = true;
+				}
+
+	            if (mEscButton != null)
+	            {
+	                if ((mDefaultButton != null) && (mDefaultButton.mMouseDown))
+	                {
+	                    mDefaultButton.mMouseFlags = default;
+	                    mDefaultButton.mMouseDown = false;
+	                }
+	                else
+						Escape();
+	                evt.mHandled = true;
+	            }
+			}
+
+			if ((evt.mKeyCode == .Return) && (mDefaultButton != null))
+			{
+				if ((!(mWidgetWindow.mFocusWidget is EditWidget)) &&
+					(!(mWidgetWindow.mFocusWidget is ButtonWidget)))
+				{
+					Submit();
+				}
+			}
+
+            if (evt.mKeyCode == KeyCode.Tab)
+            {
+                int32 dir = mWidgetWindow.IsKeyDown(KeyCode.Shift) ? -1 : 1;
+                if (HandleTab(dir))
+				{
+					evt.mHandled = true;
+				}
+            }
+        }
+    }
+}

+ 185 - 0
BeefLibs/Beefy2D/src/widgets/DockedWidget.bf

@@ -0,0 +1,185 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.gfx;
+using System.Diagnostics;
+
+namespace Beefy.widgets
+{
+    public interface IDockable
+    {
+        bool CanDock(DockingFrame frame, DockedWidget refWidget, DockingFrame.WidgetAlign align);
+        void Dock(DockingFrame frame, DockedWidget refWidget, DockingFrame.WidgetAlign align);
+        void DrawDockPreview(Graphics g);
+    }
+
+    public interface ICustomDock
+    {
+        void Draw(Graphics g);
+        void Dock(IDockable dockingWidget);
+    }
+
+    public class DockedWidget : Widget, IDockable
+    {
+        public bool mHasFillWidget;
+		public bool mIsFillWidget;
+		public bool mAutoClose = true; // Close when last tab is removed
+        public float mRequestedWidth; // Set when we drag a sizer
+        public float mRequestedHeight; // Set when we drag a sizer
+        public float mSizePriority;
+        public DockingFrame mParentDockingFrame;
+
+        public bool mAllowInterpolate;
+        public float mInterpolatePct = 1.0f;
+        public float mStartRootX;
+        public float mStartRootY;
+        public float mStartWidth;
+        public float mStartHeight;
+
+        public float mEndX;
+        public float mEndY;
+        public float mEndWidth;
+        public float mEndHeight;
+
+        public this()
+        {
+            mClipMouse = true;
+            mClipGfx = true;
+        }
+
+		public virtual DockingFrame GetRootDockingFrame()
+		{
+			var parentFrame = mParentDockingFrame;
+			while (parentFrame != null)
+			{
+				if (parentFrame.mParentDockingFrame == null)
+					break;
+				parentFrame = parentFrame.mParentDockingFrame;
+			}
+			return parentFrame;
+		}
+
+		public virtual void Rehup()
+		{
+			mHasFillWidget = mIsFillWidget;
+		}
+
+        public virtual void SetRequestedSize(float width, float height)
+        {
+            mRequestedWidth = width;
+            mRequestedHeight = height;
+        }
+
+        public virtual ICustomDock GetCustomDock(IDockable dockable, float x, float y)
+        {
+            return null;
+        }
+
+        public bool CanDock(DockingFrame frame, DockedWidget refWidget, DockingFrame.WidgetAlign align)
+        {
+            return true;
+        }
+
+        public virtual void Dock(DockingFrame frame, DockedWidget refWidget, DockingFrame.WidgetAlign align)
+        {
+            if (mParentDockingFrame != null) // TODO: Undock
+            {
+            }
+        }
+
+        public virtual void DrawDockPreview(Graphics g)
+        {
+            using (g.PushColor(0x80FFFFFF))
+                DrawAll(g);
+        }
+
+        public void StartInterpolate(float rootX, float rootY, float width, float height)
+        {
+            mInterpolatePct = 0;
+            mStartRootX = rootX;
+            mStartRootY = rootY;
+            mStartWidth = mWidth;
+            mStartHeight = mHeight;
+        }
+
+        public void StartInterpolate()
+        {
+            if ((mAllowInterpolate) && (mInterpolatePct == 1.0f))
+            {
+                mInterpolatePct = 0;
+                mParent.SelfToRootTranslate(mX, mY, out mStartRootX, out mStartRootY);
+                mStartWidth = mWidth;
+                mStartHeight = mHeight;
+
+                mEndX = mX;
+                mEndY = mY;
+                mEndWidth = mWidth;
+                mEndHeight = mHeight;
+            }
+        }
+
+        void UpdateInterpolation()
+        {
+            if (mInterpolatePct != 1.0f)
+            {
+                mInterpolatePct = Math.Min(mInterpolatePct + 0.1f, 1.0f);
+
+                float startX;
+                float startY;
+                mParent.RootToSelfTranslate(mStartRootX, mStartRootY, out startX, out startY);
+                Resize(
+                    Utils.Lerp(startX, mEndX, mInterpolatePct),
+                    Utils.Lerp(startY, mEndY, mInterpolatePct),
+                    Utils.Lerp(mStartWidth, mEndWidth, mInterpolatePct),
+                    Utils.Lerp(mStartHeight, mEndHeight, mInterpolatePct));
+            }
+        }
+
+        public override void Update()
+        {
+            base.Update();
+            UpdateInterpolation();
+
+            Debug.Assert((mParent == mParentDockingFrame) || (mParentDockingFrame == null));
+
+            // Allow to interpolate after first update
+            mAllowInterpolate = true;
+        }
+
+        public virtual void ResizeDocked(float x, float y, float width, float height)
+        {
+            if (mInterpolatePct != 1.0f)
+            {
+                mEndX = x;
+                mEndY = y;
+                mEndWidth = width;
+                mEndHeight = height;
+                UpdateInterpolation();
+            }
+            else
+                Resize(x, y, width, height);
+        }
+
+        public override void Resize(float x, float y, float width, float height)
+        {
+            base.Resize(x, y, width, height);
+        }
+    }
+
+    public class DockingProxy : DockedWidget
+    {
+        Widget mWidget;
+
+        public this(Widget widget)
+        {
+            mWidget = widget;
+            AddWidget(mWidget);
+        }
+
+        public override void Resize(float x, float y, float width, float height)
+        {
+            base.Resize(x, y, width, height);
+            mWidget.Resize(0, 0, width, height);
+        }
+    }
+}

+ 989 - 0
BeefLibs/Beefy2D/src/widgets/DockingFrame.bf

@@ -0,0 +1,989 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.theme;
+using Beefy.gfx;
+using System.Diagnostics;
+using Beefy.geom;
+
+namespace Beefy.widgets
+{    
+    public class DockingFrame : DockedWidget
+    {        
+        public enum WidgetAlign
+        {         
+            Inside,
+            Left,
+            Right,
+            Top,
+            Bottom
+        };
+
+        public enum SplitType
+        {
+            None,
+            Horz,
+            Vert
+        };                
+        
+        public List<DockedWidget> mDockedWidgets = new List<DockedWidget>() ~ delete _;
+        public SplitType mSplitType = SplitType.None;
+        public float mMinWindowSize = 32;
+        public float mDragMarginSize = 64;
+        public float mDragWindowMarginSize = 10;
+
+        public float mWindowMargin = 0;
+        public float mSplitterSize = 6.0f;
+        public float mWindowSpacing = 2.0f;
+        public int32 mDownSplitterNum = -1;
+
+        public IDockable mDraggingDock;
+        public DockedWidget mDraggingRef;
+        public WidgetAlign mDraggingAlign;
+        public ICustomDock mDraggingCustomDock ~ delete (Object)_;
+
+        public virtual void AddDockedWidget(DockedWidget widget, DockedWidget refWidget, WidgetAlign align)
+        {
+            SplitType wantSplitType = ((align == WidgetAlign.Left) || (align == WidgetAlign.Right)) ?
+                SplitType.Horz : SplitType.Vert;
+
+            if (mDockedWidgets.Count == 0)
+                wantSplitType = SplitType.None;
+
+            if ((wantSplitType != mSplitType) && (mSplitType != SplitType.None))
+            {
+                DockingFrame newChildFrame = ThemeFactory.mDefault.CreateDockingFrame(this);
+                bool hasFillWidget = mHasFillWidget;
+                                
+                if (refWidget != null)
+                {
+                    newChildFrame.mRequestedWidth = refWidget.mRequestedWidth;
+                    newChildFrame.mRequestedHeight = refWidget.mRequestedHeight;
+                    ReplaceDockedWidget(refWidget, newChildFrame);
+
+                    // Just split out the one referenced widget
+                    newChildFrame.mParentDockingFrame = this;
+                    refWidget.mParentDockingFrame = newChildFrame;
+                    newChildFrame.mDockedWidgets.Add(refWidget);
+                    newChildFrame.AddWidget(refWidget);
+
+                    newChildFrame.AddDockedWidget(widget, null, align);
+                    ResizeContent(true);
+                    return;
+                }
+                else
+                {
+                    newChildFrame.mRequestedWidth = 0;
+                    newChildFrame.mRequestedHeight = 0;
+
+                    for (DockedWidget aDockedWidget in mDockedWidgets)
+                    {
+                        if (mSplitType == SplitType.Horz)
+                        {
+                            newChildFrame.mRequestedWidth += aDockedWidget.mRequestedWidth;
+                            newChildFrame.mRequestedHeight = Math.Max(newChildFrame.mRequestedHeight, aDockedWidget.mRequestedHeight);
+                        }
+                        else
+                        {
+                            newChildFrame.mRequestedWidth = Math.Max(mRequestedWidth, aDockedWidget.mRequestedWidth);
+                            newChildFrame.mRequestedHeight += aDockedWidget.mRequestedHeight;
+                        }
+
+                        RemoveWidget(aDockedWidget);
+                        newChildFrame.mDockedWidgets.Add(aDockedWidget);
+                        newChildFrame.AddWidget(aDockedWidget);
+                        aDockedWidget.mParentDockingFrame = newChildFrame;
+                        hasFillWidget |= aDockedWidget.mHasFillWidget;
+                    }
+                    mDockedWidgets.Clear();
+                }
+
+                newChildFrame.mParentDockingFrame = this;
+                newChildFrame.mHasFillWidget = hasFillWidget;
+                newChildFrame.mSplitType = mSplitType;
+                newChildFrame.mWidth = newChildFrame.mRequestedWidth;
+                newChildFrame.mHeight = newChildFrame.mRequestedHeight;
+
+                mDockedWidgets.Add(newChildFrame);
+                AddWidget(newChildFrame);
+            }
+            
+            widget.mParentDockingFrame = this;
+            
+			//TODO:
+            /*if (fillWidget != null)
+            {
+                // Don't take more than half remaining space at a time
+                widget.mRequestedWidth = Math.Min(fillWidget.mWidth / 2, widget.mRequestedWidth);
+                widget.mRequestedHeight = Math.Min(fillWidget.mHeight / 2, widget.mRequestedHeight);
+            }*/
+
+            widget.mRequestedWidth = Math.Max(mMinWindowSize, widget.mRequestedWidth);
+            widget.mRequestedHeight = Math.Max(mMinWindowSize, widget.mRequestedHeight);            
+
+            if ((align == WidgetAlign.Left) || (align == WidgetAlign.Top))
+            {
+                if (refWidget != null)
+                    mDockedWidgets.Insert(mDockedWidgets.IndexOf(refWidget), widget);
+                else
+                    mDockedWidgets.Insert(0, widget);
+            }
+            else
+            {
+                if (refWidget != null)
+                    mDockedWidgets.Insert(mDockedWidgets.IndexOf(refWidget) + 1, widget);
+                else
+                    mDockedWidgets.Add(widget);
+            }
+
+            AddWidget(widget);
+
+            mSplitType = wantSplitType;
+
+			if ((widget.mHasFillWidget) || (widget.mIsFillWidget))
+				widget.GetRootDockingFrame().Rehup();
+            ResizeContent(true);
+        }
+
+		public override void Rehup()
+		{
+			bool allHaveSizes = true;
+
+			bool didHaveFillWidget = mHasFillWidget;
+			base.Rehup();
+			for (var dockedWiget in mDockedWidgets)
+			{
+				dockedWiget.mHasFillWidget = dockedWiget.mIsFillWidget;
+				dockedWiget.Rehup();
+				if (dockedWiget.mHasFillWidget)
+				{
+					mHasFillWidget = true;
+				}
+				if ((dockedWiget.mWidth == 0) || (dockedWiget.mHeight == 0))
+					allHaveSizes = false;
+			}
+
+			// If we turn off mHasFillWidget and go to an all-non-fill situation then we reset the size priority 
+			if (didHaveFillWidget != mHasFillWidget)
+			{
+				for (var dockedWiget in mDockedWidgets)
+					dockedWiget.mSizePriority = 0;
+
+				//if (!mHasFillWidget)
+				if (allHaveSizes)
+				{
+					for (var dockedWidget in mDockedWidgets)
+					{
+						if (mSplitType == .Horz)
+							dockedWidget.mRequestedWidth = dockedWidget.mWidth;
+						else
+							dockedWidget.mRequestedHeight = dockedWidget.mHeight;
+					}
+				}
+			}
+
+			for (var dockedWiget in mDockedWidgets)
+			{
+				// Reset the size priority to be whatever the current size is, it should stabilize sizes when
+				//  toggling mIsFillWidget
+				if (dockedWiget.mHasFillWidget == mHasFillWidget)
+				{
+					if (dockedWiget.mSizePriority == 0)
+					{
+						if (mSplitType == .Horz)
+							dockedWiget.mSizePriority = dockedWiget.mWidth;
+						else
+							dockedWiget.mSizePriority = dockedWiget.mHeight;
+					}
+				}
+				else
+					dockedWiget.mSizePriority = 0;
+			}
+		}
+
+		public override DockingFrame GetRootDockingFrame()
+		{
+			var parentFrame = this;
+			while (parentFrame != null)
+			{
+				if (parentFrame.mParentDockingFrame == null)
+					break;
+				parentFrame = parentFrame.mParentDockingFrame;
+			}
+			return parentFrame;
+		}
+
+        public void WithAllDockedWidgets(Action<DockedWidget> func)
+        {
+            for (var dockedWidget in mDockedWidgets)
+            {                
+                func(dockedWidget);
+
+                var dockingFrame = dockedWidget as DockingFrame;
+                if (dockingFrame != null)
+                {
+                    dockingFrame.WithAllDockedWidgets(func);                    
+                }
+            }
+        }
+
+        public int GetDockedWindowCount()
+        {
+            return mDockedWidgets.Count;
+        }
+
+        public void Simplify()
+        {            
+            if ((mDockedWidgets.Count == 0) && (mParentDockingFrame != null))
+                mParentDockingFrame.RemoveWidget(this);
+            else if ((mDockedWidgets.Count == 1) && (mParentDockingFrame != null))
+            {
+                // Just a single object, remove ourselves from the frame
+                DockedWidget aDockedWidget = mDockedWidgets[0];
+                mDockedWidgets.Clear();
+                RemoveWidget(aDockedWidget);
+                mParentDockingFrame.ReplaceDockedWidget(this, aDockedWidget);
+				BFApp.sApp.DeferDelete(this);
+            }            
+        }
+
+        public virtual void RemoveDockedWidget(DockedWidget dockedWidget)
+        {
+            mDockedWidgets.Remove(dockedWidget);
+            RemoveWidget(dockedWidget);
+			if (!mIsFillWidget)
+            	mHasFillWidget = GetHasFillWidget();
+            ResizeContent(true);
+
+            if ((mDockedWidgets.Count == 0) && (mParentDockingFrame == null))
+            {
+                // Automatically close when last docked widget is removed
+                //  Should only happen on tool windows
+                mWidgetWindow.Close();                
+            }            
+        }
+
+        // Used when an embedded docking frame gets down to just a single widget
+        public virtual void ReplaceDockedWidget(DockedWidget dockedWidget, DockedWidget replaceWidget)
+        {
+            int32 index = mDockedWidgets.IndexOf(dockedWidget);
+            RemoveWidget(dockedWidget);
+            mDockedWidgets[index] = replaceWidget;
+            AddWidget(replaceWidget);
+            replaceWidget.mParentDockingFrame = this;
+            ResizeContent(true);
+        }
+
+        protected virtual bool GetHasFillWidget()
+        {
+            for (DockedWidget aDockedWidget in mDockedWidgets)
+                if (aDockedWidget.mHasFillWidget)
+                    return true;
+            return false;
+        }
+
+        public virtual void StartContentInterpolate()
+        {
+            for (DockedWidget aDockedWidget in mDockedWidgets)
+            {                
+                aDockedWidget.StartInterpolate();
+            }
+        }
+
+        public virtual void ResizeContent(bool interpolate = false)
+        {
+            float sizeLeft = (mSplitType == SplitType.Horz) ? (mWidth - mWindowMargin*2) : (mHeight - mWindowMargin*2);
+            sizeLeft -= (mDockedWidgets.Count - 1) * mWindowSpacing;
+			if (sizeLeft <= 0)
+				return;
+   
+            List<DockedWidget> widgetsLeft = scope List<DockedWidget>();
+            for (DockedWidget aDockedWidget in mDockedWidgets)
+            {
+                widgetsLeft.Add(aDockedWidget);
+                if (interpolate)
+                    aDockedWidget.StartInterpolate();
+            }
+
+			//DockedWidget fillWidget = GetFillWidget();
+			//if (fillWidget != null)
+			{
+				bool hasFillWidget = false;
+				for (int32 widgetIdx = 0; widgetIdx < widgetsLeft.Count; widgetIdx++)
+				{
+					DockedWidget aDockedWidget = widgetsLeft[widgetIdx];
+					if (aDockedWidget.mHasFillWidget)
+					{
+						hasFillWidget = true;
+						break;
+					}
+				}
+
+				if (hasFillWidget)
+				{
+				    for (int32 widgetIdx = 0; widgetIdx < widgetsLeft.Count; widgetIdx++)
+				    {
+				        DockedWidget aDockedWidget = widgetsLeft[widgetIdx];
+						if (aDockedWidget.mHasFillWidget)
+							continue;
+
+				        float requestedSize = (mSplitType == SplitType.Horz) ? (aDockedWidget.mRequestedWidth) : aDockedWidget.mRequestedHeight;
+				        float size = Math.Round(Math.Max(mMinWindowSize, Math.Min(requestedSize, sizeLeft - (widgetsLeft.Count * mMinWindowSize))));
+
+				        if (mSplitType == SplitType.Horz)
+				            aDockedWidget.mWidth = size;
+				        else
+				            aDockedWidget.mHeight = size;
+
+				        sizeLeft -= size;
+				        widgetsLeft.RemoveAt(widgetIdx);
+				        widgetIdx--;
+				    }
+				}
+
+				//widgetsLeft.Add(fillWidget);
+			    /*if (mSplitType == SplitType.Horz)
+			        fillWidget.mWidth = sizeLeft;
+			    else
+			        fillWidget.mHeight = sizeLeft;*/
+			}
+
+			//if (fillWidget == null)
+			{                
+			    int32 totalPriCount = 0;
+			    int32 newPriCounts = 0;
+			    float totalPriAcc = 0.0f;
+			    float avgPri = 0;
+
+			    for (DockedWidget aDockedWidget in widgetsLeft)
+			    {
+			        totalPriAcc += aDockedWidget.mSizePriority;
+			        if (aDockedWidget.mSizePriority == 0)
+			            newPriCounts++;
+			        else
+			            totalPriCount++;
+			    }
+
+				if (totalPriCount == 0)
+				{
+					// Totally uninitialized, possibly from having a document frame and then removing it
+					for (DockedWidget aDockedWidget in widgetsLeft)
+						if ((aDockedWidget.mSizePriority == 0) && (aDockedWidget.mWidth > 0) && (aDockedWidget.mHeight > 0))
+						{
+						    aDockedWidget.mSizePriority = (mSplitType == .Horz) ? aDockedWidget.mWidth : aDockedWidget.mHeight;
+						    totalPriCount++;
+						    totalPriAcc += aDockedWidget.mSizePriority;
+							newPriCounts--;
+						}
+				}
+
+			    if (newPriCounts > 0)
+			    {
+			        if (totalPriAcc > 0)
+			            avgPri = totalPriAcc / totalPriCount;
+			        else
+			            avgPri = 1.0f / newPriCounts;
+
+			        for (DockedWidget aDockedWidget in widgetsLeft)
+			            if (aDockedWidget.mSizePriority == 0)
+			            {
+			                aDockedWidget.mSizePriority = avgPri;
+			                totalPriCount++;
+			                totalPriAcc += aDockedWidget.mSizePriority;
+			            }
+			    }
+
+
+			    float sharedWidth = sizeLeft;
+
+			    for (int32 widgetIdx = 0; widgetIdx < widgetsLeft.Count; widgetIdx++)
+			    {
+			        DockedWidget aDockedWidget = widgetsLeft[widgetIdx];
+
+			        float size = (aDockedWidget.mSizePriority / totalPriAcc) * sharedWidth;
+			        size = (float) Math.Round(size);
+
+			        if (widgetIdx == widgetsLeft.Count - 1)
+			            size = sizeLeft;
+
+			        if (mSplitType == SplitType.Horz)
+			        {
+			            aDockedWidget.mWidth = size;                        
+			        }
+			        else
+			        {
+			            aDockedWidget.mHeight = size;                        
+			        }
+			        
+			        sizeLeft -= size;
+			        widgetsLeft.RemoveAt(widgetIdx);
+			        widgetIdx--;
+			    }               
+			}
+
+            /*DockedWidget fillWidget = GetFillWidget();
+            if (fillWidget == null)
+            {                
+                int totalPriCount = 0;
+                int newPriCounts = 0;
+                float totalPriAcc = 0.0f;
+                float avgPri = 0;
+
+                foreach (DockedWidget aDockedWidget in widgetsLeft)
+                {
+                    totalPriAcc += aDockedWidget.mSizePriority;
+                    if (aDockedWidget.mSizePriority == 0)
+                        newPriCounts++;
+                    else
+                        totalPriCount++;
+                }
+
+                if (newPriCounts > 0)
+                {
+                    if (totalPriAcc > 0)
+                        avgPri = totalPriAcc / totalPriCount;
+                    else
+                        avgPri = 1.0f / newPriCounts;
+
+                    foreach (DockedWidget aDockedWidget in widgetsLeft)
+                        if (aDockedWidget.mSizePriority == 0)
+                        {
+                            aDockedWidget.mSizePriority = avgPri;
+                            totalPriCount++;
+                            totalPriAcc += aDockedWidget.mSizePriority;
+                        }
+                }
+
+
+                float sharedWidth = sizeLeft;
+
+                for (int widgetIdx = 0; widgetIdx < widgetsLeft.Count; widgetIdx++)
+                {
+                    DockedWidget aDockedWidget = widgetsLeft[widgetIdx];
+
+                    float size = (aDockedWidget.mSizePriority / totalPriAcc) * sharedWidth;
+                    size = (float) Math.Round(size);
+
+                    if (widgetIdx == widgetsLeft.Count - 1)
+                        size = sizeLeft;
+
+                    if (mSplitType == SplitType.Horz)
+                    {
+                        aDockedWidget.mWidth = size;                        
+                    }
+                    else
+                    {
+                        aDockedWidget.mHeight = size;                        
+                    }
+                    
+                    sizeLeft -= size;
+                    widgetsLeft.RemoveAt(widgetIdx);
+                    widgetIdx--;
+                }               
+            }
+            else
+            {
+                widgetsLeft.Remove(fillWidget);
+
+                for (int widgetIdx = 0; widgetIdx < widgetsLeft.Count; widgetIdx++)
+                {
+                    DockedWidget aDockedWidget = widgetsLeft[widgetIdx];
+
+                    float requestedSize = (mSplitType == SplitType.Horz) ? (aDockedWidget.mRequestedWidth) : aDockedWidget.mRequestedHeight;
+                    float size = Math.Max(mMinWindowSize, Math.Min(requestedSize, sizeLeft - (widgetsLeft.Count * mMinWindowSize)));
+
+                    if (mSplitType == SplitType.Horz)
+                        aDockedWidget.mWidth = size;
+                    else
+                        aDockedWidget.mHeight = size;
+
+                    sizeLeft -= size;
+                    widgetsLeft.RemoveAt(widgetIdx);
+                    widgetIdx--;
+                }
+
+                 if (mSplitType == SplitType.Horz)
+                    fillWidget.mWidth = sizeLeft;
+                else
+                    fillWidget.mHeight = sizeLeft;
+            }*/
+
+            float curPos = mWindowMargin;
+            for (DockedWidget aDockedWidget in mDockedWidgets)
+            {
+                float size = (mSplitType == SplitType.Horz) ? aDockedWidget.mWidth : aDockedWidget.mHeight;
+
+                if (mSplitType == SplitType.Horz)
+                {
+                    aDockedWidget.ResizeDocked(curPos, mWindowMargin, size, mHeight - mWindowMargin*2);
+                }
+                else
+                {
+                    aDockedWidget.ResizeDocked(mWindowMargin, curPos, mWidth - mWindowMargin*2, size);
+                }
+
+                curPos += size;
+                curPos += mWindowSpacing;
+            }
+        }
+
+        public override void Resize(float x, float y, float width, float height)
+        {
+            base.Resize(x, y, width, height);
+            ResizeContent();
+        }
+
+        public int32 FindSplitterAt(float x, float y)
+        {
+            if (!Contains(x, y))
+                return -1;
+
+            float curPos = 0;
+            float findPos = (mSplitType == SplitType.Horz) ? (x - mWindowMargin) : (y - mWindowMargin);
+            for (int32 widgetIdx = 0; widgetIdx < mDockedWidgets.Count; widgetIdx++)            
+            {
+                DockedWidget aDockedWidget = mDockedWidgets[widgetIdx];
+                float size = (mSplitType == SplitType.Horz) ? aDockedWidget.mWidth : aDockedWidget.mHeight;
+
+                float diff = (findPos - curPos) + mWindowSpacing + (mSplitterSize - mWindowSpacing) / 2.0f;
+                if ((diff >= 0) && (diff < mSplitterSize))
+                    return widgetIdx - 1;
+
+                curPos += size;
+                curPos += mWindowSpacing;
+            }
+
+            return -1;
+        }
+
+        DockingFrame GetDockingFrame(WidgetWindow widgetWindow)
+        {
+            if (widgetWindow == null)
+                return null;
+
+            DockingFrame dockingFrame = widgetWindow.mRootWidget as DockingFrame;
+            if (dockingFrame != null)
+                return dockingFrame;
+            
+            for (var child in widgetWindow.mRootWidget.mChildWidgets)
+            {
+                dockingFrame = child as DockingFrame;
+                if (dockingFrame != null)
+                    return dockingFrame;
+            }
+            return null;
+        }
+
+        public virtual void ShowDragTarget(IDockable draggingItem)
+        {
+            Debug.Assert(mParentDockingFrame == null);
+
+            for (BFWindow window in BFApp.sApp.mWindows)
+            {
+                if (window.mAlpha != 1.0f)
+                    continue;
+
+                WidgetWindow widgetWindow = window as WidgetWindow;
+
+                var dockingFrame = GetDockingFrame(widgetWindow);
+                if (dockingFrame != null)
+                {
+                    if ((widgetWindow.mHasMouseInside) || (widgetWindow.mHasProxyMouseInside))
+                        dockingFrame.ShowDragTarget(draggingItem, widgetWindow.mMouseX, widgetWindow.mMouseY);
+                    else
+                        dockingFrame.HideDragTargets();                        
+                }                
+            }
+        }
+
+        public virtual void HideDragTarget(IDockable draggingItem, bool executeDrag = false)
+        {
+            Debug.Assert(mParentDockingFrame == null);
+
+            for (int32 windowIdx = 0; windowIdx < BFApp.sApp.mWindows.Count; windowIdx++)
+            {
+                BFWindow window = BFApp.sApp.mWindows[windowIdx];
+                WidgetWindow widgetWindow = window as WidgetWindow;
+                if (widgetWindow != null)
+                {                    
+                    DockingFrame dockingFrame = GetDockingFrame(widgetWindow);
+                    if (dockingFrame != null)
+                        dockingFrame.HideDragTargets(executeDrag);
+                }
+            }
+        }
+
+        void HideDragTargets(bool executeDrag = false)
+        {
+            if ((executeDrag) && (mDraggingDock != null))
+            {
+                if (mDraggingCustomDock != null)
+                    mDraggingCustomDock.Dock(mDraggingDock);
+                else
+                    mDraggingDock.Dock(this, mDraggingRef, mDraggingAlign);
+            }
+            mDraggingDock = null;
+            mDraggingRef = null;
+			delete (Object)mDraggingCustomDock;
+            mDraggingCustomDock = null;
+
+            for (int32 dockedWidgetIdx = 0; dockedWidgetIdx < mDockedWidgets.Count; dockedWidgetIdx++)
+            {
+                DockedWidget aDockedWidget = mDockedWidgets[dockedWidgetIdx];
+                DockingFrame childFrame = aDockedWidget as DockingFrame;
+                if (childFrame != null)
+                    childFrame.HideDragTargets(executeDrag);
+            }
+        }
+
+
+        bool FindDragTarget(IDockable draggingItem, float x, float y, ref DockingFrame containingFrame, ref DockedWidget refWidget, ref WidgetAlign align, ref ICustomDock customDock, int32 minDist = -2)
+        {
+			//int foundMinDist = -1;
+			bool foundInSelf = false;
+
+            for (DockedWidget aDockedWidget in mDockedWidgets)
+            {
+                float childX;
+                float childY;
+                aDockedWidget.ParentToSelfTranslate(x, y, out childX, out childY);
+
+				var rect = Rect(-2, -2, aDockedWidget.mWidth + 4, aDockedWidget.mHeight + 4);
+				
+                //if (aDockedWidget.Contains(childX, childY))
+				if (rect.Contains(childX, childY))
+                {
+                    float leftDist = childX;
+                    float topDist = childY;
+                    float rightDist = aDockedWidget.mWidth - childX;
+                    float botDist = aDockedWidget.mHeight - childY;
+
+					if (Math.Min(Math.Min(Math.Min(leftDist, topDist), rightDist), botDist) < minDist)
+						continue;
+
+                    float marginX = Math.Min(mDragMarginSize, aDockedWidget.mWidth / 4);
+                    float marginY = Math.Min(mDragMarginSize, aDockedWidget.mHeight / 4);
+
+                    if ((marginX < Math.Min(leftDist, rightDist)) && (marginY < Math.Min(topDist, botDist)))
+                        align = WidgetAlign.Inside;
+                    else if (leftDist < Math.Min(topDist, Math.Min(rightDist, botDist)))
+                        align = WidgetAlign.Left;
+                    else if (topDist < Math.Min(rightDist, botDist))
+                        align = WidgetAlign.Top;
+                    else if (rightDist < botDist)
+                        align = WidgetAlign.Right;
+                    else
+                        align = WidgetAlign.Bottom;
+
+                    if (draggingItem.CanDock(this, aDockedWidget, align))
+                    {
+                        customDock = aDockedWidget.GetCustomDock(draggingItem, childX, childY);
+
+                        containingFrame = this;
+                        refWidget = aDockedWidget;
+						foundInSelf = true;
+                        break;
+                    }
+                }
+            }
+
+			int32 newMinDist = minDist;
+			if (foundInSelf)
+				newMinDist = Math.Max(minDist, 0) + 2;
+
+			for (DockedWidget aDockedWidget in mDockedWidgets)
+			{
+			    float childX;
+			    float childY;
+			    aDockedWidget.ParentToSelfTranslate(x, y, out childX, out childY);
+
+			    if (aDockedWidget.Contains(childX, childY))
+			    {
+			        DockingFrame childFrame = aDockedWidget as DockingFrame;
+			        if (childFrame != null)
+			        {
+			            childFrame.FindDragTarget(draggingItem, childX, childY, ref containingFrame, ref refWidget, ref align, ref customDock, newMinDist);
+			            break;
+			        }
+			    }
+			}
+
+            if ((mParentDockingFrame == null) && (customDock == null))
+            {
+                if (Contains(x, y))
+                {
+                    float leftDist = x;
+                    float topDist = y;
+                    float rightDist = mWidth - x;
+                    float botDist = mHeight - y;                    
+
+                    float marginX = Math.Min(mDragWindowMarginSize, mWidth / 4);
+                    float marginY = Math.Min(mDragWindowMarginSize, mHeight / 4);
+                    WidgetAlign anAlign;
+
+                    if ((marginX < Math.Min(leftDist, rightDist)) && (marginY < Math.Min(topDist, botDist)))
+                        anAlign = WidgetAlign.Inside;
+                    else if (leftDist < Math.Min(topDist, Math.Min(rightDist, botDist)))
+                        anAlign = WidgetAlign.Left;
+                    else if (topDist < Math.Min(rightDist, botDist))
+                        anAlign = WidgetAlign.Top;
+                    else if (rightDist < botDist)
+                        anAlign = WidgetAlign.Right;
+                    else
+                        anAlign = WidgetAlign.Bottom;
+
+                    if ((anAlign != WidgetAlign.Inside) && (draggingItem.CanDock(this, null, align)))
+                    {
+                        containingFrame = this;
+                        refWidget = null;
+                        customDock = null;
+                        align = anAlign;
+                    }
+                }
+            }
+
+            
+            return containingFrame != null;
+        }
+
+        void ShowDragTarget(IDockable draggingItem, float x, float y)
+        {
+            DockingFrame containingFrame = null;
+            DockedWidget refWidget = null;
+            WidgetAlign align = .Inside;
+            ICustomDock customDock = null;
+
+			/*containingFrame = null;
+			refWidget = null;
+			align = WidgetAlign.Inside;
+			customDock = null;*/
+
+			MarkDirty();
+            HideDragTargets();
+
+			if ((y < 0) && (y >= -6) && (x >= 0) && (x < mWidgetWindow.mWindowWidth))
+			{
+				containingFrame = this;
+
+				containingFrame.mDraggingDock = draggingItem;
+				containingFrame.mDraggingRef = null;
+				containingFrame.mDraggingAlign = .Top;
+
+				delete (Object)containingFrame.mDraggingCustomDock;
+				containingFrame.mDraggingCustomDock = null;
+				return;
+			}	
+
+            if (FindDragTarget(draggingItem, x, y, ref containingFrame, ref refWidget, ref align, ref customDock))
+            {
+                containingFrame.mDraggingDock = draggingItem;
+                containingFrame.mDraggingRef = refWidget;
+                containingFrame.mDraggingAlign = align;
+
+				delete (Object)containingFrame.mDraggingCustomDock;
+                containingFrame.mDraggingCustomDock = customDock;
+            }
+        }
+
+       
+        public override Widget FindWidgetByCoords(float x, float y)
+        {
+            if (FindSplitterAt(x, y) != -1)
+                return this;
+            return base.FindWidgetByCoords(x, y);
+        }
+
+        public override void MouseDown(float x, float y, int32 btn, int32 btnCount)
+        {
+            base.MouseDown(x, y, btn, btnCount);
+            mDownSplitterNum = FindSplitterAt(x, y);            
+        }        
+
+        public override void MouseMove(float x, float y)
+        {
+            base.MouseMove(x, y);
+
+            if (mDownSplitterNum != -1)
+            {
+                float wantPos = (mSplitType == SplitType.Horz) ? x : y;
+
+				DockedWidget resizedWidget = null;
+                DockedWidget widget1 = mDockedWidgets[mDownSplitterNum];
+                DockedWidget widget2 = mDockedWidgets[mDownSplitterNum + 1];
+
+				List<DockedWidget> widgetsLeft = scope List<DockedWidget>();
+				float sizeLeft = (mSplitType == SplitType.Horz) ? (mWidth - mWindowMargin*2) : (mHeight - mWindowMargin*2);
+
+				if ((widget1.mHasFillWidget) && (widget2.mHasFillWidget))
+				{
+					bool foundWidget = false;
+					for (var dockedWidget in mDockedWidgets)
+					{
+						if (dockedWidget == widget1)
+							foundWidget = true;
+						if (dockedWidget.mHasFillWidget)
+							widgetsLeft.Add(dockedWidget);
+						else
+						{
+							float curSize = (mSplitType == SplitType.Horz) ? dockedWidget.mWidth : dockedWidget.mHeight;
+							if (!foundWidget)
+								wantPos -= curSize;
+							sizeLeft -= curSize;
+						}
+					}
+				}
+				else if (widget1.mHasFillWidget != widget2.mHasFillWidget)
+				{
+					if (widget2.mHasFillWidget)
+					{
+						if (mSplitType == .Horz)
+							widget1.mRequestedWidth = wantPos - widget1.mX;
+						else
+							widget1.mRequestedHeight = wantPos - widget1.mY;
+					}
+					else
+					{
+						if (mSplitType == .Horz)
+						{
+							float totalSize = widget1.mWidth + widget2.mWidth;
+							widget2.mRequestedWidth = Math.Max(0, widget2.mRequestedWidth + (widget2.mX - wantPos));
+							widget2.mRequestedWidth = Math.Min(widget2.mRequestedWidth, totalSize - mMinWindowSize * 2);
+						}
+						else
+						{
+							float totalSize = widget1.mHeight + widget2.mHeight;
+							widget2.mRequestedHeight = Math.Max(0, widget2.mRequestedHeight + (widget2.mY - wantPos));
+							widget2.mRequestedHeight = Math.Min(widget2.mRequestedHeight, totalSize - mMinWindowSize * 2);
+						}
+						resizedWidget = widget2;
+						//Debug.WriteLine("RW:{0} WH:{1}", widget2.mRequestedWidth, widget2.mRequestedHeight);
+					}
+				}
+				else
+				{
+					bool hasFillWidget = false;
+					for (var dockedWidget in mDockedWidgets)
+					{
+						if (dockedWidget.mHasFillWidget)
+							hasFillWidget = true;
+					}
+
+					if (hasFillWidget)
+					{
+						for (var dockedWidget in mDockedWidgets)
+						{
+							// Size prioritizes will get initialized later if we remove the fill widget
+							if (!dockedWidget.mHasFillWidget)
+								dockedWidget.mSizePriority = 0;
+						}
+
+						if (mSplitType == .Horz)
+						{
+							float totalSize = widget1.mWidth + widget2.mWidth;
+							widget2.mRequestedWidth = Math.Clamp(widget2.mRequestedWidth + (widget2.mX - wantPos), mMinWindowSize, totalSize - mMinWindowSize);
+							widget1.mRequestedWidth = totalSize - widget2.mRequestedWidth;
+						}			   	 
+						else
+						{
+							float totalSize = widget1.mHeight + widget2.mHeight;
+							widget2.mRequestedHeight = Math.Clamp(widget2.mRequestedHeight + (widget2.mY - wantPos), mMinWindowSize, totalSize - mMinWindowSize);
+							widget1.mRequestedHeight = totalSize - widget2.mRequestedHeight;
+						}
+					}
+					else
+					{
+						for (var dockedWidget in mDockedWidgets)
+							widgetsLeft.Add(dockedWidget);
+					}
+				}
+
+				if (widgetsLeft.Count > 0)
+                {                                        
+                    // First we normalize to 1.0
+                    float sizePriTotal = 0.0f;
+                    for (DockedWidget aDockedWidget in widgetsLeft)
+                        sizePriTotal += aDockedWidget.mSizePriority;
+                    
+                    for (DockedWidget aDockedWidget in widgetsLeft)
+                        aDockedWidget.mSizePriority = aDockedWidget.mSizePriority / sizePriTotal;
+
+
+                    float totalPrevSize = widget1.mSizePriority + widget2.mSizePriority;
+
+                    float startCurPos = 0;
+                    for (DockedWidget aDockedWidget in widgetsLeft)
+                    {
+                        if (aDockedWidget == widget1)
+                            break;
+
+                        startCurPos += aDockedWidget.mSizePriority * sizeLeft + mWindowSpacing;
+                    }
+
+                    float wantSize = Math.Max(mMinWindowSize, wantPos - startCurPos);
+                    wantSize = Math.Min(wantSize, totalPrevSize * sizeLeft - mMinWindowSize);
+
+                    wantSize /= sizeLeft;
+                    widget1.mSizePriority = wantSize;
+                    widget2.mSizePriority = totalPrevSize - wantSize;
+                }
+
+                ResizeContent();
+
+				// Set to actual used value
+				if (resizedWidget != null)
+				{
+					if (mSplitType == .Horz)
+						resizedWidget.mRequestedWidth = resizedWidget.mWidth;
+					else
+						resizedWidget.mRequestedHeight = resizedWidget.mHeight;
+				}
+            }
+            else
+            {
+                if (FindSplitterAt(x, y) != -1)
+                {
+                    if (mSplitType == SplitType.Horz)
+                        BFApp.sApp.SetCursor(Cursor.SizeWE);
+                    else
+                        BFApp.sApp.SetCursor(Cursor.SizeNS);
+                }
+                else
+                {
+                    BFApp.sApp.SetCursor(Cursor.Pointer);
+                }
+            }
+        }
+
+        public override void MouseUp(float x, float y, int32 btn)
+        {
+            base.MouseUp(x, y, btn);            
+            mDownSplitterNum = -1;
+            MouseMove(x, y);
+        }
+
+        public override void MouseLeave()
+        {            
+            base.MouseLeave();
+            if (!mMouseDown)
+                BFApp.sApp.SetCursor(Cursor.Pointer);
+        }
+
+        public virtual void DrawDraggingDock(Graphics g)
+        {
+        }
+
+        public override void DrawAll(Graphics g)
+        {
+            base.DrawAll(g);
+
+            if (mDraggingDock != null)            
+                DrawDraggingDock(g);            
+        }
+
+        public override void Update()
+        {
+            base.Update();
+            Simplify();            
+        }
+    }
+}

+ 185 - 0
BeefLibs/Beefy2D/src/widgets/DragHelper.bf

@@ -0,0 +1,185 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Diagnostics;
+using Beefy.events;
+
+namespace Beefy.widgets
+{
+    public interface IDragInterface
+    {
+        void DragStart();
+        void DragEnd();
+        void MouseDrag(float x, float y, float dX, float dY);
+    }
+
+    public class DragHelper
+    {
+        public float mMouseX;
+        public float mMouseY;
+        public float mMouseDownX;
+        public float mMouseDownY;
+        public float mTriggerDist = 2.0f;
+        public int32 mMinDownTicks = 0;
+        public bool mAllowDrag = true;
+        public bool mIsDragging = false;
+        public MouseFlag mMouseFlags;
+        IDragInterface mDragInterface;
+        public Widget mWidget;
+        public bool mAborted;
+        public bool mPreparingForWidgetMove;        
+        int32 mDownUpdateCnt;
+        
+        public WidgetWindow mTrackingWindow;
+
+		//MouseEventHandler mMouseDownHandler;
+
+		public float RelX
+		{
+			get
+			{
+				return mMouseX - mMouseDownX;
+			}
+		}
+
+		public float RelY
+		{
+			get
+			{
+				return mMouseY - mMouseDownY;
+			}
+		}
+
+        public this(Widget widget, IDragInterface dragInterface)
+        {
+			mWidget = widget;
+			mDragInterface = dragInterface;
+
+            mWidget.mOnMouseDown.Add(new => HandleMouseDown);
+            mWidget.mOnMouseUp.Add(new => HandleMouseUp);
+            mWidget.mOnMouseMove.Add(new => HandleMouseMove);
+        }
+
+		public ~this()
+		{
+			if (mIsDragging)
+				CancelDrag();
+			//Debug.Assert(!mIsDragging);
+
+			mWidget.mOnMouseDown.Remove(scope => HandleMouseDown, true);
+            mWidget.mOnMouseUp.Remove(scope => HandleMouseUp, true);
+            mWidget.mOnMouseMove.Remove(scope => HandleMouseMove, true);
+		}
+
+		void HandleMouseDown(MouseEvent e)
+		{
+			MouseDown(e.mX, e.mY, e.mBtn, e.mBtnCount);
+		}
+
+		void HandleMouseUp(MouseEvent e)
+		{
+			MouseUp(e.mX, e.mY, e.mBtn);
+		}
+
+		void HandleMouseMove(MouseEvent e)
+		{
+			MouseMove(e.mX, e.mY);
+		}
+
+        public void HandleRemoved(Widget widget, Widget prevParent, WidgetWindow prevWindow)
+        {
+            CancelDrag();
+        }
+
+        public void HandleKeydown(KeyDownEvent theEvent)
+        {
+            if (theEvent.mKeyCode == KeyCode.Escape)
+            {
+                mAborted = true;
+                CancelDrag();
+            }
+        }        
+
+        public void MouseDown(float x, float y, int32 btn, int32 btnCount)
+        {
+            mDownUpdateCnt = mWidget.mUpdateCnt;
+            mMouseFlags |= (MouseFlag)(1 << btn);            
+            mMouseDownX = x;
+            mMouseDownY = y;
+        }
+
+        void SetHooks(bool doSet)
+        {
+			//Debug.WriteLine("SetHooks {0} {1}", this, doSet);
+
+            if (doSet)                        
+            {				
+                mTrackingWindow = mWidget.mWidgetWindow;
+                mTrackingWindow.mOnWindowKeyDown.Add(new => HandleKeydown);
+                mWidget.mOnRemovedFromParent.Add(new => HandleRemoved);
+            }
+            else
+            {                
+                mTrackingWindow.mOnWindowKeyDown.Remove(scope => HandleKeydown, true);
+                mWidget.mOnRemovedFromParent.Remove(scope => HandleRemoved, true);
+            }
+        }
+
+        public void SetPreparingForWidgetMove(bool prepare)
+        {
+            SetHooks(!prepare);
+        }
+
+        public void CancelDrag()
+        {
+            if (!mIsDragging)
+                return;
+            SetHooks(false);
+            mMouseFlags = default;
+            mIsDragging = false;
+            mDragInterface.DragEnd();            
+        }        
+
+        public void MouseUp(float x, float y, int32 btn)
+        {
+            mMouseFlags &= (MouseFlag)(~(1 << btn));
+            if (((mMouseFlags & MouseFlag.Left) == 0) && (mIsDragging))
+            {                
+                CancelDrag();
+            }
+        }
+
+        public void Update()
+        {
+            int32 ticksDown = mWidget.mUpdateCnt - mDownUpdateCnt;
+
+            if (((mMouseFlags & MouseFlag.Left) != 0) && (ticksDown >= mMinDownTicks))
+            {
+                if ((!mIsDragging) && (mAllowDrag))
+                {
+                    if ((Math.Abs(mMouseX - mMouseDownX) >= mTriggerDist) || (Math.Abs(mMouseY - mMouseDownY) >= mTriggerDist))
+                    {
+                        SetHooks(true);
+                        mPreparingForWidgetMove = false;
+                        mAborted = false;
+                        mIsDragging = true;
+                        mDragInterface.DragStart();
+                    }
+                }
+
+                if (mIsDragging)
+                {
+                    mIsDragging = true;
+                    mDragInterface.MouseDrag(mMouseX, mMouseY, mMouseX - mMouseDownX, mMouseY - mMouseDownY);
+                }
+            }
+        }
+
+        public void MouseMove(float x, float y)
+        {
+            mMouseX = x;
+            mMouseY = y;
+            Update();
+        }        
+    }
+}

+ 3425 - 0
BeefLibs/Beefy2D/src/widgets/EditWidget.bf

@@ -0,0 +1,3425 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Diagnostics;
+using System.Text;
+using System.Threading;
+using Beefy.events;
+using Beefy.gfx;
+using Beefy.utils;
+using System.IO;
+
+//#define INCLUDE_CHARDATA_CHARID
+
+namespace Beefy.widgets
+{
+    public struct EditSelection
+    {
+        public int32 mStartPos;        
+        public int32 mEndPos;
+
+        public this(int startPos = 0, int endPos = 0)
+        {
+            mStartPos = (int32)startPos;
+            mEndPos = (int32)endPos;
+        }
+
+        public EditSelection Clone()
+        {
+            EditSelection aSelection = EditSelection();
+            aSelection.mStartPos = mStartPos;            
+            aSelection.mEndPos = mEndPos;            
+            return aSelection;
+        }
+
+        public bool HasSelection
+        {
+            get { return (mStartPos != mEndPos); }
+        }
+
+        public bool IsForwardSelect
+        {
+            get { return (mStartPos < mEndPos); }
+        }
+        
+        public int MinPos
+        {
+            get { return (IsForwardSelect) ? mStartPos : mEndPos; }
+        }
+
+        public int MaxPos
+        {
+            get { return (IsForwardSelect) ? mEndPos : mStartPos; }
+        }
+
+		public int Length
+		{
+			get { return Math.Abs(mEndPos - mStartPos); }
+		}
+
+        public void GetAsForwardSelect(out int startPos, out int endPos)
+        {
+            if (IsForwardSelect)
+            {
+                startPos = mStartPos;                
+                endPos = mEndPos;                
+            }
+            else
+            {
+                startPos = mEndPos;                
+                endPos = mStartPos;                
+            }
+        }
+
+        public void MakeForwardSelect() mut
+        {
+            if (mEndPos < mStartPos)
+            {
+                int32 swap = mStartPos;
+                mStartPos = mEndPos;
+                mEndPos = swap;
+            }
+        }
+    }  
+
+    public delegate void EditWidgetEventHandler(EditEvent theEvent);
+
+    public class EditWidgetContent : Widget
+    {
+        public enum TextFlags
+        {
+            Wrap = 1
+        };        
+
+        //[StructLayout(LayoutKind.Explicit) ]
+        public struct CharData
+        {
+            public char8 mChar;
+            public uint8 mDisplayPassId;
+            public uint8 mDisplayTypeId; // For colorization, for example
+            public uint8 mDisplayFlags;
+        }
+
+        public class TextAction : UndoAction
+        {
+            public EditWidgetContent.Data mEditWidgetContentData;
+            public EditSelection? mSelection;
+            public bool mRestoreSelectionOnUndo;
+            public bool mMoveCursor = true;
+
+            public int32 mPrevTextVersionId;
+            public int32 mCursorTextPos;            
+            public LineAndColumn? mVirtualCursorPos;
+
+			// Return either the focused edit widget content, or the last one added
+			public EditWidgetContent EditWidgetContent
+			{
+				get
+				{
+					EditWidgetContent editWidgetContent = null;
+					for (var user in mEditWidgetContentData.mUsers)
+					{
+						editWidgetContent = user;
+						if (editWidgetContent.mEditWidget.mHasFocus)
+							break;
+					}
+					return editWidgetContent;
+				}
+			}
+
+            public this(EditWidgetContent editWidget)
+            {
+                mPrevTextVersionId = editWidget.mData.mCurTextVersionId;
+                mEditWidgetContentData = editWidget.mData;
+				if (editWidget.HasSelection())
+                	mSelection = editWidget.mSelection;
+                mCursorTextPos = (int32)editWidget.CursorTextPos;
+                mVirtualCursorPos = editWidget.mVirtualCursorPos;
+            }
+
+            public void SetPreviousState(bool force)
+            {
+				var editWidgetContent = EditWidgetContent;
+                mEditWidgetContentData.mCurTextVersionId = mPrevTextVersionId;
+                if ((mRestoreSelectionOnUndo) || (force))
+                    editWidgetContent.mSelection = mSelection;
+                else
+                    editWidgetContent.mSelection = null;
+                editWidgetContent.mCursorTextPos = mCursorTextPos;
+                editWidgetContent.mVirtualCursorPos = mVirtualCursorPos;
+                if (mMoveCursor)
+                    editWidgetContent.EnsureCursorVisible();                
+            }
+        }
+
+        public class SetCursorAction : TextAction
+        {
+            public this(EditWidgetContent editWidget) : base(editWidget)
+            {
+                mRestoreSelectionOnUndo = true;
+                mMoveCursor = false;
+            }
+
+            public override bool Undo()
+            {
+                SetPreviousState(false);
+                return true;
+            }
+
+            public override bool Redo()
+            {
+                SetPreviousState(true);
+                return true;
+            }
+        }
+
+        public class TextUndoBatchStart : TextAction, IUndoBatchStart
+        {
+            public String mName;
+            public UndoBatchEnd mBatchEnd;
+            public String Name
+            {
+                get
+                {
+                    return mName;
+                }
+            }
+
+            public this(EditWidgetContent editWidget, String name) 
+                : base(editWidget)
+            {
+                mName = name;
+                mBatchEnd = new UndoBatchEnd();
+                mBatchEnd.mBatchStart = this;
+            }
+
+            public override bool Undo()
+            {
+                SetPreviousState(false);
+                return true;
+            }
+
+            public IUndoBatchEnd BatchEnd
+            {
+                get
+                {
+                    return mBatchEnd;
+                }
+            }
+        }
+
+        public class DeleteSelectionAction : TextAction
+        {
+            public String mSelectionText;            
+
+            public this(EditWidgetContent editWidgetContent)
+                : base(editWidgetContent)
+            {
+                if (mSelection != null)
+				{
+					mSelectionText = new String();
+                    editWidgetContent.GetSelectionText(mSelectionText);
+				}
+            }
+
+			~this()
+			{
+				delete mSelectionText;
+			}
+
+            public override bool Undo()
+            {
+				var editWidgetContent = EditWidgetContent;
+                if (mSelection != null)
+                {
+                    bool wantSnapScroll = mEditWidgetContentData.mTextLength == 0;
+                    editWidgetContent.CursorTextPos = mSelection.Value.MinPos;
+                    editWidgetContent.PhysInsertAtCursor(mSelectionText, mMoveCursor);
+                    if (wantSnapScroll)
+                        editWidgetContent.mEditWidget.FinishScroll();
+                }
+                SetPreviousState(false);
+                return true;
+            }
+
+            public override bool Redo()
+            {
+                SetPreviousState(true);
+                if (mSelection != null)
+                    EditWidgetContent.PhysDeleteSelection(mMoveCursor);
+                return true;
+            }
+        }
+
+        public class IndentTextAction : TextAction
+        {
+            // InsertCharList is for block indent, RemoveCharList is for unindent (shift-tab)
+            public List<Tuple<int32, char8>> mRemoveCharList = new List<Tuple<int32, char8>>() ~ delete _;
+            public List<Tuple<int32, char8>> mInsertCharList = new List<Tuple<int32, char8>>() ~ delete _;
+            public EditSelection mNewSelection;
+
+            public this(EditWidgetContent editWidget)
+                : base(editWidget)
+            {
+                
+            }
+
+            public override bool Undo()
+            {
+				var editWidgetContent = EditWidgetContent;
+                for (int idx = mRemoveCharList.Count - 1; idx >= 0; idx--)
+                {
+                    var idxChar = mRemoveCharList[idx];
+					
+                    editWidgetContent.InsertText(idxChar.Item1, ToStackString!(idxChar.Item2));
+                }
+                
+                for (int idx = mInsertCharList.Count - 1; idx >= 0; idx--)                
+                {
+                    var idxChar = mInsertCharList[idx];
+                    editWidgetContent.RemoveText(idxChar.Item1, 1);
+                }
+                editWidgetContent.ContentChanged();
+                SetPreviousState(false);                
+                return true;
+            }
+
+            public override bool Redo()
+            {
+				var editWidgetContent = EditWidgetContent;
+                SetPreviousState(true);
+                for (var idxChar in mRemoveCharList)
+                    editWidgetContent.RemoveText(idxChar.Item1, 1);
+				var charStr = scope String(' ', 1);
+                for (var idxChar in mInsertCharList)
+				{
+					charStr[0] = idxChar.Item2;
+                    editWidgetContent.InsertText(idxChar.Item1, charStr);
+				}
+                editWidgetContent.ContentChanged();
+                editWidgetContent.mSelection = mNewSelection;
+                editWidgetContent.CursorTextPos = mNewSelection.mEndPos;
+
+                return true;
+            }
+        }
+
+        public class InsertTextAction : DeleteSelectionAction
+        {            
+            public String mText ~ delete _;
+
+            public this(EditWidgetContent editWidget, String text, InsertFlags insertFlags = .NoMoveCursor) 
+                : base(editWidget)
+            {                
+                mText = new String(text);
+                mRestoreSelectionOnUndo = !insertFlags.HasFlag(.NoRestoreSelectionOnUndo);                
+            }
+
+            public override bool Merge(UndoAction nextAction)
+            {
+                InsertTextAction insertTextAction = nextAction as InsertTextAction;
+                if (insertTextAction == null)
+                    return false;
+                if (insertTextAction.mSelection != null)
+                {
+                    if (mSelection == null)
+                        return false;
+
+                    if (mSelection.Value.mEndPos != insertTextAction.mSelection.Value.mStartPos)
+                        return false;
+
+                    mSelection.ValueRef.mEndPos = insertTextAction.mSelection.Value.mEndPos;
+                    mSelectionText.Append(insertTextAction.mSelectionText);
+                }
+
+                int curIdx = mCursorTextPos;
+                int nextIdx = insertTextAction.mCursorTextPos;
+                mRestoreSelectionOnUndo &= insertTextAction.mRestoreSelectionOnUndo;
+                
+                if ((nextIdx != curIdx + mText.Length) ||
+                    (mText.EndsWith("\n")) ||
+                    (insertTextAction.mText == "\n"))
+                    return false;
+
+                mText.Append(insertTextAction.mText);
+                return true;
+            }
+
+            public override bool Undo()
+            {
+				var editWidgetContent = EditWidgetContent;
+                int startIdx = (mSelection != null) ? mSelection.Value.MinPos : mCursorTextPos;
+                editWidgetContent.RemoveText(startIdx, (int32)mText.Length);
+                editWidgetContent.ContentChanged();
+                base.Undo();
+                editWidgetContent.ContentChanged();
+                editWidgetContent.mData.mCurTextVersionId = mPrevTextVersionId;
+                //mEditWidget.mVirtualCursorPos = mPrevVirtualCursorPos;
+                return true;
+            }
+
+            public override bool Redo()
+            {
+				var editWidgetContent = EditWidgetContent;
+                base.Redo();
+                bool wantSnapScroll = mEditWidgetContentData.mTextLength == 0;
+                editWidgetContent.PhysInsertAtCursor(mText, mMoveCursor);
+                if (wantSnapScroll)
+                    editWidgetContent.mEditWidget.FinishScroll();
+                return true;
+            }
+        }
+
+        public class DeleteCharAction : TextAction
+        {
+            String mText ~ delete _;
+            int32 mOffset;
+
+            public this(EditWidgetContent editWidget, int offset, int count) : 
+                base(editWidget)
+            {
+				var editWidgetContent = EditWidgetContent;
+                mOffset = (int32)offset;
+                int32 textPos = mCursorTextPos;                
+				mText = new String(count);
+                editWidgetContent.ExtractString(textPos + offset, count, mText);
+            }
+
+            public override bool Merge(UndoAction nextAction)
+            {
+                DeleteCharAction deleteCharAction = nextAction as DeleteCharAction;
+                if (deleteCharAction == null)
+                    return false;                
+                if ((deleteCharAction.mOffset < 0) != (mOffset < 0))
+                    return false;
+
+                int32 curIdx = mCursorTextPos;
+                int32 nextIdx = deleteCharAction.mCursorTextPos;                
+
+                if (nextIdx != curIdx + mOffset)
+                    return false;
+
+                mOffset += deleteCharAction.mOffset;
+
+                if (mOffset < 0)
+				{
+                    //mText = deleteCharAction.mText + mText;
+					mText.Insert(0, deleteCharAction.mText);
+				}
+                else
+				{
+                    //mText = mText + deleteCharAction.mText;
+					mText.Append(deleteCharAction.mText);
+				}
+
+                return true;
+            }
+
+            public override bool Undo()
+            {
+                int32 textPos = mCursorTextPos;
+                var editWidgetContent = EditWidgetContent;
+
+                editWidgetContent.CursorTextPos = textPos + mOffset;
+
+                editWidgetContent.PhysInsertAtCursor(mText, mMoveCursor);
+				editWidgetContent.ContentChanged();
+                SetPreviousState(false);
+                return true;
+            }
+
+            public override bool Redo()
+            {
+				var editWidgetContent = EditWidgetContent;
+                SetPreviousState(true);
+                editWidgetContent.PhysDeleteChars(mOffset, (int32)mText.Length);
+				editWidgetContent.ContentChanged();
+                return true;
+            }
+        }
+
+        public struct LineAndColumn
+        {
+            public int32 mLine;
+            public int32 mColumn;
+
+            public this(int line, int column)
+            {
+                mLine = (int32)line;
+                mColumn = (int32)column;
+            }
+        }        
+
+		public class Data
+		{
+			public CharData[] mText = new CharData[1] ~ delete _;
+			public int32 mTextLength;
+			public IdSpan mTextIdData = IdSpan.Empty ~ _.Dispose();
+			public uint8[] mTextFlags ~ delete _;
+			public List<int32> mLineStarts ~ delete _;
+			public UndoManager mUndoManager = new UndoManager() ~ delete _;
+			public int32 mNextCharId = 1; //
+			public int32 mCurTextVersionId = 1; // Changes when text is modified
+			//public int mCurComplexChangeId = 1; // Changes when text is modified by more than a single-char8acter insertion or deletion
+
+			public List<EditWidgetContent> mUsers = new List<EditWidgetContent>() ~ delete _;
+
+			public ~this()
+			{
+				
+			}
+
+			public void Ref(EditWidgetContent content)
+			{
+				mUsers.Add(content);
+			}
+
+			public void Deref(EditWidgetContent content)
+			{
+				mUsers.Remove(content);
+				if (mUsers.Count == 0)
+					delete this;
+			}
+
+			public char8 SafeGetChar(int idx)
+			{
+				if ((idx < 0) || (idx >= mTextLength))
+					return 0;
+				return mText[idx].mChar;
+			}
+
+			public char32 GetChar32(int idx)
+			{
+				int checkIdx = idx;
+				char32 c = (char32)SafeGetChar(checkIdx++);
+				if (c < (char32)0x80)
+					return c;
+
+				int8 trailingBytes = UTF8.sTrailingBytesForUTF8[c];
+				switch (trailingBytes)
+				{
+				case 3: c <<= 6; c += (int)SafeGetChar(checkIdx++); fallthrough;
+				case 2: c <<= 6; c += (int)SafeGetChar(checkIdx++); fallthrough;
+				case 1: c <<= 6; c += (int)SafeGetChar(checkIdx++); fallthrough;
+				}
+				return c - (int32)UTF8.sOffsetsFromUTF8[trailingBytes];
+			}
+
+			public char32 GetChar32(ref int idx)
+			{
+				char32 c = (char32)SafeGetChar(idx++);
+				if (c < (char32)0x80)
+					return c;
+
+				int8 trailingBytes = UTF8.sTrailingBytesForUTF8[c];
+				switch (trailingBytes)
+				{
+				case 3: c <<= 6; c += (int)SafeGetChar(idx++); fallthrough;
+				case 2: c <<= 6; c += (int)SafeGetChar(idx++); fallthrough;
+				case 1: c <<= 6; c += (int)SafeGetChar(idx++); fallthrough;
+				}
+				return c - (int32)UTF8.sOffsetsFromUTF8[trailingBytes];
+			}
+		}
+
+		public Data mData ~ _.Deref(this);
+
+        public Insets mTextInsets = new Insets() ~ delete _;
+        public float mHorzJumpSize = 1;
+        public bool mAutoHorzScroll = true;
+        public float mShowLineBottomPadding;
+		public bool mContentChanged;
+
+        public EditWidget mEditWidget;
+                
+        public float mTabSize;
+        public float mCharWidth = -1;
+		public int32 mTabLength = 4;
+        public uint8 mExtendDisplayFlags;
+		public uint8 mInsertDisplayFlags;
+		
+        public int32 mCursorBlinkTicks;                
+		public bool mCursorImplicitlyMoved;
+        public bool mJustInsertedCharPair; // Pressing backspace will delete last char8, even though cursor is between char8 pairs (ie: for brace pairs 'speculatively' inserted)
+        public EditSelection? mSelection;
+		public EditSelection? mDragSelectionUnion; // For double-clicking a word and then "dragging" the selection         
+        public bool mIsReadOnly = false;
+		public bool mWantsUndo = true;
+        public bool mIsMultiline = false;
+        public bool mWordWrap = false;
+        public float mCursorWantX; // For keyboard cursor selection, accounting for when we truncate to line end        
+        public bool mOverTypeMode = false;                
+        
+        public int32 mCursorTextPos;
+        public bool mShowCursorAtLineEnd;
+        public LineAndColumn? mVirtualCursorPos;
+        public bool mEnsureCursorVisibleOnModify = true;
+        public bool mAllowVirtualCursor;
+        public bool mAllowMaximalScroll = true; // Allows us to scroll down such that edit widget is blank except for one line of content at the top
+        public float mMaximalScrollAddedHeight; // 0 is mAllowMaximalScroll is false
+
+        public int CursorTextPos
+        {
+            get
+            {                
+                if (mCursorTextPos == -1)
+                {                    
+                    float x;
+                    float y;
+                    GetTextCoordAtLineAndColumn(mVirtualCursorPos.Value.mLine, mVirtualCursorPos.Value.mColumn, out x, out y);
+
+                    int line;
+                    int lineChar;
+                    float overflowX;
+                    GetLineCharAtCoord(x, y, out line, out lineChar, out overflowX);
+                    mCursorTextPos = (int32)GetTextIdx(line, lineChar);   
+                }
+
+                return mCursorTextPos;
+            }
+
+            set
+            {   
+             	Debug.Assert(value >= 0);
+                mVirtualCursorPos = null;
+                mCursorTextPos = (int32)value;
+            }
+        }
+        
+        public LineAndColumn CursorLineAndColumn
+        {
+            get
+            {                
+                if (mVirtualCursorPos.HasValue)
+                    return mVirtualCursorPos.Value;
+                LineAndColumn lineAndColumn;
+
+                int line;
+                int lineChar;
+                GetLineCharAtIdx(mCursorTextPos, out line, out lineChar);
+                
+                float x;
+                float y;
+                GetTextCoordAtLineChar(line, lineChar, out x, out y);
+
+				int coordLine;
+				int coordLineColumn;
+                GetLineAndColumnAtCoord(x, y, out coordLine, out coordLineColumn);
+				lineAndColumn.mLine = (int32)coordLine;
+				lineAndColumn.mColumn = (int32)coordLineColumn;
+                return lineAndColumn;
+            }
+
+            set
+            {
+                mVirtualCursorPos = value;
+                mCursorTextPos = -1;
+                Debug.Assert(mAllowVirtualCursor);
+                Debug.Assert(mVirtualCursorPos.Value.mColumn >= 0);
+            }
+        }
+
+		public bool WantsUndo
+		{
+			get
+			{
+				return (!mIsReadOnly) && (mWantsUndo);
+			}
+		}
+
+        public this(EditWidgetContent refContent = null)
+        {
+			if (refContent != null)			
+				mData = refContent.mData;
+			else
+				mData = CreateEditData();
+			mData.Ref(this);
+			mContentChanged = true;
+        }
+		
+		protected virtual Data CreateEditData()
+		{
+			return new Data();
+		}
+
+        public uint8[] mTempCopyArr;
+
+        public virtual bool CheckReadOnly()
+        {
+            return mIsReadOnly;
+        }
+
+        public bool TryGetCursorTextPos(out int textPos, out float overflowX)
+        {
+            if (mVirtualCursorPos.HasValue)
+            {
+                float x;
+                float y;
+                GetTextCoordAtLineAndColumn(mVirtualCursorPos.Value.mLine, mVirtualCursorPos.Value.mColumn, out x, out y);
+
+                int line;
+                int lineChar;                
+                bool success = GetLineCharAtCoord(x, y, out line, out lineChar, out overflowX);
+
+                textPos = GetTextIdx(line, lineChar);
+                return success;
+            }
+            textPos = mCursorTextPos;
+            overflowX = 0;
+            return true;
+        }
+
+        public bool TryGetCursorTextPos(out int textPos)
+        {
+            float overflowX;
+            return TryGetCursorTextPos(out textPos, out overflowX);
+        }
+
+		public (char32, int) GetChar32(int idx)
+		{
+			char32 c = mData.mText[idx].mChar;
+			if (c < '\x80')
+				return (c, 1);
+		
+			EditWidgetContent.CharData* readPtr = &mData.mText[idx + 1];
+			
+			int trailingBytes = UTF8.sTrailingBytesForUTF8[c];
+			switch (trailingBytes)
+			{
+			case 4: c <<= 6; c += (uint8)(readPtr++).mChar; fallthrough;
+			case 3: c <<= 6; c += (uint8)(readPtr++).mChar; fallthrough;
+			case 2:
+				c <<= 6;
+				c += (uint8)(readPtr++).mChar;
+				fallthrough;
+			case 1:
+				c <<= 6;
+				c += (uint8)(readPtr++).mChar;
+				fallthrough;
+			}
+			c -= (int32)UTF8.sOffsetsFromUTF8[trailingBytes];
+
+			return (c, trailingBytes + 1);
+		}
+
+        public void ExtractString(int startIdx, int length, String outStr)
+        {
+			Debug.Assert(length >= 0);
+			char8* ptr = outStr.PrepareBuffer(length);
+
+			CharData* char8DataPtr = mData.mText.CArray();
+			for (int32 idx = 0; idx < length; idx++)
+				ptr[idx] = char8DataPtr[idx + startIdx].mChar;
+        }
+
+		public void ExtractLine(int line, String outStr)
+		{
+			GetLinePosition(line, var lineStart, var lineEnd);
+			ExtractString(lineStart, lineEnd - lineStart, outStr);
+		}
+
+		public char8 SafeGetChar(int idx)
+		{
+			if ((idx < 0) || (idx >= mData.mTextLength))
+				return 0;
+			return mData.mText[idx].mChar;
+		}
+
+        static bool IsNonBreakingChar(char8 c)
+        {
+			// Assume any 'high' codepoints are non-breaking
+            return (c.IsLetterOrDigit) || (c == '_') || (c >= '\x80');
+        }
+
+        public override void MouseDown(float x, float y, int32 btn, int32 btnCount)
+        {
+            base.MouseDown(x, y, btn, btnCount);
+            mEditWidget.SetFocus();
+
+            if ((mSelection == null) && (mWidgetWindow.IsKeyDown(KeyCode.Shift)))
+                StartSelection();
+
+            MoveCursorToCoord(x, y);
+
+            //PrintF("~TestStruct() %d\n", mInner.mVal1);            
+
+            if ((btnCount > 1) && (!mWidgetWindow.IsKeyDown(KeyCode.Shift)))
+            {
+                // Select word
+                StartSelection();
+
+                int cursorTextPos = CursorTextPos;
+                int textPos = cursorTextPos;
+
+                if (mData.mTextLength == 0)
+                {   
+                    //Nothing
+                }
+                else if ((textPos < mData.mTextLength) && (!IsNonBreakingChar((char8)mData.mText[textPos].mChar)))
+                {                    
+                    mSelection.ValueRef.mEndPos++;
+                }
+                else
+                {
+                    while ((textPos > 0) && (IsNonBreakingChar((char8)mData.mText[textPos - 1].mChar)))
+                    {
+                        mSelection.ValueRef.mStartPos--;
+                        textPos--;
+                    }
+
+                    textPos = cursorTextPos + 1;
+                    while ((textPos <= mData.mTextLength) && ((textPos == mData.mTextLength) || (mData.mText[textPos - 1].mChar != '\n')) &&
+                        (IsNonBreakingChar((char8)mData.mText[textPos - 1].mChar)))
+                    {
+                        mSelection.ValueRef.mEndPos++;
+                        textPos++;
+                    }
+                }
+
+				mDragSelectionUnion = mSelection;
+				CursorTextPos = mSelection.Value.MaxPos;
+            }
+            else if (!mWidgetWindow.IsKeyDown(KeyCode.Shift))
+            {
+				if ((btn != 0) && (mSelection != null) && (CursorTextPos >= mSelection.Value.MinPos) && (CursorTextPos <= mSelection.Value.MaxPos))
+				{
+					// Leave selection
+				}
+				else
+					StartSelection();
+			}
+            else
+                SelectToCursor();
+        }
+
+		public override void MouseUp(float x, float y, int32 btn)
+		{
+		    base.MouseUp(x, y, btn);
+
+		    if (mMouseFlags == 0)
+		    {
+		        mDragSelectionUnion = null;
+		    }
+		}
+
+        public override void MouseMove(float x, float y)
+        {
+            base.MouseMove(x, y);
+
+            if ((mMouseDown) && (mMouseFlags == .Left))
+            {
+                MoveCursorToCoord(x, y);
+                SelectToCursor();                
+            }
+        }
+
+        public override void MouseEnter()
+        {            
+            base.MouseEnter();
+            BFApp.sApp.SetCursor(Cursor.Text);
+        }
+
+        public override void MouseLeave()
+        {            
+            base.MouseLeave();
+            BFApp.sApp.SetCursor(Cursor.Pointer);            
+        }
+
+        public virtual void ClearText()
+        {
+            SelectAll();
+            DeleteSelection();
+        }
+
+        public void PhysDeleteSelection(bool moveCursor = true)
+        {
+            int startIdx = 0;            
+            int endIdx = 0;            
+            mSelection.Value.GetAsForwardSelect(out startIdx, out endIdx);
+            
+            RemoveText(startIdx, endIdx - startIdx);
+            mSelection = null;
+            
+            CursorTextPos = startIdx;
+
+            if (endIdx != startIdx)
+                ContentChanged();
+			if (moveCursor)
+            	EnsureCursorVisible();
+        }
+
+        public virtual void DeleteSelection(bool moveCursor = true)
+        {
+            if (mSelection != null)
+            {
+				var action = new DeleteSelectionAction(this);
+				action.mMoveCursor = moveCursor;
+                mData.mUndoManager.Add(action);
+                PhysDeleteSelection();
+            }
+        }
+
+        public bool GetSelectionText(String outStr)
+        {
+            if (mSelection == null)
+                return false;
+
+            int startIdx = 0;
+            int endIdx = 0;
+            mSelection.Value.GetAsForwardSelect(out startIdx, out endIdx);
+
+            ExtractString(startIdx, endIdx - startIdx, outStr);
+			return true;
+        }
+
+        void VerifyTextIds()
+        {
+#if INCLUDE_CHARDATA_CHARID
+            /*if (mUpdateCnt > 0)
+                Debug.WriteLine("IdLen: {0}", mTextIdDataLength);*/
+
+            int curTextIdx = 0;
+            int encodeIdx = 0;
+            int curTextId = 1;
+            while (true)
+            {
+                int cmd = Utils.DecodeInt(mTextIdData, ref encodeIdx); ;
+                if (cmd == 0)
+                {
+                    Debug.Assert(encodeIdx == mTextIdDataLength);
+                    break;
+                }
+
+                if (cmd > 0)
+                {
+                    curTextId = cmd;
+                }
+                else
+                {
+                    for (int i = 0; i < -cmd; i++)
+                    {
+                        Debug.Assert(mText[curTextIdx].mCharId == curTextId);
+                        curTextIdx++;
+                        curTextId++;
+                    }
+                }
+            }
+            Debug.Assert(curTextIdx == mTextLength);
+#endif
+        }
+
+		protected virtual void AdjustCursorsAfterExternalEdit(int index, int ofs)
+		{
+#unwarn
+			int cursorPos = CursorTextPos;
+			if (mCursorTextPos >= index)
+				mCursorTextPos += (int32)ofs;
+			if (HasSelection())
+			{				
+				if (((ofs > 0) && (mSelection.mValue.mStartPos >= index)) ||
+					((ofs < 0) && (mSelection.mValue.mStartPos > index)))
+					mSelection.mValue.mStartPos += (int32)ofs;
+				if (mSelection.mValue.mEndPos > index)
+					mSelection.mValue.mEndPos += (int32)ofs;
+			}
+		}
+
+		public virtual void ApplyTextFlags(int index, String text, uint8 typeNum, uint8 flags)
+		{
+			uint8 curTypeNum = typeNum;
+
+			for (int i = 0; i < text.Length; i++)
+			{
+			    char8 c = text[i];
+			    mData.mText[i + index].mChar = (char8)c;
+#if INCLUDE_CHARDATA_CHARID
+			    mText[i + index].mCharId = mNextCharId;
+#endif                
+			    mData.mNextCharId++;
+			    mData.mText[i + index].mDisplayTypeId = curTypeNum;
+			    mData.mText[i + index].mDisplayFlags = flags;
+			    if (c.IsWhiteSpace)
+			        curTypeNum = 0;
+			}
+		}
+
+		public virtual void GetInsertFlags(int index, ref uint8 typeId, ref uint8 flags)
+		{
+			// Leave it
+		}
+
+        public virtual void InsertText(int index, String text)
+        {
+			scope AutoBeefPerf("EWC.InsertText");
+            int wantLen = mData.mTextLength + text.Length + 1;
+            while (wantLen > mData.mText.Count)
+            {
+				scope AutoBeefPerf("EWC.InsertText:Resize");
+
+                CharData[] oldText = mData.mText;
+				int allocSize = Math.Max(mData.mText.Count * 3 / 2, wantLen);
+                mData.mText = new CharData[allocSize];
+				oldText.CopyTo(mData.mText, 0, 0, mData.mTextLength);
+				delete oldText;
+            }
+
+            mData.mText.CopyTo(mData.mText, index, index + text.Length, mData.mTextLength - index);
+
+            int32 curId = mData.mNextCharId;
+            mData.mTextIdData.Insert(index, (int32)text.Length, ref curId);
+            
+            // We break attribute copying on spaces and revert back to zero
+            uint8 curTypeNum = 0;
+            uint8 curFlags = mInsertDisplayFlags;
+            GetInsertFlags(index, ref curTypeNum, ref curFlags);
+
+			ApplyTextFlags(index, text, curTypeNum, curFlags);
+
+            mData.mTextLength += (int32)text.Length;
+            mData.mCurTextVersionId++;
+			TextChanged();
+
+            VerifyTextIds();
+
+			for (var user in mData.mUsers)
+			{
+				if (user != this)
+				{
+					user.AdjustCursorsAfterExternalEdit(index, (int32)text.Length);
+					user.ContentChanged();
+				}
+			}
+        }
+    
+
+        public virtual void RemoveText(int index, int length)
+        {
+			for (var user in mData.mUsers)
+			{
+				if (user != this)
+				{
+					user.AdjustCursorsAfterExternalEdit(index, -length);
+					user.ContentChanged();
+				}
+			}
+
+            if (length == 0)
+                return;
+			mData.mText.CopyTo(mData.mText, index + length, index, mData.mTextLength - (index + length));
+            mData.mTextIdData.Remove(index, length);
+            mData.mTextLength -= (int32)length;
+
+			mData.mCurTextVersionId++;
+			TextChanged();
+
+            VerifyTextIds();			
+        }
+
+        /*public void PhysInsertAtCursor(String theString, bool moveCursor = true)
+        {            
+            int textPos = CursorTextPos;
+
+			bool atEnd = textPos == mData.mTextLength;
+
+            String str = theString;
+            int strStartIdx = 0;
+            while (true)
+            {
+                int crPos = str.IndexOf('\n', strStartIdx);
+                if (crPos == -1)
+                {
+                    if (strStartIdx != 0)
+						str = stack String(theString, strStartIdx);
+                    break;
+                }
+
+                if (mIsMultiline)
+                {
+                    //string lineString = aString.Substring(strStartIdx, crPos - strStartIdx + 1);
+					String lineString = scope String();
+					str.Substring(strStartIdx, crPos - strStartIdx + 1, lineString);
+                    strStartIdx = crPos + 1;                    
+
+                    InsertText((int32)textPos, lineString);
+                    textPos += lineString.Length;
+                }
+                else
+                {
+                    // Only allow the first line in
+                    //aString = aString.Substring(0, crPos);
+					//aString.RemoveToEnd(crPos);
+
+					str = stack String(theString, 0, crPos);
+                }
+            } 
+
+           	InsertText((int32)textPos, str);
+
+			//if (atEnd)
+				//ContentAppended(str);
+			//else
+				ContentChanged();
+            if (moveCursor)
+            {
+                textPos += str.Length;
+                MoveCursorToIdx((int32)textPos);
+                if (mEnsureCursorVisibleOnModify)
+                    EnsureCursorVisible();
+            }
+        }*/
+
+		public void PhysInsertAtCursor(String text, bool moveCursor = true)
+		{
+		    int textPos = CursorTextPos;
+
+			bool atEnd = textPos == mData.mTextLength;
+
+		    String insertStr = text;
+			if (!mIsMultiline)
+			{
+				int crPos = insertStr.IndexOf('\n');
+				if (crPos != -1)
+					insertStr = scope:: String(text, 0, crPos);
+			}
+
+		   	InsertText((int32)textPos, insertStr);
+
+			if (atEnd)
+				TextAppended(insertStr);
+			else
+				ContentChanged();
+		    if (moveCursor)
+		    {
+		        textPos += insertStr.Length;
+		        MoveCursorToIdx((int32)textPos);
+		        if (mEnsureCursorVisibleOnModify)
+		            EnsureCursorVisible();
+		    }
+		}
+
+        public void AppendText(String text)
+        {
+			scope AutoBeefPerf("EWC.AppendText");
+
+			if (mAllowVirtualCursor)
+			{
+				let prevCursorPos = CursorLineAndColumn;
+				CursorTextPos = mData.mTextLength;
+				PhysInsertAtCursor(text);
+				CursorLineAndColumn = prevCursorPos;
+			}
+			else
+			{
+				int prevCursorPos = CursorTextPos;
+				CursorTextPos = mData.mTextLength;
+				PhysInsertAtCursor(text);
+				CursorTextPos = prevCursorPos;
+			}
+        }
+
+		public enum InsertFlags
+		{
+			None,
+			NoRestoreSelectionOnUndo = 1,
+			NoMoveCursor = 2,
+			IsGroupPart = 4,
+			IsGroupStart = 8
+		}
+
+        public virtual void InsertAtCursor(String theString, InsertFlags insertFlags = .None)
+        {
+			scope AutoBeefPerf("EWC.InsertAtCursor");
+
+			
+		    
+			//String insertText = scope String();
+
+			String insertString = theString;
+
+			bool doCheck = true;
+
+			//
+			{
+				scope AutoBeefPerf("FilterCheck");
+
+				if (theString.Length > 32*1024)
+				{
+					bool[128] hasChar = default;
+
+					doCheck = false;
+					for (char32 c in theString.DecodedChars)
+					{
+						if (c <= (char32)128)
+							hasChar[(int)c] = true;
+						else if (!AllowChar(c))
+						{
+							doCheck = true;
+							break;
+						}
+					}
+
+					if (!doCheck)
+					{
+						for (int i < 128)
+						{
+							if ((hasChar[i]) && (!AllowChar((char32)i)))
+							{
+								doCheck = true;
+								break;
+							}
+						}
+					}
+				}
+
+				if (doCheck)
+				{
+					//TODO: Make this use DecodedChars
+				    for (int32 i = 0; i < insertString.Length; i++)
+				    {
+				        if (!AllowChar(insertString[i]))
+				        {
+							if (insertString == (Object)theString)
+							{
+								insertString = scope:: String();
+								insertString.Set(theString);
+							}
+
+				            insertString.Remove(i, 1);
+				            i--;
+				        }
+				    }
+				}
+			}
+
+			Debug.Assert(!theString.StartsWith("\x06"));
+
+            if ((!HasSelection()) && (mVirtualCursorPos.HasValue) && (theString != "\n"))
+            {
+                int textPos;
+                TryGetCursorTextPos(out textPos);
+
+                int line;
+                int lineChar;
+                GetLineCharAtIdx(textPos, out line, out lineChar);
+
+                float textX;
+                float textY;
+                GetTextCoordAtLineChar(line, lineChar, out textX, out textY);
+
+                float cursorX;
+                float cursorY;
+                GetTextCoordAtLineAndColumn(mVirtualCursorPos.Value.mLine, mVirtualCursorPos.Value.mColumn, out cursorX, out cursorY);
+
+				if (cursorX > textX)
+				{
+					int lineStart;
+					int lineEnd;
+
+					GetLinePosition(line, out lineStart, out lineEnd);
+					if (textPos == lineEnd)
+					{
+		                int prevTabCount = (int)((textX + 0.5f) / mTabSize);
+		                int wantTabCount = (int)((cursorX + 0.5f) / mTabSize);
+		                int wantAddTabs = wantTabCount - prevTabCount;                                
+		                if (wantAddTabs > 0)                
+		                    textX = wantTabCount * mTabSize + mTextInsets.mLeft;                
+
+		                int wantSpaces = (int)((cursorX - textX + 0.5f) / mCharWidth);
+		                //theString = new string(' ', wantSpaces) + theString;
+
+						let prevString = insertString;
+						insertString = scope:: String();
+
+						insertString.Append('\t', wantAddTabs);
+						insertString.Append(' ', wantSpaces);
+						insertString.Append(prevString);
+					}
+				}
+                //if (wantAddTabs > 0)
+                    //theString = new string('\t', wantAddTabs) + theString;
+            }
+		    
+			if ((insertString.Length == 0) && (!HasSelection()))
+                return;
+
+			if (WantsUndo)
+            {
+				var action = new InsertTextAction(this, insertString, insertFlags);
+				action.mMoveCursor = !insertFlags.HasFlag(.NoMoveCursor);
+				mData.mUndoManager.Add(action);
+			}
+
+            if (HasSelection())
+                PhysDeleteSelection(!insertFlags.HasFlag(.NoMoveCursor));
+
+            //StringBuilder insertText = new StringBuilder(theString);
+            /*for (int32 i = 0; i < insertText.Length; i++)
+            {
+                if (!AllowChar(insertText[i]))
+                {
+                    insertText.Remove(i, 1);
+                    i--;
+                }
+            }*/
+
+			/*int curIdx = 0;
+			for (var c in insertText.DecodedChars)
+			{
+				if (!AllowChar(c))
+				{
+				    insertText.Remove(curIdx, @c.NextIndex - curIdx);
+					@c.NextIndex = curIdx;
+				    continue;
+				}
+
+				curIdx = @c.NextIndex;
+			}*/
+
+            //theString = insertText.ToString();
+            PhysInsertAtCursor(insertString, !insertFlags.HasFlag(.NoMoveCursor));
+        }
+
+        public virtual void ReplaceAtCursor(char8 c)
+        {
+
+        }
+
+        public virtual void ClampCursor()
+        {
+            if (mVirtualCursorPos.HasValue)
+            {
+                var cursorPos = mVirtualCursorPos.Value;
+                cursorPos.mLine = Math.Min(GetLineCount() - 1, cursorPos.mLine);
+                mVirtualCursorPos = cursorPos;
+            }
+            mCursorTextPos = Math.Min(mCursorTextPos, mData.mTextLength);
+        }
+
+        public virtual void ContentChanged()
+        {
+            //TODO:
+            //mCursorPos = GetTextIdx(mCursorLine, mCursorCharIdx);
+			MarkDirty();
+
+            mContentChanged = true;
+			delete mData.mTextFlags;
+            mData.mTextFlags = null;
+			delete mData.mLineStarts;
+            mData.mLineStarts = null;
+            mData.mCurTextVersionId++;
+			mDragSelectionUnion = null;
+
+            mEditWidget.ContentChanged();
+
+			TextChanged();
+        }
+
+		public virtual void TextAppended(String str)
+		{
+			if ((mWordWrap) || (mData.mLineStarts == null))
+			{
+				ContentChanged();
+				return;
+			}
+
+			scope AutoBeefPerf("EWC.TextAppended");
+
+			int textIdx = mData.mTextLength - str.Length;
+
+			mData.mLineStarts.PopBack();
+			for (char8 c in str.RawChars)
+			{
+				textIdx++;
+				if (c == '\n')
+				{
+					mData.mLineStarts.Add((int32)textIdx);
+				}
+			}
+			mData.mLineStarts.Add(mData.mTextLength);
+
+			mContentChanged = true;
+
+			mData.mCurTextVersionId++;
+			mDragSelectionUnion = null;
+
+			//Debugging
+			//ContentChanged();
+			//GetTextData();
+
+			mEditWidget.ContentChanged();
+		}
+
+        //Thread mCheckThread;
+
+        public virtual void GetTextData()
+        {
+            // Generate line starts and text flags if we need to
+            
+            if (mData.mLineStarts == null)
+            {
+				scope AutoBeefPerf("EWC.GetTextData");
+
+				CharData* char8DataPtr = mData.mText.CArray();
+				uint8* textFlagsPtr = null;
+				if (mData.mTextFlags != null)
+					textFlagsPtr = mData.mTextFlags.CArray();
+
+                int32 lineIdx = 0;
+				if (textFlagsPtr != null)
+				{
+					for (int32 i < mData.mTextLength)
+					{
+					    if ((char8DataPtr[i].mChar == '\n') || ((textFlagsPtr != null) && ((textFlagsPtr[i] & ((int32)TextFlags.Wrap)) != 0)))
+					        lineIdx++;                    
+					}
+				}
+                else
+				{
+					for (int32 i < mData.mTextLength)
+					{
+					    if (char8DataPtr[i].mChar == '\n')
+					        lineIdx++;                    
+					}
+				}
+
+                mData.mLineStarts = new List<int32>();
+                mData.mLineStarts.GrowUnitialized(lineIdx + 2);
+				int32* lineStartsPtr = mData.mLineStarts.Ptr;
+				lineStartsPtr[0] = 0;
+                
+                lineIdx = 0;
+				if (textFlagsPtr != null)
+				{
+	                for (int32 i < mData.mTextLength)
+	                {
+	                    if ((textFlagsPtr != null) && ((textFlagsPtr[i] & ((int32)TextFlags.Wrap)) != 0))
+	                    {                        
+	                        lineIdx++;
+	                        lineStartsPtr[lineIdx] = i;                        
+	                    }
+	                    else if ((char8)char8DataPtr[i].mChar == '\n')
+	                    {                        
+	                        lineIdx++;
+	                        lineStartsPtr[lineIdx] = i + 1;
+	                    }
+	                }
+				}
+				else
+				{
+					for (int32 i < mData.mTextLength)
+					{
+					    if ((char8)char8DataPtr[i].mChar == '\n')
+					    {                        
+					        lineIdx++;
+					        lineStartsPtr[lineIdx] = i + 1;
+					    }
+					}
+				}
+                
+                mData.mLineStarts[lineIdx + 1] = mData.mTextLength;
+            }
+        }
+
+        public virtual void Backspace()
+        {
+            if (mJustInsertedCharPair)
+            {
+                CursorTextPos++;
+                mJustInsertedCharPair = false;
+            }
+
+            if (HasSelection())
+            {
+                DeleteSelection();
+                return;
+            }
+            mSelection = null;
+
+            int textPos = 0;
+            if (!TryGetCursorTextPos(out textPos))
+            {
+                var lineAndColumn = CursorLineAndColumn;
+                mCursorBlinkTicks = 0;
+                CursorLineAndColumn = LineAndColumn(lineAndColumn.mLine, lineAndColumn.mColumn - 1);
+                CursorMoved();
+                return;
+            }
+
+            if (textPos == 0)
+                return;
+
+            int32 removeNum = -1;
+            if (mData.mText[CursorTextPos - 1].mChar == '\n')
+            {
+                // Pulling up line - delete trailing whitespace if the line has no content
+                int lineIdx = 0;
+                int lineCharIdx = 0;
+                GetLineCharAtIdx(CursorTextPos, out lineIdx, out lineCharIdx);
+                String lineText = scope String();
+                GetLineText(lineIdx, lineText);
+				String trimmedLineText = scope String(lineText);
+				trimmedLineText.Trim();
+
+                if ((lineText.Length > 0) && (trimmedLineText.Length == 0))
+                {
+                    CursorTextPos += (int32)lineText.Length;
+                    removeNum = (int32)-lineText.Length - 1;
+                }
+            }
+
+			// Roll back past UTF8 data if necessary
+			while (true)
+			{
+				char8 c = mData.SafeGetChar(mCursorTextPos + removeNum);
+				if ((uint8)c & 0xC0 != 0x80)
+					break;
+				removeNum--;
+			}
+
+            mData.mUndoManager.Add(new DeleteCharAction(this, removeNum, -removeNum));
+            PhysDeleteChars(removeNum, -removeNum);
+        }
+        
+        public virtual void PhysDeleteChars(int32 offset, int32 length = 1)
+        {
+            int textPos = CursorTextPos;
+
+            RemoveText(textPos + offset, length);            
+            textPos += offset;
+            ContentChanged();
+            if (offset != 0)
+            {
+                MoveCursorToIdx(textPos);
+                EnsureCursorVisible();
+            }
+        }
+
+        public virtual void ClearLine()
+        {
+            int line;
+            int lineChar;
+            GetCursorLineChar(out line, out lineChar);
+
+            String lineText = scope String();
+            GetLineText(line, lineText);
+
+            var prevCursorLineAndCol = CursorLineAndColumn;
+
+            mSelection = EditSelection();
+            mSelection.ValueRef.mStartPos = (int32)(CursorTextPos - lineChar);
+            mSelection.ValueRef.mEndPos = mSelection.Value.mStartPos + (int32)lineText.Length;
+            DeleteSelection();
+
+            CursorLineAndColumn = prevCursorLineAndCol;
+        }
+
+        public virtual void DeleteLine()
+        {
+            int line;
+            int lineChar;
+            GetCursorLineChar(out line, out lineChar);
+
+            String lineText = scope String();
+            GetLineText(line, lineText);
+
+            var prevCursorLineAndCol = CursorLineAndColumn;
+
+            mSelection = EditSelection();
+            mSelection.ValueRef.mStartPos = (int32)(CursorTextPos - lineChar);
+            mSelection.ValueRef.mEndPos = mSelection.ValueRef.mStartPos + (int32)lineText.Length + 1;
+            DeleteSelection();
+
+            CursorLineAndColumn = prevCursorLineAndCol;
+        }
+        
+        public virtual void DeleteChar()
+        {
+            if (HasSelection())
+            {
+                DeleteSelection();
+                return;
+            }                        
+            mSelection = null;
+
+            int textPos = CursorTextPos;
+            if (textPos >= mData.mTextLength)
+                return;
+
+            int line;
+            int lineChar;
+            GetLineCharAtIdx(textPos, out line, out lineChar);
+
+            bool wasLineEnd = mData.mText[textPos].mChar == '\n';
+            String lineText = scope String();
+            GetLineText(line, lineText);
+
+            if (wasLineEnd)
+            {
+                if (lineText.IsWhiteSpace)
+                {                    
+                    DeleteLine();
+  
+                    GetLineCharAtIdx(CursorTextPos, out line, out lineChar);
+
+					lineText.Clear();
+                    GetLineText(line, lineText);
+                    int32 contentIdx;
+                    for (contentIdx = 0; contentIdx < lineText.Length; contentIdx++)
+                    {
+                        if (!lineText[contentIdx].IsWhiteSpace)
+                        {
+                            CursorTextPos = GetTextIdx(line, contentIdx);
+                            break;
+                        }
+                    }
+                    
+                    return;
+                }
+
+				UndoBatchStart undoBatchStart = new UndoBatchStart("PullUpLine");
+				mData.mUndoManager.Add(undoBatchStart);
+
+				// Add in any required virtual spaces
+				InsertAtCursor("");
+				textPos = CursorTextPos;
+
+                while ((textPos < mData.mTextLength) && (lineChar != 0))
+                {
+                    mData.mUndoManager.Add(new DeleteCharAction(this, 0, 1));
+                    PhysDeleteChars(0);
+
+                    char8 c = (char8)mData.mText[textPos].mChar;
+                    if ((c == '\n') || (!c.IsWhiteSpace))
+                        break;                    
+                }
+
+				mData.mUndoManager.Add(undoBatchStart.mBatchEnd);
+            }
+            else
+            {
+				int32 char8Count = 1;
+				int checkIdx = textPos + 1;
+				while (true)
+				{
+					char8 c = mData.SafeGetChar(checkIdx);
+					if (((uint8)c & 0xC0) != 0x80)
+					{
+						// Delete all the combining marks after the char8
+						var checkChar = mData.GetChar32(checkIdx);
+						if (!checkChar.IsCombiningMark)
+							break;
+					}
+					char8Count++;
+					checkIdx++;
+				}
+
+                mData.mUndoManager.Add(new DeleteCharAction(this, 0, char8Count));
+                PhysDeleteChars(0, char8Count);
+            }                        
+        }
+
+        public virtual bool AllowChar(char32 c)
+        {
+			if ((c == '\t') && (!mIsMultiline))
+				return false;
+            return true;
+        }
+        
+        public void InsertCharPair(String char8Pair)
+        {
+            InsertAtCursor(char8Pair);
+            MoveCursorToIdx(CursorTextPos - 1);            
+            mJustInsertedCharPair = true;
+        }
+
+        public virtual bool WantsInsertCharPair(char8 theChar)
+        {            
+            return false;
+        }
+
+		public virtual int32 GetTabSpaceCount()
+		{
+		    return (int32)Math.Round(mTabSize / mCharWidth);
+		}
+
+        public override void KeyChar(char32 theChar)
+        {
+            base.KeyChar(theChar);
+			char32 useChar = theChar;
+
+            mCursorBlinkTicks = 0;			
+
+            if (useChar == '\b')
+            {
+                if (!CheckReadOnly())
+                {
+					if (HasSelection())
+					{
+						DeleteSelection();
+						return;
+					}
+
+                    int32 column = CursorLineAndColumn.mColumn;
+                    int32 tabSpaceCount = GetTabSpaceCount();
+                    if ((mAllowVirtualCursor) && (column != 0) && ((column % tabSpaceCount) == 0))
+                    {
+                        int lineStart;
+                        int lineEnd;
+                        GetLinePosition(CursorLineAndColumn.mLine, out lineStart, out lineEnd);
+
+                        bool doBlockUnindent = false;
+                        int cursorTextPos = 0;
+                        float overflowX = 0;
+                        if (!TryGetCursorTextPos(out cursorTextPos, out overflowX))
+                        {
+                            cursorTextPos += (int32)Math.Round(overflowX / mCharWidth);
+
+                            for (int32 i = 0; i < tabSpaceCount; i++)
+                            {
+                                if (cursorTextPos - 1 < lineEnd)
+                                {
+                                    if (!((char8)mData.mText[cursorTextPos - 1].mChar).IsWhiteSpace)
+                                        break;
+                                    Backspace();
+                                }
+                                else
+                                {                                    
+                                    var virtualCursorPos = CursorLineAndColumn;
+                                    virtualCursorPos.mColumn--;
+                                    CursorLineAndColumn = virtualCursorPos;
+                                }
+                                cursorTextPos--;
+                            }
+
+                            return;
+                        }
+                        else if (cursorTextPos > 0)
+                        {
+                            Debug.Assert((cursorTextPos >= lineStart) && (cursorTextPos <= lineEnd));
+                            if (((char8)mData.mText[cursorTextPos - 1].mChar).IsWhiteSpace)
+                            {
+                                doBlockUnindent = true;
+                                // Cursor has to be at line end or only have whitespace infront of it (line start)
+                                if (cursorTextPos < lineEnd)
+                                {
+                                    for (int i = lineStart; i < cursorTextPos; i++)
+                                    {
+                                        if (!((char8)mData.mText[i].mChar).IsWhiteSpace)
+                                        {
+                                            doBlockUnindent = false;
+                                            break;
+                                        }
+                                    }
+                                }                                    
+                            }                            
+                        }
+
+                        if (doBlockUnindent)
+                        {
+                            BlockIndentSelection(true);
+                            return;
+                        }
+                    }
+
+                    Backspace();
+                }
+                return;
+            }
+
+            mJustInsertedCharPair = false;
+
+            switch (useChar)
+            {
+            case '\r':
+                useChar = '\n';
+                break; 
+            case '\t':
+				if (AllowChar(useChar))
+                	BlockIndentSelection(mWidgetWindow.IsKeyDown(KeyCode.Shift));
+                return;
+            }            
+            
+            if (useChar == '\n')
+            {
+                if ((!mIsMultiline) || (mWidgetWindow.IsKeyDown(KeyCode.Control)))
+                {
+					// Submit is handled in KeyDown
+                    //mEditWidget.Submit();
+                    return;
+                }
+            }                        
+
+            if ((AllowChar(useChar)) && (!CheckReadOnly()))
+            {                
+                if ((useChar == '\n') && (!mAllowVirtualCursor))
+                {              
+                    int lineIdx;
+                    int lineCharIdx;
+                    GetLineCharAtIdx(CursorTextPos, out lineIdx, out lineCharIdx);                    
+
+                    // Duplicate the tab start from the previous line
+                    if (lineIdx > 0)
+                    {
+                        int32 indentCount = 0;
+                        String aLine = scope String();
+                        GetLineText(lineIdx, aLine);
+                        for (int32 i = 0; i < Math.Min(aLine.Length, lineCharIdx); i++)
+                        {
+                            if (aLine[i] != '\t')
+                                break;
+                            indentCount++;                            
+                        }
+                        aLine.Trim();
+                        
+                        UndoBatchStart undoBatchStart = new UndoBatchStart("newline");
+                        mData.mUndoManager.Add(undoBatchStart);
+
+                        if (aLine.Length == 0)
+                        {
+                            // Remove previous line's tabs (since that line was blank)
+                            for (int32 i = 0; i < indentCount; i++)
+                                Backspace();
+                        }
+
+                        InsertAtCursor(ToStackString!(useChar));
+                        for (int32 i = 0; i < indentCount; i++)
+                            InsertAtCursor("\t");                        
+                        if (aLine.Length > 0)
+                        {
+                            char8 c = aLine[aLine.Length - 1];
+                            if ((c == '(') || (c == '[') || (c == '{'))
+                                InsertAtCursor("\t");
+                        }
+
+						if (WantsUndo)
+                        	mData.mUndoManager.Add(undoBatchStart.mBatchEnd);
+                        return;
+                    }                                                                  
+                }
+
+                bool restoreSelectionOnUndo = true;
+                if ((mOverTypeMode) && (!HasSelection()))
+                {
+                    int cursorPos = CursorTextPos;
+                    if (cursorPos < mData.mTextLength)
+                    {
+                        char8 c = (char8)mData.mText[cursorPos].mChar;
+                        if ((c != '\n') && (c != '\r'))
+                        {                            
+                            mSelection = EditSelection(cursorPos, cursorPos + 1);
+                            restoreSelectionOnUndo = false;
+                        }
+                    }
+                }
+
+				if ((useChar >= '\x20') || (useChar == '\n'))
+                {
+					InsertAtCursor(ToStackString!(useChar), restoreSelectionOnUndo ? .None : .NoRestoreSelectionOnUndo);
+				}
+            }
+
+			mCursorImplicitlyMoved = true;
+        }
+
+        public virtual float GetPageScrollTextHeight()
+        {
+            return mEditWidget.mScrollContentContainer.mHeight;
+        }
+
+        public virtual float AdjustPageScrollPos(float newY, int dir)
+        {
+            return Math.Max(0, Math.Min(newY, mHeight - mEditWidget.mScrollContentContainer.mHeight - mMaximalScrollAddedHeight));
+        }
+
+        int32 GetCharType(char8 theChar)
+        {
+            if (theChar == '\n')
+                return 2;
+            if (theChar.IsWhiteSpace)
+                return 0;
+            if (IsNonBreakingChar(theChar))
+                return 1;
+
+            // We break on each instance of these
+            switch (theChar)
+            {
+            case '<', '>', '(', ')', '[', ']', '{', '}':
+                return 3;
+            }
+            return 4;
+        }
+
+        public void GetTextCoordAtCursor(out float x, out float y)
+        {
+            if (mVirtualCursorPos.HasValue)
+            {
+                GetTextCoordAtLineAndColumn(mVirtualCursorPos.Value.mLine, mVirtualCursorPos.Value.mColumn, out x, out y);
+            }
+            else
+            {
+                int line;
+                int lineChar;
+                GetLineCharAtIdx(mCursorTextPos, out line, out lineChar);
+                GetTextCoordAtLineChar(line, lineChar, out x, out y);
+            }
+        }
+
+        public bool GetCursorLineChar(out int line, out int lineChar)
+        {
+            if (mCursorTextPos != -1)
+            {
+                GetLineCharAtIdx_Fast(mCursorTextPos, true, out line, out lineChar);
+                return true;
+            }
+
+            line = mVirtualCursorPos.Value.mLine;
+
+            float x;
+            float y;
+            GetTextCoordAtLineAndColumn(mVirtualCursorPos.Value.mLine, mVirtualCursorPos.Value.mColumn, out x, out y);
+
+            float overflowX;
+            return GetLineCharAtCoord(x, y, out line, out lineChar, out overflowX);
+        }
+
+        public virtual bool PrepareForCursorMove(int dir = 0)
+        {
+            if (mWidgetWindow.IsKeyDown(KeyCode.Shift))
+                return false;
+            if ((mSelection == null) || (!mSelection.Value.HasSelection))
+                return false;
+
+            if (dir < 0)
+                CursorTextPos = mSelection.Value.MinPos;
+            else if (dir > 0)
+                CursorTextPos = mSelection.Value.MaxPos;
+            mSelection = null;
+            return true;
+        }
+        
+        public virtual void Undo()
+        {
+			scope AutoBeefPerf("EWC.Undo");
+
+			//Profiler.StartSampling();
+			if (WantsUndo)
+            	mData.mUndoManager.Undo();
+			//Profiler.StopSampling();
+        }
+
+        public virtual void Redo()
+        {
+			scope AutoBeefPerf("EWC.Redo");
+
+			if (WantsUndo)
+            	mData.mUndoManager.Redo();
+        }
+
+		public virtual void PasteText(String text)
+		{
+		    InsertAtCursor(text);
+		}
+
+        public override void KeyDown(KeyCode keyCode, bool isRepeat)
+        {
+            base.KeyDown(keyCode, isRepeat);
+
+            if (keyCode == KeyCode.Escape)
+            {
+                mEditWidget.Cancel();
+                if (mWidgetWindow == null)
+                    return;
+            }
+
+            int lineIdx; 
+            int lineChar;
+            GetCursorLineChar(out lineIdx, out lineChar);
+
+            bool wasMoveKey = false;            
+            int prevCursorPos;
+            bool gotCursorPos = TryGetCursorTextPos(out prevCursorPos);
+
+            if (mWidgetWindow.GetKeyFlags() == KeyFlags.Ctrl)
+            {
+                switch (keyCode)
+                {
+                case (KeyCode)'A':
+                    SelectAll();
+                    break;
+                case (KeyCode)'C':
+					String selText = scope String();
+					GetSelectionText(selText);
+					if (!selText.IsWhiteSpace)
+                    	BFApp.sApp.SetClipboardText(selText);
+                    break;
+                case (KeyCode)'X':
+                    if (!CheckReadOnly())
+                    {
+						String selText = scope String();
+						GetSelectionText(selText);
+						if (!selText.IsWhiteSpace)
+						{
+	                        BFApp.sApp.SetClipboardText(selText);
+	                        DeleteSelection();
+						}
+                    }
+                    break;
+                case (KeyCode)'V':
+                    String aText = scope String();
+                	BFApp.sApp.GetClipboardText(aText);
+                    aText.Replace("\r", "");
+                    if ((aText != null) && (!CheckReadOnly()))
+                        PasteText(aText);
+                    break;
+                case (KeyCode)'Z':
+                    Undo();
+                    break;
+                case (KeyCode)'Y':
+                    Redo();
+                    break;
+				case .Return:
+					if (mIsMultiline)
+						mEditWidget.Submit();
+				default:
+                }
+            }
+
+            switch (keyCode)
+            {
+			case .Return:
+				if (!mIsMultiline)
+					mEditWidget.Submit();
+            case KeyCode.Left:
+                {
+                    if (!PrepareForCursorMove(-1))                        
+                    {
+                        PrepareForCursorMove(-1);
+
+						if (mAllowVirtualCursor)
+						{
+							bool doVirtualMove = true;
+
+							if ((mWidgetWindow.IsKeyDown(KeyCode.Shift)) || (mWidgetWindow.IsKeyDown(KeyCode.Control)))
+								doVirtualMove = false;
+							else
+							{
+								int lineStart;
+								int lineEnd;
+								GetLinePosition(lineIdx, out lineStart, out lineEnd);
+								
+								//if (gotCursorPos)
+								if (gotCursorPos)
+									doVirtualMove = false;
+							}
+
+						    if (doVirtualMove)
+						    {
+						        var lineAndColumn = CursorLineAndColumn;
+								if (lineAndColumn.mColumn > 0)
+								{
+								    /*mSelection = null;
+								    mCursorBlinkTicks = 0;
+								    CursorLineAndColumn = LineAndColumn(lineAndColumn.mLine, lineAndColumn.mColumn - 1);
+								    EnsureCursorVisible(true, false, false);
+								    CursorMoved();*/
+
+									float x;
+									float y;
+									GetTextCoordAtLineAndColumn(lineAndColumn.mLine, lineAndColumn.mColumn - 1, out x, out y);
+									MoveCursorToCoord(x, y);
+
+								    break;
+								}
+						    }
+						}
+
+                        wasMoveKey = true;
+                        int anIndex = GetTextIdx(lineIdx, lineChar);
+                        int prevCharType = (anIndex > 0) ? GetCharType((char8)mData.mText[anIndex - 1].mChar) : -1;
+                        while (true)
+                        {
+                            if (lineChar > 0)
+                                MoveCursorTo(lineIdx, lineChar - 1);
+                            else if (lineIdx > 0)
+                            {
+                                int cursorIdx = mCursorTextPos;
+								String lineText = scope String();
+								GetLineText(lineIdx - 1, lineText);
+                                MoveCursorTo(lineIdx - 1, (int32)lineText.Length);
+                                if ((!mAllowVirtualCursor) && (cursorIdx == mCursorTextPos))
+                                    MoveCursorTo(lineIdx - 1, (int32)lineText.Length - 1);
+                                break;
+                            }
+
+                            if (!mWidgetWindow.IsKeyDown(KeyCode.Control))
+                                break;
+
+                            //mInner.mVal1 = inVal;
+                            //PrintF("TestStruct() %d\n", inVal);
+
+                            GetLineCharAtIdx(CursorTextPos, out lineIdx, out lineChar);
+                            anIndex = CursorTextPos;
+                            if (anIndex == 0)
+                                break;
+
+                            char8 aChar = (char8)mData.mText[anIndex - 1].mChar;
+                            int32 char8Type = GetCharType(aChar);
+                            if (prevCharType == 3)
+                                break;
+                            if (char8Type != prevCharType)
+                            {
+                                if ((char8Type == 0) && (prevCharType != 0))
+                                    break;
+                                if ((prevCharType == 2) || (prevCharType == 1) || (prevCharType == 4))
+                                    break;
+                            }
+
+                            prevCharType = char8Type;
+                        }
+                    }
+                }
+                break;
+            case KeyCode.Right:
+                {                        
+                    if (!PrepareForCursorMove(1))
+                    {
+						if (mAllowVirtualCursor)
+						{
+							bool doVirtualMove = true;
+
+							if ((mWidgetWindow.IsKeyDown(KeyCode.Shift)) || (mWidgetWindow.IsKeyDown(KeyCode.Control)))
+								doVirtualMove = false;
+							else
+							{
+								int lineStart;
+								int lineEnd;
+								GetLinePosition(lineIdx, out lineStart, out lineEnd);
+
+								if (prevCursorPos < lineEnd)
+									doVirtualMove = false;
+							}
+
+	                        if (doVirtualMove)
+	                        {
+	                            mCursorBlinkTicks = 0;
+	                            mSelection = null;
+	                            var lineAndColumn = CursorLineAndColumn;
+	                            CursorLineAndColumn = LineAndColumn(lineAndColumn.mLine, lineAndColumn.mColumn + 1);
+	                            EnsureCursorVisible(true, false, false);
+	                            CursorMoved();
+	                            break;
+	                        }
+						}
+
+                        wasMoveKey = true;
+                        int anIndex = GetTextIdx(lineIdx, lineChar);
+                        int prevCharType = (anIndex < mData.mTextLength) ? GetCharType((char8)mData.mText[anIndex].mChar) : -1;
+                        while (true)
+                        {
+                            int lineStart;
+                            int lineEnd;
+                            GetLinePosition(lineIdx, out lineStart, out lineEnd);
+                            int lineLen = lineEnd - lineStart;
+
+                            int nextLineChar = lineChar + 1;
+                            bool isWithinLine = nextLineChar < lineLen;
+                            if (nextLineChar == lineLen)
+                            {
+                                GetTextData();
+                                if ((mData.mTextFlags == null) || ((mData.mTextFlags[lineEnd] & (int32)TextFlags.Wrap) == 0))
+                                {
+                                    isWithinLine = true;
+                                }
+                            }
+
+                            if (isWithinLine)
+                                MoveCursorTo(lineIdx, lineChar + 1, false, 1);
+                            else if (lineIdx < GetLineCount() - 1)
+                                MoveCursorTo(lineIdx + 1, 0);
+
+                            if (!mWidgetWindow.IsKeyDown(KeyCode.Control))
+                                break;
+
+                            GetLineCharAtIdx(CursorTextPos, out lineIdx, out lineChar);
+                            anIndex = GetTextIdx(lineIdx, lineChar);
+                            if (anIndex == mData.mTextLength)
+                                break;
+
+                            char8 aChar = (char8)mData.mText[anIndex].mChar;
+                            int32 char8Type = GetCharType(aChar);
+                            if (char8Type == 3)
+                                break;
+                            if (char8Type != prevCharType)
+                            {
+                                if ((char8Type != 0) && (prevCharType == 0))
+                                    break;
+                                if ((char8Type == 2) || (char8Type == 1) || (char8Type == 4))
+                                    break;
+                            }
+
+                            prevCharType = char8Type;
+                        }
+                    }
+                }
+                break;
+            case KeyCode.Up:
+				fallthrough;
+            case KeyCode.Down:
+                {                        
+                    int32 aDir = (keyCode == KeyCode.Up) ? -1 : 1;
+                    bool didSelectionMove = PrepareForCursorMove(aDir);
+                    
+                    GetCursorLineChar(out lineIdx, out lineChar);
+
+                    if (mWidgetWindow.IsKeyDown(KeyCode.Control))
+                    {
+                        //mEditWidget.Resize(mEditWidget.mX, mEditWidget.mY, mEditWidget.mWidth, mEditWidget.mHeight + aDir);
+
+                        mEditWidget.VertScrollTo(mEditWidget.mVertPos.mDest + aDir * mEditWidget.mScrollContentContainer.mHeight * 0.25f);
+                        EnsureCursorVisible(false);
+                        return;
+                    }
+
+                    wasMoveKey = true;
+                    if ((lineIdx + aDir >= 0) && (lineIdx + aDir < GetLineCount()))
+                    {
+						if (mAllowVirtualCursor)
+						{
+							float cursorX;
+							float cursorY;
+							GetTextCoordAtCursor(out cursorX, out cursorY);
+							mCursorWantX = cursorX;
+						}
+
+                        //if (!mAllowVirtualCursor)
+                        {
+                            lineIdx += aDir;
+
+                            float wantedX = mCursorWantX;
+
+                            float aX;
+                            float aY;
+                            GetTextCoordAtLineChar(lineIdx, 0, out aX, out aY);
+                            MoveCursorToCoord(mCursorWantX, aY);
+
+                            // Restore old desired X
+                            mCursorWantX = wantedX;
+                        }
+                        /*else                            
+                        {                                
+                            mCursorBlinkTicks = 0;
+                            var prevLineAndColumn = CursorLineAndColumn;
+                            CursorLineAndColumn = LineAndColumn(Math.Max(0, prevLineAndColumn.mLine + aDir), prevLineAndColumn.mColumn);
+                            CursorMoved();
+                        }*/
+                    }
+
+                    if (didSelectionMove)                        
+                        CursorToLineStart(false);                        
+                }
+                break;
+            case KeyCode.Home:
+                PrepareForCursorMove(-1);
+                wasMoveKey = true;
+                if (mWidgetWindow.IsKeyDown(KeyCode.Control))
+                    CursorToStart();
+                else
+                    CursorToLineStart(true);
+				//mCursorImplicitlyMoved = true;
+                break;
+            case KeyCode.End:
+                PrepareForCursorMove(1);
+                wasMoveKey = true;
+                if (mWidgetWindow.IsKeyDown(KeyCode.Control))
+                {
+                    CursorToEnd();
+                }
+                else
+                {
+                    CursorToLineEnd();
+                    if ((!mAllowVirtualCursor) && (mWordWrap))
+                        mShowCursorAtLineEnd = true;
+                }
+                break;
+            case KeyCode.PageUp:
+				fallthrough;
+            case KeyCode.PageDown:
+                {                        
+                    if (!mIsMultiline)
+                        break;
+
+                    wasMoveKey = true;
+                    int32 aDir = (keyCode == KeyCode.PageUp) ? -1 : 1;
+                    PrepareForCursorMove(aDir);
+
+                    float cursorX;
+                    float cursorY;
+
+                    if (mAllowVirtualCursor)
+                    {
+                        var lineAndCol = CursorLineAndColumn;
+                        GetTextCoordAtLineAndColumn(lineAndCol.mLine, lineAndCol.mColumn, out cursorX, out cursorY);
+                        mCursorWantX = cursorX;
+                    }
+                    else
+                    {
+                        GetTextCoordAtLineChar(lineIdx, lineChar, out cursorX, out cursorY);
+                    }
+                    
+                    cursorY -= (float)mEditWidget.mVertPos.mDest;                        
+                    float pageScrollTextSize = GetPageScrollTextHeight();
+                    
+                    float adjustY = pageScrollTextSize * aDir;
+                    float scrollToVal = (float)(mEditWidget.mVertPos.mDest + adjustY);
+                    
+                    float newScrollToVal = AdjustPageScrollPos(scrollToVal, aDir);
+                    cursorY += scrollToVal - newScrollToVal;
+                    scrollToVal = newScrollToVal;
+
+                    if (mEditWidget.VertScrollTo(scrollToVal))
+                    {
+                        cursorY += (float)mEditWidget.mVertPos.mDest;
+                        MoveCursorToCoord(cursorX, cursorY);
+
+                        // Restore old desired X
+                        mCursorWantX = cursorX;
+                    }
+                    else
+                    {
+                        if (aDir == -1)
+                        {
+                            lineIdx = 0;
+                            CursorTextPos = 0;
+                        }
+                        else
+                        {
+                            lineIdx = GetLineCount() - 1;
+                            CursorTextPos = GetTextIdx(lineIdx, lineChar);
+                        }
+                                                   
+                        float wantedX = mCursorWantX;
+
+                        float aX;
+                        float aY;
+                        GetTextCoordAtLineChar(lineIdx, 0, out aX, out aY);
+                        MoveCursorToCoord(mCursorWantX, aY);
+
+                        // Restore old desired X
+                        mCursorWantX = wantedX;                            
+                    }                        
+                }
+                break;
+            case KeyCode.Insert:
+                mOverTypeMode = !mOverTypeMode;
+                break;
+            case KeyCode.Delete:
+                if (!CheckReadOnly())
+                    DeleteChar();
+				mCursorImplicitlyMoved = true;
+                break;                
+			default:
+            }
+
+            if (wasMoveKey)
+            {
+                if (mWidgetWindow.IsKeyDown(KeyCode.Shift))
+                {
+                    if (!HasSelection())
+                    {
+                        StartSelection();
+                        mSelection.ValueRef.mStartPos = (int32)prevCursorPos;                        
+                    }
+                    SelectToCursor();                
+                }
+                else
+                    mSelection = null;
+
+                EnsureCursorVisible();
+            }
+        }
+        
+        public float GetCursorScreenRelY()
+        {
+            float cursorX;
+            float cursorY;
+            GetTextCoordAtCursor(out cursorX, out cursorY);
+            return (float)(cursorY - mEditWidget.mVertScrollbar.mContentPos);
+        }
+
+        public void SetCursorScreenRelY(float scrollTopDelta)
+        {
+            float cursorX;
+            float cursorY;
+            GetTextCoordAtCursor(out cursorX, out cursorY);
+            mEditWidget.mVertScrollbar.ScrollTo(cursorY - scrollTopDelta);
+        }
+
+        public override bool Contains(float x, float y)
+        {
+            return true;
+        }
+
+        public override void Update()
+        {
+			Debug.Assert((mCursorTextPos != -1) || (mVirtualCursorPos != null));
+
+ 	         base.Update();
+             if (mContentChanged)
+                 RecalcSize();
+             mCursorBlinkTicks++;
+			if (mEditWidget.mHasFocus)
+				MarkDirty();
+        }        
+
+        public virtual void RecalcSize()
+        {            
+            mContentChanged = false;
+            mEditWidget.UpdateScrollbars();
+        }
+
+        public virtual void TextChanged()
+        {
+        }
+
+        // Returns true if we have text at that location
+        //  If we return 'false' then line/theChar are set to closest positions
+        public virtual bool GetLineCharAtCoord(float x, float y, out int line, out int theChar, out float overflowX)
+        {
+            line = -1;
+            theChar = -1;
+            overflowX = 0;
+            return false;
+        }
+
+        public virtual bool GetLineAndColumnAtCoord(float x, float y, out int line, out int column)
+        {
+            line = -1;
+            column = -1;
+            return false;
+        }
+
+		public void GetColumnAtLineChar(int line, int lineChar, out int column)
+        {
+			float x;
+			float y;
+			GetTextCoordAtLineChar(line, lineChar, out x, out y);
+
+			int line2;
+			GetLineAndColumnAtCoord(x, y, out line2, out column);
+		}
+
+		public virtual void GetLineCharAtIdx(int idx, out int line, out int theChar)
+		{
+			int lineA;
+			int char8A;
+			GetLineCharAtIdx_Fast(idx, false, out lineA, out char8A);
+
+			/*int32 lineB;
+			int32 char8B;
+			GetLineCharAtIdx_Slow(idx, out lineB, out char8B);
+
+			Debug.Assert((lineA == lineB) && (char8A == char8B));*/
+
+			line = lineA;
+			theChar = char8A;
+		}
+
+		public virtual void GetLineCharAtIdx_Fast(int idx, bool checkCursorAtLineEnd, out int line, out int theChar)
+		{
+			GetTextData();
+
+			//bool isCurCursor = true;
+
+			if (idx == mData.mTextLength)
+			{
+				int32 lastLine = GetLineCount() - 1;
+				line = (int32)lastLine;
+				theChar = idx - mData.mLineStarts[lastLine];
+				return;
+			}	
+
+			int lo = 0;
+			int hi = mData.mLineStarts.Count - 2;
+
+			if (idx < 0)
+			{
+				line = 0;
+				theChar = 0;
+			}
+
+			while (lo <= hi)
+			{
+			    int i = (lo + hi) / 2;
+				var midVal = mData.mLineStarts[i];
+				int c = midVal - idx;
+			    if (c == 0)
+                {
+					if ((checkCursorAtLineEnd) && (mShowCursorAtLineEnd) && (i > 0))
+					{
+						i--;
+						midVal = mData.mLineStarts[i];
+					}
+
+					line = (int32)i;
+					theChar = idx - midVal;
+					return;
+				}    
+			    if (c < 0)
+			        lo = i + 1;                    
+			    else
+			        hi = i - 1;
+			}
+
+			if (hi < GetLineCount())
+			{
+				line = (int32)hi;
+				theChar = idx - mData.mLineStarts[hi];
+			}
+			else
+			{
+				line = (int32)mData.mLineStarts.Count - 2;
+				theChar = mData.mLineStarts[mData.mLineStarts.Count - 1] - mData.mLineStarts[mData.mLineStarts.Count - 2];
+			}
+		}
+
+        public void GetLineCharAtIdx_Slow(int32 idx, out int32 line, out int32 theChar)
+        {            
+            GetTextData();
+
+            for (int32 lineIdx = 0; lineIdx < mData.mLineStarts.Count; lineIdx++)
+            {
+                int32 lineStart = mData.mLineStarts[lineIdx];
+
+                if ((idx < lineStart) || ((idx == lineStart) && (lineIdx > 0) && (mCursorTextPos == lineStart) && (mShowCursorAtLineEnd)))
+                {
+                    line = lineIdx - 1;
+                    theChar = idx - mData.mLineStarts[lineIdx - 1];
+                    return;
+                }
+            }
+
+            if (idx < 0)
+            {
+                line = 0;
+                theChar = 0;
+            }
+            else
+            {
+                line = (int32)mData.mLineStarts.Count - 2;
+                theChar = mData.mLineStarts[mData.mLineStarts.Count - 1] - mData.mLineStarts[mData.mLineStarts.Count - 2];
+            }
+        }
+
+        public virtual float GetLineHeight(int line)
+        {
+            return 0;
+        }
+
+        public virtual int32 GetLineCount()
+        {
+            GetTextData();
+            return (int32)mData.mLineStarts.Count - 1;
+        }
+
+        public virtual void GetLinePosition(int line, out int lineStart, out int lineEnd)
+        {
+            GetTextData();
+
+            if (line >= mData.mLineStarts.Count - 1)
+            {
+                lineStart = Math.Max(0, mData.mTextLength);
+                lineEnd = mData.mTextLength;
+                return;
+            }
+
+            lineStart = mData.mLineStarts[line];
+            lineEnd = mData.mLineStarts[line + 1];
+            if ((lineEnd > lineStart) && (mData.mText[lineEnd - 1].mChar == '\n'))
+                lineEnd--;
+        }
+
+        public virtual void GetLineText(int line, String outStr)
+        {
+            GetTextData();
+
+            int lineStart = mData.mLineStarts[line];
+            int lineEnd = mData.mLineStarts[line + 1];
+
+            if ((lineEnd > lineStart) && (mData.mText[lineEnd - 1].mChar == '\n'))
+                ExtractString(lineStart, lineEnd - lineStart - 1, outStr); // Remove last char8 (it's a '\n')
+            else
+                ExtractString(lineStart, lineEnd - lineStart, outStr); // Full line
+        }
+
+        public int GetTextIdx(int line, int char8Idx)
+        {
+            GetTextData();
+            int useLine = Math.Min(line, mData.mLineStarts.Count - 1);
+            return mData.mLineStarts[useLine] + char8Idx;
+        }
+
+        public int GetCharIdIdx(int32 findCharId)
+        {
+			mData.mTextIdData.Prepare();
+            return mData.mTextIdData.GetIndexFromId(findCharId);
+        }
+
+        public int32 GetSourceCharIdAtLineChar(int line, int column)
+        {
+            int encodeIdx = 0;
+            int spanLeft = 0;
+            int32 char8Id = 0;
+
+            int curLine = 0;
+            int curColumn = 0;
+            int char8Idx = 0;
+			mData.mTextIdData.Prepare();
+            while (true)
+            {
+                while (spanLeft == 0)
+                {
+                    if (mData.mTextIdData.mData == null)
+                    {
+                        spanLeft = mData.mTextLength;
+                        continue;
+                    }
+
+                    int32 cmd = Utils.DecodeInt(mData.mTextIdData.mData, ref encodeIdx);
+                    if (cmd > 0)
+                        char8Id = cmd;
+                    else
+                        spanLeft = -cmd;
+                    if (cmd == 0)
+                        return 0;
+                }
+
+                if ((curLine == line) && (curColumn == column))
+                    return char8Id;
+
+                char8 c = (char8)mData.mText[char8Idx++].mChar;
+                if (c == '\n')
+                {
+                    if (curLine == line)
+                        return char8Id;
+                    curLine++;
+                    curColumn = 0;
+                }
+                else
+                    curColumn++;
+                spanLeft--;
+                char8Id++;
+            }            
+        }
+
+        public virtual void GetTextCoordAtLineChar(int line, int char8Idx, out float x, out float y)
+        {
+            x = 0;
+            y = 0;
+        }
+
+        public virtual void GetTextCoordAtLineAndColumn(int line, int column, out float x, out float y)
+        {
+            x = 0;
+            y = 0;
+        }
+
+		// We used to have a split between PhysCursorMoved and CursorMoved.  CursorMoved has a "ResetWantX" and was non-virtual... uh-
+		//  so what was taht for?
+        public virtual void PhysCursorMoved()
+        {
+            mJustInsertedCharPair = false;
+            mShowCursorAtLineEnd = false;
+            mCursorBlinkTicks = 0;
+			mCursorImplicitlyMoved = false;
+
+			// 
+			ResetWantX();
+        }
+
+        public void CursorMoved()
+        {
+			PhysCursorMoved();
+
+            /*mJustInsertedCharPair = false;
+            mShowCursorAtLineEnd = false;
+            mCursorBlinkTicks = 0;
+            ResetWantX();*/
+        }
+
+        public virtual void CursorToStart()
+        {            
+            MoveCursorTo(0, 0);            
+        }
+
+        public virtual void CursorToEnd()
+        {
+			String lineStr = scope String();
+			GetLineText(GetLineCount() - 1, lineStr);
+            MoveCursorTo(GetLineCount() - 1, (int32)lineStr.Length);
+            //mCursorWantX = GetDrawPos;
+        }
+
+        public virtual void CursorToLineStart(bool allowToScreenStart)
+        {
+            int lineIdx;
+            int lineChar;
+            GetCursorLineChar(out lineIdx, out lineChar);
+
+            String lineText = scope String();
+            GetLineText(lineIdx, lineText);
+
+			bool hasNonWhitespace = false;
+            int32 contentIdx;
+            for (contentIdx = 0; contentIdx < lineText.Length; contentIdx++)
+			{
+                if (!lineText[contentIdx].IsWhiteSpace)
+				{
+					hasNonWhitespace = true;
+                    break;
+				}
+			}
+
+            if ((mAllowVirtualCursor) && (!hasNonWhitespace))
+            {
+                var prevLineAndColumn = CursorLineAndColumn;
+                CursorToLineEnd();
+                if ((!allowToScreenStart) || (CursorLineAndColumn.mColumn < prevLineAndColumn.mColumn))
+                    return;
+            }
+
+            if ((allowToScreenStart) && (lineChar <= contentIdx))
+                MoveCursorTo(lineIdx, 0);
+            else
+                MoveCursorTo(lineIdx, contentIdx);
+        }
+
+        public virtual void CursorToLineEnd()
+        {
+            int line;
+            int lineChar;
+            GetCursorLineChar(out line, out lineChar);            
+
+            String curLineStr = scope String();
+            GetLineText(line, curLineStr);
+            int32 lineLen = (int32)curLineStr.Length;
+
+			
+            MoveCursorTo(line, lineLen);            
+        }        
+
+        public void StartSelection()
+        {
+            mSelection = EditSelection();
+            int textPos;
+            TryGetCursorTextPos(out textPos);
+            mSelection.ValueRef.mEndPos = mSelection.ValueRef.mStartPos = (int32)textPos;
+        }
+
+        public void SelectToCursor()
+        {
+            /*int textPos;
+            TryGetCursorTextPos(out textPos);
+            if (mSelection != null)
+            {
+                mSelection.Value.mEndPos = textPos;
+            }*/
+
+			int textPos;
+			TryGetCursorTextPos(out textPos);
+
+			if (mDragSelectionUnion != null)
+			{
+			    mSelection = EditSelection(mDragSelectionUnion.Value.MinPos, mDragSelectionUnion.Value.MaxPos);
+			    if (textPos <= mSelection.Value.mStartPos)
+			    {
+			        mSelection.ValueRef.mStartPos = (int32)Math.Max(0, textPos - 1);
+			        while ((textPos > 0) && (IsNonBreakingChar((char8)mData.mText[textPos - 1].mChar)))
+			        {                        
+			            textPos--;
+			            mSelection.ValueRef.mStartPos = (int32)textPos;
+			        }
+			        CursorTextPos = mSelection.Value.mStartPos;
+			    }
+			    else
+			    {
+			        if (textPos > mSelection.Value.mEndPos)
+			        {
+			            mSelection.ValueRef.mEndPos = (int32)textPos;
+			            while ((textPos <= mData.mTextLength) && ((textPos == mData.mTextLength) || (mData.mText[textPos - 1].mChar != '\n')) &&
+			                (IsNonBreakingChar((char8)mData.mText[textPos - 1].mChar)))
+			            {
+			                mSelection.ValueRef.mEndPos = (int32)textPos;
+			                textPos++;
+			            }
+			        }
+			        CursorTextPos = mSelection.Value.mEndPos;
+			    }                
+			}
+			else if (mSelection != null)
+			{                
+			    mSelection.ValueRef.mEndPos = (int32)textPos;
+			}
+        }
+
+        public virtual bool IsLineVisible(int line, bool useDestScrollPostion = true)
+        {
+            if (!mIsMultiline)
+                return true;
+
+            float aX;
+            float aY;            
+            GetTextCoordAtLineChar(line, 0, out aX, out aY);
+            
+            float lineHeight = GetLineHeight(line);
+
+			double scrollPos = useDestScrollPostion ? mEditWidget.mVertPos.mDest : mEditWidget.mVertPos.mSrc;
+            if (aY + lineHeight*0.9f < scrollPos + mTextInsets.mTop)
+            {
+                return false;
+            }
+            else if (aY + lineHeight*0.1f + mShowLineBottomPadding > scrollPos + mEditWidget.mScrollContentContainer.mHeight)
+            {
+                return false;
+            }
+            return true;
+        }
+
+		public bool IsCursorVisible(bool useDestScrollPostion = true)
+		{
+			return IsLineVisible(CursorLineAndColumn.mLine, useDestScrollPostion);
+		}
+
+        public virtual void EnsureCursorVisible(bool scrollView = true, bool centerView = false, bool doHorzJump = true)
+        {
+            if (mEditWidget.mScrollContentContainer.mWidth <= 0)
+                return; // Not sized yet
+
+            if (mContentChanged)
+                RecalcSize();
+
+            float horzJumpSize = doHorzJump ? mHorzJumpSize : 0;
+
+            float aX;
+            float aY;
+            GetTextCoordAtCursor(out aX, out aY);            
+
+            int line;
+            int lineChar;
+            GetCursorLineChar(out line, out lineChar);
+
+            float lineHeight = GetLineHeight(line);
+
+            if (mIsMultiline)
+            {
+                if (aY < mEditWidget.mVertPos.mDest + mTextInsets.mTop)
+                {
+                    if (scrollView)
+                    {
+                        float scrollPos = aY - mTextInsets.mTop;
+                        if (centerView)
+                        {                            
+                            scrollPos -= mEditWidget.mScrollContentContainer.mHeight * 0.50f;
+                            scrollPos = (float)Math.Round(scrollPos / lineHeight) * lineHeight;
+                        }
+                        mEditWidget.VertScrollTo(scrollPos);
+                    }
+                    else
+                    {
+                        int aLine;
+                        int aCharIdx;
+                        float overflowX;
+                        GetLineCharAtCoord(aX, (float)mEditWidget.mVertPos.mDest + mTextInsets.mTop, out aLine, out aCharIdx, out overflowX);
+
+                        float newX;
+                        float newY;
+                        GetTextCoordAtLineChar(aLine, aCharIdx, out newX, out newY);
+                        if (aY < mEditWidget.mVertPos.mDest + mTextInsets.mTop - 0.01f)
+                            GetLineCharAtCoord(newX, newY + GetLineHeight(aLine), out aLine, out aCharIdx, out overflowX);
+
+                        MoveCursorTo(aLine, aCharIdx);
+                    }
+                }
+                else if (aY + lineHeight + mShowLineBottomPadding > mEditWidget.mVertPos.mDest + mEditWidget.mScrollContentContainer.mHeight)
+                {
+                    if (scrollView)
+                    {
+                        float scrollPos = aY + lineHeight + mShowLineBottomPadding - mEditWidget.mScrollContentContainer.mHeight;
+                        if (centerView)
+                        {
+                            // Show slightly more content on bottom
+                            scrollPos += mEditWidget.mScrollContentContainer.mHeight * 0.50f;
+                            scrollPos = (float)Math.Round(scrollPos / lineHeight) * lineHeight;
+                        }
+                        mEditWidget.VertScrollTo(scrollPos);
+                    }
+                    else
+                        MoveCursorToCoord(aX, (float)mEditWidget.mVertPos.mDest + mEditWidget.mScrollContentContainer.mHeight - lineHeight);
+                }
+            }
+
+            if (mAutoHorzScroll)
+            {
+                if (aX < mEditWidget.mHorzPos.mDest + mTextInsets.mLeft)
+                    mEditWidget.HorzScrollTo(Math.Max(0, aX - mTextInsets.mLeft - horzJumpSize));
+                else if (aX >= mEditWidget.mHorzPos.mDest + mEditWidget.mScrollContentContainer.mWidth - mTextInsets.mRight)
+                {
+                    float wantScrollPos = aX - mEditWidget.mScrollContentContainer.mWidth + horzJumpSize + mTextInsets.mRight;
+                    if (mAllowVirtualCursor)
+                        TrySetHorzScroll(wantScrollPos);
+                    mEditWidget.HorzScrollTo(Math.Min(mWidth - mEditWidget.mScrollContentContainer.mWidth, wantScrollPos));
+                }
+            }
+        }
+
+        protected virtual void TrySetHorzScroll(float horzScroll)
+        {
+            mWidth = Math.Max(mWidth, horzScroll + mEditWidget.mScrollContentContainer.mWidth);
+            mEditWidget.UpdateScrollbars();
+        }
+
+        public void BlockIndentSelection(bool unIndent = false)
+        {
+			if (CheckReadOnly())
+				return;
+
+            int minLineIdx = 0;
+            int minLineCharIdx = 0;
+            int maxLineIdx = 0;
+            int maxLineCharIdx = 0;
+
+            bool isMultilineSelection = HasSelection();
+            if (isMultilineSelection)
+            {
+                GetLineCharAtIdx(mSelection.Value.MinPos, out minLineIdx, out minLineCharIdx);
+                GetLineCharAtIdx(mSelection.Value.MaxPos, out maxLineIdx, out maxLineCharIdx);
+                isMultilineSelection = maxLineIdx != minLineIdx;
+            }
+
+            if (isMultilineSelection)
+            {
+                var indentTextAction = new EditWidgetContent.IndentTextAction(this);
+                EditSelection newSel = mSelection.Value;
+
+                mSelection.ValueRef.MakeForwardSelect();
+                mSelection.ValueRef.mStartPos -= (int32)minLineCharIdx;
+
+                //int32 minPos = newSel.MinPos;
+                int32 startAdjust = 0;
+                int32 endAdjust = 0;
+
+                if (unIndent)
+                {
+                    //bool hadWsLeft = (minPos > 0) && (Char8.IsWhiteSpace((char8)mData.mText[minPos - 1].mChar));
+                    for (int lineIdx = minLineIdx; lineIdx <= maxLineIdx; lineIdx++)
+                    {
+                        int lineStart;
+                        int lineEnd;
+                        GetLinePosition(lineIdx, out lineStart, out lineEnd);
+                        //lineStart += endAdjust;
+
+                        for (int32 i = 0; i < mTabLength; i++)
+                        {
+                            char8 c = (char8)mData.mText[lineStart + i].mChar;
+                            if (((c == '\t') && (i == 0)) || (c == ' '))
+                            {
+                                indentTextAction.mRemoveCharList.Add(Tuple<int32, char8>((int32)lineStart + i + endAdjust, c));
+                                //RemoveText(lineStart, 1);
+                                endAdjust--;
+                            }
+                            if (c != ' ')
+                                break;
+                        }
+                        if ((lineIdx == minLineIdx) && (endAdjust != 0))
+                            startAdjust = endAdjust;
+                    }
+					indentTextAction.Redo();
+
+                }
+                else
+                {
+                    startAdjust++;
+                    for (int lineIdx = minLineIdx; lineIdx <= maxLineIdx; lineIdx++)
+                    {
+                        int lineStart;
+                        int lineEnd;
+                        GetLinePosition(lineIdx, out lineStart, out lineEnd);
+                        lineStart += endAdjust;
+                        //InsertText(lineStart, "\t");
+                        indentTextAction.mInsertCharList.Add(Tuple<int32, char8>((int32)lineStart, '\t'));
+                        endAdjust++;
+                    }
+
+					indentTextAction.Redo();
+                }
+                ContentChanged();
+
+                if (newSel.IsForwardSelect)
+                {
+                    newSel.mStartPos = Math.Max(newSel.mStartPos + startAdjust, 0);
+                    newSel.mEndPos += endAdjust;
+                    CursorTextPos = newSel.mEndPos;
+                }
+                else
+                {
+                    newSel.mEndPos = Math.Max(newSel.mEndPos + startAdjust, 0);
+                    newSel.mStartPos += endAdjust;
+                    CursorTextPos = newSel.mEndPos;
+                }
+
+                indentTextAction.mNewSelection = newSel;
+                if ((indentTextAction.mInsertCharList.Count > 0) ||
+                    (indentTextAction.mRemoveCharList.Count > 0))
+                {
+                    mData.mUndoManager.Add(indentTextAction);
+				}
+				else
+					delete indentTextAction;
+
+                mSelection = newSel;
+                return;
+            }
+            else // Non-block
+            {
+                int lineIdx;
+                int lineCharIdx;
+                GetLineCharAtIdx(CursorTextPos, out lineIdx, out lineCharIdx);
+                
+                if (unIndent)
+                {
+					var prevSelection = mSelection;
+					mSelection = null;
+
+                    if (lineCharIdx > 0)
+                    {
+                        String aLine = scope String();
+                        GetLineText(lineIdx, aLine);
+                        for (int32 i = 0; i < mTabLength; i++)
+                        {
+							if (lineCharIdx == 0)
+								break;
+                            char8 c = aLine[lineCharIdx - 1];
+                            if (!c.IsWhiteSpace)
+                                break;
+                            lineCharIdx--;
+							if (prevSelection != null)
+							{
+								prevSelection.mValue.mStartPos--;
+								prevSelection.mValue.mEndPos--;
+							}
+                            Backspace();
+                            if (c == '\t')
+                                break;
+                        }
+                    }
+                    else if (mAllowVirtualCursor)
+                    {
+                        var cursorPos = CursorLineAndColumn;
+                        cursorPos.mColumn = Math.Max(0, (cursorPos.mColumn - 1) / mTabLength * mTabLength);
+                        CursorLineAndColumn = cursorPos;
+                    }
+
+					mSelection = prevSelection;
+
+                    return;
+                }
+
+                String curLineText = scope String();
+                GetLineText(lineIdx, curLineText);
+
+                int32 indentCount = 1;
+				
+				String trimmedCurLineText = scope String(curLineText);
+				trimmedCurLineText.Trim();
+
+                if ((lineCharIdx == 0) && (trimmedCurLineText.Length == 0)) // Smart indent
+                {
+                    if (mAllowVirtualCursor)
+                    {
+                        if (HasSelection())
+                            DeleteSelection();
+                        var prevCursorPos = CursorLineAndColumn;
+                        CursorToLineEnd();
+                        if (prevCursorPos.mColumn >= CursorLineAndColumn.mColumn)
+                        {
+                            var cursorPos = prevCursorPos;
+                            cursorPos.mColumn = (prevCursorPos.mColumn + mTabLength) / mTabLength * mTabLength;
+                            CursorLineAndColumn = cursorPos;
+                        }
+                        return;
+                    }
+
+                    int checkLineIdx = lineIdx - 1;
+                    while (checkLineIdx >= 0)
+                    {
+                        String lineText = scope String();
+                        GetLineText(checkLineIdx, lineText);
+                        lineText.TrimEnd();
+                        int32 checkIndentCount = 0;
+                        for (; checkIndentCount < lineText.Length; checkIndentCount++)
+                        {
+                            if (lineText[checkIndentCount] != '\t')
+                                break;
+                        }
+                        if ((checkIndentCount < lineText.Length) && (lineText[checkIndentCount] == '{'))
+                            checkIndentCount++;
+                        if (checkIndentCount > 0)
+                        {
+                            indentCount = checkIndentCount;
+                            break;
+                        }
+                        checkLineIdx--;
+                    }
+                }
+
+				if (HasSelection())
+				{
+					GetLinePosition(minLineIdx, var lineStart, var lineEnd);
+					String str = scope .();
+					ExtractString(lineStart, CursorTextPos - lineStart, str);
+					if (str.IsWhiteSpace)
+					{
+						let prevSelection = mSelection.Value;
+						mSelection = null;
+						for (int32 i = 0; i < indentCount; i++)
+						    InsertAtCursor("\t");
+						GetLinePosition(minLineIdx, out lineStart, out lineEnd);
+						mSelection = EditSelection(prevSelection.mStartPos + indentCount, prevSelection.mEndPos + indentCount);
+					}
+					else
+						InsertAtCursor("\t");
+				}
+				else
+				{
+					for (int32 i = 0; i < indentCount; i++)
+						InsertAtCursor("\t");
+				}
+            }
+        }
+
+        public void SelectAll()
+        {
+            CursorToStart();
+            StartSelection();
+            CursorToEnd();
+            SelectToCursor();
+            Debug.Assert(mSelection.Value.mEndPos <= mData.mTextLength);
+        }
+
+        //public void MoveCursorTo
+
+        public void MoveCursorTo(int line, int char8Idx, bool centerCursor = false, int movingDir = 0)
+        {
+			int useCharIdx = char8Idx;
+
+            mShowCursorAtLineEnd = false;
+            CursorTextPos = GetTextIdx(line, char8Idx);
+
+			// Skip over UTF8 parts AND unicode combining marks (ie: when we have a letter with an accent mark following it)
+			while (true)
+			{
+				char8 c = mData.SafeGetChar(mCursorTextPos);
+				if (c < (char8)0x80)
+					break;
+				if ((uint8)c & 0xC0 != 0x80)
+				{
+					var checkChar = mData.GetChar32(mCursorTextPos);
+					if (!checkChar.IsCombiningMark)
+						break;
+				}
+				if (movingDir > 0)
+				{
+					useCharIdx++;
+					mCursorTextPos++;
+				}
+				else
+				{
+					if (mCursorTextPos == 0)
+						break;
+					useCharIdx--;
+					mCursorTextPos--;
+				}
+			}
+            
+            float aX;
+            float aY;
+            GetTextCoordAtLineChar(line, useCharIdx, out aX, out aY);
+            mCursorWantX = aX;
+
+            mCursorBlinkTicks = 0;
+            if (mEnsureCursorVisibleOnModify)
+                EnsureCursorVisible(true, centerCursor);
+            PhysCursorMoved();
+        }
+
+        public void ResetWantX()
+        {            
+            int line;
+            int col;
+            GetLineCharAtIdx(CursorTextPos, out line, out col);
+
+            float x;
+            float y;
+            GetTextCoordAtLineChar(line, col, out x, out y);
+            mCursorWantX = x;
+        }
+
+        public void MoveCursorToIdx(int index, bool centerCursor = false)
+        {
+            int aLine;
+            int aCharIdx;
+            GetLineCharAtIdx(index, out aLine, out aCharIdx);
+            MoveCursorTo(aLine, aCharIdx, centerCursor);
+        }
+        
+        public void MoveCursorToCoord(float x, float y)
+        {
+			bool failed = false;
+
+            int aLine;
+            int aCharIdx;
+            float overflowX;
+            failed = !GetLineCharAtCoord(x, y, out aLine, out aCharIdx, out overflowX);
+			if ((mAllowVirtualCursor) && (failed))
+			{
+			    int line;
+			    int column;
+			    GetLineAndColumnAtCoord(x, y, out line, out column);
+			    CursorLineAndColumn = LineAndColumn(line, column);
+			    mCursorBlinkTicks = 0;
+				PhysCursorMoved();
+				mShowCursorAtLineEnd = false;
+			}
+			else
+			{
+				if ((mWordWrap) && (failed))
+					mShowCursorAtLineEnd = true;
+	            MoveCursorTo(aLine, aCharIdx);
+
+	            int lineStart;
+	            int lineEnd;
+	            GetLinePosition(aLine, out lineStart, out lineEnd);
+	            if ((mWordWrap) && (aCharIdx == lineEnd - lineStart) && (lineEnd != lineStart))
+	                mShowCursorAtLineEnd = true;
+			}
+        }
+
+        public bool HasSelection()
+        {
+            return (mSelection != null) && (mSelection.Value.HasSelection);
+        }
+
+		public override void RehupScale(float oldScale, float newScale)
+		{
+			base.RehupScale(oldScale, newScale);
+			mTextInsets.Scale(newScale / oldScale);
+			//ContentChanged
+		}
+
+        //public virtual void DrawContent(
+		
+		public void ClearUndoData()
+		{
+			if (mData.mUndoManager != null)
+			{
+				mData.mUndoManager.Clear();
+			}
+		}
+
+		public bool HasUndoData()
+		{
+			if (mData.mUndoManager != null)
+			{
+				return mData.mUndoManager.GetActionCount() > 0;
+			}
+			return false;
+		}
+    }
+
+    public abstract class EditWidget : ScrollableWidget
+    {
+        public EditWidgetContent mEditWidgetContent;
+        public Event<EditWidgetEventHandler> mOnSubmit ~ _.Dispose();
+        public Event<EditWidgetEventHandler> mOnCancel ~ _.Dispose();
+        public Event<EditWidgetEventHandler> mOnContentChanged ~ _.Dispose();
+        
+        public EditWidgetContent Content
+        {
+            get { return mEditWidgetContent; }
+        }
+
+        public void GetText(String outStr)
+		{
+			mEditWidgetContent.ExtractString(0, mEditWidgetContent.mData.mTextLength, outStr);
+		}
+
+		public bool IsWhiteSpace()
+		{
+			for (int i < mEditWidgetContent.mData.mTextLength)
+			{
+				char8 c = mEditWidgetContent.mData.mText[i].mChar;
+				if (!c.IsWhiteSpace)
+					return false;
+			}
+			return true;
+		}
+
+        public virtual void Submit()
+        {
+            if (mOnSubmit.HasListeners)
+            {
+                EditEvent anEvent = scope EditEvent();
+                anEvent.mSender = this;                
+                mOnSubmit(anEvent);
+            }
+        }
+
+        public void Cancel()
+        {
+            if (mOnCancel.HasListeners)
+            {
+                EditEvent editEvent = scope EditEvent();
+                editEvent.mSender = this;
+                mOnCancel(editEvent);
+                return;
+            }
+        }
+
+        public void ContentChanged()
+        {
+            if (mOnContentChanged.HasListeners)
+            {
+                EditEvent editEvent = scope EditEvent();
+                editEvent.mSender = this;
+                mOnContentChanged(editEvent);
+                return;
+            }
+        }
+
+        public virtual void SetText(String text)
+        {
+			scope AutoBeefPerf("EditWidget.SetText");
+
+            var prevLineAndColumn = mEditWidgetContent.CursorLineAndColumn;
+            float cursorPos = (float)mVertPos.mDest;
+
+			
+            EditWidgetContent.TextUndoBatchStart undoBatchStart = null;
+			if (mEditWidgetContent.WantsUndo)
+			{
+	            undoBatchStart = new EditWidgetContent.TextUndoBatchStart(mEditWidgetContent, "setText");
+	            mEditWidgetContent.mData.mUndoManager.Add(undoBatchStart);
+			}
+
+            mEditWidgetContent.CursorToStart();
+            mEditWidgetContent.SelectAll();
+
+            mEditWidgetContent.InsertAtCursor(text);
+            if (mEditWidgetContent.mAllowVirtualCursor)
+                mEditWidgetContent.CursorLineAndColumn = prevLineAndColumn;
+            mEditWidgetContent.ClampCursor();
+
+            mVertPos.Set(cursorPos, true);
+			if (mEditWidgetContent.WantsUndo)
+            	mEditWidgetContent.mData.mUndoManager.Add(undoBatchStart.mBatchEnd);
+
+			if ((!mHasFocus) && (mHorzPos != null))
+				mHorzPos.Set(0, true);
+            //TextChanged();
+            //mEditWidgetContent.mUndoManager.Clear();
+        }
+
+        public IdSpan DuplicateTextIdData()
+        {
+            return mEditWidgetContent.mData.mTextIdData.Duplicate();
+        }
+
+        public override void MouseDown(float x, float y, int32 btn, int32 btnCount)
+        {
+            base.MouseDown(x, y, btn, btnCount);
+            
+            // If we get clicked on ourselves then just translate into editwidget space to get the focus
+            float childX;
+            float childY;
+            mEditWidgetContent.ParentToSelfTranslate(x, y, out childX, out childY);
+            mEditWidgetContent.MouseDown(childX, childY, btn, btnCount);
+        }
+
+        public override void SetFocus()
+        {            
+            mWidgetWindow.SetFocus(this);            
+        }
+
+        public override void GotFocus()
+        {
+            base.GotFocus();
+			MouseEventHandler handler = new => HandleWindowMouseDown;
+			//Debug.WriteLine("Adding handler {0}", handler);
+            mWidgetWindow.mOnMouseDown.Add(handler);
+        }
+
+		public override void LostFocus()
+		{
+			MarkDirty();
+			var widgetWindow = mWidgetWindow;
+			base.LostFocus();
+			widgetWindow.mOnMouseDown.Remove(scope => HandleWindowMouseDown, true);
+			//Debug.WriteLine("Removing handler (LostFocus)");
+		}
+
+		protected virtual bool WantsUnfocus()
+		{
+			return (!mMouseOver) && (mWidgetWindow != null) && (!ContainsWidget(mWidgetWindow.mOverWidget));
+		}
+
+        protected virtual void HandleWindowMouseDown(MouseEvent theEvent)
+        {
+            if (WantsUnfocus())
+            {
+                // Someone else got clicked on, remove focus
+                //mWidgetWindow.mMouseDownHandler.Remove(scope => HandleWindowMouseDown, true);
+				//Debug.WriteLine("Removing handler (HandleWindowMouseDown)");
+                mWidgetWindow.SetFocus(null);  
+            }
+        }
+
+        public override void RemovedFromParent(Widget previousParent, WidgetWindow previousWidgetWindow)
+        {
+            base.RemovedFromParent(previousParent, previousWidgetWindow);
+
+            //TODO: Does this cause problems if we didn't have the handler added?
+            if (previousWidgetWindow != null)
+			{
+                //previousWidgetWindow.mMouseDownHandler.Remove(scope => HandleWindowMouseDown, true);
+				//Debug.WriteLine("Removing handler (RemovedFromParent)");
+			}
+        }
+
+        public override void KeyChar(char32 theChar)
+        {
+            base.KeyChar(theChar);
+
+            mEditWidgetContent.KeyChar(theChar);
+        }
+
+        public override void Resize(float x, float y, float width, float height)
+        {
+            base.Resize(x, y, width, height);
+
+            if (mEditWidgetContent.mWordWrap) // Need to recalculate word wrapping
+                mEditWidgetContent.ContentChanged();
+        }
+
+        public override void KeyDown(KeyCode keyCode, bool isRepeat)
+        {
+            base.KeyDown(keyCode, isRepeat);
+            if (mWidgetWindow != null)
+                mEditWidgetContent.KeyDown(keyCode, isRepeat);
+        }
+
+        public void FinishScroll()
+        {
+            mVertPos.mPct = 1.0f;
+            UpdateContentPosition();
+        }
+    }
+}

+ 19 - 0
BeefLibs/Beefy2D/src/widgets/IMenu.bf

@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Beefy.widgets
+{
+    public delegate void MenuItemSelectedHandler(IMenu menu);
+    public delegate void MenuItemUpdateHandler(IMenu menu);
+
+    public interface IMenu
+    {
+		void SetDisabled(bool enable);
+    }
+
+	public interface IMenuContainer
+	{
+
+	}
+}

+ 24 - 0
BeefLibs/Beefy2D/src/widgets/ImageWidget.bf

@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.gfx;
+
+namespace Beefy.widgets
+{
+    public class ImageWidget : Widget
+    {
+        public IDrawable mImage;
+		public IDrawable mOverImage;
+		public IDrawable mDownImage;
+
+        public override void Draw(Graphics g)
+        {
+			if ((mMouseOver && mMouseDown) && (mDownImage != null))
+				g.Draw(mDownImage);
+			if ((mMouseOver) && (mOverImage != null))
+            	g.Draw(mOverImage);
+			else
+				g.Draw(mImage);
+        }
+    }
+}

+ 246 - 0
BeefLibs/Beefy2D/src/widgets/InfiniteScrollbar.bf

@@ -0,0 +1,246 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Beefy.events;
+using Beefy.geom;
+using System.Diagnostics;
+
+namespace Beefy.widgets
+{   
+    public delegate void InfiniteScrollEventHandler(double scrollDelta);
+
+    public abstract class InfiniteScrollbar : Widget, IDragInterface
+    {
+        /*public class Thumb : Widget, IDragInterface
+        {
+            public InfiniteScrollbar mScrollbar;
+            public DragHelper mDraggableHelper ~ delete _;
+
+            public this()
+            {
+                mDraggableHelper = new DragHelper(this, this);
+            }
+
+            public void DragStart()
+            {
+            }
+
+            public void DragEnd()
+            {
+                mScrollbar.ScrollSetLevel(0.0);
+            }
+
+            public void MouseDrag(float x, float y, float dX, float dY)
+            {                
+                float parentX;
+                float parentY;
+                SelfToParentTranslate(x - mDraggableHelper.mMouseDownX, y - mDraggableHelper.mMouseDownY, out parentX, out parentY);
+
+                mScrollbar.ScrollSetLevel(mScrollbar.GetAccelFracAt(parentX, parentY));
+            }
+        }*/
+
+        public class Arrow : Widget
+        {
+            public InfiniteScrollbar mScrollbar;
+            public int32 mDownTick;
+            public bool mBack;
+
+            public override void MouseDown(float x, float y, int32 btn, int32 btnCount)
+            {
+                base.MouseDown(x, y, btn, btnCount);
+                DoScroll();
+
+                mDownTick = 0;
+            }
+
+            protected virtual void DoScroll()
+            {
+                mScrollbar.FixedScroll(mBack ? -1 : 1);
+            }
+
+            public override void Update()
+            {
+                base.Update();
+
+                if ((mMouseDown) && (mMouseOver))
+                {
+                    mDownTick++;
+
+                    if (mDownTick > 20)
+                        DoScroll();
+                }                
+            }
+        }
+
+        // configuration
+        public double mFixedScrollAmt; // base amount to scroll for fixed scroll events e.g arrow clicks
+        public double mScrollDeltaLevelAmount; // amount to change level during update while mouse is held down
+        //public double mScrollMaxAccel; // absolute value maximum of acceleration (transient accel will range from [-this,this])
+        public double mScrollMaxVelocity; // absolute value maximum of velocity; effective limit will be adjusted based on canonical acceleration
+        
+        public double mScrollThumbFrac; // scaled canonical acceleration, [-1,1]
+        //public double mScrollAccel;
+        public double mScrollVelocity;
+
+        //public Thumb mThumb;        
+        public Arrow mStartArrow;
+        public Arrow mEndArrow;
+
+        public float mBaseSize;
+        public float mDualBarSizeOffset;
+
+        public int32 mDownTick;
+        public float mLastMouseX;
+        public float mLastMouseY;
+
+        public Event<InfiniteScrollEventHandler> mOnInfiniteScrollEvent ~ _.Dispose();
+
+		public DragHelper mDraggableHelper ~ delete _;
+
+		public this()
+		{
+			mDraggableHelper = new DragHelper(this, this);
+		}
+
+        public void HandleScroll(double scrollDelta)
+        {
+            if (mOnInfiniteScrollEvent.HasListeners)
+                mOnInfiniteScrollEvent(scrollDelta);
+        }
+
+		public void DragStart()
+		{
+			let thumbPos = GetThumbPos();
+			if (thumbPos.Contains(mDraggableHelper.mMouseDownX, mDraggableHelper.mMouseDownY))
+			{
+				// Is dragging thumb now
+			}
+			else
+			{
+				mDraggableHelper.CancelDrag();
+			}
+		}
+
+		public void DragEnd()
+		{
+		    ScrollSetLevel(0.0);
+		}
+
+		public void MouseDrag(float x, float y, float dX, float dY)
+		{
+		    //float parentX;
+		    //float parentY;
+		    //SelfToParentTranslate(x - mDraggableHelper.mMouseDownX, y - mDraggableHelper.mMouseDownY, out parentX, out parentY);
+
+		    //ScrollSetLevel(GetAccelFracAt(parentX, parentY));
+
+			float accelFrac = GetAccelFracAt(mDraggableHelper.RelX, mDraggableHelper.RelY);
+			ScrollSetLevel(accelFrac);
+		}
+
+        public virtual void FixedScroll(int32 delta)
+        {
+            HandleScroll(delta * mFixedScrollAmt);
+        }
+
+        public virtual void Init()
+        {
+            // test config values
+            mFixedScrollAmt = 1;
+            mScrollDeltaLevelAmount = 1.0;
+            //mScrollMaxAccel = 1000;
+            mScrollMaxVelocity = 250;
+        }
+
+        public virtual void ResizeContent()
+        {
+        }
+
+        public void UpdateScrollPosition()
+        {
+            if (Math.Abs(mScrollVelocity) > 0.0001)
+                HandleScroll(mScrollVelocity * 0.01);
+        }
+
+        public virtual void ScrollSetLevel(double scrollLevel)
+        {
+			double setScrollLevel = scrollLevel;
+            setScrollLevel = Math.Min(Math.Max(-1.0, setScrollLevel), 1.0);
+            mScrollThumbFrac = scrollLevel;
+        
+            //double vel = Math.Exp(Math.Log(mScrollMaxVelocity)*Math.Abs(mScrollThumbFrac)) - 1.0;
+
+			double vel = Math.Pow(Math.Abs(mScrollThumbFrac) * 20, 1.8);
+            if (mScrollThumbFrac < 0)
+                vel = -vel;
+            mScrollVelocity = vel;
+
+            ResizeContent();
+        }
+        public virtual void ScrollDeltaLevel(double scrollAmount)
+        {
+            //ScrollSetLevel(mScrollThumbFrac + scrollAmount);
+            HandleScroll(scrollAmount); // clicking in the bar feels better as a fixed scroll rather than changing the acceleration
+        }
+
+        public virtual float GetAccelFracAt(float dx, float dy)
+        {
+            return 0;
+        }
+
+		public virtual Rect GetThumbPos()
+		{
+			return .();
+		}
+
+        public override void Update()
+        {
+            base.Update();
+
+            if ((mMouseDown) && (mMouseOver))
+            {
+                mDownTick++;
+
+                if ((mDownTick > 20) && (mUpdateCnt % 5 == 0) && (!mDraggableHelper.mIsDragging))
+                {
+					 let thumbPos = GetThumbPos();
+
+                    if (mLastMouseY < thumbPos.Top)
+                        ScrollDeltaLevel(-mScrollDeltaLevelAmount);
+                    else if (mLastMouseY > thumbPos.Bottom)
+                        ScrollDeltaLevel(mScrollDeltaLevelAmount);
+                }
+            }
+
+            UpdateScrollPosition();
+        }
+
+        public override void MouseMove(float x, float y)
+        {
+            base.MouseMove(x, y);
+            mLastMouseX = x;
+            mLastMouseY = y;
+        }
+
+        public override void MouseDown(float x, float y, int32 btn, int32 btnCount)
+        {
+            mLastMouseX = x;
+            mLastMouseY = y;
+
+			let thumbPos = GetThumbPos();
+
+            base.MouseDown(x, y, btn, btnCount);
+            if (mLastMouseY < thumbPos.Top)
+                ScrollDeltaLevel(-mScrollDeltaLevelAmount);
+            else if (mLastMouseY > thumbPos.Bottom)
+                ScrollDeltaLevel(mScrollDeltaLevelAmount);
+            mDownTick = 0;
+        }
+
+        public override void MouseWheel(float x, float y, int32 delta)
+        {
+            FixedScroll(-delta);
+        }
+    }
+}

部分文件因为文件数量过多而无法显示