« Ruby Glimmer Days 2021, January 26th to January 29th - 4 Days of Ruby (Desktop) Gems

Day 1 - glimmer Gem - Make Desktop Apps Shine Using a Script with Two-Way Data Binding - Inside the Domain-Specific Language Engine / Construction Kit - Beware of Imitators! The Original Since 2007

Written by AndyObtiva Andy Maleh

Software Engineering Expert from Montreal, Quebec. Creator of Glimmer and Abstract Feature Branch. Speaker at RailsConf, RubyConf, AgileConf, EclipseCon, EclipseWorld. Master in Software Engineering, DePaul University, Chicago. Blogs at Code Mastery Takes Commitment To Bold Coding Adventures. Snowboarder and Drummer.

What is Glimmer?

Glimmer started out as a domain-specific language (DSL) for the Standard Widget Toolkit (SWT) and grew into a domain-specific language (DSL) engine supporting scripting multiple graphical user interfaces. Glimmer’s namesake is referring to the glimmer of ruby in graphical user interfaces.

Glimmer consists of:

What makes Glimmer special?

Glimmer offers:

Glimmer currently supports:

The Glimmer Language Scripting Syntax

Glimmer is fundamentally a domain-specific language engine and syntax consists mainly of:

The Glimmer engine allows mixing of languages, which comes in handy when doing things like rendering a desktop browser widget additionally leveraging the HTML and CSS domain-specific languages for its content.

Domain-specific languages are activated by top-level keywords (expressions denoted as TopLevelExpression). For example, the html keyword activates the Glimmer for XML and the css keyword activates the Glimmer for CSS. Glimmer automatically recognizes top-level keywords in each language and activates the it accordingly. Once done processing a nested top-level keyword, Glimmer switches back to the prior language automatically.

By default, all loaded domain-specific languages (required gems) are enabled.

For example, this shows “Hello, World!” inside a Glimmer for SWT desktop app browser widget using html and css from Glimmer for XML and Glimmer for CSS:

require 'glimmer-dsl-swt'
require 'glimmer-dsl-xml'
require 'glimmer-dsl-css'

include Glimmer

shell {
  minimum_size 130, 130
  @browser = browser {
    text html {
      head {
        meta(name: "viewport", content: "width=device-width, initial-scale=2.0")
        style {
          css {
            h1 {
              background 'yellow'
            }
          }
        }
      }
      body {
        h1 { "Hello, World!" }
      }
    }
  }
}.open

Inside the Domain-Specific Language (DSL) Engine

The Glimmer engine’s architecture is based on the following design patterns and data structures:

Glimmer’s use of the Interpreter Design Pattern in processing language scripts is also known as the Virtual Machine Architectural Style. After all, expressions are virtual machine opcodes that process nested keywords stored in a stack. I built Glimmer’s original domain-specific language back in 2007 without knowing the Virtual Machine Architectural Style (except perhaps as an esoteric technology powering java), but stumbled upon it anyways through following the Gang of Four Design Patterns mentioned above, chiefly the Interpreter Design Pattern.

Every keyword in a Glimmer language is represented by an expression that is processed by an Expression subclass selected from a chain of expressions (interpreters) pre-configured in a domain-specific language chain of responsibility via Glimmer::DSL::Engine.add_dynamic_expressions(DSLNameModule, expression_names_array).

Expressions are either:

Optionally, expressions can be parent expressions that contain other expressions, and must include the ParentExpression mixin module as such.

Additionally, every expression that serves as a top-level entry point into the language must mixin TopLevelExpression

Static expressions are optimized in performance since they pre-define methods on the Glimmer module matching the static keywords they represent (e.g. color causes creating a Glimmer#color method for processing color expressions) and completely bypass as a result the Glimmer Engine Chain of Responsibility. That said, they must be avoided if the same keyword might occur multiple times, but with different requirements for arguments, block, and parenthood type.

Every Expression sublcass must specify two methods at least:

StaticExpression sublcasses may skip the can_interpret? method since they include a default implementation for it that matches the name of the keyword from the class name by convention. For example, a color keyword would have a ColorExpression class, so color is inferred automatically from class name and used in deciding whether the class can handle a color keyword or not.

ParentExpression subclasses can optionally override this extra method, which is included by default and simply invokes the parent’s passed block to process its children:

For example, some parent widgets use their block for other reasons or process their children at very specific times, so they may override that method and disable it, or otherwise call super and do additional work.

Example of a dynamic expression:

module Glimmer
  module DSL
    module SWT
      class WidgetExpression < Expression
        include ParentExpression

        EXCLUDED_KEYWORDS = %w[shell display tab_item]

        def can_interpret?(parent, keyword, *args, &block)
          !EXCLUDED_KEYWORDS.include?(keyword) and
            parent.respond_to?(:swt_widget) and
            Glimmer::SWT::WidgetProxy.widget_exists?(keyword)
        end

        def interpret(parent, keyword, *args, &block)
          Glimmer::SWT::WidgetProxy.create(keyword, parent, args)
        end

        def add_content(parent, &block)
          super
          parent.post_add_content
        end

      end
    end
  end
end

Example of a static expression (does not need can_interpret?):

module Glimmer
  module DSL
    module Opal
      class ColorExpression < StaticExpression
        include TopLevelExpression

        def interpret(parent, keyword, *args, &block)
          Glimmer::SWT::ColorProxy.new(*args)
        end
      end
    end
  end
end

Epressions go into the glimmer/dsl/{dsl_name} namespace directory.

Also, every domain-specific language requires a glimmer/dsl/{dsl_name}/dsl.rb file, which configures the language into Glimmer via a call to:

Glimmer::DSL::Engine.add_dynamic_expressions(DSLNameModule, expression_names_array)

Expression names are underscored verions of Expression subclass names minus the _expression suffix.

For example, here is an SWT configuration:

require 'glimmer/launcher'
require Glimmer::Launcher.swt_jar_file
require 'glimmer/dsl/engine'
Dir[File.expand_path('../*_expression.rb', __FILE__)].each {|f| require f}

module Glimmer
  module DSL
    module SWT
      Engine.add_dynamic_expressions(
        SWT,
        %w[
          layout
          widget_listener
          combo_selection_data_binding
          checkbox_group_selection_data_binding
          radio_group_selection_data_binding
          list_selection_data_binding
          tree_items_data_binding
          table_items_data_binding
          data_binding
          cursor
          font
          image
          property
          block_property
          widget
          custom_widget
        ]
      )
    end
  end
end

In summary, these are the files needed to author a Glimmer domain-specific language:

Data-Binding Machinery

Data-Binding enables binding graphical user interface properties (like text and color) to model attributes (like name and age).

It relies on the Observer Design Pattern and Model-View-Presenter (MVP) Architectural Pattern - a variation on Model-View-Cotnroller (MVC).

These are the main classes concerning data-binding:

Find Out More

References

Built with Ruby (running Jekyll) on 2021-03-12 22:04:06 +0000 in 0.371 seconds.
Hosted on GitHub Pages. </> Source on GitHub. (0) Dedicated to the public domain.