konpeito 0.7.0 → 0.8.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: 50ad4b5d35d1b8abd26ee9d941a901b99e281eb9cc7d2deed777f3ebf12b2308
4
- data.tar.gz: fc91a16a60718a8a910776ab56f01a09d49ec2ee78e71082965392f1dee332e1
3
+ metadata.gz: ed1949148547cf4716d3405d71d9d79413d84201a699fefe98512ddb4202808e
4
+ data.tar.gz: cbcc7ecbea2a4c6edeab12fd5f20d5221e9dca2e379906c728d37e16b0168bea
5
5
  SHA512:
6
- metadata.gz: c2efa11a609aba145beef6d2b722028f657a6a4d5469864241cbdde1b43dc494b9cf814379788a8ede8d4715d0aceab76df3d64a1c6b14913a3e2f84d0787f1f
7
- data.tar.gz: 77860386e45ac9a5b78ead9b0f02dfc3d759668fe5ffc3d97c72c4932ca297778d27192c6a698ad264f7641a988b07905dbf6a15c32f10320678999e89a604dc
6
+ metadata.gz: 8ad8a635a74b09378cacf342e14a9fdba84434d29d3657071a72fb42d800dd8cfe84bc5a9f66ca81fd48852b880f6896c8a4aff5d83c12be03a8cbf8208b5b5d
7
+ data.tar.gz: f1635c6083bd97079fff23d07e3fe5232eadaffc21aeccdc6718689569da8afe50eba3252a188eb2aff42318317ed0ec18e5d2e1b190ac63679c0b354a27c11d
data/CHANGELOG.md CHANGED
@@ -7,13 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.0] - 2026-03-14
11
+
12
+ ### Added
13
+ - **KUI widget expansion**: text_input, checkbox, radio, toggle, spinner, status_bar,
14
+ selectable_list, list_item, table (header/row/cell), modal, tab_bar, tab_button,
15
+ tab_content — all with keyboard focus support and click-to-focus
16
+ - **KUI focus system**: Tab/arrow key navigation across all interactive widgets with
17
+ focus highlight and Enter key activation
18
+ - **KUI extended key input**: `kui_char_pressed()`, `kui_mod_pressed()`,
19
+ Delete/Home/End/PgUp/PgDn/F1-F12 key constants
20
+ - **KUI text buffer API**: `kui_textbuf_copy`, `kui_textbuf_render` for dynamic content
21
+ - **KUI GUI backend**: Full GUI implementations for all new widgets (Clay+Raylib)
22
+ - **Game framework expansion**: tween/easing (linear, quad, cubic, bounce, elastic),
23
+ screen shake, scene transitions (fade), FSM, timer system, parallax scrolling,
24
+ simple physics (AABB, gravity, friction), particle system, object pool, grid/tile
25
+ utilities, debug overlay (FPS, collision rects), gamepad abstraction, save/load
26
+ - **Game showcase demo**: "Coin Dash" score-attack platformer demonstrating
27
+ game framework API (physics, particles, tween, FSM, parallax, Camera2D)
28
+ - **Physics demo**: Platformer demo showcasing NativeArray-based physics
29
+ - **KUI form demo**: Text input, checkbox, radio, toggle showcase
30
+ - **KUI tabs demo**: Tab bar, table, modal dialog, selectable list, status bar showcase
31
+ - **KUI showcase demo**: Rich GUI app demonstrating all v0.8.0 widgets in a 4-tab layout
32
+
33
+ ### Fixed
34
+ - **NativeArray SEGV as function argument**: `convert_value()` lacked
35
+ `[:native_array, :value]` case, causing raw LLVM pointers where Ruby VALUEs
36
+ were expected
37
+ - **NativeArray SEGV in compound expressions**: `get_effective_unboxed_type()`
38
+ only checked `@variable_types` for LoadLocal nodes, causing incorrect float path
39
+ for integer arithmetic in if conditions
40
+ - **KUI text input isolation**: Only focused text_input processes keyboard input
41
+ - **KUI toggle flip-back**: Filter out termbox2 mouse release events (key 65509)
42
+ - **KUI modal layout**: Reduced TUI padding from 16 to 1 char cells
43
+ - **mruby keyword argument handling**: Optional keyword hash with `MRB_ARGS_ARG`
44
+ - **KUI scroll containers not scrolling**: `Clay_GetScrollOffset()` was not called
45
+ in `konpeito_clay_scroll` / `konpeito_clay_tui_scroll`, so `childOffset` remained
46
+ zero and scroll position was never visually applied
47
+
48
+ ## [0.7.1] - 2026-03-14
49
+
50
+ ### Fixed
51
+ - **rescue handler callback crash**: `generate_rescue_handler_with_global_flag_callback` was restoring `@mruby_gc_arena_alloca` without saving it first, causing `NameError` in begin/rescue/else code generation.
52
+
10
53
  ## [0.7.0] - 2026-03-14
11
54
 
12
55
  ### Added
13
56
  - **KUI declarative UI framework**: Pure Ruby DSL for building GUI/TUI apps with a single codebase. Wraps Clay+Raylib (GUI) or ClayTUI (TUI). Widgets: `vpanel`, `hpanel`, `fixed_panel`, `label`, `label_num`, `button`, `menu_item`, `spacer`, `divider`, `progress_bar`. Theme system and unified key event abstraction.
14
57
  - **ClayTUI stdlib**: Terminal UI backend using Clay layout engine + termbox2 rendering. Auto-detected via `ClayTUI` module reference.
15
58
  - **KonpeitoShell stdlib**: Shell execution (`exec`, `system`), environment variables (`getenv`, `setenv`), and file I/O (`read_file`, `write_file`, `append_file`, `file_exists`) for mruby backend.
16
- - **RPG framework stdlib**: Reusable RPG game components (tilemap, sprites, camera, battle system) for raylib-based games.
59
+ - **Game framework stdlib**: Reusable 2D game components (tilemap, sprites, camera, battle system) for raylib-based games.
17
60
  - **Stdlib auto-detection expansion**: `KonpeitoJSON`, `KonpeitoHTTP`, `KonpeitoCrypto`, `KonpeitoCompression` added to `STDLIB_MODULE_MAP` for automatic RBS injection when referenced in source code.
18
61
  - **KUI auto-path resolution**: `require "kui_gui"` / `require "kui_tui"` works without `-I` flags — KUI stdlib directory is automatically included in compiler search paths.
19
62
  - **KUI example apps**: Counter (GUI + TUI), minimal hello, multi-page dashboard with sidebar navigation.
@@ -336,6 +379,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
336
379
  - `%a{extern}` - external C struct wrappers
337
380
  - `%a{simd}` - SIMD vectorization
338
381
 
382
+ [0.8.0]: https://github.com/i2y/konpeito/compare/v0.7.1...v0.8.0
383
+ [0.7.1]: https://github.com/i2y/konpeito/compare/v0.7.0...v0.7.1
384
+ [0.7.0]: https://github.com/i2y/konpeito/compare/v0.6.0...v0.7.0
385
+ [0.6.0]: https://github.com/i2y/konpeito/compare/v0.5.0...v0.6.0
386
+ [0.5.0]: https://github.com/i2y/konpeito/compare/v0.4.2...v0.5.0
387
+ [0.4.2]: https://github.com/i2y/konpeito/compare/v0.4.1...v0.4.2
339
388
  [0.4.1]: https://github.com/i2y/konpeito/compare/v0.4.0...v0.4.1
340
389
  [0.4.0]: https://github.com/i2y/konpeito/compare/v0.3.1...v0.4.0
341
390
  [0.3.1]: https://github.com/i2y/konpeito/compare/v0.3.0...v0.3.1
@@ -1597,6 +1597,24 @@ module Konpeito
1597
1597
  boxed_value = func.params[i + 1] # +1 to skip self
1598
1598
  type_tag = @variable_types[param.name]
1599
1599
 
1600
+ if type_tag == :native_array
1601
+ # NativeArray parameter: the VALUE is actually a pointer address (via ptr2int).
1602
+ # Reconstruct the pointer and store directly in @variables (not alloca).
1603
+ # generate_load_local reads NativeArray values from @variables, not allocas.
1604
+ array_ptr = @builder.int2ptr(boxed_value, ptr_type, "#{param.name}_ptr")
1605
+ @variables[param.name] = array_ptr
1606
+ # Propagate element type and length from the parameter's HM type if available
1607
+ if param.type.is_a?(TypeChecker::Types::NativeArrayType)
1608
+ elem_type = param.type.element_type
1609
+ @variables["#{param.name}_len"] = LLVM::Int64.from_i(param.type.size || 0)
1610
+ if elem_type && elem_type != :Int64 && elem_type != :Float64
1611
+ @native_array_class_types ||= {}
1612
+ @native_array_class_types[param.name] = elem_type
1613
+ end
1614
+ end
1615
+ next
1616
+ end
1617
+
1600
1618
  # Unbox parameter if it's a numeric type
1601
1619
  value_to_store = case type_tag
1602
1620
  when :i64
@@ -2004,6 +2022,10 @@ module Konpeito
2004
2022
  [LLVM::Int64, :i64]
2005
2023
  elsif float_type?(ruby_type)
2006
2024
  [LLVM::Double, :double]
2025
+ elsif ruby_type.is_a?(TypeChecker::Types::NativeArrayType)
2026
+ # NativeArray parameters: alloca holds VALUE (i64) but type is :native_array
2027
+ # The actual pointer is reconstructed from the VALUE in parameter setup
2028
+ [value_type, :native_array]
2007
2029
  else
2008
2030
  [value_type, :value]
2009
2031
  end
