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.
Files changed (166) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +70 -0
  5. data/assets/fonts/Abel-Regular.ttf +0 -0
  6. data/assets/fonts/OFL.txt +93 -0
  7. data/docs/adr/000-template.md +24 -0
  8. data/docs/adr/001-native-gpu-backends.md +36 -0
  9. data/docs/adr/002-objective-c-aggregates.md +34 -0
  10. data/docs/adr/003-fiddle-call-boundary.md +33 -0
  11. data/docs/adr/README.md +10 -0
  12. data/docs/native.md +66 -0
  13. data/docs/process_pool.md +67 -0
  14. data/docs/text.md +90 -0
  15. data/docs/vector_and_list.md +52 -0
  16. data/examples/ffi_bench.rb +16 -0
  17. data/examples/linux_smoke.rb +53 -0
  18. data/examples/mac_event_check.rb +61 -0
  19. data/examples/mac_poll_bench.rb +24 -0
  20. data/examples/metal_state_check.rb +72 -0
  21. data/examples/native_bench.rb +18 -0
  22. data/examples/native_drop.rb +82 -0
  23. data/examples/native_font.rb +15 -0
  24. data/examples/native_ime.rb +50 -0
  25. data/examples/native_smoke.rb +75 -0
  26. data/examples/native_text_bench.rb +83 -0
  27. data/examples/native_watch.rb +25 -0
  28. data/examples/vulkan_triangle.rb +20 -0
  29. data/examples/word_count_worker.rb +8 -0
  30. data/lib/zaniah/anchored.rb +10 -0
  31. data/lib/zaniah/app.rb +130 -0
  32. data/lib/zaniah/bounds.rb +22 -0
  33. data/lib/zaniah/canvas.rb +15 -0
  34. data/lib/zaniah/color.rb +35 -0
  35. data/lib/zaniah/configuration.rb +5 -0
  36. data/lib/zaniah/corners.rb +7 -0
  37. data/lib/zaniah/data_compat.rb +25 -0
  38. data/lib/zaniah/device_pixels.rb +7 -0
  39. data/lib/zaniah/div.rb +5 -0
  40. data/lib/zaniah/edges.rb +7 -0
  41. data/lib/zaniah/element.rb +106 -0
  42. data/lib/zaniah/entity.rb +9 -0
  43. data/lib/zaniah/entity_context.rb +17 -0
  44. data/lib/zaniah/error.rb +5 -0
  45. data/lib/zaniah/ffi/com.rb +24 -0
  46. data/lib/zaniah/ffi/library.rb +29 -0
  47. data/lib/zaniah/ffi/objc.rb +86 -0
  48. data/lib/zaniah/ffi/struct/signature.rb +46 -0
  49. data/lib/zaniah/ffi/struct.rb +61 -0
  50. data/lib/zaniah/ffi/wayland.rb +140 -0
  51. data/lib/zaniah/frame_context.rb +14 -0
  52. data/lib/zaniah/geometry.rb +16 -0
  53. data/lib/zaniah/gpu/buffer.rb +23 -0
  54. data/lib/zaniah/gpu/frame_encoder.rb +43 -0
  55. data/lib/zaniah/gpu/instance_packing.rb +53 -0
  56. data/lib/zaniah/gpu/metal.rb +209 -0
  57. data/lib/zaniah/gpu/open_gl.rb +210 -0
  58. data/lib/zaniah/gpu/pipeline.rb +7 -0
  59. data/lib/zaniah/gpu/software.rb +126 -0
  60. data/lib/zaniah/gpu/texture.rb +33 -0
  61. data/lib/zaniah/gpu/vulkan.rb +233 -0
  62. data/lib/zaniah/gpu.rb +18 -0
  63. data/lib/zaniah/hsla.rb +14 -0
  64. data/lib/zaniah/image.rb +19 -0
  65. data/lib/zaniah/input/action_registry.rb +12 -0
  66. data/lib/zaniah/input/composition.rb +7 -0
  67. data/lib/zaniah/input/context_predicate.rb +87 -0
  68. data/lib/zaniah/input/dispatcher.rb +59 -0
  69. data/lib/zaniah/input/file_drop.rb +7 -0
  70. data/lib/zaniah/input/focus_handle.rb +24 -0
  71. data/lib/zaniah/input/key_down.rb +7 -0
  72. data/lib/zaniah/input/key_up.rb +7 -0
  73. data/lib/zaniah/input/keymap/binding.rb +9 -0
  74. data/lib/zaniah/input/keymap.rb +55 -0
  75. data/lib/zaniah/input/keystroke.rb +20 -0
  76. data/lib/zaniah/input/mouse_down.rb +7 -0
  77. data/lib/zaniah/input/mouse_move.rb +7 -0
  78. data/lib/zaniah/input/mouse_up.rb +7 -0
  79. data/lib/zaniah/input/scroll_wheel.rb +7 -0
  80. data/lib/zaniah/input/text_input.rb +7 -0
  81. data/lib/zaniah/input.rb +17 -0
  82. data/lib/zaniah/layout/available_space.rb +11 -0
  83. data/lib/zaniah/layout/engine.rb +214 -0
  84. data/lib/zaniah/layout/node.rb +36 -0
  85. data/lib/zaniah/layout/style.rb +19 -0
  86. data/lib/zaniah/layout/tree.rb +16 -0
  87. data/lib/zaniah/layout.rb +11 -0
  88. data/lib/zaniah/length.rb +14 -0
  89. data/lib/zaniah/length_units.rb +9 -0
  90. data/lib/zaniah/list/height_index.rb +68 -0
  91. data/lib/zaniah/list.rb +101 -0
  92. data/lib/zaniah/overlay.rb +10 -0
  93. data/lib/zaniah/platform/display.rb +7 -0
  94. data/lib/zaniah/platform/file_event.rb +7 -0
  95. data/lib/zaniah/platform/headless/window.rb +185 -0
  96. data/lib/zaniah/platform/linux/appearance_aware.rb +60 -0
  97. data/lib/zaniah/platform/linux/displays.rb +55 -0
  98. data/lib/zaniah/platform/linux/free_type.rb +80 -0
  99. data/lib/zaniah/platform/linux/watcher.rb +70 -0
  100. data/lib/zaniah/platform/linux/wayland_display_discovery.rb +60 -0
  101. data/lib/zaniah/platform/linux/wayland_window.rb +369 -0
  102. data/lib/zaniah/platform/linux/window.rb +418 -0
  103. data/lib/zaniah/platform/linux.rb +3 -0
  104. data/lib/zaniah/platform/mac/app.rb +67 -0
  105. data/lib/zaniah/platform/mac/core_text.rb +94 -0
  106. data/lib/zaniah/platform/mac/displays.rb +23 -0
  107. data/lib/zaniah/platform/mac/watcher.rb +63 -0
  108. data/lib/zaniah/platform/mac/window.rb +298 -0
  109. data/lib/zaniah/platform/mac.rb +19 -0
  110. data/lib/zaniah/platform/tui/input_decoder.rb +145 -0
  111. data/lib/zaniah/platform/tui/text_renderer.rb +40 -0
  112. data/lib/zaniah/platform/tui/window.rb +91 -0
  113. data/lib/zaniah/platform/windows/terminal.rb +127 -0
  114. data/lib/zaniah/platform/windows/watcher.rb +91 -0
  115. data/lib/zaniah/platform/windows/window.rb +407 -0
  116. data/lib/zaniah/platform/windows.rb +3 -0
  117. data/lib/zaniah/platform.rb +34 -0
  118. data/lib/zaniah/png.rb +118 -0
  119. data/lib/zaniah/point.rb +8 -0
  120. data/lib/zaniah/process_pool/worker.rb +7 -0
  121. data/lib/zaniah/process_pool.rb +239 -0
  122. data/lib/zaniah/process_wire.rb +127 -0
  123. data/lib/zaniah/quad_packer.rb +15 -0
  124. data/lib/zaniah/scene/sprite_batch.rb +7 -0
  125. data/lib/zaniah/scene.rb +167 -0
  126. data/lib/zaniah/scene_renderer.rb +8 -0
  127. data/lib/zaniah/size.rb +5 -0
  128. data/lib/zaniah/subscription.rb +14 -0
  129. data/lib/zaniah/svg/path.rb +134 -0
  130. data/lib/zaniah/svg/rasterizable.rb +186 -0
  131. data/lib/zaniah/svg.rb +295 -0
  132. data/lib/zaniah/task/cancelled.rb +7 -0
  133. data/lib/zaniah/task/timeout.rb +7 -0
  134. data/lib/zaniah/task.rb +51 -0
  135. data/lib/zaniah/task_executor.rb +105 -0
  136. data/lib/zaniah/text.rb +24 -0
  137. data/lib/zaniah/text_system/atlas/entry.rb +9 -0
  138. data/lib/zaniah/text_system/atlas.rb +102 -0
  139. data/lib/zaniah/text_system/atlas_cache.rb +146 -0
  140. data/lib/zaniah/text_system/font_db/face.rb +9 -0
  141. data/lib/zaniah/text_system/font_db.rb +193 -0
  142. data/lib/zaniah/text_system/glyph.rb +7 -0
  143. data/lib/zaniah/text_system/line_layout.rb +20 -0
  144. data/lib/zaniah/text_system/renderer.rb +196 -0
  145. data/lib/zaniah/text_system/shaper.rb +430 -0
  146. data/lib/zaniah/text_system/typesetter/copy_error.rb +9 -0
  147. data/lib/zaniah/text_system/typesetter.rb +168 -0
  148. data/lib/zaniah/text_system.rb +9 -0
  149. data/lib/zaniah/unicode.rb +13 -0
  150. data/lib/zaniah/uniform_list.rb +30 -0
  151. data/lib/zaniah/version.rb +5 -0
  152. data/lib/zaniah/view.rb +13 -0
  153. data/lib/zaniah.rb +29 -0
  154. data/sig/app.rbs +80 -0
  155. data/sig/elements.rbs +129 -0
  156. data/sig/gpu.rbs +108 -0
  157. data/sig/input.rbs +114 -0
  158. data/sig/layout.rbs +53 -0
  159. data/sig/native.rbs +88 -0
  160. data/sig/platform.rbs +94 -0
  161. data/sig/scene.rbs +52 -0
  162. data/sig/shaper.rbs +8 -0
  163. data/sig/text.rbs +137 -0
  164. data/sig/zaniah.rbs +120 -0
  165. data/tools/check_dependencies.rb +52 -0
  166. metadata +249 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 83f0e9d7ba2790764f55bc1e0eda5e21e31b22a54cd0a92668b24df9000d351c
