clogs 0.1.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.
- checksums.yaml +7 -0
- data/README.md +83 -0
- data/examples/kitchen_sink.rb +65 -0
- data/lib/clogs/app.rb +398 -0
- data/lib/clogs/canvas.rb +129 -0
- data/lib/clogs/clipboard.rb +56 -0
- data/lib/clogs/dialogs.rb +136 -0
- data/lib/clogs/display_service.rb +109 -0
- data/lib/clogs/drawable.rb +182 -0
- data/lib/clogs/drawables/controls.rb +568 -0
- data/lib/clogs/drawables/image.rb +198 -0
- data/lib/clogs/drawables/misc.rb +125 -0
- data/lib/clogs/drawables/shapes.rb +302 -0
- data/lib/clogs/drawables/slot.rb +128 -0
- data/lib/clogs/drawables/text.rb +217 -0
- data/lib/clogs/lacci_compat.rb +48 -0
- data/lib/clogs/log.rb +54 -0
- data/lib/clogs/painter.rb +192 -0
- data/lib/clogs/paragraph.rb +218 -0
- data/lib/clogs/style.rb +124 -0
- data/lib/clogs/text.rb +132 -0
- data/lib/clogs/ui.rb +157 -0
- data/lib/clogs/version.rb +5 -0
- data/lib/clogs.rb +92 -0
- metadata +109 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../drawable"
|
|
4
|
+
|
|
5
|
+
module Clogs
|
|
6
|
+
# Bitmaps.
|
|
7
|
+
#
|
|
8
|
+
# This is the one place where libui really lets Shoes down. libui-ng has
|
|
9
|
+
# `uiImage`, but the only thing that can display one is a table cell -- the
|
|
10
|
+
# released library exports no `uiDrawImage`, so there is no way to blit a
|
|
11
|
+
# bitmap into an area. (Clogs checks for it at runtime and uses it if a
|
|
12
|
+
# future build provides one.)
|
|
13
|
+
#
|
|
14
|
+
# The fallback is the same trick glimmer-dsl-libui uses: turn the image into
|
|
15
|
+
# filled rectangles. Clogs run-length encodes each row first, so flat art --
|
|
16
|
+
# icons, logos, screenshots of text -- costs a handful of rectangles per row
|
|
17
|
+
# instead of one per pixel. Photographs are genuinely slow and are best
|
|
18
|
+
# avoided until libui grows a draw-image call.
|
|
19
|
+
class Image < Drawable
|
|
20
|
+
class << self
|
|
21
|
+
def cache
|
|
22
|
+
@cache ||= {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Load an image and reduce it to per-row colour runs at the requested
|
|
26
|
+
# size. Cached by [path, width, height].
|
|
27
|
+
def runs_for(url, width, height)
|
|
28
|
+
key = [url, width, height]
|
|
29
|
+
cache[key] ||= build_runs(url, width, height)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Every rectangle is a separate libui fill, so a photograph can cost tens
|
|
33
|
+
# of thousands of draw calls per frame and starve the event loop. Flat
|
|
34
|
+
# art stays well under this; anything that does not gets sampled at a
|
|
35
|
+
# coarser resolution until it fits, trading sharpness for a UI that still
|
|
36
|
+
# responds. None of this is needed once libui exports uiDrawImage.
|
|
37
|
+
RUN_BUDGET = 40_000
|
|
38
|
+
|
|
39
|
+
def build_runs(url, width, height)
|
|
40
|
+
pixels, src_w, src_h = load_pixels(url)
|
|
41
|
+
return nil unless pixels
|
|
42
|
+
|
|
43
|
+
width = src_w if width.nil? || width <= 0
|
|
44
|
+
height = src_h if height.nil? || height <= 0
|
|
45
|
+
|
|
46
|
+
runs = encode(pixels, src_w, src_h, width, height, 1)
|
|
47
|
+
step = 1
|
|
48
|
+
while runs.size > RUN_BUDGET && step < 8
|
|
49
|
+
step *= 2
|
|
50
|
+
runs = encode(pixels, src_w, src_h, width, height, step)
|
|
51
|
+
end
|
|
52
|
+
[runs, width, height]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Walk the target rectangle at `step` pixels at a time, emitting one
|
|
56
|
+
# rectangle per run of equal colour.
|
|
57
|
+
def encode(pixels, src_w, src_h, width, height, step)
|
|
58
|
+
runs = []
|
|
59
|
+
0.step(height - 1, step) do |y|
|
|
60
|
+
sy = src_h == height ? y : (y * src_h / height)
|
|
61
|
+
row_start = sy * src_w
|
|
62
|
+
run_color = nil
|
|
63
|
+
run_x = 0
|
|
64
|
+
0.step(width - 1, step) do |x|
|
|
65
|
+
sx = src_w == width ? x : (x * src_w / width)
|
|
66
|
+
color = pixels[row_start + sx]
|
|
67
|
+
next if color == run_color
|
|
68
|
+
|
|
69
|
+
runs << [run_x, y, x - run_x, run_color] if run_color && run_color[3].positive?
|
|
70
|
+
run_color = color
|
|
71
|
+
run_x = x
|
|
72
|
+
end
|
|
73
|
+
runs << [run_x, y, width - run_x, run_color] if run_color && run_color[3].positive?
|
|
74
|
+
end
|
|
75
|
+
compact(runs, step)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Rows that repeat identically become one taller rectangle. Flat art
|
|
79
|
+
# collapses dramatically here.
|
|
80
|
+
def compact(runs, step = 1)
|
|
81
|
+
by_key = {}
|
|
82
|
+
result = []
|
|
83
|
+
runs.each do |x, y, w, color|
|
|
84
|
+
key = [x, w, color]
|
|
85
|
+
prev = by_key[key]
|
|
86
|
+
if prev && prev[1] + prev[3] == y
|
|
87
|
+
prev[3] += step
|
|
88
|
+
else
|
|
89
|
+
rect = [x, y, w, step, color]
|
|
90
|
+
result << rect
|
|
91
|
+
by_key[key] = rect
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
result
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def load_pixels(url)
|
|
98
|
+
path = local_path(url)
|
|
99
|
+
return nil unless path && File.exist?(path)
|
|
100
|
+
|
|
101
|
+
png = png_canvas(path)
|
|
102
|
+
return nil unless png
|
|
103
|
+
|
|
104
|
+
pixels = Array.new(png.width * png.height)
|
|
105
|
+
png.height.times do |y|
|
|
106
|
+
png.width.times do |x|
|
|
107
|
+
v = png[x, y]
|
|
108
|
+
pixels[y * png.width + x] = [(v >> 24) & 0xff, (v >> 16) & 0xff, (v >> 8) & 0xff, v & 0xff]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
[pixels, png.width, png.height]
|
|
112
|
+
rescue StandardError => e
|
|
113
|
+
warn "Clogs: could not load image #{url.inspect}: #{e.message}"
|
|
114
|
+
nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def png_canvas(path)
|
|
118
|
+
require "chunky_png"
|
|
119
|
+
return ChunkyPNG::Canvas.from_file(path) if File.extname(path).downcase == ".png"
|
|
120
|
+
|
|
121
|
+
converted = convert_to_png(path)
|
|
122
|
+
converted ? ChunkyPNG::Canvas.from_file(converted) : nil
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# chunky_png only reads PNG. Anything else goes through ImageMagick if it
|
|
126
|
+
# happens to be installed.
|
|
127
|
+
def convert_to_png(path)
|
|
128
|
+
require "tmpdir"
|
|
129
|
+
out = File.join(Dir.tmpdir, "clogs-#{File.basename(path)}.png")
|
|
130
|
+
return out if File.exist?(out)
|
|
131
|
+
return nil unless system("which convert > /dev/null 2>&1")
|
|
132
|
+
|
|
133
|
+
system("convert", path, out) ? out : nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def local_path(url)
|
|
137
|
+
return nil if url.nil?
|
|
138
|
+
|
|
139
|
+
url = url.to_s
|
|
140
|
+
return url unless url.start_with?("http://", "https://")
|
|
141
|
+
|
|
142
|
+
download(url)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def download(url)
|
|
146
|
+
require "tmpdir"
|
|
147
|
+
require "digest"
|
|
148
|
+
require "open-uri"
|
|
149
|
+
dest = File.join(Dir.tmpdir, "clogs-img-#{Digest::SHA256.hexdigest(url)[0, 16]}")
|
|
150
|
+
return dest if File.exist?(dest)
|
|
151
|
+
|
|
152
|
+
URI.parse(url).open { |io| File.binwrite(dest, io.read) }
|
|
153
|
+
dest
|
|
154
|
+
rescue StandardError => e
|
|
155
|
+
warn "Clogs: could not fetch #{url}: #{e.message}"
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def positioned?
|
|
161
|
+
!style(:left).nil? && !style(:top).nil?
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def measure(available_width)
|
|
165
|
+
req_w = requested_width(available_width)
|
|
166
|
+
req_h = requested_height(available_width)
|
|
167
|
+
loaded = self.class.runs_for(style(:url), req_w, req_h)
|
|
168
|
+
if loaded
|
|
169
|
+
@runs, @width, @height = loaded
|
|
170
|
+
else
|
|
171
|
+
@runs = nil
|
|
172
|
+
@width = req_w || 0
|
|
173
|
+
@height = req_h || 0
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def draw(painter, x, y)
|
|
178
|
+
unless @runs
|
|
179
|
+
# Nothing loaded: a light placeholder box beats an invisible hole.
|
|
180
|
+
painter.stroke_rect(x + 0.5, y + 0.5, [@width - 1, 1].max, [@height - 1, 1].max,
|
|
181
|
+
[200, 200, 200, 255], thickness: 1)
|
|
182
|
+
return
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
@runs.each do |rx, ry, rw, rh, color|
|
|
186
|
+
painter.fill_rect(x + rx, y + ry, rw, rh, color)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def clickable?
|
|
191
|
+
!style(:click).nil?
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def on_release(x, y, _button)
|
|
195
|
+
notify("click") if contains?(x, y) && style(:click)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../drawable"
|
|
4
|
+
require_relative "slot"
|
|
5
|
+
|
|
6
|
+
module Clogs
|
|
7
|
+
# `animate`, `every`, `timer`, `motion`, `click`, `hover`, `keypress`: Shoes
|
|
8
|
+
# calls that subscribe to something rather than drawing anything.
|
|
9
|
+
class SubscriptionItem < Drawable
|
|
10
|
+
def initialize(properties)
|
|
11
|
+
super
|
|
12
|
+
@frame = 0
|
|
13
|
+
start_timer
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def api_name
|
|
17
|
+
style(:shoes_api_name).to_s
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def args
|
|
21
|
+
Array(style(:args))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def measure(_available_width)
|
|
25
|
+
@width = 0
|
|
26
|
+
@height = 0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def clickable?
|
|
30
|
+
false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def start_timer
|
|
34
|
+
case api_name
|
|
35
|
+
when "animate"
|
|
36
|
+
fps = (args[0] || 10).to_i
|
|
37
|
+
fps = 10 if fps <= 0
|
|
38
|
+
schedule(1000 / fps) { notify("animate", @frame += 1) }
|
|
39
|
+
when "every"
|
|
40
|
+
seconds = (args[0] || 1).to_f
|
|
41
|
+
schedule((seconds * 1000).round) { notify("every", @frame += 1) }
|
|
42
|
+
when "timer"
|
|
43
|
+
seconds = (args[0] || 1).to_f
|
|
44
|
+
schedule((seconds * 1000).round, repeat: false) { notify("timer") }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The app may not exist yet when the subscription is created, so defer.
|
|
49
|
+
def schedule(interval, repeat: true, &block)
|
|
50
|
+
@pending_timer = [interval, repeat, block]
|
|
51
|
+
install_timer
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def install_timer
|
|
55
|
+
return unless @pending_timer
|
|
56
|
+
return unless app
|
|
57
|
+
|
|
58
|
+
interval, repeat, block = @pending_timer
|
|
59
|
+
@pending_timer = nil
|
|
60
|
+
@stopped = false
|
|
61
|
+
app.add_timer(interval, repeat: repeat) do
|
|
62
|
+
block.call unless @stopped
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def set_parent(new_parent)
|
|
67
|
+
super
|
|
68
|
+
@app = nil
|
|
69
|
+
install_timer
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def destroy_self
|
|
73
|
+
@stopped = true
|
|
74
|
+
super
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A user-defined Shoes::Widget subclass. It behaves as a slot.
|
|
79
|
+
class Widget < Slot; end
|
|
80
|
+
|
|
81
|
+
# Shoes' `mask` composites its contents as an alpha mask over the slot
|
|
82
|
+
# beneath it. libui has no group/alpha-compositing support, so the contents
|
|
83
|
+
# are drawn normally; see the coverage matrix.
|
|
84
|
+
class Mask < Slot; end
|
|
85
|
+
|
|
86
|
+
class Video < Drawable
|
|
87
|
+
def measure(available_width)
|
|
88
|
+
@width = requested_width(available_width) || 320
|
|
89
|
+
@height = requested_height(available_width) || 240
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# libui has no media support at all, so this is an honest placeholder
|
|
93
|
+
# rather than a silent no-op.
|
|
94
|
+
def draw(painter, x, y)
|
|
95
|
+
painter.fill_rect(x, y, @width, @height, [20, 20, 20, 255])
|
|
96
|
+
painter.draw(fill: [200, 200, 200, 255]) do |p|
|
|
97
|
+
cx = x + @width / 2.0
|
|
98
|
+
cy = y + @height / 2.0
|
|
99
|
+
p.move_to(cx - 12, cy - 16).line_to(cx + 16, cy).line_to(cx - 12, cy + 16).close
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class Arrow < ArtDrawable
|
|
105
|
+
def measure(available_width)
|
|
106
|
+
@width = Style.dimension(style(:width), available_width).to_i
|
|
107
|
+
@height = (@width / 2.0).round
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def draw(painter, x, y)
|
|
111
|
+
w = @width
|
|
112
|
+
h = @height
|
|
113
|
+
painter.draw(fill: fill_paint, stroke: stroke_paint, thickness: strokewidth) do |p|
|
|
114
|
+
p.move_to(x, y + h * 0.35)
|
|
115
|
+
.line_to(x + w * 0.6, y + h * 0.35)
|
|
116
|
+
.line_to(x + w * 0.6, y)
|
|
117
|
+
.line_to(x + w, y + h * 0.5)
|
|
118
|
+
.line_to(x + w * 0.6, y + h)
|
|
119
|
+
.line_to(x + w * 0.6, y + h * 0.65)
|
|
120
|
+
.line_to(x, y + h * 0.65)
|
|
121
|
+
.close
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../drawable"
|
|
4
|
+
|
|
5
|
+
module Clogs
|
|
6
|
+
# A rounded rectangle, built from four quarter-circle arcs.
|
|
7
|
+
def self.rounded_rect(path, x, y, w, h, r)
|
|
8
|
+
r = [r, w / 2.0, h / 2.0].min
|
|
9
|
+
hp = Math::PI / 2
|
|
10
|
+
path.arc_figure(x + r, y + r, r, Math::PI, hp)
|
|
11
|
+
path.arc_to(x + w - r, y + r, r, -hp, hp)
|
|
12
|
+
path.arc_to(x + w - r, y + h - r, r, 0, hp)
|
|
13
|
+
path.arc_to(x + r, y + h - r, r, hp, hp)
|
|
14
|
+
path.close
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Shoes' art drawables. In Shoes these always carry their own left/top and so
|
|
18
|
+
# are positioned within the current slot rather than flowed.
|
|
19
|
+
class ArtDrawable < Drawable
|
|
20
|
+
def positioned?
|
|
21
|
+
true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def left
|
|
25
|
+
Style.dimension(style(:left), parent_content_width).to_i
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def top
|
|
29
|
+
Style.dimension(style(:top), parent_content_width).to_i
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def parent_content_width
|
|
33
|
+
@parent.respond_to?(:instance_variable_get) ? (@parent&.instance_variable_get(:@content_width) || 0) : 0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Shoes' `fill`, `stroke`, `strokewidth` and `rotate` are *draw context*
|
|
37
|
+
# styles: set once on a slot, inherited by every art drawable made inside
|
|
38
|
+
# it. Lacci copies them onto drawables that declare them as their own
|
|
39
|
+
# styles, but drawables like Line and Shape do not, so the draw context has
|
|
40
|
+
# to be consulted directly.
|
|
41
|
+
def draw_context
|
|
42
|
+
@styles["draw_context"] || {}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def context_style(name)
|
|
46
|
+
value = style(name)
|
|
47
|
+
value.nil? ? draw_context[name.to_s] : value
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# `nofill` and `nostroke` arrive as fully transparent colours rather than
|
|
51
|
+
# as nil, so treat zero alpha as "do not paint this".
|
|
52
|
+
def fill_paint
|
|
53
|
+
opaque(Style.color(context_style(:fill), nil))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def stroke_paint
|
|
57
|
+
opaque(Style.color(context_style(:stroke), nil))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def opaque(color)
|
|
61
|
+
color && color[3].to_i.positive? ? color : nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def strokewidth
|
|
65
|
+
(context_style(:strokewidth) || 1).to_i
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def rotation
|
|
69
|
+
context_style(:rotate).to_f
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Art drawables paint relative to the slot, honouring any rotation from the
|
|
73
|
+
# draw context.
|
|
74
|
+
def paint(painter, ox, oy)
|
|
75
|
+
@abs_x = ox + @x
|
|
76
|
+
@abs_y = oy + @y
|
|
77
|
+
if rotation.zero?
|
|
78
|
+
draw(painter, @abs_x, @abs_y)
|
|
79
|
+
else
|
|
80
|
+
painter.save do |p|
|
|
81
|
+
p.rotate(rotation, @abs_x + @width / 2.0, @abs_y + @height / 2.0)
|
|
82
|
+
draw(p, @abs_x, @abs_y)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
class Rect < ArtDrawable
|
|
89
|
+
def measure(available_width)
|
|
90
|
+
@width = Style.dimension(style(:width), available_width).to_i
|
|
91
|
+
@height = Style.dimension(style(:height), available_width).to_i
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def draw(painter, x, y)
|
|
95
|
+
curve = style(:curve).to_i
|
|
96
|
+
if curve.positive?
|
|
97
|
+
painter.draw(fill: fill_paint, stroke: stroke_paint, thickness: strokewidth) do |p|
|
|
98
|
+
Clogs.rounded_rect(p, x, y, @width, @height, curve)
|
|
99
|
+
end
|
|
100
|
+
else
|
|
101
|
+
painter.fill_rect(x, y, @width, @height, fill_paint) if fill_paint
|
|
102
|
+
painter.stroke_rect(x, y, @width, @height, stroke_paint, thickness: strokewidth) if stroke_paint
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class Oval < ArtDrawable
|
|
109
|
+
def measure(available_width)
|
|
110
|
+
radius = style(:radius)
|
|
111
|
+
if radius && !style(:width)
|
|
112
|
+
@width = @height = radius.to_i * 2
|
|
113
|
+
else
|
|
114
|
+
@width = Style.dimension(style(:width), available_width).to_i
|
|
115
|
+
@height = Style.dimension(style(:height), available_width).to_i
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def draw(painter, x, y)
|
|
120
|
+
# Shoes' `center: true` means left/top name the centre, not the corner.
|
|
121
|
+
if style(:center)
|
|
122
|
+
x -= @width / 2.0
|
|
123
|
+
y -= @height / 2.0
|
|
124
|
+
end
|
|
125
|
+
painter.fill_oval(x, y, @width, @height, fill_paint) if fill_paint
|
|
126
|
+
painter.stroke_oval(x, y, @width, @height, stroke_paint, thickness: strokewidth) if stroke_paint
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
class Line < ArtDrawable
|
|
131
|
+
def measure(available_width)
|
|
132
|
+
@x2 = Style.dimension(style(:x2), available_width).to_i
|
|
133
|
+
@y2 = Style.dimension(style(:y2), available_width).to_i
|
|
134
|
+
@width = (@x2 - left).abs
|
|
135
|
+
@height = (@y2 - top).abs
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# A line's endpoints are both slot coordinates, so it cannot use the usual
|
|
139
|
+
# "draw at my top-left" convention.
|
|
140
|
+
def paint(painter, ox, oy)
|
|
141
|
+
@abs_x = ox + [left, @x2].min
|
|
142
|
+
@abs_y = oy + [top, @y2].min
|
|
143
|
+
painter.line(ox + left, oy + top, ox + @x2, oy + @y2,
|
|
144
|
+
stroke_paint || [0, 0, 0, 255], thickness: strokewidth)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
class Star < ArtDrawable
|
|
149
|
+
def points
|
|
150
|
+
(style(:points) || 10).to_i
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def outer
|
|
154
|
+
(style(:outer) || 100).to_f
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def inner
|
|
158
|
+
(style(:inner) || 50).to_f
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def measure(_available_width)
|
|
162
|
+
@width = @height = (outer * 2).round
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def draw(painter, x, y)
|
|
166
|
+
cx = x + outer
|
|
167
|
+
cy = y + outer
|
|
168
|
+
painter.draw(fill: fill_paint, stroke: stroke_paint, thickness: strokewidth) do |p|
|
|
169
|
+
(0...(points * 2)).each do |i|
|
|
170
|
+
r = i.even? ? outer : inner
|
|
171
|
+
angle = -Math::PI / 2 + i * Math::PI / points
|
|
172
|
+
px = cx + r * Math.cos(angle)
|
|
173
|
+
py = cy + r * Math.sin(angle)
|
|
174
|
+
i.zero? ? p.move_to(px, py) : p.line_to(px, py)
|
|
175
|
+
end
|
|
176
|
+
p.close
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
class Arc < ArtDrawable
|
|
182
|
+
def measure(available_width)
|
|
183
|
+
@width = Style.dimension(style(:width), available_width).to_i
|
|
184
|
+
@height = Style.dimension(style(:height), available_width).to_i
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def draw(painter, x, y)
|
|
188
|
+
a1 = style(:angle1).to_f
|
|
189
|
+
a2 = style(:angle2).to_f
|
|
190
|
+
sweep = a2 - a1
|
|
191
|
+
radius = [@width, @height].min / 2.0
|
|
192
|
+
cx = x + @width / 2.0
|
|
193
|
+
cy = y + @height / 2.0
|
|
194
|
+
painter.draw(fill: fill_paint, stroke: stroke_paint, thickness: strokewidth) do |p|
|
|
195
|
+
p.move_to(cx, cy) if style(:wedge)
|
|
196
|
+
p.arc_figure(cx, cy, radius, a1, sweep) unless style(:wedge)
|
|
197
|
+
p.arc_to(cx, cy, radius, a1, sweep) if style(:wedge)
|
|
198
|
+
p.close if style(:wedge)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# `shape { move_to ...; line_to ... }` -- Shoes records the commands and the
|
|
204
|
+
# display service replays them.
|
|
205
|
+
class Shape < ArtDrawable
|
|
206
|
+
def measure(_available_width)
|
|
207
|
+
xs = []
|
|
208
|
+
ys = []
|
|
209
|
+
Array(style(:shape_commands)).each do |cmd|
|
|
210
|
+
_name, *args = cmd
|
|
211
|
+
args.each_slice(2) { |px, py| xs << px.to_f; ys << py.to_f if py }
|
|
212
|
+
end
|
|
213
|
+
@width = xs.empty? ? 0 : xs.max.ceil
|
|
214
|
+
@height = ys.empty? ? 0 : ys.max.ceil
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def draw(painter, x, y)
|
|
218
|
+
painter.draw(fill: fill_paint, stroke: stroke_paint, thickness: strokewidth) do |p|
|
|
219
|
+
started = false
|
|
220
|
+
Array(style(:shape_commands)).each do |cmd|
|
|
221
|
+
name, *args = cmd
|
|
222
|
+
case name.to_s
|
|
223
|
+
when "move_to"
|
|
224
|
+
p.move_to(x + args[0], y + args[1])
|
|
225
|
+
started = true
|
|
226
|
+
when "line_to"
|
|
227
|
+
p.move_to(x + args[0], y + args[1]) unless started
|
|
228
|
+
started = true
|
|
229
|
+
p.line_to(x + args[0], y + args[1])
|
|
230
|
+
when "curve_to"
|
|
231
|
+
p.curve_to(x + args[2], y + args[3], x + args[4], y + args[5], x + args[0], y + args[1])
|
|
232
|
+
when "arc_to"
|
|
233
|
+
p.arc_to(x + args[0], y + args[1], args[2], args[4].to_f, args[5].to_f - args[4].to_f)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Background and Border cover the slot that contains them rather than
|
|
241
|
+
# occupying a place in its layout.
|
|
242
|
+
class SlotDecoration < Drawable
|
|
243
|
+
def measure_for_slot(width, height)
|
|
244
|
+
@width = width
|
|
245
|
+
@height = height
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def measure(_available_width); end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
class Background < SlotDecoration
|
|
252
|
+
def paint_for_slot(painter, x, y, w, h)
|
|
253
|
+
@abs_x = x
|
|
254
|
+
@abs_y = y
|
|
255
|
+
@width = w
|
|
256
|
+
@height = h
|
|
257
|
+
paint = background_paint(x, y, w, h)
|
|
258
|
+
return unless paint
|
|
259
|
+
|
|
260
|
+
curve = style(:curve).to_i
|
|
261
|
+
if curve.positive?
|
|
262
|
+
painter.draw(fill: paint) { |p| Clogs.rounded_rect(p, x, y, w, h, curve) }
|
|
263
|
+
else
|
|
264
|
+
painter.fill_rect(x, y, w, h, paint)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Shoes lets a background be a colour, or a gradient written as
|
|
269
|
+
# `background red..blue`.
|
|
270
|
+
def background_paint(x, y, w, h)
|
|
271
|
+
fill = style(:fill) || style(:color)
|
|
272
|
+
if fill.is_a?(Range) || (fill.is_a?(Array) && fill.length == 2 && fill.first.is_a?(Array))
|
|
273
|
+
from, to = fill.is_a?(Range) ? [fill.first, fill.last] : fill
|
|
274
|
+
UI.gradient_brush(UI::BRUSH_LINEAR_GRADIENT, [x, y], [x, y + h],
|
|
275
|
+
[[0.0, Style.color(from)], [1.0, Style.color(to)]])
|
|
276
|
+
else
|
|
277
|
+
_ = w
|
|
278
|
+
Style.color(fill, nil)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
class Border < SlotDecoration
|
|
284
|
+
def paint_for_slot(painter, x, y, w, h)
|
|
285
|
+
@abs_x = x
|
|
286
|
+
@abs_y = y
|
|
287
|
+
@width = w
|
|
288
|
+
@height = h
|
|
289
|
+
color = Style.color(style(:stroke), [0, 0, 0, 255])
|
|
290
|
+
thickness = (style(:strokewidth) || 1).to_i
|
|
291
|
+
inset = thickness / 2.0
|
|
292
|
+
curve = style(:curve).to_i
|
|
293
|
+
if curve.positive?
|
|
294
|
+
painter.draw(stroke: color, thickness: thickness) do |p|
|
|
295
|
+
Clogs.rounded_rect(p, x + inset, y + inset, w - thickness, h - thickness, curve)
|
|
296
|
+
end
|
|
297
|
+
else
|
|
298
|
+
painter.stroke_rect(x + inset, y + inset, w - thickness, h - thickness, color, thickness: thickness)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|