@trebco/treb 23.6.2 → 25.0.0-rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/.eslintignore +8 -0
  2. package/.eslintrc.js +164 -0
  3. package/README-shadow-DOM.md +88 -0
  4. package/README.md +37 -130
  5. package/api-config.json +29 -0
  6. package/api-generator/api-generator-types.ts +82 -0
  7. package/api-generator/api-generator.ts +1172 -0
  8. package/api-generator/package.json +3 -0
  9. package/build/treb-spreadsheet.mjs +14 -0
  10. package/{treb.d.ts → build/treb.d.ts} +293 -299
  11. package/esbuild-custom-element.mjs +336 -0
  12. package/esbuild.js +305 -0
  13. package/package.json +43 -14
  14. package/treb-base-types/package.json +5 -0
  15. package/treb-base-types/src/api_types.ts +36 -0
  16. package/treb-base-types/src/area.ts +583 -0
  17. package/treb-base-types/src/basic_types.ts +45 -0
  18. package/treb-base-types/src/cell.ts +612 -0
  19. package/treb-base-types/src/cells.ts +1066 -0
  20. package/treb-base-types/src/color.ts +124 -0
  21. package/treb-base-types/src/import.ts +71 -0
  22. package/treb-base-types/src/index-standalone.ts +29 -0
  23. package/treb-base-types/src/index.ts +42 -0
  24. package/treb-base-types/src/layout.ts +47 -0
  25. package/treb-base-types/src/localization.ts +187 -0
  26. package/treb-base-types/src/rectangle.ts +145 -0
  27. package/treb-base-types/src/render_text.ts +72 -0
  28. package/treb-base-types/src/style.ts +545 -0
  29. package/treb-base-types/src/table.ts +109 -0
  30. package/treb-base-types/src/text_part.ts +54 -0
  31. package/treb-base-types/src/theme.ts +608 -0
  32. package/treb-base-types/src/union.ts +152 -0
  33. package/treb-base-types/src/value-type.ts +164 -0
  34. package/treb-base-types/style/resizable.css +59 -0
  35. package/treb-calculator/modern.tsconfig.json +11 -0
  36. package/treb-calculator/package.json +5 -0
  37. package/treb-calculator/src/calculator.ts +2546 -0
  38. package/treb-calculator/src/complex-math.ts +558 -0
  39. package/treb-calculator/src/dag/array-vertex.ts +198 -0
  40. package/treb-calculator/src/dag/graph.ts +951 -0
  41. package/treb-calculator/src/dag/leaf_vertex.ts +118 -0
  42. package/treb-calculator/src/dag/spreadsheet_vertex.ts +327 -0
  43. package/treb-calculator/src/dag/spreadsheet_vertex_base.ts +44 -0
  44. package/treb-calculator/src/dag/vertex.ts +352 -0
  45. package/treb-calculator/src/descriptors.ts +162 -0
  46. package/treb-calculator/src/expression-calculator.ts +1069 -0
  47. package/treb-calculator/src/function-error.ts +103 -0
  48. package/treb-calculator/src/function-library.ts +103 -0
  49. package/treb-calculator/src/functions/base-functions.ts +1214 -0
  50. package/treb-calculator/src/functions/checkbox.ts +164 -0
  51. package/treb-calculator/src/functions/complex-functions.ts +253 -0
  52. package/treb-calculator/src/functions/finance-functions.ts +399 -0
  53. package/treb-calculator/src/functions/information-functions.ts +102 -0
  54. package/treb-calculator/src/functions/matrix-functions.ts +182 -0
  55. package/treb-calculator/src/functions/sparkline.ts +335 -0
  56. package/treb-calculator/src/functions/statistics-functions.ts +350 -0
  57. package/treb-calculator/src/functions/text-functions.ts +298 -0
  58. package/treb-calculator/src/index.ts +27 -0
  59. package/treb-calculator/src/notifier-types.ts +59 -0
  60. package/treb-calculator/src/primitives.ts +428 -0
  61. package/treb-calculator/src/utilities.ts +305 -0
  62. package/treb-charts/package.json +5 -0
  63. package/treb-charts/src/chart-functions.ts +156 -0
  64. package/treb-charts/src/chart-types.ts +230 -0
  65. package/treb-charts/src/chart.ts +1288 -0
  66. package/treb-charts/src/index.ts +24 -0
  67. package/treb-charts/src/main.ts +37 -0
  68. package/treb-charts/src/rectangle.ts +52 -0
  69. package/treb-charts/src/renderer.ts +1841 -0
  70. package/treb-charts/src/util.ts +122 -0
  71. package/treb-charts/style/charts.scss +221 -0
  72. package/treb-charts/style/old-charts.scss +250 -0
  73. package/treb-embed/markup/layout.html +137 -0
  74. package/treb-embed/markup/toolbar.html +175 -0
  75. package/treb-embed/modern.tsconfig.json +25 -0
  76. package/treb-embed/src/custom-element/content-types.d.ts +18 -0
  77. package/treb-embed/src/custom-element/global.d.ts +11 -0
  78. package/treb-embed/src/custom-element/spreadsheet-constructor.ts +1227 -0
  79. package/treb-embed/src/custom-element/treb-global.ts +44 -0
  80. package/treb-embed/src/custom-element/treb-spreadsheet-element.ts +52 -0
  81. package/treb-embed/src/embedded-spreadsheet.ts +5362 -0
  82. package/treb-embed/src/index.ts +16 -0
  83. package/treb-embed/src/language-model.ts +41 -0
  84. package/treb-embed/src/options.ts +320 -0
  85. package/treb-embed/src/progress-dialog.ts +228 -0
  86. package/treb-embed/src/selection-state.ts +16 -0
  87. package/treb-embed/src/spinner.ts +42 -0
  88. package/treb-embed/src/toolbar-message.ts +96 -0
  89. package/treb-embed/src/types.ts +167 -0
  90. package/treb-embed/style/autocomplete.scss +103 -0
  91. package/treb-embed/style/dark-theme.scss +114 -0
  92. package/treb-embed/style/defaults.scss +36 -0
  93. package/treb-embed/style/dialog.scss +181 -0
  94. package/treb-embed/style/dropdown-select.scss +101 -0
  95. package/treb-embed/style/formula-bar.scss +193 -0
  96. package/treb-embed/style/grid.scss +374 -0
  97. package/treb-embed/style/layout.scss +424 -0
  98. package/treb-embed/style/mouse-mask.scss +67 -0
  99. package/treb-embed/style/note.scss +92 -0
  100. package/treb-embed/style/overlay-editor.scss +102 -0
  101. package/treb-embed/style/spinner.scss +92 -0
  102. package/treb-embed/style/tab-bar.scss +228 -0
  103. package/treb-embed/style/table.scss +80 -0
  104. package/treb-embed/style/theme-defaults.scss +444 -0
  105. package/treb-embed/style/toolbar.scss +416 -0
  106. package/treb-embed/style/tooltip.scss +68 -0
  107. package/treb-embed/style/treb-icons.scss +130 -0
  108. package/treb-embed/style/treb-spreadsheet-element.scss +20 -0
  109. package/treb-embed/style/z-index.scss +43 -0
  110. package/treb-export/docs/charts.md +68 -0
  111. package/treb-export/modern.tsconfig.json +19 -0
  112. package/treb-export/package.json +4 -0
  113. package/treb-export/src/address-type.ts +77 -0
  114. package/treb-export/src/base-template.ts +22 -0
  115. package/treb-export/src/column-width.ts +85 -0
  116. package/treb-export/src/drawing2/chart-template-components2.ts +389 -0
  117. package/treb-export/src/drawing2/chart2.ts +282 -0
  118. package/treb-export/src/drawing2/column-chart-template2.ts +521 -0
  119. package/treb-export/src/drawing2/donut-chart-template2.ts +296 -0
  120. package/treb-export/src/drawing2/drawing2.ts +355 -0
  121. package/treb-export/src/drawing2/embedded-image.ts +71 -0
  122. package/treb-export/src/drawing2/scatter-chart-template2.ts +555 -0
  123. package/treb-export/src/export-worker/export-worker.ts +99 -0
  124. package/treb-export/src/export-worker/index-modern.ts +22 -0
  125. package/treb-export/src/export2.ts +2204 -0
  126. package/treb-export/src/import2.ts +882 -0
  127. package/treb-export/src/relationship.ts +36 -0
  128. package/treb-export/src/shared-strings2.ts +128 -0
  129. package/treb-export/src/template-2.ts +22 -0
  130. package/treb-export/src/unescape_xml.ts +47 -0
  131. package/treb-export/src/workbook-sheet2.ts +182 -0
  132. package/treb-export/src/workbook-style2.ts +1285 -0
  133. package/treb-export/src/workbook-theme2.ts +88 -0
  134. package/treb-export/src/workbook2.ts +491 -0
  135. package/treb-export/src/xml-utils.ts +201 -0
  136. package/treb-export/template/base/[Content_Types].xml +2 -0
  137. package/treb-export/template/base/_rels/.rels +2 -0
  138. package/treb-export/template/base/docProps/app.xml +2 -0
  139. package/treb-export/template/base/docProps/core.xml +12 -0
  140. package/treb-export/template/base/xl/_rels/workbook.xml.rels +2 -0
  141. package/treb-export/template/base/xl/sharedStrings.xml +2 -0
  142. package/treb-export/template/base/xl/styles.xml +2 -0
  143. package/treb-export/template/base/xl/theme/theme1.xml +2 -0
  144. package/treb-export/template/base/xl/workbook.xml +2 -0
  145. package/treb-export/template/base/xl/worksheets/sheet1.xml +2 -0
  146. package/treb-export/template/base.xlsx +0 -0
  147. package/treb-format/package.json +8 -0
  148. package/treb-format/src/format.test.ts +213 -0
  149. package/treb-format/src/format.ts +942 -0
  150. package/treb-format/src/format_cache.ts +199 -0
  151. package/treb-format/src/format_parser.ts +723 -0
  152. package/treb-format/src/index.ts +25 -0
  153. package/treb-format/src/number_format_section.ts +100 -0
  154. package/treb-format/src/value_parser.ts +337 -0
  155. package/treb-grid/package.json +5 -0
  156. package/treb-grid/src/editors/autocomplete.ts +394 -0
  157. package/treb-grid/src/editors/autocomplete_matcher.ts +260 -0
  158. package/treb-grid/src/editors/formula_bar.ts +473 -0
  159. package/treb-grid/src/editors/formula_editor_base.ts +910 -0
  160. package/treb-grid/src/editors/overlay_editor.ts +511 -0
  161. package/treb-grid/src/index.ts +37 -0
  162. package/treb-grid/src/layout/base_layout.ts +2618 -0
  163. package/treb-grid/src/layout/grid_layout.ts +299 -0
  164. package/treb-grid/src/layout/rectangle_cache.ts +86 -0
  165. package/treb-grid/src/render/selection-renderer.ts +414 -0
  166. package/treb-grid/src/render/svg_header_overlay.ts +93 -0
  167. package/treb-grid/src/render/svg_selection_block.ts +187 -0
  168. package/treb-grid/src/render/tile_renderer.ts +2122 -0
  169. package/treb-grid/src/types/annotation.ts +216 -0
  170. package/treb-grid/src/types/border_constants.ts +34 -0
  171. package/treb-grid/src/types/clipboard_data.ts +31 -0
  172. package/treb-grid/src/types/data_model.ts +334 -0
  173. package/treb-grid/src/types/drag_mask.ts +81 -0
  174. package/treb-grid/src/types/grid.ts +7743 -0
  175. package/treb-grid/src/types/grid_base.ts +3644 -0
  176. package/treb-grid/src/types/grid_command.ts +470 -0
  177. package/treb-grid/src/types/grid_events.ts +124 -0
  178. package/treb-grid/src/types/grid_options.ts +97 -0
  179. package/treb-grid/src/types/grid_selection.ts +60 -0
  180. package/treb-grid/src/types/named_range.ts +369 -0
  181. package/treb-grid/src/types/scale-control.ts +202 -0
  182. package/treb-grid/src/types/serialize_options.ts +72 -0
  183. package/treb-grid/src/types/set_range_options.ts +52 -0
  184. package/treb-grid/src/types/sheet.ts +3099 -0
  185. package/treb-grid/src/types/sheet_types.ts +95 -0
  186. package/treb-grid/src/types/tab_bar.ts +464 -0
  187. package/treb-grid/src/types/tile.ts +59 -0
  188. package/treb-grid/src/types/update_flags.ts +75 -0
  189. package/treb-grid/src/util/dom_utilities.ts +44 -0
  190. package/treb-grid/src/util/fontmetrics2.ts +179 -0
  191. package/treb-grid/src/util/ua.ts +104 -0
  192. package/treb-logo.svg +18 -0
  193. package/treb-parser/package.json +5 -0
  194. package/treb-parser/src/csv-parser.ts +122 -0
  195. package/treb-parser/src/index.ts +25 -0
  196. package/treb-parser/src/md-parser.ts +526 -0
  197. package/treb-parser/src/parser-types.ts +397 -0
  198. package/treb-parser/src/parser.test.ts +298 -0
  199. package/treb-parser/src/parser.ts +2673 -0
  200. package/treb-utils/package.json +5 -0
  201. package/treb-utils/src/dispatch.ts +57 -0
  202. package/treb-utils/src/event_source.ts +147 -0
  203. package/treb-utils/src/ievent_source.ts +33 -0
  204. package/treb-utils/src/index.ts +31 -0
  205. package/treb-utils/src/measurement.ts +174 -0
  206. package/treb-utils/src/resizable.ts +160 -0
  207. package/treb-utils/src/scale.ts +137 -0
  208. package/treb-utils/src/serialize_html.ts +124 -0
  209. package/treb-utils/src/template.ts +70 -0
  210. package/treb-utils/src/validate_uri.ts +61 -0
  211. package/tsconfig.json +10 -0
  212. package/tsproject.json +30 -0
  213. package/util/license-plugin-esbuild.js +86 -0
  214. package/util/list-css-vars.sh +46 -0
  215. package/README-esm.md +0 -37
  216. package/treb-bundle.css +0 -2
  217. package/treb-bundle.mjs +0 -15
