--------------------------------------------------------------------------- -- This module simplifies the creation of cairo pattern objects. -- -- In most places in awesome where a color is needed, the provided argument is -- passed to @{gears.color}, which actually calls @{create_pattern} and creates -- a pattern from a given string or table. -- -- This function can create solid, linear, radial and png patterns. -- -- A simple example for a solid pattern is a hexadecimal color specification. -- For example `#ff8000` creates a solid pattern with 100% red, 50% green and 0% -- blue. Limited support for named colors (`red`) is also provided. -- -- 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" -- The above will call @{create_radial_pattern} with the provided string, after -- stripping the `radial:` prefix. -- -- 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. -- -- @author Uli Schlachter -- @copyright 2010 Uli Schlachter -- @themelib 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 local color_string_cache = setmetatable({}, { __mode = "k" }) --- 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 number between 0 and 1 for the 'red' value (1st channel) -- @treturn number between 0 and 1 for the 'green' value (2nd channel) -- @treturn number between 0 and 1 for the 'blue' value (3rd channel) -- @treturn number between 0 and 1 for the 'opacity' value (4th channel) -- if the incoming color code only has 3 values (only rgb, not opacity) the 4th -- return value is 1. -- @treturn nil if input is incorrect -- @staticfct gears.color.parse_color -- @usage -- Both of the following will return 0, 0.4, 0.8, 1 -- gears.color.parse_color("#0066ccff") -- gears.color.parse_color("#0066cc") 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 -- @staticfct gears.color.create_solid_pattern 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 -- @staticfct gears.color.create_png_pattern 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:"` -- Alternatively, the pattern can be specified as a table: -- { type = "linear", from = { x0, y0 }, to = { x1, y1 }, -- stops = { } } -- `x0,y0` and `x1,y1` are the start and stop point of the pattern. -- For the explanation of ``, see `color.create_pattern`. -- @tparam string|table arg The argument describing the pattern. -- @return a cairo pattern object -- @staticfct gears.color.create_linear_pattern 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:"` -- Alternatively, the pattern can be specified as a table: -- { type = "radial", from = { x0, y0, r0 }, to = { x1, y1, r1 }, -- 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 ``, see `color.create_pattern`. -- @tparam string|table arg The argument describing the pattern -- @return a cairo pattern object -- @staticfct gears.color.create_radial_pattern 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 -- @staticfct gears.color.create_pattern_uncached 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 -- @treturn gears.color A cairo pattern object. -- @staticfct gears.color.create_pattern 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 -- @staticfct gears.color.create_opaque_pattern 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. -- @staticfct gears.color.recolor_image function color.recolor_image(image, new_color) image = surface.duplicate_surface(image) 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 --- Take an input color and set a different opacity. -- -- @staticfct gears.color.change_opacity -- @tparam string|pattern input The input color. -- @tparam number opacity A floating point number between 0 and 1. -- @treturn color The new color if successful or `input` if invalid. function color.change_opacity(input, opacity) input = color.create_pattern(input) local error, r, g, b, _ = input:get_rgba() if error ~= "SUCCESS" then return input end return cairo.Pattern.create_rgba(r, g, b, opacity) end --- Convert a color back to an hexadecimal color code. -- -- This takes an input color, pattern or gradient and attempt to convert it -- to a color. If this fails, `fallback` is returned. This is useful when a -- color needs to be concatenated into a Pango markup string. -- -- @staticfct gears.color.to_rgba_string -- @tparam pattern|string|gradient color Note that only solid colors can be -- convedted to the `RGBA` format. -- @tparam[opt=nil] pattern|string|gradient fallback The color to return -- if `color` cannot be converted to a string. -- @treturn string The color in `#rrggbbaa` format. -- @see gears.color.ensure_pango_color function color.to_rgba_string(col, fallback) if (not col) and (not fallback) then return nil end -- Prevent infinite recursion. if not col then return color.to_rgba_string(fallback) end if color_string_cache[col] then return color_string_cache[col] end col = color.create_pattern(col) local error1, error2, r, g, b, a = pcall(function () return col:get_rgba() end) -- Surface patterns don't have an RGBA representation. if (not error1) or error2 ~= "SUCCESS" then return color.to_rgba_string(fallback) end color_string_cache[col] = string.format( "#%02x%02x%02x%02x", math.floor(r*255), math.floor(g*255), math.floor(b*255), math.floor(a*255) ) return color_string_cache[col] end --- Get a valid color for Pango markup -- @param check_color The color to check. -- @tparam string fallback The color to return if the first is invalid. (default: black) -- @treturn string color if it is valid, else fallback. -- @staticfct gears.color.ensure_pango_color function color.ensure_pango_color(check_color, fallback) -- This will happen if `gears.color` has been called in the theme. if type(check_color) == "userdata" then return color.to_rgba_string(check_color, fallback) end check_color = tostring(check_color) -- Pango markup supports alpha, PangoColor does not. Thus, check for this. local len = #check_color if string.match(check_color, "^#%x+$") and (len == 5 or len == 9 or len == 17) then return check_color end return Pango.Color.parse(Pango.Color(), check_color) and check_color or fallback or "black" 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