Glimmer DSL for LibUI 0.2.4

Prerequisite-Free Ruby Desktop Development GUI Library

Gem Version Maintainability Join the chat at https://gitter.im/AndyObtiva/glimmer

Glimmer DSL for LibUI is a prerequisite-free Ruby desktop development GUI library. No need to pre-install any prerequisites. Just install the gem and have platform-independent native GUI that just works!

LibUI is a thin Ruby wrapper around libui, a relatively new C GUI library that renders native controls on every platform (similar to SWT, but without the heavy weight of the Java Virtual Machine).

The main trade-off in using Glimmer DSL for LibUI as opposed to Glimmer DSL for SWT or Glimmer DSL for Tk is the fact that SWT and Tk are more mature than mid-alpha libui as GUI toolkits. Still, if there is only a need to build a small simple application, Glimmer DSL for LibUI could be a good convenient choice due to having zero prerequisites beyond the dependencies included in the Ruby gem. Also, just like Glimmer DSL for Tk, its apps start instantly and have a small memory footprint. LibUI is a promising new GUI toolkit that might prove quite worthy in the future.

Glimmer DSL for LibUI aims to provide a DSL similar to the Glimmer DSL for SWT to enable more productive desktop development in Ruby with:

  • Declarative DSL syntax that visually maps to the GUI control hierarchy
  • Convention over configuration via smart defaults and automation of low-level details
  • Requiring the least amount of syntax possible to build GUI
  • Bidirectional Data-Binding to declaratively wire and automatically synchronize GUI with Business Models
  • Custom Control support
  • Scaffolding for new custom controls, apps, and gems
  • Native-Executable packaging on Mac, Windows, and Linux.

Hello, World!

require 'glimmer-dsl-libui'

include Glimmer

window('hello world').show

glimmer-dsl-libui-mac-basic-window.png glimmer-dsl-libui-linux-basic-window.png

Basic Table Progress Bar

require 'glimmer-dsl-libui'

include Glimmer

data = [
  ['task 1', 0],
  ['task 2', 15],
  ['task 3', 100],
  ['task 4', 75],
  ['task 5', -1],
]

window('Task Progress', 300, 200) {
  vertical_box {
    table {
      text_column('Task')
      progress_bar_column('Progress')

      cell_rows data # implicit data-binding
    }

    button('Mark All As Done') {
      stretchy false

      on_clicked do
        data.each_with_index do |row_data, row|
          data[row] = [row_data[0], 100] # automatically updates table due to implicit data-binding
        end
      end
    }
  }
}.show

glimmer-dsl-libui-mac-basic-table-progress-bar.png glimmer-dsl-libui-linux-basic-table-progress-bar.png

Area Gallery

require 'glimmer-dsl-libui'

include Glimmer

window('Area Gallery', 400, 400) {
  area {
    path { # declarative stable path
      square(0, 0, 100)
      square(100, 100, 400)

      fill r: 102, g: 102, b: 204
    }
    path { # declarative stable path
      rectangle(0, 100, 100, 400)
      rectangle(100, 0, 400, 100)

      fill r: 204, g: 102, b: 204
    }
    path { # declarative stable path
      figure(100, 100) {
        line(100, 400)
        line(400, 100)
        line(400, 400)

        closed true
      }

      fill r: 202, g: 102, b: 104, a: 0.5
      stroke r: 0, g: 0, b: 0
    }
    path { # declarative stable path
      figure(0, 0) {
        bezier(200, 100, 100, 200, 400, 100)
        bezier(300, 100, 100, 300, 100, 400)
        bezier(100, 300, 300, 100, 400, 400)

        closed true
      }

      fill r: 202, g: 102, b: 204, a: 0.5
      stroke r: 0, g: 0, b: 0, thickness: 2, dashes: [50, 10, 10, 10], dash_phase: -50.0
    }
    path { # declarative stable path
      circle(200, 200, 90)

      fill r: 202, g: 102, b: 204, a: 0.5
      stroke r: 0, g: 0, b: 0, thickness: 2
    }
    path { # declarative stable path
      arc(400, 220, 180, 90, 90, false)

      fill r: 204, g: 102, b: 204, a: 0.5
      stroke r: 0, g: 0, b: 0, thickness: 2, dashes: [50, 10, 10, 10], dash_phase: -50.0
    }

    on_mouse_event do |area_mouse_event|
      p area_mouse_event
    end

    on_mouse_moved do |area_mouse_event|
      puts 'moved'
    end

    on_mouse_down do |area_mouse_event|
      puts 'mouse down'
    end

    on_mouse_up do |area_mouse_event|
      puts 'mouse up'
    end

    on_mouse_drag_started do |area_mouse_event|
      puts 'drag started'
    end

    on_mouse_dragged do |area_mouse_event|
      puts 'dragged'
    end

    on_mouse_dropped do |area_mouse_event|
      puts 'dropped'
    end

    on_mouse_entered do
      puts 'entered'
    end

    on_mouse_exited do
      puts 'exited'
    end

    on_key_event do |area_key_event|
      p area_key_event
    end

    on_key_up do |area_key_event|
      puts 'key up'
    end

    on_key_down do |area_key_event|
      puts 'key down'
    end
  }
}.show

glimmer-dsl-libui-mac-area-gallery.png glimmer-dsl-libui-linux-area-gallery.png

Check Out Many More Examples Over Here!

NOTE: Glimmer DSL for LibUI is in early alpha mode (only supports included examples). Please help make better by contributing, adopting for small or low risk projects, and providing feedback. It is still an early alpha, so the more feedback and issues you report the better.

Other Glimmer DSL gems you might be interested in:

Table of Contents

Glimmer GUI DSL Concepts

The Glimmer GUI DSL provides object-oriented declarative hierarchical syntax for LibUI that:

  • Supports smart defaults (e.g. automatic on_closing listener that quits window)
  • Automates wiring of controls (e.g. button is automatically set as child of window)
  • Hides lower-level details (e.g. LibUI.main loop is started automatically when triggering show on window)
  • Nests controls according to their visual hierarchy
  • Requires the minimum amount of syntax needed to describe an app's GUI

The Glimmer GUI DSL follows these simple concepts in mapping from LibUI syntax:

  • Control: LibUI controls may be declared by lower-case underscored name (aka keyword) (e.g. window or button). Behind the scenes, they are represented by keyword methods that map to corresponding LibUI.new_keyword methods receiving args (e.g. window('hello world', 300, 200, true)).
  • Content/Properties/Listeners Block: Any keyword may be optionally followed by a Ruby curly-brace multi-line-block containing nested controls (content) and/or properties (attributes) (e.g. window('hello world', 300, 200, true) {button('greet')}). It optionally receives one arg representing the control (e.g. button('greet') {|b| on_clicked { puts b.text}})
  • Property: Control properties may be declared inside keyword blocks with lower-case underscored name followed by property value args (e.g. title "hello world" inside group). Behind the scenes, properties correspond to control_set_property methods.
  • Listener: Control listeners may be declared inside keyword blocks with listener lower-case underscored name beginning with on_ and receiving required block handler (e.g. on_clicked {puts 'clicked'} inside button). Behind the scenes, listeners correspond to control_on_event methods.

Example of an app written in LibUI's procedural imperative syntax:

require 'libui'

UI = LibUI

UI.init

main_window = UI.new_window('hello world', 300, 200, 1)

button = UI.new_button('Button')

UI.button_on_clicked(button) do
  UI.msg_box(main_window, 'Information', 'You clicked the button')
end

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

UI.window_set_child(main_window, button)
UI.control_show(main_window)

UI.main
UI.quit

Example of the same app written in Glimmer object-oriented declarative hierarchical syntax:

require 'glimmer-dsl-libui'

include Glimmer

window('hello world', 300, 200) {
  button('Button') {
    on_clicked do
      msg_box('Information', 'You clicked the button')
    end
  }

  on_closing do
    puts 'Bye Bye'
  end
}.show

Usage

Install glimmer-dsl-libui gem directly:

gem install glimmer-dsl-libui

Or install via Bundler Gemfile:

gem 'glimmer-dsl-libui', '~> 0.2.4'

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.

Example (you may copy/paste in girb):

require 'glimmer-dsl-libui'

class Application
  include Glimmer

  def launch
    window('hello world', 300, 200) {
      button('Button') {
        on_clicked do
          puts 'Button Clicked'
        end
      }
    }.show
  end
end

Application.new.launch

If you are new to Glimmer DSL for LibUI, check out Girb and Examples to quickly learn through copy/paste. You may refer to the API later on once you have gotten your feet wet with Glimmer DSL for LibUI and need more detailed reference information.

Girb (Glimmer IRB)

You can run the girb command (bin/girb if you cloned the project locally) to do some quick and dirty experimentation and learning:

girb

This gives you irb with the glimmer-dsl-libui gem loaded and the Glimmer module mixed into the main object for easy experimentation with GUI.

glimmer-dsl-libui-girb.png

For a more advanced code editing tool, check out the Meta-Example (The Example of Examples).

Gotcha: On the Mac, when you close a window opened in girb, it remains open until you enter exit or open another GUI window.

API

Any control returned by a Glimmer GUI DSL keyword declaration can be introspected for its properties and updated via object-oriented attributes (standard Ruby attr/attr= or set_attr).

Example (you may copy/paste in girb):

w = window('hello world')
puts w.title # => hello world
w.title = 'howdy'
puts w.title # => howdy
w.set_title 'aloha'
puts w.title # => aloha

Controls are wrapped as Ruby proxy objects, having a #libui method to obtain the wrapped Fiddle pointer object. Ruby proxy objects rely on composition (via Proxy Design Pattern) instead of inheritance to shield consumers from having to deal with lower-level details unless absolutely needed.

Example (you may copy/paste in girb):

w = window('hello world') # => #<Glimmer::LibUI::WindowProxy:0x00007fde4ea39fb0
w.libui # => #<Fiddle::Pointer:0x00007fde53997980 ptr=0x00007fde51352a60 size=0 free=0x0000000000000000>

Supported Controls

Control(Args) Properties Listeners
about_menu_item None on_clicked
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)
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
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
button(text as String) text (String) on_clicked
button_column(name as String) enabled (Boolean) None
checkbox(text as String) checked (Boolean), text (String) on_toggled
checkbox_column(name as String) editable (Boolean) None
checkbox_text_column(name as String) editable (Boolean), editable_checkbox (Boolean), editable_text (Boolean) None
combobox items (Array of String), selected (Integer) on_selected
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
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
date_time_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
editable_combobox items (Array of String), text (String) on_changed
entry read_only (Boolean), text (String) on_changed
figure(x=nil as Numeric, y=nil as Numeric) x (Numeric), y (Numeric), closed (Boolean) None
font_button font read-only, family as String, size as Float, weight as Integer, italic as Integer, stretch as Integer on_changed
form padded (Boolean) None
grid padded (Boolean) None
group(text as String) margined (Boolean), title (String) None
horizontal_box padded (Boolean) None
horizontal_separator None None
image(width as Numeric, height as Numeric) None None
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
image_column(name as String) None None
image_text_column(name as String) None None
label(text as String) text (String) None
line(x as Numeric, y as Numeric) x (Numeric), y (Numeric) None
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
menu(text as String) None None
menu_item(text as String) checked (Boolean) on_clicked
multiline_entry read_only (Boolean), text (String) on_changed
msg_box(window = main_window as Glimmer::LibUI::WindowProxy, title as String, description as String) None None
msg_box_error(window = main_window as Glimmer::LibUI::WindowProxy, title as String, description as String) None None
non_wrapping_multiline_entry read_only (Boolean), text (String) on_changed
password_entry read_only (Boolean), text (String) on_changed
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), stroke (Hash of :r as 0-255, :g as 0-255, :b as 0-255, :a as 0.0-1.0, :cap as (:round, :square, :flat), :join as (:miter, :round, :bevel), :thickness as Numeric, :miter_limit as Numeric, :dashes as Array of Numeric ) None
preferences_menu_item None on_clicked
progress_bar value (Numeric) None
progress_bar_column(name as String) None None
quit_menu_item None on_clicked
radio_buttons selected (Integer) on_selected
rectangle(x as Numeric, y as Numeric, width as Numeric, height as Numeric) x (Numeric), y (Numeric), width (Numeric), height (Numeric) None
search_entry read_only (Boolean), text (String) on_changed
slider(min as Numeric, max as Numeric) value (Numeric) on_changed
spinbox(min as Numeric, max as Numeric) value (Numeric) on_changed
square(x as Numeric, y as Numeric, length as Numeric) x (Numeric), y (Numeric), length (Numeric) None
tab margined (Boolean), num_pages (Integer) None
tab_item(name as String) index read-only, margined (Boolean), name read-only None
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 None
text_column(name as String) editable (Boolean) None
time_picker time (Hash of keys: sec as Integer, min as Integer, hour as Integer) on_changed
vertical_box padded (Boolean) None
vertical_separator None None
window(title as String, width as Integer, height as Integer, has_menubar as Boolean) borderless (Boolean), content_size (width Numeric, height Numeric), fullscreen (Boolean), margined (Boolean), title (String) on_closing, on_content_size_changed, on_destroy

Common Control Properties

Common Control Operations

  • destroy
  • disable
  • enable
  • hide
  • show

LibUI Operations

All operations that could normally be called on LibUI can also be called on Glimmer::LibUI, but some have enhancements as detailed below.

  • Glimmer::LibUI::queue_main(&block): queues an operation to be run on the main event loop at the earliest opportunity possible
  • Glimmer::LibUI::timer(time_in_seconds=0.1, repeat: true, &block): calls block after time_in_seconds has elapsed, repeating indefinitely unless repeat is false or an Integer for finite number of repeats. Block can return false or true to override next repetition.

Extra Dialogs

  • open_file(window as Glimmer::LibUI::WindowProxy): returns selected file (String) or nil if cancelled
  • save_file(window as Glimmer::LibUI::WindowProxy): returns selected file (String) or nil if cancelled

Extra Operations

  • ControlProxy::control_proxies: returns all instantiated control proxies in the application
  • ControlProxy::menu_proxies: returns all instantiated menu proxies in the application
  • ControlProxy::image_proxies: returns all instantiated image proxies in the application
  • ControlProxy::main_window_proxy: returns the first window proxy instantiated in the application
  • ControlProxy#window_proxy: returns the window proxy parent for a control
  • ControlProxy#content {...}: re-opens control's content to add more nested controls or properties

Table API

