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,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../drawable"
|
|
4
|
+
|
|
5
|
+
module Clogs
|
|
6
|
+
# A slot: the container drawable that does Shoes' layout.
|
|
7
|
+
#
|
|
8
|
+
# Shoes has exactly two layout modes and they are both simple:
|
|
9
|
+
#
|
|
10
|
+
# stack - children are placed one below another, each as wide as the slot
|
|
11
|
+
# flow - children are placed left to right and wrap when they run out of
|
|
12
|
+
# room, like words in a paragraph
|
|
13
|
+
#
|
|
14
|
+
# Anything with an explicit `left` and `top` is lifted out of that flow and
|
|
15
|
+
# placed at those coordinates relative to the slot, which is how Shoes
|
|
16
|
+
# programs do absolute positioning.
|
|
17
|
+
class Slot < Drawable
|
|
18
|
+
def flow?
|
|
19
|
+
false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def padding
|
|
23
|
+
@padding ||= Style.paddings(@styles)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Backgrounds and borders are children in the Shoes tree but they are not
|
|
27
|
+
# laid out: they cover the whole slot.
|
|
28
|
+
def decorations
|
|
29
|
+
@children.select { |c| c.is_a?(Background) || c.is_a?(Border) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def laid_out_children
|
|
33
|
+
@children.reject { |c| c.is_a?(Background) || c.is_a?(Border) || c.hidden? }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def measure(available_width)
|
|
37
|
+
ml, _mt, mr, _mb = margin
|
|
38
|
+
pl, pt, pr, pb = padding
|
|
39
|
+
|
|
40
|
+
outer_width = requested_width(available_width) || (available_width - ml - mr)
|
|
41
|
+
outer_width = 0 if outer_width.negative?
|
|
42
|
+
content_width = [outer_width - pl - pr, 0].max
|
|
43
|
+
|
|
44
|
+
flowed, positioned = laid_out_children.partition { |c| !c.positioned? }
|
|
45
|
+
content_height = flow? ? layout_flow(flowed, content_width) : layout_stack(flowed, content_width)
|
|
46
|
+
|
|
47
|
+
positioned.each do |child|
|
|
48
|
+
child.measure(content_width)
|
|
49
|
+
child.x = pl + Style.dimension(child.style(:left), content_width).to_i
|
|
50
|
+
child.y = pt + Style.dimension(child.style(:top), outer_width).to_i
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
@content_width = content_width
|
|
54
|
+
@width = outer_width
|
|
55
|
+
@height = requested_height(available_width) || (content_height + pt + pb)
|
|
56
|
+
decorations.each { |d| d.measure_for_slot(@width, @height) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def layout_stack(kids, content_width)
|
|
60
|
+
_pl, pt, = padding
|
|
61
|
+
pl = padding[0]
|
|
62
|
+
y = pt
|
|
63
|
+
kids.each do |child|
|
|
64
|
+
cml, cmt, _cmr, cmb = child.margin
|
|
65
|
+
child.measure(content_width)
|
|
66
|
+
child.x = pl + cml
|
|
67
|
+
child.y = y + cmt
|
|
68
|
+
y += cmt + child.height + cmb
|
|
69
|
+
end
|
|
70
|
+
y - pt
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def layout_flow(kids, content_width)
|
|
74
|
+
pl, pt, = padding
|
|
75
|
+
x = 0
|
|
76
|
+
y = 0
|
|
77
|
+
line_height = 0
|
|
78
|
+
kids.each do |child|
|
|
79
|
+
cml, cmt, cmr, cmb = child.margin
|
|
80
|
+
child.measure(content_width)
|
|
81
|
+
advance = cml + child.width + cmr
|
|
82
|
+
if x.positive? && x + advance > content_width
|
|
83
|
+
y += line_height
|
|
84
|
+
x = 0
|
|
85
|
+
line_height = 0
|
|
86
|
+
end
|
|
87
|
+
child.x = pl + x + cml
|
|
88
|
+
child.y = pt + y + cmt
|
|
89
|
+
x += advance
|
|
90
|
+
line_height = [line_height, cmt + child.height + cmb].max
|
|
91
|
+
end
|
|
92
|
+
y + line_height
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def paint(painter, ox, oy)
|
|
96
|
+
@abs_x = ox + @x
|
|
97
|
+
@abs_y = oy + @y
|
|
98
|
+
|
|
99
|
+
decorations.grep(Background).each { |d| d.paint_for_slot(painter, @abs_x, @abs_y, @width, @height) }
|
|
100
|
+
@children.each do |child|
|
|
101
|
+
next if child.hidden? || child.is_a?(Background) || child.is_a?(Border)
|
|
102
|
+
|
|
103
|
+
child.paint(painter, @abs_x, @abs_y)
|
|
104
|
+
end
|
|
105
|
+
decorations.grep(Border).each { |d| d.paint_for_slot(painter, @abs_x, @abs_y, @width, @height) }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class Stack < Slot; end
|
|
110
|
+
|
|
111
|
+
class Flow < Slot
|
|
112
|
+
def flow?
|
|
113
|
+
true
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# The invisible slot that holds an app's whole document. Shoes' top level
|
|
118
|
+
# behaves like a flow.
|
|
119
|
+
class DocumentRoot < Flow
|
|
120
|
+
def app=(app)
|
|
121
|
+
@app = app
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def app
|
|
125
|
+
@app
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../drawable"
|
|
4
|
+
require_relative "../paragraph"
|
|
5
|
+
|
|
6
|
+
module Clogs
|
|
7
|
+
# The display side of Shoes' inline text drawables: strong, em, code, link,
|
|
8
|
+
# span and friends. These have no geometry of their own -- they contribute
|
|
9
|
+
# styled runs to whichever paragraph contains them.
|
|
10
|
+
class TextDrawable < Drawable
|
|
11
|
+
def emphasis_style(base)
|
|
12
|
+
style_for_tag(base)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Overridden per tag.
|
|
16
|
+
def style_for_tag(base)
|
|
17
|
+
base
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def own_style(base)
|
|
21
|
+
s = style_for_tag(base)
|
|
22
|
+
s = s.with(size: resolved_size(s.size)) if style(:size)
|
|
23
|
+
s = s.with(color: Style.color(style(:stroke), s.color)) if style(:stroke)
|
|
24
|
+
s = s.with(bg: Style.color(style(:fill), s.bg)) if style(:fill)
|
|
25
|
+
s = s.with(family: style(:family)) if style(:family)
|
|
26
|
+
s = s.with(underline: true) if truthy_underline?
|
|
27
|
+
s = s.with(italic: true) if style(:emphasis).to_s == "italic"
|
|
28
|
+
s.with(owner: self)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def truthy_underline?
|
|
32
|
+
u = style(:underline)
|
|
33
|
+
!u.nil? && u != "" && u != "none" && u != false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def resolved_size(default)
|
|
37
|
+
Paragraph::SIZES[style(:size).to_s.to_sym] || (style(:size).is_a?(Numeric) ? style(:size) : default)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Contribute [text, style] pairs to the enclosing paragraph.
|
|
41
|
+
def runs(base_style)
|
|
42
|
+
mine = own_style(base_style)
|
|
43
|
+
Clogs.expand_text_items(style(:text_items), mine)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class Strong < TextDrawable
|
|
48
|
+
def style_for_tag(base) = base.with(bold: true)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class Em < TextDrawable
|
|
52
|
+
def style_for_tag(base) = base.with(italic: true)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class Code < TextDrawable
|
|
56
|
+
def style_for_tag(base) = base.with(family: Clogs.monospace_font_family)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class Ins < TextDrawable
|
|
60
|
+
def style_for_tag(base) = base.with(underline: true)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class Del < TextDrawable
|
|
64
|
+
# libui's attributed strings have no strikethrough attribute exposed by the
|
|
65
|
+
# Ruby binding, so del() renders as plain text. Documented in the coverage
|
|
66
|
+
# matrix rather than silently pretending.
|
|
67
|
+
def style_for_tag(base) = base
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class Sub < TextDrawable
|
|
71
|
+
def style_for_tag(base) = base.with(size: (base.size * 0.7).round)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class Sup < TextDrawable
|
|
75
|
+
def style_for_tag(base) = base.with(size: (base.size * 0.7).round)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class Span < TextDrawable; end
|
|
79
|
+
|
|
80
|
+
class Bg < TextDrawable
|
|
81
|
+
def style_for_tag(base) = base.with(bg: Style.color(style(:fill), base.bg))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
class Fg < TextDrawable
|
|
85
|
+
def style_for_tag(base) = base.with(color: Style.color(style(:stroke), base.color))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# A clickable run of text. Clicks are routed by the containing paragraph,
|
|
89
|
+
# which knows where the link's words ended up on screen.
|
|
90
|
+
class Link < TextDrawable
|
|
91
|
+
DEFAULT_COLOR = [0, 0, 238, 255].freeze
|
|
92
|
+
|
|
93
|
+
def style_for_tag(base)
|
|
94
|
+
base.with(color: DEFAULT_COLOR, underline: true)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def clickable?
|
|
98
|
+
true
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def click!(x, y)
|
|
102
|
+
notify("click", 1, x, y)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# A paragraph: the only text drawable with real geometry.
|
|
107
|
+
class Para < Drawable
|
|
108
|
+
def default_size
|
|
109
|
+
Paragraph::SIZES[:para]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def base_style
|
|
113
|
+
size = style(:size)
|
|
114
|
+
resolved = Paragraph::SIZES[size.to_s.to_sym] || (size.is_a?(Numeric) ? size.to_i : default_size)
|
|
115
|
+
Paragraph::TextStyle.new(
|
|
116
|
+
family: style(:family) || Clogs.default_font_family,
|
|
117
|
+
size: resolved,
|
|
118
|
+
bold: style(:font_weight).to_s == "bold",
|
|
119
|
+
italic: %w[italic oblique].include?(style(:emphasis).to_s),
|
|
120
|
+
underline: false,
|
|
121
|
+
color: Style.color(style(:stroke), [0, 0, 0, 255]),
|
|
122
|
+
bg: Style.color(style(:fill), nil),
|
|
123
|
+
owner: self
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def align
|
|
128
|
+
case style(:align).to_s
|
|
129
|
+
when "center" then :center
|
|
130
|
+
when "right" then :right
|
|
131
|
+
else :left
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def measure(available_width)
|
|
136
|
+
ml, _mt, mr, = margin
|
|
137
|
+
avail = requested_width(available_width) || (available_width - ml - mr)
|
|
138
|
+
@paragraph = Paragraph.new(Clogs.expand_text_items(style(:text_items), base_style), avail, align: align)
|
|
139
|
+
@width = requested_width(available_width) || [@paragraph.width.ceil, avail].min
|
|
140
|
+
@height = requested_height(available_width) || @paragraph.height.ceil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def draw(painter, x, y)
|
|
144
|
+
return unless @paragraph
|
|
145
|
+
|
|
146
|
+
@paragraph.draw(painter, x, y)
|
|
147
|
+
draw_text_cursor(painter, x, y)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Shoes' `para.cursor = n` puts a caret at character n. libui's text layouts
|
|
151
|
+
# do not expose caret geometry, but Clogs lays out word by word, so the
|
|
152
|
+
# position is a lookup plus one substring measurement.
|
|
153
|
+
def draw_text_cursor(painter, x, y)
|
|
154
|
+
position = style(:text_cursor)
|
|
155
|
+
return if position.nil? || ENV["CLOGS_NO_CARET"]
|
|
156
|
+
|
|
157
|
+
item = @paragraph.placed.find do |placed|
|
|
158
|
+
position >= placed.char_offset && position <= placed.char_offset + placed.text.length
|
|
159
|
+
end
|
|
160
|
+
item ||= @paragraph.placed.last
|
|
161
|
+
return unless item
|
|
162
|
+
|
|
163
|
+
prefix = item.text[0...(position - item.char_offset)].to_s
|
|
164
|
+
cx = x + item.x + Paragraph.measure(prefix, item.style)[0]
|
|
165
|
+
cy = y + item.y
|
|
166
|
+
painter.line(cx, cy, cx, cy + item.height, [0, 0, 0, 255], thickness: 1)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def clickable?
|
|
170
|
+
true
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Find the link (if any) under this point and fire it.
|
|
174
|
+
def on_click(x, y, button)
|
|
175
|
+
return unless @paragraph
|
|
176
|
+
|
|
177
|
+
lx = x - @abs_x
|
|
178
|
+
ly = y - @abs_y
|
|
179
|
+
link = link_at(lx, ly)
|
|
180
|
+
link&.click!(x, y)
|
|
181
|
+
_ = button
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def link_at(lx, ly)
|
|
185
|
+
owners = @paragraph.placed.map(&:owner).uniq.select { |o| o.is_a?(Link) }
|
|
186
|
+
owners.find do |owner|
|
|
187
|
+
@paragraph.boxes_for(owner).any? do |bx, by, bw, bh|
|
|
188
|
+
lx >= bx && lx < bx + bw && ly >= by && ly < by + bh
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def cursor_index_at(x, y)
|
|
194
|
+
@paragraph&.index_at(x - @abs_x, y - @abs_y) || 0
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
class << Clogs
|
|
199
|
+
# text_items is an array of Strings and linkable IDs of TextDrawables.
|
|
200
|
+
# Resolve it into a flat list of [text, style] pairs.
|
|
201
|
+
def expand_text_items(items, style)
|
|
202
|
+
return [] if items.nil?
|
|
203
|
+
|
|
204
|
+
# An item is either literal text or the linkable id of a nested text
|
|
205
|
+
# drawable such as strong() or link(). Ids are not always Strings, so
|
|
206
|
+
# ask the display service about anything that could be one.
|
|
207
|
+
Array(items).flat_map do |item|
|
|
208
|
+
peer = DisplayService.instance&.query_display_drawable_for(item, nil_ok: true)
|
|
209
|
+
if peer.is_a?(TextDrawable)
|
|
210
|
+
peer.runs(style)
|
|
211
|
+
else
|
|
212
|
+
[[item.to_s, style]]
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Small pieces of the Shoes API that Lacci 0.5.0 does not implement but that
|
|
4
|
+
# real Shoes programs use constantly. Each is defined only if the installed
|
|
5
|
+
# Lacci lacks it, so newer versions win.
|
|
6
|
+
|
|
7
|
+
class Shoes::App
|
|
8
|
+
# `app.clear { ... }` empties the top-level slot and optionally refills it.
|
|
9
|
+
# Animated Shoes programs redraw themselves this way on every frame.
|
|
10
|
+
unless method_defined?(:clear)
|
|
11
|
+
def clear(&block)
|
|
12
|
+
root = instance_variable_get(:@document_root)
|
|
13
|
+
root&.clear(&block)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# `self.mouse` returns [button, x, y]. Clogs tracks the pointer as libui
|
|
18
|
+
# reports it.
|
|
19
|
+
unless method_defined?(:mouse)
|
|
20
|
+
def mouse
|
|
21
|
+
Clogs::App.instance&.mouse_state || [0, 0, 0]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Shoes 3's `visit` opened a URL; without a browser widget the best we can do
|
|
26
|
+
# is hand it to the desktop.
|
|
27
|
+
unless method_defined?(:visit)
|
|
28
|
+
def visit(url)
|
|
29
|
+
opener = case RUBY_PLATFORM
|
|
30
|
+
when /darwin/ then "open"
|
|
31
|
+
when /mingw|mswin/ then "start"
|
|
32
|
+
else "xdg-open"
|
|
33
|
+
end
|
|
34
|
+
system(opener, url.to_s, out: File::NULL, err: File::NULL)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Shoes 3 read and wrote the system clipboard through the app.
|
|
39
|
+
unless method_defined?(:clipboard)
|
|
40
|
+
def clipboard
|
|
41
|
+
Clogs::Clipboard.read
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def clipboard=(text)
|
|
45
|
+
Clogs::Clipboard.write(text.to_s)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/clogs/log.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clogs
|
|
4
|
+
# Lacci insists on a logging implementation being plugged in before any Shoes
|
|
5
|
+
# object exists. Scarpe ships two (one backed by the `logging` gem, one
|
|
6
|
+
# simple); Clogs has its own so that a packaged app needs no logging
|
|
7
|
+
# dependency at all.
|
|
8
|
+
#
|
|
9
|
+
# Configure with SCARPE_LOG_LEVEL (debug/info/warn/error/fatal) or
|
|
10
|
+
# SCARPE_DEBUG=1.
|
|
11
|
+
class Log
|
|
12
|
+
LEVELS = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4 }.freeze
|
|
13
|
+
|
|
14
|
+
def initialize(out = $stderr)
|
|
15
|
+
@out = out
|
|
16
|
+
@level = :warn
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def configure_logger(config)
|
|
20
|
+
level = if ENV["SCARPE_LOG_LEVEL"]
|
|
21
|
+
ENV["SCARPE_LOG_LEVEL"].downcase.to_sym
|
|
22
|
+
elsif config.is_a?(Hash)
|
|
23
|
+
(config["default"] || config[:default] || "warn").to_s.to_sym
|
|
24
|
+
else
|
|
25
|
+
:warn
|
|
26
|
+
end
|
|
27
|
+
@level = LEVELS.key?(level) ? level : :warn
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def logger_for_component(component)
|
|
31
|
+
Logger.new(self, component.to_s)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def emit(component, level, message)
|
|
35
|
+
return if LEVELS[level] < LEVELS[@level]
|
|
36
|
+
|
|
37
|
+
@out.puts("[#{level.to_s.upcase}] #{component}: #{message}")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# One of these per component, matching the interface Lacci expects.
|
|
41
|
+
class Logger
|
|
42
|
+
def initialize(sink, component)
|
|
43
|
+
@sink = sink
|
|
44
|
+
@component = component
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
LEVELS.each_key do |level|
|
|
48
|
+
define_method(level) do |message = nil, &block|
|
|
49
|
+
@sink.emit(@component, level, message || block&.call)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ui"
|
|
4
|
+
|
|
5
|
+
module Clogs
|
|
6
|
+
# A path under construction. libui aborts the process if a path is ended
|
|
7
|
+
# twice or drawn before being ended, so the state is tracked here rather
|
|
8
|
+
# than left to callers.
|
|
9
|
+
class Path
|
|
10
|
+
attr_reader :ptr
|
|
11
|
+
|
|
12
|
+
def initialize(fill_mode)
|
|
13
|
+
@ptr = UI::L.draw_new_path(fill_mode)
|
|
14
|
+
@ended = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def move_to(x, y)
|
|
18
|
+
UI::L.draw_path_new_figure(@ptr, x, y)
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def line_to(x, y)
|
|
23
|
+
UI::L.draw_path_line_to(@ptr, x, y)
|
|
24
|
+
self
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def curve_to(c1x, c1y, c2x, c2y, x, y)
|
|
28
|
+
UI::L.draw_path_bezier_to(@ptr, c1x, c1y, c2x, c2y, x, y)
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def arc_to(cx, cy, radius, start_angle, sweep, negative = false)
|
|
33
|
+
UI::L.draw_path_arc_to(@ptr, cx, cy, radius, start_angle, sweep, negative ? 1 : 0)
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def arc_figure(cx, cy, radius, start_angle, sweep, negative = false)
|
|
38
|
+
UI::L.draw_path_new_figure_with_arc(@ptr, cx, cy, radius, start_angle, sweep, negative ? 1 : 0)
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def rect(x, y, w, h)
|
|
43
|
+
UI::L.draw_path_add_rectangle(@ptr, x, y, w, h)
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Four beziers approximate an ellipse closely enough for UI work and avoid
|
|
48
|
+
# needing a scale matrix per oval (libui arcs are always circular).
|
|
49
|
+
K = 0.5522847498307936
|
|
50
|
+
def oval(x, y, w, h)
|
|
51
|
+
rx = w / 2.0
|
|
52
|
+
ry = h / 2.0
|
|
53
|
+
cx = x + rx
|
|
54
|
+
cy = y + ry
|
|
55
|
+
move_to(cx + rx, cy)
|
|
56
|
+
curve_to(cx + rx, cy + ry * K, cx + rx * K, cy + ry, cx, cy + ry)
|
|
57
|
+
curve_to(cx - rx * K, cy + ry, cx - rx, cy + ry * K, cx - rx, cy)
|
|
58
|
+
curve_to(cx - rx, cy - ry * K, cx - rx * K, cy - ry, cx, cy - ry)
|
|
59
|
+
curve_to(cx + rx * K, cy - ry, cx + rx, cy - ry * K, cx + rx, cy)
|
|
60
|
+
close
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def close
|
|
64
|
+
UI::L.draw_path_close_figure(@ptr)
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def end!
|
|
69
|
+
return self if @ended
|
|
70
|
+
|
|
71
|
+
@ended = true
|
|
72
|
+
UI::L.draw_path_end(@ptr)
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def free
|
|
77
|
+
UI::L.draw_free_path(@ptr) if @ptr
|
|
78
|
+
@ptr = nil
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# An immediate-mode drawing surface over a uiDrawContext.
|
|
83
|
+
#
|
|
84
|
+
# Shoes is a canvas library: everything from a `rect` to a `button` is
|
|
85
|
+
# ultimately painted. libui's draw API is closely modelled on Cairo (paths,
|
|
86
|
+
# brushes, matrices, clipping, save/restore), so the mapping is direct.
|
|
87
|
+
class Painter
|
|
88
|
+
WINDING = UI::WINDING
|
|
89
|
+
ALTERNATE = UI::ALTERNATE
|
|
90
|
+
|
|
91
|
+
attr_reader :context, :width, :height
|
|
92
|
+
|
|
93
|
+
def initialize(context, width, height)
|
|
94
|
+
@context = context
|
|
95
|
+
@width = width
|
|
96
|
+
@height = height
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# libui has no "unclip" and no way to read back the current transform, so
|
|
100
|
+
# every clip or transform must be scoped by save/restore.
|
|
101
|
+
def save
|
|
102
|
+
UI::L.draw_save(@context)
|
|
103
|
+
yield self
|
|
104
|
+
ensure
|
|
105
|
+
UI::L.draw_restore(@context)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def translate(x, y)
|
|
109
|
+
return if x.zero? && y.zero?
|
|
110
|
+
|
|
111
|
+
apply_matrix { |m| UI::L.draw_matrix_translate(m, x, y) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def rotate(degrees, cx = 0, cy = 0)
|
|
115
|
+
apply_matrix { |m| UI::L.draw_matrix_rotate(m, cx, cy, degrees * Math::PI / 180.0) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def scale(sx, sy, cx = 0, cy = 0)
|
|
119
|
+
apply_matrix { |m| UI::L.draw_matrix_scale(m, cx, cy, sx, sy) }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def skew(ax, ay, cx = 0, cy = 0)
|
|
123
|
+
apply_matrix { |m| UI::L.draw_matrix_skew(m, cx, cy, ax * Math::PI / 180.0, ay * Math::PI / 180.0) }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def clip_rect(x, y, w, h)
|
|
127
|
+
p = Path.new(WINDING)
|
|
128
|
+
p.rect(x, y, w, h).end!
|
|
129
|
+
UI::L.draw_clip(@context, p.ptr)
|
|
130
|
+
ensure
|
|
131
|
+
p&.free
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def fill_rect(x, y, w, h, paint)
|
|
135
|
+
return if w <= 0 || h <= 0
|
|
136
|
+
|
|
137
|
+
draw(fill: paint) { |p| p.rect(x, y, w, h) }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def stroke_rect(x, y, w, h, paint, thickness: 1)
|
|
141
|
+
draw(stroke: paint, thickness: thickness) { |p| p.rect(x, y, w, h) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def fill_oval(x, y, w, h, paint)
|
|
145
|
+
draw(fill: paint) { |p| p.oval(x, y, w, h) }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def stroke_oval(x, y, w, h, paint, thickness: 1)
|
|
149
|
+
draw(stroke: paint, thickness: thickness) { |p| p.oval(x, y, w, h) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def line(x1, y1, x2, y2, paint, thickness: 1, cap: UI::CAP_FLAT)
|
|
153
|
+
draw(stroke: paint, thickness: thickness, cap: cap) do |p|
|
|
154
|
+
p.move_to(x1, y1).line_to(x2, y2)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# The workhorse: build a path in the block, then fill and/or stroke it.
|
|
159
|
+
def draw(fill: nil, stroke: nil, thickness: 1, cap: UI::CAP_FLAT, join: UI::JOIN_MITER,
|
|
160
|
+
dashes: nil, fill_mode: WINDING)
|
|
161
|
+
p = Path.new(fill_mode)
|
|
162
|
+
yield p
|
|
163
|
+
p.end!
|
|
164
|
+
UI::L.draw_fill(@context, p.ptr, brush_for(fill).to_ptr) if fill
|
|
165
|
+
if stroke
|
|
166
|
+
sp = UI.stroke_params(thickness, cap: cap, join: join, dashes: dashes)
|
|
167
|
+
UI::L.draw_stroke(@context, p.ptr, brush_for(stroke).to_ptr, sp.to_ptr)
|
|
168
|
+
end
|
|
169
|
+
ensure
|
|
170
|
+
p&.free
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def draw_text(layout, x, y)
|
|
174
|
+
UI::L.draw_text(@context, layout, x, y)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
private
|
|
178
|
+
|
|
179
|
+
def brush_for(paint)
|
|
180
|
+
return paint if paint.respond_to?(:Type)
|
|
181
|
+
|
|
182
|
+
UI.solid_brush(paint)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def apply_matrix
|
|
186
|
+
m = UI.malloc(UI::L::FFI::DrawMatrix)
|
|
187
|
+
UI::L.draw_matrix_set_identity(m)
|
|
188
|
+
yield m
|
|
189
|
+
UI::L.draw_transform(@context, m)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|