glimmer-dsl-libui 0.2.6 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -0
- data/README.md +530 -92
- data/VERSION +1 -1
- data/examples/area_gallery.rb +18 -8
- data/examples/area_gallery2.rb +26 -12
- data/examples/area_gallery3.rb +16 -6
- data/examples/area_gallery4.rb +26 -12
- data/examples/basic_table_button.rb +4 -0
- data/examples/basic_table_color.rb +39 -0
- data/examples/basic_table_progress_bar.rb +1 -1
- data/examples/color_button.rb +2 -0
- data/examples/custom_draw_text.rb +1 -1
- data/examples/custom_draw_text2.rb +1 -1
- data/examples/editable_table.rb +8 -0
- data/examples/form.rb +12 -2
- data/examples/form_table.rb +4 -0
- data/examples/meta_example.rb +27 -17
- data/examples/method_based_custom_keyword.rb +87 -0
- data/glimmer-dsl-libui.gemspec +0 -0
- data/icons/glimmer.png +0 -0
- data/lib/glimmer/libui/control_proxy/column/background_color_column_proxy.rb +38 -0
- data/lib/glimmer/libui/control_proxy/column/checkbox_text_color_column_proxy.rb +82 -0
- data/lib/glimmer/libui/control_proxy/column/image_text_color_column_proxy.rb +50 -0
- data/lib/glimmer/libui/control_proxy/column/text_color_column_proxy.rb +50 -0
- data/lib/glimmer/libui/control_proxy/editable_column.rb +1 -0
- data/lib/glimmer/libui/control_proxy/path_proxy.rb +31 -6
- data/lib/glimmer/libui/control_proxy/table_proxy.rb +53 -14
- data/lib/glimmer/libui/control_proxy/text_proxy.rb +1 -1
- data/lib/glimmer/libui/control_proxy/triple_column.rb +45 -0
- data/lib/glimmer/libui.rb +1 -1
- metadata +12 -4
    
        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.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.2.10
         | 
| 2 2 | 
             
            ## Prerequisite-Free Ruby Desktop Development GUI Library
         | 
