grid: Add border support.

This is long overdue. A bit of historical context. The grid API
is losely somewhat based on the old `radical` module, but was
heavily improved by @getzze. That version had row_span and col_span.

This made the way the previous implementation coded the border
incompatible. I spent some time back then trying to bolt it back on,
but the complexity is quite high and never made it work right.

This commit goes in another direction. Rather than draw the border,
it creates a mask where the border should *not* be, then bucket fill
the widget. This is the equivalent of CSS `border-collapse`.

It also support custom borders. This allows dashed lines and partial
borders.

The main use case will be to add border support to the calendar. It
was previously possible to partially do it using custom cell painters,
but was pretty hacky. Now that the calendar will deprecate the custom
painters in favor of `widget_template`s, a more robust alternative was
required.

The drawback of this commit is obviously the added complexity to the
most complex layout. This is why it adds many tests to cover the various
corner cases.
This commit is contained in:
Emmanuel Lepage Vallee 2022-12-04 21:48:26 -08:00
parent 2d5c4b64c5
commit f8f5e2c69b
10 changed files with 939 additions and 33 deletions

View File

@ -7,8 +7,7 @@
-- --
--@DOC_wibox_layout_grid_imperative_EXAMPLE@ --@DOC_wibox_layout_grid_imperative_EXAMPLE@
-- --
-- The same can be done using the declarative syntax. When using this mode, -- The same can be done using the declarative syntax:
-- all widgets will again the `row_span` and `col_span` properties:
-- --
--@DOC_wibox_layout_grid_declarative1_EXAMPLE@ --@DOC_wibox_layout_grid_declarative1_EXAMPLE@
-- --
@ -31,7 +30,10 @@ local pairs = pairs
local ipairs = ipairs local ipairs = ipairs
local math = math local math = math
local gtable = require("gears.table") local gtable = require("gears.table")
local gmath = require("gears.math")
local gcolor = require("gears.color")
local base = require("wibox.widget.base") local base = require("wibox.widget.base")
local cairo = require("lgi").cairo
local grid = { mt = {} } local grid = { mt = {} }
@ -74,6 +76,7 @@ local dir_properties = { "spacing", "homogeneous", "expand" }
-- @propertyunit rows -- @propertyunit rows
-- @negativeallowed false -- @negativeallowed false
-- @see forced_num_cols -- @see forced_num_cols
-- @see row_count
--- Force the number of columns of the layout. --- Force the number of columns of the layout.
-- @property forced_num_cols -- @property forced_num_cols
@ -82,6 +85,7 @@ local dir_properties = { "spacing", "homogeneous", "expand" }
-- @propertyunit columns -- @propertyunit columns
-- @negativeallowed false -- @negativeallowed false
-- @see forced_num_rows -- @see forced_num_rows
-- @see column_count
--- Set the minimum size for the columns. --- Set the minimum size for the columns.
-- --
@ -123,6 +127,12 @@ local dir_properties = { "spacing", "homogeneous", "expand" }
-- preferred `orientation`. -- preferred `orientation`.
-- --
--@DOC_wibox_layout_grid_spacing_EXAMPLE@ --@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@
--
-- @tparam[opt=0] number spacing -- @tparam[opt=0] number spacing
-- @property spacing -- @property spacing
-- @negativeallowed false -- @negativeallowed false
@ -186,6 +196,25 @@ local dir_properties = { "spacing", "homogeneous", "expand" }
-- @see vertical_homogeneous -- @see vertical_homogeneous
-- @see horizontal_homogeneous -- @see horizontal_homogeneous
--- The number of rows.
--
-- If `forced_num_rows` is set, then its value is returned, otherwise it will
-- return the maximum actual number of widgets in a row.
--
-- @property row_count
-- @tparam integer row_count
-- @readonly
-- @see forced_num_rows
--- The number of columns.
--
-- If `forced_num_cols` is set, then its value is returned, otherwise it will
-- return the maximum actual number of widgets in a column.
--
-- @property column_count
-- @readonly
-- @tparam integer column_count
-- @see forced_num_cols
--- Child widget position. Return of `get_widget_position`. --- Child widget position. Return of `get_widget_position`.
-- @field row Top row index -- @field row Top row index
@ -322,7 +351,7 @@ end
-- The widgets are assumed to span one cell. -- The widgets are assumed to span one cell.
-- --
-- If the widgets have a `row_index`, `col_index`, `col_span` -- If the widgets have a `row_index`, `col_index`, `col_span`
-- or `row_span` property, it will be honorred. -- or `row_span` property, it will be honored.
-- --
-- @method add -- @method add
-- @tparam wibox.widget ... Widgets that should be added (must at least be one) -- @tparam wibox.widget ... Widgets that should be added (must at least be one)
@ -659,6 +688,90 @@ function grid:remove_row(index)
end 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 -- Return list of children
function grid:get_children() function grid:get_children()
local ret = {} local ret = {}
@ -715,6 +828,78 @@ function grid:set_forced_num_rows(val)
end end
end end
function grid:get_row_count()
return self._private.num_rows
end
function grid:get_column_count()
return self._private.num_cols
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 -- Set the grid properties
for _, prop in ipairs(properties) do for _, prop in ipairs(properties) do
if not grid["set_" .. prop] then if not grid["set_" .. prop] then
@ -796,6 +981,41 @@ local function get_grid_sizes(self, context, orig_width, orig_height)
return rows_size, cols_size return rows_size, cols_size
end 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. -- Fit the grid layout into the given space.
-- @param context The context in which we are fit. -- @param context The context in which we are fit.
@ -805,18 +1025,42 @@ function grid:fit(context, orig_width, orig_height)
local width, height = orig_width, orig_height local width, height = orig_width, orig_height
-- Calculate the space needed -- Calculate the space needed
local function fit_direction(dir, sizes) local function fit_direction(dir, sizes, border_widths)
local m = 0 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 if self._private[dir .. "_homogeneous"] then
local max = max_value(sizes)
-- all the columns/rows have the same size -- all the columns/rows have the same size
m = #sizes * max_value(sizes) + (#sizes - 1) * self._private[dir .. "_spacing"] 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 else
-- sum the columns/rows size -- sum the columns/rows size
for _,s in ipairs(sizes) do for i, s in ipairs(sizes) do
m = m + s + self._private[dir .. "_spacing"] 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
m = m - self._private[dir .. "_spacing"]
end end
return m return m
end end
@ -824,69 +1068,117 @@ function grid:fit(context, orig_width, orig_height)
local rows_size, cols_size = get_grid_sizes(self, context, width, height) local rows_size, cols_size = get_grid_sizes(self, context, width, height)
-- compute the width -- compute the width
local used_width_max = fit_direction("horizontal", cols_size) local borders = self._private.meta_borders
local used_height_max = fit_direction("vertical", rows_size) 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 return used_width_max, used_height_max
end end
-- Layout a grid layout. local function layout_common(self, context, width, height, h_homogeneous, v_homogeneous)
-- @param context The context in which we are drawn. local result, areas = {}, {}
-- @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 local hspacing, vspacing = self._private.horizontal_spacing, self._private.vertical_spacing
-- Fit matrix cells -- Fit matrix cells
local rows_size, cols_size = get_grid_sizes(self, context, width, height) 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 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 -- 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) local single_width, single_height = max_value(cols_size), max_value(rows_size)
if self._private.horizontal_expand then if self._private.horizontal_expand then
single_width = (width - (self._private.num_cols-1)*hspacing) / self._private.num_cols single_width = (width - (self._private.num_cols-1)*hspacing - total_h) / self._private.num_cols
end end
if self._private.vertical_expand then if self._private.vertical_expand then
single_height = (height - (self._private.num_rows-1)*vspacing) / self._private.num_rows single_height = (height - (self._private.num_rows-1)*vspacing - total_v) / self._private.num_rows
end end
-- Calculate the position and size to place the widgets -- Calculate the position and size to place the widgets
local cumul_width, cumul_height = {}, {} local cumul_width, cumul_height = {}, {}
local cw, ch = 0, 0 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 for j = 1, #cols_size do
cumul_width[j] = cw cumul_width[j] = c_hor
if self._private.horizontal_homogeneous then
if h_homogeneous then
cols_size[j] = math.max(self._private.min_cols_size, single_width) cols_size[j] = math.max(self._private.min_cols_size, single_width)
elseif self._private.horizontal_expand then elseif self._private.horizontal_expand then
local hpercent = self._private.num_cols * single_width * cols_size[j] / total_expected_width 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) cols_size[j] = math.max(self._private.min_cols_size, hpercent)
end end
cw = cw + cols_size[j] + hspacing
local bw = h_bw[j+1]
c_hor = c_hor + cols_size[j] + (bw > 0 and 2 or 1)*hspacing + bw
end end
cumul_width[#cols_size + 1] = cw
cumul_width[#cols_size + 1] = c_hor
for i = 1, #rows_size do for i = 1, #rows_size do
cumul_height[i] = ch cumul_height[i] = c_ver
if self._private.vertical_homogeneous then
if v_homogeneous then
rows_size[i] = math.max(self._private.min_rows_size, single_height) rows_size[i] = math.max(self._private.min_rows_size, single_height)
elseif self._private.vertical_expand then elseif self._private.vertical_expand then
local vpercent = self._private.num_rows * single_height * rows_size[i] / total_expected_height 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) rows_size[i] = math.max(self._private.min_rows_size, vpercent)
end end
ch = ch + rows_size[i] + vspacing
local bw = v_bw[i+1]
c_ver = c_ver + rows_size[i] + (bw > 0 and 2 or 1)*vspacing + bw
end end
cumul_height[#rows_size + 1] = ch
cumul_height[#rows_size + 1] = c_ver
-- Place widgets -- Place widgets
local fill_space = true -- should be fill_space property? local fill_space = true -- should be fill_space property?
for _, v in pairs(self._private.widgets) do for _, v in pairs(self._private.widgets) do
local x, y, w, h 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 -- Round numbers to avoid decimals error, force to place tight widgets
-- and avoid redraw glitches -- and avoid redraw glitches
x = math.floor(cumul_width[v.col]) x = math.floor(cumul_width[v.col])
y = math.floor(cumul_height[v.row]) y = math.floor(cumul_height[v.row])
w = math.floor(cumul_width[v.col + v.col_span] - hspacing - x) 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] - vspacing - y) 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 -- Recalculate the width so the last widget fits
if (fill_space or self._private.horizontal_expand) and x + w > width then 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)) w = math.floor(math.max(self._private.min_cols_size, width - x))
@ -898,11 +1190,179 @@ function grid:layout(context, width, height)
-- Place the widget if it fits in the area -- Place the widget if it fits in the area
if x + w <= width and y + h <= height then if x + w <= width and y + h <= height then
table.insert(result, base.place_widget_at(v.widget, x, y, w, h)) 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
end end
return result
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. --- Reset the grid layout.
-- Remove all widgets and reset row and column counts -- Remove all widgets and reset row and column counts
@ -963,6 +1423,14 @@ local function new(orientation)
ret._private.horizontal_spacing = 0 ret._private.horizontal_spacing = 0
ret._private.vertical_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 return ret
end end