4
+ data.tar.gz: 37a96934f06671e69989c1e2e91e30917e0b6902458a09fc05fd5ef3999030f3
5
+ SHA512:
6
+ metadata.gz: 3feda2d8ba35caa3b73821eb9cae80c64d8b549e87c125dd6f55c4382d9789451bd91e0ae2e019709e1f923b662b498265085c6c0fcd268dad3a94ce7ab6c4c3
7
+ data.tar.gz: ef1e4f76251e22c1a8317677cccc3b80885aec8f3bb917d09df7a363a9dbfb6976ed19e38565a75eb5bf656b2a2f3276daada27156d79eb250d6e734ff1ad62b
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-09-11
4
+
5
+ - Initial release.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yudai Takada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # Zaniah
2
+
3
+ A Ruby UI toolkit with native GPU windows, a headless renderer, and a terminal
4
+ backend. CRuby 3.1+; YJIT recommended. Runtime dependencies are Alhena, REXML
5
+ and unicode-display_width.
6
+
7
+ The name comes from the white ground laid before paint or gold leaf: this
8
+ toolkit is the drawing surface beneath the editor.
9
+
10
+ ## Try it locally
11
+
12
+ ```sh
13
+ bundle install
14
+ bundle exec rake test
15
+ ruby --yjit examples/native_smoke.rb
16
+ gem build zaniah.gemspec
17
+ gem install --local zaniah-0.1.0.gem
18
+ ```
19
+
20
+ The gem name is not published or reserved. Install the locally built artifact
21
+ rather than an unverified public gem.
22
+
23
+ ```ruby
24
+ require "zaniah"
25
+
26
+ window = Zaniah::Platform.open_window(backend: :headless, width: 480, height: 240)
27
+ window.text_system = Zaniah::TextSystem::Renderer.new
28
+ window.draw do
29
+ Zaniah::Div.new.flex_col.p(24).gap(12).bg("#161b22")
30
+ .child(Zaniah::Text.new("Hello, Ruby", size: 24))
31
+ .child(Zaniah::Text.new("A native toolkit, written in Ruby."))
32
+ end
33
+ window.tick
34
+ window.write_png("hello.png")
35
+ window.close
36
+ ```
37
+
38
+ Choose `backend: :mac`, `:linux`, `:windows`, or `:tui` and call
39
+ `window.run` for an interactive window. Native rendering uses OS libraries via
40
+ Ruby's Fiddle, without extension compilation or a rendering subprocess.
41
+
42
+ ## Toolkit and platforms
43
+
44
+ Flex layouts, constraints, absolute positioning, wrapping, baseline alignment,
45
+ clipping, retained element state, and uniform/variable-height virtual lists are
46
+ provided. Generational entities and subscriptions connect state to frames;
47
+ foreground Fibers can await background tasks without blocking the UI.
48
+ Scene primitives include rounded rectangles, images, static SVG, shadows and
49
+ glyph atlases. OpenType ligatures and horizontal positioning use the Ruby shaper.
50
+ Pure Ruby rasterization is the default; explicit CoreText/FreeType providers and
51
+ a licensed Abel fallback font are included.
52
+
53
+ ## Development
54
+
55
+ ```sh
56
+ bundle exec rake test
57
+ ruby --yjit bench/instances.rb
58
+ ruby --yjit bench/list.rb
59
+ ```
60
+
61
+ `BUDGET=1` enables benchmark assertions.
62
+
63
+ See [native details](docs/native.md), [text configuration and shaping](docs/text.md),
64
+ [SVG and lists](docs/vector_and_list.md), [portable CPU workers](docs/process_pool.md),
65
+ and [decisions](docs/adr).
66
+
67
+ ## License
68
+
69
+ MIT; see [LICENSE.txt](LICENSE.txt). Included font notices remain alongside the
70
+ font files.
Binary file
@@ -0,0 +1,93 @@
1
+ Copyright (c) 2011, Matthew Desmond (http://www.madtype.com | mattdesmond@gmail.com),with Reserved Font Name Abel.
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,24 @@
1
+ # ADR NNN: Implementation decision title
2
+
3
+ - Status: Proposed
4
+ - Date: YYYY-MM-DD
5
+
6
+ ## Context
7
+
8
+ Describe the concrete implementation question and its compatibility, data,
9
+ runtime, or component constraints. Operational workflow and release policy do
10
+ not belong in this directory; see [the scope](README.md).
11
+
12
+ Name the credible alternatives. If there was no meaningful alternative, record
13
+ the behavior in reference documentation instead of creating an ADR.
14
+
15
+ ## Decision
16
+
17
+ Describe the durable boundary or architecture choice. Leave command syntax,
18
+ field-by-field formats, test evidence, benchmark results, and migration steps
19
+ in their authoritative documents.
20
+
21
+ ## Consequences
22
+
23
+ Describe the important positive and negative trade-offs, including what would
24
+ make this decision worth revisiting.
@@ -0,0 +1,36 @@
1
+ # ADR 001: Use Metal on macOS and OpenGL on Linux and Windows
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-09-10
5
+
6
+ ## Context
7
+
8
+ Native windows need GPU rendering while keeping the gem free of compiled
9
+ extensions. Rendering must preserve scene order, clipping, and alpha blending,
10
+ and Ruby callbacks must not enter the VM from unsupported operating-system
11
+ threads.
12
+
13
+ The credible alternatives were one OpenGL backend for every desktop, native
14
+ Metal on macOS with OpenGL elsewhere, software rendering only, or a compiled
15
+ bridge that could safely receive display-link callbacks.
16
+
17
+ ## Decision
18
+
19
+ macOS defaults to a `CAMetalLayer` and runtime-compiled Metal shaders, with
20
+ OpenGL as an explicit alternative. Linux uses OpenGL through GLX or EGL, and
21
+ Windows uses OpenGL through WGL. Headless rendering remains a separate software
22
+ backend.
23
+
24
+ Compatible adjacent scene commands are batched without reordering them. The Ruby
25
+ main thread polls native events and presents synchronized frames; native display
26
+ threads do not call Ruby through Fiddle.
27
+
28
+ ## Consequences
29
+
30
+ Applications get native GPU windows without compiling repository code, and each
31
+ platform can use a supported presentation path. The project must maintain both
32
+ Metal and OpenGL renderers and test native behavior on each target platform.
33
+
34
+ Revisit this decision if a compiled bridge becomes acceptable, Ruby gains a safe
35
+ foreign-thread callback boundary, or maintaining parallel rendering APIs costs
36
+ more than the platform-specific benefits.
@@ -0,0 +1,34 @@
1
+ # ADR 002: Describe Objective-C aggregate values with system libffi
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-09-10
5
+
6
+ ## Context
7
+
8
+ AppKit and related frameworks pass values such as `NSRect` and `NSRange` by
9
+ value. Fiddle function signatures expose scalar types but cannot describe these
10
+ aggregate arguments and return values accurately. Their calling conventions also
11
+ differ between arm64 and x86_64.
12
+
13
+ The credible alternatives were treating aggregates as pointers, writing a C
14
+ extension or generated trampoline for each signature, or describing the native
15
+ ABI through the system libffi library already used by Fiddle.
16
+
17
+ ## Decision
18
+
19
+ `FFI::Struct::Signature` describes aggregate layouts to system libffi and invokes
20
+ them with `ffi_call`. The same mechanism creates closures with aggregate
21
+ arguments and returns. Objective-C dispatch signatures are cached; arm64 uses
22
+ its native aggregate ABI, while x86_64 uses `objc_msgSend_stret` where required.
23
+
24
+ Native pointers and Ruby closures have explicit owners, and callbacks are retained
25
+ for as long as native code may invoke them.
26
+
27
+ ## Consequences
28
+
29
+ Aggregate calls follow each platform ABI without a compiler or C extension. The
30
+ trade-off is a small internal FFI layer that must model architecture-specific
31
+ dispatch and object lifetimes correctly.
32
+
33
+ Revisit this decision if Fiddle gains complete aggregate signatures or if the
34
+ project adopts a compiled native boundary for other reasons.
@@ -0,0 +1,33 @@
1
+ # ADR 003: Batch frame work across the Fiddle boundary
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-09-10
5
+
6
+ ## Context
7
+
8
+ Crossing from Ruby into native functions has fixed conversion and dispatch cost.
9
+ A renderer that makes one native call per primitive scales that cost with scene
10
+ size. Objective-C calls may also reenter Ruby on the current thread, while
11
+ terminal reads may block for an unbounded period.
12
+
13
+ The credible alternatives were per-primitive native calls, cached wrappers with
14
+ batched frame data, or moving frame encoding into a compiled extension.
15
+
16
+ ## Decision
17
+
18
+ Native function wrappers are cached by their full signature. Compatible adjacent
19
+ GPU commands are packed into instance buffers and submitted in batches rather
20
+ than one primitive at a time.
21
+
22
+ Objective-C dispatch keeps the GVL because callbacks may reenter Ruby. Blocking
23
+ ConPTY reads release it so other Ruby threads can continue.
24
+
25
+ ## Consequences
26
+
27
+ Native-call overhead depends primarily on batch boundaries instead of primitive
28
+ count, while draw order and clipping remain explicit. Batching adds packing code,
29
+ and material, texture, or clip changes still split batches.
30
+
31
+ Revisit this decision if profiling shows boundary calls are no longer material,
32
+ batch construction dominates frame time, or a compiled renderer becomes an
33
+ accepted dependency.
@@ -0,0 +1,10 @@
1
+ # Architecture Decision Records
2
+
3
+ This directory records durable implementation choices that affect component or
4
+ runtime boundaries and had credible alternatives. API reference, command syntax,
5
+ field formats, test evidence, benchmark results, migrations, release policy, and
6
+ operational workflow belong in their authoritative documentation instead.
7
+
8
+ Copy [ADR 000](000-template.md) when recording a decision. Decision records use
9
+ three-digit numbers starting at `001`, assigned without gaps. Keep accepted,
10
+ rejected, and superseded records so later entries do not need renumbering.
data/docs/native.md ADDED
@@ -0,0 +1,66 @@
1
+ # Native backends
2
+
3
+ Zaniah provides native windows on macOS, Linux, and Windows. Headless and
4
+ terminal backends are available when a graphical window is unnecessary.
5
+
6
+ ```ruby
7
+ window = Zaniah::Platform.open_window(
8
+ backend: :mac,
9
+ gpu: :metal,
10
+ width: 800,
11
+ height: 600,
12
+ title: "Zaniah"
13
+ )
14
+
15
+ window.on_input { |event| p event }
16
+ window.draw { Zaniah::Div.new.bg("#123") }
17
+ window.run
18
+ ```
19
+
20
+ | Backend | Renderer | Notes |
21
+ | --- | --- | --- |
22
+ | `:mac` | Metal (default) or OpenGL | Requires a graphical macOS session |
23
+ | `:linux` | Wayland/EGL or X11/GLX | Selected from `WAYLAND_DISPLAY` and `DISPLAY` |
24
+ | `:windows` | Win32/WGL | Requires 64-bit Ruby and Windows 10 APIs |
25
+ | `:headless` | Ruby software renderer | Deterministic rendering and PNG output |
26
+ | `:tui` | ANSI terminal | Text-grid rendering and terminal input |
27
+
28
+ On Linux, pass `display_server: :wayland` or `:x11` to require one display
29
+ server. On macOS, pass `gpu: :opengl` to use OpenGL instead of Metal.
30
+
31
+ Native backends require Ruby's Fiddle library and the platform graphics
32
+ libraries. Fiddle is not a default gem on Ruby 4, so applications using native
33
+ windows must install it. Linux file dialogs and URL opening use `zenity` and
34
+ `xdg-open` when available.
35
+
36
+ Native windows support input and IME events, resizing, display scale, clipboard,
37
+ file drops, fullscreen, cursors, URL opening, file dialogs, appearance changes,
38
+ and PNG capture. Platform availability differs; see [the RBS declarations](../sig/native.rbs)
39
+ and [platform declarations](../sig/platform.rbs) for the exact API.
40
+
41
+ ## Displays, file watching, and terminals
42
+
43
+ `Zaniah::Platform.displays` returns the available displays for a backend.
44
+ `Zaniah::Platform.watch(paths, latency:)` creates a native filesystem watcher;
45
+ call `poll(timeout:)` for events and `close` when finished. Overflow events mean
46
+ the application should rescan the watched paths.
47
+
48
+ On Windows, require `zaniah/platform/windows/terminal` to use
49
+ `Zaniah::Platform::Windows::Terminal`, which wraps ConPTY. It supports starting
50
+ a child process, nonblocking reads, writes, resizing, liveness checks, and
51
+ explicit cleanup. ConPTY requires Windows 10 version 1809 or later.
52
+
53
+ ## Development checks
54
+
55
+ Run checks on the matching host and display server:
56
+
57
+ ```sh
58
+ ruby examples/native_smoke.rb --check /tmp/zaniah.png
59
+ ruby examples/native_smoke.rb --gl --check /tmp/zaniah-gl.png
60
+ ruby examples/linux_smoke.rb
61
+ ruby examples/linux_smoke.rb --wayland
62
+ ruby examples/native_watch.rb
63
+ ```
64
+
65
+ Native input, IME, dialogs, and terminal integration also require manual checks
66
+ on the target operating system.
@@ -0,0 +1,67 @@
1
+ # CPU process workers
2
+
3
+ `ProcessPool` runs trusted CPU work in reusable Ruby child processes. For
4
+ portable spawning, put the handler in a loadable file and expose `.call(data)`:
5
+
6
+ ```ruby
7
+ # /absolute/path/word_count.rb
8
+ module WordCount
9
+ def self.call(data)
10
+ {"words" => data.fetch("text").split.length}
11
+ end
12
+ end
13
+ ```
14
+
15
+ ```ruby
16
+ require "zaniah"
17
+ require "zaniah/process_pool"
18
+
19
+ pool = Zaniah::ProcessPool.new(
20
+ workers: 2,
21
+ handler: "WordCount",
22
+ requires: [File.expand_path("word_count.rb")],
23
+ max_pending: 4
24
+ )
25
+
26
+ task = pool.submit("text" => "Hello Ruby")
27
+ result = task.await(timeout: 2)
28
+ pool.shutdown
29
+ ```
30
+
31
+ `requires:` accepts existing absolute Ruby filenames and `load_paths:` accepts
32
+ absolute directories. Workers start lazily, may keep process-local caches, and
33
+ do not guarantee that related requests use the same process. Failed workers are
34
+ replaced; lost work is not replayed.
35
+
36
+ ## Data and limits
37
+
38
+ Arguments and results must be plain JSON values: nil, booleans, integers, finite
39
+ floats, valid UTF-8 strings, arrays, and string-keyed hashes. Values are copied
40
+ at submission and received values are frozen. Custom objects, symbols, cycles,
41
+ excessive nesting, and invalid strings are rejected.
42
+
43
+ The default message limit is 16 MiB. `workers:` accepts 1–32 and `max_pending:`
44
+ defaults to twice the worker count. Submitting to a full or shut-down pool raises
45
+ `Zaniah::Error` immediately. Handlers are application code: the pool is not a
46
+ sandbox for untrusted Ruby.
47
+
48
+ ## Cancellation and cleanup
49
+
50
+ `Task#cancel` cancels queued work or terminates the child running that task. An
51
+ `await` timeout does not cancel work, so call `cancel` when abandoning a task.
52
+ Cancellation cannot undo external side effects already performed by a handler.
53
+
54
+ `shutdown(timeout: 2)` rejects new submissions, cancels outstanding tasks, and
55
+ cleans up workers. It is idempotent. If cleanup misses the deadline it raises
56
+ `Task::Timeout`, and shutdown may be retried.
57
+
58
+ The block form remains available on platforms with `fork`, but named handlers
59
+ are preferred for portability and for applications that own threads or native
60
+ resources. Both forms use the same JSON-only data contract.
61
+
62
+ See [the RBS declarations](../sig/app.rbs) for the complete API. Run the worker
63
+ tests with:
64
+
65
+ ```sh
66
+ ruby -Itest test/process_pool_test.rb
67
+ ```
data/docs/text.md ADDED
@@ -0,0 +1,90 @@
1
+ # Text system
2
+
3
+ `TextSystem::Renderer` provides layout, glyph rasterization, atlas management, and
4
+ painting. `TextSystem::Typesetter` provides layout and caret measurements without
5
+ allocating glyph textures.
6
+
7
+ ```ruby
8
+ db = Zaniah::TextSystem::FontDB.new
9
+ font = db.find(family: "Menlo", weight: 700, width: 5, style: :normal)
10
+ system = Zaniah::TextSystem::Renderer.new(font: font, font_db: db)
11
+ window.text_system = system
12
+ ```
13
+
14
+ `FontDB` discovers TTF, OTF, TTC, and OTC fonts in platform directories. It
15
+ selects faces by family, weight (`1..1000`), width (`1..9`), and style
16
+ (`:normal`, `:italic`, or `:oblique`). A bundled Abel font is the final fallback.
17
+ Call `refresh` to clear cached discovery results.
18
+
19
+ ## Providers and shaping
20
+
21
+ Defaults can be configured globally or supplied to a constructor:
22
+
23
+ ```ruby
24
+ Zaniah.configure do |config|
25
+ config.font_db = :native
26
+ config.shaper = :native
27
+ config.segmenter = :native
28
+ config.font_raster = :native
29
+ end
30
+ ```
31
+
32
+ Provider objects may be supplied instead of symbols:
33
+
34
+ | Setting | Required method | Result |
35
+ | --- | --- | --- |
36
+ | `font_db` | `find(...)`, `fallback(codepoint, primary_font)` | A font compatible with Alhena |
37
+ | `shaper` | `shape(glyphs, size:, text:)` | `Array[TextSystem::Glyph]` with UTF-8 byte clusters |
38
+ | `segmenter` | `grapheme_clusters(text)` | Strings that exactly partition the input |
39
+ | `font_raster` | `rasterize(font, glyph_id, size:, subpixel_x:)` | A glyph bitmap |
40
+
41
+ Providers may implement `close`; the text system closes them when it closes.
42
+ Custom mutable providers used by `fork` must also implement `layout_copy` and
43
+ return an independent provider.
44
+
45
+ The default Ruby shaper supports horizontal Latin, CJK, and kana text, including
46
+ common OpenType ligatures, contextual substitutions, pair positioning, and
47
+ legacy kerning. It is not a replacement for full complex-script shaping:
48
+ Arabic/Indic reordering, AAT `morx`, vertical text, complete mark positioning,
49
+ and variation-index positioning are unsupported.
50
+
51
+ Built-in rasterizers are `:native`/`:alhena`, `:freetype`, and `:coretext`
52
+ (`:core_text` is an alias). FreeType and CoreText require their platform library.
53
+ Unimplemented symbolic adapters raise `ArgumentError`; pass a provider object to
54
+ integrate another shaper, segmenter, font database, or rasterizer.
55
+
56
+ ## Background layout and startup cache
57
+
58
+ Create an independent layout-only typesetter for background work:
59
+
60
+ ```ruby
61
+ typesetter = system.fork(capacity: 32)
62
+ # use typesetter on one background worker
63
+ typesetter.close
64
+ ```
65
+
66
+ Do not mutate one text system or atlas concurrently. Font bytes may be shared,
67
+ but mutable providers and caches must be copied.
68
+
69
+ Disk-backed atlas caching is opt-in. Prewarm only after the window scale is known:
70
+
71
+ ```ruby
72
+ system = Zaniah::TextSystem::Renderer.new(cache_dir: "/path/to/app-cache/glyphs")
73
+ system.scale_factor = window.scale_factor
74
+ system.prewarm(size: 14)
75
+ window.text_system = system
76
+ ```
77
+
78
+ A missing, corrupt, or read-only cache does not prevent rendering. Applications
79
+ own cache-directory retention. See [the RBS declarations](../sig/text.rbs) for
80
+ the complete API.
81
+
82
+ ## Development checks
83
+
84
+ ```sh
85
+ BUDGET=1 ruby --yjit bench/text_frame.rb
86
+ ruby --yjit bench/atlas_startup.rb
87
+ ruby --yjit -Ilib script/shaper_oracle assets/fonts/Abel-Regular.ttf /path/to/font.ttf
88
+ ```
89
+
90
+ The shaping oracle is optional and requires `hb-shape`; it is not used at runtime.
@@ -0,0 +1,52 @@
1
+ # Vector icons and variable-height lists
2
+
3
+ `SVG` and `List` are normal elements and participate in layout, clipping, hit
4
+ testing, and scene painting.
5
+
6
+ ## Static SVG icons
7
+
8
+ ```ruby
9
+ icon = Zaniah::SVG.open("assets/search.svg", color: "#d8e4f0").w(24).h(24)
10
+ icon = Zaniah::SVG.parse(
11
+ '<svg viewBox="0 0 24 24"><path d="M4 12H20" stroke="currentColor"/></svg>'
12
+ )
13
+ ```
14
+
15
+ The renderer supports SVG paths, basic shapes, groups, transforms, view boxes,
16
+ inherited fill and stroke, `currentColor`, opacity, local `defs`/`use`, and
17
+ user-space clip paths. It is intended for static icons, not arbitrary web SVG.
18
+
19
+ Scripts, external references, CSS stylesheets, gradients, filters, masks, images,
20
+ text, markers, dash arrays, nested viewports, and object-bounding-box clips are
21
+ unsupported and raise `ArgumentError`. Input is bounded to 2 MiB, 10,000 XML
22
+ nodes, 64 levels, and 100,000 path operations; each output texture is limited to
23
+ one megapixel.
24
+
25
+ ## Variable-height lists
26
+
27
+ ```ruby
28
+ rows = Zaniah::List.new(count: 1_000_000, estimated_height: 24, overscan: 2) do |index|
29
+ Zaniah::Div.new
30
+ .h(index.even? ? 20 : 28)
31
+ .child(Zaniah::Text.new("Row #{index}"))
32
+ end.w(600).h(400)
33
+
34
+ rows.scroll_to(500_000, align: :center)
35
+ ```
36
+
37
+ Give the list a viewport height. Only visible rows and the overscan margin are
38
+ instantiated; other rows retain estimated heights until measured. Persistent row
39
+ state belongs in keyed window state or the application model.
40
+
41
+ `scroll_y=` changes the pixel offset. `scroll_to(index, align:)` accepts `:start`,
42
+ `:center`, `:end`, and `:nearest`. `update_height` supplies a measurement before
43
+ rendering, `visible_range` returns the instantiated exclusive-end range, and
44
+ `total_height` includes estimates. A list's item count is fixed; create a new
45
+ list when the count changes.
46
+
47
+ See [the element declarations](../sig/elements.rbs) for the complete API. Run:
48
+
49
+ ```sh
50
+ ruby -Ilib -Itest test/svg_and_list_test.rb
51
+ ruby --yjit -Ilib bench/list.rb
52
+ ```
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fiddle"
4
+ require "benchmark"
5
+
6
+ library = Fiddle.dlopen(RUBY_PLATFORM.match?(/mingw|mswin/) ? "msvcrt.dll" : nil)
7
+ count = Integer(ENV.fetch("N", "1000000"))
8
+ text = "a" * 16
9
+ pointer = Fiddle::Pointer[text]
10
+ [true, false].each do |gvl|
11
+ function = Fiddle::Function.new(library["strlen"], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_SIZE_T, need_gvl: gvl)
12
+ [text, pointer].each do |value|
13
+ elapsed = Benchmark.realtime { count.times { function.call(value) } }
14
+ puts "strlen gvl=#{gvl} #{value.class}: #{(elapsed * 1e6 / count).round(3)}us/call"
15
+ end
16
+ end