@@ -3029,6 +3051,9 @@ module Konpeito
3029
3051
  when [:native_class, :value]
3030
3052
  # NativeClass struct pointer to VALUE (i64)
3031
3053
  @builder.ptr2int(value, LLVM::Int64)
3054
+ when [:native_array, :value]
3055
+ # NativeArray pointer to VALUE (i64) — encode pointer address as integer
3056
+ @builder.ptr2int(value, LLVM::Int64)
3032
3057
  when [:i64, :double]
3033
3058
  @builder.si2fp(value, LLVM::Double)
3034
3059
  when [:double, :i64]
@@ -7372,6 +7397,15 @@ module Konpeito
7372
7397
  return type_tag if type_tag == :i64 || type_tag == :double
7373
7398
  end
7374
7399
 
7400
+ # Check @variable_types for any instruction with result_var.
7401
+ # This is critical for sub-expressions (e.g. NativeArrayGet, Call) whose
7402
+ # HIR type may be an unresolved TypeVar but whose actual generated type
7403
+ # is correctly tracked in @variable_types.
7404
+ if hir_value.is_a?(HIR::Instruction) && hir_value.result_var
7405
+ type_tag = @variable_types[hir_value.result_var]
7406
+ return type_tag if type_tag == :i64 || type_tag == :double
7407
+ end
7408
+
7375
7409
  # Check HIR type
7376
7410
  hir_type = get_type(hir_value)
7377
7411
  if integer_type?(hir_type)
@@ -8469,6 +8503,7 @@ module Konpeito
8469
8503
  saved_block_callback_self = @block_callback_self
8470
8504
  saved_rescue_escape_array = @rescue_escape_array
8471
8505
  saved_rescue_escape_indices = @rescue_escape_indices
8506
+ saved_gc_arena_alloca = @mruby_gc_arena_alloca
8472
8507
 
8473
8508
  # In normal mode: params[0] = self. In escape mode, set @block_callback_self after unpacking.
8474
8509
  @block_callback_self = callback_func.params[0] unless escape_var_names
@@ -78,13 +78,17 @@ module Konpeito
78
78
  # Generate license file alongside the executable
79
79
  generate_license_file
80
80
  ensure
81
- # Clean up intermediate files
81
+ # Clean up intermediate files (keep .ll for debugging if ENV['KONPEITO_KEEP_IR'] is set)
82
+ keep_ir = ENV['KONPEITO_KEEP_IR']
82
83
  all_temps = [ir_file, obj_file, init_c_file, init_obj_file, helpers_obj_file] + extra_obj_files
83
84
  all_temps.each do |f|
85
+ next if keep_ir && f&.end_with?('.ll')
84
86
  FileUtils.rm_f(f) if f && File.exist?(f)
85
87
  end
86
88
  # Also clean optimized IR if it was generated
87
- FileUtils.rm_f("#{ir_file}.opt.ll") if File.exist?("#{ir_file}.opt.ll")
89
+ unless keep_ir
90
+ FileUtils.rm_f("#{ir_file}.opt.ll") if File.exist?("#{ir_file}.opt.ll")
91
+ end
88
92
  end
89
93
  end
90
94
 
@@ -302,7 +306,7 @@ module Konpeito
302
306
 
303
307
  arity = llvm_generator.variadic_functions[mangled_name] ? -1 : func.params.size - 1
304
308
  wrapper = "mrb_wrap_#{mangled_name}"
305
- mrb_args = arity_to_mrb_args(arity)
309
+ mrb_args = effective_mrb_args(mangled_name, arity)
306
310
  lines << " mrb_define_method(mrb, #{module_var}, \"#{method_name}\", #{wrapper}, #{mrb_args});"
307
311
  end
308
312
 
@@ -316,7 +320,7 @@ module Konpeito
316
320
 
317
321
  arity = llvm_generator.variadic_functions[mangled_name] ? -1 : func.params.size - 1
318
322
  wrapper = "mrb_wrap_#{mangled_name}"
319
- mrb_args = arity_to_mrb_args(arity)
323
+ mrb_args = effective_mrb_args(mangled_name, arity)
320
324
  lines << " mrb_define_class_method(mrb, #{module_var}, \"#{method_name}\", #{wrapper}, #{mrb_args});"
321
325
  end
322
326
 
@@ -372,7 +376,7 @@ module Konpeito
372
376
 
373
377
  arity = llvm_generator.variadic_functions[mangled_name] ? -1 : func.params.size - 1
374
378
  wrapper = "mrb_wrap_#{mangled_name}"
375
- mrb_args = arity_to_mrb_args(arity)
379
+ mrb_args = effective_mrb_args(mangled_name, arity)
376
380
  lines << " mrb_define_method(mrb, #{class_var}, \"#{method_name}\", #{wrapper}, #{mrb_args});"
377
381
  end
378
382
 
@@ -386,7 +390,7 @@ module Konpeito
386
390
 
