Add new helper modules.

This commit begin to merge the Radical "view" rewrite.

Those module API replace previously hardcoded positioning
logic. Some code may eventually be upstreamed once mature
enough.
This commit is contained in:
Emmanuel Lepage Vallee 2016-02-21 02:31:31 -05:00
parent dafac374fa
commit 1711d28dcd
3 changed files with 568 additions and 0 deletions

126
hot_corner.lua Normal file
View File

@ -0,0 +1,126 @@
local capi = {screen = screen}
local wibox = require( "wibox" )
local util = require( "awful.util" )
local timer = require( "gears.timer" )
local placement = require( "radical.placement" )
local module = {}
local wibox_to_req = {}
local corners_geo = {
-- Corners
top_left = function(geo, wa) return {width = 1, height = 1} end,
top_right = function(geo, wa) return {width = 1, height = 1} end,
bottom_left = function(geo, wa) return {width = 1, height = 1} end,
bottom_right = function(geo, wa) return {width = 1, height = 1} end,
-- Sides
left = function(geo, wa) return {width = 1 , height = wa.height } end,
right = function(geo, wa) return {width = 1 , height = wa.height } end,
top = function(geo, wa) return {width = wa.width, height = 1 } end,
bottom = function(geo, wa) return {width = wa.width, height = 1 } end,
}
local function mouse_enter(w)
local req = wibox_to_req[w]
if req and req.enter then
req:enter()
end
end
local function mouse_leave(w)
local req = wibox_to_req[w]
if req and req.leave then
req:leave()
end
end
local function create_hot_corner(corner, s)
local s = s or 1
local s_geo = capi.screen[s].geometry
local w_geo = capi.screen[s].workarea
local size = corners_geo[corner](s_geo, w_geo)
local w = wibox(util.table.crush(size, {ontop=true, opacity = 0, visible=true}))
placement.corner(w, corner, s, false, false)
local req = {wibox = w, screen = s, corner = corner}
w:connect_signal("mouse::enter", mouse_enter)
wibox_to_req[w] = req
return req
end
local function create_visible_timer(w, time, req)
local t = timer{}
t.timeout = time
t:connect_signal("timeout", function()
w.visible = false
req.wibox.visible = true
t:stop()
end)
t:start()
end
function module.register_function(corner, f, s)
if not f then return end
local req = create_hot_corner(corner, s)
req.enter = f
return req
end
--- Show a wibox when `corner` is hit.
--
-- Valid corners are:
--
-- * left
-- * right
-- * top
-- * bottom
-- * top_left
-- * top_right
-- * bottom_left
-- * bottom_right
--
-- @tparam string corner A corner name
-- @param w The wibox or a function returning a wibox in case lazy loading is
-- desirable
-- @tparam[opt=all] number s The screen
-- @tparam[opt=0] number timeout The timeout (in seconds)
-- @return A request handler
function module.register_wibox(corner, w, s, timeout)
if not w then return end
local req = create_hot_corner(corner, s)
local connected = false
function req.enter()
if type(w) == "function" then
w = w(req)
end
if not connected then
w:connect_signal("mouse::leave", mouse_leave)
wibox_to_req[w] = req --FIXME leak
connected = true
end
w.visible = true
end
if not timeout then
function req.leave() w.visible = false; req.wibox.visible = true end
else
function req.leave() create_visible_timer(w, timeout, req) end
end
return req
end
--TODO watch for workarea changes
return module

205
placement.lua Normal file
View File

