Skip to content
Cecil Coupe edited this page Nov 14, 2018 · 14 revisions

Nov 13, 2018 - Add GridLayout Example. API changed Oct, 23 2018.

We define a new Shoes slot like 'thing' called 'layout' so we have flow, stack and layout. layout has many of the methods and style settings that flow does but it adds a couple of new ones and doesn't implement others. The goal is to provide a way to provide user written (Shoes/Ruby) layouts. We also intend to provide one or more other built in new layout schemes written in C.

Widget or 'ele' below refers to the Shoes native widgets like buttons or edit_lines, textblocks like para, images, svgs, plots or video visual things. Perhaps art elements to. The things you put in slots.

class MyLayout
...
end
Shoes.app do
  layout use: MyLayout.new(), width: 400, height: 300 do
    button "One"
    para "Two"
  end
end

Currently, Shoes does not compute the size of the widgets inside the user layout and has no way for you to pass that information to Shoes. Therefore, unlike flows and stacks, user layouts must specify height and width. They can be resized after creation which is a related but different concept.

Layout Protocol

The use: hash argument should include an object. Shoes will call methods of that object/class at certain points. In computer terminology that means there is a protocol to follow. What the methods do depends on what the author wants. Shoes just requires they exist with the proper arguments and return the correct value.

class MyLayout
  attr_accessor :canvas
  def initialize()
  end

  def setup(canvas, attributes)
    @canvas = canvas
  end

  def add(canvas, widget, attributes)
  end

  def remove(canvas, widget, position)
    return true
  end

  def clear()
  end

  def size(canvas, pass)
  end

  def finish()
  end

  def rules(arg)
  end
end

initialize(args...)

This is the standard Ruby initialize for your class. Arguments can be what ever you like.

setup(canvas, attributes)

This is called after the layout slot's canvas has been created. The attributes argument is the styles hash. Note that hash keys are symbols. :width and :height for example. Be cautious. There may or may not be the key you want and the value may not be what you expect. The canvas width is often 0 (zero) heights are often 0 (zero)

It is highly recommended to keep a copy of the canvas argument. The finish() callback will thank you.

add(canvas, widget, attributes)

This method is called after the widget has been added to the layout canvas/slot contents array. Attributes that the user supplied are available. Note: you can send in an style argument that Shoes doesn't define but you can use, for example,

  @ml = layout manager: MyLayout.new() do
    button "Press Me:, name: 'widget-1'
    para "User Info Demo", name: 'widget-2'
  end

:name is not a Shoes style but in the add method you can get it name = attributes && attributes[:name]. This is a feature. As a layout author, you can use this to establish a protocol with your layout user.

At some point, either here in add() or in finialize() you need to widget.move x,y to the proper place in your layout. Until then, Shoes thinks this is flow slot. It's the move that tells Shoes not to manage it and just leave it where it is. We depend this feature.

Note: In Shoes a background is a widget and it's in the canvas.contents array. Your add method will not be called when a background is added. Because you shouldn't move it around. It does that by itself.

remove(canvas, widget, position)

This is called before the widget is deleted from the canvas/slot at the given position. The position is probably the numeric position in the contents array of the canvas. You can return nil if you don't want it deleted. That's not user friendly but it's your layout. return true is recommended.

clear()

This method is called when all contents have gone away - when the slot/canvas has been deleted. An orderly Shoes quit will call this. You might not do anything here, but if you opened a SQL data base in initialize() or setup() you would want to close it here.

size(canvas, pass)

This method can be called for several situations. It will be called after Shoes has added the layout canvas and the layout {block} has run and widgets added by the block. This is when pass == 0. Odds are, there is nothing you want to do for pass == 0

Pass == 1 or higher is called when Shoes wants to paint - which is frequently. To avoid performance problems Shoes attempts to only call when the size changes. For many layout situations this might be the correct time to do an internal call to finish() for drawing contents in the new locations.

finish()

This will be called when the layout user issues a 'finish' method call. If you do all the layout in add()/remove() then this method can be short. However, if your layout requires knowing when to compute and move() all the widgets in the layout you would do that here. See the size method and setup methods. They often need to work together. Also rules() may be appropriate.

DO NOT ITERATE ATTRIBUTES

DO NOT iterate over the attributes/style hash. It has keys that will crash Shoes it you try. Even inspect can crash Shoes.

rules(arg)

This method is called when the user calls the layout rules(arg). Like finish() this a pass-thru call from the layout to the class representing the layout. It's a Shoes thing. The arg is undefined at this point. It could be an xml string or file containing layout instructions or something like that.

GridLayout - an example

This is known as the grid or gridbag layout in Java, Gtk and others. Now it can be done in Shoes! grid2

# Tests/layout/l4.rb - grid layout