387
391
  arity = llvm_generator.variadic_functions[mangled_name] ? -1 : func.params.size - 1
388
392
  wrapper = "mrb_wrap_#{mangled_name}"
389
- mrb_args = arity_to_mrb_args(arity)
393
+ mrb_args = effective_mrb_args(mangled_name, arity)
390
394
  lines << " mrb_define_class_method(mrb, #{class_var}, \"#{method_name}\", #{wrapper}, #{mrb_args});"
391
395
  end
392
396
 
@@ -408,7 +412,7 @@ module Konpeito
408
412
 
409
413
  arity = llvm_generator.variadic_functions[mangled_name] ? -1 : func.params.size - 1
410
414
  wrapper = "mrb_wrap_#{mangled_name}"
411
- mrb_args = arity_to_mrb_args(arity)
415
+ mrb_args = effective_mrb_args(mangled_name, arity)
412
416
  lines << " mrb_define_method(mrb, mrb->object_class, \"#{func_def.name}\", #{wrapper}, #{mrb_args});"
413
417
  end
414
418
 
@@ -456,13 +460,14 @@ module Konpeito
456
460
  else
457
461
  arity = llvm_func.params.size - 1 # Subtract self
458
462
 
459
- # Check if this function has only keyword args (no positional params).
460
- # If so, the kwargs hash arg is optional — caller may pass 0 args
461
- # when no keywords are specified (e.g., `footer do ... end`).
463
+ # Check if this function has keyword args.
464
+ # The kwargs hash arg should be optional — caller may pass only
465
+ # positional args when no keywords are specified.
462
466
  func_base_name = mangled_name.sub(/\Arn_/, "")
463
467
  kw_info = llvm_generator.keyword_param_functions[func_base_name] ||
464
468
  llvm_generator.keyword_param_functions[func_base_name.to_sym]
465
469
  kwargs_only = kw_info && kw_info[:regular_count] == 0 && arity == 1
470
+ has_mixed_kwargs = kw_info && kw_info[:regular_count] > 0 && arity > 1
466
471
 
467
472
  lines << "static mrb_value #{wrapper_name}(mrb_state *mrb, mrb_value self) {"
468
473
  if kwargs_only
@@ -470,6 +475,15 @@ module Konpeito
470
475
  lines << " mrb_value a0 = mrb_nil_value(), _block = mrb_nil_value();"
471
476
  lines << " mrb_get_args(mrb, \"|o&\", &a0, &_block);"
472
477
  lines << " if (mrb_nil_p(a0)) a0 = mrb_hash_new(mrb);"
478
+ elsif has_mixed_kwargs
479
+ # Positional args + optional kwargs hash — last arg defaults to empty hash
480
+ kw_hash_idx = arity - 1
481
+ req_count = kw_info[:regular_count]
482
+ lines << " mrb_value #{(0...arity).map { |i| "a#{i} = mrb_nil_value()" }.join(', ')}, _block = mrb_nil_value();"
483
+ format_str = "o" * req_count + "|o&"
484
+ args_list = (0...arity).map { |i| "&a#{i}" }.join(", ") + ", &_block"
485
+ lines << " mrb_get_args(mrb, \"#{format_str}\", #{args_list});"
486
+ lines << " if (mrb_nil_p(a#{kw_hash_idx})) a#{kw_hash_idx} = mrb_hash_new(mrb);"
473
487
  elsif arity > 0
474
488
  lines << " mrb_value #{(0...arity).map { |i| "a#{i}" }.join(', ')}, _block;"
475
489
  format_str = "o" * arity + "&"
@@ -804,6 +818,24 @@ module Konpeito
804
818
  end
805
819
  end
806
820
 
821
+ # Compute mrb_args for a function, accounting for optional keyword hash.
822
+ # Functions with keyword params can be called with or without the keyword
823
+ # hash argument, so the kwargs arg must be registered as optional.
824
+ def effective_mrb_args(mangled_name, arity)
825
+ return arity_to_mrb_args(arity) if arity <= 0
826
+
827
+ func_base_name = mangled_name.sub(/\Arn_/, "")
828
+ kw_info = llvm_generator.keyword_param_functions[func_base_name] ||
829
+ llvm_generator.keyword_param_functions[func_base_name.to_sym]
830
+ if kw_info
831
+ # Keyword hash is optional — use MRB_ARGS_ARG(required, optional)
832
+ req = kw_info[:regular_count]
833
+ "MRB_ARGS_ARG(#{req}, #{arity - req})"
834
+ else
835
+ arity_to_mrb_args(arity)
836
+ end
837
+ end
838
+
807
839
  def resolve_superclass_mrb_expr(superclass, non_native_classes)
808
840
  return "mrb->object_class" unless superclass
809
841
 
@@ -63,4 +63,20 @@ module Clay
63
63
  def self.sizing_percent() end
64
64
  def self.left_to_right() end
65
65
  def self.top_to_bottom() end
