glance-widgets-sdk 1.0.0

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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +17 -0
  3. data/README.md +47 -0
  4. data/lib/glance_widgets/box.rb +117 -0
  5. data/lib/glance_widgets/color.rb +48 -0
  6. data/lib/glance_widgets/components/bar_chart.rb +65 -0
  7. data/lib/glance_widgets/components/battery.rb +77 -0
  8. data/lib/glance_widgets/components/charts.rb +5 -0
  9. data/lib/glance_widgets/components/heatmap.rb +184 -0
  10. data/lib/glance_widgets/components/line_chart.rb +213 -0
  11. data/lib/glance_widgets/components/modern.rb +59 -0
  12. data/lib/glance_widgets/components/pixel_bar_chart.rb +106 -0
  13. data/lib/glance_widgets/components/pixel_progress.rb +151 -0
  14. data/lib/glance_widgets/components/pixel_text.rb +66 -0
  15. data/lib/glance_widgets/components/progress.rb +62 -0
  16. data/lib/glance_widgets/components/ring_progress.rb +89 -0
  17. data/lib/glance_widgets/components/shared.rb +131 -0
  18. data/lib/glance_widgets/components/signal.rb +41 -0
  19. data/lib/glance_widgets/components/stat.rb +171 -0
  20. data/lib/glance_widgets/components/status.rb +102 -0
  21. data/lib/glance_widgets/data/fonts/font-3x5.json +120 -0
  22. data/lib/glance_widgets/data/fonts/font-5x7.json +118 -0
  23. data/lib/glance_widgets/data/icons.json +5293 -0
  24. data/lib/glance_widgets/data/schemas.json +418 -0
  25. data/lib/glance_widgets/data/text-advances.json +120 -0
  26. data/lib/glance_widgets/font.rb +165 -0
  27. data/lib/glance_widgets/geometry.rb +108 -0
  28. data/lib/glance_widgets/layers.rb +152 -0
  29. data/lib/glance_widgets/matrix.rb +277 -0
  30. data/lib/glance_widgets/options.rb +31 -0
  31. data/lib/glance_widgets/output.rb +91 -0
  32. data/lib/glance_widgets/pixelate.rb +46 -0
  33. data/lib/glance_widgets/protocol.rb +217 -0
  34. data/lib/glance_widgets/raster.rb +130 -0
  35. data/lib/glance_widgets/signature.rb +30 -0
  36. data/lib/glance_widgets/support.rb +134 -0
  37. data/lib/glance_widgets/themes.rb +34 -0
  38. data/lib/glance_widgets.rb +38 -0
  39. metadata +79 -0
