From 788708ea450334771e852e19d59837f46c908e48 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 21 Feb 2016 02:31:31 -0500 Subject: [PATCH] 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. --- hot_corner.lua | 126 +++++++++++++++++++++++++ placement.lua | 205 +++++++++++++++++++++++++++++++++++++++++ smart_wibox.lua | 237 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 568 insertions(+) create mode 100644 hot_corner.lua create mode 100644 placement.lua create mode 100644 smart_wibox.lua diff --git a/hot_corner.lua b/hot_corner.lua new file mode 100644 index 0000000..1dd0c1a --- /dev/null +++ b/hot_corner.lua @@ -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 \ No newline at end of file diff --git a/placement.lua b/placement.lua new file mode 100644 index 0000000..c3864e0 --- /dev/null +++ b/placement.lua @@ -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 \ No newline at end of file diff --git a/smart_wibox.lua b/smart_wibox.lua new file mode 100644 index 0000000..c04c2c1 --- /dev/null +++ b/smart_wibox.lua @@ -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}) \ No newline at end of file