@ -0,0 +1,205 @@
local capi = {screen=screen, mouse = mouse}
local unpack = unpack or table.unpack
local mouse = require( "awful.mouse" )
local screen = require( "awful.screen" )
local cairo = require( "lgi" ).cairo
local module = {}
-- Compute the new `x` and `y`.
-- The workarea position need to be applied by the caller
local map = {
-- Corners
top_left = function(sw, sh, dw, dh) return {x=0 , y=0 } end,
top_right = function(sw, sh, dw, dh) return {x=sw-dw , y=0 } end,
bottom_left = function(sw, sh, dw, dh) return {x=0 , y=sh-dh } end,
bottom_right = function(sw, sh, dw, dh) return {x=sw-dw , y=sh-dh } end,
left = function(sw, sh, dw, dh) return {x=0 , y=sh/2-dh/2} end,
right = function(sw, sh, dw, dh) return {x=sw-dw , y=sh/2-dh/2} end,
top = function(sw, sh, dw, dh) return {x=sw/2-dw/2, y=0 } end,
bottom = function(sw, sh, dw, dh) return {x=sw/2-dw/2, y=sh-dh } end,
}
-- Create the geometry rectangle 1=best case, 2=fallback
local positions = {
left1 = function(x, y, w, h) return {x = x - w, y = y , width = w, height = h} end,
left2 = function(x, y, w, h) return {x = x - w, y = y - h , width = w, height = h} end,
right1 = function(x, y, w, h) return {x = x , y = y , width = w, height = h} end,
right2 = function(x, y, w, h) return {x = x , y = y - h , width = w, height = h} end,
top1 = function(x, y, w, h) return {x = x , y = y - h , width = w, height = h} end,
top2 = function(x, y, w, h) return {x = x - w, y = y - h , width = w, height = h} end,
bottom1 = function(x, y, w, h) return {x = x , y = y , width = w, height = h} end,
bottom2 = function(x, y, w, h) return {x = x - w, y = y , width = w, height = h} end,
}
-- Check if the proposed geometry fit in the screen
local function fit_in_screen(s, geo)
local sgeo = capi.screen[s].geometry
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 fit, else, it will be cropped
--TODO in case all directions are cropped, keep the least cropped one
return geo2.width == geo.width and geo2.height == geo.height
end
--- Move the drawable (client or wibox) `d` to a screen corner or side.
function module.corner(d, corner, s, honor_wa, update_wa)
local sgeo = capi.screen[s][honor_wa and "workarea" or "geometry"]
local dgeo = d:geometry()
local pos = map[corner](sgeo.width, sgeo.height, dgeo.width, dgeo.height)
d : geometry {
x = math.ceil(sgeo.x + pos.x) ,
y = math.ceil(sgeo.y + pos.y) ,
width = math.ceil(dgeo.width ) ,
height = math.ceil(dgeo.height ) ,
}
--TODO update_wa
end
--- Pin a drawable to a placement function.
-- Auto update the position when the size change
function module.pin(d, f, ...)
--TODO memory leak
local args = {...}
local function tracker()
f(d, unpack(args))
end
d:connect_signal("property::width" , tracker)
d:connect_signal("property::height", tracker)
tracker()
end
--- Get the possible 2D anchor points around a widget geometry.
-- This take into account the widget drawable (wibox) and try to avoid
-- overlapping.
function module.get_relative_points(geo, mode)
local use_mouse = true --TODO support modes
-- The closest points around the geometry
local dps = {}
-- Use the mouse position and the wibox/client under it
if not geo then
local draw = mouse.drawin_under_pointer()
geo = draw and draw:geometry() or capi.mouse.coords()
geo.drawable = draw
elseif geo.x and geo.width then
local coords = capi.mouse.coords()
-- Check id 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 = mouse.drawin_under_pointer()
end
--TODO add drawin_at(x,y) in the C core
end
if geo.drawable then
-- Case 1: A widget
local dgeo = geo.drawable.drawable:geometry()
-- Compute the absolute widget geometry
local abs_widget_geo = {
x = dgeo.x + geo.x,
y = dgeo.y + geo.y,
width = geo.width ,
height = geo.height ,
drawable = geo.drawable ,
}
-- Get the comparaison point
local center_point = use_mouse 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 the 4 cloest points from `center_point` around the wibox
local points = {
left = {x = dgeo.x , y = center_point.y },
right = {x = dgeo.x + dgeo.width , y = center_point.y },
top = {x = center_point.x , y = dgeo.y },
bottom = {x = center_point.y , y = dgeo.y + dgeo.height },
}
local s = geo.drawable.screen or screen.getbycoord(
center_point.x,
center_point.y
)
-- Compute the distance (dp) between the `center_point` and the sides
for k, v in pairs(points) do
local dx, dy = v.x - center_point.x, v.y - center_point.y
dps[k] = {
distance = math.sqrt(dx*dx + dy*dy),
x = v.x,
y = v.y,
screen = s
}
end
else
-- Case 2: A random geometry
--TODO
end
return dps
end
-- @tparam drawable d A wibox or client
-- @tparam table points A table with position as key and points (x,y) as value
-- @tparam[opt={}] table preferred_positions The preferred positions (position as key,
-- and index as value)
-- @treturn string The choosen position
function module.move_relative(d, points, preferred_positions)
local w,h = d.width, d.height
local pref_idx, pref_name = 99, nil
local does_fit = {}
for k,v in pairs(points) do
local geo = positions[k..1](v.x, v.y, w, h)
local fit = fit_in_screen(v.screen, geo)
-- Try the other compatible geometry
if not fit then
geo = positions[k..2](v.x, v.y, w, h)
fit = fit_in_screen(v.screen, geo)
end
does_fit[k] = fit and geo 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 preferred_positions[k] == 1 then break end
end
local pos_name = pref_name or next(does_fit)
local pos = does_fit[pos_name]
if pos then
d.x = math.ceil(pos.x)
d.y = math.ceil(pos.y)
end
return pos_name
end
return module

