diff --git a/lib/awful/screen.lua b/lib/awful/screen.lua index c1411470..08970644 100644 --- a/lib/awful/screen.lua +++ b/lib/awful/screen.lua @@ -19,6 +19,8 @@ local gdebug = require("gears.debug") local gmath = require("gears.math") local object = require("gears.object") local grect = require("gears.geometry").rectangle +local gsurf = require("gears.surface") +local cairo = require("lgi").cairo local function get_screen(s) return s and capi.screen[s] @@ -201,6 +203,25 @@ function screen.object.get_tiling_area(s) } end +--- Take a screenshot of the physical screen. +-- +-- Reading this (read only) property returns a screenshot of the physical +-- (Xinerama) screen as a cairo surface. +-- +-- @property content +-- @tparam gears.surface content + +function screen.object.get_content(s) + local geo = s.geometry + local source = gsurf(root.content()) + local target = source:create_similar(cairo.Content.COLOR, geo.width, geo.height) + local cr = cairo.Context(target) + cr:set_source_surface(source, -geo.x, -geo.y) + cr:rectangle(0, 0, geo.width, geo.height) + cr:fill() + return target +end + --- Get or set the screen padding. -- -- @deprecated awful.screen.padding diff --git a/root.c b/root.c index 4b5ebfbe..2b803089 100644 --- a/root.c +++ b/root.c @@ -500,6 +500,29 @@ luaA_root_wallpaper(lua_State *L) return 1; } + +/** Get the content of the root window as a cairo surface. + * + * @property content + * @tparam surface A cairo surface with the root window content (aka the whole surface from every screens). + * @see gears.surface + */ +static int +luaA_root_get_content(lua_State *L) +{ + cairo_surface_t *surface; + + surface = cairo_xcb_surface_create(globalconf.connection, + globalconf.screen->root, + globalconf.default_visual, + globalconf.screen->width_in_pixels, + globalconf.screen->height_in_pixels); + + lua_pushlightuserdata(L, surface); + return 1; +} + + /** Get the size of the root window. * * @return Width of the root window. @@ -608,6 +631,7 @@ const struct luaL_Reg awesome_root_methods[] = { "fake_input", luaA_root_fake_input }, { "drawins", luaA_root_drawins }, { "_wallpaper", luaA_root_wallpaper }, + { "content", luaA_root_get_content}, { "size", luaA_root_size }, { "size_mm", luaA_root_size_mm }, { "tags", luaA_root_tags }, diff --git a/tests/test-screenshot.lua b/tests/test-screenshot.lua new file mode 100644 index 00000000..34f812f8 --- /dev/null +++ b/tests/test-screenshot.lua @@ -0,0 +1,165 @@ +-- This test suite tests the various screenshot related APIs. +-- +-- Credit: https://www.reddit.com/r/awesomewm/comments/i6nf7z/need_help_writing_a_color_picker_widget_using_lgi/ +local wibox = require("wibox") +local spawn = require("awful.spawn") +local gsurface = require("gears.surface") +local lgi = require 'lgi' +local cairo = lgi.cairo +local gdk = lgi.Gdk + +-- Dummy blue client for the client.content test +-- the lua_executable portion may need to get ironed out. I need to specify 5.3 +local lua_executable = os.getenv("LUA") +if lua_executable == nil or lua_executable == "" then + lua_executable = "lua5.3" +end +local client_dim = 250 +local tiny_client_code_template = [[ +local Gtk = require('lgi').Gtk +local Gdk = require('lgi').Gdk +local class = 'client' +window = Gtk.Window {default_width=%d, default_height=%d, title='title'} +window:set_wmclass(class, class) +window:override_background_color(0, + Gdk.RGBA({red = 0, green = 0, blue = 1, alpha = 1})) +window:show_all() +Gtk:main{...} +]] +local tiny_client = { lua_executable, "-e", string.format( + tiny_client_code_template, client_dim, client_dim)} + +-- Split in the screen into 2 distict screens. +screen[1]:split() + +-- Add a green wibox on screen 1 at (100, 100) +wibox { + bg = "#00ff00", + visible = true, + width = 100, + height = 100, + x = 100, + y = 100, +} + +local corners = {} + +-- Add red squares at 1 pixel away from each screen corners. +for s in screen do + -- Top left + table.insert(corners, { + x = s.geometry.x + 1, + y = s.geometry.y + 1, + }) + + -- Top right + table.insert(corners, { + x = s.geometry.x + s.geometry.width - 11, + y = s.geometry.y + 1, + }) + + -- Bottom left + table.insert(corners, { + x = s.geometry.x + 1, + y = s.geometry.y + s.geometry.height - 11, + }) + + -- Bottom right + table.insert(corners, { + x = s.geometry.x + s.geometry.width - 11, + y = s.geometry.y + s.geometry.height - 11, + }) + +end + +-- Make 10x10 wibox in each corner to eliminate any geometry errors +for _, corner in ipairs(corners) do + corner.bg = "#ff0000" + corner.visible = true + corner.width = 10 + corner.height = 10 + + wibox(corner) +end + +local function copy_to_image_surface(content, w, h) + local sur = gsurface(content) + + local img = cairo.ImageSurface(cairo.Format.RGB24, w, h) + local cr = cairo.Context(img) + + cr:set_source_surface(sur) + cr:paint() + img:finish() + + return img +end + +local function get_pixel(img, x, y) + local bytes = gdk.pixbuf_get_from_surface(img, x, y, 1, 1):get_pixels() + return "#" .. bytes:gsub('.', function(c) return ('%02x'):format(c:byte()) end) +end + +local steps = {} + +-- Check the whole root window. +table.insert(steps, function() + local img = copy_to_image_surface(root.content(), root.size()) + local root_width, root_height = root.size() + + if get_pixel(img, 100, 100) ~= "#00ff00" then return end + if get_pixel(img, 2, 2) ~= "#ff0000" then return end + + assert(get_pixel(img, 100, 100) == "#00ff00") + assert(get_pixel(img, 199, 199) == "#00ff00") + assert(get_pixel(img, 201, 201) ~= "#00ff00") + + assert(get_pixel(img, 2, 2) == "#ff0000") + assert(get_pixel(img, root_width - 2, 2) == "#ff0000") + assert(get_pixel(img, 2, root_height - 2) == "#ff0000") + assert(get_pixel(img, root_width - 2, root_height - 2) == "#ff0000") + + return true +end) + +-- Check the screen.content +table.insert(steps, function() + for s in screen do + + local geo = s.geometry + local img = copy_to_image_surface(s.content, geo.width, geo.height) + + assert(get_pixel(img, geo.x + 4, geo.y + 4) == "#ff0000") + assert(get_pixel(img, geo.x + geo.width - 4, geo.y + 4) == "#ff0000") + assert(get_pixel(img, geo.x + 4, geo.y + geo.height - 4) == "#ff0000") + assert(get_pixel(img, geo.x + geo.width - 4, geo.y + geo.height - 4) == "#ff0000") + + end + + -- Spawn for the client.content test + assert(#client.get() == 0) + spawn(tiny_client) + + return true +end) + +-- Check the client.content +table.insert(steps, function() + if #client.get() ~= 1 then return end + local c = client.get()[1] + local geo = c:geometry() + local img = copy_to_image_surface(c.content, geo.width, geo.height) + + if get_pixel(img, math.floor(geo.width / 2), math.floor(geo.height / 2)) ~= "#0000ff" then + return + end + + -- Make sure the process finishes. Just `c:kill()` only + -- closes the window. Adding some handlers to the GTK "app" + -- created some unwanted side effects in the CI. + awesome.kill(c.pid, 9) + + return true +end) + +require("_runner").run_steps(steps)