diff options
author | ache <ache@ache.one> | 2017-03-13 23:17:19 +0100 |
---|---|---|
committer | ache <ache@ache.one> | 2017-03-13 23:17:19 +0100 |
commit | 22d656903563f75678f3634964731ccf93355dfd (patch) | |
tree | e3cb6279d95c9764093072d5e946566ea6533799 /lib/gears |
Init commit
Diffstat (limited to 'lib/gears')
-rw-r--r-- | lib/gears/cache.lua | 51 | ||||
-rw-r--r-- | lib/gears/color.lua | 346 | ||||
-rw-r--r-- | lib/gears/debug.lua | 78 | ||||
-rw-r--r-- | lib/gears/geometry.lua | 240 | ||||
-rw-r--r-- | lib/gears/init.lua | 23 | ||||
-rw-r--r-- | lib/gears/matrix.lua | 219 | ||||
-rw-r--r-- | lib/gears/object.lua | 285 | ||||
-rw-r--r-- | lib/gears/object/properties.lua | 88 | ||||
-rw-r--r-- | lib/gears/protected_call.lua | 57 | ||||
-rw-r--r-- | lib/gears/shape.lua | 785 | ||||
-rw-r--r-- | lib/gears/surface.lua | 252 | ||||
-rw-r--r-- | lib/gears/timer.lua | 187 | ||||
-rw-r--r-- | lib/gears/wallpaper.lua | 221 |
13 files changed, 2832 insertions, 0 deletions
diff --git a/lib/gears/cache.lua b/lib/gears/cache.lua new file mode 100644 index 0000000..dc5add5 --- /dev/null +++ b/lib/gears/cache.lua @@ -0,0 +1,51 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter +-- @copyright 2015 Uli Schlachter +-- @classmod gears.cache +--------------------------------------------------------------------------- + +local select = select +local setmetatable = setmetatable +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) + +local cache = {} + +--- Get an entry from the cache, creating it if it's missing. +-- @param ... Arguments for the creation callback. These are checked against the +-- cache contents for equality. +-- @return The entry from the cache +function cache:get(...) + local result = self._cache + for i = 1, select("#", ...) do + local arg = select(i, ...) + local next = result[arg] + if not next then + next = {} + result[arg] = next + end + result = next + end + local ret = result._entry + if not ret then + ret = { self._creation_cb(...) } + result._entry = ret + end + return unpack(ret) +end + +--- Create a new cache object. A cache keeps some data that can be +-- garbage-collected at any time, but might be useful to keep. +-- @param creation_cb Callback that is used for creating missing cache entries. +-- @return A new cache object. +function cache.new(creation_cb) + return setmetatable({ + _cache = setmetatable({}, { __mode = "v" }), + _creation_cb = creation_cb + }, { + __index = cache + }) +end + +return setmetatable(cache, { __call = function(_, ...) return cache.new(...) end }) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/color.lua b/lib/gears/color.lua new file mode 100644 index 0000000..f0197c1 --- /dev/null +++ b/lib/gears/color.lua @@ -0,0 +1,346 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @module gears.color +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local string = string +local table = table +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) +local tonumber = tonumber +local ipairs = ipairs +local pairs = pairs +local type = type +local lgi = require("lgi") +local cairo = lgi.cairo +local Pango = lgi.Pango +local surface = require("gears.surface") + +local color = { mt = {} } +local pattern_cache + +--- Create a pattern from a given string. +-- This function can create solid, linear, radial and png patterns. In general, +-- patterns are specified as strings formatted as "type:arguments". "arguments" +-- is specific to the pattern being used. For example, one can use +-- "radial:50,50,10:55,55,30:0,#ff0000:0.5,#00ff00:1,#0000ff". +-- Alternatively, patterns can be specified via tables. In this case, the +-- table's 'type' member specifies the type. For example: +-- { +-- type = "radial", +-- from = { 50, 50, 10 }, +-- to = { 55, 55, 30 }, +-- stops = { { 0, "#ff0000" }, { 0.5, "#00ff00" }, { 1, "#0000ff" } } +-- } +-- Any argument that cannot be understood is passed to @{create_solid_pattern}. +-- +-- Please note that you MUST NOT modify the returned pattern, for example by +-- calling :set_matrix() on it, because this function uses a cache and your +-- changes could thus have unintended side effects. Use @{create_pattern_uncached} +-- if you need to modify the returned pattern. +-- @see create_pattern_uncached, create_solid_pattern, create_png_pattern, +-- create_linear_pattern, create_radial_pattern +-- @tparam string col The string describing the pattern. +-- @return a cairo pattern object +-- @function gears.color + +--- Parse a HTML-color. +-- This function can parse colors like `#rrggbb` and `#rrggbbaa` and also `red`. +-- Max 4 chars per channel. +-- +-- @param col The color to parse +-- @treturn table 4 values representing color in RGBA format (each of them in +-- [0, 1] range) or nil if input is incorrect. +-- @usage -- This will return 0, 1, 0, 1 +-- gears.color.parse_color("#00ff00ff") +function color.parse_color(col) + local rgb = {} + if string.match(col, "^#%x+$") then + local hex_str = col:sub(2, #col) + local channels + if #hex_str % 3 == 0 then + channels = 3 + elseif #hex_str % 4 == 0 then + channels = 4 + else + return nil + end + local chars_per_channel = #hex_str / channels + if chars_per_channel > 4 then + return nil + end + local dividor = (0x10 ^ chars_per_channel) - 1 + for idx=1,#hex_str,chars_per_channel do + local channel_val = tonumber(hex_str:sub(idx,idx+chars_per_channel-1), 16) + table.insert(rgb, channel_val / dividor) + end + if channels == 3 then + table.insert(rgb, 1) + end + else + local c = Pango.Color() + if not c:parse(col) then + return nil + end + rgb = { + c.red / 0xffff, + c.green / 0xffff, + c.blue / 0xffff, + 1.0 + } + end + assert(#rgb == 4, col) + return unpack(rgb) +end + +--- Find all numbers in a string +-- +-- @tparam string s The string to parse +-- @return Each number found as a separate value +local function parse_numbers(s) + local res = {} + for k in string.gmatch(s, "-?[0-9]+[.]?[0-9]*") do + table.insert(res, tonumber(k)) + end + return unpack(res) +end + +--- Create a solid pattern +-- +-- @param col The color for the pattern +-- @return A cairo pattern object +function color.create_solid_pattern(col) + if col == nil then + col = "#000000" + elseif type(col) == "table" then + col = col.color + end + return cairo.Pattern.create_rgba(color.parse_color(col)) +end + +--- Create an image pattern from a png file +-- +-- @param file The filename of the file +-- @return a cairo pattern object +function color.create_png_pattern(file) + if type(file) == "table" then + file = file.file + end + local image = surface.load(file) + local pattern = cairo.Pattern.create_for_surface(image) + pattern:set_extend(cairo.Extend.REPEAT) + return pattern +end + +--- Add stops to the given pattern. +-- @param p The cairo pattern to add stops to +-- @param iterator An iterator that returns strings. Each of those strings +-- should be in the form place,color where place is in [0, 1]. +local function add_iterator_stops(p, iterator) + for k in iterator do + local sub = string.gmatch(k, "[^,]+") + local point, clr = sub(), sub() + p:add_color_stop_rgba(point, color.parse_color(clr)) + end +end + +--- Add a list of stops to a given pattern +local function add_stops_table(pat, arg) + for _, stop in ipairs(arg) do + pat:add_color_stop_rgba(stop[1], color.parse_color(stop[2])) + end +end + +--- Create a pattern from a string +local function string_pattern(creator, arg) + local iterator = string.gmatch(arg, "[^:]+") + -- Create a table where each entry is a number from the original string + local args = { parse_numbers(iterator()) } + local to = { parse_numbers(iterator()) } + -- Now merge those two tables + for _, v in pairs(to) do + table.insert(args, v) + end + -- And call our creator function with the values + local p = creator(unpack(args)) + + add_iterator_stops(p, iterator) + return p +end + +--- Create a linear pattern object. +-- The pattern is created from a string. This string should have the following +-- form: `"x0, y0:x1, y1:<stops>"` +-- Alternatively, the pattern can be specified as a table: +-- { type = "linear", from = { x0, y0 }, to = { x1, y1 }, +-- stops = { <stops> } } +-- `x0,y0` and `x1,y1` are the start and stop point of the pattern. +-- For the explanation of `<stops>`, see `color.create_pattern`. +-- @tparam string|table arg The argument describing the pattern. +-- @return a cairo pattern object +function color.create_linear_pattern(arg) + local pat + + if type(arg) == "string" then + return string_pattern(cairo.Pattern.create_linear, arg) + elseif type(arg) ~= "table" then + error("Wrong argument type: " .. type(arg)) + end + + pat = cairo.Pattern.create_linear(arg.from[1], arg.from[2], arg.to[1], arg.to[2]) + add_stops_table(pat, arg.stops) + return pat +end + +--- Create a radial pattern object. +-- The pattern is created from a string. This string should have the following +-- form: `"x0, y0, r0:x1, y1, r1:<stops>"` +-- Alternatively, the pattern can be specified as a table: +-- { type = "radial", from = { x0, y0, r0 }, to = { x1, y1, r1 }, +-- stops = { <stops> } } +-- `x0,y0` and `x1,y1` are the start and stop point of the pattern. +-- `r0` and `r1` are the radii of the start / stop circle. +-- For the explanation of `<stops>`, see `color.create_pattern`. +-- @tparam string|table arg The argument describing the pattern +-- @return a cairo pattern object +function color.create_radial_pattern(arg) + local pat + + if type(arg) == "string" then + return string_pattern(cairo.Pattern.create_radial, arg) + elseif type(arg) ~= "table" then + error("Wrong argument type: " .. type(arg)) + end + + pat = cairo.Pattern.create_radial(arg.from[1], arg.from[2], arg.from[3], + arg.to[1], arg.to[2], arg.to[3]) + add_stops_table(pat, arg.stops) + return pat +end + +--- Mapping of all supported color types. New entries can be added. +color.types = { + solid = color.create_solid_pattern, + png = color.create_png_pattern, + linear = color.create_linear_pattern, + radial = color.create_radial_pattern +} + +--- Create a pattern from a given string. +-- For full documentation of this function, please refer to +-- `color.create_pattern`. The difference between `color.create_pattern` +-- and this function is that this function does not insert the generated +-- objects into the pattern cache. Thus, you are allowed to modify the +-- returned object. +-- @see create_pattern +-- @param col The string describing the pattern. +-- @return a cairo pattern object +function color.create_pattern_uncached(col) + -- If it already is a cairo pattern, just leave it as that + if cairo.Pattern:is_type_of(col) then + return col + end + col = col or "#000000" + if type(col) == "string" then + local t = string.match(col, "[^:]+") + if color.types[t] then + local pos = string.len(t) + local arg = string.sub(col, pos + 2) + return color.types[t](arg) + end + elseif type(col) == "table" then + local t = col.type + if color.types[t] then + return color.types[t](col) + end + end + return color.create_solid_pattern(col) +end + +--- Create a pattern from a given string, same as `gears.color`. +-- @see gears.color +function color.create_pattern(col) + if cairo.Pattern:is_type_of(col) then + return col + end + return pattern_cache:get(col or "#000000") +end + +--- Check if a pattern is opaque. +-- A pattern is transparent if the background on which it gets drawn (with +-- operator OVER) doesn't influence the visual result. +-- @param col An argument that `create_pattern` accepts. +-- @return The pattern if it is surely opaque, else nil +function color.create_opaque_pattern(col) + local pattern = color.create_pattern(col) + local kind = pattern:get_type() + + if kind == "SOLID" then + local _, _, _, _, alpha = pattern:get_rgba() + if alpha ~= 1 then + return + end + return pattern + elseif kind == "SURFACE" then + local status, surf = pattern:get_surface() + if status ~= "SUCCESS" or surf.content ~= "COLOR" then + -- The surface has an alpha channel which *might* be non-opaque + return + end + + -- Only the "NONE" extend mode is forbidden, everything else doesn't + -- introduce transparent parts + if pattern:get_extend() == "NONE" then + return + end + + return pattern + elseif kind == "LINEAR" then + local _, stops = pattern:get_color_stop_count() + + -- No color stops or extend NONE -> pattern *might* contain transparency + if stops == 0 or pattern:get_extend() == "NONE" then + return + end + + -- Now check if any of the color stops contain transparency + for i = 0, stops - 1 do + local _, _, _, _, _, alpha = pattern:get_color_stop_rgba(i) + if alpha ~= 1 then + return + end + end + return pattern + end + + -- Unknown type, e.g. mesh or raster source or unsupported type (radial + -- gradients can do weird self-intersections) +end + +--- Fill non-transparent area of an image with a given color. +-- @param image Image or path to it. +-- @param new_color New color. +-- @return Recolored image. +function color.recolor_image(image, new_color) + if type(image) == 'string' then + image = surface.duplicate_surface(image) + end + local cr = cairo.Context.create(image) + cr:set_source(color.create_pattern(new_color)) + cr:mask(cairo.Pattern.create_for_surface(image), 0, 0) + return image +end + +function color.mt.__call(_, ...) + return color.create_pattern(...) +end + +pattern_cache = require("gears.cache").new(color.create_pattern_uncached) + +--- No color +color.transparent = color.create_pattern("#00000000") + +return setmetatable(color, color.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/debug.lua b/lib/gears/debug.lua new file mode 100644 index 0000000..55f72f5 --- /dev/null +++ b/lib/gears/debug.lua @@ -0,0 +1,78 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @module gears.debug +--------------------------------------------------------------------------- + +local tostring = tostring +local print = print +local type = type +local pairs = pairs + +local debug = {} + +--- Given a table (or any other data) return a string that contains its +-- tag, value and type. If data is a table then recursively call `dump_raw` +-- on each of its values. +-- @param data Value to inspect. +-- @param shift Spaces to indent lines with. +-- @param tag The name of the value. +-- @tparam[opt=10] int depth Depth of recursion. +-- @return a string which contains tag, value, value type and table key/value +-- pairs if data is a table. +local function dump_raw(data, shift, tag, depth) + depth = depth == nil and 10 or depth or 0 + local result = "" + + if tag then + result = result .. tostring(tag) .. " : " + end + + if type(data) == "table" and depth > 0 then + shift = (shift or "") .. " " + result = result .. tostring(data) + for k, v in pairs(data) do + result = result .. "\n" .. shift .. dump_raw(v, shift, k, depth - 1) + end + else + result = result .. tostring(data) .. " (" .. type(data) .. ")" + if depth == 0 and type(data) == "table" then + result = result .. " […]" + end + end + + return result +end + +--- Inspect the value in data. +-- @param data Value to inspect. +-- @param tag The name of the value. +-- @tparam[opt] int depth Depth of recursion. +-- @return string A string that contains the expanded value of data. +function debug.dump_return(data, tag, depth) + return dump_raw(data, nil, tag, depth) +end + +--- Print the table (or any other value) to the console. +-- @param data Table to print. +-- @param tag The name of the table. +-- @tparam[opt] int depth Depth of recursion. +function debug.dump(data, tag, depth) + print(debug.dump_return(data, tag, depth)) +end + +--- Print an warning message +-- @tparam string message The warning message to print +function debug.print_warning(message) + io.stderr:write(os.date("%Y-%m-%d %T W: ") .. tostring(message) .. "\n") +end + +--- Print an error message +-- @tparam string message The error message to print +function debug.print_error(message) + io.stderr:write(os.date("%Y-%m-%d %T E: ") .. tostring(message) .. "\n") +end + +return debug + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/geometry.lua b/lib/gears/geometry.lua new file mode 100644 index 0000000..a429abd --- /dev/null +++ b/lib/gears/geometry.lua @@ -0,0 +1,240 @@ +--------------------------------------------------------------------------- +-- +-- Helper functions used to compute geometries. +-- +-- When this module refer to a geometry table, this assume a table with at least +-- an *x*, *y*, *width* and *height* keys and numeric values. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +-- @module gears.geometry +--------------------------------------------------------------------------- +local math = math + +local gears = {geometry = {rectangle = {} } } + +--- Get the square distance between a rectangle and a point. +-- @tparam table geom A rectangle +-- @tparam number geom.x The horizontal coordinate +-- @tparam number geom.y The vertical coordinate +-- @tparam number geom.width The rectangle width +-- @tparam number geom.height The rectangle height +-- @tparam number x X coordinate of point +-- @tparam number y Y coordinate of point +-- @treturn number The squared distance of the rectangle to the provided point +function gears.geometry.rectangle.get_square_distance(geom, x, y) + local dist_x, dist_y = 0, 0 + if x < geom.x then + dist_x = geom.x - x + elseif x >= geom.x + geom.width then + dist_x = x - geom.x - geom.width + 1 + end + if y < geom.y then + dist_y = geom.y - y + elseif y >= geom.y + geom.height then + dist_y = y - geom.y - geom.height + 1 + end + return dist_x * dist_x + dist_y * dist_y +end + +--- Return the closest rectangle from `list` for a given point. +-- @tparam table list A list of geometry tables. +-- @tparam number x The x coordinate +-- @tparam number y The y coordinate +-- @return The key from the closest geometry. +function gears.geometry.rectangle.get_closest_by_coord(list, x, y) + local dist = math.huge + local ret = nil + + for k, v in pairs(list) do + local d = gears.geometry.rectangle.get_square_distance(v, x, y) + if d < dist then + ret, dist = k, d + end + end + + return ret +end + +--- Return the rectangle containing the [x, y] point. +-- +-- Note that if multiple element from the geometry list contains the point, the +-- returned result is nondeterministic. +-- +-- @tparam table list A list of geometry tables. +-- @tparam number x The x coordinate +-- @tparam number y The y coordinate +-- @return The key from the closest geometry. In case no result is found, *nil* +-- is returned. +function gears.geometry.rectangle.get_by_coord(list, x, y) + for k, geometry in pairs(list) do + if x >= geometry.x and x < geometry.x + geometry.width + and y >= geometry.y and y < geometry.y + geometry.height then + return k + end + end +end + +--- Return true whether rectangle B is in the right direction +-- compared to rectangle A. +-- @param dir The direction. +-- @param gA The geometric specification for rectangle A. +-- @param gB The geometric specification for rectangle B. +-- @return True if B is in the direction of A. +local function is_in_direction(dir, gA, gB) + if dir == "up" then + return gA.y > gB.y + elseif dir == "down" then + return gA.y < gB.y + elseif dir == "left" then + return gA.x > gB.x + elseif dir == "right" then + return gA.x < gB.x + end + return false +end + +--- Calculate distance between two points. +-- i.e: if we want to move to the right, we will take the right border +-- of the currently focused screen and the left side of the checked screen. +-- @param dir The direction. +-- @param _gA The first rectangle. +-- @param _gB The second rectangle. +-- @return The distance between the screens. +local function calculate_distance(dir, _gA, _gB) + local gAx = _gA.x + local gAy = _gA.y + local gBx = _gB.x + local gBy = _gB.y + + if dir == "up" then + gBy = _gB.y + _gB.height + elseif dir == "down" then + gAy = _gA.y + _gA.height + elseif dir == "left" then + gBx = _gB.x + _gB.width + elseif dir == "right" then + gAx = _gA.x + _gA.width + end + + return math.sqrt(math.pow(gBx - gAx, 2) + math.pow(gBy - gAy, 2)) +end + +--- Get the nearest rectangle in the given direction. Every rectangle is specified as a table +-- with *x*, *y*, *width*, *height* keys, the same as client or screen geometries. +-- @tparam string dir The direction, can be either *up*, *down*, *left* or *right*. +-- @tparam table recttbl A table of rectangle specifications. +-- @tparam table cur The current rectangle. +-- @return The index for the rectangle in recttbl closer to cur in the given direction. nil if none found. +function gears.geometry.rectangle.get_in_direction(dir, recttbl, cur) + local dist, dist_min + local target = nil + + -- We check each object + for i, rect in pairs(recttbl) do + -- Check geometry to see if object is located in the right direction. + if is_in_direction(dir, cur, rect) then + -- Calculate distance between current and checked object. + dist = calculate_distance(dir, cur, rect) + + -- If distance is shorter then keep the object. + if not target or dist < dist_min then + target = i + dist_min = dist + end + end + end + return target +end + +--- Check if an area intersect another area. +-- @param a The area. +-- @param b The other area. +-- @return True if they intersect, false otherwise. +local function area_intersect_area(a, b) + return (b.x < a.x + a.width + and b.x + b.width > a.x + and b.y < a.y + a.height + and b.y + b.height > a.y) +end + +--- Get the intersect area between a and b. +-- @tparam table a The area. +-- @tparam number a.x The horizontal coordinate +-- @tparam number a.y The vertical coordinate +-- @tparam number a.width The rectangle width +-- @tparam number a.height The rectangle height +-- @tparam table b The other area. +-- @tparam number b.x The horizontal coordinate +-- @tparam number b.y The vertical coordinate +-- @tparam number b.width The rectangle width +-- @tparam number b.height The rectangle height +-- @treturn table The intersect area. +function gears.geometry.rectangle.get_intersection(a, b) + local g = {} + g.x = math.max(a.x, b.x) + g.y = math.max(a.y, b.y) + g.width = math.min(a.x + a.width, b.x + b.width) - g.x + g.height = math.min(a.y + a.height, b.y + b.height) - g.y + return g +end + +--- Remove an area from a list, splitting the space between several area that +-- can overlap. +-- @tparam table areas Table of areas. +-- @tparam table elem Area to remove. +-- @tparam number elem.x The horizontal coordinate +-- @tparam number elem.y The vertical coordinate +-- @tparam number elem.width The rectangle width +-- @tparam number elem.height The rectangle height +-- @return The new area list. +function gears.geometry.rectangle.area_remove(areas, elem) + for i = #areas, 1, -1 do + -- Check if the 'elem' intersect + if area_intersect_area(areas[i], elem) then + -- It does? remove it + local r = table.remove(areas, i) + local inter = gears.geometry.rectangle.get_intersection(r, elem) + + if inter.x > r.x then + table.insert(areas, { + x = r.x, + y = r.y, + width = inter.x - r.x, + height = r.height + }) + end + + if inter.y > r.y then + table.insert(areas, { + x = r.x, + y = r.y, + width = r.width, + height = inter.y - r.y + }) + end + + if inter.x + inter.width < r.x + r.width then + table.insert(areas, { + x = inter.x + inter.width, + y = r.y, + width = (r.x + r.width) - (inter.x + inter.width), + height = r.height + }) + end + + if inter.y + inter.height < r.y + r.height then + table.insert(areas, { + x = r.x, + y = inter.y + inter.height, + width = r.width, + height = (r.y + r.height) - (inter.y + inter.height) + }) + end + end + end + + return areas +end + +return gears.geometry diff --git a/lib/gears/init.lua b/lib/gears/init.lua new file mode 100644 index 0000000..eae92ee --- /dev/null +++ b/lib/gears/init.lua @@ -0,0 +1,23 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @module gears +--------------------------------------------------------------------------- + + +return +{ + color = require("gears.color"); + debug = require("gears.debug"); + object = require("gears.object"); + surface = require("gears.surface"); + wallpaper = require("gears.wallpaper"); + timer = require("gears.timer"); + cache = require("gears.cache"); + matrix = require("gears.matrix"); + shape = require("gears.shape"); + protected_call = require("gears.protected_call"); + geometry = require("gears.geometry"); +} + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/matrix.lua b/lib/gears/matrix.lua new file mode 100644 index 0000000..a6bc975 --- /dev/null +++ b/lib/gears/matrix.lua @@ -0,0 +1,219 @@ +--------------------------------------------------------------------------- +-- An implementation of matrices for describing and working with affine +-- transformations. +-- @author Uli Schlachter +-- @copyright 2015 Uli Schlachter +-- @classmod gears.matrix +--------------------------------------------------------------------------- + +local cairo = require("lgi").cairo +local matrix = {} + +-- Metatable for matrix instances. This is set up near the end of the file. +local matrix_mt = {} + +--- Create a new matrix instance +-- @tparam number xx The xx transformation part. +-- @tparam number yx The yx transformation part. +-- @tparam number xy The xy transformation part. +-- @tparam number yy The yy transformation part. +-- @tparam number x0 The x0 transformation part. +-- @tparam number y0 The y0 transformation part. +-- @return A new matrix describing the given transformation. +function matrix.create(xx, yx, xy, yy, x0, y0) + return setmetatable({ + xx = xx, xy = xy, x0 = x0, + yx = yx, yy = yy, y0 = y0 + }, matrix_mt) +end + +--- Create a new translation matrix +-- @tparam number x The translation in x direction. +-- @tparam number y The translation in y direction. +-- @return A new matrix describing the given transformation. +function matrix.create_translate(x, y) + return matrix.create(1, 0, 0, 1, x, y) +end + +--- Create a new scaling matrix +-- @tparam number sx The scaling in x direction. +-- @tparam number sy The scaling in y direction. +-- @return A new matrix describing the given transformation. +function matrix.create_scale(sx, sy) + return matrix.create(sx, 0, 0, sy, 0, 0) +end + +--- Create a new rotation matrix +-- @tparam number angle The angle of the rotation in radians. +-- @return A new matrix describing the given transformation. +function matrix.create_rotate(angle) + local c, s = math.cos(angle), math.sin(angle) + return matrix.create(c, s, -s, c, 0, 0) +end + +--- Create a new rotation matrix rotating around a custom point +-- @tparam number x The horizontal rotation point +-- @tparam number y The vertical rotation point +-- @tparam number angle The angle of the rotation in radians. +-- @return A new matrix describing the given transformation. +function matrix.create_rotate_at(x, y, angle) + return matrix.create_translate( -x, -y ) + * matrix.create_rotate ( angle ) + * matrix.create_translate( x, y ) +end + +--- Translate this matrix +-- @tparam number x The translation in x direction. +-- @tparam number y The translation in y direction. +-- @return A new matrix describing the new transformation. +function matrix:translate(x, y) + return matrix.create_translate(x, y):multiply(self) +end + +--- Scale this matrix +-- @tparam number sx The scaling in x direction. +-- @tparam number sy The scaling in y direction. +-- @return A new matrix describing the new transformation. +function matrix:scale(sx, sy) + return matrix.create_scale(sx, sy):multiply(self) +end + +--- Rotate this matrix +-- @tparam number angle The angle of the rotation in radians. +-- @return A new matrix describing the new transformation. +function matrix:rotate(angle) + return matrix.create_rotate(angle):multiply(self) +end + +--- Rotate a shape from a custom point +-- @tparam number x The horizontal rotation point +-- @tparam number y The vertical rotation point +-- @tparam number angle The angle (in radiant: -2*math.pi to 2*math.pi) +-- @return A transformation object +function matrix:rotate_at(x, y, angle) + return self * matrix.create_rotate_at(x, y, angle) +end + +--- Invert this matrix +-- @return A new matrix describing the inverse transformation. +function matrix:invert() + -- Beware of math! (I just copied the algorithm from cairo's source code) + local a, b, c, d, x0, y0 = self.xx, self.yx, self.xy, self.yy, self.x0, self.y0 + local inv_det = 1/(a*d - b*c) + return matrix.create(inv_det * d, inv_det * -b, + inv_det * -c, inv_det * a, + inv_det * (c * y0 - d * x0), inv_det * (b * x0 - a * y0)) +end + +--- Multiply this matrix with another matrix. +-- The resulting matrix describes a transformation that is equivalent to first +-- applying this transformation and then the transformation from `other`. +-- Note that this function can also be called by directly multiplicating two +-- matrix instances: `a * b == a:multiply(b)`. +-- @tparam gears.matrix|cairo.Matrix other The other matrix to multiply with. +-- @return The multiplication result. +function matrix:multiply(other) + local ret = matrix.create(self.xx * other.xx + self.yx * other.xy, + self.xx * other.yx + self.yx * other.yy, + self.xy * other.xx + self.yy * other.xy, + self.xy * other.yx + self.yy * other.yy, + self.x0 * other.xx + self.y0 * other.xy + other.x0, + self.x0 * other.yx + self.y0 * other.yy + other.y0) + + return ret +end + +--- Check if two matrices are equal. +-- Note that this function cal also be called by directly comparing two matrix +-- instances: `a == b`. +-- @tparam gears.matrix|cairo.Matrix other The matrix to compare with. +-- @return True if this and the other matrix are equal. +function matrix:equals(other) + for _, k in pairs{ "xx", "xy", "yx", "yy", "x0", "y0" } do + if self[k] ~= other[k] then + return false + end + end + return true +end + +--- Get a string representation of this matrix +-- @return A string showing this matrix in column form. +function matrix:tostring() + return string.format("[[%g, %g], [%g, %g], [%g, %g]]", + self.xx, self.yx, self.xy, + self.yy, self.x0, self.y0) +end + +--- Transform a distance by this matrix. +-- The difference to @{matrix:transform_point} is that the translation part of +-- this matrix is ignored. +-- @tparam number x The x coordinate of the point. +-- @tparam number y The y coordinate of the point. +-- @treturn number The x coordinate of the transformed point. +-- @treturn number The x coordinate of the transformed point. +function matrix:transform_distance(x, y) + return self.xx * x + self.xy * y, self.yx * x + self.yy * y +end + +--- Transform a point by this matrix. +-- @tparam number x The x coordinate of the point. +-- @tparam number y The y coordinate of the point. +-- @treturn number The x coordinate of the transformed point. +-- @treturn number The y coordinate of the transformed point. +function matrix:transform_point(x, y) + x, y = self:transform_distance(x, y) + return self.x0 + x, self.y0 + y +end + +--- Calculate a bounding rectangle for transforming a rectangle by a matrix. +-- @tparam number x The x coordinate of the rectangle. +-- @tparam number y The y coordinate of the rectangle. +-- @tparam number width The width of the rectangle. +-- @tparam number height The height of the rectangle. +-- @treturn number X coordinate of the bounding rectangle. +-- @treturn number Y coordinate of the bounding rectangle. +-- @treturn number Width of the bounding rectangle. +-- @treturn number Height of the bounding rectangle. +function matrix:transform_rectangle(x, y, width, height) + -- Transform all four corners of the rectangle + local x1, y1 = self:transform_point(x, y) + local x2, y2 = self:transform_point(x, y + height) + local x3, y3 = self:transform_point(x + width, y + height) + local x4, y4 = self:transform_point(x + width, y) + -- Find the extremal points of the result + x = math.min(x1, x2, x3, x4) + y = math.min(y1, y2, y3, y4) + width = math.max(x1, x2, x3, x4) - x + height = math.max(y1, y2, y3, y4) - y + + return x, y, width, height +end + +--- Convert to a cairo matrix +-- @treturn cairo.Matrix A cairo matrix describing the same transformation. +function matrix:to_cairo_matrix() + local ret = cairo.Matrix() + ret:init(self.xx, self.yx, self.xy, self.yy, self.x0, self.y0) + return ret +end + +--- Convert to a cairo matrix +-- @tparam cairo.Matrix mat A cairo matrix describing the sought transformation +-- @treturn gears.matrix A matrix instance describing the same transformation. +function matrix.from_cairo_matrix(mat) + return matrix.create(mat.xx, mat.yx, mat.xy, mat.yy, mat.x0, mat.y0) +end + +matrix_mt.__index = matrix +matrix_mt.__newindex = error +matrix_mt.__eq = matrix.equals +matrix_mt.__mul = matrix.multiply +matrix_mt.__tostring = matrix.tostring + +--- A constant for the identity matrix. +matrix.identity = matrix.create(1, 0, 0, 1, 0, 0) + +return matrix + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/object.lua b/lib/gears/object.lua new file mode 100644 index 0000000..e6436e3 --- /dev/null +++ b/lib/gears/object.lua @@ -0,0 +1,285 @@ +--------------------------------------------------------------------------- +-- The object oriented programming base class used by various Awesome +-- widgets and components. +-- +-- It provide basic observer pattern, signaling and dynamic properties. +-- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @classmod gears.object +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local pairs = pairs +local type = type +local error = error +local properties = require("gears.object.properties") + +local object = { properties = properties, mt = {} } + +--- Verify that obj is indeed a valid object as returned by new() +local function check(obj) + if type(obj) ~= "table" or type(obj._signals) ~= "table" then + error("called on non-object") + end +end + +--- Find a given signal +-- @tparam table obj The object to search in +-- @tparam string name The signal to find +-- @treturn table The signal table +local function find_signal(obj, name) + check(obj) + if not obj._signals[name] then + assert(type(name) == "string", "name must be a string, got: " .. type(name)) + obj._signals[name] = { + strong = {}, + weak = setmetatable({}, { __mode = "kv" }) + } + end + return obj._signals[name] +end + +function object.add_signal() + require("awful.util").deprecate("Use signals without explicitly adding them. This is now done implicitly.") +end + +--- Connect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +function object:connect_signal(name, func) + assert(type(func) == "function", "callback must be a function, got: " .. type(func)) + local sig = find_signal(self, name) + assert(sig.weak[func] == nil, "Trying to connect a strong callback which is already connected weakly") + sig.strong[func] = true +end + +local function make_the_gc_obey(func) + if _VERSION <= "Lua 5.1" then + -- Lua 5.1 only has the behaviour we want if a userdata is used as the + -- value in a weak table. Thus, do some magic so that we get a userdata. + + -- luacheck: globals newproxy getfenv setfenv + local userdata = newproxy(true) + getmetatable(userdata).__gc = function() end + -- Now bind the lifetime of userdata to the lifetime of func. For this, + -- we mess with the function's environment and add a table for all the + -- various userdata that it should keep alive. + local key = "_secret_key_used_by_gears_object_in_Lua51" + local old_env = getfenv(func) + if old_env[key] then + -- Assume the code in the else branch added this and the function + -- already has its own, private environment + table.insert(old_env[key], userdata) + else + -- No table yet, add it + local new_env = { [key] = { userdata } } + setmetatable(new_env, { __index = old_env, __newindex = old_env }) + setfenv(func, new_env) + end + assert(_G[key] == nil, "Something broke, things escaped to _G") + return userdata + end + -- Lua 5.2+ already behaves the way we want with functions directly, no magic + return func +end + +--- Connect to a signal weakly. This allows the callback function to be garbage +-- collected and automatically disconnects the signal when that happens. +-- @tparam string name The name of the signal +-- @tparam function func The callback to call when the signal is emitted +function object:weak_connect_signal(name, func) + assert(type(func) == "function", "callback must be a function, got: " .. type(func)) + local sig = find_signal(self, name) + assert(sig.strong[func] == nil, "Trying to connect a weak callback which is already connected strongly") + sig.weak[func] = make_the_gc_obey(func) +end + +--- Disonnect to a signal. +-- @tparam string name The name of the signal +-- @tparam function func The callback that should be disconnected +function object:disconnect_signal(name, func) + local sig = find_signal(self, name) + sig.weak[func] = nil + sig.strong[func] = nil +end + +--- Emit a signal. +-- +-- @tparam string name The name of the signal +-- @param ... Extra arguments for the callback functions. Each connected +-- function receives the object as first argument and then any extra arguments +-- that are given to emit_signal() +function object:emit_signal(name, ...) + local sig = find_signal(self, name) + for func in pairs(sig.strong) do + func(self, ...) + end + for func in pairs(sig.weak) do + func(self, ...) + end +end + +local function get_miss(self, key) + local class = rawget(self, "_class") + + if rawget(self, "get_"..key) then + return rawget(self, "get_"..key)(self) + elseif class and class["get_"..key] then + return class["get_"..key](self) + elseif class then + return class[key] + end + +end + +local function set_miss(self, key, value) + local class = rawget(self, "_class") + + if rawget(self, "set_"..key) then + return rawget(self, "set_"..key)(self, value) + elseif class and class["set_"..key] then + return class["set_"..key](self, value) + elseif rawget(self, "_enable_auto_signals") then + local changed = class[key] ~= value + class[key] = value + + if changed then + self:emit_signal("property::"..key, value) + end + elseif (not rawget(self, "get_"..key)) + and not (class and class["get_"..key]) then + return rawset(self, key, value) + else + error("Cannot set '" .. tostring(key) .. "' on " .. tostring(self) + .. " because it is read-only") + end +end + +--- Returns a new object. You can call `:emit_signal()`, `:disconnect_signal()` +-- and `:connect_signal()` on the resulting object. +-- +-- Note that `args.enable_auto_signals` is only supported when +-- `args.enable_properties` is true. +-- +-- +-- +-- +--**Usage example output**: +-- +-- In get foo bar +-- bar +-- In set foo 42 +-- In get foo 42 +-- 42 +-- In a mathod 1 2 3 +-- nil +-- In the connection handler! a cow +-- a cow +-- +-- +-- @usage +-- -- Create a class for this object. It will be used as a backup source for +-- -- methods and accessors. It is also possible to set them directly on the +-- -- object. +--local class = {} +--function class:get_foo() +-- print('In get foo', self._foo or 'bar') +-- return self._foo or 'bar' +--end +--function class:set_foo(value) +-- print('In set foo', value) +-- -- In case it is necessary to bypass the object property system, use +-- -- `rawset` +-- rawset(self, '_foo', value) +-- -- When using custom accessors, the signals need to be handled manually +-- self:emit_signal('property::foo', value) +--end +--function class:method(a, b, c) +-- print('In a mathod', a, b, c) +--end +--local o = gears.object { +-- class = class, +-- enable_properties = true, +-- enable_auto_signals = true, +--} +--print(o.foo) +--o.foo = 42 +--print(o.foo) +--o:method(1, 2, 3) +-- -- Random properties can also be added, the signal will be emitted automatically. +--o:connect_signal('property::something', function(obj, value) +-- assert(obj == o) +-- print('In the connection handler!', value) +--end) +--print(o.something) +--o.something = 'a cow' +--print(o.something) +-- @tparam[opt={}] table args The arguments +-- @tparam[opt=false] boolean args.enable_properties Automatically call getters and setters +-- @tparam[opt=false] boolean args.enable_auto_signals Generate "property::xxxx" signals +-- when an unknown property is set. +-- @tparam[opt=nil] table args.class +-- @treturn table A new object +-- @function gears.object +local function new(args) + args = args or {} + local ret = {} + + -- Automatic signals cannot work without both miss handlers. + assert(not (args.enable_auto_signals and args.enable_properties ~= true)) + + -- Copy all our global functions to our new object + for k, v in pairs(object) do + if type(v) == "function" then + ret[k] = v + end + end + + ret._signals = {} + + local mt = {} + + -- Look for methods in another table + ret._class = args.class + ret._enable_auto_signals = args.enable_auto_signals + + -- To catch all changes, a proxy is required + if args.enable_auto_signals then + ret._class = ret._class and setmetatable({}, {__index = args.class}) or {} + end + + if args.enable_properties then + -- Check got existing get_xxxx and set_xxxx + mt.__index = get_miss + mt.__newindex = set_miss + elseif args.class then + -- Use the class table a miss handler + mt.__index = ret._class + end + + return setmetatable(ret, mt) +end + +function object.mt.__call(_, ...) + return new(...) +end + +--- Helper function to get the module name out of `debug.getinfo`. +-- @usage +-- local mt = {} +-- mt.__tostring = function(o) +-- return require("gears.object").modulename(2) +-- end +-- return setmetatable(ret, mt) +-- +-- @tparam[opt=2] integer level Level for `debug.getinfo(level, "S")`. +-- Typically 2 or 3. +-- @treturn string The module name, e.g. "wibox.container.background". +function object.modulename(level) + return debug.getinfo(level, "S").source:gsub(".*/lib/", ""):gsub("/", "."):gsub("%.lua", "") +end + +return setmetatable(object, object.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/object/properties.lua b/lib/gears/object/properties.lua new file mode 100644 index 0000000..36b8fcb --- /dev/null +++ b/lib/gears/object/properties.lua @@ -0,0 +1,88 @@ +--------------------------------------------------------------------------- +--- An helper module to map userdata __index and __newindex entries to +-- lua classes. +-- +-- @author Emmanuel Lepage-Vallee <elv1313@gmail.com> +-- @copyright 2016 Emmanuel Lepage-Vallee +-- @module gears.object.properties +--------------------------------------------------------------------------- + +local object = {} + + +--- Add the missing properties handler to a CAPI object such as client/tag/screen. +-- Valid args: +-- +-- * **getter**: A smart getter (handle property getter itself) +-- * **getter_fallback**: A dumb getter method (don't handle individual property getter) +-- * **getter_class**: A module with individual property getter/setter +-- * **getter_prefix**: A special getter prefix (like "get" or "get_" (default)) +-- * **setter**: A smart setter (handle property setter itself) +-- * **setter_fallback**: A dumb setter method (don't handle individual property setter) +-- * **setter_class**: A module with individual property getter/setter +-- * **setter_prefix**: A special setter prefix (like "set" or "set_" (default)) +-- * **auto_emit**: Emit "property::___" automatically (default: false). This is +-- ignored when setter_fallback is set or a setter is found +-- +-- @param class A standard luaobject derived object +-- @tparam[opt={}] table args A set of accessors configuration parameters +function object.capi_index_fallback(class, args) + args = args or {} + + local getter_prefix = args.getter_prefix or "get_" + local setter_prefix = args.setter_prefix or "set_" + + local getter = args.getter or function(cobj, prop) + -- Look for a getter method + if args.getter_class and args.getter_class[getter_prefix..prop] then + return args.getter_class[getter_prefix..prop](cobj) + elseif args.getter_class and args.getter_class["is_"..prop] then + return args.getter_class["is_"..prop](cobj) + end + + -- Make sure something like c:a_mutator() works + if args.getter_class and args.getter_class[prop] then + return args.getter_class[prop] + end + -- In case there is already a "dumb" getter like `awful.tag.getproperty' + if args.getter_fallback then + return args.getter_fallback(cobj, prop) + end + + -- Use the fallback property table + return cobj.data[prop] + end + + local setter = args.setter or function(cobj, prop, value) + -- Look for a setter method + if args.setter_class and args.setter_class[setter_prefix..prop] then + return args.setter_class[setter_prefix..prop](cobj, value) + end + + -- In case there is already a "dumb" setter like `awful.client.property.set' + if args.setter_fallback then + return args.setter_fallback(cobj, prop, value) + end + + -- If a getter exists but not a setter, then the property is read-only + if args.getter_class and args.getter_class[getter_prefix..prop] then + return + end + + -- Use the fallback property table + cobj.data[prop] = value + + -- Emit the signal + if args.auto_emit then + cobj:emit_signal("property::"..prop, value) + end + end + + -- Attach the accessor methods + class.set_index_miss_handler(getter) + class.set_newindex_miss_handler(setter) +end + +return setmetatable( object, {__call = function(_,...) object.capi_index_fallback(...) end}) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/protected_call.lua b/lib/gears/protected_call.lua new file mode 100644 index 0000000..c182e14 --- /dev/null +++ b/lib/gears/protected_call.lua @@ -0,0 +1,57 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter +-- @copyright 2016 Uli Schlachter +-- @module gears.protected_call +--------------------------------------------------------------------------- + +local gdebug = require("gears.debug") +local tostring = tostring +local traceback = debug.traceback +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) +local xpcall = xpcall + +local protected_call = {} + +local function error_handler(err) + gdebug.print_error(traceback("Error during a protected call: " .. tostring(err), 2)) +end + +local function handle_result(success, ...) + if success then + return ... + end +end + +local do_pcall +if _VERSION <= "Lua 5.1" then + -- Lua 5.1 doesn't support arguments in xpcall :-( + do_pcall = function(func, ...) + local args = { ... } + return handle_result(xpcall(function() + return func(unpack(args)) + end, error_handler)) + end +else + do_pcall = function(func, ...) + return handle_result(xpcall(func, error_handler, ...)) + end +end + +--- Call a function in protected mode and handle error-reporting. +-- If the function call succeeds, all results of the function are returned. +-- Otherwise, an error message is printed and nothing is returned. +-- @tparam function func The function to call +-- @param ... Arguments to the function +-- @return The result of the given function, or nothing if an error occurred. +function protected_call.call(func, ...) + return do_pcall(func, ...) +end + +local pcall_mt = {} +function pcall_mt:__call(...) + return do_pcall(...) +end + +return setmetatable(protected_call, pcall_mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/shape.lua b/lib/gears/shape.lua new file mode 100644 index 0000000..4962d78 --- /dev/null +++ b/lib/gears/shape.lua @@ -0,0 +1,785 @@ +--------------------------------------------------------------------------- +--- Module dedicated to gather common shape painters. +-- +-- It add the concept of "shape" to Awesome. A shape can be applied to a +-- background, a margin, a mask or a drawable shape bounding. +-- +-- The functions exposed by this module always take a context as first +-- parameter followed by the widget and height and additional parameters. +-- +-- The functions provided by this module only create a path in the content. +-- to actually draw the content, use `cr:fill()`, `cr:mask()`, `cr:clip()` or +-- `cr:stroke()` +-- +-- In many case, it is necessary to apply the shape using a transformation +-- such as a rotation. The preferred way to do this is to wrap the function +-- in another function calling `cr:rotate()` (or any other transformation +-- matrix). +-- +-- To specialize a shape where the API doesn't allows extra arguments to be +-- passed, it is possible to wrap the shape function like: +-- +-- local new_shape = function(cr, width, height) +-- gears.shape.rounded_rect(cr, width, height, 2) +-- end +-- +-- Many elements can be shaped. This include: +-- +-- * `client`s (see `gears.surface.apply_shape_bounding`) +-- * `wibox`es (see `wibox.shape`) +-- * All widgets (see `wibox.container.background`) +-- * The progressbar (see `wibox.widget.progressbar.bar_shape`) +-- * The graph (see `wibox.widget.graph.step_shape`) +-- * The checkboxes (see `wibox.widget.checkbox.check_shape`) +-- * Images (see `wibox.widget.imagebox.clip_shape`) +-- * The taglist tags (see `awful.widget.taglist`) +-- * The tasklist clients (see `awful.widget.tasklist`) +-- * The tooltips (see `awful.tooltip`) +-- +-- @author Emmanuel Lepage Vallee +-- @copyright 2011-2016 Emmanuel Lepage Vallee +-- @module gears.shape +--------------------------------------------------------------------------- +local g_matrix = require( "gears.matrix" ) +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) +local atan2 = math.atan2 or math.atan -- lua 5.3 compat + +local module = {} + +--- Add a rounded rectangle to the current path. +-- Note: If the radius is bigger than either half side, it will be reduced. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_rounded_rect.svg) +-- +-- @usage +--shape.rounded_rect(cr, 70, 70, 10) +--shape.rounded_rect(cr,20,70, 5) +--shape.transform(shape.rounded_rect) : translate(0,25) (cr,70,20, 5) +-- +-- @param cr A cairo content +-- @tparam number width The rectangle width +-- @tparam number height The rectangle height +-- @tparam number radius the corner radius +function module.rounded_rect(cr, width, height, radius) + + radius = radius or 10 + + if width / 2 < radius then + radius = width / 2 + end + + if height / 2 < radius then + radius = height / 2 + end + + cr:move_to(0, radius) + + cr:arc( radius , radius , radius, math.pi , 3*(math.pi/2) ) + cr:arc( width-radius, radius , radius, 3*(math.pi/2), math.pi*2 ) + cr:arc( width-radius, height-radius, radius, math.pi*2 , math.pi/2 ) + cr:arc( radius , height-radius, radius, math.pi/2 , math.pi ) + + cr:close_path() +end + +--- Add a rectangle delimited by 2 180 degree arcs to the path. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_rounded_bar.svg) +-- +-- @usage +--shape.rounded_bar(cr, 70, 70) +--shape.rounded_bar(cr, 20, 70) +--shape.rounded_bar(cr, 70, 20) +-- +-- @param cr A cairo content +-- @param width The rectangle width +-- @param height The rectangle height +function module.rounded_bar(cr, width, height) + module.rounded_rect(cr, width, height, height / 2) +end + +--- A rounded rect with only some of the corners rounded. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_partially_rounded_rect.svg) +-- +-- @usage +--shape.partially_rounded_rect(cr, 70, 70) +--shape.partially_rounded_rect(cr, 70, 70, true) +--shape.partially_rounded_rect(cr, 70, 70, true, true, false, true, 30) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam boolean tl If the top left corner is rounded +-- @tparam boolean tr If the top right corner is rounded +-- @tparam boolean br If the bottom right corner is rounded +-- @tparam boolean bl If the bottom left corner is rounded +-- @tparam number rad The corner radius +function module.partially_rounded_rect(cr, width, height, tl, tr, br, bl, rad) + rad = rad or 10 + if width / 2 < rad then + rad = width / 2 + end + + if height / 2 < rad then + rad = height / 2 + end + + -- Top left + if tl then + cr:arc( rad, rad, rad, math.pi, 3*(math.pi/2)) + else + cr:move_to(0,0) + end + + -- Top right + if tr then + cr:arc( width-rad, rad, rad, 3*(math.pi/2), math.pi*2) + else + cr:line_to(width, 0) + end + + -- Bottom right + if br then + cr:arc( width-rad, height-rad, rad, math.pi*2 , math.pi/2) + else + cr:line_to(width, height) + end + + -- Bottom left + if bl then + cr:arc( rad, height-rad, rad, math.pi/2, math.pi) + else + cr:line_to(0, height) + end + + cr:close_path() +end + +--- A rounded rectangle with a triangle at the top. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_infobubble.svg) +-- +-- @usage +--shape.infobubble(cr, 70, 70) +--shape.transform(shape.infobubble) : translate(0, 20) +-- : rotate_at(35,35,math.pi) (cr,70,20,10, 5, 35 - 5) +--shape.transform(shape.infobubble) +-- : rotate_at(35,35,3*math.pi/2) (cr,70,70, nil, nil, 40) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam[opt=5] number corner_radius The corner radius +-- @tparam[opt=10] number arrow_size The width and height of the arrow +-- @tparam[opt=width/2 - arrow_size/2] number arrow_position The position of the arrow +function module.infobubble(cr, width, height, corner_radius, arrow_size, arrow_position) + arrow_size = arrow_size or 10 + corner_radius = math.min((height-arrow_size)/2, corner_radius or 5) + arrow_position = arrow_position or width/2 - arrow_size/2 + + + cr:move_to(0 ,corner_radius+arrow_size) + + -- Top left corner + cr:arc(corner_radius, corner_radius+arrow_size, (corner_radius), math.pi, 3*(math.pi/2)) + + -- The arrow triangle (still at the top) + cr:line_to(arrow_position , arrow_size ) + cr:line_to(arrow_position + arrow_size , 0 ) + cr:line_to(arrow_position + 2*arrow_size , arrow_size ) + + -- Complete the rounded rounded rectangle + cr:arc(width-corner_radius, corner_radius+arrow_size , (corner_radius) , 3*(math.pi/2) , math.pi*2 ) + cr:arc(width-corner_radius, height-(corner_radius) , (corner_radius) , math.pi*2 , math.pi/2 ) + cr:arc(corner_radius , height-(corner_radius) , (corner_radius) , math.pi/2 , math.pi ) + + -- Close path + cr:close_path() +end + +--- A rectangle terminated by an arrow. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_rectangular_tag.svg) +-- +-- @usage +--shape.rectangular_tag(cr, 70, 70) +--shape.transform(shape.rectangular_tag) : translate(0, 30) (cr, 70, 10, 10) +--shape.transform(shape.rectangular_tag) : translate(0, 30) (cr, 70, 10, -10) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam[opt=height/2] number arrow_length The length of the arrow part +function module.rectangular_tag(cr, width, height, arrow_length) + arrow_length = arrow_length or height/2 + if arrow_length > 0 then + cr:move_to(0 , height/2 ) + cr:line_to(arrow_length , 0 ) + cr:line_to(width , 0 ) + cr:line_to(width , height ) + cr:line_to(arrow_length , height ) + else + cr:move_to(0 , 0 ) + cr:line_to(-arrow_length, height/2 ) + cr:line_to(0 , height ) + cr:line_to(width , height ) + cr:line_to(width , 0 ) + end + + cr:close_path() +end + +--- A simple arrow shape. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_arrow.svg) +-- +-- @usage +--shape.arrow(cr, 70, 70) +--shape.arrow(cr,70,70, 30, 10, 60) +--shape.transform(shape.arrow) : rotate_at(35,35,math.pi/2)(cr,70,70) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam[opt=head_width] number head_width The width of the head (/\) of the arrow +-- @tparam[opt=width /2] number shaft_width The width of the shaft of the arrow +-- @tparam[opt=height/2] number shaft_length The head_length of the shaft (the rest is the head) +function module.arrow(cr, width, height, head_width, shaft_width, shaft_length) + shaft_length = shaft_length or height / 2 + shaft_width = shaft_width or width / 2 + head_width = head_width or width + local head_length = height - shaft_length + + cr:move_to ( width/2 , 0 ) + cr:rel_line_to( head_width/2 , head_length ) + cr:rel_line_to( -(head_width-shaft_width)/2 , 0 ) + cr:rel_line_to( 0 , shaft_length ) + cr:rel_line_to( -shaft_width , 0 ) + cr:rel_line_to( 0 , -shaft_length ) + cr:rel_line_to( -(head_width-shaft_width)/2 , 0 ) + + cr:close_path() +end + +--- A squeezed hexagon filling the rectangle. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_hexagon.svg) +-- +-- @usage +--shape.hexagon(cr, 70, 70) +--shape.transform(shape.hexagon) : translate(0,15)(cr,70,20) +--shape.transform(shape.hexagon) : rotate_at(35,35,math.pi/2)(cr,70,40) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +function module.hexagon(cr, width, height) + cr:move_to(height/2,0) + cr:line_to(width-height/2,0) + cr:line_to(width,height/2) + cr:line_to(width-height/2,height) + cr:line_to(height/2,height) + cr:line_to(0,height/2) + cr:line_to(height/2,0) + cr:close_path() +end + +--- Double arrow popularized by the vim-powerline module. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_powerline.svg) +-- +-- @usage +--shape.powerline(cr, 70, 70) +--shape.transform(shape.powerline) : translate(0, 25) (cr,70,20) +--shape.transform(shape.powerline) : translate(0, 25) (cr,70,20, -20) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam[opt=height/2] number arrow_depth The width of the arrow part of the shape +function module.powerline(cr, width, height, arrow_depth) + arrow_depth = arrow_depth or height/2 + local offset = 0 + + -- Avoid going out of the (potential) clip area + if arrow_depth < 0 then + width = width + 2*arrow_depth + offset = -arrow_depth + end + + cr:move_to(offset , 0 ) + cr:line_to(offset + width - arrow_depth , 0 ) + cr:line_to(offset + width , height/2 ) + cr:line_to(offset + width - arrow_depth , height ) + cr:line_to(offset , height ) + cr:line_to(offset + arrow_depth , height/2 ) + + cr:close_path() +end + +--- An isosceles triangle. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_isosceles_triangle.svg) +-- +-- @usage +--shape.isosceles_triangle(cr, 70, 70) +--shape.isosceles_triangle(cr,20,70) +--shape.transform(shape.isosceles_triangle) : rotate_at(35, 35, math.pi/2)(cr,70,70) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +function module.isosceles_triangle(cr, width, height) + cr:move_to( width/2, 0 ) + cr:line_to( width , height ) + cr:line_to( 0 , height ) + cr:close_path() +end + +--- A cross (**+**) symbol. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_cross.svg) +-- +-- @usage +--shape.cross(cr, 70, 70) +--shape.cross(cr,20,70) +--shape.transform(shape.cross) : scale(0.5, 1)(cr,70,70) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam[opt=width/3] number thickness The cross section thickness +function module.cross(cr, width, height, thickness) + thickness = thickness or width/3 + local xpadding = (width - thickness) / 2 + local ypadding = (height - thickness) / 2 + cr:move_to(xpadding, 0) + cr:line_to(width - xpadding, 0) + cr:line_to(width - xpadding, ypadding) + cr:line_to(width , ypadding) + cr:line_to(width , height-ypadding) + cr:line_to(width - xpadding, height-ypadding) + cr:line_to(width - xpadding, height ) + cr:line_to(xpadding , height ) + cr:line_to(xpadding , height-ypadding) + cr:line_to(0 , height-ypadding) + cr:line_to(0 , ypadding ) + cr:line_to(xpadding , ypadding ) + cr:close_path() +end + +--- A similar shape to the `rounded_rect`, but with sharp corners. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_octogon.svg) +-- +-- @usage +--shape.octogon(cr, 70, 70) +--shape.octogon(cr,70,70,70/2.5) +--shape.transform(shape.octogon) : translate(0, 25) (cr,70,20) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam number corner_radius +function module.octogon(cr, width, height, corner_radius) + corner_radius = corner_radius or math.min(10, math.min(width, height)/4) + local offset = math.sqrt( (corner_radius*corner_radius) / 2 ) + + cr:move_to(offset, 0) + cr:line_to(width-offset, 0) + cr:line_to(width, offset) + cr:line_to(width, height-offset) + cr:line_to(width-offset, height) + cr:line_to(offset, height) + cr:line_to(0, height-offset) + cr:line_to(0, offset) + cr:close_path() +end + +--- A circle shape. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_circle.svg) +-- +-- @usage +--shape.circle(cr, 70, 70) +--shape.circle(cr,20,70) +--shape.transform(shape.circle) : scale(0.5, 1)(cr,70,70) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam[opt=math.min(width height) / 2)] number radius The radius +function module.circle(cr, width, height, radius) + radius = radius or math.min(width, height) / 2 + cr:move_to(width/2+radius, height/2) + cr:arc(width / 2, height / 2, radius, 0, 2*math.pi) + cr:close_path() +end + +--- A simple rectangle. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_rectangle.svg) +-- +-- @usage +--shape.rectangle(cr, 70, 70) +--shape.rectangle(cr,20,70) +--shape.transform(shape.rectangle) : scale(0.5, 1)(cr,70,70) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +function module.rectangle(cr, width, height) + cr:rectangle(0, 0, width, height) +end + +--- A diagonal parallelogram with the bottom left corner at x=0 and top right +-- at x=width. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_parallelogram.svg) +-- +-- @usage +--shape.parallelogram(cr, 70, 70) +--shape.parallelogram(cr,70,20) +--shape.transform(shape.parallelogram) : scale(0.5, 1)(cr,70,70) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam[opt=width/3] number base_width The parallelogram base width +function module.parallelogram(cr, width, height, base_width) + base_width = base_width or width/3 + cr:move_to(width-base_width, 0 ) + cr:line_to(width , 0 ) + cr:line_to(base_width , height ) + cr:line_to(0 , height ) + cr:close_path() +end + +--- A losange. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_losange.svg) +-- +-- @usage +--shape.losange(cr, 70, 70) +--shape.losange(cr,20,70) +--shape.transform(shape.losange) : scale(0.5, 1)(cr,70,70) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +function module.losange(cr, width, height) + cr:move_to(width/2 , 0 ) + cr:line_to(width , height/2 ) + cr:line_to(width/2 , height ) + cr:line_to(0 , height/2 ) + cr:close_path() +end + +--- A pie. +-- +-- The pie center is the center of the area. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_pie.svg) +-- +-- @usage +--shape.pie(cr, 70, 70) +--shape.pie(cr,70,70, 1.0471975511966, 4.1887902047864) +--shape.pie(cr,70,70, 0, 2*math.pi, 10) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam[opt=0] number start_angle The start angle (in radian) +-- @tparam[opt=math.pi/2] number end_angle The end angle (in radian) +-- @tparam[opt=math.min(width height)/2] number radius The shape height +function module.pie(cr, width, height, start_angle, end_angle, radius) + radius = radius or math.floor(math.min(width, height)/2) + start_angle, end_angle = start_angle or 0, end_angle or math.pi/2 + + -- If the shape is a circle, then avoid the lines + if math.abs(start_angle + end_angle - 2*math.pi) <= 0.01 then + cr:arc(width/2, height/2, radius, 0, 2*math.pi) + else + cr:move_to(width/2, height/2) + cr:line_to( + width/2 + math.cos(start_angle)*radius, + height/2 + math.sin(start_angle)*radius + ) + cr:arc(width/2, height/2, radius, start_angle, end_angle) + end + + cr:close_path() +end + +--- A rounded arc. +-- +-- The pie center is the center of the area. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_arc.svg) +-- +-- @usage +--shape.arc(cr,70,70, 10) +--shape.arc(cr,70,70, 10, nil, nil, true, true) +--shape.arc(cr,70,70, nil, 0, 2*math.pi) +-- +-- @param cr A cairo context +-- @tparam number width The shape width +-- @tparam number height The shape height +-- @tparam[opt=math.min(width height)/2] number thickness The arc thickness +-- @tparam[opt=0] number start_angle The start angle (in radian) +-- @tparam[opt=math.pi/2] number end_angle The end angle (in radian) +-- @tparam[opt=false] boolean start_rounded if the arc start rounded +-- @tparam[opt=false] boolean end_rounded if the arc end rounded +function module.arc(cr, width, height, thickness, start_angle, end_angle, start_rounded, end_rounded) + start_angle = start_angle or 0 + end_angle = end_angle or math.pi/2 + + -- This shape is a partial circle + local radius = math.min(width, height)/2 + + thickness = thickness or radius/2 + + local inner_radius = radius - thickness + + -- As the edge of the small arc need to touch the [start_p1, start_p2] + -- line, a small subset of the arc circumference has to be substracted + -- that's (less or more) equal to the thickness/2 (a little longer given + -- it is an arc and not a line, but it wont show) + local arc_percent = math.abs(end_angle-start_angle)/(2*math.pi) + local arc_length = ((radius-thickness/2)*2*math.pi)*arc_percent + + if start_rounded then + arc_length = arc_length - thickness/2 + + -- And back to angles + start_angle = end_angle - (arc_length/(radius - thickness/2)) + end + + if end_rounded then + arc_length = arc_length - thickness/2 + + -- And back to angles + end_angle = start_angle + (arc_length/(radius - thickness/2)) + end + + -- The path is a curcular arc joining 4 points + + -- Outer first corner + local start_p1 = { + width /2 + math.cos(start_angle)*radius, + height/2 + math.sin(start_angle)*radius + } + + if start_rounded then + + -- Inner first corner + local start_p2 = { + width /2 + math.cos(start_angle)*inner_radius, + height/2 + math.sin(start_angle)*inner_radius + } + + local median_angle = atan2( + start_p2[1] - start_p1[1], + -(start_p2[2] - start_p1[2]) + ) + + local arc_center = { + (start_p1[1] + start_p2[1])/2, + (start_p1[2] + start_p2[2])/2, + } + + cr:arc(arc_center[1], arc_center[2], thickness/2, + median_angle-math.pi/2, median_angle+math.pi/2 + ) + + else + cr:move_to(unpack(start_p1)) + end + + cr:arc(width/2, height/2, radius, start_angle, end_angle) + + if end_rounded then + + -- Outer second corner + local end_p1 = { + width /2 + math.cos(end_angle)*radius, + height/2 + math.sin(end_angle)*radius + } + + -- Inner first corner + local end_p2 = { + width /2 + math.cos(end_angle)*inner_radius, + height/2 + math.sin(end_angle)*inner_radius + } + local median_angle = atan2( + end_p2[1] - end_p1[1], + -(end_p2[2] - end_p1[2]) + ) - math.pi + + local arc_center = { + (end_p1[1] + end_p2[1])/2, + (end_p1[2] + end_p2[2])/2, + } + + cr:arc(arc_center[1], arc_center[2], thickness/2, + median_angle-math.pi/2, median_angle+math.pi/2 + ) + + end + + cr:arc_negative(width/2, height/2, inner_radius, end_angle, start_angle) + + cr:close_path() +end + +--- A partial rounded bar. How much of the rounded bar is visible depends on +-- the given percentage value. +-- +-- Note that this shape is not closed and thus filling it doesn't make much +-- sense. +-- +-- +-- +--![Usage example](../images/AUTOGEN_gears_shape_radial_progress.svg) +-- +-- @usage +--shape.radial_progress(cr, 70, 20, .3) +--shape.radial_progress(cr, 70, 20, .6) +--shape.radial_progress(cr, 70, 20, .9) +-- +-- @param cr A cairo context +-- @tparam number w The shape width +-- @tparam number h The shape height +-- @tparam number percent The progressbar percent +-- @tparam boolean hide_left Do not draw the left side of the shape +function module.radial_progress(cr, w, h, percent, hide_left) + percent = percent or 1 + local total_length = (2*(w-h))+2*((h/2)*math.pi) + local bar_percent = (w-h)/total_length + local arc_percent = ((h/2)*math.pi)/total_length + + -- Bottom line + if percent > bar_percent then + cr:move_to(h/2,h) + cr:line_to((h/2) + (w-h),h) + cr:stroke() + elseif percent < bar_percent then + cr:move_to(h/2,h) + cr:line_to(h/2+(total_length*percent),h) + cr:stroke() + end + + -- Right arc + if percent >= bar_percent+arc_percent then + cr:arc(w-h/2 , h/2, h/2,3*(math.pi/2),math.pi/2) + cr:stroke() + elseif percent > bar_percent and percent < bar_percent+(arc_percent/2) then + cr:arc(w-h/2 , h/2, h/2,(math.pi/2)-((math.pi/2)*((percent-bar_percent)/(arc_percent/2))),math.pi/2) + cr:stroke() + elseif percent >= bar_percent+arc_percent/2 and percent < bar_percent+arc_percent then + cr:arc(w-h/2 , h/2, h/2,0,math.pi/2) + cr:stroke() + local add = (math.pi/2)*((percent-bar_percent-arc_percent/2)/(arc_percent/2)) + cr:arc(w-h/2 , h/2, h/2,2*math.pi-add,0) + cr:stroke() + end + + -- Top line + if percent > 2*bar_percent+arc_percent then + cr:move_to((h/2) + (w-h),0) + cr:line_to(h/2,0) + cr:stroke() + elseif percent > bar_percent+arc_percent and percent < 2*bar_percent+arc_percent then + cr:move_to((h/2) + (w-h),0) + cr:line_to(((h/2) + (w-h))-total_length*(percent-bar_percent-arc_percent),0) + cr:stroke() + end + + -- Left arc + if not hide_left then + if percent > 0.985 then + cr:arc(h/2, h/2, h/2,math.pi/2,3*(math.pi/2)) + cr:stroke() + elseif percent > 2*bar_percent+arc_percent then + local relpercent = (percent - 2*bar_percent - arc_percent)/arc_percent + cr:arc(h/2, h/2, h/2,3*(math.pi/2)-(math.pi)*relpercent,3*(math.pi/2)) + cr:stroke() + end + end +end + +--- Adjust the shape using a transformation object +-- +-- Apply various transformations to the shape +-- +-- @usage gears.shape.transform(gears.shape.rounded_bar) +-- : rotate(math.pi/2) +-- : translate(10, 10) +-- +-- @param shape A shape function +-- @return A transformation handle, also act as a shape function +function module.transform(shape) + + -- Apply the transformation matrix and apply the shape, then restore + local function apply(self, cr, width, height, ...) + cr:save() + cr:transform(self.matrix:to_cairo_matrix()) + shape(cr, width, height, ...) + cr:restore() + end + -- Redirect function calls like :rotate() to the underlying matrix + local function index(_, key) + return function(self, ...) + self.matrix = self.matrix[key](self.matrix, ...) + return self + end + end + + local result = setmetatable({ + matrix = g_matrix.identity + }, { + __call = apply, + __index = index + }) + + return result +end + +return module + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/surface.lua b/lib/gears/surface.lua new file mode 100644 index 0000000..78f2216 --- /dev/null +++ b/lib/gears/surface.lua @@ -0,0 +1,252 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter +-- @copyright 2012 Uli Schlachter +-- @module gears.surface +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local type = type +local capi = { awesome = awesome } +local cairo = require("lgi").cairo +local color = nil +local gdebug = require("gears.debug") +local hierarchy = require("wibox.hierarchy") + +-- Keep this in sync with build-utils/lgi-check.sh! +local ver_major, ver_minor, ver_patch = string.match(require('lgi.version'), '(%d)%.(%d)%.(%d)') +if tonumber(ver_major) <= 0 and (tonumber(ver_minor) < 7 or (tonumber(ver_minor) == 7 and tonumber(ver_patch) < 1)) then + error("lgi too old, need at least version 0.7.1") +end + +local surface = { mt = {} } +local surface_cache = setmetatable({}, { __mode = 'v' }) + +local function get_default(arg) + if type(arg) == 'nil' then + return cairo.ImageSurface(cairo.Format.ARGB32, 0, 0) + end + return arg +end + +--- Try to convert the argument into an lgi cairo surface. +-- This is usually needed for loading images by file name. +-- @param _surface The surface to load or nil +-- @param default The default value to return on error; when nil, then a surface +-- in an error state is returned. +-- @return The loaded surface, or the replacement default +-- @return An error message, or nil on success +function surface.load_uncached_silently(_surface, default) + local file + -- On nil, return some sane default + if not _surface then + return get_default(default) + end + -- lgi cairo surfaces don't get changed either + if cairo.Surface:is_type_of(_surface) then + return _surface + end + -- Strings are assumed to be file names and get loaded + if type(_surface) == "string" then + local err + file = _surface + _surface, err = capi.awesome.load_image(file) + if not _surface then + return get_default(default), err + end + end + -- Everything else gets forced into a surface + return cairo.Surface(_surface, true) +end + +--- Try to convert the argument into an lgi cairo surface. +-- This is usually needed for loading images by file name and uses a cache. +-- In contrast to `load()`, errors are returned to the caller. +-- @param _surface The surface to load or nil +-- @param default The default value to return on error; when nil, then a surface +-- in an error state is returned. +-- @return The loaded surface, or the replacement default, or nil if called with +-- nil. +-- @return An error message, or nil on success +function surface.load_silently(_surface, default) + if type(_surface) == "string" then + local cache = surface_cache[_surface] + if cache then + return cache + end + local result, err = surface.load_uncached_silently(_surface, default) + if not err then + -- Cache the file + surface_cache[_surface] = result + end + return result, err + end + return surface.load_uncached_silently(_surface, default) +end + +local function do_load_and_handle_errors(_surface, func) + if type(_surface) == 'nil' then + return get_default() + end + local result, err = func(_surface, false) + if result then + return result + end + gdebug.print_error(debug.traceback( + "Failed to load '" .. tostring(_surface) .. "': " .. tostring(err))) + return get_default() +end + +--- Try to convert the argument into an lgi cairo surface. +-- This is usually needed for loading images by file name. Errors are handled +-- via `gears.debug.print_error`. +-- @param _surface The surface to load or nil +-- @return The loaded surface, or nil +function surface.load_uncached(_surface) + return do_load_and_handle_errors(_surface, surface.load_uncached_silently) +end + +--- Try to convert the argument into an lgi cairo surface. +-- This is usually needed for loading images by file name. Errors are handled +-- via `gears.debug.print_error`. +-- @param _surface The surface to load or nil +-- @return The loaded surface, or nil +function surface.load(_surface) + return do_load_and_handle_errors(_surface, surface.load_silently) +end + +function surface.mt.__call(_, ...) + return surface.load(...) +end + +--- Get the size of a cairo surface +-- @param surf The surface you are interested in +-- @return The surface's width and height +function surface.get_size(surf) + local cr = cairo.Context(surf) + local x, y, w, h = cr:clip_extents() + return w - x, h - y +end + +--- Create a copy of a cairo surface. +-- The surfaces returned by `surface.load` are cached and must not be +-- modified to avoid unintended side-effects. This function allows to create +-- a copy of a cairo surface. This copy can then be freely modified. +-- The surface returned will be as compatible as possible to the input +-- surface. For example, it will likely be of the same surface type as the +-- input. The details are explained in the `create_similar` function on a cairo +-- surface. +-- @param s Source surface. +-- @return The surface's duplicate. +function surface.duplicate_surface(s) + s = surface.load(s) + + -- Figure out surface size (this does NOT work for unbounded recording surfaces) + local cr = cairo.Context(s) + local x, y, w, h = cr:clip_extents() + + -- Create a copy + local result = s:create_similar(s.content, w - x, h - y) + cr = cairo.Context(result) + cr:set_source_surface(s, 0, 0) + cr.operator = cairo.Operator.SOURCE + cr:paint() + return result +end + +--- Create a surface from a `gears.shape` +-- Any additional parameters will be passed to the shape function +-- @tparam number width The surface width +-- @tparam number height The surface height +-- @param shape A `gears.shape` compatible function +-- @param[opt=white] shape_color The shape color or pattern +-- @param[opt=transparent] bg_color The surface background color +-- @treturn cairo.surface the new surface +function surface.load_from_shape(width, height, shape, shape_color, bg_color, ...) + color = color or require("gears.color") + + local img = cairo.ImageSurface(cairo.Format.ARGB32, width, height) + local cr = cairo.Context(img) + + cr:set_source(color(bg_color or "#00000000")) + cr:paint() + + cr:set_source(color(shape_color or "#000000")) + + shape(cr, width, height, ...) + + cr:fill() + + return img +end + +--- Apply a shape to a client or a wibox. +-- +-- If the wibox or client size change, this function need to be called +-- again. +-- @param draw A wibox or a client +-- @param shape or gears.shape function or a custom function with a context, +-- width and height as parameter. +-- @param[opt] Any additional parameters will be passed to the shape function +function surface.apply_shape_bounding(draw, shape, ...) + local geo = draw:geometry() + + local img = cairo.ImageSurface(cairo.Format.A1, geo.width, geo.height) + local cr = cairo.Context(img) + + cr:set_operator(cairo.Operator.CLEAR) + cr:set_source_rgba(0,0,0,1) + cr:paint() + cr:set_operator(cairo.Operator.SOURCE) + cr:set_source_rgba(1,1,1,1) + + shape(cr, geo.width, geo.height, ...) + + cr:fill() + + draw.shape_bounding = img._native +end + +local function no_op() end + +local function run_in_hierarchy(self, cr, width, height) + local context = {dpi=96} + local h = hierarchy.new(context, self, width, height, no_op, no_op, {}) + h:draw(context, cr) + return h +end + +--- Create an SVG file with this widget content. +-- This is dynamic, so the SVG will be updated along with the widget content. +-- because of this, the painting may happen hover multiple event loop cycles. +-- @tparam widget widget A widget +-- @tparam string path The output file path +-- @tparam number width The surface width +-- @tparam number height The surface height +-- @return The cairo surface +-- @return The hierarchy +function surface.widget_to_svg(widget, path, width, height) + local img = cairo.SvgSurface.create(path, width, height) + local cr = cairo.Context(img) + + return img, run_in_hierarchy(widget, cr, width, height) +end + +--- Create a cairo surface with this widget content. +-- This is dynamic, so the SVG will be updated along with the widget content. +-- because of this, the painting may happen hover multiple event loop cycles. +-- @tparam widget widget A widget +-- @tparam number width The surface width +-- @tparam number height The surface height +-- @param[opt=cairo.Format.ARGB32] format The surface format +-- @return The cairo surface +-- @return The hierarchy +function surface.widget_to_surface(widget, width, height, format) + local img = cairo.ImageSurface(format or cairo.Format.ARGB32, width, height) + local cr = cairo.Context(img) + + return img, run_in_hierarchy(widget, cr, width, height) +end + +return setmetatable(surface, surface.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/timer.lua b/lib/gears/timer.lua new file mode 100644 index 0000000..110c39a --- /dev/null +++ b/lib/gears/timer.lua @@ -0,0 +1,187 @@ +--------------------------------------------------------------------------- +--- Timer objects and functions. +-- +-- @author Uli Schlachter +-- @copyright 2014 Uli Schlachter +-- @classmod gears.timer +--------------------------------------------------------------------------- + +local capi = { awesome = awesome } +local ipairs = ipairs +local pairs = pairs +local setmetatable = setmetatable +local table = table +local tonumber = tonumber +local traceback = debug.traceback +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) +local glib = require("lgi").GLib +local object = require("gears.object") +local protected_call = require("gears.protected_call") + +--- Timer objects. This type of object is useful when triggering events repeatedly. +-- The timer will emit the "timeout" signal every N seconds, N being the timeout +-- value. Note that a started timer will not be garbage collected. Call `:stop` +-- to enable garbage collection. +-- @tfield number timeout Interval in seconds to emit the timeout signal. +-- Can be any value, including floating point ones (e.g. 1.5 seconds). +-- @tfield boolean started Read-only boolean field indicating if the timer has been +-- started. +-- @table timer + +--- When the timer is started. +-- @signal .start + +--- When the timer is stopped. +-- @signal .stop + +--- When the timer had a timeout event. +-- @signal .timeout + +local timer = { mt = {} } + +--- Start the timer. +function timer:start() + if self.data.source_id ~= nil then + print(traceback("timer already started")) + return + end + self.data.source_id = glib.timeout_add(glib.PRIORITY_DEFAULT, self.data.timeout * 1000, function() + protected_call(self.emit_signal, self, "timeout") + return true + end) + self:emit_signal("start") +end + +--- Stop the timer. +function timer:stop() + if self.data.source_id == nil then + print(traceback("timer not started")) + return + end + glib.source_remove(self.data.source_id) + self.data.source_id = nil + self:emit_signal("stop") +end + +--- Restart the timer. +-- This is equivalent to stopping the timer if it is running and then starting +-- it. +function timer:again() + if self.data.source_id ~= nil then + self:stop() + end + self:start() +end + +--- The timer is started. +-- @property started +-- @param boolean + +--- The timer timeout value. +-- **Signal:** property::timeout +-- @property timeout +-- @param number + +local timer_instance_mt = { + __index = function(self, property) + if property == "timeout" then + return self.data.timeout + elseif property == "started" then + return self.data.source_id ~= nil + end + + return timer[property] + end, + + __newindex = function(self, property, value) + if property == "timeout" then + self.data.timeout = tonumber(value) + self:emit_signal("property::timeout") + end + end +} + +--- Create a new timer object. +-- @tparam table args Arguments. +-- @tparam number args.timeout Timeout in seconds (e.g. 1.5). +-- @treturn timer +-- @function gears.timer +timer.new = function(args) + local ret = object() + + ret.data = { timeout = 0 } + setmetatable(ret, timer_instance_mt) + + for k, v in pairs(args) do + ret[k] = v + end + + return ret +end + +--- Create a timeout for calling some callback function. +-- When the callback function returns true, it will be called again after the +-- same timeout. If false is returned, no more calls will be done. If the +-- callback function causes an error, no more calls are done. +-- @tparam number timeout Timeout in seconds (e.g. 1.5). +-- @tparam function callback Function to run. +-- @treturn timer The timer object that was set up. +-- @see timer.weak_start_new +-- @function gears.timer.start_new +function timer.start_new(timeout, callback) + local t = timer.new({ timeout = timeout }) + t:connect_signal("timeout", function() + local cont = protected_call(callback) + if not cont then + t:stop() + end + end) + t:start() + return t +end + +--- Create a timeout for calling some callback function. +-- This function is almost identical to `timer.start_new`. The only difference +-- is that this does not prevent the callback function from being garbage +-- collected. After the callback function was collected, the timer returned +-- will automatically be stopped. +-- @tparam number timeout Timeout in seconds (e.g. 1.5). +-- @tparam function callback Function to start. +-- @treturn timer The timer object that was set up. +-- @see timer.start_new +-- @function gears.timer.weak_start_new +function timer.weak_start_new(timeout, callback) + local indirection = setmetatable({}, { __mode = "v" }) + indirection.callback = callback + return timer.start_new(timeout, function() + local cb = indirection.callback + if cb then + return cb() + end + end) +end + +local delayed_calls = {} +capi.awesome.connect_signal("refresh", function() + for _, callback in ipairs(delayed_calls) do + protected_call(unpack(callback)) + end + delayed_calls = {} +end) + +--- Call the given function at the end of the current main loop iteration +-- @tparam function callback The function that should be called +-- @param ... Arguments to the callback function +-- @function gears.timer.delayed_call +function timer.delayed_call(callback, ...) + assert(type(callback) == "function", "callback must be a function, got: " .. type(callback)) + table.insert(delayed_calls, { callback, ... }) +end + +function timer.mt.__call(_, ...) + return timer.new(...) +end + +return setmetatable(timer, timer.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/gears/wallpaper.lua b/lib/gears/wallpaper.lua new file mode 100644 index 0000000..70ecf48 --- /dev/null +++ b/lib/gears/wallpaper.lua @@ -0,0 +1,221 @@ +--------------------------------------------------------------------------- +-- @author Uli Schlachter +-- @copyright 2012 Uli Schlachter +-- @module gears.wallpaper +--------------------------------------------------------------------------- + +local cairo = require("lgi").cairo +local color = require("gears.color") +local surface = require("gears.surface") +local timer = require("gears.timer") +local root = root + +local wallpaper = { mt = {} } + +local function root_geometry() + local width, height = root.size() + return { x = 0, y = 0, width = width, height = height } +end + +-- Information about a pending wallpaper change, see prepare_context() +local pending_wallpaper = nil + +local function get_screen(s) + return s and screen[s] +end + +--- Prepare the needed state for setting a wallpaper. +-- This function returns a cairo context through which a wallpaper can be drawn. +-- The context is only valid for a short time and should not be saved in a +-- global variable. +-- @param s The screen to set the wallpaper on or nil for all screens +-- @return[1] The available geometry (table with entries width and height) +-- @return[1] A cairo context that the wallpaper should be drawn to +function wallpaper.prepare_context(s) + s = get_screen(s) + + local root_width, root_height = root.size() + local geom = s and s.geometry or root_geometry() + local source, target, cr + + if not pending_wallpaper then + -- Prepare a pending wallpaper + source = surface(root.wallpaper()) + target = source:create_similar(cairo.Content.COLOR, root_width, root_height) + + -- Set the wallpaper (delayed) + timer.delayed_call(function() + local paper = pending_wallpaper + pending_wallpaper = nil + wallpaper.set(paper.surface) + paper.surface:finish() + end) + elseif root_width > pending_wallpaper.width or root_height > pending_wallpaper.height then + -- The root window was resized while a wallpaper is pending + source = pending_wallpaper.surface + target = source:create_similar(cairo.Content.COLOR, root_width, root_height) + else + -- Draw to the already-pending wallpaper + source = nil + target = pending_wallpaper.surface + end + + cr = cairo.Context(target) + + if source then + -- Copy the old wallpaper to the new one + cr:save() + cr.operator = cairo.Operator.SOURCE + cr:set_source_surface(source, 0, 0) + cr:paint() + cr:restore() + end + + pending_wallpaper = { + surface = target, + width = root_width, + height = root_height + } + + -- Only draw to the selected area + cr:translate(geom.x, geom.y) + cr:rectangle(0, 0, geom.width, geom.height) + cr:clip() + + return geom, cr +end + +--- Set the current wallpaper. +-- @param pattern The wallpaper that should be set. This can be a cairo surface, +-- a description for gears.color or a cairo pattern. +-- @see gears.color +function wallpaper.set(pattern) + if cairo.Surface:is_type_of(pattern) then + pattern = cairo.Pattern.create_for_surface(pattern) + end + if type(pattern) == "string" or type(pattern) == "table" then + pattern = color(pattern) + end + if not cairo.Pattern:is_type_of(pattern) then + error("wallpaper.set() called with an invalid argument") + end + root.wallpaper(pattern._native) +end + +--- Set a centered wallpaper. +-- @param surf The wallpaper to center. Either a cairo surface or a file name. +-- @param s The screen whose wallpaper should be set. Can be nil, in which case +-- all screens are set. +-- @param background The background color that should be used. Gets handled via +-- gears.color. The default is black. +-- @see gears.color +function wallpaper.centered(surf, s, background) + local geom, cr = wallpaper.prepare_context(s) + surf = surface.load_uncached(surf) + background = color(background) + + -- Fill the area with the background + cr.operator = cairo.Operator.SOURCE + cr.source = background + cr:paint() + + -- Now center the surface + local w, h = surface.get_size(surf) + cr:translate((geom.width - w) / 2, (geom.height - h) / 2) + cr:rectangle(0, 0, w, h) + cr:clip() + cr:set_source_surface(surf, 0, 0) + cr:paint() + surf:finish() +end + +--- Set a tiled wallpaper. +-- @param surf The wallpaper to tile. Either a cairo surface or a file name. +-- @param s The screen whose wallpaper should be set. Can be nil, in which case +-- all screens are set. +-- @param offset This can be set to a table with entries x and y. +function wallpaper.tiled(surf, s, offset) + local _, cr = wallpaper.prepare_context(s) + + if offset then + cr:translate(offset.x, offset.y) + end + + surf = surface.load_uncached(surf) + local pattern = cairo.Pattern.create_for_surface(surf) + pattern.extend = cairo.Extend.REPEAT + cr.source = pattern + cr.operator = cairo.Operator.SOURCE + cr:paint() + surf:finish() +end + +--- Set a maximized wallpaper. +-- @param surf The wallpaper to set. Either a cairo surface or a file name. +-- @param s The screen whose wallpaper should be set. Can be nil, in which case +-- all screens are set. +-- @param ignore_aspect If this is true, the image's aspect ratio is ignored. +-- The default is to honor the aspect ratio. +-- @param offset This can be set to a table with entries x and y. +function wallpaper.maximized(surf, s, ignore_aspect, offset) + local geom, cr = wallpaper.prepare_context(s) + surf = surface.load_uncached(surf) + local w, h = surface.get_size(surf) + local aspect_w = geom.width / w + local aspect_h = geom.height / h + + if not ignore_aspect then + aspect_h = math.max(aspect_w, aspect_h) + aspect_w = math.max(aspect_w, aspect_h) + end + cr:scale(aspect_w, aspect_h) + + if offset then + cr:translate(offset.x, offset.y) + elseif not ignore_aspect then + local scaled_width = geom.width / aspect_w + local scaled_height = geom.height / aspect_h + cr:translate((scaled_width - w) / 2, (scaled_height - h) / 2) + end + + cr:set_source_surface(surf, 0, 0) + cr.operator = cairo.Operator.SOURCE + cr:paint() + surf:finish() +end + +--- Set a fitting wallpaper. +-- @param surf The wallpaper to set. Either a cairo surface or a file name. +-- @param s The screen whose wallpaper should be set. Can be nil, in which case +-- all screens are set. +-- @param background The background color that should be used. Gets handled via +-- gears.color. The default is black. +-- @see gears.color +function wallpaper.fit(surf, s, background) + local geom, cr = wallpaper.prepare_context(s) + surf = surface.load_uncached(surf) + background = color(background) + + -- Fill the area with the background + cr.operator = cairo.Operator.SOURCE + cr.source = background + cr:paint() + + -- Now fit the surface + local w, h = surface.get_size(surf) + local scale = geom.width / w + if h * scale > geom.height then + scale = geom.height / h + end + cr:translate((geom.width - (w * scale)) / 2, (geom.height - (h * scale)) / 2) + cr:rectangle(0, 0, w * scale, h * scale) + cr:clip() + cr:scale(scale, scale) + cr:set_source_surface(surf, 0, 0) + cr:paint() + surf:finish() +end + +return wallpaper + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 |