Glimmer DSL for LibUI Code Area (Ruby Tooling Future)

andyobtiva

Andy Maleh

Posted on March 26, 2022

Glimmer DSL for LibUI Code Area (Ruby Tooling Future)

Brandon Weaver has recently contacted me on the Glimmer Gitter to ask questions about Glimmer DSL for LibUI. He also mentioned the node pattern tool written by Marc-André Lafortune (a fellow Rubyist I know in Montreal), which is hosted on Heroku. Brandon said he was excited about the possibility of implementing something similar in pure Ruby using Glimmer DSL for LibUI by leveraging the rouge syntax highlighting gem. He has even blogged about the Ruby Tooling subject in the past with the title "Future of Ruby - AST Tooling", which Matz (creator of Ruby) has alluded to before.

Now comes the good news! The latest versions of Glimmer DSL for LibUI added support for the following features to facilitate pursuing the Ruby Tooling vision:

  • Class-Based Custom Controls: enable building custom controls (aka widgets) as reusable components that are maintained cleanly in separate classes.
  • Class-Based Custom Windows/Applications: facilitate building applications and reusable custom windows with less boilerplate code.
  • code_area Custom Control: renders syntax highlighted code in an area control.

Given the great productivity benefits of Glimmer DSL for LibUI, I was able to piece together the code_area custom control using the rouge gem in less than an hour.

Here is how the code_area control looks like:

basic code area

code_area opens the doors to so many Ruby Tooling exciting possibilities in Glimmer DSL for LibUI. This is the Ruby community members' chance to be first movers and build Ruby tooling libraries that are scriptable in Ruby (unlike current popular editors).

Here is the examples/basic_code_area.rb code:

# From: https://github.com/AndyObtiva/glimmer-dsl-libui#basic-code-area

require 'glimmer-dsl-libui'

class BasicCodeArea
  include Glimmer::LibUI::Application

  before_body do
    @code = <<~CODE
      # Greets target with greeting
      def greet(greeting: 'Hello', target: 'World')

        puts "\#{greeting}, \#{target}!"
      end

      greet
      greet(target: 'Robert')
      greet(greeting: 'Aloha')
      greet(greeting: 'Aloha', target: 'Nancy')
      greet(greeting: 'Howdy', target: 'Doodle')
    CODE
  end

  body {
    window('Basic Code Area', 400, 300) {
      margined true

      code_area(language: 'ruby', code: @code)
    }
  }
end

BasicCodeArea.launch
Enter fullscreen mode Exit fullscreen mode

Here is the code_area implementation code (not very long, eh!):

# From: https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/lib/glimmer/libui/custom_control/code_area.rb
require 'glimmer/libui/custom_control'

module Glimmer
  module LibUI
    module CustomControl
      class CodeArea
        class << self
          def languages
            require 'rouge'
            Rouge::Lexer.all.map {|lexer| lexer.tag}.sort
          end

          def lexers
            require 'rouge'
            Rouge::Lexer.all.sort_by(&:title)
          end
        end

        include Glimmer::LibUI::CustomControl

        REGEX_COLOR_HEX6 = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/

        option :language, default: 'ruby'
        option :theme, default: 'glimmer'
        option :code

        body {
          area {
            rectangle(0, 0, 8000, 8000) {
              fill :white
            }
            text {
              default_font family: OS.mac? ? 'Consolas' : 'Courier', size: 13, weight: :medium, italic: :normal, stretch: :normal

              syntax_highlighting(code).each do |token|
                style_data = Rouge::Theme.find(theme).new.style_for(token[:token_type])

                string(token[:token_text]) {
                  color style_data[:fg] || :black
                  background style_data[:bg] || :white
                }
              end
            }
          }
        }

        def lexer
          require 'rouge'
          require 'glimmer-dsl-libui/ext/rouge/theme/glimmer'
          # TODO Try to use Rouge::Lexer.find_fancy('guess', code) in the future to guess the language or otherwise detect it from file extension
          @lexer ||= Rouge::Lexer.find_fancy(language)
          @lexer ||= Rouge::Lexer.find_fancy('ruby') # default to Ruby if no lexer is found
        end

        def syntax_highlighting(text)
          return [] if text.to_s.strip.empty?
          @syntax_highlighting ||= {}
          unless @syntax_highlighting.keys.include?(text)
            lex = lexer.lex(text).to_a
            text_size = 0
            @syntax_highlighting[text] = lex.map do |pair|
              {token_type: pair.first, token_text: pair.last}
            end.each do |hash|
              hash[:token_index] = text_size
              text_size += hash[:token_text].size
            end
          end
          @syntax_highlighting[text]
        end
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Here is how to build class-based custom controls (address_form and address_view):

# From: https://github.com/AndyObtiva/glimmer-dsl-libui#class-based-custom-controls

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

Address = Struct.new(:street, :p_o_box, :city, :state, :zip_code)

class FormField
  include Glimmer::LibUI::CustomControl

  options :model, :attribute

  body {
    entry { |e|
      label attribute.to_s.underscore.split('_').map(&:capitalize).join(' ')
      text <=> [model, attribute]
    }
  }
end

class AddressForm
  include Glimmer::LibUI::CustomControl

  options :address

  body {
    form {
      form_field(model: address, attribute: :street)
      form_field(model: address, attribute: :p_o_box)
      form_field(model: address, attribute: :city)
      form_field(model: address, attribute: :state)
      form_field(model: address, attribute: :zip_code)
    }
  }
end

class LabelPair
  include Glimmer::LibUI::CustomControl

  options :model, :attribute, :value

  body {
    horizontal_box {
      label(attribute.to_s.underscore.split('_').map(&:capitalize).join(' '))
      label(value.to_s) {
        text <= [model, attribute]
      }
    }
  }
end

class AddressView
  include Glimmer::LibUI::CustomControl

  options :address

  body {
    vertical_box {
      address.each_pair do |attribute, value|
        label_pair(model: address, attribute: attribute, value: value)
      end
    }
  }
end

class ClassBasedCustomControls
  include Glimmer::LibUI::Application # alias: Glimmer::LibUI::CustomWindow

  before_body do
    @address1 = Address.new('123 Main St', '23923', 'Denver', 'Colorado', '80014')
    @address2 = Address.new('2038 Park Ave', '83272', 'Boston', 'Massachusetts', '02101')
  end

  body {
    window('Class-Based Custom Keyword') {
      margined true

      horizontal_box {
        vertical_box {
          label('Address 1') {
            stretchy false
          }

          address_form(address: @address1)

          horizontal_separator {
            stretchy false
          }

          label('Address 1 (Saved)') {
            stretchy false
          }

          address_view(address: @address1)
        }

        vertical_separator {
          stretchy false
        }

        vertical_box {
          label('Address 2') {
            stretchy false
          }

          address_form(address: @address2)

          horizontal_separator {
            stretchy false
          }

          label('Address 2 (Saved)') {
            stretchy false
          }

          address_view(address: @address2)
        }
      }
    }
  }
end

ClassBasedCustomControls.launch
Enter fullscreen mode Exit fullscreen mode

And, here is how reusable custom controls look like (two address forms and two address views):

class-based custom controls

Happy Glimmering!

💖 💪 🙅 🚩
andyobtiva
Andy Maleh

Posted on March 26, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related