View File

@ -0,0 +1,85 @@
--DOC_GEN_IMAGE --DOC_HIDE_START
local generic_widget_ = ...
local wibox = require("wibox")
local beautiful = require("beautiful")
local l = wibox.widget {
spacing = 10,
layout = wibox.layout.fixed.horizontal
}
local function generic_widget(txt)
return generic_widget_(txt, nil, 0)
end
--DOC_HIDE_END
local w = wibox.widget {
--DOC_HIDE_START
-- [...] Some widgets here.
{
text = "none",
row_index = 1,
col_index = 1,
col_span = 3,
widget = generic_widget
},
{
text = "first",
col_span = 2,
row_index = 2,
col_index = 1,
widget = generic_widget
},
{
text = "third",
row_index = 2,
col_index = 3,
row_span = 2,
widget = generic_widget
},
{
text = "second",
row_index = 3,
col_index = 1,
col_span = 2,
widget = generic_widget
},
{
text = "fourth",
row_index = 4,
col_index = 1,
widget = generic_widget
},
{
text = "fifth",
row_index = 4,
col_index = 2,
col_span = 2,
widget = generic_widget
},
{
text = "sixth",
row_index = 1,
col_index = 4,
row_span = 4,
widget = generic_widget
},
--DOC_HIDE_END
homogeneous = true,
spacing = 0,
border_width = 4,
border_color = beautiful.border_color,
min_cols_size = 10,
min_rows_size = 10,
layout = wibox.layout.grid,
}
--DOC_NEWLINE
w:add_column_border(1, 5 , { color = "purple" })
w:add_column_border(2, 10, { color = "cyan" })
w:add_column_border(3, 5 , { color = "magenta"})
w:add_column_border(4, 5 , { color = "black" })
w:add_column_border(5, 10, { color = "grey" })
l:add(w) --DOC_HIDE
return l, l:fit({dpi=96}, 9999, 9999) --DOC_HIDE

