crimson 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/.github/workflows/gempush.yml +44 -0
- data/.gitignore +9 -0
- data/.travis.yml +4 -0
- data/.vscode/launch.json +14 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +41 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/crimson.gemspec +25 -0
- data/example/ets.rb +22 -0
- data/example/example.rb +66 -0
- data/lib/crimson.js +10 -0
- data/lib/crimson.rb +5 -0
- data/lib/crimson/client.rb +86 -0
- data/lib/crimson/icons/close.png +0 -0
- data/lib/crimson/icons/hide.png +0 -0
- data/lib/crimson/icons/resize.png +0 -0
- data/lib/crimson/mash.rb +9 -0
- data/lib/crimson/model.rb +98 -0
- data/lib/crimson/model_change.rb +20 -0
- data/lib/crimson/notification_bus.rb +35 -0
- data/lib/crimson/object.rb +196 -0
- data/lib/crimson/server.rb +69 -0
- data/lib/crimson/utilities.rb +13 -0
- data/lib/crimson/version.rb +3 -0
- data/lib/crimson/webserver.rb +17 -0
- data/lib/crimson/widgets/bottom_resizer.rb +22 -0
- data/lib/crimson/widgets/desktop.rb +44 -0
- data/lib/crimson/widgets/form.rb +12 -0
- data/lib/crimson/widgets/input.rb +10 -0
- data/lib/crimson/widgets/left_resizer.rb +27 -0
- data/lib/crimson/widgets/resizer.rb +38 -0
- data/lib/crimson/widgets/right_resizer.rb +22 -0
- data/lib/crimson/widgets/taskbar.rb +0 -0
- data/lib/crimson/widgets/titlebar.rb +75 -0
- data/lib/crimson/widgets/top_resizer.rb +27 -0
- data/lib/crimson/widgets/window.rb +134 -0
- data/lib/html/template.html +13 -0
- data/lib/javascript/Application.js +13 -0
- data/lib/javascript/Logger.js +11 -0
- data/lib/javascript/MessageHandler.js +142 -0
- data/lib/javascript/ObjectManager.js +33 -0
- data/lib/javascript/ServerInteractor.js +42 -0
- data/lib/javascript/Utilities.js +26 -0
- metadata +135 -0
Binary file
|
Binary file
|
Binary file
|
data/lib/crimson/mash.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hashie'
|
4
|
+
require_relative 'model_change'
|
5
|
+
require_relative 'utilities'
|
6
|
+
require_relative 'mash'
|
7
|
+
|
8
|
+
module Crimson
|
9
|
+
class Model < SimpleDelegator
|
10
|
+
attr_reader :observers, :revisions, :local, :revision_number
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@observers = {}
|
14
|
+
@revisions = [Mash.new]
|
15
|
+
@revision_number = 1
|
16
|
+
@max_number_of_revisions = 2
|
17
|
+
@local = Mash.new
|
18
|
+
|
19
|
+
super(local)
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_observer(observer, handler = :on_commit)
|
23
|
+
observers[observer] = observer.method(handler)
|
24
|
+
end
|
25
|
+
|
26
|
+
def remove_observer(observer)
|
27
|
+
observers.delete(observer)
|
28
|
+
end
|
29
|
+
|
30
|
+
def notify_observers(changes)
|
31
|
+
observers.each { |_observer, handler| handler.call(self, changes) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def modify(modifications = {})
|
35
|
+
local.merge(modifications)
|
36
|
+
end
|
37
|
+
|
38
|
+
def changed?(*keys)
|
39
|
+
if keys.empty?
|
40
|
+
local != master
|
41
|
+
else
|
42
|
+
keys.map { |key| local[key] != master[key] }.any?(true)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def changes(*keys)
|
47
|
+
keys = ( master.keys | local.keys ) if keys.empty?
|
48
|
+
diff = Mash.new
|
49
|
+
|
50
|
+
keys.each do |k|
|
51
|
+
v1 = master[k]
|
52
|
+
v2 = local[k]
|
53
|
+
diff[k] = ModelChange.new(v1, v2) if v1 != v2
|
54
|
+
end
|
55
|
+
|
56
|
+
diff
|
57
|
+
end
|
58
|
+
|
59
|
+
def new_changes(*keys)
|
60
|
+
changes(*keys).transform_values(&:new_value)
|
61
|
+
end
|
62
|
+
|
63
|
+
def master
|
64
|
+
revisions.last
|
65
|
+
end
|
66
|
+
|
67
|
+
def commit!(*keys)
|
68
|
+
if changed?(*keys)
|
69
|
+
notify_observers(new_changes(*keys))
|
70
|
+
apply_changes!(*keys)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def apply_changes!(*keys)
|
75
|
+
if keys.empty?
|
76
|
+
revisions << Utilities.deep_copy(local)
|
77
|
+
else
|
78
|
+
revisions << Utilities.deep_copy(master)
|
79
|
+
keys.each { |key| master[key] = Utilities.deep_copy(local[key]) }
|
80
|
+
end
|
81
|
+
|
82
|
+
@revision_number += 1
|
83
|
+
revisions.shift if revisions.length > @max_number_of_revisions
|
84
|
+
end
|
85
|
+
|
86
|
+
def reload!
|
87
|
+
@local = Utilities.deep_copy(master)
|
88
|
+
end
|
89
|
+
|
90
|
+
def rollback!
|
91
|
+
if revisions.length > 1
|
92
|
+
@local = revisions.pop
|
93
|
+
else
|
94
|
+
reload!
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crimson
|
4
|
+
class ModelChange
|
5
|
+
attr_reader :old_value, :new_value
|
6
|
+
|
7
|
+
def initialize(old_value, new_value)
|
8
|
+
@old_value = old_value
|
9
|
+
@new_value = new_value
|
10
|
+
end
|
11
|
+
|
12
|
+
def inspect
|
13
|
+
to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
return "<#{old_value.inspect}, #{new_value.inspect}>"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
|
2
|
+
require 'hashie'
|
3
|
+
require_relative 'object'
|
4
|
+
|
5
|
+
module Crimson
|
6
|
+
class NotificationBus
|
7
|
+
attr_reader :notification_handlers
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@notification_handlers = Hashie::Mash.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def register(object)
|
14
|
+
raise ArgumentError unless object.is_a?(Crimson::Object)
|
15
|
+
raise ArgumentError if notification_handlers.key?(object.id)
|
16
|
+
|
17
|
+
notification_handlers[object.id] = object.method(:on_event)
|
18
|
+
end
|
19
|
+
|
20
|
+
def unregister(object)
|
21
|
+
raise ArgumentError unless object.is_a?(Crimson::Object)
|
22
|
+
raise ArgumentError unless notification_handlers.key?(object.id)
|
23
|
+
|
24
|
+
notification_handlers.delete(object.id)
|
25
|
+
end
|
26
|
+
|
27
|
+
def notify(message)
|
28
|
+
unless notification_handlers.key?(message.id)
|
29
|
+
raise ArgumentError, "[NotificationBus] Trying to notify unregistered '#{message.id}'."
|
30
|
+
end
|
31
|
+
|
32
|
+
notification_handlers[message.id].call(message)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tree'
|
4
|
+
require 'set'
|
5
|
+
require_relative 'model'
|
6
|
+
require_relative 'utilities'
|
7
|
+
require_relative 'mash'
|
8
|
+
|
9
|
+
module Crimson
|
10
|
+
class Object < Model
|
11
|
+
attr_reader :id, :tag, :node, :event_handlers, :added_children, :removed_children
|
12
|
+
|
13
|
+
def initialize(tag)
|
14
|
+
super()
|
15
|
+
|
16
|
+
@id = :"object_#{Utilities.generate_id}"
|
17
|
+
@tag = tag.to_sym
|
18
|
+
@node = Tree::TreeNode.new(id, self)
|
19
|
+
@event_handlers = Mash.new
|
20
|
+
|
21
|
+
@added_children = Set.new
|
22
|
+
@removed_children = Set.new
|
23
|
+
|
24
|
+
self.style = Mash.new
|
25
|
+
|
26
|
+
show
|
27
|
+
end
|
28
|
+
|
29
|
+
def hidden?
|
30
|
+
style.display.to_sym == :none
|
31
|
+
end
|
32
|
+
|
33
|
+
def shown?
|
34
|
+
!hidden?
|
35
|
+
end
|
36
|
+
|
37
|
+
def hide
|
38
|
+
style.display = :none
|
39
|
+
end
|
40
|
+
|
41
|
+
def show
|
42
|
+
style.display = :block
|
43
|
+
end
|
44
|
+
|
45
|
+
def on_event(message)
|
46
|
+
unless event_handlers.key?(message.event)
|
47
|
+
raise ArgumentError, "[Object] Trying to handle unknown event '#{message.event}' for '#{id}'."
|
48
|
+
end
|
49
|
+
|
50
|
+
event_handlers[message.event].each { |functor| functor.call(message.data) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def on(event, handler = nil, &block)
|
54
|
+
raise ArgumentError unless handler.nil? || handler.is_a?(Method) || handler.is_a?(Proc)
|
55
|
+
|
56
|
+
event_handlers[event] = [] unless event_handlers[event]
|
57
|
+
event_handlers[event] << handler unless handler.nil?
|
58
|
+
event_handlers[event] << block if block_given?
|
59
|
+
|
60
|
+
self[:events] = event_handlers.keys
|
61
|
+
commit!(:events)
|
62
|
+
end
|
63
|
+
|
64
|
+
def un(event, handler = nil)
|
65
|
+
raise ArgumentError unless event_handlers.key?(event)
|
66
|
+
|
67
|
+
event_handlers[event].delete(handler) if handler
|
68
|
+
|
69
|
+
if event_handlers[event].empty? || handler.nil?
|
70
|
+
event_handlers.delete(event)
|
71
|
+
end
|
72
|
+
|
73
|
+
self[:events] = event_handlers.keys
|
74
|
+
commit!(:events)
|
75
|
+
end
|
76
|
+
|
77
|
+
def parent
|
78
|
+
node.parent&.content
|
79
|
+
end
|
80
|
+
|
81
|
+
def parent=(new_parent)
|
82
|
+
raise ArgumentError unless new_parent.nil? || new_parent.is_a?(Crimson::Object)
|
83
|
+
|
84
|
+
parent&.remove(self)
|
85
|
+
new_parent&.add(self)
|
86
|
+
end
|
87
|
+
|
88
|
+
def add(child, at_index = -1)
|
89
|
+
raise ArgumentError unless child.is_a?(Crimson::Object)
|
90
|
+
raise ArgumentError if children.include?(child)
|
91
|
+
|
92
|
+
node.add(child.node, at_index)
|
93
|
+
|
94
|
+
self[:children] = children.map(&:id)
|
95
|
+
|
96
|
+
added_children << child
|
97
|
+
end
|
98
|
+
|
99
|
+
def remove(child)
|
100
|
+
raise ArgumentError unless child.is_a?(Crimson::Object)
|
101
|
+
raise ArgumentError unless children.include?(child)
|
102
|
+
|
103
|
+
node.remove!(child.node)
|
104
|
+
|
105
|
+
self[:children] = children.map(&:id)
|
106
|
+
|
107
|
+
removed_children << child
|
108
|
+
end
|
109
|
+
|
110
|
+
def move(child, at_index)
|
111
|
+
raise ArgumentError unless child.is_a?(Crimson::Object)
|
112
|
+
raise ArgumentError unless children.include?(child)
|
113
|
+
|
114
|
+
remove(child)
|
115
|
+
add(child, at_index)
|
116
|
+
|
117
|
+
added_children.delete(child)
|
118
|
+
removed_children.delete(child)
|
119
|
+
end
|
120
|
+
|
121
|
+
def siblings
|
122
|
+
node.siblings.map(&:content)
|
123
|
+
end
|
124
|
+
|
125
|
+
def children
|
126
|
+
node.children.map(&:content)
|
127
|
+
end
|
128
|
+
|
129
|
+
def root
|
130
|
+
node.root.content
|
131
|
+
end
|
132
|
+
|
133
|
+
def commit_tree!(*keys)
|
134
|
+
# TODO: Unsure if this algorithm works for moved objects
|
135
|
+
# eg. added, removed, then added again, under the same commit.
|
136
|
+
|
137
|
+
breadth_each do |object|
|
138
|
+
observers.keys.each do |observer|
|
139
|
+
object.added_children.each do |child|
|
140
|
+
observer.observe(child)
|
141
|
+
end
|
142
|
+
|
143
|
+
object.removed_children.each do |child|
|
144
|
+
observer.unobserve(child)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
object.added_children.clear
|
149
|
+
object.removed_children.clear
|
150
|
+
object.commit!(*keys)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def breadth_each
|
155
|
+
node.breadth_each do |subnode|
|
156
|
+
yield(subnode.content)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def postordered_each
|
161
|
+
node.postordered_each do |subnode|
|
162
|
+
yield(subnode.content)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def preordered_each
|
167
|
+
node.preordered_each do |subnode|
|
168
|
+
yield(subnode.content)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def find_descendant(descendent_id)
|
173
|
+
breadth_each { |descendent| return descendent if descendent_id == descendent.id }
|
174
|
+
end
|
175
|
+
|
176
|
+
def inspect
|
177
|
+
to_s
|
178
|
+
end
|
179
|
+
|
180
|
+
def to_s
|
181
|
+
id.to_s
|
182
|
+
end
|
183
|
+
|
184
|
+
def ==(other)
|
185
|
+
other.is_a?(Object) && other.id == id
|
186
|
+
end
|
187
|
+
|
188
|
+
def eql?(other)
|
189
|
+
self == other
|
190
|
+
end
|
191
|
+
|
192
|
+
def hash
|
193
|
+
id.hash
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thin'
|
4
|
+
require 'websocket-eventmachine-server'
|
5
|
+
require_relative 'client'
|
6
|
+
require_relative 'webserver'
|
7
|
+
require_relative 'utilities'
|
8
|
+
|
9
|
+
module Crimson
|
10
|
+
class Server
|
11
|
+
attr_reader :clients
|
12
|
+
|
13
|
+
def initialize(opts = {})
|
14
|
+
@opts = opts || {}
|
15
|
+
@clients = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def host
|
19
|
+
@opts[:host] || '0.0.0.0'
|
20
|
+
end
|
21
|
+
|
22
|
+
def on_connect(&block)
|
23
|
+
@on_connect = block if block_given?
|
24
|
+
end
|
25
|
+
|
26
|
+
def on_disconnect(&block)
|
27
|
+
@on_disconnect = block if block_given?
|
28
|
+
end
|
29
|
+
|
30
|
+
def port
|
31
|
+
@opts[:port] || 10_000
|
32
|
+
end
|
33
|
+
|
34
|
+
def run(websocket_enabled: true, webserver_enabled: true)
|
35
|
+
EM.run do
|
36
|
+
start_websocket if websocket_enabled
|
37
|
+
start_webserver if webserver_enabled
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def start_webserver
|
42
|
+
template = File.read("#{__dir__}/../html/template.html")
|
43
|
+
template.sub!("{PORT}", websocket_port.to_s)
|
44
|
+
|
45
|
+
Thin::Server.start(WebServer.new(template), host, port, signals: false)
|
46
|
+
end
|
47
|
+
|
48
|
+
def start_websocket
|
49
|
+
WebSocket::EventMachine::Server.start(host: host, port: websocket_port) do |ws|
|
50
|
+
id = :"client_#{Utilities.generate_id}"
|
51
|
+
client = Client.new(id, ws)
|
52
|
+
|
53
|
+
ws.onopen {
|
54
|
+
clients[id] = client
|
55
|
+
@on_connect.call(client)
|
56
|
+
}
|
57
|
+
|
58
|
+
ws.onclose {
|
59
|
+
@on_disconnect.call(client)
|
60
|
+
clients.delete(id)
|
61
|
+
}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def websocket_port
|
66
|
+
port + 1
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|