glimmer-dsl-libui 0.4.2 → 0.4.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/README.md +1359 -200
  4. data/VERSION +1 -1
  5. data/examples/basic_entry.rb +27 -24
  6. data/examples/basic_entry2.rb +31 -0
  7. data/examples/color_button.rb +18 -13
  8. data/examples/color_button2.rb +14 -0
  9. data/examples/dynamic_area.rb +77 -90
  10. data/examples/dynamic_area2.rb +14 -12
  11. data/examples/dynamic_area3.rb +90 -0
  12. data/examples/dynamic_area4.rb +95 -0
  13. data/examples/font_button.rb +17 -12
  14. data/examples/font_button2.rb +18 -0
  15. data/examples/form.rb +42 -30
  16. data/examples/form2.rb +37 -0
  17. data/examples/form_table.rb +100 -87
  18. data/examples/form_table2.rb +93 -0
  19. data/examples/histogram.rb +93 -91
  20. data/examples/histogram2.rb +109 -0
  21. data/examples/login.rb +45 -39
  22. data/examples/login2.rb +55 -0
  23. data/examples/login3.rb +65 -0
  24. data/examples/login4.rb +61 -0
  25. data/examples/login5.rb +43 -0
  26. data/examples/meta_example.rb +9 -6
  27. data/examples/method_based_custom_keyword.rb +8 -15
  28. data/examples/method_based_custom_keyword2.rb +97 -0
  29. data/examples/timer.rb +28 -31
  30. data/examples/timer2.rb +129 -0
  31. data/glimmer-dsl-libui.gemspec +0 -0
  32. data/lib/glimmer/dsl/libui/data_binding_expression.rb +4 -6
  33. data/lib/glimmer/libui/attributed_string.rb +3 -0
  34. data/lib/glimmer/libui/control_proxy/color_button_proxy.rb +5 -0
  35. data/lib/glimmer/libui/control_proxy/entry_proxy.rb +5 -0
  36. data/lib/glimmer/libui/control_proxy/font_button_proxy.rb +4 -0
  37. data/lib/glimmer/libui/control_proxy/multiline_entry_proxy.rb +5 -0
  38. data/lib/glimmer/libui/control_proxy/slider_proxy.rb +38 -0
  39. data/lib/glimmer/libui/control_proxy/spinbox_proxy.rb +38 -0
  40. data/lib/glimmer/libui/control_proxy.rb +4 -0
  41. data/lib/glimmer/libui/data_bindable.rb +39 -0
  42. data/lib/glimmer/libui/shape.rb +2 -0
  43. metadata +19 -2
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # [<img src="https://raw.githubusercontent.com/AndyObtiva/glimmer/master/images/glimmer-logo-hi-res.png" height=85 />](https://github.com/AndyObtiva/glimmer) Glimmer DSL for LibUI 0.4.2
1
+ # [<img src="https://raw.githubusercontent.com/AndyObtiva/glimmer/master/images/glimmer-logo-hi-res.png" height=85 />](https://github.com/AndyObtiva/glimmer) Glimmer DSL for LibUI 0.4.6
2
2
  ## Prerequisite-Free Ruby Desktop Development GUI Library