66
+
67
+ # Text Buffer System
68
+ def self.textbuf_clear(id) end
69
+ def self.textbuf_copy(dst, src) end
70
+ def self.textbuf_putchar(id, ch) end
71
+ def self.textbuf_backspace(id) end
72
+ def self.textbuf_delete(id) end
73
+ def self.textbuf_cursor_left(id) end
74
+ def self.textbuf_cursor_right(id) end
75
+ def self.textbuf_cursor_home(id) end
76
+ def self.textbuf_cursor_end(id) end
77
+ def self.textbuf_len(id) end
78
+ def self.textbuf_cursor(id) end
79
+ def self.textbuf_render(id, fid, fsz, r, g, b) end
80
+ def self.textbuf_render_range(id, start_pos, end_pos, fid, fsz, r, g, b) end
81
+ def self.text_char(ch, fid, fsz, r, g, b) end
66
82
  end
@@ -166,4 +166,48 @@ module Clay
166
166
 
167
167
  %a{cfunc: "konpeito_clay_top_to_bottom"}
168
168
  def self.top_to_bottom: () -> Integer
169
+
170
+ # ── Text Buffer System ──
171
+
172
+ %a{cfunc: "konpeito_clay_textbuf_clear"}
173
+ def self.textbuf_clear: (Integer id) -> void
174
+
175
+ %a{cfunc: "konpeito_clay_textbuf_copy"}
176
+ def self.textbuf_copy: (Integer dst, Integer src) -> void
177
+
178
+ %a{cfunc: "konpeito_clay_textbuf_putchar"}
179
+ def self.textbuf_putchar: (Integer id, Integer ch) -> void
180
+
181
+ %a{cfunc: "konpeito_clay_textbuf_backspace"}
182
+ def self.textbuf_backspace: (Integer id) -> void
183
+
184
+ %a{cfunc: "konpeito_clay_textbuf_delete"}
185
+ def self.textbuf_delete: (Integer id) -> void
186
+
187
+ %a{cfunc: "konpeito_clay_textbuf_cursor_left"}
188
+ def self.textbuf_cursor_left: (Integer id) -> void
189
+
190
+ %a{cfunc: "konpeito_clay_textbuf_cursor_right"}
191
+ def self.textbuf_cursor_right: (Integer id) -> void
192
+
193
+ %a{cfunc: "konpeito_clay_textbuf_cursor_home"}
194
+ def self.textbuf_cursor_home: (Integer id) -> void
195
+
196
+ %a{cfunc: "konpeito_clay_textbuf_cursor_end"}
197
+ def self.textbuf_cursor_end: (Integer id) -> void
198
+
199
+ %a{cfunc: "konpeito_clay_textbuf_len"}
200
+ def self.textbuf_len: (Integer id) -> Integer
201
+
202
+ %a{cfunc: "konpeito_clay_textbuf_cursor"}
203
+ def self.textbuf_cursor: (Integer id) -> Integer
204
+
205
+ %a{cfunc: "konpeito_clay_textbuf_render"}
206
+ def self.textbuf_render: (Integer id, Integer fid, Integer fsz, Float r, Float g, Float b) -> void
207
+
208
+ %a{cfunc: "konpeito_clay_textbuf_render_range"}
209
+ def self.textbuf_render_range: (Integer id, Integer start, Integer end_pos, Integer fid, Integer fsz, Float r, Float g, Float b) -> void
210
+
211
+ %a{cfunc: "konpeito_clay_text_char"}
212
+ def self.text_char: (Integer ch, Integer fid, Integer fsz, Float r, Float g, Float b) -> void
169
213
  end