class GridLayout

  attr_accessor :ncol, :nrow, :colsz, :rowsz, :widgets, :hpad, :vpad

  def initialize(attr = {})
    @ncol = 0
    @nrow = 0
    @colsz = 0
    @rowsz = 0
    @widgets = []
    @hpad = attr[:hpad] ? attr[:hpad] : 1
    @vpad = attr[:vpad] ? attr[:vpad] : 1
  end
  
  def setup(canvas, attr)
  end

  def add(canvas, ele, attr)
    col = attr[:col] ? attr[:col]-1 : 0
    row = attr[:row] ? attr[:row]-1 : 0
    rspan = attr[:rspan] 
    cspan = attr[:cspan] 
    @ncol = [@ncol, col + (cspan ? cspan : 1)].max
    @nrow = [@nrow, row + (rspan ? rspan : 1)].max
    widgets << {ele: ele, col: col, row: row, cspan: cspan, rspan: rspan}
   end
  
  def remove
  end
  
  def clear
  end
    
  def size (canvas, pass)
    return if pass == 0
    @rowsz = (canvas.height / @nrow).to_i
    @colsz = (canvas.width / @ncol).to_i
    @widgets.each do |entry| 
      x = entry[:col] * @colsz + @hpad
      y = entry[:row] * @rowsz + @vpad
      widget = entry[:ele]
      widget.move(x, y)
      if entry[:cspan]
        w = entry[:cspan] * @colsz - @hpad
        if widget.width != w 
          widget.style width: w
        end
      end
      if entry[:rspan]
        h = entry[:rspan] * @rowsz - @vpad
        if widget.height != h
          widget.style height: h
        end
      end
    end
  end
  
  def finish
  end
      
end  

Shoes.app width: 400, height: 400 do 
  stack do
    para "Before Grid"
    grid = GridLayout.new  vpad: 5, hpad: 3
    @lay = layout use: grid, width: 300, height: 305 do
      background aliceblue
      button "one", col: 1, row: 1, cspan: 2, rspan: 1 
      button "two", col: 3, row: 1, cspan: 2, rspan: 2
      para "Long String of characters", col: 2, row: 5, cspan: 2
      button "three", col: 2, row: 3, cspan: 1
      svg "#{DIR}/samples/good/paris.svg", width: 10, height: 10, col: 3, row: 3, rspan: 2,
        cspan: 3
      image "http://shoesrb.com/img/shoes-icon.png", row: 5, col: 4, cspan: 2, rspan: 2
      edit_line "foobar", row: 7, col: 1, cspan: 2
    end
    flow do
      button "+" do
        @lay.style width: (@lay.width * 1.1).to_i
      end
      button "-" do
        @lay.style width: (@lay.width * 0.9).to_i
      end
    end
  end
end

It's fun to play with and discover the rules. There's a lot going on in that short section of code. For instance, column and row numbers start with 1 not 0. Because that seems normal to me when talking about rows and columns. You can change it in two lines. Find them. If you do not include a cspan or rspan then the widget goes with the native size. That behavior is interesting when it comes to para.

App level

At some point there will be a way to set a layout for a Shoes.app (it's only a 'use:' arg to the Shoes.app hash with some additional plumbing.

Internal Layouts

At some point Shoes may include some new internal layout managers written in C. Instead of a layout class object they would be identified by a ruby symbol. For example @ml = layout use: :Vfl, width: 500, height: 300 This does not conflict with a user supplied Class named 'Vfl'. Two different things.

There is a VFL parser in Shoes 3.3.8 -r3272.

class Sample
  attr_accessor :widgets, :canvas
  
  def initialize()
    @widgets = {}
  end
  
  def setup(canvas, attr)
    @canvas = canvas
  end
  
  def add(canvas, widget, attrs)
    name = attrs && attrs[:name]
    @widgets[name] = widget
  end
  
  def contents
    return @widgets
  end
  
  def remove(canvas, widget, pos)
    return true
  end

  def size(canvas, pass)
  end
  
  def clear()
  end
  
  def finish()
  end
end

Shoes.app width: 350, height: 400, resizeable: true do
  stack do
    para "Test vfl parser"
    @cls = Sample.new()
    @lay = layout use: @cls, width: 300, height: 300 do
      para "OverConstrained", name: 'para1'
      edit_line "one", name: 'el1'
      button "two", name: 'but1'
      button "three", name: "but2"
      button "four", name: "but3"
    end
    @lay.start {
      metrics = {
        el1: 80.7, # what does this mean?
      }
      lines = [
        "H:|-[para1(but1)]-[but1]-|",
        "H:|-[el1(but2)]-[but2]-|",
        "H:[but3(but2)]-|",
        "V:|-[para1(el1)]-[el1]-|",
        "V:|-[but1(but2,but3)]-[but2]-[but3]-|"
      ]
      if @lay.vfl_parse lines: lines, views: @cls.contents, metrics: metrics
        constraints = @lay.vfl_constraints
        # display only!
        constraints.each { |c| $stderr.puts c.inspect }
        @lay.finish constraints 
      end
    }
 end
  para "After layout"
end

vfl.parse accepts a hash with 3 symbol keys, 'lines', 'views', and 'metrics'. This standard terminology for autolayout and friends. The views arg is hash of names to shoes elements/widgets. These names are used in the VFL lines. Most vfl examples on the call them 'views' so we do too. lines' is an Array of strings. Metrics is a hash of view name to a double. Unsure what that does.

The constraints retrieved from vfl_constraints is a Ruby array of constraint hashes. The 5 lines above generated 20 constraints. The mapping of the strings and things into something you could use with the cassowary-ruby gem is to be discovered by someone clever. 'Super' probably means the canvas (ie the Shoes layout space).

References

AutoLayout

  1. Gtk emeus
  2. Apple VFS

iOS AutoLayout Videos

  1. Part 1
  2. Part 2

Menu

In This Section:

Clone this wiki locally