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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83f0e9d7ba2790764f55bc1e0eda5e21e31b22a54cd0a92668b24df9000d351c
4
- data.tar.gz: 37a96934f06671e69989c1e2e91e30917e0b6902458a09fc05fd5ef3999030f3
3
+ metadata.gz: e74156d5d7a3d4ed439e9d46aac5e8fb25b65e974013937dcc1743a5ae1aa665
4
+ data.tar.gz: ad0468ac4a87fc3181974ad2b905a515b3c414e30df4b190ba07b220c3ecfbc6
5
5
  SHA512:
6
- metadata.gz: 3feda2d8ba35caa3b73821eb9cae80c64d8b549e87c125dd6f55c4382d9789451bd91e0ae2e019709e1f923b662b498265085c6c0fcd268dad3a94ce7ab6c4c3
7
- data.tar.gz: ef1e4f76251e22c1a8317677cccc3b80885aec8f3bb917d09df7a363a9dbfb6976ed19e38565a75eb5bf656b2a2f3276daada27156d79eb250d6e734ff1ad62b
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
- # Zaniah
1
+ <p align="center">
2
+ <img src="assets/logo.png" alt="Zaniah logo">
3
+ </p>
2
4
 
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.
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
- The name comes from the white ground laid before paint or gold leaf: this
8
- toolkit is the drawing surface beneath the editor.
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
- ## Try it locally
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
- The gem name is not published or reserved. Install the locally built artifact
21
- rather than an unverified public gem.
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(backend: :headless, width: 480, height: 240)
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
- 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.
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
- ## Toolkit and platforms
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
- 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.
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
- ruby --yjit bench/instances.rb
58
- ruby --yjit bench/list.rb
122
+ bundle exec rake bench
123
+ gem build --strict zaniah.gemspec
59
124
  ```
60
125
 
61
- `BUDGET=1` enables benchmark assertions.
126
+ Set `BUDGET=1` to enable benchmark assertions.
127
+
128
+ ## Contributing
62
129
 
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).
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
- MIT; see [LICENSE.txt](LICENSE.txt). Included font notices remain alongside the
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
- @executor = TaskExecutor.new
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
@@ -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)
@@ -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: Process.clock_gettime(Process::CLOCK_MONOTONIC))
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, @device, @dispatcher = Scene.new, GPU::Software.new(width, height), Input::Dispatcher.new
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: Process.clock_gettime(Process::CLOCK_MONOTONIC) + delay, shown: false}
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] && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= @tooltip[:at]
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: "#181b20", present: true)
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
@@ -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 = [Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout, -> { finish.call(Task::Timeout.new("task timed out")) }]
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 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
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 - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max].min if 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
- measurement = @measure || ->(_width, _height) { [@line ? @line.width : @text.length * @font_size * 0.6, @font_size * 1.4, @font_size] }
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zaniah
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
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 hit: (Bounds bounds, ?clip: Bounds?) { (mouse_event | FileDrop) -> (bool | :capture | nil) } -> void
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.1.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: 1980-01-02 00:00:00.000000000 Z
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.0.19
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: []