The table control must first declare its columns via one of these column keywords (mentioned in Supported Controls):

  • button_column: expects String cell values
  • checkbox_column: expects Boolean cell values
  • checkbox_text_column: expects dual-element Array of Boolean and String cell values
  • image_column: expects image cell values (produced by image and image_part keywords as per Supported Controls)
  • image_text_column: expects dual-element Array of image and String cell values
  • text_column: expects String cell values
  • progress_bar_column: expects Integer cell values

Afterwards, it must declare its cell_rows array (Array of Arrays of column cell values) and whether it is editable (Boolean) for all its columns.

Note that the cell_rows property declaration results in "implicit data-binding" between the table control and Array of Arrays (a new innovation) to provide convenience automatic support for:

  • Deleting cell rows: Calling Array#delete, Array#delete_at, Array#delete_if, or any filtering/deletion Array method automatically deletes rows in actual table control
  • Inserting cell rows: Calling Array#<<, Array#push, Array#prepend, or any insertion/addition Array method automatically inserts rows in actual table control
  • Changing cell rows: Calling Array#[]=, Array#map!, or any update Array method automatically updates rows in actual table control

Example (you may copy/paste in girb):

require 'glimmer-dsl-libui'

include Glimmer

data = [
  ['Lisa Sky', '[email protected]', '720-523-4329', 'Denver', 'CO', '80014'],
  ['Jordan Biggins', '[email protected]', '617-528-5399', 'Boston', 'MA', '02101'],
  ['Mary Glass', '[email protected]', '847-589-8788', 'Elk Grove Village', 'IL', '60007'],
  ['Darren McGrath', '[email protected]', '206-539-9283', 'Seattle', 'WA', '98101'],
  ['Melody Hanheimer', '[email protected]', '213-493-8274', 'Los Angeles', 'CA', '90001'],
]

window('Contacts', 600, 600) { |w|
  margined true

  vertical_box {
    form {
      stretchy false

      @name_entry = entry {
        label 'Name'
      }
      @email_entry = entry {
        label 'Email'
      }
      @phone_entry = entry {
        label 'Phone'
      }
      @city_entry = entry {
        label 'City'
      }
      @state_entry = entry {
        label 'State'
      }
    }

    button('Save Contact') {
      stretchy false

      on_clicked do
        new_row = [@name_entry.text, @email_entry.text, @phone_entry.text, @city_entry.text, @state_entry.text]
        if new_row.include?('')
          msg_box_error(w, 'Validation Error!', 'All fields are required! Please make sure to enter a value for all fields.')
        else
          data << new_row # automatically inserts a row into the table due to implicit data-binding
          @name_entry.text = ''
          @email_entry.text = ''
          @phone_entry.text = ''
          @city_entry.text = ''
          @state_entry.text = ''
        end
      end
    }

    table {
      text_column('Name')
      text_column('Email')
      text_column('Phone')
      text_column('City')
      text_column('State')

      cell_rows data # implicit data-binding
    }
  }
}.show

glimmer-dsl-libui-linux-form-table.png

Learn more by checking out examples.

Area API

The area control is a canvas-like control for drawing paths that can be used in one of two ways:

  • Declaratively via stable paths: useful for stable paths that will not change later on. Simply nest path and figures like rectangle and all drawing logic is generated automatically. Path proxy objects are preserved across redraws assuming there would be few stable paths (mostly for decorative reasons).
  • Semi-declaratively via on_draw listener dynamic paths: useful for more dynamic paths that will definitely change. Open an on_draw listener block that receives a area_draw_params argument and nest path and figures like rectangle and all drawing logic is generated automatically. Path proxy objects are destroyed (thrown-away) at the end of drawing, thus having less memory overhead for drawing thousands of dynamic paths.

Here is an example of a declarative area with a stable path (you may copy/paste in girb):

require 'glimmer-dsl-libui'

include Glimmer

window('Basic Area', 400, 400) {
  margined true

  vertical_box {
    area {
      path { # a stable path is added declaratively
        rectangle(0, 0, 400, 400)

        fill r: 102, g: 102, b: 204, a: 1.0
      }
    }
  }
}.show

glimmer-dsl-libui-mac-basic-area.png

Here is the same example using a semi-declarative area with on_draw listener that receives a area_draw_params argument and a dynamic path (you may copy/paste in girb):

require 'glimmer-dsl-libui'

include Glimmer

window('Basic Area', 400, 400) {
  margined true

  vertical_box {
    area {
      on_draw do |area_draw_params|
        path { # a dynamic path is added semi-declaratively inside on_draw block
          rectangle(0, 0, 400, 400)

          fill r: 102, g: 102, b: 204, a: 1.0
        }
      end
    }
  }
}.show

Check examples/dynamic_area.rb for a more detailed semi-declarative example.

path can receive a draw_fill_mode argument that can accept values :winding or :alternate and defaults to :winding.

Available nested path shapes:

  • rectangle(x as Numeric, y as Numeric, width as Numeric, height as Numeric)
  • square(x as Numeric, y as Numeric, length as Numeric)
  • arc(x_center as Numeric, y_center as Numeric, radius as Numeric, start_angle as Numeric, sweep as Numeric, is_negative as Boolean)
  • line(x as Numeric, y as Numeric)
  • 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)
  • figure(x=nil as Numeric, y=nil as Numeric) (composite that can contain other shapes) (can set closed true to connect last point to first point automatically)

Check examples/area_gallery.rb for an overiew of all path shapes.

The area_draw_params 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 supported mouse 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 Symbols 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: key 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)

Note that 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.

To redraw an area, you may call the #queue_redraw_all method, or simply #redraw.

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):

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:

transform {
  translate 100, 100
  rotate 100, 100, -9 * n
}

Alternatively:

m1 = matrix {
  translate 100, 100
  rotate 100, 100, -9 * n
}
transform m1
# and then reuse m1 elsewhere 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 paths 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 paths.

fill and stroke accept X11 color Symbols/Strings like :skyblue and 'sandybrown' or 6-number hex or 3-number hex-shorthand (as Integer or String with or without 0x prefix)

Available X11 colors can be obtained through Glimmer::LibUI.x11_colors method.

Check Basic Transform example for use of X11 colors.

Check Histogram example for use of hex colors.

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)
  • 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 the close button (can be overridden with a manual on_closing implementation that returns integer 0 for success)
  • 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 an on_clicked listener was defined on quit_menu_item and it does not return an integer, default exit behavior is assumed (main_window.destroy is called followed by LibUI.quit, returning 0).
  • All boolean property readers return true or false in Ruby instead of the libui 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 Fiddle pointer object.
  • Automatically allocate font descriptors upon instantiating font_button controls and free them when destorying font_button controls
  • Automatically allocate color value pointers upon instantiating color_button controls and free them when destorying 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
  • 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 original 0-based values, and return dst as Boolean instead of isdst as 1/0
  • Smart defaults for grid child attributes 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
  • area paths are specified declaratively with figures underneath (e.g. rectangle) and area draw listener is automatically generated
  • 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
  • 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 color like :skyblue, or 6-number hex or 3-number hex (as Integer or String with or without 0x prefix)
  • Color alpha value defaults to 1.0 when not specified.

API Gotchas

  • There is no proper way to destroy grid children due to libui not offering any API for deleting them from grid (no grid_delete similar to box_delete for horizontal_box and vertical_box).
  • table checkbox_column and checkbox_text_column checkbox editing only works on Windows and Linux (not Mac) due to a current limitation in libui.
  • It seems that arc start_angle and sweep properties are ignored by libui and always set to 0 and 360 respectively, producing a full circle.

Original API

To learn more about the LibUI API exposed through Glimmer DSL for LibUI, check out the libui C headers

Glimmer Style Guide

  • Control arguments are always wrapped by parentheses
  • Control blocks are always declared with curly braces to clearly visualize hierarchical view code and separate from logic code
  • Control property declarations always have arguments and never take a block
  • Control property arguments are never wrapped inside parentheses
  • 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.
  • Pure logic multi-line blocks that do not constitute GUI DSL view elements have do; end style to clearly separate logic code from view code.

Examples

The following examples include reimplementions of the examples in the LibUI project utilizing the Glimmer GUI DSL as well as brand new examples.

To browse all examples, simply launch the Meta-Example, which lists all examples and displays each example's code when selected. It also enables code editing to facilitate experimentation and learning.

(note that for examples that emit output to terminal/command-line via p or puts, you must run them directly to see output)

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:

ruby -r glimmer-dsl-libui -e "require 'examples/meta_example'"

Mac

glimmer-dsl-libui-mac-meta-example.png

Linux

glimmer-dsl-libui-linux-meta-example.png

New Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'
require 'facets'

class MetaExample
  include Glimmer

  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' }
      @examples = example_file_names.map { |f| f.underscore.titlecase }
    end
    @examples
  end

  def file_path_for(example)
    File.join(File.expand_path('.', __dir__), "#{example.underscore}.rb")
  end

  def glimmer_dsl_libui_file
    File.expand_path('../lib/glimmer-dsl-libui', __dir__)
  end

  def launch
    window('Meta-Example', 700, 500) {
      margined true

      horizontal_box {
        vertical_box {
          @rbs = radio_buttons {
            stretchy false
            items examples
            selected 0

            on_selected do
              @nwme.text = File.read(file_path_for(@examples[@rbs.selected]))
            end
          }
          button('Launch') {
            stretchy false

            on_clicked do
              begin
                meta_example_file = File.join(Dir.home, '.meta_example.rb')
                File.write(meta_example_file, @nwme.text)
                result = `ruby -r #{glimmer_dsl_libui_file} #{meta_example_file} 2>&1`
                msg_box('Error Running Example', result) if result.include?('error')
              rescue => e
                puts 'Unable to write code changes! Running original example...'
                system "ruby -r #{glimmer_dsl_libui_file} #{file_path_for(@examples[@rbs.selected])}"
              end
            end
          }
        }
        vertical_box {
          @nwme = non_wrapping_multiline_entry {
            text File.read(file_path_for(@examples[@rbs.selected]))
          }
        }
      }
    }.show
  end
end

MetaExample.new.launch

Basic Window

examples/basic_window.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/basic_window.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/basic_window'"

Mac

glimmer-dsl-libui-mac-basic-window.png

Linux

glimmer-dsl-libui-linux-basic-window.png

LibUI Original Version:

require 'libui'

UI = LibUI

UI.init

main_window = UI.new_window('hello world', 300, 200, 1)

UI.control_show(main_window)

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

UI.main
UI.quit

Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

window('hello world', 300, 200, true) {
  on_closing do
    puts 'Bye Bye'
  end
}.show

Glimmer DSL for LibUI Version 2 (setting window properties instead of arguments):

require 'glimmer-dsl-libui'

include Glimmer

window { # first 3 args can be set via properties with 4th arg has_menubar=true by default
  title 'hello world'
  content_size 300, 200

  on_closing do
    puts 'Bye Bye'
  end
}.show

Basic Button

examples/basic_button.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/basic_button.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/basic_button'"

Mac

glimmer-dsl-libui-mac-basic-button.png glimmer-dsl-libui-mac-basic-button-msg-box.png

Linux

glimmer-dsl-libui-linux-basic-button.png glimmer-dsl-libui-linux-basic-button-msg-box.png

LibUI Original Version:

require 'libui'

UI = LibUI

UI.init

main_window = UI.new_window('hello world', 300, 200, 1)

button = UI.new_button('Button')

UI.button_on_clicked(button) do
  UI.msg_box(main_window, 'Information', 'You clicked the button')
end

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

UI.window_set_child(main_window, button)
UI.control_show(main_window)

UI.main
UI.quit

Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

window('hello world', 300, 200) {
  button('Button') {
    on_clicked do
      msg_box('Information', 'You clicked the button')
    end
  }

  on_closing do
    puts 'Bye Bye'
  end
}.show

Basic Entry

examples/basic_entry.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/basic_entry.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/basic_entry'"

Mac

glimmer-dsl-libui-mac-basic-entry.png glimmer-dsl-libui-mac-basic-entry-msg-box.png

Linux

glimmer-dsl-libui-linux-basic-entry.png glimmer-dsl-libui-linux-basic-entry-msg-box.png

LibUI Original Version:

require 'libui'

UI = LibUI

UI.init

main_window = UI.new_window('Basic Entry', 300, 50, 1)
UI.window_on_closing(main_window) do
  puts 'Bye Bye'
  UI.control_destroy(main_window)
  UI.quit
  0
end

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

entry = UI.new_entry
UI.entry_on_changed(entry) do
  puts UI.entry_text(entry).to_s
  $stdout.flush # For Windows
end
UI.box_append(hbox, entry, 1)

button = UI.new_button('Button')
UI.button_on_clicked(button) do
  text = UI.entry_text(entry).to_s
  UI.msg_box(main_window, 'You entered', text)
  0
end

UI.box_append(hbox, button, 0)

UI.control_show(main_window)
UI.main
UI.quit

Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

window('Basic Entry', 300, 50) {
  horizontal_box {
    e = entry {
      # stretchy true # Smart default option for appending to horizontal_box

      on_changed do
        puts e.text
        $stdout.flush # For Windows
      end
    }

    button('Button') {
      stretchy false # stretchy property is available when control is nested under horizontal_box

      on_clicked do
        text = e.text
        msg_box('You entered', text)
      end
    }
  }

  on_closing do
    puts 'Bye Bye'
  end
}.show

Simple Notepad

examples/simple_notepad.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/simple_notepad.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/simple_notepad'"

Mac

glimmer-dsl-libui-mac-simple-notepad.png

Linux

glimmer-dsl-libui-linux-simple-notepad.png

LibUI Original Version:

require 'libui'

UI = LibUI

UI.init

main_window = UI.new_window('Notepad', 500, 300, 1)
UI.window_on_closing(main_window) do
  puts 'Bye Bye'
  UI.control_destroy(main_window)
  UI.quit
  0
end

vbox = UI.new_vertical_box
UI.window_set_child(main_window, vbox)

entry = UI.new_non_wrapping_multiline_entry
UI.box_append(vbox, entry, 1)

UI.control_show(main_window)
UI.main
UI.quit

Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

window('Notepad', 500, 300) {
  on_closing do
    puts 'Bye Bye'
  end

  vertical_box {
    non_wrapping_multiline_entry
  }
}.show

Midi Player

To run this example, install TiMidity and ensure timidity command is in PATH (can be installed via Homebrew on Mac or apt-get on Linux).

examples/midi_player.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/midi_player.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/midi_player'"

Mac

glimmer-dsl-libui-mac-midi-player.png glimmer-dsl-libui-mac-midi-player-msg-box.png

Linux

glimmer-dsl-libui-linux-midi-player.png glimmer-dsl-libui-linux-midi-player-msg-box.png

LibUI Original Version:

require 'libui'
UI = LibUI

