diff options
author | ache <ache@ache.one> | 2017-03-13 23:17:19 +0100 |
---|---|---|
committer | ache <ache@ache.one> | 2017-03-13 23:17:19 +0100 |
commit | 22d656903563f75678f3634964731ccf93355dfd (patch) | |
tree | e3cb6279d95c9764093072d5e946566ea6533799 /lib/awful/placement.lua |
Init commit
Diffstat (limited to 'lib/awful/placement.lua')
-rw-r--r-- | lib/awful/placement.lua | 1694 |
1 files changed, 1694 insertions, 0 deletions
diff --git a/lib/awful/placement.lua b/lib/awful/placement.lua new file mode 100644 index 0000000..d288f49 --- /dev/null +++ b/lib/awful/placement.lua @@ -0,0 +1,1694 @@ +--------------------------------------------------------------------------- +--- 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: +-- +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_compose.svg) +-- +-- +-- -- 'right' will be replaced by 'left' +-- local f = (awful.placement.right + awful.placement.left) +-- f(client.focus) +-- +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_compose2.svg) +-- +-- +-- -- Simulate Windows 7 'edge snap' (also called aero snap) feature +-- local axis = 'vertically' +-- local f = awful.placement.scale +-- + awful.placement.left +-- + (axis and awful.placement['maximize_'..axis] or nil) +-- local geo = f(client.focus, {honor_workarea=true, to_percent = 0.5}) +-- +-- <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 util = require("awful.util") +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].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 + 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 = util.table.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 + +--- 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 +-- +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_closest_mouse.svg) +-- +--**Usage example output**: +-- +-- Closest corner: top_left +-- +-- +-- @usage +-- -- Move the mouse to the closest corner of the focused client +--awful.placement.closest_corner(mouse, {include_sides=true, parent=c}) +-- -- It is possible to emulate the mouse API to get the closest corner of +-- -- random area +--local _, corner = awful.placement.closest_corner( +-- {coords=function() return {x = 100, y=100} end}, +-- {include_sides = true, bounding_rect = {x=0, y=0, width=200, height=200}} +--) +--print('Closest corner:', corner) +-- @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). +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_no_offscreen.svg) +-- +--**Usage example output**: +-- +-- Before: x=-30, y=-30, width=100, height=100 +-- After: x=10, y=10, width=100, height=100 +-- +-- +-- @usage +--awful.placement.no_offscreen(c)--, {honor_workarea=true, margins=40}) +-- @client c The client. +-- @tparam[opt=client's screen] integer screen The screen. +-- @treturn table The new client geometry. +function placement.no_offscreen(c, screen) + --HACK necessary for composition to work. The API will be changed soon + if type(screen) == "table" then + screen = nil + end + + c = c or capi.client.focus + local geometry = area_common(c) + screen = get_screen(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 + + return c:geometry { + x = geometry.x, + y = geometry.y + } +end + +--- Place the client where there's place available with minimum overlap. +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_no_overlap.svg) +-- +-- @usage +--awful.placement.no_overlap(client.focus) +--local x,y = screen[4].geometry.x, screen[4].geometry.y +-- @param c The client. +-- @treturn table The new geometry +function placement.no_overlap(c) + c = c or capi.client.focus + local geometry = area_common(c) + 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) 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 + areas = { screen.workarea } + end + for _, r in ipairs(areas) do + if r.width * r.height > new.width * new.height then + new = r + end + end + end + + -- Restore height and width + new.width = geometry.width + new.height = geometry.height + + return c:geometry({ x = new.x, y = new.y }) +end + +--- Place the client under the mouse. +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_under_mouse.svg) +-- +-- @usage +--awful.placement.under_mouse(client.focus) +-- @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) + + local bw = d.border_width or 0 + ngeo.width = ngeo.width - 2*bw + ngeo.height = ngeo.height - 2*bw + + 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. +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_next_to_mouse.svg) +-- +-- @usage +--awful.placement.next_to_mouse(client.focus) +-- @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 + util.deprecate( + "awful.placement.next_to_mouse offset argument is deprecated".. + " use awful.placement.next_to_mouse(c, {offset={x=...}})" + ) + 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. +-- +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_resize_to_mouse.svg) +-- +-- @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) + ) + + local bw = d.border_width or 0 + + for _, a in ipairs {"width", "height"} do + ngeo[a] = ngeo[a] - 2*bw + end + + -- 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 +-- +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_align.svg) +-- +-- @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 bw = d.border_width or 0 + + 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 ) - 2*bw, + height = math.ceil(dgeo.height ) - 2*bw, + } + + 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 + +--- +-- Align a client to the top left of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_top_left.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @treturn table The new geometry +-- @name top_left +-- @class function +-- +-- @usage +--awful.placement.top_left(client.focus) + +--- +-- Align a client to the top right of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_top_right.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @treturn table The new geometry +-- @name top_right +-- @class function +-- +-- @usage +--awful.placement.top_right(client.focus) + +--- +-- Align a client to the bottom left of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_bottom_left.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @treturn table The new geometry +-- @name bottom_left +-- @class function +-- +-- @usage +--awful.placement.bottom_left(client.focus) + +--- +-- Align a client to the bottom right of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_bottom_right.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @treturn table The new geometry +-- @name bottom_right +-- @class function +-- +-- @usage +--awful.placement.bottom_right(client.focus) + +--- +-- Align a client to the left of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_left.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @treturn table The new geometry +-- @name left +-- @class function +-- +-- @usage +--awful.placement.left(client.focus) + +--- +-- Align a client to the right of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_right.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @treturn table The new geometry +-- @name right +-- @class function +-- +-- @usage +--awful.placement.right(client.focus) + +--- +-- Align a client to the top of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_top.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @treturn table The new geometry +-- @name top +-- @class function +-- +-- @usage +--awful.placement.top(client.focus) +--assert(c.x == screen[1].geometry.width/2-40/2-c.border_width) + +--- +-- Align a client to the bottom of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_bottom.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @treturn table The new geometry +-- @name bottom +-- @class function +-- +-- @usage +--awful.placement.bottom(client.focus) + +--- +-- Align a client to the center of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_centered.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @treturn table The new geometry +-- @name centered +-- @class function +-- +-- @usage +--awful.placement.centered(client.focus) + +--- +-- Align a client to the vertical center of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_center_vertical.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @name center_vertical +-- @class function +-- +-- @usage +--awful.placement.center_vertical(client.focus) + +--- +-- Align a client to the horizontal center left of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_center_horizontal.svg) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @treturn table The new geometry +-- @name center_horizontal +-- @class function +-- +-- @usage +--awful.placement.center_horizontal(client.focus) + +--- Stretch a drawable in a specific direction. +-- Valid args: +-- +-- * **direction**: The stretch direction (*left*, *right*, *up*, *down*) or +-- a table with multiple directions. +-- +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_stretch.svg) +-- +-- @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 = 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 + +--- +-- Stretch the drawable to the left of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_stretch_left.svg) +-- @tparam drawable d A drawable (like `client` or `wibox`) +-- @tparam[opt={}] table args Other arguments +-- @treturn table The new geometry +-- @name stretch_left +-- @class function +-- +-- @usage +--placement.stretch_left(client.focus) + +--- +-- Stretch the drawable to the right of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_stretch_right.svg) +-- @tparam drawable d A drawable (like `client` or `wibox`) +-- @tparam[opt={}] table args Other arguments +-- @treturn table The new geometry +-- @name stretch_right +-- @class function +-- +-- @usage +--placement.stretch_right(client.focus) + +--- +-- Stretch the drawable to the top of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_stretch_up.svg) +-- @tparam drawable d A drawable (like `client` or `wibox`) +-- @tparam[opt={}] table args Other arguments +-- @treturn table The new geometry +-- @name stretch_up +-- @class function +-- +-- @usage +--placement.stretch_up(client.focus) + +--- +-- Stretch the drawable to the bottom of the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_stretch_down.svg) +-- @tparam drawable d A drawable (like `client` or `wibox`) +-- @tparam[opt={}] table args Other arguments +-- @treturn table The new geometry +-- @name stretch_down +-- @class function +-- +-- @usage +--placement.stretch_down(client.focus) + +--- 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. +-- +-- +-- +--![Usage example](../images/AUTOGEN_awful_placement_maximize.svg) +-- +-- @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 = 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 + +--- +-- Vetically maximize the drawable in the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_maximize_vertically.svg) +-- @tparam drawable d A drawable (like `client` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @name maximize_vertically +-- @class function +-- +-- @usage +--placement.maximize_vertically(c) + +--- +-- Horizontally maximize the drawable in the parent area. +-- +--![Usage example](../images/AUTOGEN_awful_placement_maximize_horizontally.svg) +-- @tparam drawable d A drawable (like `client` or `wibox`) +-- @tparam[opt={}] table args Other arguments') +-- @name maximize_horizontally +-- @class function +-- +-- @usage +--placement.maximize_horizontally(c) + +--- 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 + + local bw = d.border_width or 0 + ngeo.width = ngeo.width - 2*bw + ngeo.height = ngeo.height - 2*bw + + 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 geomtry, 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 = {} + + if #(args.preferred_positions or {}) then + for k, v in ipairs(args.preferred_positions) do + preferred_positions[v] = k + end + 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 + + memento.screen = nil --TODO use it + + d.border_width = memento.border_width + + d:geometry(memento) + return true +end + +return placement + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 |