radical/placement.lua

534 lines
18 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 tag = require( "awful.tag" ) --TODO do the opposite, include placement in awful.tag
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 }, "down" end,
left2 = function(r, w, h) return {x=r.x-w , y=r.y-h+r.height }, "up" end,
right1 = function(r, w, h) return {x=r.x , y=r.y }, "down" end,
right2 = function(r, w, h) return {x=r.x , y=r.y-h+r.height }, "up" end,
top1 = function(r, w, h) return {x=r.x , y=r.y-h }, "right" end,
top2 = function(r, w, h) return {x=r.x-w+r.width, y=r.y-h }, "left" end,
bottom1 = function(r, w, h) return {x=r.x , y=r.y }, "right" end,
bottom2 = function(r, w, h) return {x=r.x-w+r.width, y=r.y }, "left" end,
}
-- Check if the proposed geometry fit in the screen
local function fit_in_screen(s, geo) --TODO replace by fit_in_bounding
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
local function apply_geometry_ajustments(geo, delta)
return {
x = geo.x + (delta.left or 0),
y = geo.y + (delta.top or 0),
width = geo.width - (delta.left or 0) - (delta.right or 0),
height = geo.height - (delta.top or 0) - (delta.bottom or 0),
}
end
--- Get a placement bounding geometry.
-- This method offer a flexible way to build a customized outer geometry used
-- by the various functions of this module.
--
-- Valid arguments are:
--
-- * honor_padding
-- * honor_workarea
-- * margins
-- * tag
-- * parent: A parent drawable to use a base geometry
-- * bounding_rect: A bounding rectangle
--
-- @tparam[opt=mouse.screen] screen s A screen
-- @tparam[opt={}] table args The arguments
function module.get_bounding_geometry(s, args)
args = args or {}
-- If the tag has a geometry, assume it is right
if args.tag then
local geo = tag.getproperty(args.tag, "geometry")
if geo then
return geo
end
end
s = s or capi.mouse.screen
local geo = args.bounding_rect or (args.parent and parent:geometry()) or
capi.screen[s][args.honor_workarea and "workarea" or "geometry"]
if (not args.parent) and (not args.bounding_rect) and args.honor_padding then
local padding = screen.padding(s) or {}
geo = apply_geometry_ajustments(geo, padding)
end
if args.margins then
geo = apply_geometry_ajustments(geo,
type(args.margins) == "table" and args.margins or {
left = args.margins, right = args.margins,
top = args.margins, bottom = args.margins,
}
)
end
return geo
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)
-- * *honor_padding*: Take the screen padding into account (see `awful.screen.padding`)
-- * *tag*: Use a tag geometry
-- * *margins*: A table with left, right, top, bottom keys or a number
-- * *parent*: A parent drawable to use a base geometry
-- * *bounding_rect*: A bounding rectangle
--
-- @tparam drawable d 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(d, position, parent, args)
d = d or capi.client.focus
if not d then return end
args = args or {}
parent = parent or d.screen or capi.mouse.screen
-- Get the parent geometry
local parent_type = type(parent)
local sgeo = (parent_type == "screen" or parent_type == "number") and
module.get_bounding_geometry(parent, args) or parent:geometry()
local dgeo = d:geometry()
local pos = map[position](sgeo.width, sgeo.height, dgeo.width, dgeo.height)
d : 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
-- Add the alias 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
--- Stretch a drawable in a specific direction.
-- Valid args:
--
-- * *preserve_ratio*:
-- * *margins*: A margin value or table
-- * *honor_workarea*:
-- * *honor_padding*:
-- * *tag*: Use a tag geometry, this honor the workarea, padding and gaps
-- * *parent*: A parent drawable to use a base geometry
-- * *bounding_rect*: A bounding rectangle
-- * minimim_height:
-- * minimim_width:
--
-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`)
-- @tparam string direction The stretch direction (left, right, up, down)
-- @tparam[opt={}] table args The arguments
function module.stretch(d, direction, args)
d = d or capi.client.focus
if not d then return end
--TODO maybe this could be integrated with the resize matrix?
local sgeo = module.get_bounding_geometry(d.screen, args)
local dgeo = d:geometry()
local ngeo = dgeo
if direction == "left" then
ngeo.x = sgeo.x
ngeo.width = (sgeo.width - sgeo.x) - (ngeo.x + ngeo.width)
elseif direction == "right" then
ngeo.width = sgeo.width - ngeo.x
elseif direction == "up" then
ngeo.y = sgeo.y
ngeo.height = (sgeo.height - sgeo.y) - (ngeo.y + ngeo.height)
elseif direction == "down" then
ngeo.height = sgeo.height - dgeo.y
end
-- Avoid negative sizes
ngeo.width = math.max(args.minimim_width or 1, ngeo.width )
ngeo.height = math.max(args.minimim_height or 1, ngeo.height)
end
-- Add the alias functions
for k,v in ipairs {"left", "right", "up", "down"} do
module["stretch_"..v] = function(d, args) module.stretch(d, v, args) end
end
--- Maximize a drawable horizontally, vertically or both.
-- Valid args:
--
-- * *preserve_ratio*:
-- * *margins*: A margin value or table
-- * *honor_workarea*:
-- * *honor_padding*:
-- * *tag*: Use a tag geometry, this honor the workarea, padding and gaps
-- * *parent*: A parent drawable to use a base geometry
-- * *bounding_rect*: A bounding rectangle
--
-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`)
-- @tparam string axis The axis (vertically or horizontally)
-- @tparam[opt={}] table args The arguments
function module.maximize(d, axis, args)
d = d or capi.client.focus
if not d then return end
local sgeo = module.get_bounding_geometry(d.screen, args)
local dgeo = d:geometry()
local ngeo = dgeo
if (not axis) or axis:match("vertical") then
ngeo.y = sgeo.y
ngeo.height = sgeo.height
end
if (not axis) or axis:match("horizontal") then
ngeo.x = sgeo.x
ngeo.width = sgeo.width
end
d:geometry(ngeo)
end
-- Add the alias functions
for k, v in ipairs {"vertically", "horizontally"} do
module["maximize_"..v] = function(d, args) module.maximize(d, v, args) end
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)
-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`)
-- @param position A position name (see `align`) or a position function
function module.attach(d, position, ...)
d = d or capi.client.focus
if not d then return end
if type(position) == "string" then
position = module[position]
end
if not position then return end
local args = {...}
local function tracker()
position(d, unpack(args))
end
d:connect_signal("property::width" , tracker)
d: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
--- Move a drawable to a relative position next to another one.
-- @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
-- @treturn string The choosen direction
function module.move_relative(d, regions, preferred_positions) --TODO inside/outside, replace by args, allow modes
--args.geo, args.mode, args.drawable, args.regions, args.preferred_positions
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, dir = 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, dir = 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, dir} 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, dir = unpack(does_fit[pos_name])
if pos then
d.x = math.ceil(pos.x)
d.y = math.ceil(pos.y)
end
return pos_name, dir
end
return module