zaniah 0.6.2 → 0.6.3
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 +9 -0
- data/README.md +9 -0
- data/docs/adr/012-productivity-components.md +25 -0
- data/docs/components.md +16 -1
- data/docs/images.md +18 -0
- data/docs/layout.md +46 -0
- data/docs/native.md +9 -0
- data/docs/text.md +22 -0
- data/lib/zaniah/image.rb +22 -5
- data/lib/zaniah/input/keymap.rb +12 -0
- data/lib/zaniah/jpeg.rb +431 -0
- data/lib/zaniah/list/height_index.rb +3 -2
- data/lib/zaniah/platform/headless/window.rb +8 -0
- data/lib/zaniah/platform/linux/wayland_window.rb +14 -0
- data/lib/zaniah/platform/linux/window.rb +12 -0
- data/lib/zaniah/platform/mac/window.rb +15 -0
- data/lib/zaniah/platform/windows/window.rb +15 -0
- data/lib/zaniah/ui/charts.rb +299 -0
- data/lib/zaniah/ui/editors.rb +303 -13
- data/lib/zaniah/ui/grid.rb +492 -0
- data/lib/zaniah/ui/rich_text_surface.rb +302 -0
- data/lib/zaniah/ui/terminal_view.rb +22 -0
- data/lib/zaniah/ui.rb +2 -0
- data/lib/zaniah/version.rb +1 -1
- data/lib/zaniah.rb +1 -0
- data/sig/elements.rbs +2 -0
- data/sig/native.rbs +7 -0
- data/sig/platform.rbs +2 -0
- data/sig/ui.rbs +67 -2
- data/sig/zaniah.rbs +14 -0
- metadata +15 -4
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
module UI
|
|
5
|
+
class RichTextSurface < Element
|
|
6
|
+
Run = Data.define(:start, :finish, :layout, :x, :style, :color, :size)
|
|
7
|
+
Line = Data.define(:start, :finish, :x, :y, :width, :height, :ascent, :runs, :paragraph, :first_in_paragraph)
|
|
8
|
+
|
|
9
|
+
def initialize(owner)
|
|
10
|
+
super()
|
|
11
|
+
@owner = owner
|
|
12
|
+
@layout_cache = {}
|
|
13
|
+
return unless owner.instance_variable_get(:@selectable) || owner.editable?
|
|
14
|
+
|
|
15
|
+
if owner.instance_variable_get(:@selectable)
|
|
16
|
+
on_mouse_down { |event, _cx| begin_selection(event) }
|
|
17
|
+
on_drag { |event, _cx| extend_selection(event) }
|
|
18
|
+
end
|
|
19
|
+
focusable(context: owner.editable? ? {in_text_field: true} : {in_text: true}) do |action|
|
|
20
|
+
owner.editable? ? owner.text_action(action) : action == :select_all && select_all
|
|
21
|
+
end
|
|
22
|
+
@focus_handle.on_input = ->(event) { owner.input(event) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def request_layout(cx)
|
|
26
|
+
@cx = cx
|
|
27
|
+
@display_text = @owner.display_text
|
|
28
|
+
@visual_spans = display_spans
|
|
29
|
+
width = resolved_width(@style[:width], cx.window.content_size.width)
|
|
30
|
+
@font_size = @style[:font_size] || cx.theme.typography.size_md
|
|
31
|
+
@lines = layout_lines(width, cx)
|
|
32
|
+
@layout_node = Layout::Node.new(style: @style, measure: lambda do |available, _height|
|
|
33
|
+
target = @style[:width].is_a?(Numeric) ? width : [available, 0].max
|
|
34
|
+
@lines = layout_lines(target, cx) if target != width
|
|
35
|
+
[@lines.map { |line| line.x + line.width }.max || 0, @lines.last ? @lines.last.y + @lines.last.height : line_height]
|
|
36
|
+
end)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def prepaint(bounds, state, cx)
|
|
40
|
+
@bounds = bounds
|
|
41
|
+
super
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def paint(bounds, state, prepaint, cx)
|
|
45
|
+
super
|
|
46
|
+
selection = @owner.selection.range
|
|
47
|
+
paint_selection(bounds, selection, cx) if @owner.instance_variable_get(:@selectable) && !selection.begin.eql?(selection.end)
|
|
48
|
+
@lines.each do |line|
|
|
49
|
+
line.runs.each do |run|
|
|
50
|
+
next unless run.layout && cx.text_system
|
|
51
|
+
cx.text_system.paint_line(cx.scene, run.layout, x: bounds.x + line.x + run.x,
|
|
52
|
+
y: bounds.y + line.y + line.ascent, color: run.color)
|
|
53
|
+
if run.style[:link]
|
|
54
|
+
cx.scene.underline(bounds.x + line.x + run.x, bounds.y + line.y + line.ascent + run.size,
|
|
55
|
+
[run.layout.width, 1].max, color: run.color)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
paint_list_marker(bounds, line, cx)
|
|
59
|
+
end
|
|
60
|
+
paint_composition(bounds, cx) if @owner.buffer.composition
|
|
61
|
+
paint_caret(bounds, cx) if @owner.editable? && @owner.selection.collapsed? && cx.dispatcher.focused == @focus_handle
|
|
62
|
+
cx.window.text_runs.concat(@lines.flat_map do |line|
|
|
63
|
+
line.runs.map { |run| [bounds.x + line.x + run.x, bounds.y + line.y, @display_text.byteslice(run.start...run.finish), run.color] }
|
|
64
|
+
end) if cx.window.respond_to?(:text_runs)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def hit_test(point)
|
|
68
|
+
line = @lines.find { |item| point.y < item.y + item.height } || @lines.last
|
|
69
|
+
return 0 unless line
|
|
70
|
+
run = line.runs.find { |item| point.x < line.x + item.x + item.layout&.width.to_f } || line.runs.last
|
|
71
|
+
return line.start unless run
|
|
72
|
+
run.start + (run.layout ? run.layout.index_for_x(point.x - line.x - run.x) : 0).clamp(0, run.finish - run.start)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def resolved_width(value, available)
|
|
78
|
+
value = value.resolve(available) if value.is_a?(Length)
|
|
79
|
+
value.is_a?(Numeric) ? [value, 0].max : available
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def display_spans
|
|
83
|
+
spans = @owner.spans.map { |span| [span.start, span.finish, span.style] }
|
|
84
|
+
composition = @owner.buffer.composition
|
|
85
|
+
return spans unless composition
|
|
86
|
+
range = @owner.selection.range
|
|
87
|
+
at, delta = range.begin, composition.text.bytesize - range.size
|
|
88
|
+
style = @owner.style_at(at)
|
|
89
|
+
spans = spans.flat_map do |first, finish, value|
|
|
90
|
+
if finish <= range.begin
|
|
91
|
+
[[first, finish, value]]
|
|
92
|
+
elsif first >= range.end
|
|
93
|
+
[[first + delta, finish + delta, value]]
|
|
94
|
+
else
|
|
95
|
+
pieces = []
|
|
96
|
+
pieces << [first, range.begin, value] if first < range.begin
|
|
97
|
+
pieces << [range.begin + composition.text.bytesize, finish + delta, value] if finish > range.end
|
|
98
|
+
pieces
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
spans << [at, at + composition.text.bytesize, style] unless style.empty?
|
|
102
|
+
spans.sort_by(&:first)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# ponytail: each candidate prefix is shaped during line breaking; long unbroken paragraphs are quadratic. Cache wins for repeated fragments; add a streaming breaker if profiling proves this hot.
|
|
106
|
+
def layout_lines(width, cx)
|
|
107
|
+
breaker = TextSystem::LineBreaker.new(@display_text, wrap: :word, kinsoku: :push)
|
|
108
|
+
ranges = breaker.ranges_with_offsets(width) do |_candidate, first, finish|
|
|
109
|
+
fragments(first, finish).sum { |from, to, style| fragment_layout(from, to, style, cx).width }
|
|
110
|
+
end
|
|
111
|
+
y = 0.0
|
|
112
|
+
ranges.each_with_index.map do |range, index|
|
|
113
|
+
paragraph_index = @display_text.byteslice(0...range.begin).count("\n")
|
|
114
|
+
para = @owner.paragraph_style_at([range.begin, @owner.text.bytesize].min)
|
|
115
|
+
indent = Integer(para[:level] || 0) * 16 + (para[:list] && para[:list] != :none ? 20 : 0)
|
|
116
|
+
parts = fragments(range.begin, range.end).map do |first, finish, style|
|
|
117
|
+
Run.new(first, finish, fragment_layout(first, finish, style, cx), 0, style,
|
|
118
|
+
style[:color] || cx.theme.colors.text, style[:size] || @font_size)
|
|
119
|
+
end
|
|
120
|
+
width_used = parts.sum { |part| part.layout.width }
|
|
121
|
+
available = [width - indent, 0].max
|
|
122
|
+
next_range = ranges[index + 1]
|
|
123
|
+
next_paragraph = next_range && @display_text.byteslice(0...next_range.begin).count("\n")
|
|
124
|
+
if para[:align] == :justify && next_range && next_paragraph == paragraph_index
|
|
125
|
+
parts = justified_parts(parts, range, available, width_used)
|
|
126
|
+
width_used = available
|
|
127
|
+
end
|
|
128
|
+
align = para[:align] || :start
|
|
129
|
+
offset = case align
|
|
130
|
+
when :center then indent + [(available - width_used) / 2.0, 0].max
|
|
131
|
+
when :end then indent + [available - width_used, 0].max
|
|
132
|
+
else indent
|
|
133
|
+
end
|
|
134
|
+
x = 0.0
|
|
135
|
+
runs = parts.map do |part|
|
|
136
|
+
Run.new(part.start, part.finish, part.layout, x, part.style, part.color, part.size).tap { x += part.layout.width }
|
|
137
|
+
end
|
|
138
|
+
ascent = runs.map { |run| run.layout&.ascent || run.size * 0.8 }.max || @font_size * 0.8
|
|
139
|
+
descent = runs.map { |run| run.layout&.descent || run.size * 0.2 }.max || @font_size * 0.2
|
|
140
|
+
height = [line_height, ascent + descent].max
|
|
141
|
+
previous_break = @display_text.byteslice(0...range.begin).rindex("\n")
|
|
142
|
+
item = Line.new(range.begin, range.end, offset, y, width_used, height, ascent, runs, paragraph_index,
|
|
143
|
+
range.begin == (previous_break ? previous_break + 1 : 0))
|
|
144
|
+
y += height
|
|
145
|
+
item
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def justified_parts(parts, range, available, line_width)
|
|
150
|
+
return parts if available <= line_width
|
|
151
|
+
spaces = []
|
|
152
|
+
@display_text.byteslice(range).to_enum(:scan, /\s+/).each { spaces << Regexp.last_match.end(0) }
|
|
153
|
+
return parts if spaces.empty?
|
|
154
|
+
extra = (available - line_width) / spaces.length
|
|
155
|
+
parts.map do |part|
|
|
156
|
+
layout = part.layout
|
|
157
|
+
shift = ->(byte) { spaces.count { |finish| finish <= byte } * extra }
|
|
158
|
+
carets = layout.carets.map { |byte, x| [byte, x + shift.call(part.start + byte - range.begin)] }.freeze
|
|
159
|
+
glyphs = layout.glyphs.map do |glyph|
|
|
160
|
+
TextSystem::Glyph.new(glyph.font, glyph.id, glyph.start, glyph.finish,
|
|
161
|
+
glyph.x + shift.call(part.start + glyph.start - range.begin), glyph.advance)
|
|
162
|
+
end.freeze
|
|
163
|
+
adjusted = TextSystem::LineLayout.new(layout.text, glyphs, layout.width + shift.call(part.finish - range.begin),
|
|
164
|
+
layout.ascent, layout.descent, layout.size, carets)
|
|
165
|
+
Run.new(part.start, part.finish, adjusted, part.x, part.style, part.color, part.size)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def fragments(first, finish)
|
|
170
|
+
return [] if first == finish
|
|
171
|
+
boundaries = [first, finish]
|
|
172
|
+
@visual_spans.each do |from, to, _style|
|
|
173
|
+
boundaries << from if from > first && from < finish
|
|
174
|
+
boundaries << to if to > first && to < finish
|
|
175
|
+
end
|
|
176
|
+
boundaries.uniq.sort.each_cons(2).map { |from, to| [from, to, style_at_display(from)] }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def style_at_display(offset)
|
|
180
|
+
span = @visual_spans.find { |first, finish, _| offset >= first && offset < finish }
|
|
181
|
+
span ? span[2] : {}
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def fragment_layout(first, finish, style, cx)
|
|
185
|
+
text = @display_text.byteslice(first...finish)
|
|
186
|
+
size = style[:size] || @font_size
|
|
187
|
+
font = font_for(style, cx)
|
|
188
|
+
key = [text, size, font&.object_id]
|
|
189
|
+
@layout_cache[key] ||= cx.text_system&.layout_line(text, font: font, size: size) || approximate_layout(text, size)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def approximate_layout(text, size)
|
|
193
|
+
byte = 0
|
|
194
|
+
x = 0.0
|
|
195
|
+
carets = [[0, 0.0]]
|
|
196
|
+
text.grapheme_clusters.each do |cluster|
|
|
197
|
+
byte += cluster.bytesize
|
|
198
|
+
x += Unicode.width(cluster) * size * 0.6
|
|
199
|
+
carets << [byte, x]
|
|
200
|
+
end
|
|
201
|
+
TextSystem::LineLayout.new(text, [].freeze, x, size * 0.8, size * 0.2, size, carets)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def font_for(style, cx)
|
|
205
|
+
return style[:font] if style[:font]
|
|
206
|
+
return unless cx.text_system
|
|
207
|
+
font = cx.text_system.respond_to?(:font) ? cx.text_system.font : nil
|
|
208
|
+
return font unless style[:bold] || style[:italic]
|
|
209
|
+
database = cx.text_system.respond_to?(:font_db) ? cx.text_system.font_db : nil
|
|
210
|
+
database&.find(family: font&.family, weight: style[:bold] ? 700 : 400,
|
|
211
|
+
style: style[:italic] ? :italic : :normal) || font
|
|
212
|
+
rescue StandardError
|
|
213
|
+
font
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def line_height = @font_size * 1.4
|
|
217
|
+
|
|
218
|
+
def paint_selection(bounds, range, cx)
|
|
219
|
+
@lines.each do |line|
|
|
220
|
+
first, finish = [range.begin, line.start].max, [range.end, line.finish].min
|
|
221
|
+
next if finish <= first
|
|
222
|
+
line.runs.each do |run|
|
|
223
|
+
from, to = [first, run.start].max, [finish, run.finish].min
|
|
224
|
+
next if to <= from || !run.layout
|
|
225
|
+
left = run.layout.x_for_index(from - run.start)
|
|
226
|
+
right = run.layout.x_for_index(to - run.start)
|
|
227
|
+
cx.scene.quad(bounds.x + line.x + run.x + left, bounds.y + line.y,
|
|
228
|
+
[right - left, 1].max, line.height, color: cx.theme.colors.selection)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def paint_list_marker(bounds, line, cx)
|
|
234
|
+
return unless line.first_in_paragraph
|
|
235
|
+
para = @owner.paragraph_style_at([line.start, @owner.text.bytesize].min)
|
|
236
|
+
marker = case para[:list]
|
|
237
|
+
when :bullet then "•"
|
|
238
|
+
when :ordered then "#{line.paragraph + 1}."
|
|
239
|
+
else return
|
|
240
|
+
end
|
|
241
|
+
layout = cx.text_system&.layout_line(marker, size: @font_size)
|
|
242
|
+
cx.text_system&.paint_line(cx.scene, layout, x: bounds.x + line.x - 18,
|
|
243
|
+
y: bounds.y + line.y + line.ascent, color: cx.theme.colors.text)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def paint_caret(bounds, cx)
|
|
247
|
+
offset = @owner.selection.range.begin + (@owner.buffer.composition&.selection&.first || 0)
|
|
248
|
+
line = @lines.find { |item| offset.between?(item.start, item.finish) } || @lines.last
|
|
249
|
+
return unless line
|
|
250
|
+
run = line.runs.find { |item| offset.between?(item.start, item.finish) } || line.runs.last
|
|
251
|
+
x = line.x + (run ? run.x + (run.layout ? run.layout.x_for_index((offset - run.start).clamp(0, run.finish - run.start)) : 0) : 0)
|
|
252
|
+
ascent, descent = run&.layout ? [run.layout.ascent, run.layout.descent] : [@font_size * 0.8, @font_size * 0.2]
|
|
253
|
+
y, height = line.y + line.ascent - ascent, ascent + descent
|
|
254
|
+
color = run&.color || cx.theme.colors.text
|
|
255
|
+
cx.scene.layer(Scene::LAYER_FOCUS_RING) { cx.scene.quad(bounds.x + x, bounds.y + y, 1, height, color: color) }
|
|
256
|
+
cx.window.ime_state = Bounds.new(bounds.x + x, bounds.y + y, 1, height)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def paint_composition(bounds, cx)
|
|
260
|
+
offset = @owner.selection.range.begin
|
|
261
|
+
finish = offset + @owner.buffer.composition.text.bytesize
|
|
262
|
+
first, last = point_for(offset), point_for(finish)
|
|
263
|
+
cx.scene.layer(Scene::LAYER_FOCUS_RING) do
|
|
264
|
+
cx.scene.underline(bounds.x + first.x, bounds.y + first.y + @font_size * 1.25,
|
|
265
|
+
[last.x - first.x, 1].max, color: cx.theme.colors.text)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def point_for(offset)
|
|
270
|
+
line = @lines.find { |item| offset.between?(item.start, item.finish) } || @lines.last
|
|
271
|
+
return Point.new(0, 0) unless line
|
|
272
|
+
run = line.runs.find { |item| offset.between?(item.start, item.finish) } || line.runs.last
|
|
273
|
+
x = line.x + (run ? run.x + run.layout.x_for_index((offset - run.start).clamp(0, run.finish - run.start)) : 0)
|
|
274
|
+
Point.new(x, line.y)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def begin_selection(event)
|
|
278
|
+
offset = hit_test(Point.new(event.position.x - @bounds.x, event.position.y - @bounds.y))
|
|
279
|
+
@owner.selection = event.click_count == 2 ? word_selection(offset) : TextSelection.new(offset)
|
|
280
|
+
@owner.instance_variable_get(:@cx)&.window&.request_frame
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def extend_selection(event)
|
|
284
|
+
return unless @owner.selection
|
|
285
|
+
offset = hit_test(Point.new(event.position.x - @bounds.x, event.position.y - @bounds.y))
|
|
286
|
+
@owner.selection = TextSelection.new(@owner.selection.anchor, offset)
|
|
287
|
+
@owner.instance_variable_get(:@cx)&.window&.request_frame
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def word_selection(offset)
|
|
291
|
+
range = Unicode.word_range_at(@owner.text, [offset, @owner.text.bytesize].min)
|
|
292
|
+
TextSelection.new(range.begin, range.end)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def select_all
|
|
296
|
+
@owner.selection = TextSelection.new(0, @owner.text.bytesize)
|
|
297
|
+
@owner.instance_variable_get(:@cx)&.window&.request_frame
|
|
298
|
+
true
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
@@ -35,6 +35,7 @@ module Zaniah
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def select(range)
|
|
38
|
+
# The range addresses cells in row-major order.
|
|
38
39
|
@selection = range && Range === range ? range : nil
|
|
39
40
|
self
|
|
40
41
|
end
|
|
@@ -74,13 +75,34 @@ module Zaniah
|
|
|
74
75
|
cx.scene.quad(bounds.x + column * @cell_width, bounds.y + row * @cell_height,
|
|
75
76
|
(run - column) * @cell_width, @cell_height, color: color(cell.background))
|
|
76
77
|
end
|
|
78
|
+
if @selection&.cover?(row * @grid.columns + column)
|
|
79
|
+
cx.scene.quad(bounds.x + column * @cell_width, bounds.y + row * @cell_height,
|
|
80
|
+
@cell_width, @cell_height, color: "#ffffff33")
|
|
81
|
+
end
|
|
77
82
|
if cell.width != 0 && cell.text != " "
|
|
78
83
|
paint_text(cell.text, cell.foreground || @foreground, bounds, row, column, cx)
|
|
79
84
|
end
|
|
85
|
+
paint_attributes(cell, bounds, row, column, cx)
|
|
80
86
|
column += 1
|
|
81
87
|
end
|
|
82
88
|
end
|
|
83
89
|
|
|
90
|
+
def paint_attributes(cell, bounds, row, column, cx)
|
|
91
|
+
attributes = cell.attributes || {}
|
|
92
|
+
return unless cx.scene.respond_to?(:underline)
|
|
93
|
+
|
|
94
|
+
x = bounds.x + column * @cell_width
|
|
95
|
+
y = bounds.y + row * @cell_height
|
|
96
|
+
if attributes[:underline]
|
|
97
|
+
thickness = attributes[:underline] == 2 ? 2 : 1
|
|
98
|
+
cx.scene.underline(x, y + @cell_height - 2, @cell_width, color: color(cell.foreground || @foreground), thickness: thickness)
|
|
99
|
+
end
|
|
100
|
+
if attributes[:strikethrough]
|
|
101
|
+
cx.scene.underline(x, y + @cell_height * 0.52, @cell_width,
|
|
102
|
+
color: color(cell.foreground || @foreground))
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
84
106
|
def paint_text(text, tint, bounds, row, column, cx)
|
|
85
107
|
return unless cx.text_system
|
|
86
108
|
line = cx.text_system.layout_line(text, size: @cell_height * 0.78)
|
data/lib/zaniah/ui.rb
CHANGED
|
@@ -13,9 +13,11 @@ require_relative "ui/structure"
|
|
|
13
13
|
require_relative "ui/modal"
|
|
14
14
|
require_relative "ui/layout_components"
|
|
15
15
|
require_relative "ui/pane_grid"
|
|
16
|
+
require_relative "ui/grid"
|
|
16
17
|
require_relative "ui/data"
|
|
17
18
|
require_relative "ui/tree_view"
|
|
18
19
|
require_relative "ui/charts"
|
|
19
20
|
require_relative "ui/forms"
|
|
20
21
|
require_relative "ui/editors"
|
|
22
|
+
require_relative "ui/rich_text_surface"
|
|
21
23
|
require_relative "ui/terminal_view"
|
data/lib/zaniah/version.rb
CHANGED
data/lib/zaniah.rb
CHANGED
|
@@ -25,6 +25,7 @@ require_relative "zaniah/text_selection"
|
|
|
25
25
|
require_relative "zaniah/text_buffer"
|
|
26
26
|
require_relative "zaniah/scene"
|
|
27
27
|
require_relative "zaniah/png"
|
|
28
|
+
require_relative "zaniah/jpeg"
|
|
28
29
|
require_relative "zaniah/gif"
|
|
29
30
|
require_relative "zaniah/gpu"
|
|
30
31
|
require_relative "zaniah/layout"
|
data/sig/elements.rbs
CHANGED
|
@@ -125,6 +125,8 @@ module Zaniah
|
|
|
125
125
|
end
|
|
126
126
|
class Image < Element
|
|
127
127
|
def initialize: (String path) -> void
|
|
128
|
+
def self.from_bytes: (String bytes, ?format: :auto | :png | :jpeg | :jpg | :gif) -> Image
|
|
129
|
+
attr_reader frames: Array[GIF::Frame]?
|
|
128
130
|
end
|
|
129
131
|
class Canvas < Element
|
|
130
132
|
def initialize: () { (Bounds, FrameContext) -> void } -> void
|
data/sig/native.rbs
CHANGED
|
@@ -19,6 +19,7 @@ module Zaniah
|
|
|
19
19
|
def cursor_style=: (cursor style) -> void
|
|
20
20
|
def open_url: (String url) -> void
|
|
21
21
|
def toggle_fullscreen: () -> void
|
|
22
|
+
def move_to_display: (Display display) -> bool
|
|
22
23
|
end
|
|
23
24
|
end
|
|
24
25
|
module Linux
|
|
@@ -37,6 +38,7 @@ module Zaniah
|
|
|
37
38
|
def cursor_style=: (cursor style) -> void
|
|
38
39
|
def open_url: (String url) -> bool
|
|
39
40
|
def toggle_fullscreen: () -> void
|
|
41
|
+
def move_to_display: (Display display) -> bool
|
|
40
42
|
end
|
|
41
43
|
class WaylandWindow < Headless::Window
|
|
42
44
|
def initialize: (?gpu: :opengl | :gl, ?width: Numeric, ?height: Numeric, ?title: String, ?scale_factor: Numeric) -> void
|
|
@@ -53,6 +55,10 @@ module Zaniah
|
|
|
53
55
|
def cursor_style=: (cursor style) -> void
|
|
54
56
|
def open_url: (String url) -> bool
|
|
55
57
|
def toggle_fullscreen: () -> void
|
|
58
|
+
# Wayland does not support arbitrary client window placement.
|
|
59
|
+
def move_to_display: (Display display) -> bool
|
|
60
|
+
# Best-effort output preference; the compositor decides whether to honor it.
|
|
61
|
+
def fullscreen_on: (Display display) -> bool
|
|
56
62
|
end
|
|
57
63
|
end
|
|
58
64
|
module Windows
|
|
@@ -72,6 +78,7 @@ module Zaniah
|
|
|
72
78
|
def cursor_style=: (cursor style) -> void
|
|
73
79
|
def open_url: (String url) -> void
|
|
74
80
|
def toggle_fullscreen: () -> void
|
|
81
|
+
def move_to_display: (Display display) -> bool
|
|
75
82
|
end
|
|
76
83
|
class Terminal
|
|
77
84
|
attr_reader pid: Integer
|
data/sig/platform.rbs
CHANGED
|
@@ -85,6 +85,8 @@ module Zaniah
|
|
|
85
85
|
def set_cursor: (cursor style) -> cursor
|
|
86
86
|
def cursor_style=: (cursor style) -> cursor
|
|
87
87
|
def displays: () -> Array[Display]
|
|
88
|
+
# Raises Error for backends without native physical-display placement.
|
|
89
|
+
def move_to_display: (Display display) -> bool
|
|
88
90
|
def popup: () -> Popup?
|
|
89
91
|
def resize: (Numeric width, Numeric height) -> void
|
|
90
92
|
def input: (Input::event event) -> void
|
data/sig/ui.rbs
CHANGED
|
@@ -290,6 +290,36 @@ module Zaniah
|
|
|
290
290
|
def accessibility_action: (Accessibility::Node node, Symbol action) -> bool?
|
|
291
291
|
def accessibility_value: (Accessibility::Node node, Numeric value) -> bool?
|
|
292
292
|
end
|
|
293
|
+
class Grid < Component
|
|
294
|
+
class Area < Data
|
|
295
|
+
attr_reader rows: Range[Integer]
|
|
296
|
+
attr_reader columns: Range[Integer]
|
|
297
|
+
def self.new: (Range[Integer] rows, Range[Integer] columns) -> Area
|
|
298
|
+
| (rows: Range[Integer], columns: Range[Integer]) -> Area
|
|
299
|
+
end
|
|
300
|
+
attr_reader selection: Array[Area]
|
|
301
|
+
attr_reader scroll_state: ScrollState
|
|
302
|
+
attr_reader visible_rows: Range[Integer]?
|
|
303
|
+
attr_reader visible_columns: Range[Integer]?
|
|
304
|
+
def initialize: (rows: Integer, columns: Integer, ?row_height: Numeric | Proc, ?column_width: Numeric | Proc, ?frozen_rows: Integer, ?frozen_columns: Integer, ?estimated_row_height: Numeric, ?estimated_column_width: Numeric, ?overscan: Integer) { (Integer, Integer, Bounds, FrameContext) -> _Renderable? } -> void
|
|
305
|
+
def selection=: (Array[Area] areas) -> Array[Area]
|
|
306
|
+
def on_select: () { (Array[Area], Input::MouseDown?, FrameContext?) -> void } -> self
|
|
307
|
+
def on_edit: () { (Integer, Integer, Input::MouseDown?, FrameContext?) -> void } -> self
|
|
308
|
+
def on_fill: () { (Area, Area, FrameContext?) -> void } -> self
|
|
309
|
+
def on_resize: () { (Symbol, Integer, Numeric, FrameContext?) -> void } -> self
|
|
310
|
+
def set_row_height: (Integer index, Numeric height) -> self
|
|
311
|
+
def set_column_width: (Integer index, Numeric width) -> self
|
|
312
|
+
def hide_row: (Integer index) -> self
|
|
313
|
+
def unhide_row: (Integer index) -> self
|
|
314
|
+
def hide_column: (Integer index) -> self
|
|
315
|
+
def unhide_column: (Integer index) -> self
|
|
316
|
+
def hide_rows: (Array[Integer] indices, hidden: bool) -> self
|
|
317
|
+
def hide_columns: (Array[Integer] indices, hidden: bool) -> self
|
|
318
|
+
def row_hidden?: (Integer index) -> bool
|
|
319
|
+
def column_hidden?: (Integer index) -> bool
|
|
320
|
+
def freeze_panes: (rows: Integer, columns: Integer) -> self
|
|
321
|
+
def scroll_to: (row: Integer, column: Integer, ?align: :start | :center | :end | :nearest) -> self
|
|
322
|
+
end
|
|
293
323
|
class Resizable < Component
|
|
294
324
|
attr_reader width: Float
|
|
295
325
|
attr_reader height: Float
|
|
@@ -378,6 +408,22 @@ module Zaniah
|
|
|
378
408
|
end
|
|
379
409
|
class BarChart < LineChart
|
|
380
410
|
end
|
|
411
|
+
class StackedBarChart < BarChart
|
|
412
|
+
end
|
|
413
|
+
class PieChart < Sparkline
|
|
414
|
+
attr_reader slices: Array[[String, Float]]
|
|
415
|
+
def initialize: (Hash[untyped, Numeric] | Array[Numeric] data, ?width: Numeric, ?height: Numeric, ?colors: Array[color]?, ?label: String) -> void
|
|
416
|
+
end
|
|
417
|
+
class DonutChart < PieChart
|
|
418
|
+
def initialize: (Hash[untyped, Numeric] | Array[Numeric] data, ?width: Numeric, ?height: Numeric, ?colors: Array[color]?, ?label: String) -> void
|
|
419
|
+
end
|
|
420
|
+
class ScatterChart < Sparkline
|
|
421
|
+
attr_reader points: Hash[String, Array[[Float, Float]]]
|
|
422
|
+
def initialize: (Hash[untyped, Array[[Numeric, Numeric]]] | Array[[Numeric, Numeric]] series, ?width: Numeric, ?height: Numeric, ?colors: Array[color]?, ?label: String) -> void
|
|
423
|
+
end
|
|
424
|
+
class AreaChart < LineChart
|
|
425
|
+
def initialize: (Hash[untyped, Array[Numeric]] | Array[Numeric] series, ?width: Numeric, ?height: Numeric, ?colors: Array[color]?, ?stacked: bool, ?label: String) -> void
|
|
426
|
+
end
|
|
381
427
|
class CodeEditor < Component
|
|
382
428
|
attr_reader buffer: TextBuffer
|
|
383
429
|
def initialize: (?String | TextBuffer value, ?language: Symbol?, ?line_numbers: bool, ?read_only: bool) -> void
|
|
@@ -385,8 +431,27 @@ module Zaniah
|
|
|
385
431
|
def on_change: () { (String, FrameContext?) -> void } -> self
|
|
386
432
|
end
|
|
387
433
|
class RichText < Component
|
|
388
|
-
|
|
389
|
-
|
|
434
|
+
class Span < Data
|
|
435
|
+
attr_reader start: Integer
|
|
436
|
+
attr_reader finish: Integer
|
|
437
|
+
attr_reader style: Hash[Symbol, untyped]
|
|
438
|
+
end
|
|
439
|
+
attr_reader buffer: TextBuffer
|
|
440
|
+
attr_reader selection: TextSelection
|
|
441
|
+
attr_reader paragraph_styles: Array[Hash[Symbol, untyped]]
|
|
442
|
+
def initialize: (String | Array[untyped] runs, ?selectable: bool, ?editable: bool) -> void
|
|
443
|
+
def text: () -> String
|
|
444
|
+
def runs: () -> Array[Hash[Symbol, untyped]]
|
|
445
|
+
def spans: () -> Array[Span]
|
|
446
|
+
def editable?: () -> bool
|
|
447
|
+
def selection=: (TextSelection value) -> TextSelection
|
|
448
|
+
def on_change: () { (String, RichText) -> void } -> self
|
|
449
|
+
def on_select: () { (TextSelection, RichText) -> void } -> self
|
|
450
|
+
def apply: (Range[Integer] range, Hash[Symbol, untyped] style) -> self
|
|
451
|
+
def insert: (Integer offset, String value, ?style: Hash[Symbol, untyped]?) -> self
|
|
452
|
+
def delete: (Range[Integer] range) -> self
|
|
453
|
+
def replace: (Range[Integer] range, String value, ?style: Hash[Symbol, untyped]?) -> self
|
|
454
|
+
def paragraph_style: (Range[Integer] range, ?align: :start | :center | :end | :justify?, ?list: :none | :bullet | :ordered?, ?level: Integer?) -> self
|
|
390
455
|
end
|
|
391
456
|
|
|
392
457
|
class Validation
|
data/sig/zaniah.rbs
CHANGED
|
@@ -172,4 +172,18 @@ module Zaniah
|
|
|
172
172
|
def self.decode: (String bytes, ?max_pixels: Integer) -> [Integer, Integer, String]
|
|
173
173
|
def self.write: (String path, Integer width, Integer height, String rgba) -> Integer
|
|
174
174
|
end
|
|
175
|
+
module JPEG
|
|
176
|
+
class Error < ArgumentError
|
|
177
|
+
end
|
|
178
|
+
def self.decode: (String bytes, ?max_pixels: Integer) -> [Integer, Integer, String]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
module GIF
|
|
182
|
+
class Frame < Data
|
|
183
|
+
attr_reader pixels: String
|
|
184
|
+
attr_reader delay: Numeric
|
|
185
|
+
attr_reader disposal: Integer
|
|
186
|
+
end
|
|
187
|
+
def self.decode: (String bytes, ?max_pixels: Integer) -> [Integer, Integer, Array[Frame]]
|
|
188
|
+
end
|
|
175
189
|
end
|
metadata
CHANGED
|
@@ -1,29 +1,35 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zaniah
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yudai Takada
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: alhena
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
|
-
- - "
|
|
17
|
+
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
19
|
version: 0.2.0
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: 0.4.0
|
|
20
23
|
type: :runtime
|
|
21
24
|
prerelease: false
|
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
26
|
requirements:
|
|
24
|
-
- - "
|
|
27
|
+
- - ">="
|
|
25
28
|
- !ruby/object:Gem::Version
|
|
26
29
|
version: 0.2.0
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.4.0
|
|
27
33
|
- !ruby/object:Gem::Dependency
|
|
28
34
|
name: rexml
|
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -84,6 +90,7 @@ files:
|
|
|
84
90
|
- docs/adr/009-grid-scope.md
|
|
85
91
|
- docs/adr/010-text-overlay-layout.md
|
|
86
92
|
- docs/adr/011-stable-accessibility-identity.md
|
|
93
|
+
- docs/adr/012-productivity-components.md
|
|
87
94
|
- docs/adr/README.md
|
|
88
95
|
- docs/animation.md
|
|
89
96
|
- docs/components.md
|
|
@@ -149,6 +156,7 @@ files:
|
|
|
149
156
|
- docs/gallery/tooltip-top-dark.png
|
|
150
157
|
- docs/gallery/tooltip-top-high_contrast.png
|
|
151
158
|
- docs/gallery/tooltip-top-light.png
|
|
159
|
+
- docs/images.md
|
|
152
160
|
- docs/index.html
|
|
153
161
|
- docs/layout.md
|
|
154
162
|
- docs/media/overview.apng
|
|
@@ -255,6 +263,7 @@ files:
|
|
|
255
263
|
- lib/zaniah/input/scroll_wheel.rb
|
|
256
264
|
- lib/zaniah/input/text_input.rb
|
|
257
265
|
- lib/zaniah/interactivity.rb
|
|
266
|
+
- lib/zaniah/jpeg.rb
|
|
258
267
|
- lib/zaniah/layout.rb
|
|
259
268
|
- lib/zaniah/layout/available_space.rb
|
|
260
269
|
- lib/zaniah/layout/engine.rb
|
|
@@ -347,12 +356,14 @@ files:
|
|
|
347
356
|
- lib/zaniah/ui/data.rb
|
|
348
357
|
- lib/zaniah/ui/editors.rb
|
|
349
358
|
- lib/zaniah/ui/forms.rb
|
|
359
|
+
- lib/zaniah/ui/grid.rb
|
|
350
360
|
- lib/zaniah/ui/layout_components.rb
|
|
351
361
|
- lib/zaniah/ui/modal.rb
|
|
352
362
|
- lib/zaniah/ui/overlays.rb
|
|
353
363
|
- lib/zaniah/ui/pane_grid.rb
|
|
354
364
|
- lib/zaniah/ui/pickers.rb
|
|
355
365
|
- lib/zaniah/ui/primitives.rb
|
|
366
|
+
- lib/zaniah/ui/rich_text_surface.rb
|
|
356
367
|
- lib/zaniah/ui/scrollbar.rb
|
|
357
368
|
- lib/zaniah/ui/structure.rb
|
|
358
369
|
- lib/zaniah/ui/terminal_view.rb
|