3
3
  [![Gem Version](https://badge.fury.io/rb/glimmer-dsl-libui.svg)](http://badge.fury.io/rb/glimmer-dsl-libui)
4
4
  [![Join the chat at https://gitter.im/AndyObtiva/glimmer](https://badges.gitter.im/AndyObtiva/glimmer.svg)](https://gitter.im/AndyObtiva/glimmer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
@@ -14,11 +14,11 @@ Mac | Windows | Linux
14
14
  The main trade-off in using [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) as opposed to [Glimmer DSL for SWT](https://github.com/AndyObtiva/glimmer-dsl-swt) or [Glimmer DSL for Tk](https://github.com/AndyObtiva/glimmer-dsl-tk) is the fact that [SWT](https://www.eclipse.org/swt/) and [Tk](https://www.tcl.tk/) are more mature than mid-alpha [libui](https://github.com/andlabs/libui) as GUI toolkits. Still, if there is only a need to build a small simple application, [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) could be a good convenient choice due to having zero prerequisites beyond the dependencies included in the [Ruby gem](https://rubygems.org/gems/glimmer-dsl-libui). Also, just like [Glimmer DSL for Tk](https://github.com/AndyObtiva/glimmer-dsl-tk), its apps start instantly and have a small memory footprint. [LibUI](https://github.com/kojix2/LibUI) is a promising new GUI toolkit that might prove quite worthy in the future.
15
15
 
16
16
  [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) aims to provide a DSL similar to the [Glimmer DSL for SWT](https://github.com/AndyObtiva/glimmer-dsl-swt) to enable more productive desktop development in Ruby with:
17
- - Declarative DSL syntax that visually maps to the GUI control hierarchy
18
- - Convention over configuration via smart defaults and automation of low-level details
19
- - Requiring the least amount of syntax possible to build GUI
20
- - Custom Keyword support
21
- - Bidirectional Data-Binding to declaratively wire and automatically synchronize GUI with Business Models
17
+ - [Declarative DSL syntax](#glimmer-gui-dsl-concepts) that visually maps to the GUI control hierarchy
18
+ - [Convention over configuration](#smart-defaults-and-conventions) via smart defaults and automation of low-level details
19
+ - Requiring the [least amount of syntax](#glimmer-gui-dsl-concepts) possible to build GUI
20
+ - [Custom Keyword](#custom-keywords) support
21
+ - [Bidirectional/Unidirectional Data-Binding](#data-binding) to declaratively wire and automatically synchronize GUI Views with Models
22
22
  - [Far Future Plan] Scaffolding for new custom controls, apps, and gems
23
23
  - [Far Future Plan] Native-Executable packaging on Mac, Windows, and Linux.
24
24
 
@@ -373,7 +373,7 @@ gem install glimmer-dsl-libui
373
373
  Or install via Bundler `Gemfile`:
374
374
 
375
375
  ```ruby
376
- gem 'glimmer-dsl-libui', '~> 0.4.2'
376
+ gem 'glimmer-dsl-libui', '~> 0.4.6'
377
377
  ```
378
378
 
379
379
  Add `require 'glimmer-dsl-libui'` at the top, and then `include Glimmer` into the top-level main object for testing or into an actual class for serious usage.
@@ -620,93 +620,112 @@ Example (you may copy/paste in [`girb`](#girb-glimmer-irb)):
620
620
  ```ruby
621
621
  require 'glimmer-dsl-libui'
622
622
 
623
- include Glimmer
624
-
625
- data = [
626
- ['Lisa Sky', 'lisa@sky.com', '720-523-4329', 'Denver', 'CO', '80014'],
627
- ['Jordan Biggins', 'jordan@biggins.com', '617-528-5399', 'Boston', 'MA', '02101'],
628
- ['Mary Glass', 'mary@glass.com', '847-589-8788', 'Elk Grove Village', 'IL', '60007'],
629
- ['Darren McGrath', 'darren@mcgrath.com', '206-539-9283', 'Seattle', 'WA', '98101'],
630
- ['Melody Hanheimer', 'melody@hanheimer.com', '213-493-8274', 'Los Angeles', 'CA', '90001'],
631
- ]
632
-
633
- window('Contacts', 600, 600) { |w|
634
- margined true
623
+ class FormTable
624
+ include Glimmer
635
625
 
636
- vertical_box {
637
- form {
638
- stretchy false
639
-
640
- @name_entry = entry {
641
- label 'Name'
642
- }
643
- @email_entry = entry {
644
- label 'Email'
645
- }
646
- @phone_entry = entry {
647
- label 'Phone'
648
- }
649
- @city_entry = entry {
650
- label 'City'
651
- }
652
- @state_entry = entry {
653
- label 'State'
654
- }
655
- }
656
-
657
- button('Save Contact') {
658
- stretchy false
659
-
660
- on_clicked do
661
- new_row = [@name_entry.text, @email_entry.text, @phone_entry.text, @city_entry.text, @state_entry.text]
662
- if new_row.include?('')
663
- msg_box_error(w, 'Validation Error!', 'All fields are required! Please make sure to enter a value for all fields.')
664
- else
665
- data << new_row # automatically inserts a row into the table due to implicit data-binding
666
- @unfiltered_data = data.dup
667
- @name_entry.text = ''
668
- @email_entry.text = ''
669
- @phone_entry.text = ''
670
- @city_entry.text = ''
671
- @state_entry.text = ''
672
- end
673
- end
674
- }
675
-
676
- search_entry { |se|
677
- stretchy false
626
+ attr_accessor :name, :email, :phone, :city, :state, :filter_value
627
+
628
+ def initialize
629
+ @data = [
630
+ ['Lisa Sky', 'lisa@sky.com', '720-523-4329', 'Denver', 'CO', '80014'],
631
+ ['Jordan Biggins', 'jordan@biggins.com', '617-528-5399', 'Boston', 'MA', '02101'],
632
+ ['Mary Glass', 'mary@glass.com', '847-589-8788', 'Elk Grove Village', 'IL', '60007'],
633
+ ['Darren McGrath', 'darren@mcgrath.com', '206-539-9283', 'Seattle', 'WA', '98101'],
634
+ ['Melody Hanheimer', 'melody@hanheimer.com', '213-493-8274', 'Los Angeles', 'CA', '90001'],
635
+ ]
636
+ end
637
+
638
+ def launch
639
+ window('Contacts', 600, 600) { |w|
640
+ margined true
678
641
 
679
- on_changed do
680
- filter_value = se.text
681
- @unfiltered_data ||= data.dup
682
- # Unfilter first to remove any previous filters
683
- data.replace(@unfiltered_data) # affects table indirectly through implicit data-binding
684
- # Now, apply filter if entered
685
- unless filter_value.empty?
686
- data.filter! do |row_data| # affects table indirectly through implicit data-binding
687
- row_data.any? do |cell|
688
- cell.to_s.downcase.include?(filter_value.downcase)
642
+ vertical_box {
643
+ form {
644
+ stretchy false
645
+
646
+ entry {
647
+ label 'Name'
648
+ text <=> [self, :name]
649
+ }
650
+
651
+ entry {
652
+ label 'Email'
653
+ text <=> [self, :email]
654
+ }
655
+
656
+ entry {
657
+ label 'Phone'
658
+ text <=> [self, :phone]
659
+ }
660
+
661
+ entry {
662
+ label 'City'
663
+ text <=> [self, :city]
664
+ }
665
+
666
+ entry {
667
+ label 'State'
668
+ text <=> [self, :state]
669
+ }
670
+ }
671
+
672
+ button('Save Contact') {
673
+ stretchy false
674
+
675
+ on_clicked do
676
+ new_row = [name, email, phone, city, state]
677
+ if new_row.include?('')
678
+ msg_box_error(w, 'Validation Error!', 'All fields are required! Please make sure to enter a value for all fields.')
679
+ else
680
+ @data << new_row # automatically inserts a row into the table due to implicit data-binding
681
+ @unfiltered_data = @data.dup
682
+ self.name = '' # automatically clears name entry through explicit data-binding
683
+ self.email = ''
684
+ self.phone = ''
685
+ self.city = ''
686
+ self.state = ''
689
687
  end
690
688
  end
691
- end
692
- end
693
- }
689
+ }
690
+
691
+ search_entry {
692
+ stretchy false
693
+ text <=> [self, :filter_value, # bidirectional data-binding of text to self.filter_value with after_write option
694
+ after_write: ->(filter_value) { # execute after write to self.filter_value
695
+ @unfiltered_data ||= @data.dup
696
+ # Unfilter first to remove any previous filters
697
+ @data.replace(@unfiltered_data) # affects table indirectly through implicit data-binding
698
+ # Now, apply filter if entered
699
+ unless filter_value.empty?
700
+ @data.filter! do |row_data| # affects table indirectly through implicit data-binding
701
+ row_data.any? do |cell|
702
+ cell.to_s.downcase.include?(filter_value.downcase)
703
+ end
704
+ end
705
+ end
706
+ }
707
+ ]
708
+ }
709
+
710
+ table {
711
+ text_column('Name')
712
+ text_column('Email')
713
+ text_column('Phone')
714
+ text_column('City')
715
+ text_column('State')
694
716
 
695
- table {
696
- text_column('Name')
697
- text_column('Email')
698
- text_column('Phone')
699
- text_column('City')
700
- text_column('State')
717
+ cell_rows @data # implicit data-binding
718
+
719
+ on_changed do |row, type, row_data|
720
+ puts "Row #{row} #{type}: #{row_data}"
721
+ end
722
+ }
723
+ }
724
+ }.show
725
+ end
726
+ end
701
727
 
702
- cell_rows data # implicit data-binding
703
-
704
- on_changed do |row, type, row_data|
705
- puts "Row #{row} #{type}: #{row_data}"
706
- end
707
- }
708
- }
709
- }.show
728
+ FormTable.new.launch
710
729
  ```
711
730
 
712
731
  ![glimmer-dsl-libui-linux-form-table.png](images/glimmer-dsl-libui-linux-form-table.png)
@@ -779,9 +798,9 @@ Check [examples/dynamic_area.rb](#dynamic-area) for a more detailed semi-declara
779
798
  - `scroll_to(x as Numeric, y as Numeric, width as Numeric = main_window.width, height as Numeric = main_window.height)`: scrolls to `x`/`y` location with `width` and `height` viewport size.
780
799
  - `set_size(width as Numeric, height as Numeric)`: set size of scrolling area, which must must exceed that of visible viewport in order for scrolling to be enabled.
781
800
 
782
- Mac |
783
- ----|
784
- ![glimmer-dsl-libui-mac-dynamic-area.png](images/glimmer-dsl-libui-mac-basic-scrolling-area.png) ![glimmer-dsl-libui-mac-dynamic-area-updated.png](images/glimmer-dsl-libui-mac-basic-scrolling-area-scrolled.png)
801
+ Mac |Linux
802
+ ----|-----
803
+ ![glimmer-dsl-libui-mac-dynamic-area.png](images/glimmer-dsl-libui-mac-basic-scrolling-area.png) ![glimmer-dsl-libui-mac-dynamic-area-updated.png](images/glimmer-dsl-libui-mac-basic-scrolling-area-scrolled.png) | ![glimmer-dsl-libui-linux-dynamic-area.png](images/glimmer-dsl-libui-linux-basic-scrolling-area.png) ![glimmer-dsl-libui-linux-dynamic-area-updated.png](images/glimmer-dsl-libui-linux-basic-scrolling-area-scrolled.png)
785
804
 
786
805
  Check [examples/basic_scrolling_area.rb](#basic-scrolling-area) for a more detailed example.
787
806
 
@@ -877,7 +896,7 @@ Given that it is very new and is not a [libui](https://github.com/andlabs/libui)
877
896
  - Including an `image` inside an `area` `on_draw` listener improves performance due to not retaining pixel/line data in memory.
878
897
  - Supplying `width` and `height` (2nd and 3rd arguments) greatly improves performance when shrinking image.
879
898
 
880
- Currently, it is recommended to use `image` with very small `width` and `height` values only.
899
+ Currently, it is recommended to use `image` with very small `width` and `height` values only (e.g. 24x24).
881
900
 
882
901
  Setting a [`transform` `matrix`](#area-transform-matrix) is supported under `image` just like it is under `path` and `text` inside `area`.
883
902
 
@@ -1033,6 +1052,8 @@ window('Basic Image', 96, 96) {
1033
1052
 
1034
1053
  One final note is that in Linux, table images grow and shrink with the image size unlike on the Mac where table row heights are constant regardless of image sizes. As such, you may be able to repurpose a table with a single image column and a single row as an image control with more native libui rendering if you are only targeting Linux with your app.
1035
1054
 
1055
+ ![linux table image](images/glimmer-dsl-libui-linux-basic-table-image.png)
1056
+
1036
1057
  Check out [examples/basic_image.rb](#basic-image) (all versions) for examples of using `image` Glimmer custom control.
1037
1058
 
1038
1059
  #### Colors
@@ -1329,7 +1350,7 @@ The [Observer Design Pattern](https://en.wikipedia.org/wiki/Observer_pattern) (a
1329
1350
 
1330
1351
  ![MVC - Model View Controller](https://www.researchgate.net/profile/Danny-Weyns/publication/269303611/figure/fig2/AS:858133056462866@1581606272800/Smalltalk80-MVC-pattern-View-and-Controller-work-as-a-pair-allowing-the-user-to-interact.ppm)
1331
1352
 
1332
- [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) supports the [Observer Design Pattern](https://en.wikipedia.org/wiki/Observer_pattern) via the `observe(model, attribute=nil)` keyword, which can observe `Object` models with attributes, `Hash`es, and `Array`s. It automatically enhances objects as needed to support automatically notifying observers of changes via `observable#notify_observers(attribute)` method:
1353
+ [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) supports the [Observer Design Pattern](https://en.wikipedia.org/wiki/Observer_pattern) via the `observe(model, attribute_or_key=nil)` keyword, which can observe `Object` models with attributes, `Hash`es with keys, and `Array`s. It automatically enhances objects as needed to support automatically notifying observers of changes via `observable#notify_observers(attribute_or_key = nil)` method:
1333
1354
  - `Object` becomes `Glimmer::DataBinding::ObservableModel`, which supports observing specified `Object` model attributes.
1334
1355
  - `Hash` becomes `Glimmer::DataBinding::ObservableHash`, which supports observing all `Hash` keys or a specific `Hash` key
1335
1356
  - `Array` becomes `Glimmer::DataBinding::ObservableArray`, which supports observing `Array` changes like those done with `push`, `<<`, `delete`, and `map!` methods (all mutation methods).
@@ -1350,13 +1371,49 @@ See examples of the `observe` keyword at [Color The Circles](#color-the-circles)
1350
1371
 
1351
1372
  ### Data-Binding
1352
1373
 
1353
- [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) supports unidirectional (one-way) data-binding of any control/shape/attributed-string property via the `<=` symbol (indicating data is moving from the right side, which is the model, to the left side, which is the GUI view object).
1374
+ [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) supports both bidirectional (two-way) data-binding and unidirectional (one-way) data-binding.
1354
1375
 
1355
- The data-binding API is more precisely: `view_property <= [model, attribute, *options]`
1376
+ Data-binding enables writing very expressive, terse, and declarative code to synchronize View properties with Model attributes without writing many lines or pages of imperative code doing the same thing, increasing productivity immensely.
1356
1377
 
1357
- This is also known as the [Glimmer Shine](https://github.com/AndyObtiva/glimmer-dsl-swt/blob/master/docs/reference/GLIMMER_GUI_DSL_SYNTAX.md#shine) syntax for data-binding, a [Glimmer](https://github.com/AndyObtiva/glimmer)-only unique innovation.
1378
+ Data-binding automatically takes advantage of the [Observer Pattern](#observer-pattern) behind the scenes and is very well suited to declaring View property data sources piecemeal. On the other hand, explicit use of the [Observer Pattern](#observer-pattern) is sometimes more suitable when needing to make multiple View updates upon a single Model attribute change.
1358
1379
 
1359
- Example:
1380
+ Data-binding supports utilizing the [MVP (Model View Presenter)](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter) flavor of [MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) by observing both the View and a Presenter for changes and updating the opposite side upon encountering them. This enables writing more decoupled cleaner code that keeps View code and Model code disentangled and highly maintainable. For example, check out the Snake game presenters for [Grid](/examples/snake/presenter/grid.rb) and [Cell](/examples/snake/presenter/cell.rb), which act as proxies for the actual Snake game models [Snake](/examples/snake/model/snake.rb) and [Apple](/examples/snake/model/apple.rb), mediating synchronization of data between them and the [Snake View GUI](/examples/snake.rb).
1381
+
1382
+ ![MVP](https://www.researchgate.net/profile/Gilles-Perrouin/publication/320249584/figure/fig8/AS:668260987068418@1536337243385/Model-view-presenter-architecture.png)
1383
+
1384
+ [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) supports bidirectional (two-way) data-binding of the following controls/properties via the `<=>` operator (indicating data is moving in both directions between View and Model):
1385
+ - `color_button`: `color`
1386
+ - `entry`: `text`
1387
+ - `font_button`: `font`
1388
+ - `multiline_entry`: `text`
1389
+ - `non_wrapping_multiline_entry`: `text`
1390
+ - `search_entry`: `text`
1391
+ - `slider`: `value`
1392
+ - `spinbox`: `value`
1393
+
1394
+ Example of bidirectional data-binding:
1395
+
1396
+ ```ruby
1397
+ entry {
1398
+ text <=> [contract, :legal_text]
1399
+ }
1400
+ ```
1401
+
1402
+ That is data-binding a contract's legal text to an `entry` `text` property.
1403
+
1404
+ Another example of bidirectional data-binding with an option:
1405
+
1406
+ ```ruby
1407
+ entry {
1408
+ text <=> [self, :entered_text, after_write: ->(text) {puts text}]
1409
+ }
1410
+ ```
1411
+
1412
+ That is data-binding `entered_text` attribute on `self` to `entry` `text` property and printing text after write to the model.
1413
+
1414
+ [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) supports unidirectional (one-way) data-binding of any control/shape/attributed-string property via the `<=` operator (indicating data is moving from the right side, which is the Model, to the left side, which is the GUI View object).
1415
+
1416
+ Example of unidirectional data-binding:
1360
1417
 
1361
1418
  ```ruby
1362
1419
  square(0, 0, CELL_SIZE) {
@@ -1366,7 +1423,7 @@ square(0, 0, CELL_SIZE) {
1366
1423
 
1367
1424
  That is data-binding a grid cell color to a `square` shape's `fill` property. That means if the `color` attribute of the grid cell is updated, the `fill` property of the `square` shape is automatically updated accordingly.
1368
1425
 
1369
- Another Example:
1426
+ Another Example of unidirectional data-binding with an option:
1370
1427
 
1371
1428
  ```ruby
1372
1429
  window {
@@ -1376,14 +1433,36 @@ window {
1376
1433
 
1377
1434
  That is data-binding the `window` `title` property to the `score` attribute of a `@game`, but converting on read from the Model to a `String`.
1378
1435
 
1379
- Data-binding enables writing very expressive, terse, and declarative code to synchronize View properties with Model attributes instead of pages of imperative code doing the same thing.
1436
+ To summarize the data-binding API:
1437
+ - `view_property <=> [model, attribute, *read_or_write_options]`: Bidirectional (two-way) data-binding to Model attribute accessor
1438
+ - `view_property <= [model, attribute, *read_only_options]`: Unidirectional (one-way) data-binding to Model attribute reader
1439
+
1440
+ This is also known as the [Glimmer Shine](https://github.com/AndyObtiva/glimmer-dsl-swt/blob/master/docs/reference/GLIMMER_GUI_DSL_SYNTAX.md#shine) syntax for data-binding, a [Glimmer](https://github.com/AndyObtiva/glimmer)-only unique innovation that takes advantage of [Ruby](https://www.ruby-lang.org/en/)'s highly expressive syntax and malleable DSL support.
1380
1441
 
1381
- Options include:
1442
+ Data-binding options include:
1382
1443
  - `before_read {|value| ...}`: performs an operation before reading data from Model to update the View.
1383
1444
  - `on_read {|value| ...}`: converts value read from Model to update the View.
1384
1445
  - `after_read {|converted_value| ...}`: performs an operation after read from Model and updating the View.
1446
+ - `before_write {|value| ...}`: performs an operation before writing data to Model from View.
1447
+ - `on_write {|value| ...}`: converts value read from View to update the Model.
1448
+ - `after_write {|converted_value| ...}`: performs an operation after writing to Model from View.
1449
+ - `computed_by attribute` or `computed_by [attribute1, attribute2, ...]`: indicates model attribute is computed from specified attribute(s), thus updated when they are updated (see in [Login example version 2](/examples/login2.rb))
1450
+
1451
+ Note that with both `on_read` and `on_write` converters, you could pass a `Symbol` representing the name of a method on the value object to invoke.
1452
+
1453
+ Example:
1454
+
1455
+ ```ruby
1456
+ entry {
1457
+ text <=> [product, :price, on_read: :to_s, on_write: :to_i]
1458
+ }
1459
+ ```
1460
+
1461
+ Data-binding gotchas:
1462
+ - Never data-bind a control property to an attribute on the same view object with the same exact name (e.g. binding `entry` `text` property to `self` `text` attribute) as it would conflict with it. Instead, data-bind view property to an attribute with a different name on the view object or with the same name, but on a presenter or model object (e.g. data-bind `entry` `text` to `self` `legal_text` attribute or to `contract` model `text` attribute)
1463
+ - Data-binding a property utilizes the control's listener associated with the property (e.g. `on_changed` for `entry` `text`), so you cannot hook into the listener directly anymore as that would negate data-binding. Instead, you can add an `after_write: ->(val) {}` option to perform something on trigger of the control listener instead.
1385
1464
 
1386
- Learn more from data-binding usage in [Snake](#snake) and [Tic Tac Toe](#tic_tac_toe) examples.
1465
+ Learn more from data-binding usage in [Login](#login) (4 data-binding versions), [Basic Entry](#basic-entry), [Form](#form), [Form Table](#form-table), [Method-Based Custom Keyword](#method-based-custom-keyword), [Snake](#snake) and [Tic Tac Toe](#tic_tac_toe) examples.
1387
1466
 
1388
1467
  ### API Gotchas
1389
1468
 
@@ -1799,7 +1878,7 @@ Example:
1799
1878
 
1800
1879
  ## Examples
1801
1880
 
1802
- The following examples include reimplementions of the examples in the [LibUI](https://github.com/kojix2/LibUI) project utilizing the [Glimmer GUI DSL](#glimmer-gui-dsl-concepts) as well as brand new examples.
1881
+ The following examples include reimplementions of the examples in the [LibUI](https://github.com/kojix2/LibUI) project utilizing the [Glimmer GUI DSL](#glimmer-gui-dsl-concepts) (with and without data-binding) as well as brand new examples.
1803
1882
 
1804
1883
  To browse all examples, simply launch the [Meta-Example](examples/meta_example.rb), which lists all examples and displays each example's code when selected. It also enables code editing to facilitate experimentation and learning.
1805
1884
 
@@ -1826,17 +1905,23 @@ New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version
1826
1905
  ```ruby
1827
1906
  require 'glimmer-dsl-libui'
1828
1907
  require 'facets'
1908
+ require 'fileutils'
1829
1909
 
1830
1910
  class MetaExample
1831
1911
  include Glimmer
1832
1912
 
1913
+ ADDITIONAL_BASIC_EXAMPLES = ['Color Button', 'Font Button', 'Form', 'Date Time Picker', 'Simple Notepad']
1914
+
1915
+ attr_accessor :code_text
1916
+
1833
1917
  def initialize
1834
- @selected_example_index = 0
1918
+ @selected_example_index = examples_with_versions.index(basic_examples_with_versions.first)
1919
+ @code_text = File.read(file_path_for(selected_example))
1835
1920
  end
1836
1921
 
1837
1922
  def examples
1838
1923
  if @examples.nil?
1839
- example_files = Dir.glob(File.join(File.expand_path('.', __dir__), '**', '*.rb'))
1924
+ example_files = Dir.glob(File.join(File.expand_path('.', __dir__), '*.rb'))
1840
1925
  example_file_names = example_files.map { |f| File.basename(f, '.rb') }
1841
1926
  example_file_names = example_file_names.reject { |f| f == 'meta_example' || f.match(/\d$/) }
1842
1927
  @examples = example_file_names.map { |f| f.underscore.titlecase }
@@ -1850,12 +1935,20 @@ class MetaExample
1850
1935
  end
1851
1936
  end
1852
1937
 
1938
+ def basic_examples_with_versions
1939
+ examples_with_versions.select {|example| example.start_with?('Basic') || ADDITIONAL_BASIC_EXAMPLES.include?(example) }
1940
+ end
1941
+
1942
+ def advanced_examples_with_versions
1943
+ examples_with_versions - basic_examples_with_versions
1944
+ end
1945
+
1853
1946
  def file_path_for(example)
1854
1947
  File.join(File.expand_path('.', __dir__), "#{example.underscore}.rb")
1855
1948
  end
1856
1949
 
1857
1950
  def version_count_for(example)
1858
- Dir.glob(File.join(File.expand_path('.', __dir__), "#{example.underscore}*.rb")).select {|file| file.match(/\d\.rb$/)}.count + 1
1951
+ Dir.glob(File.join(File.expand_path('.', __dir__), "#{example.underscore}*.rb")).select {|file| file.match(/#{example.underscore}\d\.rb$/)}.count + 1
1859
1952
  end
1860
1953
 
1861
1954
  def glimmer_dsl_libui_file
@@ -1891,17 +1984,47 @@ class MetaExample
1891
1984
  vertical_box {
1892
1985
  stretchy false
1893
1986
 
1894
- @example_radio_buttons = radio_buttons {
1987
+ tab {
1895
1988
  stretchy false
1896
- items examples_with_versions
1897
- selected @selected_example_index
1898
1989
 
1899
- on_selected do
1900
- @selected_example_index = @example_radio_buttons.selected
1901
- example = selected_example
1902
- @code_entry.text = File.read(file_path_for(example))
1903
- @version_spinbox.value = 1
1904
- end
1990
+ tab_item('Basic') {
1991
+ vertical_box {
1992
+ @basic_example_radio_buttons = radio_buttons {
1993
+ stretchy false
1994
+ items basic_examples_with_versions
1995
+ selected basic_examples_with_versions.index(examples_with_versions[@selected_example_index])
1996
+
1997
+ on_selected do
1998
+ @selected_example_index = examples_with_versions.index(basic_examples_with_versions[@basic_example_radio_buttons.selected])
1999
+ example = selected_example
2000
+ self.code_text = File.read(file_path_for(example))
2001
+ @version_spinbox.value = 1
2002
+ end
2003
+ }
2004
+
2005
+ label # filler
2006
+ label # filler
2007
+ }
2008
+ }
2009
+
2010
+ tab_item('Advanced') {
2011
+ vertical_box {
2012
+ @advanced_example_radio_buttons = radio_buttons {
2013
+ stretchy false
2014
+ items advanced_examples_with_versions
2015
+
2016
+ on_selected do
2017
+ @selected_example_index = examples_with_versions.index(advanced_examples_with_versions[@advanced_example_radio_buttons.selected])
2018
+ example = selected_example
2019
+ self.code_text = File.read(file_path_for(example))
2020
+ @version_spinbox.value = 1
2021
+ end
2022
+ }
2023
+
2024
+ label # filler
2025
+ label # filler
2026
+ }
2027
+ }
1905
2028
  }
1906
2029
 
1907
2030
  horizontal_box {
@@ -1919,7 +2042,7 @@ class MetaExample
1919
2042
  else
1920
2043
  version_number = @version_spinbox.value == 1 ? '' : @version_spinbox.value
1921
2044
  example = "#{selected_example}#{version_number}"
1922
- @code_entry.text = File.read(file_path_for(example))
2045
+ self.code_text = File.read(file_path_for(example))
1923
2046
  end
1924
2047
  end
1925
2048
  }
@@ -1931,9 +2054,15 @@ class MetaExample
1931
2054
  button('Launch') {
1932
2055
  on_clicked do
1933
2056
  begin
1934
- meta_example_file = File.join(Dir.home, '.meta_example.rb')
1935
- File.write(meta_example_file, @code_entry.text)
1936
- run_example(meta_example_file)
2057
+ parent_dir = File.join(Dir.home, '.glimmer-dsl-libui', 'examples')
2058
+ FileUtils.mkdir_p(parent_dir)
2059
+ example_file = File.join(parent_dir, "#{selected_example.underscore}.rb")
2060
+ File.write(example_file, code_text)
2061
+ example_supporting_directory = File.expand_path(selected_example.underscore, __dir__)
2062
+ FileUtils.cp_r(example_supporting_directory, parent_dir) if Dir.exist?(example_supporting_directory)
2063
+ FileUtils.cp_r(File.expand_path('../icons', __dir__), File.dirname(parent_dir))
2064
+ FileUtils.cp_r(File.expand_path('../sounds', __dir__), File.dirname(parent_dir))
2065
+ run_example(example_file)
1937
2066
  rescue => e
1938
2067
  puts e.full_message
1939
2068
  puts 'Unable to write code changes! Running original example...'
@@ -1943,14 +2072,14 @@ class MetaExample
1943
2072
  }
1944
2073
  button('Reset') {
1945
2074
  on_clicked do
1946
- @code_entry.text = File.read(file_path_for(selected_example))
2075
+ self.code_text = File.read(file_path_for(selected_example))
1947
2076
  end
1948
2077
  }
1949
2078
  }
1950
2079
  }
1951
2080
 
1952
2081
  @code_entry = non_wrapping_multiline_entry {
1953
- text File.read(file_path_for(selected_example))
2082
+ text <=> [self, :code_text]
1954
2083
  }
1955
2084
  }
1956
2085
  }.show
@@ -2169,7 +2298,44 @@ UI.main
2169
2298
  UI.quit
2170
2299
  ```
2171
2300
 
2172
- [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version:
2301
+ [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version (with [data-binding](#data-binding)):
2302
+
2303
+ ```ruby
2304
+ require 'glimmer-dsl-libui'
2305
+
2306
+ class BasicEntry
2307
+ include Glimmer
2308
+
2309
+ attr_accessor :entry_text
2310
+
2311
+ def launch
2312
+ window('Basic Entry', 300, 50) {
2313
+ horizontal_box {
2314
+ entry {
2315
+ # stretchy true # Smart default option for appending to horizontal_box
2316
+ text <=> [self, :entry_text, after_write: ->(text) {puts text; $stdout.flush}] # bidirectional data-binding between text property and entry_text attribute, printing after write to model.
2317
+ }
2318
+
2319
+ button('Button') {
2320
+ stretchy false # stretchy property is available when control is nested under horizontal_box
2321
+
2322
+ on_clicked do
2323
+ msg_box('You entered', entry_text)
2324
+ end
2325
+ }
2326
+ }
2327
+
2328
+ on_closing do
2329
+ puts 'Bye Bye'
2330
+ end
2331
+ }.show
2332
+ end
2333
+ end
2334
+
2335
+ BasicEntry.new.launch
2336
+ ```
2337
+
2338
+ [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version 2 (without [data-binding](#data-binding)):
2173
2339
 
2174
2340
  ```ruby
2175
2341
  require 'glimmer-dsl-libui'
@@ -2327,7 +2493,33 @@ UI.quit
2327
2493
 
2328
2494
  ```
2329
2495
 
2330
- [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version:
2496
+ [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version (with [data-binding](#data-binding)):
2497
+
2498
+ ```ruby
2499
+ require 'glimmer-dsl-libui'
2500
+
2501
+ class FontButton
2502
+ include Glimmer
2503
+
2504
+ attr_accessor :font_descriptor
2505
+
2506
+ def launch
2507
+ window('hello world', 300, 200) {
2508
+ font_button {
2509
+ font <=> [self, :font_descriptor, after_write: -> { p font_descriptor }]
2510
+ }
2511
+
2512
+ on_closing do
2513
+ puts 'Bye Bye'
2514
+ end
2515
+ }.show
2516
+ end
2517
+ end
2518
+
2519
+ FontButton.new.launch
2520
+ ```
2521
+
2522
+ [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version 2 (without [data-binding](#data-binding)):
2331
2523
 
2332
2524
  ```ruby
2333
2525
  require 'glimmer-dsl-libui'
@@ -2368,7 +2560,33 @@ Mac | Windows | Linux
2368
2560
  ----|---------|------
2369
2561
  ![glimmer-dsl-libui-mac-color-button.png](images/glimmer-dsl-libui-mac-color-button.png) ![glimmer-dsl-libui-mac-color-button-selection.png](images/glimmer-dsl-libui-mac-color-button-selection.png) | ![glimmer-dsl-libui-windows-color-button.png](images/glimmer-dsl-libui-windows-color-button.png) ![glimmer-dsl-libui-windows-color-button-selection.png](images/glimmer-dsl-libui-windows-color-button-selection.png) | ![glimmer-dsl-libui-linux-color-button.png](images/glimmer-dsl-libui-linux-color-button.png) ![glimmer-dsl-libui-linux-color-button-selection.png](images/glimmer-dsl-libui-linux-color-button-selection.png)
2370
2562
 
2371
- New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version:
2563
+ New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version (with [data-binding](#data-binding)):
2564
+
2565
+ ```ruby
2566
+ require 'glimmer-dsl-libui'
2567
+
2568
+ class ColorButton
2569
+ include Glimmer
2570
+
2571
+ attr_accessor :selected_color
2572
+
2573
+ def initialize
2574
+ @selected_color = :blue
2575
+ end
2576
+
2577
+ def launch
2578
+ window('color button', 240) {
2579
+ color_button {
2580
+ color <=> [self, :selected_color, after_write: ->(color) {p color}]
2581
+ }
2582
+ }.show
2583
+ end
2584
+ end
2585
+
2586
+ ColorButton.new.launch
2587
+ ```
2588
+
2589
+ New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version 2 (without [data-binding](#data-binding)):
2372
2590
 
2373
2591
  ```ruby
2374
2592
  require 'glimmer-dsl-libui'
@@ -2493,32 +2711,84 @@ Mac | Windows | Linux
2493
2711
  ----|---------|------
2494
2712
  ![glimmer-dsl-libui-mac-form.png](images/glimmer-dsl-libui-mac-form.png) ![glimmer-dsl-libui-mac-form-msg-box.png](images/glimmer-dsl-libui-mac-form-msg-box.png) | ![glimmer-dsl-libui-windows-form.png](images/glimmer-dsl-libui-windows-form.png) ![glimmer-dsl-libui-windows-form-msg-box.png](images/glimmer-dsl-libui-windows-form-msg-box.png) | ![glimmer-dsl-libui-linux-form.png](images/glimmer-dsl-libui-linux-form.png) ![glimmer-dsl-libui-linux-form-msg-box.png](images/glimmer-dsl-libui-linux-form-msg-box.png)
2495
2713
 
2496
- New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version:
2714
+ New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version (with [data-binding](#data-binding)):
2497
2715
 
2498
2716
  ```ruby
2499
2717
  require 'glimmer-dsl-libui'
2500
2718
 
2501
- include Glimmer
2502
-
2503
- window('Form') {
2504
- margined true
2719
+ class Form
2720
+ include Glimmer
2505
2721
 
2506
- vertical_box {
2507
- form {
2508
- @first_name_entry = entry {
2509
- label 'First Name' # label property is available when control is nested under form
2510
- }
2511
-
2512
- @last_name_entry = entry {
2513
- label 'Last Name' # label property is available when control is nested under form
2514
- }
2515
-
2516
- @phone_entry = entry {
2517
- label 'Phone' # label property is available when control is nested under form
2518
- }
2722
+ attr_accessor :first_name, :last_name, :phone, :email
2723
+
2724
+ def launch
2725
+ window('Form') {
2726
+ margined true
2519
2727
 
2520
- @email_entry = entry {
2521
- label 'Email' # label property is available when control is nested under form
2728
+ vertical_box {
2729
+ form {
2730
+ entry {
2731
+ label 'First Name' # label property is available when control is nested under form
2732
+ text <=> [self, :first_name] # bidirectional data-binding of entry text property to self first_name attribute
2733
+ }
2734
+
2735
+ entry {
2736
+ label 'Last Name' # label property is available when control is nested under form
2737
+ text <=> [self, :last_name]
2738
+ }
2739
+
2740
+ entry {
2741
+ label 'Phone' # label property is available when control is nested under form
2742
+ text <=> [self, :phone]
2743
+ }
2744
+
2745
+ entry {
2746
+ label 'Email' # label property is available when control is nested under form
2747
+ text <=> [self, :email]
2748
+ }
2749
+ }
2750
+
2751
+ button('Display Info') {
2752
+ stretchy false
2753
+
2754
+ on_clicked do
2755
+ msg_box('Info', "#{first_name} #{last_name} has phone #{phone} and email #{email}")
2756
+ end
2757
+ }
2758
+ }
2759
+ }.show
2760
+ end
2761
+ end
2762
+
2763
+ Form.new.launch
2764
+ ```
2765
+
2766
+ New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version 2 (without [data-binding](#data-binding)):
2767
+
2768
+ ```ruby
2769
+ require 'glimmer-dsl-libui'
2770
+
2771
+ include Glimmer
2772
+
2773
+ window('Form') {
2774
+ margined true
2775
+
2776
+ vertical_box {
2777
+ form {
2778
+ @first_name_entry = entry {
2779
+ label 'First Name' # label property is available when control is nested under form
2780
+ }
2781
+
2782
+ @last_name_entry = entry {
2783
+ label 'Last Name' # label property is available when control is nested under form
2784
+ }
2785
+
2786
+ @phone_entry = entry {
2787
+ label 'Phone' # label property is available when control is nested under form
2788
+ }
2789
+
2790
+ @email_entry = entry {
2791
+ label 'Email' # label property is available when control is nested under form
2522
2792
  }
2523
2793
  }
2524
2794
 
@@ -5608,7 +5878,96 @@ Mac | Windows | Linux
5608
5878
  ----|---------|------
5609
5879
  ![glimmer-dsl-libui-mac-dynamic-area.png](images/glimmer-dsl-libui-mac-dynamic-area.png) ![glimmer-dsl-libui-mac-dynamic-area-updated.png](images/glimmer-dsl-libui-mac-dynamic-area-updated.png) | ![glimmer-dsl-libui-windows-dynamic-area.png](images/glimmer-dsl-libui-windows-dynamic-area.png) ![glimmer-dsl-libui-windows-dynamic-area-updated.png](images/glimmer-dsl-libui-windows-dynamic-area-updated.png) | ![glimmer-dsl-libui-linux-dynamic-area.png](images/glimmer-dsl-libui-linux-dynamic-area.png) ![glimmer-dsl-libui-linux-dynamic-area-updated.png](images/glimmer-dsl-libui-linux-dynamic-area-updated.png)
5610
5880
 
5611
- New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version:
5881
+ New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version (with [data-binding](#data-binding)):
5882
+
5883
+ ```ruby
5884
+ require 'glimmer-dsl-libui'
5885
+
5886
+ class DynamicArea
5887
+ include Glimmer
5888
+
5889
+ attr_accessor :rectangle_x, :rectangle_y, :rectangle_width, :rectangle_height, :rectangle_red, :rectangle_green, :rectangle_blue, :rectangle_alpha
5890
+
5891
+ def initialize
5892
+ @rectangle_x = 25
5893
+ @rectangle_y = 25
5894
+ @rectangle_width = 150
5895
+ @rectangle_height = 150
5896
+ @rectangle_red = 102
5897
+ @rectangle_green = 102
5898
+ @rectangle_blue = 204
5899
+ @rectangle_alpha = 100
5900
+ end
5901
+
5902
+ def launch
5903
+ window('Dynamic Area', 240, 600) {
5904
+ margined true
5905
+
5906
+ vertical_box {
5907
+ label('Rectangle Properties') {
5908
+ stretchy false
5909
+ }
5910
+
5911
+ form {
5912
+ stretchy false
5913
+
5914
+ spinbox(0, 1000) {
5915
+ label 'x'
5916
+ value <=> [self, :rectangle_x, after_write: -> {@area.queue_redraw_all}]
5917
+ }
5918
+
5919
+ spinbox(0, 1000) {
5920
+ label 'y'
5921
+ value <=> [self, :rectangle_y, after_write: -> {@area.queue_redraw_all}]
5922
+ }
5923
+
5924
+ spinbox(0, 1000) {
5925
+ label 'width'
5926
+ value <=> [self, :rectangle_width, after_write: -> {@area.queue_redraw_all}]
5927
+ }
5928
+
5929
+ spinbox(0, 1000) {
5930
+ label 'height'
5931
+ value <=> [self, :rectangle_height, after_write: -> {@area.queue_redraw_all}]
5932
+ }
5933
+
5934
+ spinbox(0, 255) {
5935
+ label 'red'
5936
+ value <=> [self, :rectangle_red, after_write: -> {@area.queue_redraw_all}]
5937
+ }
5938
+
5939
+ spinbox(0, 255) {
5940
+ label 'green'
5941
+ value <=> [self, :rectangle_green, after_write: -> {@area.queue_redraw_all}]
5942
+ }
5943
+
5944
+ spinbox(0, 255) {
5945
+ label 'blue'
5946
+ value <=> [self, :rectangle_blue, after_write: -> {@area.queue_redraw_all}]
5947
+ }
5948
+
5949
+ spinbox(0, 100) {
5950
+ label 'alpha'
5951
+ value <=> [self, :rectangle_alpha, after_write: -> {@area.queue_redraw_all}]
5952
+ }
5953
+ }
5954
+
5955
+ @area = area {
5956
+ on_draw do |area_draw_params|
5957
+ rectangle(rectangle_x, rectangle_y, rectangle_width, rectangle_height) { # a dynamic path is added semi-declaratively inside on_draw block
5958
+ fill r: rectangle_red, g: rectangle_green, b: rectangle_blue, a: rectangle_alpha / 100.0
5959
+ }
5960
+ end
5961
+ }
5962
+ }
5963
+ }.show
5964
+ end
5965
+ end
5966
+
5967
+ DynamicArea.new.launch
5968
+ ```
5969
+
5970
+ New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version 2 (without [data-binding](#data-binding)):
5612
5971
 
5613
5972
  ```ruby
5614
5973
  require 'glimmer-dsl-libui'
@@ -5710,7 +6069,102 @@ window('Dynamic Area', 240, 600) {
5710
6069
  }.show
5711
6070
  ```
5712
6071
 
5713
- New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version 2 (declarative stable `path` approach):
6072
+ New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version 3 (declarative stable `path` approach with [data-binding](#data-binding)):
6073
+
6074
+ ```ruby
6075
+ require 'glimmer-dsl-libui'
6076
+
6077
+ class DynamicArea
6078
+ include Glimmer
6079
+
6080
+ attr_accessor :rectangle_x, :rectangle_y, :rectangle_width, :rectangle_height, :rectangle_red, :rectangle_green, :rectangle_blue, :rectangle_alpha
6081
+
6082
+ def initialize
6083
+ @rectangle_x = 25
6084
+ @rectangle_y = 25
6085
+ @rectangle_width = 150
6086
+ @rectangle_height = 150
6087
+ @rectangle_red = 102
6088
+ @rectangle_green = 102
6089
+ @rectangle_blue = 204
6090
+ @rectangle_alpha = 100
6091
+ end
6092
+
6093
+ def rectangle_fill
6094
+ { r: rectangle_red, g: rectangle_green, b: rectangle_blue, a: rectangle_alpha / 100.0 }
6095
+ end
6096
+
6097
+ def launch
6098
+ window('Dynamic Area', 240, 600) {
6099
+ margined true
6100
+
6101
+ vertical_box {
6102
+ label('Rectangle Properties') {
6103
+ stretchy false
6104
+ }
6105
+
6106
+ form {
6107
+ stretchy false
6108
+
6109
+ @x_spinbox = spinbox(0, 1000) {
6110
+ label 'x'
6111
+ value <=> [self, :rectangle_x]
6112
+ }
6113
+
6114
+ @y_spinbox = spinbox(0, 1000) {
6115
+ label 'y'
6116
+ value <=> [self, :rectangle_y]
6117
+ }
6118
+
6119
+ @width_spinbox = spinbox(0, 1000) {
6120
+ label 'width'
6121
+ value <=> [self, :rectangle_width]
6122
+ }
6123
+
6124
+ @height_spinbox = spinbox(0, 1000) {
6125
+ label 'height'
6126
+ value <=> [self, :rectangle_height]
6127
+ }
6128
+
6129
+ @red_spinbox = spinbox(0, 255) {
6130
+ label 'red'
6131
+ value <=> [self, :rectangle_red]
6132
+ }
6133
+
6134
+ @green_spinbox = spinbox(0, 255) {
6135
+ label 'green'
6136
+ value <=> [self, :rectangle_green]
6137
+ }
6138
+
6139
+ @blue_spinbox = spinbox(0, 255) {
6140
+ label 'blue'
6141
+ value <=> [self, :rectangle_blue]
6142
+ }
6143
+
6144
+ @alpha_spinbox = spinbox(0, 100) {
6145
+ label 'alpha'
6146
+ value <=> [self, :rectangle_alpha]
6147
+ }
6148
+ }
6149
+
6150
+ area {
6151
+ @rectangle = rectangle { # stable implicit path shape
6152
+ x <= [self, :rectangle_x]
6153
+ y <= [self, :rectangle_y]
6154
+ width <= [self, :rectangle_width]
6155
+ height <= [self, :rectangle_height]
6156
+ fill <= [self, :rectangle_fill, computed_by: [:rectangle_red, :rectangle_green, :rectangle_blue, :rectangle_alpha]]
6157
+ }
6158
+ }
6159
+ }
6160
+ }.show
6161
+ end
6162
+ end
6163
+
6164
+ DynamicArea.new.launch
6165
+ ```
6166
+
6167
+ New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version 4 (declarative stable `path` approach without [data-binding](#data-binding)):
5714
6168
 
5715
6169
  ```ruby
5716
6170
  require 'glimmer-dsl-libui'
@@ -5802,7 +6256,7 @@ window('Dynamic Area', 240, 600) {
5802
6256
  }
5803
6257
 
5804
6258
  area {
5805
- @rectangle = rectangle(@x_spinbox.value, @y_spinbox.value, @width_spinbox.value, @height_spinbox.value) { # stable path
6259
+ @rectangle = rectangle(@x_spinbox.value, @y_spinbox.value, @width_spinbox.value, @height_spinbox.value) { # stable implicit path shape
5806
6260
  fill r: @red_spinbox.value, g: @green_spinbox.value, b: @blue_spinbox.value, a: @alpha_spinbox.value / 100.0
5807
6261
  }
5808
6262
  }
@@ -5944,7 +6398,120 @@ Mac | Windows | Linux
5944
6398
  ----|---------|------
5945
6399
  ![glimmer-dsl-libui-mac-form-table.png](images/glimmer-dsl-libui-mac-form-table.png) ![glimmer-dsl-libui-mac-form-table-contact-entered.png](images/glimmer-dsl-libui-mac-form-table-contact-entered.png) ![glimmer-dsl-libui-mac-form-table-filtered.png](images/glimmer-dsl-libui-mac-form-table-filtered.png) | ![glimmer-dsl-libui-windows-form-table.png](images/glimmer-dsl-libui-windows-form-table.png) ![glimmer-dsl-libui-windows-form-table-contact-entered.png](images/glimmer-dsl-libui-windows-form-table-contact-entered.png) ![glimmer-dsl-libui-windows-form-table-filtered.png](images/glimmer-dsl-libui-windows-form-table-filtered.png) | ![glimmer-dsl-libui-linux-form-table.png](images/glimmer-dsl-libui-linux-form-table.png) ![glimmer-dsl-libui-linux-form-table-contact-entered.png](images/glimmer-dsl-libui-linux-form-table-contact-entered.png) ![glimmer-dsl-libui-linux-form-table-filtered.png](images/glimmer-dsl-libui-linux-form-table-filtered.png)
5946
6400
 
5947
- New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version:
6401
+ New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version (with [data-binding](#data-binding)):
6402
+
6403
+ ```ruby
6404
+ require 'glimmer-dsl-libui'
6405
+
6406
+ class FormTable
6407
+ include Glimmer
6408
+
6409
+ attr_accessor :name, :email, :phone, :city, :state, :filter_value
6410
+
6411
+ def initialize
6412
+ @data = [
6413
+ ['Lisa Sky', 'lisa@sky.com', '720-523-4329', 'Denver', 'CO', '80014'],
6414
+ ['Jordan Biggins', 'jordan@biggins.com', '617-528-5399', 'Boston', 'MA', '02101'],
6415
+ ['Mary Glass', 'mary@glass.com', '847-589-8788', 'Elk Grove Village', 'IL', '60007'],
6416
+ ['Darren McGrath', 'darren@mcgrath.com', '206-539-9283', 'Seattle', 'WA', '98101'],
6417
+ ['Melody Hanheimer', 'melody@hanheimer.com', '213-493-8274', 'Los Angeles', 'CA', '90001'],
6418
+ ]
6419
+ end
6420
+
6421
+ def launch
6422
+ window('Contacts', 600, 600) { |w|
6423
+ margined true
6424
+
6425
+ vertical_box {
6426
+ form {
6427
+ stretchy false
6428
+
6429
+ entry {
6430
+ label 'Name'
6431
+ text <=> [self, :name]
6432
+ }
6433
+
6434
+ entry {
6435
+ label 'Email'
6436
+ text <=> [self, :email]
6437
+ }
6438
+
6439
+ entry {
6440
+ label 'Phone'
6441
+ text <=> [self, :phone]
6442
+ }
6443
+
6444
+ entry {
6445
+ label 'City'
6446
+ text <=> [self, :city]
6447
+ }
6448
+
6449
+ entry {
6450
+ label 'State'
6451
+ text <=> [self, :state]
6452
+ }
6453
+ }
6454
+
6455
+ button('Save Contact') {
6456
+ stretchy false
6457
+
6458
+ on_clicked do
6459
+ new_row = [name, email, phone, city, state]
6460
+ if new_row.include?('')
6461
+ msg_box_error(w, 'Validation Error!', 'All fields are required! Please make sure to enter a value for all fields.')
6462
+ else
6463
+ @data << new_row # automatically inserts a row into the table due to implicit data-binding
6464
+ @unfiltered_data = @data.dup
6465
+ self.name = '' # automatically clears name entry through explicit data-binding
6466
+ self.email = ''
6467
+ self.phone = ''
6468
+ self.city = ''
6469
+ self.state = ''
6470
+ end
6471
+ end
6472
+ }
6473
+
6474
+ search_entry {
6475
+ stretchy false
6476
+ text <=> [self, :filter_value, # bidirectional data-binding of text to self.filter_value with after_write option
6477
+ after_write: ->(filter_value) { # execute after write to self.filter_value
6478
+ @unfiltered_data ||= @data.dup
6479
+ # Unfilter first to remove any previous filters
6480
+ @data.replace(@unfiltered_data) # affects table indirectly through implicit data-binding
6481
+ # Now, apply filter if entered
6482
+ unless filter_value.empty?
6483
+ @data.filter! do |row_data| # affects table indirectly through implicit data-binding
6484
+ row_data.any? do |cell|
6485
+ cell.to_s.downcase.include?(filter_value.downcase)
6486
+ end
6487
+ end
6488
+ end
6489
+ }
6490
+ ]
6491
+ }
6492
+
6493
+ table {
6494
+ text_column('Name')
6495
+ text_column('Email')
6496
+ text_column('Phone')
6497
+ text_column('City')
6498
+ text_column('State')
6499
+
6500
+ cell_rows @data # implicit data-binding
6501
+
6502
+ on_changed do |row, type, row_data|
6503
+ puts "Row #{row} #{type}: #{row_data}"
6504
+ end
6505
+ }
6506
+ }
6507
+ }.show
6508
+ end
6509
+ end
6510
+
6511
+ FormTable.new.launch
6512
+ ```
6513
+
6514
+ New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version 2 (without [data-binding](#data-binding)):
5948
6515
 
5949
6516
  ```ruby
5950
6517
  require 'glimmer-dsl-libui'
@@ -6387,53 +6954,168 @@ UI.main
6387
6954
  UI.quit
6388
6955
  ```
6389
6956
 
6390
- [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version:
6957
+ [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version (with [data-binding](#data-binding)):
6391
6958
 
6392
6959
  ```ruby
6393
6960
  # https://github.com/jamescook/libui-ruby/blob/master/example/histogram.rb
6394
6961
 
6395
6962
  require 'glimmer-dsl-libui'
6396
6963
 
6397
- include Glimmer
6398
-
6399
- X_OFF_LEFT = 20
6400
- Y_OFF_TOP = 20
6401
- X_OFF_RIGHT = 20
6402
- Y_OFF_BOTTOM = 20
6403
- POINT_RADIUS = 5
6404
- COLOR_BLUE = 0x1E90FF
6405
-
6406
- @datapoints = 10.times.map {Random.new.rand(90)}
6407
-
6408
- def graph_size(area_width, area_height)
6409
- graph_width = area_width - X_OFF_LEFT - X_OFF_RIGHT
6410
- graph_height = area_height - Y_OFF_TOP - Y_OFF_BOTTOM
6411
- [graph_width, graph_height]
6412
- end
6413
-
6414
- def point_locations(width, height)
6415
- xincr = width / 9.0 # 10 - 1 to make the last point be at the end
6416
- yincr = height / 100.0
6417
-
6418
- @datapoints.each_with_index.map do |value, i|
6419
- val = 100 - value
6420
- [xincr * i, yincr * val]
6964
+ class Histogram
6965
+ include Glimmer
6966
+
6967
+ X_OFF_LEFT = 20
6968
+ Y_OFF_TOP = 20
6969
+ X_OFF_RIGHT = 20
6970
+ Y_OFF_BOTTOM = 20
6971
+ POINT_RADIUS = 5
6972
+ COLOR_BLUE = Glimmer::LibUI.interpret_color(0x1E90FF)
6973
+
6974
+ attr_accessor :datapoints, :histogram_color
6975
+
6976
+ def initialize
6977
+ @datapoints = 10.times.map {Random.new.rand(90)}
6978
+ @histogram_color = COLOR_BLUE
6421
6979
  end
6422
- end
6423
-
6424
- # method-based custom control representing a graph path
6425
- def graph_path(width, height, should_extend, &block)
6426
- locations = point_locations(width, height).flatten
6427
- path {
6428
- if should_extend
6429
- polygon(locations + [width, height, 0, height])
6430
- else
6431
- polyline(locations)
6980
+
6981
+ def graph_size(area_width, area_height)
6982
+ graph_width = area_width - X_OFF_LEFT - X_OFF_RIGHT
6983
+ graph_height = area_height - Y_OFF_TOP - Y_OFF_BOTTOM
6984
+ [graph_width, graph_height]
6985
+ end
6986
+
6987
+ def point_locations(width, height)
6988
+ xincr = width / 9.0 # 10 - 1 to make the last point be at the end
6989
+ yincr = height / 100.0
6990
+
6991
+ @datapoints.each_with_index.map do |value, i|
6992
+ val = 100 - value
6993
+ [xincr * i, yincr * val]
6432
6994
  end
6433
-
6434
- # apply a transform to the coordinate space for this path so (0, 0) is the top-left corner of the graph
6435
- transform {
6436
- translate X_OFF_LEFT, Y_OFF_TOP
6995
+ end
6996
+
6997
+ # method-based custom control representing a graph path
6998
+ def graph_path(width, height, should_extend, &block)
6999
+ locations = point_locations(width, height).flatten
7000
+ path {
7001
+ if should_extend
7002
+ polygon(locations + [width, height, 0, height])
7003
+ else
7004
+ polyline(locations)
7005
+ end
7006
+
7007
+ # apply a transform to the coordinate space for this path so (0, 0) is the top-left corner of the graph
7008
+ transform {
7009
+ translate X_OFF_LEFT, Y_OFF_TOP
7010
+ }
7011
+
7012
+ block.call
7013
+ }
7014
+ end
7015
+
7016
+ def launch
7017
+ window('histogram example', 640, 480) {
7018
+ margined true
7019
+
7020
+ horizontal_box {
7021
+ vertical_box {
7022
+ stretchy false
7023
+
7024
+ 10.times do |i|
7025
+ spinbox(0, 100) { |sb|
7026
+ stretchy false
7027
+ value <=> [self, "datapoints[#{i}]", after_write: -> { @area.queue_redraw_all }]
7028
+ }
7029
+ end
7030
+
7031
+ color_button { |cb|
7032
+ stretchy false
7033
+ color <=> [self, :histogram_color, after_write: -> { @area.queue_redraw_all }]
7034
+ }
7035
+ }
7036
+
7037
+ @area = area {
7038
+ on_draw do |area_draw_params|
7039
+ rectangle(0, 0, area_draw_params[:area_width], area_draw_params[:area_height]) {
7040
+ fill 0xFFFFFF
7041
+ }
7042
+
7043
+ graph_width, graph_height = *graph_size(area_draw_params[:area_width], area_draw_params[:area_height])
7044
+
7045
+ figure(X_OFF_LEFT, Y_OFF_TOP) {
7046
+ line(X_OFF_LEFT, Y_OFF_TOP + graph_height)
7047
+ line(X_OFF_LEFT + graph_width, Y_OFF_TOP + graph_height)
7048
+
7049
+ stroke 0x000000, thickness: 2, miter_limit: 10
7050
+ }
7051
+
7052
+ # now create the fill for the graph below the graph line
7053
+ graph_path(graph_width, graph_height, true) {
7054
+ fill @histogram_color.merge(a: 0.5)
7055
+ }
7056
+
7057
+ # now draw the histogram line
7058
+ graph_path(graph_width, graph_height, false) {
7059
+ stroke @histogram_color.merge(thickness: 2, miter_limit: 10)
7060
+ }
7061
+ end
7062
+ }
7063
+ }
7064
+ }.show
7065
+ end
7066
+ end
7067
+
7068
+ Histogram.new.launch
7069
+ ```
7070
+
7071
+ [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version 2 (without [data-binding](#data-binding)):
7072
+
7073
+ ```ruby
7074
+ # https://github.com/jamescook/libui-ruby/blob/master/example/histogram.rb
7075
+
7076
+ require 'glimmer-dsl-libui'
7077
+
7078
+ include Glimmer
7079
+
7080
+ X_OFF_LEFT = 20
7081
+ Y_OFF_TOP = 20
7082
+ X_OFF_RIGHT = 20
7083
+ Y_OFF_BOTTOM = 20
7084
+ POINT_RADIUS = 5
7085
+ COLOR_BLUE = Glimmer::LibUI.interpret_color(0x1E90FF)
7086
+
7087
+ @datapoints = 10.times.map {Random.new.rand(90)}
7088
+ @color = COLOR_BLUE
7089
+
7090
+ def graph_size(area_width, area_height)
7091
+ graph_width = area_width - X_OFF_LEFT - X_OFF_RIGHT
7092
+ graph_height = area_height - Y_OFF_TOP - Y_OFF_BOTTOM
7093
+ [graph_width, graph_height]
7094
+ end
7095
+
7096
+ def point_locations(width, height)
7097
+ xincr = width / 9.0 # 10 - 1 to make the last point be at the end
7098
+ yincr = height / 100.0
7099
+
7100
+ @datapoints.each_with_index.map do |value, i|
7101
+ val = 100 - value
7102
+ [xincr * i, yincr * val]
7103
+ end
7104
+ end
7105
+
7106
+ # method-based custom control representing a graph path
7107
+ def graph_path(width, height, should_extend, &block)
7108
+ locations = point_locations(width, height).flatten
7109
+ path {
7110
+ if should_extend
7111
+ polygon(locations + [width, height, 0, height])
7112
+ else
7113
+ polyline(locations)
7114
+ end
7115
+
7116
+ # apply a transform to the coordinate space for this path so (0, 0) is the top-left corner of the graph
7117
+ transform {
7118
+ translate X_OFF_LEFT, Y_OFF_TOP
6437
7119
  }
6438
7120
 
6439
7121
  block.call
@@ -6459,11 +7141,12 @@ window('histogram example', 640, 480) {
6459
7141
  }
6460
7142
  end
6461
7143
 
6462
- @color_button = color_button {
7144
+ color_button { |cb|
6463
7145
  stretchy false
6464
7146
  color COLOR_BLUE
6465
7147
 
6466
7148
  on_changed do
7149
+ @color = cb.color
6467
7150
  @area.queue_redraw_all
6468
7151
  end
6469
7152
  }
@@ -6471,31 +7154,27 @@ window('histogram example', 640, 480) {
6471
7154
 
6472
7155
  @area = area {
6473
7156
  on_draw do |area_draw_params|
6474
- path {
6475
- rectangle(0, 0, area_draw_params[:area_width], area_draw_params[:area_height])
6476
-
7157
+ rectangle(0, 0, area_draw_params[:area_width], area_draw_params[:area_height]) {
6477
7158
  fill 0xFFFFFF
6478
7159
  }
6479
7160
 
6480
7161
  graph_width, graph_height = *graph_size(area_draw_params[:area_width], area_draw_params[:area_height])
6481
7162
 
6482
- path {
6483
- figure(X_OFF_LEFT, Y_OFF_TOP) {
6484
- line(X_OFF_LEFT, Y_OFF_TOP + graph_height)
6485
- line(X_OFF_LEFT + graph_width, Y_OFF_TOP + graph_height)
6486
- }
7163
+ figure(X_OFF_LEFT, Y_OFF_TOP) {
7164
+ line(X_OFF_LEFT, Y_OFF_TOP + graph_height)
7165
+ line(X_OFF_LEFT + graph_width, Y_OFF_TOP + graph_height)
6487
7166
 
6488
7167
  stroke 0x000000, thickness: 2, miter_limit: 10
6489
7168
  }
6490
7169
 
6491
7170
  # now create the fill for the graph below the graph line
6492
7171
  graph_path(graph_width, graph_height, true) {
6493
- fill @color_button.color.merge(a: 0.5)
7172
+ fill @color.merge(a: 0.5)
6494
7173
  }
6495
7174
 
6496
7175
  # now draw the histogram line
6497
7176
  graph_path(graph_width, graph_height, false) {
6498
- stroke @color_button.color.merge(thickness: 2, miter_limit: 10)
7177
+ stroke @color.merge(thickness: 2, miter_limit: 10)
6499
7178
  }
6500
7179
  end
6501
7180
  }
@@ -6523,7 +7202,259 @@ Mac | Windows | Linux
6523
7202
  ----|---------|------
6524
7203
  ![glimmer-dsl-libui-mac-login.png](images/glimmer-dsl-libui-mac-login.png) ![glimmer-dsl-libui-mac-login-logged-in.png](images/glimmer-dsl-libui-mac-login-logged-in.png) | ![glimmer-dsl-libui-windows-login.png](images/glimmer-dsl-libui-windows-login.png) ![glimmer-dsl-libui-windows-login-logged-in.png](images/glimmer-dsl-libui-windows-login-logged-in.png) | ![glimmer-dsl-libui-linux-login.png](images/glimmer-dsl-libui-linux-login.png) ![glimmer-dsl-libui-linux-login-logged-in.png](images/glimmer-dsl-libui-linux-login-logged-in.png)
6525
7204
 
6526
- New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version:
7205
+ New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version (with [data-binding](#data-binding)):
7206
+
7207
+ ```ruby
7208
+ require 'glimmer-dsl-libui'
7209
+
7210
+ class Login
7211
+ include Glimmer
7212
+
7213
+ attr_accessor :username, :password, :logged_in
7214
+
7215
+ def launch
7216
+ window('Login') {
7217
+ margined true
7218
+
7219
+ vertical_box {
7220
+ form {
7221
+ entry {
7222
+ label 'Username:'
7223
+ text <=> [self, :username]
7224
+ enabled <= [self, :logged_in, on_read: :!]
7225
+ }
7226
+
7227
+ password_entry {
7228
+ label 'Password:'
7229
+ text <=> [self, :password]
7230
+ enabled <= [self, :logged_in, on_read: :!]
7231
+ }
7232
+ }
7233
+
7234
+ horizontal_box {
7235
+ button('Login') {
7236
+ enabled <= [self, :logged_in, on_read: :!]
7237
+
7238
+ on_clicked do
7239
+ self.logged_in = true
7240
+ end
7241
+ }
7242
+
7243
+ button('Logout') {
7244
+ enabled <= [self, :logged_in]
7245
+
7246
+ on_clicked do
7247
+ self.logged_in = false
7248
+ self.username = ''
7249
+ self.password = ''
7250
+ end
7251
+ }
7252
+ }
7253
+ }
7254
+ }.show
7255
+ end
7256
+ end
7257
+
7258
+ Login.new.launch
7259
+ ```
7260
+
7261
+ New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version 2 (with [data-binding](#data-binding)):
7262
+
7263
+ ```ruby
7264
+ require 'glimmer-dsl-libui'
7265
+
7266
+ class Login
7267
+ include Glimmer
7268
+
7269
+ attr_accessor :username, :password, :logged_in
7270
+
7271
+ def logged_out
7272
+ !logged_in
7273
+ end
7274
+
7275
+ def launch
7276
+ window('Login') {
7277
+ margined true
7278
+
7279
+ vertical_box {
7280
+ form {
7281
+ entry {
7282
+ label 'Username:'
7283
+ text <=> [self, :username]
7284
+ enabled <= [self, :logged_out, computed_by: :logged_in] # computed_by option ensures being notified of changes to logged_in
7285
+ }
7286
+
7287
+ password_entry {
7288
+ label 'Password:'
7289
+ text <=> [self, :password]
7290
+ enabled <= [self, :logged_out, computed_by: :logged_in]
7291
+ }
7292
+ }
7293
+
7294
+ horizontal_box {
7295
+ button('Login') {
7296
+ enabled <= [self, :logged_out, computed_by: :logged_in]
7297
+
7298
+ on_clicked do
7299
+ self.logged_in = true
7300
+ end
7301
+ }
7302
+
7303
+ button('Logout') {
7304
+ enabled <= [self, :logged_in]
7305
+
7306
+ on_clicked do
7307
+ self.logged_in = false
7308
+ self.username = ''
7309
+ self.password = ''
7310
+ end
7311
+ }
7312
+ }
7313
+ }
7314
+ }.show
7315
+ end
7316
+ end
7317
+
7318
+ Login.new.launch
7319
+ ```
7320
+
7321
+ New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version 3 (with [data-binding](#data-binding)):
7322
+
7323
+ ```ruby
7324
+ require 'glimmer-dsl-libui'
7325
+
7326
+ class Login
7327
+ include Glimmer
7328
+
7329
+ attr_accessor :username, :password
7330
+ attr_reader :logged_in
7331
+
7332
+ def logged_in=(value)
7333
+ @logged_in = value
7334
+ self.logged_out = !value # calling logged_out= method notifies logged_out observers
7335
+ end
7336
+
7337
+ def logged_out=(value)
7338
+ self.logged_in = !value unless logged_in == !value
7339
+ end
7340
+
7341
+ def logged_out
7342
+ !logged_in
7343
+ end
7344
+
7345
+ def launch
7346
+ window('Login') {
7347
+ margined true
7348
+
7349
+ vertical_box {
7350
+ form {
7351
+ entry {
7352
+ label 'Username:'
7353
+ text <=> [self, :username]
7354
+ enabled <= [self, :logged_out]
7355
+ }
7356
+
7357
+ password_entry {
7358
+ label 'Password:'
7359
+ text <=> [self, :password]
7360
+ enabled <= [self, :logged_out]
7361
+ }
7362
+ }
7363
+
7364
+ horizontal_box {
7365
+ button('Login') {
7366
+ enabled <= [self, :logged_out]
7367
+
7368
+ on_clicked do
7369
+ self.logged_in = true
7370
+ end
7371
+ }
7372
+
7373
+ button('Logout') {
7374
+ enabled <= [self, :logged_in]
7375
+
7376
+ on_clicked do
7377
+ self.logged_in = false
7378
+ self.username = ''
7379
+ self.password = ''
7380
+ end
7381
+ }
7382
+ }
7383
+ }
7384
+ }.show
7385
+ end
7386
+ end
7387
+
7388
+ Login.new.launch
7389
+ ```
7390
+
7391
+ New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version 4 (with [data-binding](#data-binding)):
7392
+
7393
+ ```ruby
7394
+ require 'glimmer-dsl-libui'
7395
+
7396
+ class Login
7397
+ include Glimmer
7398
+
7399
+ attr_accessor :username, :password
7400
+ attr_reader :logged_in
7401
+
7402
+ def logged_in=(value)
7403
+ @logged_in = value
7404
+ notify_observers(:logged_out) # manually notify observers of logged_out upon logged_in changes; this method comes automatically from enhancement as Glimmer::DataBinding::ObservableModel via data-binding
7405
+ end
7406
+
7407
+ def logged_out
7408
+ !logged_in
7409
+ end
7410
+
7411
+ def launch
7412
+ window('Login') {
7413
+ margined true
7414
+
7415
+ vertical_box {
7416
+ form {
7417
+ entry {
7418
+ label 'Username:'
7419
+ text <=> [self, :username]
7420
+ enabled <= [self, :logged_out]
7421
+ }
7422
+
7423
+ password_entry {
7424
+ label 'Password:'
7425
+ text <=> [self, :password]
7426
+ enabled <= [self, :logged_out]
7427
+ }
7428
+ }
7429
+
7430
+ horizontal_box {
7431
+ button('Login') {
7432
+ enabled <= [self, :logged_out]
7433
+
7434
+ on_clicked do
7435
+ self.logged_in = true
7436
+ end
7437
+ }
7438
+
7439
+ button('Logout') {
7440
+ enabled <= [self, :logged_in]
7441
+
7442
+ on_clicked do
7443
+ self.logged_in = false
7444
+ self.username = ''
7445
+ self.password = ''
7446
+ end
7447
+ }
7448
+ }
7449
+ }
7450
+ }.show
7451
+ end
7452
+ end
7453
+
7454
+ Login.new.launch
7455
+ ```
7456
+
7457
+ New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version 5 (without [data-binding](#data-binding)):
6527
7458
 
6528
7459
  ```ruby
6529
7460
  require 'glimmer-dsl-libui'
@@ -6591,7 +7522,102 @@ Mac | Windows | Linux
6591
7522
  ----|---------|------
6592
7523
  ![glimmer-dsl-libui-mac-method-based-custom-keyword.png](images/glimmer-dsl-libui-mac-method-based-custom-keyword.png) | ![glimmer-dsl-libui-windows-method-based-custom-keyword.png](images/glimmer-dsl-libui-windows-method-based-custom-keyword.png) | ![glimmer-dsl-libui-linux-method-based-custom-keyword.png](images/glimmer-dsl-libui-linux-method-based-custom-keyword.png)
6593
7524
 
6594
- New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version:
7525
+ New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version (with [data-binding](#data-binding)):
7526
+
7527
+ ```ruby
7528
+ require 'glimmer-dsl-libui'
7529
+ require 'facets'
7530
+
7531
+ include Glimmer
7532
+
7533
+ Address = Struct.new(:street, :p_o_box, :city, :state, :zip_code)
7534
+
7535
+ def form_field(model, attribute)
7536
+ attribute = attribute.to_s
7537
+ entry { |e|
7538
+ label attribute.underscore.split('_').map(&:capitalize).join(' ')
7539
+ text <=> [model, attribute]
7540
+ }
7541
+ end
7542
+
7543
+ def address_form(address)
7544
+ form {
7545
+ form_field(address, :street)
7546
+ form_field(address, :p_o_box)
7547
+ form_field(address, :city)
7548
+ form_field(address, :state)
7549
+ form_field(address, :zip_code)
7550
+ }
7551
+ end
7552
+
7553
+ def label_pair(model, attribute, value)
7554
+ horizontal_box {
7555
+ label(attribute.to_s.underscore.split('_').map(&:capitalize).join(' '))
7556
+ label(value.to_s) {
7557
+ text <= [model, attribute]
7558
+ }
7559
+ }
7560
+ end
7561
+
7562
+ def address(address)
7563
+ vertical_box {
7564
+ address.each_pair do |attribute, value|
7565
+ label_pair(address, attribute, value)
7566
+ end
7567
+ }
7568
+ end
7569
+
7570
+ address1 = Address.new('123 Main St', '23923', 'Denver', 'Colorado', '80014')
7571
+ address2 = Address.new('2038 Park Ave', '83272', 'Boston', 'Massachusetts', '02101')
7572
+
7573
+ window('Method-Based Custom Keyword') {
7574
+ margined true
7575
+
7576
+ horizontal_box {
7577
+ vertical_box {
7578
+ label('Address 1') {
7579
+ stretchy false
7580
+ }
7581
+
7582
+ address_form(address1)
7583
+
7584
+ horizontal_separator {
7585
+ stretchy false
7586
+ }
7587
+
7588
+ label('Address 1 (Saved)') {
7589
+ stretchy false
7590
+ }
7591
+
7592
+ address(address1)
7593
+ }
7594
+
7595
+ vertical_separator {
7596
+ stretchy false
7597
+ }
7598
+
7599
+ vertical_box {
7600
+ label('Address 2') {
7601
+ stretchy false
7602
+ }
7603
+
7604
+ address_form(address2)
7605
+
7606
+ horizontal_separator {
7607
+ stretchy false
7608
+ }
7609
+
7610
+ label('Address 2 (Saved)') {
7611
+ stretchy false
7612
+ }
7613
+
7614
+ address(address2)
7615
+ }
7616
+ }
7617
+ }.show
7618
+ ```
7619
+
7620
+ New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version 2 (without [data-binding](#data-binding)):
6595
7621
 
6596
7622
  ```ruby
6597
7623
  require 'glimmer-dsl-libui'
@@ -7537,7 +8563,140 @@ Mac | Windows | Linux
7537
8563
  ----|---------|------
7538
8564
  ![glimmer-dsl-libui-mac-timer.png](images/glimmer-dsl-libui-mac-timer.png) ![glimmer-dsl-libui-mac-timer-in-progress.png](images/glimmer-dsl-libui-mac-timer-in-progress.png) | ![glimmer-dsl-libui-windows-timer.png](images/glimmer-dsl-libui-windows-timer.png) ![glimmer-dsl-libui-windows-timer-in-progress.png](images/glimmer-dsl-libui-windows-timer-in-progress.png) | ![glimmer-dsl-libui-linux-timer.png](images/glimmer-dsl-libui-linux-timer.png) ![glimmer-dsl-libui-linux-timer-in-progress.png](images/glimmer-dsl-libui-linux-timer-in-progress.png)
7539
8565
 
7540
- New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version:
8566
+ New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version (with [data-binding](#data-binding)):
8567
+
8568
+ ```ruby
8569
+ require 'glimmer-dsl-libui'
8570
+
8571
+ class Timer
8572
+ include Glimmer
8573
+
8574
+ SECOND_MAX = 59
8575
+ MINUTE_MAX = 59
8576
+ HOUR_MAX = 23
8577
+
8578
+ attr_accessor :hour, :min, :sec, :started, :played
8579
+
8580
+ def initialize
8581
+ @pid = nil
8582
+ @alarm_file = File.expand_path('../sounds/AlanWalker-Faded.mid', __dir__)
8583
+ @hour = @min = @sec = 0
8584
+ at_exit { stop_alarm }
8585
+ setup_timer
8586
+ create_gui
8587
+ end
8588
+
8589
+ def stop_alarm
8590
+ if @pid
8591
+ Process.kill(:SIGKILL, @pid) if @th.alive?
8592
+ @pid = nil
8593
+ end
8594
+ end
8595
+
8596
+ def play_alarm
8597
+ stop_alarm
8598
+ if @pid.nil?
8599
+ begin
8600
+ @pid = spawn "timidity -G 0.0-10.0 #{@alarm_file}"
8601
+ @th = Process.detach @pid
8602
+ rescue Errno::ENOENT
8603
+ warn 'Timidty++ not found. Please install Timidity++.'
8604
+ warn 'https://sourceforge.net/projects/timidity/'
8605
+ end
8606
+ end
8607
+ end
8608
+
8609
+ def setup_timer
8610
+ unless @setup_timer
8611
+ Glimmer::LibUI.timer(1) do
8612
+ if @started
8613
+ seconds = @sec
8614
+ minutes = @min
8615
+ hours = @hour
8616
+ if seconds > 0
8617
+ self.sec = seconds -= 1
8618
+ end
8619
+ if seconds == 0
8620
+ if minutes > 0
8621
+ self.min = minutes -= 1
8622
+ self.sec = seconds = SECOND_MAX
8623
+ end
8624
+ if minutes == 0
8625
+ if hours > 0
8626
+ self.hour = hours -= 1
8627
+ self.min = minutes = MINUTE_MAX
8628
+ self.sec = seconds = SECOND_MAX
8629
+ end
8630
+ if hours == 0 && minutes == 0 && seconds == 0
8631
+ self.started = false
8632
+ unless @played
8633
+ play_alarm
8634
+ msg_box('Alarm', 'Countdown Is Finished!')
8635
+ self.played = true
8636
+ end
8637
+ end
8638
+ end
8639
+ end
8640
+ end
8641
+ end
8642
+ @setup_timer = true
8643
+ end
8644
+ end
8645
+
8646
+ def create_gui
8647
+ window('Timer') {
8648
+ margined true
8649
+
8650
+ group('Countdown') {
8651
+ vertical_box {
8652
+ horizontal_box {
8653
+ spinbox(0, HOUR_MAX) {
8654
+ stretchy false
8655
+ value <=> [self, :hour]
8656
+ }
8657
+ label(':') {
8658
+ stretchy false
8659
+ }
8660
+ spinbox(0, MINUTE_MAX) {
8661
+ stretchy false
8662
+ value <=> [self, :min]
8663
+ }
8664
+ label(':') {
8665
+ stretchy false
8666
+ }
8667
+ spinbox(0, SECOND_MAX) {
8668
+ stretchy false
8669
+ value <=> [self, :sec]
8670
+ }
8671
+ }
8672
+ horizontal_box {
8673
+ button('Start') {
8674
+ enabled <= [self, :started, on_read: :!]
8675
+
8676
+ on_clicked do
8677
+ self.started = true
8678
+ self.played = false
8679
+ end
8680
+ }
8681
+
8682
+ button('Stop') {
8683
+ enabled <= [self, :started]
8684
+
8685
+ on_clicked do
8686
+ self.started = false
8687
+ end
8688
+ }
8689
+ }
8690
+ }
8691
+ }
8692
+ }.show
8693
+ end
8694
+ end
8695
+
8696
+ Timer.new
8697
+ ```
8698
+
8699
+ New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version 2 (without [data-binding](#data-binding)):
7541
8700
 
7542
8701
  ```ruby
7543
8702
  require 'glimmer-dsl-libui'