diff --git a/tests/_multi_screen.lua b/tests/_multi_screen.lua new file mode 100644 index 00000000..fab8f367 --- /dev/null +++ b/tests/_multi_screen.lua @@ -0,0 +1,512 @@ +-- Helper module to replicate a set of steps across multiple screen scenarios +-- Ideas for improvements: +-- * Export an iterator so the suits can call the screen change step by step +-- themselves instead of the current all or nothing +-- * Offer multiple scenarios such as screen add, remove, resize/swap instead +-- of having to test all corner cases all the time. +local wibox = require("wibox") +local surface = require("gears.surface") +local wallpaper = require("gears.wallpaper") +local color = require("gears.color") +local shape = require("gears.shape") + +local module = {} + +-- Get the original canvas size before it is "lost" +local canvas_w, canvas_h = root.size() + +local half_w, half_h = math.floor(canvas_w/2), math.floor(canvas_h/2) +local quarter_w = math.floor(canvas_w/4) + +-- Create virtual screens. +-- The steps will be executed on each set of screens. +local dispositions = { + -- A simple classic, 2 identical screens side by side + { + function() + return { + x = 0, + y = 0, + width = half_w, + height = canvas_h, + } + end, + function() + return { + x = half_w, + y = 0, + width = half_w, + height = canvas_h, + } + end, + }, + + -- Take the previous disposition and swap the screens + { + function() + return { + x = half_w, + y = 0, + width = half_w, + height = canvas_h, + } + end, + function() + return { + x = 0, + y = 0, + width = half_w, + height = canvas_h, + } + end, + keep = true, + }, + + -- Take the previous disposition and resize the leftmost one + { + function() + return { + x = quarter_w, + y = 0, + width = 3*quarter_w, + height = canvas_h, + } + end, + function() + return { + x = 0, + y = 0, + width = quarter_w, + height = half_h, + } + end, + keep = true, + }, + + -- Take the previous disposition and remove the leftmost screen + { + function() + return { + x = quarter_w, + y = 0, + width = 3*quarter_w, + height = canvas_h, + } + end, + function() + return nil + end, + keep = true, + }, + + -- Less used, but still quite common vertical setup + { + function() + return { + x = 0, + y = 0, + width = canvas_w, + height = half_h, + } + end, + function() + return { + x = 0, + y = half_h, + width = canvas_w, + height = half_h, + } + end + }, + + -- Another popular setup, 22" screens with a better 31" in the middle. + -- So, 2 smaller vertical screen with a larger central horizontal one. + { + -- Left + function() + return { + x = 0, + y = 0, + width = quarter_w, + height = canvas_h, + } + end, + -- Right, this test non-continous screen index on purpose, as this setup + -- Often requires dual-GPU, it _will_ happen. + function() + return { + x = canvas_w-quarter_w, + y = 0, + width = quarter_w, + height = canvas_h, + } + end, + -- Center + function() + return { + x = quarter_w, + y = 0, + width = half_w, + height = math.floor(canvas_h*(9/16)), + } + end, + }, + + -- Same as the previous one, but with the gap centered + { + -- Left + function() + return { + x = 0, + y = 0, + width = quarter_w, + height = canvas_h, + } + end, + -- Right, this test non-continous screen index on purpose, as this setup + -- Often requires dual-GPU, it _will_ happen. + function() + return { + x = canvas_w-quarter_w, + y = 0, + width = quarter_w, + height = canvas_h, + } + end, + -- Center + function() + return { + x = quarter_w, + y = math.ceil((canvas_h-(canvas_h*(9/16)))/2), + width = half_w, + height = math.floor(canvas_h*(9/16)), + } + end, + + -- Keep the same screens as the last test, just move them + keep = true, + }, + + -- AMD Eyefinity / (old) NVIDIA MOSAIC style config (symmetric grid) + { + function() + return { + x = 0, + y = 0, + width = half_w, + height = half_h, + } + end, + function() + return { + x = half_w, + y = 0, + width = half_w, + height = half_h, + } + end, + function() + return { + x = 0, + y = half_h, + width = half_w, + height = half_h, + } + end, + function() + return { + x = half_w, + y = half_h, + width = half_w, + height = half_h, + } + end, + }, + + -- Corner case 1: Non-continuous screens + -- If there is nothing and client is drag&dropped into that area, some + -- geometry callbacks may break in nil index. + { + function() + return { + x = 0, + y = 0, + width = quarter_w, + height = canvas_h, + } + end, + function() + return { + x = canvas_w-quarter_w, + y = 0, + width = quarter_w, + height = canvas_h, + } + end, + }, + + -- Corner case 2: Nothing at 0x0. + -- As some position may fallback to 0x0 this need to be tested often. It + -- also caused issues such as #154 + { + function() + return { + x = 0, + y = half_w, + width = half_w, + height = half_w, + } + end, + function() + return { + x = half_w, + y = 0, + width = half_w, + height = canvas_h, + } + end + }, + + -- Corner case 3: Many very small screens. + -- On the embedded side of the compuverse, it is possible + -- to buy 32x32 RGB OLED screens. They are usually used to display single + -- status icons, but why not use them as a desktop! This is a critical + -- market for AwesomeWM. Here's a 256px wide strip of tiny screens. + -- This may cause wibars to move to the wrong screen accidentally + { + function() return { x = 0 , y = 0, width = 32, height = 32, } end, + function() return { x = 32 , y = 0, width = 32, height = 32, } end, + function() return { x = 64 , y = 0, width = 32, height = 32, } end, + function() return { x = 96 , y = 0, width = 32, height = 32, } end, + function() return { x = 128, y = 0, width = 32, height = 32, } end, + function() return { x = 160, y = 0, width = 32, height = 32, } end, + function() return { x = 192, y = 0, width = 32, height = 32, } end, + function() return { x = 224, y = 0, width = 32, height = 32, } end, + }, + + -- Corner case 4: A screen taller than more than 1 other screen. + -- this may cause some issues with client resize and drag&drop move + { + function() + return { + x = 0, + y = 0, + width = half_w, + height = canvas_h, + } + end, + function() + return { + x = half_w, + y = 0, + width = half_w, + height = math.floor(canvas_h/3), + } + end, + function() + return { + x = half_w, + y = math.floor(canvas_h/3), + width = half_w, + height = math.floor(canvas_h/3), + } + end, + function() + return { + x = half_w, + y = 2*math.floor(canvas_h/3), + width = half_w, + height = math.floor(canvas_h/3), + } + end + }, + + -- The overlapping corner case isn't supported. There is valid use case, + -- such as a projector with a smaller resolution than the laptop screen + -- in non-scaling mirror mode, but it isn't worth the complexity it brings. +} + +local function check_tag_indexes() + for s in screen do + for i, t in ipairs(s.tags) do + assert(t.index == i) + assert(t.screen == s) + end + end +end + +local colors = { + "#000030", + "#300000", + "#043000", + "#302E00", + "#002C30", + "#300030", + "#301C00", + "#140030", +} + +-- Paint it black +local function clear_screen() + for s in screen do + local sur = surface.widget_to_surface( + wibox.widget { + bg = "#000000", + widget = wibox.container.background + }, + s.geometry.width, + s.geometry.height + ) + wallpaper.fit(sur, s, "#000000") + end +end + +-- Make it easier to debug the tests by showing the screen geometry when the +-- tests are executed. +local function show_screens() + wallpaper.set(color("#000000")) -- Should this clear the wallpaper? It doesn't + + -- Add a wallpaper on each screen + for i=1, screen.count() do + local s = screen[i] + + local w = wibox.widget { + { + text = table.concat{ + "Screen: ",i,"\n", + s.geometry.width,"x",s.geometry.height, + "+",s.geometry.x,",",s.geometry.y + }, + valign = "center", + align = "center", + widget = wibox.widget.textbox, + }, + bg = colors[i], + fg = "#ffffff", + shape_border_color = "#ff0000", + shape_border_width = 1, + shape = shape.rectangle, + widget = wibox.container.background + } + local sur = surface.widget_to_surface( + w, + s.geometry.width, + s.geometry.height + ) + wallpaper.fit(sur, s) + end +end + +local function add_steps(real_steps, new_steps) + for _, dispo in ipairs(dispositions) do + -- Cleanup + table.insert(real_steps, function() + if #client.get() == 0 then return true end + + for _, c in ipairs(client.get()) do + c:kill() + end + end) + + table.insert(real_steps, function() + clear_screen() + local keep = dispo.keep or false + local old = {} + local geos = {} + + check_tag_indexes() + + if keep then + for _, sf in ipairs(dispo) do + local geo = sf and sf() or nil + + -- If the function return nothing, assume the screen need to + -- be destroyed. + table.insert(geos, geo or false) + end + + -- Removed screens need to be explicit. + assert(#geos >= screen.count()) + + -- Keep a cache to avoid working with invalid data + local old_screens = {} + for s in screen do + table.insert(old_screens, s) + end + + for i, s in ipairs(old_screens) do + -- Remove the screen (if no new geometry is given + if not geos[i] then + s:fake_remove() + else + local cur_geo = s.geometry + for _, v in ipairs {"x", "y", "width", "height" } do + cur_geo[v] = geos[i][v] or cur_geo[v] + end + s:fake_resize( + cur_geo.x, + cur_geo.y, + cur_geo.width, + cur_geo.height + ) + end + end + + -- Add the remaining screens + for i=#old_screens + 1, #geos do + local geo = geos[i] + screen.fake_add(geo.x, geo.y, geo.width, geo.height) + end + + check_tag_indexes() + else + -- Move all existing screens out of the way (to avoid temporary overlapping) + for s in screen do + s:fake_resize(canvas_w, canvas_h, 1, 1) + table.insert(old, s) + end + + -- Add the new screens + for _, sf in ipairs(dispo) do + local geo = sf() + screen.fake_add(geo.x, geo.y, geo.width, geo.height) + table.insert(geos, geo) + end + + -- Remove old screens + for _, s in ipairs(old) do + s:fake_remove() + end + end + + show_screens() + + -- Check the result is correct + local expected_count = 0 + for _,v in ipairs(geos) do + expected_count = expected_count + (v and 1 or 0) + end + + assert(expected_count == screen.count()) + for k, geo in ipairs(geos) do + if geo then + local sgeo = screen[k].geometry + assert(geo.x == sgeo.x) + assert(geo.y == sgeo.y) + assert(geo.width == sgeo.width ) + assert(geo.height == sgeo.height) + end + end + + return true + end) + + for _, step in ipairs(new_steps) do + table.insert(real_steps, step) + end + end +end + +return setmetatable(module, { + __call = function(_,...) return add_steps(...) end +})