Merge branch 'feature/better-text-edit' into 'develop'

Better text edit

See merge request cc-ru/ocelot/ocelot-desktop!120
This commit is contained in:
Dmitry Zhidenkov 2025-09-03 09:04:41 +00:00
commit 6db5ff3f37
26 changed files with 774 additions and 523 deletions

BIN
sprites/icons/Cut.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

BIN
sprites/icons/Paste.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

View File

@ -68,7 +68,9 @@ TextInputBorderDisabled = #666666
TextInputBorderFocused = #336666 TextInputBorderFocused = #336666
TextInputBackground = #aaaaaa TextInputBackground = #aaaaaa
TextInputBackgroundActive = #bbbbbb TextInputBackgroundActive = #bbbbbb
TextInputBackgroundSelected = #336666
TextInputForeground = #333333 TextInputForeground = #333333
TextInputForegroundSelected = #aaaaaa
TextInputForegroundDisabled = #888888 TextInputForegroundDisabled = #888888
TextInputBorderError = #aa8888 TextInputBorderError = #aa8888
TextInputBorderErrorDisabled = #aa8888 TextInputBorderErrorDisabled = #aa8888

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 150 KiB

View File

@ -47,46 +47,48 @@ icons/Code 321 632 16 16
icons/ComponentBus 338 632 16 16 icons/ComponentBus 338 632 16 16
icons/Copy 355 632 16 16 icons/Copy 355 632 16 16
icons/Cross 372 632 16 16 icons/Cross 372 632 16 16
icons/Delete 389 632 16 16 icons/Cut 389 632 16 16
icons/Delete 406 632 16 16
icons/DragLMB 509 567 21 14 icons/DragLMB 509 567 21 14
icons/DragRMB 531 567 21 14 icons/DragRMB 531 567 21 14
icons/EEPROM 406 632 16 16 icons/EEPROM 423 632 16 16
icons/Edit 423 632 16 16 icons/Edit 440 632 16 16
icons/Eject 440 632 16 16 icons/Eject 457 632 16 16
icons/File 457 632 16 16 icons/File 474 632 16 16
icons/Floppy 474 632 16 16 icons/Floppy 491 632 16 16
icons/Folder 491 632 16 16 icons/Folder 508 632 16 16
icons/FolderSlash 508 632 16 16 icons/FolderSlash 525 632 16 16
icons/Grid 177 567 22 22 icons/Grid 177 567 22 22
icons/GridOff 200 567 22 22 icons/GridOff 200 567 22 22
icons/Guitar 525 632 16 16 icons/Guitar 542 632 16 16
icons/HDD 542 632 16 16 icons/HDD 559 632 16 16
icons/Help 559 632 16 16 icons/Help 576 632 16 16
icons/Home 223 567 22 22 icons/Home 223 567 22 22
icons/Keyboard 576 632 16 16 icons/Keyboard 593 632 16 16
icons/KeyboardOff 593 632 16 16 icons/KeyboardOff 610 632 16 16
icons/LMB 453 434 11 14 icons/LMB 453 434 11 14
icons/Label 610 632 16 16 icons/Label 627 632 16 16
icons/LinesHorizontal 627 632 16 16 icons/LinesHorizontal 644 632 16 16
icons/Link 644 632 16 16 icons/Link 661 632 16 16
icons/LinkSlash 661 632 16 16 icons/LinkSlash 678 632 16 16
icons/Memory 678 632 16 16 icons/Memory 695 632 16 16
icons/Microchip 695 632 16 16 icons/Microchip 712 632 16 16
icons/NA 712 632 16 16 icons/NA 729 632 16 16
icons/NotificationError 477 434 11 11 icons/NotificationError 477 434 11 11
icons/NotificationInfo 489 434 11 11 icons/NotificationInfo 489 434 11 11
icons/NotificationWarning 501 434 11 11 icons/NotificationWarning 501 434 11 11
icons/Ocelot 729 632 16 16 icons/Ocelot 746 632 16 16
icons/Pause 746 632 16 16 icons/Paste 763 632 16 16
icons/Pause 780 632 16 16
icons/Pin 408 434 14 14 icons/Pin 408 434 14 14
icons/Play 763 632 16 16 icons/Play 797 632 16 16
icons/Plus 780 632 16 16 icons/Plus 814 632 16 16
icons/Power 797 632 16 16 icons/Power 831 632 16 16
icons/RMB 465 434 11 14 icons/RMB 465 434 11 14
icons/Restart 814 632 16 16 icons/Restart 848 632 16 16
icons/Save 831 632 16 16 icons/Save 865 632 16 16
icons/SaveAs 848 632 16 16 icons/SaveAs 882 632 16 16
icons/Server 865 632 16 16 icons/Server 899 632 16 16
icons/SettingsKeymap 356 434 12 17 icons/SettingsKeymap 356 434 12 17
icons/SettingsSound 369 434 12 17 icons/SettingsSound 369 434 12 17
icons/SettingsSystem 382 434 12 17 icons/SettingsSystem 382 434 12 17
@ -100,90 +102,90 @@ icons/SideSouth 573 434 11 11
icons/SideUndefined 585 434 11 11 icons/SideUndefined 585 434 11 11
icons/SideUp 597 434 11 11 icons/SideUp 597 434 11 11
icons/SideWest 609 434 11 11 icons/SideWest 609 434 11 11
icons/Tier0 882 632 16 16 icons/Tier0 916 632 16 16
icons/Tier1 899 632 16 16 icons/Tier1 933 632 16 16
icons/Tier2 916 632 16 16 icons/Tier2 950 632 16 16
icons/Tiers 933 632 16 16 icons/Tiers 967 632 16 16
icons/Unpin 423 434 14 14 icons/Unpin 423 434 14 14
icons/WaveLFSR 254 540 24 10 icons/WaveLFSR 288 540 24 10
icons/WaveNoise 279 540 24 10 icons/WaveNoise 313 540 24 10
icons/WaveSawtooth 304 540 24 10 icons/WaveSawtooth 338 540 24 10
icons/WaveSine 329 540 24 10 icons/WaveSine 363 540 24 10
icons/WaveSquare 354 540 24 10 icons/WaveSquare 388 540 24 10
icons/WaveTriangle 379 540 24 10 icons/WaveTriangle 413 540 24 10
icons/Window 950 632 16 16 icons/Window 984 632 16 16
icons/WireArrowLeft 281 344 4 8 icons/WireArrowLeft 281 344 4 8
icons/WireArrowRight 286 344 4 8 icons/WireArrowRight 286 344 4 8
items/APU0 49 655 16 96 items/APU0 49 655 16 96
items/APU1 66 655 16 96 items/APU1 66 655 16 96
items/APU2 83 655 16 96 items/APU2 83 655 16 96
items/CPU0 967 632 16 16 items/CPU0 1001 632 16 16
items/CPU1 984 632 16 16 items/CPU1 358 674 16 16
items/CPU2 1001 632 16 16 items/CPU2 375 674 16 16
items/CardBase 358 674 16 16 items/CardBase 392 674 16 16
items/CircuitBoard 375 674 16 16 items/CircuitBoard 409 674 16 16
items/ComponentBus0 392 674 16 16 items/ComponentBus0 426 674 16 16
items/ComponentBus1 409 674 16 16 items/ComponentBus1 443 674 16 16
items/ComponentBus2 426 674 16 16 items/ComponentBus2 460 674 16 16
items/ComponentBus3 443 674 16 16 items/ComponentBus3 477 674 16 16
items/DataCard0 49 526 16 128 items/DataCard0 49 526 16 128
items/DataCard1 66 526 16 128 items/DataCard1 66 526 16 128
items/DataCard2 83 526 16 128 items/DataCard2 83 526 16 128
items/DebugCard 460 674 16 16 items/DebugCard 494 674 16 16
items/DiskDriveMountable 477 674 16 16 items/DiskDriveMountable 511 674 16 16
items/EEPROM 494 674 16 16 items/EEPROM 528 674 16 16
items/FloppyDisk_dyeBlack 511 674 16 16 items/FloppyDisk_dyeBlack 545 674 16 16
items/FloppyDisk_dyeBlue 528 674 16 16 items/FloppyDisk_dyeBlue 562 674 16 16
items/FloppyDisk_dyeBrown 545 674 16 16 items/FloppyDisk_dyeBrown 579 674 16 16
items/FloppyDisk_dyeCyan 562 674 16 16 items/FloppyDisk_dyeCyan 596 674 16 16
items/FloppyDisk_dyeGray 579 674 16 16 items/FloppyDisk_dyeGray 613 674 16 16
items/FloppyDisk_dyeGreen 596 674 16 16 items/FloppyDisk_dyeGreen 630 674 16 16
items/FloppyDisk_dyeLightBlue 613 674 16 16 items/FloppyDisk_dyeLightBlue 647 674 16 16
items/FloppyDisk_dyeLightGray 630 674 16 16 items/FloppyDisk_dyeLightGray 664 674 16 16
items/FloppyDisk_dyeLime 647 674 16 16 items/FloppyDisk_dyeLime 681 674 16 16
items/FloppyDisk_dyeMagenta 664 674 16 16 items/FloppyDisk_dyeMagenta 698 674 16 16
items/FloppyDisk_dyeOrange 681 674 16 16 items/FloppyDisk_dyeOrange 715 674 16 16
items/FloppyDisk_dyePink 698 674 16 16 items/FloppyDisk_dyePink 732 674 16 16
items/FloppyDisk_dyePurple 715 674 16 16 items/FloppyDisk_dyePurple 749 674 16 16
items/FloppyDisk_dyeRed 732 674 16 16 items/FloppyDisk_dyeRed 766 674 16 16
items/FloppyDisk_dyeWhite 749 674 16 16 items/FloppyDisk_dyeWhite 783 674 16 16
items/FloppyDisk_dyeYellow 766 674 16 16 items/FloppyDisk_dyeYellow 800 674 16 16
items/GraphicsCard0 783 674 16 16 items/GraphicsCard0 817 674 16 16
items/GraphicsCard1 800 674 16 16 items/GraphicsCard1 834 674 16 16
items/GraphicsCard2 817 674 16 16 items/GraphicsCard2 851 674 16 16
items/HardDiskDrive0 834 674 16 16 items/HardDiskDrive0 868 674 16 16
items/HardDiskDrive1 851 674 16 16 items/HardDiskDrive1 885 674 16 16
items/HardDiskDrive2 868 674 16 16 items/HardDiskDrive2 902 674 16 16
items/InternetCard 143 567 16 32 items/InternetCard 143 567 16 32
items/LinkedCard 100 655 16 96 items/LinkedCard 100 655 16 96
items/Memory0 885 674 16 16 items/Memory0 919 674 16 16
items/Memory1 902 674 16 16 items/Memory1 936 674 16 16
items/Memory2 919 674 16 16 items/Memory2 953 674 16 16
items/Memory3 936 674 16 16 items/Memory3 970 674 16 16
items/Memory4 953 674 16 16 items/Memory4 987 674 16 16
items/Memory5 970 674 16 16 items/Memory5 1004 674 16 16
items/Memory6 987 674 16 16 items/Memory6 134 707 16 16
items/NetworkCard 1004 674 16 16 items/NetworkCard 151 707 16 16
items/OcelotCard 100 526 16 128 items/OcelotCard 100 526 16 128
items/RedstoneCard0 134 707 16 16 items/RedstoneCard0 168 707 16 16
items/RedstoneCard1 151 707 16 16 items/RedstoneCard1 185 707 16 16
items/SelfDestructingCard 160 567 16 32 items/SelfDestructingCard 160 567 16 32
items/Server0 168 707 16 16 items/Server0 202 707 16 16
items/Server1 185 707 16 16 items/Server1 219 707 16 16
items/Server2 202 707 16 16 items/Server2 236 707 16 16
items/Server3 219 707 16 16 items/Server3 253 707 16 16
items/SoundCard 117 526 16 128 items/SoundCard 117 526 16 128
items/TapeCopper 236 707 16 16 items/TapeCopper 270 707 16 16
items/TapeDiamond 253 707 16 16 items/TapeDiamond 287 707 16 16
items/TapeGold 270 707 16 16 items/TapeGold 304 707 16 16
items/TapeGreg 287 707 16 16 items/TapeGreg 321 707 16 16
items/TapeIg 304 707 16 16 items/TapeIg 338 707 16 16
items/TapeIron 321 707 16 16 items/TapeIron 355 707 16 16
items/TapeNetherStar 338 707 16 16 items/TapeNetherStar 372 707 16 16
items/TapeSteel 355 707 16 16 items/TapeSteel 389 707 16 16
items/WirelessNetworkCard0 372 707 16 16 items/WirelessNetworkCard0 406 707 16 16
items/WirelessNetworkCard1 389 707 16 16 items/WirelessNetworkCard1 423 707 16 16
light-panel/BookmarkLeft 235 540 18 14 light-panel/BookmarkLeft 269 540 18 14
light-panel/BookmarkRight 197 600 20 14 light-panel/BookmarkRight 197 600 20 14
light-panel/BorderB 296 344 4 4 light-panel/BorderB 296 344 4 4
light-panel/BorderL 284 353 4 2 light-panel/BorderL 284 353 4 2
@ -196,94 +198,94 @@ light-panel/CornerTR 326 344 4 4
light-panel/Fill 410 344 2 2 light-panel/Fill 410 344 2 2
light-panel/Vent 279 305 2 38 light-panel/Vent 279 305 2 38
nodes/Cable 300 316 8 8 nodes/Cable 300 316 8 8
nodes/Camera 406 707 16 16 nodes/Camera 440 707 16 16
nodes/Chest 438 434 14 14 nodes/Chest 438 434 14 14
nodes/HologramProjector0 423 707 16 16 nodes/HologramProjector0 457 707 16 16
nodes/HologramProjector1 440 707 16 16 nodes/HologramProjector1 474 707 16 16
nodes/IronNoteBlock 457 707 16 16 nodes/IronNoteBlock 491 707 16 16
nodes/Lamp 474 707 16 16 nodes/Lamp 508 707 16 16
nodes/LampFrame 491 707 16 16 nodes/LampFrame 525 707 16 16
nodes/LampGlow 49 305 128 128 nodes/LampGlow 49 305 128 128
nodes/NewNode 508 707 16 16 nodes/NewNode 542 707 16 16
nodes/NoteBlock 525 707 16 16 nodes/NoteBlock 559 707 16 16
nodes/OpenFMRadio 542 707 16 16 nodes/OpenFMRadio 576 707 16 16
nodes/Relay 559 707 16 16 nodes/Relay 593 707 16 16
nodes/TapeDrive 576 707 16 16 nodes/TapeDrive 610 707 16 16
nodes/computer/Default 593 707 16 16 nodes/computer/Default 627 707 16 16
nodes/computer/DiskActivity 610 707 16 16 nodes/computer/DiskActivity 644 707 16 16
nodes/computer/Error 627 707 16 16 nodes/computer/Error 661 707 16 16
nodes/computer/On 644 707 16 16 nodes/computer/On 678 707 16 16
nodes/disk-drive/Default 661 707 16 16 nodes/disk-drive/Default 695 707 16 16
nodes/disk-drive/DiskActivity 678 707 16 16 nodes/disk-drive/DiskActivity 712 707 16 16
nodes/disk-drive/Floppy 695 707 16 16 nodes/disk-drive/Floppy 729 707 16 16
nodes/holidays/Christmas 134 674 32 32 nodes/holidays/Christmas 134 674 32 32
nodes/holidays/Halloween 167 674 32 32 nodes/holidays/Halloween 167 674 32 32
nodes/holidays/Valentines 200 674 32 32 nodes/holidays/Valentines 200 674 32 32
nodes/microcontroller/Default 712 707 16 16 nodes/microcontroller/Default 746 707 16 16
nodes/microcontroller/Error 729 707 16 16 nodes/microcontroller/Error 763 707 16 16
nodes/microcontroller/On 746 707 16 16 nodes/microcontroller/On 780 707 16 16
nodes/ocelot-block/Default 117 655 16 80 nodes/ocelot-block/Default 117 655 16 80
nodes/ocelot-block/Rx 763 707 16 16 nodes/ocelot-block/Rx 797 707 16 16
nodes/ocelot-block/Tx 780 707 16 16 nodes/ocelot-block/Tx 814 707 16 16
nodes/rack/Default 797 707 16 16 nodes/rack/Default 831 707 16 16
nodes/rack/Empty 814 707 16 16 nodes/rack/Empty 848 707 16 16
nodes/rack/drive/0/Default 831 707 16 16 nodes/rack/drive/0/Default 865 707 16 16
nodes/rack/drive/0/DiskActivity 848 707 16 16 nodes/rack/drive/0/DiskActivity 882 707 16 16
nodes/rack/drive/0/Floppy 865 707 16 16 nodes/rack/drive/0/Floppy 899 707 16 16
nodes/rack/drive/1/Default 882 707 16 16 nodes/rack/drive/1/Default 916 707 16 16
nodes/rack/drive/1/DiskActivity 899 707 16 16 nodes/rack/drive/1/DiskActivity 933 707 16 16
nodes/rack/drive/1/Floppy 916 707 16 16 nodes/rack/drive/1/Floppy 950 707 16 16
nodes/rack/drive/2/Default 933 707 16 16 nodes/rack/drive/2/Default 967 707 16 16
nodes/rack/drive/2/DiskActivity 950 707 16 16 nodes/rack/drive/2/DiskActivity 984 707 16 16
nodes/rack/drive/2/Floppy 967 707 16 16 nodes/rack/drive/2/Floppy 1001 707 16 16
nodes/rack/drive/3/Default 984 707 16 16 nodes/rack/drive/3/Default 266 655 16 16
nodes/rack/drive/3/DiskActivity 1001 707 16 16 nodes/rack/drive/3/DiskActivity 283 655 16 16
nodes/rack/drive/3/Floppy 266 655 16 16 nodes/rack/drive/3/Floppy 300 655 16 16
nodes/rack/drive/Floppy 283 655 16 16 nodes/rack/drive/Floppy 317 655 16 16
nodes/rack/server/0/Default 300 655 16 16 nodes/rack/server/0/Default 334 655 16 16
nodes/rack/server/0/DiskActivity 317 655 16 16 nodes/rack/server/0/DiskActivity 351 655 16 16
nodes/rack/server/0/Error 334 655 16 16 nodes/rack/server/0/Error 368 655 16 16
nodes/rack/server/0/NetworkActivity 351 655 16 16 nodes/rack/server/0/NetworkActivity 385 655 16 16
nodes/rack/server/0/On 368 655 16 16 nodes/rack/server/0/On 402 655 16 16
nodes/rack/server/1/Default 385 655 16 16 nodes/rack/server/1/Default 419 655 16 16
nodes/rack/server/1/DiskActivity 402 655 16 16 nodes/rack/server/1/DiskActivity 436 655 16 16
nodes/rack/server/1/Error 419 655 16 16 nodes/rack/server/1/Error 453 655 16 16
nodes/rack/server/1/NetworkActivity 436 655 16 16 nodes/rack/server/1/NetworkActivity 470 655 16 16
nodes/rack/server/1/On 453 655 16 16 nodes/rack/server/1/On 487 655 16 16
nodes/rack/server/2/Default 470 655 16 16 nodes/rack/server/2/Default 504 655 16 16
nodes/rack/server/2/DiskActivity 487 655 16 16 nodes/rack/server/2/DiskActivity 521 655 16 16
nodes/rack/server/2/Error 504 655 16 16 nodes/rack/server/2/Error 538 655 16 16
nodes/rack/server/2/NetworkActivity 521 655 16 16 nodes/rack/server/2/NetworkActivity 555 655 16 16
nodes/rack/server/2/On 538 655 16 16 nodes/rack/server/2/On 572 655 16 16
nodes/rack/server/3/Default 555 655 16 16 nodes/rack/server/3/Default 589 655 16 16
nodes/rack/server/3/DiskActivity 572 655 16 16 nodes/rack/server/3/DiskActivity 606 655 16 16
nodes/rack/server/3/Error 589 655 16 16 nodes/rack/server/3/Error 623 655 16 16
nodes/rack/server/3/NetworkActivity 606 655 16 16 nodes/rack/server/3/NetworkActivity 640 655 16 16
nodes/rack/server/3/On 623 655 16 16 nodes/rack/server/3/On 657 655 16 16
nodes/raid/0/DiskActivity 640 655 16 16 nodes/raid/0/DiskActivity 674 655 16 16
nodes/raid/0/Error 657 655 16 16 nodes/raid/0/Error 691 655 16 16
nodes/raid/1/DiskActivity 674 655 16 16 nodes/raid/1/DiskActivity 708 655 16 16
nodes/raid/1/Error 691 655 16 16 nodes/raid/1/Error 725 655 16 16
nodes/raid/2/DiskActivity 708 655 16 16 nodes/raid/2/DiskActivity 742 655 16 16
nodes/raid/2/Error 725 655 16 16 nodes/raid/2/Error 759 655 16 16
nodes/raid/Default 742 655 16 16 nodes/raid/Default 776 655 16 16
nodes/screen/BottomLeft 759 655 16 16 nodes/screen/BottomLeft 793 655 16 16
nodes/screen/BottomMiddle 776 655 16 16 nodes/screen/BottomMiddle 810 655 16 16
nodes/screen/BottomRight 793 655 16 16 nodes/screen/BottomRight 827 655 16 16
nodes/screen/ColumnBottom 810 655 16 16 nodes/screen/ColumnBottom 844 655 16 16
nodes/screen/ColumnMiddle 827 655 16 16 nodes/screen/ColumnMiddle 861 655 16 16
nodes/screen/ColumnTop 844 655 16 16 nodes/screen/ColumnTop 878 655 16 16
nodes/screen/Middle 861 655 16 16 nodes/screen/Middle 895 655 16 16
nodes/screen/MiddleLeft 878 655 16 16 nodes/screen/MiddleLeft 912 655 16 16
nodes/screen/MiddleRight 895 655 16 16 nodes/screen/MiddleRight 929 655 16 16
nodes/screen/PowerOnOverlay 912 655 16 16 nodes/screen/PowerOnOverlay 946 655 16 16
nodes/screen/RowLeft 929 655 16 16 nodes/screen/RowLeft 963 655 16 16
nodes/screen/RowMiddle 946 655 16 16 nodes/screen/RowMiddle 980 655 16 16
nodes/screen/RowRight 963 655 16 16 nodes/screen/RowRight 997 655 16 16
nodes/screen/Standalone 980 655 16 16 nodes/screen/Standalone 201 540 16 16
nodes/screen/TopLeft 997 655 16 16 nodes/screen/TopLeft 218 540 16 16
nodes/screen/TopMiddle 201 540 16 16 nodes/screen/TopMiddle 235 540 16 16
nodes/screen/TopRight 218 540 16 16 nodes/screen/TopRight 252 540 16 16
panel/BorderB 331 344 4 4 panel/BorderB 331 344 4 4
panel/BorderL 289 353 4 2 panel/BorderL 289 353 4 2
panel/BorderR 336 344 4 4 panel/BorderR 336 344 4 4