View File

@ -0,0 +1,85 @@
--DOC_GEN_IMAGE --DOC_HIDE_START
local generic_widget_ = ...
local wibox = require("wibox")
local beautiful = require("beautiful")
local l = wibox.widget {
spacing = 10,
layout = wibox.layout.fixed.horizontal
}
local function generic_widget(txt)
return generic_widget_(txt, nil, 0)
end
--DOC_HIDE_END
local w = wibox.widget {
-- [...] Some widgets here.
--DOC_HIDE_START
{
text = "none",
row_index = 1,
col_index = 1,
col_span = 3,
widget = generic_widget
},
{
text = "first",
col_span = 2,
row_index = 2,
col_index = 1,
widget = generic_widget
},
{
text = "third",
row_index = 2,
col_index = 3,
row_span = 2,
widget = generic_widget
},
{
text = "second",
row_index = 3,
col_index = 1,
col_span = 2,
widget = generic_widget
},
{
text = "fourth",
row_index = 4,
col_index = 1,
widget = generic_widget
},
{
text = "fifth",
row_index = 4,
col_index = 2,
col_span = 2,
widget = generic_widget
},
{
text = "sixth",
row_index = 1,
col_index = 4,
row_span = 4,
widget = generic_widget
},
--DOC_HIDE_END
homogeneous = true,
spacing = 0,
border_width = 4,
border_color = beautiful.border_color,
min_cols_size = 10,
min_rows_size = 10,
layout = wibox.layout.grid,
}
--DOC_NEWLINE
w:add_row_border(1, 40, { color = "red" })
w:add_row_border(2, 5 , { color = "green" , dashes = {5, 3, 10, 3}})
w:add_row_border(3, 10, { color = "blue" , dashes = {5, 3, 10, 3}, dash_offset = 5})
w:add_row_border(4, 30, { color = "orange", dashes = {5, 40}, caps = "round"})
w:add_row_border(5, 10, { color = "yellow"})
l:add(w) --DOC_HIDE
return l, l:fit({dpi=96}, 9999, 9999) --DOC_HIDE

