kojix2 / LibUI

A portable GUI library for Ruby
MIT License
203 stars 10 forks source link

Table Selection API #69

Open AndyObtiva opened 1 year ago

AndyObtiva commented 1 year ago

libui-ng recently merged a table selection API: https://github.com/libui-ng/libui-ng/pull/73

Could you please provide FFI bindings for that API in Ruby so that I could expose in Glimmer DSL for LibUI's GUI DSL?

I got a request about it from a user over here: https://github.com/AndyObtiva/glimmer-dsl-libui/issues/41

You could make a small release with a newer version of libui-ng and the table selection API only. Afterwards, you could continue to make small releases that add more and more FFI bindings in Ruby for other APIs that got added to libui-ng recently. But, to start, the table selection API should be enough for me to be able to expose it and make a release in Glimmer DSL for LibUI

kojix2 commented 1 year ago

Hello I have added recent libui functions and structures. I have not checked that it works. However, very few people seem to be using the pre-release version, so I have released it. Before I forget this issue exists. Please let me know if you have any problems. Thank you very much.

https://github.com/kojix2/LibUI/commit/921157e4b4dd6c8bc193e6e32dc48dd442372be3

AndyObtiva commented 1 year ago

Thank you! I missed those new table methods the last time I supported a pre version of your bindings.

But, my code crashes whenever I try to use any of the new table listeners:

Perhaps their new API differs from the older common API for listeners?

Could you please share code using the listeners above that works using LibUI alone so that I could translate it to Glimmer DSL for LibUI code (please include table_header_on_clicked too in your example)?

kojix2 commented 1 year ago

Sure. If you use a block to receive arguments, it works like this.

require 'libui'

UI = LibUI

UI.init

main_window = UI.new_window('Animal sounds', 300, 200, 1)

hbox = UI.new_horizontal_box
UI.window_set_child(main_window, hbox)

data = [
  %w[cat meow],
  %w[dog woof],
  %w[chicken cock-a-doodle-doo],
  %w[horse neigh],
  %w[cow moo]
]

# Protects BlockCaller objects from garbage collection.
@block_callers = []
def rbcallback(*args, &block)
  args << [0] if args.size == 1 # Argument types are ommited
  block_caller = Fiddle::Closure::BlockCaller.new(*args, &block)
  @block_callers << block_caller
  block_caller
end

model_handler = UI::FFI::TableModelHandler.malloc
model_handler.to_ptr.free = Fiddle::RUBY_FREE
model_handler.NumColumns   = rbcallback(4) { 2 }
model_handler.ColumnType   = rbcallback(4) { 0 }
model_handler.NumRows      = rbcallback(4) { 5 }
model_handler.CellValue    = rbcallback(1, [1, 1, 4, 4]) do |_, _, row, column|
  UI.new_table_value_string(data[row][column])
end
model_handler.SetCellValue = rbcallback(0, [0]) {}

model = UI.new_table_model(model_handler)

table_params = UI::FFI::TableParams.malloc
table_params.to_ptr.free = Fiddle::RUBY_FREE
table_params.Model = model
table_params.RowBackgroundColorModelColumn = -1

table = UI.new_table(table_params)
UI.table_append_text_column(table, 'Animal', 0, -1)
UI.table_append_text_column(table, 'Description', 1, -1)
UI.table_set_selection_mode(table, UI::TableSelectionModeZeroOrMany)

UI.table_on_row_clicked(table) do |_, row_idx| 
  puts "#{data[row_idx][0]} \"#{data[row_idx][1]}\""
end

UI.table_on_row_double_clicked(table) do |_, row_idx|
  puts "#{data[row_idx][0]} \"#{data[row_idx][1]}!!\"".upcase
end

UI.table_on_selection_changed(table) do |ptr|
  tsp = UI.table_get_selection(ptr)
  ts = UI::FFI::TableSelection.new(tsp)
  if ts.NumRows > 0 # 
    p selected: ts.Rows[0, Fiddle::SIZEOF_INT * ts.NumRows].unpack("i*")
  end
  UI.free_table_selection(tsp)
