1654 lines
55 KiB
Lua
1654 lines
55 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 floating = require("awful.layout.suit.floating")
|
|
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
|
|
|
|
-- Cleanup the override because otherwise it might leak into
|
|
-- future calls.
|
|
rawset(args, "override_geometry", nil)
|
|
|
|
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
|
|
local outer_positions = {
|
|
left_front = function(r, w, _) return {x=r.x-w , y=r.y }, "front" end,
|
|
left_back = function(r, w, h) return {x=r.x-w , y=r.y-h+r.height }, "back" end,
|
|
left_middle = function(r, w, h) return {x=r.x-w , y=r.y-h/2+r.height/2 }, "middle" end,
|
|
right_front = function(r, _, _) return {x=r.x , y=r.y }, "front" end,
|
|
right_back = function(r, _, h) return {x=r.x , y=r.y-h+r.height }, "back" end,
|
|
right_middle = function(r, _, h) return {x=r.x , y=r.y-h/2+r.height/2 }, "middle" end,
|
|
top_front = function(r, _, h) return {x=r.x , y=r.y-h }, "front" end,
|
|
top_back = function(r, w, h) return {x=r.x-w+r.width , y=r.y-h }, "back" end,
|
|
top_middle = function(r, w, h) return {x=r.x-w/2+r.width/2, y=r.y-h }, "middle" end,
|
|
bottom_front = function(r, _, _) return {x=r.x , y=r.y }, "front" end,
|
|
bottom_back = function(r, w, _) return {x=r.x-w+r.width , y=r.y }, "back" end,
|
|
bottom_middle = function(r, w, _) return {x=r.x-w/2+r.width/2, y=r.y }, "middle" 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 adjust 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
|
|
if obj.get_bounding_geometry then
|
|
-- It is a screen, it doesn't support setting new sizes.
|
|
return obj:get_bounding_geometry(args)
|
|
end
|
|
|
|
-- It is either a drawable or something that implement its API
|
|
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
|
|
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 parent geometry using one way or another depending on the object
|
|
-- Type
|
|
local bw, dgeo = 0, {x=0, y=0, width=1, height=1}
|
|
|
|
-- Detect various types of geometry table and (try) to get rid of the
|
|
-- differences so the code below don't have to care anymore.
|
|
if geo.drawin then
|
|
bw, dgeo = geo.drawin._border_width, geo.drawin:geometry()
|
|
elseif geo.drawable and geo.drawable.get_wibox then
|
|
bw = geo.drawable.get_wibox().border_width
|
|
dgeo = geo.drawable.get_wibox():geometry()
|
|
elseif geo.drawable and geo.drawable.drawable then
|
|
bw, dgeo = 0, geo.drawable.drawable:geometry()
|
|
else
|
|
-- The placement isn't done on an object at all, having no border is
|
|
-- normal.
|
|
assert(mode == "geometry")
|
|
end
|
|
|
|
-- Add the infamous border size
|
|
dgeo.width = dgeo.width + 2*bw
|
|
dgeo.height = dgeo.height + 2*bw
|
|
|
|
-- Compute the absolute widget geometry
|
|
local abs_widget_geo = is_absolute and dgeo or {
|
|
x = dgeo.x + geo.x + bw,
|
|
y = dgeo.y + geo.y + bw,
|
|
width = geo.width ,
|
|
height = geo.height ,
|
|
drawable = geo.drawable ,
|
|
}
|
|
|
|
abs_widget_geo.drawable_geo = geo.drawable and dgeo 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.
|
|
-- @staticfct awful.placement.closest_corner
|
|
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@
|
|
-- @tparam 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.
|
|
-- @staticfct awful.placement.no_offscreen
|
|
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 = geometry_common(c, args)
|
|
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
|
|
|
|
-- Check whether the client is on at least one of selected tags (similar to
|
|
-- `c:isvisible()`, but without checks for the hidden or minimized state).
|
|
local function client_on_selected_tags(c)
|
|
if c.sticky then
|
|
return true
|
|
else
|
|
for _, t in pairs(c:tags()) do
|
|
if t.selected then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- Check whether the client would be visible if the specified set of tags is
|
|
-- selected (using the same set of conditions as `c:isvisible()`, but checking
|
|
-- against the specified tags instead of currently selected ones).
|
|
local function client_visible_on_tags(c, tags)
|
|
if c.hidden or c.minimized then
|
|
return false
|
|
elseif c.sticky then
|
|
return true
|
|
else
|
|
for _, t in pairs(c:tags()) do
|
|
if gtable.hasitem(tags, t) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
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
|
|
-- @staticfct awful.placement.no_overlap
|
|
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, curlay
|
|
if client_on_selected_tags(c) then
|
|
cls = screen:get_clients(false)
|
|
local t = screen.selected_tag
|
|
curlay = t.layout or floating
|
|
else
|
|
-- When placing a client on unselected tags, place it as if all tags of
|
|
-- that client are selected.
|
|
local tags = c:tags()
|
|
cls = {}
|
|
for _, other_c in pairs(capi.client.get(screen)) do
|
|
if client_visible_on_tags(other_c, tags) then
|
|
table.insert(cls, other_c)
|
|
end
|
|
end
|
|
curlay = tags[1] and tags[1].layout
|
|
end
|
|
local areas = { screen.workarea }
|
|
for _, cl in pairs(cls) do
|
|
if cl ~= c
|
|
and cl.type ~= "desktop"
|
|
and (cl.floating or curlay == 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
|
|
-- @staticfct awful.placement.under_mouse
|
|
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
|
|
-- @staticfct awful.placement.next_to_mouse
|
|
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
|
|
-- @staticfct awful.placement.resize_to_mouse
|
|
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
|
|
-- @staticfct awful.placement.align
|
|
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
|
|
-- @staticfct awful.placement.stretch
|
|
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
|
|
-- @staticfct awful.placement.maximize
|
|
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
|
|
-- @staticfct awful.placement.scale
|
|
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.
|
|
--
|
|
-- This placement function offers two additional settings to align the drawable
|
|
-- alongside the parent geometry. The first one, the position, sets the side
|
|
-- relative to the parent. The second one, the anchor, set the alignment within
|
|
-- the side selected by the `preferred_positions`. Both settings are tables of
|
|
-- priorities. The first available slot will be used. If there isn't enough
|
|
-- space, then it will fallback to the next until it is possible to fit the
|
|
-- drawable. This is meant to avoid going offscreen.
|
|
--
|
|
-- The `args.preferred_positions` look like this:
|
|
--
|
|
-- {"top", "right", "left", "bottom"}
|
|
--
|
|
-- The `args.preferred_anchors` are:
|
|
--
|
|
-- * "front": The closest to the origin (0,0)
|
|
-- * "middle": Centered aligned with the parent
|
|
-- * "back": The opposite side compared to `front`
|
|
--
|
|
-- 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.
|
|
--
|
|
--@DOC_awful_placement_next_to_EXAMPLE@
|
|
--
|
|
-- The `args.mode` parameters allows to control from which `next_to` takes its
|
|
-- source object from. The valid values are:
|
|
--
|
|
-- * geometry: Next to this geometry, `args.geometry` has to be set.
|
|
-- * cursor: Next to the mouse.
|
|
-- * cursor_inside
|
|
-- * geometry_inside
|
|
--
|
|
-- @tparam drawable d A wibox or client
|
|
-- @tparam table args
|
|
-- @tparam string args.mode The mode
|
|
-- @tparam string|table args.preferred_positions The preferred positions (in order)
|
|
-- @tparam string|table args.preferred_anchors The preferred anchor(s) (in order)
|
|
-- @tparam string args.geometry A geometry inside the other drawable
|
|
-- @treturn table The new geometry
|
|
-- @treturn string The choosen position ("left", "right", "top" or "bottom")
|
|
-- @treturn string The choosen anchor ("front", "middle" or "back")
|
|
-- @staticfct awful.placement.next_to
|
|
function placement.next_to(d, args)
|
|
args = add_context(args, "next_to")
|
|
d = d or capi.client.focus
|
|
|
|
local osize = type(d.geometry) == "function" and d:geometry() or d.geometry
|
|
local original_pos, original_anchors = args.preferred_positions, args.preferred_anchors
|
|
|
|
if type(original_pos) == "string" then
|
|
original_pos = {original_pos}
|
|
end
|
|
|
|
if type(original_anchors) == "string" then
|
|
original_anchors = {original_anchors}
|
|
end
|
|
|
|
local preferred_positions = {}
|
|
local preferred_anchors = #(original_anchors or {}) > 0 and
|
|
original_anchors or {"front", "back", "middle"}
|
|
|
|
for k, v in ipairs(original_pos 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)
|
|
|
|
-- Order the regions with the preferred_positions, then the defaults
|
|
local sorted_regions, default_positions = {}, {"left", "right", "bottom", "top"}
|
|
|
|
for _, pos in ipairs(original_pos or {}) do
|
|
for idx, def in ipairs(default_positions) do
|
|
if def == pos then
|
|
table.remove(default_positions, idx)
|
|
break
|
|
end
|
|
end
|
|
|
|
table.insert(sorted_regions, {name = pos, region = regions[pos]})
|
|
end
|
|
|
|
for _, pos in ipairs(default_positions) do
|
|
table.insert(sorted_regions, {name = pos, region = regions[pos]})
|
|
end
|
|
|
|
-- Check each possible slot around the drawable (8 total), see what fits
|
|
-- and order them by preferred_positions
|
|
local does_fit = {}
|
|
for _, pos in ipairs(sorted_regions) do
|
|
local geo, dir, fit
|
|
|
|
-- Try each anchor until one that fits is found
|
|
for _, anchor in ipairs(preferred_anchors) do
|
|
geo, dir = outer_positions[pos.name.."_"..anchor](pos.region, dgeo.width, dgeo.height)
|
|
|
|
geo.width, geo.height = dgeo.width, dgeo.height
|
|
|
|
fit = fit_in_bounding(pos.region, geo, args)
|
|
|
|
if fit then break end
|
|
end
|
|
|
|
does_fit[pos.name] = fit and {geo, dir} or nil
|
|
|
|
-- preferred_positions is optional
|
|
local better_pos_idx = preferred_positions[pos.name]
|
|
and preferred_positions[pos.name] < pref_idx or false
|
|
|
|
if fit and (better_pos_idx or not pref_name) then
|
|
pref_idx = preferred_positions[pos.name]
|
|
pref_name = pos.name
|
|
end
|
|
|
|
-- No need to continue
|
|
if fit then break end
|
|
end
|
|
|
|
if not pref_name then return end
|
|
|
|
assert(does_fit[pref_name])
|
|
|
|
local ngeo, dir = unpack(does_fit[pref_name])
|
|
|
|
-- The requested placement isn't possible due to the lack of space, better
|
|
-- do nothing an try random things
|
|
if not ngeo then return end
|
|
|
|
remove_border(d, args, ngeo)
|
|
|
|
geometry_common(d, args, ngeo)
|
|
|
|
attach(d, placement.next_to, args)
|
|
|
|
local ret = fix_new_geometry(ngeo, args, true)
|
|
|
|
-- Make sure the geometry didn't change, it would indicate an
|
|
-- "off by border" issue.
|
|
assert((not osize.width) or ret.width == d.width)
|
|
assert((not osize.height) or ret.height == d.height)
|
|
|
|
return ret, pref_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
|
|
-- @staticfct awful.placement.restore
|
|
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
|
|
|
|
--- Skip all preceeding results of placement pipeline for fullscreen clients.
|
|
--@DOC_awful_placement_skip_fullscreen_EXAMPLE@
|
|
-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`)
|
|
-- @tparam[opt={}] table args Other arguments
|
|
-- @treturn table The new geometry
|
|
-- @staticfct awful.placement.skip_fullscreen
|
|
function placement.skip_fullscreen(d, args)
|
|
args = add_context(args, "skip_fullscreen")
|
|
d = d or capi.client.focus
|
|
|
|
if d.fullscreen then
|
|
return {get_screen(d.screen).geometry, {}, true}
|
|
else
|
|
local ngeo = geometry_common(d, args)
|
|
remove_border(d, args, ngeo)
|
|
geometry_common(d, args, ngeo)
|
|
return fix_new_geometry(ngeo, args, true)
|
|
end
|
|
end
|
|
|
|
return placement
|
|
|
|
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80
|