--------------------------------------------------------------------------- --- Mouse snapping related functions -- -- @author Julien Danjou <julien@danjou.info> -- @copyright 2008 Julien Danjou -- @submodule mouse --------------------------------------------------------------------------- local aclient = require("awful.client") local resize = require("awful.mouse.resize") local aplace = require("awful.placement") local wibox = require("wibox") local beautiful = require("beautiful") local color = require("gears.color") local shape = require("gears.shape") local cairo = require("lgi").cairo local capi = { root = root, mouse = mouse, screen = screen, client = client, mousegrabber = mousegrabber, } local module = { default_distance = 8 } local placeholder_w = nil local function show_placeholder(geo) if not geo then if placeholder_w then placeholder_w.visible = false end return end placeholder_w = placeholder_w or wibox { ontop = true, bg = color(beautiful.snap_bg or beautiful.bg_urgent or "#ff0000"), } placeholder_w:geometry(geo) local img = cairo.ImageSurface(cairo.Format.A1, geo.width, geo.height) local cr = cairo.Context(img) cr:set_operator(cairo.Operator.CLEAR) cr:set_source_rgba(0,0,0,1) cr:paint() cr:set_operator(cairo.Operator.SOURCE) cr:set_source_rgba(1,1,1,1) local line_width = beautiful.snap_border_width or 5 cr:set_line_width(beautiful.xresources.apply_dpi(line_width)) local f = beautiful.snap_shape or function() cr:translate(line_width,line_width) shape.rounded_rect(cr,geo.width-2*line_width,geo.height-2*line_width, 10) end f(cr, geo.width, geo.height) cr:stroke() placeholder_w.shape_bounding = img._native placeholder_w.visible = true end local function build_placement(snap, axis) return aplace.scale + aplace[snap] + ( axis and aplace["maximize_"..axis] or nil ) end local function detect_screen_edges(c, snap) local coords = capi.mouse.coords() local sg = c.screen.geometry local v, h = nil if math.abs(coords.x) <= snap + sg.x and coords.x >= sg.x then h = "left" elseif math.abs((sg.x + sg.width) - coords.x) <= snap then h = "right" end if math.abs(coords.y) <= snap + sg.y and coords.y >= sg.y then v = "top" elseif math.abs((sg.y + sg.height) - coords.y) <= snap then v = "bottom" end return v, h end local current_snap, current_axis = nil local function detect_areasnap(c, distance) local old_snap = current_snap local v, h = detect_screen_edges(c, distance) if v and h then current_snap = v.."_"..h else current_snap = v or h or nil end if old_snap == current_snap then return end current_axis = ((v and not h) and "horizontally") or ((h and not v) and "vertically") or nil -- Show the expected geometry outline show_placeholder( current_snap and build_placement(current_snap, current_axis)(c, { to_percent = 0.5, honor_workarea = true, pretend = true }) or nil ) end local function apply_areasnap(c, args) if not current_snap then return end -- Remove the move offset args.offset = {} placeholder_w.visible = false return build_placement(current_snap, current_axis)(c,{ to_percent = 0.5, honor_workarea = true, }) end local function snap_outside(g, sg, snap) if g.x < snap + sg.x + sg.width and g.x > sg.x + sg.width then g.x = sg.x + sg.width elseif g.x + g.width < sg.x and g.x + g.width > sg.x - snap then g.x = sg.x - g.width end if g.y < snap + sg.y + sg.height and g.y > sg.y + sg.height then g.y = sg.y + sg.height elseif g.y + g.height < sg.y and g.y + g.height > sg.y - snap then g.y = sg.y - g.height end return g end local function snap_inside(g, sg, snap) local edgev = 'none' local edgeh = 'none' if math.abs(g.x) < snap + sg.x and g.x > sg.x then edgev = 'left' g.x = sg.x elseif math.abs((sg.x + sg.width) - (g.x + g.width)) < snap then edgev = 'right' g.x = sg.x + sg.width - g.width end if math.abs(g.y) < snap + sg.y and g.y > sg.y then edgeh = 'top' g.y = sg.y elseif math.abs((sg.y + sg.height) - (g.y + g.height)) < snap then edgeh = 'bottom' g.y = sg.y + sg.height - g.height end -- What is the dominant dimension? if g.width > g.height then return g, edgeh else return g, edgev end end --- Snap a client to the closest client or screen edge. -- @function awful.mouse.snap -- @param c The client to snap. -- @param snap The pixel to snap clients. -- @param x The client x coordinate. -- @param y The client y coordinate. -- @param fixed_x True if the client isn't allowed to move in the x direction. -- @param fixed_y True if the client isn't allowed to move in the y direction. function module.snap(c, snap, x, y, fixed_x, fixed_y) snap = snap or module.default_distance c = c or capi.client.focus local cur_geom = c:geometry() local geom = c:geometry() geom.width = geom.width + (2 * c.border_width) geom.height = geom.height + (2 * c.border_width) local edge geom.x = x or geom.x geom.y = y or geom.y geom, edge = snap_inside(geom, c.screen.geometry, snap) geom = snap_inside(geom, c.screen.workarea, snap) -- Allow certain windows to snap to the edge of the workarea. -- Only allow docking to workarea for consistency/to avoid problems. if c.dockable then local struts = c:struts() struts['left'] = 0 struts['right'] = 0 struts['top'] = 0 struts['bottom'] = 0 if edge ~= "none" and c.floating then if edge == "left" or edge == "right" then struts[edge] = cur_geom.width elseif edge == "top" or edge == "bottom" then struts[edge] = cur_geom.height end end c:struts(struts) end for _, snapper in ipairs(aclient.visible(c.screen)) do if snapper ~= c then local snapper_geom = snapper:geometry() snapper_geom.width = snapper_geom.width + (2 * snapper.border_width) snapper_geom.height = snapper_geom.height + (2 * snapper.border_width) geom = snap_outside(geom, snapper_geom, snap) end end geom.width = geom.width - (2 * c.border_width) geom.height = geom.height - (2 * c.border_width) -- It's easiest to undo changes afterwards if they're not allowed if fixed_x then geom.x = cur_geom.x end if fixed_y then geom.y = cur_geom.y end return geom end -- Enable edge snapping resize.add_move_callback(function(c, geo, args) -- Screen edge snapping (areosnap) if (module.edge_enabled ~= false) and args and (args.snap == nil or args.snap) then detect_areasnap(c, 16) end -- Snapping between clients if (module.client_enabled ~= false) and args and (args.snap == nil or args.snap) then return module.snap(c, args.snap, geo.x, geo.y) end end, "mouse.move") -- Apply the aerosnap resize.add_leave_callback(function(c, _, args) if module.edge_enabled == false then return end return apply_areasnap(c, args) end, "mouse.move") return setmetatable(module, {__call = function(_, ...) return module.snap(...) end})