class TinyMidiPlayer
  VERSION = '0.0.1'

  def initialize
    UI.init
    @pid = nil
    @music_directory = File.expand_path(ARGV[0] || '~/Music/')
    @midi_files      = Dir.glob(File.join(@music_directory, '**/*.mid'))
                          .sort_by { |path| File.basename(path) }
    at_exit { stop_midi }
    create_gui
  end

  def stop_midi
    if @pid
      if @th.alive?
        Process.kill(:SIGKILL, @pid)
        @pid = nil
      else
        @pid = nil
      end
    end
  end

  def play_midi
    stop_midi
    if @pid.nil? && @selected_file
      begin
        @pid = spawn "timidity #{@selected_file}"
        @th = Process.detach @pid
      rescue Errno::ENOENT
        warn 'Timidty++ not found. Please install Timidity++.'
        warn 'https://sourceforge.net/projects/timidity/'
      end
    end
  end

  def show_version(main_window)
    UI.msg_box(main_window,
               'Tiny Midi Player',
               "Written in Ruby\n" \
               "https://github.com/kojix2/libui\n" \
               "Version #{VERSION}")
  end

  def create_gui
    help_menu = UI.new_menu('Help')
    version_item = UI.menu_append_item(help_menu, 'Version')

    UI.new_window('Tiny Midi Player', 200, 50, 1).tap do |main_window|
      UI.menu_item_on_clicked(version_item) { show_version(main_window) }

      UI.window_on_closing(main_window) do
        UI.control_destroy(main_window)
        UI.quit
        0
      end

      UI.new_horizontal_box.tap do |hbox|
        UI.new_vertical_box.tap do |vbox|
          UI.new_button('â–¶').tap do |button1|
            UI.button_on_clicked(button1) { play_midi }
            UI.box_append(vbox, button1, 1)
          end
          UI.new_button('â– ').tap do |button2|
            UI.button_on_clicked(button2) { stop_midi }
            UI.box_append(vbox, button2, 1)
          end
          UI.box_append(hbox, vbox, 0)
        end
        UI.window_set_child(main_window, hbox)

        UI.new_combobox.tap do |cbox|
          @midi_files.each do |path|
            name = File.basename(path)
            UI.combobox_append(cbox, name)
          end
          UI.combobox_on_selected(cbox) do |ptr|
            @selected_file = @midi_files[UI.combobox_selected(ptr)]
            play_midi if @th&.alive?
            0
          end
          UI.box_append(hbox, cbox, 1)
        end
      end
      UI.control_show(main_window)
    end
    UI.main
    UI.quit
  end
end

TinyMidiPlayer.new

Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

class TinyMidiPlayer
  include Glimmer

  VERSION = '0.0.1'

  def initialize
    @pid = nil
    @music_directory = File.expand_path('../sounds', __dir__)
    @midi_files      = Dir.glob(File.join(@music_directory, '**/*.mid'))
                          .sort_by { |path| File.basename(path) }
    at_exit { stop_midi }
    create_gui
  end

  def stop_midi
    if @pid
      if @th.alive?
        Process.kill(:SIGKILL, @pid)
        @pid = nil
      else
        @pid = nil
      end
    end
  end

  def play_midi
    stop_midi
    if @pid.nil? && @selected_file
      begin
        @pid = spawn "timidity #{@selected_file}"
        @th = Process.detach @pid
      rescue Errno::ENOENT
        warn 'Timidty++ not found. Please install Timidity++.'
        warn 'https://sourceforge.net/projects/timidity/'
      end
    end
  end

  def show_version
    msg_box('Tiny Midi Player',
              "Written in Ruby\n" \
                "https://github.com/kojix2/libui\n" \
                "Version #{VERSION}")
  end

  def create_gui
    menu('Help') {
      menu_item('Version') {
        on_clicked do
          show_version
        end
      }
    }
    window('Tiny Midi Player', 200, 50) {
      horizontal_box {
        vertical_box {
          stretchy false

          button('â–¶') {
            on_clicked do
              play_midi
            end
          }
          button('â– ') {
            on_clicked do
              stop_midi
            end
          }
        }

        combobox { |c|
          items @midi_files.map { |path| File.basename(path) }

          on_selected do
            @selected_file = @midi_files[c.selected]
            play_midi if @th&.alive?
          end
        }
      }
    }.show
  end
end

TinyMidiPlayer.new

examples/control_gallery.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/control_gallery.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/control_gallery'"

Mac

glimmer-dsl-libui-mac-control-gallery.png

Linux

glimmer-dsl-libui-linux-control-gallery.png

LibUI Original Version:

require 'libui'
UI = LibUI

UI.init

should_quit = proc do
  puts 'Bye Bye'
  UI.control_destroy(MAIN_WINDOW)
  UI.quit
  0
end

# File menu
menu = UI.new_menu('File')
open_menu_item = UI.menu_append_item(menu, 'Open')
UI.menu_item_on_clicked(open_menu_item) do
  pt = UI.open_file(MAIN_WINDOW)
  puts pt unless pt.null?
end
save_menu_item = UI.menu_append_item(menu, 'Save')
UI.menu_item_on_clicked(save_menu_item) do
  pt = UI.save_file(MAIN_WINDOW)
  puts pt unless pt.null?
end

UI.menu_append_quit_item(menu)
UI.on_should_quit(should_quit)

# Edit menu
edit_menu = UI.new_menu('Edit')
UI.menu_append_check_item(edit_menu, 'Checkable Item_')
UI.menu_append_separator(edit_menu)
disabled_item = UI.menu_append_item(edit_menu, 'Disabled Item_')
UI.menu_item_disable(disabled_item)

preferences = UI.menu_append_preferences_item(menu)

# Help menu
help_menu = UI.new_menu('Help')
UI.menu_append_item(help_menu, 'Help')
UI.menu_append_about_item(help_menu)

# Main Window
MAIN_WINDOW = UI.new_window('Control Gallery', 600, 500, 1)
UI.window_set_margined(MAIN_WINDOW, 1)
UI.window_on_closing(MAIN_WINDOW, should_quit)

vbox = UI.new_vertical_box
UI.window_set_child(MAIN_WINDOW, vbox)
hbox = UI.new_horizontal_box
UI.box_set_padded(vbox, 1)
UI.box_set_padded(hbox, 1)

UI.box_append(vbox, hbox, 1)

# Group - Basic Controls
group = UI.new_group('Basic Controls')
UI.group_set_margined(group, 1)
UI.box_append(hbox, group, 1) # OSX bug?

inner = UI.new_vertical_box
UI.box_set_padded(inner, 1)
UI.group_set_child(group, inner)

# Button
button = UI.new_button('Button')
UI.button_on_clicked(button) do
  UI.msg_box(MAIN_WINDOW, 'Information', 'You clicked the button')
end
UI.box_append(inner, button, 0)

# Checkbox
checkbox = UI.new_checkbox('Checkbox')
UI.checkbox_on_toggled(checkbox) do |ptr|
  checked = UI.checkbox_checked(ptr) == 1
  UI.window_set_title(MAIN_WINDOW, "Checkbox is #{checked}")
  UI.checkbox_set_text(ptr, "I am the checkbox (#{checked})")
end
UI.box_append(inner, checkbox, 0)

# Label
UI.box_append(inner, UI.new_label('Label'), 0)

# Separator
UI.box_append(inner, UI.new_horizontal_separator, 0)

# Date Picker
UI.box_append(inner, UI.new_date_picker, 0)

# Time Picker
UI.box_append(inner, UI.new_time_picker, 0)

# Date Time Picker
UI.box_append(inner, UI.new_date_time_picker, 0)

# Font Button
UI.box_append(inner, UI.new_font_button, 0)

# Color Button
UI.box_append(inner, UI.new_color_button, 0)

inner2 = UI.new_vertical_box
UI.box_set_padded(inner2, 1)
UI.box_append(hbox, inner2, 1)

# Group - Numbers
group = UI.new_group('Numbers')
UI.group_set_margined(group, 1)
UI.box_append(inner2, group, 0)

inner = UI.new_vertical_box
UI.box_set_padded(inner, 1)
UI.group_set_child(group, inner)

# Spinbox
spinbox = UI.new_spinbox(0, 100)
UI.spinbox_set_value(spinbox, 42)
UI.spinbox_on_changed(spinbox) do |ptr|
  puts "New Spinbox value: #{UI.spinbox_value(ptr)}"
end
UI.box_append(inner, spinbox, 0)

# Slider
slider = UI.new_slider(0, 100)
UI.box_append(inner, slider, 0)

# Progressbar
progressbar = UI.new_progress_bar
UI.box_append(inner, progressbar, 0)

UI.slider_on_changed(slider) do |ptr|
  v = UI.slider_value(ptr)
  puts "New Slider value: #{v}"
  UI.progress_bar_set_value(progressbar, v)
end

# Group - Lists
group = UI.new_group('Lists')
UI.group_set_margined(group, 1)
UI.box_append(inner2, group, 0)

inner = UI.new_vertical_box
UI.box_set_padded(inner, 1)
UI.group_set_child(group, inner)

# Combobox
cbox = UI.new_combobox
UI.combobox_append(cbox, 'combobox Item 1')
UI.combobox_append(cbox, 'combobox Item 2')
UI.combobox_append(cbox, 'combobox Item 3')
UI.box_append(inner, cbox, 0)
UI.combobox_on_selected(cbox) do |ptr|
  puts "New combobox selection: #{UI.combobox_selected(ptr)}"
end

# Editable Combobox
ebox = UI.new_editable_combobox
UI.editable_combobox_append(ebox, 'Editable Item 1')
UI.editable_combobox_append(ebox, 'Editable Item 2')
UI.editable_combobox_append(ebox, 'Editable Item 3')
UI.box_append(inner, ebox, 0)

# Radio Buttons
rb = UI.new_radio_buttons
UI.radio_buttons_append(rb, 'Radio Button 1')
UI.radio_buttons_append(rb, 'Radio Button 2')
UI.radio_buttons_append(rb, 'Radio Button 3')
UI.box_append(inner, rb, 1)

# Tab
tab = UI.new_tab
hbox1 = UI.new_horizontal_box
hbox2 = UI.new_horizontal_box
UI.tab_append(tab, 'Page 1', hbox1)
UI.tab_append(tab, 'Page 2', hbox2)
UI.tab_append(tab, 'Page 3', UI.new_horizontal_box)
UI.box_append(inner2, tab, 1)

# Text Entry
text_entry = UI.new_entry
UI.entry_set_text text_entry, 'Please enter your feelings'
UI.entry_on_changed(text_entry) do |ptr|
  puts "Current textbox data: '#{UI.entry_text(ptr)}'"
end
UI.box_append(hbox1, text_entry, 1)

UI.control_show(MAIN_WINDOW)

UI.main
UI.quit

Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

menu('File') {
  menu_item('Open') {
    on_clicked do
      file = open_file
      puts file unless file.nil?
    end
  }

  menu_item('Save') {
    on_clicked do
      file = save_file
      puts file unless file.nil?
    end
  }

  quit_menu_item {
    on_clicked do
      puts 'Bye Bye'
    end
  }

  preferences_menu_item # Can optionally contain an on_clicked listener
}

menu('Edit') {
  check_menu_item('Checkable Item_')
  separator_menu_item
  menu_item('Disabled Item_') {
    enabled false
  }
}

menu('Help') {
  menu_item('Help')

  about_menu_item # Can optionally contain an on_clicked listener
}

MAIN_WINDOW = window('Control Gallery', 600, 500) {
  margined true

  on_closing do
    puts 'Bye Bye'
  end

  vertical_box {
    horizontal_box {
      group('Basic Controls') {
        vertical_box {
          button('Button') {
            stretchy false

            on_clicked do
              msg_box('Information', 'You clicked the button')
            end
          }

          checkbox('Checkbox') {
            stretchy false

            on_toggled do |c|
              checked = c.checked?
              MAIN_WINDOW.title = "Checkbox is #{checked}"
              c.text = "I am the checkbox (#{checked})"
            end
          }

          label('Label') { stretchy false }

          horizontal_separator { stretchy false }

          date_picker { stretchy false }

          time_picker { stretchy false }

          date_time_picker { stretchy false }

          font_button { stretchy false }

          color_button { stretchy false }
        }
      }

      vertical_box {
        group('Numbers') {
          stretchy false

          vertical_box {
            spinbox(0, 100) {
              stretchy false
              value 42

              on_changed do |s|
                puts "New Spinbox value: #{s.value}"
              end
            }

            slider(0, 100) {
              stretchy false

              on_changed do |s|
                v = s.value
                puts "New Slider value: #{v}"
                @progress_bar.value = v
              end
            }

            @progress_bar = progress_bar { stretchy false }
          }
        }

        group('Lists') {
          stretchy false

          vertical_box {
            combobox {
              stretchy false
              items 'combobox Item 1', 'combobox Item 2', 'combobox Item 3' # also accepts a single array argument

              on_selected do |c|
                puts "New combobox selection: #{c.selected}"
              end
            }

            editable_combobox {
              stretchy false
              items 'Editable Item 1', 'Editable Item 2', 'Editable Item 3' # also accepts a single array argument
            }

            radio_buttons {
              items 'Radio Button 1', 'Radio Button 2', 'Radio Button 3' # also accepts a single array argument
            }
          }
        }

        tab {
          tab_item('Page 1') {
            horizontal_box {
              entry {
                text 'Please enter your feelings'

                on_changed do |e|
                  puts "Current textbox data: '#{e.text}'"
                end
              }
            }
          }

          tab_item('Page 2') {
            horizontal_box
          }

          tab_item('Page 3') {
            horizontal_box
          }
        }
      }
    }
  }
}

MAIN_WINDOW.show

Font Button

examples/font_button.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/font_button.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/font_button'"

Mac

glimmer-dsl-libui-mac-font-button.png glimmer-dsl-libui-mac-font-button-selection.png

Linux

glimmer-dsl-libui-linux-font-button.png glimmer-dsl-libui-linux-font-button-selection.png

LibUI Original Version:

require 'libui'

UI = LibUI

UI.init

main_window = UI.new_window('hello world', 300, 200, 1)

font_button = UI.new_font_button
font_descriptor = UI::FFI::FontDescriptor.malloc
UI.font_button_on_changed(font_button) do
  UI.font_button_font(font_button, font_descriptor)
  p family: font_descriptor.Family.to_s,
    size: font_descriptor.Size,
    weight: font_descriptor.Weight,
    italic: font_descriptor.Italic,
    stretch: font_descriptor.Stretch
end

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

UI.window_set_child(main_window, font_button)
UI.control_show(main_window)

UI.main
UI.quit

Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

