-
Notifications
You must be signed in to change notification settings - Fork 599
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
layout.dynamic: Add a generic resize handler
It look in the widget hierarchy for layouts that support ratio resizing, then set the new size.
- Loading branch information
Showing
2 changed files
with
370 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,368 @@ | ||
--------------------------------------------------------------------------- | ||
--- Utilities required to resize a client wrapped in ratio layouts | ||
-- | ||
-- @author Emmanuel Lepage Vallee <elv1313@gmail.com> | ||
-- @copyright 2016 Emmanuel Lepage Vallee | ||
-- @release @AWESOME_VERSION@ | ||
-- @module awful.layout.dynamic.resize | ||
--------------------------------------------------------------------------- | ||
|
||
local util = require( "awful.util" ) | ||
local tag = require( "awful.tag" ) | ||
local screen = require( "awful.screen" ) | ||
local cairo = require( "lgi" ).cairo | ||
|
||
local capi = { | ||
mouse = mouse , | ||
client = client , | ||
mousegrabber = mousegrabber, | ||
} | ||
|
||
local module = {} | ||
|
||
local mode = "live" | ||
local req = "request::geometry" | ||
local callbacks = {} | ||
|
||
-- 3x3 matrix of potential resize point | ||
local corners3x3 = {{"top_left" , "top" , "top_right" }, | ||
{"left" , nil , "right" }, | ||
{"bottom_left", "bottom" , "bottom_right"}} | ||
|
||
-- 2x2 matrix of potential resize point, fallback when center is hit in the 3x3 | ||
local corners2x2 = {{"top_left" , "top_right" }, | ||
{"bottom_left", "bottom_right"}} | ||
|
||
-- Some parameters to correctly compute the final size | ||
local map = { | ||
-- Corners | ||
top_left = {p1= nil , p2={1,1}, x_only=false, y_only=false}, | ||
top_right = {p1={0,1} , p2= nil , x_only=false, y_only=false}, | ||
bottom_left = {p1= nil , p2={1,0}, x_only=false, y_only=false}, | ||
bottom_right = {p1={0,0} , p2= nil , x_only=false, y_only=false}, | ||
|
||
-- Sides | ||
left = {p1= nil , p2={1,1}, x_only=true , y_only=false}, | ||
right = {p1={0,0} , p2= nil , x_only=true , y_only=false}, | ||
top = {p1= nil , p2={1,1}, x_only=false, y_only=true }, | ||
bottom = {p1={0,0} , p2= nil , x_only=false, y_only=true }, | ||
} | ||
|
||
-- Even if keyboard resize could be implemented using the same logic as mouse, | ||
-- emulating the pointer is not worth it. This matrix is multiplied by the | ||
-- pixel delta will create the correct transformation. | ||
local map2 = { | ||
-- x width<-i i-->y height | ||
-- ^-----, | | ,-----^ | ||
left = {-1, 1, 0, 0}, | ||
right = { 0, 1, 0, 0}, | ||
up = { 0, 0,-1, 1}, | ||
down = { 0, 0, 0, 1}, | ||
} | ||
|
||
-- Convert a rectangle and matrix info into a point | ||
local function rect_to_point(rect, corner_i, corner_j, n) | ||
return { | ||
x = rect.x + corner_i * math.floor(rect.width / (n-1)), | ||
y = rect.y + corner_j * math.floor(rect.height / (n-1)), | ||
} | ||
end | ||
|
||
-- Ajust client geometry to represent actual size | ||
local function wrapper_geometry(c) | ||
|
||
-- This isn't really right, as the client could be hidden, but good enough | ||
local t = tag.selected(c.screen) | ||
local gap = tag.getgap(t) | ||
|
||
local geo = c:geometry() | ||
|
||
return { | ||
width = geo.width + 2*c.border_width + 2*gap, | ||
height = geo.height + 2*c.border_width + 2*gap, | ||
x = geo.x - gap, | ||
y = geo.y - gap | ||
} | ||
end | ||
|
||
-- Move the cursor to the corner to the closest side or corner | ||
-- This assume the mouse is within the client. If not, use `awful.mouse`. | ||
local function move_to_corner(client) | ||
local pos = capi.mouse.coords() | ||
|
||
local c_geo = wrapper_geometry(client) | ||
|
||
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, offset) | ||
n = _n | ||
corner_i = -math.ceil( ( (c_geo.x - pos.x) * n) / c_geo.width ) | ||
corner_j = -math.ceil( ( (c_geo.y - pos.y) * n) / c_geo.height ) | ||
return mat[corner_j + 1][corner_i + 1] | ||
end | ||
|
||
-- If the point is in the center, use the cloest corner | ||
local corner = f(3, corners3x3) or f(2, corners2x2) | ||
|
||
-- Transpose the corner back to the original size | ||
capi.mouse.coords(rect_to_point(c_geo, corner_i, corner_j , n)) | ||
|
||
return corner | ||
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 | ||
|
||
--- Resize, the client | ||
-- | ||
-- Valid `args` are: | ||
-- | ||
-- * *enter_callback*: A function called before the `mousegrabber` start | ||
-- * *move_callback*: A function called when the mouse move | ||
-- * *leave_callback*: A function called before the `mousegrabber` is released | ||
-- * *mode*: The resize mode | ||
-- | ||
-- @tparam client client A client | ||
-- @tparam string corner This parameter exist for compatibility and is ignored | ||
-- @tparam number x This parameter exist for compatibility and is ignored | ||
-- @tparam number y This parameter exist for compatibility and is ignored | ||
-- @tparam table args Various argument to configure the mouse resize behavior | ||
function module.generic_mouse_resize_handler(client, corner, x, y, args) | ||
local args, corner = args or {}, move_to_corner(client) | ||
local m = args.mode or mode | ||
|
||
local enter_cb = args.enter_callback or callbacks.enter | ||
if enter_cb then | ||
enter_cb(client, geo, corner) | ||
end | ||
|
||
capi.mousegrabber.run(function (_mouse) | ||
-- Create a vector from top_left and one from bottom_right | ||
local p0 = {x = _mouse.x, y = _mouse.y} | ||
local geo = wrapper_geometry(client) | ||
|
||
-- Use p0 (mouse), p1 and p2 to create a rectangle | ||
local pts = map[corner] | ||
local p1 = pts.p1 and rect_to_point(geo, pts.p1[1], pts.p1[2], 2) or p0 | ||
local p2 = pts.p2 and rect_to_point(geo, pts.p2[1], pts.p2[2], 2) or p0 | ||
|
||
-- Create top_left and bottom_right points, convert to rectangle | ||
geo = rect_from_points( | ||
pts.y_only and geo.x or math.min(p1.x, p2.x), | ||
pts.x_only and geo.y or math.min(p1.y, p2.y), | ||
pts.y_only and geo.x + geo.width or math.max(p2.x, p1.x), | ||
pts.x_only and geo.y + geo.height or math.max(p2.y, p1.y) | ||
) | ||
|
||
local move_cb = args.move_callback or callbacks.move | ||
|
||
-- Resize everytime the mouse move (default behavior) | ||
if m == "live" then | ||
client:emit_signal( req, "mouse.resize", geo, {corner=corner} ) | ||
elseif move_cb then | ||
move_cb(client, geo, corner) | ||
end | ||
|
||
-- Quit when the button is released | ||
for k,v in pairs(_mouse.buttons) do | ||
if v then return true end | ||
end | ||
|
||
-- Only resize after the mouse is released, this avoid losing content | ||
-- in resize sensitive apps such as XTerm or allow external modules | ||
-- to implement custom resizing | ||
if m == "after" then | ||
client:emit_signal( req, "mouse.resize", geo, {corner=corner} ) | ||
end | ||
|
||
local leave_cb = args.leave_callback or callbacks.leave | ||
if leave_cb then | ||
leave_cb(client, geo, corner) | ||
end | ||
|
||
return false | ||
end, "cross") | ||
end | ||
|
||
-- Loop the path between the client widget and the layout to find nodes | ||
-- capable of resizing in both directions | ||
local function ratio_lookup(handler, wrapper) | ||
local idx, parent, path = handler.widget:index(wrapper, true) | ||
local res = {} | ||
|
||
local full_path = util.table.join({parent}, path) | ||
|
||
for i=#full_path, 1, -1 do | ||
local w = full_path[i] | ||
if w.inc_ratio then | ||
res[w.dir] = res[w.dir] or { | ||
layout = w, | ||
widget = full_path[i-1] or wrapper | ||
} | ||
end | ||
end | ||
|
||
return res | ||
end | ||
|
||
-- Compute the new ratio before, for and after geo | ||
local function compute_ratio(workarea, geo) | ||
local x_before = (geo.x - workarea.x) / workarea.width | ||
local x_self = (geo.width ) / workarea.width | ||
local x_after = 1 - x_before - x_self | ||
local y_before = (geo.y - workarea.y) / workarea.height | ||
local y_self = (geo.height ) / workarea.height | ||
local y_after = 1 - y_before - y_self | ||
|
||
return { | ||
x = { x_before, x_self, x_after }, | ||
y = { y_before, y_self, y_after }, | ||
} | ||
end | ||
|
||
--- If there is a ratio based layout somewhere, try to get all geometry updated | ||
-- This method is mostly for internal purpose, use `ajust_geometry` | ||
-- @param handler The layout handler | ||
-- @tparam client c The client | ||
-- @param widget The client wrapper | ||
-- @param geo The new geometry | ||
function module.update_ratio(handler, c, widget, geo) | ||
local ratio_wdgs = ratio_lookup(handler, widget) | ||
|
||
-- Layouts only span one screen, fit `geo` into that screen | ||
local region = cairo.Region.create_rectangle(cairo.RectangleInt(geo)) | ||
region:intersect(cairo.Region.create_rectangle( | ||
cairo.RectangleInt(handler.param.workarea) | ||
)) | ||
geo = region:get_rectangle(0) | ||
|
||
-- Don't waste time, it will go wrong anyway | ||
if geo.width == 0 or geo.height == 0 then return end | ||
|
||
local ratio = compute_ratio(handler.param.workarea, geo) | ||
|
||
if ratio_wdgs.x then | ||
ratio_wdgs.x.layout:ajust_widget_ratio(ratio_wdgs.x.widget, unpack(ratio.x)) | ||
end | ||
if ratio_wdgs.y then | ||
ratio_wdgs.y.layout:ajust_widget_ratio(ratio_wdgs.y.widget, unpack(ratio.y)) | ||
end | ||
|
||
handler.widget:emit_signal("widget::redraw_needed") | ||
end | ||
|
||
--- Helper function to get a wrapper when only knowing the client | ||
-- @tparam[opt=client.focus] client c The client | ||
-- @tparam[opt=c.first_tag] tag t The tag. If `nil`, then the first *selected* tag | ||
-- will be used. | ||
-- @return The handler | ||
-- @return The client wrapper widget | ||
function module.get_handler_and_wrapper(c, t) | ||
local c = c or capi.client.focus | ||
|
||
if not c then return nil, nil end | ||
|
||
-- Find a selected tag | ||
local t = t | ||
if not t then | ||
for k,v in ipairs(c:tags()) do | ||
if v.selected then | ||
t = v | ||
break | ||
end | ||
end | ||
end | ||
|
||
-- Better than nothing | ||
t = t or c.first_tag | ||
|
||
if not t then return end | ||
|
||
-- In case multiple tags are selected, always pick the first one | ||
local tags = tag.selectedlist(c.screen) | ||
if #tags > 1 then | ||
t = tags[1] | ||
end | ||
|
||
local handler = tag.getproperty(t, "layout") | ||
|
||
|
||
if not handler or not handler.is_dynamic then return end | ||
|
||
local wrapper = handler.client_to_wrapper[c] | ||
|
||
return handler, wrapper | ||
end | ||
|
||
--- Set the resize mode. | ||
-- The available modes are: | ||
-- | ||
-- * **live**: Resize the layout everytime the mouse move | ||
-- * **after**: Resize the layout only when the mouse is released | ||
-- | ||
-- Some clients, such as XTerm, may lose information if resized too often. | ||
-- | ||
-- @tparam string m The mode | ||
function module:set_mode(m) | ||
assert(m == "live" or m == "after") | ||
mode = m | ||
end | ||
|
||
--- Set the default initialization callback. | ||
-- This callback will be executed before the mouse grabbing start | ||
-- @tparam function cb The callback (or nil) | ||
function module:set_enter_callback(cb) | ||
callbacks.enter = cb | ||
end | ||
|
||
--- Set the default "move" callback. | ||
-- This callback is executed in "after" mode (see `set_mode`) instead of | ||
-- applying the operation. | ||
-- @tparam function cb The callback (or nil) | ||
function module:set_move_callback(cb) | ||
callbacks.move = cb | ||
end | ||
|
||
--- Set the default "leave" callback | ||
-- This callback is executed just before the `mousegrabber` stop | ||
-- @tparam function cb The callback (or nil) | ||
function module:set_leave_callback(cb) | ||
callbacks.leave = cb | ||
end | ||
|
||
--- Ajust the size of a client in a give tag. | ||
-- @tparam client c The client to resize | ||
-- @tparam number dp The pixel delta | ||
-- @tparam string direction The direction ("left", "right", "up", "down") | ||
-- @tparam[opt=c.first_tag] tag t The tag | ||
function module.ajust_geometry(c, dp, direction, t) | ||
-- Validate everything is correct | ||
if (not c) or (not direction) or (not map2[direction]) then return end | ||
|
||
local handler, wrapper = module.get_handler_and_wrapper(c, t) | ||
|
||
if (not handler) or (not wrapper) then return end | ||
|
||
local geo = wrapper_geometry(c) | ||
|
||
-- Add the pixel delta where necessary | ||
local params = map2[direction] | ||
for k, v in ipairs {"x", "width", "y", "height"} do | ||
geo[v] = geo[v] + params[k]*dp | ||
end | ||
|
||
-- Apply the new size | ||
module.update_ratio(handler, c, wrapper, geo) | ||
end | ||
|
||
capi.client.add_signal("request::geometry") | ||
|
||
return module |