View File

@ -0,0 +1,62 @@
--DOC_GEN_IMAGE --DOC_HIDE_START --DOC_NO_USAGE
local generic_widget_ = ...
local wibox = require("wibox")
local l = wibox.widget {
spacing = 10,
layout = wibox.layout.fixed.vertical
}
local function generic_widget(txt)
return generic_widget_(txt, nil, 0)
end
--DOC_HIDE_END
for _, expand in ipairs { true, false } do
for _, homogeneous in ipairs { true, false } do
--DOC_HIDE_START
local row = wibox.widget {
spacing = 10,
layout = wibox.layout.fixed.horizontal
}
l:add(wibox.widget.textbox(
"<b>`expand = "..tostring(expand).."`, `homogeneous = "..tostring(homogeneous).."`</b>:"
))
l:add(row)
--DOC_HIDE_END
for _, width in ipairs { 0, 1, 2, 4, 10 } do
local w = wibox.widget {
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,
homogeneous = homogeneous,
spacing = 10,
border_width = {
inner = width,
outer = 1.5 * width,
},
border_color = "red",
expand = expand,
forced_height = expand and 200 or nil,
layout = wibox.layout.grid,
}
--DOC_HIDE_START
row:add(wibox.widget {
wibox.widget.textbox("<i> `border_width = ".. width .."`</i>: "),
w,
layout = wibox.layout.fixed.vertical
})
--DOC_HIDE_END
end
end
end
--DOC_HIDE_START
return l, l:fit({dpi=96}, 9999, 9999)

