Add a widget hierarchy implementation

A widget hierarchy describes the position of widgets. The hierarchy is a
recursive tree of widget hierarchy instances. This functionality depends on a
:layout function that is not yet implemented on widgets, but will be added
later.

Signed-off-by: Uli Schlachter <psychon@znc.in>
This commit is contained in:
Uli Schlachter 2015-08-12 11:53:01 +02:00
parent 4785b63755
commit 02f67b61b4
3 changed files with 532 additions and 0 deletions

285
lib/wibox/hierarchy.lua Normal file
View File

@ -0,0 +1,285 @@
---------------------------------------------------------------------------
-- Management of widget hierarchies. Each widget hierarchy object has a widget
-- for which it saves e.g. size and transformation in its parent. Also, each
-- widget has a number of children.
--
-- @author Uli Schlachter
-- @copyright 2015 Uli Schlachter
-- @release @AWESOME_VERSION@
-- @module wibox.hierarchy
---------------------------------------------------------------------------
local matrix = require("gears.matrix")
local cairo = require("lgi").cairo
local base = require("wibox.widget.base")
local hierarchy = {}
--- Create a new widget hierarchy that has no parent.
-- @param context The context in which we are laid out.
-- @param widget The widget that is at the base of the hierarchy.
-- @param width The available width for this hierarchy
-- @param height The available height for this hierarchy
-- @param redraw_callback Callback that is called with the corresponding widget
-- hierarchy on widget::redraw_needed on some widget.
-- @param layout_callback Callback that is called with the corresponding widget
-- hierarchy on widget::layout_changed on some widget.
-- @param callback_arg A second argument that is given to the above callbacks.
-- @param root The root of the widget hierarchy or nil if this creates the root.
-- @return A new widget hierarchy
local function hierarchy_new(context, widget, width, height, redraw_callback, layout_callback, callback_arg, root)
local children = base.layout_widget(context, widget, width, height)
local draws_x1, draws_y1, draws_x2, draws_y2 = 0, 0, width, height
local result = {
_parent = nil,
_root = nil,
_matrix = cairo.Matrix.create_identity(),
_widget = widget,
_size = {
width = width,
height = height
},
_draw_extents = nil,
_children = {}
}
result._root = root or result
result._redraw = function() redraw_callback(result, callback_arg) end
result._layout = function() layout_callback(result, callback_arg) end
widget:weak_connect_signal("widget::redraw_needed", result._redraw)
widget:weak_connect_signal("widget::layout_changed", result._layout)
for _, w in ipairs(children or {}) do
local r = hierarchy_new(context, w._widget, w._width, w._height,
redraw_callback, layout_callback, callback_arg, result._root)
r._matrix = w._matrix
r._parent = result
table.insert(result._children, r)
-- Update our drawing extents
local s = r._draw_extents
local px, py, pwidth, pheight = matrix.transform_rectangle(r._matrix,
s.x, s.y, s.width, s.height)
local px2, py2 = px + pwidth, py + pheight
draws_x1 = math.min(draws_x1, px)
draws_y1 = math.min(draws_y1, py)
draws_x2 = math.max(draws_x2, px2)
draws_y2 = math.max(draws_y2, py2)
end
result._draw_extents = {
x = draws_x1,
y = draws_y1,
width = draws_x2 - draws_x1,
height = draws_y2 - draws_y1
}
for k, f in pairs(hierarchy) do
if type(f) == "function" then
result[k] = f
end
end
return result
end
--- Create a new widget hierarchy that has no parent.
-- @param context The context in which we are laid out.
-- @param widget The widget that is at the base of the hierarchy.
-- @param width The available width for this hierarchy.
-- @param height The available height for this hierarchy.
-- @param redraw_callback Callback that is called with the corresponding widget
-- hierarchy on widget::redraw_needed on some widget.
-- @param layout_callback Callback that is called with the corresponding widget
-- hierarchy on widget::layout_changed on some widget.
-- @param callback_arg A second argument that is given to the above callbacks.
-- @return A new widget hierarchy
function hierarchy.new(context, widget, width, height, redraw_callback, layout_callback, callback_arg)
return hierarchy_new(context, widget, width, height, redraw_callback, layout_callback, callback_arg, nil)
end
--- Get the parent hierarchy of this widget hierarchy (or nil).
function hierarchy:get_parent()
return self._parent
end
--- Get the widget that this hierarchy manages.
function hierarchy:get_widget()
return self._widget
end
--- Get a cairo matrix that transforms to the parent's coordinate space from
-- this hierarchy's coordinate system.
-- @return A cairo matrix describing the transformation.
function hierarchy:get_matrix_to_parent()
return matrix.copy(self._matrix)
end
--- Get a cairo matrix that transforms to the base of this hierarchy's
-- coordinate system (aka the coordinate system of the device that this
-- hierarchy is applied upon) from this hierarchy's coordinate system.
-- @return A cairo matrix describing the transformation.
function hierarchy:get_matrix_to_device()
if not self._matrix_to_device then
local m = cairo.Matrix.create_identity()
local state = self
while state ~= nil do
m:multiply(m, state._matrix)
state = state._parent
end
self._matrix_to_device = m
end
return matrix.copy(self._matrix_to_device)
end
--- Get a cairo matrix that transforms from the parent's coordinate space into
-- this hierarchy's coordinate system.
-- @return A cairo matrix describing the transformation.
function hierarchy:get_matrix_from_parent()
local m = self:get_matrix_to_parent()
m:invert()
return m
end
--- Get a cairo matrix that transforms from the base of this hierarchy's
-- coordinate system (aka the coordinate system of the device that this
-- hierarchy is applied upon) into this hierarchy's coordinate system.
-- @return A cairo matrix describing the transformation.
function hierarchy:get_matrix_from_device()
local m = self:get_matrix_to_device()
m:invert()
return m
end
--- Get the extents that this hierarchy possibly draws to (in the current coordinate space).
-- This includes the size of this element plus the size of all children
-- (after applying the corresponding transformation).
-- @return x, y, width, height
function hierarchy:get_draw_extents()
local ext = self._draw_extents
return ext.x, ext.y, ext.width, ext.height
end
--- Get the size that this hierarchy logically covers (in the current coordinate space).
-- @return width, height
function hierarchy:get_size()
local ext = self._size
return ext.width, ext.height
end
--- Get a list of all children.
-- @return List of all children hierarchies.
function hierarchy:get_children()
return self._children
end
--- Compare two widget hierarchies and compute a cairo Region that contains all
-- rectangles that aren't the same between both hierarchies.
-- @param other The hierarchy to compare with
-- @return A cairo Region containing the differences.
function hierarchy:find_differences(other)
local region = cairo.Region.create()
local function needs_redraw(h)
local m = h:get_matrix_to_device()
local p = h._draw_extents
local x, y, width, height = matrix.transform_rectangle(m, p.x, p.y, p.width, p.height)
local x1, y1 = math.floor(x), math.floor(y)
local x2, y2 = math.ceil(x + width), math.ceil(y + height)
region:union_rectangle(cairo.RectangleInt({
x = x1, y = y1, width = x2 - x1, height = y2 - y1
}))
end
local compare
compare = function(self, other)
local s_size, o_size = self._size, other._size
if s_size.width ~= o_size.width or s_size.height ~= o_size.height or
#self._children ~= #other._children or self._widget ~= other._widget or
not matrix.equals(self._matrix, other._matrix) then
needs_redraw(self)
needs_redraw(other)
else
for i = 1, #self._children do
compare(self._children[i], other._children[i])
end
end
end
compare(self, other)
return region
end
--- Does the given cairo context have an empty clip (aka "no drawing possible")?
local function empty_clip(cr)
local x, y, width, height = cr:clip_extents()
return width == 0 or height == 0
end
--- Draw a hierarchy to some cairo context.
-- This function draws the widgets in this widget hierarchy to the given cairo
-- context. The context's clip is used to skip parts that aren't visible.
-- @param context The context in which widgets are drawn.
-- @param cr The cairo context that is used for drawing.
function hierarchy:draw(context, cr)
local widget = self:get_widget()
if not widget.visible then
return
end
cr:save()
cr:transform(self:get_matrix_to_parent())
-- Clip to the draw extents
cr:rectangle(self:get_draw_extents())
cr:clip()
-- Draw if needed
if not empty_clip(cr) then
local opacity = widget.opacity
local function call(func, extra_arg1, extra_arg2)
if not func then return end
local function error_function(err)
print(debug.traceback("Error while drawing widget: " .. tostring(err), 2))
end
if not extra_arg2 then
xpcall(function()
func(widget, arg, cr, self:get_size())
end, error_function)
else
xpcall(function()
func(widget, arg, extra_arg1, extra_arg2, cr, self:get_size())
end, error_function)
end
end
-- Prepare opacity handling
if opacity ~= 1 then
cr:push_group()
end
-- Draw the widget
cr:save()
cr:rectangle(0, 0, self:get_size())
cr:clip()
call(widget.draw)
cr:restore()
-- Draw its children (We already clipped to the draw extents above)
call(widget.before_draw_children)
for i, wi in ipairs(self:get_children()) do
call(widget.before_draw_child, i, wi:get_widget())
wi:draw(context, cr)
call(widget.after_draw_child, i, wi:get_widget())
end
call(widget.after_draw_children)
-- Apply opacity
if opacity ~= 1 then
cr:pop_group_to_source()
cr.operator = cairo.Operator.OVER
cr:paint_with_alpha(opacity)
end
end
cr:restore()
end
return hierarchy
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80

