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,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clogs
|
|
4
|
+
# libui has no clipboard API at all, so Clogs shells out to whatever the
|
|
5
|
+
# platform provides. If nothing is available, copy and paste quietly do
|
|
6
|
+
# nothing rather than raising in the middle of someone's keystroke.
|
|
7
|
+
module Clipboard
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def read
|
|
11
|
+
cmd = read_command
|
|
12
|
+
return @fallback.to_s unless cmd
|
|
13
|
+
|
|
14
|
+
out = `#{cmd} 2>/dev/null`
|
|
15
|
+
$?.success? ? out : @fallback.to_s
|
|
16
|
+
rescue StandardError
|
|
17
|
+
@fallback.to_s
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def write(text)
|
|
21
|
+
@fallback = text
|
|
22
|
+
cmd = write_command
|
|
23
|
+
return unless cmd
|
|
24
|
+
|
|
25
|
+
IO.popen(cmd, "w") { |io| io.write(text) }
|
|
26
|
+
rescue StandardError
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def read_command
|
|
31
|
+
@read_command ||= case RUBY_PLATFORM
|
|
32
|
+
when /darwin/ then "pbpaste"
|
|
33
|
+
when /mingw|mswin/ then "powershell -command Get-Clipboard"
|
|
34
|
+
else
|
|
35
|
+
if system("which xclip > /dev/null 2>&1")
|
|
36
|
+
"xclip -selection clipboard -o"
|
|
37
|
+
elsif system("which xsel > /dev/null 2>&1")
|
|
38
|
+
"xsel --clipboard --output"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def write_command
|
|
44
|
+
@write_command ||= case RUBY_PLATFORM
|
|
45
|
+
when /darwin/ then "pbcopy"
|
|
46
|
+
when /mingw|mswin/ then "clip"
|
|
47
|
+
else
|
|
48
|
+
if system("which xclip > /dev/null 2>&1")
|
|
49
|
+
"xclip -selection clipboard -i"
|
|
50
|
+
elsif system("which xsel > /dev/null 2>&1")
|
|
51
|
+
"xsel --clipboard --input"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ui"
|
|
4
|
+
|
|
5
|
+
module Clogs
|
|
6
|
+
# Shoes' modal helpers.
|
|
7
|
+
#
|
|
8
|
+
# libui gives us exactly two dialogs of its own -- a message box and a file
|
|
9
|
+
# picker -- and neither can ask a question. `ask` and `confirm` are therefore
|
|
10
|
+
# built as small real windows made of native libui controls, run on a nested
|
|
11
|
+
# `uiMainStep` loop so they block the caller the way Shoes expects.
|
|
12
|
+
module Dialogs
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def alert(parent, message)
|
|
16
|
+
UI::L.msg_box(parent, "", message.to_s)
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def error(parent, message)
|
|
21
|
+
UI::L.msg_box_error(parent, "", message.to_s)
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def open_file(parent)
|
|
26
|
+
ptr = UI::L.open_file(parent)
|
|
27
|
+
pointer_to_string(ptr)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def save_file(parent)
|
|
31
|
+
ptr = UI::L.save_file(parent)
|
|
32
|
+
pointer_to_string(ptr)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def open_folder(parent)
|
|
36
|
+
return nil unless UI::L.respond_to?(:open_folder)
|
|
37
|
+
|
|
38
|
+
pointer_to_string(UI::L.open_folder(parent))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def pointer_to_string(ptr)
|
|
42
|
+
return nil if ptr.nil? || ptr.null?
|
|
43
|
+
|
|
44
|
+
str = ptr.to_s
|
|
45
|
+
UI::L.free_text(ptr)
|
|
46
|
+
str.empty? ? nil : str
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# A one-line text prompt. Returns the string, or nil if cancelled.
|
|
50
|
+
def ask(parent, message, secret: false)
|
|
51
|
+
result = nil
|
|
52
|
+
modal(parent, "", 320, 100) do |win, box, finish|
|
|
53
|
+
UI::L.box_append(box, label(message.to_s), 0)
|
|
54
|
+
entry = secret ? UI::L.new_password_entry : UI::L.new_entry
|
|
55
|
+
UI::L.box_append(box, entry, 0)
|
|
56
|
+
|
|
57
|
+
buttons = UI::L.new_horizontal_box
|
|
58
|
+
UI::L.box_set_padded(buttons, 1)
|
|
59
|
+
ok = UI::L.new_button("OK")
|
|
60
|
+
cancel = UI::L.new_button("Cancel")
|
|
61
|
+
UI::L.box_append(buttons, ok, 1)
|
|
62
|
+
UI::L.box_append(buttons, cancel, 1)
|
|
63
|
+
UI::L.box_append(box, buttons, 0)
|
|
64
|
+
|
|
65
|
+
UI::L.button_on_clicked(ok) do
|
|
66
|
+
result = UI::L.entry_text(entry).to_s
|
|
67
|
+
finish.call
|
|
68
|
+
0
|
|
69
|
+
end
|
|
70
|
+
UI::L.button_on_clicked(cancel) do
|
|
71
|
+
result = nil
|
|
72
|
+
finish.call
|
|
73
|
+
0
|
|
74
|
+
end
|
|
75
|
+
_ = win
|
|
76
|
+
end
|
|
77
|
+
result
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# A yes/no question. Returns true or false.
|
|
81
|
+
def confirm(parent, question)
|
|
82
|
+
result = false
|
|
83
|
+
modal(parent, "", 320, 90) do |_win, box, finish|
|
|
84
|
+
UI::L.box_append(box, label(question.to_s), 0)
|
|
85
|
+
buttons = UI::L.new_horizontal_box
|
|
86
|
+
UI::L.box_set_padded(buttons, 1)
|
|
87
|
+
yes = UI::L.new_button("OK")
|
|
88
|
+
no = UI::L.new_button("Cancel")
|
|
89
|
+
UI::L.box_append(buttons, yes, 1)
|
|
90
|
+
UI::L.box_append(buttons, no, 1)
|
|
91
|
+
UI::L.box_append(box, buttons, 0)
|
|
92
|
+
|
|
93
|
+
UI::L.button_on_clicked(yes) do
|
|
94
|
+
result = true
|
|
95
|
+
finish.call
|
|
96
|
+
0
|
|
97
|
+
end
|
|
98
|
+
UI::L.button_on_clicked(no) do
|
|
99
|
+
result = false
|
|
100
|
+
finish.call
|
|
101
|
+
0
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
result
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def label(text)
|
|
108
|
+
UI::L.new_label(text)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Run a modal window on a nested event loop. libui does not allow uiMain to
|
|
112
|
+
# be re-entered, but stepping it by hand is fine and is how a blocking
|
|
113
|
+
# dialog has to work here.
|
|
114
|
+
def modal(parent, title, width, height)
|
|
115
|
+
win = UI::L.new_window(title, width, height, 0)
|
|
116
|
+
UI::L.window_set_margined(win, 1)
|
|
117
|
+
box = UI::L.new_vertical_box
|
|
118
|
+
UI::L.box_set_padded(box, 1)
|
|
119
|
+
UI::L.window_set_child(win, box)
|
|
120
|
+
|
|
121
|
+
done = false
|
|
122
|
+
finish = -> { done = true }
|
|
123
|
+
UI::L.window_on_closing(win) do
|
|
124
|
+
done = true
|
|
125
|
+
0
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
yield win, box, finish
|
|
129
|
+
|
|
130
|
+
UI::L.control_show(win)
|
|
131
|
+
UI::L.main_step(1) until done
|
|
132
|
+
UI::L.control_destroy(win)
|
|
133
|
+
_ = parent
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "drawable"
|
|
4
|
+
require_relative "app"
|
|
5
|
+
|
|
6
|
+
module Clogs
|
|
7
|
+
# Lacci's half of the bargain: it hands us a Shoes drawable class name and an
|
|
8
|
+
# id, and expects a display-side peer back. Everything after that happens
|
|
9
|
+
# through events on that id.
|
|
10
|
+
class DisplayService < Shoes::DisplayService
|
|
11
|
+
class << self
|
|
12
|
+
attr_accessor :instance
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
raise Shoes::SingletonError, "Clogs::DisplayService is a singleton!" if DisplayService.instance
|
|
17
|
+
|
|
18
|
+
DisplayService.instance = self
|
|
19
|
+
super()
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Shoes class name (minus namespace) => Clogs peer class.
|
|
23
|
+
PEERS = {
|
|
24
|
+
"App" => "App",
|
|
25
|
+
"DocumentRoot" => "DocumentRoot",
|
|
26
|
+
"Stack" => "Stack",
|
|
27
|
+
"Flow" => "Flow",
|
|
28
|
+
"Shape" => "Shape",
|
|
29
|
+
"Mask" => "Mask",
|
|
30
|
+
"Para" => "Para",
|
|
31
|
+
"Link" => "Link",
|
|
32
|
+
"LinkHover" => "Link",
|
|
33
|
+
"Strong" => "Strong",
|
|
34
|
+
"Em" => "Em",
|
|
35
|
+
"Code" => "Code",
|
|
36
|
+
"Del" => "Del",
|
|
37
|
+
"Ins" => "Ins",
|
|
38
|
+
"Sub" => "Sub",
|
|
39
|
+
"Sup" => "Sup",
|
|
40
|
+
"Span" => "Span",
|
|
41
|
+
"Bg" => "Bg",
|
|
42
|
+
"Fg" => "Fg",
|
|
43
|
+
"Button" => "Button",
|
|
44
|
+
"Check" => "Check",
|
|
45
|
+
"Radio" => "Radio",
|
|
46
|
+
"Progress" => "Progress",
|
|
47
|
+
"EditLine" => "EditLine",
|
|
48
|
+
"EditBox" => "EditBox",
|
|
49
|
+
"ListBox" => "ListBox",
|
|
50
|
+
"Image" => "Image",
|
|
51
|
+
"Video" => "Video",
|
|
52
|
+
"Arrow" => "Arrow",
|
|
53
|
+
"Rect" => "Rect",
|
|
54
|
+
"Oval" => "Oval",
|
|
55
|
+
"Line" => "Line",
|
|
56
|
+
"Star" => "Star",
|
|
57
|
+
"Arc" => "Arc",
|
|
58
|
+
"Background" => "Background",
|
|
59
|
+
"Border" => "Border",
|
|
60
|
+
"SubscriptionItem" => "SubscriptionItem"
|
|
61
|
+
}.freeze
|
|
62
|
+
|
|
63
|
+
def create_display_drawable_for(drawable_class_name, drawable_id, properties, parent_id:, is_widget:)
|
|
64
|
+
existing = query_display_drawable_for(drawable_id, nil_ok: true)
|
|
65
|
+
return existing if existing
|
|
66
|
+
|
|
67
|
+
properties = properties.dup
|
|
68
|
+
properties["shoes_linkable_id"] ||= drawable_id
|
|
69
|
+
|
|
70
|
+
peer = build_peer(drawable_class_name, properties, is_widget)
|
|
71
|
+
set_drawable_pairing(drawable_id, peer)
|
|
72
|
+
|
|
73
|
+
if peer.is_a?(App)
|
|
74
|
+
@app = peer
|
|
75
|
+
else
|
|
76
|
+
parent = parent_id ? query_display_drawable_for(parent_id, nil_ok: true) : nil
|
|
77
|
+
peer.set_parent(parent)
|
|
78
|
+
@document_root = peer if peer.is_a?(DocumentRoot)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Lacci builds the document root *before* the app, so neither one can
|
|
82
|
+
# simply look the other up on creation. Whichever arrives second links
|
|
83
|
+
# them: the root is the app's canvas contents but is not its child.
|
|
84
|
+
@app.document_root = @document_root if @app && @document_root && @app.document_root.nil?
|
|
85
|
+
|
|
86
|
+
peer
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def build_peer(class_name, properties, is_widget)
|
|
90
|
+
short = class_name.to_s.split("::").last
|
|
91
|
+
return Widget.new(properties) if is_widget && !PEERS.key?(short)
|
|
92
|
+
|
|
93
|
+
peer_name = PEERS[short]
|
|
94
|
+
if peer_name
|
|
95
|
+
Clogs.const_get(peer_name).new(properties)
|
|
96
|
+
else
|
|
97
|
+
# A Shoes drawable Clogs does not know about still needs a peer so the
|
|
98
|
+
# tree stays consistent; it simply draws nothing.
|
|
99
|
+
warn "Clogs: no peer for #{class_name}, ignoring it visually."
|
|
100
|
+
Drawable.new(properties)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def destroy
|
|
105
|
+
@app&.destroy
|
|
106
|
+
DisplayService.instance = nil
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "style"
|
|
4
|
+
|
|
5
|
+
module Clogs
|
|
6
|
+
# Base class for every Clogs peer: the display-side half of a Shoes drawable.
|
|
7
|
+
#
|
|
8
|
+
# Lacci owns the Shoes-side object and tells us about it through three events
|
|
9
|
+
# (`parent`, `prop_change`, `destroy`). We own pixels: where the thing sits,
|
|
10
|
+
# how big it is, and what it looks like.
|
|
11
|
+
#
|
|
12
|
+
# The layout contract is two passes:
|
|
13
|
+
#
|
|
14
|
+
# measure(available_width) -> sets @width / @height
|
|
15
|
+
# paint(painter, ox, oy) -> draws, and records absolute geometry so that
|
|
16
|
+
# mouse hit-testing has something to test
|
|
17
|
+
class Drawable < Shoes::Linkable
|
|
18
|
+
attr_accessor :parent, :x, :y
|
|
19
|
+
attr_reader :children, :shoes_linkable_id, :styles, :abs_x, :abs_y, :width, :height
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
# Peer class lookup by Shoes class name, e.g. "Shoes::Button" -> Clogs::Button.
|
|
23
|
+
def for_shoes_class(name)
|
|
24
|
+
short = name.split("::").last
|
|
25
|
+
Clogs.const_get(short) if Clogs.const_defined?(short, false)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(properties)
|
|
30
|
+
@styles = {}
|
|
31
|
+
properties.each { |k, v| @styles[k.to_s] = v }
|
|
32
|
+
@shoes_linkable_id = @styles.delete("shoes_linkable_id")
|
|
33
|
+
@children = []
|
|
34
|
+
@x = @y = 0
|
|
35
|
+
@width = @height = 0
|
|
36
|
+
|
|
37
|
+
super(linkable_id: @shoes_linkable_id)
|
|
38
|
+
|
|
39
|
+
bind_shoes_event(event_name: "parent", target: @shoes_linkable_id) do |new_parent_id|
|
|
40
|
+
new_parent = DisplayService.instance.query_display_drawable_for(new_parent_id, nil_ok: true)
|
|
41
|
+
set_parent(new_parent) unless new_parent == @parent
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
bind_shoes_event(event_name: "prop_change", target: @shoes_linkable_id) do |changes|
|
|
45
|
+
changes.each { |k, v| @styles[k.to_s] = v }
|
|
46
|
+
properties_changed(changes)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
bind_shoes_event(event_name: "destroy", target: @shoes_linkable_id) do
|
|
50
|
+
destroy_self
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def properties_changed(_changes)
|
|
55
|
+
needs_layout!
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def destroy_self
|
|
59
|
+
set_parent(nil)
|
|
60
|
+
needs_layout!
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def set_parent(new_parent)
|
|
64
|
+
@parent&.remove_child(self)
|
|
65
|
+
new_parent&.add_child(self)
|
|
66
|
+
@parent = new_parent
|
|
67
|
+
needs_layout!
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def add_child(child)
|
|
71
|
+
@children << child unless @children.include?(child)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def remove_child(child)
|
|
75
|
+
@children.delete(child)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The owning Clogs::App, found by walking up the tree.
|
|
79
|
+
def app
|
|
80
|
+
@app ||= @parent&.app
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def needs_layout!
|
|
84
|
+
app&.needs_layout!
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def redraw!
|
|
88
|
+
app&.redraw!
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# ---- styles -------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
def style(name)
|
|
94
|
+
@styles[name.to_s]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def hidden?
|
|
98
|
+
!!style(:hidden)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# A drawable with an explicit left/top is taken out of the normal flow and
|
|
102
|
+
# placed at those coordinates within its slot, exactly as in Shoes.
|
|
103
|
+
def positioned?
|
|
104
|
+
!style(:left).nil? && !style(:top).nil?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def margin
|
|
108
|
+
@margin ||= Style.margins(@styles)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def requested_width(available)
|
|
112
|
+
Style.dimension(style(:width), available)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def requested_height(available)
|
|
116
|
+
Style.dimension(style(:height), available)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# ---- layout -------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
# Subclasses override. Must set @width and @height.
|
|
122
|
+
def measure(available_width)
|
|
123
|
+
@width = requested_width(available_width) || 0
|
|
124
|
+
@height = requested_height(available_width) || 0
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def paint(painter, ox, oy)
|
|
128
|
+
@abs_x = ox + @x
|
|
129
|
+
@abs_y = oy + @y
|
|
130
|
+
draw(painter, @abs_x, @abs_y)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def draw(_painter, _x, _y); end
|
|
134
|
+
|
|
135
|
+
# ---- events -------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def contains?(px, py)
|
|
138
|
+
return false if @abs_x.nil? || hidden?
|
|
139
|
+
|
|
140
|
+
px >= @abs_x && px < @abs_x + @width && py >= @abs_y && py < @abs_y + @height
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Overridden by controls that want the keyboard.
|
|
144
|
+
def focusable?
|
|
145
|
+
false
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# In Shoes any drawable can carry a click handler, so every peer is a
|
|
149
|
+
# candidate; the topmost one under the pointer wins.
|
|
150
|
+
def clickable?
|
|
151
|
+
true
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def on_click(_x, _y, _button); end
|
|
155
|
+
|
|
156
|
+
def on_release(x, y, button)
|
|
157
|
+
notify("click", button, x.round, y.round) if contains?(x, y)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def on_mouse_move(_x, _y); end
|
|
161
|
+
|
|
162
|
+
def on_key(_event)
|
|
163
|
+
false
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def focus_gained; end
|
|
167
|
+
|
|
168
|
+
def focus_lost; end
|
|
169
|
+
|
|
170
|
+
def notify(event_name, *args)
|
|
171
|
+
send_shoes_event(*args, event_name: event_name, target: @shoes_linkable_id)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Depth-first list of visible peers in paint order, for hit testing.
|
|
175
|
+
def each_peer(&block)
|
|
176
|
+
return if hidden?
|
|
177
|
+
|
|
178
|
+
block.call(self)
|
|
179
|
+
@children.each { |c| c.each_peer(&block) }
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|