View File

@ -1,7 +1,7 @@
package ocelot.desktop.util package ocelot.desktop.graphics
import ocelot.desktop.geometry.Rect2D import ocelot.desktop.geometry.Rect2D
import ocelot.desktop.graphics.Texture import ocelot.desktop.util.{Logging, Resource}
import org.lwjgl.opengl.GL11 import org.lwjgl.opengl.GL11
import totoro.ocelot.brain.util.FontUtils import totoro.ocelot.brain.util.FontUtils
@ -15,8 +15,8 @@ import scala.io.{Codec, Source}
class Font(val name: String, val fontSize: Int) extends Resource with Logging { class Font(val name: String, val fontSize: Int) extends Resource with Logging {
val AtlasWidth = 4096 val AtlasWidth = 4096
val AtlasHeight = 4096 val AtlasHeight = 4096
var glyphCount = 0 private var glyphCount = 0
var outOfRangeGlyphCount = 0 private var outOfRangeGlyphCount = 0
private val atlas: BufferedImage = { private val atlas: BufferedImage = {
val icmArr = Array(0.toByte, 0xff.toByte) val icmArr = Array(0.toByte, 0xff.toByte)
@ -134,3 +134,14 @@ class Font(val name: String, val fontSize: Int) extends Resource with Logging {
texture.freeResource() texture.freeResource()
} }
} }
object Font extends Resource {
val NormalFont = new Font("unscii-16", 16)
val SmallFont = new Font("unscii-8", 8)
override def freeResource(): Unit = {
super.freeResource()
NormalFont.freeResource()
SmallFont.freeResource()
}
}

View File

@ -6,7 +6,7 @@ import ocelot.desktop.graphics.Texture.MinFilteringMode
import ocelot.desktop.graphics.mesh.{Mesh2D, MeshInstance2D, MeshVertex2D} import ocelot.desktop.graphics.mesh.{Mesh2D, MeshInstance2D, MeshVertex2D}
import ocelot.desktop.graphics.render.InstanceRenderer import ocelot.desktop.graphics.render.InstanceRenderer
import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.UiHandler
import ocelot.desktop.util.{Font, Logging, Resource, Spritesheet} import ocelot.desktop.util.{Logging, Resource, Spritesheet}
import org.lwjgl.BufferUtils import org.lwjgl.BufferUtils
import org.lwjgl.opengl.{ARBFramebufferObject, GL11, GL21, GL30} import org.lwjgl.opengl.{ARBFramebufferObject, GL11, GL21, GL30}
@ -25,9 +25,7 @@ class Graphics(private var width: Int, private var height: Int, private var scal
private val shaderProgram = new ShaderProgram("general") private val shaderProgram = new ShaderProgram("general")
private val renderer = new InstanceRenderer[MeshVertex2D, MeshInstance2D](Mesh2D.quad, MeshInstance2D, shaderProgram) private val renderer = new InstanceRenderer[MeshVertex2D, MeshInstance2D](Mesh2D.quad, MeshInstance2D, shaderProgram)
private[graphics] val normalFont = new Font("unscii-16", 16) private var _font: Font = Font.NormalFont
private val smallFont = new Font("unscii-8", 8)
private var _font: Font = normalFont
private var oldFont: Font = _font private var oldFont: Font = _font
private val stack = mutable.Stack[GraphicsState](GraphicsState()) private val stack = mutable.Stack[GraphicsState](GraphicsState())
@ -87,8 +85,6 @@ class Graphics(private var width: Int, private var height: Int, private var scal
offscreenTexture.freeResource() offscreenTexture.freeResource()
Spritesheet.freeResource() Spritesheet.freeResource()
smallFont.freeResource()
normalFont.freeResource()
renderer.freeResource() renderer.freeResource()
shaderProgram.freeResource() shaderProgram.freeResource()
screenShaderProgram.freeResource() screenShaderProgram.freeResource()
@ -97,15 +93,15 @@ class Graphics(private var width: Int, private var height: Int, private var scal
def font: Font = _font def font: Font = _font
def setNormalFont(): Unit = { def setNormalFont(): Unit = {
if (_font == normalFont) return if (_font == Font.NormalFont) return
flush() flush()
_font = normalFont _font = Font.NormalFont
} }
def setSmallFont(): Unit = { def setSmallFont(): Unit = {
if (_font == smallFont) return if (_font == Font.SmallFont) return
flush() flush()
_font = smallFont _font = Font.SmallFont
} }
def save(): Unit = { def save(): Unit = {

View File

@ -205,6 +205,8 @@ object IconSource {
val Delete: IconSource = get("Delete") val Delete: IconSource = get("Delete")
val Label: IconSource = get("Label") val Label: IconSource = get("Label")
val Copy: IconSource = get("Copy") val Copy: IconSource = get("Copy")
val Cut: IconSource = get("Cut")
val Paste: IconSource = get("Paste")
val AspectRatio: IconSource = get("AspectRatio") val AspectRatio: IconSource = get("AspectRatio")
val Eject: IconSource = get("Eject") val Eject: IconSource = get("Eject")
val Restart: IconSource = get("Restart") val Restart: IconSource = get("Restart")

View File

@ -4,7 +4,7 @@ import ocelot.desktop.color.{Color, RGBAColorNorm}
import ocelot.desktop.geometry.Transform2D import ocelot.desktop.geometry.Transform2D
import ocelot.desktop.graphics.mesh.{Mesh2D, MeshInstance2D, MeshVertex2D} import ocelot.desktop.graphics.mesh.{Mesh2D, MeshInstance2D, MeshVertex2D}
import ocelot.desktop.graphics.render.InstanceRenderer import ocelot.desktop.graphics.render.InstanceRenderer
import ocelot.desktop.util.{Font, Resource, Spritesheet} import ocelot.desktop.util.{Resource, Spritesheet}
import org.lwjgl.opengl.{ARBFramebufferObject, GL11, GL21, GL30} import org.lwjgl.opengl.{ARBFramebufferObject, GL11, GL21, GL30}
import java.nio.ByteBuffer import java.nio.ByteBuffer
@ -19,7 +19,7 @@ class ScreenViewport(graphics: Graphics, private var _width: Int, private var _h
private val shaderProgram = graphics.screenShaderProgram private val shaderProgram = graphics.screenShaderProgram
private val renderer = new InstanceRenderer[MeshVertex2D, MeshInstance2D](Mesh2D.quad, MeshInstance2D, shaderProgram) private val renderer = new InstanceRenderer[MeshVertex2D, MeshInstance2D](Mesh2D.quad, MeshInstance2D, shaderProgram)
private val _font = graphics.normalFont private val _font = Font.NormalFont
private val spriteRect = Spritesheet.sprites("Empty") private val spriteRect = Spritesheet.sprites("Empty")
private val emptySpriteTrans = private val emptySpriteTrans =

View File

@ -3,7 +3,7 @@ package ocelot.desktop.ui
import buildinfo.BuildInfo import buildinfo.BuildInfo
import ocelot.desktop.audio.{Audio, SoundBuffers, SoundSource} import ocelot.desktop.audio.{Audio, SoundBuffers, SoundSource}
import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D} import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D}
import ocelot.desktop.graphics.Graphics import ocelot.desktop.graphics.{Font, Graphics}
import ocelot.desktop.ui.event.handlers.HoverHandler import ocelot.desktop.ui.event.handlers.HoverHandler
import ocelot.desktop.ui.event.sources.{BrainEvents, KeyEvents, MouseEvents, ScrollEvents} import ocelot.desktop.ui.event.sources.{BrainEvents, KeyEvents, MouseEvents, ScrollEvents}
import ocelot.desktop.ui.event.{Capturing, CapturingEvent, Dispatchable, HoverEvent, MouseEvent} import ocelot.desktop.ui.event.{Capturing, CapturingEvent, Dispatchable, HoverEvent, MouseEvent}
@ -80,7 +80,7 @@ object UiHandler extends Logging {
_clipboard.getData(DataFlavor.stringFlavor).toString _clipboard.getData(DataFlavor.stringFlavor).toString
} catch { } catch {
case _: UnsupportedFlavorException => case _: UnsupportedFlavorException =>
logger.debug("Trying to paste non-Unicode content from clipboard! Replaced with empty string.") logger.debug("Trying to paste non-Unicode content from clipboard! Replaced with an empty string.")
"" ""
} }
} }
@ -418,6 +418,7 @@ object UiHandler extends Logging {
KeyEvents.destroy() KeyEvents.destroy()
MouseEvents.destroy() MouseEvents.destroy()
graphics.freeResource() graphics.freeResource()
Font.freeResource()
Audio.removeAllSources() Audio.removeAllSources()
SoundBuffers.freeResource() SoundBuffers.freeResource()
Display.destroy() Display.destroy()
@ -430,10 +431,9 @@ object UiHandler extends Logging {
} }
} }
private def dispatchCapturing(target: Widget)(event: CapturingEvent): Unit = { private def dispatchCapturing(dispatchOrder: DispatchOrder)(event: CapturingEvent): Unit = {
val ancestors = target.ancestors.toSeq dispatchEvent(dispatchOrder.capture)(Capturing(event))
dispatchEvent(ancestors.reverseIterator ++ Some(target))(Capturing(event)) dispatchEvent(dispatchOrder.targets.reverseIterator)(event)
dispatchEvent(Some(target))(event)
} }
private def dispatchBrainEvents(): Unit = { private def dispatchBrainEvents(): Unit = {
@ -474,15 +474,13 @@ object UiHandler extends Logging {
MouseEvents.releaseButtons() MouseEvents.releaseButtons()
} }
val broadcastDispatchOrder = DispatchOrder.broadcast
// TODO: dispatch to the focused widget instead of broadcasting to the entire hierarchy. // TODO: dispatch to the focused widget instead of broadcasting to the entire hierarchy.
for (event <- KeyEvents.events) { for (event <- KeyEvents.events) {
dispatchEvent(hierarchy)(Capturing(event)) dispatchCapturing(broadcastDispatchOrder)(event)
dispatchEvent()(event)
} }
MouseEvents.events
.foreach(dispatchEvent(hierarchy.reverseIterator.filter(w => w.enabled && w.receiveAllMouseEvents)))
val scrollTarget = hierarchy.reverseIterator val scrollTarget = hierarchy.reverseIterator
.find(w => w.receiveScrollEvents && w.clippedBounds.contains(mousePos)) .find(w => w.receiveScrollEvents && w.clippedBounds.contains(mousePos))
@ -490,30 +488,33 @@ object UiHandler extends Logging {
.find(w => w.enabled && w.receiveMouseEvents && w.clippedBounds.contains(mousePos)) .find(w => w.enabled && w.receiveMouseEvents && w.clippedBounds.contains(mousePos))
for (scrollTarget <- scrollTarget) { for (scrollTarget <- scrollTarget) {
ScrollEvents.events.foreach(dispatchCapturing(scrollTarget)) ScrollEvents.events.foreach(dispatchCapturing(DispatchOrder.resolve(scrollTarget)))
} }
val mouseEventDispatchOrder = DispatchOrder.resolve(
hierarchy
.iterator
.filter(w => mouseTarget.contains(w) || w.enabled && w.receiveAllMouseEvents)
.toSeq
)
for (event <- MouseEvents.events) { for (event <- MouseEvents.events) {
if (event.state == MouseEvent.State.Pressed) { if (event.state == MouseEvent.State.Pressed) {
for (mouseTarget <- mouseTarget) { dispatchCapturing(mouseEventDispatchOrder)(event)
dispatchCapturing(mouseTarget)(event)
}
} else { } else {
dispatchEvent(hierarchy)(Capturing(event)) dispatchCapturing(broadcastDispatchOrder)(event)
dispatchEvent(hierarchy.reverseIterator)(event)
} }
} }
hierarchy.reverseIterator.foreach { val hoverLeaveDispatchOrder = DispatchOrder.resolve(hierarchy.iterator.collect({
case h: HoverHandler if !mouseTarget.contains(h) && h._mouseOver.update(false) => case h: HoverHandler if !mouseTarget.contains(h) && h._mouseOver.update(false) => h
dispatchCapturing(h)(HoverEvent(HoverEvent.State.Leave)) }).toSeq)
case _ => dispatchCapturing(hoverLeaveDispatchOrder)(HoverEvent(HoverEvent.State.Leave))
}
mouseTarget.foreach { mouseTarget.foreach {
case h: HoverHandler if h._mouseOver.update(true) => case h: HoverHandler if h._mouseOver.update(true) =>
dispatchCapturing(h)(HoverEvent(HoverEvent.State.Enter)) dispatchCapturing(DispatchOrder.resolve(h))(HoverEvent(HoverEvent.State.Enter))
case _ => case _ =>
} }
@ -548,4 +549,43 @@ object UiHandler extends Logging {
graphics.flush() graphics.flush()
graphics.update() graphics.update()
} }
/**
* Provides a dispatch order for [[CapturingEvent]]s.
* @param capture widgets to deliver events during capture phase, in hierarchy pre-order.
* @param targets widgets to deliver events during target phase, in hierarchy pre-order.
*/
private case class DispatchOrder(capture: collection.Seq[Widget], targets: collection.Seq[Widget])
private object DispatchOrder {
/**
* Creates a dispatch order for delivering events to multiple targets.
* @param targets event targets, in hierarchy pre-order.
*/
def resolve(targets: collection.Seq[Widget]): DispatchOrder = {
val capture = mutable.Set.empty[Widget]
for (widget <- targets) {
capture += widget
val ancestors = widget.ancestors
while (ancestors.hasNext && capture.add(ancestors.next())) {}
}
DispatchOrder(
capture = hierarchy.iterator.filter(capture).toSeq,
targets = targets,
)
}
/**
* Creates a dispatch order for delivering event to a single `target`.
*/
def resolve(target: Widget): DispatchOrder = {
val capture = (Iterator(target) ++ target.ancestors.iterator).toSeq
DispatchOrder(capture, Seq(target))
}
def broadcast: DispatchOrder = DispatchOrder(hierarchy, hierarchy)
}
} }

