From 20030e6f932d222e074d6d6d11d71f4be389e379 Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sat, 16 Jan 2016 01:13:07 -0500 Subject: [PATCH 1/2] wibox.layout: Add new 'stack' layout This layout display the widgets on top of each other. It can also optionally display only the first one. The most common use case is to create a composited widget. Other use case include the creation of a "paged" stack to only display the most relevant widget without adding extra complexity to the parent layout. --- lib/wibox/layout/init.lua | 1 + lib/wibox/layout/stack.lua | 188 +++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 lib/wibox/layout/stack.lua diff --git a/lib/wibox/layout/init.lua b/lib/wibox/layout/init.lua index f9e1c355..66fbafec 100644 --- a/lib/wibox/layout/init.lua +++ b/lib/wibox/layout/init.lua @@ -17,6 +17,7 @@ return mirror = require("wibox.layout.mirror"); constraint = require("wibox.layout.constraint"); scroll = require("wibox.layout.scroll"); + stack = require("wibox.layout.stack"); } -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/layout/stack.lua b/lib/wibox/layout/stack.lua new file mode 100644 index 00000000..ec11febc --- /dev/null +++ b/lib/wibox/layout/stack.lua @@ -0,0 +1,188 @@ +--------------------------------------------------------------------------- +-- A stacked layout. +-- +-- This layout display widgets on top of each other. It can be used to overlay +-- a `wibox.widget.textbox` on top of a `awful.widget.progressbar` or manage +-- "pages" where only one is visible at any given moment. +-- +-- The indices are going from 1 (the bottom of the stack) up to the top of +-- the stack. The order can be changed either using `:swap` or `:raise`. +-- +-- @author Emmanuel Lepage Vallee +-- @copyright 2016 Emmanuel Lepage Vallee +-- @release @AWESOME_VERSION@ +-- @classmod wibox.layout.stack +--------------------------------------------------------------------------- + +local base = require("wibox.widget.base" ) +local fixed = require("wibox.layout.fixed") +local table = table +local pairs = pairs +local floor = math.floor +local util = require("awful.util") + +local stack = {mt={}} + +--- Get all direct children widgets +-- @param layout The layout you are modifying. +-- @return a list of all widgets +-- @name get_children +-- @class function + +--- Add some widgets to the given stack layout +-- @param layout The layout you are modifying. +-- @tparam widget ... Widgets that should be added (must at least be one) +-- @name add +-- @class function + +--- Set a widget at a specific index, replace the current one +-- @tparam number index A widget or a widget index +-- @param widget2 The widget to take the place of the first one +-- @treturn boolean If the operation is successful +-- @name set +-- @class function + +--- Remove a widget from the layout +-- @tparam index The widget index to remove +-- @treturn boolean index If the operation is successful +-- @name remove +-- @class function + +--- Reset a stack layout. This removes all widgets from the layout. +-- @param layout The layout you are modifying. +-- @name reset +-- @class function + +--- Replace the first instance of `widget` in the layout with `widget2` +-- @param widget The widget to replace +-- @param widget2 The widget to replace `widget` with +-- @tparam[opt=false] boolean recursive Digg in all compatible layouts to find the widget. +-- @treturn boolean If the operation is successful +-- @name replace_widget +-- @class function + +--- Swap 2 widgets in a layout +-- @tparam number index1 The first widget index +-- @tparam number index2 The second widget index +-- @treturn boolean If the operation is successful +-- @name swap +-- @class function + +--- Swap 2 widgets in a layout +-- If widget1 is present multiple time, only the first instance is swapped +-- @param widget1 The first widget +-- @param widget2 The second widget +-- @tparam[opt=false] boolean recursive Digg in all compatible layouts to find the widget. +-- @treturn boolean If the operation is successful +-- @name swap_widgets +-- @class function + +--- Insert a new widget in the layout at position `index` +-- @tparam number index The position +-- @param widget The widget +-- @treturn boolean If the operation is successful +-- @name insert +-- @class function + +--- Remove one or more widgets from the layout +-- The last parameter can be a boolean, forcing a recursive seach of the +-- widget(s) to remove. +-- @param widget ... Widgets that should be removed (must at least be one) +-- @treturn boolean If the operation is successful +-- @name remove_widgets +-- @class function + +--- Add spacing between each layout widgets +-- @param spacing Spacing between widgets. +-- @name set_spacing +-- @class function + +--- Layout a stack layout. Each widget get drawn on top of each other +-- @param context The context in which we are drawn. +-- @param width The available width. +-- @param height The available height. +function stack:layout(context, width, height) + local result = {} + local spacing = self._spacing + + for k, v in pairs(self.widgets) do + table.insert(result, base.place_widget_at(v, spacing, spacing, width - 2*spacing, height - 2*spacing)) + if self._top_only then break end + end + + return result +end + +--- Fit the stack layout into the given space +-- @param context The context in which we are fit. +-- @param orig_width The available width. +-- @param orig_height The available height. +function stack:fit(context, orig_width, orig_height) + local max_w, max_h = 0,0 + local spacing = self._spacing + + for k, v in pairs(self.widgets) do + local w, h = base.fit_widget(self, context, v, orig_width, orig_height) + max_w, max_h = math.max(max_w, w+2*spacing), math.max(max_h, h+2*spacing) + end + + return math.min(max_w, orig_width), math.min(max_h, orig_height) +end + +--- Get if only the first stack widget is drawn +-- @return If the only the first stack widget is drawn +function stack:get_display_top_only() + return self._top_only +end + +--- Only draw the first widget of the stack, ignore others +-- @tparam boolean top_only Only draw the top stack widget +function stack:set_display_top_only(top_only) + self._top_only = top_only +end + +--- Raise a widget at `index` to the top of the stack +-- @tparam number index the widget index to raise +function stack:raise(index) + if (not index) or self.widgets[index] then return end + + local w = self.widgets[index] + table.remove(self.widgets, index) + table.insert(self.widgets, w) + + self:emit_signal("widget::layout_changed") +end + +--- Raise the first instance of `widget` +-- @param widget The widget to raise +-- @tparam[opt=false] boolean recursive Also look deeper in the hierarchy to +-- find the widget +function stack:raise_widget(widget, recursive) + local idx, layout = self:index(widget, recursive) + + if not idx or not layout then return end + + -- Bubble up in the stack until the right index is found + while layout and layout ~= self do + idx, layout = self:index(layout, recursive) + end + + if layout == self and idx ~= 1 then + self:raise(idx) + end +end + +local function new(dir, widget1, ...) + local ret = fixed.horizontal(...) + + util.table.crush(ret, stack) + + return ret +end + +function stack.mt:__call(...) + return new(...) +end + +return setmetatable(stack, stack.mt) +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 From 0a39d196c7144e386c4924598b8f8f0418c8070a Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Sun, 10 Jan 2016 02:39:14 -0500 Subject: [PATCH 2/2] wibox.layout: Add the new 'ratio' layout This layout allow each widgets to take 'r' percent of the total space, where 'r' is configurable. It re-implement the 'wfact' system used by `awful.layout.suit.tile` --- lib/wibox/layout/init.lua | 1 + lib/wibox/layout/ratio.lua | 364 +++++++++++++++++++++++++++++++++++++ 2 files changed, 365 insertions(+) create mode 100644 lib/wibox/layout/ratio.lua diff --git a/lib/wibox/layout/init.lua b/lib/wibox/layout/init.lua index 66fbafec..386d9f19 100644 --- a/lib/wibox/layout/init.lua +++ b/lib/wibox/layout/init.lua @@ -17,6 +17,7 @@ return mirror = require("wibox.layout.mirror"); constraint = require("wibox.layout.constraint"); scroll = require("wibox.layout.scroll"); + ratio = require("wibox.layout.ratio"); stack = require("wibox.layout.stack"); } diff --git a/lib/wibox/layout/ratio.lua b/lib/wibox/layout/ratio.lua new file mode 100644 index 00000000..201e1de6 --- /dev/null +++ b/lib/wibox/layout/ratio.lua @@ -0,0 +1,364 @@ +--------------------------------------------------------------------------- +--- A layout filling all the available space. Each widget is assigned a +-- ratio (percentage) of the total space. Multiple methods are available to +-- ajust this ratio. +-- @author Emmanuel Lepage Vallee +-- @copyright 2016 Emmanuel Lepage Vallee +-- @release @AWESOME_VERSION@ +-- @classmod wibox.layout.ratio +--------------------------------------------------------------------------- + +local base = require("wibox.widget.base" ) +local flex = require("wibox.layout.flex" ) +local fixed = require("wibox.layout.fixed") +local table = table +local pairs = pairs +local floor = math.floor +local util = require("awful.util") + +local ratio = {} + +--- Set a widget at a specific index, replace the current one +-- @tparam number index A widget or a widget index +-- @param widget2 The widget to take the place of the first one +-- @treturn boolean If the operation is successful +-- @name set +-- @class function + +--- Replace the first instance of `widget` in the layout with `widget2` +-- @param widget The widget to replace +-- @param widget2 The widget to replace `widget` with +-- @tparam[opt=false] boolean recursive Digg in all compatible layouts to find the widget. +-- @treturn boolean If the operation is successful +-- @name replace_widget +-- @class function + +--- Swap 2 widgets in a layout +-- @tparam number index1 The first widget index +-- @tparam number index2 The second widget index +-- @treturn boolean If the operation is successful +-- @name swap +-- @class function + +--- Swap 2 widgets in a layout +-- If widget1 is present multiple time, only the first instance is swapped +-- @param widget1 The first widget +-- @param widget2 The second widget +-- @tparam[opt=false] boolean recursive Digg in all compatible layouts to find the widget. +-- @treturn boolean If the operation is successful +-- @name swap_widgets +-- @class function + +--- Get all children of this layout. +-- @param layout The layout you are modifying. +-- @return a list of all widgets +-- @name get_children +-- @class function + +--- Fit the ratio layout into the given space +-- @param layout The layout you are modifying. +-- @param context The context in which we are fit. +-- @tparam number orig_width The available width. +-- @tparam number orig_height The available height. +-- @name fit +-- @class function + +--- Reset a ratio layout. This removes all widgets from the layout. +-- @param layout The layout you are modifying. +-- @name reset +-- @class function + +-- Compute the sum of all ratio (ideally, it should be 1) +local function gen_sum(self, i_s, i_e) + local sum, new_w = 0,0 + + for i = i_s or 1, i_e or #self.widgets do + if self._ratios[i] then + sum = sum + self._ratios[i] + else + new_w = new_w + 1 + end + end + + return sum, new_w +end + +-- The ratios are expressed as percentages. For this to work, the sum of all +-- ratio must be 1. This function attempt to ajust them. Space can be taken +-- from or added to a ratio when widgets are being added or removed. If a +-- specific ratio must be enforced for a widget, it has to be done with the +-- `ajust_ratio` method after each insertion or deletion +local function normalize(self) + local count = #self.widgets + if count == 0 then return end + + -- Instead of adding "if" everywhere, just handle this common case + if count == 1 then + self._ratios = { 1 } + return + end + + local sum, new_w = gen_sum(self) + local old_count = #self.widgets - new_w + + local to_add = (sum == 0) and 1 or (sum / old_count) + + -- Make sure all widgets have a ratio + for i=1, #self.widgets do + if not self._ratios[i] then + self._ratios[i] = to_add + end + end + + sum = sum + to_add*new_w + + local delta, new_sum = (1 - sum) / count,0 + + -- Increase or decrease each ratio so it the sum become 1 + for i=1, #self.widgets do + self._ratios[i] = self._ratios[i] + delta + new_sum = new_sum + self._ratios[i] + end + + -- Floating points is not an exact science, but it should still be close + -- to 1.00. + assert(new_sum > 0.99 and new_sum < 1.01) +end + +--- Layout a ratio layout. Each widget gets a share of the size proportional +-- to its ratio +-- @param context The context in which we are drawn. +-- @tparam number width The available width. +-- @tparam number height The available height. +function ratio:layout(context, width, height) + local result = {} + local pos,spacing = 0, self._spacing + local num = #self.widgets + local total_spacing = (spacing*(num-1)) + + --normalize(self) + + for k, v in ipairs(self.widgets) do + local space = nil + local x, y, w, h + + if self.dir == "y" then + space = height * self._ratios[k] + x, y = 0, util.round(pos) + w, h = width, floor(space) + else + space = width * self._ratios[k] + x, y = util.round(pos), 0 + w, h = floor(space), height + end + + table.insert(result, base.place_widget_at(v, x, y, w, h)) + + pos = pos + space + spacing + + -- Make sure all widgets fit in the layout, if they aren't, something + -- went wrong + if (self.dir == "y" and util.round(pos) >= height) or + (self.dir ~= "y" and util.round(pos) >= width) then + break + end + end + + return result +end + +--- Increase the ratio of "widget" +-- If the increment produce an invalid ratio (not between 0 and 1), the method +-- do nothing. +-- @tparam number index The widget index to change +-- @tparam number increment An floating point value between -1 and 1 where the +-- end result is within 0 and 1 +function ratio:inc_ratio(index, increment) + if #self.widgets == 1 or (not index) or (not self._ratios[index]) + or increment < -1 or increment > 1 then + return + end + + local widget = self.widgets[index] + + assert(self._ratios[index]) + + self:set_ratio(index, self._ratios[index] + increment) +end + +--- Increment the ratio of the first instance of `widget` +-- If the increment produce an invalid ratio (not between 0 and 1), the method +-- do nothing. +-- @param widget The widget to ajust +-- @tparam number increment An floating point value between -1 and 1 where the +-- end result is within 0 and 1 +function ratio:inc_widget_ratio(widget, increment) + if not widget or not increment then return end + + local index = self:index(widget) + + self:inc_ratio(index, increment) +end + +--- Set the ratio of the widget at position `index` +-- @tparam number index The index of the widget to change +-- @tparam number percent An floating point value between 0 and 1 +function ratio:set_ratio(index, percent) + if not percent or #self.widgets == 1 or not index or not self.widgets[index] + or percent < 0 or percent > 1 then + return + end + + local old = self._ratios[index] + + -- Remove what has to be cleared from all widget + local delta = ( (percent-old) / (#self.widgets-1) ) + + for k, v in pairs(self.widgets) do + self._ratios[k] = self._ratios[k] - delta + end + + -- Set the new ratio + self._ratios[index] = percent + + -- As some widgets may now have a slightly negative ratio, normalize again + normalize(self) + + self:emit_signal("widget::layout_changed") +end + +--- Set the ratio of "widget" +-- @param widget The widget to ajust +-- @tparam number percent An floating point value between 0 and 1 +function ratio:set_widget_ratio(widget, percent) + local index = self:index(widget) + + self:set_ratio(index, percent) +end + +--- Update all widgets to match a set of a ratio. +-- The sum of before, itself and after must be 1 or nothing will be done +-- @tparam number index The index of the widget to change +-- @tparam number before The sum of the ratio before the widget +-- @tparam number itself The ratio for "widget" +-- @tparam number after The sum of the ratio after the widget +function ratio:ajust_ratio(index, before, itself, after) + if not self.widgets[index] or not before or not itself or not after then + return + end + + local sum = before + itself + after + + -- As documented, it is the caller job to come up with valid numbers + if math.min(before, itself, after) < 0 then return end + if sum > 1.01 or sum < -0.99 then return end + + -- Compute the before and after offset to be applied to each widgets + local before_count, after_count = index-1, #self.widgets - index + + local b, a = gen_sum(self, 1, index-1), gen_sum(self, index+1) + + local db, da = (before - b)/before_count, (after - a)/after_count + + -- Apply the new ratio + self._ratios[index] = itself + + -- Equality split the delta among widgets before and after + for i = 1, index -1 do + self._ratios[i] = self._ratios[i] + db + end + for i = index+1, #self.widgets do + self._ratios[i] = self._ratios[i] + da + end + + -- Remove potential negative ratio + normalize(self) + + self:emit_signal("widget::layout_changed") +end + +--- Update all widgets to match a set of a ratio +-- @param widget The widget to ajust +-- @tparam number before The sum of the ratio before the widget +-- @tparam number itself The ratio for "widget" +-- @tparam number after The sum of the ratio after the widget +function ratio:ajust_widget_ratio(widget, before, itself, after) + local index = self:index(widget) + self:ajust_ratio(index, before, itself, after) +end + +--- Add some widgets to the given fixed layout +-- @tparam widget ... Widgets that should be added (must at least be one) +function ratio:add(...) + -- No table.pack in Lua 5.1 :-( + local args = { n=select('#', ...), ... } + assert(args.n > 0, "need at least one widget to add") + for i=1, args.n do + base.check_widget(args[i]) + table.insert(self.widgets, args[i]) + end + + normalize(self) + self:emit_signal("widget::layout_changed") +end + +--- Remove a widget from the layout +-- @tparam number index The widget index to remove +-- @treturn boolean index If the operation is successful +function ratio:remove(index) + if not index or not self.widgets[index] then return false end + + table.remove(self._ratios, index) + table.remove(self.widgets, index) + + normalize(self) + + self:emit_signal("widget::layout_changed") + + return true +end + +--- Insert a new widget in the layout at position `index` +-- @tparam number index The position +-- @param widget The widget +function ratio:insert(index, widget) + if not index or index < 1 or index > #self.widgets + 1 then return false end + + base.check_widget(widget) + + table.insert(self.widgets, index, widget) + + normalize(self) + + self:emit_signal("widget::layout_changed") +end + +local function get_layout(dir, widget1, ...) + local ret = flex[dir](widget1, ...) + + util.table.crush(ret, ratio) + + ret.fill_space = nil + + ret._ratios = {} + + return ret +end + +--- Returns a new horizontal ratio layout. A ratio layout shares the available space +-- equally among all widgets. Widgets can be added via :add(widget). +-- @tparam widget ... Widgets that should be added to the layout. +function ratio.horizontal(...) + return get_layout("horizontal", ...) +end + +--- Returns a new vertical ratio layout. A ratio layout shares the available space +-- equally among all widgets. Widgets can be added via :add(widget). +-- @tparam widget ... Widgets that should be added to the layout. +function ratio.vertical(...) + return get_layout("vertical", ...) +end + +return ratio + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80