zaniah 0.2.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +13 -0
- data/LICENSE.txt +18 -0
- data/README.md +14 -4
- data/assets/icons/check.svg +1 -0
- data/assets/icons/close.svg +1 -0
- data/assets/icons/info.svg +1 -0
- data/assets/icons/menu.svg +1 -0
- data/assets/icons/search.svg +1 -0
- data/assets/icons/warning.svg +1 -0
- data/docs/accessibility.md +61 -0
- data/docs/adr/004-style-resolution.md +17 -0
- data/docs/adr/005-instance-layout.md +43 -0
- data/docs/adr/006-component-model.md +17 -0
- data/docs/adr/007-text-buffer.md +17 -0
- data/docs/adr/008-accessibility-bridge.md +19 -0
- data/docs/adr/009-grid-scope.md +17 -0
- data/docs/animation.md +46 -0
- data/docs/components.md +109 -0
- data/docs/gallery/command-palette-dark.png +0 -0
- data/docs/gallery/command-palette-high_contrast.png +0 -0
- data/docs/gallery/command-palette-light.png +0 -0
- data/docs/gallery/components-dark.png +0 -0
- data/docs/gallery/components-high_contrast.png +0 -0
- data/docs/gallery/components-light.png +0 -0
- data/docs/gallery/context-menu-dark.png +0 -0
- data/docs/gallery/context-menu-high_contrast.png +0 -0
- data/docs/gallery/context-menu-light.png +0 -0
- data/docs/gallery/dialog-dark.png +0 -0
- data/docs/gallery/dialog-high_contrast.png +0 -0
- data/docs/gallery/dialog-light.png +0 -0
- data/docs/gallery/drawer-left-dark.png +0 -0
- data/docs/gallery/drawer-left-high_contrast.png +0 -0
- data/docs/gallery/drawer-left-light.png +0 -0
- data/docs/gallery/drawer-right-dark.png +0 -0
- data/docs/gallery/drawer-right-high_contrast.png +0 -0
- data/docs/gallery/drawer-right-light.png +0 -0
- data/docs/gallery/menu-dark.png +0 -0
- data/docs/gallery/menu-high_contrast.png +0 -0
- data/docs/gallery/menu-light.png +0 -0
- data/docs/gallery/modal-dark.png +0 -0
- data/docs/gallery/modal-high_contrast.png +0 -0
- data/docs/gallery/modal-light.png +0 -0
- data/docs/gallery/popover-bottom-dark.png +0 -0
- data/docs/gallery/popover-bottom-high_contrast.png +0 -0
- data/docs/gallery/popover-bottom-light.png +0 -0
- data/docs/gallery/popover-left-dark.png +0 -0
- data/docs/gallery/popover-left-high_contrast.png +0 -0
- data/docs/gallery/popover-left-light.png +0 -0
- data/docs/gallery/popover-right-dark.png +0 -0
- data/docs/gallery/popover-right-high_contrast.png +0 -0
- data/docs/gallery/popover-right-light.png +0 -0
- data/docs/gallery/popover-top-dark.png +0 -0
- data/docs/gallery/popover-top-high_contrast.png +0 -0
- data/docs/gallery/popover-top-light.png +0 -0
- data/docs/gallery/toast-danger-dark.png +0 -0
- data/docs/gallery/toast-danger-high_contrast.png +0 -0
- data/docs/gallery/toast-danger-light.png +0 -0
- data/docs/gallery/toast-info-dark.png +0 -0
- data/docs/gallery/toast-info-high_contrast.png +0 -0
- data/docs/gallery/toast-info-light.png +0 -0
- data/docs/gallery/toast-success-dark.png +0 -0
- data/docs/gallery/toast-success-high_contrast.png +0 -0
- data/docs/gallery/toast-success-light.png +0 -0
- data/docs/gallery/toast-warning-dark.png +0 -0
- data/docs/gallery/toast-warning-high_contrast.png +0 -0
- data/docs/gallery/toast-warning-light.png +0 -0
- data/docs/gallery/tooltip-bottom-dark.png +0 -0
- data/docs/gallery/tooltip-bottom-high_contrast.png +0 -0
- data/docs/gallery/tooltip-bottom-light.png +0 -0
- data/docs/gallery/tooltip-left-dark.png +0 -0
- data/docs/gallery/tooltip-left-high_contrast.png +0 -0
- data/docs/gallery/tooltip-left-light.png +0 -0
- data/docs/gallery/tooltip-right-dark.png +0 -0
- data/docs/gallery/tooltip-right-high_contrast.png +0 -0
- data/docs/gallery/tooltip-right-light.png +0 -0
- data/docs/gallery/tooltip-top-dark.png +0 -0
- data/docs/gallery/tooltip-top-high_contrast.png +0 -0
- data/docs/gallery/tooltip-top-light.png +0 -0
- data/docs/index.html +74 -0
- data/docs/layout.md +31 -0
- data/docs/native.md +7 -2
- data/docs/site.css +32 -0
- data/docs/text.md +35 -0
- data/docs/theme.md +50 -0
- data/docs/tui.md +36 -0
- data/examples/gallery.rb +155 -0
- data/examples/native_ime.rb +13 -6
- data/lib/zaniah/accessibility/linux/service.rb +361 -0
- data/lib/zaniah/accessibility/linux.rb +23 -0
- data/lib/zaniah/accessibility/mac.rb +136 -0
- data/lib/zaniah/accessibility/native_tree.rb +53 -0
- data/lib/zaniah/accessibility/node.rb +12 -0
- data/lib/zaniah/accessibility/tree.rb +61 -0
- data/lib/zaniah/accessibility/windows/provider.rb +273 -0
- data/lib/zaniah/accessibility/windows.rb +38 -0
- data/lib/zaniah/accessibility.rb +63 -0
- data/lib/zaniah/animation/animator.rb +95 -0
- data/lib/zaniah/animation/easing.rb +45 -0
- data/lib/zaniah/animation/spring.rb +36 -0
- data/lib/zaniah/animation/transition.rb +5 -0
- data/lib/zaniah/animation.rb +6 -0
- data/lib/zaniah/app.rb +22 -2
- data/lib/zaniah/color.rb +16 -0
- data/lib/zaniah/devtools/hot_reload.rb +40 -0
- data/lib/zaniah/devtools/inspector.rb +111 -0
- data/lib/zaniah/devtools/stats_overlay.rb +16 -0
- data/lib/zaniah/devtools.rb +13 -0
- data/lib/zaniah/element.rb +210 -15
- data/lib/zaniah/frame_context.rb +4 -1
- data/lib/zaniah/gpu/frame_encoder.rb +10 -1
- data/lib/zaniah/gpu/instance_packing.rb +33 -5
- data/lib/zaniah/gpu/metal.rb +26 -10
- data/lib/zaniah/gpu/open_gl.rb +35 -14
- data/lib/zaniah/gpu/software.rb +116 -14
- data/lib/zaniah/gpu/vulkan/scene.frag +51 -0
- data/lib/zaniah/gpu/vulkan/scene.frag.spv +0 -0
- data/lib/zaniah/gpu/vulkan/scene.vert +50 -0
- data/lib/zaniah/gpu/vulkan/scene.vert.spv +0 -0
- data/lib/zaniah/gpu/vulkan.rb +282 -36
- data/lib/zaniah/gpu.rb +8 -3
- data/lib/zaniah/input/dispatcher.rb +96 -12
- data/lib/zaniah/input/focus_handle.rb +13 -4
- data/lib/zaniah/input/focus_tree.rb +77 -0
- data/lib/zaniah/input/keymap.rb +77 -0
- data/lib/zaniah/input.rb +1 -0
- data/lib/zaniah/interactivity.rb +30 -0
- data/lib/zaniah/layout/engine.rb +173 -36
- data/lib/zaniah/layout/grid.rb +134 -0
- data/lib/zaniah/layout/style.rb +16 -1
- data/lib/zaniah/layout.rb +1 -0
- data/lib/zaniah/length.rb +3 -0
- data/lib/zaniah/length_units.rb +7 -0
- data/lib/zaniah/list.rb +27 -15
- data/lib/zaniah/platform/appearance.rb +11 -0
- data/lib/zaniah/platform/headless/window.rb +101 -9
- data/lib/zaniah/platform/linux/appearance_aware.rb +8 -0
- data/lib/zaniah/platform/linux/wayland_window.rb +3 -1
- data/lib/zaniah/platform/linux/window.rb +4 -2
- data/lib/zaniah/platform/mac/app.rb +1 -1
- data/lib/zaniah/platform/mac/core_text.rb +3 -0
- data/lib/zaniah/platform/mac/window.rb +9 -1
- data/lib/zaniah/platform/tui/window.rb +1 -1
- data/lib/zaniah/platform/windows/window.rb +12 -2
- data/lib/zaniah/platform.rb +1 -0
- data/lib/zaniah/scene.rb +137 -28
- data/lib/zaniah/scroll_state.rb +117 -0
- data/lib/zaniah/scroll_view.rb +94 -0
- data/lib/zaniah/style/gradient.rb +28 -0
- data/lib/zaniah/style/ring.rb +13 -0
- data/lib/zaniah/style/shadow.rb +13 -0
- data/lib/zaniah/style/style_set.rb +43 -0
- data/lib/zaniah/style/transform.rb +40 -0
- data/lib/zaniah/svg.rb +19 -0
- data/lib/zaniah/text.rb +261 -6
- data/lib/zaniah/text_buffer.rb +119 -0
- data/lib/zaniah/text_selection.rb +18 -0
- data/lib/zaniah/text_system/atlas_cache.rb +1 -1
- data/lib/zaniah/text_system/fallback.rb +24 -0
- data/lib/zaniah/text_system/font_db.rb +4 -1
- data/lib/zaniah/text_system/kinsoku.rb +28 -0
- data/lib/zaniah/text_system/line_breaker.rb +54 -0
- data/lib/zaniah/text_system/paragraph.rb +99 -0
- data/lib/zaniah/text_system/renderer.rb +13 -2
- data/lib/zaniah/text_system/typesetter.rb +4 -0
- data/lib/zaniah/text_system.rb +3 -0
- data/lib/zaniah/theme.rb +39 -0
- data/lib/zaniah/ui/charts.rb +165 -0
- data/lib/zaniah/ui/choice_fields.rb +134 -0
- data/lib/zaniah/ui/component.rb +73 -0
- data/lib/zaniah/ui/controls.rb +439 -0
- data/lib/zaniah/ui/data.rb +364 -0
- data/lib/zaniah/ui/editors.rb +67 -0
- data/lib/zaniah/ui/forms.rb +131 -0
- data/lib/zaniah/ui/layout_components.rb +205 -0
- data/lib/zaniah/ui/modal.rb +129 -0
- data/lib/zaniah/ui/overlays.rb +282 -0
- data/lib/zaniah/ui/pickers.rb +169 -0
- data/lib/zaniah/ui/primitives.rb +199 -0
- data/lib/zaniah/ui/scrollbar.rb +104 -0
- data/lib/zaniah/ui/structure.rb +224 -0
- data/lib/zaniah/ui/text_inputs.rb +169 -0
- data/lib/zaniah/ui/variants.rb +23 -0
- data/lib/zaniah/ui.rb +18 -0
- data/lib/zaniah/unicode.rb +42 -0
- data/lib/zaniah/uniform_list.rb +27 -6
- data/lib/zaniah/version.rb +1 -1
- data/lib/zaniah.rb +14 -0
- data/sig/accessibility.rbs +34 -0
- data/sig/animation.rbs +54 -0
- data/sig/app.rbs +2 -1
- data/sig/devtools.rbs +37 -0
- data/sig/elements.rbs +88 -6
- data/sig/gpu.rbs +9 -2
- data/sig/input.rbs +31 -5
- data/sig/layout.rbs +2 -1
- data/sig/platform.rbs +24 -4
- data/sig/scene.rbs +15 -2
- data/sig/style.rbs +82 -0
- data/sig/text.rbs +35 -0
- data/sig/theme.rbs +64 -0
- data/sig/ui.rbs +390 -0
- data/sig/zaniah.rbs +56 -3
- data/tools/generate_component_gallery.rb +48 -0
- metadata +147 -2
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fiddle"
|
|
4
|
+
|
|
5
|
+
module Zaniah
|
|
6
|
+
module Accessibility
|
|
7
|
+
module Windows
|
|
8
|
+
module Provider
|
|
9
|
+
P = Fiddle::TYPE_VOIDP
|
|
10
|
+
I = Fiddle::TYPE_INT
|
|
11
|
+
U = Fiddle::TYPE_UINT
|
|
12
|
+
N = Fiddle::TYPE_INTPTR_T
|
|
13
|
+
S_OK = 0
|
|
14
|
+
E_NOINTERFACE = -2_147_467_262
|
|
15
|
+
E_FAIL = -2_147_467_259
|
|
16
|
+
UIA_APPEND_RUNTIME_ID = -1
|
|
17
|
+
|
|
18
|
+
GUIDS = {
|
|
19
|
+
unknown: "00000000-0000-0000-c000-000000000046",
|
|
20
|
+
simple: "d6dd68d1-86fd-4332-8666-9abedea2d24c",
|
|
21
|
+
fragment: "f7063da8-8359-439c-9297-bbc5299a7d87",
|
|
22
|
+
root: "620ce2a5-ab8f-40a9-86cb-de3c75599b58",
|
|
23
|
+
invoke: "54fcb24b-e18e-47a2-b4d3-eccbe77599a2"
|
|
24
|
+
}.transform_values { |value| first, second, third, fourth, fifth = value.split("-"); [first.hex, second.hex, third.hex].pack("Vvv") + [fourth + fifth].pack("H*") }.freeze
|
|
25
|
+
|
|
26
|
+
CONTROL_TYPES = {
|
|
27
|
+
button: 50_000, checkbox: 50_002, switch: 50_002, combobox: 50_003, textbox: 50_004,
|
|
28
|
+
searchbox: 50_004, link: 50_005, image: 50_006, listitem: 50_007, list: 50_008,
|
|
29
|
+
listbox: 50_008, menu: 50_009, menubar: 50_010, menuitem: 50_011,
|
|
30
|
+
progressbar: 50_012, meter: 50_012, radio: 50_013, slider: 50_015, status: 50_017,
|
|
31
|
+
tabpanel: 50_018, tab: 50_019, text: 50_020, heading: 50_020, toolbar: 50_021,
|
|
32
|
+
tooltip: 50_022, tree: 50_023, treeitem: 50_024, group: 50_026, radiogroup: 50_026,
|
|
33
|
+
form: 50_026, table: 50_036, row: 50_029, cell: 50_029, columnheader: 50_035,
|
|
34
|
+
dialog: 50_032, navigation: 50_033, separator: 50_038
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
class Bridge
|
|
38
|
+
attr_reader :window, :tree, :root_provider
|
|
39
|
+
|
|
40
|
+
def initialize(window)
|
|
41
|
+
@window, @providers = window, {}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def update(root)
|
|
45
|
+
@tree = NativeTree.new(root)
|
|
46
|
+
@tree.each do |entry|
|
|
47
|
+
(@providers[entry.path] ||= Element.new(self, entry.path)).entry = entry
|
|
48
|
+
end
|
|
49
|
+
@root_provider = @providers[@tree.root.path]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def provider(entry) = entry && @providers[entry.path]
|
|
53
|
+
def entry(path) = @tree[path]
|
|
54
|
+
|
|
55
|
+
def screen_bounds(entry)
|
|
56
|
+
bounds = @tree.bounds(entry)
|
|
57
|
+
point = [(bounds.x * scale).round, (bounds.y * scale).round].pack("l2")
|
|
58
|
+
user.fn(:ClientToScreen, [P, P], I).call(@window.handle, point)
|
|
59
|
+
x, y = point.unpack("l2")
|
|
60
|
+
[x.to_f, y.to_f, bounds.width * scale, bounds.height * scale]
|
|
61
|
+
rescue NoMethodError
|
|
62
|
+
[bounds.x, bounds.y, bounds.width, bounds.height]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def provider_at(x, y)
|
|
66
|
+
point = [x.round, y.round].pack("l2")
|
|
67
|
+
user.fn(:ScreenToClient, [P, P], I).call(@window.handle, point)
|
|
68
|
+
px, py = point.unpack("l2")
|
|
69
|
+
provider(@tree.hit(Point.new(px / scale, py / scale)))
|
|
70
|
+
rescue NoMethodError
|
|
71
|
+
provider(@tree.hit(Point.new(x, y)))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def focused_provider = provider(@tree.focused(@window)) || @root_provider
|
|
75
|
+
|
|
76
|
+
def return_provider(wparam, lparam)
|
|
77
|
+
core.fn(:UiaReturnRawElementProvider, [P, N, N, P], N)
|
|
78
|
+
.call(@window.handle, wparam, lparam, @root_provider.pointer(:simple))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def host_provider(output)
|
|
82
|
+
core.fn(:UiaHostProviderFromHwnd, [P, P], I).call(@window.handle, output)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def bstr(value)
|
|
86
|
+
bytes = value.to_s.encode("UTF-16LE").b + "\0\0".b
|
|
87
|
+
ole.fn(:SysAllocString, [P], P).call(Fiddle::Pointer[bytes])
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def safe_array(values)
|
|
91
|
+
array = ole.fn(:SafeArrayCreateVector, [Fiddle::TYPE_USHORT, Fiddle::TYPE_LONG, U], P).call(3, 0, values.length)
|
|
92
|
+
data = [0].pack("J")
|
|
93
|
+
return 0 if array.to_i.zero? || ole.fn(:SafeArrayAccessData, [P, P], I).call(array, data) != S_OK
|
|
94
|
+
Fiddle::Pointer.new(data.unpack1("J"))[0, values.length * 4] = values.pack("l*")
|
|
95
|
+
ole.fn(:SafeArrayUnaccessData, [P], I).call(array)
|
|
96
|
+
array
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def scale = @window.scale_factor || 1.0
|
|
102
|
+
def user = @window.instance_variable_get(:@user)
|
|
103
|
+
def core = (@core ||= Zaniah::FFI::Library.new("UIAutomationCore.dll"))
|
|
104
|
+
def ole = (@ole ||= Zaniah::FFI::Library.new("oleaut32.dll"))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
class Element
|
|
108
|
+
INTERFACES = {}
|
|
109
|
+
class << self
|
|
110
|
+
attr_reader :vtables, :closures
|
|
111
|
+
|
|
112
|
+
def install
|
|
113
|
+
return if @vtables
|
|
114
|
+
@closures = []
|
|
115
|
+
unknown = [closure(I, [P, P, P]) { |this, iid, output| interface(this).query(iid, output) },
|
|
116
|
+
closure(U, [P]) { |this| interface(this).add_ref }, closure(U, [P]) { |this| interface(this).release }]
|
|
117
|
+
simple = unknown + [closure(I, [P, P]) { |_this, output| write_int(output, 1) },
|
|
118
|
+
closure(I, [P, I, P]) { |this, pattern, output| interface(this).pattern(pattern, output) },
|
|
119
|
+
closure(I, [P, I, P]) { |this, property, output| interface(this).property(property, output) },
|
|
120
|
+
closure(I, [P, P]) { |this, output| interface(this).host(output) }]
|
|
121
|
+
fragment = unknown + [closure(I, [P, I, P]) { |this, direction, output| interface(this).navigate(direction, output) },
|
|
122
|
+
closure(I, [P, P]) { |this, output| interface(this).runtime_id(output) },
|
|
123
|
+
closure(I, [P, P]) { |this, output| interface(this).bounding_rectangle(output) },
|
|
124
|
+
closure(I, [P, P]) { |_this, output| write_pointer(output, 0) },
|
|
125
|
+
closure(I, [P]) { |this| interface(this).set_focus },
|
|
126
|
+
closure(I, [P, P]) { |this, output| interface(this).fragment_root(output) }]
|
|
127
|
+
root = unknown + [closure(I, [P, Fiddle::TYPE_DOUBLE, Fiddle::TYPE_DOUBLE, P]) { |this, x, y, output| interface(this).from_point(x, y, output) },
|
|
128
|
+
closure(I, [P, P]) { |this, output| interface(this).focus(output) }]
|
|
129
|
+
invoke = unknown + [closure(I, [P]) { |this| interface(this).invoke }]
|
|
130
|
+
@vtables = {simple: vtable(simple), fragment: vtable(fragment), root: vtable(root), invoke: vtable(invoke)}.freeze
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def closure(result, arguments, &block)
|
|
134
|
+
value = Fiddle::Closure::BlockCaller.new(result, arguments) do |*values|
|
|
135
|
+
block.call(*values)
|
|
136
|
+
rescue StandardError
|
|
137
|
+
result == I ? E_FAIL : 1
|
|
138
|
+
end
|
|
139
|
+
@closures << value
|
|
140
|
+
value
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def vtable(functions)
|
|
144
|
+
pointer = Fiddle::Pointer.malloc(functions.length * Fiddle::SIZEOF_VOIDP)
|
|
145
|
+
pointer[0, functions.length * Fiddle::SIZEOF_VOIDP] = functions.map(&:to_i).pack("J*")
|
|
146
|
+
pointer
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def interface(pointer) = INTERFACES.fetch(pointer.to_i)
|
|
150
|
+
def write_pointer(output, value) = (Fiddle::Pointer.new(output)[0, Fiddle::SIZEOF_VOIDP] = [value.to_i].pack("J"); S_OK)
|
|
151
|
+
def write_int(output, value) = (Fiddle::Pointer.new(output)[0, 4] = [value].pack("l"); S_OK)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
attr_accessor :entry
|
|
155
|
+
attr_reader :bridge
|
|
156
|
+
|
|
157
|
+
def initialize(bridge, path)
|
|
158
|
+
self.class.install
|
|
159
|
+
@bridge, @path, @references, @interfaces = bridge, path, 1, {}
|
|
160
|
+
%i[simple fragment root invoke].each do |kind|
|
|
161
|
+
pointer = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
|
|
162
|
+
pointer[0, Fiddle::SIZEOF_VOIDP] = [self.class.vtables.fetch(kind).to_i].pack("J")
|
|
163
|
+
@interfaces[kind] = pointer
|
|
164
|
+
INTERFACES[pointer.to_i] = self
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def pointer(kind) = @interfaces.fetch(kind).to_i
|
|
169
|
+
|
|
170
|
+
def query(iid, output)
|
|
171
|
+
requested = GUIDS.key(Fiddle::Pointer.new(iid)[0, 16])
|
|
172
|
+
requested = :simple if requested == :unknown
|
|
173
|
+
supported = requested && (requested != :root || @entry.parent.nil?) && (requested != :invoke || invokable?)
|
|
174
|
+
return self.class.write_pointer(output, 0) && E_NOINTERFACE unless supported
|
|
175
|
+
add_ref
|
|
176
|
+
self.class.write_pointer(output, pointer(requested))
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def add_ref = (@references += 1)
|
|
180
|
+
def release = (@references = [@references - 1, 1].max)
|
|
181
|
+
|
|
182
|
+
def pattern(pattern, output)
|
|
183
|
+
return self.class.write_pointer(output, 0) unless pattern == 10_000 && invokable?
|
|
184
|
+
add_ref
|
|
185
|
+
self.class.write_pointer(output, pointer(:invoke))
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def property(property, output)
|
|
189
|
+
variant = Fiddle::Pointer.new(output)
|
|
190
|
+
variant[0, 16] = "\0" * 16
|
|
191
|
+
case property
|
|
192
|
+
when 30_003 then variant_i4(variant, CONTROL_TYPES.fetch(@entry.node.role, 50_025))
|
|
193
|
+
when 30_005 then variant_bstr(variant, @entry.node.label || @entry.node.value&.to_s || @entry.node.role.to_s)
|
|
194
|
+
when 30_008 then variant_bool(variant, @bridge.focused_provider.equal?(self))
|
|
195
|
+
when 30_009 then variant_bool(variant, focusable?)
|
|
196
|
+
when 30_010 then variant_bool(variant, !@entry.node.states[:disabled])
|
|
197
|
+
when 30_012 then variant_bstr(variant, "Zaniah")
|
|
198
|
+
when 30_016, 30_017 then variant_bool(variant, true)
|
|
199
|
+
when 30_022 then variant_bool(variant, @bridge.tree.bounds(@entry).empty?)
|
|
200
|
+
when 30_024 then variant_bstr(variant, "Zaniah")
|
|
201
|
+
end
|
|
202
|
+
S_OK
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def host(output) = @entry.parent ? self.class.write_pointer(output, 0) : @bridge.host_provider(output)
|
|
206
|
+
|
|
207
|
+
def navigate(direction, output)
|
|
208
|
+
target = case direction
|
|
209
|
+
when 0 then @entry.parent
|
|
210
|
+
when 1, 2
|
|
211
|
+
siblings = @entry.parent&.children || []
|
|
212
|
+
index = siblings.index(@entry)
|
|
213
|
+
index && siblings[index + (direction == 1 ? 1 : -1)] if index && (direction == 1 || index.positive?)
|
|
214
|
+
when 3 then @entry.children.first
|
|
215
|
+
when 4 then @entry.children.last
|
|
216
|
+
end
|
|
217
|
+
provider = @bridge.provider(target)
|
|
218
|
+
provider&.add_ref
|
|
219
|
+
self.class.write_pointer(output, provider&.pointer(:fragment) || 0)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def runtime_id(output)
|
|
223
|
+
array = @bridge.safe_array([UIA_APPEND_RUNTIME_ID, @entry.runtime_id])
|
|
224
|
+
self.class.write_pointer(output, array)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def bounding_rectangle(output)
|
|
228
|
+
Fiddle::Pointer.new(output)[0, 32] = @bridge.screen_bounds(@entry).pack("d4")
|
|
229
|
+
S_OK
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def set_focus
|
|
233
|
+
bounds = @bridge.tree.bounds(@entry)
|
|
234
|
+
Accessibility.focus_at(@bridge.window, Point.new(bounds.x + bounds.width / 2.0, bounds.y + bounds.height / 2.0))
|
|
235
|
+
S_OK
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def fragment_root(output)
|
|
239
|
+
root = @bridge.root_provider
|
|
240
|
+
root.add_ref
|
|
241
|
+
self.class.write_pointer(output, root.pointer(:root))
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def from_point(x, y, output)
|
|
245
|
+
provider = @bridge.provider_at(x, y) || @bridge.root_provider
|
|
246
|
+
provider.add_ref
|
|
247
|
+
self.class.write_pointer(output, provider.pointer(:fragment))
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def focus(output)
|
|
251
|
+
provider = @bridge.focused_provider
|
|
252
|
+
provider.add_ref
|
|
253
|
+
self.class.write_pointer(output, provider.pointer(:fragment))
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def invoke
|
|
257
|
+
action = %i[press toggle select sort expand collapse dismiss].find { |candidate| @entry.node.actions.include?(candidate) }
|
|
258
|
+
Accessibility.perform(@bridge.window, @entry.node, action, bounds: @bridge.tree.bounds(@entry))
|
|
259
|
+
S_OK
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
private
|
|
263
|
+
|
|
264
|
+
def invokable? = @entry.node.actions.any? { |action| %i[press toggle select sort expand collapse dismiss].include?(action) }
|
|
265
|
+
def focusable? = !@entry.node.states[:disabled] && (!@entry.node.actions.empty? || %i[textbox searchbox slider combobox].include?(@entry.node.role))
|
|
266
|
+
def variant_i4(pointer, value) = (pointer[0, 2] = [3].pack("v"); pointer[8, 4] = [value].pack("l"))
|
|
267
|
+
def variant_bool(pointer, value) = (pointer[0, 2] = [11].pack("v"); pointer[8, 2] = [value ? -1 : 0].pack("s"))
|
|
268
|
+
def variant_bstr(pointer, value) = (pointer[0, 2] = [8].pack("v"); pointer[8, Fiddle::SIZEOF_VOIDP] = [@bridge.bstr(value)].pack("J"))
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "windows/provider"
|
|
4
|
+
|
|
5
|
+
module Zaniah
|
|
6
|
+
module Accessibility
|
|
7
|
+
module Windows
|
|
8
|
+
EVENT_OBJECT_REORDER = 0x8004
|
|
9
|
+
OBJID_CLIENT = -4
|
|
10
|
+
CHILDID_SELF = 0
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def publish(window, root, _changes)
|
|
15
|
+
return unless root && window.respond_to?(:handle)
|
|
16
|
+
bridge = bridges[window.handle.to_i] ||= Provider::Bridge.new(window)
|
|
17
|
+
bridge.update(root)
|
|
18
|
+
user = window.instance_variable_get(:@user)
|
|
19
|
+
return unless user
|
|
20
|
+
user.fn(:NotifyWinEvent,
|
|
21
|
+
[Fiddle::TYPE_UINT, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_INT], Fiddle::TYPE_VOID)
|
|
22
|
+
.call(EVENT_OBJECT_REORDER, window.handle, OBJID_CLIENT, CHILDID_SELF)
|
|
23
|
+
rescue Fiddle::DLError
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def provider_result(window, wparam, lparam)
|
|
28
|
+
return unless (lparam.to_i & 0xffffffff) == 0xffffffe7
|
|
29
|
+
bridge = bridges[window.handle.to_i]
|
|
30
|
+
return unless bridge&.root_provider
|
|
31
|
+
bridge.return_provider(wparam, lparam)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def close(window) = bridges.delete(window.handle.to_i)
|
|
35
|
+
def bridges = (@bridges ||= {})
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "accessibility/node"
|
|
4
|
+
require_relative "accessibility/tree"
|
|
5
|
+
require_relative "accessibility/native_tree"
|
|
6
|
+
|
|
7
|
+
module Zaniah
|
|
8
|
+
module Accessibility
|
|
9
|
+
autoload :Mac, File.expand_path("accessibility/mac", __dir__)
|
|
10
|
+
autoload :Windows, File.expand_path("accessibility/windows", __dir__)
|
|
11
|
+
autoload :Linux, File.expand_path("accessibility/linux", __dir__)
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def publish(window, root, changes)
|
|
16
|
+
adapter = case window.class.name
|
|
17
|
+
when /::Mac::/ then Mac
|
|
18
|
+
when /::Windows::/ then Windows
|
|
19
|
+
when /::Linux::/ then Linux
|
|
20
|
+
end
|
|
21
|
+
adapter&.publish(window, root, changes)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def poll(window)
|
|
25
|
+
Linux.poll(window) if window.class.name.match?(/::Linux::/)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def close(window)
|
|
29
|
+
case window.class.name
|
|
30
|
+
when /::Mac::/ then Mac.close(window)
|
|
31
|
+
when /::Windows::/ then Windows.close(window)
|
|
32
|
+
when /::Linux::/ then Linux.close(window)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def perform(window, node, action, bounds: node&.bounds)
|
|
37
|
+
return false unless node && node.actions.include?(action)
|
|
38
|
+
if action == :dismiss
|
|
39
|
+
window.input(Input::KeyDown.new("esc", false))
|
|
40
|
+
return true
|
|
41
|
+
end
|
|
42
|
+
return false unless bounds
|
|
43
|
+
position = Point.new(bounds.x + bounds.width / 2.0, bounds.y + bounds.height / 2.0)
|
|
44
|
+
if %i[increment decrement].include?(action)
|
|
45
|
+
focus_at(window, position)
|
|
46
|
+
window.dispatcher.key(action == :increment ? "right" : "left")
|
|
47
|
+
else
|
|
48
|
+
window.input(Input::MouseDown.new(position, :left, [], 1))
|
|
49
|
+
window.input(Input::MouseUp.new(position, :left, []))
|
|
50
|
+
end
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def focus_at(window, position)
|
|
55
|
+
hit = window.dispatcher.hits.reverse.find { |candidate| candidate.bounds&.contains?(position) }
|
|
56
|
+
owner = hit&.owner
|
|
57
|
+
owner = owner.parent while owner && !owner.respond_to?(:focus_handle)
|
|
58
|
+
handle = owner&.focus_handle
|
|
59
|
+
window.dispatcher.focus(handle, origin: :programmatic) if handle
|
|
60
|
+
!handle.nil?
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
class Animation
|
|
5
|
+
attr_reader :key, :from, :to, :started_at, :duration, :easing
|
|
6
|
+
|
|
7
|
+
def initialize(key, from:, to:, started_at:, duration:, easing:, on_complete: nil)
|
|
8
|
+
@key, @from, @to, @started_at = key, from, to, started_at
|
|
9
|
+
@duration, @easing, @on_complete = duration, easing, on_complete
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def sample(now)
|
|
13
|
+
progress = duration.zero? ? 1.0 : ((now - started_at) / duration).clamp(0, 1)
|
|
14
|
+
[Animation.interpolate(from, to, easing.call(progress)), progress >= 1]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def complete = @on_complete&.call
|
|
18
|
+
|
|
19
|
+
def self.interpolatable?(from, to)
|
|
20
|
+
(from.is_a?(Numeric) && to.is_a?(Numeric)) ||
|
|
21
|
+
(from.is_a?(Color) && to.is_a?(Color)) ||
|
|
22
|
+
(from.is_a?(Length) && to.is_a?(Length) && from.unit == to.unit) ||
|
|
23
|
+
(from.is_a?(Transform) && to.is_a?(Transform))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.interpolate(from, to, progress)
|
|
27
|
+
return from + (to - from) * progress if from.is_a?(Numeric) && to.is_a?(Numeric)
|
|
28
|
+
return Color.new(*from.to_a.zip(to.to_a).map { |left, right| left + (right - left) * progress }) if from.is_a?(Color) && to.is_a?(Color)
|
|
29
|
+
return Length.new(from.value + (to.value - from.value) * progress, from.unit) if from.is_a?(Length) && to.is_a?(Length) && from.unit == to.unit
|
|
30
|
+
return Transform.new(*from.to_a.zip(to.to_a).map { |left, right| left + (right - left) * progress }) if from.is_a?(Transform) && to.is_a?(Transform)
|
|
31
|
+
progress >= 1 ? to : from
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class Animator
|
|
36
|
+
attr_reader :reduced_motion
|
|
37
|
+
|
|
38
|
+
def initialize(clock:, reduced_motion: false)
|
|
39
|
+
@clock, @reduced_motion = clock, reduced_motion
|
|
40
|
+
@animations, @values = {}, {}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def reduced_motion=(value)
|
|
44
|
+
@reduced_motion = !!value
|
|
45
|
+
@animations.keys.each { |key| finish(key) } if @reduced_motion
|
|
46
|
+
@reduced_motion
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def animate(key, from:, to:, duration:, easing: :linear, &on_complete)
|
|
50
|
+
duration = Float(duration)
|
|
51
|
+
raise ArgumentError, "duration must be finite and nonnegative" unless duration.finite? && duration >= 0
|
|
52
|
+
duration = 0.0 if reduced_motion
|
|
53
|
+
animation = Animation.new(key, from: from, to: to, started_at: @clock.call,
|
|
54
|
+
duration: duration, easing: Easing.resolve(easing), on_complete: on_complete)
|
|
55
|
+
@values[key] = from
|
|
56
|
+
if duration.zero?
|
|
57
|
+
@values[key] = to
|
|
58
|
+
animation.complete
|
|
59
|
+
else
|
|
60
|
+
@animations[key] = animation
|
|
61
|
+
end
|
|
62
|
+
animation
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def spring(key, to:, stiffness: 170, damping: 26, mass: 1, from: value(key, 0), &on_complete)
|
|
66
|
+
easing = Easing.spring(stiffness: stiffness, damping: damping, mass: mass)
|
|
67
|
+
animate(key, from: from, to: to, duration: easing.settling_duration, easing: easing, &on_complete)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def sample(now = @clock.call)
|
|
71
|
+
@animations.dup.each do |key, animation|
|
|
72
|
+
@values[key], finished = animation.sample(now)
|
|
73
|
+
finish(key) if finished
|
|
74
|
+
end
|
|
75
|
+
@values.dup.freeze
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def active? = !@animations.empty?
|
|
79
|
+
def animating?(key) = @animations.key?(key)
|
|
80
|
+
def value(key, fallback = nil) = @values.fetch(key, fallback)
|
|
81
|
+
|
|
82
|
+
def cancel(key)
|
|
83
|
+
@animations.delete(key)
|
|
84
|
+
@values.delete(key)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def finish(key)
|
|
88
|
+
animation = @animations.delete(key)
|
|
89
|
+
return @values[key] unless animation
|
|
90
|
+
@values[key] = animation.to
|
|
91
|
+
animation.complete
|
|
92
|
+
@values[key]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
module Easing
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def linear = ->(value) { value }
|
|
8
|
+
def ease_in = cubic_bezier(0.42, 0, 1, 1)
|
|
9
|
+
def ease_out = cubic_bezier(0, 0, 0.58, 1)
|
|
10
|
+
def ease_in_out = cubic_bezier(0.42, 0, 0.58, 1)
|
|
11
|
+
|
|
12
|
+
def resolve(value)
|
|
13
|
+
return value if value.respond_to?(:call)
|
|
14
|
+
public_send(value || :linear)
|
|
15
|
+
rescue NoMethodError
|
|
16
|
+
raise ArgumentError, "unknown easing #{value.inspect}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def cubic_bezier(x1, y1, x2, y2)
|
|
20
|
+
values = [x1, y1, x2, y2].map { |value| Float(value) }
|
|
21
|
+
raise ArgumentError, "cubic-bezier x values must be between 0 and 1" unless values.values_at(0, 2).all? { |value| value.between?(0, 1) }
|
|
22
|
+
x1, y1, x2, y2 = values
|
|
23
|
+
lambda do |input|
|
|
24
|
+
input = input.to_f.clamp(0, 1)
|
|
25
|
+
low, high = 0.0, 1.0
|
|
26
|
+
16.times do
|
|
27
|
+
time = (low + high) / 2.0
|
|
28
|
+
sample = bezier(time, x1, x2)
|
|
29
|
+
sample < input ? low = time : high = time
|
|
30
|
+
end
|
|
31
|
+
bezier((low + high) / 2.0, y1, y2)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def spring(stiffness: 170, damping: 26, mass: 1)
|
|
36
|
+
Spring.new(stiffness: stiffness, damping: damping, mass: mass)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def bezier(time, first, second)
|
|
40
|
+
inverse = 1 - time
|
|
41
|
+
3 * inverse * inverse * time * first + 3 * inverse * time * time * second + time**3
|
|
42
|
+
end
|
|
43
|
+
private_class_method :bezier
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
class Spring
|
|
5
|
+
attr_reader :stiffness, :damping, :mass, :settling_duration
|
|
6
|
+
|
|
7
|
+
def initialize(stiffness:, damping:, mass: 1)
|
|
8
|
+
@stiffness, @damping, @mass = Float(stiffness), Float(damping), Float(mass)
|
|
9
|
+
raise ArgumentError, "spring values must be finite and positive" unless [@stiffness, @damping, @mass].all? { |value| value.finite? && value.positive? }
|
|
10
|
+
@settling_duration = [-Math.log(0.001) / slowest_decay, 10.0].min
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(progress)
|
|
14
|
+
time = progress.to_f.clamp(0, 1) * settling_duration
|
|
15
|
+
ratio = damping / (2 * Math.sqrt(stiffness * mass))
|
|
16
|
+
frequency = Math.sqrt(stiffness / mass)
|
|
17
|
+
return 1 - Math.exp(-frequency * time) * (1 + frequency * time) if (ratio - 1).abs < 1e-6
|
|
18
|
+
if ratio < 1
|
|
19
|
+
damped = frequency * Math.sqrt(1 - ratio * ratio)
|
|
20
|
+
1 - Math.exp(-ratio * frequency * time) * (Math.cos(damped * time) + ratio * frequency / damped * Math.sin(damped * time))
|
|
21
|
+
else
|
|
22
|
+
root = Math.sqrt(ratio * ratio - 1)
|
|
23
|
+
first, second = -frequency * (ratio - root), -frequency * (ratio + root)
|
|
24
|
+
1 - (second * Math.exp(first * time) - first * Math.exp(second * time)) / (second - first)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def slowest_decay
|
|
31
|
+
ratio = damping / (2 * Math.sqrt(stiffness * mass))
|
|
32
|
+
frequency = Math.sqrt(stiffness / mass)
|
|
33
|
+
ratio <= 1 ? ratio * frequency : frequency * (ratio - Math.sqrt(ratio * ratio - 1))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/zaniah/app.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Zaniah
|
|
|
10
10
|
attr_reader :windows, :executor
|
|
11
11
|
|
|
12
12
|
def initialize(clock: MONOTONIC_CLOCK)
|
|
13
|
-
@slots, @generations, @free, @windows, @globals = [], [], [], [], {}
|
|
13
|
+
@slots, @generations, @free, @windows, @globals = [], [], [], [], {theme: Theme.dark}
|
|
14
14
|
@listeners, @effects, @updating, @flushing = {}, [], 0, false
|
|
15
15
|
@clock = clock
|
|
16
16
|
@executor = TaskExecutor.new(clock: clock)
|
|
@@ -106,25 +106,45 @@ module Zaniah
|
|
|
106
106
|
def open_window(**options, &render)
|
|
107
107
|
options[:clock] ||= @clock
|
|
108
108
|
window = Platform.open_window(**options)
|
|
109
|
+
window.app = self
|
|
109
110
|
@windows << window
|
|
111
|
+
@globals[:theme] = platform_theme(window)
|
|
112
|
+
window.on_appearance do |appearance|
|
|
113
|
+
@globals[:theme] = platform_theme(window, appearance)
|
|
114
|
+
@windows.each(&:request_frame)
|
|
115
|
+
end
|
|
110
116
|
window.draw(&render) if render
|
|
111
117
|
window
|
|
112
118
|
end
|
|
113
119
|
|
|
114
120
|
def run
|
|
115
121
|
until @windows.empty? || @windows.all?(&:closed?)
|
|
122
|
+
@hot_reloads&.each(&:poll)
|
|
116
123
|
@executor.drain
|
|
117
124
|
@windows.reject(&:closed?).each(&:tick)
|
|
118
|
-
@executor.wait(0.05) unless @windows.any?
|
|
125
|
+
@executor.wait(0.05) unless @windows.any? { |window| window.dirty? || window.animation_active? }
|
|
119
126
|
end
|
|
120
127
|
ensure
|
|
128
|
+
@hot_reloads&.each(&:close)
|
|
121
129
|
@executor.shutdown
|
|
122
130
|
end
|
|
123
131
|
|
|
132
|
+
def hot_reload(paths, **options, &block)
|
|
133
|
+
require_relative "devtools"
|
|
134
|
+
reload = DevTools::HotReload.new(self, paths, **options, &block)
|
|
135
|
+
(@hot_reloads ||= []) << reload
|
|
136
|
+
reload
|
|
137
|
+
end
|
|
138
|
+
|
|
124
139
|
def alive?(entity) = @generations[entity.id] == entity.generation && !@free.include?(entity.id) && entity.id < @slots.length
|
|
125
140
|
|
|
126
141
|
private
|
|
127
142
|
|
|
143
|
+
def platform_theme(window, appearance = window.appearance)
|
|
144
|
+
theme = Theme.for(appearance)
|
|
145
|
+
window.reduced_motion? ? theme.with(motion: theme.motion.with(reduced: true)) : theme
|
|
146
|
+
end
|
|
147
|
+
|
|
128
148
|
def validate(entity)
|
|
129
149
|
raise Error, "entity has been released" unless alive?(entity)
|
|
130
150
|
end
|
data/lib/zaniah/color.rb
CHANGED
|
@@ -14,6 +14,7 @@ module Zaniah
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def opacity(value) = Color.new(r, g, b, a * value)
|
|
17
|
+
def with_alpha(value) = Color.new(r, g, b, value.to_f.clamp(0, 1))
|
|
17
18
|
def premultiplied = [r * a, g * a, b * a, a]
|
|
18
19
|
def to_a = [r, g, b, a]
|
|
19
20
|
|
|
@@ -21,6 +22,21 @@ module Zaniah
|
|
|
21
22
|
Color.new(*to_a.zip(Color.parse(other).to_a).map { |x, y| x + (y - x) * amount })
|
|
22
23
|
end
|
|
23
24
|
|
|
25
|
+
def lighten(amount)
|
|
26
|
+
value = to_hsla
|
|
27
|
+
HSLA.new(value.h, value.s, (value.l + amount).clamp(0, 1), value.a).to_rgba
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def darken(amount) = lighten(-amount)
|
|
31
|
+
|
|
32
|
+
def contrast_ratio(other)
|
|
33
|
+
values = [self, Color.parse(other)].map do |color|
|
|
34
|
+
channels = [color.r, color.g, color.b].map { |channel| channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055)**2.4 }
|
|
35
|
+
0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2]
|
|
36
|
+
end
|
|
37
|
+
(values.max + 0.05) / (values.min + 0.05)
|
|
38
|
+
end
|
|
39
|
+
|
|
24
40
|
def to_hsla
|
|
25
41
|
max, min = [r, g, b].max, [r, g, b].min
|
|
26
42
|
delta, lightness = max - min, (max + min) / 2.0
|