Source code for glooey.drawing.grid

#!/usr/bin/env python3

import math
import autoprop
from vecrec import Vector, Rect
from glooey.helpers import *

[docs]@autoprop class Grid:
[docs] def __init__(self, *, bounding_rect=None, min_cell_rects=None, num_rows=0, num_cols=0, padding=None, inner_padding=None, outer_padding=None, row_heights=None, col_widths=None, default_row_height='expand', default_col_width='expand'): # Attributes that the user can set to affect the shape of the grid. self._bounding_rect = bounding_rect or Rect.null() self._min_cell_rects = min_cell_rects or {} self._requested_num_rows = num_rows self._requested_num_cols = num_cols self._inner_padding = first_not_none((inner_padding, padding, 0)) self._outer_padding = first_not_none((outer_padding, padding, 0)) self._requested_row_heights = row_heights or {} self._requested_col_widths = col_widths or {} self._default_row_height = default_row_height self._default_col_width = default_col_width # Read-only attributes that reflect the current state of the grid. self._num_rows = 0 self._num_cols = 0 self._max_cell_heights = {} self._max_cell_widths = {} self._fixed_rows = set() self._expandable_rows = set() self._fixed_cols = set() self._expandable_cols = set() self._fixed_row_heights = {} self._fixed_col_widths = {} self._min_expandable_row_heights = {} self._min_expandable_col_widths = {} self._padding_height = 0 self._padding_width = 0 self._min_height = 0 self._min_width = 0 self._row_heights = {} self._col_widths = {} self._width = 0 self._height = 0 self._row_tops = {} self._col_lefts = {} self._cell_rects = {} # Attributes that manage the cache. self._is_shape_stale = True self._is_claim_stale = True self._are_cells_stale = True
[docs] def make_claim(self, min_cell_rects=None): if min_cell_rects is not None: self.min_cell_rects = min_cell_rects self._update_claim() return self._min_width, self._min_height
[docs] def make_cells(self, bounding_rect=None): if bounding_rect is not None: self.bounding_rect = bounding_rect self._update_cells() return self._cell_rects
[docs] def find_cell_under_mouse(self, x, y): # The >=/<= comparisons in this method were chosen to be compatible # with the comparisons in Widget.is_under_mouse(). That method counts # points that are on any edge of a widget as being over that widget. # The >=/<= comparisons do the same thing here. # # I initially wrote this method using an inclusive operator on one side # and an exclusive one on the other, to avoid any ambiguity in the case # where there's no padding. For example, imagine a 2x2 grid with no # padding. In theory, the point exactly in the middle is over all four # cells. In practice, the algorithm will identify the top-left-most # cell first and return it. So the algorithm isn't really ambiguous, # but it is more dependent on what's really an implementation detail. # Find the row the mouse is over. for i in range(self._num_rows): row_top = self._row_tops[i] row_bottom = row_top - self._row_heights[i] if row_top >= y >= row_bottom: break else: return None # Find the col the mouse is over. for j in range(self._num_cols): col_left = self._col_lefts[j] col_right = col_left + self._col_widths[j] if col_left <= x <= col_right: break else: return None return i, j
[docs] def get_width(self): return self._width
[docs] def get_height(self): return self._height
[docs] def get_rect(self): return Rect.from_size(self._width, self._height)
[docs] def get_min_width(self): return self._min_width
[docs] def get_min_height(self): return self._min_height
min_height = property(get_min_height)
[docs] def get_min_bounding_rect(self): return Rect.from_size(self._min_width, self._min_height)
[docs] def get_cell_rects(self): return self._cell_rects
cell_rects = property(get_cell_rects)
[docs] def get_bounding_rect(self): return self._bounding_rect
[docs] def set_bounding_rect(self, new_rect): if self._bounding_rect != new_rect: self._bounding_rect = new_rect self._invalidate_cells()
[docs] def get_min_cell_rect(self, i, j): return self._min_cell_rects[i,j]
[docs] def set_min_cell_rect(self, i, j, new_rect): if (i,j) not in self._min_cell_rects or \ self._min_cell_rects[i,j] != new_rect: self._min_cell_rects[i,j] = new_rect self._invalidate_shape()
[docs] def del_min_cell_rect(self, i, j): if (i,j) in self._min_cell_rects: del self._min_cell_rects[i,j] self._invalidate_shape()
[docs] def get_min_cell_rects(self): return self._min_cell_rects
[docs] def set_min_cell_rects(self, new_rects): if self._min_cell_rects != new_rects: self._min_cell_rects = new_rects self._invalidate_shape()
[docs] def del_min_cell_rects(self): if self._min_cell_rects: self._min_cell_rects = {} self._invalidate_shape()
[docs] def get_num_rows(self): return self._num_rows
[docs] def set_num_rows(self, new_num): self._requested_num_rows = new_num self._invalidate_shape()
[docs] def get_num_cols(self): return self._num_cols
[docs] def set_num_cols(self, new_num): self._requested_num_cols = new_num self._invalidate_shape()
[docs] def get_padding(self): return self._inner_padding, self._outer_padding
[docs] def set_padding(self, new_padding): self._inner_padding = new_padding self._outer_padding = new_padding self._invalidate_claim()
[docs] def get_inner_padding(self): return self._inner_padding
[docs] def set_inner_padding(self, new_padding): self._inner_padding = new_padding self._invalidate_claim()
[docs] def get_outer_padding(self): return self._outer_padding
[docs] def set_outer_padding(self, new_padding): self._outer_padding = new_padding self._invalidate_claim()
[docs] def get_row_height(self, i): return self._row_heights[i]
[docs] def set_row_height(self, i, new_height): self._requested_row_heights[i] = new_height self._invalidate_claim()
[docs] def del_row_height(self, i): if i in self._requested_row_heights: del self._requested_row_heights[i] self._invalidate_claim()
[docs] def get_row_heights(self): return self._row_heights
[docs] def set_row_heights(self, new_heights): self._requested_row_heights = new_heights self._invalidate_claim()
[docs] def del_row_heights(self): self._requested_row_heights = {} self._invalidate_claim()
[docs] def get_col_width(self, j): return self._col_widths[j]
[docs] def set_col_width(self, j, new_width): self._requested_col_widths[j] = new_width self._invalidate_claim()
[docs] def del_col_width(self, j): if j in self._requested_col_widths: del self._requested_col_widths[j] self._invalidate_claim()
[docs] def get_col_widths(self): return self._col_widths
[docs] def set_col_widths(self, new_widths): self._requested_col_widths = new_widths self._invalidate_claim()
[docs] def del_col_widths(self): self._requested_col_widths = {} self._invalidate_claim()
[docs] def get_default_row_height(self): return self._default_row_height
[docs] def set_default_row_height(self, new_height): self._default_row_height = new_height self._invalidate_claim()
[docs] def get_default_col_width(self): return self._default_col_width
[docs] def set_default_col_width(self, new_width): self._default_col_width = new_width self._invalidate_claim()
[docs] def get_requested_num_rows(self): return self._requested_num_rows
[docs] def get_requested_num_cols(self): return self._requested_num_cols
requested_num_cols = property(get_requested_num_cols)
[docs] def get_requested_row_height(self, i): return self._requested_row_heights[i]
[docs] def get_requested_row_heights(self): return self._requested_row_heights
[docs] def get_requested_col_width(self, i): return self._requested_col_widths[i]
[docs] def get_requested_col_widths(self): return self._requested_col_widths
[docs] def _invalidate_shape(self): self._is_shape_stale = True self._invalidate_claim()
[docs] def _invalidate_claim(self): self._is_claim_stale = True self._invalidate_cells()
[docs] def _invalidate_cells(self): self._are_cells_stale = True
[docs] def _update_shape(self): if self._is_shape_stale: self._find_num_rows() self._find_num_cols() self._find_max_cell_dimensions() self.is_shape_stale = False
[docs] def _update_claim(self): if self._is_claim_stale: self._update_shape() self._find_which_rows_expand() self._find_which_cols_expand() self._find_fixed_row_heights() self._find_fixed_col_widths() self._find_min_expandable_row_heights() self._find_min_expandable_col_widths() self._find_padding_height() self._find_padding_width() self._find_min_height() self._find_min_width() self._is_claim_stale = False
[docs] def _update_cells(self): if self._are_cells_stale: self._update_claim() if self._bounding_rect.width < self._min_width: raise UsageError("grid cannot fit in {0[0]}x{0[1]}, need to be at least {1} px wide.".format(self._bounding_rect.size, self._min_width)) if self._bounding_rect.height < self._min_height: raise UsageError("grid cannot fit in {0[0]}x{0[1]}, need to be at least {1} px tall.".format(self._bounding_rect.size, self._min_height)) self._find_row_heights() self._find_col_widths() self._find_cell_rects() self._are_cells_stale = False
[docs] def _find_num_rows(self): min_num_rows = 0 for i,j in self._min_cell_rects: min_num_rows = max(i+1, min_num_rows) if self._requested_num_rows: self._num_rows = self._requested_num_rows else: self._num_rows = min_num_rows if self._num_rows < min_num_rows: raise UsageError("not enough rows requested")
[docs] def _find_num_cols(self): min_num_cols = 0 for i,j in self._min_cell_rects: min_num_cols = max(j+1, min_num_cols) if self._requested_num_cols: self._num_cols = self._requested_num_cols else: self._num_cols = min_num_cols if self._num_cols < min_num_cols: raise UsageError("not enough columns requested")
[docs] def _find_max_cell_dimensions(self): """ Find the tallest and widest cell in each dimension. """ self._max_cell_heights = {} self._max_cell_widths = {} for i,j in self._min_cell_rects: # Use -math.inf so that negative cell sizes can be used. self._max_cell_heights[i] = max( self._min_cell_rects[i,j].height, self._max_cell_heights.get(i, -math.inf)) self._max_cell_widths[j] = max( self._min_cell_rects[i,j].width, self._max_cell_widths.get(j, -math.inf))
[docs] def _find_which_rows_expand(self): self._fixed_rows = set() self._expandable_rows = set() for i in range(self._num_rows): size_request = self._get_requested_row_height(i) if isinstance(size_request, int): self._fixed_rows.add(i) elif size_request == 'expand': self._expandable_rows.add(i) else: raise UsageError("illegal row height: {}".format(repr(size_request))) self._num_fixed_rows = len(self._fixed_rows) self._num_expandable_rows = len(self._expandable_rows)
[docs] def _find_which_cols_expand(self): self._fixed_cols = set() self._expandable_cols = set() for j in range(self._num_cols): size_request = self._get_requested_col_width(j) if isinstance(size_request, int): self._fixed_cols.add(j) elif size_request == 'expand': self._expandable_cols.add(j) else: raise UsageError("illegal col width: {}".format(repr(size_request))) self._num_fixed_cols = len(self._fixed_cols) self._num_expandable_cols = len(self._expandable_cols)
[docs] def _find_fixed_row_heights(self): self._fixed_row_heights = {} for i in self._fixed_rows: # Use -math.inf so that negative cell sizes can be used. self._fixed_row_heights[i] = max( self._get_requested_row_height(i), self._max_cell_heights.get(i, -math.inf))
[docs] def _find_fixed_col_widths(self): self._fixed_col_widths = {} for j in self._fixed_cols: # Use -math.inf so that negative cell sizes can be used. self._fixed_col_widths[j] = max( self._get_requested_col_width(j), self._max_cell_widths.get(j, -math.inf))
[docs] def _find_min_expandable_row_heights(self): self._min_expandable_row_heights = {} for i in self._expandable_rows: self._min_expandable_row_heights[i] = \ self._max_cell_heights.get(i, 0)
[docs] def _find_min_expandable_col_widths(self): self._min_expandable_col_widths = {} for j in self._expandable_cols: self._min_expandable_col_widths[j] = \ self._max_cell_widths.get(j, 0)
[docs] def _find_padding_height(self): self._padding_height = \ + self._inner_padding * (self._num_rows - 1) \ + self._outer_padding * 2
[docs] def _find_padding_width(self): self._padding_width = \ + self._inner_padding * (self._num_cols - 1) \ + self._outer_padding * 2
[docs] def _find_min_height(self): min_expandable_height = max( self._min_expandable_row_heights.values() or [0]) self._min_height = \ + sum(self._fixed_row_heights.values()) \ + min_expandable_height * self._num_expandable_rows \ + self._padding_height
[docs] def _find_min_width(self): min_expandable_width = max( self._min_expandable_col_widths.values() or [0]) self._min_width = \ + sum(self._fixed_col_widths.values()) \ + min_expandable_width * self._num_expandable_cols \ + self._padding_width
[docs] def _find_row_heights(self): self._row_heights = self._fixed_row_heights.copy() if self._num_expandable_rows: expandable_row_height = ( + self._bounding_rect.height - sum(self._fixed_row_heights.values()) - self._padding_height ) / self._num_expandable_rows for i in self._expandable_rows: self._row_heights[i] = expandable_row_height self._height = \ + sum(self._row_heights.values()) \ + self._padding_height
[docs] def _find_col_widths(self): self._col_widths = self._fixed_col_widths.copy() if self._num_expandable_cols: expandable_col_width = ( + self._bounding_rect.width - sum(self._fixed_col_widths.values()) - self._padding_width ) / self._num_expandable_cols for j in self._expandable_cols: self._col_widths[j] = expandable_col_width self._width = \ + sum(self._col_widths.values()) \ + self._padding_width
[docs] def _find_cell_rects(self): self._row_tops = {} self._col_lefts = {} self._cell_rects = {} top_cursor = self._bounding_rect.top for i in range(self._num_rows): top_cursor -= self._get_row_padding(i) left_cursor = self._bounding_rect.left row_height = self._row_heights[i] self._row_tops[i] = top_cursor for j in range(self._num_cols): left_cursor += self._get_col_padding(j) col_width = self._col_widths[j] self._cell_rects[i,j] = Rect.from_size(col_width, row_height) self._cell_rects[i,j].top_left = left_cursor, top_cursor self._col_lefts[j] = left_cursor left_cursor += col_width top_cursor -= row_height
[docs] def _get_requested_row_height(self, i): return self._requested_row_heights.get(i, self._default_row_height)
[docs] def _get_requested_col_width(self, j): return self._requested_col_widths.get(j, self._default_col_width)
[docs] def _get_row_padding(self, i): return self._outer_padding if i == 0 else self._inner_padding
[docs] def _get_col_padding(self, j): return self._outer_padding if j == 0 else self._inner_padding
[docs]def make_grid(rect, cells={}, num_rows=0, num_cols=0, padding=None, inner_padding=None, outer_padding=None, row_heights={}, col_widths={}, default_row_height='expand', default_col_width='expand'): """ Return rectangles for each cell in the specified grid. The rectangles are returned in a dictionary where the keys are (row, col) tuples. """ grid = Grid( bounding_rect=rect, min_cell_rects=cells, num_rows=num_rows, num_cols=num_cols, padding=padding, inner_padding=inner_padding, outer_padding=outer_padding, row_heights=row_heights, col_widths=col_widths, default_row_height=default_row_height, default_col_width=default_col_width, ) return grid.make_cells()