mirror of
https://github.com/awesomeWM/awesome
synced 2024-11-17 07:47:41 +01:00
f6f57e7ba0
Some code in awesome concatenate color strings. This means it blows up when the pattern are used instead of color, with this, it fails gracefully (by returning the unmodified pattern).
447 lines
15 KiB
Lua
447 lines
15 KiB
Lua
---------------------------------------------------------------------------
|
|
-- 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:<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
|
|
-- @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:<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
|
|
-- @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
|
|
-- @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 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
|