ruby-sfml 3.0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +101 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +245 -0
  5. data/ext/ruby-sfml/extconf.rb +69 -0
  6. data/lib/sfml/assets/fonts/DejaVuSans.LICENSE.txt +78 -0
  7. data/lib/sfml/assets/fonts/DejaVuSans.ttf +0 -0
  8. data/lib/sfml/assets.rb +121 -0
  9. data/lib/sfml/audio/listener.rb +55 -0
  10. data/lib/sfml/audio/music.rb +88 -0
  11. data/lib/sfml/audio/sound.rb +102 -0
  12. data/lib/sfml/audio/sound_buffer.rb +38 -0
  13. data/lib/sfml/audio/sound_buffer_recorder.rb +71 -0
  14. data/lib/sfml/audio/sound_recorder.rb +30 -0
  15. data/lib/sfml/c/audio.rb +106 -0
  16. data/lib/sfml/c/graphics.rb +425 -0
  17. data/lib/sfml/c/network.rb +79 -0
  18. data/lib/sfml/c/system.rb +43 -0
  19. data/lib/sfml/c/window.rb +186 -0
  20. data/lib/sfml/c.rb +72 -0
  21. data/lib/sfml/game.rb +101 -0
  22. data/lib/sfml/graphics/blend_mode.rb +108 -0
  23. data/lib/sfml/graphics/circle_shape.rb +67 -0
  24. data/lib/sfml/graphics/color.rb +89 -0
  25. data/lib/sfml/graphics/convex_shape.rb +82 -0
  26. data/lib/sfml/graphics/font.rb +67 -0
  27. data/lib/sfml/graphics/image.rb +125 -0
  28. data/lib/sfml/graphics/rectangle_shape.rb +62 -0
  29. data/lib/sfml/graphics/render_states.rb +56 -0
  30. data/lib/sfml/graphics/render_target.rb +146 -0
  31. data/lib/sfml/graphics/render_texture.rb +72 -0
  32. data/lib/sfml/graphics/render_window.rb +154 -0
  33. data/lib/sfml/graphics/shader.rb +132 -0
  34. data/lib/sfml/graphics/sprite.rb +75 -0
  35. data/lib/sfml/graphics/text.rb +144 -0
  36. data/lib/sfml/graphics/texture.rb +79 -0
  37. data/lib/sfml/graphics/transform.rb +150 -0
  38. data/lib/sfml/graphics/transformable.rb +74 -0
  39. data/lib/sfml/graphics/vertex.rb +53 -0
  40. data/lib/sfml/graphics/vertex_array.rb +114 -0
  41. data/lib/sfml/graphics/view.rb +126 -0
  42. data/lib/sfml/network/ip_address.rb +67 -0
  43. data/lib/sfml/network/tcp_listener.rb +61 -0
  44. data/lib/sfml/network/tcp_socket.rb +74 -0
  45. data/lib/sfml/network/udp_socket.rb +71 -0
  46. data/lib/sfml/system/clock.rb +44 -0
  47. data/lib/sfml/system/rect.rb +64 -0
  48. data/lib/sfml/system/time.rb +48 -0
  49. data/lib/sfml/system/vector2.rb +66 -0
  50. data/lib/sfml/system/vector3.rb +63 -0
  51. data/lib/sfml/version.rb +19 -0
  52. data/lib/sfml/window/clipboard.rb +38 -0
  53. data/lib/sfml/window/cursor.rb +68 -0
  54. data/lib/sfml/window/event.rb +133 -0
  55. data/lib/sfml/window/joystick.rb +90 -0
  56. data/lib/sfml/window/keyboard.rb +60 -0
  57. data/lib/sfml/window/mouse.rb +71 -0
  58. data/lib/sfml/window/video_mode.rb +37 -0
  59. data/lib/sfml/window/window.rb +149 -0
  60. data/lib/sfml.rb +98 -0
  61. data/ruby-sfml.gemspec +38 -0
  62. metadata +163 -0
