mirror of
https://github.com/awesomeWM/awesome
synced 2024-11-17 07:47:41 +01:00
db11c7e9da
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>
1476 lines
49 KiB
Lua
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 <elv1313@gmail.com>
|
|
-- @author Julien Danjou <julien@danjou.info>
|
|
-- @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
|