View File

@ -0,0 +1,5 @@
package ocelot.desktop.ui.event
import ocelot.desktop.geometry.Vector2D
case class DoubleClickEvent(button: MouseEvent.Button.Value, mousePos: Vector2D) extends Event

View File

@ -1,5 +1,8 @@
package ocelot.desktop.ui.event package ocelot.desktop.ui.event
case class MouseEvent(state: MouseEvent.State.Value, button: MouseEvent.Button.Value)(val stateChanged: Boolean)
extends CapturingEvent
object MouseEvent { object MouseEvent {
object State extends Enumeration { object State extends Enumeration {
val Pressed, Released = Value val Pressed, Released = Value
@ -10,6 +13,14 @@ object MouseEvent {
val Right: Button.Value = Value(1) val Right: Button.Value = Value(1)
val Middle: Button.Value = Value(2) val Middle: Button.Value = Value(2)
} }
}
case class MouseEvent(state: MouseEvent.State.Value, button: MouseEvent.Button.Value) extends CapturingEvent object StateChanged {
def unapply(event: MouseEvent): Option[(State.Value, Button.Value)] = {
if (event.stateChanged) {
MouseEvent.unapply(event)
} else {
None
}
}
}
}

View File

@ -2,8 +2,8 @@ package ocelot.desktop.ui.event.handlers
import ocelot.desktop.geometry.Vector2D import ocelot.desktop.geometry.Vector2D
import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.handlers.MouseHandler.Tolerance import ocelot.desktop.ui.event.handlers.MouseHandler.{DoubleClickTime, Tolerance}
import ocelot.desktop.ui.event.{ClickEvent, DragEvent, MouseEvent} import ocelot.desktop.ui.event.{ClickEvent, DoubleClickEvent, DragEvent, MouseEvent}
import ocelot.desktop.ui.widget.Widget import ocelot.desktop.ui.widget.Widget
import scala.collection.mutable import scala.collection.mutable
@ -13,13 +13,23 @@ trait MouseHandler extends Widget {
private val prevPositions = new mutable.HashMap[MouseEvent.Button.Value, Vector2D]() private val prevPositions = new mutable.HashMap[MouseEvent.Button.Value, Vector2D]()
private val dragButtons = new mutable.HashSet[MouseEvent.Button.Value]() private val dragButtons = new mutable.HashSet[MouseEvent.Button.Value]()
private var lastClickPosition: Vector2D = Vector2D.Zero
private var lastClickTime: Long = 0
private var lastClickButton = MouseEvent.Button.Left
override def receiveMouseEvents: Boolean = receiveClickEvents || receiveDragEvents override def receiveMouseEvents: Boolean = receiveClickEvents || receiveDragEvents
protected def receiveClickEvents: Boolean = false protected def receiveClickEvents: Boolean = false
protected def receiveDragEvents: Boolean = false protected def receiveDragEvents: Boolean = false
/** If `true`, a [[ClickEvent]] will be registered even if the mouse button is released /**
* If `true`, drag events, once they start, will be sent on every update cycle even if the mouse does not move.
*/
protected def spamDragEvents: Boolean = true
/**
* If `true`, a [[ClickEvent]] will be registered even if the mouse button is released
* outside the tolerance threshold, as long as it stays within the widget's bounds. * outside the tolerance threshold, as long as it stays within the widget's bounds.
*/ */
protected def allowClickReleaseOutsideThreshold: Boolean = !receiveDragEvents protected def allowClickReleaseOutsideThreshold: Boolean = !receiveDragEvents
@ -39,7 +49,7 @@ trait MouseHandler extends Widget {
if (allowClickReleaseOutsideThreshold) { if (allowClickReleaseOutsideThreshold) {
clippedBounds.contains(mousePos) clippedBounds.contains(mousePos)
} else { } else {
(p - mousePos).lengthSquared < Tolerance * Tolerance withinTolerance(p, mousePos)
} }
}) })
) )
@ -50,11 +60,23 @@ trait MouseHandler extends Widget {
if (clicked) { if (clicked) {
handleEvent(ClickEvent(button, mousePos)) handleEvent(ClickEvent(button, mousePos))
val inTimeForDoubleClick = (System.currentTimeMillis() - lastClickTime) < DoubleClickTime * 1000
val sameButton = lastClickButton == button
val roughlySamePosition = withinTolerance(lastClickPosition, mousePos)
if (inTimeForDoubleClick && sameButton && roughlySamePosition) {
handleEvent(DoubleClickEvent(button, mousePos))
}
lastClickTime = System.currentTimeMillis()
lastClickPosition = mousePos
lastClickButton = button
} }
startPositions.remove(button) startPositions.remove(button)
} }
private def withinTolerance(a: Vector2D, b: Vector2D): Boolean = (b - a).lengthSquared < Tolerance * Tolerance
override def update(): Unit = { override def update(): Unit = {
super.update() super.update()
@ -65,14 +87,14 @@ trait MouseHandler extends Widget {
val mousePos = UiHandler.mousePosition val mousePos = UiHandler.mousePosition
for ((button, startPos) <- startPositions) { for ((button, startPos) <- startPositions) {
if (!dragButtons.contains(button) && (startPos - mousePos).lengthSquared > Tolerance * Tolerance) { if (!dragButtons.contains(button) && !withinTolerance(startPos, mousePos)) {
handleEvent(DragEvent(DragEvent.State.Start, button, mousePos, startPos, Vector2D(0, 0))) handleEvent(DragEvent(DragEvent.State.Start, button, mousePos, startPos, Vector2D(0, 0)))
dragButtons += button dragButtons += button
prevPositions += (button -> mousePos) prevPositions += (button -> mousePos)
} }
} }
dragButtons.foreach(button => { for (button <- dragButtons if spamDragEvents || prevPositions(button) != mousePos) {
handleEvent( handleEvent(
DragEvent( DragEvent(
DragEvent.State.Drag, DragEvent.State.Drag,
@ -82,12 +104,13 @@ trait MouseHandler extends Widget {
mousePos - prevPositions(button), mousePos - prevPositions(button),
) )
) )
prevPositions(button) = mousePos prevPositions(button) = mousePos
} }
)
} }
} }
object MouseHandler { object MouseHandler {
private val Tolerance = 8 private val Tolerance = 8
private val DoubleClickTime = 0.2
} }