@@ -0,0 +1,2122 @@
1
+ /*
2
+ * This file is part of TREB.
3
+ *
4
+ * TREB is free software: you can redistribute it and/or modify it under the
5
+ * terms of the GNU General Public License as published by the Free Software
6
+ * Foundation, either version 3 of the License, or (at your option) any
7
+ * later version.
8
+ *
9
+ * TREB is distributed in the hope that it will be useful, but WITHOUT ANY
10
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
+ * details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License along
15
+ * with TREB. If not, see <https://www.gnu.org/licenses/>.
16
+ *
17
+ * Copyright 2022-2023 trebco, llc.
18
+ * info@treb.app
19
+ *
20
+ */
21
+
22
+ import { TextPartFlag, ICellAddress, Style, ValueType,
23
+ PreparedText, RenderTextPart,
24
+ Cell, Area, Size, Rectangle,
25
+ Theme, ThemeColor, ThemeColor2, Table } from 'treb-base-types';
26
+
27
+ import type { Tile } from '../types/tile';
28
+ import { FontMetricsCache as FontMetricsCache2 } from '../util/fontmetrics2';
29
+ import { FormattedString, MDParser } from 'treb-parser';
30
+ import type { BaseLayout, TileRange } from '../layout/base_layout';
31
+ import type { DataModel, ViewModel } from '../types/data_model';
32
+ import type { GridOptions } from '../types/grid_options';
33
+
34
+ const BASELINE = 'bottom';
35
+ const WK = /webkit/i.test(typeof navigator === 'undefined' ? '' : navigator?.userAgent || '') ? 1 : 0;
36
+
37
+ interface FontSet {
38
+ base: string,
39
+ strong: string,
40
+ emphasis: string,
41
+ strong_emphasis: string,
42
+ }
43
+
44
+ interface OverflowCellInfo {
45
+ address: ICellAddress;
46
+ cell: Cell;
47
+ border: Rectangle;
48
+ background: Rectangle;
49
+ grid: Rectangle;
50
+ }
51
+
52
+
53
+ interface RenderCellResult {
54
+
55
+ tile_overflow_bottom?: boolean;
56
+ tile_overflow_right?: boolean;
57
+
58
+ // this can happen if a cell overflows to the left.
59
+ tile_overflow_left?: boolean;
60
+
61
+ width?: number;
62
+ height?: number;
63
+ left?: number;
64
+
65
+ }
66
+
67
+ interface OverflowRecord {
68
+ head: ICellAddress;
69
+ area: Area;
70
+ tile: Tile;
71
+ }
72
+
73
+ export class TileRenderer {
74
+
75
+ // removing last_font because we are doing more complex
76
+ // font manipulation for MD text
77
+ // protected last_font?: string;
78
+
79
+ protected readonly cell_edge_buffer = 4;
80
+
81
+ /**
82
+ * a record of cell overflows, also used for merges if they cross tile
83
+ * boundaries. on render, we check if an overflow(ed) cell is dirty; if
84
+ * so, this forces update of dependent cells.
85
+ */
86
+ protected overflow_areas: OverflowRecord[] = [];
87
+
88
+ protected buffer_canvas: HTMLCanvasElement;
89
+ protected buffer_context!: CanvasRenderingContext2D;
90
+ protected buffer_canvas_size: Size = { width: 256, height: 256 };
91
+
92
+ constructor(
93
+ protected theme: Theme,
94
+ protected layout: BaseLayout,
95
+ protected model: DataModel,
96
+ protected view: ViewModel,
97
+ protected options: GridOptions, ) {
98
+
99
+ // this.buffer_canvas = document.createElement('canvas');
100
+
101
+ this.buffer_canvas = layout.buffer_canvas;
102
+
103
+ this.buffer_canvas.width = this.buffer_canvas_size.width;
104
+ this.buffer_canvas.height = this.buffer_canvas_size.height;
105
+
106
+ // we need this attached to the document so it inherits fonts properly.
107
+ // in fact layout should manage it and then hand it to us (or we can grab it)
108
+
109
+ // this.buffer_canvas.classList.add('treb-buffer-canvas');
110
+ // document.body.appendChild(this.buffer_canvas);
111
+
112
+ const context = this.buffer_canvas.getContext('2d', { alpha: false });
113
+
114
+ if (context) {
115
+ const scale = this.layout.dpr;
116
+ this.buffer_context = context;
117
+ this.buffer_context.setTransform(scale, 0, 0, scale, 0, 0);
118
+ this.buffer_context.textAlign = 'left';
119
+ this.buffer_context.textBaseline = BASELINE; // 'alphabetic';
120
+ }
121
+
122
+ // this.UpdateTheme();
123
+
124
+ /*
125
+ if (this.theme.grid_cell?.font_size?.value){
126
+ if (this.theme.grid_cell.font_size.unit === 'px') {
127
+ FontMetricsCache.base_size_px = this.theme.grid_cell.font_size.value;
128
+ }
129
+ else if (this.theme.grid_cell.font_size.unit === 'pt') {
130
+ FontMetricsCache.base_size_px = this.theme.grid_cell.font_size.value * 4 / 3;
131
+ }
132
+ }
133
+ */
134
+
135
+ }
136
+
137
+ /**
138
+ * we manage overflow blocks to simplify (more or less) rendering,
139
+ * but they break in the event of insert/delete row/column. we need
140
+ * to adjust, or perhaps flush, when we insert/delete columns.
141
+ *
142
+ */
143
+ public FlushOverflows() {
144
+
145
+ // flush all, mark dirty and drop areas.
146
+
147
+ const cells = this.view.active_sheet.cells;
148
+ cells.IterateAll(cell => {
149
+ if (cell.renderer_data?.overflowed) {
150
+ cell.renderer_data = undefined;
151
+ cell.render_clean[this.view.view_index] = false;
152
+ }
153
+ });
154
+
155
+ for (const overflow_area of this.overflow_areas) {
156
+ overflow_area.tile.dirty = true;
157
+ }
158
+
159
+ this.overflow_areas = [];
160
+
161
+ }
162
+
163
+ /**
164
+ * use one of the tile contexts to measure text. we are using the tile
165
+ * context because it's attached to the DOM, and style is applied. we need
166
+ * that for the root font size, in case font size in the style is relative
167
+ * (which it should be).
168
+ *
169
+ * we could use the buffer context, if that were attached to the DOM, but
170
+ * at the moment it is not so this is a shortcut. since we're not actually
171
+ * painting, it's not too bad, but we still fetch the context every time.
172
+ * hopefully it's cached.
173
+ *
174
+ * FIXME: if you're doing it this way, maybe pass in an array of strings/
175
+ * fonts, to avoid getting the context every time?
176
+ *
177
+ * @param text
178
+ * @param font
179
+ */
180
+ public MeasureText(text: string, font?: string): TextMetrics {
181
+
182
+ const context = this.layout.grid_tiles[0][0].getContext('2d', { alpha: false });
183
+
184
+ if (!context) {
185
+ throw new Error('invalid context');
186
+ }
187
+
188
+ if (font) {
189
+ context.font = font;
190
+ }
191
+
192
+ return context.measureText(text);
193
+ }
194
+
195
+ /**
196
+ * when drawing to the buffered canvas, (1) ensure it's large enough,
197
+ * and (2) set transform as necessary (we may be overflowing to the left).
198
+ */
199
+ public EnsureBuffer(width = 0, height = 0, offset = 0): void {
200
+
201
+ // console.info('eb', width, height, offset);
202
+
203
+ const scale = this.layout.dpr;
204
+ width = width * scale;
205
+ height = height * scale;
206
+ offset = offset * scale;
207
+
208
+ if (width > this.buffer_canvas_size.width
209
+ || height > this.buffer_canvas_size.height) {
210
+
211
+ this.buffer_canvas_size.width = Math.max(Math.ceil(width / 256) * 256, this.buffer_canvas_size.width);
212
+ this.buffer_canvas_size.height = Math.max(Math.ceil(height / 256) * 256, this.buffer_canvas_size.height);
213
+
214
+ // console.info('size ->', this.buffer_canvas_size);
215
+
216
+ this.buffer_canvas.width = this.buffer_canvas_size.width;
217
+ this.buffer_canvas.height = this.buffer_canvas_size.height;
218
+
219
+ const context = this.buffer_canvas.getContext('2d', { alpha: false });
220
+
221
+ if (context) {
222
+ this.buffer_context = context;
223
+ this.buffer_context.textAlign = 'left';
224
+ this.buffer_context.textBaseline = BASELINE;
225
+ }
226
+
227
+ }
228
+
229
+ this.buffer_context.setTransform(scale, 0, 0, scale, offset, 0);
230
+
231
+ }
232
+
233
+ /**
234
+ * check all overflow areas. if any elements are dirty, mark all elements
235
+ * as dirty (FIXME: and remove the list?)
236
+ */
237
+ public OverflowDirty(full_tile = false): void {
238
+
239
+ const mutated = [];
240
+
241
+ for (const overflow of this.overflow_areas) {
242
+ const row = overflow.area.start.row;
243
+ let dirty = full_tile; // false;
244
+ if (!dirty) {
245
+ for (let column = overflow.area.start.column; !dirty && column <= overflow.area.end.column; column++) {
246
+ const cell = this.view.active_sheet.cells.GetCell({ row, column }, false);
247
+ dirty = !!(cell && !cell.render_clean[this.view.view_index]);
248
+ }
249
+ }
250
+ if (dirty) {
251
+ for (let column = overflow.area.start.column; column <= overflow.area.end.column; column++) {
252
+ const cell = this.view.active_sheet.cells.GetCell({ row, column }, false);
253
+ if (cell) {
254
+ cell.render_clean[this.view.view_index] = false;
255
+ if (cell.renderer_data && cell.renderer_data.overflowed) {
256
+ cell.renderer_data = undefined;
257
+ }
258
+ }
259
+ }
260
+ overflow.tile.dirty = true;
261
+ }
262
+ else mutated.push(overflow);
263
+ }
264
+
265
+ this.overflow_areas = mutated;
266
+
267
+ }
268
+
269
+
270
+ /**
271
+ *
272
+ */
273
+ public RenderCorner(/* selection: GridSelection */): void {
274
+
275
+ const corner = this.layout.corner_canvas;
276
+ const context = (corner as HTMLCanvasElement).getContext('2d', { alpha: false });
277
+
278
+ if (!context) {
279
+ throw new Error('invalid context');
280
+ }
281
+
282
+ const m2 = FontMetricsCache2.Get(Style.Font(this.theme.headers || {}, this.layout.scale));
283
+
284
+ const scale = this.layout.dpr;
285
+ const header_size = this.layout.header_offset;
286
+
287
+ let x = header_size.x;
288
+ for (let i = 0; i < this.view.active_sheet.freeze.columns; i++) {
289
+ x += this.layout.ColumnWidth(i);
290
+ }
291
+
292
+ let y = header_size.y;
293
+ for (let i = 0; i < this.view.active_sheet.freeze.rows; i++) {
294
+ y += this.layout.RowHeight(i);
295
+ }
296
+
297
+ context.setTransform(scale, 0, 0, scale, 0, 0);
298
+ context.fillStyle = this.theme.headers?.fill ? ThemeColor2(this.theme, this.theme.headers.fill) : '';
299
+
300
+ context.fillRect(0, 0, x, header_size.y);
301
+ context.fillRect(0, 0, header_size.x, y);
302
+
303
+ // we have to split this into two parts because of the new
304
+ // header grid color. do the header part first...
305
+
306
+ context.strokeStyle = this.theme.headers_grid_color || '';
307
+
308
+ context.beginPath();
309
+ context.moveTo(header_size.x - 0.5, 0);
310
+ context.lineTo(header_size.x - 0.5, header_size.y);
311
+ context.moveTo(0, header_size.y - 0.5);
312
+ context.lineTo(header_size.x, header_size.y - 0.5);
313
+ context.stroke();
314
+
315
+ // actually we can bail out first
316
+
317
+ if (!this.view.active_sheet.freeze.columns && !this.view.active_sheet.freeze.rows) return;
318
+
319
+ // then do the other part with the regular grid color
320
+
321
+ context.strokeStyle = this.theme.grid_color || '';
322
+
323
+ context.beginPath();
324
+ if (y !== header_size.y) {
325
+ context.moveTo(header_size.x - 0.5, header_size.y);
326
+ context.lineTo(header_size.x - 0.5, y);
327
+ }
328
+ if (x !== header_size.x) {
329
+ context.moveTo(header_size.x, header_size.y - 0.5);
330
+ context.lineTo(x, header_size.y - 0.5);
331
+ }
332
+ context.stroke();
333
+
334
+ // here we go back to the header grid color for the breaks
335
+
336
+ context.strokeStyle = this.theme.headers_grid_color || '';
337
+
338
+ // NOTE: if headers are hidden (which is done by setting width/height to
339
+ // 0 or 1 pixel) we don't want to render them here.
340
+
341
+ context.textAlign = 'center';
342
+ context.textBaseline = 'middle';
343
+ context.font = Style.Font(this.theme.headers||{}, this.layout.scale);
344
+
345
+ context.fillStyle = ThemeColor2(this.theme, this.theme.headers?.text);
346
+
347
+ if (this.view.active_sheet.freeze.rows && this.layout.header_offset.x > 1) {
348
+
349
+ context.setTransform(scale, 0, 0, scale, 0, 0);
350
+ context.translate(0, header_size.y);
351
+ context.beginPath();
352
+ context.moveTo(0, 0 - 0.5);
353
+ context.lineTo(header_size.x, 0 - 0.5);
354
+ context.stroke();
355
+
356
+ this.RenderRowLabels(context, 0, this.view.active_sheet.freeze.rows - 1, m2.block);
357
+
358
+ }
359
+
360
+ if (this.view.active_sheet.freeze.columns && this.layout.header_offset.y > 1) {
361
+
362
+ context.setTransform(scale, 0, 0, scale, 0, 0);
363
+ context.translate(header_size.x, 0);
364
+
365
+ // what is this doing? it's not consistent with the column paint routine...
366
+
367
+ // A: it's different. it's drawing the line at the left of the header,
368
+ // which otherwise wouldn't render [really?]. we already have the line
369
+ // at the bottom rendered above.
370
+
371
+ context.beginPath();
372
+ context.moveTo(0 - 0.5, 0);
373
+ context.lineTo(0 - 0.5, header_size.y);
374
+ context.stroke();
375
+
376
+ this.RenderColumnLabels(context, 0, this.view.active_sheet.freeze.columns - 1);
377
+
378
+ }
379
+
380
+ /////
381
+
382
+ }
383
+
384
+ /**
385
+ * unifying because headers and corner both render labels.
386
+ *
387
+ * @param context
388
+ * @param column
389
+ * @param end
390
+ */
391
+ public RenderColumnLabels(context: CanvasRenderingContext2D, column: number, end: number) {
392
+
393
+ const header_y = this.layout.header_offset.y;
394
+
395
+ context.fillStyle = ThemeColor2(this.theme, this.theme.headers?.text, 0);
396
+
397
+ context.beginPath();
398
+
399
+ for (; column <= end; column++) {
400
+ const width = this.layout.ColumnWidth(column);
401
+ const text = Area.ColumnToLabel(column);
402
+ const metrics = context.measureText(text);
403
+ if (width > metrics.width) {
404
+ context.fillText(text, width / 2, header_y / 2 + 1);
405
+ }
406
+ context.moveTo(width - 0.5, 0);
407
+ context.lineTo(width - 0.5, header_y);
408
+ context.translate(width, 0);
409
+ }
410
+
411
+ context.stroke();
412
+
413
+ }
414
+
415
+ public RenderRowLabels(context: CanvasRenderingContext2D, row: number, end: number, block: number) {
416
+
417
+ const header_x = this.layout.header_offset.x;
418
+
419
+ context.fillStyle = ThemeColor2(this.theme, this.theme.headers?.text, 0);
420
+
421
+ context.beginPath();
422
+
423
+ for (; row <= end; row++) {
424
+ const height = this.layout.RowHeight(row);
425
+ if (height >= block * 1.2) {
426
+ context.fillText(`${row + 1}`, header_x / 2, height / 2 + 1);
427
+ }
428
+ context.moveTo(0, height - 0.5);
429
+ context.lineTo(header_x, height - 0.5);
430
+ context.translate(0, height);
431
+ }
432
+
433
+ context.stroke();
434
+
435
+ }
436
+
437
+ /**
438
+ */
439
+ public RenderHeaders(tiles: TileRange /*, selection: GridSelection*/, force = false): void {
440
+
441
+ const scale = this.layout.dpr;
442
+ const header_size = this.layout.header_offset;
443
+ const m2 = FontMetricsCache2.Get(Style.Font(this.theme.headers || {}, this.layout.scale));
444
+
445
+ for (let column = tiles.start.column; column <= tiles.end.column; column++) {
446
+
447
+ const tile = this.layout.column_header_tiles[column];
448
+ if (tile.dirty || force) {
449
+
450
+ const context = tile.getContext('2d', { alpha: false });
451
+ if (!context) continue;
452
+ context.setTransform(scale, 0, 0, scale, 0, 0);
453
+
454
+ context.textAlign = 'center';
455
+ context.textBaseline = 'middle';
456
+ context.font = Style.Font(this.theme.headers||{}, this.layout.scale);
457
+
458
+ context.fillStyle = this.theme.headers?.fill ? ThemeColor2(this.theme, this.theme.headers.fill) : '';
459
+ context.fillRect(0, 0, tile.logical_size.width, this.layout.header_offset.y);
460
+
461
+ // context.strokeStyle = this.theme.grid_color || '';
462
+ context.strokeStyle = this.theme.headers_grid_color || '';
463
+
464
+ // this draws a line at the bottom of the header
465
+ // (using regular grid color)
466
+
467
+ // looks better using the header grid color
468
+
469
+ context.beginPath();
470
+ context.moveTo(0, header_size.y - 0.5);
471
+ context.lineTo(tile.logical_size.width, header_size.y - 0.5);
472
+ context.stroke();
473
+
474
+ // then we switch to the header color for the edges
475
+
476
+ context.strokeStyle = this.theme.headers_grid_color || '';
477
+
478
+ this.RenderColumnLabels(context, tile.first_cell.column, tile.last_cell.column);
479
+
480
+ tile.dirty = false;
481
+ }
482
+
483
+ }
484
+
485
+ for (let row = tiles.start.row; row <= tiles.end.row; row++) {
486
+
487
+ const tile = this.layout.row_header_tiles[row];
488
+ if (tile.dirty || force) {
489
+
490
+ const context = tile.getContext('2d', { alpha: false });
491
+ if (!context) continue;
492
+ context.fillStyle = this.theme.headers?.fill ? ThemeColor2(this.theme, this.theme.headers.fill) : '';
493
+
494
+ context.setTransform(scale, 0, 0, scale, 0, 0);
495
+
496
+ context.textAlign = 'center';
497
+ context.textBaseline = 'middle';
498
+ context.font = Style.Font(this.theme.headers||{}, this.layout.scale);
499
+
500
+ context.fillRect(0, 0, this.layout.header_offset.x, tile.logical_size.height);
501
+
502
+ // context.strokeStyle = this.theme.grid_color || '';
503
+ context.strokeStyle = this.theme.headers_grid_color || '';
504
+
505
+ context.beginPath();
506
+ context.moveTo(header_size.x - 0.5, 0);
507
+ context.lineTo(header_size.x - 0.5, tile.logical_size.height);
508
+ context.stroke();
509
+
510
+ context.strokeStyle = this.theme.headers_grid_color || '';
511
+
512
+ this.RenderRowLabels(context, tile.first_cell.row, tile.last_cell.row, m2.block);
513
+
514
+ tile.dirty = false;
515
+ }
516
+ }
517
+
518
+ if (this.view.active_sheet.freeze.rows || this.view.active_sheet.freeze.columns) {
519
+ this.RenderCorner();
520
+ }
521
+
522
+ }
523
+
524
+ /**
525
+ *
526
+ * @param tile starting tile
527
+ * @param scale scale
528
+ * @param dx tile offset, in tiles
529
+ * @param dy tile offset, in tiles
530
+ * @param left (original) translation, in scaled pixels
531
+ * @param top (original) translation, in scaled pixels
532
+ * @param result buffer info
533
+ */
534
+ public CopyToAdjacent(
535
+ tile: Tile,
536
+ scale: number,
537
+ dx: -1 | 0 | 1,
538
+ dy: -1 | 0 | 1,
539
+ left: number,
540
+ top: number,
541
+ result: RenderCellResult): void {
542
+
543
+ const adjacent = this.layout.AdjacentTile(tile, dy, dx);
544
+ if (!adjacent) return; // FIXME: warn?
545
+
546
+ let x = left;
547
+ let y = top;
548
+
549
+ if (dx > 0) {
550
+ x = left - (tile.pixel_end.x - tile.pixel_start.x) * scale;
551
+ }
552
+ else if (dx < 0) {
553
+ x = left + (adjacent.pixel_end.x - adjacent.pixel_start.x) * scale;
554
+ }
555
+ if (dy > 0) {
556
+ y = top - (tile.pixel_end.y - tile.pixel_start.y) * scale;
557
+ }
558
+
559
+ const context = adjacent.getContext('2d', { alpha: false });
560
+ if (context) {
561
+ context.setTransform(scale, 0, 0, scale, x, y);
562
+ context.drawImage(this.buffer_canvas,
563
+ 0, 0, (result.width || 0) * scale, (result.height || 0) * scale,
564
+ result.left || 0, 0, result.width || 0, result.height || 0);
565
+ }
566
+
567
+ }
568
+
569
+ /** render a tile */
570
+ public Render(tile: Tile): void {
571
+
572
+ const context = tile.getContext('2d', { alpha: false });
573
+ if (!context) { return; } // should throw
574
+
575
+ context.textBaseline = BASELINE;
576
+
577
+ const scale = this.layout.dpr;
578
+
579
+ // const render_list: Array<{row: number, column: number, cell: Cell}> = [];
580
+
581
+ // this.last_font = undefined;
582
+ context.setTransform(scale, 0, 0, scale, 0, 0);
583
+
584
+ let left = 0;
585
+ let top = 0;
586
+
587
+ // console.info('r', tile.first_cell);
588
+
589
+ for (let column = tile.first_cell.column; column <= tile.last_cell.column; column++) {
590
+ const width = this.layout.ColumnWidth(column);
591
+ if (!width) continue;
592
+ top = 0;
593
+ for (let row = tile.first_cell.row; row <= tile.last_cell.row; row++) {
594
+ const height = this.layout.RowHeight(row);
595
+ if (height) {
596
+
597
+ context.setTransform(scale, 0, 0, scale, left, top);
598
+ const cell = this.view.active_sheet.CellData({ row, column });
599
+
600
+ if (tile.needs_full_repaint || !cell.render_clean[this.view.view_index]) {
601
+
602
+ const result = this.RenderCell(tile, cell, context, { row, column }, width, height,
603
+ (tile.pixel_start.x + left),
604
+ (tile.pixel_start.y + top));
605
+
606
+ // render_list.push({row, column, cell});
607
+
608
+ if (result.tile_overflow_right) {
609
+ this.CopyToAdjacent(tile, scale, 1, 0, left, top, result);
610
+ }
611
+ if (result.tile_overflow_left) {
612
+ this.CopyToAdjacent(tile, scale, -1, 0, left, top, result);
613
+ }
614
+ if (result.tile_overflow_bottom) {
615
+ this.CopyToAdjacent(tile, scale, 0, 1, left, top, result);
616
+ }
617
+
618
+ }
619
+
620
+ }
621
+ top += (height * scale);
622
+ }
623
+ left += (width * scale);
624
+ }
625
+
626
+ if (!this.view.active_sheet.freeze.rows && !this.view.active_sheet.freeze.columns) return; // render_list;
627
+
628
+ // paint to headers
629
+
630
+ let copy_height = 0;
631
+ let copy_width = 0;
632
+
633
+ if (tile.first_cell.row <= this.view.active_sheet.freeze.rows - 1) {
634
+ for (let i = tile.first_cell.row; i < this.view.active_sheet.freeze.rows && i <= tile.last_cell.row; i++) {
635
+ copy_height += this.layout.RowHeight(i);
636
+ }
637
+ }
638
+ if (tile.first_cell.column <= this.view.active_sheet.freeze.columns - 1) {
639
+ for (let i = tile.first_cell.column; i < this.view.active_sheet.freeze.columns && i <= tile.last_cell.column; i++) {
640
+ copy_width += this.layout.ColumnWidth(i);
641
+ }
642
+ }
643
+
644
+ if (copy_height) {
645
+
646
+ // get tile header
647
+ const header = this.layout.frozen_row_tiles[tile.tile_position.column];
648
+ if (!header) throw new Error('can\'t find matching header tile');
649
+
650
+ const header_context = header.getContext('2d', { alpha: true });
651
+ if (!header_context) throw new Error('header context failed');
652
+
653
+ // FIXME: offset for !first tile
654
+
655
+ header_context.setTransform(scale, 0, 0, scale, 0, 0); // this.model.sheet.header_offset.y * scale);
656
+
657
+ header_context.drawImage(tile, 0, 0, tile.logical_size.width * scale,
658
+ copy_height * scale, 0, 0, tile.logical_size.width, copy_height);
659
+
660
+ }
661
+ if (copy_width) {
662
+
663
+ // get tile header
664
+ const header = this.layout.frozen_column_tiles[tile.tile_position.row];
665
+ if (!header) throw new Error('can\'t find matching header tile');
666
+
667
+ const header_context = header.getContext('2d', { alpha: true });
668
+ if (!header_context) throw new Error('header context failed');
669
+
670
+ // FIXME: offset for !first tile
671
+
672
+ header_context.setTransform(scale, 0, 0, scale, 0, 0);
673
+
674
+ header_context.drawImage(tile, 0, 0, copy_width * scale,
675
+ tile.logical_size.height * scale, 0, 0, copy_width, tile.logical_size.height);
676
+
677
+ }
678
+ if (copy_width && copy_height) {
679
+
680
+ const corner_context = this.layout.corner_canvas.getContext('2d', { alpha: 'false' }) as CanvasRenderingContext2D;
681
+ if (!corner_context) throw new Error('corner context failed');
682
+
683
+ // FIXME: offset for !first tile
684
+
685
+ corner_context.setTransform(scale, 0, 0, scale,
686
+ this.layout.header_offset.x * scale,
687
+ this.layout.header_offset.y * scale);
688
+
689
+ corner_context.drawImage(tile, 0, 0, copy_width * scale,
690
+ copy_height * scale, 0, 0, copy_width, copy_height);
691
+
692
+ }
693
+
694
+ return; // render_list;
695
+
696
+ }
697
+
698
+ /**
699
+ * split and measure text. can be cached. there are actually two completely
700
+ * separate operations here, which we're consolidating for convenience (and
701
+ * because they never overlap).
702
+ *
703
+ * UPDATED returning a 2d array, where the first dimension represents lines
704
+ * and the second dimension represents components
705
+ */
706
+ protected PrepText(context: CanvasRenderingContext2D,
707
+ fonts: FontSet,
708
+ cell: Cell,
709
+ cell_width: number /*, override_text?: string*/ ): PreparedText {
710
+
711
+ const strings: RenderTextPart[] = [];
712
+ const style: Style.Properties = cell.style || {};
713
+
714
+ let pad_entry: RenderTextPart | undefined;
715
+ let composite_width = 0;
716
+
717
+ let override_formatting: string | undefined;
718
+ let formatted = cell.editing ? '' : cell.formatted; // <-- empty on editing, to remove overflows
719
+
720
+ if (Array.isArray(formatted)) {
721
+
722
+ // type 1 is a multi-part formatted string; used for number formats.
723
+ // we support invisible characters and padded (expanded) characters
724
+
725
+ // FIXME: is there any case where this would include md? ...
726
+ // (potentially yes? what happens if you have a string in a number-formatted cell?)
727
+
728
+ // this is a single line, with number formatting
729
+
730
+ for (const part of formatted) {
731
+ if (part.flag === TextPartFlag.formatting) {
732
+ override_formatting = part.text;
733
+ continue;
734
+ }
735
+
736
+ const mt_width = context.measureText(part.text).width;
737
+ const render_part: RenderTextPart = {
738
+ width: mt_width,
739
+ text: part.text,
740
+ hidden: part.flag === TextPartFlag.hidden
741
+ };
742
+
743
+ strings.push(render_part);
744
+
745
+ if (part.flag === TextPartFlag.padded) {
746
+ pad_entry = render_part;
747
+ }
748
+ else {
749
+ composite_width += mt_width;
750
+ }
751
+ }
752
+
753
+ if (pad_entry) {
754
+
755
+ const text = pad_entry.text;
756
+ const text_width = pad_entry.width;
757
+ const balance = cell_width - composite_width - (2 * this.cell_edge_buffer);
758
+
759
+ pad_entry.width = Math.max(0, balance);
760
+
761
+ if (balance > 0) {
762
+ const count = Math.floor(balance / text_width);
763
+ for (let i = 1; i < count; i++) {
764
+ pad_entry.text += text;
765
+ }
766
+ composite_width = cell_width - (2 * this.cell_edge_buffer);
767
+ }
768
+ else {
769
+ pad_entry.text = '';
770
+ }
771
+
772
+ }
773
+
774
+ return { strings: [strings], format: override_formatting, width: composite_width };
775
+
776
+ }
777
+ else if (formatted) {
778
+
779
+ // type 2 is a single string, but may be split into newlines either
780
+ // explicitly or implicitly via wrap
781
+
782
+ // ALSO we don't show leading apostrophes, as those indicate a string
783
+
784
+ if (cell.type === ValueType.string && formatted[0] === '\'') {
785
+ formatted = formatted.slice(1);
786
+ }
787
+
788
+ let md: FormattedString[][];
789
+
790
+ if (this.options.markdown) {
791
+ md = MDParser.instance.Parse(formatted);
792
+ }
793
+ else {
794
+ md = MDParser.instance.Dummy(formatted);
795
+ context.font = fonts.base; // never changes
796
+ }
797
+
798
+ // if we are not wrapping, we don't have to do any trimming. if we
799
+ // are wrapping, leave whitespace attached to the front; possibly trim
800
+ // whitespace in between tokens (this should be attached to tokens, but
801
+ // possibly not...)
802
+
803
+ let max_width = 0;
804
+
805
+ // for wrapping
806
+
807
+ const bound = cell_width - (2 * this.cell_edge_buffer);
808
+ const strings: RenderTextPart[][] = [];
809
+
810
+ if (style.wrap) {
811
+
812
+ for (const line of md) {
813
+
814
+ // we should probably normalize whitespace -- because formatting
815
+ // may put some whitespace before tokens, other whitespace after
816
+ // tokens, and so on. it's confusing.
817
+
818
+ for (let i = 1; i < line.length; i++) {
819
+ const test = line[i].text.match(/^(\s+)/);
820
+ if (test) {
821
+ line[i - 1].text += test[1];
822
+ line[i].text = line[i].text.replace(/^\s+/, '');
823
+ }
824
+ }
825
+
826
+ // that leads leading whitespace on the first token, which we
827
+ // probably can't resolve (we could just drop it, I guess)
828
+
829
+
830
+ // next we need to measure each word:
831
+
832
+ interface WordMetric {
833
+ part: FormattedString,
834
+ text: string, // UNTRIMMED
835
+ trimmed: number,
836
+ width: number,
837
+ }
838
+
839
+ const words: WordMetric[] = [];
840
+
841
+ for (const element of line) {
842
+
843
+ if (this.options.markdown) {
844
+ if (element.strong && element.emphasis) {
845
+ context.font = fonts.strong_emphasis;
846
+ }
847
+ else if (element.strong) {
848
+ context.font = fonts.strong;
849
+ }
850
+ else if (element.emphasis) {
851
+ context.font = fonts.emphasis;
852
+ }
853
+ else {
854
+ context.font = fonts.base;
855
+ }
856
+ }
857
+
858
+ const split = element.text.match(/\S+\s*/g); // preserve extra whitespace on the same line...
859
+ if (split && split.length) {
860
+ for (const word of split) {
861
+
862
+ // FIXME: maybe overoptimizing, but this is measuring the same
863
+ // text twice; could reduce...
864
+
865
+ const trimmed = context.measureText(word.trim()).width;
866
+ const width = context.measureText(word).width; // including trailing whitespace
867
+ words.push({part: element, text: word, trimmed, width});
868
+
869
+ }
870
+ }
871
+ }
872
+
873
+ // now we can construct wrapped lines. we don't split words, so
874
+ // we always have at least one word on a line.
875
+
876
+ while (words.length) {
877
+
878
+ // add first word. line length is _trimmed_ length.
879
+
880
+ let last = words.shift() as WordMetric; // NOT undefined
881
+
882
+ const line2 = [last];
883
+ let line_width = last.trimmed;
884
+
885
+ // add more words? check bounds first
886
+
887
+ while (line_width < bound && words.length) {
888
+
889
+ // we're holding the trim width on the last word, but to
890
+ // test we need the untrimmed width
891
+
892
+ const word = words[0];
893
+ const test = line_width - last.trimmed + last.width + word.trimmed;
894
+
895
+ if (test >= bound) {
896
+ break; // line finished
897
+ }
898
+
899
+ // add this word to the line, remove it from the stack
900
+
901
+ last = word;
902
+ line2.push(word);
903
+ line_width = test;
904
+ words.shift();
905
+
906
+ }
907
+
908
+ // trim the last word, then insert a row (we're relying on the
909
+ // fact that this points at the last entry in the array)
910
+
911
+ last.text = last.text.trim();
912
+ last.width = last.trimmed;
913
+
914
+ strings.push(line2.map((metric) => {
915
+ return {
916
+ ...metric.part,
917
+ hidden: false,
918
+ width: metric.width,
919
+ text: metric.text,
920
+ };
921
+ }));
922
+
923
+ }
924
+
925
+ }
926
+
927
+ }
928
+ else {
929
+
930
+ // simple case
931
+
932
+ for (const line of md) {
933
+ const parts: RenderTextPart[] = [];
934
+
935
+ let line_width = 0;
936
+
937
+ for (const element of line) {
938
+
939
+ if (this.options.markdown) {
940
+ if (element.strong && element.emphasis) {
941
+ context.font = fonts.strong_emphasis;
942
+ }
943
+ else if (element.strong) {
944
+ context.font = fonts.strong;
945
+ }
946
+ else if (element.emphasis) {
947
+ context.font = fonts.emphasis;
948
+ }
949
+ else {
950
+ context.font = fonts.base;
951
+ }
952
+ }
953
+
954
+ const width = context.measureText(element.text).width;
955
+ line_width += width;
956
+
957
+ parts.push({
958
+ ...element,
959
+ hidden: false,
960
+ width,
961
+ });
962
+
963
+ }
964
+
965
+ max_width = Math.max(max_width, line_width);
966
+
967
+ strings.push(parts);
968
+
969
+ }
970
+ }
971
+
972
+ return { strings, width: max_width };
973
+
974
+ }
975
+
976
+ return {
977
+ strings: [[{ text: '', hidden: false, width: 0 }]],
978
+ width: 0,
979
+ };
980
+
981
+ }
982
+
983
+ protected ResolveColors(style: Style.Properties): Style.Properties {
984
+
985
+ const resolved = {...style};
986
+ resolved.text = { text: ThemeColor2(this.theme, style.text, 1) };
987
+
988
+ // TODO: other colors
989
+
990
+ return resolved;
991
+
992
+ }
993
+
994
+ protected RenderCellBorders(
995
+ address: ICellAddress,
996
+ context: CanvasRenderingContext2D,
997
+ style: Style.Properties,
998
+ left = 0, top = 0, width = 0, height = 0): void {
999
+
1000
+ // cell borders is one of those things that seems simple, even trivial,
1001
+ // until you actually try to do it. then it turns out to be ridiculously
1002
+ // complicated.
1003
+
1004
+ // one complicating factor that we are adding is that we don't necessarily
1005
+ // paint in order, because we may update single cells at a time. so we need
1006
+ // to account for shared borders in two directions.
1007
+
1008
+ // general rules:
1009
+ //
1010
+ // (1) borders take priority over fills
1011
+ //
1012
+ // (2) bottom cell, then right cell, take priority over this cell (except
1013
+ // with regards to rule 1, so our border takes precendence over bottom
1014
+ // cell fill, but not bottom cell border).
1015
+ //
1016
+ // some other things to note:
1017
+ //
1018
+ // - double borders (we only handle double-bottom, atm) flow _into_ the
1019
+ // neighboring cell, instead of just using the shared border. in this
1020
+ // case the shared edge should be colored wrt to the cell that owns the
1021
+ // double border, either that cell's fill or default.
1022
+ //
1023
+ // - if we have a fill, we are painting the shared border; but in this case
1024
+ // you also have to consider the top-left corner, which could be a border
1025
+ // owned by a cell offset by (-1, -1) and because of rule 1, above, that
1026
+ // pixel needs to stay border.
1027
+ //
1028
+ // - that theoretically applies to other corners as well, but somehow that
1029
+ // hasn't come up? (...)
1030
+ //
1031
+ // - instead of clipping all the corners, when necessary, why not just paint
1032
+ // the diagonals? might save time
1033
+
1034
+
1035
+ // I think there are some opportunities for caching here (TODO)
1036
+
1037
+ // ---
1038
+
1039
+ // (moved to sheet, using numpad naming)
1040
+
1041
+ const numpad = this.view.active_sheet.SurroundingStyle(address, this.theme.table);
1042
+
1043
+
1044
+ // --- start with fills ----------------------------------------------------
1045
+
1046
+ // paint top background
1047
+
1048
+ let color = ThemeColor2(this.theme, numpad[8].fill);
1049
+ if (color) {
1050
+ context.fillStyle = color
1051
+ context.fillRect(left + 0, top - 1, width, 1);
1052
+ }
1053
+
1054
+ // paint left background
1055
+
1056
+ color = ThemeColor2(this.theme, numpad[4].fill);
1057
+ if (color) {
1058
+ context.fillStyle = color
1059
+ context.fillRect(left - 1, top, 1, height);
1060
+ }
1061
+
1062
+ // paint our background. note this one goes up, left
1063
+
1064
+ color = ThemeColor2(this.theme, style.fill);
1065
+ if (color) {
1066
+ context.fillStyle = color;
1067
+ context.fillRect(left - 1, top - 1, width + 1, height + 1);
1068
+ }
1069
+
1070
+ // fill of cell to the right
1071
+
1072
+ color = ThemeColor2(this.theme, numpad[6].fill);
1073
+ if (color) {
1074
+ context.fillStyle = color;
1075
+ context.fillRect(left + width - 1, top - 1, 1, height + 1);
1076
+
1077
+ }
1078
+
1079
+ // fill of cell underneath
1080
+
1081
+ color = ThemeColor2(this.theme, numpad[2].fill);
1082
+ if (color) {
1083
+ context.fillStyle = color;
1084
+ context.fillRect(left - 1, top + height - 1, width + 1, 1);
1085
+ }
1086
+
1087
+ // --- corner borders ------------------------------------------------------
1088
+
1089
+ if (numpad[6].border_top && !numpad[6].border_left) {
1090
+ context.fillStyle = ThemeColor2(this.theme, numpad[6].border_top_fill, 1);
1091
+ context.fillRect(left + width - 1, top - 2 + numpad[6].border_top, 1, 1);
1092
+ }
1093
+ if (numpad[9].border_left) {
1094
+ context.fillStyle = ThemeColor2(this.theme, numpad[9].border_left_fill, 1);
1095
+ context.fillRect(left + width - 1, top - 1, 1, 1);
1096
+ }
1097
+ if (numpad[9].border_bottom) {
1098
+ context.fillStyle = ThemeColor2(this.theme, numpad[9].border_bottom_fill, 1);
1099
+ context.fillRect(left + width - 1, top - 2 + numpad[9].border_bottom, 1, 1);
1100
+ }
1101
+
1102
+ if (numpad[4].border_top && !numpad[4].border_right) {
1103
+ context.fillStyle = ThemeColor2(this.theme, numpad[4].border_right_fill, 1);
1104
+ context.fillRect(left - 1, top - 2 + numpad[4].border_top, 1, 1);
1105
+ }
1106
+ if (numpad[7].border_right) {
1107
+ context.fillStyle = ThemeColor2(this.theme, numpad[7].border_right_fill, 1);
1108
+ context.fillRect(left - 1, top - 1, 1, 1);
1109
+ }
1110
+ if (numpad[7].border_bottom) {
1111
+ context.fillStyle = ThemeColor2(this.theme, numpad[7].border_bottom_fill, 1);
1112
+ context.fillRect(left - 1, top - 2 + numpad[7].border_bottom, 1, 1);
1113
+ }
1114
+
1115
+ if (numpad[6].border_bottom && !numpad[6].border_left) {
1116
+ context.fillStyle = ThemeColor2(this.theme, numpad[6].border_bottom_fill, 1);
1117
+ context.fillRect(left + width - 1, top + height - numpad[6].border_bottom, 1, 1);
1118
+ }
1119
+ if (numpad[3].border_left) {
1120
+ context.fillStyle = ThemeColor2(this.theme, numpad[3].border_left_fill, 1);
1121
+ context.fillRect(left + width - 1, top + height - 1, 1, 1);
1122
+ }
1123
+ if (numpad[3].border_top) {
1124
+ context.fillStyle = ThemeColor2(this.theme, numpad[3].border_top_fill, 1);
1125
+ context.fillRect(left + width - 1, top + height - numpad[3].border_top, 1, 1);
1126
+ }
1127
+
1128
+ if (numpad[4].border_bottom && !numpad[4].border_right) {
1129
+ context.fillStyle = ThemeColor2(this.theme, numpad[4].border_bottom_fill, 1);
1130
+ context.fillRect(left - 1, top + height - numpad[4].border_bottom, 1, 1);
1131
+ }
1132
+ if (numpad[1].border_right) {
1133
+ context.fillStyle = ThemeColor2(this.theme, numpad[1].border_right_fill, 1);
1134
+ context.fillRect(left - 1, top + height - 1, 1, 1);
1135
+ }
1136
+ if (numpad[1].border_top) {
1137
+ context.fillStyle = ThemeColor2(this.theme, numpad[1].border_top_fill, 1);
1138
+ context.fillRect(left - 1, top + height - numpad[1].border_top, 1, 1);
1139
+ }
1140
+
1141
+ // --- neighbor borders ----------------------------------------------------
1142
+
1143
+ // paint top border
1144
+
1145
+ if (numpad[8].border_bottom) {
1146
+ context.fillStyle = ThemeColor2(this.theme, numpad[8].border_bottom_fill, 1);
1147
+ if (numpad[8].border_bottom === 2) {
1148
+ context.fillRect(left - 1, top - 2, width + 1, 1);
1149
+ context.fillRect(left - 1, top - 0, width + 1, 1);
1150
+ context.fillStyle = ThemeColor2(this.theme, numpad[8].fill)
1151
+ || ThemeColor(this.theme, this.theme.grid_cell?.fill) || '#fff';
1152
+ context.fillRect(left - 1, top - 1, width + 1, 1);
1153
+ }
1154
+ else {
1155
+ context.fillRect(left - 1, top - 1, width + 1, 1);
1156
+ }
1157
+ }
1158
+
1159
+ // paint left border
1160
+
1161
+ if (numpad[4].border_right) {
1162
+ context.fillStyle = ThemeColor2(this.theme, numpad[4].border_right_fill, 1);
1163
+ context.fillRect(left - 1, top - 1, 1, height + 1);
1164
+ }
1165
+
1166
+ // paint right border?
1167
+
1168
+ if (numpad[6].border_left) {
1169
+ context.fillStyle = ThemeColor2(this.theme, numpad[4].border_left_fill, 1);
1170
+ context.fillRect(left + width - 1, top - 1, 1, height + 1);
1171
+ }
1172
+
1173
+ // bottom? (...)
1174
+
1175
+ if (numpad[2].border_top) {
1176
+ context.fillStyle = ThemeColor2(this.theme, numpad[2].border_top_fill, 1);
1177
+ if (numpad[2].border_top === 2) {
1178
+ context.fillRect(left - 1, top + height - 2, width + 1, 1);
1179
+ context.fillRect(left - 1, top + height - 0, width + 1, 1);
1180
+ context.fillStyle = ThemeColor2(this.theme, numpad[2].fill)
1181
+ || ThemeColor(this.theme, this.theme.grid_cell?.fill) || '#fff';
1182
+ context.fillRect(left - 1, top + height - 1, width + 1, 1);
1183
+ }
1184
+ else {
1185
+ context.fillRect(left - 1, top + height - 1, width + 1, 1);
1186
+ }
1187
+ }
1188
+
1189
+ // -- our borders ----------------------------------------------------------
1190
+
1191
+ if (style.border_top) {
1192
+ context.fillStyle = ThemeColor2(this.theme, style.border_top_fill, 1);
1193
+ if (style.border_top === 2) {
1194
+ context.fillRect(left - 1, top - 2, width + 1, 1);
1195
+ context.fillRect(left - 1, top + 0, width + 1, 1);
1196
+ context.fillStyle = ThemeColor2(this.theme, style.fill)
1197
+ || ThemeColor(this.theme, this.theme.grid_cell?.fill) || '#fff';
1198
+ context.fillRect(left - 1, top - 1, width + 1, 1);
1199
+ }
1200
+ else {
1201
+ context.fillRect(left - 1, top - 1, width + 1, 1);
1202
+ }
1203
+ }
1204
+
1205
+ if (style.border_left) {
1206
+ context.fillStyle = ThemeColor2(this.theme, style.border_left_fill, 1);
1207
+ context.fillRect(left - 1, top - 1, 1, height + 1);
1208
+ }
1209
+
1210
+ if (style.border_right) {
1211
+ context.fillStyle = ThemeColor2(this.theme, style.border_right_fill, 1);
1212
+ context.fillRect(left + width - 1, top - 1, 1, height + 1);
1213
+ }
1214
+
1215
+ if (style.border_bottom) {
1216
+ context.fillStyle = ThemeColor2(this.theme, style.border_bottom_fill, 1);
1217
+ if (style.border_bottom === 2) {
1218
+ context.fillRect(left - 1, top + height - 2, width + 1, 1);
1219
+ context.fillRect(left - 1, top + height + 0, width + 1, 1);
1220
+ context.fillStyle = ThemeColor2(this.theme, style.fill)
1221
+ || ThemeColor(this.theme, this.theme.grid_cell?.fill) || '#fff';
1222
+ context.fillRect(left - 1, top + height - 1, width + 1, 1);
1223
+ }
1224
+ else {
1225
+ context.fillRect(left - 1, top + height - 1, width + 1, 1);
1226
+ }
1227
+ }
1228
+
1229
+ }
1230
+
1231
+ /**
1232
+ * paint background image, offset and tiled.
1233
+ */
1234
+ protected PaintBackgroundImage(
1235
+ context: CanvasRenderingContext2D,
1236
+ image: HTMLImageElement,
1237
+ left: number,
1238
+ top: number,
1239
+ width: number,
1240
+ height: number,
1241
+ render_left = 0,
1242
+ render_top = 0,
1243
+ offset = 0) {
1244
+
1245
+ // there's no explicit broken flag, but we can infer from size
1246
+ if (!image.width || !image.height) {
1247
+ return;
1248
+ }
1249
+
1250
+ const scale = (this.layout.scale || 1) * this.layout.dpr;
1251
+
1252
+ const source_left = (left / scale) % image.width;
1253
+ const source_top = (top / scale) % image.height;
1254
+
1255
+ const source_width = width / scale;
1256
+ const source_height = height / scale;
1257
+
1258
+ const roll_x = (source_left + source_width) > image.width;
1259
+ const roll_y = (source_top + source_height) > image.height;
1260
+
1261
+ if (roll_x) {
1262
+ context.drawImage(image,
1263
+ source_left - image.width,
1264
+ source_top,
1265
+ source_width,
1266
+ source_height,
1267
+ render_left,
1268
+ render_top,
1269
+ width - offset,
1270
+ height - offset);
1271
+ }
1272
+
1273
+ if (roll_y) {
1274
+ context.drawImage(image,
1275
+ source_left,
1276
+ source_top - image.height,
1277
+ source_width,
1278
+ source_height,
1279
+ render_left,
1280
+ render_top,
1281
+ width - offset,
1282
+ height - offset);
1283
+ }
1284
+
1285
+ if (roll_x && roll_y) {
1286
+ context.drawImage(image,
1287
+ source_left - image.width,
1288
+ source_top - image.height,
1289
+ source_width,
1290
+ source_height,
1291
+ render_left,
1292
+ render_top,
1293
+ width - offset,
1294
+ height - offset);
1295
+ }
1296
+
1297
+ context.drawImage(image,
1298
+ source_left,
1299
+ source_top,
1300
+ source_width,
1301
+ source_height,
1302
+ render_left,
1303
+ render_top,
1304
+ width - offset,
1305
+ height - offset);
1306
+
1307
+ }
1308
+
1309
+ protected RenderCellBackground(
1310
+ note: boolean,
1311
+ address: ICellAddress,
1312
+ context: CanvasRenderingContext2D,
1313
+ style: Style.Properties,
1314
+ width: number, height: number, cell_left = 0, cell_top = 0): void {
1315
+
1316
+ // so here we draw the background and the bottom and right grid edges.
1317
+ // fill is enclosed here, the border method has logic for border colors,
1318
+ // because it turns out to be complicated.
1319
+
1320
+ context.fillStyle = this.theme.grid_color;
1321
+ context.fillRect(0, 0, width, height);
1322
+
1323
+ if (this.view.active_sheet.image) {
1324
+ this.PaintBackgroundImage(
1325
+ context,
1326
+ this.view.active_sheet.image,
1327
+ cell_left,
1328
+ cell_top,
1329
+ width,
1330
+ height, 0, 0, 1);
1331
+ }
1332
+ else {
1333
+
1334
+ const fill = ThemeColor2(this.theme, style.fill);
1335
+ if (fill) {
1336
+ context.fillStyle = fill;
1337
+ context.fillRect(0, 0, width - 1, height - 1);
1338
+ }
1339
+ else {
1340
+ context.fillStyle = ThemeColor(this.theme, this.theme.grid_cell?.fill) || '#fff';
1341
+ context.fillRect(0, 0, width - 1, height - 1);
1342
+ }
1343
+
1344
+ }
1345
+
1346
+ // the next call actually paints background, if we have a background color
1347
+
1348
+ this.RenderCellBorders(address, context, style, 0, 0, width, height);
1349
+
1350
+ // so we need to draw the note icon after that
1351
+
1352
+ // why is this here? (it's rendered as background, I guess)
1353
+
1354
+ if (note) {
1355
+
1356
+ const offset_x = 2;
1357
+ const offset_y = 1;
1358
+ const length = 8;
1359
+
1360
+ // FIXME: why is the default in here, and not in theme defaults?
1361
+ // actually it is in theme defaults, probably was here first.
1362
+
1363
+ context.fillStyle = this.theme.note_marker_color;
1364
+ context.beginPath();
1365
+ context.moveTo(width - offset_x, offset_y);
1366
+ context.lineTo(width - offset_x - length, offset_y);
1367
+ context.lineTo(width - offset_x, offset_y + length);
1368
+ context.lineTo(width - offset_x, offset_y);
1369
+ context.fill();
1370
+ }
1371
+
1372
+ }
1373
+
1374
+ /**
1375
+ * refactoring render to allow rendering to buffered canvas, in the
1376
+ * case of tile overflow. this is problematic because as the code stands
1377
+ * now, it paints before determining if there's an overflow. so we need
1378
+ * to move some paint calls around.
1379
+ */
1380
+ protected RenderCell(
1381
+ tile: Tile,
1382
+ cell: Cell,
1383
+ context: CanvasRenderingContext2D,
1384
+ address: ICellAddress,
1385
+ width: number,
1386
+ height: number,
1387
+ cell_left = 0,
1388
+ cell_top = 0,
1389
+ ): RenderCellResult {
1390
+
1391
+ const result: RenderCellResult = {};
1392
+
1393
+ // preserve the flag, then unset so we don't have to track around
1394
+
1395
+ const dirty = !cell.render_clean[this.view.view_index];
1396
+ cell.render_clean[this.view.view_index] = true;
1397
+
1398
+ // special case for overflows (this has been set by someone to the left)
1399
+
1400
+ if (tile.needs_full_repaint &&
1401
+ cell.renderer_data?.overflowed) {
1402
+
1403
+ return {};
1404
+ }
1405
+
1406
+ let style: Style.Properties = cell.style ? {...cell.style} : {};
1407
+
1408
+ if (cell.table) {
1409
+ style = this.view.active_sheet.CellStyleData(address, cell.table.theme || this.theme.table) || {};
1410
+ }
1411
+
1412
+ if (cell.merge_area) {
1413
+
1414
+ if ((address.row === cell.merge_area.start.row) &&
1415
+ (address.column === cell.merge_area.start.column)) {
1416
+
1417
+ for (let column = cell.merge_area.start.column + 1; column <= cell.merge_area.end.column; column++) {
1418
+ width += this.layout.ColumnWidth(column);
1419
+ }
1420
+
1421
+ for (let row = cell.merge_area.start.row + 1; row <= cell.merge_area.end.row; row++) {
1422
+ height += this.layout.RowHeight(row);
1423
+ }
1424
+
1425
+ // get last cell for borders
1426
+
1427
+ if (cell.merge_area.count > 1) {
1428
+ const end_cell_style = this.view.active_sheet.CellStyleData(cell.merge_area.end);
1429
+ if (end_cell_style) {
1430
+ style.border_bottom = end_cell_style.border_bottom;
1431
+ style.border_right = end_cell_style.border_right;
1432
+ style.border_bottom_fill = end_cell_style.border_bottom_fill;
1433
+ style.border_right_fill = end_cell_style.border_right_fill;
1434
+ }
1435
+ }
1436
+
1437
+ // check if we are going to overflow into another tile right or down
1438
+
1439
+ if (cell.merge_area.end.column > tile.last_cell.column) {
1440
+ result.tile_overflow_right = true;
1441
+ }
1442
+
1443
+ if (cell.merge_area.end.row > tile.last_cell.row) {
1444
+ result.tile_overflow_bottom = true;
1445
+ }
1446
+
1447
+ // there's an issue with merges that cross tiles and resizing; they
1448
+ // don't get painted properly. we can reuse the overflow record list
1449
+ // to fix this.
1450
+
1451
+ // NOTE: this refers to _tile_ overflows, not cell overflows. we
1452
+ // should change the name to make this clearer.
1453
+
1454
+ if (result.tile_overflow_bottom || result.tile_overflow_right) {
1455
+ this.overflow_areas.push({
1456
+ tile,
1457
+ head: { ...address },
1458
+ area: new Area(cell.merge_area.start, cell.merge_area.end),
1459
+ });
1460
+ }
1461
+
1462
+ }
1463
+ else {
1464
+ return {};
1465
+ }
1466
+ }
1467
+
1468
+ // want to do some surgery here, need to consider any side-effects.
1469
+
1470
+ // specifically, to support hyperlinks, I want to (1) do the text
1471
+ // calculation before calling the cell's render_function (so we can figure
1472
+ // out layout); and (2) let the render function indicate that it does not
1473
+ // want to exit, i.e. it's only a prerender for calc purposes.
1474
+
1475
+ // although that layout calc won't be good enough to account for things
1476
+ // like overflow... also here we are just splitting the string, not
1477
+ // generating text boxes (think about justification, wrap)
1478
+
1479
+ // doing this a little differently... render function can pass but can
1480
+ // also ask us to preserve layout (text rectangles)
1481
+
1482
+ // let preserve_layout_info = false;
1483
+ // let renderer_title: string|undefined;
1484
+ // let override_text: string|undefined;
1485
+
1486
+ // ...updating...
1487
+
1488
+ const preserve_layout_info = !!cell.hyperlink;
1489
+
1490
+ if (cell.render_function) {
1491
+ this.RenderCellBackground(
1492
+ !!cell.note,
1493
+ address,
1494
+ context,
1495
+ style,
1496
+ width,
1497
+ height);
1498
+
1499
+ context.strokeStyle = context.fillStyle = ThemeColor2(this.theme, style.text, 1);
1500
+
1501
+ // there's an issue with theme colors, the function may not be able
1502
+ // to translate so we need to update the style (using a copy) to
1503
+ // resolve colors
1504
+
1505
+ const apply_style = this.ResolveColors(style);
1506
+
1507
+ const render_result = cell.render_function.call(undefined, {
1508
+ width, height, context, cell, style: apply_style, scale: this.layout.scale || 1,
1509
+ });
1510
+
1511
+ if (render_result.handled) {
1512
+ return result;
1513
+ }
1514
+
1515
+ /*
1516
+ if (render_result.metrics) {
1517
+ preserve_layout_info = true;
1518
+ }
1519
+
1520
+ if (render_result.title) {
1521
+ renderer_title = render_result.title;
1522
+ }
1523
+
1524
+ if (typeof render_result.override_text !== 'undefined') {
1525
+ override_text = render_result.override_text;
1526
+ }
1527
+ */
1528
+
1529
+ }
1530
+
1531
+ // if there's no context, we just need to render the background
1532
+ // and border; but it still might be overflowed (via merge)
1533
+
1534
+ /*
1535
+
1536
+ this is breaking rendering. not sure if it is because of buffering
1537
+ (that doesn't work, below) or because of overflow, but in any event
1538
+ it doesn't work. we should fix, or at least jump over any font stuff
1539
+ below.
1540
+
1541
+ I suspect it was written at an earlier iteration of the overall render
1542
+ routine, and then got out of sync.
1543
+
1544
+ TODO/FIXME
1545
+
1546
+ if (!cell.formatted) {
1547
+ this.RenderCellBackground(
1548
+ !!cell.note,
1549
+ address,
1550
+ (result.tile_overflow_bottom || result.tile_overflow_right) ?
1551
+ this.buffer_context : context, style, width, height);
1552
+ return result;
1553
+ }
1554
+
1555
+ */
1556
+
1557
+ // NOTE: this is OK to do in the original context, even if we're
1558
+ // (eventually) painting to the buffer context. just remember to set
1559
+ // font in the buffer context.
1560
+
1561
+ const fonts: FontSet = {
1562
+ base: Style.Font(style, this.layout.scale),
1563
+ strong: Style.Font({...style, bold: true}, this.layout.scale),
1564
+ emphasis: Style.Font({...style, italic: true}, this.layout.scale),
1565
+ strong_emphasis: Style.Font({...style, bold: true, italic: true}, this.layout.scale),
1566
+ };
1567
+
1568
+ context.font = fonts.base;
1569
+
1570
+ //
1571
+ // NOTE: we appear to be updating render data on cell size changes
1572
+ // (width/height) to account for line breaks, but that should only
1573
+ // be necessary if the text is wrapped -- correct? maybe we can skip
1574
+ //
1575
+ // (FIXME/TODO)
1576
+ //
1577
+
1578
+ if (dirty || !cell.renderer_data || cell.renderer_data.width !== width || cell.renderer_data.height !== height) {
1579
+ const text_data = this.PrepText(context, fonts, cell, width);
1580
+
1581
+ cell.renderer_data = {
1582
+ text_data,
1583
+ width,
1584
+ height,
1585
+ };
1586
+
1587
+ }
1588
+
1589
+ const text_data: PreparedText = cell.renderer_data.text_data as PreparedText;
1590
+
1591
+ // overflow is always a huge headache. here are the basic rules:
1592
+
1593
+ // (1) only strings can overflow. numbers get ### treatment.
1594
+ // (2) wrapped and merged cells cannot overflow.
1595
+ // (3) overflow is horizontal only.
1596
+ // (4) overflow can extend indefinitely.
1597
+
1598
+ // Q: what about DQ?
1599
+
1600
+ const overflow = text_data.width > (width - 2 * this.cell_edge_buffer);
1601
+
1602
+ let paint_right = width;
1603
+ let paint_left = 0;
1604
+
1605
+ let clip = false;
1606
+
1607
+ const is_number = (
1608
+ cell.type === ValueType.number ||
1609
+ cell.calculated_type === ValueType.number ||
1610
+ cell.type === ValueType.complex ||
1611
+ cell.calculated_type === ValueType.complex ||
1612
+ cell.type === ValueType.dimensioned_quantity ||
1613
+ cell.calculated_type === ValueType.dimensioned_quantity );
1614
+
1615
+ let horizontal_align = style.horizontal_align;
1616
+ if (!horizontal_align) {
1617
+ horizontal_align = is_number ? Style.HorizontalAlign.Right : Style.HorizontalAlign.Left;
1618
+ }
1619
+
1620
+ // NOTE: text rendering options (align, baseline) are set globally
1621
+ // when the tile is created, so we don't need to set them repeatedly here.
1622
+
1623
+ // we cache some data for drawing backgrounds under overflows, if necessary,
1624
+ // so we can do draw calls after we figure out if we need to buffer or not
1625
+
1626
+ // UPDATE: we have a case where there's a super-long string trying to
1627
+ // render/overflow, and it's breaking everything. we need to address some
1628
+ // caps/limits. WIP.
1629
+
1630
+ const overflow_backgrounds: OverflowCellInfo[] = [];
1631
+
1632
+ if (overflow) {
1633
+
1634
+ const can_overflow = (cell.type !== ValueType.number &&
1635
+ cell.calculated_type !== ValueType.number &&
1636
+ !style.wrap &&
1637
+ !cell.merge_area);
1638
+
1639
+ if (can_overflow) {
1640
+
1641
+ // check how far we want to overflow left and right (pixels)
1642
+
1643
+ // FIXME: should be (buffer * 2), no?
1644
+
1645
+ const delta = text_data.width - width + this.cell_edge_buffer;
1646
+
1647
+ let overflow_pixels_left = 0;
1648
+ let overflow_pixels_right = 0;
1649
+
1650
+ if (horizontal_align === Style.HorizontalAlign.Center) {
1651
+ overflow_pixels_left = overflow_pixels_right = delta / 2;
1652
+ }
1653
+ else if (horizontal_align === Style.HorizontalAlign.Right) {
1654
+ overflow_pixels_left = delta;
1655
+ }
1656
+ else {
1657
+ overflow_pixels_right = delta;
1658
+ }
1659
+
1660
+ // calculate overflow into adjacent columns
1661
+
1662
+ let overflow_right_column = address.column;
1663
+ let overflow_left_column = address.column;
1664
+
1665
+ // cap at max. use actual max, not sheet max (which reflects the
1666
+ // extent of spreadsheet data, but not visible cells).
1667
+
1668
+ while (overflow_pixels_right > 0 && overflow_right_column < this.layout.last_column) {
1669
+ overflow_right_column++;
1670
+
1671
+ const target_address = { row: address.row, column: overflow_right_column };
1672
+ const target_cell = this.view.active_sheet.CellData(target_address);
1673
+ const target_width = this.layout.ColumnWidth(overflow_right_column);
1674
+ overflow_pixels_right -= target_width;
1675
+ if (target_cell && !target_cell.type && !target_cell.calculated_type) {
1676
+
1677
+ overflow_backgrounds.push({
1678
+ address: target_address,
1679
+ cell: target_cell,
1680
+ grid: new Rectangle(paint_right, 0, target_width, height),
1681
+ background: new Rectangle(paint_right - 1, 0, target_width, height - 1),
1682
+ border: new Rectangle(paint_right, 0, target_width, height),
1683
+ });
1684
+
1685
+ paint_right += target_width;
1686
+
1687
+ // set render data for cells we are going to overflow into;
1688
+ // that will keep them from getting painted. we only need to
1689
+ // do that on the right side.
1690
+
1691
+ target_cell.render_clean[this.view.view_index] = true;
1692
+ target_cell.renderer_data = {
1693
+ overflowed: true,
1694
+ };
1695
+ }
1696
+ else {
1697
+
1698
+ // we actually don't have to clip to the right, assuming
1699
+ // we're going to paint the cells anyway... right?
1700
+ // A: not necessarily, because we might not be painting the cell _now_.
1701
+
1702
+ clip = true; // need to clip
1703
+
1704
+ break;
1705
+ }
1706
+ }
1707
+
1708
+ if (overflow_right_column > tile.last_cell.column) {
1709
+ result.tile_overflow_right = true;
1710
+ }
1711
+
1712
+ while (overflow_pixels_left > 0 && overflow_left_column >= 1) {
1713
+ overflow_left_column--;
1714
+
1715
+ const target_address = { row: address.row, column: overflow_left_column };
1716
+ const target_cell = this.view.active_sheet.CellData(target_address);
1717
+ const target_width = this.layout.ColumnWidth(overflow_left_column);
1718
+ overflow_pixels_left -= target_width;
1719
+ if (target_cell && !target_cell.type && !target_cell.calculated_type) {
1720
+
1721
+ paint_left -= target_width;
1722
+
1723
+ overflow_backgrounds.push({
1724
+ address: target_address,
1725
+ cell: target_cell,
1726
+ grid: new Rectangle(paint_left, 0, target_width, height),
1727
+ background: new Rectangle(paint_left, 0, target_width, height - 1),
1728
+ border: new Rectangle(paint_left, 0, target_width, height),
1729
+ });
1730
+
1731
+ }
1732
+ else {
1733
+ clip = true; // need to clip
1734
+ break;
1735
+ }
1736
+ }
1737
+
1738
+ if (overflow_left_column < tile.first_cell.column) {
1739
+ result.tile_overflow_left = true;
1740
+ }
1741
+
1742
+ // push overflow onto the list
1743
+
1744
+ this.overflow_areas.push({
1745
+ head: { ...address }, tile, area: new Area(
1746
+ { row: address.row, column: overflow_left_column },
1747
+ { row: address.row, column: overflow_right_column })
1748
+ });
1749
+
1750
+ }
1751
+ else {
1752
+
1753
+ // don't clip numbers, we are going to ### them
1754
+
1755
+ clip = !is_number; // (cell.type !== ValueType.number && cell.calculated_type !== ValueType.number);
1756
+
1757
+ }
1758
+
1759
+ }
1760
+
1761
+ let buffering = false;
1762
+
1763
+ // now we can render into either the primary context or the buffer
1764
+ // context. note we don't have to clip for buffered contexts, as we're
1765
+ // going to copy.
1766
+
1767
+ const original_context = context;
1768
+
1769
+ if (result.tile_overflow_bottom || result.tile_overflow_left || result.tile_overflow_right) {
1770
+
1771
+ buffering = true;
1772
+ // console.info("buffering", result, {paint_left, paint_right, text_data});
1773
+
1774
+ result.width = paint_right - paint_left;
1775
+ result.height = height;
1776
+ result.left = paint_left;
1777
+
1778
+ this.EnsureBuffer(result.width + 1, height + 1, -paint_left);
1779
+
1780
+ context = this.buffer_context;
1781
+ context.font = fonts.base;
1782
+
1783
+ }
1784
+
1785
+ this.RenderCellBackground(!!cell.note, address, context, style, width, height, cell_left, cell_top);
1786
+
1787
+ // Q: why are we doing this inline instead of using the background method?
1788
+
1789
+ for (const element of overflow_backgrounds) {
1790
+
1791
+ if ( element.cell.style?.fill &&
1792
+ (element.cell.style.fill.text || element.cell.style.fill.theme || element.cell.style.fill.theme === 0) &&
1793
+ !this.options.grid_over_background) {
1794
+
1795
+ context.fillStyle = ThemeColor(this.theme, element.cell.style.fill);
1796
+ context.fillRect(element.grid.left, element.grid.top, element.grid.width, element.grid.height);
1797
+ }
1798
+ else {
1799
+ context.fillStyle = this.theme.grid_color || '';
1800
+ context.fillRect(element.grid.left, element.grid.top, element.grid.width, element.grid.height);
1801
+
1802
+ if (this.view.active_sheet.image) {
1803
+ this.PaintBackgroundImage(
1804
+ context,
1805
+ this.view.active_sheet.image,
1806
+ cell_left + element.background.left,
1807
+ cell_top + element.background.top,
1808
+ element.background.width,
1809
+ element.background.height,
1810
+ element.background.left,
1811
+ element.background.top,
1812
+ 0 );
1813
+ }
1814
+ else {
1815
+ context.fillStyle = this.theme.grid_cell?.fill ? ThemeColor(this.theme, this.theme.grid_cell.fill) : '';
1816
+ context.fillRect(element.background.left, element.background.top,
1817
+ element.background.width, element.background.height);
1818
+ }
1819
+ }
1820
+
1821
+ if (element.cell.style) {
1822
+
1823
+ this.RenderCellBorders(element.address, context, element.cell.style,
1824
+ element.border.left, element.border.top, element.border.width, element.border.height);
1825
+ }
1826
+
1827
+ }
1828
+
1829
+ // NOTE: we are getting fontmetrics based on the base font (so ignoring italic
1830
+ // and bold variants). this should be OK because we use it for height, mostly.
1831
+ // not sure about invisible text (FIXME)
1832
+
1833
+ const m2 = FontMetricsCache2.Get(fonts.base, this.theme.grid_cell?.font_size?.value);
1834
+ // console.info("FB", fonts.base, m2);
1835
+
1836
+ // set stroke for underline
1837
+
1838
+ // FIXME: color here should default to style, not ''. it's working only
1839
+ // because our default style happens to be the default color. that applies
1840
+ // to text color, background color and border color.
1841
+
1842
+ context.lineWidth = 1;
1843
+
1844
+ context.strokeStyle = context.fillStyle =
1845
+ text_data.format ? text_data.format : ThemeColor2(this.theme, style.text, 1);
1846
+
1847
+ context.beginPath();
1848
+
1849
+ let left = this.cell_edge_buffer;
1850
+
1851
+ const line_height = 1.25;
1852
+
1853
+ //const line_count = text_data.single ? 1 : text_data.strings.length;
1854
+ const line_count = text_data.strings.length;
1855
+ const text_height = (line_count * m2.block * line_height);
1856
+
1857
+ // we stopped clipping initially because it was expensive -- but then
1858
+ // we were doing it on every cell. it's hard to imagine that clipping
1859
+ // is more expensive than buffering (painting to a second canvas and
1860
+ // copying). let's test clipping just in the case of unpainted overflow.
1861
+
1862
+ // don't clip if buffering, it's not necessary
1863
+
1864
+ clip = (clip || (text_height >= height)) && !buffering;
1865
+
1866
+ if (clip) {
1867
+ context.save();
1868
+ context.beginPath();
1869
+ context.moveTo(paint_left + 1.5, 0);
1870
+ context.lineTo(paint_left + 1.5, height);
1871
+ context.lineTo(paint_right - 1.5, height);
1872
+ context.lineTo(paint_right - 1.5, 0);
1873
+ context.clip();
1874
+ }
1875
+
1876
+ // path for underline. if there's no underline, it won't do anything.
1877
+
1878
+ context.beginPath();
1879
+
1880
+ // baseline looks OK, if you account for descenders.
1881
+
1882
+ let original_baseline = Math.round(height - 2 - (m2.block * line_height * (line_count - 1)) + WK); // switched baseline to "bottom"
1883
+
1884
+ switch (style.vertical_align) {
1885
+ case Style.VerticalAlign.Top:
1886
+ original_baseline = Math.round(m2.block * line_height) + 1;
1887
+ break;
1888
+ case Style.VerticalAlign.Middle:
1889
+ original_baseline = Math.round((height - text_height) / 2 + m2.block * line_height);
1890
+ break;
1891
+ }
1892
+
1893
+ if ((cell.type === ValueType.number ||
1894
+ cell.calculated_type === ValueType.number ||
1895
+ cell.type === ValueType.complex ||
1896
+ cell.calculated_type === ValueType.complex) && overflow) {
1897
+
1898
+ // number overflow is easy
1899
+
1900
+ const count = Math.floor((width - 2 * this.cell_edge_buffer) / m2.hash);
1901
+
1902
+ let text = '';
1903
+ for (let i = 0; i < count; i++) { text += '#'; }
1904
+ const text_width = context.measureText(text).width;
1905
+
1906
+ if (horizontal_align === Style.HorizontalAlign.Center) {
1907
+ left = Math.round((width - text_width) / 2);
1908
+ }
1909
+ else if (horizontal_align === Style.HorizontalAlign.Right) {
1910
+ left = width - this.cell_edge_buffer - text_width;
1911
+ }
1912
+
1913
+ context.fillText(text, left, original_baseline);
1914
+
1915
+ }
1916
+ else {
1917
+
1918
+ // unifying the old "single" and "!single" branches. now the data is
1919
+ // an array of rows, each of which is an array of elements. elements
1920
+ // may have different formatting.
1921
+
1922
+ let baseline = original_baseline;
1923
+ let index = 0;
1924
+
1925
+ for (const line of text_data.strings) {
1926
+
1927
+ // FIXME: cache line width
1928
+
1929
+ let line_width = 0;
1930
+ for (const part of line) { line_width += part.width; }
1931
+
1932
+ if (horizontal_align === Style.HorizontalAlign.Center) {
1933
+ left = Math.round((width - line_width) / 2);
1934
+ }
1935
+ else if (horizontal_align === Style.HorizontalAlign.Right) {
1936
+ left = width - this.cell_edge_buffer - line_width;
1937
+ }
1938
+
1939
+ /*
1940
+ if (style.font_underline) {
1941
+ const underline_y = Math.floor(baseline + 1.5 - m2.descender - WK) + .5; // metrics.block - 3.5 - metrics.ascent - 3;
1942
+ context.moveTo(left, underline_y);
1943
+ context.lineTo(left + line_width, underline_y);
1944
+ }
1945
+
1946
+ if (style.font_strike) {
1947
+ const strike_y = Math.floor(baseline - m2.descender - m2.ascender / 2) + .5;
1948
+ context.moveTo(left, strike_y);
1949
+ context.lineTo(left + line_width, strike_y);
1950
+ }
1951
+ */
1952
+
1953
+ const underline_y = Math.floor(baseline + 1.5 - m2.descender - WK) + .5; // metrics.block - 3.5 - metrics.ascent - 3;
1954
+ const strike_y = Math.floor(baseline - m2.descender - m2.ascender / 2) + .5;
1955
+
1956
+ let x = left;
1957
+ for (const part of line) {
1958
+
1959
+ if (part.strong && part.emphasis) {
1960
+ context.font = fonts.strong_emphasis;
1961
+ }
1962
+ else if (part.strong) {
1963
+ context.font = fonts.strong;
1964
+ }
1965
+ else if (part.emphasis) {
1966
+ context.font = fonts.emphasis;
1967
+ }
1968
+ else {
1969
+ context.font = fonts.base;
1970
+ }
1971
+
1972
+ if (!part.hidden) {
1973
+
1974
+ if (part.text) {
1975
+ // console.info({text: part.text, x, baseline, clip, buffering, text_data});
1976
+ context.fillText(part.text, x, baseline);
1977
+ }
1978
+
1979
+ if (style.underline) {
1980
+ context.moveTo(x, underline_y);
1981
+ context.lineTo(x + part.width, underline_y);
1982
+ }
1983
+
1984
+ if (style.strike || part.strike) {
1985
+ context.moveTo(x, strike_y);
1986
+ context.lineTo(x + part.width, strike_y);
1987
+ }
1988
+
1989
+ // we're putting this inside the test block so
1990
+ // that you can't click on hidden text. not sure
1991
+ // if it works, though.
1992
+
1993
+ if (preserve_layout_info) {
1994
+ part.left = x;
1995
+ part.top = baseline - m2.block;
1996
+ part.height = m2.block;
1997
+ }
1998
+
1999
+ }
2000
+
2001
+ x += part.width;
2002
+
2003
+ }
2004
+
2005
+ index++;
2006
+ baseline = Math.round(original_baseline + index * m2.block * line_height);
2007
+
2008
+ }
2009
+
2010
+
2011
+ }
2012
+
2013
+ /*
2014
+ else if (text_data.single) {
2015
+
2016
+ // const cached_font = context.font;
2017
+ // const italic_font = /italic/i.test(cached_font) ? cached_font : 'italic ' + cached_font;
2018
+
2019
+ // single refers to single-line text that has multiple components,
2020
+ // including spacing or hidden text. single line text (not formatted)
2021
+ // probably doesn't have this flag set, it will use the next block.
2022
+ // these could (should?) be consolidated
2023
+
2024
+ if (horizontal_align === Style.HorizontalAlign.Center) {
2025
+ left = Math.round((width - text_data.width) / 2);
2026
+ }
2027
+ else if (horizontal_align === Style.HorizontalAlign.Right) {
2028
+ left = width - this.cell_edge_buffer - text_data.width;
2029
+ }
2030
+
2031
+ const underline_y = Math.floor(original_baseline + 1.5 - m2.descender - WK) + .5; // metrics.block - 3.5 - metrics.ascent - 3;
2032
+ const strike_y = Math.floor(original_baseline - m2.descender - m2.ascender / 2) + .5;
2033
+
2034
+ // we want a single underline, possibly spanning hidden elements,
2035
+ // but not starting or stopping on a hidden element (usually invisible
2036
+ // parentheses).
2037
+
2038
+ for (const part of text_data.strings) {
2039
+ if (!part.hidden) {
2040
+
2041
+ context.fillText(part.text, left, original_baseline);
2042
+
2043
+ if (style.font_underline) {
2044
+ context.moveTo(left, underline_y);
2045
+ context.lineTo(left + part.width, underline_y);
2046
+ }
2047
+ if (style.font_strike) {
2048
+ context.moveTo(left, strike_y);
2049
+ context.lineTo(left + part.width, strike_y);
2050
+ }
2051
+ }
2052
+
2053
+ if (preserve_layout_info) {
2054
+ part.left = left;
2055
+ part.top = original_baseline - m2.block;
2056
+ part.height = m2.block;
2057
+ }
2058
+
2059
+ left += part.width;
2060
+ }
2061
+
2062
+ }
2063
+ else {
2064
+
2065
+ let baseline = original_baseline;
2066
+ let index = 0;
2067
+
2068
+ for (const part of text_data.strings) {
2069
+
2070
+ // here we justify based on part, each line might have different width
2071
+
2072
+ if (horizontal_align === Style.HorizontalAlign.Center) {
2073
+ left = Math.round((width - part.width) / 2);
2074
+ }
2075
+ else if (horizontal_align === Style.HorizontalAlign.Right) {
2076
+ left = width - this.cell_edge_buffer - part.width;
2077
+ }
2078
+
2079
+ if (style.font_underline) {
2080
+ const underline_y = Math.floor(baseline + 1.5 - m2.descender - WK) + .5; // metrics.block - 3.5 - metrics.ascent - 3;
2081
+ context.moveTo(left, underline_y);
2082
+ context.lineTo(left + part.width, underline_y);
2083
+ }
2084
+
2085
+ if (style.font_strike) {
2086
+ const strike_y = Math.floor(baseline - m2.descender - m2.ascender / 2) + .5;
2087
+ context.moveTo(left, strike_y);
2088
+ context.lineTo(left + part.width, strike_y);
2089
+ }
2090
+
2091
+ context.fillText(part.text, left, baseline);
2092
+
2093
+ if (preserve_layout_info) {
2094
+ part.left = left;
2095
+ part.top = baseline - m2.block;
2096
+ part.height = m2.block;
2097
+ }
2098
+
2099
+ index++;
2100
+ baseline = Math.round(original_baseline + index * m2.block * line_height);
2101
+ }
2102
+
2103
+ }
2104
+ */
2105
+
2106
+ context.stroke();
2107
+
2108
+ if (clip) {
2109
+ context.restore();
2110
+ }
2111
+ else if (buffering) {
2112
+ const scale = this.layout.dpr;
2113
+ original_context.drawImage(this.buffer_canvas,
2114
+ 0, 0, (result.width || 0) * scale,
2115
+ height * scale, paint_left, 0, result.width || 0, height);
2116
+ }
2117
+
2118
+ return result;
2119
+
2120
+ }
2121
+
2122
+ }