| 3 3 | 
             
            [](http://badge.fury.io/rb/glimmer-dsl-libui)
         | 
| 4 4 | 
             
            [](https://codeclimate.com/github/AndyObtiva/glimmer-dsl-libui/maintainability)
         | 
| @@ -14,10 +14,10 @@ The main trade-off in using [Glimmer DSL for LibUI](https://rubygems.org/gems/gl | |
| 14 14 | 
             
            - Declarative DSL syntax that visually maps to the GUI control hierarchy
         | 
| 15 15 | 
             
            - Convention over configuration via smart defaults and automation of low-level details
         | 
| 16 16 | 
             
            - Requiring the least amount of syntax possible to build GUI
         | 
| 17 | 
            -
            - Bidirectional Data-Binding to declaratively wire and automatically synchronize GUI with Business Models
         | 
| 18 17 | 
             
            - Custom Control support
         | 
| 19 | 
            -
            -  | 
| 20 | 
            -
            -  | 
| 18 | 
            +
            - [Far Future Plan] Bidirectional Data-Binding to declaratively wire and automatically synchronize GUI with Business Models
         | 
| 19 | 
            +
            - [Far Future Plan] Scaffolding for new custom controls, apps, and gems
         | 
| 20 | 
            +
            - [Far Future Plan] Native-Executable packaging on Mac, Windows, and Linux.
         | 
| 21 21 |  | 
| 22 22 | 
             
            Hello, World!
         | 
| 23 23 |  | 
| @@ -61,7 +61,7 @@ window('Task Progress', 300, 200) { | |
| 61 61 |  | 
| 62 62 | 
             
                  on_clicked do
         | 
| 63 63 | 
             
                    data.each_with_index do |row_data, row|
         | 
| 64 | 
            -
                      data[row] =  | 
| 64 | 
            +
                      data[row][1] = 100 # automatically updates table due to implicit data-binding
         | 
| 65 65 | 
             
                    end
         | 
| 66 66 | 
             
                  end
         | 
| 67 67 | 
             
                }
         | 
| @@ -84,14 +84,15 @@ window('Area Gallery', 400, 400) { | |
| 84 84 | 
             
                path { # declarative stable path
         | 
| 85 85 | 
             
                  square(0, 0, 100)
         | 
| 86 86 | 
             
                  square(100, 100, 400)
         | 
| 87 | 
            -
             | 
| 87 | 
            +
             | 
| 88 88 | 
             
                  fill r: 102, g: 102, b: 204
         | 
| 89 89 | 
             
                }
         | 
| 90 90 | 
             
                path { # declarative stable path
         | 
| 91 91 | 
             
                  rectangle(0, 100, 100, 400)
         | 
| 92 92 | 
             
                  rectangle(100, 0, 400, 100)
         | 
| 93 | 
            -
             | 
| 94 | 
            -
                   | 
| 93 | 
            +
             | 
| 94 | 
            +
                  # linear gradient (has x0, y0, x1, y1, and stops)
         | 
| 95 | 
            +
                  fill x0: 10, y0: 10, x1: 350, y1: 350, stops: [{pos: 0.25, r: 204, g: 102, b: 204}, {pos: 0.75, r: 102, g: 102, b: 204}]
         | 
| 95 96 | 
             
                }
         | 
| 96 97 | 
             
                path { # declarative stable path
         | 
| 97 98 | 
             
                  figure(100, 100) {
         | 
| @@ -117,17 +118,26 @@ window('Area Gallery', 400, 400) { | |
| 117 118 | 
             
                  fill r: 202, g: 102, b: 204, a: 0.5
         | 
| 118 119 | 
             
                  stroke r: 0, g: 0, b: 0, thickness: 2, dashes: [50, 10, 10, 10], dash_phase: -50.0
         | 
| 119 120 | 
             
                }
         | 
| 121 | 
            +
                path { # declarative stable path
         | 
| 122 | 
            +
                  arc(400, 220, 180, 90, 90, false)
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                  # radial gradient (has an outer_radius in addition to x0, y0, x1, y1, and stops)
         | 
| 125 | 
            +
                  fill outer_radius: 90, x0: 0, y0: 0, x1: 500, y1: 500, stops: [{pos: 0.25, r: 102, g: 102, b: 204, a: 0.5}, {pos: 0.75, r: 204, g: 102, b: 204}]
         | 
| 126 | 
            +
                  stroke r: 0, g: 0, b: 0, thickness: 2, dashes: [50, 10, 10, 10], dash_phase: -50.0
         | 
| 127 | 
            +
                }
         | 
| 120 128 | 
             
                path { # declarative stable path
         | 
| 121 129 | 
             
                  circle(200, 200, 90)
         | 
| 122 130 |  | 
| 123 131 | 
             
                  fill r: 202, g: 102, b: 204, a: 0.5
         | 
| 124 132 | 
             
                  stroke r: 0, g: 0, b: 0, thickness: 2
         | 
| 125 133 | 
             
                }
         | 
| 126 | 
            -
                 | 
| 127 | 
            -
                   | 
| 128 | 
            -
             | 
| 129 | 
            -
             | 
| 130 | 
            -
             | 
| 134 | 
            +
                text(160, 40, 100) { # x, y, width
         | 
| 135 | 
            +
                  string {
         | 
| 136 | 
            +
                    font family: 'Times', size: 14
         | 
| 137 | 
            +
                    color :black
         | 
| 138 | 
            +
                    
         | 
| 139 | 
            +
                    'Area Gallery'
         | 
| 140 | 
            +
                  }
         | 
| 131 141 | 
             
                }
         | 
| 132 142 |  | 
| 133 143 | 
             
                on_mouse_event do |area_mouse_event|
         | 
| @@ -186,7 +196,7 @@ window('Area Gallery', 400, 400) { | |
| 186 196 |  | 
| 187 197 | 
             
            [Check Out Many More Examples Over Here!](#examples)
         | 
| 188 198 |  | 
| 189 | 
            -
            NOTE: [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) is in  | 
| 199 | 
            +
            NOTE: [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) is feature-complete and in beta mode (though the C [libui](https://github.com/andlabs/libui) is still mid-alpha). Please help make better by contributing, adopting for small or low risk projects, and providing feedback. The more feedback and issues you report the better.
         | 
| 190 200 |  | 
| 191 201 | 
             
            Other [Glimmer](https://rubygems.org/gems/glimmer) DSL gems you might be interested in:
         | 
| 192 202 | 
             
            - [glimmer-dsl-swt](https://github.com/AndyObtiva/glimmer-dsl-swt): Glimmer DSL for SWT (JRuby Desktop Development GUI Framework)
         | 
| @@ -197,7 +207,7 @@ Other [Glimmer](https://rubygems.org/gems/glimmer) DSL gems you might be interes | |
| 197 207 |  | 
| 198 208 | 
             
            ## Table of Contents
         | 
| 199 209 |  | 
| 200 | 
            -
            - [Glimmer DSL for LibUI 0.2. | 
| 210 | 
            +
            - [Glimmer DSL for LibUI 0.2.10](#-glimmer-dsl-for-libui-0210)
         | 
| 201 211 | 
             
              - [Glimmer GUI DSL Concepts](#glimmer-gui-dsl-concepts)
         | 
| 202 212 | 
             
              - [Usage](#usage)
         | 
| 203 213 | 
             
              - [Girb (Glimmer IRB)](#girb-glimmer-irb)
         | 
| @@ -211,8 +221,10 @@ Other [Glimmer](https://rubygems.org/gems/glimmer) DSL gems you might be interes | |
| 211 221 | 
             
                - [Table API](#table-api)
         | 
| 212 222 | 
             
                - [Area API](#area-api)
         | 
| 213 223 | 
             
                - [Smart Defaults and Conventions](#smart-defaults-and-conventions)
         | 
| 224 | 
            +
                - [Custom Keywords](#custom-keywords)
         | 
| 214 225 | 
             
                - [API Gotchas](#api-gotchas)
         | 
| 215 226 | 
             
                - [Original API](#original-api)
         | 
| 227 | 
            +
              - [Packaging](#packaging)
         | 
| 216 228 | 
             
              - [Glimmer Style Guide](#glimmer-style-guide)
         | 
| 217 229 | 
             
              - [Examples](#examples)
         | 
| 218 230 | 
             
                - [Basic Window](#basic-window)
         | 
| @@ -235,6 +247,7 @@ Other [Glimmer](https://rubygems.org/gems/glimmer) DSL gems you might be interes | |
| 235 247 | 
             
                - [Basic Table Checkbox](#basic-table-checkbox)
         | 
| 236 248 | 
             
                - [Basic Table Checkbox Text](#basic-table-checkbox-text)
         | 
| 237 249 | 
             
                - [Basic Table Progress Bar](#basic-table-progress-bar)
         | 
| 250 | 
            +
                - [Basic Table Color](#basic-table-color)
         | 
| 238 251 | 
             
                - [Form Table](#form-table)
         | 
| 239 252 | 
             
                - [Basic Area](#basic-area)
         | 
| 240 253 | 
             
                - [Dynamic Area](#dynamic-area)
         | 
| @@ -246,6 +259,7 @@ Other [Glimmer](https://rubygems.org/gems/glimmer) DSL gems you might be interes | |
| 246 259 | 
             
                - [Color The Circles](#color-the-circles)
         | 
| 247 260 | 
             
                - [Basic Draw Text](#basic-draw-text)
         | 
| 248 261 | 
             
                - [Custom Draw Text](#custom-draw-text)
         | 
| 262 | 
            +
                - [Method-Based Custom Keyword](#method-based-custom-keyword)
         | 
| 249 263 | 
             
              - [Contributing to glimmer-dsl-libui](#contributing-to-glimmer-dsl-libui)
         | 
| 250 264 | 
             
              - [Help](#help)
         | 
| 251 265 | 
             
                - [Issues](#issues)
         | 
| @@ -333,7 +347,7 @@ gem install glimmer-dsl-libui | |
| 333 347 | 
             
            Or install via Bundler `Gemfile`:
         | 
| 334 348 |  | 
| 335 349 | 
             
            ```ruby
         | 
| 336 | 
            -
            gem 'glimmer-dsl-libui', '~> 0.2. | 
| 350 | 
            +
            gem 'glimmer-dsl-libui', '~> 0.2.10'
         | 
| 337 351 | 
             
            ```
         | 
| 338 352 |  | 
| 339 353 | 
             
            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.
         | 
| @@ -409,12 +423,14 @@ Control(Args) | Properties | Listeners | |
| 409 423 | 
             
            `about_menu_item` | None | `on_clicked`
         | 
| 410 424 | 
             
            `area` | None | `on_draw(area_draw_params)`, `on_mouse_event(area_mouse_event)`, `on_mouse_down(area_mouse_event)`, `on_mouse_up(area_mouse_event)`, `on_mouse_drag_started(area_mouse_event)`, `on_mouse_dragged(area_mouse_event)`, `on_mouse_dropped(area_mouse_event)`, `on_mouse_entered`, `on_mouse_exited`, `on_key_event(area_key_event)`, `on_key_down(area_key_event)`, `on_key_up(area_key_event)`
         | 
| 411 425 | 
             
            `arc(x_center as Numeric, y_center as Numeric, radius as Numeric, start_angle as Numeric, sweep as Numeric, is_negative as Boolean)` | `x_center` (`Numeric`), `y_center` (`Numeric`), `radius` (`Numeric`), `start_angle` (`Numeric`), `sweep` (`Numeric`), `is_negative` (Boolean) | None
         | 
| 426 | 
            +
            `background_color_column(name as String)` | None | None
         | 
| 412 427 | 
             
            `bezier(c1_x as Numeric, c1_y as Numeric, c2_x as Numeric, c2_y as Numeric, end_x as Numeric, end_y as Numeric)` | `c1_x` (`Numeric`), `c1_y` (`Numeric`), `c2_x` (`Numeric`), `c2_y` (`Numeric`), `end_x` (`Numeric`), `end_y` (`Numeric`) | None
         | 
| 413 428 | 
             
            `button(text as String)` | `text` (`String`) | `on_clicked`
         | 
| 414 429 | 
             
            `button_column(name as String)` | `enabled` (Boolean) | None
         | 
| 415 430 | 
             
            `checkbox(text as String)` | `checked` (Boolean), `text` (`String`) | `on_toggled`
         | 
| 416 431 | 
             
            `checkbox_column(name as String)` | `editable` (Boolean) | None
         | 
| 417 432 | 
             
            `checkbox_text_column(name as String)` | `editable` (Boolean), `editable_checkbox` (Boolean), `editable_text` (Boolean) | None
         | 
| 433 | 
            +
            `checkbox_text_color_column(name as String)` | `editable` (Boolean), `editable_checkbox` (Boolean), `editable_text` (Boolean) | None
         | 
| 418 434 | 
             
            `combobox` | `items` (`Array` of `String`), `selected` (`Integer`) | `on_selected`
         | 
| 419 435 | 
             
            `color_button` | `color` (Array of `red` as `Float`, `green` as `Float`, `blue` as `Float`, `alpha` as `Float`), `red` as `Float`, `green` as `Float`, `blue` as `Float`, `alpha` as `Float` | `on_changed`
         | 
| 420 436 | 
             
            `date_picker` | `time` (`Hash` of keys: `sec` as `Integer`, `min` as `Integer`, `hour` as `Integer`, `mday` as `Integer`, `mon` as `Integer`, `year` as `Integer`, `wday` as `Integer`, `yday` as `Integer`, `dst` as Boolean) | `on_changed`
         | 
| @@ -432,6 +448,7 @@ Control(Args) | Properties | Listeners | |
| 432 448 | 
             
            `image_part(pixels as String [encoded image rgba byte array], width as Numeric, height as Numeric, byte_stride as Numeric [usually width*4])` | None | None
         | 
| 433 449 | 
             
            `image_column(name as String)` | None | None
         | 
| 434 450 | 
             
            `image_text_column(name as String)` | None | None
         | 
| 451 | 
            +
            `image_text_color_column(name as String)` | None | None
         | 
| 435 452 | 
             
            `label(text as String)` | `text` (`String`) | None
         | 
| 436 453 | 
             
            `line(x as Numeric, y as Numeric)` | `x` (`Numeric`), `y` (`Numeric`) | None
         | 
| 437 454 | 
             
            `matrix(m11 = nil as Numeric, m12 = nil as Numeric, m21 = nil as Numeric, m22 = nil as Numeric, m31 = nil as Numeric, m32 = nil as Numeric)` | `m11` (`Numeric`), `m12` (`Numeric`), `m21` (`Numeric`), `m22` (`Numeric`), `m31` (`Numeric`), `m32` (`Numeric`) | None
         | 
| @@ -442,7 +459,7 @@ Control(Args) | Properties | Listeners | |
| 442 459 | 
             
            `msg_box_error(window = main_window as Glimmer::LibUI::WindowProxy, title as String, description as String)` | None | None
         | 
| 443 460 | 
             
            `non_wrapping_multiline_entry` | `read_only` (Boolean), `text` (`String`) | `on_changed`
         | 
| 444 461 | 
             
            `password_entry` | `read_only` (Boolean), `text` (`String`) | `on_changed`
         | 
| 445 | 
            -
            `path(draw_fill_mode = :winding)` | `fill` (`Hash` of `:r` as `0`-`255`, `:g` as `0`-`255`, `:b` as `0`-`255`, `:a` as `0.0`-`1.0 | 
| 462 | 
            +
            `path(draw_fill_mode = :winding)` | `fill` (`Hash` of `:r` as `0`-`255`, `:g` as `0`-`255`, `:b` as `0`-`255`, `:a` as `0.0`-`1.0`, hex, or [X11](https://en.wikipedia.org/wiki/X11_color_names) color), `stroke` (`Hash` of `:r` as `0`-`255`, `:g` as `0`-`255`, `:b` as `0`-`255`, `:a` as `0.0`-`1.0`, hex, or [X11](https://en.wikipedia.org/wiki/X11_color_names) color), `:cap` as (`:round`, `:square`, `:flat`), `:join` as (`:miter`, `:round`, `:bevel`), `:thickness` as `Numeric`, `:miter_limit` as `Numeric`, `:dashes` as `Array` of `Numeric` ) | None
         | 
| 446 463 | 
             
            `preferences_menu_item` | None | `on_clicked`
         | 
| 447 464 | 
             
            `progress_bar` | `value` (`Numeric`) | None
         | 
| 448 465 | 
             
            `progress_bar_column(name as String)` | None | None
         | 
| @@ -453,10 +470,13 @@ Control(Args) | Properties | Listeners | |
| 453 470 | 
             
            `slider(min as Numeric, max as Numeric)` | `value` (`Numeric`) | `on_changed`
         | 
| 454 471 | 
             
            `spinbox(min as Numeric, max as Numeric)` | `value` (`Numeric`) | `on_changed`
         | 
| 455 472 | 
             
            `square(x as Numeric, y as Numeric, length as Numeric)` | `x` (`Numeric`), `y` (`Numeric`), `length` (`Numeric`) | None
         | 
| 473 | 
            +
            `string` | `font`, `color` (`Hash` of `:r` as `0`-`255`, `:g` as `0`-`255`, `:b` as `0`-`255`, `:a` as `0.0`-`1.0`, hex, or [X11](https://en.wikipedia.org/wiki/X11_color_names) color), `background` (`Hash` of `:r` as `0`-`255`, `:g` as `0`-`255`, `:b` as `0`-`255`, `:a` as `0.0`-`1.0`, hex, or [X11](https://en.wikipedia.org/wiki/X11_color_names) color), `underline`, `underline_color` (`Hash` of `:r` as `0`-`255`, `:g` as `0`-`255`, `:b` as `0`-`255`, `:a` as `0.0`-`1.0`, hex, or [X11](https://en.wikipedia.org/wiki/X11_color_names) color), `open_type_features` | None
         | 
| 456 474 | 
             
            `tab` | `margined` (Boolean), `num_pages` (`Integer`) | None
         | 
| 457 475 | 
             
            `tab_item(name as String)` | `index` [read-only] (`Integer`), `margined` (Boolean), `name` [read-only] (`String`) | None
         | 
| 458 | 
            -
            `table` | `cell_rows` (`Array` (rows) of `Arrays` (row columns) of cell values (e.g. `String` values for `text_column` cells or `Array` of `image`/`String` for `image_text_column`)), `editable` as Boolean |  | 
| 476 | 
            +
            `table` | `cell_rows` (`Array` (rows) of `Arrays` (row columns) of cell values (e.g. `String` values for `text_column` cells or `Array` of `image`/`String` for `image_text_column`)), `editable` as Boolean | `on_changed {|row, type, row_data| ...}`, `on_edited {|row, row_data| ...}`
         | 
| 477 | 
            +
            `text(x = 0 as Numeric, y = 0 as Numeric, width = area_width as Numeric)` | `align`, `default_font` | None
         | 
| 459 478 | 
             
            `text_column(name as String)` | `editable` (Boolean) | None
         | 
| 479 | 
            +
            `text_color_column(name as String)` | `editable` (Boolean) | None
         | 
| 460 480 | 
             
            `time_picker` | `time` (`Hash` of keys: `sec` as `Integer`, `min` as `Integer`, `hour` as `Integer`) | `on_changed`
         | 
| 461 481 | 
             
            `vertical_box` | `padded` (Boolean) | None
         | 
| 462 482 | 
             
            `vertical_separator` | None | None
         | 
| @@ -510,12 +530,16 @@ All operations that could normally be called on `LibUI` can also be called on `G | |
| 510 530 | 
             
            ### Table API
         | 
| 511 531 |  | 
| 512 532 | 
             
            The `table` control must first declare its columns via one of these column keywords (mentioned in [Supported Controls](#supported-controls)):
         | 
| 533 | 
            +
              - `background_color_column`: expects color cell values
         | 
| 513 534 | 
             
              - `button_column`: expects `String` cell values
         | 
| 514 535 | 
             
              - `checkbox_column`: expects Boolean cell values
         | 
| 515 536 | 
             
              - `checkbox_text_column`: expects dual-element `Array` of Boolean and `String` cell values
         | 
| 537 | 
            +
              - `checkbox_text_color_column`: expects triple-element `Array` of Boolean, `String`, and color cell values
         | 
| 516 538 | 
             
              - `image_column`: expects `image` cell values (produced by `image` and `image_part` keywords as per [Supported Controls](#supported-controls))
         | 
| 517 539 | 
             
              - `image_text_column`: expects dual-element `Array` of `image` and `String` cell values
         | 
| 540 | 
            +
              - `image_text_color_column`: expects triple-element `Array` of `image`, `String`, and color cell values
         | 
| 518 541 | 
             
              - `text_column`: expects `String` cell values
         | 
| 542 | 
            +
              - `text_color_column`: expects dual-element `Array` of `String` and color cell values
         | 
| 519 543 | 
             
              - `progress_bar_column`: expects `Integer` cell values
         | 
| 520 544 |  | 
| 521 545 | 
             
            Afterwards, it must declare its `cell_rows` array (`Array` of `Array`s of column cell values) and whether it is `editable` (Boolean) for all its columns.
         | 
| @@ -798,11 +822,11 @@ To draw `text` in an `area`, you simply nest a `text(x, y, width)` control direc | |
| 798 822 |  | 
| 799 823 | 
             
            `string` can have the following properties:
         | 
| 800 824 | 
             
            - `font`: font descriptor hash consisting of `:family`, `:size`, `:weight` (`[:minimum, :thin, :ultra_light, :light, :book, :normal, :medium, :semi_bold, :bold, :ultra_bold, :heavy, :ultra_heavy, :maximum]`), `:italic` (`[:normal, :oblique, :italic]`), and `:stretch` (`[:ultra_condensed, :extra_condensed, :condensed, :semi_condensed, :normal, :semi_expanded, :expanded, :extra_expanded, :ultra_expanded]`) key values
         | 
| 801 | 
            -
            - `color`: rgba, hex, or  | 
| 802 | 
            -
            - `background`: rgba, hex, or  | 
| 825 | 
            +
            - `color`: rgba, hex, or [X11](https://en.wikipedia.org/wiki/X11_color_names) color
         | 
| 826 | 
            +
            - `background`: rgba, hex, or [X11](https://en.wikipedia.org/wiki/X11_color_names) color
         | 
| 803 827 | 
             
            - `underline`: one of `:none`, `:single`, `:double`, `:suggestion`, `:color_custom`, `:color_spelling`, `:color_grammar`, `:color_auxiliary`
         | 
| 804 | 
            -
            - `underline_color`: one of `:spelling`, `:grammar`, `:auxiliary`, rgba, hex, or  | 
| 805 | 
            -
            - `open_type_features`:  | 
| 828 | 
            +
            - `underline_color`: one of `:spelling`, `:grammar`, `:auxiliary`, rgba, hex, or [X11](https://en.wikipedia.org/wiki/X11_color_names) color
         | 
| 829 | 
            +
            - `open_type_features`: Open Type Features (https://www.microsoft.com/typography/otspec/featuretags.htm) consist of `open_type_tag`s nested in content block, which accept (`a`, `b`, `c`, `d`, `Integer`) arguments.
         | 
| 806 830 |  | 
| 807 831 | 
             
            Example (you may copy/paste in [`girb`](#girb-glimmer-irb)):
         | 
| 808 832 |  | 
| @@ -871,16 +895,126 @@ window('area text drawing') { | |
| 871 895 | 
             
            - Colors may be passed in as a hash of `:r`, `:g`, `:b`, `:a`, or `:red`, `:green`, `:blue`, `:alpha`, or [X11](https://en.wikipedia.org/wiki/X11_color_names) color like `:skyblue`, or 6-number hex or 3-number hex (as `Integer` or `String` with or without `0x` prefix)
         | 
| 872 896 | 
             
            - Color alpha value defaults to `1.0` when not specified.
         | 
| 873 897 |  | 
| 898 | 
            +
            ### Custom Keywords
         | 
| 899 | 
            +
             | 
| 900 | 
            +
            To define custom keywords, simply define a method representing the custom control you want. To make reusable, you can define in modules and simply include the modules in the view classes that need them.
         | 
| 901 | 
            +
             | 
| 902 | 
            +
            Example that defines `field`, `address_form`, `label_pair`, and `address` keywords (you may copy/paste in [`girb`](#girb-glimmer-irb)):
         | 
| 903 | 
            +
             | 
| 904 | 
            +
            ```ruby
         | 
| 905 | 
            +
            require 'glimmer-dsl-libui'
         | 
| 906 | 
            +
            require 'facets'
         | 
| 907 | 
            +
             | 
| 908 | 
            +
            include Glimmer
         | 
| 909 | 
            +
             | 
| 910 | 
            +
            Address = Struct.new(:street, :p_o_box, :city, :state, :zip_code)
         | 
| 911 | 
            +
             | 
| 912 | 
            +
            def field(model, property)
         | 
| 913 | 
            +
              property = property.to_s
         | 
| 914 | 
            +
              entry { |e|
         | 
| 915 | 
            +
                label property.underscore.split('_').map(&:capitalize).join(' ')
         | 
| 916 | 
            +
                text model.send(property).to_s
         | 
| 917 | 
            +
             | 
| 918 | 
            +
                on_changed do
         | 
| 919 | 
            +
                  model.send("#{property}=", e.text)
         | 
| 920 | 
            +
                end
         | 
| 921 | 
            +
              }
         | 
| 922 | 
            +
            end
         | 
| 923 | 
            +
             | 
| 924 | 
            +
            def address_form(address)
         | 
| 925 | 
            +
              form {
         | 
| 926 | 
            +
                field(address, :street)
         | 
| 927 | 
            +
                field(address, :p_o_box)
         | 
| 928 | 
            +
                field(address, :city)
         | 
| 929 | 
            +
                field(address, :state)
         | 
| 930 | 
            +
                field(address, :zip_code)
         | 
| 931 | 
            +
              }
         | 
| 932 | 
            +
            end
         | 
| 933 | 
            +
             | 
| 934 | 
            +
            def label_pair(model, attribute, value)
         | 
| 935 | 
            +
              name_label = nil
         | 
| 936 | 
            +
              value_label = nil
         | 
| 937 | 
            +
              horizontal_box {
         | 
| 938 | 
            +
                name_label = label(attribute.to_s.underscore.split('_').map(&:capitalize).join(' '))
         | 
| 939 | 
            +
                value_label = label(value.to_s)
         | 
| 940 | 
            +
              }
         | 
| 941 | 
            +
              Glimmer::DataBinding::Observer.proc do
         | 
| 942 | 
            +
                value_label.text = model.send(attribute)
         | 
| 943 | 
            +
              end.observe(model, attribute)
         | 
| 944 | 
            +
            end
         | 
| 945 | 
            +
             | 
| 946 | 
            +
            def address(address)
         | 
| 947 | 
            +
              vertical_box {
         | 
| 948 | 
            +
                address.each_pair do |attribute, value|
         | 
| 949 | 
            +
                  label_pair(address, attribute, value)
         | 
| 950 | 
            +
                end
         | 
| 951 | 
            +
              }
         | 
| 952 | 
            +
            end
         | 
| 953 | 
            +
             | 
| 954 | 
            +
            address1 = Address.new('123 Main St', '23923', 'Denver', 'Colorado', '80014')
         | 
| 955 | 
            +
            address2 = Address.new('2038 Park Ave', '83272', 'Boston', 'Massachusetts', '02101')
         | 
| 956 | 
            +
             | 
| 957 | 
            +
            window('Method-Based Custom Keyword') {
         | 
| 958 | 
            +
              margined true
         | 
| 959 | 
            +
              
         | 
| 960 | 
            +
              horizontal_box {
         | 
| 961 | 
            +
                vertical_box {
         | 
| 962 | 
            +
                  label('Address 1') {
         | 
| 963 | 
            +
                    stretchy false
         | 
| 964 | 
            +
                  }
         | 
| 965 | 
            +
                  address_form(address1)
         | 
| 966 | 
            +
                  horizontal_separator {
         | 
| 967 | 
            +
                    stretchy false
         | 
| 968 | 
            +
                  }
         | 
| 969 | 
            +
                  label('Address 1 (Saved)') {
         | 
| 970 | 
            +
                    stretchy false
         | 
| 971 | 
            +
                  }
         | 
| 972 | 
            +
                  address(address1)
         | 
| 973 | 
            +
                }
         | 
| 974 | 
            +
                vertical_separator {
         | 
| 975 | 
            +
                  stretchy false
         | 
| 976 | 
            +
                }
         | 
| 977 | 
            +
                vertical_box {
         | 
| 978 | 
            +
                  label('Address 2') {
         | 
| 979 | 
            +
                    stretchy false
         | 
| 980 | 
            +
                  }
         | 
| 981 | 
            +
                  address_form(address2)
         | 
| 982 | 
            +
                  horizontal_separator {
         | 
| 983 | 
            +
                    stretchy false
         | 
| 984 | 
            +
                  }
         | 
| 985 | 
            +
                  label('Address 2 (Saved)') {
         | 
| 986 | 
            +
                    stretchy false
         | 
| 987 | 
            +
                  }
         | 
| 988 | 
            +
                  address(address2)
         | 
| 989 | 
            +
                }
         | 
| 990 | 
            +
              }
         | 
| 991 | 
            +
            }.show
         | 
| 992 | 
            +
            ```
         | 
| 993 | 
            +
             | 
| 994 | 
            +
            
         | 
| 995 | 
            +
             | 
| 874 996 | 
             
            ### API Gotchas
         | 
| 875 997 |  | 
| 876 998 | 
             
            - There is no proper way to destroy `grid` children due to [libui](https://github.com/andlabs/libui) not offering any API for deleting them from `grid` (no `grid_delete` similar to `box_delete` for `horizontal_box` and `vertical_box`).
         | 
| 877 999 | 
             
            - `table` `checkbox_column` and `checkbox_text_column` checkbox editing only works on Windows and Linux (not Mac) due to a current limitation in [libui](https://github.com/andlabs/ui/issues/357).
         | 
| 878 | 
            -
            -  | 
| 1000 | 
            +
            - `text` `align` property seems not to work on the Mac ([libui](https://github.com/andlabs/libui) has an [issue](https://github.com/andlabs/libui/pull/407) about it)
         | 
| 879 1001 |  | 
| 880 1002 | 
             
            ### Original API
         | 
| 881 1003 |  | 
| 882 | 
            -
            To learn more about the [LibUI](https://github.com/kojix2/LibUI) API exposed through [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) | 
| 883 | 
            -
             | 
| 1004 | 
            +
            To learn more about the [LibUI](https://github.com/kojix2/LibUI) API exposed through [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui):
         | 
| 1005 | 
            +
            - Check out [LibUI ffi.rb](https://github.com/kojix2/LibUI/blob/main/lib/libui/ffi.rb)
         | 
| 1006 | 
            +
            - Check out the [libui C headers](https://github.com/andlabs/libui/blob/master/ui.h)
         | 
| 1007 | 
            +
            - Check out the [Go UI (Golang LibUI) documentation](https://pkg.go.dev/github.com/andlabs/ui) for an alternative well-documented [libui](https://github.com/andlabs/libui) reference.
         | 
| 1008 | 
            +
             | 
| 1009 | 
            +
            ## Packaging
         | 
| 1010 | 
            +
             | 
| 1011 | 
            +
            I am documenting options for packaging, which I have not tried myself, but figured they would still be useful to add to the README.md until I can expand further effort into supporting packaging.
         | 
| 1012 | 
            +
             | 
| 1013 | 
            +
            For Windows, the [LibUI](https://github.com/kojix2/LibUI) project recommends [OCRA](https://github.com/larsch/ocra) (One-Click Ruby Application), which builds Windows executables from Ruby source.
         | 
| 1014 | 
            +
             | 
| 1015 | 
            +
            For Mac, consider [Platybus](https://github.com/sveinbjornt/Platypus) (builds a native Mac app from a Ruby script)
         | 
| 1016 | 
            +
             | 
| 1017 | 
            +
            For Linux, simply package your app as a [Ruby Gem](https://guides.rubygems.org/what-is-a-gem/) and [build rpm package from Ruby Gem](https://www.redpill-linpro.com/sysadvent/2015/12/07/building-rpms-from-gems.html) or [build deb package from Ruby Gem](https://openpreservation.org/blogs/building-debian-package-ruby-program/).
         | 
| 884 1018 |  | 
| 885 1019 | 
             
            ## Glimmer Style Guide
         | 
| 886 1020 |  | 
| @@ -897,8 +1031,6 @@ The following examples include reimplementions of the examples in the [LibUI](ht | |
| 897 1031 |  | 
| 898 1032 | 
             
            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.
         | 
| 899 1033 |  | 
| 900 | 
            -
            (note that for examples that emit output to terminal/command-line via `p` or `puts`, you must run them directly to see output)
         | 
| 901 | 
            -
             | 
| 902 1034 | 
             
            [examples/meta_example.rb](examples/meta_example.rb)
         | 
| 903 1035 |  | 
| 904 1036 | 
             
            Run with this command from the root of the project if you cloned the project:
         | 
| @@ -924,65 +1056,118 @@ Linux | |
| 924 1056 | 
             
            New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version:
         | 
| 925 1057 |  | 
| 926 1058 | 
             
            ```ruby
         | 
| 1059 | 
            +
            # frozen_string_literal: true
         | 
| 1060 | 
            +
             | 
| 927 1061 | 
             
            require 'glimmer-dsl-libui'
         | 
| 928 1062 | 
             
            require 'facets'
         | 
| 929 1063 |  | 
| 930 1064 | 
             
            class MetaExample
         | 
| 931 1065 | 
             
              include Glimmer
         | 
| 932 1066 |  | 
| 1067 | 
            +
              def initialize
         | 
| 1068 | 
            +
                @selected_example_index = 0
         | 
| 1069 | 
            +
              end
         | 
| 1070 | 
            +
              
         | 
| 933 1071 | 
             
              def examples
         | 
| 934 1072 | 
             
                if @examples.nil?
         | 
| 935 1073 | 
             
                  example_files = Dir.glob(File.join(File.expand_path('.', __dir__), '**', '*.rb'))
         | 
| 936 1074 | 
             
                  example_file_names = example_files.map { |f| File.basename(f, '.rb') }
         | 
| 937 | 
            -
                  example_file_names = example_file_names.reject { |f| f == 'meta_example' }
         | 
| 1075 | 
            +
                  example_file_names = example_file_names.reject { |f| f == 'meta_example' || f.match(/\d$/) }
         | 
| 938 1076 | 
             
                  @examples = example_file_names.map { |f| f.underscore.titlecase }
         | 
| 939 1077 | 
             
                end
         | 
| 940 1078 | 
             
                @examples
         | 
| 941 1079 | 
             
              end
         | 
| 942 1080 |  | 
| 1081 | 
            +
              def examples_with_versions
         | 
| 1082 | 
            +
                examples.map do |example|
         | 
| 1083 | 
            +
                  version_count_for(example) > 1 ? "#{example} (#{version_count_for(example)} versions)" : example
         | 
| 1084 | 
            +
                end
         | 
| 1085 | 
            +
              end
         | 
| 1086 | 
            +
              
         | 
| 943 1087 | 
             
              def file_path_for(example)
         | 
| 944 1088 | 
             
                File.join(File.expand_path('.', __dir__), "#{example.underscore}.rb")
         | 
| 945 1089 | 
             
              end
         | 
| 946 1090 |  | 
| 1091 | 
            +
              def version_count_for(example)
         | 
| 1092 | 
            +
                Dir.glob(File.join(File.expand_path('.', __dir__), "#{example.underscore}*.rb")).select {|file| file.match(/\d\.rb$/)}.count + 1
         | 
| 1093 | 
            +
              end
         | 
| 1094 | 
            +
              
         | 
| 947 1095 | 
             
              def glimmer_dsl_libui_file
         | 
| 948 1096 | 
             
                File.expand_path('../lib/glimmer-dsl-libui', __dir__)
         | 
| 949 1097 | 
             
              end
         | 
| 950 1098 |  | 
| 1099 | 
            +
              def selected_example
         | 
| 1100 | 
            +
                examples[@selected_example_index]
         | 
| 1101 | 
            +
              end
         | 
| 1102 | 
            +
              
         | 
| 951 1103 | 
             
              def launch
         | 
| 952 1104 | 
             
                window('Meta-Example', 700, 500) {
         | 
| 953 1105 | 
             
                  margined true
         | 
| 954 1106 |  | 
| 955 1107 | 
             
                  horizontal_box {
         | 
| 956 1108 | 
             
                    vertical_box {
         | 
| 957 | 
            -
                       | 
| 1109 | 
            +
                      stretchy false
         | 
| 1110 | 
            +
                      
         | 
| 1111 | 
            +
                      @example_radio_buttons = radio_buttons {
         | 
| 958 1112 | 
             
                        stretchy false
         | 
| 959 | 
            -
                        items  | 
| 960 | 
            -
                        selected  | 
| 1113 | 
            +
                        items examples_with_versions
         | 
| 1114 | 
            +
                        selected @selected_example_index
         | 
| 961 1115 |  | 
| 962 1116 | 
             
                        on_selected do
         | 
| 963 | 
            -
                          @ | 
| 1117 | 
            +
                          @selected_example_index = @example_radio_buttons.selected
         | 
| 1118 | 
            +
                          example = selected_example
         | 
| 1119 | 
            +
                          @code_entry.text = File.read(file_path_for(example))
         | 
| 1120 | 
            +
                          @version_spinbox.value = 1
         | 
| 964 1121 | 
             
                        end
         | 
| 965 1122 | 
             
                      }
         | 
| 966 | 
            -
                       | 
| 1123 | 
            +
                      
         | 
| 1124 | 
            +
                      horizontal_box {
         | 
| 1125 | 
            +
                        label('Version') {
         | 
| 1126 | 
            +
                          stretchy false
         | 
| 1127 | 
            +
                        }
         | 
| 1128 | 
            +
                        
         | 
| 1129 | 
            +
                        @version_spinbox = spinbox(1, 100) {
         | 
| 1130 | 
            +
                          value 1
         | 
| 1131 | 
            +
                          
         | 
| 1132 | 
            +
                          on_changed do
         | 
| 1133 | 
            +
                            example = selected_example
         | 
| 1134 | 
            +
                            if @version_spinbox.value > version_count_for(example)
         | 
| 1135 | 
            +
                              @version_spinbox.value -= 1
         | 
| 1136 | 
            +
                            else
         | 
| 1137 | 
            +
                              version_number = @version_spinbox.value == 1 ? '' : @version_spinbox.value
         | 
| 1138 | 
            +
                              example = "#{selected_example}#{version_number}"
         | 
| 1139 | 
            +
                              @code_entry.text = File.read(file_path_for(example))
         | 
| 1140 | 
            +
                            end
         | 
| 1141 | 
            +
                          end
         | 
| 1142 | 
            +
                        }
         | 
| 1143 | 
            +
                      }
         | 
| 1144 | 
            +
                      
         | 
| 1145 | 
            +
                      horizontal_box {
         | 
| 967 1146 | 
             
                        stretchy false
         | 
| 968 1147 |  | 
| 969 | 
            -
                         | 
| 970 | 
            -
                           | 
| 971 | 
            -
                             | 
| 972 | 
            -
             | 
| 973 | 
            -
             | 
| 974 | 
            -
             | 
| 975 | 
            -
             | 
| 976 | 
            -
                             | 
| 977 | 
            -
             | 
| 1148 | 
            +
                        button('Launch') {
         | 
| 1149 | 
            +
                          on_clicked do
         | 
| 1150 | 
            +
                            begin
         | 
| 1151 | 
            +
                              meta_example_file = File.join(Dir.home, '.meta_example.rb')
         | 
| 1152 | 
            +
                              File.write(meta_example_file, @code_entry.text)
         | 
| 1153 | 
            +
                              result = `ruby -r #{glimmer_dsl_libui_file} #{meta_example_file} 2>&1`
         | 
| 1154 | 
            +
                              msg_box('Error Running Example', result) if result.include?('error')
         | 
| 1155 | 
            +
                            rescue => e
         | 
| 1156 | 
            +
                              puts 'Unable to write code changes! Running original example...'
         | 
| 1157 | 
            +
                              system "ruby -r #{glimmer_dsl_libui_file} #{file_path_for(selected_example)}"
         | 
| 1158 | 
            +
                            end
         | 
| 978 1159 | 
             
                          end
         | 
| 979 | 
            -
                         | 
| 1160 | 
            +
                        }
         | 
| 1161 | 
            +
                        button('Reset') {
         | 
| 1162 | 
            +
                          on_clicked do
         | 
| 1163 | 
            +
                            @code_entry.text = File.read(file_path_for(selected_example))
         | 
| 1164 | 
            +
                          end
         | 
| 1165 | 
            +
                        }
         | 
| 980 1166 | 
             
                      }
         | 
| 981 1167 | 
             
                    }
         | 
| 982 | 
            -
                     | 
| 983 | 
            -
             | 
| 984 | 
            -
             | 
| 985 | 
            -
                      }
         | 
| 1168 | 
            +
                    
         | 
| 1169 | 
            +
                    @code_entry = non_wrapping_multiline_entry {
         | 
| 1170 | 
            +
                      text File.read(file_path_for(selected_example))
         | 
| 986 1171 | 
             
                    }
         | 
| 987 1172 | 
             
                  }
         | 
| 988 1173 | 
             
                }.show
         | 
| @@ -2049,6 +2234,8 @@ include Glimmer | |
| 2049 2234 |  | 
| 2050 2235 | 
             
            window('color button', 230) {
         | 
| 2051 2236 | 
             
              color_button { |cb|
         | 
| 2237 | 
            +
                color :blue
         | 
| 2238 | 
            +
                
         | 
| 2052 2239 | 
             
                on_changed do
         | 
| 2053 2240 | 
             
                  rgba = cb.color
         | 
| 2054 2241 | 
             
                  p rgba
         | 
| @@ -2513,6 +2700,14 @@ window('Editable animal sounds', 300, 200) { | |
| 2513 2700 |  | 
| 2514 2701 | 
             
                  cell_rows data
         | 
| 2515 2702 | 
             
                  editable true
         | 
| 2703 | 
            +
                  
         | 
| 2704 | 
            +
                  on_changed do |row, type, row_data| # fires on all changes (even ones happening through data array)
         | 
| 2705 | 
            +
                    puts "Row #{row} #{type}: #{row_data}"
         | 
| 2706 | 
            +
                  end
         | 
| 2707 | 
            +
                  
         | 
| 2708 | 
            +
                  on_edited do |row, row_data| # only fires on direct table editing
         | 
| 2709 | 
            +
                    puts "Row #{row} edited: #{row_data}"
         | 
| 2710 | 
            +
                  end
         | 
| 2516 2711 | 
             
                }
         | 
| 2517 2712 | 
             
              }
         | 
| 2518 2713 |  | 
| @@ -2584,7 +2779,7 @@ window('Editable column animal sounds', 400, 200) { | |
| 2584 2779 |  | 
| 2585 2780 | 
             
            ### Basic Table Image
         | 
| 2586 2781 |  | 
| 2587 | 
            -
            This example  | 
| 2782 | 
            +
            This example requires pre-installing `chunky_png` Ruby gem:
         | 
| 2588 2783 |  | 
| 2589 2784 | 
             
            ```
         | 
| 2590 2785 | 
             
            gem install chunky_png -v1.4.0
         | 
| @@ -2871,6 +3066,10 @@ window('Animal sounds', 300, 200) { | |
| 2871 3066 | 
             
                  }
         | 
| 2872 3067 |  | 
| 2873 3068 | 
             
                  cell_rows data # implicit data-binding
         | 
| 3069 | 
            +
                  
         | 
| 3070 | 
            +
                  on_changed do |row, type, row_data|
         | 
| 3071 | 
            +
                    puts "Row #{row} #{type}: #{row_data}"
         | 
| 3072 | 
            +
                  end
         | 
| 2874 3073 | 
             
                }
         | 
| 2875 3074 | 
             
              }
         | 
| 2876 3075 | 
             
            }.show
         | 
| @@ -3033,7 +3232,7 @@ window('Task Progress', 300, 200) { | |
| 3033 3232 |  | 
| 3034 3233 | 
             
                  on_clicked do
         | 
| 3035 3234 | 
             
                    data.each_with_index do |row_data, row|
         | 
| 3036 | 
            -
                      data[row] =  | 
| 3235 | 
            +
                      data[row][1] = 100 # automatically updates table due to implicit data-binding
         | 
| 3037 3236 | 
             
                    end
         | 
| 3038 3237 | 
             
                  end
         | 
| 3039 3238 | 
             
                }
         | 
| @@ -3041,6 +3240,78 @@ window('Task Progress', 300, 200) { | |
| 3041 3240 | 
             
            }.show
         | 
| 3042 3241 | 
             
            ```
         | 
| 3043 3242 |  | 
| 3243 | 
            +
            ### Basic Table Color
         | 
| 3244 | 
            +
             | 
| 3245 | 
            +
            This example requires pre-installing `chunky_png` Ruby gem:
         | 
| 3246 | 
            +
             | 
| 3247 | 
            +
            ```
         | 
| 3248 | 
            +
            gem install chunky_png -v1.4.0
         | 
| 3249 | 
            +
            ```
         | 
| 3250 | 
            +
             | 
| 3251 | 
            +
            [examples/basic_table_color.rb](examples/basic_table_color.rb)
         | 
| 3252 | 
            +
             | 
| 3253 | 
            +
            Run with this command from the root of the project if you cloned the project:
         | 
| 3254 | 
            +
             | 
| 3255 | 
            +
            ```
         | 
| 3256 | 
            +
            ruby -r './lib/glimmer-dsl-libui' examples/basic_table_color.rb
         | 
| 3257 | 
            +
            ```
         | 
| 3258 | 
            +
             | 
| 3259 | 
            +
            Run with this command if you installed the [Ruby gem](https://rubygems.org/gems/glimmer-dsl-libui):
         | 
| 3260 | 
            +
             | 
| 3261 | 
            +
            ```
         | 
| 3262 | 
            +
            ruby -r glimmer-dsl-libui -e "require 'examples/basic_table_color'"
         | 
| 3263 | 
            +
            ```
         | 
| 3264 | 
            +
             | 
| 3265 | 
            +
            Mac
         | 
| 3266 | 
            +
             | 
| 3267 | 
            +
            
         | 
| 3268 | 
            +
             | 
| 3269 | 
            +
            Linux
         | 
| 3270 | 
            +
             | 
| 3271 | 
            +
            
         | 
| 3272 | 
            +
             | 
| 3273 | 
            +
            New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version:
         | 
| 3274 | 
            +
             | 
| 3275 | 
            +
            ```ruby
         | 
| 3276 | 
            +
            require 'glimmer-dsl-libui'
         | 
| 3277 | 
            +
            require 'chunky_png'
         | 
| 3278 | 
            +
             | 
| 3279 | 
            +
            include Glimmer
         | 
| 3280 | 
            +
             | 
| 3281 | 
            +
            f = File.open(File.expand_path('../icons/glimmer.png', __dir__))
         | 
| 3282 | 
            +
            canvas = ChunkyPNG::Canvas.from_io(f)
         | 
| 3283 | 
            +
            f.close
         | 
| 3284 | 
            +
            canvas.resample_nearest_neighbor!(24, 24)
         | 
| 3285 | 
            +
            data = canvas.to_rgba_stream
         | 
| 3286 | 
            +
            width = canvas.width
         | 
| 3287 | 
            +
            height = canvas.height
         | 
| 3288 | 
            +
            img = image {
         | 
| 3289 | 
            +
              image_part(data, width, height, width * 4)
         | 
| 3290 | 
            +
            }
         | 
| 3291 | 
            +
             | 
| 3292 | 
            +
            data = [
         | 
| 3293 | 
            +
              [['cat', :red]      , ['meow', :blue]                  , [true, 'mammal', :green], [img, 'Glimmer', :dark_blue], {r: 255, g: 120, b: 0, a: 0.5}],
         | 
| 3294 | 
            +
              [['dog', :yellow]   , ['woof', {r: 240, g: 32, b: 32}] , [true, 'mammal', :green], [img, 'Glimmer', :dark_blue], :skyblue],
         | 
| 3295 | 
            +
              [['chicken', :beige], ['cock-a-doodle-doo', :blue]     , [false, 'mammal', :red] , [img, 'Glimmer', :beige], {r: 5, g: 120, b: 110}],
         | 
| 3296 | 
            +
              [['horse', :purple] , ['neigh', {r: 240, g: 32, b: 32}], [true, 'mammal', :green], [img, 'Glimmer', :dark_blue], '13a1fb'],
         | 
| 3297 | 
            +
              [['cow', :gray]     , ['moo', :blue]                   , [true, 'mammal', :green], [img, 'Glimmer', :brown], 0x12ff02]
         | 
| 3298 | 
            +
            ]
         | 
| 3299 | 
            +
             | 
| 3300 | 
            +
            window('Animals', 500, 200) {
         | 
| 3301 | 
            +
              horizontal_box {
         | 
| 3302 | 
            +
                table {
         | 
| 3303 | 
            +
                  text_color_column('Animal')
         | 
| 3304 | 
            +
                  text_color_column('Sound')
         | 
| 3305 | 
            +
                  checkbox_text_color_column('Description')
         | 
| 3306 | 
            +
                  image_text_color_column('GUI')
         | 
| 3307 | 
            +
                  background_color_column('Mammal')
         | 
| 3308 | 
            +
             | 
| 3309 | 
            +
                  cell_rows data
         | 
| 3310 | 
            +
                }
         | 
| 3311 | 
            +
              }
         | 
| 3312 | 
            +
            }.show
         | 
| 3313 | 
            +
            ```
         | 
| 3314 | 
            +
             | 
| 3044 3315 | 
             
            ### Form Table
         | 
| 3045 3316 |  | 
| 3046 3317 | 
             
            [examples/form_table.rb](examples/form_table.rb)
         | 
| @@ -3156,6 +3427,10 @@ window('Contacts', 600, 600) { |w| | |
| 3156 3427 | 
             
                  text_column('State')
         | 
| 3157 3428 |  | 
| 3158 3429 | 
             
                  cell_rows data # implicit data-binding
         | 
| 3430 | 
            +
                  
         | 
| 3431 | 
            +
                  on_changed do |row, type, row_data|
         | 
| 3432 | 
            +
                    puts "Row #{row} #{type}: #{row_data}"
         | 
| 3433 | 
            +
                  end
         | 
| 3159 3434 | 
             
                }
         | 
| 3160 3435 | 
             
              }
         | 
| 3161 3436 | 
             
            }.show
         | 
| @@ -3551,14 +3826,15 @@ window('Area Gallery', 400, 400) { | |
| 3551 3826 | 
             
                path { # declarative stable path
         | 
| 3552 3827 | 
             
                  square(0, 0, 100)
         | 
| 3553 3828 | 
             
                  square(100, 100, 400)
         | 
| 3554 | 
            -
             | 
| 3829 | 
            +
             | 
| 3555 3830 | 
             
                  fill r: 102, g: 102, b: 204
         | 
| 3556 3831 | 
             
                }
         | 
| 3557 3832 | 
             
                path { # declarative stable path
         | 
| 3558 3833 | 
             
                  rectangle(0, 100, 100, 400)
         | 
| 3559 3834 | 
             
                  rectangle(100, 0, 400, 100)
         | 
| 3560 | 
            -
             | 
| 3561 | 
            -
                   | 
| 3835 | 
            +
             | 
| 3836 | 
            +
                  # linear gradient (has x0, y0, x1, y1, and stops)
         | 
| 3837 | 
            +
                  fill x0: 10, y0: 10, x1: 350, y1: 350, stops: [{pos: 0.25, r: 204, g: 102, b: 204}, {pos: 0.75, r: 102, g: 102, b: 204}]
         | 
| 3562 3838 | 
             
                }
         | 
| 3563 3839 | 
             
                path { # declarative stable path
         | 
| 3564 3840 | 
             
                  figure(100, 100) {
         | 
| @@ -3584,17 +3860,26 @@ window('Area Gallery', 400, 400) { | |
| 3584 3860 | 
             
                  fill r: 202, g: 102, b: 204, a: 0.5
         | 
| 3585 3861 | 
             
                  stroke r: 0, g: 0, b: 0, thickness: 2, dashes: [50, 10, 10, 10], dash_phase: -50.0
         | 
| 3586 3862 | 
             
                }
         | 
| 3863 | 
            +
                path { # declarative stable path
         | 
| 3864 | 
            +
                  arc(400, 220, 180, 90, 90, false)
         | 
| 3865 | 
            +
             | 
| 3866 | 
            +
                  # radial gradient (has an outer_radius in addition to x0, y0, x1, y1, and stops)
         | 
| 3867 | 
            +
                  fill outer_radius: 90, x0: 0, y0: 0, x1: 500, y1: 500, stops: [{pos: 0.25, r: 102, g: 102, b: 204, a: 0.5}, {pos: 0.75, r: 204, g: 102, b: 204}]
         | 
| 3868 | 
            +
                  stroke r: 0, g: 0, b: 0, thickness: 2, dashes: [50, 10, 10, 10], dash_phase: -50.0
         | 
| 3869 | 
            +
                }
         | 
| 3587 3870 | 
             
                path { # declarative stable path
         | 
| 3588 3871 | 
             
                  circle(200, 200, 90)
         | 
| 3589 3872 |  | 
| 3590 3873 | 
             
                  fill r: 202, g: 102, b: 204, a: 0.5
         | 
| 3591 3874 | 
             
                  stroke r: 0, g: 0, b: 0, thickness: 2
         | 
| 3592 3875 | 
             
                }
         | 
| 3593 | 
            -
                 | 
| 3594 | 
            -
                   | 
| 3595 | 
            -
             | 
| 3596 | 
            -
             | 
| 3597 | 
            -
             | 
| 3876 | 
            +
                text(160, 40, 100) { # x, y, width
         | 
| 3877 | 
            +
                  string {
         | 
| 3878 | 
            +
                    font family: 'Times', size: 14
         | 
| 3879 | 
            +
                    color :black
         | 
| 3880 | 
            +
                    
         | 
| 3881 | 
            +
                    'Area Gallery'
         | 
| 3882 | 
            +
                  }
         | 
| 3598 3883 | 
             
                }
         | 
| 3599 3884 |  | 
| 3600 3885 | 
             
                on_mouse_event do |area_mouse_event|
         | 
| @@ -3685,7 +3970,8 @@ window('Area Gallery', 400, 400) { | |
| 3685 3970 | 
             
                    height 100
         | 
| 3686 3971 | 
             
                  }
         | 
| 3687 3972 |  | 
| 3688 | 
            -
                   | 
| 3973 | 
            +
                  # linear gradient (has x0, y0, x1, y1, and stops)
         | 
| 3974 | 
            +
                  fill x0: 10, y0: 10, x1: 350, y1: 350, stops: [{pos: 0.25, r: 204, g: 102, b: 204}, {pos: 0.75, r: 102, g: 102, b: 204}]
         | 
| 3689 3975 | 
             
                }
         | 
| 3690 3976 | 
             
                path { # declarative stable path
         | 
| 3691 3977 | 
             
                  figure {
         | 
| @@ -3747,16 +4033,6 @@ window('Area Gallery', 400, 400) { | |
| 3747 4033 | 
             
                  fill r: 202, g: 102, b: 204, a: 0.5
         | 
| 3748 4034 | 
             
                  stroke r: 0, g: 0, b: 0, thickness: 2, dashes: [50, 10, 10, 10], dash_phase: -50.0
         | 
| 3749 4035 | 
             
                }
         | 
| 3750 | 
            -
                path { # declarative stable path
         | 
| 3751 | 
            -
                  circle {
         | 
| 3752 | 
            -
                    x_center 200
         | 
| 3753 | 
            -
                    y_center 200
         | 
| 3754 | 
            -
                    radius 90
         | 
| 3755 | 
            -
                  }
         | 
| 3756 | 
            -
             | 
| 3757 | 
            -
                  fill r: 202, g: 102, b: 204, a: 0.5
         | 
| 3758 | 
            -
                  stroke r: 0, g: 0, b: 0, thickness: 2
         | 
| 3759 | 
            -
                }
         | 
| 3760 4036 | 
             
                path { # declarative stable path
         | 
| 3761 4037 | 
             
                  arc {
         | 
| 3762 4038 | 
             
                    x_center 400
         | 
| @@ -3767,9 +4043,32 @@ window('Area Gallery', 400, 400) { | |
| 3767 4043 | 
             
                    is_negative false
         | 
| 3768 4044 | 
             
                  }
         | 
| 3769 4045 |  | 
| 3770 | 
            -
                   | 
| 4046 | 
            +
                  # radial gradient (has an outer_radius in addition to x0, y0, x1, y1, and stops)
         | 
| 4047 | 
            +
                  fill outer_radius: 90, x0: 0, y0: 0, x1: 500, y1: 500, stops: [{pos: 0.25, r: 102, g: 102, b: 204, a: 0.5}, {pos: 0.75, r: 204, g: 102, b: 204}]
         | 
| 3771 4048 | 
             
                  stroke r: 0, g: 0, b: 0, thickness: 2, dashes: [50, 10, 10, 10], dash_phase: -50.0
         | 
| 3772 4049 | 
             
                }
         | 
| 4050 | 
            +
                path { # declarative stable path
         | 
| 4051 | 
            +
                  circle {
         | 
| 4052 | 
            +
                    x_center 200
         | 
| 4053 | 
            +
                    y_center 200
         | 
| 4054 | 
            +
                    radius 90
         | 
| 4055 | 
            +
                  }
         | 
| 4056 | 
            +
             | 
| 4057 | 
            +
                  fill r: 202, g: 102, b: 204, a: 0.5
         | 
| 4058 | 
            +
                  stroke r: 0, g: 0, b: 0, thickness: 2
         | 
| 4059 | 
            +
                }
         | 
| 4060 | 
            +
                text {
         | 
| 4061 | 
            +
                  x 160
         | 
| 4062 | 
            +
                  y 40
         | 
| 4063 | 
            +
                  width 100
         | 
| 4064 | 
            +
                  
         | 
| 4065 | 
            +
                  string {
         | 
| 4066 | 
            +
                    font family: 'Times', size: 14
         | 
| 4067 | 
            +
                    color :black
         | 
| 4068 | 
            +
                    
         | 
| 4069 | 
            +
                    'Area Gallery'
         | 
| 4070 | 
            +
                  }
         | 
| 4071 | 
            +
                }
         | 
| 3773 4072 |  | 
| 3774 4073 | 
             
                on_mouse_event do |area_mouse_event|
         | 
| 3775 4074 | 
             
                  p area_mouse_event
         | 
| @@ -3842,7 +4141,8 @@ window('Area Gallery', 400, 400) { | |
| 3842 4141 | 
             
                    rectangle(0, 100, 100, 400)
         | 
| 3843 4142 | 
             
                    rectangle(100, 0, 400, 100)
         | 
| 3844 4143 |  | 
| 3845 | 
            -
                     | 
| 4144 | 
            +
                    # linear gradient (has x0, y0, x1, y1, and stops)
         | 
| 4145 | 
            +
                    fill x0: 10, y0: 10, x1: 350, y1: 350, stops: [{pos: 0.25, r: 204, g: 102, b: 204}, {pos: 0.75, r: 102, g: 102, b: 204}]
         | 
| 3846 4146 | 
             
                  }
         | 
| 3847 4147 | 
             
                  path { # a dynamic path is added semi-declaratively inside on_draw block
         | 
| 3848 4148 | 
             
                    figure(100, 100) {
         | 
| @@ -3868,17 +4168,26 @@ window('Area Gallery', 400, 400) { | |
| 3868 4168 | 
             
                    fill r: 202, g: 102, b: 204, a: 0.5
         | 
| 3869 4169 | 
             
                    stroke r: 0, g: 0, b: 0, thickness: 2, dashes: [50, 10, 10, 10], dash_phase: -50.0
         | 
| 3870 4170 | 
             
                  }
         | 
| 4171 | 
            +
                  path { # a dynamic path is added semi-declaratively inside on_draw block
         | 
| 4172 | 
            +
                    arc(400, 220, 180, 90, 90, false)
         | 
| 4173 | 
            +
              
         | 
| 4174 | 
            +
                    # radial gradient (has an outer_radius in addition to x0, y0, x1, y1, and stops)
         | 
| 4175 | 
            +
                    fill outer_radius: 90, x0: 0, y0: 0, x1: 500, y1: 500, stops: [{pos: 0.25, r: 102, g: 102, b: 204, a: 0.5}, {pos: 0.75, r: 204, g: 102, b: 204}]
         | 
| 4176 | 
            +
                    stroke r: 0, g: 0, b: 0, thickness: 2, dashes: [50, 10, 10, 10], dash_phase: -50.0
         | 
| 4177 | 
            +
                  }
         | 
| 3871 4178 | 
             
                  path { # a dynamic path is added semi-declaratively inside on_draw block
         | 
| 3872 4179 | 
             
                    circle(200, 200, 90)
         | 
| 3873 4180 |  | 
| 3874 4181 | 
             
                    fill r: 202, g: 102, b: 204, a: 0.5
         | 
| 3875 4182 | 
             
                    stroke r: 0, g: 0, b: 0, thickness: 2
         | 
| 3876 4183 | 
             
                  }
         | 
| 3877 | 
            -
                   | 
| 3878 | 
            -
                     | 
| 3879 | 
            -
             | 
| 3880 | 
            -
             | 
| 3881 | 
            -
             | 
| 4184 | 
            +
                  text(160, 40, 100) { # x, y, width
         | 
| 4185 | 
            +
                    string {
         | 
| 4186 | 
            +
                      font family: 'Times', size: 14
         | 
| 4187 | 
            +
                      color :black
         | 
| 4188 | 
            +
                      
         | 
| 4189 | 
            +
                      'Area Gallery'
         | 
| 4190 | 
            +
                    }
         | 
| 3882 4191 | 
             
                  }
         | 
| 3883 4192 | 
             
                end
         | 
| 3884 4193 |  | 
| @@ -3971,7 +4280,8 @@ window('Area Gallery', 400, 400) { | |
| 3971 4280 | 
             
                      height 100
         | 
| 3972 4281 | 
             
                    }
         | 
| 3973 4282 |  | 
| 3974 | 
            -
                     | 
| 4283 | 
            +
                    # linear gradient (has x0, y0, x1, y1, and stops)
         | 
| 4284 | 
            +
                    fill x0: 10, y0: 10, x1: 350, y1: 350, stops: [{pos: 0.25, r: 204, g: 102, b: 204}, {pos: 0.75, r: 102, g: 102, b: 204}]
         | 
| 3975 4285 | 
             
                  }
         | 
| 3976 4286 | 
             
                  path { # a dynamic path is added semi-declaratively inside on_draw block
         | 
| 3977 4287 | 
             
                    figure {
         | 
| @@ -4033,16 +4343,6 @@ window('Area Gallery', 400, 400) { | |
| 4033 4343 | 
             
                    fill r: 202, g: 102, b: 204, a: 0.5
         | 
| 4034 4344 | 
             
                    stroke r: 0, g: 0, b: 0, thickness: 2, dashes: [50, 10, 10, 10], dash_phase: -50.0
         | 
| 4035 4345 | 
             
                  }
         | 
| 4036 | 
            -
                  path { # a dynamic path is added semi-declaratively inside on_draw block
         | 
| 4037 | 
            -
                    circle {
         | 
| 4038 | 
            -
                      x_center 200
         | 
| 4039 | 
            -
                      y_center 200
         | 
| 4040 | 
            -
                      radius 90
         | 
| 4041 | 
            -
                    }
         | 
| 4042 | 
            -
              
         | 
| 4043 | 
            -
                    fill r: 202, g: 102, b: 204, a: 0.5
         | 
| 4044 | 
            -
                    stroke r: 0, g: 0, b: 0, thickness: 2
         | 
| 4045 | 
            -
                  }
         | 
| 4046 4346 | 
             
                  path { # a dynamic path is added semi-declaratively inside on_draw block
         | 
| 4047 4347 | 
             
                    arc {
         | 
| 4048 4348 | 
             
                      x_center 400
         | 
| @@ -4053,9 +4353,32 @@ window('Area Gallery', 400, 400) { | |
| 4053 4353 | 
             
                      is_negative false
         | 
| 4054 4354 | 
             
                    }
         | 
| 4055 4355 |  | 
| 4056 | 
            -
                     | 
| 4356 | 
            +
                    # radial gradient (has an outer_radius in addition to x0, y0, x1, y1, and stops)
         | 
| 4357 | 
            +
                    fill outer_radius: 90, x0: 0, y0: 0, x1: 500, y1: 500, stops: [{pos: 0.25, r: 102, g: 102, b: 204, a: 0.5}, {pos: 0.75, r: 204, g: 102, b: 204}]
         | 
| 4057 4358 | 
             
                    stroke r: 0, g: 0, b: 0, thickness: 2, dashes: [50, 10, 10, 10], dash_phase: -50.0
         | 
| 4058 4359 | 
             
                  }
         | 
| 4360 | 
            +
                  path { # a dynamic path is added semi-declaratively inside on_draw block
         | 
| 4361 | 
            +
                    circle {
         | 
| 4362 | 
            +
                      x_center 200
         | 
| 4363 | 
            +
                      y_center 200
         | 
| 4364 | 
            +
                      radius 90
         | 
| 4365 | 
            +
                    }
         | 
| 4366 | 
            +
              
         | 
| 4367 | 
            +
                    fill r: 202, g: 102, b: 204, a: 0.5
         | 
| 4368 | 
            +
                    stroke r: 0, g: 0, b: 0, thickness: 2
         | 
| 4369 | 
            +
                  }
         | 
| 4370 | 
            +
                  text {
         | 
| 4371 | 
            +
                    x 160
         | 
| 4372 | 
            +
                    y 40
         | 
| 4373 | 
            +
                    width 100
         | 
| 4374 | 
            +
                    
         | 
| 4375 | 
            +
                    string {
         | 
| 4376 | 
            +
                      font family: 'Times', size: 14
         | 
| 4377 | 
            +
                      color :black
         | 
| 4378 | 
            +
                      
         | 
| 4379 | 
            +
                      'Area Gallery'
         | 
| 4380 | 
            +
                    }
         | 
| 4381 | 
            +
                  }
         | 
| 4059 4382 | 
             
                end
         | 
| 4060 4383 |  | 
| 4061 4384 | 
             
                on_mouse_event do |area_mouse_event|
         | 
| @@ -5441,11 +5764,11 @@ class CustomDrawText | |
| 5441 5764 | 
             
                        text { # default arguments for x, y, and width are (0, 0, area_draw_params[:area_width])
         | 
| 5442 5765 | 
             
                          # align :left # default alignment
         | 
| 5443 5766 |  | 
| 5444 | 
            -
                           | 
| 5445 | 
            -
                            font @font | 
| 5446 | 
            -
                            color @color | 
| 5447 | 
            -
                            background @background | 
| 5448 | 
            -
                            underline @underline | 
| 5767 | 
            +
                          string {
         | 
| 5768 | 
            +
                            font @font
         | 
| 5769 | 
            +
                            color @color
         | 
| 5770 | 
            +
                            background @background
         | 
| 5771 | 
            +
                            underline @underline
         | 
| 5449 5772 |  | 
| 5450 5773 | 
             
                            '  At last Ygramul sensed that something was coming toward ' \
         | 
| 5451 5774 | 
             
                            'her. With the speed of lightning, she turned about, confronting ' \
         | 
| @@ -5479,7 +5802,122 @@ class CustomDrawText | |
| 5479 5802 | 
             
            end
         | 
| 5480 5803 |  | 
| 5481 5804 | 
             
            CustomDrawText.new.launch
         | 
| 5805 | 
            +
            ```
         | 
| 5806 | 
            +
             | 
| 5807 | 
            +
            ### Method-Based Custom Keyword
         | 
| 5808 | 
            +
             | 
| 5809 | 
            +
            [examples/method_based_custom_keyword.rb](examples/method_based_custom_keyword.rb)
         | 
| 5810 | 
            +
             | 
| 5811 | 
            +
            Run with this command from the root of the project if you cloned the project:
         | 
| 5812 | 
            +
             | 
| 5813 | 
            +
            ```
         | 
| 5814 | 
            +
            ruby -r './lib/glimmer-dsl-libui' examples/method_based_custom_keyword.rb
         | 
| 5815 | 
            +
            ```
         | 
| 5816 | 
            +
             | 
| 5817 | 
            +
            Run with this command if you installed the [Ruby gem](https://rubygems.org/gems/glimmer-dsl-libui):
         | 
| 5818 | 
            +
             | 
| 5819 | 
            +
            ```
         | 
| 5820 | 
            +
            ruby -r glimmer-dsl-libui -e "require 'examples/method_based_custom_keyword'"
         | 
| 5821 | 
            +
            ```
         | 
| 5822 | 
            +
             | 
| 5823 | 
            +
            Mac
         | 
| 5824 | 
            +
             | 
| 5825 | 
            +
            
         | 
| 5826 | 
            +
             | 
| 5827 | 
            +
            Linux
         | 
| 5828 | 
            +
             | 
| 5829 | 
            +
            
         | 
| 5830 | 
            +
             | 
| 5831 | 
            +
            New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version:
         | 
| 5482 5832 |  | 
| 5833 | 
            +
            ```ruby
         | 
| 5834 | 
            +
            require 'glimmer-dsl-libui'
         | 
| 5835 | 
            +
            require 'facets'
         | 
| 5836 | 
            +
             | 
| 5837 | 
            +
            include Glimmer
         | 
| 5838 | 
            +
             | 
| 5839 | 
            +
            Address = Struct.new(:street, :p_o_box, :city, :state, :zip_code)
         | 
| 5840 | 
            +
             | 
| 5841 | 
            +
            def field(model, property)
         | 
| 5842 | 
            +
              property = property.to_s
         | 
| 5843 | 
            +
              entry { |e|
         | 
| 5844 | 
            +
                label property.underscore.split('_').map(&:capitalize).join(' ')
         | 
| 5845 | 
            +
                text model.send(property).to_s
         | 
| 5846 | 
            +
             | 
| 5847 | 
            +
                on_changed do
         | 
| 5848 | 
            +
                  model.send("#{property}=", e.text)
         | 
| 5849 | 
            +
                end
         | 
| 5850 | 
            +
              }
         | 
| 5851 | 
            +
            end
         | 
| 5852 | 
            +
             | 
| 5853 | 
            +
            def address_form(address)
         | 
| 5854 | 
            +
              form {
         | 
| 5855 | 
            +
                field(address, :street)
         | 
| 5856 | 
            +
                field(address, :p_o_box)
         | 
| 5857 | 
            +
                field(address, :city)
         | 
| 5858 | 
            +
                field(address, :state)
         | 
| 5859 | 
            +
                field(address, :zip_code)
         | 
| 5860 | 
            +
              }
         | 
| 5861 | 
            +
            end
         | 
| 5862 | 
            +
             | 
| 5863 | 
            +
            def label_pair(model, attribute, value)
         | 
| 5864 | 
            +
              name_label = nil
         | 
| 5865 | 
            +
              value_label = nil
         | 
| 5866 | 
            +
              horizontal_box {
         | 
| 5867 | 
            +
                name_label = label(attribute.to_s.underscore.split('_').map(&:capitalize).join(' '))
         | 
| 5868 | 
            +
                value_label = label(value.to_s)
         | 
| 5869 | 
            +
              }
         | 
| 5870 | 
            +
              Glimmer::DataBinding::Observer.proc do
         | 
| 5871 | 
            +
                value_label.text = model.send(attribute)
         | 
| 5872 | 
            +
              end.observe(model, attribute)
         | 
| 5873 | 
            +
            end
         | 
| 5874 | 
            +
             | 
| 5875 | 
            +
            def address(address)
         | 
| 5876 | 
            +
              vertical_box {
         | 
| 5877 | 
            +
                address.each_pair do |attribute, value|
         | 
| 5878 | 
            +
                  label_pair(address, attribute, value)
         | 
| 5879 | 
            +
                end
         | 
| 5880 | 
            +
              }
         | 
| 5881 | 
            +
            end
         | 
| 5882 | 
            +
             | 
| 5883 | 
            +
            address1 = Address.new('123 Main St', '23923', 'Denver', 'Colorado', '80014')
         | 
| 5884 | 
            +
            address2 = Address.new('2038 Park Ave', '83272', 'Boston', 'Massachusetts', '02101')
         | 
| 5885 | 
            +
             | 
| 5886 | 
            +
            window('Method-Based Custom Keyword') {
         | 
| 5887 | 
            +
              margined true
         | 
| 5888 | 
            +
              
         | 
| 5889 | 
            +
              horizontal_box {
         | 
| 5890 | 
            +
                vertical_box {
         | 
| 5891 | 
            +
                  label('Address 1') {
         | 
| 5892 | 
            +
                    stretchy false
         | 
| 5893 | 
            +
                  }
         | 
| 5894 | 
            +
                  address_form(address1)
         | 
| 5895 | 
            +
                  horizontal_separator {
         | 
| 5896 | 
            +
                    stretchy false
         | 
| 5897 | 
            +
                  }
         | 
| 5898 | 
            +
                  label('Address 1 (Saved)') {
         | 
| 5899 | 
            +
                    stretchy false
         | 
| 5900 | 
            +
                  }
         | 
| 5901 | 
            +
                  address(address1)
         | 
| 5902 | 
            +
                }
         | 
| 5903 | 
            +
                vertical_separator {
         | 
| 5904 | 
            +
                  stretchy false
         | 
| 5905 | 
            +
                }
         | 
| 5906 | 
            +
                vertical_box {
         | 
| 5907 | 
            +
                  label('Address 2') {
         | 
| 5908 | 
            +
                    stretchy false
         | 
| 5909 | 
            +
                  }
         | 
| 5910 | 
            +
                  address_form(address2)
         | 
| 5911 | 
            +
                  horizontal_separator {
         | 
| 5912 | 
            +
                    stretchy false
         | 
| 5913 | 
            +
                  }
         | 
| 5914 | 
            +
                  label('Address 2 (Saved)') {
         | 
| 5915 | 
            +
                    stretchy false
         | 
| 5916 | 
            +
                  }
         | 
| 5917 | 
            +
                  address(address2)
         | 
| 5918 | 
            +
                }
         | 
| 5919 | 
            +
              }
         | 
| 5920 | 
            +
            }.show
         | 
| 5483 5921 | 
             
            ```
         | 
| 5484 5922 |  | 
| 5485 5923 | 
             
            ## Contributing to glimmer-dsl-libui
         |