oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1package store
2
3import "fmt"
4
5// Column widths and row heights.
6//
7// Row.Height and Col.Width have been on the models since the first
8// schema and nothing has ever written one. The consequence is visible
9// on the first screen anybody opens: a column headed "Invoice Quarter"
10// shows "Invoice Qua", and no amount of dragging fixes it because the
11// drag has nowhere to be saved. A spreadsheet whose columns cannot be
12// widened is not one.
13//
14// Sizes are per TAB and stored against the stable row and column ids,
15// like everything else here — so widening a column and then inserting
16// one to its left keeps the width on the column somebody widened.
17//
18// Zero means "no size of its own", not "zero pixels". A tab with no
19// sizes at all is the common case and costs nothing: the client leaves
20// the library's defaults alone rather than being told the default
21// twice.
22
23// minCellSize and maxCellSize bound what may be stored.
24//
25// The floor is above zero on purpose: a column dragged to nothing is a
26// column nobody can find again to drag back, and the grid offers no
27// "unhide" because nothing here hides. The ceiling stops one column
28// from being wider than any screen, which has the same effect as
29// hiding every column after it.
30const (
31 minCellSize = 16
32 maxCellSize = 2000
33)
34
35// Sizes reads a tab's column widths and row heights, keyed by grid
36// POSITION rather than stable id.
37//
38// Position, because the caller is painting a grid and has no use for
39// ids — the same reasoning as gridCell's R and C. Only non-zero entries
40// are returned, so the map is empty for the ordinary unsized tab.
41func (h *Handle) Sizes() (cols map[int]int, rows map[int]int, err error) {
42 cols, rows = map[int]int{}, map[int]int{}
43
44 var colRows []Col
45 if err := h.db.Select("position", "width").
46 Where("tab_id = ? AND width > 0", h.tabID).Find(&colRows).Error; err != nil {
47 return nil, nil, fmt.Errorf("store: read column widths: %w", err)
48 }
49 for _, c := range colRows {
50 cols[c.Position] = c.Width
51 }
52
53 var rowRows []Row
54 if err := h.db.Select("position", "height").
55 Where("tab_id = ? AND height > 0", h.tabID).Find(&rowRows).Error; err != nil {
56 return nil, nil, fmt.Errorf("store: read row heights: %w", err)
57 }
58 for _, r := range rowRows {
59 rows[r.Position] = r.Height
60 }
61 return cols, rows, nil
62}
63
64// SetColWidth stores a column's width in pixels.
65//
66// The column must already be part of the tab — callers reach it through
67// EnsureCol, the same way a write does. Sizing a column that does not
68// exist yet is a real thing to want (widen an empty column before
69// typing in it), and EnsureCol is what makes the coordinate real; doing
70// it here instead would put grid positions back into a method that
71// otherwise speaks only in stable ids.
72func (h *Handle) SetColWidth(colID uint64, width int) error {
73 if err := checkSize(width); err != nil {
74 return err
75 }
76 return h.updateSize(&Col{}, "width", colID, width, "column")
77}
78
79// SetRowHeight is SetColWidth's twin.
80func (h *Handle) SetRowHeight(rowID uint64, height int) error {
81 if err := checkSize(height); err != nil {
82 return err
83 }
84 return h.updateSize(&Row{}, "height", rowID, height, "row")
85}
86
87func checkSize(v int) error {
88 if v < minCellSize || v > maxCellSize {
89 return fmt.Errorf("store: size %d is outside %d..%d", v, minCellSize, maxCellSize)
90 }
91 return nil
92}
93
94// updateSize writes one size, scoped to this tab.
95//
96// Scoped to the tab as well as the id, not because the id is
97// ambiguous — it is a primary key — but because it makes the query
98// refuse to write to another tab's row if a caller ever passes an id
99// from the wrong handle. The id alone would happily do it.
100func (h *Handle) updateSize(model any, column string, id uint64, v int, what string) error {
101 res := h.db.Model(model).
102 Where("id = ? AND tab_id = ?", id, h.tabID).
103 Update(column, v)
104 if res.Error != nil {
105 return fmt.Errorf("store: set %s size: %w", what, res.Error)
106 }
107 if res.RowsAffected == 0 {
108 return fmt.Errorf("store: %s %d is not part of tab %d", what, id, h.tabID)
109 }
110 return nil
111}
112