View File

@ -0,0 +1,91 @@
--DOC_GEN_IMAGE --DOC_HIDE_START --DOC_NO_USAGE
local generic_widget_ = ...
local wibox = require("wibox")
local beautiful = require("beautiful")
local l = wibox.widget {
spacing = 10,
layout = wibox.layout.fixed.horizontal
}
local function generic_widget(txt)
return generic_widget_(txt, nil, 0)
end
--DOC_HIDE_END
local w = wibox.widget {
--DOC_HIDE_START
-- [...] Some widgets here.
{
text = "none",
row_index = 1,
col_index = 1,
col_span = 3,
widget = generic_widget
},
{
text = "first",
col_span = 2,
row_index = 2,
col_index = 1,
widget = generic_widget
},
{
text = "third",
row_index = 2,
col_index = 3,
row_span = 2,
widget = generic_widget
},
{
text = "second",
row_index = 3,
col_index = 1,
col_span = 2,
widget = generic_widget
},
{
text = "fourth",
row_index = 4,
col_index = 1,
widget = generic_widget
},
{
text = "fifth",
row_index = 4,
col_index = 2,
col_span = 2,
widget = generic_widget
},
{
text = "sixth",
row_index = 1,
col_index = 4,
row_span = 4,
widget = generic_widget
},
--DOC_HIDE_END
homogeneous = true,
spacing = 0,
border_width = 4,
border_color = beautiful.border_color,
min_cols_size = 10,
min_rows_size = 10,
layout = wibox.layout.grid,
}
--DOC_NEWLINE
w:add_row_border(1, 40, { color = "red" })
w:add_row_border(2, 5 , { color = "green" , dashes = {5, 3, 10, 3}})
w:add_row_border(3, 10, { color = "blue" , dashes = {5, 3, 10, 3}, dash_offset = 5})
w:add_row_border(4, 30, { color = "orange", dashes = {5, 40}, caps = "round"})
w:add_row_border(5, 10, { color = "yellow"})
--DOC_NEWLINE
w:add_column_border(1, 5, { color = "purple"})
w:add_column_border(2, 10, { color = "cyan"})
w:add_column_border(3, 5, { color = "magenta"})
w:add_column_border(4, 5, { color = "black"})
w:add_column_border(5, 10, { color = "grey"})
l:add(w) --DOC_HIDE
return l, l:fit({dpi=96}, 9999, 9999) --DOC_HIDE

View File

@ -0,0 +1,40 @@
--DOC_GEN_IMAGE --DOC_HIDE_START --DOC_NO_USAGE
local generic_widget_ = ...
local wibox = require("wibox") --DOC_HIDE
local l = wibox.widget {
spacing = 10,
layout = wibox.layout.fixed.horizontal
}
local function generic_widget(txt)
return generic_widget_(txt, nil, 0)
end
--DOC_HIDE_END
for _, width in ipairs { 0, 1, 2, 4, 10 } do
local w = wibox.widget {
generic_widget( "first" ),
generic_widget( "second" ),
generic_widget( "third" ),
generic_widget( "fourth" ),
forced_num_cols = 2,
forced_num_rows = 2,
homogeneous = true,
spacing = width,
border_width = 1,
border_color = "red",
layout = wibox.layout.grid,
}
--DOC_HIDE_START
l:add(wibox.widget {
wibox.widget.textbox("<b> `spacing = ".. width .."`</b>: "),
w,
layout = wibox.layout.fixed.vertical
})
--DOC_HIDE_END
end
--DOC_HIDE_START
return l, l:fit({dpi=96}, 9999, 9999)

