From 9a3cdab534a9507a366cd1d2acb960e03a72b705 Mon Sep 17 00:00:00 2001 From: getzze Date: Fri, 10 Feb 2017 00:10:01 +0000 Subject: [PATCH 1/2] Add grid layout Remove dependency to awful and remove get_cell_sizes function Remove matrix and add superpose property Update documentation, remove beautiful dep Add insert, extend and remove columns and rows Change x and y to horizontal and vertical --- lib/wibox/layout/grid.lua | 935 ++++++++++++++++++++++++++++++++++++++ lib/wibox/layout/init.lua | 1 + 2 files changed, 936 insertions(+) create mode 100644 lib/wibox/layout/grid.lua diff --git a/lib/wibox/layout/grid.lua b/lib/wibox/layout/grid.lua new file mode 100644 index 00000000..d4b35766 --- /dev/null +++ b/lib/wibox/layout/grid.lua @@ -0,0 +1,935 @@ +--------------------------------------------------------------------------- +--- A grid layout. +-- +-- Widgets spanning several columns or rows cannot be included using the +-- declarative system. +-- Instead, create the grid layout and call the `add_widget_at` method. +-- +--@DOC_wibox_layout_grid_imperative_EXAMPLE@ +-- +-- Using the declarative system, widgets are automatically added next to each +-- other spanning only one cell. +-- +--@DOC_wibox_layout_defaults_grid_EXAMPLE@ +-- @author getzze +-- @copyright 2017 getzze +-- @classmod wibox.layout.grid +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) +local table = table +local pairs = pairs +local ipairs = ipairs +local math = math +local util = require("awful.util") +local base = require("wibox.widget.base") + +local grid = { mt = {} } + +local properties = { + "orientation", "superpose", + "forced_num_rows", "forced_num_cols", + "min_cols_size", "min_rows_size", + } + +local dir_properties = { "spacing", "homogeneous", "expand" } + + + +--- Set the preferred orientation of the grid layout. +-- +-- Allowed values are "horizontal" and "vertical". +-- When calling `get_next_empty`, empty cells are browsed differently: +-- +-- * for "horizontal", the grid can be extended horizontally. The current +-- column is filled first; if no empty cell is found up to `forced_num_rows`, +-- the next column is filled, creating it if it does not exist. +-- +-- * for "vertical", the grid can be extended vertically. The current row is +-- filled first; if no empty cell is found up to `forced_num_cols`, the next +-- row is filled, creating it if it does not exist. +-- +--@DOC_wibox_layout_grid_orientation_EXAMPLE@ +-- @param[opt="vertical"] string Preferred orientation: "horizontal" or "vertical". +-- @property orientation + +--- Allow to superpose widgets in the same cell. +-- If false, check before adding a new widget if it will superpose with another +-- widget and prevent from adding it. +-- +--@DOC_wibox_layout_grid_superpose_EXAMPLE@ +-- @param[opt=false] boolean +-- @property superpose + +--- Force the number of rows of the layout. +-- @property forced_num_rows +-- @tparam[opt=nil] number|nil number Forced number of rows (`nil` for automatic). + +--- Force the number of columns of the layout. +-- @property forced_num_cols +-- @tparam[opt=nil] number|nil number Forced number of columns (`nil` for automatic). + +--- Set the minimum size for the columns. +-- +--@DOC_wibox_layout_grid_min_size_EXAMPLE@ +-- @param[opt=0] number Minimum size of the columns. +-- @property min_cols_size + +--- Set the minimum size for the rows. +-- @param[opt=0] number Minimum size of the rows. +-- @property min_rows_size + +--- The spacing between columns. +-- +-- see `spacing` +-- +-- @param[opt=0] number The spacing +-- @property horizontal_spacing + +--- The spacing between rows. +-- +-- see `spacing` +-- +-- @param[opt=0] number The spacing +-- @property vertical_spacing + +--- The spacing between rows and columns. +-- Set both `horizontal_spacing` and `vertical_spacing` to the same value. +-- Get the value `horizontal_spacing` or `vertical_spacing` defined by the +-- preferred `orientation`. +-- +--@DOC_wibox_layout_grid_spacing_EXAMPLE@ +-- @param[opt=0] number The spacing +-- @property spacing + +--- Controls if the columns are expanded to use all the available width. +-- +-- see `expand` +-- +-- @param[opt=false] boolean Expand the grid into the available space +-- @property horizontal_expand + +--- Controls if the rows are expanded to use all the available height. +-- +-- see `expand` +-- +-- @param[opt=false] boolean Expand the grid into the available space +-- @property vertical_expand + +--- Controls if the columns/rows are expanded to use all the available space. +-- Set both `horizontal_expand` and `vertical_expand` to the same value. +-- Get the value `horizontal_expand` or `vertical_expand` defined by the +-- preferred `orientation`. +-- +--@DOC_wibox_layout_grid_expand_EXAMPLE@ +-- @param[opt=false] boolean Expand the grid into the available space +-- @property expand + +--- Controls if the columns all have the same width or if the width of each +-- column depends on the content. +-- +-- see `homogeneous` +-- +-- @param[opt=true] boolean All the columns have the same width. +-- @property horizontal_homogeneous + +--- Controls if the rows all have the same height or if the height of each row +-- depends on the content. +-- +-- see `homogeneous` +-- +-- @param[opt=true] boolean All the rows have the same height. +-- @property vertical_homogeneous + +--- Controls if the columns/rows all have the same size or if the size depends +-- on the content. +-- Set both `horizontal_homogeneous` and `vertical_homogeneous` to the same value. +-- Get the value `horizontal_homogeneous` or `vertical_homogeneous` defined +-- by the preferred `orientation`. +-- +--@DOC_wibox_layout_grid_expand_EXAMPLE@ +-- @param[opt=true] boolean All the columns/rows have the same size. +-- @property homogeneous + +--- Child widget position. Return of `get_widget_position`. +-- @field row Top row index +-- @field col Left column index +-- @field row_span Number of rows to span +-- @field col_span Number of columns to span +-- @table position + +-- Return the maximum value of a table. +local function max_value(t) + local m = 0 + for _,v in ipairs(t) do + if m < v then m = v end + end + return m +end + +-- Return the sum of the values in the table. +local function sum_values(t) + local m = 0 + for _,v in ipairs(t) do + m = m + v + end + return m +end + + +-- Find a widget in a widget_table, by matching the coordinates. +-- Using the `row`:`col` coordinates, and the spans `row_span` and `col_span` +-- @tparam table widgets_table Table of the widgets present in the grid +-- @tparam number row Row number for the top left corner of the widget +-- @tparam number col Column number for the top left corner of the widget +-- @tparam number row_span The number of rows the widget spans (default to 1) +-- @tparam number col_span The number of columns the widget spans (default to 1) +-- @treturn table Table of index of widget_table +local function find_widgets_at(widgets_table, row, col, row_span, col_span) + if not row or row < 1 or not col or col < 1 then return nil end + row_span = (row_span and row_span > 0) and row_span or 1 + col_span = (col_span and col_span > 0) and col_span or 1 + local ret = {} + for index, data in ipairs(widgets_table) do + -- If one rectangular widget is on left side of other + local test_horizontal = not (row > data.row + data.row_span - 1 + or data.row > row + row_span - 1) + -- If one rectangular widget is above other + local test_vertical = not (col > data.col + data.col_span - 1 + or data.col > col + col_span - 1) + if test_horizontal and test_vertical then + table.insert(ret, index) + end + end + -- reverse sort for safe removal of indices + table.sort(ret, function(a,b) return a>b end) + return #ret > 0 and ret or nil +end + + +-- Find a widget in a widget_table, by matching the object. +-- @tparam table widgets_table Table of the widgets present in the grid +-- @param widget The widget to find +-- @treturn number|nil The index of the widget in widget_table, `nil` if not found +local function find_widget(widgets_table, widget) + for index, data in ipairs(widgets_table) do + if data.widget == widget then + return index + end + end + return nil +end + +--- Get the number of rows and columns occupied by the widgets in the grid. +-- @treturn number,number The number of rows and columns +function grid:get_dimension() + return self._private.num_rows, self._private.num_cols +end + +-- Update the number of rows and columns occupied by the widgets in the grid. +local function update_dimension(self) + local num_rows, num_cols = 0, 0 + if self._private.forced_num_rows then + num_rows = self._private.forced_num_rows + end + if self._private.forced_num_cols then + num_cols = self._private.forced_num_cols + end + + for _, data in ipairs(self._private.widgets) do + num_rows = math.max(num_rows, data.row + data.row_span - 1) + num_cols = math.max(num_cols, data.col + data.col_span - 1) + end + self._private.num_rows = num_rows + self._private.num_cols = num_cols +end + + +--- Find the next available cell to insert a widget. +-- The grid is browsed according to the `orientation`. +-- @tparam[opt=1] number hint_row The row coordinate of the last occupied cell. +-- @tparam[opt=1] number hint_column The column coordinate of the last occupied cell. +-- @return number,number The row,column coordinate of the next empty cell +function grid:get_next_empty(hint_row, hint_column) + local row = (hint_row and hint_row > 0) and hint_row or 1 + local column = (hint_column and hint_column > 0) and hint_column or 1 + + local next_field + if self._private.orientation == "vertical" then + next_field = function(x, y) + if y < self._private.num_cols then + return x, y+1 + end + return x+1,1 + end + elseif self._private.orientation == "horizontal" then + next_field = function(x, y) + if x < self._private.num_rows then + return x+1, y + end + return 1,y+1 + end + end + while true do + if find_widgets_at(self._private.widgets, row, column, 1, 1) == nil then + return row, column + end + row, column = next_field(row, column) + end +end + + +--- Add some widgets to the given grid layout. +-- The widgets are assumed to span one cell +-- @param ... Widgets that should be added (must at least be one) +function grid:add(...) + local args = { n=select('#', ...), ... } + assert(args.n > 0, "need at least one widget to add") + local row, column + for i=1, args.n do + local w = args[i] + -- Get the next empty coordinate to insert the widget + row, column = self:get_next_empty(row, column) + self:add_widget_at(w, row, column, 1, 1) + end +end + +--- Add a widget to the grid layout at specific coordinate. +-- +--@DOC_wibox_layout_grid_add_EXAMPLE@ +-- @param child Widget that should be added +-- @tparam number row Row number for the top left corner of the widget +-- @tparam number col Column number for the top left corner of the widget +-- @tparam[opt=1] number row_span The number of rows the widget spans. +-- @tparam[opt=1] number col_span The number of columns the widget spans. +-- @treturn boolean index If the operation is successful +function grid:add_widget_at(child, row, col, row_span, col_span) + if not row or row < 1 or not col or col < 1 then return false end + row_span = (row_span and row_span > 0) and row_span or 1 + col_span = (col_span and col_span > 0) and col_span or 1 + + -- check if the object is a widget + base.check_widget(child) + + -- test if the new widget superpose with existing ones + local superpose = find_widgets_at( + self._private.widgets, row, col, row_span, col_span + ) + + if not self._private.superpose and superpose then + return false + end + + -- Add grid information attached to the widget + local child_data = { + widget = child, + row = row, + col = col, + row_span = row_span, + col_span = col_span + } + table.insert(self._private.widgets, child_data) + + -- Update the row and column numbers + self._private.num_rows = math.max(self._private.num_rows, row + row_span - 1) + self._private.num_cols = math.max(self._private.num_cols, col + col_span - 1) + + self:emit_signal("widget::layout_changed") + self:emit_signal("widget::redraw_needed") + return true +end + +--- Remove one or more widgets from the layout. +-- @param ... Widgets that should be removed (must at least be one) +-- @treturn boolean If the operation is successful +function grid:remove(...) + local args = { ... } + local ret = false + for _, rem_widget in ipairs(args) do + local index = find_widget(self._private.widgets, rem_widget) + if index ~= nil then + table.remove(self._private.widgets, index) + ret = true + end + end + if ret then + -- Recalculate num_rows and num_cols + update_dimension(self) + self:emit_signal("widget::layout_changed") + self:emit_signal("widget::redraw_needed") + end + return ret +end + + +--- Remove widgets at the coordinates. +-- +--@DOC_wibox_layout_grid_remove_EXAMPLE@ +-- @tparam number row The row coordinate of the widget to remove +-- @tparam number col The column coordinate of the widget to remove +-- @tparam[opt=1] number row_span The number of rows the area to remove spans. +-- @tparam[opt=1] number col_span The number of columns the area to remove spans. +-- @treturn boolean If the operation is successful (widgets found) +function grid:remove_widgets_at(row, col, row_span, col_span) + local widget_indices = find_widgets_at( + self._private.widgets, row, col, row_span, col_span + ) + if widget_indices == nil then return false end + + for _,index in ipairs(widget_indices) do + table.remove(self._private.widgets, index) + end + -- Recalculate num_rows and num_cols + update_dimension(self) + self:emit_signal("widget::layout_changed") + self:emit_signal("widget::redraw_needed") + return true +end + +--- Return the coordinates of the widget. +-- @param widget The widget +-- @treturn table The `position` table of the coordinates in the grid, with +-- fields `row`, `col`, `row_span` and `col_span`. +function grid:get_widget_position(widget) + local index = find_widget(self._private.widgets, widget) + if index == nil then return nil end + local data = self._private.widgets[index] + local ret = {} + ret["row"] = data.row + ret["col"] = data.col + ret["row_span"] = data.row_span + ret["col_span"] = data.col_span + return ret +end + + +--- Return the widgets at the coordinates. +-- @tparam number row The row coordinate of the widget +-- @tparam number col The column coordinate of the widget +-- @tparam[opt=1] number row_span The number of rows to span. +-- @tparam[opt=1] number col_span The number of columns to span. +-- @treturn table The widget(s) found at the specific coordinates, nil if no widgets found +function grid:get_widgets_at(row, col, row_span, col_span) + local widget_indices = find_widgets_at( + self._private.widgets, row, col, row_span, col_span + ) + + if widget_indices == nil then return nil end + + local ret = {} + for _,index in ipairs(widget_indices) do + local data = self._private.widgets[index] + table.insert(ret, data.widget) + end + + return #ret > 0 and ret or nil +end + +--- Replace old widget by new widget, spanning the same columns and rows. +-- @param old The widget to remove +-- @param new The widget to add +-- @treturn boolean If the operation is successful (widget found) +function grid:replace_widget(old, new) + -- check if the new object is a widget + local status = pcall(function () base.check_widget(new) end) + if not status then return false end + -- find the old widget + local index = find_widget(self._private.widgets, old) + if index == nil then return false end + + -- get old widget position + local data = self._private.widgets[index] + local row, col, row_span, col_span = data.row, data.col, data.row_span, data.col_span + + table.remove(self._private.widgets, index) + return self:add_widget_at(new, row, col, row_span, col_span) +end + +-- Update position of the widgets when inserting, adding or removing a row or a column. +-- @tparam table table_widgets Table of widgets +-- @tparam string orientation Orientation of the line: "horizontal" -> column, "vertical" -> row +-- @tparam number index Index of the line +-- @tparam string mode insert, extend or remove +-- @tparam boolean after Add the line after the index instead of inserting it before. +-- @tparam boolean extend Extend the line at index instead of inserting an empty line. +local function update_widgets_position(table_widgets, orientation, index, mode) + local t = orientation == "horizontal" and "col" or "row" + local to_remove = {} + -- inc : Index increment or decrement + -- first : Offset index for top-left cell of the widgets to shift + -- last : Offset index for bottom-right cell of the widgets to resize + local inc, first, last + if mode == "remove" then + inc, first, last = -1, 1, 1 + elseif mode == "insert" then + inc, first, last = 1, 0, 0 + elseif mode == "extend" then + inc, first, last = 1, 1, 0 + else + return + end + for i, data in ipairs(table_widgets) do + -- single widget in the line + if mode == "remove" and data[t] == index and data[t .. "_span"] == 1 then + table.insert(to_remove, i) + -- widgets to shift + elseif data[t] >= index + first then + data[t] = data[t] + inc + -- widgets to resize + elseif data[t] + data[t .. "_span"] - 1 >= index + last then + data[t .. "_span"] = data[t .. "_span"] + inc + end + end + if mode == "remove" then + -- reverse sort to remove + table.sort(to_remove, function(a,b) return a>b end) + -- Remove widgets + for _,i in ipairs(to_remove) do + table.remove(table_widgets, i) + end + end +end + +--- Insert column at index. +-- +--@DOC_wibox_layout_grid_insert_column_EXAMPLE@ +-- @tparam number|nil index Insert the new column at index. If `nil`, the column is added at the end. +-- @treturn number The index of the inserted column +function grid:insert_column(index) + if index == nil or index > self._private.num_cols + 1 or index < 1 then + index = self._private.num_cols + 1 + end + -- Update widget positions + update_widgets_position(self._private.widgets, "horizontal", index, "insert") + -- Recalculate number of rows and columns + self._private.num_cols = self._private.num_cols + 1 + return index +end + +--- Extend column at index. +-- +--@DOC_wibox_layout_grid_extend_column_EXAMPLE@ +-- @tparam number|nil index Extend the column at index. If `nil`, the last column is extended. +-- @treturn number The index of the extended column +function grid:extend_column(index) + if index == nil or index > self._private.num_cols or index < 1 then + index = self._private.num_cols + end + -- Update widget positions + update_widgets_position(self._private.widgets, "horizontal", index, "extend") + -- Recalculate number of rows and columns + self._private.num_cols = self._private.num_cols + 1 + return index +end + +--- Remove column at index. +-- +--@DOC_wibox_layout_grid_remove_column_EXAMPLE@ +-- @tparam number|nil index Remove column at index. If `nil`, the last column is removed. +-- @treturn number The index of the removed column +function grid:remove_column(index) + if index == nil or index > self._private.num_cols or index < 1 then + index = self._private.num_cols + end + -- Update widget positions + update_widgets_position(self._private.widgets, "horizontal", index, "remove") + -- Recalculate number of rows and columns + update_dimension(self) + return index +end + +--- Insert row at index. +-- +-- see `insert_column` +-- @tparam number|nil index Insert the new row at index. If `nil`, the row is added at the end. +-- @treturn number The index of the inserted row +function grid:insert_row(index) + if index == nil or index > self._private.num_rows + 1 or index < 1 then + index = self._private.num_rows + 1 + end + -- Update widget positions + update_widgets_position(self._private.widgets, "vertical", index, "insert") + -- Recalculate number of rows and columns + self._private.num_rows = self._private.num_rows + 1 + return index +end + +--- Extend row at index. +-- +-- see `extend_column` +-- @tparam number|nil index Extend the row at index. If `nil`, the last row is extended. +-- @treturn number The index of the extended row +function grid:extend_row(index) + if index == nil or index > self._private.num_rows or index < 1 then + index = self._private.num_rows + end + -- Update widget positions + update_widgets_position(self._private.widgets, "vertical", index, "extend") + -- Recalculate number of rows and columns + self._private.num_rows = self._private.num_rows + 1 + return index +end + +--- Remove row at index. +-- +-- see `remove_column` +-- @tparam number|nil index Remove row at index. If `nil`, the last row is removed. +-- @treturn number The index of the removed row +function grid:remove_row(index) + if index == nil or index > self._private.num_rows or index < 1 then + index = self._private.num_rows + end + -- Update widget positions + update_widgets_position(self._private.widgets, "vertical", index, "remove") + -- Recalculate number of rows and columns + update_dimension(self) + return index +end + + +-- Return list of children +function grid:get_children() + local ret = {} + for _, data in ipairs(self._private.widgets) do + table.insert(ret, data.widget) + end + return ret +end + +-- Add list of children to the layout +function grid:set_children(children) + self:reset() + if #children > 0 then + self:add(unpack(children)) + end +end + +-- Set the preferred orientation of the grid layout. +function grid:set_orientation(val) + if self._private.orientation ~= val and (val == "horizontal" or val == "vertical") then + self._private.orientation = val + end +end + +-- Set the minimum size for the columns. +function grid:set_min_cols_size(val) + if self._private.min_cols_size ~= val and val >= 0 then + self._private.min_cols_size = val + end +end + +-- Set the minimum size for the rows. +function grid:set_min_rows_size(val) + if self._private.min_rows_size ~= val and val >= 0 then + self._private.min_rows_size = val + end +end + +-- Force the number of columns of the layout. +function grid:set_forced_num_cols(val) + if self._private.forced_num_cols ~= val then + self._private.forced_num_cols = val + update_dimension(self) + self:emit_signal("widget::layout_changed") + end +end + +-- Force the number of rows of the layout. +function grid:set_forced_num_rows(val) + if self._private.forced_num_rows ~= val then + self._private.forced_num_rows = val + update_dimension(self) + self:emit_signal("widget::layout_changed") + end +end + +-- Set the grid properties +for _, prop in ipairs(properties) do + if not grid["set_" .. prop] then + grid["set_"..prop] = function(self, value) + if self._private[prop] ~= value then + self._private[prop] = value + self:emit_signal("widget::layout_changed") + end + end + end + if not grid["get_" .. prop] then + grid["get_"..prop] = function(self) + return self._private[prop] + end + end +end + +-- Set the directional grid properties +-- create a couple of properties by prepending `horizontal_` and `vertical_` +-- create a common property for the two directions: +-- setting the common property sets both directional properties +-- getting the common property returns the directional property +-- defined by the `orientation` property +for _, prop in ipairs(dir_properties) do + for _,dir in ipairs{"horizontal_, vertical_"} do + local dir_prop = dir .. "_" .. prop + grid["set_"..dir_prop] = function(self, value) + if self._private[dir_prop] ~= value then + self._private[dir_prop] = value + self:emit_signal("widget::layout_changed") + end + end + grid["get_"..dir_prop] = function(self) + return self._private[dir_prop] + end + end + + -- Non-directional options + grid["set_"..prop] = function(self, value) + if self._private["horizontal_"..prop] ~= value or self._private["vertical_"..prop] ~= value then + self._private["horizontal_"..prop] = value + self._private["vertical_"..prop] = value + self:emit_signal("widget::layout_changed") + end + end + grid["get_"..prop] = function(self) + return self._private[self._private.orientation .. "_" .. prop] + end +end + + +-- Return two tables of the fitted sizes of the rows and columns +-- @treturn table,table Tables of row heights and column widths +local function get_grid_sizes(self, context, orig_width, orig_height) + local rows_size = {} + local cols_size = {} + + -- Set the row and column sizes to the minimum value + for i = 1,self._private.num_rows do + rows_size[i] = self._private.min_rows_size + end + for j = 1,self._private.num_cols do + cols_size[j] = self._private.min_cols_size + end + + -- Calculate cell sizes + for _, data in ipairs(self._private.widgets) do + local w, h = base.fit_widget(self, context, data.widget, orig_width, orig_height) + h = math.max( self._private.min_rows_size, h / data.row_span ) + w = math.max( self._private.min_cols_size, w / data.col_span ) + -- update the row and column maximum size + for i = data.row, data.row + data.row_span - 1 do + if h > rows_size[i] then rows_size[i] = h end + end + for j = data.col, data.col + data.col_span - 1 do + if w > cols_size[j] then cols_size[j] = w end + end + end + return rows_size, cols_size +end + + +-- Fit the grid 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 grid:fit(context, orig_width, orig_height) + local width, height = orig_width, orig_height + + -- Calculate the space needed + local function fit_direction(dir, sizes) + local m = 0 + if self._private[dir .. "_homogeneous"] then + -- all the columns/rows have the same size + m = #sizes * max_value(sizes) + (#sizes - 1) * self._private[dir .. "_spacing"] + else + -- sum the columns/rows size + for _,s in ipairs(sizes) do + m = m + s + self._private[dir .. "_spacing"] + end + m = m - self._private[dir .. "_spacing"] + end + return m + end + + -- fit matrix cells + local rows_size, cols_size = get_grid_sizes(self, context, width, height) + + -- compute the width + local used_width_max = fit_direction("horizontal", cols_size) + local used_height_max = fit_direction("vertical", rows_size) + + return used_width_max, used_height_max +end + +-- Layout a grid layout. +-- @param context The context in which we are drawn. +-- @param width The available width. +-- @param height The available height. +function grid:layout(context, width, height) + local result = {} + local hspacing, vspacing = self._private.horizontal_spacing, self._private.vertical_spacing + + -- Fit matrix cells + local rows_size, cols_size = get_grid_sizes(self, context, width, height) + local total_expected_width, total_expected_height = sum_values(cols_size), sum_values(rows_size) + + -- Figure out the maximum size we can give out to sub-widgets + local single_width, single_height = max_value(cols_size), max_value(rows_size) + if self._private.horizontal_expand then + single_width = (width - (self._private.num_cols-1)*hspacing) / self._private.num_cols + end + if self._private.vertical_expand then + single_height = (height - (self._private.num_rows-1)*vspacing) / self._private.num_rows + end + + -- Calculate the position and size to place the widgets + local cumul_width, cumul_height = {}, {} + local cw, ch = 0, 0 + for j = 1, #cols_size do + cumul_width[j] = cw + if self._private.horizontal_homogeneous then + cols_size[j] = math.max(self._private.min_cols_size, single_width) + elseif self._private.horizontal_expand then + local hpercent = self._private.num_cols * single_width * cols_size[j] / total_expected_width + cols_size[j] = math.max(self._private.min_cols_size, hpercent) + end + cw = cw + cols_size[j] + hspacing + end + cumul_width[#cols_size + 1] = cw + for i = 1, #rows_size do + cumul_height[i] = ch + if self._private.vertical_homogeneous then + rows_size[i] = math.max(self._private.min_rows_size, single_height) + elseif self._private.vertical_expand then + local vpercent = self._private.num_rows * single_height * rows_size[i] / total_expected_height + rows_size[i] = math.max(self._private.min_rows_size, vpercent) + end + ch = ch + rows_size[i] + vspacing + end + cumul_height[#rows_size + 1] = ch + + -- Place widgets + local fill_space = true -- should be fill_space property? + for _, v in pairs(self._private.widgets) do + local x, y, w, h + -- Round numbers to avoid decimals error, force to place tight widgets + -- and avoid redraw glitches + x = math.floor(cumul_width[v.col]) + y = math.floor(cumul_height[v.row]) + w = math.floor(cumul_width[v.col + v.col_span] - hspacing - x) + h = math.floor(cumul_height[v.row + v.row_span] - vspacing - y) + -- Recalculate the width so the last widget fits + if (fill_space or self._private.horizontal_expand) and x + w > width then + w = math.floor(math.max(self._private.min_cols_size, width - x)) + end + -- Recalculate the height so the last widget fits + if (fill_space or self._private.vertical_expand) and y + h > height then + h = math.floor(math.max(self._private.min_rows_size, height - y)) + end + -- Place the widget if it fits in the area + if x + w <= width and y + h <= height then + table.insert(result, base.place_widget_at(v.widget, x, y, w, h)) + end + end + return result +end + + +--- Reset the grid layout. +-- Remove all widgets and reset row and column counts +-- +-- **Signal:** widget::reset +function grid:reset() + self._private.widgets = {} + -- reset the number of columns and rows to the forced value or to 0 + self._private.num_rows = self._private.forced_num_rows ~= nil + and self._private.forced_num_rows or 0 + self._private.num_cols = self._private.forced_num_cols ~= nil + and self._private.forced_num_cols or 0 + -- emit signals + self:emit_signal("widget::layout_changed") + self:emit_signal("widget::reset") +end + +--- When the layout is reset. +-- This signal is emitted when the layout has been reset, +-- all the widgets removed and the row and column counts reset. +-- @signal widget::reset + + + +--- Return a new grid layout. +-- +-- A grid layout sets widgets in a grids of custom number of rows and columns. +-- @tparam[opt="y"] string orientation The preferred grid extension direction. +-- @function wibox.layout.grid +local function new(orientation) + -- Preference for vertical direction: fill rows first, extend grid with new row + local dir = (orientation == "horizontal"or orientation == "vertical") + and orientation or "vertical" + + local ret = base.make_widget(nil, nil, {enable_properties = true}) + + util.table.crush(ret, grid, true) + + ret._private.orientation = dir + ret._private.widgets = {} + ret._private.num_rows = 0 + ret._private.num_cols = 0 + ret._private.rows_size = {} + ret._private.cols_size = {} + + ret._private.superpose = false + ret._private.forced_num_rows = nil + ret._private.forced_num_cols = nil + ret._private.min_rows_size = 0 + ret._private.min_cols_size = 0 + + ret._private.horizontal_homogeneous = true + ret._private.vertical_homogeneous = true + ret._private.horizontal_expand = false + ret._private.vertical_expand = false + ret._private.horizontal_spacing = 0 + ret._private.vertical_spacing = 0 + + return ret +end + +--- Return a new horizontal grid layout. +-- +-- Each new widget is positioned below the last widget on the same column +-- up to `forced_num_rows`. Then the next column is filled, creating it if it doesn't exist. +-- @tparam number|nil forced_num_rows Forced number of rows (`nil` for automatic). +-- @tparam widget ... Widgets that should be added to the layout. +-- @function wibox.layout.grid.horizontal +function grid.horizontal(forced_num_rows, ...) + local ret = new("horizontal") + ret:set_forced_num_rows(forced_num_rows) + ret:add(...) + return ret +end + +--- Return a new vertical grid layout. +-- +-- Each new widget is positioned left of the last widget on the same row +-- up to `forced_num_cols`. Then the next row is filled, creating it if it doesn't exist. +-- @tparam number|nil forced_num_cols Forced number of columns (`nil` for automatic). +-- @tparam widget ... Widgets that should be added to the layout. +-- @function wibox.layout.grid.vertical +function grid.vertical(forced_num_cols, ...) + local ret = new("vertical") + ret:set_forced_num_cols(forced_num_cols) + ret:add(...) + return ret +end + + +function grid.mt:__call(...) + return new(...) +end + +--@DOC_widget_COMMON@ + +--@DOC_object_COMMON@ + +return setmetatable(grid, grid.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/wibox/layout/init.lua b/lib/wibox/layout/init.lua index 4a9b006c..d07e6d3b 100644 --- a/lib/wibox/layout/init.lua +++ b/lib/wibox/layout/init.lua @@ -18,6 +18,7 @@ return setmetatable({ scroll = require("wibox.layout.scroll"); ratio = require("wibox.layout.ratio"); stack = require("wibox.layout.stack"); + grid = require("wibox.layout.grid"); }, {__call = function(_, args) return base.make_widget_declarative(args) end}) -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 From bf2d658380890f9b85cc58c56371d8984ec427dd Mon Sep 17 00:00:00 2001 From: getzze Date: Fri, 10 Feb 2017 00:10:12 +0000 Subject: [PATCH 2/2] Add tests for grid layout Update tests Update tests Separate spacing and padding tests Update tests amd add imperative usage Change fixed_size to homogeneous property Update tests --- tests/examples/wibox/layout/defaults/grid.lua | 16 +++ tests/examples/wibox/layout/grid/add.lua | 16 +++ tests/examples/wibox/layout/grid/expand.lua | 106 ++++++++++++++++++ .../wibox/layout/grid/extend_column.lua | 11 ++ .../examples/wibox/layout/grid/imperative.lua | 27 +++++ .../wibox/layout/grid/insert_column.lua | 12 ++ tests/examples/wibox/layout/grid/min_size.lua | 49 ++++++++ .../wibox/layout/grid/orientation.lua | 71 ++++++++++++ tests/examples/wibox/layout/grid/remove.lua | 12 ++ .../wibox/layout/grid/remove_column.lua | 12 ++ tests/examples/wibox/layout/grid/spacing.lua | 56 +++++++++ .../examples/wibox/layout/grid/superpose.lua | 20 ++++ 12 files changed, 408 insertions(+) create mode 100644 tests/examples/wibox/layout/defaults/grid.lua create mode 100644 tests/examples/wibox/layout/grid/add.lua create mode 100644 tests/examples/wibox/layout/grid/expand.lua create mode 100644 tests/examples/wibox/layout/grid/extend_column.lua create mode 100644 tests/examples/wibox/layout/grid/imperative.lua create mode 100644 tests/examples/wibox/layout/grid/insert_column.lua create mode 100644 tests/examples/wibox/layout/grid/min_size.lua create mode 100644 tests/examples/wibox/layout/grid/orientation.lua create mode 100644 tests/examples/wibox/layout/grid/remove.lua create mode 100644 tests/examples/wibox/layout/grid/remove_column.lua create mode 100644 tests/examples/wibox/layout/grid/spacing.lua create mode 100644 tests/examples/wibox/layout/grid/superpose.lua diff --git a/tests/examples/wibox/layout/defaults/grid.lua b/tests/examples/wibox/layout/defaults/grid.lua new file mode 100644 index 00000000..8c7dc36c --- /dev/null +++ b/tests/examples/wibox/layout/defaults/grid.lua @@ -0,0 +1,16 @@ +local generic_widget = ... --DOC_HIDE +local wibox = require("wibox") --DOC_HIDE + +return --DOC_HIDE +wibox.widget { + generic_widget( "first" ), + generic_widget( "second" ), + generic_widget( "third" ), + generic_widget( "fourth" ), + forced_num_cols = 2, + forced_num_rows = 2, + homogeneous = true, + expand = true, + layout = wibox.layout.grid +} +, nil, 60 --DOC_HIDE diff --git a/tests/examples/wibox/layout/grid/add.lua b/tests/examples/wibox/layout/grid/add.lua new file mode 100644 index 00000000..de9c7515 --- /dev/null +++ b/tests/examples/wibox/layout/grid/add.lua @@ -0,0 +1,16 @@ +local generic_widget, generic_before_after = ... --DOC_HIDE_ALL +local wibox = require("wibox") --DOC_HIDE +local beautiful = require("beautiful") --DOC_HIDE + +print("l:add_widget_at(new, 1, 4, 1, 1)") --DOC_HIDE + +local w = generic_before_after(wibox.layout.grid, { + forced_num_cols = 3, + forced_num_rows = 2, + homogeneous = true, +}, 6, "add_widget_at", {wibox.widget( --DOC_HIDE + generic_widget("__new__",beautiful.bg_highlight) --DOC_HIDE + ), 1, 4, 1, 1 --DOC_HIDE +}) --DOC_HIDE + +return w, w:fit({dpi=96}, 9999, 9999) --DOC_HIDE diff --git a/tests/examples/wibox/layout/grid/expand.lua b/tests/examples/wibox/layout/grid/expand.lua new file mode 100644 index 00000000..fa103f60 --- /dev/null +++ b/tests/examples/wibox/layout/grid/expand.lua @@ -0,0 +1,106 @@ +local generic_widget = ... --DOC_HIDE_ALL +local wibox = require("wibox") +local beautiful = require("beautiful") + +local w = wibox.widget { + { + { + { + markup = "expand = false\nhomogeneous = false", + widget = wibox.widget.textbox + }, + { + { + generic_widget( "short 1" ), + generic_widget( "-------- long 1 --------" ), + generic_widget( "short 2" ), + generic_widget( "-------- long 2 --------" ), + forced_num_cols = 2, + forced_num_rows = 2, + expand = false, + homogeneous = false, + layout = wibox.layout.grid, + }, + margins = 1, + color = beautiful.border_color, + layout = wibox.container.margin, + }, + layout = wibox.layout.fixed.vertical + }, + { + { + markup = "expand = true\nhomogeneous = false", + widget = wibox.widget.textbox + }, + { + { + generic_widget( "short 1" ), + generic_widget( "-------- long 1 --------" ), + generic_widget( "short 2" ), + generic_widget( "-------- long 2 --------" ), + forced_num_cols = 2, + forced_num_rows = 2, + expand = true, + homogeneous = false, + layout = wibox.layout.grid, + }, + margins = 1, + color = beautiful.border_color, + layout = wibox.container.margin, + }, + layout = wibox.layout.fixed.vertical + }, + layout = wibox.layout.flex.horizontal + }, + { + { + { + markup = "expand = false\nhomogeneous = true", + widget = wibox.widget.textbox + }, + { + { + generic_widget( "short 1" ), + generic_widget( "-------- long 1 --------" ), + generic_widget( "short 2" ), + generic_widget( "-------- long 2 --------" ), + forced_num_cols = 2, + forced_num_rows = 2, + expand = false, + homogeneous = true, + layout = wibox.layout.grid, + }, + margins = 1, + color = beautiful.border_color, + layout = wibox.container.margin, + }, + layout = wibox.layout.fixed.vertical + }, + { + { + markup = "expand = true\nhomogeneous = true", + widget = wibox.widget.textbox + }, + { + { + generic_widget( "short 1" ), + generic_widget( "-------- long 1 --------" ), + generic_widget( "short 2" ), + generic_widget( "-------- long 2 --------" ), + forced_num_cols = 2, + forced_num_rows = 2, + expand = true, + homogeneous = true, + layout = wibox.layout.grid, + }, + margins = 1, + color = beautiful.border_color, + layout = wibox.container.margin, + }, + layout = wibox.layout.fixed.vertical + }, + layout = wibox.layout.flex.horizontal + }, + layout = wibox.layout.fixed.vertical +} +return w, 600, 150 diff --git a/tests/examples/wibox/layout/grid/extend_column.lua b/tests/examples/wibox/layout/grid/extend_column.lua new file mode 100644 index 00000000..019efbc9 --- /dev/null +++ b/tests/examples/wibox/layout/grid/extend_column.lua @@ -0,0 +1,11 @@ +local _, generic_before_after = ... --DOC_HIDE_ALL +local wibox = require("wibox") --DOC_HIDE + +print("l:extend_column(2)") --DOC_HIDE + +local w = generic_before_after(wibox.layout.grid, { + forced_num_cols = 3, + forced_num_rows = 2, + homogeneous = true, +}, 6, "extend_column", {2}) --DOC_HIDE +return w, w:fit({dpi=96}, 9999, 9999) --DOC_HIDE diff --git a/tests/examples/wibox/layout/grid/imperative.lua b/tests/examples/wibox/layout/grid/imperative.lua new file mode 100644 index 00000000..b76bfd7e --- /dev/null +++ b/tests/examples/wibox/layout/grid/imperative.lua @@ -0,0 +1,27 @@ +local generic_widget = ... --DOC_NO_USAGE --DOC_HIDE +local wibox = require("wibox") --DOC_HIDE + +local first = wibox.widget(generic_widget( "first" )) --DOC_HIDE +local second = wibox.widget(generic_widget( "second" )) --DOC_HIDE +local third = wibox.widget(generic_widget( "t\nh\ni\nr\nd" )) --DOC_HIDE +local fourth = wibox.widget(generic_widget( "fourth" )) --DOC_HIDE +local fifth = wibox.widget(generic_widget( "fifth" )) --DOC_HIDE +local lorem = wibox.widget(generic_widget("Lorem ipsum dolor sit amet, consectetur " .. --DOC_HIDE + "adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")) --DOC_HIDE + + local l = wibox.widget { + homogeneous = true, + spacing = 5, + min_cols_size = 10, + min_rows_size = 10, + layout = wibox.layout.grid, + } + l:add_widget_at(first , 2, 1, 1, 2) + l:add_widget_at(second, 3, 1, 1, 2) + l:add_widget_at(third , 2, 3, 2, 1) + l:add_widget_at(fourth, 4, 1, 1, 1) + l:add_widget_at(fifth , 4, 2, 1, 2) + l:insert_row(1) + l:add_widget_at(lorem, 1, 1, 1, 3) + +return l, l:fit({dpi=96}, 300, 200) --DOC_HIDE diff --git a/tests/examples/wibox/layout/grid/insert_column.lua b/tests/examples/wibox/layout/grid/insert_column.lua new file mode 100644 index 00000000..83e014f6 --- /dev/null +++ b/tests/examples/wibox/layout/grid/insert_column.lua @@ -0,0 +1,12 @@ +local _, generic_before_after = ... --DOC_HIDE_ALL +local wibox = require("wibox") --DOC_HIDE + +print("l:insert_column(2)") --DOC_HIDE + +local w = generic_before_after(wibox.layout.grid, { + forced_num_cols = 3, + forced_num_rows = 2, + homogeneous = true, +} +, 6, "insert_column", {2}) --DOC_HIDE +return w, w:fit({dpi=96}, 9999, 9999) --DOC_HIDE diff --git a/tests/examples/wibox/layout/grid/min_size.lua b/tests/examples/wibox/layout/grid/min_size.lua new file mode 100644 index 00000000..3a6eb7f2 --- /dev/null +++ b/tests/examples/wibox/layout/grid/min_size.lua @@ -0,0 +1,49 @@ +local generic_widget = ... --DOC_HIDE_ALL +local wibox = require("wibox") --DOC_HIDE +local beautiful = require("beautiful") --DOC_HIDE + +local w = wibox.widget { + { + { + markup = "min_cols_size = 0", + widget = wibox.widget.textbox + }, + { + { + generic_widget( "first" ), + generic_widget( "second" ), + forced_num_cols = 2, + min_cols_size = 0, + homogeneous = true, + layout = wibox.layout.grid, + }, + margins = 1, + color = beautiful.border_color, + layout = wibox.container.margin, + }, + layout = wibox.layout.fixed.vertical + }, + { + { + markup = "min_cols_size = 100", + widget = wibox.widget.textbox + }, + { + { + generic_widget( "first" ), + generic_widget( "second" ), + forced_num_cols = 2, + min_cols_size = 100, + homogeneous = true, + layout = wibox.layout.grid, + }, + margins = 1, + color = beautiful.border_color, + layout = wibox.container.margin, + }, + layout = wibox.layout.fixed.vertical + }, + layout = wibox.layout.fixed.horizontal +} +return w, w:fit({dpi=96}, 9999, 9999) +--return w, 500, 50 diff --git a/tests/examples/wibox/layout/grid/orientation.lua b/tests/examples/wibox/layout/grid/orientation.lua new file mode 100644 index 00000000..823f94f7 --- /dev/null +++ b/tests/examples/wibox/layout/grid/orientation.lua @@ -0,0 +1,71 @@ +local generic_widget = ... --DOC_HIDE_ALL +local wibox = require("wibox") --DOC_HIDE +local beautiful = require("beautiful") --DOC_HIDE + +print([[l = wibox.layout { + forced_num_cols = 2, + forced_num_rows = 2, + homogeneous = true, + layout = wibox.layout.grid +} +l:set_orientation("vertical") -- change to "horizontal" +l:add(...) +]]) --DOC_HIDE + +return --DOC_HIDE +wibox.widget { + { + { + markup = "orientation = 'vertical'", + widget = wibox.widget.textbox + }, + { + { + generic_widget( "first" ), + generic_widget( "second" ), + generic_widget( "third" ), + generic_widget( "fourth" ), + generic_widget( "fifth" ), + generic_widget( "sixth" ), + forced_num_cols = 2, + forced_num_rows = 2, + orientation = "vertical", + expand = false, + homogeneous = true, + layout = wibox.layout.grid, + }, + margins = 1, + color = beautiful.border_color, + layout = wibox.container.margin, + }, + layout = wibox.layout.fixed.vertical + }, + { + { + markup = "orientation = 'horizontal'", + widget = wibox.widget.textbox + }, + { + { + generic_widget( "first" ), + generic_widget( "second" ), + generic_widget( "third" ), + generic_widget( "fourth" ), + generic_widget( "fifth" ), + generic_widget( "sixth" ), + forced_num_cols = 2, + forced_num_rows = 2, + orientation = 'horizontal', + expand = false, + homogeneous = true, + layout = wibox.layout.grid, + }, + margins = 1, + color = beautiful.border_color, + layout = wibox.container.margin, + }, + layout = wibox.layout.fixed.vertical + }, + layout = wibox.layout.fixed.horizontal +} +, 300, 90 --DOC_HIDE diff --git a/tests/examples/wibox/layout/grid/remove.lua b/tests/examples/wibox/layout/grid/remove.lua new file mode 100644 index 00000000..bd67c663 --- /dev/null +++ b/tests/examples/wibox/layout/grid/remove.lua @@ -0,0 +1,12 @@ +local _, generic_before_after = ... --DOC_HIDE_ALL +local wibox = require("wibox") --DOC_HIDE + +print("l:remove_widgets_at(1,1)") --DOC_HIDE + +local w = generic_before_after(wibox.layout.grid, { + forced_num_cols = 3, + forced_num_rows = 2, + homogeneous = true, +}, 6, "remove_widgets_at", {1, 1}) --DOC_HIDE + +return w, w:fit({dpi=96}, 9999, 9999) --DOC_HIDE diff --git a/tests/examples/wibox/layout/grid/remove_column.lua b/tests/examples/wibox/layout/grid/remove_column.lua new file mode 100644 index 00000000..2a184d2f --- /dev/null +++ b/tests/examples/wibox/layout/grid/remove_column.lua @@ -0,0 +1,12 @@ +local _, generic_before_after = ... --DOC_HIDE_ALL +local wibox = require("wibox") --DOC_HIDE + +print("l:remove_column(2)") --DOC_HIDE + +local w = generic_before_after(wibox.layout.grid, { + forced_num_cols = 3, + forced_num_rows = 2, + homogeneous = true, +}, 6, "remove_column", {2} --DOC_HIDE +) +return w, w:fit({dpi=96}, 9999, 9999) --DOC_HIDE diff --git a/tests/examples/wibox/layout/grid/spacing.lua b/tests/examples/wibox/layout/grid/spacing.lua new file mode 100644 index 00000000..99695d5d --- /dev/null +++ b/tests/examples/wibox/layout/grid/spacing.lua @@ -0,0 +1,56 @@ +local generic_widget = ... --DOC_HIDE_ALL +local wibox = require("wibox") --DOC_HIDE +local beautiful = require("beautiful") --DOC_HIDE + +local w = wibox.widget { + { + { + markup = "spacing = 0", + widget = wibox.widget.textbox + }, + { + { + generic_widget( "first" ), + generic_widget( "second" ), + generic_widget( "third" ), + generic_widget( "fourth" ), + forced_num_cols = 2, + forced_num_rows = 2, + homogeneous = true, + spacing = 0, + padding = 0, + layout = wibox.layout.grid, + }, + margins = 1, + color = beautiful.border_color, + layout = wibox.container.margin, + }, + layout = wibox.layout.fixed.vertical + }, + { + { + markup = "spacing = 10", + widget = wibox.widget.textbox + }, + { + { + generic_widget( "first" ), + generic_widget( "second" ), + generic_widget( "third" ), + generic_widget( "fourth" ), + forced_num_cols = 2, + forced_num_rows = 2, + homogeneous = true, + spacing = 10, + padding = 0, + layout = wibox.layout.grid, + }, + margins = 1, + color = beautiful.border_color, + layout = wibox.container.margin, + }, + layout = wibox.layout.fixed.vertical + }, + layout = wibox.layout.flex.horizontal +} +return w, w:fit({dpi=96}, 9999, 9999) diff --git a/tests/examples/wibox/layout/grid/superpose.lua b/tests/examples/wibox/layout/grid/superpose.lua new file mode 100644 index 00000000..0800aef8 --- /dev/null +++ b/tests/examples/wibox/layout/grid/superpose.lua @@ -0,0 +1,20 @@ +local generic_widget = ... --DOC_HIDE_ALL +local wibox = require("wibox") +local beautiful = require("beautiful") + +local w = wibox.widget { + generic_widget( "first" ), + generic_widget( "second" ), + generic_widget( "third" ), + generic_widget( "fourth" ), + forced_num_cols = 2, + forced_num_rows = 2, + superpose = true, + homogeneous = true, + layout = wibox.layout.grid, +} +w:add_widget_at(wibox.widget( + generic_widget("fifth",beautiful.bg_highlight) + ), 1, 1, 1, 2) + +return w, w:fit({dpi=96}, 9999, 9999)