awesome/lib/wibox/layout/grid.lua

1632 lines
55 KiB
Lua

---------------------------------------------------------------------------
--- Place multiple widgets in multiple rows and columns.
--
-- 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@
--
-- The same can be done using the declarative syntax:
--
--@DOC_wibox_layout_grid_declarative1_EXAMPLE@
--
-- When `col_index` and `row_index` are not provided, the widgets are
-- automatically added next to each other spanning only one cell:
--
--@DOC_wibox_layout_grid_declarative2_EXAMPLE@
--
--@DOC_wibox_layout_defaults_grid_EXAMPLE@
-- @author getzze
-- @copyright 2017 getzze
-- @layoutmod wibox.layout.grid
-- @supermodule wibox.widget.base
---------------------------------------------------------------------------
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 gtable = require("gears.table")
local gmath = require("gears.math")
local gcolor = require("gears.color")
local gdebug = require("gears.debug")
local base = require("wibox.widget.base")
local cairo = require("lgi").cairo
local grid = { mt = {} }
local properties = {
"orientation", "superpose",
"forced_row_count", "forced_column_count",
}
local dir_properties = { "spacing", "homogeneous", "expand" }
--- Set the preferred orientation of the grid layout.
--
-- When calling `get_next_empty`, empty cells are browsed differently.
--
--@DOC_wibox_layout_grid_orientation_EXAMPLE@
-- @tparam[opt="vertical"] string orientation Preferred orientation.
-- @propertyvalue "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.
-- @propertyvalue "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.
-- @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@
-- @tparam[opt=false] boolean superpose
-- @property superpose
--- Force the number of rows of the layout.
--
-- Deprecated, use `row_count`.
--
-- @deprecatedproperty forced_num_rows
-- @tparam[opt=nil] number|nil forced_num_rows
-- @propertytype nil Automatically determine the number of rows.
-- @propertyunit rows
-- @negativeallowed false
-- @see forced_num_cols
-- @see row_count
--- Force the number of columns of the layout.
--
-- Deprecated, use `column_count`.
--
-- @deprecatedproperty forced_num_cols
-- @tparam[opt=nil] number|nil forced_num_cols
-- @propertytype nil Automatically determine the number of columns.'
-- @propertyunit columns
-- @negativeallowed false
-- @see forced_num_rows
-- @see column_count
--- Set the minimum size for the columns.
--
--@DOC_wibox_layout_grid_min_size_EXAMPLE@
-- @tparam[opt=0] number minimum_column_width Minimum size of the columns.
-- @property minimum_column_width
-- @propertyunit pixel
-- @negativeallowed false
-- @see minimum_row_height
--- Set the minimum size for the columns.
--
-- Deprecated, use `minimum_column_width`.
--
--@DOC_wibox_layout_grid_min_size_EXAMPLE@
-- @tparam[opt=0] number min_cols_size Minimum size of the columns.
-- @deprecatedproperty min_cols_size
-- @propertyunit pixel
-- @negativeallowed false
-- @see minimum_row_height
--- Set the minimum size for the rows.
-- @tparam[opt=0] number minimum_row_height Minimum size of the rows.
-- @property minimum_row_height
-- @propertyunit pixel
-- @negativeallowed false
-- @see min_cols_size
--- Set the minimum size for the rows.
--
-- Deprecated, use `minimum_row_height`.
--
-- @tparam[opt=0] number min_rows_size Minimum size of the rows.
-- @deprecatedproperty min_rows_size
-- @propertyunit pixel
-- @negativeallowed false
-- @see min_cols_size
--- The spacing between columns.
--
-- Deprecated, use `spacing`.
--
-- @tparam[opt=0] number horizontal_spacing
-- @deprecatedproperty horizontal_spacing
-- @propertyunit pixel
-- @negativeallowed false
-- @see spacing
-- @see vertical_spacing
--- The spacing between rows.
--
-- Deprecated, use `spacing`.
--
-- @tparam[opt=0] number vertical_spacing
-- @deprecatedproperty vertical_spacing
-- @propertyunit pixel
-- @negativeallowed false
-- @see spacing
-- @see horizontal_spacing
--- The spacing between rows and columns.
--
-- Get the value `horizontal_spacing` or `vertical_spacing` defined by the
-- preferred `orientation`.
--
--@DOC_wibox_layout_grid_spacing_EXAMPLE@
--
-- When a border is present, the spacing is applied on both side of the border,
-- thus is twice as large:
--
-- @DOC_wibox_layout_grid_border_width3_EXAMPLE@
--
-- @property spacing
-- @tparam[opt=0] number|table spacing
-- @tparam number spacing.vertical The vertical spacing.
-- @tparam number spacing.horizontal The horizontal spacing.
-- @propertytype number The same value for the `"vertical"` and `"horizontal"`
-- aspects.
-- @propertytype table Different values for the `"vertical"` and `"horizontal"`
-- aspects.
-- @propertyunit pixel
-- @negativeallowed false
-- @see vertical_spacing
-- @see horizontal_spacing
--- Controls if the columns are expanded to use all the available width.
--
-- Deprecated, use `expand`.
--
-- @tparam[opt=false] boolean horizontal_expand Expand the grid into the available space
-- @deprecatedproperty horizontal_expand
-- @see expand
-- @see vertical_expand
--- Controls if the rows are expanded to use all the available height.
--
-- Deprecated, use `expand`.
--
-- @tparam[opt=false] boolean vertical_expand Expand the grid into the available space
-- @deprecatedproperty vertical_expand
-- @see expand
-- @see horizontal_expand
--- Controls if the columns/rows are expanded to use all the available space.
--
-- Get the value `horizontal_expand` or `vertical_expand` defined by the
-- preferred `orientation`.
--
--@DOC_wibox_layout_grid_expand_EXAMPLE@
-- @property expand
-- @tparam[opt=false] boolean|table expand Expand the grid into the available space
-- @tparam boolean expand.vertical The vertical expand.
-- @tparam boolean expand.horizontal The horizontal expand.
-- @propertytype number The same value for the `"vertical"` and `"horizontal"`
-- aspects.
-- @propertytype table Different values for the `"vertical"` and `"horizontal"`
-- aspects.
-- @see horizontal_expand
-- @see vertical_expand
--- Controls if the columns all have the same width or if the width of each
-- column depends on the content.
--
-- Deprecated, use `homogeneous`
--
-- @tparam[opt=true] boolean horizontal_homogeneous All the columns have the same width.
-- @deprecatedproperty horizontal_homogeneous
-- @see vertical_homogeneous
-- @see homogeneous
--- Controls if the rows all have the same height or if the height of each row
-- depends on the content.
--
-- Deprecated, use `homogeneous`
--
-- @tparam[opt=true] boolean vertical_homogeneous All the rows have the same height.
-- @deprecatedproperty vertical_homogeneous
-- @see homogeneous
-- @see horizontal_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@
-- @property homogeneous
-- @tparam[opt=true] boolean|table homogeneous All the columns/rows have the same size.
-- @tparam boolean homogeneous.vertical The vertical homogeneous value.
-- @tparam boolean homogeneous.horizontal The horizontal homogeneous value.
-- @propertytype number The same value for the `"vertical"` and `"horizontal"`
-- aspects.
-- @propertytype table Different values for the `"vertical"` and `"horizontal"`
-- aspects.
-- @see vertical_homogeneous
-- @see horizontal_homogeneous
--- The number of rows.
--
-- Unless manually set, the value will be automatically determined base on the
-- `orientation`.
--
-- @property row_count
-- @tparam integer row_count
-- @negativeallowed false
-- @propertydefault autogenerated
-- @see forced_num_rows
--- The number of columns.
--
-- Unless manually set, the value will be automatically determined base on the
-- `orientation`.
--
-- @property column_count
-- @tparam integer column_count
-- @negativeallowed false
-- @propertydefault autogenerated
-- @see forced_num_cols
--- 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.
-- @deprecatedmethod get_dimension
-- @treturn number,number The number of rows and columns
-- @see row_count
-- @see column_count
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`.
-- @method get_next_empty
-- @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.
--
-- If the widgets have a `row_index`, `col_index`, `col_span`
-- or `row_span` property, it will be honored.
--
-- @method add
-- @tparam wibox.widget ... Widgets that should be added (must at least be one)
-- @interface layout
-- @noreturn
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,
w.row_index or row,
w.col_index or column,
w.row_span or 1,
w.col_span or 1
)
end
end
--- Add a widget to the grid layout at specific coordinate.
--
-- You can now use `:add {row_index = 1, col_index = 1}` instead of this method.
--
--@DOC_wibox_layout_grid_add_EXAMPLE@
--
-- @deprecatedmethod add_widget_at
-- @tparam wibox.widget 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
child = base.make_widget_from_value(child)
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.
-- @method remove
-- @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@
--
-- @method remove_widgets_at
-- @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.
-- @method get_widget_position
-- @tparam widget 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.
-- @method get_widgets_at
-- @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.
-- @method replace_widget
-- @tparam widget old The widget to remove
-- @tparam widget 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@
--
-- @method insert_column
-- @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@
--
-- @method extend_column
-- @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@
--
-- @method remove_column
-- @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`
--
-- @method insert_row
-- @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`
--
-- @method extend_row
-- @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`
--
-- @method remove_row
-- @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
--- Add row border.
--
-- This method allows to set the width/color or a specific row rather than use
-- the same values for all the rows.
--
-- @DOC_wibox_layout_grid_add_row_border1_EXAMPLE@
--
-- @method add_row_border
-- @tparam integer index The row index. `1` is the top border (outer) border.
-- @tparam[opt=nil] integer|nil height The border height. If `nil` is passed,
-- then the `border_width.outer` will be user for index `1` and
-- `row_count + 1`, otherwise, `border_width.inner` will be used.
-- @tparam[opt={}] table args
-- @tparam[opt=nil] color args.color The border color. If `nil` is passed,
-- then the `border_color.outer` will be user for index `1` and
-- `row_count + 1`, otherwise, `border_color.inner` will be used.
-- @tparam[opt={1}] table args.dashes The dash pattern used for the line. By default,
-- it is a solid line.
-- @tparam[opt=0] number args.dash_offset If the line has `dashes`, then this is the
-- initial offset. Note that line are draw left to right and top to bottom.
-- @tparam[opt="butt"] string args.caps How the dashes ends are drawn. Either
-- `"butt"` (default), `"round"` or `"square"`
-- @noreturn
-- @see add_column_border
--- Add column border.
--
-- This method allows to set the width/color or a specific column rather than use
-- the same values for all the columns.
--
-- @DOC_wibox_layout_grid_add_column_border1_EXAMPLE@
--
-- @method add_column_border
-- @tparam integer index The column index. `1` is the top border (outer) border.
-- @tparam[opt=nil] integer|nil height The border height. If `nil` is passed,
-- then the `border_width.outer` will be user for index `1` and
-- `column_count + 1`, otherwise, `border_width.inner` will be used.
-- @tparam[opt={}] table args
-- @tparam[opt=nil] color args.color The border color. If `nil` is passed,
-- then the `border_color.outer` will be user for index `1` and
-- `row_count + 1`, otherwise, `border_color.inner` will be used.
-- @tparam[opt={1}] table args.dashes The dash pattern used for the line. By default,
-- it is a solid line.
-- @tparam[opt=0] number args.dash_offset If the line has `dashes`, then this is the
-- initial offset. Note that line are draw left to right and top to bottom.
-- @tparam[opt="butt"] string args.caps How the dashes ends are drawn. Either
-- `"butt"` (default), `"round"` or `"square"`
-- @noreturn
-- @see add_column_border
--- The border width.
--
-- @DOC_wibox_layout_grid_border_width1_EXAMPLE@
--
-- If `add_row_border` or `add_column_border` is used, it takes precedence and
-- is drawn on top of the `border_color` mask. Using both `border_width` and
-- `add_row_border` at the same time makes little sense:
--
-- @DOC_wibox_layout_grid_border_width2_EXAMPLE@
--
-- It is also possible to set the inner and outer borders separately:
--
-- @DOC_wibox_layout_grid_border_width4_EXAMPLE@
--
-- @property border_width
-- @tparam[opt=0] integer|table border_width
-- @tparam integer border_width.inner
-- @tparam integer border_width.outer
-- @propertytype integer Use the same value for inner and outer borders.
-- @propertytype table Specify a different value for the inner and outer borders.
-- @negativeallowed false
-- @see border_color
-- @see add_column_border
-- @see add_row_border
--- The border color for the table outer border.
-- @property border_color
-- @tparam[opt=0] color|table border_color
-- @tparam color border_color.inner
-- @tparam color border_color.outer
-- @propertytype color Use the same value for inner and outer borders.
-- @propertytype table Specify a different value for the inner and outer borders.
-- @see border_width
-- 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
function grid:set_forced_num_cols(val)
gdebug.deprecate(
"The `.column_count = "..tostring(val).."`.",
{deprecated_in=5}
)
self:set_column_count(val)
end
function grid:set_forced_num_rows(val)
gdebug.deprecate(
"The `row_count = "..tostring(val).."`.",
{deprecated_in=5}
)
self:set_row_count(val)
end
-- Force the number of columns of the layout.
function grid:set_column_count(val)
if self._private.forced_num_cols ~= val then
self._private.forced_num_cols = val
update_dimension(self)
self:emit_signal("property::column_count", val)
self:emit_signal("widget::layout_changed")
end
end
-- Force the number of rows of the layout.
function grid:set_row_count(val)
if self._private.forced_num_rows ~= val then
self._private.forced_num_rows = val
update_dimension(self)
self:emit_signal("property::row_count", val)
self:emit_signal("widget::layout_changed")
end
end
function grid:get_row_count()
return self._private.forced_num_rows or self._private.num_rows
end
function grid:get_column_count()
return self._private.forced_num_cols or self._private.num_cols
end
function grid:set_minimum_column_width(val)
if self._private.min_cols_size ~= val then
self._private.min_cols_size = val
update_dimension(self)
self:emit_signal("property::minimum_column_width", val)
self:emit_signal("widget::layout_changed")
end
end
function grid:set_minimum_row_height(val)
if self._private.min_rows_size ~= val then
self._private.min_rows_size = val
update_dimension(self)
self:emit_signal("property::minimum_column_width", val)
self:emit_signal("widget::layout_changed")
end
end
function grid:set_min_cols_size(val)
gdebug.deprecate(
"The `.minimum_column_width = "..tostring(val).."`.",
{deprecated_in=5}
)
self:set_minimum_column_width(val)
end
function grid:set_min_rows_size(val)
gdebug.deprecate(
"The `.minimum_column_width = "..tostring(val).."`.",
{deprecated_in=5}
)
self:set_minimum_row_height(val)
end
function grid:get_minimum_column_width()
return self._private.min_cols_size
end
function grid:get_minimum_row_height()
return self._private.min_rows_size
end
function grid:get_min_cols_size()
return self._private.min_cols_size
end
function grid:get_min_rows_size()
return self._private.min_rows_size
end
function grid:set_border_width(val)
self._private.border_width = type(val) == "table" and val or {
inner = val or 0,
outer = val or 0,
}
-- Enforce integers. Not doing so makes the masking code more complex. Also,
-- most of the time, not using integer is probably an user mistake (DPI
-- related or ratio related).
self._private.border_width.inner = gmath.round(self._private.border_width.inner)
self._private.border_width.outer = gmath.round(self._private.border_width.outer)
-- Drawing the border takes both a lot of memory (for the cached masks)
-- and CPU, so make sure it is no-op for the 99% of cases where there is
-- no border.
self._private.has_border = self._private.border_width.inner ~= 0
or self._private.border_width.outer ~= 0
self:emit_signal("property::border_width", self._private.border_width)
self:emit_signal("widget::layout_changed")
end
function grid:set_border_color(val)
if type(val) == "table" then
self._private.border_color = {
inner = gcolor(val.inner),
outer = gcolor(val.outer),
}
else
self._private.border_color = {
inner = gcolor(val),
outer = gcolor(val),
}
end
self:emit_signal("property::border_color", self._private.border_color)
self:emit_signal("widget::redraw_needed")
end
function grid:add_row_border(index, height, args)
self._private.has_border = true
self._private.custom_border_width.rows[index] = {
size = height,
color = args.color and gcolor(args.color),
dashes = args.dashes,
offset = args.dash_offset,
caps = args.caps,
}
self:emit_signal("widget::layout_changed")
end
function grid:add_column_border(index, width, args)
self._private.has_border = true
self._private.custom_border_width.cols[index] = {
size = width,
color = args.color and gcolor(args.color),
dashes = args.dashes,
offset = args.dash_offset,
caps = args.caps,
}
self:emit_signal("widget::layout_changed")
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("property::"..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)
gdebug.deprecate(
"The `".. dir_prop .."` property is deprecated. Use `".. prop .."`",
{deprecated_in=5}
)
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)
gdebug.deprecate(
"The `".. dir_prop .."` property is deprecated. Use `".. prop .."`",
{deprecated_in=5}
)
return self._private[dir_prop]
end
end
grid["set_"..prop] = function(self, value)
if type(value) ~= "table" then
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("property::"..prop, value)
self:emit_signal("widget::layout_changed")
end
else
self._private["horizontal_"..prop] = value.horizontal
self._private["vertical_"..prop] = value.vertical
self:emit_signal("property::"..prop, value)
self:emit_signal("widget::layout_changed")
end
end
grid["get_"..prop] = function(self)
return {
vertical = self._private["vertical_" .. prop],
horizontal = self._private["horizontal_" .. 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
-- All the code to get the width of a specific border.
--
-- This table module supports partial borders and "just add a border" modes.
local function setup_border_widths(self)
self._private.border_width = {inner = 0, outer = 0}
self._private.custom_border_width = {rows = {}, cols = {}}
self._private.border_color = {}
-- Use a metatable to get the defaults.
local function meta_border_common(custom, row_or_col)
return setmetatable({}, {
__index = function(_, k)
-- Handle custom borders.
if custom[k] then
return custom[k].size
end
local size = self[row_or_col.."_count"]
if k == 1 or k == size + 1 then
return self._private.border_width.outer
else
return self._private.border_width.inner
end
end
})
end
local hfb = self._private.custom_border_width.rows
local vfb = self._private.custom_border_width.cols
self._private.meta_borders = {
rows = meta_border_common(hfb, "row"),
cols = meta_border_common(vfb, "column"),
}
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, border_widths)
local m = border_widths[1]
local space = self._private[dir .. "_spacing"]
-- First border
m = m > 0 and m + space or m
if self._private[dir .. "_homogeneous"] then
local max = max_value(sizes)
-- all the columns/rows have the same size
if self._private.has_border then
-- Not all borders are identical, so the loop is required.
for i in ipairs(sizes) do
local bw = border_widths[i+1]
-- When there is a border, it needs the spacing on both sides.
m = m + max + (space*(bw > 0 and 2 or 1)) + bw
end
else
-- Much simpler.
m = #sizes * max + (#sizes - 1) * space
end
else
-- sum the columns/rows size
for i, s in ipairs(sizes) do
local bw = border_widths[i+1]
-- When there is a border, it needs the spacing on both sides.
m = m + s + (space * (bw > 0 and 2 or 1)) + bw
end
end
return m
end
-- fit matrix cells
local rows_size, cols_size = get_grid_sizes(self, context, width, height)
-- compute the width
local borders = self._private.meta_borders
local used_width_max = fit_direction("horizontal", cols_size, borders.cols)
local used_height_max = fit_direction("vertical", rows_size, borders.rows)
return used_width_max, used_height_max
end
local function layout_common(self, context, width, height, h_homogeneous, v_homogeneous)
local result, areas = {}, {}
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)
local h_bw, v_bw = self._private.meta_borders.cols, self._private.meta_borders.rows
-- Do it once, the result wont change unless widgets are added.
if self._private.has_border and not self._private.area_cache.total_horizontal_border_width then
-- Also add the "second" spacing here. This avoid having some `if` below.
local total_h = h_bw[1] + h_bw[#cols_size+1] + 1*hspacing
local total_v = v_bw[1] + v_bw[#rows_size+1] + 1*vspacing
for j = 1, #cols_size do
local bw = h_bw[j+1]
total_h = total_h + bw + hspacing*(bw > 0 and 1 or 0)
end
for i = 1, #rows_size do
local bw = v_bw[i+1]
total_v = total_v + bw + vspacing*(bw > 0 and 1 or 0)
end
self._private.area_cache.total_horizontal_border_width = total_h - h_bw[1]
self._private.area_cache.total_vertical_border_width = total_v - v_bw[1]
end
local total_h = self._private.area_cache.total_horizontal_border_width or 0
local total_v = self._private.area_cache.total_vertical_border_width or 0
-- 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 - total_h) / self._private.num_cols
end
if self._private.vertical_expand then
single_height = (height - (self._private.num_rows-1)*vspacing - total_v) / self._private.num_rows
end
-- Calculate the position and size to place the widgets
local cumul_width, cumul_height = {}, {}
local c_hor, c_ver = h_bw[1], v_bw[1]
-- If there is an outer border, then it needs inner spacing too.
c_hor, c_ver = c_hor > 0 and c_hor + hspacing or 0, c_ver > 0 and c_ver + vspacing or 0
for j = 1, #cols_size do
cumul_width[j] = c_hor
if h_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
local bw = h_bw[j+1]
c_hor = c_hor + cols_size[j] + (bw > 0 and 2 or 1)*hspacing + bw
end
cumul_width[#cols_size + 1] = c_hor
for i = 1, #rows_size do
cumul_height[i] = c_ver
if v_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
local bw = v_bw[i+1]
c_ver = c_ver + rows_size[i] + (bw > 0 and 2 or 1)*vspacing + bw
end
cumul_height[#rows_size + 1] = c_ver
-- Place widgets
local fill_space = true -- should be fill_space property?
for _, v in pairs(self._private.widgets) do
local x, y, w, h
-- If there is a border, then the spacing is needed on both sides.
local col_bw, row_bw = h_bw[v.col+v.col_span], v_bw[v.row+v.row_span]
local col_spacing = hspacing * (col_bw > 0 and 2 or 1)
local row_spacing = vspacing * (row_bw > 0 and 2 or 1)
-- 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] - col_spacing - x - col_bw)
h = math.floor(cumul_height[v.row + v.row_span] - row_spacing - y - row_bw)
-- Handle large spacing and/or border_width. The grid doesn't support
-- dropping widgets. It would be very hard to implement.
w, h = math.max(0, w), math.max(0, h)
-- 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))
table.insert(areas, {
x = x - hspacing,
y = y - vspacing,
width = w + col_spacing,
height = h + row_spacing,
})
end
end
-- Sometime, the `:fit()` size and `:layout()` size are different, thus it's
-- important to say where the widget actually ends.
areas.end_x = cumul_width[#cumul_width] - hspacing
areas.end_y = cumul_height[#cumul_height] - vspacing
areas.column_count = #cols_size
areas.row_count = #rows_size
areas.cols = cumul_width
areas.rows = cumul_height
return result, areas
end
local function get_area_cache_hash(width, height)
return width*1.5+height*15
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 l, areas = layout_common(
self,
context,
width,
height,
self._private.horizontal_homogeneous,
self._private.vertical_homogeneous
)
self._private.area_cache[get_area_cache_hash(width, height)] = areas
return l
end
local function create_border_mask(self, areas, default_color)
if areas.surface then return areas.surface end
local meta = self._private.meta_borders
local top, bottom = meta.rows[1], meta.rows[areas.row_count+1]
local left, right = meta.cols[1], meta.cols[areas.column_count+1]
-- A1 is fine because :layout() aligns to pixel boundary and `border_width`
-- are integers.
local img = cairo.RecordingSurface(cairo.Content.COLOR_ALPHA, cairo.Rectangle {
x = 0,
y = 0,
width = areas.end_x + right,
height = areas.end_y + bottom
})
local cr = cairo.Context(img)
cr:set_source(default_color)
local bw_i, bw_o = self._private.border_width.inner, self._private.border_width.outer
if bw_i ~= bw_o then
if bw_o then
if self._private.border_color.outer then
cr:set_source(self._private.border_color.outer)
end
-- Clip the outside region. It cannot use `cr:set_line_width()` because
-- each border might be different.
cr:rectangle(0, 0, areas.end_x, top)
cr:rectangle(0, areas.end_y - bottom, areas.end_x, bottom)
cr:rectangle(0, top, left, areas.end_y - top - bottom)
cr:rectangle(areas.end_x - right, top, right, areas.end_y - top - bottom)
cr:clip()
cr:paint()
cr:reset_clip()
end
cr:rectangle(left, top, areas.end_x - top - bottom, areas.end_y - left - right)
cr:clip()
else
cr:rectangle(0,0, areas.end_x, areas.end_y)
cr:clip()
end
if bw_i then
if self._private.border_color.inner then
cr:set_source(self._private.border_color.inner)
end
cr:rectangle(0, 0, areas.end_x, areas.end_y)
cr:fill()
end
-- Add the custom horizontal and borders.
-- This is a lifeline for users who want borders only on specific places.
-- Implementing word processing style borders would be overkill and
-- too hard to maintain.
for _, orientation in ipairs { "rows", "cols" } do
for row, args in pairs(self._private.custom_border_width[orientation]) do
local line_height = meta[orientation][row]
cr:save()
cr:rectangle(0,0, areas.end_x, areas.end_y)
cr:clip()
cr:set_line_width(line_height)
if args.dashes then
cr:set_dash(args.dashes, #args.dashes, args.offset or 0)
end
if args.caps then
cr:set_line_cap(cairo.LineCap[args.caps:upper()])
end
cr:set_source(args.color)
-- Cairo draw the stroke equally on both side, for `line_height/2` is
-- needed.
local y = (row == 1 and line_height or areas[orientation][row] or 0) - math.ceil(line_height/2)
if orientation == "rows" then
cr:move_to(0, y)
cr:line_to(areas.end_x, y)
else
cr:move_to(y, 0)
cr:line_to(y, areas.end_y)
end
cr:stroke()
cr:restore()
end
end
-- Remove the area used by widgets. This needs to be done regardless of the
-- border mode to handle row/col span.
cr:set_operator(cairo.Operator.CLEAR)
for _, area in ipairs(areas) do
cr:rectangle(area.x, area.y, area.width, area.height)
end
cr:fill()
areas.surface = img
return img
end
-- Draw the border.
function grid:after_draw_children(ctx, cr, width, height)
if not self._private.has_border then return end
local hash = get_area_cache_hash(width, height)
if not self._private.area_cache[hash] then
self._private.area_cache[hash] = select(2, layout_common(
self,
ctx,
width,
height,
self._private.horizontal_homogeneous,
self._private.vertical_homogeneous
))
end
local areas = self._private.area_cache[hash]
cr:set_source_surface(create_border_mask(self, areas, cr:get_source()), 0 ,0)
cr:paint()
end
--- Reset the grid layout.
-- Remove all widgets and reset row and column counts
--
-- @method reset
-- @emits reset
-- @noreturn
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.
-- @constructorfct 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})
gtable.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
ret._private.area_cache, ret._private.border_color = {}, {}
ret:connect_signal("widget::layout_changed", function(self)
self._private.area_cache = {}
end)
setup_border_widths(ret)
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.
-- @constructorfct wibox.layout.grid.horizontal
function grid.horizontal(forced_num_rows, widget, ...)
local ret = new("horizontal")
ret:set_forced_num_rows(forced_num_rows)
if widget then
ret:add(widget, ...)
end
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.
-- @constructorfct wibox.layout.grid.vertical
function grid.vertical(forced_num_cols, widget, ...)
local ret = new("vertical")
ret:set_forced_num_cols(forced_num_cols)
if widget then
ret:add(widget, ...)
end
return ret
end
function grid.mt:__call(...)
return new(...)
end
--@DOC_fixed_COMMON@
return setmetatable(grid, grid.mt)
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80