Merge pull request #458 from psychon/explicit_widget_deps

Explicitly track dependencies between widgets

Closes https://github.com/awesomeWM/awesome/pull/458.
This commit is contained in:
Daniel Hahler 2015-09-21 21:12:36 +02:00
commit b0d7e6bb6c
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

@ -18,41 +18,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, ...)
@ -61,24 +32,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
-- }}} -- }}}
@ -91,12 +74,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
@ -107,12 +93,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)
@ -130,12 +114,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
@ -145,10 +132,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
@ -382,7 +366,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