View File

@ -0,0 +1,67 @@
--DOC_GEN_IMAGE --DOC_HIDE_START --DOC_NO_USAGE
local generic_widget_ = ...
local wibox = require("wibox")
local gears = { color = require("gears.color") }
local l = wibox.widget {
spacing = 10,
layout = wibox.layout.fixed.horizontal
}
local function generic_widget(txt)
return generic_widget_(txt, nil, 0)
end
--DOC_HIDE_END
for _, width in ipairs { 0, 1, 2, 4, 10 } do
local w = wibox.widget {
generic_widget( "first" ),
generic_widget( "second" ),
generic_widget( "third" ),
generic_widget( "fourth" ),
forced_num_cols = 2,
forced_num_rows = 2,
homogeneous = true,
spacing = 10,
border_width = {
inner = width,
outer = 10 - width,
},
border_color = {
inner = gears.color {
type = "linear",
from = { 0 , 0 },
to = { 100, 100 },
stops = {
{ 0, "#ff0000" },
{ 1, "#ffff00" },
}
},
outer = gears.color {
type = "linear",
from = { 0 , 0 },
to = { 100, 100 },
stops = {
{ 0, "#0000ff" },
{ 1, "#ff0000" },
}
},
},
layout = wibox.layout.grid,
}
--DOC_HIDE_START
l:add(wibox.widget {
wibox.widget.textbox(
"<b>"..
"`border_width.outer = ".. (10-width) .."`\n"..
"`border_width.inner = ".. width .."`"..
"</b>:"),
w,
layout = wibox.layout.fixed.vertical
})
--DOC_HIDE_STOP
end
--DOC_HIDE_START
return l, l:fit({dpi=96}, 9999, 9999)

View File

@ -1,6 +1,7 @@
--DOC_GEN_IMAGE --DOC_HIDE_START --DOC_GEN_IMAGE --DOC_HIDE_START
local generic_widget = ... --DOC_NO_USAGE local generic_widget = ... --DOC_NO_USAGE
local wibox = require("wibox") local wibox = require("wibox")
local beautiful = require("beautiful")
--DOC_HIDE_END --DOC_HIDE_END
@ -53,9 +54,11 @@ local wibox = require("wibox")
}, },
homogeneous = true, homogeneous = true,
spacing = 5, spacing = 5,
border_width = 1,
border_color = beautiful.border_color,
min_cols_size = 10, min_cols_size = 10,
min_rows_size = 10, min_rows_size = 10,
layout = wibox.layout.grid, layout = wibox.layout.grid,
} }
return l, l:fit({dpi=96}, 300, 200) --DOC_HIDE return l, l:fit({dpi=96}, 400, 200) --DOC_HIDE

View File

@ -66,6 +66,7 @@ wibox.widget {
}, },
layout = wibox.layout.fixed.vertical layout = wibox.layout.fixed.vertical
}, },
spacing = 5,
layout = wibox.layout.fixed.horizontal layout = wibox.layout.fixed.horizontal
} }
, 300, 90 --DOC_HIDE , 300, 90 --DOC_HIDE

View File

@ -1,8 +1,12 @@
--DOC_GEN_IMAGE --DOC_GEN_IMAGE
local generic_widget = ... --DOC_HIDE_ALL local generic_widget_ = ... --DOC_HIDE_ALL
local wibox = require("wibox") --DOC_HIDE local wibox = require("wibox") --DOC_HIDE
local beautiful = require("beautiful") --DOC_HIDE local beautiful = require("beautiful") --DOC_HIDE
local function generic_widget(txt)
return generic_widget_(txt, nil, 0)
end
local w = wibox.widget { local w = wibox.widget {
{ {
{ {