View File

@ -29,6 +29,7 @@ local wibox = { mt = {} }
wibox.layout = require("wibox.layout") wibox.layout = require("wibox.layout")
wibox.widget = require("wibox.widget") wibox.widget = require("wibox.widget")
wibox.drawable = require("wibox.drawable") wibox.drawable = require("wibox.drawable")
wibox.hierarchy = require("wibox.hierarchy")
--- Set the widget that the wibox displays --- Set the widget that the wibox displays
function wibox:set_widget(widget) function wibox:set_widget(widget)

View File

@ -0,0 +1,246 @@
---------------------------------------------------------------------------
-- @author Uli Schlachter
-- @copyright 2015 Uli Schlachter
---------------------------------------------------------------------------
local hierarchy = require("wibox.hierarchy")
local cairo = require("lgi").cairo
local matrix = require("gears.matrix")
local object = require("gears.object")
local function make_widget(children)
local result = object()
result:add_signal("widget::redraw_needed")
result:add_signal("widget::layout_changed")
result.visible = true
result.layout = function()
return children
end
return result
end
local function make_child(widget, width, height, matrix)
return { _widget = widget, _width = width, _height = height, _matrix = matrix }
end
describe("wibox.hierarchy", function()
describe("Accessor functions", function()
local widget, instance
before_each(function()
local function nop() end
local context = {}
widget = make_widget(nil)
instance = hierarchy.new(context, widget, 10, 20, nop, nop)
end)
it("get_parent", function()
assert.is_nil(instance:get_parent())
end)
it("get_widget", function()
assert.is.equal(instance:get_widget(), widget)
end)
it("get_matrix_to_parent", function()
assert.is_true(matrix.equals(cairo.Matrix.create_identity(),
instance:get_matrix_to_parent()))
end)
it("get_matrix_to_device", function()
assert.is_true(matrix.equals(cairo.Matrix.create_identity(),
instance:get_matrix_to_device()))
end)
it("get_matrix_from_parent", function()
assert.is_true(matrix.equals(cairo.Matrix.create_identity(),
instance:get_matrix_from_parent()))
end)
it("get_matrix_from_device", function()
assert.is_true(matrix.equals(cairo.Matrix.create_identity(),
instance:get_matrix_from_device()))
end)
it("get_draw_extents", function()
assert.is.same({ instance:get_draw_extents() }, { 0, 0, 10, 20 })
end)
it("get_size", function()
assert.is.same({ instance:get_size() }, { 10, 20 })
end)
it("get_children", function()
assert.is.same(instance:get_children(), {})
end)
end)
it("disconnect works", function()
local child = make_widget(nil)
local parent = make_widget({
make_child(child, 2, 5, cairo.Matrix.create_translate(10, 0))
})
local extra_arg = {}
local child_redraws, child_layouts = 0, 0
local parent_redraws, parent_layouts = 0, 0
local function redraw(arg, extra)
assert.is.equal(extra_arg, extra)
if arg:get_widget() == child then
child_redraws = child_redraws + 1
elseif arg:get_widget() == parent then
parent_redraws = parent_redraws + 1
else
error("Unknown widget")
end
end
local function layout(arg, extra)
assert.is.equal(extra_arg, extra)
if arg:get_widget() == child then
child_layouts = child_layouts + 1
elseif arg:get_widget() == parent then
parent_layouts = parent_layouts + 1
else
error("Unknown widget")
end
end
local context = {}
local instance = hierarchy.new(context, parent, 15, 20, redraw, layout, extra_arg)
-- There should be a connection
parent:emit_signal("widget::redraw_needed")
assert.is.same({ 0, 0, 1, 0 }, { child_redraws, child_layouts, parent_redraws, parent_layouts })
child:emit_signal("widget::redraw_needed")
assert.is.same({ 1, 0, 1, 0 }, { child_redraws, child_layouts, parent_redraws, parent_layouts })
child:emit_signal("widget::layout_changed")
assert.is.same({ 1, 1, 1, 0 }, { child_redraws, child_layouts, parent_redraws, parent_layouts })
parent:emit_signal("widget::layout_changed")
assert.is.same({ 1, 1, 1, 1 }, { child_redraws, child_layouts, parent_redraws, parent_layouts })
-- Garbage-collect the hierarchy
instance = nil
collectgarbage("collect")
-- No connections should be left
parent:emit_signal("widget::redraw_needed")
child:emit_signal("widget::redraw_needed")
child:emit_signal("widget::layout_changed")
parent:emit_signal("widget::layout_changed")
assert.is.same({ 1, 1, 1, 1 }, { child_redraws, child_layouts, parent_redraws, parent_layouts })
end)
describe("children", function()
local child, intermediate, parent
local hierarchy_child, hierarchy_intermediate, hierarchy_parent
before_each(function()
child = make_widget(nil)
intermediate = make_widget({
make_child(child, 10, 20, cairo.Matrix.create_translate(0, 5))
})
parent = make_widget({
make_child(intermediate, 5, 2, cairo.Matrix.create_translate(4, 0))
})
local function nop() end
local context = {}
hierarchy_parent = hierarchy.new(context, parent, 15, 16, nop, nop)
-- This also tests get_children
local children = hierarchy_parent:get_children()
assert.is.equal(#children, 1)
hierarchy_intermediate = children[1]
local children = hierarchy_intermediate:get_children()
assert.is.equal(#children, 1)
hierarchy_child = children[1]
end)
it("get_parent", function()
assert.is.equal(hierarchy_child:get_parent(), hierarchy_intermediate)
assert.is.equal(hierarchy_intermediate:get_parent(), hierarchy_parent)
assert.is_nil(hierarchy_parent:get_parent())
end)
it("get_widget", function()
assert.is.equal(hierarchy_child:get_widget(), child)
assert.is.equal(hierarchy_intermediate:get_widget(), intermediate)
assert.is.equal(hierarchy_parent:get_widget(), parent)
end)
it("get_matrix_to_parent", function()
assert.is_true(matrix.equals(hierarchy_child:get_matrix_to_parent(), cairo.Matrix.create_translate(0, 5)))
assert.is_true(matrix.equals(hierarchy_intermediate:get_matrix_to_parent(), cairo.Matrix.create_translate(4, 0)))
assert.is_true(matrix.equals(hierarchy_parent:get_matrix_to_parent(), cairo.Matrix.create_identity()))
end)
it("get_matrix_to_device", function()
assert.is_true(matrix.equals(hierarchy_child:get_matrix_to_device(), cairo.Matrix.create_translate(4, 5)))
assert.is_true(matrix.equals(hierarchy_intermediate:get_matrix_to_device(), cairo.Matrix.create_translate(4, 0)))
assert.is_true(matrix.equals(hierarchy_parent:get_matrix_to_device(), cairo.Matrix.create_identity()))
end)
it("get_matrix_from_parent", function()
assert.is_true(matrix.equals(hierarchy_child:get_matrix_from_parent(), cairo.Matrix.create_translate(0, -5)))
assert.is_true(matrix.equals(hierarchy_intermediate:get_matrix_from_parent(), cairo.Matrix.create_translate(-4, 0)))
assert.is_true(matrix.equals(hierarchy_parent:get_matrix_from_parent(), cairo.Matrix.create_identity()))
end)
it("get_matrix_from_device", function()
assert.is_true(matrix.equals(hierarchy_child:get_matrix_from_device(), cairo.Matrix.create_translate(-4, -5)))
assert.is_true(matrix.equals(hierarchy_intermediate:get_matrix_from_device(), cairo.Matrix.create_translate(-4, 0)))
assert.is_true(matrix.equals(hierarchy_parent:get_matrix_from_device(), cairo.Matrix.create_identity()))
end)
it("get_draw_extents", function()
assert.is.same({ hierarchy_child:get_draw_extents() }, { 0, 0, 10, 20 })
assert.is.same({ hierarchy_intermediate:get_draw_extents() }, { 0, 0, 10, 25 })
assert.is.same({ hierarchy_parent:get_draw_extents() }, { 0, 0, 15, 25 })
end)
it("get_size", function()
assert.is.same({ hierarchy_child:get_size() }, { 10, 20 })
assert.is.same({ hierarchy_intermediate:get_size() }, { 5, 2 })
assert.is.same({ hierarchy_parent:get_size() }, { 15, 16 })
end)
end)
describe("find_differences", function()
local child, intermediate, parent
local instance
local function nop() end
before_each(function()
child = make_widget(nil)
intermediate = make_widget({
make_child(child, 10, 20, cairo.Matrix.create_translate(0, 5))
})
parent = make_widget({
make_child(intermediate, 5, 2, cairo.Matrix.create_translate(4, 0))
})
local context = {}
instance = hierarchy.new(context, parent, 15, 16, nop, nop)
end)
it("No difference", function()
local context = {}
local instance2 = hierarchy.new(context, parent, 15, 16, nop, nop)
local region = instance:find_differences(instance2)
assert.is.equal(region:num_rectangles(), 0)
end)
it("child moved", function()
intermediate.layout = function()
return { make_child(child, 10, 20, cairo.Matrix.create_translate(0, 4)) }
end
local context = {}
local instance2 = hierarchy.new(context, parent, 15, 16, nop, nop)
local region = instance:find_differences(instance2)
assert.is.equal(region:num_rectangles(), 1)
local rect = region:get_rectangle(0)
-- The widget drew to 4, 5, 10, 20 before and 4, 4, 10, 20 after
assert.is.same({ rect.x, rect.y, rect.width, rect.height }, { 4, 4, 10, 21 })
end)
end)
end)
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80