Explicitly track dependencies between widgets

Before this, dependencies between widgets where implicitly discovered by
recursive calls to base.fit_widget() and base.layout_widget(). However, it is
too easy to get this wrong (just call one of these functions from outside of a
widget's :fit() / :layout() function) and the resulting mess would be hard to
debug.

Thus, this commit changes the API so that callers have to identify themselves
and we can explicitly record the dependency between the widgets involved.

This also fixes a bug where no dependencies were tracked for widgets after
:set_visible(false). Whoops...

Sorry for breaking the API for adding this.

Signed-off-by: Uli Schlachter <psychon@znc.in>
This commit is contained in:
Uli Schlachter 2015-09-17 15:01:50 +02:00
parent 2330040eb5
commit 41a8fabf4c
11 changed files with 59 additions and 73 deletions

View File

@ -12,12 +12,13 @@
local matrix = require("gears.matrix") local matrix = require("gears.matrix")
local cairo = require("lgi").cairo local cairo = require("lgi").cairo
local base = require("wibox.widget.base") local base = require("wibox.widget.base")
local no_parent = base.no_parent_I_know_what_I_am_doing
local hierarchy = {} local hierarchy = {}
local function hierarchy_new(context, widget, width, height, redraw_callback, layout_callback, callback_arg, local function hierarchy_new(context, widget, width, height, redraw_callback, layout_callback, callback_arg,
matrix_to_parent, matrix_to_device) matrix_to_parent, matrix_to_device)
local children = base.layout_widget(context, widget, width, height) local children = base.layout_widget(no_parent, context, widget, width, height)
local draws_x1, draws_y1, draws_x2, draws_y2 = 0, 0, width, height local draws_x1, draws_y1, draws_x2, draws_y2 = 0, 0, width, height
local result = { local result = {
_matrix = matrix_to_parent, _matrix = matrix_to_parent,

View File

@ -40,7 +40,7 @@ function align:layout(context, width, height)
-- if the second widget doesn't exist, we will prioritise the first one -- if the second widget doesn't exist, we will prioritise the first one
-- instead -- instead
if self._expand ~= "inside" and self.second then if self._expand ~= "inside" and self.second then
local w, h = base.fit_widget(context, self.second, width, height) local w, h = base.fit_widget(self, context, self.second, width, height)
size_second = self.dir == "y" and h or w size_second = self.dir == "y" and h or w
-- if all the space is taken, skip the rest, and draw just the middle -- if all the space is taken, skip the rest, and draw just the middle
-- widget -- widget
@ -59,7 +59,7 @@ function align:layout(context, width, height)
-- into the remaining space -- into the remaining space
if self._expand ~= "outside" then if self._expand ~= "outside" then
if self.dir == "y" then if self.dir == "y" then
_, h = base.fit_widget(context, self.first, width, size_remains) _, h = base.fit_widget(self, context, self.first, width, size_remains)
size_first = h size_first = h
-- for "inside", the third widget will get a chance to use the -- for "inside", the third widget will get a chance to use the
-- remaining space, then the middle widget. For "none" we give -- remaining space, then the middle widget. For "none" we give
@ -70,7 +70,7 @@ function align:layout(context, width, height)
size_remains = size_remains - h size_remains = size_remains - h
end end
else else
w, _ = base.fit_widget(context, self.first, size_remains, height) w, _ = base.fit_widget(self, context, self.first, size_remains, height)
size_first = w size_first = w
if self._expand == "inside" or not self.second then if self._expand == "inside" or not self.second then
size_remains = size_remains - w size_remains = size_remains - w
@ -90,13 +90,13 @@ function align:layout(context, width, height)
local w, h, _ = width, height, nil local w, h, _ = width, height, nil
if self._expand ~= "outside" then if self._expand ~= "outside" then
if self.dir == "y" then if self.dir == "y" then
_, h = base.fit_widget(context, self.third, width, size_remains) _, h = base.fit_widget(self, context, self.third, width, size_remains)
-- give the middle widget the rest of the space for "inside" mode -- give the middle widget the rest of the space for "inside" mode
if self._expand == "inside" then if self._expand == "inside" then
size_remains = size_remains - h size_remains = size_remains - h
end end
else else
w, _ = base.fit_widget(context, self.third, size_remains, height) w, _ = base.fit_widget(self, context, self.third, size_remains, height)
if self._expand == "inside" then if self._expand == "inside" then
size_remains = size_remains - w size_remains = size_remains - w
end end
@ -125,10 +125,10 @@ function align:layout(context, width, height)
end end
else else
if self.dir == "y" then if self.dir == "y" then
_, h = base.fit_widget(context, self.second, width, size_second) _, h = base.fit_widget(self, context, self.second, width, size_second)
y = floor( (height - h)/2 ) y = floor( (height - h)/2 )
else else
w, _ = base.fit_widget(context, self.second, size_second, height) w, _ = base.fit_widget(self, context, self.second, size_second, height)
x = floor( (width -w)/2 ) x = floor( (width -w)/2 )
end end
end end
@ -175,7 +175,7 @@ function align:fit(context, orig_width, orig_height)
local used_in_other = 0 local used_in_other = 0
for k, v in pairs{self.first, self.second, self.third} do for k, v in pairs{self.first, self.second, self.third} do
local w, h = base.fit_widget(context, v, orig_width, orig_height) local w, h = base.fit_widget(self, context, v, orig_width, orig_height)
local max = self.dir == "y" and w or h local max = self.dir == "y" and w or h
if max > used_in_other then if max > used_in_other then

View File

@ -27,7 +27,7 @@ function constraint:fit(context, width, height)
w = self._strategy(width, self._width) w = self._strategy(width, self._width)
h = self._strategy(height, self._height) h = self._strategy(height, self._height)
w, h = base.fit_widget(context, self.widget, w, h) w, h = base.fit_widget(self, context, self.widget, w, h)
else else
w, h = 0, 0 w, h = 0, 0
end end

View File

@ -26,7 +26,7 @@ function fixed:layout(context, width, height)
x, y = 0, pos x, y = 0, pos
w, h = width, height - pos w, h = width, height - pos
if k ~= #self.widgets or not self._fill_space then if k ~= #self.widgets or not self._fill_space then
_, h = base.fit_widget(context, v, w, h); _, h = base.fit_widget(self, context, v, w, h);
end end
pos = pos + h + spacing pos = pos + h + spacing
in_dir = h in_dir = h
@ -34,7 +34,7 @@ function fixed:layout(context, width, height)
x, y = pos, 0 x, y = pos, 0
w, h = width - pos, height w, h = width - pos, height
if k ~= #self.widgets or not self._fill_space then if k ~= #self.widgets or not self._fill_space then
w, _ = base.fit_widget(context, v, w, h); w, _ = base.fit_widget(self, context, v, w, h);
end end
pos = pos + w + spacing pos = pos + w + spacing
in_dir = w in_dir = w
@ -65,7 +65,7 @@ function fixed:fit(context, orig_width, orig_height)
local used_in_dir, used_max = 0, 0 local used_in_dir, used_max = 0, 0
for k, v in pairs(self.widgets) do for k, v in pairs(self.widgets) do
local w, h = base.fit_widget(context, v, width, height) local w, h = base.fit_widget(self, context, v, width, height)
local in_dir, max local in_dir, max
if self.dir == "y" then if self.dir == "y" then
max, in_dir = w, h max, in_dir = w, h

View File

@ -73,7 +73,7 @@ function flex:fit(context, orig_width, orig_height)
local sub_width = self.dir == "y" and orig_width or orig_width / #self.widgets local sub_width = self.dir == "y" and orig_width or orig_width / #self.widgets
for k, v in pairs(self.widgets) do for k, v in pairs(self.widgets) do
local w, h = base.fit_widget(context, v, sub_width, sub_height) local w, h = base.fit_widget(self, context, v, sub_width, sub_height)
local max = self.dir == "y" and w or h local max = self.dir == "y" and w or h
if max > used_in_other then if max > used_in_other then

View File

@ -53,7 +53,7 @@ function margin:fit(context, width, height)
local extra_h = self.top + self.bottom local extra_h = self.top + self.bottom
local w, h = 0, 0 local w, h = 0, 0
if self.widget then if self.widget then
w, h = base.fit_widget(context, self.widget, width - extra_w, height - extra_h) w, h = base.fit_widget(self, context, self.widget, width - extra_w, height - extra_h)
end end
return w + extra_w, h + extra_h return w + extra_w, h + extra_h
end end

View File

@ -45,7 +45,7 @@ function mirror:fit(context, ...)
if not self.widget then if not self.widget then
return 0, 0 return 0, 0
end end
return base.fit_widget(context, self.widget, ...) return base.fit_widget(self, context, self.widget, ...)
end end
--- Set the widget that this layout mirrors. --- Set the widget that this layout mirrors.

View File

@ -54,7 +54,7 @@ function rotate:fit(context, width, height)
if not self.widget then if not self.widget then
return 0, 0 return 0, 0
end end
return transform(self, base.fit_widget(context, self.widget, transform(self, width, height))) return transform(self, base.fit_widget(self, context, self.widget, transform(self, width, height)))
end end
--- Set the widget that this layout rotates. --- Set the widget that this layout rotates.

View File

@ -52,7 +52,7 @@ function background:fit(context, width, height)
return 0, 0 return 0, 0
end end
return base.fit_widget(context, self.widget, width, height) return base.fit_widget(self, context, self.widget, width, height)
end end
--- Set the widget that is drawn on top of the background --- Set the widget that is drawn on top of the background

View File

@ -19,41 +19,12 @@ local base = {}
-- {{{ Caches -- {{{ Caches
local call_stack = {}
-- Indexes are widgets, allow them to be garbage-collected -- Indexes are widgets, allow them to be garbage-collected
local widget_dependencies = setmetatable({}, { __mode = "k" }) local widget_dependencies = setmetatable({}, { __mode = "k" })
-- Don't do this in unit tests -- Get the cache of the given kind for this widget. This returns a gears.cache
if awesome and awesome.connect_signal then
-- Reset the call stack at each refresh. This fixes things up in case there was
-- an error in some callback and thus put_cache() wasn't called (if this
-- happens, we possibly recorded too many deps, but so what?)
awesome.connect_signal("refresh", function()
call_stack = {}
end)
end
-- When you call get_cache_and_record_deps(), the widget is recorded in a stack
-- until the following put_cache(). All other calls to
-- get_cache_and_record_deps() that happen during this will cause a dependency
-- between the widgets that are involved to be recorded. This information is
-- used by clear_caches() to also clear all caches of dependent widgets.
-- Get the caches for a widget and record its dependencies. All following
-- cache-uses will record this widgets as a dependency. This returns a function
-- that calls the callback of kind `kind` on the widget. -- that calls the callback of kind `kind` on the widget.
local function get_cache_and_record_deps(widget, kind) local function get_cache(widget, kind)
-- Record dependencies (each entry in the call stack depends on `widget`)
local deps = widget_dependencies[widget] or {}
for _, w in pairs(call_stack) do
deps[w] = true
end
widget_dependencies[widget] = deps
-- Add widget to call stack
table.insert(call_stack, widget)
-- Create cache if needed
if not widget._widget_caches[kind] then if not widget._widget_caches[kind] then
widget._widget_caches[kind] = cache.new(function(...) widget._widget_caches[kind] = cache.new(function(...)
return widget[kind](widget, ...) return widget[kind](widget, ...)
@ -62,24 +33,36 @@ local function get_cache_and_record_deps(widget, kind)
return widget._widget_caches[kind] return widget._widget_caches[kind]
end end
-- Each call to the above function should be followed by a call to this -- Special value to skip the dependency recording that is normally done by
-- function. Everything in-between is recorded as a dependency (it's -- base.fit_widget() and base.layout_widget(). The caller must ensure that no
-- complicated...). -- caches depend on the result of the call and/or must handle the childs
local function put_cache(widget) -- widget::layout_changed signal correctly when using this.
assert(#call_stack ~= 0) base.no_parent_I_know_what_I_am_doing = {}
if table.remove(call_stack) ~= widget then
put_cache(widget) -- Record a dependency from parent to child: The layout of parent depends on the
-- layout of child.
local function record_dependency(parent, child)
if parent == base.no_parent_I_know_what_I_am_doing then
return
end end
base.check_widget(parent)
base.check_widget(child)
local deps = widget_dependencies[child] or {}
deps[parent] = true
widget_dependencies[child] = deps
end end
-- Clear the caches for `widget` and all widgets that depend on it. -- Clear the caches for `widget` and all widgets that depend on it.
local function clear_caches(widget) local clear_caches
for w in pairs(widget_dependencies[widget] or {}) do function clear_caches(widget)
widget_dependencies[w] = {} local deps = widget_dependencies[widget] or {}
w._widget_caches = {}
end
widget_dependencies[widget] = {} widget_dependencies[widget] = {}
widget._widget_caches = {} widget._widget_caches = {}
for w in pairs(deps) do
clear_caches(w)
end
end end
-- }}} -- }}}
@ -92,12 +75,15 @@ end
--- Fit a widget for the given available width and height. This calls the --- Fit a widget for the given available width and height. This calls the
-- widget's `:fit` callback and caches the result for later use. Never call -- widget's `:fit` callback and caches the result for later use. Never call
-- `:fit` directly, but always through this function! -- `:fit` directly, but always through this function!
-- @param parent The parent widget which requests this information.
-- @param context The context in which we are fit. -- @param context The context in which we are fit.
-- @param widget The widget to fit (this uses widget:fit(width, height)). -- @param widget The widget to fit (this uses widget:fit(width, height)).
-- @param width The available width for the widget -- @param width The available width for the widget
-- @param height The available height for the widget -- @param height The available height for the widget
-- @return The width and height that the widget wants to use -- @return The width and height that the widget wants to use
function base.fit_widget(context, widget, width, height) function base.fit_widget(parent, context, widget, width, height)
record_dependency(parent, widget)
if not widget.visible then if not widget.visible then
return 0, 0 return 0, 0
end end
@ -108,12 +94,10 @@ function base.fit_widget(context, widget, width, height)
local w, h = 0, 0 local w, h = 0, 0
if widget.fit then if widget.fit then
local cache = get_cache_and_record_deps(widget, "fit") w, h = get_cache(widget, "fit"):get(context, width, height)
w, h = cache:get(context, width, height)
put_cache(widget)
else else
-- If it has no fit method, calculate based on the size of children -- If it has no fit method, calculate based on the size of children
local children = base.layout_widget(context, widget, width, height) local children = base.layout_widget(parent, context, widget, width, height)
for _, info in ipairs(children or {}) do for _, info in ipairs(children or {}) do
local x, y, w2, h2 = matrix.transform_rectangle(info._matrix, local x, y, w2, h2 = matrix.transform_rectangle(info._matrix,
0, 0, info._width, info._height) 0, 0, info._width, info._height)
@ -131,12 +115,15 @@ end
-- widget's `:layout` callback and caches the result for later use. Never call -- widget's `:layout` callback and caches the result for later use. Never call
-- `:layout` directly, but always through this function! However, normally there -- `:layout` directly, but always through this function! However, normally there
-- shouldn't be any reason why you need to use this function. -- shouldn't be any reason why you need to use this function.
-- @param parent The parent widget which requests this information.
-- @param context The context in which we are laid out. -- @param context The context in which we are laid out.
-- @param widget The widget to layout (this uses widget:layout(context, width, height)). -- @param widget The widget to layout (this uses widget:layout(context, width, height)).
-- @param width The available width for the widget -- @param width The available width for the widget
-- @param height The available height for the widget -- @param height The available height for the widget
-- @return The result from the widget's `:layout` callback. -- @return The result from the widget's `:layout` callback.
function base.layout_widget(context, widget, width, height) function base.layout_widget(parent, context, widget, width, height)
record_dependency(parent, widget)
if not widget.visible then if not widget.visible then
return return
end end
@ -146,10 +133,7 @@ function base.layout_widget(context, widget, width, height)
local height = math.max(0, height) local height = math.max(0, height)
if widget.layout then if widget.layout then
local cache = get_cache_and_record_deps(widget, "layout") return get_cache(widget, "layout"):get(context, width, height)
local result = cache:get(context, width, height)
put_cache(widget)
return result
end end
end end
@ -383,7 +367,7 @@ function base.make_widget(proxy, widget_name)
if proxy then if proxy then
ret.fit = function(_, context, width, height) ret.fit = function(_, context, width, height)
return base.fit_widget(context, proxy, width, height) return base.fit_widget(ret, context, proxy, width, height)
end end
ret.layout = function(_, context, width, height) ret.layout = function(_, context, width, height)
return { base.place_widget_at(proxy, 0, 0, width, height) } return { base.place_widget_at(proxy, 0, 0, width, height) }

View File

@ -9,6 +9,7 @@ local matrix_equals = require("gears.matrix").equals
local base = require("wibox.widget.base") local base = require("wibox.widget.base")
local say = require("say") local say = require("say")
local assert = require("luassert") local assert = require("luassert")
local no_parent = base.no_parent_I_know_what_I_am_doing
-- {{{ Own widget-based assertions -- {{{ Own widget-based assertions
local function widget_fit(state, arguments) local function widget_fit(state, arguments)
@ -19,7 +20,7 @@ local function widget_fit(state, arguments)
local widget = arguments[1] local widget = arguments[1]
local given = arguments[2] local given = arguments[2]
local expected = arguments[3] local expected = arguments[3]
local w, h = base.fit_widget({ "fake context" }, widget, given[1], given[2]) local w, h = base.fit_widget(no_parent, { "fake context" }, widget, given[1], given[2])
local fits = expected[1] == w and expected[2] == h local fits = expected[1] == w and expected[2] == h
if state.mod == fits then if state.mod == fits then