View File

@ -26,13 +26,13 @@ object MouseEvents {
if (MouseEvent.Button.values.map(_.id).contains(buttonIdx)) { if (MouseEvent.Button.values.map(_.id).contains(buttonIdx)) {
val button = MouseEvent.Button(buttonIdx) val button = MouseEvent.Button(buttonIdx)
val state = if (Mouse.getEventButtonState) MouseEvent.State.Pressed else MouseEvent.State.Released val state = if (Mouse.getEventButtonState) MouseEvent.State.Pressed else MouseEvent.State.Released
_events += MouseEvent(state, button) val changed = state match {
state match {
case MouseEvent.State.Pressed => case MouseEvent.State.Pressed =>
_pressedButtons += button _pressedButtons.add(button)
case MouseEvent.State.Released => case MouseEvent.State.Released =>
_pressedButtons -= button _pressedButtons.remove(button)
} }
_events += MouseEvent(state, button)(changed)
} }
val delta = Mouse.getEventDWheel val delta = Mouse.getEventDWheel
@ -49,7 +49,7 @@ object MouseEvents {
def releaseButtons(): Unit = { def releaseButtons(): Unit = {
for (button <- pressedButtons) { for (button <- pressedButtons) {
_events += MouseEvent(MouseEvent.State.Released, button) _events += MouseEvent(MouseEvent.State.Released, button)(stateChanged = true)
} }
_pressedButtons.clear() _pressedButtons.clear()

View File

@ -16,8 +16,8 @@ class Button(tooltip: Option[Tooltip] = None) extends Widget with MouseHandler w
def this(tooltip: Tooltip) = this(Some(tooltip)) def this(tooltip: Tooltip) = this(Some(tooltip))
protected def colorScheme: ColorScheme = ColorScheme.General protected def colorScheme: ColorScheme = ColorScheme.General
override protected val hoverAnimationColorDefault: Color = colorScheme("ButtonBackground") override protected val HoverAnimationColorDefault: Color = colorScheme("ButtonBackground")
override protected val hoverAnimationColorActive: Color = colorScheme("ButtonBackgroundActive") override protected val HoverAnimationColorActive: Color = colorScheme("ButtonBackgroundActive")
def text: String = "" def text: String = ""

View File

@ -45,7 +45,8 @@ class ChangeSimulationSpeedDialog extends ModalDialog {
override def onInput(text: String): Unit = { override def onInput(text: String): Unit = {
tickInterval = parseInput(text).map { interval => tickInterval = parseInput(text).map { interval =>
inputTPS.setInput(formatTPS(interval)) val tps = formatTPS(interval)
if (inputTPS.text != tps) inputTPS.text = tps
interval interval
} }
} }
@ -66,7 +67,8 @@ class ChangeSimulationSpeedDialog extends ModalDialog {
override def onInput(text: String): Unit = { override def onInput(text: String): Unit = {
tickInterval = parseInput(text).map { interval => tickInterval = parseInput(text).map { interval =>
inputMSPT.setInput(formatMSPT(interval)) val mspt = formatMSPT(interval)
if (inputMSPT.text != mspt) inputMSPT.text = mspt
interval interval
} }
} }

View File

@ -12,8 +12,8 @@ import ocelot.desktop.util.DrawUtils
class Checkbox(val label: String, val initialValue: Boolean = false, val isSmall: Boolean = false) class Checkbox(val label: String, val initialValue: Boolean = false, val isSmall: Boolean = false)
extends Widget with MouseHandler with HoverAnimation { extends Widget with MouseHandler with HoverAnimation {
override protected val hoverAnimationColorDefault: Color = ColorScheme("CheckboxBackground") override protected val HoverAnimationColorDefault: Color = ColorScheme("CheckboxBackground")
override protected val hoverAnimationColorActive: Color = ColorScheme("CheckboxBackgroundActive") override protected val HoverAnimationColorActive: Color = ColorScheme("CheckboxBackgroundActive")
private var _checked: Boolean = initialValue private var _checked: Boolean = initialValue

View File

@ -14,8 +14,8 @@ import ocelot.desktop.util.DrawUtils
class Slider(var value: Float, val text: String, val snapPoints: Int = 0) class Slider(var value: Float, val text: String, val snapPoints: Int = 0)
extends Widget with MouseHandler with HoverAnimation { extends Widget with MouseHandler with HoverAnimation {
override protected val hoverAnimationColorDefault: Color = ColorScheme("SliderBackground") override protected val HoverAnimationColorDefault: Color = ColorScheme("SliderBackground")
override protected val hoverAnimationColorActive: Color = ColorScheme("SliderBackgroundActive") override protected val HoverAnimationColorActive: Color = ColorScheme("SliderBackgroundActive")
def onValueChanged(value: Float): Unit = {} def onValueChanged(value: Float): Unit = {}
def onValueFinal(value: Float): Unit = {} def onValueFinal(value: Float): Unit = {}

View File

@ -3,22 +3,71 @@ package ocelot.desktop.ui.widget
import ocelot.desktop.ColorScheme import ocelot.desktop.ColorScheme
import ocelot.desktop.color.Color import ocelot.desktop.color.Color
import ocelot.desktop.geometry.Size2D import ocelot.desktop.geometry.Size2D
import ocelot.desktop.graphics.Graphics import ocelot.desktop.graphics.{Font, Graphics, IconSource}
import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.handlers.MouseHandler import ocelot.desktop.ui.event.handlers.MouseHandler
import ocelot.desktop.ui.event.sources.KeyEvents import ocelot.desktop.ui.event.sources.KeyEvents
import ocelot.desktop.ui.event.{ClickEvent, KeyEvent, MouseEvent} import ocelot.desktop.ui.event.{DoubleClickEvent, DragEvent, KeyEvent, MouseEvent}
import ocelot.desktop.ui.widget.TextInput.{Cursor, Selection, Text}
import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
import ocelot.desktop.ui.widget.traits.HoverAnimation import ocelot.desktop.ui.widget.traits.HoverAnimation
import ocelot.desktop.util.DrawUtils import ocelot.desktop.util.{DrawUtils, Register, Watcher}
import ocelot.desktop.util.animation.ColorAnimation import ocelot.desktop.util.animation.ColorAnimation
import org.lwjgl.input.Keyboard import org.lwjgl.input.Keyboard
import scala.collection.mutable.ArrayBuffer import java.lang.Character.isWhitespace
class TextInput(val initialText: String = "") extends Widget with MouseHandler with HoverAnimation { class TextInput(val initialText: String = "") extends Widget with MouseHandler with HoverAnimation {
override protected val hoverAnimationColorDefault: Color = ColorScheme("TextInputBackground") private val CursorBlinkTime = 2f
override protected val hoverAnimationColorActive: Color = ColorScheme("TextInputBackgroundActive") private val PlaceholderForegroundColor: Color = ColorScheme("TextInputForegroundDisabled")
private val BackgroundSelectedColor: Color = ColorScheme("TextInputBackgroundSelected")
private val ForegroundSelectedColor: Color = ColorScheme("TextInputForegroundSelected")
override protected val HoverAnimationColorDefault: Color = ColorScheme("TextInputBackground")
override protected val HoverAnimationColorActive: Color = ColorScheme("TextInputBackgroundActive")
// model
private val _text: Text = new Text(initialText.codePoints().toArray)
private val cursor: Cursor = new Cursor()
private val selectionWatcher = new Watcher[Option[Selection]](None)
// updated after all events are processed so that event handlers can refer to the previous position.
private val prevCursorPosition = Register.sampling(cursor.position)
// view
private var isFocused = false
private var scroll = 0f
private var blinkTimer = 0f
private var cursorOffset = 0f
private var selectionOffsets: Option[(Int, Int)] = None
private val enabledRegister = Register.sampling(enabled)
private def selection: Option[Selection] = selectionWatcher.value
private def selection_=(newValue: Option[Selection]): Unit = {
selectionWatcher.value = newValue
}
cursor.onChange(position => {
cursorOffset = charsWidth(_text.chars, 0, position)
blinkTimer = 0
adjustScroll()
})
selectionWatcher.onChange(newValue => {
selectionOffsets = newValue.map {
case Selection.Ordered(start, end) =>
(
charsWidth(_text.chars, 0, start),
charsWidth(_text.chars, 0, end),
)
}
})
private val foregroundAnimation = new ColorAnimation(targetForegroundColor, 7f)
private val borderAnimation = new ColorAnimation(targetBorderColor, 7f)
// public API
// -------------------------------------------------------------------------------------------------------------------
def onInput(text: String): Unit = {} def onInput(text: String): Unit = {}
def onConfirm(): Unit = { def onConfirm(): Unit = {
@ -28,125 +77,28 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
def validator(text: String): Boolean = true def validator(text: String): Boolean = true
final def isInputValid: Boolean = validator(text) final def isInputValid: Boolean = validator(text)
var isFocused = false def text: String = new String(_text.chars, 0, _text.chars.length)
def text_=(value: String): Unit = {
def text: String = chars.mkString _text.chars = value.codePoints().toArray
def text_=(value: String): Unit = chars = value.toCharArray selection = None
cursor.position = cursor.position max 0 min _text.chars.length
protected var placeholder: Array[Char] = "".toCharArray
def placeholder_=(value: String): Unit = placeholder = value.toCharArray
private var cursorPos = 0
private var cursorOffset = 0f
private var scroll = 0f
private val CursorBlinkTime = 2f
private var blinkTimer = 0f
private var chars = initialText.toCharArray
private var textWidth = 0
private var textChanged = false
private val events = ArrayBuffer[TextEvent]()
override protected def receiveClickEvents: Boolean = true
// TODO: implement text selection
// override protected def receiveDragEvents: Boolean = true
override protected def allowClickReleaseOutsideThreshold: Boolean = false
override def receiveAllMouseEvents = true
private var prevEnabled = enabled
eventHandlers += {
case MouseEvent(MouseEvent.State.Released, MouseEvent.Button.Left) =>
if (isFocused && !clippedBounds.contains(UiHandler.mousePosition)) {
unfocus()
}
case ClickEvent(MouseEvent.Button.Left, pos) if enabled =>
focus()
events += new CursorMoveTo(pos.x)
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_LEFT, _) if isFocused =>
events += new CursorMoveLeft
event.consume()
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_RIGHT, _) if isFocused =>
events += new CursorMoveRight
event.consume()
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_HOME, _) if isFocused =>
events += new CursorMoveStart
event.consume()
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_END, _) if isFocused =>
events += new CursorMoveEnd
event.consume()
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_BACK, _) if isFocused =>
events += new EraseCharBack
event.consume()
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_DELETE, _) if isFocused =>
events += new EraseCharFront
event.consume()
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_INSERT, _) if isFocused =>
// TODO: insert the whole clipboard string at once instead of going char-by-char.
for (char <- UiHandler.clipboard.toCharArray) {
events += new WriteChar(char)
}
event.consume()
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_V, _)
if isFocused
&& KeyEvents.isControlDown =>
// TODO: insert the whole clipboard string at once instead of going char-by-char.
for (char <- UiHandler.clipboard.toCharArray) {
events += new WriteChar(char)
}
event.consume()
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_RETURN, _) if isFocused =>
onConfirm()
event.consume()
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, _, char) if isFocused && !char.isControl =>
events += new WriteChar(char)
event.consume()
} }
def setInput(text: String): Unit = { private def selectedText: String = selection match {
this.chars = text.toCharArray case Some(Selection.Ordered(start, end)) => new String(_text.chars, start, end)
cursorPos = 0 case None => ""
cursorOffset = 0
textWidth = 0
textChanged = true
} }
override def minimumSize: Size2D = Size2D(200, 24) protected var placeholder: Array[Int] = Array.empty
override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 24) def placeholder_=(value: String): Unit = placeholder = value.codePoints().toArray
private val foregroundAnimation = new ColorAnimation(targetForegroundColor, 7f)
private val borderAnimation = new ColorAnimation(targetBorderColor, 7f)
private def updateAnimationTargets(): Unit = {
foregroundAnimation.goto(targetForegroundColor)
borderAnimation.goto(targetBorderColor)
}
def focus(): Unit = { def focus(): Unit = {
if (!isFocused) { if (!isFocused) {
if (enabled) { if (enabled) {
isFocused = true isFocused = true
} }
updateAnimationTargets() updateAnimationTargets()
} }
blinkTimer = 0 blinkTimer = 0
} }
@ -157,8 +109,298 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
} }
} }
// widget management
// -------------------------------------------------------------------------------------------------------------------
override def minimumSize: Size2D = Size2D(200, 24)
override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 24)
override def receiveAllMouseEvents = true
override protected def receiveDragEvents: Boolean = true
override protected def receiveClickEvents: Boolean = true
override protected def spamDragEvents: Boolean = false
private def mouseInBounds: Boolean = clippedBounds.contains(UiHandler.mousePosition)
protected def font: Font = Font.NormalFont
private def charWidth(codePoint: Int): Int = font.charWidth(codePoint)
/**
* Calculates given text width in pixels.
* @param from inclusive
* @param to exclusive
*/
// noinspection SameParameterValue
private def charsWidth(chars: Array[Int], from: Int, to: Int): Int = {
var width = 0
for (index <- (from max 0) until (to min chars.length)) {
width += font.charWidth(chars(index))
}
width
}
eventHandlers += {
case MouseEvent.StateChanged(MouseEvent.State.Pressed, _) if enabled =>
val inBounds = mouseInBounds
if (isFocused && !inBounds) unfocus()
if (!isFocused && inBounds) focus()
if (isFocused) {
val pos = pixelToCursorPosition(UiHandler.mousePosition.x - bounds.x)
cursor.position = pos
}
}
eventHandlers += {
case MouseEvent.StateChanged(MouseEvent.State.Pressed, MouseEvent.Button.Right) if isFocused && mouseInBounds =>
val menu = new ContextMenu
if (selection.nonEmpty) {
menu.addEntry(ContextMenuEntry("Cut", IconSource.Icons.Cut) { cutSelection() })
menu.addEntry(ContextMenuEntry("Copy", IconSource.Icons.Copy) { copySelection() })
}
if (UiHandler.clipboard.nonEmpty) {
menu.addEntry(ContextMenuEntry("Paste", IconSource.Icons.Paste) { pasteSelection() })
}
if (_text.chars.nonEmpty) {
if (menu.children.nonEmpty) {
menu.addSeparator()
}
menu.addEntry(ContextMenuEntry("Select all") { selectAll() })
}
root.get.contextMenus.open(menu)
case MouseEvent.StateChanged(MouseEvent.State.Pressed, _) if isFocused && mouseInBounds =>
selection = None
case DoubleClickEvent(MouseEvent.Button.Left, _) if isFocused && mouseInBounds =>
selectWord()
case DragEvent(DragEvent.State.Start | DragEvent.State.Drag, MouseEvent.Button.Left, mouse) if isFocused =>
val pos = if (mouse.y < bounds.y) {
0
} else if (mouse.y > bounds.max.y) {
_text.chars.length
} else {
pixelToCursorPosition(mouse.x - bounds.x)
}
extendSelection(pos, prevCursorPosition.value)
cursor.position = pos
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_LEFT, _) if isFocused =>
handleKeyMovement(selection match {
case Some(Selection.Ordered(start, _)) if !KeyEvents.isShiftDown => start
case _ => cursor.position - 1
})
event.consume()
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_RIGHT, _) if isFocused =>
handleKeyMovement(selection match {
case Some(Selection.Ordered(_, end)) if !KeyEvents.isShiftDown => end
case _ => cursor.position + 1
})
event.consume()
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_HOME, _) if isFocused =>
handleKeyMovement(0)
event.consume()
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_END, _) if isFocused =>
handleKeyMovement(_text.chars.length)
event.consume()
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_BACK, _) if isFocused =>
if (selection.nonEmpty) {
deleteSelection()
} else {
val (lhs, rhs) = _text.chars.splitAt(cursor.position)
if (!lhs.isEmpty) {
_text.chars = lhs.take(lhs.length - 1) ++ rhs
cursor.position -= 1
}
}
event.consume()
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_DELETE, _) if isFocused =>
if (selection.nonEmpty) {
deleteSelection()
} else {
val (lhs, rhs) = _text.chars.splitAt(cursor.position)
if (!rhs.isEmpty) {
_text.chars = lhs ++ rhs.drop(1)
}
}
event.consume()
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_A, _)
if isFocused && KeyEvents.isControlDown =>
selectAll()
event.consume()
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_W, _)
if isFocused && KeyEvents.isControlDown =>
selectWord()
event.consume()
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_C, _)
if isFocused && KeyEvents.isControlDown && selection.nonEmpty =>
copySelection()
event.consume()
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_X, _)
if isFocused && KeyEvents.isControlDown && selection.nonEmpty =>
cutSelection()
event.consume()
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_INSERT, _) if isFocused =>
pasteSelection()
event.consume()
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_V, _)
if isFocused && KeyEvents.isControlDown =>
pasteSelection()
event.consume()
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_RETURN, _) if isFocused =>
onConfirm()
event.consume()
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, _, char) if isFocused && !char.isControl =>
if (selection.nonEmpty) {
deleteSelection()
}
writeChar(char)
event.consume()
}
/**
* Will try to find out which cursor position corresponds to the point on the screen.
* @param x widget-local coordinate
*/
private def pixelToCursorPosition(x: Float): Int = {
val absoluteX = x + scroll - 4
var width = 0
var pos = 0
while (width < absoluteX && pos < _text.chars.length) {
width += charWidth(_text.chars(pos))
if (width < absoluteX) pos += 1
}
pos
}
private def extendSelection(position: Int, cursorPosition: Int = cursor.position): Unit = {
selection = Selection(selection.fold(cursorPosition)(_.start), position)
}
private def handleKeyMovement(position: Int): Unit = {
if (KeyEvents.isShiftDown) {
extendSelection(position)
} else {
selection = None
}
cursor.position = position
}
private def selectAll(): Unit = {
selection = Selection(0, _text.chars.length)
}
private def selectWord(): Unit = {
val from = 0 max (_text.chars.lastIndexWhere(isWhitespace, cursor.position - 1) + 1)
val to = _text.chars.indexWhere(isWhitespace, cursor.position)
val clampedTo = if (to >= 0 && to < _text.chars.length) to else _text.chars.length
selection = Selection(from, clampedTo)
}
private def copySelection(): Unit = {
UiHandler.clipboard = selectedText
}
private def cutSelection(): Unit = {
UiHandler.clipboard = selectedText
deleteSelection()
}
private def pasteSelection(): Unit = {
if (selection.nonEmpty) deleteSelection()
writeString(UiHandler.clipboard)
}
private def deleteSelection(): Unit = {
for (Selection.Ordered(start, end) <- selection) {
_text.chars = _text.chars.take(start) ++ _text.chars.drop(end)
cursor.position = start
}
}
private def writeString(string: String): Unit = {
val (lhs, rhs) = _text.chars.splitAt(cursor.position)
val array = string.codePoints().toArray
_text.chars = lhs ++ array ++ rhs
cursor.position += array.length
}
private def writeChar(codePoint: Int): Unit = {
val (lhs, rhs) = _text.chars.splitAt(cursor.position)
_text.chars = lhs ++ Array(codePoint) ++ rhs
cursor.position += 1
}
/**
* Apply a set of corrections to the scroll to make sure the cursor and text stay visible
*/
private def adjustScroll(): Unit = {
// make cursor visible
if (cursorOffset < scroll) scroll = cursorOffset
if (cursorOffset - scroll > size.width - 16) scroll = cursorOffset - size.width + 16
// apply pressure from the left (to maximize visible text, for nicer editing experience)
val fullTextWidth = charsWidth(_text.chars, 0, _text.chars.length)
val areaWidth = size.width - 16
if (fullTextWidth > areaWidth && fullTextWidth - scroll < areaWidth) scroll = fullTextWidth - areaWidth
}
override def update(): Unit = {
super.update()
// process state changes
if (_text.changed()) {
onInput(text)
updateAnimationTargets()
adjustScroll()
}
if (enabledRegister.update()) {
updateAnimationTargets()
}
if (isFocused && !enabled) {
unfocus()
}
// update everything
prevCursorPosition.update()
foregroundAnimation.update()
borderAnimation.update()
blinkTimer = (blinkTimer + UiHandler.dt) % CursorBlinkTime
}
private def updateAnimationTargets(): Unit = {
foregroundAnimation.goto(targetForegroundColor)
borderAnimation.goto(targetBorderColor)
}
private def targetBorderColor: Color = ColorScheme( private def targetBorderColor: Color = ColorScheme(
if (validator(chars.mkString)) { if (validator(text)) {
if (isFocused) "TextInputBorderFocused" if (isFocused) "TextInputBorderFocused"
else if (!enabled) "TextInputBorderDisabled" else if (!enabled) "TextInputBorderDisabled"
else "TextInputBorder" else "TextInputBorder"
@ -168,39 +410,33 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
else "TextInputBorderError" else "TextInputBorderError"
} }
) )
private def targetForegroundColor: Color = ColorScheme( private def targetForegroundColor: Color = ColorScheme(
if (!enabled) "TextInputForegroundDisabled" if (!enabled) "TextInputForegroundDisabled"
else "TextInputForeground" else "TextInputForeground"
) )
private val placeholderForegroundColor: Color = ColorScheme("TextInputForegroundDisabled")
override def draw(g: Graphics): Unit = { override def draw(g: Graphics): Unit = {
if (textWidth == 0f && chars.nonEmpty)
textWidth = chars.map(g.font.charWidth(_)).sum
textChanged = false
for (event <- events) event.handle(g)
events.clear()
if (textChanged) {
val str = chars.mkString
onInput(str)
updateAnimationTargets()
}
g.rect(bounds, hoverAnimation.color) g.rect(bounds, hoverAnimation.color)
DrawUtils.ring(g, position.x, position.y, size.width, size.height, thickness = 2, borderAnimation.color) DrawUtils.ring(g, position.x, position.y, size.width, size.height, thickness = 2, borderAnimation.color)
g.setScissor(position.x + 4, position.y, size.width - 8f, size.height) g.setScissor(position.x + 4, position.y, size.width - 8f, size.height)
for ((start, end) <- selectionOffsets) {
val width = end - start
g.rect(position.x + start + 8 - scroll, position.y + 4, width, size.height - 8, BackgroundSelectedColor)
}
g.background = Color.Transparent g.background = Color.Transparent
g.foreground = if (chars.nonEmpty || isFocused) foregroundAnimation.color else placeholderForegroundColor val foreground = if (_text.chars.nonEmpty || isFocused) foregroundAnimation.color else PlaceholderForegroundColor
g.foreground = foreground
var charOffset = 0 var charOffset = 0
val charsToDisplay = if (chars.nonEmpty || isFocused) chars else placeholder val charsToDisplay = if (_text.chars.nonEmpty || isFocused) _text.chars else placeholder
for (char <- charsToDisplay) { for (char <- charsToDisplay) {
for ((start, end) <- selectionOffsets) {
g.foreground = if (charOffset >= start && charOffset < end) ForegroundSelectedColor else foreground
}
g.char(position.x + 8 + charOffset - scroll, position.y + 4, char) g.char(position.x + 8 + charOffset - scroll, position.y + 4, char)
charOffset += g.font.charWidth(char) charOffset += g.font.charWidth(char)
} }
@ -209,149 +445,36 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
g.rect(position.x + 7 + cursorOffset - scroll, position.y + 4, 2, 16, borderAnimation.color) g.rect(position.x + 7 + cursorOffset - scroll, position.y + 4, 2, 16, borderAnimation.color)
} }
} }
}
override def update(): Unit = { object TextInput {
super.update() class Text(initialValue: Array[Int]) extends Watcher(initialValue) {
def chars: Array[Int] = value
val nextEnabled = enabled def chars_=(newValue: Array[Int]): Unit = value = newValue
if (nextEnabled != prevEnabled) {
updateAnimationTargets()
prevEnabled = nextEnabled
}
if (isFocused && !enabled) {
unfocus()
}
foregroundAnimation.update()
borderAnimation.update()
blinkTimer = (blinkTimer + UiHandler.dt) % CursorBlinkTime
} }
private def charWidth(g: Graphics, c: Char): Int = g.font.charWidth(c) class Cursor(initialValue: Int = 0) extends Watcher(initialValue) {
def position: Int = value
// noinspection SameParameterValue def position_=(newValue: Int): Unit = value = newValue
private def charsWidth(g: Graphics, chars: Array[Char], first: Int, last: Int): Int = {
var width = 0
for (index <- first to last) {
width += g.font.charWidth(chars(index))
}
width
} }
private def adjustScroll(): Unit = { case class Selection(start: Int, end: Int) {
if (cursorOffset < scroll) require(start != end)
scroll = cursorOffset
if (cursorOffset - scroll > size.width - 16)
scroll = cursorOffset - size.width + 16
} }
private abstract class TextEvent { object Selection {
def handle(g: Graphics): Unit def apply(start: Int, end: Int): Option[Selection] = {
} Option.when(start != end) {
new Selection(start, end)
// TODO: refactor this mess. have actions only move the cursor position explicitly.
// then calculate the cursor offset (incl. the scroll offset) based on that automatically
// rather than incrementally in the actions.
private class CursorMoveLeft extends TextEvent {
override def handle(g: Graphics): Unit = {
if (cursorPos == 0) return
cursorOffset -= charWidth(g, chars(cursorPos - 1))
cursorPos -= 1
blinkTimer = 0
adjustScroll()
}
}
private class CursorMoveRight extends TextEvent {
override def handle(g: Graphics): Unit = {
if (cursorPos >= chars.length) return
cursorOffset += charWidth(g, chars(cursorPos))
cursorPos += 1
blinkTimer = 0
adjustScroll()
}
}
private class CursorMoveStart extends TextEvent {
override def handle(g: Graphics): Unit = {
cursorPos = 0
cursorOffset = 0
blinkTimer = 0
scroll = 0
}
}
private class CursorMoveEnd extends TextEvent {
override def handle(g: Graphics): Unit = {
cursorPos = chars.length
cursorOffset = textWidth
blinkTimer = 0
scroll = (textWidth - size.width + 16).max(0)
}
}
private class CursorMoveTo(mouseX: Float) extends TextEvent {
override def handle(g: Graphics): Unit = {
val absoluteX = mouseX - bounds.x + scroll - 4
var width = 0
var pos = 0
while (width < absoluteX && pos < chars.length) {
width += g.font.charWidth(chars(pos))
if (width < absoluteX) pos += 1
} }
cursorPos = chars.length.min(pos).max(0)
cursorOffset = if (cursorPos > 0) charsWidth(g, chars, 0, (cursorPos - 1).max(0)) else 0
blinkTimer = 0
adjustScroll()
} }
}
private class EraseCharBack extends TextEvent { object Ordered {
override def handle(g: Graphics): Unit = { def unapply(selection: Selection): Some[(Int, Int)] = {
val (lhs, rhs) = chars.splitAt(cursorPos) val Selection(start, end) = selection
if (lhs.isEmpty) return
val cw = charWidth(g, lhs.last) Some((start min end, start max end))
chars = lhs.take(lhs.length - 1) ++ rhs }
textChanged = true
textWidth -= cw
cursorOffset -= cw
cursorPos -= 1
blinkTimer = 0
scroll = (scroll - cw).max(0)
}
}
private class EraseCharFront extends TextEvent {
override def handle(g: Graphics): Unit = {
val (lhs, rhs) = chars.splitAt(cursorPos)
if (rhs.isEmpty) return
val cw = charWidth(g, rhs.head)
chars = lhs ++ rhs.drop(1)
textChanged = true
textWidth -= cw
blinkTimer = 0
if (rhs.drop(1).map(charWidth(g, _)).sum < size.width - 16)
scroll = (scroll - cw).max(0)
}
}
private class WriteChar(char: Char) extends TextEvent {
override def handle(g: Graphics): Unit = {
val (lhs, rhs) = chars.splitAt(cursorPos)
chars = lhs ++ Array(char) ++ rhs
textChanged = true
textWidth += charWidth(g, char)
(new CursorMoveRight).handle(g)
} }
} }
} }

