diff --git a/libs/canvas/canvas/table.h b/libs/canvas/canvas/table.h new file mode 100644 index 0000000000..7d1d26d123 --- /dev/null +++ b/libs/canvas/canvas/table.h @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2021 Paul Davis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef __CANVAS_TABLE_H__ +#define __CANVAS_TABLE_H__ + +#include + +#include "canvas/rectangle.h" + +namespace ArdourCanvas +{ + +class Rectangle; + +class LIBCANVAS_API Table : public Rectangle +{ +public: + Table (Canvas *); + Table (Item *); + +#if 0 + void set_spacing (double s); + void set_padding (double top, double right = -1.0, double bottom = -1.0, double left = -1.0); + void set_margin (double top, double right = -1.0, double bottom = -1.0, double left = -1.0); + + /* aliases so that CSS box model terms work */ + void set_border_width (double w) { set_outline_width (w); } + void set_border_color (Gtkmm2ext::Color c) { set_outline_color (c); } + + void set_collapse_on_hide (bool); + void set_homogenous (bool); +#endif + + void compute_bounding_box () const; + void size_request (double& w, double& h) const; + void size_allocate_children (Rect const & r); + void _size_allocate (Rect const & r); + + struct Index { + Index (uint32_t xv, uint32_t yv) : x (xv), y (yv) {} + + bool operator== (Index const & other) const { + return x == other.x && y == other.y; + } + + uint32_t x; + uint32_t y; + }; + + void attach (Index upper_left, Index lower_right, Item*, PackOptions row_options = PackOptions (0), PackOptions col_options = PackOptions (0)); + + protected: + void child_changed (bool bbox_changed); + + private: + Distance top_margin; + Distance right_margin; + Distance bottom_margin; + Distance left_margin; + Distance row_spacing; + Distance col_spacing; + + bool collapse_on_hide; + bool homogenous; + bool draw_hgrid; + bool draw_vgrid; + + mutable bool ignore_child_changes; + + struct CellInfo { + Item* content; + PackOptions row_options; + PackOptions col_options; + Rect cell; + Duple natural_size; + Duple allocate_size; + Duple full_size; + + CellInfo (Item* i, PackOptions ro, PackOptions co, Rect c) + : content (i) + , row_options (ro) + , col_options (co) + , cell (c) + {} + }; + + struct index_hash { + std::size_t operator() (Table::Index const & i) const { + /* use upper left coordinate for hash */ + return ((uint64_t) i.x << 32) | i.y; + } + }; + + /* cell lookup, indexed by upper left corner only + */ + + typedef std::unordered_map Cells; + Cells cells; + + struct AxisInfo { + uint32_t expanders; + uint32_t shrinkers; + Distance natural_size; + Distance expand; + Distance shrink; + + AxisInfo() : expanders (0), shrinkers (0), natural_size (0) {} + }; + + typedef std::vector AxisInfos; + AxisInfos row_info; + AxisInfos col_info; + + void layout (); +}; + +} /* namespace */ + +#endif /* __CANVAS_TABLE_H__ */ diff --git a/libs/canvas/table.cc b/libs/canvas/table.cc new file mode 100644 index 0000000000..ba5f9ac863 --- /dev/null +++ b/libs/canvas/table.cc @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2021 Paul Davis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "canvas/table.h" + +using namespace ArdourCanvas; +using namespace PBD; + +using std::cerr; +using std::endl; + + +Table::Table (Canvas* canvas) + : Rectangle (canvas) +{ +} + +Table::Table (Item* item) + : Rectangle (item) +{ +} + +void +Table::attach (Table::Index upper_left, Table::Index lower_right, Item* item, PackOptions row_options, PackOptions col_options) +{ + cells.insert ({ Index (upper_left.x, upper_left.y), CellInfo (item, row_options, col_options, Rect (upper_left.x, upper_left.y, lower_right.x, lower_right.y)) }); +} + +void +Table::child_changed (bool bbox_changed) +{ + if (ignore_child_changes) { + return; + } + + Item::child_changed (bbox_changed); + size_allocate_children (_allocation); +} + +void +Table::compute_bounding_box() const +{ + _bounding_box = Rect(); + if (cells.empty()) { + bb_clean (); + return; + } + + add_child_bounding_boxes (!collapse_on_hide); + + if (_bounding_box) { +#if 0 + Rect r = _bounding_box; + + _bounding_box = r.expand (top_padding + outline_width() + top_margin, + right_padding + outline_width() + right_margin, + bottom_padding + outline_width() + bottom_margin, + left_padding + outline_width() + left_margin); +#endif + } + + bb_clean (); + +} + +void +Table::size_request (Distance& w, Distance& h) const +{ + int rowmax = 0; + int colmax = 0; + + for (auto& ci : cells) { + CellInfo const & c (ci.second); + + if (c.cell.x1 > rowmax) { + rowmax = c.cell.x1; + } + + if (c.cell.y1 > colmax) { + colmax = c.cell.y1; + } + } + + AxisInfos rinfo; + AxisInfos cinfo; + + rinfo.resize (rowmax); + cinfo.resize (colmax); + + for (auto& ci : cells) { + + Distance cw; + Distance ch; + + CellInfo const & c (ci.second); + c.content->size_request (cw, ch); + + rinfo[c.cell.x0].natural_size += cw; + cinfo[c.cell.y0].natural_size += ch; + } + + w = 0; + h = 0; + + for (auto& ai : rinfo) { + w = std::max (w, ai.natural_size); + } + + for (auto& ai : cinfo) { + h = std::max (h, ai.natural_size); + } +} + +void +Table::_size_allocate (Rect const & r) +{ + /* nothing to do here */ +} + +void +Table::layout () +{ + size_allocate_children (_allocation); +} + +void +Table::size_allocate_children (Rect const & within) +{ + /* step 1: traverse all current cells and determine how many rows and + * columns we need. While doing that, get the current natural size of + * each cell. + */ + + int rowmax = 0; + int colmax = 0; + + for (auto& ci : cells) { + CellInfo const & c (ci.second); + if (c.cell.x1 > rowmax) { + rowmax = c.cell.x1; + } + if (c.cell.y1 > colmax) { + colmax = c.cell.y1; + } + } + + row_info.clear (); + col_info.clear (); + + row_info.resize (rowmax); + col_info.resize (colmax); + + for (auto& ci : cells) { + + CellInfo & c (ci.second); + + c.content->size_request (c.natural_size.x, c.natural_size.y); + + row_info[c.cell.x0].natural_size += c.natural_size.x; + + if (c.row_options & PackExpand) { + row_info[c.cell.x0].expanders++; + } + + if (c.row_options & PackShrink) { + col_info[c.cell.x0].shrinkers++; + } + + if (c.cell.x1 != c.cell.x0) { + row_info[c.cell.x1].natural_size += c.natural_size.x; + if (c.row_options & PackExpand) { + row_info[c.cell.x1].expanders++; + } + if (c.row_options & PackShrink) { + row_info[c.cell.x1].shrinkers++; + } + } + + + col_info[c.cell.y0].natural_size += c.natural_size.y; + + if (c.row_options & PackExpand) { + col_info[c.cell.y0].expanders++; + } + + if (c.row_options & PackShrink) { + col_info[c.cell.y0].shrinkers++; + } + + if (c.cell.y1 != c.cell.y0) { + col_info[c.cell.y1].natural_size += c.natural_size.y; + + if (c.col_options & PackExpand) { + col_info[c.cell.y1].expanders++; + } + if (c.row_options & PackShrink) { + col_info[c.cell.y1].shrinkers++; + } + } + + } + + /* rows with nothing in them are still counted as existing. This is a + * design decision, not a logic inevitability. + */ + + const uint32_t rows = rowmax; + const uint32_t cols = colmax; + + /* Find the tallest column and widest row */ + + Distance natural_row_width = 0.; + Distance natural_col_height = 0.; + + for (AxisInfos::iterator ai = row_info.begin(); ai != row_info.end(); ++ai) { + natural_row_width = std::max (natural_row_width, ai->natural_size); + } + for (AxisInfos::iterator ai = col_info.begin(); ai != col_info.end(); ++ai) { + natural_col_height = std::max (natural_col_height, ai->natural_size); + } + + /* step two: compare the natural size to the size we've been given + * + * If the natural size is less than the allocated size, then find the + * difference, divide it by the number of expanding items per + * (row|col). Divide the total size by the number of (rows|cols), then + * iterate. Allocate expanders the per-cell size plus the extra for + * expansion. Allocate shrinkers/default just the per-cell size. + * + * If the natural size if greated than the allocated size, find the + * difference, divide it by the number of shrinking items per + * (row|col). Divide the total size by the number of (rows|cols), then + * iterate. Allocate shrinkers the per-cell size minus the excess for + * shrinking. Allocate expanders/default just the per-cell size. + * + */ + + Distance per_cell_width = within.width() / cols; + + for (AxisInfos::iterator ai = row_info.begin(); ai != row_info.end(); ++ai) { + if (natural_row_width < within.width() && ai->expanders) { + Distance delta = within.width() - natural_row_width; + ai->expand = delta / ai->expanders; + } else if (natural_row_width > within.width() && ai->shrinkers) { + Distance delta = within.width() - natural_row_width; + ai->shrink = delta / ai->shrinkers; + } + } + + Distance per_cell_height = within.height() / rows; + + for (AxisInfos::iterator ai = col_info.begin(); ai != col_info.end(); ++ai) { + if (natural_col_height < within.height() && ai->expanders) { + Distance delta = within.height() - natural_col_height; + ai->expand = delta / ai->expanders; + } else if (natural_col_height > within.height() && ai->shrinkers) { + Distance delta = within.height() - natural_col_height; + ai->shrink = delta / ai->shrinkers; + } + } + + for (auto& ci : cells) { + + CellInfo & c (ci.second); + + Distance w; + Distance h; + AxisInfo& col (col_info[c.cell.y0]); + AxisInfo& row (col_info[c.cell.x0]); + + if (c.row_options & PackExpand) { + w = per_cell_width + row.expand; + } else if (c.row_options & PackShrink) { + w = per_cell_width + row.shrink; /* note: row_shrink is negative */ + } else { + w = per_cell_width; + } + + if (c.col_options & PackExpand) { + h = per_cell_height + col.expand; + } else if (c.col_options & PackShrink) { + h = per_cell_height + col.shrink; /* note: col_shrink is negative */ + } else { + h = per_cell_height; + } + + c.allocate_size = Duple (w, h); + } + + /* final pass: actually allocate position for each cell */ + + Distance hdistance = 0.; + Distance vdistance = 0.; + + for (uint32_t r = 0; r < rows; ++r) { + + for (uint32_t c = 0; c < cols; ++c) { + + Index idx (r, c); + Cells::iterator ci = cells.find (idx); + + if (ci != cells.end()) { + + Rect rect = Rect (hdistance, vdistance, hdistance + ci->second.allocate_size.x, vdistance + ci->second.allocate_size.y); + ci->second.content->size_allocate (rect); + } else { + /* this cell (r, c) has no item starting within it */ + } + } + + hdistance = 0.; + } +} diff --git a/libs/canvas/wscript b/libs/canvas/wscript index 1ba44f02d9..b1cfe5ad37 100644 --- a/libs/canvas/wscript +++ b/libs/canvas/wscript @@ -60,6 +60,7 @@ canvas_sources = [ 'scroll_group.cc', 'stateful_image.cc', 'step_button.cc', + 'table.cc', 'text.cc', 'tracking_text.cc', 'types.cc',