awesome/lib/awful/placement.lua
Sergey Vlasov db11c7e9da awful.placement: Fix client size corruption in no_overlap
The awful.placement.no_overlap function was adding the window border
width to the client width and height (this is performed in
area_common(), which is called by geometry_common()), but did not
reverse this operation by calling remove_border() before returning the
final geometry; because of this, using no_overlap resulted in increasing
the window width and height by 2*border_width.

The bug was probably introduced in commit ebcc19844e (before
that commit no_overlap changed the window position directly instead of
relying on the new placement infrastructure), but was not noticed
because of other problems (e.g., in the default configuration the result
of no_overlap was overridden by the buggy no_offscreen).

Signed-off-by: Sergey Vlasov <sigprof@gmail.com>
2018-09-13 08:23:36 +03:00

1476 lines
49 KiB
Lua

---------------------------------------------------------------------------
--- Algorithms used to place various drawables.
--
-- The functions provided by this module all follow the same arguments
-- conventions. This allow:
--
-- * To use them in various other module as
-- [visitor objects](https://en.wikipedia.org/wiki/Visitor_pattern)
-- * Turn each function into an API with various common customization parameters.
-- * Re-use the same functions for the `mouse`, `client`s, `screen`s and `wibox`es
--
--
-- <h3>Compositing</h3>
--
-- It is possible to compose placement function using the `+` or `*` operator:
--
--@DOC_awful_placement_compose_EXAMPLE@
--
--@DOC_awful_placement_compose2_EXAMPLE@
--
-- <h3>Common arguments</h3>
--
-- **pretend** (*boolean*):
--
-- Do not apply the new geometry. This is useful if only the return values is
-- necessary.
--
-- **honor_workarea** (*boolean*):
--
-- Take workarea into account when placing the drawable (default: false)
--
-- **honor_padding** (*boolean*):
--
-- Take the screen padding into account (see `screen.padding`)
--
-- **tag** (*tag*):
--
-- Use a tag geometry
--
-- **margins** (*number* or *table*):
--
-- A table with left, right, top, bottom keys or a number
--
-- **parent** (client, wibox, mouse or screen):
--
-- A parent drawable to use a base geometry
--
-- **bounding_rect** (table):
--
-- A bounding rectangle
--
-- **attach** (*boolean*):
--
-- When the parent geometry (like the screen) changes, re-apply the placement
-- function. This will add a `detach_callback` function to the drawable. Call
-- this to detach the function. This will be called automatically when a new
-- attached function is set.
--
-- **offset** (*table or number*):
--
-- The offset(s) to apply to the new geometry.
--
-- **store_geometry** (*boolean*):
--
-- Keep a single history of each type of placement. It can be restored using
-- `awful.placement.restore` by setting the right `context` argument.
--
-- When either the parent or the screen geometry change, call the placement
-- function again.
--
-- **update_workarea** (*boolean*):
--
-- If *attach* is true, also update the screen workarea.
--
-- @author Emmanuel Lepage Vallee &lt;elv1313@gmail.com&gt;
-- @author Julien Danjou &lt;julien@danjou.info&gt;
-- @copyright 2008 Julien Danjou, Emmanuel Lepage Vallee 2016
-- @module awful.placement
---------------------------------------------------------------------------
-- Grab environment we need
local ipairs = ipairs
local pairs = pairs
local math = math
local table = table
local capi =
{
screen = screen,
mouse = mouse,
client = client
}
local client = require("awful.client")
local layout = require("awful.layout")
local a_screen = require("awful.screen")
local grect = require("gears.geometry").rectangle
local gdebug = require("gears.debug")
local gtable = require("gears.table")
local cairo = require( "lgi" ).cairo
local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1)
local function get_screen(s)
return s and capi.screen[s]
end
local wrap_client = nil
local placement
-- Store function -> keys
local reverse_align_map = {}
-- Forward declarations
local area_common
local wibox_update_strut
local attach
--- Allow multiple placement functions to be daisy chained.
-- This also allow the functions to be aware they are being chained and act
-- upon the previous nodes results to avoid unnecessary processing or deduce
-- extra paramaters/arguments.
local function compose(...)
local queue = {}
local nodes = {...}
-- Allow placement.foo + (var == 42 and placement.bar)
if not nodes[2] then
return nodes[1]
end
-- nodes[1] == self, nodes[2] == other
for _, w in ipairs(nodes) do
-- Build an execution queue
if w.context and w.context == "compose" then
for _, elem in ipairs(w.queue or {}) do
table.insert(queue, elem)
end
else
table.insert(queue, w)
end
end
local ret
ret = wrap_client(function(d, args, ...)
local rets = {}
local last_geo = nil
-- As some functions may have to take into account results from
-- previously execued ones, add the `composition_results` hint.
args = setmetatable({composition_results=rets}, {__index=args})
-- Only apply the geometry once, not once per chain node, to do this,
-- Force the "pretend" argument and restore the original value for
-- the last node.
local attach_real = args.attach
args.pretend = true
args.attach = false
args.offset = {}
for k, f in ipairs(queue) do
if k == #queue then
-- Let them fallback to the parent table
args.pretend = nil
args.offset = nil
end
local r = {f(d, args, ...)}
last_geo = r[1] or last_geo
args.override_geometry = last_geo
-- Keep the return value, store one per context
if f.context then
-- When 2 composition queue are executed, merge the return values
if f.context == "compose" then
for k2,v in pairs(r) do
rets[k2] = v
end
else
rets[f.context] = r
end
end
end
if attach_real then
args.attach = true
attach(d, ret, args)
end
return last_geo, rets
end, "compose")
ret.queue = queue
return ret
end
wrap_client = function(f, context)
return setmetatable(
{
is_placement= true,
context = context,
},
{
__call = function(_,...) return f(...) end,
__add = compose, -- Composition is usually defined as +
__mul = compose -- Make sense if you think of the functions as matrices
}
)
end
local placement_private = {}
-- The module is a proxy in front of the "real" functions.
-- This allow syntax like:
--
-- (awful.placement.no_overlap + awful.placement.no_offscreen)(c)
--
placement = setmetatable({}, {
__index = placement_private,
__newindex = function(_, k, f)
placement_private[k] = wrap_client(f, k)
end
})
-- 3x3 matrix of the valid sides and corners
local corners3x3 = {{"top_left" , "top" , "top_right" },
{"left" , nil , "right" },
{"bottom_left", "bottom" , "bottom_right"}}
-- 2x2 matrix of the valid sides and corners
local corners2x2 = {{"top_left" , "top_right" },
{"bottom_left", "bottom_right"}}
-- Compute the new `x` and `y`.
-- The workarea position need to be applied by the caller
local align_map = {
top_left = function(_ , _ , _ , _ ) return {x=0 , y=0 } end,
top_right = function(sw, _ , dw, _ ) return {x=sw-dw , y=0 } end,
bottom_left = function(_ , sh, _ , 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(_ , sh, _ , 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, _ , dw, _ ) 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(_ , sh, _ , dh) return {x= nil , y=sh-dh } end,
center_horizontal = function(sw, _ , dw, _ ) return {x=sw/2-dw/2, y= nil } end,
}
-- Some parameters to correctly compute the final size
local resize_to_point_map = {
-- Corners
top_left = {p1= nil , p2={1,1}, x_only=false, y_only=false, align="bottom_right"},
top_right = {p1={0,1} , p2= nil , x_only=false, y_only=false, align="bottom_left" },
bottom_left = {p1= nil , p2={1,0}, x_only=false, y_only=false, align="top_right" },
bottom_right = {p1={0,0} , p2= nil , x_only=false, y_only=false, align="top_left" },
-- Sides
left = {p1= nil , p2={1,1}, x_only=true , y_only=false, align="top_right" },
right = {p1={0,0} , p2= nil , x_only=true , y_only=false, align="top_left" },
top = {p1= nil , p2={1,1}, x_only=false, y_only=true , align="bottom_left" },
bottom = {p1={0,0} , p2= nil , x_only=false, y_only=true , align="top_left" },
}
-- Outer position matrix
-- 1=best case, 2=fallback
local outer_positions = {
left1 = function(r, w, _) 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, _, _) return {x=r.x , y=r.y }, "down" end,
right2 = function(r, _, h) return {x=r.x , y=r.y-h+r.height }, "up" end,
top1 = function(r, _, 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, _, _) return {x=r.x , y=r.y }, "right" end,
bottom2 = function(r, w, _) return {x=r.x-w+r.width, y=r.y }, "left" end,
}
--- Add a context to the arguments.
-- This function extend the argument table. The context is used by some
-- internal helper methods. If there already is a context, it has priority and
-- is kept.
local function add_context(args, context)
return setmetatable({context = (args or {}).context or context }, {__index=args})
end
local data = setmetatable({}, { __mode = 'k' })
--- Store a drawable geometry (per context) in a weak table.
-- @param d The drawin
-- @tparam string reqtype The context.
local function store_geometry(d, reqtype)
if not data[d] then data[d] = {} end
if not data[d][reqtype] then data[d][reqtype] = {} end
data[d][reqtype] = d:geometry()
data[d][reqtype].screen = d.screen
data[d][reqtype].sgeo = d.screen and d.screen.geometry or nil
data[d][reqtype].border_width = d.border_width
end
--- Get the margins and offset
-- @tparam table args The arguments
-- @treturn table The margins
-- @treturn table The offsets
local function get_decoration(args)
local offset = args.offset
-- Offset are "blind" values added to the output
offset = type(offset) == "number" and {
x = offset,
y = offset,
width = offset,
height = offset,
} or args.offset or {}
-- Margins are distances on each side to substract from the area`
local m = type(args.margins) == "table" and args.margins or {
left = args.margins or 0 , right = args.margins or 0,
top = args.margins or 0 , bottom = args.margins or 0
}
return m, offset
end
--- Apply some modifications before applying the new geometry.
-- @tparam table new_geo The new geometry
-- @tparam table args The common arguments
-- @tparam boolean force Always ajust the geometry, even in pretent mode. This
-- should only be used when returning the final geometry as it would otherwise
-- mess the pipeline.
-- @treturn table|nil The new geometry
local function fix_new_geometry(new_geo, args, force)
if (args.pretend and not force) or not new_geo then return nil end
local m, offset = get_decoration(args)
return {
x = new_geo.x and (new_geo.x + (offset.x or 0) + (m.left or 0) ),
y = new_geo.y and (new_geo.y + (offset.y or 0) + (m.top or 0) ),
width = new_geo.width and math.max(
1, (new_geo.width + (offset.width or 0) - (m.left or 0) - (m.right or 0) )
),
height = new_geo.height and math.max(
1, (new_geo.height + (offset.height or 0) - (m.top or 0) - (m.bottom or 0) )
),
}
end
-- Get the area covered by a drawin.
-- @param d The drawin
-- @tparam[opt=nil] table new_geo A new geometry
-- @tparam[opt=false] boolean ignore_border_width Ignore the border
-- @tparam table args the method arguments
-- @treturn The drawin's area.
area_common = function(d, new_geo, ignore_border_width, args)
-- The C side expect no arguments, nil isn't valid
if new_geo and args.zap_border_width then
d.border_width = 0
end
local geometry = new_geo and d:geometry(new_geo) or d:geometry()
local border = ignore_border_width and 0 or d.border_width or 0
-- When using the placement composition along with the "pretend"
-- option, it is necessary to keep a "virtual" geometry.
if args and args.override_geometry then
geometry = gtable.clone(args.override_geometry)
end
geometry.width = geometry.width + 2 * border
geometry.height = geometry.height + 2 * border
return geometry
end
--- Get (and optionally set) an object geometry.
-- Some elements, such as `mouse` and `screen` don't have a `:geometry()`
-- methods.
-- @param obj An object
-- @tparam table args the method arguments
-- @tparam[opt=nil] table new_geo A new geometry to replace the existing one
-- @tparam[opt=false] boolean ignore_border_width Ignore the border
-- @treturn table A table with *x*, *y*, *width* and *height*.
local function geometry_common(obj, args, new_geo, ignore_border_width)
-- Store the current geometry in a singleton-memento
if args.store_geometry and new_geo and args.context then
store_geometry(obj, args.context)
end
-- It's a mouse
if obj.coords then
local coords = fix_new_geometry(new_geo, args)
and obj.coords(new_geo) or obj.coords()
return {x=coords.x, y=coords.y, width=0, height=0}
elseif obj.geometry then
local geo = obj.geometry
-- It is either a drawable or something that implement its API
if type(geo) == "function" then
local dgeo = area_common(
obj, fix_new_geometry(new_geo, args), ignore_border_width, args
)
-- Apply the margins
if args.margins then
local delta = get_decoration(args)
return {
x = dgeo.x - (delta.left or 0),
y = dgeo.y - (delta.top or 0),
width = dgeo.width + (delta.left or 0) + (delta.right or 0),
height = dgeo.height + (delta.top or 0) + (delta.bottom or 0),
}
end
return dgeo
end
-- It is a screen, it doesn't support setting new sizes.
return obj:get_bounding_geometry(args)
else
assert(false, "Invalid object")
end
end
--- Get the parent geometry from the standardized arguments API shared by all
-- `awful.placement` methods.
-- @param obj A screen or a drawable
-- @tparam table args the method arguments
-- @treturn table A table with *x*, *y*, *width* and *height*.
local function get_parent_geometry(obj, args)
-- Didable override_geometry, context and other to avoid mutating the state
-- or using the wrong geo.
if args.bounding_rect then
return args.bounding_rect
elseif args.parent then
return geometry_common(args.parent, {})
elseif obj.screen then
return geometry_common(obj.screen, {
honor_padding = args.honor_padding,
honor_workarea = args.honor_workarea
})
else
return geometry_common(capi.screen[capi.mouse.screen], args)
end
end
--- Move a point into an area.
-- This doesn't change the *width* and *height* values, allowing the target
-- area to be smaller than the source one.
-- @tparam table source The (larger) geometry to move `target` into
-- @tparam table target The area to move into `source`
-- @treturn table A table with *x* and *y* keys
local function move_into_geometry(source, target)
local ret = {x = target.x, y = target.y}
-- Horizontally
if ret.x < source.x then
ret.x = source.x
elseif ret.x > source.x + source.width then
ret.x = source.x + source.width - 1
end
-- Vertically
if ret.y < source.y then
ret.y = source.y
elseif ret.y > source.y + source.height then
ret.y = source.y + source.height - 1
end
return ret
end
-- Update the workarea
wibox_update_strut = function(d, position, args)
-- 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 = area_common(d)
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 }
local m = get_decoration(args)
if vertical then
for _, v in ipairs {"right", "left"} do
if (not position) or position:match(v) then
struts[v] = geo.width + m[v]
end
end
else
for _, v in ipairs {"top", "bottom"} do
if (not position) or position:match(v) then
struts[v] = geo.height + m[v]
end
end
end
-- Update the workarea
d:struts(struts)
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`, `mouse`
-- or `wibox`)
-- @param position_f A position name (see `align`) or a position function
-- @tparam[opt={}] table args Other arguments
attach = function(d, position_f, args)
args = args or {}
if args.pretend then return end
if not args.attach then return end
-- Avoid a connection loop
args = setmetatable({attach=false}, {__index=args})
d = d or capi.client.focus
if not d then return end
if type(position_f) == "string" then
position_f = placement[position_f]
end
if not position_f then return end
-- If there is multiple attached function, there is an high risk of infinite
-- loop. While some combinaisons are harmless, other are very hard to debug.
--
-- Use the placement composition to build explicit multi step attached
-- placement functions.
if d.detach_callback then
d.detach_callback()
d.detach_callback = nil
end
local function tracker()
position_f(d, args)
end
d:connect_signal("property::width" , tracker)
d:connect_signal("property::height" , tracker)
d:connect_signal("property::border_width", tracker)
local function tracker_struts()
--TODO this is too fragile and doesn't work with all methods.
wibox_update_strut(d, d.position or reverse_align_map[position_f], args)
end
local parent = args.parent or d.screen
if args.update_workarea then
d:connect_signal("property::geometry" , tracker_struts)
d:connect_signal("property::visible" , tracker_struts)
capi.client.connect_signal("property::struts", tracker_struts)
tracker_struts()
elseif parent == d.screen then
if args.honor_workarea then
parent:connect_signal("property::workarea", tracker)
end
if args.honor_padding then
parent:connect_signal("property::padding", tracker)
end
end
-- If there is a parent drawable, screen, also track it.
-- Note that tracking the mouse is not supported
if parent and parent.connect_signal then
parent:connect_signal("property::geometry" , tracker)
end
-- Create a way to detach a placement function
function d.detach_callback()
d:disconnect_signal("property::width" , tracker)
d:disconnect_signal("property::height" , tracker)
d:disconnect_signal("property::border_width", tracker)
if parent then
parent:disconnect_signal("property::geometry" , tracker)
if parent == d.screen then
if args.honor_workarea then
parent:disconnect_signal("property::workarea", tracker)
end
if args.honor_padding then
parent:disconnect_signal("property::padding", tracker)
end
end
end
if args.update_workarea then
d:disconnect_signal("property::geometry" , tracker_struts)
d:disconnect_signal("property::visible" , tracker_struts)
capi.client.disconnect_signal("property::struts", tracker_struts)
end
end
end
-- Convert 2 points into a rectangle
local function rect_from_points(p1x, p1y, p2x, p2y)
return {
x = p1x,
y = p1y,
width = p2x - p1x,
height = p2y - p1y,
}
end
-- Convert a rectangle and matrix info into a point
local function rect_to_point(rect, corner_i, corner_j)
return {
x = rect.x + corner_i * math.floor(rect.width ),
y = rect.y + corner_j * math.floor(rect.height),
}
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 == "geometry" 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 == "geometry_inside" then
-- The widget absolute geometry, unchanged
return {h=abs_geo, v=abs_geo}
end
end
-- When a rectangle is embedded into a bigger one, get the regions around
-- the outline of the bigger rectangle closest to the smaller one (on each side)
local function get_relative_regions(geo, mode, is_absolute)
-- Use the mouse position and the wibox/client under it
if not geo then
local draw = capi.mouse.current_wibox
geo = draw and draw:geometry() or capi.mouse.coords()
geo.drawable = draw
elseif is_absolute then
-- Some signals are a bit inconsistent in their arguments convention.
-- This little hack tries to mitigate the issue.
geo.drawable = geo -- is a wibox or client, geometry and object are one
-- and the same.
elseif (not geo.drawable) and geo.x and geo.width then
local coords = capi.mouse.coords()
-- Check if 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 = capi.mouse.current_wibox
end
-- Maybe there is a client
if (not geo.drawable) and capi.mouse.current_client then
geo.drawable = capi.mouse.current_client
end
end
-- Get the drawable geometry
local dpos = geo.drawable and (
geo.drawable.drawable and
geo.drawable.drawable:geometry()
or geo.drawable:geometry()
) or {x=0, y=0}
-- Compute the absolute widget geometry
local abs_widget_geo = is_absolute and geo or {
x = dpos.x + geo.x ,
y = dpos.y + geo.y ,
width = geo.width ,
height = geo.height ,
drawable = geo.drawable ,
}
abs_widget_geo.drawable_geo = geo.drawable and dpos or geo
-- Get the point for comparison.
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)
-- Get the 4 closest points from `center_point` around the wibox
local regions = {
left = {x = cs.h.x , y = cs.h.y },
right = {x = cs.h.x+cs.h.width, y = cs.h.y },
top = {x = cs.v.x , y = cs.v.y },
bottom = {x = cs.v.x , y = cs.v.y+cs.v.height},
}
-- Assume the section is part of a single screen until someone complains.
-- It is much faster to compute and getting it wrong probably has no side
-- effects.
local s = geo.drawable and geo.drawable.screen or a_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 _, 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 = capi.screen[s]
end
return regions
end
-- Check if the proposed geometry fits the screen
local function fit_in_bounding(obj, geo, args)
local sgeo = get_parent_geometry(obj, args)
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 fits, otherwise it will be cropped.
return geo2.width == geo.width and geo2.height == geo.height
end
-- Remove border from drawable geometry
local function remove_border(drawable, args, geo)
local bw = (not args.ignore_border_width) and drawable.border_width or 0
geo.width = geo.width - 2*bw
geo.height = geo.height - 2*bw
end
--- Move a drawable to the closest corner of the parent geometry (such as the
-- screen).
--
-- Valid arguments include the common ones and:
--
-- * **include_sides**: Also include the left, right, top and bottom positions
--
--@DOC_awful_placement_closest_mouse_EXAMPLE@
-- @tparam[opt=client.focus] drawable d A drawable (like `client`, `mouse`
-- or `wibox`)
-- @tparam[opt={}] table args The arguments
-- @treturn table The new geometry
-- @treturn string The corner name
function placement.closest_corner(d, args)
args = add_context(args, "closest_corner")
d = d or capi.client.focus
local sgeo = get_parent_geometry(d, args)
local dgeo = geometry_common(d, args)
local pos = move_into_geometry(sgeo, dgeo)
local corner_i, corner_j, n
-- Use the product of 3 to get the closest point in a NxN matrix
local function f(_n, mat)
n = _n
-- The +1 is required to avoid a rounding error when
-- pos.x == sgeo.x+sgeo.width
corner_i = -math.ceil( ( (sgeo.x - pos.x) * n) / (sgeo.width + 1))
corner_j = -math.ceil( ( (sgeo.y - pos.y) * n) / (sgeo.height + 1))
return mat[corner_j + 1][corner_i + 1]
end
-- Turn the area into a grid and snap to the cloest point. This size of the
-- grid will increase the accuracy. A 2x2 matrix only include the corners,
-- at 3x3, this include the sides too technically, a random size would work,
-- but without corner names.
local grid_size = args.include_sides and 3 or 2
-- If the point is in the center, use the closest corner
local corner = grid_size == 3 and f(3, corners3x3) or f(2, corners2x2)
-- Transpose the corner back to the original size
local new_args = setmetatable({position = corner}, {__index=args})
local ngeo = placement_private.align(d, new_args)
return fix_new_geometry(ngeo, args, true), corner
end
--- Place the client so no part of it will be outside the screen (workarea).
--@DOC_awful_placement_no_offscreen_EXAMPLE@
-- @client c The client.
-- @tparam[opt={}] table args The arguments
-- @tparam[opt=client's screen] integer args.screen The screen.
-- @treturn table The new client geometry.
function placement.no_offscreen(c, args)
--compatibility with the old API
if type(args) == "number" or type(args) == "screen" then
gdebug.deprecate(
"awful.placement.no_offscreen screen argument is deprecated"..
" use awful.placement.no_offscreen(c, {screen=...})",
{deprecated_in=5}
)
args = { screen = args }
end
c = c or capi.client.focus
args = add_context(args, "no_offscreen")
local geometry = area_common(c)
local screen = get_screen(args.screen or c.screen or a_screen.getbycoord(geometry.x, geometry.y))
local screen_geometry = screen.workarea
if geometry.x + geometry.width > screen_geometry.x + screen_geometry.width then
geometry.x = screen_geometry.x + screen_geometry.width - geometry.width
end
if geometry.x < screen_geometry.x then
geometry.x = screen_geometry.x
end
if geometry.y + geometry.height > screen_geometry.y + screen_geometry.height then
geometry.y = screen_geometry.y + screen_geometry.height - geometry.height
end
if geometry.y < screen_geometry.y then
geometry.y = screen_geometry.y
end
remove_border(c, args, geometry)
geometry_common(c, args, geometry)
return fix_new_geometry(geometry, args, true)
end
--- Place the client where there's place available with minimum overlap.
--@DOC_awful_placement_no_overlap_EXAMPLE@
-- @param c The client.
-- @tparam[opt={}] table args Other arguments
-- @treturn table The new geometry
function placement.no_overlap(c, args)
c = c or capi.client.focus
args = add_context(args, "no_overlap")
local geometry = geometry_common(c, args)
local screen = get_screen(c.screen or a_screen.getbycoord(geometry.x, geometry.y))
local cls = client.visible(screen)
local curlay = layout.get()
local areas = { screen.workarea }
for _, cl in pairs(cls) do
if cl ~= c
and cl.type ~= "desktop"
and (cl.floating or curlay == layout.suit.floating)
and not (cl.maximized or cl.fullscreen) then
areas = grect.area_remove(areas, area_common(cl))
end
end
-- Look for available space
local found = false
local new = { x = geometry.x, y = geometry.y, width = 0, height = 0 }
for _, r in ipairs(areas) do
if r.width >= geometry.width
and r.height >= geometry.height
and r.width * r.height > new.width * new.height then
found = true
new = r
-- Check if the client's current position is available
-- and prefer that one (why move it around pointlessly?)
if geometry.x >= r.x
and geometry.y >= r.y
and geometry.x + geometry.width <= r.x + r.width
and geometry.y + geometry.height <= r.y + r.height then
new.x = geometry.x
new.y = geometry.y
end
end
end
-- We did not find an area with enough space for our size:
-- just take the biggest available one and go in.
-- This makes sure to have the whole screen's area in case it has been
-- removed.
if not found then
if #areas > 0 then
for _, r in ipairs(areas) do
if r.width * r.height > new.width * new.height then
new = r
end
end
elseif grect.area_intersect_area(geometry, screen.workarea) then
new.x = geometry.x
new.y = geometry.y
else
new.x = screen.workarea.x
new.y = screen.workarea.y
end
end
-- Restore height and width
new.width = geometry.width
new.height = geometry.height
remove_border(c, args, new)
geometry_common(c, args, new)
return fix_new_geometry(new, args, true)
end
--- Place the client under the mouse.
--@DOC_awful_placement_under_mouse_EXAMPLE@
-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`)
-- @tparam[opt={}] table args Other arguments
-- @treturn table The new geometry
function placement.under_mouse(d, args)
args = add_context(args, "under_mouse")
d = d or capi.client.focus
local m_coords = capi.mouse.coords()
local ngeo = geometry_common(d, args)
ngeo.x = math.floor(m_coords.x - ngeo.width / 2)
ngeo.y = math.floor(m_coords.y - ngeo.height / 2)
remove_border(d, args, ngeo)
geometry_common(d, args, ngeo)
return fix_new_geometry(ngeo, args, true)
end
--- Place the client next to the mouse.
--
-- It will place `c` next to the mouse pointer, trying the following positions
-- in this order: right, left, above and below.
--@DOC_awful_placement_next_to_mouse_EXAMPLE@
-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`)
-- @tparam[opt={}] table args Other arguments
-- @treturn table The new geometry
function placement.next_to_mouse(d, args)
if type(args) == "number" then
gdebug.deprecate(
"awful.placement.next_to_mouse offset argument is deprecated"..
" use awful.placement.next_to_mouse(c, {offset={x=...}})",
{deprecated_in=4}
)
args = nil
end
local old_args = args or {}
args = add_context(args, "next_to_mouse")
d = d or capi.client.focus
local sgeo = get_parent_geometry(d, args)
args.pretend = true
args.parent = capi.mouse
local ngeo = placement.left(d, args)
if ngeo.x + ngeo.width > sgeo.x+sgeo.width then
ngeo = placement.right(d, args)
else
-- It is _next_ to mouse, not under_mouse
ngeo.x = ngeo.x+1
end
args.pretend = old_args.pretend
geometry_common(d, args, ngeo)
attach(d, placement.next_to_mouse, old_args)
return fix_new_geometry(ngeo, args, true)
end
--- Resize the drawable to the cursor.
--
-- Valid args:
--
-- * *axis*: The axis (vertical or horizontal). If none is
-- specified, then the drawable will be resized on both axis.
--
--@DOC_awful_placement_resize_to_mouse_EXAMPLE@
-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`)
-- @tparam[opt={}] table args Other arguments
-- @treturn table The new geometry
function placement.resize_to_mouse(d, args)
d = d or capi.client.focus
args = add_context(args, "resize_to_mouse")
local coords = capi.mouse.coords()
local ngeo = geometry_common(d, args)
local h_only = args.axis == "horizontal"
local v_only = args.axis == "vertical"
-- To support both growing and shrinking the drawable, it is necessary
-- to decide to use either "north or south" and "east or west" directions.
-- Otherwise, the result will always be 1x1
local _, closest_corner = placement.closest_corner(capi.mouse, {
parent = d,
pretend = true,
include_sides = args.include_sides or false,
})
-- Given "include_sides" wasn't set, it will always return a name
-- with the 2 axis. If only one axis is needed, adjust the result
if h_only then
closest_corner = closest_corner:match("left") or closest_corner:match("right")
elseif v_only then
closest_corner = closest_corner:match("top") or closest_corner:match("bottom")
end
-- Use p0 (mouse), p1 and p2 to create a rectangle
local pts = resize_to_point_map[closest_corner]
local p1 = pts.p1 and rect_to_point(ngeo, pts.p1[1], pts.p1[2]) or coords
local p2 = pts.p2 and rect_to_point(ngeo, pts.p2[1], pts.p2[2]) or coords
-- Create top_left and bottom_right points, convert to rectangle
ngeo = rect_from_points(
pts.y_only and ngeo.x or math.min(p1.x, p2.x),
pts.x_only and ngeo.y or math.min(p1.y, p2.y),
pts.y_only and ngeo.x + ngeo.width or math.max(p2.x, p1.x),
pts.x_only and ngeo.y + ngeo.height or math.max(p2.y, p1.y)
)
remove_border(d, args, ngeo)
-- Now, correct the geometry by the given size_hints offset
if d.apply_size_hints then
local w, h = d:apply_size_hints(
ngeo.width,
ngeo.height
)
local offset = align_map[pts.align](w, h, ngeo.width, ngeo.height)
ngeo.x = ngeo.x - offset.x
ngeo.y = ngeo.y - offset.y
end
geometry_common(d, args, ngeo)
return fix_new_geometry(ngeo, args, true)
end
--- Move the drawable (client or wibox) `d` to a screen position or side.
--
-- Supported args.positions are:
--
-- * top_left
-- * top_right
-- * bottom_left
-- * bottom_right
-- * left
-- * right
-- * top
-- * bottom
-- * centered
-- * center_vertical
-- * center_horizontal
--
--@DOC_awful_placement_align_EXAMPLE@
-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`)
-- @tparam[opt={}] table args Other arguments
-- @treturn table The new geometry
function placement.align(d, args)
args = add_context(args, "align")
d = d or capi.client.focus
if not d or not args.position then return end
local sgeo = get_parent_geometry(d, args)
local dgeo = geometry_common(d, args)
local pos = align_map[args.position](
sgeo.width ,
sgeo.height,
dgeo.width ,
dgeo.height
)
local ngeo = {
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 ) ,
}
remove_border(d, args, ngeo)
geometry_common(d, args, ngeo)
attach(d, placement[args.position], args)
return fix_new_geometry(ngeo, args, true)
end
-- Add the alias functions
for k in pairs(align_map) do
placement[k] = function(d, args)
args = add_context(args, k)
args.position = k
return placement_private.align(d, args)
end
reverse_align_map[placement[k]] = k
end
-- Add the documentation for align alias
---@DOC_awful_placement_top_left_EXAMPLE@
---@DOC_awful_placement_top_right_EXAMPLE@
---@DOC_awful_placement_bottom_left_EXAMPLE@
---@DOC_awful_placement_bottom_right_EXAMPLE@
---@DOC_awful_placement_left_EXAMPLE@
---@DOC_awful_placement_right_EXAMPLE@
---@DOC_awful_placement_top_EXAMPLE@
---@DOC_awful_placement_bottom_EXAMPLE@
---@DOC_awful_placement_centered_EXAMPLE@
---@DOC_awful_placement_center_vertical_EXAMPLE@
---@DOC_awful_placement_center_horizontal_EXAMPLE@
--- Stretch a drawable in a specific direction.
-- Valid args:
--
-- * **direction**: The stretch direction (*left*, *right*, *up*, *down*) or
-- a table with multiple directions.
--
--@DOC_awful_placement_stretch_EXAMPLE@
-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`)
-- @tparam[opt={}] table args The arguments
-- @treturn table The new geometry
function placement.stretch(d, args)
args = add_context(args, "stretch")
d = d or capi.client.focus
if not d or not args.direction then return end
-- In case there is multiple directions, call `stretch` for each of them
if type(args.direction) == "table" then
for _, dir in ipairs(args.direction) do
args.direction = dir
placement_private.stretch(dir, args)
end
return
end
local sgeo = get_parent_geometry(d, args)
local dgeo = geometry_common(d, args)
local ngeo = geometry_common(d, args, nil, true)
local bw = (not args.ignore_border_width) and d.border_width or 0
if args.direction == "left" then
ngeo.x = sgeo.x
ngeo.width = dgeo.width + (dgeo.x - ngeo.x)
elseif args.direction == "right" then
ngeo.width = sgeo.width - ngeo.x - 2*bw
elseif args.direction == "up" then
ngeo.y = sgeo.y
ngeo.height = dgeo.height + (dgeo.y - ngeo.y)
elseif args.direction == "down" then
ngeo.height = sgeo.height - dgeo.y - 2*bw
else
assert(false)
end
-- Avoid negative sizes if args.parent isn't compatible
ngeo.width = math.max(args.minimim_width or 1, ngeo.width )
ngeo.height = math.max(args.minimim_height or 1, ngeo.height)
geometry_common(d, args, ngeo)
attach(d, placement["stretch_"..args.direction], args)
return fix_new_geometry(ngeo, args, true)
end
-- Add the alias functions
for _,v in ipairs {"left", "right", "up", "down"} do
placement["stretch_"..v] = function(d, args)
args = add_context(args, "stretch_"..v)
args.direction = v
return placement_private.stretch(d, args)
end
end
---@DOC_awful_placement_stretch_left_EXAMPLE@
---@DOC_awful_placement_stretch_right_EXAMPLE@
---@DOC_awful_placement_stretch_up_EXAMPLE@
---@DOC_awful_placement_stretch_down_EXAMPLE@
--- Maximize a drawable horizontally, vertically or both.
-- Valid args:
--
-- * *axis*:The axis (vertical or horizontal). If none is
-- specified, then the drawable will be maximized on both axis.
--
--@DOC_awful_placement_maximize_EXAMPLE@
-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`)
-- @tparam[opt={}] table args The arguments
-- @treturn table The new geometry
function placement.maximize(d, args)
args = add_context(args, "maximize")
d = d or capi.client.focus
if not d then return end
local sgeo = get_parent_geometry(d, args)
local ngeo = geometry_common(d, args, nil, true)
local bw = (not args.ignore_border_width) and d.border_width or 0
if (not args.axis) or args.axis :match "vertical" then
ngeo.y = sgeo.y
ngeo.height = sgeo.height - 2*bw
end
if (not args.axis) or args.axis :match "horizontal" then
ngeo.x = sgeo.x
ngeo.width = sgeo.width - 2*bw
end
geometry_common(d, args, ngeo)
attach(d, placement.maximize, args)
return fix_new_geometry(ngeo, args, true)
end
-- Add the alias functions
for _, v in ipairs {"vertically", "horizontally"} do
placement["maximize_"..v] = function(d2, args)
args = add_context(args, "maximize_"..v)
args.axis = v
return placement_private.maximize(d2, args)
end
end
---@DOC_awful_placement_maximize_vertically_EXAMPLE@
---@DOC_awful_placement_maximize_horizontally_EXAMPLE@
--- Scale the drawable by either a relative or absolute percent.
--
-- Valid args:
--
-- **to_percent** : A number between 0 and 1. It represent a percent related to
-- the parent geometry.
-- **by_percent** : A number between 0 and 1. It represent a percent related to
-- the current size.
-- **direction**: Nothing or "left", "right", "up", "down".
--
-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`)
-- @tparam[opt={}] table args The arguments
-- @treturn table The new geometry
function placement.scale(d, args)
args = add_context(args, "scale_to_percent")
d = d or capi.client.focus
local to_percent = args.to_percent
local by_percent = args.by_percent
local percent = to_percent or by_percent
local direction = args.direction
local sgeo = get_parent_geometry(d, args)
local ngeo = geometry_common(d, args, nil)
local old_area = {width = ngeo.width, height = ngeo.height}
if (not direction) or direction == "left" or direction == "right" then
ngeo.width = (to_percent and sgeo or ngeo).width*percent
if direction == "left" then
ngeo.x = ngeo.x - (ngeo.width - old_area.width)
end
end
if (not direction) or direction == "up" or direction == "down" then
ngeo.height = (to_percent and sgeo or ngeo).height*percent
if direction == "up" then
ngeo.y = ngeo.y - (ngeo.height - old_area.height)
end
end
remove_border(d, args, ngeo)
geometry_common(d, args, ngeo)
attach(d, placement.maximize, args)
return fix_new_geometry(ngeo, args, true)
end
--- Move a drawable to a relative position next to another one.
--
-- The `args.preferred_positions` look like this:
--
-- {"top", "right", "left", "bottom"}
--
-- In that case, if there is room on the top of the geometry, then it will have
-- priority, followed by all the others, in order.
--
-- @tparam drawable d A wibox or client
-- @tparam table args
-- @tparam string args.mode The mode
-- @tparam string args.preferred_positions The preferred positions (in order)
-- @tparam string args.geometry A geometry inside the other drawable
-- @treturn table The new geometry
-- @treturn string The choosen position
-- @treturn string The choosen direction
function placement.next_to(d, args)
args = add_context(args, "next_to")
d = d or capi.client.focus
local preferred_positions = {}
for k, v in ipairs(args.preferred_positions or {}) do
preferred_positions[v] = k
end
local dgeo = geometry_common(d, args)
local pref_idx, pref_name = 99, nil
local mode,wgeo = args.mode
if args.geometry then
mode = "geometry"
wgeo = args.geometry
else
local pos = capi.mouse.current_widget_geometry
if pos then
wgeo, mode = pos, "cursor"
elseif capi.mouse.current_client then
wgeo, mode = capi.mouse.current_client:geometry(), "cursor"
end
end
if not wgeo then return end
-- See get_relative_regions comments
local is_absolute = wgeo.ontop ~= nil
local regions = get_relative_regions(wgeo, mode, is_absolute)
-- Check each possible slot around the drawable (8 total), see what fits
-- and order them by preferred_positions
local does_fit = {}
for k,v in pairs(regions) do
local geo, dir = outer_positions[k.."1"](v, dgeo.width, dgeo.height)
geo.width, geo.height = dgeo.width, dgeo.height
local fit = fit_in_bounding(v.screen, geo, args)
-- Try the other compatible geometry
if not fit then
geo, dir = outer_positions[k.."2"](v, dgeo.width, dgeo.height)
geo.width, geo.height = dgeo.width, dgeo.height
fit = fit_in_bounding(v.screen, geo, args)
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 ngeo, dir = unpack(does_fit[pos_name] or {}) --FIXME why does this happen
geometry_common(d, args, ngeo)
attach(d, placement.next_to, args)
return fix_new_geometry(ngeo, args, true), pos_name, dir
end
--- Restore the geometry.
-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`)
-- @tparam[opt={}] table args The arguments
-- @treturn boolean If the geometry was restored
function placement.restore(d, args)
if not args or not args.context then return false end
d = d or capi.client.focus
if not data[d] then return false end
local memento = data[d][args.context]
if not memento then return false end
local x, y = memento.x, memento.y
-- Some people consider that once moved to another screen, then
-- the memento needs to be upgraded. For now this is only true for
-- maximization until someone complains.
if memento.sgeo and memento.screen and memento.screen.valid
and args.context == "maximize" and d.screen
and get_screen(memento.screen) ~= get_screen(d.screen) then
-- Use the absolute geometry as the memento also does
local sgeo = get_screen(d.screen).geometry
x = sgeo.x + (memento.x - memento.sgeo.x)
y = sgeo.y + (memento.y - memento.sgeo.y)
end
d.border_width = memento.border_width
-- Don't use the memento as it would be "destructive", since `x`, `y`
-- and `screen` have to be modified.
d:geometry {
x = x,
y = y,
width = memento.width,
height = memento.height,
}
return true
end
return placement
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80