diff --git a/tests/examples/sequences/template.lua b/tests/examples/sequences/template.lua
new file mode 100644
index 00000000..e18d23fa
--- /dev/null
+++ b/tests/examples/sequences/template.lua
@@ -0,0 +1,789 @@
+
+-- This template provides a timeline like display for tag changes.
+-- Basically a taglist with some pretty dots on the side. Note that the way it
+-- copy tags is fragile and probably not really supportable.
+
+local file_path, image_path = ...
+require("_common_template")(...)
+local capi = {client = client, screen = screen}
+local Pango = require("lgi").Pango
+local PangoCairo = require("lgi").PangoCairo
+require("awful.screen")
+require("awful.tag")
+local floating_l = require("awful.layout.suit.floating")
+local taglist = require("awful.widget.taglist")
+local gtable = require("gears.table")
+local shape = require("gears.shape")
+local color = require("gears.color")
+local wibox = require("wibox")
+local beautiful = require("beautiful")
+
+local bar_size, radius = 18, 2
+local screen_scale_factor = 5
+
+local history = {}
+
+local module = {}
+
+-- Draw a mouse cursor at [x,y]
+local function draw_mouse(cr, x, y)
+ cr:set_source_rgb(1, 0, 0)
+ cr:move_to(x, y)
+ cr:rel_line_to( 0, 10)
+ cr:rel_line_to( 3, -2)
+ cr:rel_line_to( 3, 4)
+ cr:rel_line_to( 2, 0)
+ cr:rel_line_to(-3, -4)
+ cr:rel_line_to( 4, 0)
+ cr:close_path()
+ cr:fill()
+end
+
+-- Instead of returning the maximum size, return the preferred one.
+local function fixed_fit2(self, context, orig_width, orig_height)
+ local width, height = orig_width, orig_height
+ local used_in_dir, used_max = 0, 0
+
+ for _, v in pairs(self._private.widgets) do
+ local w, h = wibox.widget.base.fit_widget(self, context, v, width, height)
+
+ -- Catch bugs before they crash Chrome (Firefox handles this better)
+ assert(w < 5000)
+ assert(h < 5000)
+
+ local in_dir, max
+ if self._private.dir == "y" then
+ max, in_dir = w, h
+ height = height - in_dir
+ else
+ in_dir, max = w, h
+ width = width - in_dir
+ end
+ if max > used_max then
+ used_max = max
+ end
+ used_in_dir = used_in_dir + in_dir
+ end
+
+ local spacing = self._private.spacing * (#self._private.widgets-1)
+
+ -- Catch bugs before they crash Chrome (Firefox handles this better)
+ assert(used_max < 9000)
+ assert(used_in_dir < 9000)
+
+ if self._private.dir == "y" then
+ return used_max, used_in_dir + spacing
+ end
+ return used_in_dir + spacing, used_max
+end
+
+-- Imported from the Collision module.
+local function draw_lines()
+ local ret = wibox.widget.base.make_widget()
+
+ function ret:fit()
+ local pager_w = math.max(root.size()/screen_scale_factor, 60)
+
+ --FIXME support multiple screens.
+ local w = (#screen[1].tags * pager_w)
+ + ((#screen[1].tags - 1)*5)
+
+ return w, self.widget_pos and #self.widget_pos*6 or 30
+ end
+
+ function ret:draw(_, cr, _, h)
+ if (not self.widget_pos) or (not self.pager_pos) then return end
+
+ cr:set_line_width(1)
+ cr:set_source_rgba(0,0,0,0.3)
+
+ local count = #self.widget_pos
+
+ for k, t in ipairs(self.widget_pos) do
+ local point1 = {x = t.widget_x, y = 0, width = t.widget_width, height = 1}
+ local point2 = {x = t.pager_x, y = 0, width = t.pager_width, height = h}
+ assert(point1.x and point1.width)
+ assert(point2.x and point2.width)
+
+ local dx = (point1.x == 0 and radius or 0) + (point1.width and point1.width/2 or 0)
+
+ cr:move_to(bar_size+dx+point1.x, point1.y+2*radius)
+ cr:line_to(bar_size+dx+point1.x, point2.y+(count-k)*((h-2*radius)/count)+2*radius)
+ cr:line_to(point2.x+point2.width/2, point2.y+(count-k)*((h-2*radius)/count)+2*radius)
+ cr:line_to(point2.x+point2.width/2, point2.y+point2.height)
+ cr:stroke()
+
+ cr:arc(bar_size+dx+point1.x, point1.y+2*radius, radius, 0, 2*math.pi)
+ cr:fill()
+
+ cr:arc(point2.x+point2.width/2, point2.y+point2.height-radius, radius, 0, 2*math.pi)
+ cr:fill()
+ end
+ end
+
+ return ret
+end
+
+local function gen_vertical_line(args)
+ args = args or {}
+
+ local w = wibox.widget.base.make_widget()
+
+ function w:draw(_, cr, w2, h)
+ cr:set_source_rgba(0,0,0,0.5)
+
+ if args.begin then
+ cr:rectangle(w2/2-0.5, h/2, 1, h/2)
+ elseif args.finish then
+ cr:rectangle(w2/2-0.5, 0, 1, h/2)
+ else
+ cr:rectangle(w2/2-0.5, 0, 1, h)
+ end
+
+ cr:fill()
+
+ if args.dot then
+ cr:arc(w2/2, args.center and h/2 or w2/2 ,bar_size/4, 0, 2*math.pi)
+ cr:set_source_rgb(1,1,1)
+ cr:fill_preserve()
+ cr:set_source_rgba(0,0,0,0.5)
+ cr:stroke()
+ end
+ end
+
+ function w:fit()
+ return bar_size, bar_size
+ end
+
+ return w
+end
+
+local function gen_taglist_layout_proxy(tags, w2, name)
+ local l = wibox.layout.fixed.horizontal()
+ l.fit = fixed_fit2
+
+ local layout = l.layout
+
+ l.layout = function(self,context, width, height)
+ local ret = layout(self,context, width, height)
+
+ for k, v in ipairs(ret) do
+ tags[k][name.."_x" ] = v._matrix.x0
+ tags[k][name.."_width"] = v._width
+ end
+
+ if w2 then
+ w2[name.."_pos"] = tags
+
+ if not w2[name.."_configured"] then
+ rawset(w2, name.."_configured", true)
+ w2:emit_signal("widget::redraw_needed")
+ w2:emit_signal("widget::layout_changed")
+ end
+ end
+
+ return ret
+ end
+
+ return l
+end
+
+local function gen_fake_taglist_wibar(tags, w2)
+ local layout = gen_taglist_layout_proxy(tags, w2, "widget")
+ local w = wibox.widget {
+ {
+ {
+ forced_height = bar_size,
+ forced_width = bar_size,
+ image = beautiful.awesome_icon,
+ widget = wibox.widget.imagebox,
+ },
+ taglist {
+ forced_height = 14,
+ forced_width = 300,
+ layout = layout,
+ screen = screen[1],
+ filter = taglist.filter.all,
+ source = function() return tags end,
+ },
+ fit = fixed_fit2,
+ layout = wibox.layout.fixed.horizontal,
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ }
+
+ -- Make sure it nevers goes unbounded by accident.
+ local w3, h3 = w:fit({dpi=96}, 9999, 9999)
+ assert(w3 < 5000 and h3 < 5000)
+
+ return w
+end
+
+local function gen_cls(c,results)
+ local ret = setmetatable({},{__index = function(_, i)
+ local ret2 = c[i]
+ if type(ret2) == "function" then
+ if i == "geometry" then
+ return function(_, val)
+ if val then
+ c:geometry(gtable.crush(c:geometry(), val))
+ -- Make a copy as the original will be changed
+ results[c] = gtable.clone(c:geometry())
+ end
+ return c:geometry()
+ end
+ else
+ return function(_,...) return ret2(c,...) end
+ end
+ end
+ return ret2
+ end})
+ return ret
+end
+
+local function fake_arrange(tag)
+ local cls,results,flt = {},setmetatable({},{__mode="k"}),{}
+ local _, l = tag.screen, tag.layout
+ local focus, focus_wrap = capi.client.focus, nil
+ for _ ,c in ipairs (tag:clients()) do
+ -- Handle floating client separately
+ if not c.minimized then
+ local floating = c.floating
+ if (not floating) and (l ~= floating_l) then
+ cls[#cls+1] = gen_cls(c,results)
+ if c == focus then
+ focus_wrap = cls[#cls]
+ end
+ else
+ flt[#flt+1] = gtable.clone(c:geometry())
+ flt[#flt].c = c
+ end
+ end
+ end
+
+ -- The magnifier layout require a focussed client
+ -- there wont be any as that layout is not selected
+ -- take one at random or (TODO) use stack data
+ if not focus_wrap then
+ focus_wrap = cls[1]
+ end
+
+ local param = {
+ tag = tag,
+ screen = 1,
+ clients = cls,
+ focus = focus_wrap,
+ geometries = setmetatable({}, {__mode = "k"}),
+ workarea = tag.screen.workarea,
+ useless_gap = tag.gaps or 4,
+ apply_size_hints = false,
+ }
+
+ l.arrange(param)
+
+ local ret = {}
+
+ for _, geo_src in ipairs {param.geometries, flt } do
+ for c, geo in pairs(geo_src) do
+ geo.c = geo.c or c
+ table.insert(ret, geo)
+ end
+ end
+
+ return ret
+end
+
+local function gen_fake_clients(tag, args)
+ local pager = wibox.widget.base.make_widget()
+
+ function pager:fit()
+ return 60, 48
+ end
+
+ if not tag then return end
+
+ local sgeo = tag.screen.geometry
+
+ local show_name = args.display_client_name or args.display_label
+
+ function pager:draw(_, cr, w, h)
+ if not tag.client_geo then return end
+
+ for _, geom in ipairs(tag.client_geo) do
+ local x = (geom.x*w)/sgeo.width
+ local y = (geom.y*h)/sgeo.height
+ local width = (geom.width*w)/sgeo.width
+ local height = (geom.height*h)/sgeo.height
+ cr:set_source(color(geom.c.color or beautiful.bg_normal))
+ cr:rectangle(x,y,width,height)
+ cr:fill_preserve()
+ cr:set_source(color(geom.c.border_color or beautiful.border_color))
+ cr:stroke()
+
+ if show_name and type(geom.c) == "table" and geom.c.name then
+ cr:set_source_rgb(0, 0, 0)
+ cr:move_to(x + 2, y + height - 2)
+ cr:show_text(geom.c.name)
+ end
+ end
+
+ -- Draw the screen outline.
+ cr:set_source(color("#00000044"))
+ cr:set_line_width(1.5)
+ cr:set_dash({10,4},1)
+ cr:rectangle(0, 0, w, h)
+ cr:stroke()
+ end
+
+ return pager
+end
+
+local function gen_fake_pager_widget(tags, w2, args)
+ local layout = gen_taglist_layout_proxy(tags, w2, "pager")
+ layout.spacing = 10
+
+ for _, t in ipairs(tags) do
+ layout:add(wibox.widget {
+ gen_fake_clients(t, args),
+ widget = wibox.container.background
+ })
+ end
+
+ return layout
+end
+
+local function wrap_timeline(w, dot)
+ return wibox.widget {
+ gen_vertical_line { dot = dot or false},
+ {
+ w,
+ top = dot and 5 or 0,
+ bottom = dot and 5 or 0,
+ left = 0,
+ widget = wibox.container.margin
+ },
+ fit = fixed_fit2,
+ layout = wibox.layout.fixed.horizontal
+ }
+end
+
+local function gen_label(text)
+ return wibox.widget {
+ gen_vertical_line {
+ dot = true,
+ begin = text == "Begin",
+ finish = text == "End",
+ center = true,
+ },
+ {
+ {
+ {
+ {
+ markup = ""..text.." ",
+ forced_width = 50,
+ widget = wibox.widget.textbox
+ },
+ top = 2,
+ bottom = 2,
+ right = 5,
+ left = 10,
+ widget = wibox.container.margin
+ },
+ shape = shape.rectangular_tag,
+ border_width = 2,
+ border_color = beautiful.border_color,
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ },
+ top = 10,
+ bottom = 10,
+ widget = wibox.container.margin
+ },
+ fit = fixed_fit2,
+ layout = wibox.layout.fixed.horizontal
+ }
+end
+
+local function draw_info(s, cr, factor)
+ cr:set_source_rgba(0, 0, 0, 0.4)
+
+ local pctx = PangoCairo.font_map_get_default():create_context()
+ local playout = Pango.Layout.new(pctx)
+ local pdesc = Pango.FontDescription()
+ pdesc:set_absolute_size(11 * Pango.SCALE)
+ playout:set_font_description(pdesc)
+
+ local rows = {
+ "primary", "index", "geometry", "dpi", "dpi range", "outputs:"
+ }
+
+ local dpi_range = s.minimum_dpi and s.preferred_dpi and s.maximum_dpi
+ and (s.minimum_dpi.."-"..s.preferred_dpi.."-"..s.maximum_dpi)
+ or s.dpi.."-"..s.dpi
+
+ local values = {
+ s.primary and "true" or "false",
+ s.index,
+ s.x..":"..s.y.." "..s.width.."x"..s.height,
+ s.dpi,
+ dpi_range,
+ "",
+ }
+
+ for n, o in pairs(s.outputs) do
+ table.insert(rows, " "..n)
+ table.insert(values,
+ math.ceil(o.mm_width).."mm x "..math.ceil(o.mm_height).."mm"
+ )
+ end
+
+ local col1_width, col2_width, height = 0, 0, 0
+
+ -- Get the extents of the longest label.
+ for k, label in ipairs(rows) do
+ local attr, parsed = Pango.parse_markup(label..":", -1, 0)
+ playout.attributes, playout.text = attr, parsed
+ local _, logical = playout:get_pixel_extents()
+ col1_width = math.max(col1_width, logical.width+10)
+
+ attr, parsed = Pango.parse_markup(values[k], -1, 0)
+ playout.attributes, playout.text = attr, parsed
+ _, logical = playout:get_pixel_extents()
+ col2_width = math.max(col2_width, logical.width+10)
+
+ height = math.max(height, logical.height)
+ end
+
+ local dx = (s.width*factor - col1_width - col2_width - 5)/2
+ local dy = (s.height*factor - #values*height)/2 - height
+
+ -- Draw everything.
+ for k, label in ipairs(rows) do
+ local attr, parsed = Pango.parse_markup(label..":", -1, 0)
+ playout.attributes, playout.text = attr, parsed
+ playout:get_pixel_extents()
+ cr:move_to(dx, dy)
+ cr:show_layout(playout)
+
+ attr, parsed = Pango.parse_markup(values[k], -1, 0)
+ playout.attributes, playout.text = attr, parsed
+ local _, logical = playout:get_pixel_extents()
+ cr:move_to( dx+col1_width+5, dy)
+ cr:show_layout(playout)
+
+ dy = dy + 5 + logical.height
+ end
+end
+
+local function gen_ruler(h_or_v, factor, margins)
+ local ret = wibox.widget.base.make_widget()
+
+ function ret:fit()
+ local w, h
+ local rw, rh = root.size()
+ rw, rh = rw*factor, rh*factor
+
+ if h_or_v == "vertical" then
+ w = 1
+ h = rh + margins.top/2 + margins.bottom/2
+ else
+ w = rw + margins.left/2 + margins.right/2
+ h = 1
+ end
+
+ return w, h
+ end
+
+ function ret:draw(_, cr, w, h)
+ cr:set_source(color("#77000033"))
+ cr:set_line_width(2)
+ cr:set_dash({1,1},1)
+ cr:move_to(0, 0)
+ cr:line_to(w == 1 and 0 or w, h == 1 and 0 or h)
+ cr:stroke()
+ end
+
+ return ret
+end
+
+-- When multiple tags are present, only show the selected tag(s) for each screen.
+local function gen_screens(l, screens, args)
+ local margins = {left=50, right=50, top=30, bottom=30}
+
+ local ret = wibox.layout.manual()
+
+ local sreen_copies = {}
+
+ -- Keep a copy because it can change.
+ local rw, rh = root.size()
+
+ -- Find the current origin.
+ local x0, y0 = math.huge, math.huge
+
+ for s in screen do
+ x0, y0 = math.min(x0, s.geometry.x), math.min(y0, s.geometry.y)
+ local scr_cpy = gtable.clone(s.geometry, false)
+ scr_cpy.outputs = gtable.clone(s.outputs, false)
+ scr_cpy.primary = screen.primary == s
+
+ for _, prop in ipairs {
+ "dpi", "index", "maximum_dpi", "minimum_dpi", "preferred_dpi" } do
+ scr_cpy[prop] = s[prop]
+ end
+
+ table.insert(sreen_copies, scr_cpy)
+ end
+
+ function ret:fit()
+ local w = margins.left+(x0+rw)/screen_scale_factor + 5 + margins.right
+ local h = margins.top +(y0+rh)/screen_scale_factor + 5 + margins.bottom
+ return w, h
+ end
+
+ -- Add the rulers.
+ for _, s in ipairs(sreen_copies) do
+ ret:add_at(
+ gen_ruler("vertical" , 1/screen_scale_factor, margins),
+ {x=margins.left+s.x/screen_scale_factor, y =margins.top/2}
+ )
+ ret:add_at(
+ gen_ruler("vertical" , 1/screen_scale_factor, margins),
+ {x=margins.left+s.x/screen_scale_factor+s.width/screen_scale_factor, y =margins.top/2}
+ )
+ ret:add_at(
+ gen_ruler("horizontal", 1/screen_scale_factor, margins),
+ {y=margins.top+s.y/screen_scale_factor, x =margins.left/2}
+ )
+ ret:add_at(
+ gen_ruler("horizontal", 1/screen_scale_factor, margins),
+ {y=margins.top+s.y/screen_scale_factor+s.height/screen_scale_factor, x =margins.left/2}
+ )
+ end
+
+ -- Print an outline for the screens
+ for k, s in ipairs(sreen_copies) do
+ s.widget = wibox.widget.base.make_widget()
+
+ local wb = gen_fake_taglist_wibar(screens[k].tags)
+ wb.forced_width = s.width/screen_scale_factor
+
+ -- The clients have an absolute geometry, transform to relative.
+ if screens[k].tags[1] then
+ for _, geo in ipairs(screens[k].tags[1].client_geo) do
+ geo.x = geo.x - s.x
+ geo.y = geo.y - s.y
+ end
+ end
+
+ local clients_w = gen_fake_clients(screens[k].tags[1], args)
+
+ local content = wibox.widget {
+ wb,
+ clients_w,
+ nil,
+ layout = wibox.layout.align.vertical,
+ forced_width = s.width/screen_scale_factor,
+ }
+
+ function s.widget:fit()
+ return s.width/screen_scale_factor, s.height/screen_scale_factor
+ end
+
+ function s.widget:draw(_, cr, w, h)
+ cr:set_source(color("#00000044"))
+ cr:set_line_width(1.5)
+ cr:set_dash({10,4},1)
+ cr:rectangle(1,1,w-2,h-2)
+ cr:stroke()
+
+ if args.display_label ~= false then
+ draw_info(s, cr, 1/screen_scale_factor)
+ end
+ end
+
+ function s.widget:after_draw_children(_, cr)
+ if args.display_mouse and mouse.screen.index == s.index then
+ local rel_x = mouse.coords().x - s.x
+ local rel_y = mouse.coords().y - s.y
+ draw_mouse(cr, rel_x/screen_scale_factor+5, rel_y/screen_scale_factor+5)
+ end
+ end
+
+ function s.widget:layout(_, width, height)
+ return { wibox.widget.base.place_widget_at(
+ content, 0, 0, width, height
+ ) }
+ end
+
+ ret:add_at(s.widget, {x=margins.left+s.x/screen_scale_factor, y=margins.top+s.y/screen_scale_factor})
+ end
+
+ l:add(wrap_timeline(wibox.widget {
+ markup = "Current tags:",
+ opacity = 0.5,
+ widget = wibox.widget.textbox
+ }, true))
+ l:add(wrap_timeline(ret,false))
+end
+
+-- When a single screen is present, show all tags.
+local function gen_noscreen(l, tags, args)
+ local w2 = draw_lines()
+ l:add(wrap_timeline(wibox.widget {
+ markup = "Current screens:",
+ opacity = 0.5,
+ widget = wibox.widget.textbox
+ }, true))
+
+ local wrapped_wibar = wibox.widget {
+ gen_fake_taglist_wibar(tags, w2),
+ fill_space = false,
+ fit = fixed_fit2,
+ layout = wibox.layout.fixed.horizontal
+ }
+
+ l:add(wrap_timeline(wrapped_wibar, false))
+
+ if #capi.client.get() > 0 or args.show_empty then
+ local w3, h3 = w2:fit({dpi=96}, 9999, 9999)
+ assert(w3 < 5000 and h3 < 5000)
+
+ l:add(wrap_timeline(w2, false))
+ l:add(wrap_timeline(gen_fake_pager_widget(tags, w2, args), false))
+ end
+end
+
+local function gen_timeline(args)
+ local l = wibox.layout.fixed.vertical()
+ l.fit = fixed_fit2
+
+ l:add(gen_label("Begin"))
+
+ for _, event in ipairs(history) do
+ local ret = event.callback()
+ if event.event == "event" then
+ l:add(wrap_timeline(wibox.widget {
+ markup = ""..event.description.."",
+ widget = wibox.widget.textbox
+ }, true))
+ elseif event.event == "tags" and #ret == 1 and not args.display_screen then
+ gen_noscreen(l, ret[1].tags, args)
+ elseif event.event == "tags" and (#ret > 1 or args.display_screen) then
+ gen_screens(l, ret, args)
+ end
+ end
+
+ -- Spacing.
+ l:add(wrap_timeline( wibox.widget {
+ draw = function() end,
+ fit = function() return 1, 10 end,
+ widget = wibox.widget.base.make_widget()
+ }))
+
+ l:add(gen_label("End"))
+
+ return l
+end
+
+local function wrap_with_arrows(widget)
+ local w = widget:fit({dpi=96}, 9999,9999)
+
+ local arrows = wibox.widget.base.make_widget()
+
+ function arrows:fit()
+ return w, 10
+ end
+
+ function arrows:draw(_, cr, w2, h)
+
+ cr:set_line_width(1)
+ cr:set_source_rgba(1, 0, 0, 0.3)
+
+ w2 = math.min(640, w2)
+
+ local x = (w2 % 24) / 2
+
+ while x + 15 < w2 do
+ cr:move_to(x+2 , 0 )
+ cr:line_to(x+10 , h-1)
+ cr:line_to(x+20 , 0 )
+ cr:stroke()
+ x = x + 24
+ end
+ end
+
+ assert(w < 5000)
+
+ return wibox.widget {
+ widget,
+ {
+ draw = function() end,
+ fit = function() return 1, 10 end,
+ widget = wibox.widget.base.make_widget()
+ },
+ {
+ markup = "Code for this sequence",
+ align = "center",
+ foced_width = w,
+ widget = wibox.widget.textbox,
+ },
+ arrows,
+ forced_width = w,
+ fit = fixed_fit2,
+ layout = wibox.layout.fixed.vertical
+ }
+end
+
+function module.display_tags()
+ local function do_it()
+ local ret = {}
+ for s in screen do
+ local st = {}
+ for _, t in ipairs(s.tags) do
+ -- Copy just enough for the taglist to work.
+ table.insert(st, {
+ name = t.name,
+ selected = t.selected,
+ icon = t.icon,
+ screen = t.screen,
+ data = t.data,
+ clients = t.clients,
+ layout = t.layout,
+ master_width_factor = t.master_width_factor,
+ client_geo = fake_arrange(t),
+ })
+ assert(#st[#st].client_geo == #t:clients())
+ end
+ table.insert(ret, {tags=st})
+ end
+ return ret
+ end
+
+ table.insert(history, {event="tags", callback = do_it})
+end
+
+function module.add_event(description, callback)
+ assert(description and callback)
+ table.insert(history, {
+ event = "event",
+ description = description,
+ callback = callback
+ })
+end
+
+function module.execute(args)
+ local widget = gen_timeline(args or {})
+ require("gears.timer").run_delayed_calls_now()
+ require("gears.timer").run_delayed_calls_now()
+ require("gears.timer").run_delayed_calls_now()
+
+ if (not args) or args.show_code_pointer ~= false then
+ widget = wrap_with_arrows(widget)
+ end
+
+ local w, h = widget:fit({dpi=96}, 9999,9999)
+ wibox.widget.draw_to_svg_file(widget, image_path..".svg", w, h)
+end
+
+loadfile(file_path)(module)