-
Notifications
You must be signed in to change notification settings - Fork 19
Layout class
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.
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
This is the standard Ruby initialize for your class. Arguments can be what ever you like.
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.
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.
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.
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.
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.
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 over the attributes/style hash. It has keys that will crash Shoes it you try. Even inspect can crash Shoes.
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.
This is known as the grid or gridbag layout in Java, Gtk and others. Now it can be done in Shoes!
# 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.
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.
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).