Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save altamic/176557 to your computer and use it in GitHub Desktop.
Save altamic/176557 to your computer and use it in GitHub Desktop.

Martin DeMello's Gooey Challenge

Original URL: http://hackety.org/2008/06/12/martinDemellosGooeyChallenge.html

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment