Wednesday, January 11, 2023

Glimmer DSL for LibUI Table Lazy Loading

Glimmer DSL for LibUI 0.6.1 has been released with support for table lazy loading via Enumerator (or Enumerator::Lazy). As a result, the table control can now handle millions of rows and renders instantly without waiting for all data to be loaded given that it is loaded lazily as the user scrolls through the table. That enables applications with a lot of data (or with data that needs to be loaded/generated) to start instantly. A new example, Lazy Table (4 versions), has been included to demonstrate table lazy loading (read on to learn more about it). Of course, from a usability and user experience point of view, it might still not be a good idea to display millions of rows, yet to display only a few via pagination, like by using the Refined Table custom control that was released and blogged about a while ago.

Glimmer DSL for LibUI 0.6.0 was a final release that included many changes implemented as pre versions previously, including a new C libui version (libui-ng Nov 13, 2022), which includes some low-level fixes and new features for the libui GUI toolkit.


Change Log (both 0.6.1 & 0.6.0):

  • examples/lazy_table.rb (4 versions) table lazy loading with a million rows via Enumerator or Enumerator::Lazy to enable instant app startup time
  • Support table cell_rows implicit data-binding to a collection of models (only supported an array of arrays before in implicit data-binding)
  • Upgrade to libui Ruby gem version 0.1.0.pre.0, which includes a newer C libui alpha release (libui-ng Nov 13, 2022)
  • Support table cell_rows Enumerator or Enumerator::Lazy value to do lazy loading of data upon display of rows instead of immediate loading of all table data, thus improving performance of table initial render for very large datasets
  • Fix issue with table progress_bar_column not getting updated successfully on Windows if there were dual or triple columns before it.
  • Fix issue with table progress_bar_column not getting updated successfully on Windows if data-binding table to an array of models instead of an array of arrays
  • Fix issue with table checkbox_column checkbox editing not working in Mac by including a new C libui-ng release
  • Fix issue with table checkbox_text_column checkbox editing not working in Mac or Windows by including a new C libui-ng release
  • Update examples/basic_table_checkbox.rb to enable editing checkbox values
  • Update examples/basic_table_checkbox_text.rb to enable editing checkbox/text values
  • [final] Optimize table scrolling performance when having many rows and columns (prevent recalculation of expanded_cell_rows on every cell evaluation). Resolve: #38
  • [final] refined_table pagination: false option to disable pagination, but keep filtering.
  • [final] Fix issue with Glimmer::LibUI::interpret_color support for [r, g, b, a] Array-based colors, returning [r, g, b] only without alpha value
  • [final] Fix issue "Cannot add rows to a table that started empty": #36
  • [final] window #open method as alias to #show
  • [final] window #focused? read-only property
  • [final] Document window on_focus_changed listener
  • [final] Update examples/basic_child_window.rb to demo on_focus_changed and focused?
  • [final] open_folder support
  • [final] examples/control_gallery.rb now includes an "Open Folder" File menu item


Lazy Table example:



Lazy Table (using a well encapsulated Enumerator subclass)
# From: https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#lazy-table
require 'glimmer-dsl-libui'
class LazyTable
Contact = Struct.new(:name, :email, :phone, :city, :state)
# Extending Enumerator enables building a collection generator in an encapsulated maintainable fashion
class ContactGenerator < Enumerator
NAMES_FIRST = %w[
Liam Noah William James Oliver Benjamin Elijah Lucas Mason Logan Alexander Ethan Jacob Michael Daniel Henry Jackson Sebastian
Aiden Matthew Samuel David Joseph Carter Owen Wyatt John Jack Luke Jayden Dylan Grayson Levi Isaac Gabriel Julian Mateo
Anthony Jaxon Lincoln Joshua Christopher Andrew Theodore Caleb Ryan Asher Nathan Thomas Leo Isaiah Charles Josiah Hudson
Christian Hunter Connor Eli Ezra Aaron Landon Adrian Jonathan Nolan Jeremiah Easton Elias Colton Cameron Carson Robert Angel
Maverick Nicholas Dominic Jaxson Greyson Adam Ian Austin Santiago Jordan Cooper Brayden Roman Evan Ezekiel Xaviar Jose Jace
Jameson Leonardo Axel Everett Kayden Miles Sawyer Jason Emma Olivia Bartholomew Corey Danielle Eva Felicity
]
NAMES_LAST = %w[
Smith Johnson Williams Brown Jones Miller Davis Wilson Anderson Taylor George Harrington Iverson Jackson Korby Levinson
]
CITIES = [
'Bellesville', 'Lombardia', 'Steepleton', 'Deerenstein', 'Schwartz', 'Hollandia', 'Saint Pete', 'Grandville', 'London',
'Berlin', 'Elktown', 'Paris', 'Garrison', 'Muncy', 'St Louis',
]
STATES = [ 'AK', 'AL', 'AR', 'AZ', 'CA', 'CO', 'CT', 'DC', 'DE', 'FL', 'GA',
'HI', 'IA', 'ID', 'IL', 'IN', 'KS', 'KY', 'LA', 'MA', 'MD', 'ME',
'MI', 'MN', 'MO', 'MS', 'MT', 'NC', 'ND', 'NE', 'NH', 'NJ', 'NM',
'NV', 'NY', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC', 'SD', 'TN', 'TX',
'UT', 'VA', 'VT', 'WA', 'WI', 'WV', 'WY']
def initialize(contact_count)
# Make sure to pass super constructor size argument as it gets used by Glimmer DSL for LibUI
# to determine the number of rows in the table before generating all its data
super(contact_count) do |yielder|
contact_count.times do |index|
# Data will get lazy loaded into the table as the user scrolls through.
# After data is built, it is cached long-term, till updating table `cell_rows`.
yielder << contact_for(index)
end
end
end
def contact_for(index)
number = index + 1
first_name = NAMES_FIRST.sample
last_name = NAMES_LAST.sample
phone = 10.times.map { rand(10) }.yield_self { |numbers| [numbers[0..2], numbers[3..5], numbers[6..9]].map(&:join).join('-') }
city = CITIES.sample
state = STATES.sample
Contact.new("#{first_name} #{last_name}", "#{first_name.downcase}#{number}@#{last_name.downcase}.com", phone, city, state)
end
end
include Glimmer::LibUI::Application
body {
window("1,000,000 Lazy Loaded Contacts", 600, 700) {
margined true
vertical_box {
table {
text_column('Name')
text_column('Email')
text_column('Phone')
text_column('City')
text_column('State')
cell_rows ContactGenerator.new(1_000_000)
}
}
}
}
end
LazyTable.launch

