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:
`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:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
Here is the `code_area` implementation code (not very long, eh!):
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
Here is how to build class-based custom controls (`address_form` and `address_view`):
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
And, here is how reusable custom controls look like (two address forms and two address views):
Happy Glimmering!
No comments:
Post a Comment