View File

@ -69,9 +69,12 @@ class ContextMenuEntry(
override protected def receiveClickEvents: Boolean = true override protected def receiveClickEvents: Boolean = true
eventHandlers += { eventHandlers += {
case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) if !contextMenu.isOpening => case event @ MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) if !contextMenu.isOpening =>
clickSoundSource.press.play() clickSoundSource.press.play()
case ClickEvent(MouseEvent.Button.Left, _) if !contextMenu.isOpening => clicked() event.consume()
case event @ ClickEvent(MouseEvent.Button.Left, _) if !contextMenu.isOpening =>
clicked()
event.consume()
case HoverEvent(HoverEvent.State.Enter) => enter() case HoverEvent(HoverEvent.State.Enter) => enter()
case HoverEvent(HoverEvent.State.Leave) if !isGhost => leave() case HoverEvent(HoverEvent.State.Leave) if !isGhost => leave()
} }

View File

@ -158,7 +158,7 @@ class SystemSettingsTab extends SettingsTab with Logging {
private def setConfigPath(path: String): Unit = { private def setConfigPath(path: String): Unit = {
Settings.get.brainCustomConfigPath = Some(path) Settings.get.brainCustomConfigPath = Some(path)
textInput.setInput(path) textInput.text = path
restartWarning.isVisible = true restartWarning.isVisible = true
} }

View File

@ -157,8 +157,8 @@ class StatusBar extends Widget {
override def receiveMouseEvents: Boolean = true override def receiveMouseEvents: Boolean = true
override protected val hoverAnimationColorActive: Color = ColorScheme("StatusBarActive") override protected val HoverAnimationColorActive: Color = ColorScheme("StatusBarActive")
override protected val hoverAnimationColorDefault: Color = hoverAnimationColorActive.toRGBANorm.withAlpha(0) override protected val HoverAnimationColorDefault: Color = HoverAnimationColorActive.toRGBANorm.withAlpha(0)
override def draw(g: Graphics): Unit = { override def draw(g: Graphics): Unit = {
g.rect(bounds, hoverAnimation.color) g.rect(bounds, hoverAnimation.color)

View File

@ -13,23 +13,23 @@ import ocelot.desktop.util.animation.ColorAnimation
*/ */
trait HoverAnimation extends Widget with EventAware with HoverHandler with Updatable { trait HoverAnimation extends Widget with EventAware with HoverHandler with Updatable {
//noinspection ScalaWeakerAccess //noinspection ScalaWeakerAccess
protected val hoverAnimationSpeedEnter: Float = AnimationSpeedHoverEnter protected val HoverAnimationSpeedEnter: Float = AnimationSpeedHoverEnter
//noinspection ScalaWeakerAccess //noinspection ScalaWeakerAccess
protected val hoverAnimationSpeedLeave: Float = AnimationSpeedHoverLeave protected val HoverAnimationSpeedLeave: Float = AnimationSpeedHoverLeave
protected val hoverAnimationColorDefault: Color = ColorScheme("ButtonBackground") protected val HoverAnimationColorDefault: Color = ColorScheme("ButtonBackground")
protected val hoverAnimationColorActive: Color = ColorScheme("ButtonBackgroundActive") protected val HoverAnimationColorActive: Color = ColorScheme("ButtonBackgroundActive")
protected lazy val hoverAnimation: ColorAnimation = protected lazy val hoverAnimation: ColorAnimation =
new ColorAnimation(hoverAnimationColorDefault, hoverAnimationSpeedEnter) new ColorAnimation(HoverAnimationColorDefault, HoverAnimationSpeedEnter)
eventHandlers += { eventHandlers += {
case Capturing(HoverEvent(HoverEvent.State.Enter)) => case Capturing(HoverEvent(HoverEvent.State.Enter)) =>
hoverAnimation.speed = hoverAnimationSpeedEnter hoverAnimation.speed = HoverAnimationSpeedEnter
hoverAnimation.goto(hoverAnimationColorActive) hoverAnimation.goto(HoverAnimationColorActive)
case Capturing(HoverEvent(HoverEvent.State.Leave)) => case Capturing(HoverEvent(HoverEvent.State.Leave)) =>
hoverAnimation.speed = hoverAnimationSpeedLeave hoverAnimation.speed = HoverAnimationSpeedLeave
hoverAnimation.goto(hoverAnimationColorDefault) hoverAnimation.goto(HoverAnimationColorDefault)
} }
override def update(): Unit = { override def update(): Unit = {

View File

@ -1,7 +1,5 @@
package ocelot.desktop.util package ocelot.desktop.util
import ocelot.desktop.ui.widget.Updatable
/** /**
* Stores a value updated by calls to [[update]]. * Stores a value updated by calls to [[update]].
*/ */

View File

@ -0,0 +1,33 @@
package ocelot.desktop.util
/**
* Keeps a reference to an object
* and tells whether there were any changes to the value since the last check.
*/
class Watcher[T](initialValue: T) {
private var dirty = false
private var _callback: Option[T => Unit] = None
private var _value: T = initialValue
def value: T = _value
def value_=(newValue: T): Unit = {
dirty = _value != newValue
if (dirty) {
_callback.foreach(_(newValue))
}
_value = newValue
}
def onChange(callback: T => Unit): Unit = _callback = Some(callback)
def changed(): Boolean = {
if (dirty) {
dirty = false
true
} else false
}
}
object Watcher {
def apply[T](value: T) = new Watcher(value)
}

View File

@ -24,7 +24,7 @@ class OcelotInterfaceWindow(storage: OcelotInterfaceLogStorage) extends PanelWin
children :+= new TextInput() { children :+= new TextInput() {
override def onConfirm(): Unit = { override def onConfirm(): Unit = {
pushLine(text) pushLine(text)
setInput("") text = ""
} }
} }