Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

[RFC] Implement wibox.widget.template: An abstract widget that handles a preset of concrete widget. #3421

Open
wants to merge 31 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
08109a6
add(w.w.template) template widget
Aire-One Aug 28, 2021
d09a704
fix(spec) add `awesome.api_level` preload
Aire-One Aug 28, 2021
18ae7bb
add(spec) `wibox.widget.template` unit tests
Aire-One Aug 28, 2021
d5dbb9d
add(update_callback) allow nil callback
Aire-One Sep 29, 2021
a005967
fix: variable declaration
Aire-One Sep 29, 2021
2f2beb6
add: natural management of update arguments
Aire-One Sep 29, 2021
b824d99
fix(update_args) move to _private property
Aire-One Sep 30, 2021
29ea4dd
fix(spec) remove dead code
Aire-One Sep 30, 2021
f4088ca
fix: rename parameter
Aire-One Oct 23, 2021
e7db5e3
fix: format
Aire-One Oct 23, 2021
086eb14
add(w.w.template) implement widget as child
Aire-One Nov 11, 2021
33d5de5
add(w.w.template) constructor `buttons` param
Aire-One Nov 11, 2021
f12bb57
fix(w.w.template) fit and layout methods
Aire-One Nov 11, 2021
cd5dda3
fix(w.w.template) draw
Aire-One Nov 11, 2021
aaec225
fix(w.w.template) free queued_updates array
Aire-One Nov 11, 2021
45a458a
doc(w.w.template) module description
Aire-One Nov 12, 2021
bfe4832
doc(w.w.template) current methods
Aire-One Nov 12, 2021
8803878
doc(w.w.template) 3rd party lib usage example
Aire-One Nov 12, 2021
d5382b0
add(spec) more `wibox.widget.template` tests
Aire-One Nov 25, 2021
2542c3b
add(w.w.template) constructor accepts widget props
Aire-One Nov 25, 2021
2dab6c3
add(w.w.template) set_template check widget
Aire-One Nov 25, 2021
a7932b1
add(w.w.template) template can't be a callback
Aire-One Nov 26, 2021
495922c
shims: Fix some missing client property::* signals.
Elv13 Oct 16, 2022
67b2b26
template: Bring to feature parity with the awful.widget.common implem…
Elv13 Oct 16, 2022
3765efa
wibox.template: Remove the `args` in favor of passing the template di…
Elv13 Oct 23, 2022
0759096
template: Rename to `wibox.template`.
Elv13 Oct 23, 2022
cc7feb9
doc: Fix a compiler warning.
Elv13 Oct 23, 2022
7aab54e
template: Make sure the template is loaded when calling `get_children…
Elv13 Oct 23, 2022
8fc30ae
Port all "old" `widget_template` to use `wibox.template`.
Elv13 Oct 23, 2022
452d21a
tests: Test `wibox.template` `:set_property()`.
Elv13 Oct 23, 2022
c8016f0
template: Fix forced_height/forced_width and other fixup.
Elv13 Nov 13, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/wibox/widget/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ local widget = {
slider = require("wibox.widget.slider");
calendar = require("wibox.widget.calendar");
separator = require("wibox.widget.separator");
template = require("wibox.widget.template");
}

setmetatable(widget, {
Expand Down
129 changes: 129 additions & 0 deletions lib/wibox/widget/template.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
---------------------------------------------------------------------------
-- An abstract widget that handles a preset of concrete widget.
--
-- The `wibox.widget.template` widget is an abstraction layer that contains a
-- concrete widget definition. The template widget can be used to build widgets
-- that the user can customize at their will, thanks to the template mechanism.
--
-- @author Aire-One
-- @copyright 2021 Aire-One <aireone@aireone.xyz>
--
-- @classmod wibox.widget.template
-- @supermodule wibox.widget.base
---------------------------------------------------------------------------

local wbase = require("wibox.widget.base")
local gtable = require("gears.table")
local gtimer = require("gears.timer")

local template = {
mt = {},
queued_updates = {},
}

function template:fit(...)
return self._private.widget:fit(...)
end

function template:draw(...)
return self._private.widget:draw(...)
end

function template:_do_update_now()
if type(self._private.update_callback) == "function" then
self._private.update_callback(self, self._private.update_args)
end

self._private.update_args = nil
template.queued_updates[self] = false
end

--- Update the widget.
-- This function will call the `update_callback` function at the end of the
-- current GLib event loop. Updates are batched by event loop, it means that the
-- widget can only be update once by event loop. If the `template:update` method
-- is called multiple times during the same GLib event loop, only the first call
-- will be run.
Aire-One marked this conversation as resolved.
Show resolved Hide resolved
-- All arguments are passed to the queued `update_callback` call.
function template:update(args)
if type(args) == "table" then
self._private.update_args = gtable.crush(
gtable.clone(self._private.update_args or {}, false),
args
)
end

if not template.queued_updates[self] then
gtimer.delayed_call(function()
self:_do_update_now()
end)
template.queued_updates[self] = true
end
end

function template:set_template(widget_template)
local widget = type(widget_template) == "function" and widget_template()
or widget_template
or wbase.empty_widget()

self._private.template = widget
Elv13 marked this conversation as resolved.
Show resolved Hide resolved
self._private.widget = wbase.make_widget_from_value(widget)

-- We need to connect to these signals to actually redraw the template
-- widget when its child needs to.
local signals = {
"widget::redraw_needed",
"widget::layout_changed",
}
for _, signal in pairs(signals) do
self._private.widget:connect_signal(signal, function(...)
self:emit_signal(signal, ...)
end)
end

self:emit_signal("widget::redraw_needed")
end

function template:get_widget()
return self._private.widget
end

function template:set_update_callback(update_callback)
assert(type(update_callback) == "function" or update_callback == nil)

self._private.update_callback = update_callback
end

-- @hidden
function template:set_update_now(update_now)
if update_now then
self:update()
end
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name doesn't make sense. set_[something] implies a setter for a property, but this just triggers a function call.
And the update_now argument exists only to call self:update() in the constructor, so the entire method could just be replaced with an if in the constructor:

-    ret:set_update_now(args.update_now)
+    if args.update_now then
+        ret:update()
+    end

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is required to make the declarative pattern work as expected.

When creating a widget with the declarative pattern, the constructor doesn't receive all the properties as parameters. Instead, the properties are updated after the widget instantiation.

local w = wibox.widget {
   update_callback = update_fct,
   update_now = true,
   widget = widget.template,
}

The update_now = true property is "transformed" to the invocation of the template:set_update_now method.

Note that this is why the whole method is documented as a "Hack" and is @hidden.

Copy link
Contributor

@sclu1034 sclu1034 Nov 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it could make sense to just call it update_now(), which drill also checks for.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a clean code PoV, maybe, but at the same time adding too many ifs to drill() might not be super scalable. Another of my PR has some code to make meta-extending the DOM easier using some _ prefixed extender "I am special, call this and I will do my special stuff" functions. That might be the solution on the longer term.

However for now I think the hack proposed here is good enough. Unexpected, but not in a way which impact end users.

In the even longer term, which is dubious, there is also that issue which calls to replace how the DOM is created and move it to the hierarchy because in practice right now, you can't merge 2 DOMs and that makes :get_children_by_id() unreliable when some composed widgets are used... Including this very widget.... This would be an opportunity to revisit some of the bolt-on hacks I made to restore the declarative widget system. Historically, AwesomeWM 3.1-3.4 had it, but 3.5 had a fully rewritten (and much, much better) widget system. It was fully imperative, which annoyed a large number of users who could not port their config. The code was also very heavy for something like rc.lua, which is supposed to appear like a config file. Back then I mostly did modules rather than contribute to the core, so I wrote the declarative thing as the retrograde module. It eventually got reworked/merged into the core, but was always a bolted on hack rather than something engineered alongside wibox.hierarchy. So it would benefit from a larger rework to be more flexible/integrated.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a clean code PoV, maybe, but at the same time adding too many ifs to drill() might not be super scalable.

I'm not sure to follow what you mean. This if branch already exists.

I have to agree with @sclu1034. A setter is unneeded here. I have somehow missed this feature of drill, otherwise I would have used it instead.


--- Create a new `wibox.widget.template` instance.
-- @tparam[opt] table args
-- @tparam[opt] table|widget|function args.template The widget template to use.
-- @tparam[opt] function args.update_callback The callback function to update the widget.
-- @treturn wibox.widget.template The new instance.
function template.new(args)
args = args or {}

local ret = wbase.make_widget(nil, nil, { enable_properties = true })

gtable.crush(ret, template, true)

ret:set_template(args.template)
ret:set_update_callback(args.update_callback)
ret:set_update_now(args.update_now)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the update_callback still make sense with the new :set_property and :bind_property methods?

It seems like these 2 new methods are a more powerful and easier way to achieve what the user would have manually written in the update_callback.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I'm aware, those two things are not interchangeable.
:set_property is for the developer who creates a widget that uses the template, to assign values at named roles in an unknown template.
update_callback is for the user who creates the template, to do stuff when values change.

E.g. awful.widget.tasklist will call :set_property internally, the user that creates a tasklist will assign an update_callback to change stuff in the template dynamically.


return ret
end

function template.mt:__call(...)
return template.new(...)
end

return setmetatable(template, template.mt)

-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80
4 changes: 2 additions & 2 deletions spec/preload.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ require "lgi"

-- Always show deprecated messages
_G.awesome = {
version = "v9999",
api_level = 9999,
version = "v9999",
api_level = 9999,
}

-- "fix" some intentional beautiful breakage done by .travis.yml
Expand Down
73 changes: 73 additions & 0 deletions spec/wibox/widget/template_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---------------------------------------------------------------------------
-- @author Aire-One
-- @copyright 2021 Aire-One
---------------------------------------------------------------------------

_G.awesome.connect_signal = function() end

local template = require("wibox.widget.template")
local gtimer = require("gears.timer")

describe("wibox.widget.template", function()
local widget

before_each(function()
widget = template()
end)

describe("widget:update()", function()
it("batch calls", function()
local spied_update_callback = spy.new(function() end)

widget.update_callback = function(...) spied_update_callback(...) end

-- Multiple calls to update
widget:update()
widget:update()
widget:update()

-- update_callback shouldn't be called before the end of the event loop
assert.spy(spied_update_callback).was.called(0)

gtimer.run_delayed_calls_now()

-- updates are batched, so only 1 call should have been performed
assert.spy(spied_update_callback).was.called(1)
end)

it("update parameters", function()
local spied_update_callback = spy.new(function() end)
local args = { foo = "string" }

widget.update_callback = function(...) spied_update_callback(...) end

widget:update(args)

gtimer.run_delayed_calls_now()

assert.spy(spied_update_callback).was.called_with(
match.is_ref(widget),
match.is_same(args)
)
end)

it("crush update parameters", function()
local spied_update_callback = spy.new(function() end)

widget.update_callback = function(...) spied_update_callback(...) end

widget:update { foo = "bar" }
widget:update { bar = 10 }

gtimer.run_delayed_calls_now()

assert.spy(spied_update_callback).was.called_with(
match.is_ref(widget),
match.is_same { foo = "bar", bar = 10 }
)
end)

end)
end)

-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80