@@ -275,6 +275,7 @@ void konpeito_clay_border(double r, double g, double b, double a,
275
275
  void konpeito_clay_scroll(int horizontal, int vertical) {
276
276
  g_decl.clip.horizontal = horizontal != 0;
277
277
  g_decl.clip.vertical = vertical != 0;
278
+ g_decl.clip.childOffset = Clay_GetScrollOffset();
278
279
  }
279
280
 
280
281
  /* Floating element */
@@ -491,3 +492,148 @@ int konpeito_clay_sizing_fixed(void) { return 2; }
491
492
  int konpeito_clay_sizing_percent(void){ return 3; }
492
493
  int konpeito_clay_left_to_right(void) { return 0; }
493
494
  int konpeito_clay_top_to_bottom(void) { return 1; }
495
+
496
+ /* ═══════════════════════════════════════════
497
+ * Text Buffer System
498
+ * ═══════════════════════════════════════════
499
+ * 8 independent text buffers for text_input widgets.
500
+ * Each buffer holds up to 255 characters (256 bytes including null).
501
+ * Buffer operations are GC-free — no mruby String allocation.
502
+ */
503
+
504
+ #define TEXTBUF_COUNT 8
505
+ #define TEXTBUF_SIZE 256
506
+
507
+ static char g_textbufs[TEXTBUF_COUNT][TEXTBUF_SIZE];
508
+ static int g_textbuf_lens[TEXTBUF_COUNT];
509
+ static int g_textbuf_cursors[TEXTBUF_COUNT];
510
+
511
+ void konpeito_clay_textbuf_clear(int id) {
512
+ if (id < 0 || id >= TEXTBUF_COUNT) return;
513
+ g_textbufs[id][0] = '\0';
514
+ g_textbuf_lens[id] = 0;
515
+ g_textbuf_cursors[id] = 0;
516
+ }
517
+
518
+ void konpeito_clay_textbuf_copy(int dst, int src) {
519
+ if (dst < 0 || dst >= TEXTBUF_COUNT) return;
520
+ if (src < 0 || src >= TEXTBUF_COUNT) return;
521
+ int len = g_textbuf_lens[src];
522
+ for (int i = 0; i <= len; i++) {
523
+ g_textbufs[dst][i] = g_textbufs[src][i];
524
+ }
525
+ g_textbuf_lens[dst] = len;
526
+ g_textbuf_cursors[dst] = len;
527
+ }
528
+
529
+ void konpeito_clay_textbuf_putchar(int id, int ch) {
530
+ if (id < 0 || id >= TEXTBUF_COUNT) return;
531
+ int len = g_textbuf_lens[id];
532
+ int cur = g_textbuf_cursors[id];
533
+ if (len >= TEXTBUF_SIZE - 1) return;
534
+ for (int i = len; i > cur; i--) {
535
+ g_textbufs[id][i] = g_textbufs[id][i - 1];
536
+ }
537
+ g_textbufs[id][cur] = (char)ch;
538
+ g_textbuf_lens[id] = len + 1;
539
+ g_textbuf_cursors[id] = cur + 1;
540
+ g_textbufs[id][len + 1] = '\0';
541
+ }
542
+
543
+ void konpeito_clay_textbuf_backspace(int id) {
544
+ if (id < 0 || id >= TEXTBUF_COUNT) return;
545
+ int cur = g_textbuf_cursors[id];
546
+ int len = g_textbuf_lens[id];
547
+ if (cur <= 0) return;
548
+ for (int i = cur - 1; i < len - 1; i++) {
549
+ g_textbufs[id][i] = g_textbufs[id][i + 1];
550
+ }
551
+ g_textbuf_lens[id] = len - 1;
552
+ g_textbuf_cursors[id] = cur - 1;
553
+ g_textbufs[id][len - 1] = '\0';
554
+ }
555
+
556
+ void konpeito_clay_textbuf_delete(int id) {
557
+ if (id < 0 || id >= TEXTBUF_COUNT) return;
558
+ int cur = g_textbuf_cursors[id];
559
+ int len = g_textbuf_lens[id];
560
+ if (cur >= len) return;
561
+ for (int i = cur; i < len - 1; i++) {
562
+ g_textbufs[id][i] = g_textbufs[id][i + 1];
563
+ }
564
+ g_textbuf_lens[id] = len - 1;
565
+ g_textbufs[id][len - 1] = '\0';
566
+ }
567
+
568
+ void konpeito_clay_textbuf_cursor_left(int id) {
569
+ if (id < 0 || id >= TEXTBUF_COUNT) return;
570
+ if (g_textbuf_cursors[id] > 0) g_textbuf_cursors[id]--;
571
+ }
572
+
573
+ void konpeito_clay_textbuf_cursor_right(int id) {
574
+ if (id < 0 || id >= TEXTBUF_COUNT) return;
575
+ if (g_textbuf_cursors[id] < g_textbuf_lens[id]) g_textbuf_cursors[id]++;
576
+ }
577
+
578
+ void konpeito_clay_textbuf_cursor_home(int id) {
579
+ if (id < 0 || id >= TEXTBUF_COUNT) return;
580
+ g_textbuf_cursors[id] = 0;
581
+ }
582
+
583
+ void konpeito_clay_textbuf_cursor_end(int id) {
584
+ if (id < 0 || id >= TEXTBUF_COUNT) return;
585
+ g_textbuf_cursors[id] = g_textbuf_lens[id];
586
+ }
587
+
588
+ int konpeito_clay_textbuf_len(int id) {
589
+ if (id < 0 || id >= TEXTBUF_COUNT) return 0;
590
+ return g_textbuf_lens[id];
591
+ }
592
+
593
+ int konpeito_clay_textbuf_cursor(int id) {
594
+ if (id < 0 || id >= TEXTBUF_COUNT) return 0;
595
+ return g_textbuf_cursors[id];
596
+ }
597
+
598
+ void konpeito_clay_textbuf_render(int id, int fid, int fsz, double r, double g, double b) {
599
+ if (id < 0 || id >= TEXTBUF_COUNT) return;
600
+ if (g_textbuf_lens[id] == 0) return;
601
+ flush_config();
602
+ Clay_TextElementConfig cfg = {
603
+ .textColor = {(float)r, (float)g, (float)b, 255.0f},
604
+ .fontId = (uint16_t)fid, .fontSize = (uint16_t)fsz, .wrapMode = 0
605
+ };
606
+ Clay_String cs = make_string(g_textbufs[id]);
607
+ Clay__OpenTextElement(cs, Clay__StoreTextElementConfig(cfg));
608
+ }
609
+
610
+ void konpeito_clay_textbuf_render_range(int id, int start, int end, int fid, int fsz,
611
+ double r, double g, double b) {
612
+ if (id < 0 || id >= TEXTBUF_COUNT) return;
613
+ int len = g_textbuf_lens[id];
614
+ if (start >= len || start >= end) return;
615
+ if (end > len) end = len;
616
+ int range_len = end - start;
617
+ flush_config();
618
+ const char *pooled = pool_string(g_textbufs[id] + start, range_len);
619
+ Clay_String cs = {.isStaticallyAllocated = false, .length = range_len, .chars = pooled};
620
+ Clay_TextElementConfig cfg = {
621
+ .textColor = {(float)r, (float)g, (float)b, 255.0f},
622
+ .fontId = (uint16_t)fid, .fontSize = (uint16_t)fsz, .wrapMode = 0
623
+ };
624
+ Clay__OpenTextElement(cs, Clay__StoreTextElementConfig(cfg));
625
+ }
626
+
627
+ void konpeito_clay_text_char(int ch, int fid, int fsz, double r, double g, double b) {
628
+ if (ch < 32 || ch > 126) return;
629
+ char buf[2];
630
+ buf[0] = (char)ch;
631
+ buf[1] = '\0';
632
+ flush_config();
633
+ Clay_TextElementConfig cfg = {
634
+ .textColor = {(float)r, (float)g, (float)b, 255.0f},
635
+ .fontId = (uint16_t)fid, .fontSize = (uint16_t)fsz, .wrapMode = 0
636
+ };
637
+ Clay_String cs = make_string(buf);
638
+ Clay__OpenTextElement(cs, Clay__StoreTextElementConfig(cfg));
639
+ }
@@ -109,4 +109,45 @@ module ClayTUI
109
109
 
110
110
  # Color Helper
111
111
  def self.rgb(r, g, b) end
112
+
113
+ # Extended Key Constants
114
+ def self.key_delete() end
115
+ def self.key_home() end
116
+ def self.key_end() end
117
+ def self.key_pgup() end
118
+ def self.key_pgdn() end
119
+ def self.key_f1() end
120
+ def self.key_f2() end
121
+ def self.key_f3() end
122
+ def self.key_f4() end
123
+ def self.key_f5() end
124
+ def self.key_f6() end
125
+ def self.key_f7() end
126
+ def self.key_f8() end
127
+ def self.key_f9() end
128
+ def self.key_f10() end
129
+ def self.key_f11() end
130
+ def self.key_f12() end
131
+
132
+ # Modifier Keys
133
+ def self.event_mod() end
134
+ def self.mod_alt() end
135
+ def self.mod_ctrl() end
136
+ def self.mod_shift() end
137
+
138
+ # Text Buffer System
139
+ def self.textbuf_clear(id) end
140
+ def self.textbuf_copy(dst, src) end
141
+ def self.textbuf_putchar(id, ch) end
142
+ def self.textbuf_backspace(id) end
143
+ def self.textbuf_delete(id) end
144
+ def self.textbuf_cursor_left(id) end
145
+ def self.textbuf_cursor_right(id) end
146
+ def self.textbuf_cursor_home(id) end
147
+ def self.textbuf_cursor_end(id) end
148
+ def self.textbuf_len(id) end
149
+ def self.textbuf_cursor(id) end
150
+ def self.textbuf_render(id, r, g, b) end
151
+ def self.textbuf_render_range(id, start_pos, end_pos, r, g, b) end
152
+ def self.text_char(ch, r, g, b) end
112
153
  end
@@ -255,4 +255,115 @@ module ClayTUI
255
255
  # Build a truecolor value from RGB components
256
256
  %a{cfunc: "konpeito_clay_tui_rgb"}
257
257
  def self.rgb: (Integer r, Integer g, Integer b) -> Integer
258
+
259
+ # ── Extended Key Constants ──
260
+
261
+ %a{cfunc: "konpeito_clay_tui_key_delete"}
262
+ def self.key_delete: () -> Integer
263
+
264
+ %a{cfunc: "konpeito_clay_tui_key_home"}
265
+ def self.key_home: () -> Integer
266
+
267
+ %a{cfunc: "konpeito_clay_tui_key_end"}
268
+ def self.key_end: () -> Integer
269
+
270
+ %a{cfunc: "konpeito_clay_tui_key_pgup"}
271
+ def self.key_pgup: () -> Integer
272
+
273
+ %a{cfunc: "konpeito_clay_tui_key_pgdn"}
274
+ def self.key_pgdn: () -> Integer
275
+
276
+ %a{cfunc: "konpeito_clay_tui_key_f1"}
277
+ def self.key_f1: () -> Integer
278
+
279
+ %a{cfunc: "konpeito_clay_tui_key_f2"}
280
+ def self.key_f2: () -> Integer
281
+
282
+ %a{cfunc: "konpeito_clay_tui_key_f3"}
283
+ def self.key_f3: () -> Integer
284
+
285
+ %a{cfunc: "konpeito_clay_tui_key_f4"}
286
+ def self.key_f4: () -> Integer
287
+
288
+ %a{cfunc: "konpeito_clay_tui_key_f5"}
289
+ def self.key_f5: () -> Integer
290
+
291
+ %a{cfunc: "konpeito_clay_tui_key_f6"}
292
+ def self.key_f6: () -> Integer
293
+
294
+ %a{cfunc: "konpeito_clay_tui_key_f7"}
295
+ def self.key_f7: () -> Integer
296
+
297
+ %a{cfunc: "konpeito_clay_tui_key_f8"}
298
+ def self.key_f8: () -> Integer
299
+
300
+ %a{cfunc: "konpeito_clay_tui_key_f9"}
301
+ def self.key_f9: () -> Integer
302
+
303
+ %a{cfunc: "konpeito_clay_tui_key_f10"}
304
+ def self.key_f10: () -> Integer
305
+
306
+ %a{cfunc: "konpeito_clay_tui_key_f11"}
307
+ def self.key_f11: () -> Integer
308
+
309
+ %a{cfunc: "konpeito_clay_tui_key_f12"}
310
+ def self.key_f12: () -> Integer
311
+
312
+ # ── Modifier Keys ──
313
+
314
+ %a{cfunc: "konpeito_clay_tui_event_mod"}
315
+ def self.event_mod: () -> Integer
316
+
317
+ %a{cfunc: "konpeito_clay_tui_mod_alt"}
318
+ def self.mod_alt: () -> Integer
319
+
320
+ %a{cfunc: "konpeito_clay_tui_mod_ctrl"}
321
+ def self.mod_ctrl: () -> Integer
322
+
323
+ %a{cfunc: "konpeito_clay_tui_mod_shift"}
324
+ def self.mod_shift: () -> Integer
325
+
326
+ # ── Text Buffer System ──
327
+
328
+ %a{cfunc: "konpeito_clay_tui_textbuf_clear"}
329
+ def self.textbuf_clear: (Integer id) -> void
330
+
331
+ %a{cfunc: "konpeito_clay_tui_textbuf_copy"}
332
+ def self.textbuf_copy: (Integer dst, Integer src) -> void
333
+
334
+ %a{cfunc: "konpeito_clay_tui_textbuf_putchar"}
335
+ def self.textbuf_putchar: (Integer id, Integer ch) -> void
336
+
337
+ %a{cfunc: "konpeito_clay_tui_textbuf_backspace"}
338
+ def self.textbuf_backspace: (Integer id) -> void
339
+
340
+ %a{cfunc: "konpeito_clay_tui_textbuf_delete"}
341
+ def self.textbuf_delete: (Integer id) -> void
342
+
343
+ %a{cfunc: "konpeito_clay_tui_textbuf_cursor_left"}
344
+ def self.textbuf_cursor_left: (Integer id) -> void
345
+
346
+ %a{cfunc: "konpeito_clay_tui_textbuf_cursor_right"}
347
+ def self.textbuf_cursor_right: (Integer id) -> void
348
+
349
+ %a{cfunc: "konpeito_clay_tui_textbuf_cursor_home"}
350
+ def self.textbuf_cursor_home: (Integer id) -> void
351
+
352
+ %a{cfunc: "konpeito_clay_tui_textbuf_cursor_end"}
353
+ def self.textbuf_cursor_end: (Integer id) -> void
354
+
355
+ %a{cfunc: "konpeito_clay_tui_textbuf_len"}
356
+ def self.textbuf_len: (Integer id) -> Integer
357
+
358
+ %a{cfunc: "konpeito_clay_tui_textbuf_cursor"}
359
+ def self.textbuf_cursor: (Integer id) -> Integer
360
+
361
+ %a{cfunc: "konpeito_clay_tui_textbuf_render"}
362
+ def self.textbuf_render: (Integer id, Integer r, Integer g, Integer b) -> void
363
+
364
+ %a{cfunc: "konpeito_clay_tui_textbuf_render_range"}
365
+ def self.textbuf_render_range: (Integer id, Integer start, Integer end_pos, Integer r, Integer g, Integer b) -> void
366
+
367
+ %a{cfunc: "konpeito_clay_tui_text_char"}
368
+ def self.text_char: (Integer ch, Integer r, Integer g, Integer b) -> void
258
369
  end