end

UI.table_header_on_clicked(table) do |_, col_idx|
  puts col_idx
end

UI.box_append(hbox, table, 1)
UI.control_show(main_window)

UI.window_on_closing(main_window) do
  puts 'Bye Bye'
  UI.control_destroy(main_window)
  UI.free_table_model(model)
  UI.quit
  0
end

UI.main
UI.quit

It's not like Ruby, it's terrible code...

rubyFeedback commented 1 year ago

very few people seem to be using the pre-release version

This may be because it can be a hassle to test things that are unstable or semi-stable.

Personally I actually never (or rather almost never) use pre-release versions. I much prefer using "stable" releases but keep on updating within that tag, if possible. So for instance:

Version: 10.1 Version: 10.2 Version: 10.3

and so forth.

t's not like Ruby, it's terrible code...

Yeah. C. :)

Ruby is like a prettier syntactic sugar over C at the end of the day.

I should have learned C first - after having learned Ruby, C seems so messy in comparison ...

AndyObtiva commented 1 year ago

Thank you very much for the quick response @kojix2 . I will try to run the code on my machine to support it in Glimmer DSL for LibUI.

It's not like Ruby, it's terrible code...

It is great code for the middle architectural layer (binding) between high-level Ruby code and low-level C code. Thanks again. I will test the code when I get a chance and close this issue again if it ends up working for me.

AndyObtiva commented 1 year ago

OK, the code works, and I got it working in Glimmer DSL for LibUI locally too.

Thanks again.

I am closing this.

AndyObtiva commented 1 year ago

One more thing. @kojix2 could you please provide code examples of setting selection on a table (LibUI.table_set_selection), including both:

And, if you could cover the following with an example (just in case), it would be helpful too:

AndyObtiva commented 1 year ago

OK, I figured out how to set selection by first building a selection struct:

          ts = ::LibUI::FFI::TableSelection.malloc
          ts.NumRows = value.is_a?(Array) ? value.size : 1
          ts.Rows = [value].flatten.pack('i*')

So, I'm good for now as far as selection.

I already made a release for Glimmer DSL for LibUI that supports Table Selection API in version 0.0.7: https://rubygems.org/gems/glimmer-dsl-libui/versions/0.7.0

There is a new example too: https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/examples/basic_table_selection.rb

Screen Shot 2023-03-11 at 5 51 47 PM

I need to add support for the table column listener next: LibUI.table_header_on_clicked

kojix2 commented 1 year ago

I tried a new example. It's amazing!


ChatGPT's solution appears to be incorrect and has been removed!

kojix2 commented 1 year ago

I think the following is one solution. It is not that good, though.

https://github.com/kojix2/LibUI/commit/59f757cd0f0d8e393463d5ec0eba1d3ac8af126e

In the new version of Fiddle

font_descriptor = UI::FFI::FontDescriptor.malloc(Fiddle::RUBY_FREE)

is supported. However, this may not work with the old Fiddle that is included with older Ruby.

kojix2 commented 1 year ago

I see. uiTableSelection

typedef struct uiTableSelection uiTableSelection;
struct uiTableSelection
{
    int NumRows; //! < Number of selected rows.
    int *Rows; //! < Array containing selected row indices, NULL on empty selection.
};

is only a part of the allocated memory. Metadata is allocated before Rows. uiFreeTableSelection frees this metadata.

If you try to free a FFI::TableSelection allocated by Ruby using uiFreeTableSelection, you will get an error, which seems to be caused by the metadata not existing.

AndyObtiva commented 1 year ago

I definitely got an error every time I tried to free the table selection struct memory. If you could figure that out too, that would be great.

Could it be that the LibUI table automatically frees the memory of the table selection struct as soon as we set the selection on the table? If that is true, then perhaps that is the reason why we can’t free the memory afterwards.