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:
parent
ce5cdb49ed
commit
211907def2
|
@ -96,6 +96,8 @@ local a_screen = require("awful.screen")
|
||||||
local grect = require("gears.geometry").rectangle
|
local grect = require("gears.geometry").rectangle
|
||||||
local util = require("awful.util")
|
local util = require("awful.util")
|
||||||
local dpi = require("beautiful").xresources.apply_dpi
|
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)
|
local function get_screen(s)
|
||||||
return s and capi.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" },
|
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.
|
--- Add a context to the arguments.
|
||||||
-- This function extend the argument table. The context is used by some
|
-- 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
|
-- 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
|
||||||
end
|
end
|
||||||
|
|
||||||
-- If there is a parent drawable, screen or mouse, also track it
|
-- If there is a parent drawable, screen, also track it.
|
||||||
if parent then
|
-- Note that tracking the mouse is not supported
|
||||||
|
if parent and parent.connect_signal then
|
||||||
parent:connect_signal("property::geometry" , tracker)
|
parent:connect_signal("property::geometry" , tracker)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -603,6 +619,158 @@ local function rect_to_point(rect, corner_i, corner_j)
|
||||||
}
|
}
|
||||||
end
|
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
|
--- Move a drawable to the closest corner of the parent geometry (such as the
|
||||||
-- screen).
|
-- screen).
|
||||||
--
|
--
|
||||||
|
@ -1087,6 +1255,10 @@ for _, v in ipairs {"vertically", "horizontally"} do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@DOC_awful_placement_maximize_vertically_EXAMPLE@
|
||||||
|
|
||||||
|
---@DOC_awful_placement_maximize_horizontally_EXAMPLE@
|
||||||
|
|
||||||
--- Scale the drawable by either a relative or absolute percent.
|
--- Scale the drawable by either a relative or absolute percent.
|
||||||
--
|
--
|
||||||
-- Valid args:
|
-- Valid args:
|
||||||
|
@ -1143,9 +1315,94 @@ function placement.scale(d, args)
|
||||||
return fix_new_geometry(ngeo, args, true)
|
return fix_new_geometry(ngeo, args, true)
|
||||||
end
|
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.
|
--- Restore the geometry.
|
||||||
-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`)
|
-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`)
|
||||||
|
|
Loading…
Reference in New Issue