` element in html, which contains a string of text). Alternatively, you can nest attributed `string(string_value) {[attributes]}` if `string_value` is a short single-line string. An attributed `string` value can be changed dynamically via its `string` property.
`text` has the following properties:
- `default_font`:
- `align`: `:left` (default), `:center`, or `:right` (`align` currently seems not to work on the Mac)
- `x`: x coordinate in relation to parent `area` top-left corner
- `y`: y coordinate in relation to parent `area` top-left corner
- `width` (default: area width - x*2): width of text to display
`string` has the following properties:
- `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
- `color`: rgba, hex, or [X11](https://en.wikipedia.org/wiki/X11_color_names) color
- `background`: rgba, hex, or [X11](https://en.wikipedia.org/wiki/X11_color_names) color
- `underline`: one of `:none`, `:single`, `:double`, `:suggestion`, `:color_custom`, `:color_spelling`, `:color_grammar`, `:color_auxiliary`
- `underline_color`: one of `:spelling`, `:grammar`, `:auxiliary`, rgba, hex, or [X11](https://en.wikipedia.org/wiki/X11_color_names) color
- `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.
- `string`: string value (`String`)
Example (you may copy/paste in [`girb`](#girb-glimmer-irb)):
```ruby
require 'glimmer-dsl-libui'
include Glimmer
window('area text drawing') {
area {
text {
default_font family: 'Helvetica', size: 12, weight: :normal, italic: :normal, stretch: :normal
string('This ') {
font size: 20, weight: :bold, italic: :normal, stretch: :normal
color r: 128, g: 0, b: 0, a: 1
}
string('is ') {
font size: 20, weight: :bold, italic: :normal, stretch: :normal
color r: 0, g: 128, b: 0, a: 1
}
string('a ') {
font size: 20, weight: :bold, italic: :normal, stretch: :normal
color r: 0, g: 0, b: 128, a: 1
}
string('short ') {
font size: 20, weight: :bold, italic: :italic, stretch: :normal
color r: 128, g: 128, b: 0, a: 1
}
string('attributed ') {
font size: 20, weight: :bold, italic: :normal, stretch: :normal
color r: 0, g: 128, b: 128, a: 1
}
string("string \n\n") {
font size: 20, weight: :bold, italic: :normal, stretch: :normal
color r: 128, g: 0, b: 128, a: 1
}
string {
font family: 'Georgia', size: 13, weight: :medium, italic: :normal, stretch: :normal
color r: 0, g: 128, b: 255, a: 1
background r: 255, g: 255, b: 0, a: 0.5
underline :single
underline_color :spelling
open_type_features {
open_type_tag 'l', 'i', 'g', 'a', 0
open_type_tag 'l', 'i', 'g', 'a', 1
}
"This is a demonstration\n" \
"of a very long\n" \
"attributed string\n" \
"spanning multiple lines\n\n"
}
}
}
}.show
```
![glimmer-dsl-libui-mac-area-text-drawing.png](/images/glimmer-dsl-libui-mac-area-text-drawing.png)
You may checkout [examples/basic_draw_text.rb](/docs/examples/GLIMMER-DSL-LIBUI-BASIC-EXAMPLES.md#basic-draw-text) and [examples/custom_draw_text.rb](/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#custom-draw-text) for examples of using `text` inside `area`.
Mac | Windows | Linux
----|---------|------
![glimmer-dsl-libui-mac-custom-draw-text-changed.png](images/glimmer-dsl-libui-mac-custom-draw-text-changed.png) | ![glimmer-dsl-libui-windows-custom-draw-text-changed.png](images/glimmer-dsl-libui-windows-custom-draw-text-changed.png) | ![glimmer-dsl-libui-linux-custom-draw-text-changed.png](images/glimmer-dsl-libui-linux-custom-draw-text-changed.png)
#### Area Image
**(ALPHA FEATURE)**
[libui-ng](https://github.com/libui-ng/libui-ng) does not support `image` rendering outside of `table` yet.
However, [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) adds a special `image(file as String path or web URL, width as Numeric, height as Numeric)` custom control that renders an image unto an `area` pixel by pixel (and when possible to optimize, line by line).
Given that it is very new and is not a [libui-ng](https://github.com/libui-ng/libui-ng)-native control, please keep these notes in mind:
- It only supports the `.png` file format.
- [libui-ng](https://github.com/libui-ng/libui-ng) pixel-by-pixel rendering performance is slow.
- Including an `image` inside an `area` `on_draw` listener improves performance due to not retaining pixel/line data in memory.
- Supplying `width` and `height` options greatly improves performance when shrinking image (e.g. `image('somefile.png', width: 24, height: 24)`). You can also supply one of the two dimensions, and the other one gets calculated automatically while preserving original aspect ratio (e.g. `image('somefile.png', height: 24)`)
- [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) lets you optionally specify `x` and `y` in addition to `file`, `width` and `height` (5 arguments total) to offset image location.
Currently, it is recommended to use `image` with very small `width` and `height` values only (e.g. 24x24).
Setting a [`transform` `matrix`](#area-transform-matrix) is supported under `image` just like it is under `path` and `text` inside `area`.
Example of using `image` declaratively (you may copy/paste in [`girb`](#girb-glimmer-irb)):
Mac | Windows | Linux
----|---------|------
![glimmer-dsl-libui-mac-basic-image.png](images/glimmer-dsl-libui-mac-basic-image.png) | ![glimmer-dsl-libui-windows-basic-image.png](images/glimmer-dsl-libui-windows-basic-image.png) | ![glimmer-dsl-libui-linux-basic-image.png](images/glimmer-dsl-libui-linux-basic-image.png)
```ruby
require 'glimmer-dsl-libui'
include Glimmer
window('Basic Image', 96, 96) {
area {
image(File.expand_path('icons/glimmer.png', __dir__), height: 96) # width is automatically calculated from height while preserving original aspect ratio
# image(File.expand_path('icons/glimmer.png', __dir__), width: 96, height: 96) # you can specify both width and height options
# image(File.expand_path('icons/glimmer.png', __dir__), 96, 96) # you can specify width, height as args
# image(File.expand_path('../icons/glimmer.png', __dir__), 0, 0, 96, 96) # you can specify x, y, width, height args as alternative
# image(File.expand_path('../icons/glimmer.png', __dir__), x: 0, y: 0, width: 96, height: 96) # you can specify x, y, width, height options as alternative
}
}.show
```
Example of better performance via `on_draw` (you may copy/paste in [`girb`](#girb-glimmer-irb)):
```ruby
require 'glimmer-dsl-libui'
include Glimmer
window('Basic Image', 96, 96) {
area {
on_draw do |area_draw_params|
image(File.expand_path('icons/glimmer.png', __dir__), 96, 96)
end
}
}.show
```
Example of using `image` declaratively with explicit properties (you may copy/paste in [`girb`](#girb-glimmer-irb)):
```ruby
require 'glimmer-dsl-libui'
include Glimmer
window('Basic Image', 96, 96) {
area {
image {
file File.expand_path('icons/glimmer.png', __dir__)
# x 0 # default
# y 0 # default
width 96
height 96
}
}
}.show
```
Example of better performance via `on_draw` with explicit properties (you may copy/paste in [`girb`](#girb-glimmer-irb)):
```ruby
require 'glimmer-dsl-libui'
include Glimmer
window('Basic Image', 96, 96) {
area {
on_draw do |area_draw_params|
image {
file File.expand_path('icons/glimmer.png', __dir__)
width 96
height 96
}
end
}
}.show
```
If you need to render an image pixel by pixel (e.g. to support a format other than `.png`) for very exceptional scenarios, you may use this example as a guide, including a line-merge optimization for neighboring horizontal pixels with the same color:
```ruby
# This is the manual way of rendering an image unto an area control.
# It could come in handy in special situations.
# Otherwise, it is recommended to simply utilize the `image` control that
# can be nested under area or area on_draw listener to automate all this work.
require 'glimmer-dsl-libui'
require 'chunky_png'
include Glimmer
puts 'Parsing image...'; $stdout.flush
f = File.open(File.expand_path('icons/glimmer.png', __dir__))
canvas = ChunkyPNG::Canvas.from_io(f)
f.close
canvas.resample_nearest_neighbor!(96, 96)
data = canvas.to_rgba_stream
width = canvas.width
height = canvas.height
puts "Image width: #{width}"
puts "Image height: #{height}"
puts 'Parsing colors...'; $stdout.flush
color_maps = height.times.map do |y|
width.times.map do |x|
r = data[(y*width + x)*4].ord
g = data[(y*width + x)*4 + 1].ord
b = data[(y*width + x)*4 + 2].ord
a = data[(y*width + x)*4 + 3].ord
{x: x, y: y, color: {r: r, g: g, b: b, a: a}}
end
end.flatten
puts "#{color_maps.size} pixels to render..."; $stdout.flush
puts 'Parsing shapes...'; $stdout.flush
shape_maps = []
original_color_maps = color_maps.dup
indexed_original_color_maps = Hash[original_color_maps.each_with_index.to_a]
color_maps.each do |color_map|
index = indexed_original_color_maps[color_map]
@rectangle_start_x ||= color_map[:x]
@rectangle_width ||= 1
if color_map[:x] < width - 1 && color_map[:color] == original_color_maps[index + 1][:color]
@rectangle_width += 1
else
if color_map[:x] > 0 && color_map[:color] == original_color_maps[index - 1][:color]
shape_maps << {x: @rectangle_start_x, y: color_map[:y], width: @rectangle_width, height: 1, color: color_map[:color]}
else
shape_maps << {x: color_map[:x], y: color_map[:y], width: 1, height: 1, color: color_map[:color]}
end
@rectangle_width = 1
@rectangle_start_x = color_map[:x] == width - 1 ? 0 : color_map[:x] + 1
end
end
puts "#{shape_maps.size} shapes to render..."; $stdout.flush
puts 'Rendering image...'; $stdout.flush
window('Basic Image', 96, 96) {
area {
on_draw do |area_draw_params|
shape_maps.each do |shape_map|
path {
rectangle(shape_map[:x], shape_map[:y], shape_map[:width], shape_map[:height])
fill shape_map[:color]
}
end
end
}
}.show
```
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.
![linux table image](images/glimmer-dsl-libui-linux-basic-table-image.png)
Check out [examples/basic_image.rb](/docs/examples/GLIMMER-DSL-LIBUI-BASIC-EXAMPLES.md#basic-image) (all versions) for examples of using `image` Glimmer custom control.
#### Colors
`fill` and `stroke` accept [X11](https://en.wikipedia.org/wiki/X11_color_names) color `Symbol`s/`String`s like `:skyblue` and `'sandybrown'` or 6-char hex or 3-char hex-shorthand (as `Integer` or `String` with or without `0x` prefix)
Available [X11 colors](https://en.wikipedia.org/wiki/X11_color_names) can be obtained through `Glimmer::LibUI.x11_colors` method.
Check [Basic Transform](/docs/examples/GLIMMER-DSL-LIBUI-BASIC-EXAMPLES.md#basic-transform) example for use of [X11](https://en.wikipedia.org/wiki/X11_color_names) colors.
Check [Histogram](/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#histogram) example for use of hex colors.
#### Area Draw Params
The `area_draw_params` `Hash` argument for `on_draw` block is a hash consisting of the following keys:
- `:context`: the drawing context object
- `:area_width`: area width
- `:area_height`: area height
- `:clip_x`: clip region top-left x coordinate
- `:clip_y`: clip region top-left y coordinate
- `:clip_width`: clip region width
- `:clip_height`: clip region height
In general, it is recommended to use declarative stable paths whenever feasible since they require less code and simpler maintenance. But, in more advanced cases, semi-declarative dynamic paths could be used instead, especially if there are thousands of dynamic paths that need maximum performance and low memory footprint.
#### Area Listeners
`area` supports a number of keyboard and mouse listeners to enable observing the control for user interaction to execute some logic.
The same listeners can be nested directly under `area` shapes like `rectangle` and `circle`, and [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) will automatically detect when the mouse lands within those shapes to constrain triggering the listeners by the shape regions.
`area` supported listeners are:
- `on_key_event {|area_key_event| ...}`: general catch-all key event (recommend using fine-grained key events below instead)
- `on_key_down {|area_key_event| ...}`
- `on_key_up {|area_key_event| ...}`
- `on_mouse_event {|area_mouse_event| ...}`: general catch-all mouse event (recommend using fine-grained mouse events below instead)
- `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_mouse_crossed {|left| ...}` (NOT RECOMMENDED; it does what `on_mouse_entered` and `on_mouse_exited` do by returning a `left` argument indicating if mouse left `area`)
- `on_drag_broken {...}` (NOT RECOMMENDED; varies per platforms; use `on_mouse_dropped` instead)
The `area_mouse_event` `Hash` argument for mouse events that receive it (e.g. `on_mouse_up`, `on_mouse_dragged`) consist of the following hash keys:
- `:x`: mouse x location in relation to area's top-left-corner
- `:y`: mouse y location in relation to area's top-left-corner
- `:area_width`: area current width
- `:area_height`: area current height
- `:down`: mouse pressed button (e.g. `1` is left button, `3` is right button)
- `:up`: mouse depressed button (e.g. `1` is left button, `3` is right button)
- `:count`: count of mouse clicks (e.g. `2` for double-click, `1` for single-click)
- `:modifers`: `Array` of `Symbol`s from one of the following: `[:command, :shift, :alt, :control]`
- `:held`: mouse held button during dragging (e.g. `1` is left button, `4` is right button)
The `area_key_event` `Hash` argument for keyboard events that receive it (e.g. `on_key_up`, `on_key_down`) consist of the following hash keys:
- `:key`: key character (`String`)
- `:key_value` (alias: `:key_code`): key code value (`Integer`). Useful in rare cases for numeric processing of keys instead of dealing with as `:key` character `String`
- `:ext_key`: non-character extra key (`Symbol`) from `Glimmer::LibUI.enum_symbols(:ext_key)` such as `:left`, `:right`, `:escape`, `:insert`
- `:ext_key_value`: non-character extra key value (`Integer`). Useful in rare cases for numeric processing of extra keys instead of dealing with as `:ext_key` `Symbol`
- `:modifier`: modifier key pressed alone (e.g. `:shift` or `:control`)
- `:modifiers`: modifier keys pressed simultaneously with `:key`, `:ext_key`, or `:modifier`
- `:up`: indicates if key has been released or not (Boolean)
#### Area Methods/Attributes
To redraw an `area`, you may call the `#queue_redraw_all` method, or simply `#redraw`.
`area` has the following Glimmer-added API methods/attributes:
- `request_auto_redraw`: requests auto redraw upon changes to nested stable `path` or shapes
- `pause_auto_redraw`: pause auto redraw upon changes to nested stable `path` or shapes (useful to avoid too many micro-change redraws, to group all redraws as one after many micro-changes)
- `resume_auto_redraw`: resume auto redraw upon changes to nested stable `path` or shapes
- `auto_redraw_enabled`/`auto_redraw_enabled?`/`auto_redraw_enabled=`: an attribute to disable/enable auto redraw on an `area` upon changes to nested stable `path` or shapes
#### Area Transform Matrix
A transform `matrix` can be set on a path by building a `matrix(m11 = nil, m12 = nil, m21 = nil, m22 = nil, m31 = nil, m32 = nil) {operations}` proxy object and then setting via `transform` property, or alternatively by building and setting the matrix in one call to `transform(m11 = nil, m12 = nil, m21 = nil, m22 = nil, m31 = nil, m32 = nil) {operations}` passing it the matrix arguments and/or content operations.
When instantiating a `matrix` object, it always starts with identity matrix.
Here are the following operations that can be performed in a `matrix` body:
- `identity` [alias: `set_identity`]: resets matrix to identity matrix
- `translate(x as Numeric, y as Numeric)`
- `scale(x_center = 0 as Numeric, y_center = 0 as Numeric, x as Numeric, y as Numeric)`
- `skew(x = 0 as Numeric, y = 0 as Numeric, x_amount as Numeric, y_amount as Numeric)`
- `rotate(x = 0 as Numeric, y = 0 as Numeric, degrees as Numeric)`
Example of using transform matrix (you may copy/paste in [`girb`](#girb-glimmer-irb)):
```ruby
require 'glimmer-dsl-libui'
include Glimmer
window('Basic Transform', 350, 350) {
area {
path {
square(0, 0, 350)
fill r: 255, g: 255, b: 0
}
40.times do |n|
path {
square(0, 0, 100)
fill r: [255 - n*5, 0].max, g: [n*5, 255].min, b: 0, a: 0.5
stroke :black, thickness: 2
transform {
skew 0.15, 0.15
translate 50, 50
rotate 100, 100, -9 * n
scale 1.1, 1.1
}
}
end
}
}.show
```
Keep in mind that this part could be written differently when there is a need to reuse the matrix:
```ruby
transform {
translate 100, 100
rotate 100, 100, -9 * n
}
```
Alternatively:
```ruby
m1 = matrix {
translate 100, 100
rotate 100, 100, -9 * n
}
transform m1
# and then reuse m1 elsewhere too
```
You can set a `matrix`/`transform` on `area` directly to conveniently apply to all nested `path`s too.
Note that `area`, `path`, and nested shapes are all truly declarative, meaning they do not care about the ordering of calls to `fill`, `stroke`, and `transform`. Furthermore, any transform that is applied is reversed at the end of the block, so you never have to worry about the ordering of `transform` calls among different paths. You simply set a transform on the `path`s that need it and it is guaranteed to be called before all its content is drawn, and then undone afterwards to avoid affecting later paths. Matrix `transform` can be set on an entire `area` too, applying to all nested `path`s.
#### Area Animation
If you need to animate `area` vector graphics, you just have to use the [`Glimmer::LibUI::timer`](#libui-operations) method along with making changes to shape attributes.
Spinner example that has a fully customizable method-based custom control called `spinner`, which is destroyed if you click on it (you may copy/paste in [`girb`](#girb-glimmer-irb)):
```ruby
require 'glimmer-dsl-libui'
class SpinnerExample
include Glimmer
SIZE = 120
def initialize
create_gui
end
def launch
@main_window.show
end
def create_gui
@main_window = window {
title 'Spinner'
content_size SIZE*2, SIZE*2
horizontal_box {
padded false
vertical_box {
padded false
spinner(size: SIZE)
spinner(size: SIZE, fill_color: [42, 153, 214])
}
vertical_box {
padded false
spinner(size: SIZE/2.0, fill_color: :orange)
spinner(size: SIZE/2.0, fill_color: {x0: 0, y0: 0, x1: SIZE/2.0, y1: SIZE/2.0, stops: [{pos: 0.25, r: 204, g: 102, b: 204}, {pos: 1, r: 2, g: 2, b: 254}]})
spinner(size: SIZE/2.0, fill_color: :green, unfilled_color: :yellow)
spinner(size: SIZE/2.0, fill_color: :white, unfilled_color: :gray, background_color: :black)
}
}
}
end
def spinner(size: 40.0, fill_color: :gray, background_color: :white, unfilled_color: {r: 243, g: 243, b: 243}, donut_percentage: 0.25)
arc1 = arc2 = nil
area { |the_area|
rectangle(0, 0, size, size) {
fill background_color
}
circle(size/2.0, size/2.0, size/2.0) {
fill fill_color
}
arc1 = arc(size/2.0, size/2.0, size/2.0, 0, 180) {
fill unfilled_color
}
arc2 = arc(size/2.0, size/2.0, size/2.0, 90, 180) {
fill unfilled_color
}
circle(size/2.0, size/2.0, (size/2.0)*(1.0 - donut_percentage)) {
fill background_color
}
on_mouse_up do
the_area.destroy
end
}.tap do
Glimmer::LibUI.timer(0.05) do
delta = 10
arc1.start_angle += delta
arc2.start_angle += delta
end
end
end
end
SpinnerExample.new.launch
```
![mac spinner](/images/glimmer-dsl-libui-mac-spinner.gif)
### Smart Defaults and Conventions
- `horizontal_box`, `vertical_box`, `grid`, and `form` controls have `padded` as `true` upon instantiation to ensure more user-friendly GUI by default
- `group` controls have `margined` as `true` upon instantiation to ensure more user-friendly GUI by default
- All controls nested under a `horizontal_box`, `vertical_box`, and `form` have `stretchy` property (fill maximum space) as `true` by default (passed to `box_append`/`form_append` method)
- If an event listener is repeated under a control (e.g. two `on_clicked {}` listeners under `button`), it does not overwrite the previous listener, yet it is added to an `Array` of listeners for the event. [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) provides multiple-event-listener support unlike [LibUI](https://github.com/kojix2/LibUI)
- `window` instatiation args can be left off, having the following defaults when unspecified: `title` as `''`, `width` as `190`, `height` as `150`, and `has_menubar` as `true`)
- `window` has an `on_closing` listener by default that quits application upon hitting close button if window is the main window, not a child window (the behavior can be overridden with a manual `on_closing` implementation that returns integer `1`/`true` for closing and `0`/`false` for remaining open).
- `group` has `title` property default to `''` if not specified in instantiation args, so it can be instantiated without args with `title` property specified in nested block (e.g. `group {title 'Address'; ...}`)
- `button`, `checkbox`, and `label` have `text` default to `''` if not specified in instantiation args, so they can be instantiated without args with `text` property specified in nested block (e.g. `button {text 'Greet'; on_clicked {puts 'Hello'}}`)
- `quit_menu_item` has an `on_clicked` listener by default that quits application upon selecting the quit menu item (can be overridden with a manual `on_clicked` implementation that returns integer `0` for success)
- If an `on_closing` listener was defined on `window` and it does not return an integer, default exit behavior is assumed (`window.destroy` is called followed by `LibUI.quit`, returning `0`).
- If multiple `on_closing` listeners were added for `window`, and none return an integer, they are all executed. On the other hand, if one of them returns an integer, it is counted as the final return value and stops the chain of listener execution.
- If an `on_clicked` listener was defined on `quit_menu_item` and it does not return an integer, default exit behavior is assumed (`quit_menu_item.destroy` and `main_window.destroy` are called followed by `LibUI.quit`, returning `0`).
- If multiple `on_clicked` listeners were added for `quit_menu_item`, and none return an integer, they are all executed. On the other hand, if one of them returns an integer, it is counted as the final return value and stops the chain of listener execution.
- All boolean property readers return `true` or `false` in Ruby instead of the [libui-ng](https://github.com/libui-ng/libui-ng) original `0` or `1` in C.
- All boolean property writers accept `true`/`false` in addition to `1`/`0` in Ruby
- All string property readers return a `String` object in Ruby instead of the [libui-ng](https://github.com/libui-ng/libui-ng) Fiddle pointer object.
- Automatically allocate font descriptors upon instantiating `font_button` controls and free them when destroying `font_button` controls
- Automatically allocate color value pointers upon instantiating `color_button` controls and free them when destroying `color_button` controls
- On the Mac, if no `menu` items were added, an automatic `quit_menu_item` is added to enable quitting with CTRL+Q
- When destroying a control nested under a `horizontal_box` or `vertical_box`, it is automatically deleted from the box's children
- When destroying a control nested under a `form`, it is automatically deleted from the form's children
- When destroying a control nested under a `window` or `group`, it is automatically unset as their child to allow successful destruction
- When destroying a control that has a data-binding to a model attribute, the data-binding observer registration is automatically deregistered
- For `date_time_picker`, `date_picker`, and `time_picker`, make sure `time` hash values for `mon`, `wday`, and `yday` are 1-based instead of [libui-ng](https://github.com/libui-ng/libui-ng) original 0-based values, and return `dst` as Boolean instead of `isdst` as `1`/`0`
- Smart defaults for `grid` child properties are `left` (`0`), `top` (`0`), `xspan` (`1`), `yspan` (`1`), `hexpand` (`false`), `halign` (`:fill`), `vexpand` (`false`), and `valign` (`:fill`)
- The `table` control automatically constructs required `TableModelHandler`, `TableModel`, and `TableParams`, calculating all their arguments from `cell_rows` and `editable` properties (e.g. `NumRows`) as well as nested columns (e.g. `text_column`)
- Table model instances are automatically freed from memory after `window` is destroyed.
- Table `cell_rows` data has implicit data-binding to table cell values for deletion, insertion, and change (done by diffing `cell_rows` value before and after change and auto-informing `table` of deletions [`LibUI.table_model_row_deleted`], insertions [`LibUI.table_model_row_deleted`], and changes [`LibUI.table_model_row_changed`]). When deleting data rows from `cell_rows` array, then actual rows from the `table` are automatically deleted. When inserting data rows into `cell_rows` array, then actual `table` rows are automatically inserted. When updating data rows in `cell_rows` array, then actual `table` rows are automatically updated.
- `image` instances are automatically freed from memory after `window` is destroyed.
- `image` `width` and `height` can be left off if it has one `image_part` only as they default to the same `width` and `height` of the `image_part`
- Automatically provide shifted `:key` characters in `area_key_event` provided in `area` key listeners `on_key_event`, `on_key_down`, and `on_key_up`
- `scrolling_area` `width` and `height` default to main window width and height if not specified.
- `scrolling_area` `#scroll_to` 3rd and 4th arguments (`width` and `height`) default to main window width and height if not specified.
- `area` paths are specified declaratively with shapes/figures underneath (e.g. `rectangle`), and `area` draw listener is automatically generated
- `area` path shapes can be added directly under `area` without declaring `path` explicitly as a convenient shorthand
- `line` and `bezier` automatically start a new figure if placed outside of `figure`
- Observe figure properties (e.g. `rectangle` `width`) for changes and automatically redraw containing area accordingly
- Observe `path` `fill` and `stroke` hashes for changes and automatically redraw containing area accordingly
- Observe `text` and `string` properties for changes and automatically redraw containing area accordingly
- All controls are protected from garbage collection until no longer needed (explicitly destroyed), so there is no need to worry about surprises.
- All resources are freed automatically once no longer needed or left to garbage collection.
- When nesting an `area` directly underneath `window` (without a layout control like `vertical_box`), it is automatically reparented with `vertical_box` in between the `window` and `area` since it would not show up on Linux otherwise.
- 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-char hex or 3-char hex (as `Integer` or `String` with or without `0x` prefix)
- Color alpha value defaults to `1.0` when not specified.
### Custom Keywords
Custom keywords can be defined to represent custom controls (components) that provide new features or act as composites of [existing controls](#supported-keywords) that need to be reused multiple times in an application or across multiple applications. Custom keywords save a lot of development time, improving productivity and maintainability immensely.
For example, you can define a custom `address_view` control as an aggregate of multiple `label` controls to reuse multiple times as a standard address View, displaying street, city, state, and zip code.
There are two ways to define custom keywords:
- Method-Based: simply define a method representing the custom control you want (e.g. `address_view`) with any arguments needed (e.g. `address(address_model)`).
- Class-Based: define a class matching the camelcased name of the custom control by convention (e.g. the `address_view` custom control keyword would have a class called `AddressView`) and `include Glimmer::LibUI::CustomControl`. Classes add the benefit of being able to distribute the custom controls into separate files and reuse externally from multiple places or share via Ruby gems.
It is OK to use the terms "custom keyword" and "custom control" synonymously though "custom keyword" is a broader term that covers things other than controls too like custom shapes (e.g. `cylinder`), custom attributed strings (e.g. `alternating_color_string`), and custom transforms (`isometric_transform`).
Example that defines `form_field`, `address_form`, `label_pair`, and `address_view` keywords (you may copy/paste in [`girb`](#girb-glimmer-irb)):
```ruby
require 'glimmer-dsl-libui'
require 'facets'
include Glimmer
Address = Struct.new(:street, :p_o_box, :city, :state, :zip_code)
def form_field(model, attribute)
attribute = attribute.to_s
entry { |e|
label attribute.underscore.split('_').map(&:capitalize).join(' ')
text <=> [model, attribute]
}
end
def address_form(address_model)
form {
form_field(address_model, :street)
form_field(address_model, :p_o_box)
form_field(address_model, :city)
form_field(address_model, :state)
form_field(address_model, :zip_code)
}
end
def label_pair(model, attribute, value)
horizontal_box {
label(attribute.to_s.underscore.split('_').map(&:capitalize).join(' '))
label(value.to_s) {
text <= [model, attribute]
}
}
end
def address_view(address_model)
vertical_box {
address_model.each_pair do |attribute, value|
label_pair(address_model, attribute, value)
end
}
end
address1 = Address.new('123 Main St', '23923', 'Denver', 'Colorado', '80014')
address2 = Address.new('2038 Park Ave', '83272', 'Boston', 'Massachusetts', '02101')
window('Method-Based Custom Keyword') {
margined true
horizontal_box {
vertical_box {
label('Address 1') {
stretchy false
}
address_form(address1)
horizontal_separator {
stretchy false
}
label('Address 1 (Saved)') {
stretchy false
}
address_view(address1)
}
vertical_separator {
stretchy false
}
vertical_box {
label('Address 2') {
stretchy false
}
address_form(address2)
horizontal_separator {
stretchy false
}
label('Address 2 (Saved)') {
stretchy false
}
address_view(address2)
}
}
}.show
```
![glimmer-dsl-libui-mac-method-based-custom-keyword.png](images/glimmer-dsl-libui-mac-method-based-custom-keyword.png)
You can also define Custom Window keywords, that is custom controls with `window` being the body root. These are also known as Applications. To define a Custom Window, you `include Glimmer::LibUI::CustomWindow` or `include Glimmer:LibUI::Application` and then you can invoke the `::launch` method on the class.
Example (you may copy/paste in [`girb`](#girb-glimmer-irb)):
```ruby
require 'glimmer-dsl-libui'
require 'facets'
Address = Struct.new(:street, :p_o_box, :city, :state, :zip_code)
class FormField
include Glimmer::LibUI::CustomControl
options :model, :attribute
body {
entry { |e|
label attribute.to_s.underscore.split('_').map(&:capitalize).join(' ')
text <=> [model, attribute]
}
}
end
class AddressForm
include Glimmer::LibUI::CustomControl
options :address
body {
form {
form_field(model: address, attribute: :street)
form_field(model: address, attribute: :p_o_box)
form_field(model: address, attribute: :city)
form_field(model: address, attribute: :state)
form_field(model: address, attribute: :zip_code)
}
}
end
class LabelPair
include Glimmer::LibUI::CustomControl
options :model, :attribute, :value
body {
horizontal_box {
label(attribute.to_s.underscore.split('_').map(&:capitalize).join(' '))
label(value.to_s) {
text <= [model, attribute]
}
}
}
end
class AddressView
include Glimmer::LibUI::CustomControl
options :address
body {
vertical_box {
address.each_pair do |attribute, value|
label_pair(model: address, attribute: attribute, value: value)
end
}
}
end
class ClassBasedCustomControls
include Glimmer::LibUI::Application # alias: Glimmer::LibUI::CustomWindow
before_body do
@address1 = Address.new('123 Main St', '23923', 'Denver', 'Colorado', '80014')
@address2 = Address.new('2038 Park Ave', '83272', 'Boston', 'Massachusetts', '02101')
end
body {
window('Class-Based Custom Keyword') {
margined true
horizontal_box {
vertical_box {
label('Address 1') {
stretchy false
}
address_form(address: @address1)
horizontal_separator {
stretchy false
}
label('Address 1 (Saved)') {
stretchy false
}
address_view(address: @address1)
}
vertical_separator {
stretchy false
}
vertical_box {
label('Address 2') {
stretchy false
}
address_form(address: @address2)
horizontal_separator {
stretchy false
}
label('Address 2 (Saved)') {
stretchy false
}
address_view(address: @address2)
}
}
}
}
end
ClassBasedCustomControls.launch
```
![glimmer-dsl-libui-mac-method-based-custom-keyword.png](images/glimmer-dsl-libui-mac-method-based-custom-keyword.png)
The [`area`](#area-api) control can be utilized to build non-native custom controls from scratch by leveraging vector graphics, formattable text, keyboard events, and mouse events. This is demonstrated in the [Area-Based Custom Controls](/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#area-based-custom-controls) example.
Defining custom keywords enables unlimited extension of the [Glimmer GUI DSL](#glimmer-gui-dsl). The sky is the limit on what can be done with custom keywords as a result. You can compose new visual vocabulary to build applications in any domain from higher concepts rather than [mere standard controls](#supported-keywords). For example, in a traffic signaling app, you could define `street`, `light_signal`, `traffic_sign`, and `car` as custom keywords and build your application from these concepts directly, saving enormous time and achieving much higher productivity.
Learn more from custom keyword usage in [Method-Based Custom Keyword](/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#method-based-custom-keyword), [Area-Based Custom Controls](/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#area-based-custom-controls), [Basic Scrolling Area](/docs/examples/GLIMMER-DSL-LIBUI-BASIC-EXAMPLES.md#basic-scrolling-area), [Histogram](/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#histogram), and [Tetris](/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#tetris) examples.
### Observer Pattern
The [Observer Design Pattern](https://en.wikipedia.org/wiki/Observer_pattern) (a.k.a. Observer Pattern) is fundamental to building GUIs (Graphical User Interfaces) following the [MVC (Model View Controller) Architectural Pattern](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) or any of its variations like [MVP (Model View Presenter)](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter). In the original Smalltalk-MVC, the View observes the Model for changes and updates itself accordingly.
![MVC - Model View Controller](https://developer.mozilla.org/en-US/docs/Glossary/MVC/model-view-controller-light-blue.png)
[Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) supports the [Observer Design Pattern](https://en.wikipedia.org/wiki/Observer_pattern) via control listeners in the View layer (e.g. `on_clicked` or `on_closing`) and via the `observe(model, attribute_or_key=nil)` keyword in the Model layer, 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:
- `Object` becomes `Glimmer::DataBinding::ObservableModel`, which supports observing specified `Object` model attributes.
- `Hash` becomes `Glimmer::DataBinding::ObservableHash`, which supports observing all `Hash` keys or a specific `Hash` key
- `Array` becomes `Glimmer::DataBinding::ObservableArray`, which supports observing `Array` changes like those done with `push`, `<<`, `delete`, and `map!` methods (all mutation methods).
Example:
```ruby
observe(person, :name) do |new_name|
@name_label.text = new_name
end
```
That observes a person's name attribute for changes and updates the name `label` `text` property accordingly.
[Learn about Glimmer's Observer Pattern capabilities and options in more detail at the Glimmer project page.](https://github.com/AndyObtiva/glimmer#data-binding-library)
See examples of the `observe` keyword at [Color The Circles](/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#color-the-circles), [Method-Based Custom Keyword](/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#method-based-custom-keyword), [Snake](/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#snake), and [Tetris](/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#tetris).
### Data-Binding
[Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) supports both bidirectional (two-way) data-binding and unidirectional (one-way) data-binding.
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.
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.
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).
![MVP](https://upload.wikimedia.org/wikipedia/commons/d/dc/Model_View_Presenter_GUI_Design_Pattern.png)
#### Bidirectional (Two-Way) Data-Binding
[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):
- `checkbox`: `checked`
- `check_menu_item`: `checked`
- `color_button`: `color`
- `combobox`: `selected`, `selected_item`
- `date_picker`: `time`
- `date_time_picker`: `time`
- `editable_combobox`: `text`
- `entry`: `text`
- `font_button`: `font`
- `multiline_entry`: `text`
- `non_wrapping_multiline_entry`: `text`
- `radio_buttons`: `selected`
- `radio_menu_item`: `checked`
- `search_entry`: `text`
- `slider`: `value`
- `spinbox`: `value`
- `table`: `cell_rows`, `selection`
- `table` columns (e.g. `text_column`): `sort_indicator`
- `time_picker`: `time`
Example of bidirectional data-binding:
```ruby
entry {
text <=> [contract, :legal_text]
}
```
That is data-binding a contract's legal text to an `entry` `text` property.
Another example of bidirectional data-binding with an option:
```ruby
entry {
text <=> [self, :entered_text, after_write: ->(text) {puts text}]
}
```
That is data-binding `entered_text` attribute on `self` to `entry` `text` property and printing text after write to the model.
##### Table Data-Binding
One note about `table` `cell_rows` data-binding is that it works with either:
- Raw data `Array` (rows) of `Array`s (column cells) (see [Form Table example versions 4-5](https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#form-table)).
- Model `Array` (rows) of objects having attributes (column cells) matching the underscored names of `table` columns by convention. Model attribute names can be overridden when needed by passing an `Array` enumerating all mapped model attributes in the order of `table` columns or alternatively a `Hash` mapping only the column names that have model attribute names different from their table column underscored version (see [Form Table example versions 1-3](https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#form-table)).
- Raw data `Enumerator` or `Enumerator::Lazy` that generates `Array`s (column cells). This option enables rendering a table instantly even if it contained millions of rows, before all its rows have been generated. Rows are gradually generated as the user scrolls through the table.
- Model `Enumerator` or `Enumerator::Lazy` that generates objects having attributes (column cells) matching the underscored names of `table` columns. This option enables rendering a table instantly even if it contained millions of rows, before all its rows have been generated. Rows are gradually generated as the user scrolls through the table (see [Lazy Table](https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#lazy-table) example).
Example of `table` implicit data-binding of `cell_rows` to raw data `Array` of `Array`s (you may copy/paste in [`girb`](#girb-glimmer-irb)):
```ruby
require 'glimmer-dsl-libui'
include Glimmer
data = [
['Lisa Sky', 'lisa@sky.com', '720-523-4329', 'Denver', 'CO'],
['Jordan Biggins', 'jordan@biggins.com', '617-528-5399', 'Boston', 'MA'],
['Mary Glass', 'mary@glass.com', '847-589-8788', 'Elk Grove Village', 'IL'],
['Darren McGrath', 'darren@mcgrath.com', '206-539-9283', 'Seattle', 'WA'],
['Melody Hanheimer', 'melody@hanheimer.com', '213-493-8274', 'Los Angeles', 'CA'],
]
window('Contacts', 600, 600) {
table {
text_column('Name')
text_column('Email')
text_column('Phone')
text_column('City')
text_column('State')
cell_rows data
}
}.show
```
Example of `table` explicit data-binding of `cell_rows` to Model `Array` (you may copy/paste in [`girb`](#girb-glimmer-irb)):
```ruby
require 'glimmer-dsl-libui'
class SomeTable
Contact = Struct.new(:name, :email, :phone, :city, :state)
include Glimmer
attr_accessor :contacts
def initialize
@contacts = [
Contact.new('Lisa Sky', 'lisa@sky.com', '720-523-4329', 'Denver', 'CO'),
Contact.new('Jordan Biggins', 'jordan@biggins.com', '617-528-5399', 'Boston', 'MA'),
Contact.new('Mary Glass', 'mary@glass.com', '847-589-8788', 'Elk Grove Village', 'IL'),
Contact.new('Darren McGrath', 'darren@mcgrath.com', '206-539-9283', 'Seattle', 'WA'),
Contact.new('Melody Hanheimer', 'melody@hanheimer.com', '213-493-8274', 'Los Angeles', 'CA'),
]
end
def launch
window('Contacts', 600, 200) {
table {
text_column('Name')
text_column('Email')
text_column('Phone')
text_column('City')
text_column('State')
cell_rows <=> [self, :contacts] # explicit data-binding to self.contacts Model Array, auto-inferring model attribute names from underscored table column names by convention
}
}.show
end
end
SomeTable.new.launch
```
Example of `table` explicit data-binding of `cell_rows` to Model `Array` with `column_attributes` `Hash` mapping for custom column names (you may copy/paste in [`girb`](#girb-glimmer-irb)):
```ruby
require 'glimmer-dsl-libui'
class SomeTable
Contact = Struct.new(:name, :email, :phone, :city, :state)
include Glimmer
attr_accessor :contacts
def initialize
@contacts = [
Contact.new('Lisa Sky', 'lisa@sky.com', '720-523-4329', 'Denver', 'CO'),
Contact.new('Jordan Biggins', 'jordan@biggins.com', '617-528-5399', 'Boston', 'MA'),
Contact.new('Mary Glass', 'mary@glass.com', '847-589-8788', 'Elk Grove Village', 'IL'),
Contact.new('Darren McGrath', 'darren@mcgrath.com', '206-539-9283', 'Seattle', 'WA'),
Contact.new('Melody Hanheimer', 'melody@hanheimer.com', '213-493-8274', 'Los Angeles', 'CA'),
]
end
def launch
window('Contacts', 600, 200) {
table {
text_column('Name')
text_column('Email')
text_column('Phone')
text_column('City/Town')
text_column('State/Province')
cell_rows <=> [self, :contacts, column_attributes: {'City/Town' => :city, 'State/Province' => :state}]
}
}.show
end
end
SomeTable.new.launch
```
Example of `table` explicit data-binding of `cell_rows` to Model `Array` with complete `column_attributes` `Array` mapping (you may copy/paste in [`girb`](#girb-glimmer-irb)):
```ruby
require 'glimmer-dsl-libui'
class SomeTable
Contact = Struct.new(:name, :email, :phone, :city, :state)
include Glimmer
attr_accessor :contacts
def initialize
@contacts = [
Contact.new('Lisa Sky', 'lisa@sky.com', '720-523-4329', 'Denver', 'CO'),
Contact.new('Jordan Biggins', 'jordan@biggins.com', '617-528-5399', 'Boston', 'MA'),
Contact.new('Mary Glass', 'mary@glass.com', '847-589-8788', 'Elk Grove Village', 'IL'),
Contact.new('Darren McGrath', 'darren@mcgrath.com', '206-539-9283', 'Seattle', 'WA'),
Contact.new('Melody Hanheimer', 'melody@hanheimer.com', '213-493-8274', 'Los Angeles', 'CA'),
]
end
def launch
window('Contacts', 600, 200) {
table {
text_column('Full Name')
text_column('Email Address')
text_column('Phone Number')
text_column('City or Town')
text_column('State or Province')
cell_rows <=> [self, :contacts, column_attributes: [:name, :email, :phone, :city, :state]]
}
}.show
end
end
SomeTable.new.launch
```
#### Unidirectional (One-Way) Data-Binding
[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).
Example of unidirectional data-binding:
```ruby
square(0, 0, CELL_SIZE) {
fill <= [@grid.cells[row][column], :color]
}
```
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.
Another Example of unidirectional data-binding with an option:
```ruby
window {
title <= [@game, :score, on_read: -> (score) {"Glimmer Snake (Score: #{@game.score})"}]
}
```
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`.
#### Data-Binding API
To summarize the data-binding API:
- `view_property <=> [model, attribute, *read_or_write_options]`: Bidirectional (two-way) data-binding to Model attribute accessor
- `view_property <= [model, attribute, *read_only_options]`: Unidirectional (one-way) data-binding to Model attribute reader
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.
Data-bound model attribute can be:
- **Direct:** `Symbol` representing attribute reader/writer (e.g. `[person, :name`])
- **Nested:** `String` representing nested attribute path (e.g. `[company, 'address.street']`). That results in "nested data-binding"
- **Indexed:** `String` containing array attribute index (e.g. `[customer, 'addresses[0].street']`). That results in "indexed data-binding"
- **Keyed:** `String` containing hash attribute key (e.g. `[customer, 'addresses[:main].street']`). That results in "keyed data-binding"
Data-binding options include:
- `before_read {|value| ...}`: performs an operation before reading data from Model to update View.
- `on_read {|value| ...}`: converts value read from Model to update the View.
- `after_read {|converted_value| ...}`: performs an operation after read from Model to update View.
- `before_write {|value| ...}`: performs an operation before writing data to Model from View.
- `on_write {|value| ...}`: converts value read from View to update the Model.
- `after_write {|converted_value| ...}`: performs an operation after writing to Model from View.
- `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)). That is known as "computed data-binding".
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.
Example:
```ruby
entry {
text <=> [product, :price, on_read: :to_s, on_write: :to_i]
}
```
Learn more from data-binding usage in [Login](/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#login) (4 data-binding versions), [Basic Entry](/docs/examples/GLIMMER-DSL-LIBUI-BASIC-EXAMPLES.md#basic-entry), [Form](/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#form), [Form Table](/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#form-table) (5 data-binding versions), [Method-Based Custom Keyword](/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#method-based-custom-keyword), [Snake](/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#snake) and [Tic Tac Toe](/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md#tic_tac_toe) examples.
#### Data-Binding Gotchas
- 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)
- Data-binding a property utilizes the control's listener associated with the property (e.g. `on_changed` for `entry` `text`), so although you can add another listener if you want ([Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) will happily add your listener to the list of listeners that will get notified by a certain event), sometimes it is recommended that you add an `after_read: ->(val) {}` or `after_write: ->(val) {}` block instead to perform something after data-binding reads from or writes to the Model attribute.
- Data-binding a View control to another View control directly is not a good practice as it causes tight-coupling. Instead, data-bind both View controls to the same Presenter/Model attribute, and that keeps them in sync while keeping the code decoupled.
### API Gotchas
- There is no proper way to destroy `grid` children due to [libui-ng](https://github.com/libui-ng/libui-ng) not offering any API for deleting them from `grid` (no `grid_delete` similar to `box_delete` for `horizontal_box` and `vertical_box`).
- `text` `align` property seems not to work on the Mac ([libui-ng](https://github.com/libui-ng/libui-ng) has an [issue](https://github.com/andlabs/libui/pull/407) about it)
- `text` `string` `background` does not work on Windows due to an [issue in libui](https://github.com/andlabs/libui/issues/347).
- `table` `progress_bar` column on Windows cannot be updated with a positive value if it started initially with `-1` (it ignores update to avoid crashing due to an issue in [libui](https://github.com/andlabs/libui) on Windows.
- `radio_buttons` on Linux has an issue where it always selects the first item even if you did not set its `selected` value or set it to `-1` (meaning unselected). It works correctly on Mac and Windows.
- It seems that [libui](https://github.com/andlabs/libui) does not support nesting multiple `area` controls under a `grid` as only the first one shows up in that scenario. To workaround that limitation, use a `vertical_box` with nested `horizontal_box`s instead to include multiple `area`s in a GUI.
- As per the code of [examples/basic_transform.rb](/docs/examples/GLIMMER-DSL-LIBUI-BASIC-EXAMPLES.md#basic-transform), Windows requires different ordering of transforms than Mac and Linux.
- `scrolling_area#scroll_to` does not seem to work on Windows and Linux, but works fine on Mac
### Original API
Here are all the lower-level [LibUI](https://github.com/kojix2/LibUI) API methods utilized by [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui):
`alloc_control`, `append_features`, `area_begin_user_window_move`, `area_begin_user_window_resize`, `area_queue_redraw_all`, `area_scroll_to`, `area_set_size`, `attribute_color`, `attribute_family`, `attribute_features`, `attribute_get_type`, `attribute_italic`, `attribute_size`, `attribute_stretch`, `attribute_underline`, `attribute_underline_color`, `attribute_weight`, `attributed_string_append_unattributed`, `attributed_string_byte_index_to_grapheme`, `attributed_string_delete`, `attributed_string_for_each_attribute`, `attributed_string_grapheme_to_byte_index`, `attributed_string_insert_at_unattributed`, `attributed_string_len`, `attributed_string_num_graphemes`, `attributed_string_set_attribute`, `attributed_string_string`, `box_append`, `box_delete`, `box_padded`, `box_set_padded`, `button_on_clicked`, `button_set_text`, `button_text`, `checkbox_checked`, `checkbox_on_toggled`, `checkbox_set_checked`, `checkbox_set_text`, `checkbox_text`, `color_button_color`, `color_button_on_changed`, `color_button_set_color`, `combobox_append`, `combobox_on_selected`, `combobox_selected`, `combobox_set_selected`, `control_destroy`, `control_disable`, `control_enable`, `control_enabled`, `control_enabled_to_user`, `control_handle`, `control_hide`, `control_parent`, `control_set_parent`, `control_show`, `control_toplevel`, `control_verify_set_parent`, `control_visible`, `date_time_picker_on_changed`, `date_time_picker_set_time`, `date_time_picker_time`, `draw_clip`, `draw_fill`, `draw_free_path`, `draw_free_text_layout`, `draw_matrix_invert`, `draw_matrix_invertible`, `draw_matrix_multiply`, `draw_matrix_rotate`, `draw_matrix_scale`, `draw_matrix_set_identity`, `draw_matrix_skew`, `draw_matrix_transform_point`, `draw_matrix_transform_size`, `draw_matrix_translate`, `draw_new_path`, `draw_new_text_layout`, `draw_path_add_rectangle`, `draw_path_arc_to`, `draw_path_bezier_to`, `draw_path_close_figure`, `draw_path_end`, `draw_path_line_to`, `draw_path_new_figure`, `draw_path_new_figure_with_arc`, `draw_restore`, `draw_save`, `draw_stroke`, `draw_text`, `draw_text_layout_extents`, `draw_transform`, `editable_combobox_append`, `editable_combobox_on_changed`, `editable_combobox_set_text`, `editable_combobox_text`, `entry_on_changed`, `entry_read_only`, `entry_set_read_only`, `entry_set_text`, `entry_text`, `ffi_lib`, `ffi_lib=`, `font_button_font`, `font_button_on_changed`, `form_append`, `form_delete`, `form_padded`, `form_set_padded`, `free_attribute`, `free_attributed_string`, `free_control`, `free_font_button_font`, `free_image`, `free_init_error`, `free_open_type_features`, `free_table_model`, `free_table_value`, `free_text`, `grid_append`, `grid_insert_at`, `grid_padded`, `grid_set_padded`, `group_margined`, `group_set_child`, `group_set_margined`, `group_set_title`, `group_title`, `image_append`, `init`, `label_set_text`, `label_text`, `main`, `main_step`, `main_steps`, `menu_append_about_item`, `menu_append_check_item`, `menu_append_item`, `menu_append_preferences_item`, `menu_append_quit_item`, `menu_append_separator`, `menu_item_checked`, `menu_item_disable`, `menu_item_enable`, `menu_item_on_clicked`, `menu_item_set_checked`, `msg_box`, `msg_box_error`, `multiline_entry_append`, `multiline_entry_on_changed`, `multiline_entry_read_only`, `multiline_entry_set_read_only`, `multiline_entry_set_text`, `multiline_entry_text`, `new_area`, `new_attributed_string`, `new_background_attribute`, `new_button`, `new_checkbox`, `new_color_attribute`, `new_color_button`, `new_combobox`, `new_date_picker`, `new_date_time_picker`, `new_editable_combobox`, `new_entry`, `new_family_attribute`, `new_features_attribute`, `new_font_button`, `new_form`, `new_grid`, `new_group`, `new_horizontal_box`, `new_horizontal_separator`, `new_image`, `new_italic_attribute`, `new_label`, `new_menu`, `new_multiline_entry`, `new_non_wrapping_multiline_entry`, `new_open_type_features`, `new_password_entry`, `new_progress_bar`, `new_radio_buttons`, `new_scrolling_area`, `new_search_entry`, `new_size_attribute`, `new_slider`, `new_spinbox`, `new_stretch_attribute`, `new_tab`, `new_table`, `new_table_model`, `new_table_value_color`, `new_table_value_image`, `new_table_value_int`, `new_table_value_string`, `new_time_picker`, `new_underline_attribute`, `new_underline_color_attribute`, `new_vertical_box`, `new_vertical_separator`, `new_weight_attribute`, `new_window`, `on_should_quit`, `open_file`, `open_folder`, `open_type_features_add`, `open_type_features_clone`, `open_type_features_for_each`, `open_type_features_get`, `open_type_features_remove`, `progress_bar_set_value`, `progress_bar_value`, `queue_main`, `quit`, `radio_buttons_append`, `radio_buttons_on_selected`, `radio_buttons_selected`, `radio_buttons_set_selected`, `save_file`, `slider_on_changed`, `slider_set_value`, `slider_value`, `spinbox_on_changed`, `spinbox_set_value`, `spinbox_value`, `tab_append`, `tab_delete`, `tab_insert_at`, `tab_margined`, `tab_num_pages`, `tab_set_margined`, `table_append_button_column`, `table_append_checkbox_column`, `table_append_checkbox_text_column`, `table_append_image_column`, `table_append_image_text_column`, `table_append_progress_bar_column`, `table_append_text_column`, `table_model_row_changed`, `table_model_row_deleted`, `table_model_row_inserted`, `table_value_color`, `table_value_get_type`, `table_value_image`, `table_value_int`, `table_value_string`, `timer`, `uninit`, `user_bug_cannot_set_parent_on_toplevel`, `window_borderless`, `window_content_size`, `window_focused`, `window_fullscreen`, `window_margined`, `window_on_closing`, `window_on_content_size_changed`, `window_on_focus_changed`, `window_set_borderless`, `window_set_child`, `window_set_content_size`, `window_set_fullscreen`, `window_set_margined`, `window_set_title`, `window_title`
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):
- Check out [LibUI ffi.rb](https://github.com/kojix2/LibUI/blob/main/lib/libui/ffi.rb)
- Check out the [libui C Headers](https://github.com/andlabs/libui/blob/master/ui.h)
- Check out the [Go UI (Golang LibUI) API Documentation](https://pkg.go.dev/github.com/andlabs/ui) for an alternative well-documented [libui](https://github.com/andlabs/libui) reference.
## Packaging
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.
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.
For Mac, consider [Platypus](https://github.com/sveinbjornt/Platypus) (builds a native Mac app from a Ruby script)
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/).
Also, there is a promising project called [ruby-packer](https://github.com/pmq20/ruby-packer) that supports all platforms.
## Glimmer Style Guide
**1 - Control arguments are always wrapped by parentheses.**
Example:
```ruby
label('Name')
```
**2 - Control blocks are always declared with curly braces to clearly visualize hierarchical view code and separate from logic code.**
Example:
```ruby
group('Basic Controls') {
vertical_box {
button('Button') {
}
}
}
```
**3 - Control property declarations always have arguments that are not wrapped inside parentheses and typically do not take a block.**
Example:
```ruby
stretchy false
value 42
```
**4 - Control listeners are always declared starting with on_ prefix and affixing listener event method name afterwards in underscored lowercase form. Their multi-line blocks have a `do; end` style.**
Example:
```ruby
button('Click') {
on_clicked do
msg_box('Information', 'You clicked the button')
end
}
```
**5 - Iterator multi-line blocks always have `do; end` style to clearly separate logic code from view code.**
Example:
```ruby
@field_hash.keys.each do |field|
label(field) {
stretchy false
}
entry {
on_changed do |control|
@field_hash[field] = control.text
end
}
end
```
**6 - In a widget's content block, attributes are declared first, with layout management attributes on top (e.g. `stretchy false`); an empty line separates attributes from nested widgets and listeners following afterwards.**
Example:
```ruby
group('Numbers') {
stretchy false
vertical_box {
spinbox(0, 100) {
stretchy false
value 42
on_changed do |s|
puts "New Spinbox value: #{s.value}"
$stdout.flush # for Windows
end
}
}
}
```
**7 - Unlike attributes, nested widgets with a content block and listeners are always separated from each other by an empty line to make readability easier except where it helps to group two widgets together (e.g. label and described entry).**
Example:
```ruby
area {
path { # needs an empty line afterwards
square(0, 0, 100) # does not have a content block, so no empty line is needed
square(100, 100, 400) # does not have a content block, so no empty line is needed
fill r: 102, g: 102, b: 204
}
path { # needs an empty line afterwards
rectangle(0, 100, 100, 400) # does not have a content block, so no empty line is needed
rectangle(100, 0, 400, 100) # does not have a content block, so no empty line is needed
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}]
}
polygon(100, 100, 100, 400, 400, 100, 400, 400) { # needs an empty line afterwards
fill r: 202, g: 102, b: 104, a: 0.5 # attributes do not need an empty line separator
stroke r: 0, g: 0, b: 0 # attributes do not need an empty line separator
}
on_mouse_up do |area_mouse_event| # needs an empty line afterwards
puts 'mouse up'
end
on_key_up do |area_key_event| # needs an empty line afterwards
puts 'key up'
end
}
```
## Examples
The following [basic](/docs/examples/GLIMMER-DSL-LIBUI-BASIC-EXAMPLES.md) and [advanced](/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md) 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](#data-binding)) as well as brand new examples.
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.
[examples/meta_example.rb](examples/meta_example.rb)
Run with this command from the root of the project if you cloned the project:
```
ruby -r './lib/glimmer-dsl-libui' examples/meta_example.rb
```
Run with this command if you installed the [Ruby gem](https://rubygems.org/gems/glimmer-dsl-libui):
```
ruby -r glimmer-dsl-libui -e "require 'examples/meta_example'"
```
Mac | Windows | Linux
----|---------|------
![glimmer-dsl-libui-mac-meta-example.png](images/glimmer-dsl-libui-mac-meta-example.png) | ![glimmer-dsl-libui-windows-meta-example.png](images/glimmer-dsl-libui-windows-meta-example.png) | ![glimmer-dsl-libui-linux-meta-example.png](images/glimmer-dsl-libui-linux-meta-example.png)
New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version:
```ruby
require 'glimmer-dsl-libui'
require 'facets'
require 'fileutils'
class MetaExample
include Glimmer
ADDITIONAL_BASIC_EXAMPLES = ['Color Button', 'Font Button', 'Form', 'Date Time Picker', 'Simple Notepad']
attr_accessor :code_text
def initialize
@selected_example_index = examples_with_versions.index(basic_examples_with_versions.first)
@code_text = File.read(file_path_for(selected_example))
end
def examples
if @examples.nil?
example_files = Dir.glob(File.join(File.expand_path('.', __dir__), '*.rb'))
example_file_names = example_files.map { |f| File.basename(f, '.rb') }
example_file_names = example_file_names.reject { |f| f == 'meta_example' || f.match(/\d$/) }
@examples = example_file_names.map { |f| f.underscore.titlecase }
end
@examples
end
def examples_with_versions
examples.map do |example|
version_count_for(example) > 1 ? "#{example} (#{version_count_for(example)} versions)" : example
end
end
def basic_examples_with_versions
examples_with_versions.select {|example| example.start_with?('Basic') || ADDITIONAL_BASIC_EXAMPLES.include?(example) }
end
def advanced_examples_with_versions
examples_with_versions - basic_examples_with_versions
end
def file_path_for(example)
File.join(File.expand_path('.', __dir__), "#{example.underscore}.rb")
end
def version_count_for(example)
Dir.glob(File.join(File.expand_path('.', __dir__), "#{example.underscore}*.rb")).select {|file| file.match(/#{example.underscore}\d\.rb$/)}.count + 1
end
def glimmer_dsl_libui_file
File.expand_path('../lib/glimmer-dsl-libui', __dir__)
end
def selected_example
examples[@selected_example_index]
end
def run_example(example)
Thread.new do
command = "#{RbConfig.ruby} -r #{glimmer_dsl_libui_file} #{example} 2>&1"
result = ''
IO.popen(command) do |f|
sleep(0.0001) # yield to main thread
f.each_line do |line|
result << line
puts line
$stdout.flush # for Windows
sleep(0.0001) # yield to main thread
end
end
Glimmer::LibUI.queue_main { msg_box('Error Running Example', result) } if result.downcase.include?('error')
end
end
def launch
window('Meta-Example', 700, 500) {
margined true
horizontal_box {
vertical_box {
stretchy false
tab {
stretchy false
tab_item('Basic') {
vertical_box {
@basic_example_radio_buttons = radio_buttons {
stretchy false
items basic_examples_with_versions
selected basic_examples_with_versions.index(examples_with_versions[@selected_example_index])
on_selected do
@selected_example_index = examples_with_versions.index(basic_examples_with_versions[@basic_example_radio_buttons.selected])
example = selected_example
self.code_text = File.read(file_path_for(example))
@version_spinbox.value = 1
end
}
label # filler
label # filler
}
}
tab_item('Advanced') {
vertical_box {
@advanced_example_radio_buttons = radio_buttons {
stretchy false
items advanced_examples_with_versions
on_selected do
@selected_example_index = examples_with_versions.index(advanced_examples_with_versions[@advanced_example_radio_buttons.selected])
example = selected_example
self.code_text = File.read(file_path_for(example))
@version_spinbox.value = 1
end
}
label # filler
label # filler
}
}
}
horizontal_box {
label('Version') {
stretchy false
}
@version_spinbox = spinbox(1, 100) {
value 1
on_changed do
example = selected_example
if @version_spinbox.value > version_count_for(example)
@version_spinbox.value -= 1
else
version_number = @version_spinbox.value == 1 ? '' : @version_spinbox.value
example = "#{selected_example}#{version_number}"
self.code_text = File.read(file_path_for(example))
end
end
}
}
horizontal_box {
stretchy false
button('Launch') {
on_clicked do
begin
parent_dir = File.join(Dir.home, '.glimmer-dsl-libui', 'examples')
FileUtils.mkdir_p(parent_dir)
example_file = File.join(parent_dir, "#{selected_example.underscore}.rb")
File.write(example_file, code_text)
example_supporting_directory = File.expand_path(selected_example.underscore, __dir__)
FileUtils.cp_r(example_supporting_directory, parent_dir) if Dir.exist?(example_supporting_directory)
FileUtils.cp_r(File.expand_path('../icons', __dir__), File.dirname(parent_dir))
FileUtils.cp_r(File.expand_path('../sounds', __dir__), File.dirname(parent_dir))
run_example(example_file)
rescue => e
puts e.full_message
puts 'Unable to write code changes! Running original example...'
run_example(file_path_for(selected_example))
end
end
}
button('Reset') {
on_clicked do
self.code_text = File.read(file_path_for(selected_example))
end
}
}
}
@code_entry = non_wrapping_multiline_entry {
text <=> [self, :code_text]
}
}
}.show
end
end
MetaExample.new.launch
```
### Basic Examples
[docs/examples/GLIMMER-DSL-LIBUI-BASIC-EXAMPLES.md](/docs/examples/GLIMMER-DSL-LIBUI-BASIC-EXAMPLES.md)
Mac | Windows | Linux
----|---------|------
![glimmer-dsl-libui-mac-basic-window.png](/images/glimmer-dsl-libui-mac-basic-window.png) | ![glimmer-dsl-libui-windows-basic-window.png](/images/glimmer-dsl-libui-windows-basic-window.png) | ![glimmer-dsl-libui-linux-basic-window.png](/images/glimmer-dsl-libui-linux-basic-window.png)
### Advanced Examples
[docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md](/docs/examples/GLIMMER-DSL-LIBUI-ADVANCED-EXAMPLES.md)
Mac | Windows | Linux
----|---------|------
![glimmer-dsl-libui-mac-form-table.png](/images/glimmer-dsl-libui-mac-form-table.png) | ![glimmer-dsl-libui-windows-form-table.png](/images/glimmer-dsl-libui-windows-form-table.png) | ![glimmer-dsl-libui-linux-form-table.png](/images/glimmer-dsl-libui-linux-form-table.png)
## Applications
Here are some applications built with [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui)
### Manga2PDF
Download and merge manga images into a single pdf file.
https://github.com/PinGunter/manga2pdf
![manga2pdf screenshot](https://raw.githubusercontent.com/PinGunter/manga2pdf/master/screenshots/manga2pdf-gui.png)
### Befunge98 GUI
Ruby implementation of the Befunge-98 programmming language.
https://github.com/AndyObtiva/befunge98/tree/gui
![befunge98 gui screenshot](https://raw.githubusercontent.com/AndyObtiva/befunge98/master/gui/glimmer-dsl-libui/befunge98_gui_glimmer_dsl_libui/screenshots/befunge98_gui_glimmer_dsl_libui_example.png)
### i3off Gtk Ruby
https://github.com/iraamaro/i3off-gtk-ruby
### Chess
https://github.com/mikeweber/chess
### RubyCrumbler
NLP (Natural Language Processing) App
https://github.com/joh-ga/RubyCrumbler
![mac_31](https://user-images.githubusercontent.com/72874215/159339948-b7ae1bf2-60c1-4dae-ac1a-4e13a6048ef0.gif)
### Rubio-Radio
https://github.com/kojix2/rubio-radio
![rubio radio screenshot](https://raw.githubusercontent.com/kojix2/rubio-radio/main/screenshots/rubio-radio-linux.png)
### PMV Calc
https://github.com/bzanchet/pmv-calc
![PMV Calc](https://raw.githubusercontent.com/AndyObtiva/pmv-calc/screenshot/screenshots/PMV-Calc.png)
### Suika Box
https://github.com/kojix2/suikabox
![suika box screenshot](https://raw.githubusercontent.com/AndyObtiva/suikabox/main/screenshot.png)
### HTS Grid
https://github.com/kojix2/htsgrid
![hts grid screenshot](https://raw.githubusercontent.com/AndyObtiva/htsgrid/main/screenshot-00.png)
## Process
[Glimmer Process](https://github.com/AndyObtiva/glimmer/blob/master/PROCESS.md)
## Resources
- [Code Master Blog](https://andymaleh.blogspot.com/search/label/LibUI)
- [LibUI Ruby Bindings](https://github.com/kojix2/LibUI)
- [libui C Library](https://github.com/andlabs/libui)
- [libui-ng C Library (Newer Maintained Fork)](https://github.com/libui-ng/libui-ng)
- [Go UI (Golang LibUI) API Documentation](https://pkg.go.dev/github.com/andlabs/ui)
## Help
### Issues
If you encounter [issues](https://github.com/AndyObtiva/glimmer-dsl-libui/issues) that are not reported, discover missing features that are not mentioned in [TODO.md](TODO.md), or think up better ways to use [libui-ng](https://github.com/libui-ng/libui-ng) than what is possible with [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui), you may submit an [issue](https://github.com/AndyObtiva/glimmer-dsl-libui/issues/new) or [pull request](https://github.com/AndyObtiva/glimmer-dsl-libui/compare) on [GitHub](https://github.com). In the meantime, you may try older gem versions of [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) till you find one that works.
### Chat
If you need live help, try to [![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)
## Planned Features and Feature Suggestions
These features have been planned or suggested. You might see them in a future version of [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui). You are welcome to contribute more feature suggestions.
[TODO.md](TODO.md)
## Change Log
[CHANGELOG.md](CHANGELOG.md)
## Contributing
If you would like to contribute to the project, please adhere to the [Open-Source Etiquette](https://github.com/AndyObtiva/open-source-etiquette) to ensure the best results.
- Check out the latest master to make sure the feature hasn't been
implemented or the bug hasn't been fixed yet.
- Check out the issue tracker to make sure someone already hasn't
requested it and/or contributed it.
- Fork the project.
- Start a feature/bugfix branch.
- Commit and push until you are happy with your contribution.
- Make sure to add tests for it. This is important so I don't break it
in a future version unintentionally.
- Please try not to mess with the Rakefile, version, or history. If
you want to have your own version, or is otherwise necessary, that
is fine, but please isolate to its own commit so I can cherry-pick
around it.
Note that the latest development sometimes takes place in the [development](https://github.com/AndyObtiva/glimmer-dsl-libui/tree/development) branch (usually deleted once merged back to [master](https://github.com/AndyObtiva/glimmer-dsl-libui)).
## Contributors
* [Andy Maleh](https://github.com/AndyObtiva) (Founder)
[Click here to view contributor commits.](https://github.com/AndyObtiva/glimmer-dsl-libui/graphs/contributors)
## License
[MIT](LICENSE.txt)
Copyright (c) 2021-2023 Andy Maleh
--
[](https://github.com/AndyObtiva/glimmer) Built for [Glimmer](https://github.com/AndyObtiva/glimmer) (DSL Framework).