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
data/lib/zaniah/layout/style.rb
CHANGED
|
@@ -8,12 +8,27 @@ module Zaniah
|
|
|
8
8
|
flex_grow: 0, flex_shrink: 1, flex_basis: :auto, width: :auto, height: :auto,
|
|
9
9
|
min_width: 0, min_height: 0, max_width: Float::INFINITY, max_height: Float::INFINITY,
|
|
10
10
|
padding: 0, margin: 0, border: 0, gap: 0, position: :relative,
|
|
11
|
-
|
|
11
|
+
direction: :ltr, padding_start: nil, padding_end: nil,
|
|
12
|
+
margin_start: nil, margin_end: nil, border_start: nil, border_end: nil,
|
|
13
|
+
overflow: :visible, aspect_ratio: nil, grid_template_columns: nil,
|
|
14
|
+
grid_template_rows: nil, grid_column: nil, grid_row: nil, row_gap: nil,
|
|
15
|
+
column_gap: nil, z_index: 0, opacity: 1.0, transform: nil,
|
|
16
|
+
background: nil, border_widths: nil, border_style: :solid,
|
|
17
|
+
border_color: nil, corner_radii: nil, shadows: nil, cursor: nil,
|
|
18
|
+
ring: nil, text_color: nil, font_size: nil, font_family: nil,
|
|
19
|
+
line_height: nil, letter_spacing: nil, text_align: :start,
|
|
20
|
+
text_wrap: :wrap, text_overflow: :clip}.freeze
|
|
21
|
+
INHERITED = %i[text_color font_size font_family line_height letter_spacing].freeze
|
|
12
22
|
|
|
13
23
|
def initialize(**properties) = @values = DEFAULTS.merge(properties).freeze
|
|
14
24
|
def [](name) = @values[name]
|
|
15
25
|
def merge(**properties) = Style.new(**@values.merge(properties))
|
|
16
26
|
def to_h = @values
|
|
27
|
+
|
|
28
|
+
def inherit(parent)
|
|
29
|
+
inherited = INHERITED.to_h { |name| [name, self[name] || parent[name]] }
|
|
30
|
+
merge(**inherited)
|
|
31
|
+
end
|
|
17
32
|
end
|
|
18
33
|
end
|
|
19
34
|
end
|
data/lib/zaniah/layout.rb
CHANGED
data/lib/zaniah/length.rb
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Zaniah
|
|
4
|
+
MinMax = Data.define(:min, :max)
|
|
5
|
+
|
|
4
6
|
Length = Data.define(:value, :unit) do
|
|
5
7
|
def resolve(available, rem: 16)
|
|
6
8
|
case unit
|
|
7
9
|
when :px then value
|
|
8
10
|
when :percent then available * value / 100.0
|
|
9
11
|
when :rem then rem * value
|
|
12
|
+
when :fr then raise ArgumentError, "fr lengths are only valid in grid tracks"
|
|
10
13
|
else raise ArgumentError, "unknown length unit #{unit}"
|
|
11
14
|
end
|
|
12
15
|
end
|
data/lib/zaniah/length_units.rb
CHANGED
|
@@ -5,5 +5,12 @@ module Zaniah
|
|
|
5
5
|
def px(value) = Length.new(value, :px)
|
|
6
6
|
def rems(value) = Length.new(value, :rem)
|
|
7
7
|
def percent(value) = Length.new(value, :percent)
|
|
8
|
+
def fr(value) = Length.new(value, :fr)
|
|
9
|
+
def minmax(min, max) = MinMax.new(min, max)
|
|
10
|
+
|
|
11
|
+
def repeat(count, track)
|
|
12
|
+
raise ArgumentError, "repeat count must be a positive integer" unless count.is_a?(Integer) && count.positive?
|
|
13
|
+
Array.new(count, track)
|
|
14
|
+
end
|
|
8
15
|
end
|
|
9
16
|
end
|
data/lib/zaniah/list.rb
CHANGED
|
@@ -4,32 +4,37 @@ module Zaniah
|
|
|
4
4
|
# A variable-height, viewport-only list. Unknown rows use estimated_height;
|
|
5
5
|
# measuring visible rows refines the prefix sums without moving the anchor.
|
|
6
6
|
class List < Element
|
|
7
|
-
attr_reader :
|
|
7
|
+
attr_reader :visible_range, :heights, :scroll_state
|
|
8
8
|
|
|
9
9
|
def initialize(count:, estimated_height: 24, overscan: 2, &render_item)
|
|
10
10
|
super()
|
|
11
11
|
raise ArgumentError, "a row renderer is required" unless render_item
|
|
12
12
|
raise ArgumentError, "overscan must be a nonnegative integer" unless overscan.is_a?(Integer) && overscan >= 0
|
|
13
13
|
@heights, @overscan, @render_item = HeightIndex.new(count, estimated_height), overscan, render_item
|
|
14
|
-
@
|
|
14
|
+
@scroll_state = ScrollState.new(axis: :vertical)
|
|
15
|
+
@scroll_state.update(content_size: Size.new(0, @heights.total), viewport_size: Size.new(0, 0))
|
|
16
|
+
@viewport, @visible_range = 0.0, 0...0
|
|
15
17
|
style(overflow: :hidden)
|
|
16
18
|
on_scroll_wheel do |event, cx|
|
|
17
|
-
|
|
19
|
+
@scroll_state.glide_by(event.delta.y, animator: cx.animator, key: [:scroll, object_id],
|
|
20
|
+
duration: cx.theme.motion.reduced? ? 0 : cx.theme.motion.duration_slow)
|
|
18
21
|
cx.window.request_frame
|
|
19
22
|
end
|
|
20
23
|
end
|
|
21
24
|
|
|
22
25
|
def total_height = @heights.total
|
|
26
|
+
def scroll_y = @scroll_state.offset.y
|
|
23
27
|
|
|
24
28
|
def scroll_y=(value)
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
@scroll_state.scroll_to(value)
|
|
30
|
+
value
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
def update_height(index, height)
|
|
30
|
-
anchor = @heights.index_at(
|
|
34
|
+
anchor = @heights.index_at(scroll_y)
|
|
31
35
|
delta = @heights.update(index, height)
|
|
32
|
-
|
|
36
|
+
@scroll_state.update(content_size: Size.new(@scroll_state.content_size.width, total_height), viewport_size: @scroll_state.viewport_size)
|
|
37
|
+
@scroll_state.preserve_anchor(delta) if index < anchor
|
|
33
38
|
self
|
|
34
39
|
end
|
|
35
40
|
|
|
@@ -40,7 +45,7 @@ module Zaniah
|
|
|
40
45
|
when :center then top - (@viewport - height) / 2
|
|
41
46
|
when :end then top + height - @viewport
|
|
42
47
|
when :nearest
|
|
43
|
-
top <
|
|
48
|
+
top < scroll_y ? top : (top + height > scroll_y + @viewport ? top + height - @viewport : scroll_y)
|
|
44
49
|
else raise ArgumentError, "unknown alignment #{align.inspect}"
|
|
45
50
|
end
|
|
46
51
|
self.scroll_y = offset
|
|
@@ -49,11 +54,16 @@ module Zaniah
|
|
|
49
54
|
|
|
50
55
|
def request_layout(cx)
|
|
51
56
|
@state = cx.state(@key, &@state_initializer) if @key && @state_initializer
|
|
57
|
+
if cx.respond_to?(:animator)
|
|
58
|
+
@scroll_state.animation(animator: cx.animator, key: [:scroll, object_id],
|
|
59
|
+
duration: cx.theme.motion.reduced? ? 0 : cx.theme.motion.duration_slow)
|
|
60
|
+
end
|
|
61
|
+
@scroll_state.sample_glide
|
|
52
62
|
@viewport = dimension(:height, cx.window.content_size.height)
|
|
53
63
|
width = dimension(:width, cx.window.content_size.width)
|
|
54
|
-
|
|
55
|
-
anchor = @heights.index_at(
|
|
56
|
-
within =
|
|
64
|
+
@scroll_state.update(content_size: Size.new(width, total_height), viewport_size: Size.new(width, @viewport))
|
|
65
|
+
anchor = @heights.index_at(scroll_y)
|
|
66
|
+
within = scroll_y - @heights.prefix(anchor)
|
|
57
67
|
measured, engine = {}, Layout::Engine.new
|
|
58
68
|
loop do
|
|
59
69
|
@visible_range = viewport_range
|
|
@@ -61,19 +71,21 @@ module Zaniah
|
|
|
61
71
|
break if missing.empty?
|
|
62
72
|
missing.each do |index|
|
|
63
73
|
element = @render_item.call(index)
|
|
64
|
-
raise TypeError, "row renderer must return
|
|
74
|
+
raise TypeError, "row renderer must return a renderable element" unless element.respond_to?(:request_layout) && element.respond_to?(:prepaint) && element.respond_to?(:paint)
|
|
65
75
|
node = element.request_layout(cx)
|
|
66
76
|
_natural_width, height = engine.measure(node, width: width, height: @viewport)
|
|
67
77
|
@heights.update(index, [height, 1.0].max)
|
|
68
78
|
measured[index] = [element, node]
|
|
69
79
|
end
|
|
80
|
+
@scroll_state.update(content_size: Size.new(width, total_height), viewport_size: Size.new(width, @viewport))
|
|
70
81
|
self.scroll_y = @heights.prefix(anchor) + [within, anchor < @heights.count ? @heights[anchor] : 0].min
|
|
71
82
|
end
|
|
72
83
|
@children = @visible_range.map { |index| measured.fetch(index).first }
|
|
84
|
+
@children.each { |child| child.send(:parent=, self) }
|
|
73
85
|
nodes = @visible_range.map do |index|
|
|
74
86
|
node = measured.fetch(index).last
|
|
75
87
|
node.style = node.style.merge(position: :absolute, left: 0, right: 0,
|
|
76
|
-
top: @heights.prefix(index) -
|
|
88
|
+
top: @heights.prefix(index) - scroll_y, height: @heights[index])
|
|
77
89
|
node
|
|
78
90
|
end
|
|
79
91
|
@layout_node = Layout::Node.new(style: @style, children: nodes,
|
|
@@ -90,8 +102,8 @@ module Zaniah
|
|
|
90
102
|
|
|
91
103
|
def viewport_range
|
|
92
104
|
return 0...0 if @heights.count.zero? || @viewport.zero?
|
|
93
|
-
first = [@heights.index_at(
|
|
94
|
-
last = [@heights.index_at(
|
|
105
|
+
first = [@heights.index_at(scroll_y) - @overscan, 0].max
|
|
106
|
+
last = [@heights.index_at(scroll_y + @viewport) + 1 + @overscan, @heights.count].min
|
|
95
107
|
first...last
|
|
96
108
|
end
|
|
97
109
|
end
|
|
@@ -6,20 +6,24 @@ module Zaniah
|
|
|
6
6
|
Popup = Data.define(:labels, :enabled, :selected_index, :bounds)
|
|
7
7
|
|
|
8
8
|
class Window
|
|
9
|
+
include Appearance
|
|
9
10
|
DEFAULT_CLEAR = "#181b20"
|
|
10
11
|
|
|
11
|
-
attr_reader :content_size, :scene, :device, :dispatcher, :scale_factor, :text_runs
|
|
12
|
-
attr_accessor :text_system, :ime_state, :title
|
|
12
|
+
attr_reader :content_size, :scene, :device, :dispatcher, :scale_factor, :text_runs, :pointer_position, :cursor_style, :animator, :clock, :accessibility_tree, :accessibility_revision, :frame_stats
|
|
13
|
+
attr_accessor :text_system, :ime_state, :title, :app, :devtools
|
|
13
14
|
|
|
14
15
|
def initialize(width: 800, height: 600, title: "Zaniah UI", scale_factor: 1,
|
|
15
16
|
keymap: nil, clock: MONOTONIC_CLOCK)
|
|
16
17
|
@content_size, @scale_factor, @title = Size.new(width, height), scale_factor, title
|
|
17
18
|
@scene = Scene.new
|
|
18
19
|
@device = GPU::Software.new(width, height)
|
|
19
|
-
@dispatcher = Input::Dispatcher.new(keymap: keymap || Input::Keymap.
|
|
20
|
+
@dispatcher = Input::Dispatcher.new(keymap: keymap || Input::Keymap.default_ui(clock: clock))
|
|
20
21
|
@clock = clock
|
|
22
|
+
@animator = Animator.new(clock: clock)
|
|
23
|
+
@accessibility_tree, @accessibility_revision = Accessibility::Tree.new, 0
|
|
24
|
+
@frame_stats = {fps: 0.0, frame_ms: 0.0, layout_ms: 0.0, prepaint_ms: 0.0, paint_ms: 0.0, command_count: 0}.freeze
|
|
21
25
|
@state, @used_state, @text_runs = {}, {}, []
|
|
22
|
-
@dirty, @closed = true, false
|
|
26
|
+
@dirty, @closed, @pointer_down, @cursor_style = true, false, false, :arrow
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
def on_input(&block) = @on_input = block
|
|
@@ -31,10 +35,25 @@ module Zaniah
|
|
|
31
35
|
def draw(&block) = @draw = block
|
|
32
36
|
def request_frame = @dirty = true
|
|
33
37
|
def dirty? = @dirty
|
|
38
|
+
def animation_active? = !!@animator&.active?
|
|
34
39
|
def closed? = @closed
|
|
40
|
+
def pointer_down? = @pointer_down
|
|
41
|
+
def set_cursor(style)
|
|
42
|
+
raise ArgumentError, "unknown cursor #{style}" unless %i[arrow text pointer crosshair resize_horizontal resize_vertical].include?(style)
|
|
43
|
+
return style if @cursor_style == style
|
|
44
|
+
self.cursor_style = style
|
|
45
|
+
@cursor_style = style
|
|
46
|
+
end
|
|
47
|
+
def cursor_style=(style)
|
|
48
|
+
@cursor_style = style
|
|
49
|
+
end
|
|
35
50
|
def displays = [Display.new(0, "Headless", Bounds.new(0, 0, @content_size.width, @content_size.height), @scale_factor, true)]
|
|
36
51
|
|
|
37
52
|
def popup
|
|
53
|
+
if @popup_component.respond_to?(:popup_data)
|
|
54
|
+
data = @popup_component.popup_data
|
|
55
|
+
return Popup.new(labels: data[:labels], enabled: data[:enabled], selected_index: data[:selected_index], bounds: data[:bounds])
|
|
56
|
+
end
|
|
38
57
|
return unless @menu
|
|
39
58
|
|
|
40
59
|
Popup.new(labels: @menu[:items].map { |label, _| label.dup.freeze }.freeze,
|
|
@@ -51,13 +70,19 @@ module Zaniah
|
|
|
51
70
|
|
|
52
71
|
def input(event)
|
|
53
72
|
return if popup_input(event)
|
|
73
|
+
@pointer_position = event.position if event.respond_to?(:position)
|
|
74
|
+
@pointer_down = true if event.is_a?(Input::MouseDown)
|
|
75
|
+
@pointer_down = false if event.is_a?(Input::MouseUp)
|
|
54
76
|
@tooltip_offered = false if event.is_a?(Input::MouseMove)
|
|
55
77
|
@tooltip = nil if event.is_a?(Input::MouseDown) || event.is_a?(Input::KeyDown)
|
|
56
78
|
@on_input&.call(event)
|
|
79
|
+
@devtools&.handle_input(event)
|
|
57
80
|
if event.respond_to?(:position)
|
|
58
81
|
@dispatcher.mouse(event)
|
|
59
82
|
elsif event.is_a?(Input::KeyDown)
|
|
60
83
|
@dispatcher.key(event.keystroke)
|
|
84
|
+
else
|
|
85
|
+
@dispatcher.input(event)
|
|
61
86
|
end
|
|
62
87
|
@tooltip = nil if event.is_a?(Input::MouseMove) && !@tooltip_offered
|
|
63
88
|
request_frame
|
|
@@ -71,6 +96,13 @@ module Zaniah
|
|
|
71
96
|
|
|
72
97
|
def context_menu(items, position: Point.new(0, 0))
|
|
73
98
|
raise ArgumentError, "menu items must be label/callback pairs" unless items.is_a?(Array) && items.all? { |item| item.is_a?(Array) && item.length == 2 && item.first.is_a?(String) && (item.last.nil? || item.last.respond_to?(:call)) }
|
|
99
|
+
if defined?(Zaniah::UI::ContextMenu)
|
|
100
|
+
@popup_component = Zaniah::UI::ContextMenu.new(items, anchor: position)
|
|
101
|
+
@menu = nil
|
|
102
|
+
@tooltip = nil
|
|
103
|
+
request_frame
|
|
104
|
+
return @popup_component
|
|
105
|
+
end
|
|
74
106
|
@menu = {items: items.map(&:dup), position: position, index: items.index { |_, callback| callback } || 0}
|
|
75
107
|
@tooltip = nil
|
|
76
108
|
request_frame
|
|
@@ -91,14 +123,20 @@ module Zaniah
|
|
|
91
123
|
@tooltip[:shown] = true
|
|
92
124
|
request_frame
|
|
93
125
|
end
|
|
126
|
+
request_frame if @animator&.active?
|
|
94
127
|
return unless @dirty && !@closed
|
|
95
128
|
@dirty = false
|
|
96
129
|
result = @draw&.call(self)
|
|
97
|
-
|
|
98
|
-
|
|
130
|
+
if result.respond_to?(:request_layout) && result.respond_to?(:prepaint) && result.respond_to?(:paint)
|
|
131
|
+
render(result)
|
|
132
|
+
elsif result.is_a?(Scene)
|
|
133
|
+
@device.render(result)
|
|
134
|
+
end
|
|
135
|
+
request_frame if @animator&.active?
|
|
99
136
|
end
|
|
100
137
|
|
|
101
138
|
def render(element, clear: DEFAULT_CLEAR, present: true)
|
|
139
|
+
frame_started = MONOTONIC_CLOCK.call
|
|
102
140
|
@text_system.scale_factor = @scale_factor if @text_system.respond_to?(:scale_factor=)
|
|
103
141
|
@scene.clear
|
|
104
142
|
@text_runs.clear
|
|
@@ -106,12 +144,31 @@ module Zaniah
|
|
|
106
144
|
@used_state.clear
|
|
107
145
|
@dispatcher.clear_hits
|
|
108
146
|
cx = FrameContext.new(self)
|
|
147
|
+
@animator.reduced_motion = cx.theme.motion.reduced?
|
|
148
|
+
layout_started = MONOTONIC_CLOCK.call
|
|
109
149
|
root = element.request_layout(cx)
|
|
110
150
|
Layout::Engine.new.compute(root, width: @content_size.width, height: @content_size.height)
|
|
151
|
+
layout_finished = MONOTONIC_CLOCK.call
|
|
152
|
+
@animator.sample(@clock.call)
|
|
111
153
|
element.prepaint(root.bounds, nil, cx)
|
|
154
|
+
cx.interactivity.resolve(@dispatcher, @pointer_position, @pointer_down)
|
|
155
|
+
prepaint_finished = MONOTONIC_CLOCK.call
|
|
156
|
+
set_cursor(:arrow)
|
|
112
157
|
element.paint(root.bounds, nil, nil, cx)
|
|
113
158
|
paint_popups
|
|
159
|
+
paint_finished = MONOTONIC_CLOCK.call
|
|
160
|
+
overlays = [@popup_component].compact
|
|
161
|
+
accessibility_changed(@accessibility_tree.root, @accessibility_tree.changes) if @accessibility_tree.update(element, cx, overlays: overlays)
|
|
114
162
|
@state.delete_if { |key, _| !@used_state[key] }
|
|
163
|
+
elapsed = frame_started - (@last_frame_at || frame_started)
|
|
164
|
+
fps = elapsed.positive? ? 1.0 / elapsed : 0.0
|
|
165
|
+
@smoothed_fps = @smoothed_fps ? @smoothed_fps * 0.9 + fps * 0.1 : fps
|
|
166
|
+
@last_frame_at = frame_started
|
|
167
|
+
@frame_stats = {fps: @smoothed_fps, frame_ms: (paint_finished - frame_started) * 1000,
|
|
168
|
+
layout_ms: (layout_finished - layout_started) * 1000,
|
|
169
|
+
prepaint_ms: (prepaint_finished - layout_finished) * 1000,
|
|
170
|
+
paint_ms: (paint_finished - prepaint_finished) * 1000,
|
|
171
|
+
command_count: @scene.commands.length / 4}.freeze
|
|
115
172
|
@on_frame&.call(element, clear)
|
|
116
173
|
@device.render(@scene, clear: clear) if present
|
|
117
174
|
@text_system.end_frame if @text_system.respond_to?(:end_frame)
|
|
@@ -123,11 +180,34 @@ module Zaniah
|
|
|
123
180
|
@state[key] = yield
|
|
124
181
|
end
|
|
125
182
|
|
|
183
|
+
def accessibility_changed(root, changes)
|
|
184
|
+
@accessibility_revision += 1
|
|
185
|
+
Accessibility.publish(self, root, changes)
|
|
186
|
+
end
|
|
187
|
+
|
|
126
188
|
def write_png(path) = @device.write_png(path)
|
|
127
189
|
|
|
128
190
|
private
|
|
129
191
|
|
|
130
192
|
def popup_input(event)
|
|
193
|
+
if @popup_component
|
|
194
|
+
unless @popup_component.open?
|
|
195
|
+
@popup_component = nil
|
|
196
|
+
return false
|
|
197
|
+
end
|
|
198
|
+
@pointer_position = event.position if event.respond_to?(:position)
|
|
199
|
+
@pointer_down = true if event.is_a?(Input::MouseDown)
|
|
200
|
+
@pointer_down = false if event.is_a?(Input::MouseUp)
|
|
201
|
+
if event.respond_to?(:position)
|
|
202
|
+
@dispatcher.mouse(event)
|
|
203
|
+
elsif event.is_a?(Input::KeyDown)
|
|
204
|
+
@dispatcher.key(event.keystroke)
|
|
205
|
+
else
|
|
206
|
+
@dispatcher.input(event)
|
|
207
|
+
end
|
|
208
|
+
request_frame
|
|
209
|
+
return true
|
|
210
|
+
end
|
|
131
211
|
return false unless @menu
|
|
132
212
|
if event.is_a?(Input::KeyDown)
|
|
133
213
|
key = Input::Keystroke.normalize(event.keystroke)
|
|
@@ -174,9 +254,11 @@ module Zaniah
|
|
|
174
254
|
end
|
|
175
255
|
|
|
176
256
|
def paint_popups
|
|
177
|
-
if @
|
|
257
|
+
if @popup_component
|
|
258
|
+
paint_popup_component(@popup_component)
|
|
259
|
+
elsif @menu
|
|
178
260
|
box = popup_bounds(@menu)
|
|
179
|
-
@scene.layer(
|
|
261
|
+
@scene.layer(Scene::LAYER_POPUP) do
|
|
180
262
|
@scene.quad(box.x, box.y, box.width, box.height, color: "#202936", radius: 4, border_width: 1, border_color: "#526176")
|
|
181
263
|
@scene.clip(box) do
|
|
182
264
|
@menu[:items].each_with_index do |(label, callback), index|
|
|
@@ -186,17 +268,27 @@ module Zaniah
|
|
|
186
268
|
end
|
|
187
269
|
end
|
|
188
270
|
end
|
|
271
|
+
elsif @tooltip && @tooltip[:shown] && defined?(Zaniah::UI::Tooltip)
|
|
272
|
+
paint_popup_component(Zaniah::UI::Tooltip.new(@tooltip[:text], anchor: @tooltip[:position]))
|
|
189
273
|
elsif @tooltip && @tooltip[:shown]
|
|
190
274
|
text = @tooltip[:text]
|
|
191
275
|
position = @tooltip[:position]
|
|
192
276
|
width = [text.length * 8 + 20, @content_size.width].min
|
|
193
277
|
x, y = position.x.clamp(0, @content_size.width - width), (position.y + 20).clamp(0, [@content_size.height - 30, 0].max)
|
|
194
|
-
@scene.layer(
|
|
278
|
+
@scene.layer(Scene::LAYER_TOOLTIP) do
|
|
195
279
|
@scene.quad(x, y, width, 28, color: "#202936", radius: 4, border_width: 1, border_color: "#526176")
|
|
196
280
|
@scene.clip(Bounds.new(x, y, width, 28)) { popup_text(text, x + 10, y + 5, "#edf2f7") }
|
|
197
281
|
end
|
|
198
282
|
end
|
|
199
283
|
end
|
|
284
|
+
|
|
285
|
+
def paint_popup_component(component)
|
|
286
|
+
cx = FrameContext.new(self)
|
|
287
|
+
root = component.request_layout(cx)
|
|
288
|
+
Layout::Engine.new.compute(root, width: @content_size.width, height: @content_size.height)
|
|
289
|
+
component.prepaint(root.bounds, nil, cx)
|
|
290
|
+
component.paint(root.bounds, nil, nil, cx)
|
|
291
|
+
end
|
|
200
292
|
end
|
|
201
293
|
end
|
|
202
294
|
end
|
|
@@ -17,6 +17,14 @@ module Zaniah
|
|
|
17
17
|
ENV.fetch("GTK_THEME", "").downcase.include?("dark") ? :dark : :light
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
def reduced_motion?
|
|
21
|
+
return @reduced_motion if instance_variable_defined?(:@reduced_motion)
|
|
22
|
+
output, status = Open3.capture2e("gsettings", "get", "org.gnome.desktop.interface", "enable-animations")
|
|
23
|
+
@reduced_motion = status.success? && output.strip == "false"
|
|
24
|
+
rescue Errno::ENOENT
|
|
25
|
+
@reduced_motion = false
|
|
26
|
+
end
|
|
27
|
+
|
|
20
28
|
def on_appearance(&block)
|
|
21
29
|
@on_appearance = block
|
|
22
30
|
close_appearance
|
|
@@ -130,6 +130,7 @@ module Zaniah
|
|
|
130
130
|
return if closed?
|
|
131
131
|
@connection.poll
|
|
132
132
|
poll_appearance
|
|
133
|
+
Accessibility.poll(self)
|
|
133
134
|
return close if @close_requested
|
|
134
135
|
finish_drop if @pending_drop
|
|
135
136
|
if @repeat_key && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= @repeat_at
|
|
@@ -140,7 +141,7 @@ module Zaniah
|
|
|
140
141
|
end
|
|
141
142
|
def run
|
|
142
143
|
until closed?
|
|
143
|
-
@connection.poll(timeout: dirty? ? 0 : 0.05)
|
|
144
|
+
@connection.poll(timeout: dirty? || animation_active? ? 0 : 0.05)
|
|
144
145
|
tick
|
|
145
146
|
end
|
|
146
147
|
end
|
|
@@ -345,6 +346,7 @@ module Zaniah
|
|
|
345
346
|
end
|
|
346
347
|
def close
|
|
347
348
|
return false unless super
|
|
349
|
+
Accessibility.close(self)
|
|
348
350
|
close_appearance
|
|
349
351
|
cleanup_native
|
|
350
352
|
true
|
|
@@ -108,6 +108,7 @@ module Zaniah
|
|
|
108
108
|
def tick
|
|
109
109
|
poll_events unless closed?
|
|
110
110
|
poll_appearance unless closed?
|
|
111
|
+
Accessibility.poll(self) unless closed?
|
|
111
112
|
raise @native_error if @native_error
|
|
112
113
|
super
|
|
113
114
|
end
|
|
@@ -116,7 +117,7 @@ module Zaniah
|
|
|
116
117
|
io = IO.for_fd(fd, autoclose: false)
|
|
117
118
|
until closed?
|
|
118
119
|
tick
|
|
119
|
-
IO.select([io], nil, nil, 0.05) unless dirty? || closed?
|
|
120
|
+
IO.select([io], nil, nil, 0.05) unless dirty? || animation_active? || closed?
|
|
120
121
|
end
|
|
121
122
|
end
|
|
122
123
|
def poll_events
|
|
@@ -285,7 +286,7 @@ module Zaniah
|
|
|
285
286
|
next if line.start_with?("#")
|
|
286
287
|
uri = URI.parse(line.strip) rescue nil
|
|
287
288
|
next unless uri&.scheme == "file" && [nil, "", "localhost"].include?(uri.host)
|
|
288
|
-
path = URI::
|
|
289
|
+
path = URI::RFC2396_PARSER.unescape(uri.path).force_encoding("UTF-8")
|
|
289
290
|
path if path.start_with?("/") && !path.include?("\0") && path.valid_encoding?
|
|
290
291
|
end
|
|
291
292
|
end
|
|
@@ -402,6 +403,7 @@ module Zaniah
|
|
|
402
403
|
end
|
|
403
404
|
def close
|
|
404
405
|
return false unless super
|
|
406
|
+
Accessibility.close(self)
|
|
405
407
|
close_appearance
|
|
406
408
|
x(:XDestroyIC, [P], V, @ic) if @ic
|
|
407
409
|
x(:XCloseIM, [P], I, @im) if @im && !@im.null?
|
|
@@ -57,7 +57,7 @@ module Zaniah
|
|
|
57
57
|
def run
|
|
58
58
|
yield self if block_given?
|
|
59
59
|
until WINDOWS.values.uniq.all?(&:closed?)
|
|
60
|
-
poll(wait: WINDOWS.values.any?
|
|
60
|
+
poll(wait: WINDOWS.values.any? { |window| window.dirty? || window.animation_active? } ? 0 : 0.05)
|
|
61
61
|
WINDOWS.values.uniq.each { |window| window.tick(poll_events: false) }
|
|
62
62
|
end
|
|
63
63
|
end
|
|
@@ -7,6 +7,9 @@ module Zaniah
|
|
|
7
7
|
module Mac
|
|
8
8
|
class CoreText
|
|
9
9
|
P, L, D, V = Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG, Fiddle::TYPE_DOUBLE, Fiddle::TYPE_VOID
|
|
10
|
+
# Initializing CoreText first can leave AppKit's later NSFont cache invalid.
|
|
11
|
+
APPKIT = FFI::Library.new("/System/Library/Frameworks/AppKit.framework/AppKit")
|
|
12
|
+
|
|
10
13
|
def initialize
|
|
11
14
|
require "alhena"
|
|
12
15
|
@cf = FFI::Library.new("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")
|
|
@@ -124,12 +124,13 @@ module Zaniah
|
|
|
124
124
|
end
|
|
125
125
|
def run
|
|
126
126
|
until closed?
|
|
127
|
-
App.instance.poll(wait: dirty? ? 0 : 0.05)
|
|
127
|
+
App.instance.poll(wait: dirty? || animation_active? ? 0 : 0.05)
|
|
128
128
|
tick(poll_events: false)
|
|
129
129
|
end
|
|
130
130
|
end
|
|
131
131
|
def close
|
|
132
132
|
return false unless super
|
|
133
|
+
Accessibility.close(self)
|
|
133
134
|
WINDOWS.delete_if { |_, window| window.equal?(self) }
|
|
134
135
|
O.send(@handle, "setDelegate:", 0, args: [:pointer], result: :void)
|
|
135
136
|
O.send(@handle, "close", result: :void)
|
|
@@ -244,6 +245,12 @@ module Zaniah
|
|
|
244
245
|
name = O.text(O.send(O.send(@view, "effectiveAppearance"), "name"))
|
|
245
246
|
name.include?("Dark") ? :dark : :light
|
|
246
247
|
end
|
|
248
|
+
def reduced_motion?
|
|
249
|
+
workspace = O.send(O.klass("NSWorkspace"), "sharedWorkspace")
|
|
250
|
+
selector = O.selector("accessibilityDisplayShouldReduceMotion")
|
|
251
|
+
return false if O.send(workspace, "respondsToSelector:", selector, args: [:pointer], result: :bool).zero?
|
|
252
|
+
O.send(workspace, "accessibilityDisplayShouldReduceMotion", result: :bool) != 0
|
|
253
|
+
end
|
|
247
254
|
def on_appearance(&block) = @on_appearance = block
|
|
248
255
|
def appearance_changed
|
|
249
256
|
@on_appearance&.call(appearance)
|
|
@@ -254,6 +261,7 @@ module Zaniah
|
|
|
254
261
|
end
|
|
255
262
|
def context_action(index) = @context_actions&.fetch(index)&.call
|
|
256
263
|
def context_menu(items, position: Point.new(0, 0))
|
|
264
|
+
return super if defined?(Zaniah::UI::ContextMenu)
|
|
257
265
|
menu = O.new("NSMenu")
|
|
258
266
|
O.send(menu, "setAutoenablesItems:", 0, args: [:bool], result: :void)
|
|
259
267
|
@context_actions = []
|
|
@@ -9,7 +9,7 @@ module Zaniah
|
|
|
9
9
|
module Windows
|
|
10
10
|
def self.displays
|
|
11
11
|
user, scale = FFI::Library.new("user32.dll"), FFI::Library.new("shcore.dll")
|
|
12
|
-
ptype, int
|
|
12
|
+
ptype, int = Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT
|
|
13
13
|
result, error = [], nil
|
|
14
14
|
callback = Fiddle::Closure::BlockCaller.new(int, [ptype, ptype, ptype, Fiddle::TYPE_INTPTR_T]) do |monitor, _dc, _rect, _data|
|
|
15
15
|
begin
|
|
@@ -128,7 +128,7 @@ module Zaniah
|
|
|
128
128
|
def run
|
|
129
129
|
until closed?
|
|
130
130
|
tick
|
|
131
|
-
@user.fn(:MsgWaitForMultipleObjectsEx, [U, P, U, U, U], U).call(0, 0, 50, 0x04FF, 4) unless dirty? || closed?
|
|
131
|
+
@user.fn(:MsgWaitForMultipleObjectsEx, [U, P, U, U, U], U).call(0, 0, 50, 0x04FF, 4) unless dirty? || animation_active? || closed?
|
|
132
132
|
end
|
|
133
133
|
end
|
|
134
134
|
def native_callback
|
|
@@ -146,6 +146,9 @@ module Zaniah
|
|
|
146
146
|
def message(message, wparam, lparam)
|
|
147
147
|
case message
|
|
148
148
|
when 0x0010 then close; return 0
|
|
149
|
+
when 0x003d
|
|
150
|
+
result = Accessibility::Windows.provider_result(self, wparam, lparam)
|
|
151
|
+
return result if result
|
|
149
152
|
when 0x0233
|
|
150
153
|
dropped_files(wparam)
|
|
151
154
|
return 0
|
|
@@ -327,8 +330,14 @@ module Zaniah
|
|
|
327
330
|
wide("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"), wide("AppsUseLightTheme"), 0x10, 0, value, size)
|
|
328
331
|
result.zero? && value.unpack1("I").zero? ? :dark : :light
|
|
329
332
|
end
|
|
333
|
+
def reduced_motion?
|
|
334
|
+
enabled = [1].pack("I")
|
|
335
|
+
result = @user.fn(:SystemParametersInfoW, [U, U, P, U], I).call(0x1042, 0, enabled, 0)
|
|
336
|
+
!result.zero? && enabled.unpack1("I").zero?
|
|
337
|
+
end
|
|
330
338
|
def on_appearance(&block) = @on_appearance = block
|
|
331
339
|
def context_menu(items, position: nil)
|
|
340
|
+
return super(items, position: position || Point.new(0, 0)) if defined?(Zaniah::UI::ContextMenu)
|
|
332
341
|
menu = @user.fn(:CreatePopupMenu, [], P).call
|
|
333
342
|
raise Error, "CreatePopupMenu failed" if menu.null?
|
|
334
343
|
items.each_with_index do |(label, action), index|
|
|
@@ -393,6 +402,7 @@ module Zaniah
|
|
|
393
402
|
end
|
|
394
403
|
def close
|
|
395
404
|
return false unless super
|
|
405
|
+
Accessibility.close(self)
|
|
396
406
|
@gl.fn(:wglMakeCurrent, [P, P], I).call(0, 0)
|
|
397
407
|
@gl.fn(:wglDeleteContext, [P], I).call(@context)
|
|
398
408
|
@user.fn(:ReleaseDC, [P, P], I).call(@handle, @dc)
|