aboutsummaryrefslogtreecommitdiff
path: root/.config/awesome/ui/statusbar/panel/widgets/imagebox.lua
diff options
context:
space:
mode:
Diffstat (limited to '.config/awesome/ui/statusbar/panel/widgets/imagebox.lua')
-rw-r--r--.config/awesome/ui/statusbar/panel/widgets/imagebox.lua706
1 files changed, 706 insertions, 0 deletions
diff --git a/.config/awesome/ui/statusbar/panel/widgets/imagebox.lua b/.config/awesome/ui/statusbar/panel/widgets/imagebox.lua
new file mode 100644
index 0000000..8c6e8a5
--- /dev/null
+++ b/.config/awesome/ui/statusbar/panel/widgets/imagebox.lua
@@ -0,0 +1,706 @@
+---------------------------------------------------------------------------
+-- A widget to display an image.
+--
+-- The `wibox.widget.imagebox` is part of the Awesome WM's widget system
+-- (see @{03-declarative-layout.md}).
+--
+-- This widget displays an image. The image can be a file,
+-- a cairo image surface, or an rsvg handle object (see the
+-- [image property](#image)).
+--
+-- Examples using a `wibox.widget.imagebox`:
+-- ---
+--
+-- @DOC_wibox_widget_defaults_imagebox_EXAMPLE@
+--
+-- Alternatively, you can declare the `imagebox` widget using the
+-- declarative pattern (both variants are strictly equivalent):
+--
+-- @DOC_wibox_widget_declarative-pattern_imagebox_EXAMPLE@
+--
+-- @author Uli Schlachter
+-- @copyright 2010 Uli Schlachter
+-- @widgetmod wibox.widget.imagebox
+-- @supermodule wibox.widget.base
+---------------------------------------------------------------------------
+
+local lgi = require("lgi")
+local cairo = lgi.cairo
+
+local base = require("wibox.widget.base")
+local surface = require("gears.surface")
+local gtable = require("gears.table")
+local gdebug = require("gears.debug")
+local setmetatable = setmetatable
+local type = type
+local math = math
+
+local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1)
+
+-- Safe load for optional Rsvg module
+local Rsvg = nil
+do
+ local success, err = pcall(function() Rsvg = lgi.Rsvg end)
+ if not success then
+ gdebug.print_warning(debug.traceback("Could not load Rsvg: " .. tostring(err)))
+ end
+end
+
+local imagebox = { mt = {} }
+
+local rsvg_handle_cache = setmetatable({}, { __mode = 'k' })
+
+---Load rsvg handle form image file
+-- @tparam string file Path to svg file.
+-- @return Rsvg handle
+-- @treturn table A table where cached data can be stored.
+local function load_rsvg_handle(file)
+ if not Rsvg then return end
+
+ local cache = (rsvg_handle_cache[file] or {})["handle"]
+
+ if cache then
+ return cache, rsvg_handle_cache[file]
+ end
+
+ local handle, err
+
+ if file:match("<[?]?xml") or file:match("<svg") then
+ handle, err = Rsvg.Handle.new_from_data(file)
+ else
+ handle, err = Rsvg.Handle.new_from_file(file)
+ end
+
+ if not err then
+ rsvg_handle_cache[file] = rsvg_handle_cache[file] or {}
+ rsvg_handle_cache[file]["handle"] = handle
+ return handle, rsvg_handle_cache[file]
+ end
+end
+
+---Apply cairo surface for given imagebox widget
+local function set_surface(ib, surf)
+ local is_surf_valid = surf.width > 0 and surf.height > 0
+ if not is_surf_valid then return false end
+
+ ib._private.default = { width = surf.width, height = surf.height }
+ ib._private.handle = nil
+ ib._private.image = surf
+ return true
+end
+
+---Apply RsvgHandle for given imagebox widget
+local function set_handle(ib, handle, cache)
+ local dim = handle:get_dimensions()
+ local is_handle_valid = dim.width > 0 and dim.height > 0
+ if not is_handle_valid then return false end
+
+ ib._private.default = { width = dim.width, height = dim.height }
+ ib._private.handle = handle
+ ib._private.cache = cache
+ ib._private.image = nil
+
+ return true
+end
+
+---Try to load some image object from file then apply it to imagebox.
+---@tparam table ib Imagebox
+---@tparam string file Image file name
+---@tparam function image_loader Function to load image object from file
+---@tparam function image_setter Function to set image object to imagebox
+---@treturn boolean True if image was successfully applied
+local function load_and_apply(ib, file, image_loader, image_setter)
+ local image_applied
+ local object, cache = image_loader(file)
+
+ if object then
+ image_applied = image_setter(ib, object, cache)
+ end
+ return image_applied
+end
+
+---Update the cached size depending on the stylesheet and dpi.
+--
+-- It's necessary because a single RSVG handle can be used by
+-- many imageboxes. So DPI and Stylesheet need to be set each time.
+local function update_dpi(self, ctx)
+ if not self._private.handle then return end
+
+ local dpi = self._private.auto_dpi and
+ ctx.dpi or
+ self._private.dpi or
+ nil
+
+ local need_dpi = dpi and
+ self._private.last_dpi ~= dpi
+
+ local need_style = self._private.handle.set_stylesheet and
+ self._private.stylesheet
+
+ local old_size = self._private.default and self._private.default.width
+
+ if dpi and dpi ~= self._private.cache.dpi then
+ if type(dpi) == "table" then
+ self._private.handle:set_dpi_x_y(dpi.x, dpi.y)
+ else
+ self._private.handle:set_dpi(dpi)
+ end
+ end
+
+ if need_style and self._private.cache.stylesheet ~= self._private.stylesheet then
+ self._private.handle:set_stylesheet(self._private.stylesheet)
+ end
+
+ -- Reload the size.
+ if need_dpi or (need_style and self._private.stylesheet ~= self._private.last_stylesheet) then
+ set_handle(self, self._private.handle, self._private.cache)
+ end
+
+ self._private.last_dpi = dpi
+ self._private.cache.dpi = dpi
+ self._private.last_stylesheet = self._private.stylesheet
+ self._private.cache.stylesheet = self._private.stylesheet
+
+ -- This can happen in the constructor when `dpi` is set after `image`.
+ if old_size and old_size ~= self._private.default.width then
+ self:emit_signal("widget::redraw_needed")
+ self:emit_signal("widget::layout_changed")
+ end
+end
+
+-- Draw an imagebox with the given cairo context in the given geometry.
+function imagebox:draw(ctx, cr, width, height)
+ if width == 0 or height == 0 or not self._private.default then return end
+
+ -- For valign = "top" and halign = "left"
+ local translate = {
+ x = 0,
+ y = 0,
+ }
+
+ update_dpi(self, ctx)
+
+ local w, h = self._private.default.width, self._private.default.height
+
+ if self._private.resize then
+ -- That's for the "fit" policy.
+ local aspects = {
+ w = width / w,
+ h = height / h
+ }
+
+ local policy = {
+ w = self._private.horizontal_fit_policy or "auto",
+ h = self._private.vertical_fit_policy or "auto"
+ }
+
+ for _, aspect in ipairs {"w", "h"} do
+ if self._private.upscale == false and (w < width and h < height) then
+ aspects[aspect] = 1
+ elseif self._private.downscale == false and (w >= width and h >= height) then
+ aspects[aspect] = 1
+ elseif policy[aspect] == "none" then
+ aspects[aspect] = 1
+ elseif policy[aspect] == "auto" then
+ aspects[aspect] = math.min(width / w, height / h)
+ elseif policy[aspect] == "cover" then
+ aspects[aspect] = math.max(width / w, height / h)
+ end
+ end
+
+ if self._private.halign == "center" then
+ translate.x = math.floor((width - w*aspects.w)/2)
+ elseif self._private.halign == "right" then
+ translate.x = math.floor(width - (w*aspects.w))
+ end
+
+ if self._private.valign == "center" then
+ translate.y = math.floor((height - h*aspects.h)/2)
+ elseif self._private.valign == "bottom" then
+ translate.y = math.floor(height - (h*aspects.h))
+ end
+
+ cr:translate(translate.x, translate.y)
+
+ -- Before using the scale, make sure it is below the threshold.
+ local threshold, max_factor = self._private.max_scaling_factor, math.max(aspects.w, aspects.h)
+
+ if threshold and threshold > 0 and threshold < max_factor then
+ aspects.w = (aspects.w*threshold)/max_factor
+ aspects.h = (aspects.h*threshold)/max_factor
+ end
+
+ -- Set the clip
+ if self._private.clip_shape then
+ cr:clip(self._private.clip_shape(cr, w*aspects.w, h*aspects.h, unpack(self._private.clip_args)))
+ end
+
+ cr:scale(aspects.w, aspects.h)
+ else
+ if self._private.halign == "center" then
+ translate.x = math.floor((width - w)/2)
+ elseif self._private.halign == "right" then
+ translate.x = math.floor(width - w)
+ end
+
+ if self._private.valign == "center" then
+ translate.y = math.floor((height - h)/2)
+ elseif self._private.valign == "bottom" then
+ translate.y = math.floor(height - h)
+ end
+
+ cr:translate(translate.x, translate.y)
+
+ -- Set the clip
+ if self._private.clip_shape then
+ cr:clip(self._private.clip_shape(cr, w, h, unpack(self._private.clip_args)))
+ end
+ end
+
+ if self._private.handle then
+ self._private.handle:render_cairo(cr)
+ else
+ cr:set_source_surface(self._private.image, 0, 0)
+
+ local filter = self._private.scaling_quality
+
+ if filter then
+ cr:get_source():set_filter(cairo.Filter[filter:upper()])
+ end
+
+ cr:paint()
+ end
+end
+
+-- Fit the imagebox into the given geometry
+function imagebox:fit(ctx, width, height)
+ if not self._private.default then return 0, 0 end
+
+ update_dpi(self, ctx)
+
+ local w, h = self._private.default.width, self._private.default.height
+
+ if w <= width and h <= height and self._private.upscale == false then
+ return w, h
+ end
+
+ if (w < width or h < height) and self._private.downscale == false then
+ return w, h
+ end
+
+ if self._private.resize or w > width or h > height then
+ local aspect = math.min(width / w, height / h)
+ return w * aspect, h * aspect
+ end
+
+ return w, h
+end
+
+--- The image rendered by the `imagebox`.
+--
+-- @property image
+-- @tparam[opt=nil] image|nil image
+-- @propemits false false
+
+--- Set the `imagebox` image.
+--
+-- The image can be a file, a cairo image surface, or an rsvg handle object
+-- (see the [image property](#image)).
+-- @method set_image
+-- @hidden
+-- @tparam image image The image to render.
+-- @treturn boolean `true` on success, `false` if the image cannot be used.
+-- @usage my_imagebox:set_image(beautiful.awesome_icon)
+-- @usage my_imagebox:set_image('/usr/share/icons/theme/my_icon.png')
+-- @see image
+function imagebox:set_image(image)
+ local setup_succeed
+
+ -- Keep the original to prevent the cache from being GCed.
+ self._private.original_image = image
+
+ if type(image) == "userdata" and not (Rsvg and Rsvg.Handle:is_type_of(image)) then
+ -- This function is not documented to handle userdata objects, but
+ -- historically it did, and it did by just assuming they refer to a
+ -- cairo surface.
+ image = surface.load(image)
+ end
+
+ if type(image) == "string" then
+ -- try to load rsvg handle from file
+ setup_succeed = load_and_apply(self, image, load_rsvg_handle, set_handle)
+
+ if not setup_succeed then
+ -- rsvg handle failed, try to load cairo surface with pixbuf
+ setup_succeed = load_and_apply(self, image, surface.load, set_surface)
+ end
+ elseif Rsvg and Rsvg.Handle:is_type_of(image) then
+ -- try to apply given rsvg handle
+ rsvg_handle_cache[image] = rsvg_handle_cache[image] or {}
+ setup_succeed = set_handle(self, image, rsvg_handle_cache[image])
+ elseif cairo.Surface:is_type_of(image) then
+ -- try to apply given cairo surface
+ setup_succeed = set_surface(self, image)
+ elseif not image then
+ -- nil as argument mean full imagebox reset
+ setup_succeed = true
+ self._private.handle = nil
+ self._private.image = nil
+ self._private.default = nil
+ end
+
+ if not setup_succeed then return false end
+
+ self:emit_signal("widget::redraw_needed")
+ self:emit_signal("widget::layout_changed")
+ self:emit_signal("property::image")
+ return true
+end
+
+--- Set a clip shape for this imagebox.
+--
+-- A clip shape defines an area and dimension to which the content should be
+-- trimmed.
+--
+-- @DOC_wibox_widget_imagebox_clip_shape_EXAMPLE@
+--
+-- @property clip_shape
+-- @tparam[opt=gears.shape.rectangle] shape clip_shape A `gears.shape` compatible shape function.
+-- @propemits true false
+-- @see gears.shape
+
+--- Set a clip shape for this imagebox.
+--
+-- A clip shape defines an area and dimensions to which the content should be
+-- trimmed.
+--
+-- Additional parameters will be passed to the clip shape function.
+--
+-- @tparam function|gears.shape clip_shape A `gears_shape` compatible shape function.
+-- @method set_clip_shape
+-- @hidden
+-- @see gears.shape
+-- @see clip_shape
+function imagebox:set_clip_shape(clip_shape, ...)
+ self._private.clip_shape = clip_shape
+ self._private.clip_args = {...}
+ self:emit_signal("widget::redraw_needed")
+ self:emit_signal("property::clip_shape", clip_shape)
+end
+
+--- Should the image be resized to fit into the available space?
+--
+-- Note that `upscale` and `downscale` can affect the value of `resize`.
+-- If conflicting values are passed to the constructor, then the result
+-- is undefined.
+--
+-- @DOC_wibox_widget_imagebox_resize_EXAMPLE@
+-- @property resize
+-- @propemits true false
+-- @tparam[opt=true] boolean resize
+
+--- Allow the image to be upscaled (made bigger).
+--
+-- Note that `upscale` and `downscale` can affect the value of `resize`.
+-- If conflicting values are passed to the constructor, then the result
+-- is undefined.
+--
+-- @DOC_wibox_widget_imagebox_upscale_EXAMPLE@
+-- @property upscale
+-- @tparam[opt=self.resize] boolean upscale
+-- @see downscale
+-- @see resize
+
+--- Allow the image to be downscaled (made smaller).
+--
+-- Note that `upscale` and `downscale` can affect the value of `resize`.
+-- If conflicting values are passed to the constructor, then the result
+-- is undefined.
+--
+-- @DOC_wibox_widget_imagebox_downscale_EXAMPLE@
+-- @property downscale
+-- @tparam[opt=self.resize] boolean downscale
+-- @see upscale
+-- @see resize
+
+--- Set the SVG CSS stylesheet.
+--
+-- If the image is an SVG (vector graphics), this property allows to set
+-- a CSS stylesheet. It can be used to set colors and much more.
+--
+-- Note that this property is a string, not a path. If the stylesheet is
+-- stored on disk, read the content first.
+--
+--@DOC_wibox_widget_imagebox_stylesheet_EXAMPLE@
+--
+-- @property stylesheet
+-- @tparam[opt=""] string stylesheet
+-- @propemits true false
+
+--- Set the SVG DPI (dot per inch).
+--
+-- Force a specific DPI when rendering the `.svg`. For other file formats,
+-- this does nothing.
+--
+-- It can either be a number of a table containing the `x` and `y` keys.
+--
+-- Please note that DPI and `resize` can "fight" each other and end up
+-- making the image smaller instead of bigger.
+--
+--@DOC_wibox_widget_imagebox_dpi_EXAMPLE@
+--
+-- @property dpi
+-- @tparam[opt=96] number|table dpi
+-- @negativeallowed false
+-- @propemits true false
+-- @see auto_dpi
+
+--- Use the object DPI when rendering the SVG.
+--
+-- By default, the SVG are interpreted as-is. When this property is set,
+-- the screen DPI will be passed to the SVG renderer. Depending on which
+-- tool was used to create the `.svg`, this may do nothing at all. However,
+-- for example, if the `.svg` uses `<text>` elements and doesn't have an
+-- hardcoded stylesheet, the result will differ.
+--
+-- @property auto_dpi
+-- @tparam[opt=false] boolean auto_dpi
+-- @propemits true false
+-- @see dpi
+
+for _, prop in ipairs {"stylesheet", "dpi", "auto_dpi"} do
+ imagebox["set_" .. prop] = function(self, value)
+ -- It will be set in :fit and :draw. The handle is shared
+ -- by multiple imagebox, so it cannot be set just once.
+ self._private[prop] = value
+
+ self:emit_signal("widget::redraw_needed")
+ self:emit_signal("widget::layout_changed")
+ self:emit_signal("property::" .. prop)
+ end
+end
+
+function imagebox:set_resize(allowed)
+ self._private.resize = allowed
+
+ if allowed then
+ self._private.downscale = true
+ self._private.upscale = true
+ self:emit_signal("property::downscale", allowed)
+ self:emit_signal("property::upscale", allowed)
+ end
+
+ self:emit_signal("widget::redraw_needed")
+ self:emit_signal("widget::layout_changed")
+ self:emit_signal("property::resize", allowed)
+end
+
+for _, prop in ipairs {"downscale", "upscale" } do
+ imagebox["set_" .. prop] = function(self, allowed)
+ self._private[prop] = allowed
+
+ if self._private.resize ~= (self._private.upscale or self._private.downscale) then
+ self._private.resize = self._private.upscale or self._private.downscale
+ self:emit_signal("property::resize", self._private.resize)
+ end
+
+ self:emit_signal("widget::redraw_needed")
+ self:emit_signal("widget::layout_changed")
+ self:emit_signal("property::"..prop, allowed)
+ end
+end
+
+--- Set the horizontal fit policy.
+--
+-- Here is the result for a 22x32 image:
+--
+-- @DOC_wibox_widget_imagebox_horizontal_fit_policy_EXAMPLE@
+--
+-- @property horizontal_fit_policy
+-- @tparam[opt="auto"] string horizontal_fit_policy
+-- @propertyvalue "auto" Honor the `resize` variable and preserve the aspect ratio.
+-- @propertyvalue "none" Do not resize at all.
+-- @propertyvalue "fit" Resize to the widget width.
+-- @propertyvalue "cover" Resize to fill widget and preserve the aspect ratio.
+-- @propemits true false
+-- @see vertical_fit_policy
+-- @see resize
+
+--- Set the vertical fit policy.
+--
+-- Here is the result for a 32x22 image:
+--
+-- @DOC_wibox_widget_imagebox_vertical_fit_policy_EXAMPLE@
+--
+-- @property vertical_fit_policy
+-- @tparam[opt="auto"] string vertical_fit_policy
+-- @propertyvalue "auto" Honor the `resize` variable and preserve the aspect ratio.
+-- @propertyvalue "none" Do not resize at all.
+-- @propertyvalue "fit" Resize to the widget height.
+-- @propertyvalue "cover" Resize to fill widget and preserve the aspect ratio.
+-- @propemits true false
+-- @see horizontal_fit_policy
+-- @see resize
+
+
+--- The vertical alignment.
+--
+-- @DOC_wibox_widget_imagebox_valign_EXAMPLE@
+--
+-- @property valign
+-- @tparam[opt="center"] string valign
+-- @propertyvalue "top"
+-- @propertyvalue "center"
+-- @propertyvalue "bottom"
+-- @propemits true false
+-- @see wibox.container.place
+-- @see halign
+
+--- The horizontal alignment.
+--
+-- @DOC_wibox_widget_imagebox_halign_EXAMPLE@
+--
+-- @property halign
+-- @tparam[opt="center"] string halign
+-- @propertyvalue "left"
+-- @propertyvalue "center"
+-- @propertyvalue "right"
+-- @propemits true false
+-- @see wibox.container.place
+-- @see valign
+
+--- The maximum scaling factor.
+--
+-- If an image is scaled too much, it gets very blurry. This
+-- property allows to limit the scaling.
+-- Use the properties `valign` and `halign` to control how the image will be
+-- aligned.
+--
+-- In the example below, the original size is 22x22
+--
+-- @DOC_wibox_widget_imagebox_max_scaling_factor_EXAMPLE@
+--
+-- @property max_scaling_factor
+-- @tparam[opt=0] number max_scaling_factor Use `0` for "no limit".
+-- @negativeallowed false
+-- @propemits true false
+-- @see valign
+-- @see halign
+-- @see scaling_quality
+
+--- Set the scaling aligorithm.
+--
+-- Depending on how the image is used, what is the "correct" way to
+-- scale can change. For example, upscaling a pixel art image should
+-- not make it blurry. However, scaling up a photo should not make it
+-- blocky.
+--
+--<table class='widget_list' border=1>
+-- <tr style='font-weight: bold;'>
+-- <th align='center'>Value</th>
+-- <th align='center'>Description</th>
+-- </tr>
+-- <tr><td>fast</td><td>A high-performance filter</td></tr>
+-- <tr><td>good</td><td>A reasonable-performance filter</td></tr>
+-- <tr><td>best</td><td>The highest-quality available</td></tr>
+-- <tr><td>nearest</td><td>Nearest-neighbor filtering (blocky)</td></tr>
+-- <tr><td>bilinear</td><td>Linear interpolation in two dimensions</td></tr>
+--</table>
+--
+-- The image used in the example below has a resolution of 32x22 and is
+-- intentionally blocky to highlight the difference.
+-- It is zoomed by a factor of 3.
+--
+-- @DOC_wibox_widget_imagebox_scaling_quality_EXAMPLE@
+--
+-- @property scaling_quality
+-- @tparam[opt="good"] string scaling_quality
+-- @propertyvalue "fast" A high-performance filter.
+-- @propertyvalue "good" A reasonable-performance filter.
+-- @propertyvalue "best" The highest-quality available.
+-- @propertyvalue "nearest" Nearest-neighbor filtering (blocky).
+-- @propertyvalue "bilinear" Linear interpolation in two dimensions.
+-- @propemits true false
+-- @see resize
+-- @see horizontal_fit_policy
+-- @see vertical_fit_policy
+-- @see max_scaling_factor
+
+local defaults = {
+ halign = "left",
+ valign = "top",
+ horizontal_fit_policy = "auto",
+ vertical_fit_policy = "auto",
+ max_scaling_factor = 0,
+ scaling_quality = "good"
+}
+
+local function get_default(prop, value)
+ if value == nil then return defaults[prop] end
+
+ return value
+end
+
+for prop in pairs(defaults) do
+ imagebox["set_"..prop] = function(self, value)
+ if value == self._private[prop] then return end
+
+ self._private[prop] = get_default(prop, value)
+ self:emit_signal("widget::redraw_needed")
+ self:emit_signal("property::"..prop, self._private[prop])
+ end
+
+ imagebox["get_"..prop] = function(self)
+ if self._private[prop] == nil then
+ return defaults[prop]
+ end
+
+ return self._private[prop]
+ end
+end
+
+--- Returns a new `wibox.widget.imagebox` instance.
+--
+-- This is the constructor of `wibox.widget.imagebox`. It creates a new
+-- instance of imagebox widget.
+--
+-- Alternatively, the declarative layout syntax can handle
+-- `wibox.widget.imagebox` instanciation.
+--
+-- The image can be a file, a cairo image surface, or an rsvg handle object
+-- (see the [image property](#image)).
+--
+-- Any additional arguments will be passed to the clip shape function.
+-- @tparam[opt] image image The image to display (may be `nil`).
+-- @tparam[opt] boolean resize_allowed If `false`, the image will be
+-- clipped, else it will be resized to fit into the available space.
+-- @tparam[opt] function clip_shape A `gears.shape` compatible function.
+-- @treturn wibox.widget.imagebox A new `wibox.widget.imagebox` widget instance.
+-- @constructorfct wibox.widget.imagebox
+local function new(image, resize_allowed, clip_shape, ...)
+ local ret = base.make_widget(nil, nil, {enable_properties = true})
+
+ gtable.crush(ret, imagebox, true)
+ ret._private.resize = true
+
+ if image then
+ ret:set_image(image)
+ end
+
+ if resize_allowed ~= nil then
+ ret.resize = resize_allowed
+ end
+
+ ret._private.clip_shape = clip_shape
+ ret._private.clip_args = {...}
+
+ return ret
+end
+
+function imagebox.mt:__call(...)
+ return new(...)
+end
+
+return setmetatable(imagebox, imagebox.mt)
+
+-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80