window('hello world', 300, 200) {
  font_button { |fb|
    on_changed do
      font_descriptor = fb.font
      p font_descriptor
    end
  }

  on_closing do
    puts 'Bye Bye'
  end
}.show

Color Button

examples/color_button.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/color_button.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/color_button'"

Mac

glimmer-dsl-libui-mac-color-button.png glimmer-dsl-libui-mac-color-button-selection.png

Linux

glimmer-dsl-libui-linux-color-button.png glimmer-dsl-libui-linux-color-button-selection.png

New Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

window('color button', 230) {
  color_button { |cb|
    on_changed do
      rgba = cb.color
      p rgba
    end
  }
}.show

Date Time Picker

examples/date_time_picker.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/date_time_picker.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/date_time_picker'"

Mac

glimmer-dsl-libui-mac-date-time-picker.png

Linux

glimmer-dsl-libui-linux-date-time-picker.png

LibUI Original Version:

require 'libui'

UI = LibUI

UI.init

vbox = UI.new_vertical_box

date_time_picker = UI.new_date_time_picker

time = UI::FFI::TM.malloc

UI.date_time_picker_on_changed(date_time_picker) do
  UI.date_time_picker_time(date_time_picker, time)
  p sec: time.tm_sec,
    min: time.tm_min,
    hour: time.tm_hour,
    mday: time.tm_mday,
    mon: time.tm_mon,
    year: time.tm_year,
    wday: time.tm_wday,
    yday: time.tm_yday,
    isdst: time.tm_isdst
end
UI.box_append(vbox, date_time_picker, 1)

main_window = UI.new_window('Date Time Pickers', 300, 200, 1)
UI.window_on_closing(main_window) do
  puts 'Bye Bye'
  UI.control_destroy(main_window)
  UI.quit
  0
end
UI.window_set_child(main_window, vbox)
UI.control_show(main_window)

UI.main
UI.quit

Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

window('Date Time Pickers', 300, 200) {
  vertical_box {
    date_time_picker { |dtp|
      on_changed do
        time = dtp.time
        p time
      end
    }
  }

  on_closing do
    puts 'Bye Bye'
  end
}.show

Grid

examples/grid.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/grid.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/grid'"

Mac

glimmer-dsl-libui-mac-grid-span.png glimmer-dsl-libui-mac-grid-expand.png glimmer-dsl-libui-mac-grid-align.png

Linux

glimmer-dsl-libui-linux-grid-span.png glimmer-dsl-libui-linux-grid-expand.png glimmer-dsl-libui-linux-grid-align.png

New Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

window('Grid') {
  tab {
    tab_item('Span') {
      grid {
        4.times { |top_value|
          4.times { |left_value|
            label("(#{left_value}, #{top_value}) xspan1\nyspan1") {
              left left_value
              top top_value
              hexpand true
              vexpand true
            }
          }
        }
        label("(0, 4) xspan2\nyspan1 more text fits horizontally") {
          left 0
          top 4
          xspan 2
        }
        label("(2, 4) xspan2\nyspan1 more text fits horizontally") {
          left 2
          top 4
          xspan 2
        }
        label("(0, 5) xspan1\nyspan2\nmore text\nfits vertically") {
          left 0
          top 5
          yspan 2
        }
        label("(0, 7) xspan1\nyspan2\nmore text\nfits vertically") {
          left 0
          top 7
          yspan 2
        }
        label("(1, 5) xspan3\nyspan4 a lot more text fits horizontally than before\nand\neven\na lot\nmore text\nfits vertically\nthan\nbefore") {
          left 1
          top 5
          xspan 3
          yspan 4
        }
      }
    }
    tab_item('Expand') {
      grid {
        label("(0, 0) hexpand/vexpand\nall available horizontal space is taken\nand\nall\navailable\nvertical\nspace\nis\ntaken") {
          left 0
          top 0
          hexpand true
          vexpand true
        }
        label("(1, 0)") {
          left 1
          top 0
        }
        label("(0, 1)") {
          left 0
          top 1
        }
        label("(1, 1)") {
          left 1
          top 1
        }
      }
    }
    tab_item('Align') {
      grid {
        label("(0, 0) halign/valign fill\nall available horizontal space is taken\nand\nall\navailable\nvertical\nspace\nis\ntaken") {
          left 0
          top 0
          hexpand true unless OS.mac? # on Mac, only the first label is given all space, so avoid expanding
          vexpand true unless OS.mac? # on Mac, only the first label is given all space, so avoid expanding
          halign :fill
          valign :fill
        }
        label("(1, 0) halign/valign start") {
          left 1
          top 0
          hexpand true unless OS.mac? # on Mac, only the first label is given all space, so avoid expanding
          vexpand true unless OS.mac? # on Mac, only the first label is given all space, so avoid expanding
          halign :start
          valign :start
        }
        label("(0, 1) halign/valign center") {
          left 0
          top 1
          hexpand true unless OS.mac? # on Mac, only the first label is given all space, so avoid expanding
          vexpand true unless OS.mac? # on Mac, only the first label is given all space, so avoid expanding
          halign :center
          valign :center
        }
        label("(1, 1) halign/valign end") {
          left 1
          top 1
          hexpand true unless OS.mac? # on Mac, only the first label is given all space, so avoid expanding
          vexpand true unless OS.mac? # on Mac, only the first label is given all space, so avoid expanding
          halign :end
          valign :end
        }
      }
    }
  }
}.show

Form

examples/form.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/form.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/form'"

Mac

glimmer-dsl-libui-mac-form.png glimmer-dsl-libui-mac-form-msg-box.png

Linux

glimmer-dsl-libui-linux-form.png glimmer-dsl-libui-linux-form-msg-box.png

New Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

window('Form') {
  margined true

  vertical_box {
    form {
      @first_name_entry = entry {
        label 'First Name' # label property is available when control is nested under form
      }

      @last_name_entry = entry {
        label 'Last Name' # label property is available when control is nested under form
      }
    }

    button('Display Name') {
      on_clicked do
        msg_box('Name', "#{@first_name_entry.text} #{@last_name_entry.text}")
      end
    }
  }
}.show

Basic Table

examples/basic_table.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/basic_table.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/basic_table'"

Mac

glimmer-dsl-libui-mac-basic-table.png

Linux

glimmer-dsl-libui-linux-basic-table.png

LibUI Original Version:

require 'libui'

UI = LibUI

UI.init

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

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

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

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

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

model = UI.new_table_model(model_handler)

table_params = UI::FFI::TableParams.malloc
table_params.Model = model
table_params.RowBackgroundColorModelColumn = -1

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

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

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

UI.main
UI.quit

Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

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

window('Animal sounds', 300, 200) {
  horizontal_box {
    table {
      text_column('Animal')
      text_column('Description')

      cell_rows data
    }
  }

  on_closing do
    puts 'Bye Bye'
  end
}.show

Editable Table

examples/editable_table.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/editable_table.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/editable_table'"

Mac

glimmer-dsl-libui-mac-editable-table.png glimmer-dsl-libui-mac-editable-table-editing.png glimmer-dsl-libui-mac-editable-table-edited.png

Linux

glimmer-dsl-libui-linux-editable-table.png glimmer-dsl-libui-linux-editable-table-editing.png glimmer-dsl-libui-linux-editable-table-edited.png

New Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

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

window('Editable animal sounds', 300, 200) {
  horizontal_box {
    table {
      text_column('Animal')
      text_column('Description')

      cell_rows data
      editable true
    }
  }

  on_closing do
    puts 'Bye Bye'
  end
}.show

Editable Column Table

examples/editable_column_table.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/editable_column_table.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/editable_column_table'"

Mac

glimmer-dsl-libui-mac-editable-column-table-editing.png glimmer-dsl-libui-mac-editable-column-table-edited.png

Linux

glimmer-dsl-libui-linux-editable-column-table-editing.png glimmer-dsl-libui-linux-editable-column-table-edited.png

New Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

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

window('Editable column animal sounds', 400, 200) {
  horizontal_box {
    table {
      text_column('Animal')
      text_column('Description')
      text_column('Sound (Editable)') {
        editable true
      }

      cell_rows data
    }
  }

  on_closing do
    puts 'Bye Bye'
  end
}.show

Basic Table Image

This example has a prerequisite of installing chunky_png Ruby gem:

gem install chunky_png -v1.4.0

Also, note that behavior varies per platform (i.e. how table chooses to size images by default).

examples/basic_table_image.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/basic_table_image.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/basic_table_image'"

Mac

glimmer-dsl-libui-mac-basic-table-image.png

Linux

glimmer-dsl-libui-linux-basic-table-image.png

LibUI Original Version:

# NOTE:
# This example displays images that can be freely downloaded from the Studio Ghibli website.

require 'libui'
require 'chunky_png'
require 'open-uri'

UI = LibUI

UI.init

main_window = UI.new_window('The Red Turtle', 310, 350, 0)

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

IMAGES = []

50.times do |i|
  url = format('https://www.ghibli.jp/gallery/thumb-redturtle%03d.png', (i + 1))
  puts "Processing Image: #{url}"
  f = URI.open(url)
  canvas = ChunkyPNG::Canvas.from_io(f)
  f.close
  data = canvas.to_rgba_stream
  width = canvas.width
  height = canvas.height
  image = UI.new_image(width, height)
  UI.image_append(image, data, width, height, width * 4)
  IMAGES << image
rescue StandardError => e
  warn url, e.message
end

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

model_handler = UI::FFI::TableModelHandler.malloc
model_handler.NumColumns   = rbcallback(4) { 1 }
model_handler.ColumnType   = rbcallback(4) { 1 } # Image
model_handler.NumRows      = rbcallback(4) { IMAGES.size }
model_handler.CellValue    = rbcallback(1, [1, 1, 4, 4]) do |_, _, row, _column|
  UI.new_table_value_image(IMAGES[row])
end
model_handler.SetCellValue = rbcallback(0, [0]) {}

model = UI.new_table_model(model_handler)

table_params = UI::FFI::TableParams.malloc
table_params.Model = model
table_params.RowBackgroundColorModelColumn = -1

table = UI.new_table(table_params)
UI.table_append_image_column(table, 'www.ghibli.jp/works/red-turtle', 0)

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

UI.window_on_closing(main_window) do
  puts 'Bye Bye'
  UI.control_destroy(main_window)
  UI.free_table_model(model)
  IMAGES.each { |i| UI.free_image(i) }
  UI.quit
  0
end

UI.main
UI.quit

Glimmer DSL for LibUI Version:

# NOTE:
# This example displays images that can be freely downloaded from the Studio Ghibli website.

require 'glimmer-dsl-libui'
require 'chunky_png'
require 'open-uri'

include Glimmer

IMAGE_ROWS = []

50.times do |i|
  url = format('https://www.ghibli.jp/gallery/thumb-redturtle%03d.png', (i + 1))
  puts "Processing Image: #{url}"
  f = URI.open(url)
  canvas = ChunkyPNG::Canvas.from_io(f)
  f.close
  data = canvas.to_rgba_stream
  width = canvas.width
  height = canvas.height
  img = image {
    image_part(data, width, height, width * 4)
  }
  IMAGE_ROWS << [img] # array of one column cell
rescue StandardError => e
  warn url, e.message
end

window('The Red Turtle', 310, 350, false) {
  horizontal_box {
    table {
      image_column('www.ghibli.jp/works/red-turtle', 0)

      cell_rows IMAGE_ROWS
    }
  }

  on_closing do
    puts 'Bye Bye'
  end
}.show

Basic Table Image Text

This example has a prerequisite of installing chunky_png Ruby gem:

gem install chunky_png -v1.4.0

Also, note that behavior varies per platform (i.e. how table chooses to size images by default).

examples/basic_table_image_text.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/basic_table_image_text.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/basic_table_image_text'"

Mac

glimmer-dsl-libui-mac-basic-table-image-text.png

Linux

glimmer-dsl-libui-linux-basic-table-image-text.png

New Glimmer DSL for LibUI Version:

# NOTE:
# This example displays images that can be freely downloaded from the Studio Ghibli website.

require 'glimmer-dsl-libui'
require 'chunky_png'
require 'open-uri'

include Glimmer

IMAGE_ROWS = []

5.times do |i|
  url = format('https://www.ghibli.jp/gallery/thumb-redturtle%03d.png', (i + 1))
  puts "Processing Image: #{url}"
  f = URI.open(url)
  canvas = ChunkyPNG::Canvas.from_io(f)
  f.close
  data = canvas.to_rgba_stream
  width = canvas.width
  height = canvas.height
  img = image {
    image_part(data, width, height, width * 4)
  }
  text = url.sub('https://www.ghibli.jp/gallery/thumb-redturtle', '').sub('.png', '')
  IMAGE_ROWS << [[img, text], [img, text]] # cell values are dual-element arrays
rescue StandardError => e
  warn url, e.message
end

window('The Red Turtle', 670, 350) {
  horizontal_box {
    table {
      image_text_column('image/number')
      image_text_column('image/number (editable)') {
        editable true
      }

      cell_rows IMAGE_ROWS
    }
  }
}.show

Basic Table Button

examples/basic_table_button.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/basic_table_button.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/basic_table_button'"

Mac

glimmer-dsl-libui-mac-basic-table-button.png glimmer-dsl-libui-mac-basic-table-button-deleted.png

Linux

glimmer-dsl-libui-linux-basic-table-button.png glimmer-dsl-libui-linux-basic-table-button-deleted.png

New Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

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

window('Animal sounds', 300, 200) {
  horizontal_box {
    table {
      text_column('Animal')
      text_column('Description')
      button_column('Action') {
        on_clicked do |row|
          data.delete_at(row) # automatically deletes actual table row due to implicit data-binding
        end
      }

      cell_rows data # implicit data-binding
    }
  }
}.show

Basic Table Checkbox

examples/basic_table_checkbox.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/basic_table_checkbox.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/basic_table_checkbox'"

Mac

glimmer-dsl-libui-mac-basic-table-checkbox.png

Linux

glimmer-dsl-libui-linux-basic-table-checkbox.png

New Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

data = [
  ['cat', 'meow', true],
  ['dog', 'woof', true],
  ['chicken', 'cock-a-doodle-doo', false],
  ['horse', 'neigh', true],
  ['cow', 'moo', true]
]

window('Animal sounds', 300, 200) {
  horizontal_box {
    table {
      text_column('Animal')
      text_column('Description')
      checkbox_column('Mammal')

      cell_rows data
    }
  }
}.show

Basic Table Checkbox Text

examples/basic_table_checkbox_text.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/basic_table_checkbox_text.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/basic_table_checkbox_text'"

Mac

glimmer-dsl-libui-mac-basic-table-checkbox-text.png

Linux

glimmer-dsl-libui-linux-basic-table-checkbox-text.png

New Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

data = [
  ['cat', 'meow', [true, 'mammal']],
  ['dog', 'woof', [true, 'mammal']],
  ['chicken', 'cock-a-doodle-doo', [false, 'mammal']],
  ['horse', 'neigh', [true, 'mammal']],
  ['cow', 'moo', [true, 'mammal']]
]

window('Animal sounds', 400, 200) {
  horizontal_box {
    table {
      text_column('Animal')
      text_column('Sound')
      checkbox_text_column('Description')

      cell_rows data
    }
  }
}.show

Basic Table Progress Bar

examples/basic_table_progress_bar.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/basic_table_progress_bar.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/basic_table_progress_bar'"

Mac

glimmer-dsl-libui-mac-basic-table-progress-bar.png

Linux

glimmer-dsl-libui-linux-basic-table-progress-bar.png

New Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

data = [
  ['task 1', 0],
  ['task 2', 15],
  ['task 3', 100],
  ['task 4', 75],
  ['task 5', -1],
]

window('Task Progress', 300, 200) {
  vertical_box {
    table {
      text_column('Task')
      progress_bar_column('Progress')

      cell_rows data # implicit data-binding
    }

    button('Mark All As Done') {
      stretchy false

      on_clicked do
        data.each_with_index do |row_data, row|
          data[row] = [row_data[0], 100] # automatically updates table due to implicit data-binding
        end
      end
    }
  }
}.show

Form Table

examples/form_table.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/form_table.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/form_table'"

Mac

glimmer-dsl-libui-mac-form-table.png glimmer-dsl-libui-mac-form-table-contact-entered.png glimmer-dsl-libui-mac-form-table-filtered.png

Linux

glimmer-dsl-libui-linux-form-table.png glimmer-dsl-libui-linux-form-table-contact-entered.png glimmer-dsl-libui-linux-form-table-filtered.png

New Glimmer DSL for LibUI Version:

# frozen_string_literal: true

require 'glimmer-dsl-libui'

include Glimmer

data = [
  ['Lisa Sky', '[email protected]', '720-523-4329', 'Denver', 'CO', '80014'],
  ['Jordan Biggins', '[email protected]', '617-528-5399', 'Boston', 'MA', '02101'],
  ['Mary Glass', '[email protected]', '847-589-8788', 'Elk Grove Village', 'IL', '60007'],
  ['Darren McGrath', '[email protected]', '206-539-9283', 'Seattle', 'WA', '98101'],
  ['Melody Hanheimer', '[email protected]', '213-493-8274', 'Los Angeles', 'CA', '90001'],
]

window('Contacts', 600, 600) { |w|
  margined true

  vertical_box {
    form {
      stretchy false

      @name_entry = entry {
        label 'Name'
      }
      @email_entry = entry {
        label 'Email'
      }
      @phone_entry = entry {
        label 'Phone'
      }
      @city_entry = entry {
        label 'City'
      }
      @state_entry = entry {
        label 'State'
      }
    }

    button('Save Contact') {
      stretchy false

      on_clicked do
        new_row = [@name_entry.text, @email_entry.text, @phone_entry.text, @city_entry.text, @state_entry.text]
        if new_row.include?('')
          msg_box_error(w, 'Validation Error!', 'All fields are required! Please make sure to enter a value for all fields.')
        else
          data << new_row # automatically inserts a row into the table due to implicit data-binding
          @unfiltered_data = data.dup
          @name_entry.text = ''
          @email_entry.text = ''
          @phone_entry.text = ''
          @city_entry.text = ''
          @state_entry.text = ''
        end
      end
    }

    search_entry { |se|
      stretchy false

      on_changed do
        filter_value = se.text
        @unfiltered_data ||= data.dup
        # Unfilter first to remove any previous filters
        data.replace(@unfiltered_data) # affects table indirectly through implicit data-binding
        # Now, apply filter if entered
        unless filter_value.empty?
          data.filter! do |row_data| # affects table indirectly through implicit data-binding
            row_data.any? do |cell|
              cell.to_s.downcase.include?(filter_value.downcase)
            end
          end
        end
      end
    }

    table {
      text_column('Name')
      text_column('Email')
      text_column('Phone')
      text_column('City')
      text_column('State')

      cell_rows data # implicit data-binding
    }
  }
}.show

Basic Area

examples/basic_area.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/basic_area.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/basic_area'"

Mac

glimmer-dsl-libui-mac-basic-area.png

Linux

glimmer-dsl-libui-linux-basic-area.png

LibUI Original Version:

require 'libui'

UI = LibUI

UI.init

handler = UI::FFI::AreaHandler.malloc
area    = UI.new_area(handler)
brush   = UI::FFI::DrawBrush.malloc

handler_draw_event = Fiddle::Closure::BlockCaller.new(0, [1, 1, 1]) do |_, _, area_draw_params|
  path = UI.draw_new_path(0)
  UI.draw_path_add_rectangle(path, 0, 0, 400, 400)
  UI.draw_path_end(path)
  brush.Type = 0
  brush.R = 0.4
  brush.G = 0.4
  brush.B = 0.8
  brush.A = 1.0
  area_draw_params = UI::FFI::AreaDrawParams.new(area_draw_params)
  UI.draw_fill(area_draw_params.Context, path, brush.to_ptr)
  UI.draw_free_path(path)
end

handler.Draw         = handler_draw_event
handler.MouseEvent   = Fiddle::Closure::BlockCaller.new(0, [0]) {}
handler.MouseCrossed = Fiddle::Closure::BlockCaller.new(0, [0]) {}
handler.DragBroken   = Fiddle::Closure::BlockCaller.new(0, [0]) {}
handler.KeyEvent     = Fiddle::Closure::BlockCaller.new(0, [0]) {}

box = UI.new_vertical_box
UI.box_set_padded(box, 1)
UI.box_append(box, area, 1)

main_window = UI.new_window('Basic Area', 400, 400, 1)
UI.window_set_margined(main_window, 1)
UI.window_set_child(main_window, box)

UI.window_on_closing(main_window) do
  UI.control_destroy(main_window)
  UI.quit
  0
end
UI.control_show(main_window)

UI.main
UI.quit

Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

window('Basic Area', 400, 400) {
  margined true

  vertical_box {
    area {
      path { # a stable path is added declaratively
        rectangle(0, 0, 400, 400)

        fill r: 102, g: 102, b: 204, a: 1.0
      }
    }
  }
}.show

Glimmer DSL for LibUI Version 2 (semi-declarative on_draw dynamic path approach):

require 'glimmer-dsl-libui'

include Glimmer

window('Basic Area', 400, 400) {
  margined true

  vertical_box {
    area {
      on_draw do |area_draw_params|
        path { # a dynamic path is added semi-declaratively inside on_draw block
          rectangle(0, 0, 400, 400)

          fill r: 102, g: 102, b: 204, a: 1.0
        }
      end
    }
  }
}.show

Dynamic Area

examples/dynamic_area.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/dynamic_area.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/dynamic_area'"

Mac

glimmer-dsl-libui-mac-dynamic-area.png glimmer-dsl-libui-mac-dynamic-area-updated.png

Linux

glimmer-dsl-libui-linux-dynamic-area.png glimmer-dsl-libui-linux-dynamic-area-updated.png

New Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

window('Dynamic Area', 240, 500) {
  margined true

  vertical_box {
    label('Rectangle Properties') {
      stretchy false
    }

    form {
      stretchy false

      @x_spinbox = spinbox(0, 1000) {
        label 'x'
        value 25

        on_changed do
          @area.queue_redraw_all
        end
      }

      @y_spinbox = spinbox(0, 1000) {
        label 'y'
        value 25

        on_changed do
          @area.queue_redraw_all
        end
      }

      @width_spinbox = spinbox(0, 1000) {
        label 'width'
        value 150

        on_changed do
          @area.queue_redraw_all
        end
      }

      @height_spinbox = spinbox(0, 1000) {
        label 'height'
        value 150

        on_changed do
          @area.queue_redraw_all
        end
      }

      @red_spinbox = spinbox(0, 255) {
        label 'red'
        value 102

        on_changed do
          @area.queue_redraw_all
        end
      }

      @green_spinbox = spinbox(0, 255) {
        label 'green'
        value 102

        on_changed do
          @area.queue_redraw_all
        end
      }

      @blue_spinbox = spinbox(0, 255) {
        label 'blue'
        value 204

        on_changed do
          @area.queue_redraw_all
        end
      }

      @alpha_spinbox = spinbox(0, 100) {
        label 'alpha'
        value 100

        on_changed do
          @area.queue_redraw_all
        end
      }
    }

    @area = area {
      on_draw do |area_draw_params|
        path { # a dynamic path is added semi-declaratively inside on_draw block
          rectangle(@x_spinbox.value, @y_spinbox.value, @width_spinbox.value, @height_spinbox.value)

          fill r: @red_spinbox.value, g: @green_spinbox.value, b: @blue_spinbox.value, a: @alpha_spinbox.value / 100.0
        }
      end
    }
  }
}.show

New Glimmer DSL for LibUI Version 2 (declarative stable path approach):

require 'glimmer-dsl-libui'

include Glimmer

window('Dynamic Area', 240, 600) {
  margined true

  vertical_box {
    label('Rectangle Properties') {
      stretchy false
    }

    form {
      stretchy false

      @x_spinbox = spinbox(0, 1000) {
        label 'x'
        value 25

        on_changed do
          @rectangle.x = @x_spinbox.value # updating properties automatically triggers area.queue_redraw_all
        end
      }

      @y_spinbox = spinbox(0, 1000) {
        label 'y'
        value 25

        on_changed do
          @rectangle.y = @y_spinbox.value # updating properties automatically triggers area.queue_redraw_all
        end
      }

      @width_spinbox = spinbox(0, 1000) {
        label 'width'
        value 150

        on_changed do
          @rectangle.width = @width_spinbox.value # updating properties automatically triggers area.queue_redraw_all
        end
      }

      @height_spinbox = spinbox(0, 1000) {
        label 'height'
        value 150

        on_changed do
          @rectangle.height = @height_spinbox.value # updating properties automatically triggers area.queue_redraw_all
        end
      }

      @red_spinbox = spinbox(0, 255) {
        label 'red'
        value 102

        on_changed do
          @path.fill[:r] = @red_spinbox.value # updating hash properties automatically triggers area.queue_redraw_all
        end
      }

      @green_spinbox = spinbox(0, 255) {
        label 'green'
        value 102

        on_changed do
          @path.fill[:g] = @green_spinbox.value # updating hash properties automatically triggers area.queue_redraw_all
        end
      }

      @blue_spinbox = spinbox(0, 255) {
        label 'blue'
        value 204

        on_changed do
          @path.fill[:b] = @blue_spinbox.value # updating hash properties automatically triggers area.queue_redraw_all
        end
      }

      @alpha_spinbox = spinbox(0, 100) {
        label 'alpha'
        value 100

        on_changed do
          @path.fill[:a] = @alpha_spinbox.value / 100.0 # updating hash properties automatically triggers area.queue_redraw_all
        end
      }
    }

    area {
      @path = path { # stable path
        @rectangle = rectangle(@x_spinbox.value, @y_spinbox.value, @width_spinbox.value, @height_spinbox.value)

        fill r: @red_spinbox.value, g: @green_spinbox.value, b: @blue_spinbox.value, a: @alpha_spinbox.value / 100.0
      }
    }
  }
}.show

examples/area_gallery.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/area_gallery.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/area_gallery'"

Mac

glimmer-dsl-libui-mac-area-gallery.png

Linux

glimmer-dsl-libui-linux-area-gallery.png

New Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

window('Area Gallery', 400, 400) {
  area {
    path { # declarative stable path
      square(0, 0, 100)
      square(100, 100, 400)

      fill r: 102, g: 102, b: 204
    }
    path { # declarative stable path
      rectangle(0, 100, 100, 400)
      rectangle(100, 0, 400, 100)

      fill r: 204, g: 102, b: 204
    }
    path { # declarative stable path
      figure(100, 100) {
        line(100, 400)
        line(400, 100)
        line(400, 400)

        closed true
      }

      fill r: 202, g: 102, b: 104, a: 0.5
      stroke r: 0, g: 0, b: 0
    }
    path { # declarative stable path
      figure(0, 0) {
        bezier(200, 100, 100, 200, 400, 100)
        bezier(300, 100, 100, 300, 100, 400)
        bezier(100, 300, 300, 100, 400, 400)

        closed true
      }

      fill r: 202, g: 102, b: 204, a: 0.5
      stroke r: 0, g: 0, b: 0, thickness: 2, dashes: [50, 10, 10, 10], dash_phase: -50.0
    }
    path { # declarative stable path
      circle(200, 200, 90)

      fill r: 202, g: 102, b: 204, a: 0.5
      stroke r: 0, g: 0, b: 0, thickness: 2
    }
    path { # declarative stable path
      arc(400, 220, 180, 90, 90, false)

      fill r: 204, g: 102, b: 204, a: 0.5
      stroke r: 0, g: 0, b: 0, thickness: 2, dashes: [50, 10, 10, 10], dash_phase: -50.0
    }

    on_mouse_event do |area_mouse_event|
      p area_mouse_event
    end

    on_mouse_moved do |area_mouse_event|
      puts 'moved'
    end

    on_mouse_down do |area_mouse_event|
      puts 'mouse down'
    end

    on_mouse_up do |area_mouse_event|
      puts 'mouse up'
    end

    on_mouse_drag_started do |area_mouse_event|
      puts 'drag started'
    end

    on_mouse_dragged do |area_mouse_event|
      puts 'dragged'
    end

    on_mouse_dropped do |area_mouse_event|
      puts 'dropped'
    end

    on_mouse_entered do
      puts 'entered'
    end

    on_mouse_exited do
      puts 'exited'
    end

    on_key_event do |area_key_event|
      p area_key_event
    end

    on_key_up do |area_key_event|
      puts 'key up'
    end

    on_key_down do |area_key_event|
      puts 'key down'
    end
  }
}.show

New Glimmer DSL for LibUI Version 2 (setting shape properties instead of arguments):

require 'glimmer-dsl-libui'

include Glimmer

