Skip to content

Commit

Permalink
layout.dynamic: Add a generic resize handler
Browse files Browse the repository at this point in the history
It look in the widget hierarchy for layouts that support ratio
resizing, then set the new size.
  • Loading branch information
Elv13 committed Feb 11, 2016
1 parent 41a7ad5 commit 0cfa30d
Show file tree
Hide file tree
Showing 2 changed files with 370 additions and 0 deletions.
2 changes: 2 additions & 0 deletions lib/awful/layout/dynamic/base.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ local client = require( "awful.client" )
local hierarchy = require( "wibox.hierarchy" )
local aw_layout = require( "awful.layout" )
local cairo = require( "lgi" ).cairo
local resize = require( "awful.layout.dynamic.resize" )
local l_wrapper = require( "awful.layout.dynamic.wrapper" )
local xresources = require( "beautiful.xresources" )

Expand Down Expand Up @@ -274,6 +275,7 @@ function module.register(name, bl, ...)
l_obj.arrange = l_obj.arrange or function() end

l_obj.mouse_resize_handler = l.mouse_resize_handler
or resize.generic_mouse_resize_handler

--TODO implement :reset() here

Expand Down
368 changes: 368 additions & 0 deletions lib/awful/layout/dynamic/resize.lua
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

0 comments on commit 0cfa30d

Please sign in to comment.