awesome/lib/awful/wallpaper.lua

875 lines
26 KiB
Lua

---------------------------------------------------------------------------
--- Allows to use the wibox widget system to draw the wallpaper.
--
-- Rather than simply having a function to set an image
-- (stretched, centered or tiled) like most wallpaper tools, this module
-- leverage the full widget system to draw the wallpaper. Note that the result
-- is **not** interactive. If you want an interactive wallpaper, better use
-- a `wibox` object with the `below` property set to `true` and maximized
-- using `awful.placement.maximized`.
--
-- It is possible to create an `awful.wallpaper` object from any places, but
-- it is recommanded to do it from the `request::wallpaper` signal handler.
-- That signal is called everytime something which could affect the wallpaper
-- rendering changes, such as new screens.
--
-- Single image
-- ============
--
-- This is the default `rc.lua` wallpaper format. It fills the whole screen
-- and stretches the image while keeping the aspect ratio.
--
--@DOC_awful_wallpaper_mazimized1_EXAMPLE@
--
-- If the image aspect ratio doesn't match, the `bg` property can be used to
-- fill the empty area:
--
--@DOC_awful_wallpaper_mazimized2_EXAMPLE@
--
-- It is also possible to stretch the image:
--
--@DOC_awful_wallpaper_mazimized3_EXAMPLE@
--
-- Finally, it is also possible to use simpler "branding" in a corner using
-- `awful.placement`:
--
--@DOC_awful_wallpaper_corner1_EXAMPLE@
--
-- Tiled
-- =====
--
-- This example tiles an image:
--
--@DOC_awful_wallpaper_tiled1_EXAMPLE@
--
-- This one tiles a shape using the `wibox.widget.separator` widget:
--
--@DOC_awful_wallpaper_tiled2_EXAMPLE@
--
-- See the `wibox.container.tile` for more advanced tiling configuration
-- options.
--
-- Solid colors and gradients
-- ==========================
--
-- Solid colors can be set using the `bg` property mentionned above. It
-- is also possible to set gradients:
--
--@DOC_awful_wallpaper_gradient1_EXAMPLE@
--
--@DOC_awful_wallpaper_gradient2_EXAMPLE@
--
-- Widgets
-- =======
--
-- It is possible to create a wallpaper using any widgets. However, keep
-- in mind that the wallpaper surface is not interactive, so some widgets
-- like the sliders will render, but will not behave correctly. Also, it
-- is not recommanded to update the wallpaper too often. This is very slow.
--
--@DOC_awful_wallpaper_widget2_EXAMPLE@
--
-- Cairo graphics API
-- ==================
--
-- AwesomeWM widgets are backed by Cairo. So it is always possible to get
-- access to the Cairo context directly to do some vector art:
--
--@DOC_awful_wallpaper_widget1_EXAMPLE@
--
--
-- SVG vector images
-- =================
--
-- SVG are supported if `librsvg` is installed. Please note that `librsvg`
-- doesn't implement all filters you might find in the latest version of
-- your web browser. It is possible some advanced SVG will not look exactly
-- as they do in a web browser or even Inkscape. However, for most images,
-- it should look identical.
--
-- Our SVG support goes beyond simple rendering. It is possible to set a
-- custom CSS stylesheet (see `wibox.widget.imagebox.stylesheet`):
--
--@DOC_awful_wallpaper_svg_EXAMPLE@
--
-- Note that in the example above, it is raw SVG code, but it is also possible
-- to use a file path. If you have a `.svgz`, you need to uncompress it first
-- using `gunzip` or a software like Inkscape.
--
-- Multiple screen
-- ===============
--
-- The default `rc.lua` creates a new wallpaper everytime `request::wallpaper`
-- is emitted. This is well suited for having a single wallpaper per screen.
-- It is also much simpler to implement slideshows and add/remove screens.
--
-- However, it isn't wall suited for wallpaper rendered across multiple screens.
-- For this case, it is better to capture the return value of `awful.wallpaper {}`
-- as a global variable. Then manually call `add_screen` and `remove_screen` when
-- needed. A shortcut can be to do:
--
-- @DOC_text_awful_wallpaper_multi_screen_EXAMPLE@
--
-- Slideshow
-- =========
--
-- Slideshows (changing the wallpaper after a few minutes) can be implemented
-- directly using a timer and callback, but it is more elegant to simply request
-- a new wallpaper, then get a random image from within the request handler. This
-- way, corner cases such as adding and removing screens are handled:
--
--@DOC_awful_wallpaper_slideshow1_EXAMPLE@
--
-- @author Emmanuel Lepage Vallee <elv1313@gmail.com>
-- @copyright 2019 Emmanuel Lepage Vallee
-- @popupmod awful.wallpaper
---------------------------------------------------------------------------
require("awful._compat")
local gtable = require( "gears.table" )
local gobject = require( "gears.object" )
local gcolor = require( "gears.color" )
local gtimer = require( "gears.timer" )
local surface = require( "gears.surface" )
local base = require( "wibox.widget.base" )
local background = require( "wibox.container.background")
local beautiful = require( "beautiful" )
local cairo = require( "lgi" ).cairo
local draw = require( "wibox.widget" ).draw_to_cairo_context
local grect = require( "gears.geometry" ).rectangle
local capi = { screen = screen, root = root }
local module = {}
local function get_screen(s)
return s and capi.screen[s]
end
-- Screen as key, wallpaper as values.
local pending_repaint = setmetatable({}, {__mode = 'k'})
local backgrounds = setmetatable({}, {__mode = 'k'})
local panning_modes = {}
-- Get a list of all screen areas.
local function get_rectangles(screens, honor_workarea, honor_padding)
local ret = {}
for _, s in ipairs(screens) do
table.insert(ret, s:get_bounding_geometry {
honor_padding = honor_padding,
honor_workarea = honor_workarea
})
end
return ret
end
-- Outer perimeter of all rectangles.
function panning_modes.outer(self)
local rectangles = get_rectangles(self.screens, self.honor_workarea, self.honor_padding)
local p1, p2 = {x = math.huge, y = math.huge}, {x = 0, y = 0}
for _, rect in ipairs(rectangles) do
p1.x, p1.y = math.min(p1.x, rect.x), math.min(p1.y, rect.y)
p2.x, p2.y = math.max(p2.x, rect.x + rect.width), math.max(p2.y, rect.y + rect.height)
end
-- Never try to paint this, it would freeze the system.
assert(p1.x ~= math.huge and p1.y ~= math.huge, "Setting wallpaper failed"..#self.screens)
return {
x = p1.x,
y = p1.y,
width = p2.x - p1.x,
height = p2.y - p1.y,
}
end
-- Horizontal inner perimeter of all rectangles.
function panning_modes.inner_horizontal(self)
local rectangles = get_rectangles(self.screens, self.honor_workarea, self.honor_padding)
local p1, p2 = {x = math.huge, y = 0}, {x = 0, y = math.huge}
for _, rect in ipairs(rectangles) do
p1.x, p1.y = math.min(p1.x, rect.x), math.max(p1.y, rect.y)
p2.x, p2.y = math.max(p2.x, rect.x + rect.width), math.min(p2.y, rect.y + rect.height)
end
-- Never try to paint this, it would freeze the system.
assert(p1.x ~= math.huge and p2.y ~= math.huge, "Setting wallpaper failed")
return {
x = p1.x,
y = p1.y,
width = p2.x - p1.x,
height = p2.y - p1.y,
}
end
-- Vertical inner perimeter of all rectangles.
function panning_modes.inner_vertical(self)
local rectangles = get_rectangles(self.screens, self.honor_workarea, self.honor_padding)
local p1, p2 = {x = 0, y = math.huge}, {x = math.huge, y = 0}
for _, rect in ipairs(rectangles) do
p1.x, p1.y = math.max(p1.x, rect.x), math.min(p1.y, rect.y)
p2.x, p2.y = math.min(p2.x, rect.x + rect.width), math.max(p2.y, rect.y + rect.height)
end
-- Never try to paint this, it would freeze the system.
assert(p1.y ~= math.huge and p2.a ~= math.huge, "Setting wallpaper failed")
return {
x = p1.x,
y = p1.y,
width = p2.x - p1.x,
height = p2.y - p1.y,
}
end
-- Best or vertical and horizontal "inner" modes.
function panning_modes.inner(self)
local vert = panning_modes.inner_vertical(self)
local hori = panning_modes.inner_horizontal(self)
if vert.width <= 0 or vert.height <= 0 then return hori end
if hori.width <= 0 or hori.height <= 0 then return vert end
if vert.width * vert.height > hori.width * hori.height then
return vert
else
return hori
end
end
local function paint()
if not next(pending_repaint) then return end
local root_width, root_height = capi.root.size()
-- Get the current wallpaper content.
local source = surface(root.wallpaper())
local target, cr
-- It's possible that a wallpaper for 1 screen is set using another tool, so make
-- sure we copy the current content.
if source then
target = source:create_similar(cairo.Content.COLOR, root_width, root_height)
cr = cairo.Context(target)
-- Copy the old wallpaper to the new one
cr:save()
cr.operator = cairo.Operator.SOURCE
cr:set_source_surface(source, 0, 0)
for s in screen do
cr:rectangle(
s.geometry.x,
s.geometry.y,
s.geometry.width,
s.geometry.height
)
end
cr:clip()
cr:paint()
cr:restore()
else
target = cairo.ImageSurface(cairo.Format.RGB32, root_width, root_height)
cr = cairo.Context(target)
end
local walls = {}
for _, wall in pairs(backgrounds) do
walls[wall] = true
end
-- Not supposed to happen, but there is enough API surface for
-- it to be a side effect of some signals. Calling the panning
-- mode callback with zero screen is not supported.
if not next(walls) then
return
end
for wall in pairs(walls) do
local geo = type(wall._private.panning_area) == "function" and
wall._private.panning_area(wall) or
panning_modes[wall._private.panning_area](wall)
-- If false, this panning area isn't well suited for the screen geometry.
if geo.width > 0 or geo.height > 0 then
local uncovered_areas = grect.area_remove(get_rectangles(wall.screens, false, false), geo)
cr:save()
-- Prevent overwrite then there is multiple non-continuous screens.
for _, s in ipairs(wall.screens) do
cr:rectangle(
s.geometry.x,
s.geometry.y,
s.geometry.width,
s.geometry.height
)
end
cr:clip()
-- The older surface might contain garbage, optionally clean it.
if wall.uncovered_areas_color then
cr:set_source(gcolor(wall.uncovered_areas_color))
for _, area in ipairs(uncovered_areas) do
cr:rectangle(area.x, area.y, area.width, area.height)
cr:fill()
end
end
if not wall._private.container then
wall._private.container = background()
wall._private.container.bg = wall._private.bg or beautiful.wallpaper_bg or "#000000"
wall._private.container.fg = wall._private.fg or beautiful.wallpaper_fg or "#ffffff"
wall._private.container.widget = wall.widget
end
local a_context = {
dpi = wall._private.context.dpi
}
-- Pick the lowest DPI.
if not a_context.dpi then
a_context.dpi = math.huge
for _, s in ipairs(wall.screens) do
a_context.dpi = math.min(
s.dpi and s.dpi or s.preferred_dpi, a_context.dpi
)
end
end
-- Fallback.
if not a_context.dpi then
a_context.dpi = 96
end
cr:translate(geo.x, geo.y)
draw(wall._private.container, cr, geo.width, geo.height, a_context)
cr:restore()
end
end
-- Set the wallpaper.
local pattern = cairo.Pattern.create_for_surface(target)
capi.root.wallpaper(pattern)
-- Limit some potential GC induced increase in memory usage.
-- But really, is someone is trying to apply wallpaper changes more
-- often than the GC is executed, they are doing it wrong.
target:finish()
end
local mutex = false
-- Uploading the surface to X11 is *very* resource intensive. Given the updates
-- will often happen in batch (like startup), make sure to only do one "real"
-- update.
local function update()
if mutex then return end
mutex = true
gtimer.delayed_call(function()
-- Remove the mutex first in case `paint()` raises an exception.
mutex = false
paint()
end)
end
capi.screen.connect_signal("removed", function(s)
if not backgrounds[s] then return end
backgrounds[s]:remove_screen(s)
update()
end)
capi.screen.connect_signal("property::geometry", function(s)
if not backgrounds[s] then return end
backgrounds[s]:repaint()
end)
--- The wallpaper widget.
--
-- When set, instead of using the `image_path` or `surface` properties, the
-- wallpaper will be defined as a normal `wibox` widget tree.
--
-- @property widget
-- @tparam wibox.widget widget
-- @see wibox.widget.imagebox
-- @see wibox.container.tile
--- The wallpaper DPI (dots per inch).
--
-- Each screen has a DPI. This value will be used by default, but sometime it
-- is useful to override the screen DPI and use a custom one. This makes
-- possible, for example, to draw the widgets bigger than they would otherwise
-- be.
--
-- If not DPI is defined, it will use the smallest DPI from any of the screen.
--
-- In this example, there is 3 screens with DPI of 100, 200 and 300. As you can
-- see, only the text size is affected. Many widgetds are DPI aware, but not all
-- of them. This is either because DPI isn't relevant to them or simply because it
-- isn't supported (like `wibox.widget.graph`).
--
-- @DOC_awful_wallpaper_dpi1_EXAMPLE@
--
-- @property dpi
-- @tparam[opt=screen.dpi] number dpi
-- @see screen
-- @see screen.dpi
--- The wallpaper screen.
--
-- Note that there can only be one wallpaper per screen. If there is more, one
-- will be chosen and all other ignored.
--
-- @property screen
-- @tparam screen screen
-- @see screens
-- @see add_screen
-- @see remove_screen
--- A list of screen for this wallpaper.
--
--@DOC_awful_wallpaper_screens1_EXAMPLE@
--
-- Some large wallpaper are made to span multiple screens.
-- @property screens
-- @tparam table screens
-- @see screen
-- @see add_screen
-- @see remove_screen
-- @see detach
--- The background color.
--
-- It will be used as the "fill" color if the `image` doesn't take all the
-- screen space. It will also be the default background for the `widget.
--
-- As usual with colors in `AwesomeWM`, it can also be a gradient or a pattern.
--
-- @property bg
-- @tparam gears.color bg
-- @usebeautiful beautiful.wallpaper_bg
-- @see gears.color
--- The foreground color.
--
-- This will be used by the `widget` (if any).
--
-- As usual with colors in `AwesomeWM`, it can also be a gradient or a pattern.
--
-- @property fg
-- @tparam gears.color fg
-- @see gears.color
--- The default wallpaper background color.
-- @beautiful beautiful.wallpaper_bg
-- @tparam gears.color wallpaper_bg
-- @usebeautiful beautiful.wallpaper_fg
-- @see bg
--- The default wallpaper foreground color.
--
-- This is useful when using widgets or text in the wallpaper. A wallpaper
-- created from a single image wont use this.
--
-- @beautiful beautiful.wallpaper_fg
-- @tparam gears.color wallpaper_fg
-- @see bg
--- Honor the workarea.
--
-- When set to `true`, the wallpaper will only fill the workarea space instead
-- of the entire screen. This means it wont be drawn below the `awful.wibar` or
-- docked clients. This is useful when using opaque bars. Note that it can cause
-- aspect ratio issues for the wallpaper `image` and add bars colored with the
-- `bg` color on the sides.
--
--@DOC_awful_wallpaper_workarea1_EXAMPLE@
--
-- @property honor_workarea
-- @tparam[opt=false] boolean honor_workarea
-- @see honor_padding
-- @see uncovered_areas
--- Honor the screen padding.
--
-- When set, this will look at the `screen.padding` property to restrict the
-- area where the wallpaper is rendered.
--
-- @DOC_awful_wallpaper_padding1_EXAMPLE@
--
-- @property honor_padding
-- @tparam boolean honor_padding
-- @see honor_workarea
-- @see uncovered_areas
--- Returns the list of screen(s) area which won't be covered by the wallpaper.
--
-- When `honor_workarea`, `honor_padding` or panning are used, some section of
-- the screen won't have a wallpaper. This returns a list of areas tables. Each
-- table has a `x`, `y`, `width` and `height` key.
--
-- @property uncovered_areas
-- @tparam table uncovered_areas
-- @see honor_workarea
-- @see honor_padding
-- @see uncovered_areas_color
--- The color for the uncovered areas.
--
-- Some application rely on the wallpaper for "fake" transparency. Even if an
-- area is hidden under a wibar (or other clients), its background can still
-- become visible. If you use such application and change your screen geometry
-- often enough, it is possible some areas would become filled with the remains
-- of previous wallpapers. This property allows to clean those areas with a solid
-- color or a gradient.
--
-- @property uncovered_areas_color
-- @tparam gears.color uncovered_areas_color
-- @see uncovered_areas
--- Defines where the wallpaper is placed when there is multiple screens.
--
-- When there is more than 1 screen, it is possible they don't have the same
-- resolution, position or orientation. Panning the wallpaper over them may look
-- better if a continuous rectangle is used rather than creating a virtual rectangle
-- around all screens.
--
-- The default algorithms are:
--
-- **outer:** *(default)*
--
-- Draw an imaginary rectangle around all screens.
--
-- @DOC_awful_wallpaper_panning_outer_EXAMPLE@
--
-- **inner:**
--
-- Take the largest area or either `inner_horizontal` or `inner_vertical`.
--
-- @DOC_awful_wallpaper_panning_inner_EXAMPLE@
--
-- **inner_horizontal:**
--
-- Take the smallest `x` value, the largest `x+width`, the smallest `y`
-- and the smallest `y+height`.
--
-- @DOC_awful_wallpaper_panning_inner_horizontal_EXAMPLE@
--
-- **inner_vertical:**
--
-- Take the smallest `y` value, the largest `y+height`, the smallest `x`
-- and the smallest `x+width`.
--
-- @DOC_awful_wallpaper_panning_inner_vertical_EXAMPLE@
--
-- **Custom function:**
--
-- It is also possible to define a custom function.
--
-- @DOC_awful_wallpaper_panning_custom_EXAMPLE@
--
-- @property panning_area
-- @tparam function|string panning_area
-- @see uncovered_areas
function module:set_panning_area(value)
value = value or "outer"
assert(type(value) == "function" or panning_modes[value], "Invalid panning mode: "..tostring(value))
self._private.panning_area = value
self:repaint()
self:emit_signal("property::panning_area", value)
end
function module:set_widget(w)
self._private.widget = base.make_widget_from_value(w)
if self._private.container then
self._private.container.widget = self._private.widget
end
self:repaint()
end
function module:get_widget()
return self._private.widget
end
function module:set_dpi(dpi)
self._private.context.dpi = dpi
self:repaint()
end
function module:get_dpi()
return self._private.context.dpi
end
function module:set_screen(s)
if not s then return end
self:_clear()
self:add_screen(s)
end
for _, prop in ipairs {"bg", "fg"} do
module["set_"..prop] = function(self, color)
if self._private.container then
self._private.container[prop] = color
end
self._private[prop] = color
self:repaint()
end
end
function module:get_uncovered_areas()
local geo = type(self._private.panning_area) == "function" and
self._private.panning_area(self) or
panning_modes[self._private.panning_area](self)
return grect.area_remove(get_rectangles(self.screens, false, false), geo)
end
function module:set_screens(screens)
local to_rem = {}
-- All screens.
-- The copy is needed because it's a metatable, `ipairs` doesn't work
-- correctly in all Lua versions.
if screens == capi.screen then
screens = {}
for s in capi.screen do
table.insert(screens, s)
end
end
for _, s in ipairs(screens) do
to_rem[get_screen(s)] = true
end
for _, s in ipairs(self.screens) do
to_rem[get_screen(s)] = nil
end
for _, s in ipairs(screens) do
s = get_screen(s)
self:add_screen(s)
to_rem[s] = nil
end
for s, remove in pairs(to_rem) do
if remove then
self:remove_screen(s)
end
end
end
function module:get_screens()
return self._private.screens
end
--- Add another screen (enable panning).
--
-- **Before:**
--
--@DOC_awful_wallpaper_add_screen1_EXAMPLE@
--
-- **After:**
--
--@DOC_awful_wallpaper_add_screen2_EXAMPLE@
--
-- Also note that adding a non-continuous screen might not work well,
-- but will not automatically add the screens in between:
--
--@DOC_awful_wallpaper_add_screen3_EXAMPLE@
--
-- @method add_screen
-- @tparam screen screen The screen object.
-- @noreturn
-- @see remove_screen
function module:add_screen(s)
s = get_screen(s)
for _, s2 in ipairs(self._private.screens) do
if s == s2 then return end
end
table.insert(self._private.screens, s)
if backgrounds[s] and backgrounds[s] ~= self then
backgrounds[s]:remove_screen(s)
end
backgrounds[s] = self
self:repaint()
end
--- Detach the wallpaper from all screens.
--
-- Adding a new wallpaper to a screen will automatically
-- detach the older one. However there is some case when
-- it is useful to call this manually. For example, when
-- adding a new panned wallpaper, it is possible that 2
-- wallpaper will have an overlap.
--
-- @method detach
-- @noreturn
-- @see remove_screen
-- @see add_screen
function module:detach()
local screens = gtable.clone(self.screens)
for _, s in ipairs(screens) do
self:remove_screen(s)
end
end
function module:_clear()
self._private.screens = setmetatable({}, {__mode = "v"})
update()
end
--- Repaint the wallpaper.
--
-- By default, even if the widget changes, the wallpaper will **NOT** be
-- automatically repainted. Repainting the native X11 wallpaper is slow and
-- it would be too easy to accidentally cause a performance problem. If you
-- really need to repaint the wallpaper, call this method.
--
-- @method repaint
-- @noreturn
function module:repaint()
for _, s in ipairs(self._private.screens) do
pending_repaint[s] = true
end
update()
end
--- Remove a screen.
--
-- Calling this will remove a screen, but will **not** repaint its area.
-- In this example, the wallpaper was spanning all 3 screens and the
-- first screen was removed:
--
-- @DOC_awful_wallpaper_remove_screen1_EXAMPLE@
--
-- As you can see, the content of screen 1 still looks like it is part of
-- the 3 screen wallpaper. The only use case for calling this method is if
-- you use a 3rd party tools to change the wallpaper.
--
-- If you wish to simply remove a screen and not have leftover content, it is
-- simpler to just create a new wallpaper for that screen:
--
-- @DOC_awful_wallpaper_remove_screen2_EXAMPLE@
--
-- @method remove_screen
-- @tparam screen screen The screen to remove.
-- @treturn boolean `true` if the screen was removed and `false` if the screen
-- wasn't found.
-- @see detach
-- @see add_screen
-- @see screens
function module:remove_screen(s)
local ret = false
s = get_screen(s)
for k, s2 in ipairs(self._private.screens) do
if s == s2 then
table.remove(self._private.screens, k)
ret = true
end
end
backgrounds[s] = nil
self:repaint()
return ret
end
--- Create a wallpaper.
--
-- Note that all parameters are not required. Please refer to the
-- module description and examples to understand parameters usages.
--
-- @constructorfct awful.wallpaper
-- @tparam table args
-- @tparam[opt] wibox.widget args.widget The wallpaper widget.
-- @tparam[opt] number args.dpi The wallpaper DPI (dots per inch).
-- @tparam[opt] screen args.screen The wallpaper screen.
-- @tparam[opt] table args.screens A list of screen for this wallpaper.
-- Use this parameter as a remplacement for `args.screen` to manage multiscreen wallpaper.
-- (Note: the expected table should be an array-like table `{screen1, screen2, ...}`)
-- @tparam[opt] gears.color args.bg The background color.
-- @tparam[opt] gears.color args.fg The foreground color.
-- @tparam[opt] gears.color args.uncovered_areas_color The color for the uncovered areas.
-- @tparam[opt] boolean args.honor_workarea Honor the workarea.
-- @tparam[opt] boolean args.honor_padding Honor the screen padding.
-- @tparam[opt] table args.uncovered_areas Returns the list of screen(s) area which won't be covered by the wallpaper.
-- @tparam[opt] function|string args.panning_area Defines where the wallpaper is placed when there is multiple screens.
local function new(_, args)
args = args or {}
local ret = gobject {
enable_auto_signals = true,
enable_properties = true,
}
rawset(ret, "_private", {})
ret._private.context = {}
ret._private.panning_area = "outer"
gtable.crush(ret, module, true)
ret:_clear()
-- Set the screen or screens first to avoid a race condition
-- with the other setters.
local args_screen, args_screens = args.screen, args.screens
if args_screen then
ret.screen = args_screen
elseif args_screens then
ret.screens = args_screens
end
-- Avoid crushing `screen` and `screens` twice.
args.screen, args.screens = nil, nil
gtable.crush(ret, args, false)
args.screen, args.screens = args_screen, args_screens
return ret
end
return setmetatable(module, {__call = new})