radical/placement.lua

376 lines
13 KiB
Lua

local capi = {screen=screen, mouse = mouse}
local unpack = unpack or table.unpack
local mouse = require( "awful.mouse" )
local screen = require( "awful.screen" )
local cairo = require( "lgi" ).cairo
local module = {}
-- Compute the new `x` and `y`.
-- The workarea position need to be applied by the caller
local map = {
top_left = function(sw, sh, dw, dh) return {x=0 , y=0 } end,
top_right = function(sw, sh, dw, dh) return {x=sw-dw , y=0 } end,
bottom_left = function(sw, sh, dw, dh) return {x=0 , y=sh-dh } end,
bottom_right = function(sw, sh, dw, dh) return {x=sw-dw , y=sh-dh } end,
left = function(sw, sh, dw, dh) return {x=0 , y=sh/2-dh/2} end,
right = function(sw, sh, dw, dh) return {x=sw-dw , y=sh/2-dh/2} end,
top = function(sw, sh, dw, dh) return {x=sw/2-dw/2, y=0 } end,
bottom = function(sw, sh, dw, dh) return {x=sw/2-dw/2, y=sh-dh } end,
centered = function(sw, sh, dw, dh) return {x=sw/2-dw/2, y=sh/2-dh/2} end,
center_vertical = function(sw, sh, dw, dh) return {x= nil , y=sh-dh } end,
center_horizontal = function(sw, sh, dw, dh) return {x=sw/2-dw/2, y= nil } end,
}
-- Store function -> keys
local reverse_map = {}
-- Create the geometry rectangle 1=best case, 2=fallback
local positions = {
left1 = function(r, w, h) return {x=r.x-w , y=r.y } end,
left2 = function(r, w, h) return {x=r.x-w , y=r.y-h+r.height } end,
right1 = function(r, w, h) return {x=r.x , y=r.y } end,
right2 = function(r, w, h) return {x=r.x , y=r.y-h+r.height } end,
top1 = function(r, w, h) return {x=r.x , y=r.y-h } end,
top2 = function(r, w, h) return {x=r.x-w+r.width, y=r.y-h } end,
bottom1 = function(r, w, h) return {x=r.x , y=r.y } end,
bottom2 = function(r, w, h) return {x=r.x-w+r.width, y=r.y } end,
}
-- Check if the proposed geometry fit in the screen
local function fit_in_screen(s, geo)
local sgeo = capi.screen[s].geometry
local region = cairo.Region.create_rectangle(cairo.RectangleInt(sgeo))
region:intersect(cairo.Region.create_rectangle(
cairo.RectangleInt(geo)
))
local geo2 = region:get_rectangle(0)
-- If the geometry is the same, then it fit, else, it will be cropped
--TODO in case all directions are cropped, keep the least cropped one
return geo2.width == geo.width and geo2.height == geo.height
end
--- Move the drawable (client or wibox) `d` to a screen position or side.
--
-- Supported positions are:
--
-- * top_left
-- * top_right
-- * bottom_left
-- * bottom_right
-- * left
-- * right
-- * top
-- * bottom
-- * centered
-- * center_vertical
-- * center_horizontal
--
-- The valid other arguments are:
--
-- * *honor_workarea*: Take workarea into account when placing the drawable (default: false)
--
-- @param drawable A drawable (like `client` or `wibox`)
-- @tparam string position One of the position mentionned above
-- @param[opt=d.screen or capi.mouse.screen] parent The parent geometry
-- @tparam[opt={}] table args Other arguments
function module.align(drawable, position, parent, args)
args = args or {}
parent = parent or drawable.screen or capi.mouse.screen
-- Get the parent geometry
local parent_type = type(parent)
local sgeo = (parent_type == "screen" or parent_type == "number") and
capi.screen[parent][args.honor_workarea and "workarea" or "geometry"] or
parent:geometry()
local dgeo = drawable:geometry()
local pos = map[position](sgeo.width, sgeo.height, dgeo.width, dgeo.height)
drawable : geometry {
x = pos.x and math.ceil(sgeo.x + pos.x) or dgeo.x,
y = pos.y and math.ceil(sgeo.y + pos.y) or dgeo.y,
width = math.ceil(dgeo.width ) ,
height = math.ceil(dgeo.height ) ,
}
end
-- Create many placement functions
for k,v in pairs(map) do
module[k] = function(d, p, args)
module.align(d, k, p, args)
end
reverse_map[module[k]] = k
end
--- Pin a drawable to a placement function.
-- Automatically update the position when the size change.
-- All other arguments will be passed to the `position` function (if any)
-- @param drawable A drawable (like `client` or `wibox`)
-- @param position A position name (see `align`) or a position function
function module.attach(drawable, position, ...)
if type(position) == "string" then
position = module[position]
end
if not position then return end
local args = {...}
local function tracker()
position(drawable, unpack(args))
end
drawable:connect_signal("property::width" , tracker)
drawable:connect_signal("property::height", tracker)
tracker()
end
-- Update the workarea
local function wibox_update_strut(d, position)
-- If the drawable isn't visible, remove the struts
if not d.visible then
d:struts { left = 0, right = 0, bottom = 0, top = 0 }
return
end
-- Detect horizontal or vertical drawables
local geo = d:geometry()
local vertical = geo.width < geo.height
-- Look into the `position` string to find the relevants sides to crop from
-- the workarea
local struts = { left = 0, right = 0, bottom = 0, top = 0 }
if vertical then
for k, v in ipairs {"right", "left"} do
if (not position) or position:match(v) then
struts[v] = geo.width + 2 * d.border_width
end
end
else
for k, v in ipairs {"top", "bottom"} do
if (not position) or position:match(v) then
struts[v] = geo.height + 2 * d.border_width
end
end
end
-- Update the workarea
d:struts(struts)
end
function module.attach_struts(d, f, ...)
module.attach(d, f, ...)
--TODO if there is multiple attach_struts, update them, see `raise_attached_struts`
local function tracker()
wibox_update_strut(d, reverse_map[f])
end
d:connect_signal("property::geometry" , tracker)
d:connect_signal("property::visible" , tracker)
tracker()
end
--- Move a drawable to the "top priority" of attached_structs
function module.raise_attached_struts()
end
-- Create a pair of rectangles used to set the relative areas.
-- v=vertical, h=horizontal
local function get_cross_sections(abs_geo, mode)
if not mode or mode == "cursor" then
-- A 1px cross section centered around the mouse position
local coords = capi.mouse.coords()
return {
h = {
x = abs_geo.drawable_geo.x ,
y = coords.y ,
width = abs_geo.drawable_geo.width ,
height = 1 ,
},
v = {
x = coords.x ,
y = abs_geo.drawable_geo.y ,
width = 1 ,
height = abs_geo.drawable_geo.height,
}
}
elseif mode == "widget" then
-- The widget geometry extended to reach the end of the drawable
return {
h = {
x = abs_geo.drawable_geo.x ,
y = abs_geo.y ,
width = abs_geo.drawable_geo.width ,
height = abs_geo.height ,
},
v = {
x = abs_geo.x ,
y = abs_geo.drawable_geo.y ,
width = abs_geo.width ,
height = abs_geo.drawable_geo.height,
}
}
elseif mode == "cursor_inside" then
-- A 1x1 rectangle centered around the mouse position
local coords = capi.mouse.coords()
coords.width,coords.height = 1,1
return {h=coords, v=coords}
elseif mode == "widget_inside" then
-- The widget absolute geometry, unchanged
return {h=abs_geo, v=abs_geo}
end
assert(false)
end
--- Get the possible 2D anchor points around a widget geometry.
-- This take into account the widget drawable (wibox) and try to avoid
-- overlapping.
--
-- Valid arguments are:
--
-- * xoffset
-- * yoffset
-- * margins: A table with "left", "right", "top" and "bottom" as key or a number
--
-- @tparam table geo A geometry table with optional "drawable" member
-- @tparam[opt="widget"] string mode TODO document
-- @tparam[opt={}] table args
function module.get_relative_points(geo, mode, args) --TODO rename regions
mode = mode or "widget"
args = args or {}
-- Use the mouse position and the wibox/client under it
if not geo then
local draw = mouse.drawin_under_pointer()
geo = draw and draw:geometry() or capi.mouse.coords()
geo.drawable = draw
elseif (not geo.drawable) and geo.x and geo.width then
local coords = capi.mouse.coords()
-- Check id the mouse is in the rect
if coords.x > geo.x and coords.x < geo.x+geo.width and
coords.y > geo.y and coords.y < geo.y+geo.height then
geo.drawable = mouse.drawin_under_pointer()
end
end
-- Get the drawable geometry
local dpos = geo.drawable and geo.drawable.drawable:geometry() or {x=0, y=0}
-- Compute the absolute widget geometry
local abs_widget_geo = {
x = dpos.x + geo.x ,
y = dpos.y + geo.y ,
width = geo.width ,
height = geo.height ,
drawable = geo.drawable ,
drawable_geo = geo.drawable and dpos or geo,
}
-- Get the comparaison point
local center_point = mode:match("cursor") and capi.mouse.coords() or {
x = abs_widget_geo.x + abs_widget_geo.width / 2,
y = abs_widget_geo.y + abs_widget_geo.height / 2,
}
-- Get widget regions for both axis
local cs = get_cross_sections(abs_widget_geo, mode)
-- Set the offset
local xoff, yoff = args.xoffset or 0, args.yoffset or 0 --TODO add margins
-- Get the 4 closest points from `center_point` around the wibox
local regions = {
left = {x = xoff+cs.h.x , y = yoff+cs.h.y },
right = {x = xoff+cs.h.x+cs.h.width, y = yoff+cs.h.y },
top = {x = xoff+cs.v.x , y = yoff+cs.v.y },
bottom = {x = xoff+cs.v.x , y = yoff+cs.v.y+cs.v.height},
}
-- Assume the section is part of a single screen until someone complain.
-- It is much faster to compute and getting it wrong probably have no side
-- effects.
local s = geo.drawable and geo.drawable.screen or screen.getbycoord(
center_point.x,
center_point.y
)
-- Compute the distance (dp) between the `center_point` and the sides.
-- This is only relevant for "cursor" and "cursor_inside" modes.
for k, v in pairs(regions) do
local dx, dy = v.x - center_point.x, v.y - center_point.y
v.distance = math.sqrt(dx*dx + dy*dy)
v.width = cs.v.width
v.height = cs.h.height
v.screen = s
end
return regions
end
-- @tparam drawable d A wibox or client
-- @tparam table regions A table with position as key and regions (x,y,w,h) as value
-- @tparam[opt={}] table preferred_positions The preferred positions (position as key,
-- and index as value)
-- @treturn string The choosen position
function module.move_relative(d, regions, preferred_positions) --TODO inside/outside
local w,h = d.width, d.height
local pref_idx, pref_name = 99, nil
local does_fit = {}
for k,v in pairs(regions) do
local geo = positions[k..1](v, w, h)
geo.width, geo.height = w, h
local fit = fit_in_screen(v.screen, geo)
-- Try the other compatible geometry
if not fit then
geo = positions[k..2](v, w, h)
geo.width, geo.height = w, h
fit = fit_in_screen(v.screen, geo)
end
does_fit[k] = fit and geo or nil
if fit and preferred_positions[k] and preferred_positions[k] < pref_idx then
pref_idx = preferred_positions[k]
pref_name = k
end
-- No need to continue
if fit and preferred_positions[k] == 1 then break end
end
local pos_name = pref_name or next(does_fit)
local pos = does_fit[pos_name]
if pos then
d.x = math.ceil(pos.x)
d.y = math.ceil(pos.y)
end
return pos_name
end
return module