window('Area Gallery', 400, 400) {
  area {
    path { # declarative stable path
      square {
        x 0
        y 0
        length 100
      }
      square {
        x 100
        y 100
        length 400
      }

      fill r: 102, g: 102, b: 204
    }
    path { # declarative stable path
      rectangle {
        x 0
        y 100
        width 100
        height 400
      }
      rectangle {
        x 100
        y 0
        width 400
        height 100
      }

      fill r: 204, g: 102, b: 204
    }
    path { # declarative stable path
      figure {
        x 100
        y 100

        line {
          x 100
          y 400
        }
        line {
          x 400
          y 100
        }
        line {
          x 400
          y 400
        }

        closed true
      }

      fill r: 202, g: 102, b: 104, a: 0.5
      stroke r: 0, g: 0, b: 0
    }
    path { # declarative stable path
      figure {
        x 0
        y 0

        bezier {
          c1_x 200
          c1_y 100
          c2_x 100
          c2_y 200
          end_x 400
          end_y 100
        }
        bezier {
          c1_x 300
          c1_y 100
          c2_x 100
          c2_y 300
          end_x 100
          end_y 400
        }
        bezier {
          c1_x 100
          c1_y 300
          c2_x 300
          c2_y 100
          end_x 400
          end_y 400
        }

        closed true
      }

      fill r: 202, g: 102, b: 204, a: 0.5
      stroke r: 0, g: 0, b: 0, thickness: 2, dashes: [50, 10, 10, 10], dash_phase: -50.0
    }
    path { # declarative stable path
      circle {
        x_center 200
        y_center 200
        radius 90
      }

      fill r: 202, g: 102, b: 204, a: 0.5
      stroke r: 0, g: 0, b: 0, thickness: 2
    }
    path { # declarative stable path
      arc {
        x_center 400
        y_center 220
        radius 180
        start_angle 90
        sweep 90
        is_negative false
      }

      fill r: 204, g: 102, b: 204, a: 0.5
      stroke r: 0, g: 0, b: 0, thickness: 2, dashes: [50, 10, 10, 10], dash_phase: -50.0
    }

    on_mouse_event do |area_mouse_event|
      p area_mouse_event
    end

    on_mouse_moved do |area_mouse_event|
      puts 'moved'
    end

    on_mouse_down do |area_mouse_event|
      puts 'mouse down'
    end

    on_mouse_up do |area_mouse_event|
      puts 'mouse up'
    end

    on_mouse_drag_started do |area_mouse_event|
      puts 'drag started'
    end

    on_mouse_dragged do |area_mouse_event|
      puts 'dragged'
    end

    on_mouse_dropped do |area_mouse_event|
      puts 'dropped'
    end

    on_mouse_entered do
      puts 'entered'
    end

    on_mouse_exited do
      puts 'exited'
    end

    on_key_event do |area_key_event|
      p area_key_event
    end

    on_key_up do |area_key_event|
      puts 'key up'
    end

    on_key_down do |area_key_event|
      puts 'key down'
    end
  }
}.show

New Glimmer DSL for LibUI Version 3 (semi-declarative on_draw dynamic path approach):

require 'glimmer-dsl-libui'

include Glimmer

window('Area Gallery', 400, 400) {
  area {
    on_draw do |area_draw_params|
      path { # a dynamic path is added semi-declaratively inside on_draw block
        square(0, 0, 100)
        square(100, 100, 400)

        fill r: 102, g: 102, b: 204
      }
      path { # a dynamic path is added semi-declaratively inside on_draw block
        rectangle(0, 100, 100, 400)
        rectangle(100, 0, 400, 100)

        fill r: 204, g: 102, b: 204
      }
      path { # a dynamic path is added semi-declaratively inside on_draw block
        figure(100, 100) {
          line(100, 400)
          line(400, 100)
          line(400, 400)

          closed true
        }

        fill r: 202, g: 102, b: 104, a: 0.5
        stroke r: 0, g: 0, b: 0
      }
      path { # a dynamic path is added semi-declaratively inside on_draw block
        figure(0, 0) {
          bezier(200, 100, 100, 200, 400, 100)
          bezier(300, 100, 100, 300, 100, 400)
          bezier(100, 300, 300, 100, 400, 400)

          closed true
        }

        fill r: 202, g: 102, b: 204, a: 0.5
        stroke r: 0, g: 0, b: 0, thickness: 2, dashes: [50, 10, 10, 10], dash_phase: -50.0
      }
      path { # a dynamic path is added semi-declaratively inside on_draw block
        circle(200, 200, 90)

        fill r: 202, g: 102, b: 204, a: 0.5
        stroke r: 0, g: 0, b: 0, thickness: 2
      }
      path { # a dynamic path is added semi-declaratively inside on_draw block
        arc(400, 220, 180, 90, 90, false)

        fill r: 204, g: 102, b: 204, a: 0.5
        stroke r: 0, g: 0, b: 0, thickness: 2, dashes: [50, 10, 10, 10], dash_phase: -50.0
      }
    end

    on_mouse_event do |area_mouse_event|
      p area_mouse_event
    end

    on_mouse_moved do |area_mouse_event|
      puts 'moved'
    end

    on_mouse_down do |area_mouse_event|
      puts 'mouse down'
    end

    on_mouse_up do |area_mouse_event|
      puts 'mouse up'
    end

    on_mouse_drag_started do |area_mouse_event|
      puts 'drag started'
    end

    on_mouse_dragged do |area_mouse_event|
      puts 'dragged'
    end

    on_mouse_dropped do |area_mouse_event|
      puts 'dropped'
    end

    on_mouse_entered do
      puts 'entered'
    end

    on_mouse_exited do
      puts 'exited'
    end

    on_key_event do |area_key_event|
      p area_key_event
    end

    on_key_up do |area_key_event|
      puts 'key up'
    end

    on_key_down do |area_key_event|
      puts 'key down'
    end
  }
}.show

New Glimmer DSL for LibUI Version 4 (setting shape properties instead of arguments with semi-declarative on_draw dynamic path approach):

require 'glimmer-dsl-libui'

include Glimmer

window('Area Gallery', 400, 400) {
  area {
    on_draw do |area_draw_params|
      path { # a dynamic path is added semi-declaratively inside on_draw block
        square {
          x 0
          y 0
          length 100
        }
        square {
          x 100
          y 100
          length 400
        }

        fill r: 102, g: 102, b: 204
      }
      path { # a dynamic path is added semi-declaratively inside on_draw block
        rectangle {
          x 0
          y 100
          width 100
          height 400
        }
        rectangle {
          x 100
          y 0
          width 400
          height 100
        }

        fill r: 204, g: 102, b: 204
      }
      path { # a dynamic path is added semi-declaratively inside on_draw block
        figure {
          x 100
          y 100

          line {
            x 100
            y 400
          }
          line {
            x 400
            y 100
          }
          line {
            x 400
            y 400
          }

          closed true
        }

        fill r: 202, g: 102, b: 104, a: 0.5
        stroke r: 0, g: 0, b: 0
      }
      path { # a dynamic path is added semi-declaratively inside on_draw block
        figure {
          x 0
          y 0

          bezier {
            c1_x 200
            c1_y 100
            c2_x 100
            c2_y 200
            end_x 400
            end_y 100
          }
          bezier {
            c1_x 300
            c1_y 100
            c2_x 100
            c2_y 300
            end_x 100
            end_y 400
          }
          bezier {
            c1_x 100
            c1_y 300
            c2_x 300
            c2_y 100
            end_x 400
            end_y 400
          }

          closed true
        }

        fill r: 202, g: 102, b: 204, a: 0.5
        stroke r: 0, g: 0, b: 0, thickness: 2, dashes: [50, 10, 10, 10], dash_phase: -50.0
      }
      path { # a dynamic path is added semi-declaratively inside on_draw block
        circle {
          x_center 200
          y_center 200
          radius 90
        }

        fill r: 202, g: 102, b: 204, a: 0.5
        stroke r: 0, g: 0, b: 0, thickness: 2
      }
      path { # a dynamic path is added semi-declaratively inside on_draw block
        arc {
          x_center 400
          y_center 220
          radius 180
          start_angle 90
          sweep 90
          is_negative false
        }

        fill r: 204, g: 102, b: 204, a: 0.5
        stroke r: 0, g: 0, b: 0, thickness: 2, dashes: [50, 10, 10, 10], dash_phase: -50.0
      }
    end

    on_mouse_event do |area_mouse_event|
      p area_mouse_event
    end

    on_mouse_moved do |area_mouse_event|
      puts 'moved'
    end

    on_mouse_down do |area_mouse_event|
      puts 'mouse down'
    end

    on_mouse_up do |area_mouse_event|
      puts 'mouse up'
    end

    on_mouse_drag_started do |area_mouse_event|
      puts 'drag started'
    end

    on_mouse_dragged do |area_mouse_event|
      puts 'dragged'
    end

    on_mouse_dropped do |area_mouse_event|
      puts 'dropped'
    end

    on_mouse_entered do
      puts 'entered'
    end

    on_mouse_exited do
      puts 'exited'
    end

    on_key_event do |area_key_event|
      p area_key_event
    end

    on_key_up do |area_key_event|
      puts 'key up'
    end

    on_key_down do |area_key_event|
      puts 'key down'
    end
  }
}.show

Histogram

examples/histogram.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/histogram.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/histogram'"

Mac

glimmer-dsl-libui-mac-histogram.png

Linux

glimmer-dsl-libui-linux-histogram.png

LibUI Original Version:

# https://github.com/jamescook/libui-ruby/blob/master/example/histogram.rb

require 'libui'

UI = LibUI

X_OFF_LEFT   = 20
Y_OFF_TOP    = 20
X_OFF_RIGHT  = 20
Y_OFF_BOTTOM = 20
POINT_RADIUS = 5

init         = UI.init
handler      = UI::FFI::AreaHandler.malloc
histogram    = UI.new_area(handler)
brush        = UI::FFI::DrawBrush.malloc
color_button = UI.new_color_button
blue         = 0x1E90FF
datapoints   = []

def graph_size(area_width, area_height)
  graph_width = area_width - X_OFF_LEFT - X_OFF_RIGHT
  graph_height = area_height - Y_OFF_TOP - Y_OFF_BOTTOM
  [graph_width, graph_height]
end

matrix = UI::FFI::DrawMatrix.malloc

def point_locations(datapoints, width, height)
  xincr = width / 9.0 # 10 - 1 to make the last point be at the end
  yincr = height / 100.0

  data = []
  datapoints.each_with_index do |dp, i|
    val = 100 - UI.spinbox_value(dp)
    data << [xincr * i, yincr * val]
    i += 1
  end

  data
end

def construct_graph(datapoints, width, height, should_extend)
  locations = point_locations(datapoints, width, height)
  path = UI.draw_new_path(0) # winding
  first_location = locations[0] # x and y
  UI.draw_path_new_figure(path, first_location[0], first_location[1])
  locations.each do |loc|
    UI.draw_path_line_to(path, loc[0], loc[1])
  end

  if should_extend
    UI.draw_path_line_to(path, width, height)
    UI.draw_path_line_to(path, 0, height)
    UI.draw_path_close_figure(path)
  end

  UI.draw_path_end(path)

  path
end

handler_draw_event = Fiddle::Closure::BlockCaller.new(
  0, [1, 1, 1]
) do |_area_handler, _area, area_draw_params|
  area_draw_params = UI::FFI::AreaDrawParams.new(area_draw_params)
  path = UI.draw_new_path(0) # winding
  UI.draw_path_add_rectangle(path, 0, 0, area_draw_params.AreaWidth, area_draw_params.AreaHeight)
  UI.draw_path_end(path)
  set_solid_brush(brush, 0xFFFFFF, 1.0) # white
  UI.draw_fill(area_draw_params.Context, path, brush.to_ptr)
  UI.draw_free_path(path)
  dsp = UI::FFI::DrawStrokeParams.malloc
  dsp.Cap = 0 # flat
  dsp.Join = 0 # miter
  dsp.Thickness = 2
  dsp.MiterLimit = 10 # DEFAULT_MITER_LIMIT
  dashes = Fiddle::Pointer.malloc(8)
  dsp.Dashes = dashes
  dsp.NumDashes = 0
  dsp.DashPhase = 0

  # draw axes
  set_solid_brush(brush, 0x000000, 1.0) # black
  graph_width, graph_height = *graph_size(area_draw_params.AreaWidth, area_draw_params.AreaHeight)

  path = UI.draw_new_path(0) # winding
  UI.draw_path_new_figure(path, X_OFF_LEFT, Y_OFF_TOP)
  UI.draw_path_line_to(path, X_OFF_LEFT, Y_OFF_TOP + graph_height)
  UI.draw_path_line_to(path, X_OFF_LEFT + graph_width, Y_OFF_TOP + graph_height)
  UI.draw_path_end(path)
  UI.draw_stroke(area_draw_params.Context, path, brush, dsp)
  UI.draw_free_path(path)

  # now transform the coordinate space so (0, 0) is the top-left corner of the graph
  UI.draw_matrix_set_identity(matrix)
  UI.draw_matrix_translate(matrix, X_OFF_LEFT, Y_OFF_TOP)
  UI.draw_transform(area_draw_params.Context, matrix)

  # now get the color for the graph itself and set up the brush
  #  uiColorButtonColor(colorButton, &graphR, &graphG, &graphB, &graphA)
  graph_r = Fiddle::Pointer.malloc(8) # double
  graph_g = Fiddle::Pointer.malloc(8) # double
  graph_b = Fiddle::Pointer.malloc(8) # double
  graph_a = Fiddle::Pointer.malloc(8) # double

  UI.color_button_color(color_button, graph_r, graph_g, graph_b, graph_a)
  brush.Type = 0 # solid
  brush.R = graph_r[0, 8].unpack1('d')
  brush.G = graph_g[0, 8].unpack1('d')
  brush.B = graph_b[0, 8].unpack1('d')

  # now create the fill for the graph below the graph line
  path = construct_graph(datapoints, graph_width, graph_height, true)
  brush.A = graph_a[0, 8].unpack1('d') / 2.0
  UI.draw_fill(area_draw_params.Context, path, brush)
  UI.draw_free_path(path)

  # now draw the histogram line
  path = construct_graph(datapoints, graph_width, graph_height, false)
  brush.A = graph_a[0, 8].unpack1('d')
  UI.draw_stroke(area_draw_params.Context, path, brush, dsp)
  UI.draw_free_path(path)
end

handler.Draw         = handler_draw_event

# Assigning to local variables
# This is intended to protect Fiddle::Closure from garbage collection.
# See https://github.com/kojix2/LibUI/issues/8
handler.MouseEvent   = (c1 = Fiddle::Closure::BlockCaller.new(0, [0]) {})
handler.MouseCrossed = (c2 = Fiddle::Closure::BlockCaller.new(0, [0]) {})
handler.DragBroken   = (c3 = Fiddle::Closure::BlockCaller.new(0, [0]) {})
handler.KeyEvent     = (c4 = Fiddle::Closure::BlockCaller.new(1, [0]) { 0 })

UI.freeInitError(init) unless init.nil?

hbox = UI.new_horizontal_box
UI.box_set_padded(hbox, 1)