237
smart_wibox.lua Normal file
View File

@ -0,0 +1,237 @@
---------------------------------------------------------------------------
--- A rather hacky way to create free-floating widgets.
--
-- Hopefully this will be more maintainable then the old Radical hard-coded
-- positioning code.
--
-- @author Emmanuel Lepage Vallee
-- @copyright 2016 Emmanuel Lepage Vallee
-- @release @AWESOME_VERSION@
-- @module radical.smart_wibox
---------------------------------------------------------------------------
local capi = {mouse = mouse, screen = screen}
local wibox = require( "wibox" )
local util = require( "awful.util" )
local surface = require( "gears.surface" )
local glib = require( "lgi" ).GLib
local beautiful = require( "beautiful" )
local color = require( "gears.color" )
local screen = require( "awful.screen" )
local mouse = require( "awful.mouse" )
local placement = require( "radical.placement")
local unpack = unpack or table.unpack
local module = {}
local main_widget = {}
-- Get the optimal direction for the wibox
-- This (try to) avoid going offscreen
local function set_position(self)
local points = rawget(self, "possible_positions") or {}
local preferred_positions = rawget(self, "_preferred_directions") or {}
local pos_name = placement.move_relative(self, points, preferred_positions)
if pos_name ~= rawget(self, "position") then
self:emit_signal("property::position", pos_name)
rawset(self, "position", pos_name)
end
end
--- Fit this widget into the given area
function main_widget:fit(context, width, height)
if not self.widget then
return 0, 0
end
return wibox.widget.base.fit_widget(self, context, self.widget, width, height)
end
--- Layout this widget
function main_widget:layout(context, width, height)
if self.widget then
local w, h = wibox.widget.base.fit_widget(self, context, self.widget, 9999, 9999)
glib.idle_add(glib.PRIORITY_HIGH_IDLE, function()
self._wb.width = math.ceil(w or 1)
self._wb.height = math.ceil(h or 1)
set_position(self._wb)
end)
return { wibox.widget.base.place_widget_at(self.widget, 0, 0, width, height) }
end
end
--- Set the widget that is drawn on top of the background
function main_widget:set_widget(widget)
if widget then
wibox.widget.base.check_widget(widget)
end
self.widget = widget
self:emit_signal("widget::layout_changed")
end
--- Get the number of children element
-- @treturn table The children
function main_widget:get_children()
return {self.widget}
end
--- Replace the layout children
-- This layout only accept one children, all others will be ignored
-- @tparam table children A table composed of valid widgets
function main_widget:set_children(children)
self.widget = children and children[1]
self:emit_signal("widget::layout_changed")
end
function main_widget:before_draw_children(context, cr, width, height)
-- Update the wibox shape bounding. This module use custom painter instead
-- of a shape clip to get antialiasing.
if self._wb._shape and (width ~= self.prev_width or height ~= self.prev_height) then
surface.apply_shape_bounding(self._wb, self._wb._shape, unpack(self._wb._shape_args))
self.prev_width = width
self.prev_height = height
end
-- There is nothing else to do. The wibox background painter will do
end
-- Draw the border after the content to emulate the shape_clip
function main_widget:after_draw_children(context, cr, width, height)
local border_width = self._wb._shape_border_width
if not border_width then return end
cr:translate(border_width/2, border_width/2)
cr:set_line_width(border_width)
cr:set_source(self._wb._shape_border_color)
self._wb._shape(cr, width-border_width, height-border_width, unpack(self._wb._shape_args or {}))
cr:stroke()
end
local wb_func = {}
--- Set the wibox shape.
-- All other paramaters will be passed to the `s` function
-- @param s A `gears.shape` compatible function
function wb_func:set_shape(s, ...)
rawset(self, "_shape" , s )
rawset(self, "_shape_args" , {...} )
self.widget:emit_signal("widget::layout_changed")
end
--- Set the wibox shape border color.
-- Note that this is independant from the wibox border_color.
-- The default are `beautiful.menu_border_color` or `beautiful.border_color`.
-- The there is no border, then this function will do nothing.
-- @param The border color or nil
function wb_func:set_shape_border_color(col)
rawset(self,"_shape_border_color", col and color(col) or color(beautiful.menu_border_color or beautiful.border_color))
self.widget:emit_signal("widget::layout_changed")
end
--- Set the shape border (clip) width.
-- The shape will be used to draw the border. Any content within the border
-- will be hidden.
-- @tparam number width The border width
function wb_func:set_shape_border_width(width)
rawset(self,"_shape_border_width", width)
self.widget:emit_signal("widget::layout_changed")
end
--- Set the preferred wibox directions relative to its parent.
-- Valid directions are:
-- * left
-- * right
-- * top
-- * bottom
-- @tparam string ... One of more directions (in the preferred order)
function wb_func:set_preferred_positions(...)
local dirs = {}
for k, v in ipairs{...} do
dirs[v] = k
end
rawset(self, "_preferred_directions", dirs)
end
--- Move the wibox to a position relative to `geo`.
-- This will try to avoid overlapping the source wibox and auto-detect the right
-- direction to avoid going off-screen.
-- @param[opt=mouse.coords()] geo A geometry table. It is given as parameter
-- from buttons callbacks and signals such as `mouse::enter`.
-- @param use_mouse Use the mouse position instead of the widget center as
-- reference point.
function wb_func:move_by_parent(geo, use_mouse)
local dps = placement.get_relative_points(d, rgeo, mode)
rawset(self, "possible_positions", dps)
set_position(self)
end
function wb_func:move_by_mouse()
--TODO
end
function wb_func:set_voffset(offset)
end
function wb_func:set_hoffset(offset)
end
--- A brilliant idea to totally turn the whole hierarchy on its head
-- and create a widget that own a wibox...
local function create_auto_resize_widget(self, wdg, args)
assert(wdg)
local ii = wibox.widget.base.make_widget()
util.table.crush(ii, main_widget)
ii:set_widget(wdg)
-- Create a wibox to host the widget
local w = wibox(args or {})
-- Wibox use metatable inheritance, rawset is necessary
for k, v in pairs(wb_func) do
rawset(w, k, v)
end
-- Cross-link the wibox and widget
ii._wb = w
w:set_widget(ii)
rawset(w, "widget", wdg)
-- Changing the widget is not supported
rawset(w, "set_widget", function()end)
w:set_shape_border_color()
w:add_signal("property::position")
if args and args.preferred_positions then
if type(args.preferred_positions) == "table" then
w:set_preferred_positions(unpack(args.preferred_positions))
else
w:set_preferred_positions(args.preferred_positions)
end
end
if args.shape then
w:set_shape(args.shape, unpack(args.shape_args or {}))
end
for k,v in ipairs{"shape_border_color", "shape_border_width"} do
if args[v] then
w["set_"..v](w, args[v])
end
end
return w
end
return setmetatable(module, {__call = create_auto_resize_widget})