imagebox: Add DPI and CSS Stylesheet properties for SVG images.

This exposes the `librsvg` DPI and Stylesheet properties. This
is a groundwork commit to port the xresources theme to use a SVG
wallpaper instead of hardcoded Cairo code to generate the colorfar
logo.
This commit is contained in:
Emmanuel Lepage Vallee 2021-09-11 23:07:27 -07:00
parent 883cdb7f41
commit d8bc791818
1 changed files with 168 additions and 50 deletions

View File

@ -48,7 +48,7 @@ end
local imagebox = { mt = {} } local imagebox = { mt = {} }
local rsvg_handle_cache = setmetatable({}, { __mode = 'v' }) local rsvg_handle_cache = setmetatable({}, { __mode = 'k' })
---Load rsvg handle form image file ---Load rsvg handle form image file
---@tparam string file Path to svg file. ---@tparam string file Path to svg file.
@ -56,28 +56,119 @@ local rsvg_handle_cache = setmetatable({}, { __mode = 'v' })
local function load_rsvg_handle(file) local function load_rsvg_handle(file)
if not Rsvg then return end if not Rsvg then return end
local cache = rsvg_handle_cache[file] local cache = (rsvg_handle_cache[file] or {})["handle"]
if cache then if cache then
return cache return cache, rsvg_handle_cache[file]
end end
local handle, err = nil, nil local handle, err
if file:match("<[?]?xml") then if file:match("<[?]?xml") or file:match("<svg") then
handle, err = Rsvg.Handle.new_from_data(file) handle, err = Rsvg.Handle.new_from_data(file)
else else
handle, err = Rsvg.Handle.new_from_file(file) handle, err = Rsvg.Handle.new_from_file(file)
end end
if not err then if not err then
rsvg_handle_cache[file] = handle rsvg_handle_cache[file] = rsvg_handle_cache[file] or {}
return handle 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_surd_valid = surf.width > 0 and surf.height > 0
if not is_surd_valid then return 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 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
end end
-- Draw an imagebox with the given cairo context in the given geometry. -- Draw an imagebox with the given cairo context in the given geometry.
function imagebox:draw(_, cr, width, height) function imagebox:draw(ctx, cr, width, height)
if width == 0 or height == 0 or not self._private.default then return end if width == 0 or height == 0 or not self._private.default then return end
-- For valign = "top" and halign = "left" -- For valign = "top" and halign = "left"
@ -86,6 +177,8 @@ function imagebox:draw(_, cr, width, height)
y = 0, y = 0,
} }
update_dpi(self, ctx)
local w, h = self._private.default.width, self._private.default.height local w, h = self._private.default.width, self._private.default.height
if self._private.resize then if self._private.resize then
@ -177,8 +270,11 @@ function imagebox:draw(_, cr, width, height)
end end
-- Fit the imagebox into the given geometry -- Fit the imagebox into the given geometry
function imagebox:fit(_, width, height) function imagebox:fit(ctx, width, height)
if not self._private.default then return 0, 0 end 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 local w, h = self._private.default.width, self._private.default.height
if w <= width and h <= height and self._private.upscale == false then if w <= width and h <= height and self._private.upscale == false then
@ -197,44 +293,6 @@ function imagebox:fit(_, width, height)
return w, h return w, h
end end
---Apply cairo surface for given imagebox widget
local function set_surface(ib, surf)
local is_surd_valid = surf.width > 0 and surf.height > 0
if not is_surd_valid then return 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)
local dim = handle:get_dimensions()
local is_handle_valid = dim.width > 0 and dim.height > 0
if not is_handle_valid then return end
ib._private.default = { width = dim.width, height = dim.height }
ib._private.handle = handle
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 = image_loader(file)
if object then
image_applied = image_setter(ib, object)
end
return image_applied
end
--- The image rendered by the `imagebox`. --- The image rendered by the `imagebox`.
-- --
-- It can can be any of the following: -- It can can be any of the following:
@ -245,7 +303,7 @@ end
-- * `nil`: Unset the image. -- * `nil`: Unset the image.
-- --
-- @property image -- @property image
-- @tparam image image The image to render. -- @tparam image image The image to render. test
-- @propemits false false -- @propemits false false
--- Set the `imagebox` image. --- Set the `imagebox` image.
@ -262,7 +320,10 @@ end
function imagebox:set_image(image) function imagebox:set_image(image)
local setup_succeed local setup_succeed
if type(image) == "userdata" then -- Keep the original to prevent the cache from being GCed.
self._private.original_iamge = 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 -- This function is not documented to handle userdata objects, but
-- historically it did, and it did by just assuming they refer to a -- historically it did, and it did by just assuming they refer to a
-- cairo surface. -- cairo surface.
@ -279,7 +340,8 @@ function imagebox:set_image(image)
end end
elseif Rsvg and Rsvg.Handle:is_type_of(image) then elseif Rsvg and Rsvg.Handle:is_type_of(image) then
-- try to apply given rsvg handle -- try to apply given rsvg handle
setup_succeed = set_handle(self, image) 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 elseif cairo.Surface:is_type_of(image) then
-- try to apply given cairo surface -- try to apply given cairo surface
setup_succeed = set_surface(self, image) setup_succeed = set_surface(self, image)
@ -365,6 +427,62 @@ end
-- @see upscale -- @see upscale
-- @see resize -- @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 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 number|table dpi
-- @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) function imagebox:set_resize(allowed)
self._private.resize = allowed self._private.resize = allowed