Lazy Table (using a well encapsulated Enumerator::Lazy Subclass)
# From: https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#lazy-table
require 'glimmer-dsl-libui'
class LazyTable
Contact = Struct.new(:name, :email, :phone, :city, :state)
# Extending Enumerator::Lazy enables building a collection generator in an encapsulated maintainable fashion
class ContactGenerator < Enumerator::Lazy
NAMES_FIRST = %w[
Liam Noah William James Oliver Benjamin Elijah Lucas Mason Logan Alexander Ethan Jacob Michael Daniel Henry Jackson Sebastian
Aiden Matthew Samuel David Joseph Carter Owen Wyatt John Jack Luke Jayden Dylan Grayson Levi Isaac Gabriel Julian Mateo
Anthony Jaxon Lincoln Joshua Christopher Andrew Theodore Caleb Ryan Asher Nathan Thomas Leo Isaiah Charles Josiah Hudson
Christian Hunter Connor Eli Ezra Aaron Landon Adrian Jonathan Nolan Jeremiah Easton Elias Colton Cameron Carson Robert Angel
Maverick Nicholas Dominic Jaxson Greyson Adam Ian Austin Santiago Jordan Cooper Brayden Roman Evan Ezekiel Xaviar Jose Jace
Jameson Leonardo Axel Everett Kayden Miles Sawyer Jason Emma Olivia Bartholomew Corey Danielle Eva Felicity
]
NAMES_LAST = %w[
Smith Johnson Williams Brown Jones Miller Davis Wilson Anderson Taylor George Harrington Iverson Jackson Korby Levinson
]
CITIES = [
'Bellesville', 'Lombardia', 'Steepleton', 'Deerenstein', 'Schwartz', 'Hollandia', 'Saint Pete', 'Grandville', 'London',
'Berlin', 'Elktown', 'Paris', 'Garrison', 'Muncy', 'St Louis',
]
STATES = [ 'AK', 'AL', 'AR', 'AZ', 'CA', 'CO', 'CT', 'DC', 'DE', 'FL', 'GA',
'HI', 'IA', 'ID', 'IL', 'IN', 'KS', 'KY', 'LA', 'MA', 'MD', 'ME',
'MI', 'MN', 'MO', 'MS', 'MT', 'NC', 'ND', 'NE', 'NH', 'NJ', 'NM',
'NV', 'NY', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC', 'SD', 'TN', 'TX',
'UT', 'VA', 'VT', 'WA', 'WI', 'WV', 'WY']
def initialize(contact_count)
# Make sure to pass super constructor size (2nd) argument as it gets used by Glimmer DSL for LibUI
# to determine the number of rows in the table before generating all its data
super(contact_count.times, contact_count) do |yielder, index|
# Data will get lazy loaded into the table as the user scrolls through.
# After data is built, it is cached long-term, till updating table `cell_rows`.
yielder << contact_for(index)
end
end
def contact_for(index)
number = index + 1
first_name = NAMES_FIRST.sample
last_name = NAMES_LAST.sample
phone = 10.times.map { rand(10) }.yield_self { |numbers| [numbers[0..2], numbers[3..5], numbers[6..9]].map(&:join).join('-') }
city = CITIES.sample
state = STATES.sample
Contact.new("#{first_name} #{last_name}", "#{first_name.downcase}#{number}@#{last_name.downcase}.com", phone, city, state)
end
end
include Glimmer::LibUI::Application
body {
window("1,000,000 Lazy Loaded Contacts", 600, 700) {
margined true
vertical_box {
table {
text_column('Name')
text_column('Email')
text_column('Phone')
text_column('City')
text_column('State')
cell_rows ContactGenerator.new(1_000_000)
}
}
}
}
end
LazyTable.launch

Glimmer on!!!

3 comments:

daddie888 said...

I tried the sample code on both Linux and Windows, on Linux no problem but on Windows I get C:/Ruby/lib/ruby/gems/3.1.0/gems/glimmer-dsl-libui-0.6.1/lib/glimmer/libui/control_proxy/table_proxy.rb:515:in `block in apply_windows_fix': undefined method `<<' for #:each> (NoMethodError)

@cell_rows << new_row
^^
from C:/Ruby/lib/ruby/gems/3.1.0/gems/glimmer-dsl-libui-0.6.1/lib/glimmer/libui.rb:161:in `block in queue_main'
from C:/Ruby/lib/ruby/3.1.0/fiddle/closure.rb:45:in `call'
from C:/Ruby/lib/ruby/gems/3.1.0/gems/libui-0.1.0.pre.0-x64-mingw/lib/libui/ffi.rb:20:in `call'
from C:/Ruby/lib/ruby/gems/3.1.0/gems/libui-0.1.0.pre.0-x64-mingw/lib/libui/ffi.rb:20:in `uiMain'
from C:/Ruby/lib/ruby/gems/3.1.0/gems/libui-0.1.0.pre.0-x64-mingw/lib/libui/libui_base.rb:46:in `public_send'
from C:/Ruby/lib/ruby/gems/3.1.0/gems/libui-0.1.0.pre.0-x64-mingw/lib/libui/libui_base.rb:46:in `block (2 levels) in '
from C:/Ruby/lib/ruby/gems/3.1.0/gems/glimmer-dsl-libui-0.6.1/lib/glimmer/libui/control_proxy/window_proxy.rb:69:in `show'
from C:/Ruby/lib/ruby/gems/3.1.0/gems/glimmer-dsl-libui-0.6.1/lib/glimmer/libui/custom_window.rb:59:in `show'
from (eval):7:in `launch'
from C:/Box Sync/ruby_werk/glimmer/lazy_loading1.rb:76:in `'

Andy Maleh said...

Thank you for reporting.

I just confirmed the issue and fixed it in version 0.6.2:
https://rubygems.org/gems/glimmer-dsl-libui/versions/0.6.2

daddie888 said...

And now it works ! Thanks and a very nice job ! Finally a Ruby GUI that works without dependencies ! Can't wait to see the next version and features.