placement: Add `next_to`

This commit add the last placement function imported from the
Radical module.

It allows to place a wibox/client next to another object. It tries
to find the best fit. It also support wibox widgets.

This is intended for tooltips and menus, but can also be used in
`awful.rules` to place the new client as close as possible to the
focused one without overlap.
This commit is contained in:
Emmanuel Lepage Vallee 2016-05-28 22:38:40 -04:00
parent ce5cdb49ed
commit 211907def2
1 changed files with 261 additions and 4 deletions

View File

@ -96,6 +96,8 @@ local a_screen = require("awful.screen")
local grect = require("gears.geometry").rectangle
local util = require("awful.util")
local dpi = require("beautiful").xresources.apply_dpi
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]
@ -260,6 +262,19 @@ local resize_to_point_map = {
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
@ -553,8 +568,9 @@ attach = function(d, position_f, args)
end
end
-- If there is a parent drawable, screen or mouse, also track it
if parent then
-- 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
@ -603,6 +619,158 @@ local function rect_to_point(rect, corner_i, corner_j)
}
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).
--
@ -1087,6 +1255,10 @@ for _, v in ipairs {"vertically", "horizontally"} do
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:
@ -1143,9 +1315,94 @@ function placement.scale(d, args)
return fix_new_geometry(ngeo, args, true)
end
---@DOC_awful_placement_maximize_vertically_EXAMPLE@
--- 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
---@DOC_awful_placement_maximize_horizontally_EXAMPLE@
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`)