415 lines
12 KiB
Lua
415 lines
12 KiB
Lua
---------------------------------------------------------------------------
|
|
-- DPI detection code. --
|
|
---------------------------------------------------------------------------
|
|
|
|
local capi = {screen = screen}
|
|
local gtable = require("gears.table")
|
|
local grect = require("gears.geometry").rectangle
|
|
local gdebug = require("gears.debug")
|
|
|
|
local module = {}
|
|
|
|
local ascreen, data = nil, nil
|
|
|
|
-- Metric to Imperial conversion constant.
|
|
local mm_per_inch = 25.4
|
|
|
|
local xft_dpi, fallback_dpi
|
|
|
|
local function get_fallback_dpi()
|
|
-- Following Keith Packard's whitepaper on Xft,
|
|
-- https://keithp.com/~keithp/talks/xtc2001/paper/xft.html#sec-editing
|
|
-- the proper fallback for Xft.dpi is the vertical DPI reported by
|
|
-- the X server. This will generally be 96 on Xorg, unless the user
|
|
-- has configured it differently
|
|
if not fallback_dpi then
|
|
local _, h = root.size()
|
|
local _, hmm = root.size_mm()
|
|
fallback_dpi = hmm ~= 0 and h * mm_per_inch / hmm
|
|
end
|
|
|
|
return fallback_dpi or 96
|
|
end
|
|
|
|
local function dpi_for_output(viewport, output)
|
|
local dpi = nil
|
|
local geo = viewport.geometry
|
|
|
|
-- Ignore outputs with width/height 0
|
|
if output.mm_width ~= 0 and output.mm_height ~= 0 then
|
|
local dpix = geo.width * mm_per_inch / output.mm_width
|
|
local dpiy = geo.height * mm_per_inch / output.mm_height
|
|
dpi = math.min(dpix, dpiy, dpi or dpix)
|
|
elseif ascreen._get_xft_dpi() then
|
|
dpi = ascreen._get_xft_dpi()
|
|
end
|
|
|
|
return dpi or get_fallback_dpi()
|
|
end
|
|
|
|
local function dpis_for_outputs(viewport)
|
|
local max_dpi, min_dpi, max_size, min_size = 0, math.huge, 0, math.huge
|
|
|
|
for _, o in pairs(viewport.outputs) do
|
|
local dpi = dpi_for_output(viewport, o)
|
|
o.dpi = dpi
|
|
|
|
max_dpi = math.max(max_dpi, dpi)
|
|
min_dpi = math.min(min_dpi, dpi)
|
|
|
|
-- Compute the diagonal size.
|
|
if o.mm_width and o.mm_height then
|
|
o.mm_size = math.sqrt(o.mm_width^2 + o.mm_height^2)
|
|
o.inch_size = o.mm_size/mm_per_inch
|
|
max_size = math.max(max_size, o.mm_size)
|
|
min_size = math.min(min_size, o.mm_size)
|
|
end
|
|
end
|
|
|
|
-- When there is no output.
|
|
if min_dpi == math.huge then
|
|
min_dpi = get_fallback_dpi()
|
|
max_dpi = min_dpi
|
|
end
|
|
|
|
--TODO Some output may have a lower resolution than the viewport, so their
|
|
-- DPI is currently wrong. Once fixed, the preferred DPI can become
|
|
-- different from the minimum one.
|
|
local pref_dpi = min_dpi
|
|
|
|
viewport.minimum_dpi = min_dpi
|
|
viewport.maximum_dpi = max_dpi
|
|
viewport.preferred_dpi = pref_dpi
|
|
|
|
-- Guess the diagonal size using the DPI.
|
|
if min_size == math.huge then
|
|
for _, o in pairs(viewport.outputs) do
|
|
local geo = o.geometry
|
|
if geo then
|
|
o.mm_size = math.sqrt(geo.width^2 + geo.height^2)/o.dpi
|
|
max_size = math.max(max_size, o.mm_size)
|
|
min_size = math.min(min_size, o.mm_size)
|
|
end
|
|
end
|
|
|
|
-- In case there is no output information.
|
|
if min_size == math.huge then
|
|
local geo = viewport.geometry
|
|
local size = math.sqrt(geo.width^2 + geo.height^2)/max_dpi
|
|
|
|
max_size, min_size = size, size
|
|
end
|
|
end
|
|
|
|
viewport.mm_minimum_size = min_size
|
|
viewport.mm_maximum_size = max_size
|
|
viewport.inch_minimum_size = min_size/mm_per_inch
|
|
viewport.inch_maximum_size = max_size/mm_per_inch
|
|
|
|
return max_dpi, min_dpi, pref_dpi
|
|
end
|
|
|
|
local function update_outputs(old_viewport, new_viewport)
|
|
gtable.diff_merge(
|
|
old_viewport.outputs,
|
|
new_viewport.outputs,
|
|
function(o)
|
|
return o.name or (
|
|
(o.mm_height or -7)*9999 * (o.mm_width or 5)*123
|
|
)
|
|
end,
|
|
gtable.crush
|
|
)
|
|
end
|
|
|
|
-- Fetch the current viewports and compare them to the caches ones.
|
|
--
|
|
-- The idea is to keep whatever metadata kept within the existing ones and know
|
|
-- what is added and removed.
|
|
local function update_viewports(force)
|
|
if #ascreen._viewports > 0 and not force then return ascreen._viewports end
|
|
|
|
local new = ascreen._get_viewports()
|
|
|
|
local _, add, rem = gtable.diff_merge(
|
|
ascreen._viewports,
|
|
new,
|
|
function(a) return a.id end,
|
|
update_outputs
|
|
)
|
|
|
|
for _, viewport in ipairs(ascreen._viewports) do
|
|
dpis_for_outputs(viewport)
|
|
end
|
|
|
|
assert(#ascreen._viewports > 0 or #new == 0)
|
|
|
|
return ascreen._viewports, add, rem
|
|
end
|
|
|
|
-- Compute more useful viewport metadata frrom_sparse(add)om the list of output.
|
|
-- @treturn table An viewport with more information.
|
|
local function update_screen_viewport(s)
|
|
local viewport = s.data.viewport
|
|
|
|
if #ascreen._viewports == 0 then
|
|
ascreen._viewports = update_viewports(false)
|
|
end
|
|
|
|
-- The maximum is equal to the screen viewport, so no need for many loops.
|
|
if not viewport then
|
|
local big_a, i_size = nil, 0
|
|
|
|
for _, a in ipairs(ascreen._viewports) do
|
|
local int = grect.get_intersection(a.geometry, s.geometry)
|
|
|
|
if int.width*int.height > i_size then
|
|
big_a, i_size = a, int.width*int.height
|
|
end
|
|
|
|
if i_size == s.geometry.width*s.geometry.height then break end
|
|
end
|
|
|
|
if big_a then
|
|
viewport, s.data.viewport = big_a, big_a
|
|
end
|
|
end
|
|
|
|
if not viewport then
|
|
gdebug.print_warning("Screen "..tostring(s)..
|
|
" doesn't overlap a known physical monitor")
|
|
end
|
|
end
|
|
|
|
function module.create_screen_handler(viewport)
|
|
local geo = viewport.geometry
|
|
|
|
local s = capi.screen.fake_add(
|
|
geo.x,
|
|
geo.y,
|
|
geo.width,
|
|
geo.height,
|
|
{_managed = true}
|
|
)
|
|
|
|
update_screen_viewport(s)
|
|
|
|
s:emit_signal("request::desktop_decoration")
|
|
s:emit_signal("request::wallpaper")
|
|
|
|
-- Will call all `connect_for_each_screen`.
|
|
s:emit_signal("added")
|
|
end
|
|
|
|
function module.remove_screen_handler(viewport)
|
|
for s in capi.screen do
|
|
if s.data.viewport and s.data.viewport.id == viewport.id then
|
|
s:fake_remove()
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
function module.resize_screen_handler(old_viewport, new_viewport)
|
|
for s in capi.screen do
|
|
if s.data.viewport and s.data.viewport.id == old_viewport.id then
|
|
local ngeo = new_viewport.geometry
|
|
s:fake_resize(
|
|
ngeo.x, ngeo.y, ngeo.width, ngeo.height
|
|
)
|
|
s.data.viewport = new_viewport
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Xft.dpi is explicit user configuration, so honor it.
|
|
-- (It isn't a local function to allow the tests to replace it)
|
|
function module._get_xft_dpi()
|
|
if not xft_dpi then
|
|
xft_dpi = tonumber(awesome.xrdb_get_value("", "Xft.dpi")) or false
|
|
end
|
|
|
|
return xft_dpi
|
|
end
|
|
|
|
-- Some of them may be present twice or a giant viewport may exist which
|
|
-- encompasses everything.
|
|
local function deduplicate_viewports(vps)
|
|
local min_x, min_y, max_x, max_y = math.huge, math.huge, 0, 0
|
|
|
|
-- Sort the viewports by `x`.
|
|
for _, vp in ipairs(vps) do
|
|
local geo = vp.geometry
|
|
min_x = math.min(min_x, geo.x)
|
|
max_x = math.max(max_x, geo.x+geo.width)
|
|
min_y = math.min(min_y, geo.y)
|
|
max_y = math.max(max_y, geo.y+geo.height)
|
|
end
|
|
|
|
-- Remove the "encompass everything" viewport (if any).
|
|
if #vps > 1 then
|
|
for k, vp in ipairs(vps) do
|
|
local geo = vp.geometry
|
|
if geo.x == min_x and geo.y == min_y
|
|
and geo.x+geo.width == max_x and geo.y+geo.height == max_y then
|
|
table.remove(vps, k)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
--TODO when the rules are added, mark the viewports that are embedded into
|
|
-- each other so the rules can decide to use the smaller or larger viewport
|
|
-- when creating the screen. This happens a lot then connecting to
|
|
-- projectors when doing presentations.
|
|
|
|
return vps
|
|
end
|
|
|
|
-- Provide a way for the tests to replace `capi.screen._viewports`.
|
|
function module._get_viewports()
|
|
assert(type(capi.screen._viewports()) == "table")
|
|
return deduplicate_viewports(capi.screen._viewports())
|
|
end
|
|
|
|
local function get_dpi(s)
|
|
if s.data.dpi or s.data.dpi_cache then
|
|
return s.data.dpi or s.data.dpi_cache
|
|
end
|
|
|
|
if not s.data.viewport then
|
|
update_screen_viewport(s)
|
|
end
|
|
|
|
-- Pick a DPI (from best to worst).
|
|
local dpi = ascreen._get_xft_dpi()
|
|
or (s.data.viewport and s.data.viewport.preferred_dpi or nil)
|
|
or get_fallback_dpi()
|
|
|
|
-- Pick the screen DPI depending on the `autodpi` settings.
|
|
-- Historically, AwesomeWM size unit was the pixel. This assumption is
|
|
-- present in a lot, if not most, user config and is why this cannot be
|
|
-- enable by default for existing users.
|
|
s.data.dpi_cache = data.autodpi and dpi
|
|
or ascreen._get_xft_dpi()
|
|
or get_fallback_dpi()
|
|
|
|
return s.data.dpi_cache
|
|
end
|
|
|
|
local function set_dpi(s, dpi)
|
|
s.data.dpi = dpi
|
|
end
|
|
|
|
screen.connect_signal("request::create", module.create_screen_handler)
|
|
screen.connect_signal("request::remove", module.remove_screen_handler)
|
|
screen.connect_signal("request::resize", module.resize_screen_handler)
|
|
|
|
-- Create some screens when none exist. This can happen when AwesomeWM is
|
|
-- started with `--screen manual` and no handler is used.
|
|
capi.screen.connect_signal("scanned", function()
|
|
if capi.screen.count() == 0 then
|
|
-- Private API to scan for screens now.
|
|
if #ascreen._get_viewports() == 0 then
|
|
capi.screen._scan_quiet()
|
|
end
|
|
|
|
local cur_viewports = ascreen._get_viewports()
|
|
|
|
if #cur_viewports > 0 then
|
|
for _, viewport in ipairs(cur_viewports) do
|
|
module.create_screen_handler(viewport)
|
|
end
|
|
else
|
|
capi.screen.fake_add(0, 0, 640, 480)
|
|
end
|
|
|
|
assert(capi.screen.count() > 0, "Creating screens failed")
|
|
end
|
|
end)
|
|
|
|
-- This is the (undocumented) signal sent by capi.
|
|
capi.screen.connect_signal("property::_viewports", function(a)
|
|
if capi.screen.automatic_factory then return end
|
|
|
|
assert(#a > 0)
|
|
|
|
local _, added, removed = update_viewports(true)
|
|
|
|
local resized = {}
|
|
|
|
-- Try to detect viewports being replaced or resized.
|
|
for k2, viewport in ipairs(removed) do
|
|
local candidate, best_size, best_idx = {}, 0, nil
|
|
|
|
for k, viewport2 in ipairs(added) do
|
|
local int = grect.get_intersection(viewport.geometry, viewport2.geometry)
|
|
|
|
if (int.width*int.height) > best_size then
|
|
best_size, best_idx, candidate = (int.width*int.height), k, viewport2
|
|
end
|
|
end
|
|
|
|
if candidate and best_size > 0 then
|
|
table.insert(resized, {removed[k2], added[best_idx]})
|
|
removed[k2] = nil
|
|
table.remove(added , best_idx)
|
|
end
|
|
end
|
|
|
|
gtable.from_sparse(removed)
|
|
|
|
-- Drop the cache.
|
|
for s in capi.screen do
|
|
s.data.dpi_cache = nil
|
|
end
|
|
|
|
capi.screen.emit_signal("property::viewports", ascreen._get_viewports())
|
|
|
|
-- First, ask for screens for these new viewport.
|
|
for _, viewport in ipairs(added) do
|
|
capi.screen.emit_signal("request::create", viewport, {context="viewports_changed"})
|
|
end
|
|
|
|
-- Before removing anything, make sure to resize existing screen as it may
|
|
-- affect where clients will go when the screens are removed.
|
|
for _, p in ipairs(resized) do
|
|
capi.screen.emit_signal("request::resize", p[1], p[2], {context="viewports_changed"})
|
|
end
|
|
|
|
-- Remove the now out-of-view screens.
|
|
for _, viewport in ipairs(removed) do
|
|
capi.screen.emit_signal("request::remove", viewport, {context="viewports_changed"})
|
|
end
|
|
|
|
end)
|
|
|
|
-- Add the DPI related properties
|
|
return function(screen, d)
|
|
ascreen, data = screen, d
|
|
|
|
-- "Lua" copy of the CAPI viewports. It has more metadata.
|
|
ascreen._viewports = {}
|
|
gtable.crush(ascreen, module, true)
|
|
|
|
ascreen.object.set_dpi = set_dpi
|
|
ascreen.object.get_dpi = get_dpi
|
|
|
|
for _, prop in ipairs {"minimum_dpi" , "maximum_dpi" ,
|
|
"mm_maximum_width" , "mm_minimum_width" ,
|
|
"inch_maximum_width", "inch_minimum_width",
|
|
"preferred_dpi" } do
|
|
|
|
screen.object["get_"..prop] = function(s)
|
|
if not s.data.viewport then
|
|
update_screen_viewport(s)
|
|
end
|
|
|
|
local a = s.data.viewport or {}
|
|
|
|
return a[prop] or nil
|
|
end
|
|
end
|
|
end
|