zaniah 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +70 -0
- data/assets/fonts/Abel-Regular.ttf +0 -0
- data/assets/fonts/OFL.txt +93 -0
- data/docs/adr/000-template.md +24 -0
- data/docs/adr/001-native-gpu-backends.md +36 -0
- data/docs/adr/002-objective-c-aggregates.md +34 -0
- data/docs/adr/003-fiddle-call-boundary.md +33 -0
- data/docs/adr/README.md +10 -0
- data/docs/native.md +66 -0
- data/docs/process_pool.md +67 -0
- data/docs/text.md +90 -0
- data/docs/vector_and_list.md +52 -0
- data/examples/ffi_bench.rb +16 -0
- data/examples/linux_smoke.rb +53 -0
- data/examples/mac_event_check.rb +61 -0
- data/examples/mac_poll_bench.rb +24 -0
- data/examples/metal_state_check.rb +72 -0
- data/examples/native_bench.rb +18 -0
- data/examples/native_drop.rb +82 -0
- data/examples/native_font.rb +15 -0
- data/examples/native_ime.rb +50 -0
- data/examples/native_smoke.rb +75 -0
- data/examples/native_text_bench.rb +83 -0
- data/examples/native_watch.rb +25 -0
- data/examples/vulkan_triangle.rb +20 -0
- data/examples/word_count_worker.rb +8 -0
- data/lib/zaniah/anchored.rb +10 -0
- data/lib/zaniah/app.rb +130 -0
- data/lib/zaniah/bounds.rb +22 -0
- data/lib/zaniah/canvas.rb +15 -0
- data/lib/zaniah/color.rb +35 -0
- data/lib/zaniah/configuration.rb +5 -0
- data/lib/zaniah/corners.rb +7 -0
- data/lib/zaniah/data_compat.rb +25 -0
- data/lib/zaniah/device_pixels.rb +7 -0
- data/lib/zaniah/div.rb +5 -0
- data/lib/zaniah/edges.rb +7 -0
- data/lib/zaniah/element.rb +106 -0
- data/lib/zaniah/entity.rb +9 -0
- data/lib/zaniah/entity_context.rb +17 -0
- data/lib/zaniah/error.rb +5 -0
- data/lib/zaniah/ffi/com.rb +24 -0
- data/lib/zaniah/ffi/library.rb +29 -0
- data/lib/zaniah/ffi/objc.rb +86 -0
- data/lib/zaniah/ffi/struct/signature.rb +46 -0
- data/lib/zaniah/ffi/struct.rb +61 -0
- data/lib/zaniah/ffi/wayland.rb +140 -0
- data/lib/zaniah/frame_context.rb +14 -0
- data/lib/zaniah/geometry.rb +16 -0
- data/lib/zaniah/gpu/buffer.rb +23 -0
- data/lib/zaniah/gpu/frame_encoder.rb +43 -0
- data/lib/zaniah/gpu/instance_packing.rb +53 -0
- data/lib/zaniah/gpu/metal.rb +209 -0
- data/lib/zaniah/gpu/open_gl.rb +210 -0
- data/lib/zaniah/gpu/pipeline.rb +7 -0
- data/lib/zaniah/gpu/software.rb +126 -0
- data/lib/zaniah/gpu/texture.rb +33 -0
- data/lib/zaniah/gpu/vulkan.rb +233 -0
- data/lib/zaniah/gpu.rb +18 -0
- data/lib/zaniah/hsla.rb +14 -0
- data/lib/zaniah/image.rb +19 -0
- data/lib/zaniah/input/action_registry.rb +12 -0
- data/lib/zaniah/input/composition.rb +7 -0
- data/lib/zaniah/input/context_predicate.rb +87 -0
- data/lib/zaniah/input/dispatcher.rb +59 -0
- data/lib/zaniah/input/file_drop.rb +7 -0
- data/lib/zaniah/input/focus_handle.rb +24 -0
- data/lib/zaniah/input/key_down.rb +7 -0
- data/lib/zaniah/input/key_up.rb +7 -0
- data/lib/zaniah/input/keymap/binding.rb +9 -0
- data/lib/zaniah/input/keymap.rb +55 -0
- data/lib/zaniah/input/keystroke.rb +20 -0
- data/lib/zaniah/input/mouse_down.rb +7 -0
- data/lib/zaniah/input/mouse_move.rb +7 -0
- data/lib/zaniah/input/mouse_up.rb +7 -0
- data/lib/zaniah/input/scroll_wheel.rb +7 -0
- data/lib/zaniah/input/text_input.rb +7 -0
- data/lib/zaniah/input.rb +17 -0
- data/lib/zaniah/layout/available_space.rb +11 -0
- data/lib/zaniah/layout/engine.rb +214 -0
- data/lib/zaniah/layout/node.rb +36 -0
- data/lib/zaniah/layout/style.rb +19 -0
- data/lib/zaniah/layout/tree.rb +16 -0
- data/lib/zaniah/layout.rb +11 -0
- data/lib/zaniah/length.rb +14 -0
- data/lib/zaniah/length_units.rb +9 -0
- data/lib/zaniah/list/height_index.rb +68 -0
- data/lib/zaniah/list.rb +101 -0
- data/lib/zaniah/overlay.rb +10 -0
- data/lib/zaniah/platform/display.rb +7 -0
- data/lib/zaniah/platform/file_event.rb +7 -0
- data/lib/zaniah/platform/headless/window.rb +185 -0
- data/lib/zaniah/platform/linux/appearance_aware.rb +60 -0
- data/lib/zaniah/platform/linux/displays.rb +55 -0
- data/lib/zaniah/platform/linux/free_type.rb +80 -0
- data/lib/zaniah/platform/linux/watcher.rb +70 -0
- data/lib/zaniah/platform/linux/wayland_display_discovery.rb +60 -0
- data/lib/zaniah/platform/linux/wayland_window.rb +369 -0
- data/lib/zaniah/platform/linux/window.rb +418 -0
- data/lib/zaniah/platform/linux.rb +3 -0
- data/lib/zaniah/platform/mac/app.rb +67 -0
- data/lib/zaniah/platform/mac/core_text.rb +94 -0
- data/lib/zaniah/platform/mac/displays.rb +23 -0
- data/lib/zaniah/platform/mac/watcher.rb +63 -0
- data/lib/zaniah/platform/mac/window.rb +298 -0
- data/lib/zaniah/platform/mac.rb +19 -0
- data/lib/zaniah/platform/tui/input_decoder.rb +145 -0
- data/lib/zaniah/platform/tui/text_renderer.rb +40 -0
- data/lib/zaniah/platform/tui/window.rb +91 -0
- data/lib/zaniah/platform/windows/terminal.rb +127 -0
- data/lib/zaniah/platform/windows/watcher.rb +91 -0
- data/lib/zaniah/platform/windows/window.rb +407 -0
- data/lib/zaniah/platform/windows.rb +3 -0
- data/lib/zaniah/platform.rb +34 -0
- data/lib/zaniah/png.rb +118 -0
- data/lib/zaniah/point.rb +8 -0
- data/lib/zaniah/process_pool/worker.rb +7 -0
- data/lib/zaniah/process_pool.rb +239 -0
- data/lib/zaniah/process_wire.rb +127 -0
- data/lib/zaniah/quad_packer.rb +15 -0
- data/lib/zaniah/scene/sprite_batch.rb +7 -0
- data/lib/zaniah/scene.rb +167 -0
- data/lib/zaniah/scene_renderer.rb +8 -0
- data/lib/zaniah/size.rb +5 -0
- data/lib/zaniah/subscription.rb +14 -0
- data/lib/zaniah/svg/path.rb +134 -0
- data/lib/zaniah/svg/rasterizable.rb +186 -0
- data/lib/zaniah/svg.rb +295 -0
- data/lib/zaniah/task/cancelled.rb +7 -0
- data/lib/zaniah/task/timeout.rb +7 -0
- data/lib/zaniah/task.rb +51 -0
- data/lib/zaniah/task_executor.rb +105 -0
- data/lib/zaniah/text.rb +24 -0
- data/lib/zaniah/text_system/atlas/entry.rb +9 -0
- data/lib/zaniah/text_system/atlas.rb +102 -0
- data/lib/zaniah/text_system/atlas_cache.rb +146 -0
- data/lib/zaniah/text_system/font_db/face.rb +9 -0
- data/lib/zaniah/text_system/font_db.rb +193 -0
- data/lib/zaniah/text_system/glyph.rb +7 -0
- data/lib/zaniah/text_system/line_layout.rb +20 -0
- data/lib/zaniah/text_system/renderer.rb +196 -0
- data/lib/zaniah/text_system/shaper.rb +430 -0
- data/lib/zaniah/text_system/typesetter/copy_error.rb +9 -0
- data/lib/zaniah/text_system/typesetter.rb +168 -0
- data/lib/zaniah/text_system.rb +9 -0
- data/lib/zaniah/unicode.rb +13 -0
- data/lib/zaniah/uniform_list.rb +30 -0
- data/lib/zaniah/version.rb +5 -0
- data/lib/zaniah/view.rb +13 -0
- data/lib/zaniah.rb +29 -0
- data/sig/app.rbs +80 -0
- data/sig/elements.rbs +129 -0
- data/sig/gpu.rbs +108 -0
- data/sig/input.rbs +114 -0
- data/sig/layout.rbs +53 -0
- data/sig/native.rbs +88 -0
- data/sig/platform.rbs +94 -0
- data/sig/scene.rbs +52 -0
- data/sig/shaper.rbs +8 -0
- data/sig/text.rbs +137 -0
- data/sig/zaniah.rbs +120 -0
- data/tools/check_dependencies.rb +52 -0
- metadata +249 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fiddle/import"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "open3"
|
|
6
|
+
require_relative "../../ffi/library"
|
|
7
|
+
require_relative "../../gpu/open_gl"
|
|
8
|
+
require_relative "appearance_aware"
|
|
9
|
+
require_relative "displays"
|
|
10
|
+
|
|
11
|
+
module Zaniah
|
|
12
|
+
module Platform
|
|
13
|
+
module Linux
|
|
14
|
+
module Types
|
|
15
|
+
extend Fiddle::Importer
|
|
16
|
+
Visual = struct ["void *visual", "unsigned long visualid", "int screen", "int depth", "int c_class", "unsigned long red_mask", "unsigned long green_mask", "unsigned long blue_mask", "int colormap_size", "int bits_per_rgb"]
|
|
17
|
+
Attributes = struct ["unsigned long background_pixmap", "unsigned long background_pixel", "unsigned long border_pixmap", "unsigned long border_pixel", "int bit_gravity", "int win_gravity", "int backing_store", "unsigned long backing_planes", "unsigned long backing_pixel", "int save_under", "long event_mask", "long do_not_propagate_mask", "int override_redirect", "unsigned long colormap", "unsigned long cursor"]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# X11 works on Xorg and through XWayland on Wayland sessions.
|
|
21
|
+
class Window < Headless::Window
|
|
22
|
+
include AppearanceAware
|
|
23
|
+
def displays = Linux.displays(display_server: :x11)
|
|
24
|
+
I, L, P, V = Fiddle::TYPE_INT, Fiddle::TYPE_LONG, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOID
|
|
25
|
+
attr_reader :handle, :display
|
|
26
|
+
def self.new(display_server: :auto, **options)
|
|
27
|
+
raise ArgumentError, "unknown Linux display server" unless [:auto, :x11, :wayland].include?(display_server)
|
|
28
|
+
if display_server == :wayland || (display_server == :auto && ENV["WAYLAND_DISPLAY"])
|
|
29
|
+
begin
|
|
30
|
+
require_relative "wayland_window"
|
|
31
|
+
return WaylandWindow.new(**options)
|
|
32
|
+
rescue LoadError, Error => error
|
|
33
|
+
raise if display_server == :wayland || !ENV["DISPLAY"]
|
|
34
|
+
warn "Zaniah UI: Wayland unavailable (#{error.message}); trying X11"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
super(**options)
|
|
38
|
+
end
|
|
39
|
+
def initialize(gpu: :opengl, **options)
|
|
40
|
+
super(**options)
|
|
41
|
+
@x = FFI::Library.new("libX11.so.6")
|
|
42
|
+
@gl = FFI::Library.new("libGL.so.1")
|
|
43
|
+
@display = x(:XOpenDisplay, [P], P, 0)
|
|
44
|
+
raise Error, "cannot open X display; set DISPLAY (Wayland requires XWayland)" if @display.null?
|
|
45
|
+
@screen = x(:XDefaultScreen, [P], I, @display)
|
|
46
|
+
@root = x(:XRootWindow, [P, I], L, @display, @screen)
|
|
47
|
+
attributes = [0x8012, 1, 0x8010, 1, 0x8011, 1, 8, 8, 9, 8, 10, 8, 11, 8, 5, 1, 0].pack("i*")
|
|
48
|
+
count = [0].pack("i")
|
|
49
|
+
configs = @gl.fn(:glXChooseFBConfig, [P, I, P, P], P).call(@display, @screen, attributes, count)
|
|
50
|
+
raise Error, "no GLX framebuffer configuration" if configs.null? || count.unpack1("i").zero?
|
|
51
|
+
@config = Fiddle::Pointer.new(configs)[0, Fiddle::SIZEOF_VOIDP].unpack1("J")
|
|
52
|
+
visual_ptr = @gl.fn(:glXGetVisualFromFBConfig, [P, P], P).call(@display, @config)
|
|
53
|
+
visual = Types::Visual.new(visual_ptr)
|
|
54
|
+
@colormap = x(:XCreateColormap, [P, L, P, I], L, @display, @root, visual.visual, 0)
|
|
55
|
+
attrs = Types::Attributes.malloc(Fiddle::RUBY_FREE)
|
|
56
|
+
attrs.to_ptr[0, Types::Attributes.size] = "\0" * Types::Attributes.size
|
|
57
|
+
attrs.colormap = @colormap
|
|
58
|
+
attrs.event_mask = (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 6) | (1 << 15) | (1 << 17) | (1 << 21) | (1 << 22)
|
|
59
|
+
@handle = x(:XCreateWindow, [P, L, I, I, I, I, I, I, I, P, L, P], L, @display, @root, 0, 0, content_size.width.to_i, content_size.height.to_i, 0, visual.depth, 1, visual.visual, (1 << 11) | (1 << 13), attrs)
|
|
60
|
+
x(:XFree, [P], I, visual_ptr)
|
|
61
|
+
x(:XFree, [P], I, configs)
|
|
62
|
+
raise Error, "XCreateWindow failed" if @handle.zero?
|
|
63
|
+
@atoms = {}
|
|
64
|
+
protocol = [atom("WM_DELETE_WINDOW")].pack("L!")
|
|
65
|
+
x(:XSetWMProtocols, [P, L, P, I], I, @display, @handle, protocol, 1)
|
|
66
|
+
property(@handle, atom("XdndAware"), 4, [5].pack("L!"), format: 32)
|
|
67
|
+
self.title = @title
|
|
68
|
+
dpi = @x.fn(:XResourceManagerString, [P], P).call(@display)
|
|
69
|
+
@scale_factor = dpi.null? ? 1.0 : (dpi.to_s[/Xft\.dpi:\s*(\d+(?:\.\d+)?)/, 1]&.to_f || 96) / 96.0
|
|
70
|
+
@scale_factor = @scale_factor.clamp(1.0, 4.0)
|
|
71
|
+
@content_size = Size.new(content_size.width / @scale_factor, content_size.height / @scale_factor)
|
|
72
|
+
@device.release
|
|
73
|
+
@device = create_device(gpu)
|
|
74
|
+
x(:XMapWindow, [P, L], I, @display, @handle)
|
|
75
|
+
x(:XFlush, [P], I, @display)
|
|
76
|
+
setup_ime
|
|
77
|
+
setup_display_events
|
|
78
|
+
end
|
|
79
|
+
def x(name, types, result, *values) = @x.fn(name, types, result).call(*values)
|
|
80
|
+
def atom(name) = @atoms[name] ||= x(:XInternAtom, [P, P, I], L, @display, name, 0)
|
|
81
|
+
def create_device(backend)
|
|
82
|
+
raise ArgumentError, "X11 supports :opengl" unless [:opengl, :gl].include?(backend)
|
|
83
|
+
resolver = ->(name) { @gl.fn(:glXGetProcAddressARB, [P], P).call(name) }
|
|
84
|
+
address = resolver.call("glXCreateContextAttribsARB")
|
|
85
|
+
raise Error, "GLX_ARB_create_context unavailable" if address.null?
|
|
86
|
+
create = Fiddle::Function.new(address, [P, P, P, I, P], P)
|
|
87
|
+
attributes = [0x2091, 3, 0x2092, 3, 0x9126, 1, 0].pack("i*")
|
|
88
|
+
@context = create.call(@display, @config, 0, 1, attributes)
|
|
89
|
+
raise Error, "OpenGL 3.3 context unavailable" if @context.null?
|
|
90
|
+
make_current
|
|
91
|
+
extension = @gl.fn(:glXQueryExtensionsString, [P, I], P).call(@display, @screen).to_s
|
|
92
|
+
if extension.split.include?("GLX_EXT_swap_control")
|
|
93
|
+
Fiddle::Function.new(resolver.call("glXSwapIntervalEXT"), [P, L, I], V).call(@display, @handle, 1)
|
|
94
|
+
end
|
|
95
|
+
GPU::OpenGL.new(self, library: @gl, resolver: resolver)
|
|
96
|
+
end
|
|
97
|
+
def make_current
|
|
98
|
+
ok = @gl.fn(:glXMakeCurrent, [P, L, P], I).call(@display, @handle, @context)
|
|
99
|
+
raise Error, "glXMakeCurrent failed" if ok.zero?
|
|
100
|
+
end
|
|
101
|
+
def swap_buffers = @gl.fn(:glXSwapBuffers, [P, L], V).call(@display, @handle)
|
|
102
|
+
def title=(title)
|
|
103
|
+
super
|
|
104
|
+
return unless @handle
|
|
105
|
+
x(:XStoreName, [P, L, P], I, @display, @handle, title)
|
|
106
|
+
property(@handle, atom("_NET_WM_NAME"), atom("UTF8_STRING"), title.encode("UTF-8"))
|
|
107
|
+
end
|
|
108
|
+
def tick
|
|
109
|
+
poll_events unless closed?
|
|
110
|
+
poll_appearance unless closed?
|
|
111
|
+
raise @native_error if @native_error
|
|
112
|
+
super
|
|
113
|
+
end
|
|
114
|
+
def run
|
|
115
|
+
fd = x(:XConnectionNumber, [P], I, @display)
|
|
116
|
+
io = IO.for_fd(fd, autoclose: false)
|
|
117
|
+
until closed?
|
|
118
|
+
tick
|
|
119
|
+
IO.select([io], nil, nil, 0.05) unless dirty? || closed?
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
def poll_events
|
|
123
|
+
event = "\0".b * 192
|
|
124
|
+
while !closed? && x(:XPending, [P], I, @display).positive?
|
|
125
|
+
x(:XNextEvent, [P, P], I, @display, event)
|
|
126
|
+
# XIM protocol replies target a private Xlib window, not our view.
|
|
127
|
+
# None preserves the original event target for the registered filter.
|
|
128
|
+
next if x(:XFilterEvent, [P, L], I, event, 0) != 0
|
|
129
|
+
type = event.unpack1("i")
|
|
130
|
+
if @randr_event && [@randr_event, @randr_event + 1].include?(type)
|
|
131
|
+
refresh_scale
|
|
132
|
+
request_frame
|
|
133
|
+
next
|
|
134
|
+
end
|
|
135
|
+
case type
|
|
136
|
+
when 2, 3 then key_event(event, type)
|
|
137
|
+
when 4, 5, 6 then mouse_event(event, type)
|
|
138
|
+
when 9 then x(:XSetICFocus, [P], V, @ic) if @ic
|
|
139
|
+
when 10 then x(:XUnsetICFocus, [P], V, @ic) if @ic
|
|
140
|
+
when 12 then request_frame
|
|
141
|
+
when 22
|
|
142
|
+
width, height = event[56, 8].unpack("i2")
|
|
143
|
+
resize(width / @scale_factor, height / @scale_factor) if width.positive? && height.positive?
|
|
144
|
+
@on_moved&.call(event[48, 8].unpack("i2"))
|
|
145
|
+
when 28 then refresh_scale if event[40, 8].unpack1("L!") == atom("RESOURCE_MANAGER")
|
|
146
|
+
when 30 then selection_request(event)
|
|
147
|
+
when 31
|
|
148
|
+
name = event[56, 8].unpack1("L!")
|
|
149
|
+
event[40, 8].unpack1("L!") == atom("XdndSelection") ? finish_drop(name) : @selection_received = name
|
|
150
|
+
when 33 then client_message(event)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
def modifiers(state) = [[4, "ctrl"], [8, "alt"], [1, "shift"], [64, "cmd"]].filter_map { |mask, name| name unless (state & mask).zero? }
|
|
155
|
+
def key_event(event, type)
|
|
156
|
+
state = event[80, 4].unpack1("I")
|
|
157
|
+
keysym = x(:XLookupKeysym, [P, I], L, event, 0)
|
|
158
|
+
name = x(:XKeysymToString, [L], P, keysym)
|
|
159
|
+
key = name.null? ? "" : name.to_s.downcase
|
|
160
|
+
key = {"return" => "enter", "escape" => "esc", "backspace" => "backspace", "prior" => "pageup", "next" => "pagedown"}.fetch(key, key)
|
|
161
|
+
key = (modifiers(state) + [key]).join("-")
|
|
162
|
+
input(type == 2 ? Input::KeyDown.new(key, false) : Input::KeyUp.new(key)) unless type == 2 && @preedit && !@preedit.empty?
|
|
163
|
+
return unless type == 2 && (state & (4 | 64)).zero?
|
|
164
|
+
bytes, symbol, status = "\0".b * 4096, [0].pack("L!"), [0].pack("i")
|
|
165
|
+
length = if @ic
|
|
166
|
+
x(:Xutf8LookupString, [P, P, P, I, P, P], I, @ic, event, bytes, bytes.bytesize, symbol, status)
|
|
167
|
+
else
|
|
168
|
+
x(:XLookupString, [P, P, I, P, P], I, event, bytes, bytes.bytesize, symbol, 0)
|
|
169
|
+
end
|
|
170
|
+
if length > bytes.bytesize
|
|
171
|
+
bytes = "\0".b * length
|
|
172
|
+
length = x(:Xutf8LookupString, [P, P, P, I, P, P], I, @ic, event, bytes, bytes.bytesize, symbol, status)
|
|
173
|
+
end
|
|
174
|
+
text = bytes.byteslice(0, [length, 0].max).force_encoding("UTF-8").scrub
|
|
175
|
+
input(Input::TextInput.new(text)) unless text.empty? || text.match?(/\A[\x00-\x1f\x7f]+\z/)
|
|
176
|
+
end
|
|
177
|
+
def mouse_event(event, type)
|
|
178
|
+
px, py = event[64, 8].unpack("i2")
|
|
179
|
+
position = Point.new(px / @scale_factor, py / @scale_factor)
|
|
180
|
+
state, button = event[80, 8].unpack("I2")
|
|
181
|
+
mods = modifiers(state)
|
|
182
|
+
if type == 4 && (4..7).cover?(button)
|
|
183
|
+
delta = button <= 5 ? Point.new(0, button == 4 ? -40 : 40) : Point.new(button == 6 ? -40 : 40, 0)
|
|
184
|
+
input(Input::ScrollWheel.new(position, delta, :changed, mods))
|
|
185
|
+
elsif !(4..7).cover?(button) || type == 6
|
|
186
|
+
button = {1 => :left, 2 => :middle, 3 => :right}.fetch(button, :other)
|
|
187
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
188
|
+
count = @last_click && @last_click[0] == button && now - @last_click[1] < 0.4 && (position.x - @last_click[2].x).abs < 4 && (position.y - @last_click[2].y).abs < 4 ? 2 : 1
|
|
189
|
+
@last_click = [button, now, position] if type == 4
|
|
190
|
+
input(type == 6 ? Input::MouseMove.new(position, mods) : type == 4 ? Input::MouseDown.new(position, button, mods, count) : Input::MouseUp.new(position, button, mods))
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
def setup_ime
|
|
194
|
+
libc = FFI::Library.new(nil)
|
|
195
|
+
libc.fn(:setlocale, [I, P], P).call(0, "")
|
|
196
|
+
x(:XSetLocaleModifiers, [P], P, "")
|
|
197
|
+
@im = x(:XOpenIM, [P] * 4, P, @display, 0, 0, 0)
|
|
198
|
+
return if @im.null?
|
|
199
|
+
function = @x.fn(:XCreateIC, [P, Fiddle::TYPE_VARIADIC], P)
|
|
200
|
+
@preedit, @ime_callbacks = "", []
|
|
201
|
+
callbacks = {
|
|
202
|
+
"preeditStartCallback" => [I, ->(_data) { @preedit = ""; -1 }],
|
|
203
|
+
"preeditDoneCallback" => [V, ->(_data) { @preedit = ""; input(Input::Composition.new("", [0, 0])) }],
|
|
204
|
+
"preeditDrawCallback" => [V, ->(data) { preedit_draw(data) }],
|
|
205
|
+
"preeditCaretCallback" => [V, ->(data) { input(Input::Composition.new(@preedit, [Fiddle::Pointer.new(data)[0, 4].unpack1("i"), 0])) }]
|
|
206
|
+
}.map do |name, (result, handler)|
|
|
207
|
+
callback = Fiddle::Closure::BlockCaller.new(result, [P, P, P]) do |_ic, _client, data|
|
|
208
|
+
begin
|
|
209
|
+
handler.call(data)
|
|
210
|
+
rescue StandardError => error
|
|
211
|
+
@native_error = error
|
|
212
|
+
result == I ? 0 : nil
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
record = [0, callback.to_i].pack("J2")
|
|
216
|
+
@ime_callbacks << [callback, record]
|
|
217
|
+
[P, name, P, record]
|
|
218
|
+
end.flatten
|
|
219
|
+
nested = @x.fn(:XVaCreateNestedList, [I, Fiddle::TYPE_VARIADIC], P).call(0, *callbacks, P, 0)
|
|
220
|
+
@ic = function.call(@im, P, "inputStyle", L, 0x0402, P, "clientWindow", L, @handle, P, "focusWindow", L, @handle, P, "preeditAttributes", P, nested, P, 0)
|
|
221
|
+
x(:XFree, [P], I, nested) unless nested.null?
|
|
222
|
+
@ic = function.call(@im, P, "inputStyle", L, 0x0408, P, "clientWindow", L, @handle, P, "focusWindow", L, @handle, P, 0) if @ic.null?
|
|
223
|
+
@ic = nil if @ic.null?
|
|
224
|
+
end
|
|
225
|
+
def preedit_draw(data)
|
|
226
|
+
pointer = Fiddle::Pointer.new(data)
|
|
227
|
+
caret, first, length = pointer[0, 12].unpack("i3")
|
|
228
|
+
native_text = pointer[16, 8].unpack1("J")
|
|
229
|
+
text = ""
|
|
230
|
+
unless native_text.zero?
|
|
231
|
+
descriptor = Fiddle::Pointer.new(native_text)
|
|
232
|
+
count = descriptor[0, 2].unpack1("S")
|
|
233
|
+
wide = descriptor[16, 4].unpack1("i") != 0
|
|
234
|
+
string = Fiddle::Pointer.new(descriptor[24, 8].unpack1("J"))
|
|
235
|
+
text = wide ? string[0, count * 4].unpack("I*").pack("U*") : string.to_s.force_encoding("UTF-8").scrub unless string.null?
|
|
236
|
+
end
|
|
237
|
+
characters = @preedit.chars
|
|
238
|
+
characters[first, length] = text.chars
|
|
239
|
+
@preedit = characters.join
|
|
240
|
+
input(Input::Composition.new(@preedit, [caret, 0]))
|
|
241
|
+
end
|
|
242
|
+
def ime_state=(bounds)
|
|
243
|
+
@ime_state = bounds
|
|
244
|
+
return unless @ic && bounds.is_a?(Bounds)
|
|
245
|
+
point = [(bounds.x * @scale_factor).round.clamp(-32_768, 32_767), (bounds.bottom * @scale_factor).round.clamp(-32_768, 32_767)].pack("s2")
|
|
246
|
+
nested = @x.fn(:XVaCreateNestedList, [I, Fiddle::TYPE_VARIADIC], P).call(0, P, "spotLocation", P, point, P, 0)
|
|
247
|
+
@x.fn(:XSetICValues, [P, Fiddle::TYPE_VARIADIC], P).call(@ic, P, "preeditAttributes", P, nested, P, 0)
|
|
248
|
+
x(:XFree, [P], I, nested)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def property(window, name, type, bytes, format: 8)
|
|
252
|
+
x(:XChangeProperty, [P, L, L, L, I, I, P, I], I, @display, window, name, type, format, 0, bytes, format == 8 ? bytes.bytesize : bytes.bytesize / Fiddle::SIZEOF_LONG)
|
|
253
|
+
end
|
|
254
|
+
def setup_display_events
|
|
255
|
+
x(:XSelectInput, [P, L, L], I, @display, @root, 1 << 22)
|
|
256
|
+
@randr = FFI::Library.new("libXrandr.so.2")
|
|
257
|
+
base, error = [0].pack("i"), [0].pack("i")
|
|
258
|
+
if @randr.fn(:XRRQueryExtension, [P, P, P], I).call(@display, base, error) != 0
|
|
259
|
+
@randr_event = base.unpack1("i")
|
|
260
|
+
@randr.fn(:XRRSelectInput, [P, L, I], V).call(@display, @root, 1 | 2 | 4 | 64)
|
|
261
|
+
end
|
|
262
|
+
rescue LoadError
|
|
263
|
+
# X11 without RandR still gets global Xft DPI property notifications.
|
|
264
|
+
@randr = nil
|
|
265
|
+
end
|
|
266
|
+
def refresh_scale
|
|
267
|
+
resource = read_property(@root, atom("RESOURCE_MANAGER"), delete: false)
|
|
268
|
+
factor = ((resource[/Xft\.dpi:\s*(\d+(?:\.\d+)?)/, 1]&.to_f || 96) / 96.0).clamp(1.0, 4.0)
|
|
269
|
+
return if factor == @scale_factor
|
|
270
|
+
width, height = content_size.width * @scale_factor, content_size.height * @scale_factor
|
|
271
|
+
@scale_factor = factor
|
|
272
|
+
resize(width / factor, height / factor)
|
|
273
|
+
end
|
|
274
|
+
def clipboard=(value)
|
|
275
|
+
@clipboard = value.to_s.encode("UTF-8")
|
|
276
|
+
x(:XSetSelectionOwner, [P, L, L, L], I, @display, atom("CLIPBOARD"), @handle, 0)
|
|
277
|
+
x(:XFlush, [P], I, @display)
|
|
278
|
+
end
|
|
279
|
+
def clipboard = selection("UTF8_STRING")
|
|
280
|
+
def clipboard_paths
|
|
281
|
+
self.class.file_paths(selection("text/uri-list"))
|
|
282
|
+
end
|
|
283
|
+
def self.file_paths(bytes)
|
|
284
|
+
bytes.lines.filter_map do |line|
|
|
285
|
+
next if line.start_with?("#")
|
|
286
|
+
uri = URI.parse(line.strip) rescue nil
|
|
287
|
+
next unless uri&.scheme == "file" && [nil, "", "localhost"].include?(uri.host)
|
|
288
|
+
path = URI::DEFAULT_PARSER.unescape(uri.path).force_encoding("UTF-8")
|
|
289
|
+
path if path.start_with?("/") && !path.include?("\0") && path.valid_encoding?
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
def selection(target)
|
|
293
|
+
return @clipboard.to_s if x(:XGetSelectionOwner, [P, L], L, @display, atom("CLIPBOARD")) == @handle
|
|
294
|
+
@selection_received = nil
|
|
295
|
+
name = atom("ZANIAH_SELECTION")
|
|
296
|
+
x(:XConvertSelection, [P, L, L, L, L, L], I, @display, atom("CLIPBOARD"), atom(target), name, @handle, 0)
|
|
297
|
+
x(:XFlush, [P], I, @display)
|
|
298
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 2
|
|
299
|
+
while @selection_received.nil? && Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
|
|
300
|
+
poll_events
|
|
301
|
+
IO.select([IO.for_fd(x(:XConnectionNumber, [P], I, @display), autoclose: false)], nil, nil, 0.01)
|
|
302
|
+
end
|
|
303
|
+
return "" unless @selection_received && @selection_received != 0
|
|
304
|
+
read_property(@handle, name).force_encoding("UTF-8").scrub
|
|
305
|
+
end
|
|
306
|
+
def read_property(window, name, delete: true)
|
|
307
|
+
actual_type, format, length, remaining, pointer = [0].pack("L!"), [0].pack("i"), [0].pack("L!"), [0].pack("L!"), [0].pack("J")
|
|
308
|
+
x(:XGetWindowProperty, [P, L, L, L, L, I, L, P, P, P, P, P], I, @display, window, name, 0, 4_194_304, delete ? 1 : 0, 0, actual_type, format, length, remaining, pointer)
|
|
309
|
+
data = Fiddle::Pointer.new(pointer.unpack1("J"))
|
|
310
|
+
return "" if data.null?
|
|
311
|
+
raise Error, "incremental X11 clipboard transfer is not supported" if actual_type.unpack1("L!") == atom("INCR")
|
|
312
|
+
raise Error, "X11 selection exceeds 16 MiB" unless remaining.unpack1("L!").zero?
|
|
313
|
+
size = {8 => 1, 16 => 2, 32 => Fiddle::SIZEOF_LONG}.fetch(format.unpack1("i"), 0)
|
|
314
|
+
data[0, length.unpack1("L!") * size]
|
|
315
|
+
ensure
|
|
316
|
+
x(:XFree, [P], I, data) if data && !data.null?
|
|
317
|
+
end
|
|
318
|
+
def client_message(event)
|
|
319
|
+
type, data = event[40, 8].unpack1("L!"), event[56, 40].unpack("L!5")
|
|
320
|
+
case type
|
|
321
|
+
when atom("WM_PROTOCOLS") then close if data[0] == atom("WM_DELETE_WINDOW")
|
|
322
|
+
when atom("XdndEnter")
|
|
323
|
+
@drag_source = data[0]
|
|
324
|
+
types = data[1].odd? ? read_property(@drag_source, atom("XdndTypeList"), delete: false).unpack("L!*") : data[2, 3]
|
|
325
|
+
@drag_accept = types.include?(atom("text/uri-list"))
|
|
326
|
+
when atom("XdndLeave") then @drag_source = nil
|
|
327
|
+
when atom("XdndPosition")
|
|
328
|
+
return unless data[0] == @drag_source
|
|
329
|
+
root_x, root_y = [data[2] >> 16, data[2] & 0xffff].map { |n| n >= 0x8000 ? n - 0x10000 : n }
|
|
330
|
+
left, top, child = [0].pack("i"), [0].pack("i"), [0].pack("L!")
|
|
331
|
+
x(:XTranslateCoordinates, [P, L, L, I, I, P, P, P], I, @display, @root, @handle, root_x, root_y, left, top, child)
|
|
332
|
+
@drag_position = Point.new(left.unpack1("i") / @scale_factor, top.unpack1("i") / @scale_factor)
|
|
333
|
+
send_client_message(@drag_source, "XdndStatus", [@handle, @drag_accept ? 3 : 2, 0, 0, @drag_accept ? atom("XdndActionCopy") : 0])
|
|
334
|
+
when atom("XdndDrop")
|
|
335
|
+
return unless data[0] == @drag_source
|
|
336
|
+
return finish_drop(0) unless @drag_accept
|
|
337
|
+
x(:XConvertSelection, [P, L, L, L, L, L], I, @display, atom("XdndSelection"), atom("text/uri-list"), atom("ZANIAH_DROP"), @handle, data[2])
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
def finish_drop(property_name)
|
|
341
|
+
return unless @drag_source
|
|
342
|
+
paths = property_name.zero? ? [] : self.class.file_paths(read_property(@handle, property_name))
|
|
343
|
+
send_client_message(@drag_source, "XdndFinished", [@handle, paths.empty? ? 0 : 1, paths.empty? ? 0 : atom("XdndActionCopy"), 0, 0])
|
|
344
|
+
@drag_source = nil
|
|
345
|
+
input(Input::FileDrop.new(paths.freeze, @drag_position || Point.new(0, 0))) unless paths.empty?
|
|
346
|
+
end
|
|
347
|
+
def send_client_message(window, name, data)
|
|
348
|
+
event = "\0".b * 192
|
|
349
|
+
event[0, 4] = [33].pack("i")
|
|
350
|
+
event[24, 24] = [@display.to_i, window, atom(name)].pack("J3")
|
|
351
|
+
event[48, 4] = [32].pack("i")
|
|
352
|
+
event[56, 40] = data.pack("L!5")
|
|
353
|
+
x(:XSendEvent, [P, L, I, L, P], I, @display, window, 0, 0, event)
|
|
354
|
+
x(:XFlush, [P], I, @display)
|
|
355
|
+
end
|
|
356
|
+
def selection_request(event)
|
|
357
|
+
requestor, selection, target, prop, time = event[40, 40].unpack("L!5")
|
|
358
|
+
prop = target if prop.zero?
|
|
359
|
+
if target == atom("TARGETS")
|
|
360
|
+
property(requestor, prop, 4, [atom("TARGETS"), atom("UTF8_STRING"), atom("STRING")].pack("L!3"), format: 32)
|
|
361
|
+
elsif [atom("UTF8_STRING"), atom("STRING")].include?(target)
|
|
362
|
+
property(requestor, prop, target, @clipboard.to_s)
|
|
363
|
+
else
|
|
364
|
+
prop = 0
|
|
365
|
+
end
|
|
366
|
+
reply = "\0".b * 192
|
|
367
|
+
reply[0, 4] = [31].pack("i")
|
|
368
|
+
reply[24, 8] = [@display.to_i].pack("J")
|
|
369
|
+
reply[32, 40] = [requestor, selection, target, prop, time].pack("L!5")
|
|
370
|
+
x(:XSendEvent, [P, L, I, L, P], I, @display, requestor, 0, 0, reply)
|
|
371
|
+
x(:XFlush, [P], I, @display)
|
|
372
|
+
end
|
|
373
|
+
def cursor_style=(style)
|
|
374
|
+
number = {arrow: 68, text: 152, pointer: 60, crosshair: 34, resize_horizontal: 108, resize_vertical: 116}.fetch(style)
|
|
375
|
+
cursor = x(:XCreateFontCursor, [P, I], L, @display, number)
|
|
376
|
+
x(:XDefineCursor, [P, L, L], I, @display, @handle, cursor)
|
|
377
|
+
x(:XFreeCursor, [P, L], I, @display, cursor)
|
|
378
|
+
end
|
|
379
|
+
def prompt_for_paths(multiple: false, directories: false, save: false)
|
|
380
|
+
command = ["zenity", "--file-selection", "--separator=\n"]
|
|
381
|
+
command << "--multiple" if multiple
|
|
382
|
+
command << "--directory" if directories
|
|
383
|
+
command << "--save" << "--confirm-overwrite" if save
|
|
384
|
+
output, status = Open3.capture2(*command)
|
|
385
|
+
status.success? ? output.lines.map(&:chomp) : []
|
|
386
|
+
rescue Errno::ENOENT
|
|
387
|
+
raise Error, "file selection requires the desktop's zenity command"
|
|
388
|
+
end
|
|
389
|
+
def open_url(url)
|
|
390
|
+
Process.detach(Process.spawn("xdg-open", url.to_s, out: File::NULL, err: File::NULL))
|
|
391
|
+
true
|
|
392
|
+
end
|
|
393
|
+
def toggle_fullscreen
|
|
394
|
+
event = "\0".b * 192
|
|
395
|
+
event[0, 4] = [33].pack("i")
|
|
396
|
+
event[24, 8] = [@display.to_i].pack("J")
|
|
397
|
+
event[32, 8] = [@handle].pack("L!")
|
|
398
|
+
event[40, 8] = [atom("_NET_WM_STATE")].pack("L!")
|
|
399
|
+
event[48, 4] = [32].pack("i")
|
|
400
|
+
event[56, 24] = [2, atom("_NET_WM_STATE_FULLSCREEN"), 0].pack("L!3")
|
|
401
|
+
x(:XSendEvent, [P, L, I, L, P], I, @display, @root, 0, (1 << 19) | (1 << 20), event)
|
|
402
|
+
end
|
|
403
|
+
def close
|
|
404
|
+
return false unless super
|
|
405
|
+
close_appearance
|
|
406
|
+
x(:XDestroyIC, [P], V, @ic) if @ic
|
|
407
|
+
x(:XCloseIM, [P], I, @im) if @im && !@im.null?
|
|
408
|
+
@gl.fn(:glXMakeCurrent, [P, L, P], I).call(@display, 0, 0)
|
|
409
|
+
@gl.fn(:glXDestroyContext, [P, P], V).call(@display, @context)
|
|
410
|
+
x(:XDestroyWindow, [P, L], I, @display, @handle)
|
|
411
|
+
x(:XFreeColormap, [P, L], I, @display, @colormap)
|
|
412
|
+
x(:XCloseDisplay, [P], I, @display)
|
|
413
|
+
true
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
module Platform
|
|
5
|
+
module Mac
|
|
6
|
+
class App
|
|
7
|
+
EVENT_MASK = (1 << 64) - 1
|
|
8
|
+
EVENT_ARGUMENTS = [:ulong, :pointer, :pointer, :bool].freeze
|
|
9
|
+
POINTER_ARGUMENT = [:pointer].freeze
|
|
10
|
+
WAIT_ARGUMENT = [:double].freeze
|
|
11
|
+
|
|
12
|
+
attr_reader :handle
|
|
13
|
+
def self.instance = @instance ||= new
|
|
14
|
+
def initialize
|
|
15
|
+
raise Error, "Cocoa must run on the main Ruby thread" unless Thread.current == Thread.main
|
|
16
|
+
@handle = O.send(O.klass("NSApplication"), "sharedApplication")
|
|
17
|
+
@run_loop_mode = O.send(O.string("kCFRunLoopDefaultMode"), "retain")
|
|
18
|
+
O.subclass("ZaniahApplicationDelegate", "NSObject", protocols: ["NSApplicationDelegate"]) do |klass|
|
|
19
|
+
O.method(klass, "applicationShouldTerminate:", args: [:pointer], result: :ulong, encoding: "Q@:@") do |_delegate, _, _application|
|
|
20
|
+
WINDOWS.values.uniq.each { |window| break unless window.close }
|
|
21
|
+
# Ruby's window loop owns shutdown and ensures; Cocoa must not exit
|
|
22
|
+
# the process before close handlers have checked unsaved buffers.
|
|
23
|
+
0
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
@delegate = O.new("ZaniahApplicationDelegate")
|
|
27
|
+
O.send(@handle, "setDelegate:", @delegate, args: [:pointer], result: :void)
|
|
28
|
+
O.send(@handle, "setActivationPolicy:", 0, args: [:long], result: :bool)
|
|
29
|
+
menu, item, submenu = O.new("NSMenu"), O.new("NSMenuItem"), O.new("NSMenu")
|
|
30
|
+
quit = O.send(O.alloc("NSMenuItem"), "initWithTitle:action:keyEquivalent:", O.string("Quit"), O.selector("terminate:"), O.string("q"), args: [:pointer] * 3)
|
|
31
|
+
O.send(submenu, "addItem:", quit, args: [:pointer], result: :void)
|
|
32
|
+
O.send(item, "setSubmenu:", submenu, args: [:pointer], result: :void)
|
|
33
|
+
O.send(menu, "addItem:", item, args: [:pointer], result: :void)
|
|
34
|
+
O.send(@handle, "setMainMenu:", menu, args: [:pointer], result: :void)
|
|
35
|
+
[quit, submenu, item, menu].each { |object| O.release(object) }
|
|
36
|
+
O.send(@handle, "finishLaunching", result: :void)
|
|
37
|
+
O.send(@handle, "activateIgnoringOtherApps:", 1, args: [:bool], result: :void)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def poll(wait: 0)
|
|
41
|
+
pool = O.new("NSAutoreleasePool")
|
|
42
|
+
# Cocoa treats a nil deadline as distantPast: drain queued events
|
|
43
|
+
# without allocating an NSDate for each nonblocking poll.
|
|
44
|
+
deadline = wait.zero? ? 0 : O.send(O.klass("NSDate"), "dateWithTimeIntervalSinceNow:", wait, args: WAIT_ARGUMENT)
|
|
45
|
+
loop do
|
|
46
|
+
event = O.send(@handle, "nextEventMatchingMask:untilDate:inMode:dequeue:", EVENT_MASK,
|
|
47
|
+
deadline, @run_loop_mode, 1, args: EVENT_ARGUMENTS)
|
|
48
|
+
break if event.zero?
|
|
49
|
+
O.send(@handle, "sendEvent:", event, args: POINTER_ARGUMENT, result: :void)
|
|
50
|
+
deadline = 0
|
|
51
|
+
end
|
|
52
|
+
O.send(@handle, "updateWindows", result: :void)
|
|
53
|
+
ensure
|
|
54
|
+
O.release(pool) if pool
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def run
|
|
58
|
+
yield self if block_given?
|
|
59
|
+
until WINDOWS.values.uniq.all?(&:closed?)
|
|
60
|
+
poll(wait: WINDOWS.values.any?(&:dirty?) ? 0 : 0.05)
|
|
61
|
+
WINDOWS.values.uniq.each { |window| window.tick(poll_events: false) }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../ffi/struct"
|
|
4
|
+
|
|
5
|
+
module Zaniah
|
|
6
|
+
module Platform
|
|
7
|
+
module Mac
|
|
8
|
+
class CoreText
|
|
9
|
+
P, L, D, V = Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG, Fiddle::TYPE_DOUBLE, Fiddle::TYPE_VOID
|
|
10
|
+
def initialize
|
|
11
|
+
require "alhena"
|
|
12
|
+
@cf = FFI::Library.new("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")
|
|
13
|
+
@ct = FFI::Library.new("/System/Library/Frameworks/CoreText.framework/CoreText")
|
|
14
|
+
@cg = FFI::Library.new("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")
|
|
15
|
+
@descriptors, @fonts = {}, {}
|
|
16
|
+
@bounds = FFI::Struct::Signature.new([:pointer], :rect)
|
|
17
|
+
end
|
|
18
|
+
def cache_key
|
|
19
|
+
require "etc"
|
|
20
|
+
@cache_key ||= "coretext/#{RUBY_PLATFORM}/#{Etc.uname.values_at(:release, :version).join('/')}".freeze
|
|
21
|
+
end
|
|
22
|
+
def native_font(font, size)
|
|
23
|
+
@fonts[[font, size]] ||= begin
|
|
24
|
+
descriptor = @descriptors[font] ||= begin
|
|
25
|
+
bytes = font.data
|
|
26
|
+
data = @cf.fn(:CFDataCreate, [P, P, L], P).call(0, bytes, bytes.bytesize)
|
|
27
|
+
list = @ct.fn(:CTFontManagerCreateFontDescriptorsFromData, [P], P).call(data)
|
|
28
|
+
raise Error, "CoreText cannot read font data" if list.null?
|
|
29
|
+
count = @cf.fn(:CFArrayGetCount, [P], L).call(list)
|
|
30
|
+
raise Error, "CoreText font collection index out of range" unless font.index < count
|
|
31
|
+
item = @cf.fn(:CFArrayGetValueAtIndex, [P, L], P).call(list, font.index)
|
|
32
|
+
item = @cf.fn(:CFRetain, [P], P).call(item)
|
|
33
|
+
@cf.fn(:CFRelease, [P], V).call(list)
|
|
34
|
+
@cf.fn(:CFRelease, [P], V).call(data)
|
|
35
|
+
item
|
|
36
|
+
end
|
|
37
|
+
handle = @ct.fn(:CTFontCreateWithFontDescriptor, [P, D, P], P).call(descriptor, size, 0)
|
|
38
|
+
font.axis_values.empty? ? handle : varied_font(handle, font.axis_values)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
def varied_font(handle, axes)
|
|
42
|
+
descriptor = @ct.fn(:CTFontCopyFontDescriptor, [P], P).call(handle)
|
|
43
|
+
axes.each do |tag, value|
|
|
44
|
+
key = @cf.fn(:CFNumberCreate, [P, Fiddle::TYPE_INT, P], P).call(0, 3, [tag.unpack1("N")].pack("i"))
|
|
45
|
+
next_descriptor = @ct.fn(:CTFontDescriptorCreateCopyWithVariation, [P, P, D], P).call(descriptor, key, value)
|
|
46
|
+
@cf.fn(:CFRelease, [P], V).call(key)
|
|
47
|
+
@cf.fn(:CFRelease, [P], V).call(descriptor)
|
|
48
|
+
descriptor = next_descriptor
|
|
49
|
+
end
|
|
50
|
+
result = @ct.fn(:CTFontCreateCopyWithAttributes, [P, D, P, P], P).call(handle, 0, 0, descriptor)
|
|
51
|
+
raise Error, "CoreText rejected font variations" if result.null?
|
|
52
|
+
result
|
|
53
|
+
ensure
|
|
54
|
+
@cf.fn(:CFRelease, [P], V).call(handle)
|
|
55
|
+
@cf.fn(:CFRelease, [P], V).call(descriptor) if descriptor
|
|
56
|
+
end
|
|
57
|
+
def rasterize(font, glyph, size:, subpixel_x: 0, **_options)
|
|
58
|
+
raise ArgumentError, "invalid font size" unless size.is_a?(Numeric) && size.finite? && size.positive?
|
|
59
|
+
raise ArgumentError, "invalid glyph ID" unless glyph.is_a?(Integer) && glyph.between?(0, font.glyph_count - 1)
|
|
60
|
+
raise ArgumentError, "invalid subpixel position" unless subpixel_x.is_a?(Numeric) && subpixel_x.finite?
|
|
61
|
+
handle = native_font(font, size)
|
|
62
|
+
path = @ct.fn(:CTFontCreatePathForGlyph, [P, Fiddle::TYPE_USHORT, P], P).call(handle, glyph, 0)
|
|
63
|
+
return Alhena::Bitmap.new(width: 0, height: 0, coverage: "") if path.null?
|
|
64
|
+
x, y, width, height = @bounds.call(@cg.handle["CGPathGetBoundingBox"], path)
|
|
65
|
+
left, bottom = (x + subpixel_x).floor, y.floor
|
|
66
|
+
right, top = (x + width + subpixel_x).ceil, (y + height).ceil
|
|
67
|
+
width, height = right - left, top - bottom
|
|
68
|
+
return Alhena::Bitmap.new(width: 0, height: 0, coverage: "") if width.zero? || height.zero?
|
|
69
|
+
raise Error, "CoreText bitmap exceeds size limit" if width * height > 16_777_216
|
|
70
|
+
bytes = "\0".b * (width * height)
|
|
71
|
+
context = @cg.fn(:CGBitmapContextCreate, [P, Fiddle::TYPE_SIZE_T, Fiddle::TYPE_SIZE_T, Fiddle::TYPE_SIZE_T, Fiddle::TYPE_SIZE_T, P, Fiddle::TYPE_UINT], P).call(bytes, width, height, 8, width, 0, 7)
|
|
72
|
+
raise Error, "CoreGraphics alpha bitmap context unavailable" if context.null?
|
|
73
|
+
@cg.fn(:CGContextSetShouldAntialias, [P, Fiddle::TYPE_BOOL], V).call(context, 1)
|
|
74
|
+
@cg.fn(:CGContextSetGrayFillColor, [P, D, D], V).call(context, 1, 1)
|
|
75
|
+
@cg.fn(:CGContextTranslateCTM, [P, D, D], V).call(context, -left + subpixel_x, -bottom)
|
|
76
|
+
# Filling the CoreText outline avoids CTFontDrawGlyphs pixel snapping
|
|
77
|
+
# and preserves the requested fractional position in an R8 context.
|
|
78
|
+
@cg.fn(:CGContextAddPath, [P, P], V).call(context, path)
|
|
79
|
+
@cg.fn(:CGContextFillPath, [P], V).call(context)
|
|
80
|
+
Alhena::Bitmap.new(width: width, height: height, left: left, top: top, coverage: bytes)
|
|
81
|
+
ensure
|
|
82
|
+
@cg.fn(:CGContextRelease, [P], V).call(context) if context && !context.null?
|
|
83
|
+
@cg.fn(:CGPathRelease, [P], V).call(path) if path && !path.null?
|
|
84
|
+
end
|
|
85
|
+
def close
|
|
86
|
+
@fonts.each_value { |font| @cf.fn(:CFRelease, [P], V).call(font) }
|
|
87
|
+
@descriptors.each_value { |descriptor| @cf.fn(:CFRelease, [P], V).call(descriptor) }
|
|
88
|
+
@fonts.clear
|
|
89
|
+
@descriptors.clear
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
module Platform
|
|
5
|
+
module Mac
|
|
6
|
+
def self.displays
|
|
7
|
+
list = O.send(O.klass("NSScreen"), "screens")
|
|
8
|
+
count = O.send(list, "count", result: :ulong)
|
|
9
|
+
return [] if count.zero?
|
|
10
|
+
first = O.send(list, "objectAtIndex:", 0, args: [:ulong])
|
|
11
|
+
primary_height = O.send(first, "frame", result: :rect)[3]
|
|
12
|
+
Array.new(count) do |index|
|
|
13
|
+
screen = O.send(list, "objectAtIndex:", index, args: [:ulong])
|
|
14
|
+
x, y, width, height = O.send(screen, "frame", result: :rect)
|
|
15
|
+
description = O.send(screen, "deviceDescription")
|
|
16
|
+
identifier = O.send(description, "objectForKey:", O.string("NSScreenNumber"), args: [:pointer])
|
|
17
|
+
Display.new(O.send(identifier, "unsignedIntValue", result: :uint), O.text(O.send(screen, "localizedName")),
|
|
18
|
+
Bounds.new(x, primary_height - y - height, width, height), O.send(screen, "backingScaleFactor", result: :double), index.zero?)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|