@@ -0,0 +1,74 @@
1
+ module SFML
2
+ module Network
3
+ # A TCP client socket. Connect to a server, send/receive bytes,
4
+ # disconnect.
5
+ #
6
+ # sock = SFML::Network::TcpSocket.new
7
+ # case sock.connect(SFML::Network::IpAddress::LOCALHOST, port: 8080)
8
+ # when :done then ...
9
+ # end
10
+ #
11
+ # sock.send("hello")
12
+ # status, bytes = sock.receive(max: 1024)
13
+ #
14
+ # By default sockets are blocking; set `socket.blocking = false`
15
+ # for non-blocking polling, where send/receive may return :not_ready.
16
+ class TcpSocket
17
+ def initialize
18
+ ptr = C::Network.sfTcpSocket_create
19
+ raise Error, "sfTcpSocket_create returned NULL" if ptr.null?
20
+ @handle = FFI::AutoPointer.new(ptr, C::Network.method(:sfTcpSocket_destroy))
21
+ end
22
+
23
+ # Open a connection. Returns one of the SOCKET_STATUSES symbols
24
+ # (:done, :not_ready, :disconnected, :error). `timeout` is a
25
+ # SFML::Time; SFML::Time.zero (default) blocks forever.
26
+ def connect(address, port:, timeout: SFML::Time.zero)
27
+ addr = address.is_a?(IpAddress) ? address : IpAddress.from_string(address)
28
+ code = C::Network.sfTcpSocket_connect(@handle, addr.struct, Integer(port), timeout.to_native)
29
+ C::Network::STATUSES[code]
30
+ end
31
+
32
+ def disconnect
33
+ C::Network.sfTcpSocket_disconnect(@handle)
34
+ self
35
+ end
36
+
37
+ def send(data)
38
+ bytes = data.to_s
39
+ buf = FFI::MemoryPointer.new(:uint8, bytes.bytesize)
40
+ buf.write_bytes(bytes)
41
+ code = C::Network.sfTcpSocket_send(@handle, buf, bytes.bytesize)
42
+ C::Network::STATUSES[code]
43
+ end
44
+
45
+ # Read up to `max` bytes. Returns [status, data] — `data` is a
46
+ # binary String when status is :done, nil otherwise.
47
+ def receive(max: 4096)
48
+ buf = FFI::MemoryPointer.new(:uint8, Integer(max))
49
+ received = FFI::MemoryPointer.new(:size_t)
50
+ code = C::Network.sfTcpSocket_receive(@handle, buf, Integer(max), received)
51
+ status = C::Network::STATUSES[code]
52
+
53
+ return [status, nil] unless status == :done
54
+ n = received.read(:size_t)
55
+ [status, buf.read_bytes(n)]
56
+ end
57
+
58
+ def blocking? = C::Network.sfTcpSocket_isBlocking(@handle)
59
+
60
+ def blocking=(value)
61
+ C::Network.sfTcpSocket_setBlocking(@handle, value ? true : false)
62
+ end
63
+
64
+ def local_port = C::Network.sfTcpSocket_getLocalPort(@handle)
65
+ def remote_port = C::Network.sfTcpSocket_getRemotePort(@handle)
66
+
67
+ def remote_address
68
+ IpAddress.wrap(C::Network.sfTcpSocket_getRemoteAddress(@handle))
69
+ end
70
+
71
+ attr_reader :handle # :nodoc:
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,71 @@
1
+ module SFML
2
+ module Network
3
+ # Connectionless UDP datagram socket. Bind a port to receive,
4
+ # send to (address, port). Stateless — every send/receive specifies
5
+ # the peer.
6
+ #
7
+ # sock = SFML::Network::UdpSocket.new
8
+ # sock.bind(port: 9999)
9
+ # sock.send("hello", to: SFML::Network::IpAddress::LOCALHOST, port: 9000)
10
+ # status, bytes, addr, port = sock.receive(max: 1024)
11
+ class UdpSocket
12
+ MAX_DATAGRAM_SIZE = C::Network.sfUdpSocket_maxDatagramSize
13
+
14
+ def initialize
15
+ ptr = C::Network.sfUdpSocket_create
16
+ raise Error, "sfUdpSocket_create returned NULL" if ptr.null?
17
+ @handle = FFI::AutoPointer.new(ptr, C::Network.method(:sfUdpSocket_destroy))
18
+ end
19
+
20
+ def bind(port:, address: IpAddress::ANY)
21
+ addr = address.is_a?(IpAddress) ? address : IpAddress.from_string(address)
22
+ code = C::Network.sfUdpSocket_bind(@handle, Integer(port), addr.struct)
23
+ C::Network::STATUSES[code]
24
+ end
25
+
26
+ def unbind
27
+ C::Network.sfUdpSocket_unbind(@handle)
28
+ self
29
+ end
30
+
31
+ def send(data, to:, port:)
32
+ bytes = data.to_s
33
+ addr = to.is_a?(IpAddress) ? to : IpAddress.from_string(to)
34
+ buf = FFI::MemoryPointer.new(:uint8, bytes.bytesize)
35
+ buf.write_bytes(bytes)
36
+ code = C::Network.sfUdpSocket_send(@handle, buf, bytes.bytesize, addr.struct, Integer(port))
37
+ C::Network::STATUSES[code]
38
+ end
39
+
40
+ # Returns [status, data, sender_ip, sender_port].
41
+ # `data` is a binary String when status is :done, otherwise nil.
42
+ def receive(max: 1024)
43
+ buf = FFI::MemoryPointer.new(:uint8, Integer(max))
44
+ received_size = FFI::MemoryPointer.new(:size_t)
45
+ sender_addr = C::Network::IpAddress.new
46
+ sender_port = FFI::MemoryPointer.new(:uint16)
47
+
48
+ code = C::Network.sfUdpSocket_receive(
49
+ @handle, buf, Integer(max), received_size, sender_addr.pointer, sender_port,
50
+ )
51
+ status = C::Network::STATUSES[code]
52
+
53
+ return [status, nil, nil, nil] unless status == :done
54
+ n = received_size.read(:size_t)
55
+ sip = IpAddress.wrap(sender_addr)
56
+ sport = sender_port.read(:uint16)
57
+ [status, buf.read_bytes(n), sip, sport]
58
+ end
59
+
60
+ def blocking? = C::Network.sfUdpSocket_isBlocking(@handle)
61
+
62
+ def blocking=(value)
63
+ C::Network.sfUdpSocket_setBlocking(@handle, value ? true : false)
64
+ end
65
+
66
+ def local_port = C::Network.sfUdpSocket_getLocalPort(@handle)
67
+
68
+ attr_reader :handle # :nodoc:
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,44 @@
1
+ module SFML
2
+ # Monotonic high-resolution timer. Wraps sfClock from CSFML.
3
+ #
4
+ # clock = SFML::Clock.new
5
+ # # ... do work ...
6
+ # puts clock.elapsed_time.as_seconds
7
+ # clock.restart # returns the elapsed time and resets to zero
8
+ class Clock
9
+ def initialize
10
+ ptr = C::System.sfClock_create
11
+ raise Error, "sfClock_create returned NULL" if ptr.null?
12
+ @handle = FFI::AutoPointer.new(ptr, C::System.method(:sfClock_destroy))
13
+ end
14
+
15
+ def elapsed_time
16
+ Time.from_native(C::System.sfClock_getElapsedTime(@handle))
17
+ end
18
+ alias elapsed elapsed_time
19
+
20
+ def running?
21
+ C::System.sfClock_isRunning(@handle)
22
+ end
23
+
24
+ def start
25
+ C::System.sfClock_start(@handle)
26
+ self
27
+ end
28
+
29
+ def stop
30
+ C::System.sfClock_stop(@handle)
31
+ self
32
+ end
33
+
34
+ def restart
35
+ Time.from_native(C::System.sfClock_restart(@handle))
36
+ end
37
+
38
+ def reset
39
+ Time.from_native(C::System.sfClock_reset(@handle))
40
+ end
41
+
42
+ attr_reader :handle # :nodoc:
43
+ end
44
+ end
@@ -0,0 +1,64 @@
1
+ module SFML
2
+ # An axis-aligned rectangle. Mirrors sfFloatRect / sfIntRect — those structs
3
+ # are just (position: Vector2, size: Vector2). We deliberately keep one
4
+ # Ruby class for both the float and int variants since pattern-matching
5
+ # `case bounds in {position: {x:, y:}, size: {x: w, y: h}}` is what the
6
+ # users reach for either way.
7
+ #
8
+ # r = SFML::Rect.new([10, 20], [100, 50])
9
+ # r.contains?([50, 30]) #=> true
10
+ # r.right #=> 110
11
+ #
12
+ # Used by Text#local_bounds, Text#global_bounds, Sprite#texture_rect, etc.
13
+ class Rect
14
+ attr_reader :position, :size
15
+
16
+ def initialize(position, size)
17
+ @position = position.is_a?(Vector2) ? position : Vector2.new(*position)
18
+ @size = size.is_a?(Vector2) ? size : Vector2.new(*size)
19
+ freeze
20
+ end
21
+
22
+ def x = @position.x
23
+ def y = @position.y
24
+ def width = @size.x
25
+ def height = @size.y
26
+ alias left x
27
+ alias top y
28
+ def right = @position.x + @size.x
29
+ def bottom = @position.y + @size.y
30
+
31
+ def contains?(point)
32
+ px, py = point.is_a?(Vector2) ? [point.x, point.y] : [point[0], point[1]]
33
+ px >= left && px < right && py >= top && py < bottom
34
+ end
35
+
36
+ def intersects?(other)
37
+ left < other.right &&
38
+ right > other.left &&
39
+ top < other.bottom &&
40
+ bottom > other.top
41
+ end
42
+
43
+ def ==(other)
44
+ other.is_a?(Rect) && @position == other.position && @size == other.size
45
+ end
46
+ alias eql? ==
47
+ def hash = [@position, @size].hash
48
+
49
+ def to_a = [x, y, width, height]
50
+ def to_h = { position: @position, size: @size }
51
+ def deconstruct = [x, y, width, height]
52
+ def deconstruct_keys(_keys) = { position: @position, size: @size }
53
+
54
+ def to_s = "Rect(x=#{x}, y=#{y}, w=#{width}, h=#{height})"
55
+ alias inspect to_s
56
+
57
+ def self.from_native(struct) # :nodoc:
58
+ new(
59
+ Vector2.new(struct[:position][:x], struct[:position][:y]),
60
+ Vector2.new(struct[:size][:x], struct[:size][:y]),
61
+ )
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,48 @@
1
+ module SFML
2
+ # Represents a time value. Stored internally as microseconds (int64), same
3
+ # as sfTime in CSFML. Immutable and comparable.
4
+ #
5
+ # SFML::Time.seconds(1.5) #=> 1_500_000 µs
6
+ # SFML::Time.milliseconds(500)
7
+ # SFML::Time.microseconds(42)
8
+ # SFML::Time.zero
9
+ class Time
10
+ include Comparable
11
+
12
+ attr_reader :microseconds
13
+
14
+ def self.seconds(value) = new((value * 1_000_000).to_i)
15
+ def self.milliseconds(value) = new(Integer(value) * 1_000)
16
+ def self.microseconds(value) = new(Integer(value))
17
+ def self.zero = new(0)
18
+
19
+ def initialize(microseconds)
20
+ @microseconds = Integer(microseconds)
21
+ freeze
22
+ end
23
+
24
+ def as_seconds = @microseconds / 1_000_000.0
25
+ def as_milliseconds = @microseconds / 1_000
26
+ def as_microseconds = @microseconds
27
+
28
+ def +(other) = Time.new(@microseconds + other.microseconds)
29
+ def -(other) = Time.new(@microseconds - other.microseconds)
30
+ def -@ = Time.new(-@microseconds)
31
+ def <=>(other) = @microseconds <=> other.microseconds
32
+
33
+ def hash = @microseconds.hash
34
+ def eql?(other) = other.is_a?(Time) && @microseconds == other.microseconds
35
+ alias == eql?
36
+
37
+ def to_s = "#<SFML::Time #{as_seconds}s>"
38
+ alias inspect to_s
39
+
40
+ def self.from_native(struct) # :nodoc:
41
+ new(struct[:microseconds])
42
+ end
43
+
44
+ def to_native # :nodoc:
45
+ C::System::Time.new.tap { |t| t[:microseconds] = @microseconds }
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,66 @@
1
+ module SFML
2
+ # 2D vector with float components. Operator-friendly and pattern-matchable.
3
+ #
4
+ # v = SFML::Vector2[3, 4]
5
+ # v.length #=> 5.0
6
+ # v + Vector2[1, 1] #=> Vector2(4, 5)
7
+ # v * 2 #=> Vector2(6, 8)
8
+ # x, y = v # destructures via to_a
9
+ class Vector2
10
+ attr_reader :x, :y
11
+
12
+ def self.[](x, y) = new(x, y)
13
+ def self.zero = new(0, 0)
14
+
15
+ def initialize(x = 0, y = 0)
16
+ @x = x
17
+ @y = y
18
+ freeze
19
+ end
20
+
21
+ def +(other) = Vector2.new(@x + other.x, @y + other.y)
22
+ def -(other) = Vector2.new(@x - other.x, @y - other.y)
23
+ def *(scalar) = Vector2.new(@x * scalar, @y * scalar)
24
+ def /(scalar) = Vector2.new(@x / scalar.to_f, @y / scalar.to_f)
25
+ def -@ = Vector2.new(-@x, -@y)
26
+
27
+ # Lets Ruby evaluate `2 * vec` as `vec * 2`. Without this, Numeric#*
28
+ # would raise TypeError because it doesn't know about Vector2.
29
+ def coerce(other)
30
+ raise TypeError, "Vector2 cannot coerce #{other.class}" unless other.is_a?(Numeric)
31
+ [self, other]
32
+ end
33
+
34
+ def ==(other) = other.is_a?(Vector2) && @x == other.x && @y == other.y
35
+ alias eql? ==
36
+ def hash = [@x, @y].hash
37
+
38
+ def length = Math.sqrt(length_sq)
39
+ def length_sq = (@x * @x) + (@y * @y)
40
+
41
+ def normalize
42
+ len = length
43
+ return Vector2.zero if len.zero?
44
+ self / len
45
+ end
46
+
47
+ def dot(other) = (@x * other.x) + (@y * other.y)
48
+ def cross(other) = (@x * other.y) - (@y * other.x)
49
+
50
+ def to_a = [@x, @y]
51
+ def to_h = { x: @x, y: @y }
52
+ def deconstruct = [@x, @y]
53
+ def deconstruct_keys(_keys) = { x: @x, y: @y }
54
+
55
+ def to_s = "Vector2(#{@x}, #{@y})"
56
+ alias inspect to_s
57
+
58
+ def self.from_native(struct) # :nodoc:
59
+ new(struct[:x], struct[:y])
60
+ end
61
+
62
+ def to_native_f # :nodoc:
63
+ C::System::Vector2f.new.tap { |v| v[:x] = @x; v[:y] = @y }
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,63 @@
1
+ module SFML
2
+ # 3D vector with float components. Mirrors Vector2's surface.
3
+ class Vector3
4
+ attr_reader :x, :y, :z
5
+
6
+ def self.[](x, y, z) = new(x, y, z)
7
+ def self.zero = new(0, 0, 0)
8
+
9
+ def initialize(x = 0, y = 0, z = 0)
10
+ @x = x
11
+ @y = y
12
+ @z = z
13
+ freeze
14
+ end
15
+
16
+ def +(other) = Vector3.new(@x + other.x, @y + other.y, @z + other.z)
17
+ def -(other) = Vector3.new(@x - other.x, @y - other.y, @z - other.z)
18
+ def *(scalar) = Vector3.new(@x * scalar, @y * scalar, @z * scalar)
19
+ def /(scalar) = Vector3.new(@x / scalar.to_f, @y / scalar.to_f, @z / scalar.to_f)
20
+ def -@ = Vector3.new(-@x, -@y, -@z)
21
+
22
+ def ==(other)
23
+ other.is_a?(Vector3) && @x == other.x && @y == other.y && @z == other.z
24
+ end
25
+ alias eql? ==
26
+ def hash = [@x, @y, @z].hash
27
+
28
+ def length = Math.sqrt(length_sq)
29
+ def length_sq = (@x * @x) + (@y * @y) + (@z * @z)
30
+
31
+ def normalize
32
+ len = length
33
+ return Vector3.zero if len.zero?
34
+ self / len
35
+ end
36
+
37
+ def dot(other) = (@x * other.x) + (@y * other.y) + (@z * other.z)
38
+
39
+ def cross(other)
40
+ Vector3.new(
41
+ (@y * other.z) - (@z * other.y),
42
+ (@z * other.x) - (@x * other.z),
43
+ (@x * other.y) - (@y * other.x)
44
+ )
45
+ end
46
+
47
+ def to_a = [@x, @y, @z]
48
+ def to_h = { x: @x, y: @y, z: @z }
49
+ def deconstruct = [@x, @y, @z]
50
+ def deconstruct_keys(_keys) = { x: @x, y: @y, z: @z }
51
+
52
+ def to_s = "Vector3(#{@x}, #{@y}, #{@z})"
53
+ alias inspect to_s
54
+
55
+ def self.from_native(struct) # :nodoc:
56
+ new(struct[:x], struct[:y], struct[:z])
57
+ end
58
+
59
+ def to_native_f # :nodoc:
60
+ C::System::Vector3f.new.tap { |v| v[:x] = @x; v[:y] = @y; v[:z] = @z }
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,19 @@
1
+ module SFML
2
+ # The SFML/CSFML release this gem is built against. End users can
3
+ # check this at runtime to assert binding compatibility.
4
+ CSFML_VERSION = "3.0.0"
5
+
6
+ # Gem version. The first three segments mirror CSFML_VERSION exactly;
7
+ # the trailing fourth segment is our own patch number for fixes /
8
+ # additions made on top of the same upstream CSFML release.
9
+ #
10
+ # Examples on a hypothetical timeline:
11
+ #
12
+ # "3.0.0.0" — first cut against CSFML 3.0.0
13
+ # "3.0.0.1" — our bug fix, still on CSFML 3.0.0
14
+ # "3.0.0.2" — another patch
15
+ # "3.0.1.0" — CSFML 3.0.1 ships, we re-cut from upstream
16
+ # "3.0.1.1" — our patch on top of CSFML 3.0.1
17
+ # "3.1.0.0" — CSFML 3.1.0 ships, we add new bindings
18
+ VERSION = "3.0.0.0"
19
+ end
@@ -0,0 +1,38 @@
1
+ module SFML
2
+ # The system clipboard. UTF-8 in, UTF-8 out — even when the OS stores
3
+ # it as wide chars, we go through CSFML's `*UnicodeString` variants
4
+ # and convert in Ruby so non-ASCII text round-trips losslessly.
5
+ #
6
+ # SFML::Clipboard.text #=> "whatever was last copied"
7
+ # SFML::Clipboard.text = "пример"
8
+ #
9
+ # Useful inside text-input UIs (Ctrl+C / Ctrl+V handlers) and for
10
+ # quick "copy that error message" buttons in tools.
11
+ module Clipboard
12
+ module_function
13
+
14
+ def text
15
+ ptr = C::Window.sfClipboard_getUnicodeString
16
+ return "" if ptr.null?
17
+
18
+ codepoints = []
19
+ offset = 0
20
+ loop do
21
+ cp = ptr.get_uint32(offset)
22
+ break if cp.zero?
23
+ codepoints << cp
24
+ offset += 4
25
+ end
26
+ codepoints.pack("U*")
27
+ end
28
+
29
+ def text=(value)
30
+ str = value.to_s.encode("UTF-8")
31
+ cps = str.unpack("U*")
32
+ buf = FFI::MemoryPointer.new(:uint32, cps.length + 1)
33
+ buf.write_array_of_uint32(cps + [0])
34
+ C::Window.sfClipboard_setUnicodeString(buf)
35
+ str
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,68 @@
1
+ module SFML
2
+ # A mouse cursor — either a built-in system shape, or a custom bitmap.
3
+ # Apply it with `window.cursor = cursor`.
4
+ #
5
+ # window.cursor = SFML::Cursor.system(:hand)
6
+ # window.cursor = SFML::Cursor.system(:not_allowed)
7
+ #
8
+ # pixels = "..." # 32×32 RGBA bytes
9
+ # window.cursor = SFML::Cursor.from_pixels(32, 32, pixels, hotspot: [16, 16])
10
+ #
11
+ # See SFML::Cursor::TYPES for the full list of system shapes.
12
+ class Cursor
13
+ # Order matches sfCursorType in CSFML/Window/Cursor.h.
14
+ TYPES = %i[
15
+ arrow arrow_wait wait text hand
16
+ size_horizontal size_vertical
17
+ size_top_left_bottom_right size_bottom_left_top_right
18
+ size_left size_right size_top size_bottom
19
+ size_top_left size_bottom_right size_bottom_left size_top_right
20
+ size_all cross help not_allowed
21
+ ].freeze
22
+ TYPE_INDEX = TYPES.each_with_index.to_h.freeze
23
+
24
+ # Build a cursor matching one of the OS-supplied shapes. Some types
25
+ # (the directional `size_*` family beyond Horizontal/Vertical) only
26
+ # render distinct shapes on Linux — on macOS / Windows they fall
27
+ # back to the closest equivalent.
28
+ def self.system(type)
29
+ code = TYPE_INDEX.fetch(type) do
30
+ raise ArgumentError, "Unknown cursor type: #{type.inspect}. " \
31
+ "Expected one of: #{TYPES.inspect}"
32
+ end
33
+ ptr = C::Window.sfCursor_createFromSystem(code)
34
+ raise Error, "sfCursor_createFromSystem returned NULL for #{type.inspect}" if ptr.null?
35
+ _wrap(ptr)
36
+ end
37
+
38
+ # Build a cursor from raw RGBA pixels. `pixels` must be exactly
39
+ # width*height*4 bytes. `hotspot:` is the [x, y] within the cursor
40
+ # image that maps to the actual click point (e.g. tip of an arrow).
41
+ def self.from_pixels(width, height, pixels, hotspot: [0, 0])
42
+ expected = Integer(width) * Integer(height) * 4
43
+ raise ArgumentError, "pixels must be #{expected} bytes, got #{pixels.bytesize}" if pixels.bytesize != expected
44
+
45
+ buf = FFI::MemoryPointer.new(:uint8, expected)
46
+ buf.write_bytes(pixels)
47
+
48
+ size = C::System::Vector2u.new
49
+ size[:x] = Integer(width); size[:y] = Integer(height)
50
+
51
+ hot = C::System::Vector2u.new
52
+ hot[:x] = Integer(hotspot[0]); hot[:y] = Integer(hotspot[1])
53
+
54
+ ptr = C::Window.sfCursor_createFromPixels(buf, size, hot)
55
+ raise Error, "sfCursor_createFromPixels returned NULL" if ptr.null?
56
+ _wrap(ptr)
57
+ end
58
+
59
+ attr_reader :handle # :nodoc:
60
+
61
+ # @!visibility private
62
+ def self._wrap(ptr)
63
+ cursor = allocate
64
+ cursor.instance_variable_set(:@handle, FFI::AutoPointer.new(ptr, C::Window.method(:sfCursor_destroy)))
65
+ cursor
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,133 @@
1
+ module SFML
2
+ # A window event. Holds a type symbol and a Hash of type-specific data.
3
+ # Pattern-match it directly; that's the intended interface:
4
+ #
5
+ # case event
6
+ # in {type: :closed} then ...
7
+ # in {type: :key_pressed, code: :escape} then ...
8
+ # in {type: :resized, size:} then ...
9
+ # in {type: :mouse_moved, position: {x:, y:}} then ...
10
+ # in {type: :mouse_wheel_scrolled, delta:} then ...
11
+ # end
12
+ #
13
+ # Type-specific fields are also reachable as plain methods (`event.code`,
14
+ # `event.position`) for the cases when pattern matching is overkill.
15
+ class Event
16
+ # Order matches sfMouseButton in CSFML 3: left, right, middle,
17
+ # extra1, extra2. (CSFML 2 called the last two x_button1/x_button2,
18
+ # but SFML 3 dropped the X-button terminology.)
19
+ MOUSE_BUTTONS = %i[left right middle extra1 extra2].freeze
20
+ MOUSE_WHEELS = %i[vertical horizontal].freeze
21
+
22
+ attr_reader :type, :data
23
+
24
+ def initialize(type, data = {})
25
+ @type = type
26
+ @data = data.freeze
27
+ freeze
28
+ end
29
+
30
+ def [](key) = @data[key]
31
+ def fetch(*args, &) = @data.fetch(*args, &)
32
+
33
+ def deconstruct_keys(_keys)
34
+ { type: @type, **@data }
35
+ end
36
+
37
+ def respond_to_missing?(name, _private = false)
38
+ @data.key?(name) || super
39
+ end
40
+
41
+ def method_missing(name, *args)
42
+ return @data[name] if args.empty? && @data.key?(name)
43
+ super
44
+ end
45
+
46
+ def to_s = "#<SFML::Event #{@type} #{@data.inspect}>"
47
+ alias inspect to_s
48
+
49
+ # Reads the sfEvent union buffer, decodes the discriminator, and reads
50
+ # the type-specific variant. CSFML lays each variant out with the
51
+ # sfEventType as the first field, so we can re-interpret the same memory
52
+ # as the right struct.
53
+ def self.from_native(buffer)
54
+ type_index = buffer[:type]
55
+ type_sym = C::Window::EVENT_TYPES[type_index] || :unknown
56
+
57
+ data = decode(type_sym, buffer.to_ptr)
58
+ new(type_sym, data)
59
+ end
60
+
61
+ def self.decode(type_sym, ptr)
62
+ case type_sym
63
+ when :closed, :focus_lost, :focus_gained,
64
+ :mouse_entered, :mouse_left
65
+ EMPTY
66
+
67
+ when :resized
68
+ s = C::Window::SizeEvent.new(ptr)
69
+ { size: Vector2.new(s[:size][:x], s[:size][:y]) }
70
+
71
+ when :key_pressed, :key_released
72
+ k = C::Window::KeyEvent.new(ptr)
73
+ {
74
+ code: Keyboard.code_to_symbol(k[:code]),
75
+ alt: k[:alt],
76
+ control: k[:control],
77
+ shift: k[:shift],
78
+ system: k[:system],
79
+ }
80
+
81
+ when :text_entered
82
+ t = C::Window::TextEvent.new(ptr)
83
+ { unicode: t[:unicode], char: [t[:unicode]].pack("U*") }
84
+
85
+ when :mouse_moved
86
+ m = C::Window::MouseMoveEvent.new(ptr)
87
+ { position: Vector2.new(m[:position][:x], m[:position][:y]) }
88
+
89
+ when :mouse_moved_raw
90
+ m = C::Window::MouseMoveEvent.new(ptr)
91
+ # Raw event re-uses the same struct shape but the field is a delta.
92
+ { delta: Vector2.new(m[:position][:x], m[:position][:y]) }
93
+
94
+ when :mouse_button_pressed, :mouse_button_released
95
+ b = C::Window::MouseButtonEvent.new(ptr)
96
+ {
97
+ button: MOUSE_BUTTONS[b[:button]] || :unknown,
98
+ position: Vector2.new(b[:position][:x], b[:position][:y]),
99
+ }
100
+
101
+ when :mouse_wheel_scrolled
102
+ w = C::Window::MouseWheelScrollEvent.new(ptr)
103
+ {
104
+ wheel: MOUSE_WHEELS[w[:wheel]] || :unknown,
105
+ delta: w[:delta],
106
+ position: Vector2.new(w[:position][:x], w[:position][:y]),
107
+ }
108
+
109
+ when :joystick_button_pressed, :joystick_button_released
110
+ b = C::Window::JoystickButtonEvent.new(ptr)
111
+ { joystick_id: b[:joystick_id], button: b[:button] }
112
+
113
+ when :joystick_moved
114
+ m = C::Window::JoystickMoveEvent.new(ptr)
115
+ {
116
+ joystick_id: m[:joystick_id],
117
+ axis: Joystick::AXES[m[:axis]] || :unknown,
118
+ position: m[:position],
119
+ }
120
+
121
+ when :joystick_connected, :joystick_disconnected
122
+ c = C::Window::JoystickConnectEvent.new(ptr)
123
+ { joystick_id: c[:joystick_id] }
124
+
125
+ else
126
+ EMPTY
127
+ end
128
+ end
129
+
130
+ EMPTY = {}.freeze
131
+ private_constant :EMPTY
132
+ end
133
+ end