vbox = UI.new_vertical_box
UI.box_set_padded(vbox, 1)
UI.box_append(hbox, vbox, 0)
UI.box_append(hbox, histogram, 1)

datapoints = Array.new(10) do
  UI.new_spinbox(0, 100).tap do |datapoint|
    UI.spinbox_set_value(datapoint, Random.new.rand(90))
    UI.spinbox_on_changed(datapoint) do
      UI.area_queue_redraw_all(histogram)
    end
    UI.box_append(vbox, datapoint, 0)
  end
end

def set_solid_brush(brush, color, alpha)
  brush.Type = 0 # solid
  brush.R = ((color >> 16) & 0xFF) / 255.0
  brush.G = ((color >> 8) & 0xFF) / 255.0
  brush.B = (color & 0xFF) / 255.0
  brush.A = alpha
  brush
end

set_solid_brush(brush, blue, 1.0)
UI.color_button_set_color(color_button, brush.R, brush.G, brush.B, brush.A)

UI.color_button_on_changed(color_button) do
  UI.area_queue_redraw_all(histogram)
end

UI.box_append(vbox, color_button, 0)

MAIN_WINDOW = UI.new_window('histogram example', 640, 480, 1)
UI.window_set_margined(MAIN_WINDOW, 1)
UI.window_set_child(MAIN_WINDOW, hbox)

should_quit = proc do |_ptr|
  UI.control_destroy(MAIN_WINDOW)
  UI.quit
  0
end

UI.window_on_closing(MAIN_WINDOW, should_quit)
UI.on_should_quit(should_quit)
UI.control_show(MAIN_WINDOW)

UI.main
UI.quit

Glimmer DSL for LibUI Version:

# https://github.com/jamescook/libui-ruby/blob/master/example/histogram.rb

require 'glimmer-dsl-libui'

include Glimmer

X_OFF_LEFT   = 20
Y_OFF_TOP    = 20
X_OFF_RIGHT  = 20
Y_OFF_BOTTOM = 20
POINT_RADIUS = 5
COLOR_BLUE   = 0x1E90FF

@datapoints   = 10.times.map {Random.new.rand(90)}

def graph_size(area_width, area_height)
  graph_width = area_width - X_OFF_LEFT - X_OFF_RIGHT
  graph_height = area_height - Y_OFF_TOP - Y_OFF_BOTTOM
  [graph_width, graph_height]
end

def point_locations(width, height)
  xincr = width / 9.0 # 10 - 1 to make the last point be at the end
  yincr = height / 100.0

  @datapoints.each_with_index.map do |value, i|
    val = 100 - value
    [xincr * i, yincr * val]
  end
end

# method-based custom control representing a graph path
def graph_path(width, height, should_extend, &block)
  locations = point_locations(width, height)
  path {
    first_location = locations[0] # x and y
    figure(first_location[0], first_location[1]) {
      locations.each do |loc|
        line(loc[0], loc[1])
      end
      if should_extend
        line(width, height)
        line(0, height)

        closed true
      end
    }

    # apply a transform to the coordinate space for this path so (0, 0) is the top-left corner of the graph
    transform {
      translate X_OFF_LEFT, Y_OFF_TOP
    }

    block.call
  }
end

window('histogram example', 640, 480) {
  margined true

  horizontal_box {
    vertical_box {
      stretchy false

      10.times do |i|
        spinbox(0, 100) { |sb|
          stretchy false
          value @datapoints[i]

          on_changed do
            @datapoints[i] = sb.value
            @area.queue_redraw_all
          end
        }
      end

      @color_button = color_button {
        stretchy false
        color COLOR_BLUE

        on_changed do
          @area.queue_redraw_all
        end
      }
    }

    @area = area {
      on_draw do |area_draw_params|
        path {
          rectangle(0, 0, area_draw_params[:area_width], area_draw_params[:area_height])

          fill 0xFFFFFF
        }

        graph_width, graph_height = *graph_size(area_draw_params[:area_width], area_draw_params[:area_height])

        path {
          figure(X_OFF_LEFT, Y_OFF_TOP) {
            line(X_OFF_LEFT, Y_OFF_TOP + graph_height)
            line(X_OFF_LEFT + graph_width, Y_OFF_TOP + graph_height)
          }

          stroke 0x000000, thickness: 2, miter_limit: 10
        }

        # now create the fill for the graph below the graph line
        graph_path(graph_width, graph_height, true) {
          fill @color_button.color.merge(a: 0.5)
        }

        # now draw the histogram line
        graph_path(graph_width, graph_height, false) {
          stroke @color_button.color.merge(thickness: 2, miter_limit: 10)
        }
      end
    }
  }
}.show

Basic Transform

examples/basic_transform.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/basic_transform.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/basic_transform'"

Mac

glimmer-dsl-libui-mac-basic-transform.png

Linux

glimmer-dsl-libui-linux-basic-transform.png

New Glimmer DSL for LibUI Version:

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

Login

examples/login.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/login.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/login'"

Mac

glimmer-dsl-libui-mac-login.png glimmer-dsl-libui-mac-login-logged-in.png

Linux

glimmer-dsl-libui-linux-login.png glimmer-dsl-libui-linux-login-logged-in.png

New Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

include Glimmer

window('Login') {
  margined true

  vertical_box {
    form {
      @username_entry = entry {
        label 'Username:'
      }

      @password_entry = password_entry {
        label 'Password:'
      }
    }

    horizontal_box {
      @login_button = button('Login') {
        on_clicked do
          @username_entry.enabled = false
          @password_entry.enabled = false
          @login_button.enabled = false
          @logout_button.enabled = true
        end
      }

      @logout_button = button('Logout') {
        enabled false

        on_clicked do
          @username_entry.text = ''
          @password_entry.text = ''
          @username_entry.enabled = true
          @password_entry.enabled = true
          @login_button.enabled = true
          @logout_button.enabled = false
        end
      }
    }
  }
}.show

Timer

To run this example, install TiMidity and ensure timidity command is in PATH (can be installed via Homebrew on Mac or apt-get on Linux).

examples/timer.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/timer.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/timer'"

Mac

glimmer-dsl-libui-mac-timer.png glimmer-dsl-libui-mac-timer-in-progress.png

Linux

glimmer-dsl-libui-linux-timer.png glimmer-dsl-libui-linux-timer-in-progress.png

New Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

class Timer
  include Glimmer

  SECOND_MAX = 59
  MINUTE_MAX = 59
  HOUR_MAX = 23

  def initialize
    @pid = nil
    @alarm_file = File.expand_path('../sounds/AlanWalker-Faded.mid', __dir__)
    at_exit { stop_alarm }
    setup_timer
    create_gui
  end

  def stop_alarm
    if @pid
      if @th.alive?
        Process.kill(:SIGKILL, @pid)
        @pid = nil
      else
        @pid = nil
      end
    end
  end

  def play_alarm
    stop_alarm
    if @pid.nil?
      begin
        @pid = spawn "timidity -G 0.0-10.0 #{@alarm_file}"
        @th = Process.detach @pid
      rescue Errno::ENOENT
        warn 'Timidty++ not found. Please install Timidity++.'
        warn 'https://sourceforge.net/projects/timidity/'
      end
    end
  end

  def setup_timer
    unless @setup_timer
      Glimmer::LibUI.timer(1) do
        if @started
          seconds = @sec_spinbox.value
          minutes = @min_spinbox.value
          hours = @hour_spinbox.value
          if seconds > 0
            @sec_spinbox.value = seconds -= 1
          end
          if seconds == 0
            if minutes > 0
              @min_spinbox.value = minutes -= 1
              @sec_spinbox.value = seconds = SECOND_MAX
            end
            if minutes == 0
              if hours > 0
                @hour_spinbox.value = hours -= 1
                @min_spinbox.value = minutes = MINUTE_MAX
                @sec_spinbox.value = seconds = SECOND_MAX
              end
              if hours == 0 && minutes == 0 && seconds == 0
                @start_button.enabled = true
                @stop_button.enabled = false
                @started = false
                unless @played
                  play_alarm
                  msg_box('Alarm', 'Countdown Is Finished!')
                  @played = true
                end
              end
            end
          end
        end
      end
      @setup_timer = true
    end
  end

  def create_gui
    window('Timer') {
      margined true

      group('Countdown') {
        vertical_box {
          horizontal_box {
            @hour_spinbox = spinbox(0, HOUR_MAX) {
              stretchy false
              value 0
            }
            label(':') {
              stretchy false
            }
            @min_spinbox = spinbox(0, MINUTE_MAX) {
              stretchy false
              value 0
            }
            label(':') {
              stretchy false
            }
            @sec_spinbox = spinbox(0, SECOND_MAX) {
              stretchy false
              value 0
            }
          }
          horizontal_box {
            @start_button = button('Start') {
              on_clicked do
                @start_button.enabled = false
                @stop_button.enabled = true
                @started = true
                @played = false
              end
            }

            @stop_button = button('Stop') {
              enabled false

              on_clicked do
                @start_button.enabled = true
                @stop_button.enabled = false
                @started = false
              end
            }
          }
        }
      }
    }.show
  end
end

Timer.new

Color The Circles

examples/color_the_circles.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/color_the_circles.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/color_the_circles'"

Mac

glimmer-dsl-libui-mac-color-the-circles.png glimmer-dsl-libui-mac-color-the-circles-lost.png glimmer-dsl-libui-mac-color-the-circles-won.png

Linux

glimmer-dsl-libui-linux-color-the-circles.png glimmer-dsl-libui-linux-color-the-circles-lost.png glimmer-dsl-libui-linux-color-the-circles-won.png

New Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

class ColorTheCircles
  include Glimmer

  WINDOW_WIDTH = 800
  WINDOW_HEIGHT = 600
  CIRCLE_MIN_RADIUS = 15
  CIRCLE_MAX_RADIUS = 50
  MARGIN_WIDTH = 55
  MARGIN_HEIGHT = 155
  TIME_MAX_EASY = 4
  TIME_MAX_MEDIUM = 3
  TIME_MAX_HARD = 2
  TIME_MAX_INSANE = 1

  attr_accessor :score

  def initialize
    @circles_data = []
    @score = 0
    @time_max = TIME_MAX_HARD
    register_observers
    setup_circle_factory
  end

  def register_observers
    observer = Glimmer::DataBinding::Observer.proc do |new_score|
      @score_label.text = new_score.to_s
      if new_score == -20
        msg_box('You Lost!', 'Sorry! Your score reached -20')
        restart_game
      elsif new_score == 0
        msg_box('You Won!', 'Congratulations! Your score reached 0')
        restart_game
      end
    end
    observer.observe(self, :score) # automatically enhances self to become Glimmer::DataBinding::ObservableModel and notify observer on score attribute changes
  end

  def setup_circle_factory
    consumer = Proc.new do
      if @circles_data.empty?
        # start with 3 circles to make more challenging
        add_circle until @circles_data.size > 3
      else
        add_circle
      end
      delay = rand * @time_max
      Glimmer::LibUI.timer(delay, repeat: false, &consumer)
    end
    Glimmer::LibUI.queue_main(&consumer)
  end

  def add_circle
    circle_x_center = rand * (WINDOW_WIDTH - MARGIN_WIDTH - CIRCLE_MAX_RADIUS) + CIRCLE_MAX_RADIUS
    circle_y_center = rand * (WINDOW_HEIGHT - MARGIN_HEIGHT - CIRCLE_MAX_RADIUS) + CIRCLE_MAX_RADIUS
    circle_radius = rand * (CIRCLE_MAX_RADIUS - CIRCLE_MIN_RADIUS) + CIRCLE_MIN_RADIUS
    stroke_color = Glimmer::LibUI.x11_colors.sample
    @circles_data << {
      args: [circle_x_center, circle_y_center, circle_radius],
      fill: nil,
      stroke: stroke_color
    }
    @area.queue_redraw_all
    self.score -= 1 # notifies score observers automatically of change
  end

  def restart_game
    @score = 0 # update variable directly to avoid notifying observers
    @circles_data.clear
  end

  def launch
    menu('Actions') {
      menu_item('Restart') {
        on_clicked do
          restart_game
        end
      }

      quit_menu_item
    }

    menu('Difficulty') {
      radio_menu_item('Easy') {
        on_clicked do
          @time_max = TIME_MAX_EASY
        end
      }

      radio_menu_item('Medium') {
        on_clicked do
          @time_max = TIME_MAX_MEDIUM
        end
      }

      radio_menu_item('Hard') {
        checked true

        on_clicked do
          @time_max = TIME_MAX_HARD
        end
      }

      radio_menu_item('Insane') {
        on_clicked do
          @time_max = TIME_MAX_INSANE
        end
      }
    }

    menu('Help') {
      menu_item('Instructions') {
        on_clicked do
          msg_box('Instructions', "Score goes down as circles are added.\nIf it reaches -20, you lose!\n\nClick circles to color and score!\nOnce score reaches 0, you win!\n\nBeware of concealed light-colored circles!\nThey are revealed once darker circles intersect them.\n\nThere are four levels of difficulty.\nChange via difficulty menu if the game gets too tough.")
        end
      }
    }

    window('Color The Circles', WINDOW_WIDTH, WINDOW_HEIGHT) {
      margined true

      grid {
        button('Restart') {
          left 0
          top 0
          halign :center

          on_clicked do
            restart_game
          end
        }

        label('Score goes down as circles are added. If it reaches -20, you lose!') {
          left 0
          top 1
          halign :center
        }

        label('Click circles to color and score! Once score reaches 0, you win!') {
          left 0
          top 2
          halign :center
        }

        horizontal_box {
          left 0
          top 3
          halign :center

          label('Score:') {
            stretchy false
          }

          @score_label = label(@score.to_s) {
            stretchy false
          }
        }

        vertical_box {
          left 0
          top 4
          hexpand true
          vexpand true
          halign :fill
          valign :fill

          @area = area {
            on_draw do |area_draw_params|
              path {
                rectangle(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT)

                fill :white
              }

              @circles_data.each do |circle_data|
                path {
                  circle_data[:circle] = circle(*circle_data[:args])

                  fill circle_data[:fill]
                  stroke circle_data[:stroke]
                }
              end
            end

            on_mouse_down do |area_mouse_event|
              clicked_circle_data = @circles_data.find do |circle_data|
                circle_data[:fill].nil? && circle_data[:circle].include?(area_mouse_event[:x], area_mouse_event[:y])
              end
              if clicked_circle_data
                clicked_circle_data[:fill] = clicked_circle_data[:stroke]
                @area.queue_redraw_all
                self.score += 1 # notifies score observers automatically of change
              end
            end
          }
        }

      }
    }.show
  end
