zaniah 0.1.0 → 0.2.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 +6 -0
- data/README.md +100 -34
- data/assets/logo.png +0 -0
- data/lib/zaniah/app.rb +4 -2
- data/lib/zaniah/element.rb +3 -1
- data/lib/zaniah/input/dispatcher.rb +6 -3
- data/lib/zaniah/input/keymap.rb +3 -3
- data/lib/zaniah/platform/headless/window.rb +23 -5
- data/lib/zaniah/task_executor.rb +5 -4
- data/lib/zaniah/text.rb +5 -1
- data/lib/zaniah/version.rb +1 -1
- data/lib/zaniah.rb +2 -0
- data/sig/app.rbs +3 -3
- data/sig/elements.rbs +6 -0
- data/sig/input.rbs +10 -2
- data/sig/platform.rbs +18 -7
- data/sig/zaniah.rbs +2 -0
- metadata +8 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e74156d5d7a3d4ed439e9d46aac5e8fb25b65e974013937dcc1743a5ae1aa665
|
|
4
|
+
data.tar.gz: ad0468ac4a87fc3181974ad2b905a515b3c414e30df4b190ba07b220c3ecfbc6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f65083c0483107b854879b2303b4bb87d4c05debcaba6167d7335596628d5ccab4d4ccf310d99fd2c10958a3edefa7355d9d9d0229541dc1bc6cba639c8ffc87
|
|
7
|
+
data.tar.gz: 5881db80886ae7543493bdaebc459301968075b1a41a375e93b0f91a1566829bd5c3169190ff342cfed7e4bd2dc2b573827e7331f2e920a22fd6e2c874d34242
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0 — 2026-09-11
|
|
4
|
+
|
|
5
|
+
- Add read-only element, hit-region, popup, and frame inspection APIs.
|
|
6
|
+
- Add injectable monotonic clocks and headless-window keymaps.
|
|
7
|
+
- Keep adjacent compact text rows separate in the TUI renderer.
|
|
8
|
+
|
|
3
9
|
## 0.1.0 — 2026-09-11
|
|
4
10
|
|
|
5
11
|
- Initial release.
|
data/README.md
CHANGED
|
@@ -1,29 +1,77 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/logo.png" alt="Zaniah logo">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
<strong>Ruby UI toolkit with native GPU windows, headless rendering, and a terminal backend</strong>
|
|
7
|
+
</p>
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://rubygems.org/gems/zaniah"><img src="https://img.shields.io/gem/v/zaniah.svg?colorB=319e8c" alt="Gem Version"></a>
|
|
11
|
+
<a href="https://rubygems.org/gems/zaniah"><img src="https://img.shields.io/gem/dt/zaniah.svg" alt="Downloads"></a>
|
|
12
|
+
<a href="https://github.com/noxdea/zaniah/actions/workflows/main.yml"><img src="https://github.com/noxdea/zaniah/actions/workflows/main.yml/badge.svg" alt="CI"></a>
|
|
13
|
+
<img src="https://img.shields.io/badge/Ruby-%3E%3D%203.1-CC342D.svg" alt="Ruby 3.1+">
|
|
14
|
+
<a href="LICENSE.txt"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="MIT License"></a>
|
|
15
|
+
</p>
|
|
9
16
|
|
|
10
|
-
|
|
17
|
+
<p align="center">
|
|
18
|
+
<a href="#features">Features</a> ·
|
|
19
|
+
<a href="#installation">Installation</a> ·
|
|
20
|
+
<a href="#quick-start">Quick Start</a> ·
|
|
21
|
+
<a href="#platforms">Platforms</a> ·
|
|
22
|
+
<a href="#documentation">Documentation</a>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
Zaniah is a pure Ruby UI toolkit for building desktop and terminal interfaces.
|
|
28
|
+
It renders through native GPU APIs, a deterministic headless backend, or an ANSI
|
|
29
|
+
terminal without extension compilation or a rendering subprocess.
|
|
30
|
+
|
|
31
|
+
The name comes from the white ground laid before paint or gold leaf: Zaniah is
|
|
32
|
+
the drawing surface beneath the editor.
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- Flex, constraint, absolute, wrapping, and baseline-aware layouts
|
|
37
|
+
- Rounded rectangles, images, static SVG, shadows, clipping, and glyph atlases
|
|
38
|
+
- Retained element state, subscriptions, and non-blocking background tasks
|
|
39
|
+
- Uniform and variable-height virtual lists
|
|
40
|
+
- OpenType ligatures and horizontal positioning with configurable text providers
|
|
41
|
+
- Native input, IME, clipboard, file-drop, display, and filesystem events
|
|
42
|
+
- RBS declarations for the public API
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
Add Zaniah to your Gemfile:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
gem "zaniah"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then install:
|
|
11
53
|
|
|
12
54
|
```sh
|
|
13
55
|
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
56
|
```
|
|
19
57
|
|
|
20
|
-
|
|
21
|
-
|
|
58
|
+
### Requirements
|
|
59
|
+
|
|
60
|
+
- CRuby 3.1 or later
|
|
61
|
+
- YJIT recommended
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
Create `hello.rb`:
|
|
22
66
|
|
|
23
67
|
```ruby
|
|
24
68
|
require "zaniah"
|
|
25
69
|
|
|
26
|
-
window = Zaniah::Platform.open_window(
|
|
70
|
+
window = Zaniah::Platform.open_window(
|
|
71
|
+
backend: :headless,
|
|
72
|
+
width: 480,
|
|
73
|
+
height: 240
|
|
74
|
+
)
|
|
27
75
|
window.text_system = Zaniah::TextSystem::Renderer.new
|
|
28
76
|
window.draw do
|
|
29
77
|
Zaniah::Div.new.flex_col.p(24).gap(12).bg("#161b22")
|
|
@@ -35,36 +83,54 @@ window.write_png("hello.png")
|
|
|
35
83
|
window.close
|
|
36
84
|
```
|
|
37
85
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
86
|
+
Run it to write `hello.png`:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
bundle exec ruby hello.rb
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
For an interactive window, choose `backend: :mac`, `:linux`, `:windows`, or
|
|
93
|
+
`:tui`, then call `window.run` instead of `window.tick`.
|
|
94
|
+
|
|
95
|
+
## Platforms
|
|
96
|
+
|
|
97
|
+
| Backend | Renderer | Notes |
|
|
98
|
+
| --- | --- | --- |
|
|
99
|
+
| `:mac` | Metal or OpenGL | Native macOS window |
|
|
100
|
+
| `:linux` | Wayland/EGL or X11/GLX | Selected from the current display environment |
|
|
101
|
+
| `:windows` | Win32/WGL | Requires 64-bit Ruby and Windows 10 APIs |
|
|
102
|
+
| `:headless` | Pure Ruby software renderer | Deterministic rendering and PNG output |
|
|
103
|
+
| `:tui` | ANSI terminal | Text-grid rendering and terminal input |
|
|
41
104
|
|
|
42
|
-
|
|
105
|
+
Native backends use the operating system libraries through Ruby's Fiddle. See
|
|
106
|
+
[Native backends](docs/native.md) for platform requirements and checks.
|
|
43
107
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
108
|
+
## Documentation
|
|
109
|
+
|
|
110
|
+
- [Native backends](docs/native.md) — windows, displays, file watching, and terminals
|
|
111
|
+
- [Text system](docs/text.md) — font discovery, shaping, rasterization, and caching
|
|
112
|
+
- [SVG and lists](docs/vector_and_list.md) — static vector icons and virtual lists
|
|
113
|
+
- [CPU process workers](docs/process_pool.md) — portable background CPU work
|
|
114
|
+
- [Architecture decisions](docs/adr) — design records and rationale
|
|
115
|
+
- [RBS declarations](sig) — public API signatures
|
|
52
116
|
|
|
53
117
|
## Development
|
|
54
118
|
|
|
55
119
|
```sh
|
|
120
|
+
bundle install
|
|
56
121
|
bundle exec rake test
|
|
57
|
-
|
|
58
|
-
|
|
122
|
+
bundle exec rake bench
|
|
123
|
+
gem build --strict zaniah.gemspec
|
|
59
124
|
```
|
|
60
125
|
|
|
61
|
-
`BUDGET=1`
|
|
126
|
+
Set `BUDGET=1` to enable benchmark assertions.
|
|
127
|
+
|
|
128
|
+
## Contributing
|
|
62
129
|
|
|
63
|
-
|
|
64
|
-
[
|
|
65
|
-
and [decisions](docs/adr).
|
|
130
|
+
Bug reports and pull requests are welcome at
|
|
131
|
+
[github.com/noxdea/zaniah](https://github.com/noxdea/zaniah).
|
|
66
132
|
|
|
67
133
|
## License
|
|
68
134
|
|
|
69
|
-
|
|
70
|
-
font files.
|
|
135
|
+
Zaniah is released under the [MIT License](LICENSE.txt). Included font notices
|
|
136
|
+
remain alongside the font files.
|
data/assets/logo.png
ADDED
|
Binary file
|
data/lib/zaniah/app.rb
CHANGED
|
@@ -9,10 +9,11 @@ module Zaniah
|
|
|
9
9
|
class App
|
|
10
10
|
attr_reader :windows, :executor
|
|
11
11
|
|
|
12
|
-
def initialize
|
|
12
|
+
def initialize(clock: MONOTONIC_CLOCK)
|
|
13
13
|
@slots, @generations, @free, @windows, @globals = [], [], [], [], {}
|
|
14
14
|
@listeners, @effects, @updating, @flushing = {}, [], 0, false
|
|
15
|
-
@
|
|
15
|
+
@clock = clock
|
|
16
|
+
@executor = TaskExecutor.new(clock: clock)
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def new_entity
|
|
@@ -103,6 +104,7 @@ module Zaniah
|
|
|
103
104
|
end
|
|
104
105
|
|
|
105
106
|
def open_window(**options, &render)
|
|
107
|
+
options[:clock] ||= @clock
|
|
106
108
|
window = Platform.open_window(**options)
|
|
107
109
|
@windows << window
|
|
108
110
|
window.draw(&render) if render
|
data/lib/zaniah/element.rb
CHANGED
|
@@ -28,6 +28,8 @@ module Zaniah
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def key(value) = (@key = value; self)
|
|
31
|
+
def test_id(value = (getter = true)) = getter ? @test_id : (@test_id = value.to_s.freeze; self)
|
|
32
|
+
def handlers = @handlers.keys.freeze
|
|
31
33
|
def with_state(&initial) = (@state_initializer = initial; self)
|
|
32
34
|
def bg(color) = (@background = color; self)
|
|
33
35
|
def border_color(color) = (@border_color = color; self)
|
|
@@ -62,7 +64,7 @@ module Zaniah
|
|
|
62
64
|
|
|
63
65
|
def prepaint(bounds, _state, cx)
|
|
64
66
|
unless @handlers.empty? && !@tooltip && !@context_menu
|
|
65
|
-
cx.dispatcher.hit(bounds) do |event|
|
|
67
|
+
cx.dispatcher.hit(bounds, owner: self) do |event|
|
|
66
68
|
if event.is_a?(Input::MouseDown) && event.button == :right && @context_menu
|
|
67
69
|
cx.window.context_menu(@context_menu, position: event.position)
|
|
68
70
|
next true
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Zaniah
|
|
4
4
|
module Input
|
|
5
|
+
Hit = Data.define(:bounds, :owner)
|
|
6
|
+
|
|
5
7
|
class Dispatcher
|
|
6
8
|
attr_reader :focused
|
|
7
9
|
|
|
@@ -26,10 +28,11 @@ module Zaniah
|
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
def clear_hits = @hits.clear
|
|
31
|
+
def hits = @hits.map { |bounds, _, owner| Hit.new(bounds, owner) }.freeze
|
|
29
32
|
|
|
30
|
-
def hit(bounds, clip: nil, &handler)
|
|
33
|
+
def hit(bounds, clip: nil, owner: nil, &handler)
|
|
31
34
|
bounds = bounds.intersect(@clip) if @clip
|
|
32
|
-
@hits << [clip ? bounds.intersect(clip) : bounds, handler]
|
|
35
|
+
@hits << [clip ? bounds.intersect(clip) : bounds, handler, owner]
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
def clip(bounds)
|
|
@@ -46,7 +49,7 @@ module Zaniah
|
|
|
46
49
|
@capture = nil if event.is_a?(MouseUp)
|
|
47
50
|
return true
|
|
48
51
|
end
|
|
49
|
-
@hits.reverse_each do |bounds, handler|
|
|
52
|
+
@hits.reverse_each do |bounds, handler, _owner|
|
|
50
53
|
next unless bounds.contains?(event.position)
|
|
51
54
|
handled = handler.call(event)
|
|
52
55
|
@capture = handler if handled == :capture && event.is_a?(MouseDown)
|
data/lib/zaniah/input/keymap.rb
CHANGED
|
@@ -7,8 +7,8 @@ require_relative "context_predicate"
|
|
|
7
7
|
module Zaniah
|
|
8
8
|
module Input
|
|
9
9
|
class Keymap
|
|
10
|
-
def initialize(timeout: 1.0)
|
|
11
|
-
@bindings, @pending, @last_time, @timeout = [], [], 0, timeout
|
|
10
|
+
def initialize(timeout: 1.0, clock: MONOTONIC_CLOCK)
|
|
11
|
+
@bindings, @pending, @last_time, @timeout, @clock = [], [], 0, timeout, clock
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def bind(keys, action, context: "")
|
|
@@ -23,7 +23,7 @@ module Zaniah
|
|
|
23
23
|
self
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def dispatch(key, context: {}, now:
|
|
26
|
+
def dispatch(key, context: {}, now: @clock.call)
|
|
27
27
|
@pending.clear if now - @last_time > @timeout
|
|
28
28
|
@last_time = now
|
|
29
29
|
key = Keystroke.normalize(key)
|
|
@@ -3,13 +3,21 @@
|
|
|
3
3
|
module Zaniah
|
|
4
4
|
module Platform
|
|
5
5
|
module Headless
|
|
6
|
+
Popup = Data.define(:labels, :enabled, :selected_index, :bounds)
|
|
7
|
+
|
|
6
8
|
class Window
|
|
9
|
+
DEFAULT_CLEAR = "#181b20"
|
|
10
|
+
|
|
7
11
|
attr_reader :content_size, :scene, :device, :dispatcher, :scale_factor, :text_runs
|
|
8
12
|
attr_accessor :text_system, :ime_state, :title
|
|
9
13
|
|
|
10
|
-
def initialize(width: 800, height: 600, title: "Zaniah UI", scale_factor: 1
|
|
14
|
+
def initialize(width: 800, height: 600, title: "Zaniah UI", scale_factor: 1,
|
|
15
|
+
keymap: nil, clock: MONOTONIC_CLOCK)
|
|
11
16
|
@content_size, @scale_factor, @title = Size.new(width, height), scale_factor, title
|
|
12
|
-
@scene
|
|
17
|
+
@scene = Scene.new
|
|
18
|
+
@device = GPU::Software.new(width, height)
|
|
19
|
+
@dispatcher = Input::Dispatcher.new(keymap: keymap || Input::Keymap.new(clock: clock))
|
|
20
|
+
@clock = clock
|
|
13
21
|
@state, @used_state, @text_runs = {}, {}, []
|
|
14
22
|
@dirty, @closed = true, false
|
|
15
23
|
end
|
|
@@ -19,12 +27,21 @@ module Zaniah
|
|
|
19
27
|
def on_close(&block) = @on_close = block
|
|
20
28
|
def on_moved(&block) = @on_moved = block
|
|
21
29
|
def on_tick(&block) = @on_tick = block
|
|
30
|
+
def on_frame(&block) = @on_frame = block
|
|
22
31
|
def draw(&block) = @draw = block
|
|
23
32
|
def request_frame = @dirty = true
|
|
24
33
|
def dirty? = @dirty
|
|
25
34
|
def closed? = @closed
|
|
26
35
|
def displays = [Display.new(0, "Headless", Bounds.new(0, 0, @content_size.width, @content_size.height), @scale_factor, true)]
|
|
27
36
|
|
|
37
|
+
def popup
|
|
38
|
+
return unless @menu
|
|
39
|
+
|
|
40
|
+
Popup.new(labels: @menu[:items].map { |label, _| label.dup.freeze }.freeze,
|
|
41
|
+
enabled: @menu[:items].map { |_, callback| !callback.nil? }.freeze,
|
|
42
|
+
selected_index: @menu[:index], bounds: popup_bounds(@menu))
|
|
43
|
+
end
|
|
44
|
+
|
|
28
45
|
def resize(width, height)
|
|
29
46
|
@content_size = Size.new(width, height)
|
|
30
47
|
@device.resize(width, height)
|
|
@@ -49,7 +66,7 @@ module Zaniah
|
|
|
49
66
|
def offer_tooltip(text, position:, delay: 0.5)
|
|
50
67
|
@tooltip_offered = true
|
|
51
68
|
return if @tooltip && @tooltip[:text] == text
|
|
52
|
-
@tooltip = {text: text.to_s, position: position, at:
|
|
69
|
+
@tooltip = {text: text.to_s, position: position, at: @clock.call + delay, shown: false}
|
|
53
70
|
end
|
|
54
71
|
|
|
55
72
|
def context_menu(items, position: Point.new(0, 0))
|
|
@@ -70,7 +87,7 @@ module Zaniah
|
|
|
70
87
|
|
|
71
88
|
def tick
|
|
72
89
|
@on_tick&.call
|
|
73
|
-
if @tooltip && !@tooltip[:shown] &&
|
|
90
|
+
if @tooltip && !@tooltip[:shown] && @clock.call >= @tooltip[:at]
|
|
74
91
|
@tooltip[:shown] = true
|
|
75
92
|
request_frame
|
|
76
93
|
end
|
|
@@ -81,7 +98,7 @@ module Zaniah
|
|
|
81
98
|
@device.render(result) if result.is_a?(Scene)
|
|
82
99
|
end
|
|
83
100
|
|
|
84
|
-
def render(element, clear:
|
|
101
|
+
def render(element, clear: DEFAULT_CLEAR, present: true)
|
|
85
102
|
@text_system.scale_factor = @scale_factor if @text_system.respond_to?(:scale_factor=)
|
|
86
103
|
@scene.clear
|
|
87
104
|
@text_runs.clear
|
|
@@ -95,6 +112,7 @@ module Zaniah
|
|
|
95
112
|
element.paint(root.bounds, nil, nil, cx)
|
|
96
113
|
paint_popups
|
|
97
114
|
@state.delete_if { |key, _| !@used_state[key] }
|
|
115
|
+
@on_frame&.call(element, clear)
|
|
98
116
|
@device.render(@scene, clear: clear) if present
|
|
99
117
|
@text_system.end_frame if @text_system.respond_to?(:end_frame)
|
|
100
118
|
end
|
data/lib/zaniah/task_executor.rb
CHANGED
|
@@ -7,9 +7,10 @@ module Zaniah
|
|
|
7
7
|
class TaskExecutor
|
|
8
8
|
def self.current = Thread.current[:zaniah_executor]
|
|
9
9
|
|
|
10
|
-
def initialize(workers: 2)
|
|
10
|
+
def initialize(workers: 2, clock: MONOTONIC_CLOCK)
|
|
11
11
|
@jobs, @foreground = Queue.new, Queue.new
|
|
12
12
|
@wake, @condition = Mutex.new, ConditionVariable.new
|
|
13
|
+
@clock = clock
|
|
13
14
|
@timers = []
|
|
14
15
|
@threads = Array.new(workers) do
|
|
15
16
|
Thread.new do
|
|
@@ -63,7 +64,7 @@ module Zaniah
|
|
|
63
64
|
cancellation = owner.on_complete { post { subscription.detach; @timers.delete(deadline) if deadline } }
|
|
64
65
|
if timeout
|
|
65
66
|
raise ArgumentError, "timeout must be nonnegative" unless timeout.is_a?(Numeric) && timeout >= 0
|
|
66
|
-
deadline = [
|
|
67
|
+
deadline = [@clock.call + timeout, -> { finish.call(Task::Timeout.new("task timed out")) }]
|
|
67
68
|
@timers << deadline
|
|
68
69
|
end
|
|
69
70
|
Fiber.yield
|
|
@@ -81,7 +82,7 @@ module Zaniah
|
|
|
81
82
|
end
|
|
82
83
|
|
|
83
84
|
def drain
|
|
84
|
-
now =
|
|
85
|
+
now = @clock.call
|
|
85
86
|
due, @timers = @timers.partition { |deadline, _| deadline <= now }
|
|
86
87
|
due.each { |_, callback| callback.call }
|
|
87
88
|
loop { @foreground.pop(true).call }
|
|
@@ -91,7 +92,7 @@ module Zaniah
|
|
|
91
92
|
|
|
92
93
|
def wait(seconds)
|
|
93
94
|
deadline = @timers.map(&:first).min
|
|
94
|
-
seconds = [seconds, [deadline -
|
|
95
|
+
seconds = [seconds, [deadline - @clock.call, 0].max].min if deadline
|
|
95
96
|
@wake.synchronize { @condition.wait(@wake, seconds) if @foreground.empty? }
|
|
96
97
|
end
|
|
97
98
|
|
data/lib/zaniah/text.rb
CHANGED
|
@@ -2,16 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
module Zaniah
|
|
4
4
|
class Text < Element
|
|
5
|
+
attr_reader :text, :font_size
|
|
6
|
+
|
|
5
7
|
def initialize(text, size: 14, color: "#ddd", font: nil)
|
|
6
8
|
super()
|
|
7
9
|
@text, @font_size, @color, @font = text, size, color, font
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
def measured(&block) = (@measure = block; self)
|
|
13
|
+
def text_color = @color
|
|
11
14
|
|
|
12
15
|
def request_layout(cx)
|
|
13
16
|
@line = cx.text_system&.layout_line(@text, font: @font, size: @font_size)
|
|
14
|
-
|
|
17
|
+
line_height = @line ? @line.ascent + @line.descent : 0
|
|
18
|
+
measurement = @measure || ->(_width, _height) { [@line ? @line.width : @text.length * @font_size * 0.6, [@font_size * 1.4, line_height].max, @font_size] }
|
|
15
19
|
@layout_node = Layout::Node.new(style: @style, measure: measurement)
|
|
16
20
|
end
|
|
17
21
|
|
data/lib/zaniah/version.rb
CHANGED
data/lib/zaniah.rb
CHANGED
|
@@ -6,6 +6,8 @@ require_relative "zaniah/error"
|
|
|
6
6
|
require_relative "zaniah/configuration"
|
|
7
7
|
|
|
8
8
|
module Zaniah
|
|
9
|
+
MONOTONIC_CLOCK = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
10
|
+
|
|
9
11
|
def self.configuration
|
|
10
12
|
@configuration ||= Configuration.new(font_raster: :native, shaper: :native,
|
|
11
13
|
font_db: :native, segmenter: :native)
|
data/sig/app.rbs
CHANGED
|
@@ -30,7 +30,7 @@ module Zaniah
|
|
|
30
30
|
class App
|
|
31
31
|
attr_reader windows: Array[Platform::Headless::Window]
|
|
32
32
|
attr_reader executor: TaskExecutor
|
|
33
|
-
def initialize: () -> void
|
|
33
|
+
def initialize: (?clock: clock) -> void
|
|
34
34
|
def new_entity: [T] () { (EntityContext[T]) -> T } -> Entity[T]
|
|
35
35
|
def read: [T] (Entity[T] entity) -> T
|
|
36
36
|
def update: [T, U] (Entity[T] entity) { (T, EntityContext[T]) -> U } -> U
|
|
@@ -43,7 +43,7 @@ module Zaniah
|
|
|
43
43
|
def listen: [T] (:notify | :emit kind, Entity[T] target, ?owner: Entity[untyped]?) { (*untyped) -> void } -> Subscription
|
|
44
44
|
def enqueue: (:notify | :emit kind, Entity[untyped] entity, ?untyped event) -> void
|
|
45
45
|
def flush_effects: () -> void
|
|
46
|
-
def open_window: (?backend: Platform::backend, ?width: Numeric, ?height: Numeric, ?title: String, ?scale_factor: Numeric, ?input: IO, ?output: IO, ?gpu: :metal | :opengl | :gl, ?display_server: :auto | :x11 | :wayland) ?{ (Platform::Headless::Window) -> (Element | Scene | nil) } -> Platform::Headless::Window
|
|
46
|
+
def open_window: (?backend: Platform::backend, ?width: Numeric, ?height: Numeric, ?title: String, ?scale_factor: Numeric, ?input: IO, ?output: IO, ?gpu: :metal | :opengl | :gl, ?display_server: :auto | :x11 | :wayland, ?keymap: Input::Keymap?, ?clock: clock) ?{ (Platform::Headless::Window) -> (Element | Scene | nil) } -> Platform::Headless::Window
|
|
47
47
|
def run: () -> void
|
|
48
48
|
end
|
|
49
49
|
|
|
@@ -62,7 +62,7 @@ module Zaniah
|
|
|
62
62
|
end
|
|
63
63
|
class TaskExecutor
|
|
64
64
|
def self.current: () -> TaskExecutor?
|
|
65
|
-
def initialize: (?workers: Integer) -> void
|
|
65
|
+
def initialize: (?workers: Integer, ?clock: clock) -> void
|
|
66
66
|
def background: [T] () { () -> T } -> Task[T]
|
|
67
67
|
def spawn: [T] () { () -> T } -> Task[T]
|
|
68
68
|
def await: [T] (Task[T] task, ?timeout: Numeric?) -> T
|
data/sig/elements.rbs
CHANGED
|
@@ -18,6 +18,9 @@ module Zaniah
|
|
|
18
18
|
| (Array[Element?] elements) -> self
|
|
19
19
|
def style: (**Layout::style_value properties) -> self
|
|
20
20
|
def key: (Object value) -> self
|
|
21
|
+
def test_id: () -> String?
|
|
22
|
+
| (String value) -> self
|
|
23
|
+
def handlers: () -> Array[Symbol]
|
|
21
24
|
def with_state: [T] () { () -> T } -> self
|
|
22
25
|
def bg: (color color) -> self
|
|
23
26
|
def border_color: (color color) -> self
|
|
@@ -57,6 +60,9 @@ module Zaniah
|
|
|
57
60
|
class Div < Element
|
|
58
61
|
end
|
|
59
62
|
class Text < Element
|
|
63
|
+
attr_reader text: String
|
|
64
|
+
attr_reader font_size: Numeric
|
|
65
|
+
attr_reader text_color: color
|
|
60
66
|
def initialize: (String text, ?size: Numeric, ?color: color, ?font: TextSystem::_Font?) -> void
|
|
61
67
|
def measured: () { (Numeric width, Numeric height) -> Layout::measurement } -> self
|
|
62
68
|
end
|
data/sig/input.rbs
CHANGED
|
@@ -7,6 +7,13 @@ module Zaniah
|
|
|
7
7
|
type event = KeyDown | KeyUp | TextInput | Composition | mouse_event | FileDrop
|
|
8
8
|
type mouse_event = MouseDown | MouseUp | MouseMove | ScrollWheel
|
|
9
9
|
|
|
10
|
+
class Hit < Data
|
|
11
|
+
attr_reader bounds: Bounds
|
|
12
|
+
attr_reader owner: Element?
|
|
13
|
+
def self.new: (Bounds bounds, Element? owner) -> Hit
|
|
14
|
+
| (bounds: Bounds, owner: Element?) -> Hit
|
|
15
|
+
end
|
|
16
|
+
|
|
10
17
|
class KeyDown < Data
|
|
11
18
|
attr_reader keystroke: String
|
|
12
19
|
attr_reader is_held: bool
|
|
@@ -79,7 +86,7 @@ module Zaniah
|
|
|
79
86
|
def self.new: (Array[String] keys, ContextPredicate predicate, action action) -> Binding
|
|
80
87
|
| (keys: Array[String], predicate: ContextPredicate, action: action) -> Binding
|
|
81
88
|
end
|
|
82
|
-
def initialize: (?timeout: Numeric) -> void
|
|
89
|
+
def initialize: (?timeout: Numeric, ?clock: clock) -> void
|
|
83
90
|
def bind: (String keys, action action, ?context: String) -> self
|
|
84
91
|
def load_json: (String text) -> self
|
|
85
92
|
def dispatch: (String key, ?context: context, ?now: Numeric) -> (action | :pending | nil)
|
|
@@ -100,7 +107,8 @@ module Zaniah
|
|
|
100
107
|
def focus: (FocusHandle? handle) -> void
|
|
101
108
|
def key: (String key) -> (action | :pending | nil)
|
|
102
109
|
def clear_hits: () -> void
|
|
103
|
-
def
|
|
110
|
+
def hits: () -> Array[Hit]
|
|
111
|
+
def hit: (Bounds bounds, ?clip: Bounds?, ?owner: Element?) { (mouse_event | FileDrop) -> (bool | :capture | nil) } -> void
|
|
104
112
|
def clip: [T] (Bounds bounds) { () -> T } -> T
|
|
105
113
|
def mouse: (mouse_event | FileDrop event) -> bool
|
|
106
114
|
end
|
data/sig/platform.rbs
CHANGED
|
@@ -15,11 +15,11 @@ module Zaniah
|
|
|
15
15
|
end
|
|
16
16
|
def self.displays: (?backend: backend | :auto, ?display_server: :auto | :x11 | :wayland) -> Array[Display]
|
|
17
17
|
|
|
18
|
-
def self.open_window: (?backend: :headless, ?width: Numeric, ?height: Numeric, ?title: String, ?scale_factor: Numeric) -> Headless::Window
|
|
19
|
-
| (backend: :tui, ?input: IO, ?output: IO, ?width: Numeric, ?height: Numeric, ?title: String, ?scale_factor: Numeric) -> TUI::Window
|
|
20
|
-
| (backend: :mac, ?gpu: :metal | :opengl | :gl, ?width: Numeric, ?height: Numeric, ?title: String, ?scale_factor: Numeric) -> Mac::Window
|
|
21
|
-
| (backend: :linux, ?display_server: :auto | :x11 | :wayland, ?gpu: :opengl | :gl, ?width: Numeric, ?height: Numeric, ?title: String, ?scale_factor: Numeric) -> (Linux::Window | Linux::WaylandWindow)
|
|
22
|
-
| (backend: :windows, ?gpu: :opengl | :gl, ?width: Numeric, ?height: Numeric, ?title: String, ?scale_factor: Numeric) -> Windows::Window
|
|
18
|
+
def self.open_window: (?backend: :headless, ?width: Numeric, ?height: Numeric, ?title: String, ?scale_factor: Numeric, ?keymap: Input::Keymap?, ?clock: clock) -> Headless::Window
|
|
19
|
+
| (backend: :tui, ?input: IO, ?output: IO, ?width: Numeric, ?height: Numeric, ?title: String, ?scale_factor: Numeric, ?keymap: Input::Keymap?, ?clock: clock) -> TUI::Window
|
|
20
|
+
| (backend: :mac, ?gpu: :metal | :opengl | :gl, ?width: Numeric, ?height: Numeric, ?title: String, ?scale_factor: Numeric, ?keymap: Input::Keymap?, ?clock: clock) -> Mac::Window
|
|
21
|
+
| (backend: :linux, ?display_server: :auto | :x11 | :wayland, ?gpu: :opengl | :gl, ?width: Numeric, ?height: Numeric, ?title: String, ?scale_factor: Numeric, ?keymap: Input::Keymap?, ?clock: clock) -> (Linux::Window | Linux::WaylandWindow)
|
|
22
|
+
| (backend: :windows, ?gpu: :opengl | :gl, ?width: Numeric, ?height: Numeric, ?title: String, ?scale_factor: Numeric, ?keymap: Input::Keymap?, ?clock: clock) -> Windows::Window
|
|
23
23
|
|
|
24
24
|
interface _Watcher
|
|
25
25
|
def poll: (?timeout: Numeric) -> Array[FileEvent]
|
|
@@ -35,7 +35,16 @@ module Zaniah
|
|
|
35
35
|
def self.watch: (String | Array[String] paths, ?latency: Numeric) -> _Watcher
|
|
36
36
|
|
|
37
37
|
module Headless
|
|
38
|
+
class Popup < Data
|
|
39
|
+
attr_reader labels: Array[String]
|
|
40
|
+
attr_reader enabled: Array[bool]
|
|
41
|
+
attr_reader selected_index: Integer
|
|
42
|
+
attr_reader bounds: Bounds
|
|
43
|
+
def self.new: (Array[String] labels, Array[bool] enabled, Integer selected_index, Bounds bounds) -> Popup
|
|
44
|
+
| (labels: Array[String], enabled: Array[bool], selected_index: Integer, bounds: Bounds) -> Popup
|
|
45
|
+
end
|
|
38
46
|
class Window
|
|
47
|
+
DEFAULT_CLEAR: String
|
|
39
48
|
attr_reader content_size: Size
|
|
40
49
|
attr_reader scene: Scene
|
|
41
50
|
attr_reader device: GPU::_Device
|
|
@@ -45,17 +54,19 @@ module Zaniah
|
|
|
45
54
|
attr_accessor text_system: TextSystem::_System?
|
|
46
55
|
attr_accessor ime_state: Bounds?
|
|
47
56
|
attr_accessor title: String
|
|
48
|
-
def initialize: (?width: Numeric, ?height: Numeric, ?title: String, ?scale_factor: Numeric) -> void
|
|
57
|
+
def initialize: (?width: Numeric, ?height: Numeric, ?title: String, ?scale_factor: Numeric, ?keymap: Input::Keymap?, ?clock: clock) -> void
|
|
49
58
|
def on_input: () { (Input::event) -> void } -> void
|
|
50
59
|
def on_resize: () { (Size) -> void } -> void
|
|
51
60
|
def on_close: () { () -> (bool | nil) } -> void
|
|
52
61
|
def on_moved: () { ([Numeric, Numeric]) -> void } -> void
|
|
53
62
|
def on_tick: () { () -> void } -> void
|
|
63
|
+
def on_frame: () { (Element, color) -> void } -> void
|
|
54
64
|
def draw: () { (Window) -> (Element | Scene | nil) } -> void
|
|
55
65
|
def request_frame: () -> bool
|
|
56
66
|
def dirty?: () -> bool
|
|
57
67
|
def closed?: () -> bool
|
|
58
68
|
def displays: () -> Array[Display]
|
|
69
|
+
def popup: () -> Popup?
|
|
59
70
|
def resize: (Numeric width, Numeric height) -> void
|
|
60
71
|
def input: (Input::event event) -> void
|
|
61
72
|
def offer_tooltip: (String text, position: Point, ?delay: Numeric) -> void
|
|
@@ -75,7 +86,7 @@ module Zaniah
|
|
|
75
86
|
def flush_escape: () -> bool
|
|
76
87
|
end
|
|
77
88
|
class Window < Headless::Window
|
|
78
|
-
def initialize: (?input: IO, ?output: IO, ?width: Numeric, ?height: Numeric, ?title: String, ?scale_factor: Numeric) -> void
|
|
89
|
+
def initialize: (?input: IO, ?output: IO, ?width: Numeric, ?height: Numeric, ?title: String, ?scale_factor: Numeric, ?keymap: Input::Keymap?, ?clock: clock) -> void
|
|
79
90
|
def run: () -> void
|
|
80
91
|
def feed_input: (String bytes) -> void
|
|
81
92
|
end
|
data/sig/zaniah.rbs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module Zaniah
|
|
2
2
|
VERSION: String
|
|
3
|
+
MONOTONIC_CLOCK: clock
|
|
3
4
|
type color = Color | String | [Numeric, Numeric, Numeric] | [Numeric, Numeric, Numeric, Numeric]
|
|
5
|
+
type clock = ^() -> Numeric
|
|
4
6
|
type dimension = Numeric | Length | :auto
|
|
5
7
|
type radius = Numeric | [Numeric, Numeric, Numeric, Numeric]
|
|
6
8
|
type menu = Array[[String, (^( ) -> void)?]]
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zaniah
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ydah
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-09-11 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: alhena
|
|
@@ -51,6 +52,7 @@ dependencies:
|
|
|
51
52
|
- - "~>"
|
|
52
53
|
- !ruby/object:Gem::Version
|
|
53
54
|
version: '3.2'
|
|
55
|
+
description:
|
|
54
56
|
email:
|
|
55
57
|
- t.yudai92@gmail.com
|
|
56
58
|
executables: []
|
|
@@ -62,6 +64,7 @@ files:
|
|
|
62
64
|
- README.md
|
|
63
65
|
- assets/fonts/Abel-Regular.ttf
|
|
64
66
|
- assets/fonts/OFL.txt
|
|
67
|
+
- assets/logo.png
|
|
65
68
|
- docs/adr/000-template.md
|
|
66
69
|
- docs/adr/001-native-gpu-backends.md
|
|
67
70
|
- docs/adr/002-objective-c-aggregates.md
|
|
@@ -229,6 +232,7 @@ metadata:
|
|
|
229
232
|
changelog_uri: https://github.com/noxdea/zaniah/blob/main/CHANGELOG.md
|
|
230
233
|
allowed_push_host: https://rubygems.org
|
|
231
234
|
rubygems_mfa_required: 'true'
|
|
235
|
+
post_install_message:
|
|
232
236
|
rdoc_options: []
|
|
233
237
|
require_paths:
|
|
234
238
|
- lib
|
|
@@ -243,7 +247,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
243
247
|
- !ruby/object:Gem::Version
|
|
244
248
|
version: '0'
|
|
245
249
|
requirements: []
|
|
246
|
-
rubygems_version: 4.
|
|
250
|
+
rubygems_version: 3.4.19
|
|
251
|
+
signing_key:
|
|
247
252
|
specification_version: 4
|
|
248
253
|
summary: A pure Ruby UI framework with native and headless rendering
|
|
249
254
|
test_files: []
|