@@ -0,0 +1,418 @@
1
+ {
2
+ "pixel": {
3
+ "color": "color",
4
+ "opacity": "number",
5
+ "cornerRadius": "number",
6
+ "rotation": "number",
7
+ "halo": "number",
8
+ "empty": "boolean"
9
+ },
10
+ "layout": {
11
+ "gapMode": {
12
+ "enum": [
13
+ "zero",
14
+ "near",
15
+ "default",
16
+ "far",
17
+ "extraFar"
18
+ ]
19
+ },
20
+ "edgeInsetRatio": "number",
21
+ "cellAspectRatio": "number",
22
+ "columnGapRatio": "number",
23
+ "rowGapRatio": "number"
24
+ },
25
+ "layers": {
26
+ "text": {
27
+ "content": {
28
+ "text": "textValue"
29
+ },
30
+ "style": {
31
+ "effectType": {
32
+ "enum": [
33
+ "none",
34
+ "mask",
35
+ "vignette",
36
+ "readability"
37
+ ]
38
+ },
39
+ "backgroundColor": "color",
40
+ "opacity": "number",
41
+ "cornerRadiusRatio": "number",
42
+ "borderColor": "color",
43
+ "borderOpacity": "number",
44
+ "borderWidth": "number",
45
+ "gradientEnabled": "boolean",
46
+ "gradientColors": {
47
+ "arrayOf": "color"
48
+ },
49
+ "gradientStops": {
50
+ "arrayOf": "number"
51
+ },
52
+ "gradientAngle": "number",
53
+ "maskColor": "color",
54
+ "maskOpacity": "number",
55
+ "vignetteIntensity": "number",
56
+ "readabilityGradient": {
57
+ "enum": [
58
+ "none",
59
+ "top",
60
+ "bottom",
61
+ "left",
62
+ "right",
63
+ "both",
64
+ "topLeftDiagonal",
65
+ "topRightDiagonal"
66
+ ]
67
+ },
68
+ "color": "color",
69
+ "textOpacity": "number",
70
+ "fontSizeRatio": "number",
71
+ "fontWeight": {
72
+ "enum": [
73
+ "300",
74
+ "400",
75
+ "500",
76
+ "600",
77
+ "700"
78
+ ]
79
+ },
80
+ "fontFamily": {
81
+ "enum": [
82
+ "system",
83
+ "systemMonospace",
84
+ "monospace"
85
+ ]
86
+ },
87
+ "fontFamilyFallback": {
88
+ "arrayOf": "string"
89
+ },
90
+ "letterSpacing": "number",
91
+ "baselineRatio": "number",
92
+ "horizontalAlignment": {
93
+ "enum": [
94
+ "left",
95
+ "center",
96
+ "right"
97
+ ]
98
+ },
99
+ "verticalAlignment": {
100
+ "enum": [
101
+ "top",
102
+ "center",
103
+ "bottom"
104
+ ]
105
+ },
106
+ "autoWrap": "boolean",
107
+ "paddingRatio": "number"
108
+ }
109
+ },
110
+ "line": {
111
+ "content": {
112
+ "direction": {
113
+ "enum": [
114
+ "horizontal",
115
+ "vertical"
116
+ ]
117
+ }
118
+ },
119
+ "style": {
120
+ "color": "color",
121
+ "opacity": "number",
122
+ "strokeWidth": "number",
123
+ "pattern": {
124
+ "enum": [
125
+ "solid",
126
+ "dashed",
127
+ "dotted"
128
+ ]
129
+ },
130
+ "cap": {
131
+ "enum": [
132
+ "round",
133
+ "butt"
134
+ ]
135
+ },
136
+ "dashLength": "number",
137
+ "dashGap": "number",
138
+ "gradientEnabled": "boolean",
139
+ "gradientColors": {
140
+ "arrayOf": "color"
141
+ },
142
+ "gradientStops": {
143
+ "arrayOf": "number"
144
+ }
145
+ }
146
+ },
147
+ "shape": {
148
+ "content": {},
149
+ "style": {
150
+ "effectType": {
151
+ "enum": [
152
+ "none",
153
+ "mask",
154
+ "vignette",
155
+ "readability"
156
+ ]
157
+ },
158
+ "backgroundColor": "color",
159
+ "opacity": "number",
160
+ "cornerRadiusRatio": "number",
161
+ "borderColor": "color",
162
+ "borderOpacity": "number",
163
+ "borderWidth": "number",
164
+ "gradientEnabled": "boolean",
165
+ "gradientColors": {
166
+ "arrayOf": "color"
167
+ },
168
+ "gradientStops": {
169
+ "arrayOf": "number"
170
+ },
171
+ "gradientAngle": "number",
172
+ "maskColor": "color",
173
+ "maskOpacity": "number",
174
+ "vignetteIntensity": "number",
175
+ "readabilityGradient": {
176
+ "enum": [
177
+ "none",
178
+ "top",
179
+ "bottom",
180
+ "left",
181
+ "right",
182
+ "both",
183
+ "topLeftDiagonal",
184
+ "topRightDiagonal"
185
+ ]
186
+ },
187
+ "color": "color"
188
+ }
189
+ },
190
+ "icon": {
191
+ "content": {
192
+ "icon": "string",
193
+ "iconCodePoint": "integer",
194
+ "iconFont": {
195
+ "enum": [
196
+ "material",
197
+ "phosphorBold",
198
+ "devicon",
199
+ "simpleIcons"
200
+ ]
201
+ },
202
+ "url": "string"
203
+ },
204
+ "style": {
205
+ "color": "color",
206
+ "textOpacity": "number",
207
+ "iconGlowEnabled": "boolean",
208
+ "iconGlowColor": "color",
209
+ "iconGlowOpacity": "number",
210
+ "iconGlowRadius": "number"
211
+ }
212
+ },
213
+ "image": {
214
+ "content": {
215
+ "url": "string"
216
+ },
217
+ "style": {
218
+ "effectType": {
219
+ "enum": [
220
+ "none",
221
+ "mask",
222
+ "vignette",
223
+ "readability"
224
+ ]
225
+ },
226
+ "backgroundColor": "color",
227
+ "opacity": "number",
228
+ "cornerRadiusRatio": "number",
229
+ "borderColor": "color",
230
+ "borderOpacity": "number",
231
+ "borderWidth": "number",
232
+ "gradientEnabled": "boolean",
233
+ "gradientColors": {
234
+ "arrayOf": "color"
235
+ },
236
+ "gradientStops": {
237
+ "arrayOf": "number"
238
+ },
239
+ "gradientAngle": "number",
240
+ "maskColor": "color",
241
+ "maskOpacity": "number",
242
+ "vignetteIntensity": "number",
243
+ "readabilityGradient": {
244
+ "enum": [
245
+ "none",
246
+ "top",
247
+ "bottom",
248
+ "left",
249
+ "right",
250
+ "both",
251
+ "topLeftDiagonal",
252
+ "topRightDiagonal"
253
+ ]
254
+ },
255
+ "color": "color"
256
+ }
257
+ },
258
+ "matrix": {
259
+ "content": {
260
+ "pixels": {
261
+ "arrayOf": {
262
+ "arrayOf": {
263
+ "object": {
264
+ "color": "color",
265
+ "opacity": "number",
266
+ "cornerRadius": "number",
267
+ "rotation": "number",
268
+ "halo": "number",
269
+ "empty": "boolean"
270
+ }
271
+ }
272
+ }
273
+ },
274
+ "default": {
275
+ "object": {
276
+ "color": "color",
277
+ "opacity": "number",
278
+ "cornerRadius": "number",
279
+ "rotation": "number",
280
+ "halo": "number",
281
+ "empty": "boolean"
282
+ }
283
+ },
284
+ "batch": {
285
+ "arrayOf": {
286
+ "object": {}
287
+ }
288
+ },
289
+ "layout": {
290
+ "object": {
291
+ "gapMode": {
292
+ "enum": [
293
+ "zero",
294
+ "near",
295
+ "default",
296
+ "far",
297
+ "extraFar"
298
+ ]
299
+ },
300
+ "edgeInsetRatio": "number",
301
+ "cellAspectRatio": "number",
302
+ "columnGapRatio": "number",
303
+ "rowGapRatio": "number"
304
+ }
305
+ }
306
+ },
307
+ "style": {}
308
+ },
309
+ "curve": {
310
+ "content": {
311
+ "mode": {
312
+ "enum": [
313
+ "samples",
314
+ "function"
315
+ ]
316
+ },
317
+ "points": {
318
+ "arrayOf": {
319
+ "arrayOf": "number",
320
+ "length": 2
321
+ }
322
+ },
323
+ "expression": "string",
324
+ "parameters": {
325
+ "record": "number"
326
+ },
327
+ "domain": {
328
+ "arrayOf": "number",
329
+ "length": 2
330
+ },
331
+ "yDomain": {
332
+ "arrayOf": "number",
333
+ "length": 2
334
+ },
335
+ "interpolation": {
336
+ "enum": [
337
+ "linear",
338
+ "stepAfter",
339
+ "monotone"
340
+ ]
341
+ },
342
+ "markers": {
343
+ "arrayOf": {
344
+ "object": {
345
+ "at": {
346
+ "object": {
347
+ "x": "number",
348
+ "sampleIndex": "integer",
349
+ "offsetPt": {
350
+ "arrayOf": "number",
351
+ "length": 2
352
+ }
353
+ }
354
+ },
355
+ "symbol": {
356
+ "object": {
357
+ "radiusPt": "number",
358
+ "fill": "color"
359
+ }
360
+ },
361
+ "label": "string",
362
+ "labelStyle": {
363
+ "object": {
364
+ "color": "color",
365
+ "fontSizePt": "number",
366
+ "position": {
367
+ "enum": [
368
+ "top",
369
+ "bottom",
370
+ "left",
371
+ "right"
372
+ ]
373
+ }
374
+ }
375
+ }
376
+ }
377
+ }
378
+ }
379
+ },
380
+ "style": {
381
+ "color": "color",
382
+ "opacity": "number",
383
+ "strokeWidth": "number",
384
+ "lineCap": {
385
+ "enum": [
386
+ "round",
387
+ "butt",
388
+ "square"
389
+ ]
390
+ },
391
+ "lineJoin": {
392
+ "enum": [
393
+ "round",
394
+ "bevel",
395
+ "miter"
396
+ ]
397
+ },
398
+ "dashLength": "number",
399
+ "dashGap": "number",
400
+ "fillColor": "color",
401
+ "fillOpacity": "number",
402
+ "plotPadding": "number"
403
+ }
404
+ }
405
+ },
406
+ "frame": {
407
+ "left": "number",
408
+ "top": "number",
409
+ "right": "number",
410
+ "bottom": "number",
411
+ "width": "number",
412
+ "height": "number"
413
+ },
414
+ "transform": {
415
+ "translateX": "number",
416
+ "translateY": "number"
417
+ }
418
+ }
@@ -0,0 +1,120 @@
1
+ {
2
+ "ascii": [
3
+ 282,
4
+ 357,
5
+ 569,
6
+ 672,
7
+ 672,
8
+ 1036,
9
+ 745,
10
+ 348,
11
+ 429,
12
+ 429,
13
+ 485,
14
+ 672,
15
+ 348,
16
+ 485,
17
+ 348,
18
+ 335,
19
+ 685,
20
+ 512,
21
+ 644,
22
+ 669,
23
+ 688,
24
+ 664,
25
+ 684,
26
+ 605,
27
+ 694,
28
+ 684,
29
+ 348,
30
+ 348,
31
+ 672,
32
+ 672,
33
+ 672,
34
+ 558,
35
+ 928,
36
+ 733,
37
+ 694,
38
+ 742,
39
+ 748,
40
+ 623,
41
+ 598,
42
+ 761,
43
+ 784,
44
+ 319,
45
+ 602,
46
+ 710,
47
+ 595,
48
+ 907,
49
+ 769,
50
+ 787,
51
+ 674,
52
+ 787,
53
+ 695,
54
+ 676,
55
+ 661,
56
+ 761,
57
+ 722,
58
+ 1010,
59
+ 730,
60
+ 709,
61
+ 676,
62
+ 429,
63
+ 335,
64
+ 429,
65
+ 672,
66
+ 628,
67
+ 500,
68
+ 590,
69
+ 652,
70
+ 588,
71
+ 652,
72
+ 602,
73
+ 409,
74
+ 646,
75
+ 633,
76
+ 288,
77
+ 288,
78
+ 602,
79
+ 295,
80
+ 933,
81
+ 628,
82
+ 620,
83
+ 647,
84
+ 648,
85
+ 436,
86
+ 567,
87
+ 413,
88
+ 628,
89
+ 584,
90
+ 848,
91
+ 581,
92
+ 598,
93
+ 567,
94
+ 429,
95
+ 300,
96
+ 429,
97
+ 672
98
+ ],
99
+ "extra": {
100
+ "°": 485,
101
+ "±": 672,
102
+ "×": 672,
103
+ "÷": 672,
104
+ "·": 348,
105
+ "•": 485,
106
+ "●": 952,
107
+ "–": 628,
108
+ "—": 907,
109
+ "…": 955,
110
+ "€": 702,
111
+ "£": 672,
112
+ "¥": 672,
113
+ "←": 944,
114
+ "→": 944,
115
+ "↑": 816,
116
+ "↓": 816,
117
+ "▲": 779,
118
+ "▼": 779
119
+ }
120
+ }
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GlanceWidgets
4
+ DEFAULT_FONT_NAME = '3x5'
5
+ DEFAULT_LETTER_SPACING = 1
6
+ DEFAULT_LINE_SPACING = 1
7
+ ANCHOR_BOX_WIDTH = 1
8
+
9
+ class PixelFont
10
+ attr_reader :name, :height, :descent, :spacing, :line_height
11
+
12
+ def initialize(definition)
13
+ @name = definition.fetch('name')
14
+ @height = definition.fetch('height')
15
+ @descent, @spacing = DataValues.fetch_or(definition, 'descent', 0), DataValues.fetch_or(definition, 'spacing', 1)
16
+ Raster.integers('Font dimensions', [height, descent, spacing])
17
+ raise ArgumentError, 'Invalid font height/descent' unless height >= 1 && descent >= 0
18
+ @line_height = height + descent
19
+ @fallback, @glyphs = definition['fallback'], {}
20
+ fixed = definition['advance']
21
+ Raster.integers('Font advance', [fixed]) unless fixed.nil?
22
+ definition.fetch('glyphs').each { |character, glyph| compile(character, glyph, fixed) }
23
+ DataValues.fetch_or(definition, 'aliases', {}).each do |alias_name, target|
24
+ unless alias_name.each_char.count == 1 && @glyphs.key?(target)
25
+ raise ArgumentError, 'Invalid font alias'
26
+ end
27
+ @glyphs[alias_name] ||= @glyphs[target]
28
+ end
29
+ raise ArgumentError, 'Font fallback has no glyph' if @fallback && !@glyphs.key?(@fallback)
30
+ end
31
+
32
+ def monospace_advance
33
+ @glyphs.values.map { |g| [g['width'], g['advance']].max }.max || 0
34
+ end
35
+
36
+ def has_glyph(character)
37
+ @glyphs.key?(character)
38
+ end
39
+
40
+ def glyph_for(character)
41
+ glyph = @glyphs[character] || @glyphs[@fallback]
42
+ raise ArgumentError, "Font #{name} has no glyph for #{character.inspect}" unless glyph
43
+ DataValues.copy(glyph)
44
+ end
45
+
46
+ private
47
+
48
+ def compile(character, definition, fixed)
49
+ raise ArgumentError, 'Glyph key must be one Unicode code point' unless character.each_char.count == 1
50
+ rows = DataValues.copy(definition.is_a?(Array) ? definition : definition.fetch('rows'))
51
+ width = rows.empty? ? 0 : rows[0].length
52
+ unless rows.length <= line_height && rows.all? { |row| row.length == width && /\A[#.]*\z/.match(row) }
53
+ raise ArgumentError, 'Invalid glyph rows'
54
+ end
55
+ advance = fixed || (definition.is_a?(Hash) ? DataValues.fetch_or(definition, 'advance', width) : width)
56
+ if rows.empty? && fixed.nil? && (!definition.is_a?(Hash) || !DataValues.set?(definition, 'advance'))
57
+ raise ArgumentError, 'Glyph without rows requires an advance'
58
+ end
59
+ Raster.integers('Glyph advance', [advance])
60
+ raise ArgumentError, 'Invalid glyph advance' if advance < 0 || (fixed && width > fixed)
61
+ rows += Array.new([0, height - rows.length].max, '.' * width)
62
+ ink_bottom = rows.each_index.select { |i| rows[i].include?('#') }.map { |i| i + 1 }.max || 0
63
+ @glyphs[character] = { 'rows' => rows, 'width' => width, 'advance' => advance, 'inkBottom' => ink_bottom }
64
+ end
65
+ end
66
+
67
+ FONT_REGISTRY = {}
68
+
69
+ def define_font(definition)
70
+ raise ArgumentError, 'Font already defined' if FONT_REGISTRY.key?(definition.fetch('name'))
71
+ font = PixelFont.new(definition)
72
+ FONT_REGISTRY[font.name] = font
73
+ end
74
+
75
+ def get_font(name)
76
+ FONT_REGISTRY.fetch(name) { raise ArgumentError, "Unknown font: #{name}" }
77
+ end
78
+
79
+ def resolve_font(font = nil)
80
+ font ||= DEFAULT_FONT_NAME
81
+ return get_font(font) if font.is_a?(String)
82
+ font.is_a?(PixelFont) ? font : PixelFont.new(font)
83
+ end
84
+
85
+ module TextLayout
86
+ module_function
87
+
88
+ def width(text, font, spacing, fixed = nil)
89
+ characters = text.each_char.to_a
90
+ return 0 if characters.empty?
91
+ characters.sum { |char| fixed || font.glyph_for(char)['advance'] } + spacing * (characters.length - 1)
92
+ end
93
+
94
+ def wrap(paragraph, font, spacing, max_width, fixed = nil)
95
+ return [paragraph] unless max_width
96
+ result, current = [], nil
97
+ paragraph.split(/ /, -1).each do |word|
98
+ rest = word.each_char.to_a
99
+ candidate = current.nil? ? word : current + ' ' + word
100
+ if width(candidate, font, spacing, fixed) <= max_width
101
+ current = candidate
102
+ next
103
+ end
104
+ result << current unless current.nil?
105
+ while rest.length > 1 && width(rest.join, font, spacing, fixed) > max_width
106
+ length = 1
107
+ length += 1 while length < rest.length && width(rest[0..length].join, font, spacing, fixed) <= max_width
108
+ result << rest.shift(length).join
109
+ end
110
+ current = rest.join
111
+ end
112
+ result << (current || '')
113
+ end
114
+ end
115
+
116
+ def layout_text(content, options = {})
117
+ font = resolve_font(options['font'])
118
+ fixed = options['monospace'] ? font.monospace_advance : nil
119
+ spacing, line_spacing = DataValues.fetch_or(options, 'letterSpacing', font.spacing), DataValues.fetch_or(options, 'lineSpacing', 1)
120
+ max_width = options['maxWidth']
121
+ box_width = DataValues.fetch_or(options, 'boxWidth', max_width || 1)
122
+ Raster.integers('Text layout spacing', [spacing, line_spacing, box_width])
123
+ if max_width
124
+ Raster.integers('maxWidth', [max_width])
125
+ raise ArgumentError, 'maxWidth must be positive' if max_width < 1
126
+ end
127
+ align = DataValues.fetch_or(options, 'align', 'left')
128
+ raise ArgumentError, 'Invalid text alignment' unless %w[left center right].include?(align)
129
+ lines = content.split(/\n/, -1).flat_map { |line| TextLayout.wrap(line, font, spacing, max_width, fixed) }
130
+ lines = [''] if lines.empty?
131
+ glyphs, widest = [], 0
132
+ pitch = font.line_height + line_spacing
133
+ lines.each_with_index do |line, index|
134
+ line_width = TextLayout.width(line, font, spacing, fixed)
135
+ widest = [widest, line_width].max
136
+ pen = case align
137
+ when 'left' then 0
138
+ when 'right' then box_width - line_width
139
+ else ((box_width - line_width) / 2.0).floor
140
+ end
141
+ line.each_char do |character|
142
+ glyph = font.glyph_for(character)
143
+ glyphs << { 'glyph' => glyph, 'x' => pen + (fixed ? (fixed - glyph['width']) / 2 : 0), 'y' => index * pitch }
144
+ pen += (fixed || glyph['advance']) + spacing
145
+ end
146
+ end
147
+ bottom = [font.height, *lines.last.each_char.map { |c| font.glyph_for(c)['inkBottom'] }].max
148
+ { 'width' => widest, 'height' => (lines.length - 1) * pitch + bottom, 'lines' => lines, 'glyphs' => glyphs }
149
+ end
150
+
151
+ def measure_text(content, font = nil, options = {})
152
+ layout_text(content, options.merge('font' => font)).select { |key, _| %w[width height lines].include?(key) }
153
+ end
154
+
155
+ def text_matrix(content, options = {})
156
+ metrics = measure_text(content, options['font'], options)
157
+ matrix = Matrix.new([1, metrics['width']].max, [1, metrics['height']].max, options)
158
+ drawing = options.merge('boxWidth' => metrics['width'])
159
+ drawing['style'] = options['color'] if DataValues.set?(options, 'color')
160
+ matrix.text(content, 0, 0, drawing)
161
+ end
162
+
163
+ FONT_3X5 = define_font(DataValues.load('fonts/font-3x5.json'))
164
+ FONT_5X7 = define_font(DataValues.load('fonts/font-5x7.json'))
165
+ end