end

ColorTheCircles.new.launch

Basic Draw Text

examples/basic_draw_text.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/basic_draw_text.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/basic_draw_text'"

Mac

glimmer-dsl-libui-mac-basic-draw-text.png

Linux

glimmer-dsl-libui-linux-basic-draw-text.png

LibUI Original Version:

require 'libui'

UI = LibUI

UI.init

handler = UI::FFI::AreaHandler.malloc
area    = UI.new_area(handler)

# Michael Ende (1929-1995)
# The Neverending Story is a fantasy novel by German writer Michael Ende,
# The English version, translated by Ralph Manheim, was published in 1983.

TITLE = 'Michael Ende (1929-1995) The Neverending Story'

str1 = \
  '  At last Ygramul sensed that something was coming toward ' \
  'her. With the speed of lightning, she turned about, confronting ' \
  'Atreyu with an enormous steel-blue face. Her single eye had a ' \
  'vertical pupil, which stared at Atreyu with inconceivable malignancy. '

str2 = \
  '  A cry of fear escaped Bastian. '

str3 = \
  '  A cry of terror passed through the ravine and echoed from ' \
  'side to side. Ygramul turned her eye to left and right, to see if ' \
  'someone else had arrived, for that sound could not have been ' \
  'made by the boy who stood there as though paralyzed with ' \
  'horror. '

str4 = \
  '  Could she have heard my cry? Bastion wondered in alarm. ' \
  "But that's not possible. "

str5 = \
  '  And then Atreyu heard Ygramuls voice. It was very high ' \
  'and slightly hoarse, not at all the right kind of voice for that ' \
  'enormous face. Her lips did not move as she spoke. It was the ' \
  'buzzing of a great swarm of hornets that shaped itself into ' \
  'words. '

str = ''
attr_str = UI.new_attributed_string(str)

def attr_str.append(what, color)
  case color
  when :red
    color_attribute = UI.new_color_attribute(0.0, 0.5, 0.0, 0.7)
  when :green
    color_attribute = UI.new_color_attribute(0.5, 0.0, 0.25, 0.7)
  end
  start = UI.attributed_string_len(self)
  UI.attributed_string_append_unattributed(self, what)
  UI.attributed_string_set_attribute(self, color_attribute, start, start + what.size)
  UI.attributed_string_append_unattributed(self, "\n\n")
end

attr_str.append(str1, :green)
attr_str.append(str2, :red)
attr_str.append(str3, :green)
attr_str.append(str4, :red)
attr_str.append(str5, :green)

Georgia = 'Georgia'

handler_draw_event = Fiddle::Closure::BlockCaller.new(0, [1, 1, 1]) do |_, _, adp|
  area_draw_params = UI::FFI::AreaDrawParams.new(adp)
  default_font = UI::FFI::FontDescriptor.malloc
  default_font.Family = Georgia
  default_font.Size = 13
  default_font.Weight = 500
  default_font.Italic = 0
  default_font.Stretch = 4
  params = UI::FFI::DrawTextLayoutParams.malloc

  # UI.font_button_font(font_button, default_font)
  params.String = attr_str
  params.DefaultFont = default_font
  params.Width = area_draw_params.AreaWidth
  params.Align = 0
  text_layout = UI.draw_new_text_layout(params)
  UI.draw_text(area_draw_params.Context, text_layout, 0, 0)
  UI.draw_free_text_layout(text_layout)
end

handler.Draw         = handler_draw_event
# Assigning to local variables
# This is intended to protect Fiddle::Closure from garbage collection.
handler.MouseEvent   = (c1 = Fiddle::Closure::BlockCaller.new(0, [0]) {})
handler.MouseCrossed = (c2 = Fiddle::Closure::BlockCaller.new(0, [0]) {})
handler.DragBroken   = (c3 = Fiddle::Closure::BlockCaller.new(0, [0]) {})
handler.KeyEvent     = (c4 = Fiddle::Closure::BlockCaller.new(0, [0]) {})

box = UI.new_vertical_box
UI.box_set_padded(box, 1)
UI.box_append(box, area, 1)

main_window = UI.new_window(TITLE, 600, 400, 1)
UI.window_set_margined(main_window, 1)
UI.window_set_child(main_window, box)

UI.window_on_closing(main_window) do
  UI.control_destroy(main_window)
  UI.quit
  0
end
UI.control_show(main_window)

UI.main
UI.quit

Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

# Michael Ende (1929-1995)
# The Neverending Story is a fantasy novel by German writer Michael Ende,
# The English version, translated by Ralph Manheim, was published in 1983.
class BasicDrawText
  include Glimmer

  def alternating_color_string(initial: false, &block)
    @index = 0 if initial
    @index += 1
    string {
      if @index.odd?
        color r: 0.5, g: 0, b: 0.25, a: 0.7
      else
        color r: 0, g: 0.5, b: 0, a: 0.7
      end

      block.call + "\n\n"
    }
  end

  def launch
    window('Michael Ende (1929-1995) The Neverending Story', 600, 400) {
      margined true

      area {
        text { # default arguments for x, y, and width are (0, 0, area_draw_params[:area_width])
          # align :left # default alignment
          default_font family: 'Georgia', size: 13, weight: :medium, italic: :normal, stretch: :normal

          alternating_color_string(initial: true) {
            '  At last Ygramul sensed that something was coming toward ' \
            'her. With the speed of lightning, she turned about, confronting ' \
            'Atreyu with an enormous steel-blue face. Her single eye had a ' \
            'vertical pupil, which stared at Atreyu with inconceivable malignancy. '
          }
          alternating_color_string {
            '  A cry of fear escaped Bastian. '
          }
          alternating_color_string {
            '  A cry of terror passed through the ravine and echoed from ' \
            'side to side. Ygramul turned her eye to left and right, to see if ' \
            'someone else had arrived, for that sound could not have been ' \
            'made by the boy who stood there as though paralyzed with ' \
            'horror. '
          }
          alternating_color_string {
            '  Could she have heard my cry? Bastion wondered in alarm. ' \
            "But that's not possible. "
          }
          alternating_color_string {
            '  And then Atreyu heard Ygramuls voice. It was very high ' \
            'and slightly hoarse, not at all the right kind of voice for that ' \
            'enormous face. Her lips did not move as she spoke. It was the ' \
            'buzzing of a great swarm of hornets that shaped itself into ' \
            'words. '
          }
        }
      }
    }.show
  end
end

BasicDrawText.new.launch

Glimmer DSL for LibUI Version 2:

require 'glimmer-dsl-libui'

# Michael Ende (1929-1995)
# The Neverending Story is a fantasy novel by German writer Michael Ende,
# The English version, translated by Ralph Manheim, was published in 1983.
class BasicDrawText
  include Glimmer

  def alternating_color_string(initial: false, &block)
    @index = 0 if initial
    @index += 1
    string {
      if @index.odd?
        color r: 0.5, g: 0, b: 0.25, a: 0.7
      else
        color r: 0, g: 0.5, b: 0, a: 0.7
      end

      block.call + "\n\n"
    }
  end

  def launch
    window('Michael Ende (1929-1995) The Neverending Story', 600, 400) {
      margined true

      area {
        on_draw do |area_draw_params|
          text { # default arguments for x, y, and width are (0, 0, area_draw_params[:area_width])
            # align :left # default alignment
            default_font family: 'Georgia', size: 13, weight: :medium, italic: :normal, stretch: :normal

            alternating_color_string(initial: true) {
              '  At last Ygramul sensed that something was coming toward ' \
              'her. With the speed of lightning, she turned about, confronting ' \
              'Atreyu with an enormous steel-blue face. Her single eye had a ' \
              'vertical pupil, which stared at Atreyu with inconceivable malignancy. '
            }
            alternating_color_string {
              '  A cry of fear escaped Bastian. '
            }
            alternating_color_string {
              '  A cry of terror passed through the ravine and echoed from ' \
              'side to side. Ygramul turned her eye to left and right, to see if ' \
              'someone else had arrived, for that sound could not have been ' \
              'made by the boy who stood there as though paralyzed with ' \
              'horror. '
            }
            alternating_color_string {
              '  Could she have heard my cry? Bastion wondered in alarm. ' \
              "But that's not possible. "
            }
            alternating_color_string {
              '  And then Atreyu heard Ygramuls voice. It was very high ' \
              'and slightly hoarse, not at all the right kind of voice for that ' \
              'enormous face. Her lips did not move as she spoke. It was the ' \
              'buzzing of a great swarm of hornets that shaped itself into ' \
              'words. '
            }
          }
        end
      }
    }.show
  end
end

BasicDrawText.new.launch

Custom Draw Text

examples/custom_draw_text.rb

Run with this command from the root of the project if you cloned the project:

ruby -r './lib/glimmer-dsl-libui' examples/custom_draw_text.rb

Run with this command if you installed the Ruby gem:

ruby -r glimmer-dsl-libui -e "require 'examples/custom_draw_text'"

Mac

glimmer-dsl-libui-mac-custom-draw-text.png glimmer-dsl-libui-mac-custom-draw-text-changed.png

Linux

glimmer-dsl-libui-linux-custom-draw-text.png glimmer-dsl-libui-linux-custom-draw-text-changed.png

New Glimmer DSL for LibUI Version:

require 'glimmer-dsl-libui'

# Michael Ende (1929-1995)
# The Neverending Story is a fantasy novel by German writer Michael Ende,
# The English version, translated by Ralph Manheim, was published in 1983.
class CustomDrawText
  include Glimmer

  def launch
    window('Michael Ende (1929-1995) The Neverending Story', 600, 400) {
      margined true

      vertical_box {
        form {
          stretchy false

          font_button { |fb|
            label 'Font'

            on_changed do
              @string.font = fb.font
            end
          }
          color_button { |cb|
            label 'Color'

            on_changed do
              @string.color = cb.color
            end
          }
          color_button { |cb|
            label 'Background'

            on_changed do
              @string.background = cb.color
            end
          }
          combobox { |c|
            label 'Underline'
            items Glimmer::LibUI.enum_symbols(:underline).map(&:to_s).map {|word| word.split('_').map(&:capitalize).join(' ')}
            selected 'None'

            on_selected do
              @string.underline = c.selected_item.underscore
            end
          }
        }

        area {
          text { # default arguments for x, y, and width are (0, 0, area_draw_params[:area_width])
            # align :left # default alignment

            @string = string {
              '  At last Ygramul sensed that something was coming toward ' \
              'her. With the speed of lightning, she turned about, confronting ' \
              'Atreyu with an enormous steel-blue face. Her single eye had a ' \
              'vertical pupil, which stared at Atreyu with inconceivable malignancy. ' \
              "\n\n" \
              '  A cry of fear escaped Bastian. ' \
              "\n\n" \
              '  A cry of terror passed through the ravine and echoed from ' \
              'side to side. Ygramul turned her eye to left and right, to see if ' \
              'someone else had arrived, for that sound could not have been ' \
              'made by the boy who stood there as though paralyzed with ' \
              'horror. ' \
              "\n\n" \
              '  Could she have heard my cry? Bastion wondered in alarm. ' \
              "But that's not possible. " \
              "\n\n" \
              '  And then Atreyu heard Ygramuls voice. It was very high ' \
              'and slightly hoarse, not at all the right kind of voice for that ' \
              'enormous face. Her lips did not move as she spoke. It was the ' \
              'buzzing of a great swarm of hornets that shaped itself into ' \
              'words. ' \
              "\n\n"
            }
          }
        }
      }
    }.show
  end
end

CustomDrawText.new.launch

New Glimmer DSL for LibUI Version 2:

require 'glimmer-dsl-libui'

# Michael Ende (1929-1995)
# The Neverending Story is a fantasy novel by German writer Michael Ende,
# The English version, translated by Ralph Manheim, was published in 1983.
class CustomDrawText
  include Glimmer

  def launch
    window('Michael Ende (1929-1995) The Neverending Story', 600, 400) {
      margined true

      vertical_box {
        form {
          stretchy false

          font_button { |fb|
            label 'Font'

            on_changed do
              @font = fb.font
              @area.queue_redraw_all
            end
          }
          color_button { |cb|
            label 'Color'

            on_changed do
              @color = cb.color
              @area.queue_redraw_all
            end
          }
          color_button { |cb|
            label 'Background'

            on_changed do
              @background = cb.color
              @area.queue_redraw_all
            end
          }
          combobox { |c|
            label 'Underline'
            items Glimmer::LibUI.enum_symbols(:underline).map(&:to_s).map {|word| word.split('_').map(&:capitalize).join(' ')}
            selected 'None'

            on_selected do
              @underline = c.selected_item.underscore
              @area.queue_redraw_all
            end
          }
        }

        @area = area {
          on_draw do |area_draw_params|
            text { # default arguments for x, y, and width are (0, 0, area_draw_params[:area_width])
              # align :left # default alignment

              @string = string {
                font @font unless @font.nil?
                color @color unless @color.nil?
                background @background unless @background.nil?
                underline @underline unless @underline.nil?

                '  At last Ygramul sensed that something was coming toward ' \
                'her. With the speed of lightning, she turned about, confronting ' \
                'Atreyu with an enormous steel-blue face. Her single eye had a ' \
                'vertical pupil, which stared at Atreyu with inconceivable malignancy. ' \
                "\n\n" \
                '  A cry of fear escaped Bastian. ' \
                "\n\n" \
                '  A cry of terror passed through the ravine and echoed from ' \
                'side to side. Ygramul turned her eye to left and right, to see if ' \
                'someone else had arrived, for that sound could not have been ' \
                'made by the boy who stood there as though paralyzed with ' \
                'horror. ' \
                "\n\n" \
                '  Could she have heard my cry? Bastion wondered in alarm. ' \
                "But that's not possible. " \
                "\n\n" \
                '  And then Atreyu heard Ygramuls voice. It was very high ' \
                'and slightly hoarse, not at all the right kind of voice for that ' \
                'enormous face. Her lips did not move as she spoke. It was the ' \
                'buzzing of a great swarm of hornets that shaped itself into ' \
                'words. ' \
                "\n\n"
              }
            }
          end
        }
      }
    }.show
  end
end

CustomDrawText.new.launch

Contributing to glimmer-dsl-libui

  • 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.

Help

Issues

You may submit issues on GitHub.

Click here to submit an issue.

Chat

If you need live help, try to Join the chat at https://gitter.im/AndyObtiva/glimmer

Process

Glimmer Process

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. You are welcome to contribute more feature suggestions.

TODO.md

Change Log

CHANGELOG.md

Contributors

Click here to view contributor commits.

License

MIT

Copyright (c) 2021 Andy Maleh

--

Built for Glimmer (DSL Framework).