Martin DeMello's Gooey Challenge

June 12th 12:57
by why

Martin DeMello: One of the most interesting facets of a desktop GUI system is how easy it makes it to go off the beaten track, particularly how well you can add “first class” components to the system. (Using ‘first class’ here to mean ‘on an equal footing with the widgets supplied by the toolkit’). Also, as a ruby programmer, I’d naturally rather not drop down into C (or Java) to do this.

So how does Shoes hold up against this challenge? And i mean: without hacking anything new into Shoes, using syntax introduced before the challenge was posed.

Well, let’s go over the four parts of the contest:

  1. A component consisting of a series of existing components hooked together to act as a single widget. (Such as an icon widget that incorporates a picture and a textfield. With options to turn off or size the image and make the text editable.)
  2. A component built ‘from scratch’ atop a canvas, that is, handling its own drawing and event management. (Like a speedometer-type dial with a configurable range and tick interval.)
  3. A component combining a canvas and existing widgets. (For example, a box that holds a component and paints a customised border around it.)
  4. A container that takes a collection of widgets and lays them out according to some userdefined algorithm. (He suggests a pure-ruby implementation of a wrapbox, but I thought this point might be better illustrated by a cascading container.)

My initial response had a few simple code examples, but I didn’t take the chance to do all the examples just as Martin described. That e-mail touches on how Shoes custom widgets work.

Now I’ve had a minute and I’d like to present just my new, unadorned entries. In each of these, a custom Shoes widget is setup by inheriting from the Widget class. And Shoes then creates a method using the lowercased name of the class which is used in the app. (And you can try these out with today’s super-fresh recent builds.)


Icon Widget (challenge1.rb)

class Icon < Widget
  attr_accessor :image, :caption
  def initialize opts = {}
    @stack = stack
    @image = @stack.image(*opts[:image]) if opts[:image]
    @caption = @stack.para(*opts[:text])
  end
  def edit
    return if @edit
    @caption.hide
    @stack.append do
      @edit = edit_line :width => 200, :text => @caption
    end
  end
  def save
    return unless @edit
    @caption.replace @edit.text
    @edit.remove
    @caption.show
  end
end

Shoes.app do
  stack do
    @icon = icon :image => "static/shoes-icon.png",
                 :text => "Welcome!"

    button("image.hide") { @icon.image.hide }
    button("image.show") { @icon.image.show }
    button("image.size") { @icon.image.style :width => 64, :height => 64 }
    button("text.edit") { @icon.edit }
    button("text.save") { @icon.save }
  end
end

Speedometer Widget (challenge2.rb)

class Speedometer < Widget
  attr_accessor :range, :tick, :position
  def initialize opts = {}
    @range = opts[:range] || 200
    @tick = opts[:tick] || 10
    @position = opts[:position] || 0
    @cx, @cy = self.left + 110, self.top + 100

    nostroke
    rect :top => self.top, :left => self.left,
      :width => 220, :height => 200
    nofill
    stroke white
    oval :left => @cx - 50, :top => @cy - 50, :radius => 50
    (ticks + 1).times do |i|
      radial_line 225 + ((270.0 / ticks) * i), 70..80
      radial_line 225 + ((270.0 / ticks) * i), 45..49
    end
    strokewidth 2
    oval :left => @cx - 70, :top => @cy - 70, :radius => 70
    stroke lightgreen
    oval :left => @cx - 5, :top => @cy - 5, :radius => 5
    @needle = radial_line 225 + ((270.0 / @range) * @position), 0..90
  end
  def ticks; @range / @tick end
  def radial_line deg, r
    pos = ((deg / 360.0) * (2.0 * Math::PI)) - (Math::PI / 2.0)
    line (Math.cos(pos) * r.begin) + @cx, (Math.sin(pos) * r.begin) + @cy,
      (Math.cos(pos) * r.end) + @cx, (Math.sin(pos) * r.end) + @cy
  end
  def position= pos
    @position = pos
    @needle.remove
    append do
      @needle = radial_line 225 + ((270.0 / @range) * @position), 0..90
    end
  end
end

Shoes.app do
  stack do
    para "Enter a number between 0 and 100"
    flow do
      @p = edit_line
      button "OK" do
        @s.position = @p.text.to_i
      end
    end

    @s = speedometer :range => 100, :ticks => 10
  end
end

Native Button with Custom Border (challenge3.rb)

class BorderButton < Widget
  def initialize *args, &blk
    opts = args.detect { |a| a.is_a? Hash }

    border opts[:border], :strokewidth => opts[:strokewidth]

    args[args.index(opts)] = opts.
      merge(:width => opts[:width] - (opts[:strokewidth] * 2))
    stack(:margin => opts[:strokewidth]).button *args, &blk
  end
end

Shoes.app do
  borderbutton "OK", :width => 200,
    :strokewidth => 4, :border => "#000".."#FFF" do
      alert("PROOF!")
  end   
end

Cascading Container (challenge4.rb)

class Cascade < Widget
  def initialize &blk
    instance_eval &blk
  end
  def draw(a,b)
    x, y = 0, 0
    contents.each do |e|
      if x != e.left && y != e.top
        e.move x, y
      end
      x += e.height
      y += e.width
    end
    super(a,b)
  end
end

Shoes.app do
  cascade do
    button "1"
    button "2"
    button "3"
  end
end

Now begin the comments …

5 comments

Awesome!

said on June 12th 16:18

I’m giving it a try right now…

kamran

said on June 13th 00:12

This is very cool

David

said on June 18th 13:49

What happened to Hoodwink’d? Why is http://hoodwink.d sending me to a site titled “Online Generic Viagra”?

cygo

said on June 18th 15:34

A competitor?

http://www.sproutcore.com

Axis

said on June 21st 13:41

In addition to a repository of Shoes apps, how about a repository of Shoes widgets? That could keep the core of Shoes simple, while also allowing you to go shopping for all those exotic shoes that other toolkits have.

Comments are closed for this entry.